From 4e50087612b16db07a642b42dca7339e8dab17a7 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 16 May 2025 15:32:31 +0100 Subject: [PATCH 001/291] Refactoring 1 --- include/xrpl/basics/base_uint.h | 7 + include/xrpl/beast/utility/instrumentation.h | 5 + include/xrpl/protocol/Protocol.h | 2 + include/xrpl/protocol/SField.h | 6 +- include/xrpl/protocol/STLedgerEntry.h | 9 +- include/xrpl/protocol/STObject.h | 2 +- include/xrpl/protocol/STValidation.h | 2 +- include/xrpl/protocol/TxFlags.h | 5 +- include/xrpl/protocol/TxFormats.h | 3 +- include/xrpl/protocol/{FeeUnits.h => Units.h} | 285 ++++++----- include/xrpl/protocol/XRPAmount.h | 4 +- include/xrpl/protocol/detail/sfields.macro | 6 +- .../xrpl/protocol/detail/transactions.macro | 443 +++++++++++++++--- include/xrpl/protocol/jss.h | 2 +- src/libxrpl/protocol/Permissions.cpp | 4 +- src/libxrpl/protocol/SField.cpp | 22 +- src/libxrpl/protocol/TxFormats.cpp | 2 +- src/test/app/AMMCalc_test.cpp | 2 +- src/test/app/AMM_test.cpp | 2 +- src/test/app/Credentials_test.cpp | 9 - src/test/app/DID_test.cpp | 8 - src/test/app/ValidatorSite_test.cpp | 2 +- src/test/app/Vault_test.cpp | 8 +- src/test/basics/FileUtilities_test.cpp | 2 +- .../{FeeUnits_test.cpp => Units_test.cpp} | 32 +- src/test/basics/base_uint_test.cpp | 5 + src/test/core/Config_test.cpp | 6 +- src/test/csf/Digraph.h | 6 +- src/test/jtx/Env.h | 6 + src/test/jtx/TestHelpers.h | 347 +++++++++++--- src/test/jtx/amount.h | 85 +++- src/test/jtx/balance.h | 8 +- src/test/jtx/fee.h | 5 + src/test/jtx/flags.h | 32 +- src/test/jtx/impl/Env.cpp | 20 + src/test/jtx/impl/TestHelpers.cpp | 22 +- src/test/jtx/impl/balance.cpp | 55 ++- src/test/jtx/impl/fee.cpp | 6 +- src/test/jtx/impl/owners.cpp | 8 +- src/test/jtx/owners.h | 7 +- src/test/jtx/require.h | 7 +- src/test/jtx/tags.h | 10 + src/test/ledger/Invariants_test.cpp | 123 ++++- src/test/nodestore/import_test.cpp | 3 +- src/test/unit_test/FileDirGuard.h | 3 +- src/test/unit_test/multi_runner.cpp | 6 +- src/test/unit_test/multi_runner.h | 3 +- src/xrpld/app/misc/detail/LoadFeeTrack.cpp | 2 +- src/xrpld/app/tx/detail/DeleteAccount.cpp | 2 +- src/xrpld/app/tx/detail/InvariantCheck.cpp | 235 +++++++--- src/xrpld/app/tx/detail/InvariantCheck.h | 31 +- src/xrpld/app/tx/detail/Transactor.cpp | 5 +- src/xrpld/app/tx/detail/Transactor.h | 2 + src/xrpld/app/tx/detail/applySteps.cpp | 68 +-- src/xrpld/ledger/View.h | 16 + src/xrpld/ledger/detail/View.cpp | 105 ++++- 56 files changed, 1582 insertions(+), 531 deletions(-) rename include/xrpl/protocol/{FeeUnits.h => Units.h} (64%) rename src/test/basics/{FeeUnits_test.cpp => Units_test.cpp} (92%) diff --git a/include/xrpl/basics/base_uint.h b/include/xrpl/basics/base_uint.h index d36bf74c54..1eacb16385 100644 --- a/include/xrpl/basics/base_uint.h +++ b/include/xrpl/basics/base_uint.h @@ -632,6 +632,13 @@ to_string(base_uint const& a) return strHex(a.cbegin(), a.cend()); } +template +inline std::string +to_short_string(base_uint const& a) +{ + return to_string(a).substr(0, 8) + "..."; +} + template inline std::ostream& operator<<(std::ostream& out, base_uint const& u) diff --git a/include/xrpl/beast/utility/instrumentation.h b/include/xrpl/beast/utility/instrumentation.h index 72c48959a0..3594855eef 100644 --- a/include/xrpl/beast/utility/instrumentation.h +++ b/include/xrpl/beast/utility/instrumentation.h @@ -39,11 +39,16 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. #endif #define XRPL_ASSERT ALWAYS_OR_UNREACHABLE +#define XRPL_ASSERT_PARTS(cond, function, description, ...) \ + XRPL_ASSERT(cond, function " : " description) // How to use the instrumentation macros: // // * XRPL_ASSERT if cond must be true but the line might not be reached during // fuzzing. Same like `assert` in normal use. +// * XRPL_ASSERT_PARTS is for convenience, and works like XRPL_ASSERT, but +// splits the message param into "function" and "description", then joins +// them with " : " before passing to XRPL_ASSERT. // * ALWAYS if cond must be true _and_ the line must be reached during fuzzing. // Same like `assert` in normal use. // * REACHABLE if the line must be reached during fuzzing diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 49bad8a076..1a49d9d09e 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -23,6 +23,8 @@ #include #include #include +#include +#include #include diff --git a/include/xrpl/protocol/SField.h b/include/xrpl/protocol/SField.h index 777cfa02ba..a72332b888 100644 --- a/include/xrpl/protocol/SField.h +++ b/include/xrpl/protocol/SField.h @@ -22,6 +22,7 @@ #include #include +#include #include #include @@ -149,7 +150,9 @@ public: sMD_DeleteFinal = 0x04, // final value when it is deleted sMD_Create = 0x08, // value when it's created sMD_Always = 0x10, // value when node containing it is affected at all - sMD_BaseTen = 0x20, + sMD_BaseTen = 0x20, // value is treated as base 10, overriding behavior + sMD_PseudoAccount = 0x40, // if this field is set in an ACCOUNT_ROOT + // _only_, then it is a pseudo-account sMD_Default = sMD_ChangeOrig | sMD_ChangeNew | sMD_DeleteFinal | sMD_Create }; @@ -306,6 +309,7 @@ public: private: static int num; static std::map knownCodeToField; + static std::map knownNameToField; }; /** A field with a type known at compile time. */ diff --git a/include/xrpl/protocol/STLedgerEntry.h b/include/xrpl/protocol/STLedgerEntry.h index 3609a04d4b..571c7af5fe 100644 --- a/include/xrpl/protocol/STLedgerEntry.h +++ b/include/xrpl/protocol/STLedgerEntry.h @@ -26,7 +26,9 @@ namespace ripple { class Rules; +namespace test { class Invariants_test; +} class STLedgerEntry final : public STObject, public CountedObject { @@ -36,6 +38,8 @@ class STLedgerEntry final : public STObject, public CountedObject public: using pointer = std::shared_ptr; using ref = std::shared_ptr const&; + using const_pointer = std::shared_ptr; + using const_ref = std::shared_ptr const&; /** Create an empty object with the given key and type. */ explicit STLedgerEntry(Keylet const& k); @@ -54,7 +58,7 @@ public: getText() const override; Json::Value - getJson(JsonOptions options) const override; + getJson(JsonOptions options = JsonOptions::none) const override; /** Returns the 'key' (or 'index') of this item. The key identifies this entry's position in @@ -84,7 +88,8 @@ private: void setSLEType(); - friend Invariants_test; // this test wants access to the private type_ + friend test::Invariants_test; // this test wants access to the private + // type_ STBase* copy(std::size_t n, void* buf) const override; diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index 6cd083ef85..b3cb561390 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -25,7 +25,6 @@ #include #include #include -#include #include #include #include @@ -34,6 +33,7 @@ #include #include #include +#include #include #include diff --git a/include/xrpl/protocol/STValidation.h b/include/xrpl/protocol/STValidation.h index 11ec733c01..f87923c940 100644 --- a/include/xrpl/protocol/STValidation.h +++ b/include/xrpl/protocol/STValidation.h @@ -22,10 +22,10 @@ #include #include -#include #include #include #include +#include #include #include diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index 505000cfd6..b2da99594a 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -130,6 +130,8 @@ constexpr std::uint32_t tfTrustSetPermissionMask = (~tfTrustSetMask) & (~tfTrust // EnableAmendment flags: constexpr std::uint32_t tfGotMajority = 0x00010000; constexpr std::uint32_t tfLostMajority = 0x00020000; +constexpr std::uint32_t tfChangeMask = + ~( tfUniversal | tfGotMajority | tfLostMajority); // PaymentChannelClaim flags: constexpr std::uint32_t tfRenew = 0x00010000; @@ -144,7 +146,8 @@ constexpr std::uint32_t const tfTransferable = 0x00000008; constexpr std::uint32_t const tfMutable = 0x00000010; // MPTokenIssuanceCreate flags: -// NOTE - there is intentionally no flag here for lsfMPTLocked, which this transaction cannot mutate. +// NOTE - there is intentionally no flag here for lsfMPTLocked, which +// this transaction cannot mutate. constexpr std::uint32_t const tfMPTCanLock = lsfMPTCanLock; constexpr std::uint32_t const tfMPTRequireAuth = lsfMPTRequireAuth; constexpr std::uint32_t const tfMPTCanEscrow = lsfMPTCanEscrow; diff --git a/include/xrpl/protocol/TxFormats.h b/include/xrpl/protocol/TxFormats.h index 70b721a3d7..60066536ce 100644 --- a/include/xrpl/protocol/TxFormats.h +++ b/include/xrpl/protocol/TxFormats.h @@ -59,7 +59,8 @@ enum TxType : std::uint16_t #pragma push_macro("TRANSACTION") #undef TRANSACTION -#define TRANSACTION(tag, value, name, delegatable, fields) tag = value, +#define TRANSACTION(tag, value, ...) \ + tag = value, #include diff --git a/include/xrpl/protocol/FeeUnits.h b/include/xrpl/protocol/Units.h similarity index 64% rename from include/xrpl/protocol/FeeUnits.h rename to include/xrpl/protocol/Units.h index c6949a434c..f92623eec6 100644 --- a/include/xrpl/protocol/FeeUnits.h +++ b/include/xrpl/protocol/Units.h @@ -16,8 +16,8 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ -#ifndef BASICS_FEES_H_INCLUDED -#define BASICS_FEES_H_INCLUDED +#ifndef PROTOCOL_UNITS_H_INCLUDED +#define PROTOCOL_UNITS_H_INCLUDED #include #include @@ -38,23 +38,23 @@ namespace ripple { -namespace feeunit { +namespace unit { /** "drops" are the smallest divisible amount of XRP. This is what most of the code uses. */ struct dropTag; -/** "fee units" calculations are a not-really-unitless value that is used - to express the cost of a given transaction vs. a reference transaction. - They are primarily used by the Transactor classes. */ -struct feeunitTag; /** "fee levels" are used by the transaction queue to compare the relative cost of transactions that require different levels of effort to process. See also: src/ripple/app/misc/FeeEscalation.md#fee-level */ struct feelevelTag; -/** unitless values are plain scalars wrapped in a TaggedFee. They are +/** unitless values are plain scalars wrapped in a ValueUnit. They are used for calculations in this header. */ struct unitlessTag; +/** Units to represent basis points (bips) and 1/10 basis points */ +class BipsTag; +class TenthBipsTag; + template using enable_if_unit_t = typename std::enable_if_t< std::is_class_v && std::is_object_v && @@ -62,32 +62,33 @@ using enable_if_unit_t = typename std::enable_if_t< /** `is_usable_unit_v` is checked to ensure that only values with known valid type tags can be used (sometimes transparently) in - non-fee contexts. At the time of implementation, this includes + non-unit contexts. At the time of implementation, this includes all known tags, but more may be added in the future, and they should not be added automatically unless determined to be appropriate. */ template > constexpr bool is_usable_unit_v = - std::is_same_v || std::is_same_v || std::is_same_v || - std::is_same_v; + std::is_same_v || + std::is_same_v || + std::is_same_v; template -class TaggedFee : private boost::totally_ordered>, - private boost::additive>, - private boost::equality_comparable, T>, - private boost::dividable, T>, - private boost::modable, T>, - private boost::unit_steppable> +class ValueUnit : private boost::totally_ordered>, + private boost::additive>, + private boost::equality_comparable, T>, + private boost::dividable, T>, + private boost::modable, T>, + private boost::unit_steppable> { public: using unit_type = UnitTag; using value_type = T; private: - value_type fee_; + value_type value_; protected: template @@ -95,44 +96,44 @@ protected: std::is_arithmetic_v && std::is_arithmetic_v && std::is_convertible_v; - template > - static constexpr bool is_compatiblefee_v = - is_compatible_v && - std::is_same_v; + template > + static constexpr bool is_compatiblevalue_v = + is_compatible_v && + std::is_same_v; template using enable_if_compatible_t = typename std::enable_if_t>; - template - using enable_if_compatiblefee_t = - typename std::enable_if_t>; + template + using enable_if_compatiblevalue_t = + typename std::enable_if_t>; public: - TaggedFee() = default; - constexpr TaggedFee(TaggedFee const& other) = default; - constexpr TaggedFee& - operator=(TaggedFee const& other) = default; + ValueUnit() = default; + constexpr ValueUnit(ValueUnit const& other) = default; + constexpr ValueUnit& + operator=(ValueUnit const& other) = default; - constexpr explicit TaggedFee(beast::Zero) : fee_(0) + constexpr explicit ValueUnit(beast::Zero) : value_(0) { } - constexpr TaggedFee& + constexpr ValueUnit& operator=(beast::Zero) { - fee_ = 0; + value_ = 0; return *this; } - constexpr explicit TaggedFee(value_type fee) : fee_(fee) + constexpr explicit ValueUnit(value_type value) : value_(value) { } - TaggedFee& - operator=(value_type fee) + constexpr ValueUnit& + operator=(value_type value) { - fee_ = fee; + value_ = value; return *this; } @@ -144,153 +145,181 @@ public: class = std::enable_if_t< is_compatible_v && is_safetocasttovalue_v>> - constexpr TaggedFee(TaggedFee const& fee) - : TaggedFee(safe_cast(fee.fee())) + constexpr ValueUnit(ValueUnit const& value) + : ValueUnit(safe_cast(value.value())) { } - constexpr TaggedFee + constexpr ValueUnit + operator+(value_type const& rhs) const + { + return ValueUnit{value_ + rhs}; + } + + friend constexpr ValueUnit + operator+(value_type lhs, ValueUnit const& rhs) + { + // addition is commutative + return rhs + lhs; + } + + constexpr ValueUnit + operator-(value_type const& rhs) const + { + return ValueUnit{value_ - rhs}; + } + + friend constexpr ValueUnit + operator-(value_type lhs, ValueUnit const& rhs) + { + // subtraction is NOT commutative, but (lhs + (-rhs)) is addition, which + // is + return -rhs + lhs; + } + + constexpr ValueUnit operator*(value_type const& rhs) const { - return TaggedFee{fee_ * rhs}; + return ValueUnit{value_ * rhs}; } - friend constexpr TaggedFee - operator*(value_type lhs, TaggedFee const& rhs) + friend constexpr ValueUnit + operator*(value_type lhs, ValueUnit const& rhs) { // multiplication is commutative return rhs * lhs; } constexpr value_type - operator/(TaggedFee const& rhs) const + operator/(ValueUnit const& rhs) const { - return fee_ / rhs.fee_; + return value_ / rhs.value_; } - TaggedFee& - operator+=(TaggedFee const& other) + ValueUnit& + operator+=(ValueUnit const& other) { - fee_ += other.fee(); + value_ += other.value(); return *this; } - TaggedFee& - operator-=(TaggedFee const& other) + ValueUnit& + operator-=(ValueUnit const& other) { - fee_ -= other.fee(); + value_ -= other.value(); return *this; } - TaggedFee& + ValueUnit& operator++() { - ++fee_; + ++value_; return *this; } - TaggedFee& + ValueUnit& operator--() { - --fee_; + --value_; return *this; } - TaggedFee& + ValueUnit& operator*=(value_type const& rhs) { - fee_ *= rhs; + value_ *= rhs; return *this; } - TaggedFee& + ValueUnit& operator/=(value_type const& rhs) { - fee_ /= rhs; + value_ /= rhs; return *this; } template - std::enable_if_t, TaggedFee&> + std::enable_if_t, ValueUnit&> operator%=(value_type const& rhs) { - fee_ %= rhs; + value_ %= rhs; return *this; } - TaggedFee + ValueUnit operator-() const { static_assert( - std::is_signed_v, "- operator illegal on unsigned fee types"); - return TaggedFee{-fee_}; + std::is_signed_v, "- operator illegal on unsigned value types"); + return ValueUnit{-value_}; } - bool - operator==(TaggedFee const& other) const + constexpr bool + operator==(ValueUnit const& other) const { - return fee_ == other.fee_; + return value_ == other.value_; } template > - bool - operator==(TaggedFee const& other) const + constexpr bool + operator==(ValueUnit const& other) const { - return fee_ == other.fee(); + return value_ == other.value(); } - bool + constexpr bool operator==(value_type other) const { - return fee_ == other; + return value_ == other; } template > - bool - operator!=(TaggedFee const& other) const + constexpr bool + operator!=(ValueUnit const& other) const { return !operator==(other); } - bool - operator<(TaggedFee const& other) const + constexpr bool + operator<(ValueUnit const& other) const { - return fee_ < other.fee_; + return value_ < other.value_; } /** Returns true if the amount is not zero */ explicit constexpr operator bool() const noexcept { - return fee_ != 0; + return value_ != 0; } /** Return the sign of the amount */ constexpr int signum() const noexcept { - return (fee_ < 0) ? -1 : (fee_ ? 1 : 0); + return (value_ < 0) ? -1 : (value_ ? 1 : 0); } /** Returns the number of drops */ + // TODO: Move this to a new class, maybe with the old "TaggedFee" name constexpr value_type fee() const { - return fee_; + return value_; } template constexpr double - decimalFromReference(TaggedFee reference) const + decimalFromReference(ValueUnit reference) const { - return static_cast(fee_) / reference.fee(); + return static_cast(value_) / reference.value(); } // `is_usable_unit_v` is checked to ensure that only values with // known valid type tags can be converted to JSON. At the time // of implementation, that includes all known tags, but more may // be added in the future. - std::enable_if_t, Json::Value> + std::enable_if_t, Json::Value> jsonClipped() const { if constexpr (std::is_integral_v) @@ -303,15 +332,15 @@ public: constexpr auto min = std::numeric_limits::min(); constexpr auto max = std::numeric_limits::max(); - if (fee_ < min) + if (value_ < min) return min; - if (fee_ > max) + if (value_ > max) return max; - return static_cast(fee_); + return static_cast(value_); } else { - return fee_; + return value_; } } @@ -322,30 +351,30 @@ public: constexpr value_type value() const { - return fee_; + return value_; } friend std::istream& - operator>>(std::istream& s, TaggedFee& val) + operator>>(std::istream& s, ValueUnit& val) { - s >> val.fee_; + s >> val.value_; return s; } }; -// Output Fees as just their numeric value. +// Output Values as just their numeric value. template std::basic_ostream& -operator<<(std::basic_ostream& os, TaggedFee const& q) +operator<<(std::basic_ostream& os, ValueUnit const& q) { return os << q.value(); } template std::string -to_string(TaggedFee const& amount) +to_string(ValueUnit const& amount) { - return std::to_string(amount.fee()); + return std::to_string(amount.value()); } template > @@ -408,10 +437,10 @@ using enable_muldiv_commute_t = typename std::enable_if_t>; template -TaggedFee +ValueUnit scalar(T value) { - return TaggedFee{value}; + return ValueUnit{value}; } template < @@ -422,18 +451,17 @@ template < std::optional mulDivU(Source1 value, Dest mul, Source2 div) { - // Fees can never be negative in any context. + // values can never be negative in any context. if (value.value() < 0 || mul.value() < 0 || div.value() < 0) { // split the asserts so if one hits, the user can tell which // without a debugger. XRPL_ASSERT( - value.value() >= 0, - "ripple::feeunit::mulDivU : minimum value input"); + value.value() >= 0, "ripple::unit::mulDivU : minimum value input"); XRPL_ASSERT( - mul.value() >= 0, "ripple::feeunit::mulDivU : minimum mul input"); + mul.value() >= 0, "ripple::unit::mulDivU : minimum mul input"); XRPL_ASSERT( - div.value() >= 0, "ripple::feeunit::mulDivU : minimum div input"); + div.value() >= 0, "ripple::unit::mulDivU : minimum div input"); return std::nullopt; } @@ -466,46 +494,57 @@ mulDivU(Source1 value, Dest mul, Source2 div) return Dest{static_cast(quotient)}; } -} // namespace feeunit +} // namespace unit +// Fee Levels template -using FeeLevel = feeunit::TaggedFee; +using FeeLevel = unit::ValueUnit; using FeeLevel64 = FeeLevel; using FeeLevelDouble = FeeLevel; +// Basis points (Bips) +template +using Bips = unit::ValueUnit; +using Bips16 = Bips; +using Bips32 = Bips; +template +using TenthBips = unit::ValueUnit; +using TenthBips16 = TenthBips; +using TenthBips32 = TenthBips; + template < class Source1, class Source2, class Dest, - class = feeunit::enable_muldiv_t> + class = unit::enable_muldiv_t> std::optional mulDiv(Source1 value, Dest mul, Source2 div) { - return feeunit::mulDivU(value, mul, div); + return unit::mulDivU(value, mul, div); } template < class Source1, class Source2, class Dest, - class = feeunit::enable_muldiv_commute_t> + class = unit::enable_muldiv_commute_t> std::optional mulDiv(Dest value, Source1 mul, Source2 div) { // Multiplication is commutative - return feeunit::mulDivU(mul, value, div); + return unit::mulDivU(mul, value, div); } -template > +template > std::optional mulDiv(std::uint64_t value, Dest mul, std::uint64_t div) { // Give the scalars a non-tag so the // unit-handling version gets called. - return feeunit::mulDivU(feeunit::scalar(value), mul, feeunit::scalar(div)); + return unit::mulDivU(unit::scalar(value), mul, unit::scalar(div)); } -template > +template > std::optional mulDiv(Dest value, std::uint64_t mul, std::uint64_t div) { @@ -516,13 +555,13 @@ mulDiv(Dest value, std::uint64_t mul, std::uint64_t div) template < class Source1, class Source2, - class = feeunit::enable_muldiv_sources_t> + class = unit::enable_muldiv_sources_t> std::optional mulDiv(Source1 value, std::uint64_t mul, Source2 div) { // Give the scalars a dimensionless unit so the // unit-handling version gets called. - auto unitresult = feeunit::mulDivU(value, feeunit::scalar(mul), div); + auto unitresult = unit::mulDivU(value, unit::scalar(mul), div); if (!unitresult) return std::nullopt; @@ -533,7 +572,7 @@ mulDiv(Source1 value, std::uint64_t mul, Source2 div) template < class Source1, class Source2, - class = feeunit::enable_muldiv_sources_t> + class = unit::enable_muldiv_sources_t> std::optional mulDiv(std::uint64_t value, Source1 mul, Source2 div) { @@ -553,6 +592,16 @@ safe_cast(Src s) noexcept return Dest{safe_cast(s.value())}; } +template +constexpr std::enable_if_t< + std::is_integral_v && std::is_integral_v, + Dest> +safe_cast(Src s) noexcept +{ + // Dest may not have an explicit value constructor + return Dest{safe_cast(s)}; +} + template constexpr std::enable_if_t< std::is_same_v && @@ -565,6 +614,16 @@ unsafe_cast(Src s) noexcept return Dest{unsafe_cast(s.value())}; } +template +constexpr std::enable_if_t< + std::is_integral_v && std::is_integral_v, + Dest> +unsafe_cast(Src s) noexcept +{ + // Dest may not have an explicit value constructor + return Dest{unsafe_cast(s)}; +} + } // namespace ripple -#endif // BASICS_FEES_H_INCLUDED +#endif // PROTOCOL_UNITS_H_INCLUDED diff --git a/include/xrpl/protocol/XRPAmount.h b/include/xrpl/protocol/XRPAmount.h index 332735dc6f..a7a013d625 100644 --- a/include/xrpl/protocol/XRPAmount.h +++ b/include/xrpl/protocol/XRPAmount.h @@ -24,7 +24,7 @@ #include #include #include -#include +#include #include #include @@ -42,7 +42,7 @@ class XRPAmount : private boost::totally_ordered, private boost::additive { public: - using unit_type = feeunit::dropTag; + using unit_type = unit::dropTag; using value_type = std::int64_t; private: diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 63bc52de6a..40d118c684 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -172,7 +172,8 @@ TYPED_SFIELD(sfNFTokenID, UINT256, 10) TYPED_SFIELD(sfEmitParentTxnID, UINT256, 11) TYPED_SFIELD(sfEmitNonce, UINT256, 12) TYPED_SFIELD(sfEmitHookHash, UINT256, 13) -TYPED_SFIELD(sfAMMID, UINT256, 14) +TYPED_SFIELD(sfAMMID, UINT256, 14, + SField::sMD_PseudoAccount |SField::sMD_Default) // 256-bit (uncommon) TYPED_SFIELD(sfBookDirectory, UINT256, 16) @@ -194,7 +195,8 @@ TYPED_SFIELD(sfHookHash, UINT256, 31) TYPED_SFIELD(sfHookNamespace, UINT256, 32) TYPED_SFIELD(sfHookSetTxnID, UINT256, 33) TYPED_SFIELD(sfDomainID, UINT256, 34) -TYPED_SFIELD(sfVaultID, UINT256, 35) +TYPED_SFIELD(sfVaultID, UINT256, 35, + SField::sMD_PseudoAccount | SField::sMD_Default) // number (common) TYPED_SFIELD(sfNumber, NUMBER, 1) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 0f614df692..b61bf3135f 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -22,14 +22,47 @@ #endif /** - * TRANSACTION(tag, value, name, delegatable, fields) + * TRANSACTION(tag, value, name, delegatable, privileges, fields) + * + * To ease maintenance, you may replace any unneeded values with "..." + * e.g. #define TRANSACTION(tag, value, name, ...) * * You must define a transactor class in the `ripple` namespace named `name`, - * and include its header in `src/xrpld/app/tx/detail/applySteps.cpp`. + * and include its header alongside the TRANSACTOR definition using this + * format: + * #if TRANSACTION_INCLUDE + * # include + * #endif + * + * The `privileges` parameter of the TRANSACTION macro is a bitfield + * defining which operations the transaction can perform. + * + * The values are only used in InvariantCheck.cpp + * Valid values are + noPriv - The transaction can not do any of the enumerated operations + createAcct - The transaction can create a new ACCOUNT_ROOT object. + createPseudoAcct - The transaction can create a pseudo account, + which implies createAcct + mustDeleteAcct - The transaction must delete an ACCOUNT_ROOT object + mayDeleteAcct - The transaction may delete an ACCOUNT_ROOT object, + but does not have to + overrideFreeze - The transaction can override some freeze rules + changeNFTCounts - The transaction can mint or burn an NFT + createMPTIssuance - The transaction can create a new MPT issuance + destroyMPTIssuance - The transaction can destroy an MPT issuance + mustAuthorizeMPT - The transaction MUST create or delete an MPT + object (except by issuer) + mayAuthorizeMPT - The transaction MAY create or delete an MPT + object (except by issuer) */ /** This transaction type executes a payment. */ -TRANSACTION(ttPAYMENT, 0, Payment, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttPAYMENT, 0, Payment, + Delegation::delegatable, + createAcct, ({ {sfDestination, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, {sfSendMax, soeOPTIONAL, soeMPTSupported}, @@ -41,7 +74,12 @@ TRANSACTION(ttPAYMENT, 0, Payment, Delegation::delegatable, ({ })) /** This transaction type creates an escrow object. */ -TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, + Delegation::delegatable, + noPriv, ({ {sfDestination, soeREQUIRED}, {sfAmount, soeREQUIRED}, {sfCondition, soeOPTIONAL}, @@ -51,7 +89,9 @@ TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, Delegation::delegatable, ({ })) /** This transaction type completes an existing escrow. */ -TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, Delegation::delegatable, ({ +TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, + Delegation::delegatable, + noPriv, ({ {sfOwner, soeREQUIRED}, {sfOfferSequence, soeREQUIRED}, {sfFulfillment, soeOPTIONAL}, @@ -61,7 +101,12 @@ TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, Delegation::delegatable, ({ /** This transaction type adjusts various account settings. */ -TRANSACTION(ttACCOUNT_SET, 3, AccountSet, Delegation::notDelegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttACCOUNT_SET, 3, AccountSet, + Delegation::notDelegatable, + noPriv, ({ {sfEmailHash, soeOPTIONAL}, {sfWalletLocator, soeOPTIONAL}, {sfWalletSize, soeOPTIONAL}, @@ -75,20 +120,35 @@ TRANSACTION(ttACCOUNT_SET, 3, AccountSet, Delegation::notDelegatable, ({ })) /** This transaction type cancels an existing escrow. */ -TRANSACTION(ttESCROW_CANCEL, 4, EscrowCancel, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttESCROW_CANCEL, 4, EscrowCancel, + Delegation::delegatable, + noPriv, ({ {sfOwner, soeREQUIRED}, {sfOfferSequence, soeREQUIRED}, })) /** This transaction type sets or clears an account's "regular key". */ -TRANSACTION(ttREGULAR_KEY_SET, 5, SetRegularKey, Delegation::notDelegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttREGULAR_KEY_SET, 5, SetRegularKey, + Delegation::notDelegatable, + noPriv, ({ {sfRegularKey, soeOPTIONAL}, })) // 6 deprecated /** This transaction type creates an offer to trade one asset for another. */ -TRANSACTION(ttOFFER_CREATE, 7, OfferCreate, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttOFFER_CREATE, 7, OfferCreate, + Delegation::delegatable, + noPriv, ({ {sfTakerPays, soeREQUIRED}, {sfTakerGets, soeREQUIRED}, {sfExpiration, soeOPTIONAL}, @@ -96,14 +156,24 @@ TRANSACTION(ttOFFER_CREATE, 7, OfferCreate, Delegation::delegatable, ({ })) /** This transaction type cancels existing offers to trade one asset for another. */ -TRANSACTION(ttOFFER_CANCEL, 8, OfferCancel, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttOFFER_CANCEL, 8, OfferCancel, + Delegation::delegatable, + noPriv, ({ {sfOfferSequence, soeREQUIRED}, })) // 9 deprecated /** This transaction type creates a new set of tickets. */ -TRANSACTION(ttTICKET_CREATE, 10, TicketCreate, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttTICKET_CREATE, 10, TicketCreate, + Delegation::delegatable, + noPriv, ({ {sfTicketCount, soeREQUIRED}, })) @@ -112,13 +182,23 @@ TRANSACTION(ttTICKET_CREATE, 10, TicketCreate, Delegation::delegatable, ({ /** This transaction type modifies the signer list associated with an account. */ // The SignerEntries are optional because a SignerList is deleted by // setting the SignerQuorum to zero and omitting SignerEntries. -TRANSACTION(ttSIGNER_LIST_SET, 12, SignerListSet, Delegation::notDelegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttSIGNER_LIST_SET, 12, SignerListSet, + Delegation::notDelegatable, + noPriv, ({ {sfSignerQuorum, soeREQUIRED}, {sfSignerEntries, soeOPTIONAL}, })) /** This transaction type creates a new unidirectional XRP payment channel. */ -TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, + Delegation::delegatable, + noPriv, ({ {sfDestination, soeREQUIRED}, {sfAmount, soeREQUIRED}, {sfSettleDelay, soeREQUIRED}, @@ -128,14 +208,18 @@ TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, Delegation::delegatable, })) /** This transaction type funds an existing unidirectional XRP payment channel. */ -TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund, Delegation::delegatable, ({ +TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund, + Delegation::delegatable, + noPriv, ({ {sfChannel, soeREQUIRED}, {sfAmount, soeREQUIRED}, {sfExpiration, soeOPTIONAL}, })) /** This transaction type submits a claim against an existing unidirectional payment channel. */ -TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, Delegation::delegatable, ({ +TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, + Delegation::delegatable, + noPriv, ({ {sfChannel, soeREQUIRED}, {sfAmount, soeOPTIONAL}, {sfBalance, soeOPTIONAL}, @@ -145,7 +229,12 @@ TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, Delegation::delegatable, ( })) /** This transaction type creates a new check. */ -TRANSACTION(ttCHECK_CREATE, 16, CheckCreate, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttCHECK_CREATE, 16, CheckCreate, + Delegation::delegatable, + noPriv, ({ {sfDestination, soeREQUIRED}, {sfSendMax, soeREQUIRED}, {sfExpiration, soeOPTIONAL}, @@ -154,19 +243,34 @@ TRANSACTION(ttCHECK_CREATE, 16, CheckCreate, Delegation::delegatable, ({ })) /** This transaction type cashes an existing check. */ -TRANSACTION(ttCHECK_CASH, 17, CheckCash, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttCHECK_CASH, 17, CheckCash, + Delegation::delegatable, + noPriv, ({ {sfCheckID, soeREQUIRED}, {sfAmount, soeOPTIONAL}, {sfDeliverMin, soeOPTIONAL}, })) /** This transaction type cancels an existing check. */ -TRANSACTION(ttCHECK_CANCEL, 18, CheckCancel, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttCHECK_CANCEL, 18, CheckCancel, + Delegation::delegatable, + noPriv, ({ {sfCheckID, soeREQUIRED}, })) /** This transaction type grants or revokes authorization to transfer funds. */ -TRANSACTION(ttDEPOSIT_PREAUTH, 19, DepositPreauth, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttDEPOSIT_PREAUTH, 19, DepositPreauth, + Delegation::delegatable, + noPriv, ({ {sfAuthorize, soeOPTIONAL}, {sfUnauthorize, soeOPTIONAL}, {sfAuthorizeCredentials, soeOPTIONAL}, @@ -174,14 +278,24 @@ TRANSACTION(ttDEPOSIT_PREAUTH, 19, DepositPreauth, Delegation::delegatable, ({ })) /** This transaction type modifies a trustline between two accounts. */ -TRANSACTION(ttTRUST_SET, 20, TrustSet, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttTRUST_SET, 20, TrustSet, + Delegation::delegatable, + noPriv, ({ {sfLimitAmount, soeOPTIONAL}, {sfQualityIn, soeOPTIONAL}, {sfQualityOut, soeOPTIONAL}, })) /** This transaction type deletes an existing account. */ -TRANSACTION(ttACCOUNT_DELETE, 21, AccountDelete, Delegation::notDelegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttACCOUNT_DELETE, 21, AccountDelete, + Delegation::notDelegatable, + mustDeleteAcct, ({ {sfDestination, soeREQUIRED}, {sfDestinationTag, soeOPTIONAL}, {sfCredentialIDs, soeOPTIONAL}, @@ -190,7 +304,12 @@ TRANSACTION(ttACCOUNT_DELETE, 21, AccountDelete, Delegation::notDelegatable, ({ // 22 reserved /** This transaction mints a new NFT. */ -TRANSACTION(ttNFTOKEN_MINT, 25, NFTokenMint, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttNFTOKEN_MINT, 25, NFTokenMint, + Delegation::delegatable, + changeNFTCounts, ({ {sfNFTokenTaxon, soeREQUIRED}, {sfTransferFee, soeOPTIONAL}, {sfIssuer, soeOPTIONAL}, @@ -201,13 +320,23 @@ TRANSACTION(ttNFTOKEN_MINT, 25, NFTokenMint, Delegation::delegatable, ({ })) /** This transaction burns (i.e. destroys) an existing NFT. */ -TRANSACTION(ttNFTOKEN_BURN, 26, NFTokenBurn, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttNFTOKEN_BURN, 26, NFTokenBurn, + Delegation::delegatable, + changeNFTCounts, ({ {sfNFTokenID, soeREQUIRED}, {sfOwner, soeOPTIONAL}, })) /** This transaction creates a new offer to buy or sell an NFT. */ -TRANSACTION(ttNFTOKEN_CREATE_OFFER, 27, NFTokenCreateOffer, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttNFTOKEN_CREATE_OFFER, 27, NFTokenCreateOffer, + Delegation::delegatable, + noPriv, ({ {sfNFTokenID, soeREQUIRED}, {sfAmount, soeREQUIRED}, {sfDestination, soeOPTIONAL}, @@ -216,25 +345,45 @@ TRANSACTION(ttNFTOKEN_CREATE_OFFER, 27, NFTokenCreateOffer, Delegation::delegata })) /** This transaction cancels an existing offer to buy or sell an existing NFT. */ -TRANSACTION(ttNFTOKEN_CANCEL_OFFER, 28, NFTokenCancelOffer, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttNFTOKEN_CANCEL_OFFER, 28, NFTokenCancelOffer, + Delegation::delegatable, + noPriv, ({ {sfNFTokenOffers, soeREQUIRED}, })) /** This transaction accepts an existing offer to buy or sell an existing NFT. */ -TRANSACTION(ttNFTOKEN_ACCEPT_OFFER, 29, NFTokenAcceptOffer, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttNFTOKEN_ACCEPT_OFFER, 29, NFTokenAcceptOffer, + Delegation::delegatable, + noPriv, ({ {sfNFTokenBuyOffer, soeOPTIONAL}, {sfNFTokenSellOffer, soeOPTIONAL}, {sfNFTokenBrokerFee, soeOPTIONAL}, })) /** This transaction claws back issued tokens. */ -TRANSACTION(ttCLAWBACK, 30, Clawback, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttCLAWBACK, 30, Clawback, + Delegation::delegatable, + noPriv, ({ {sfAmount, soeREQUIRED, soeMPTSupported}, {sfHolder, soeOPTIONAL}, })) /** This transaction claws back tokens from an AMM pool. */ -TRANSACTION(ttAMM_CLAWBACK, 31, AMMClawback, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttAMM_CLAWBACK, 31, AMMClawback, + Delegation::delegatable, + mayDeleteAcct | overrideFreeze, ({ {sfHolder, soeREQUIRED}, {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, @@ -242,14 +391,24 @@ TRANSACTION(ttAMM_CLAWBACK, 31, AMMClawback, Delegation::delegatable, ({ })) /** This transaction type creates an AMM instance */ -TRANSACTION(ttAMM_CREATE, 35, AMMCreate, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttAMM_CREATE, 35, AMMCreate, + Delegation::delegatable, + createPseudoAcct, ({ {sfAmount, soeREQUIRED}, {sfAmount2, soeREQUIRED}, {sfTradingFee, soeREQUIRED}, })) /** This transaction type deposits into an AMM instance */ -TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit, + Delegation::delegatable, + noPriv, ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, {sfAmount, soeOPTIONAL}, @@ -260,7 +419,12 @@ TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit, Delegation::delegatable, ({ })) /** This transaction type withdraws from an AMM instance */ -TRANSACTION(ttAMM_WITHDRAW, 37, AMMWithdraw, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttAMM_WITHDRAW, 37, AMMWithdraw, + Delegation::delegatable, + mayDeleteAcct, ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, {sfAmount, soeOPTIONAL}, @@ -270,14 +434,24 @@ TRANSACTION(ttAMM_WITHDRAW, 37, AMMWithdraw, Delegation::delegatable, ({ })) /** This transaction type votes for the trading fee */ -TRANSACTION(ttAMM_VOTE, 38, AMMVote, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttAMM_VOTE, 38, AMMVote, + Delegation::delegatable, + noPriv, ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, {sfTradingFee, soeREQUIRED}, })) /** This transaction type bids for the auction slot */ -TRANSACTION(ttAMM_BID, 39, AMMBid, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttAMM_BID, 39, AMMBid, + Delegation::delegatable, + noPriv, ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, {sfBidMin, soeOPTIONAL}, @@ -286,20 +460,32 @@ TRANSACTION(ttAMM_BID, 39, AMMBid, Delegation::delegatable, ({ })) /** This transaction type deletes AMM in the empty state */ -TRANSACTION(ttAMM_DELETE, 40, AMMDelete, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttAMM_DELETE, 40, AMMDelete, + Delegation::delegatable, + mustDeleteAcct, ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, })) /** This transactions creates a crosschain sequence number */ -TRANSACTION(ttXCHAIN_CREATE_CLAIM_ID, 41, XChainCreateClaimID, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttXCHAIN_CREATE_CLAIM_ID, 41, XChainCreateClaimID, + Delegation::delegatable, + noPriv, ({ {sfXChainBridge, soeREQUIRED}, {sfSignatureReward, soeREQUIRED}, {sfOtherChainSource, soeREQUIRED}, })) /** This transactions initiates a crosschain transaction */ -TRANSACTION(ttXCHAIN_COMMIT, 42, XChainCommit, Delegation::delegatable, ({ +TRANSACTION(ttXCHAIN_COMMIT, 42, XChainCommit, + Delegation::delegatable, + noPriv, ({ {sfXChainBridge, soeREQUIRED}, {sfXChainClaimID, soeREQUIRED}, {sfAmount, soeREQUIRED}, @@ -307,7 +493,9 @@ TRANSACTION(ttXCHAIN_COMMIT, 42, XChainCommit, Delegation::delegatable, ({ })) /** This transaction completes a crosschain transaction */ -TRANSACTION(ttXCHAIN_CLAIM, 43, XChainClaim, Delegation::delegatable, ({ +TRANSACTION(ttXCHAIN_CLAIM, 43, XChainClaim, + Delegation::delegatable, + noPriv, ({ {sfXChainBridge, soeREQUIRED}, {sfXChainClaimID, soeREQUIRED}, {sfDestination, soeREQUIRED}, @@ -316,7 +504,9 @@ TRANSACTION(ttXCHAIN_CLAIM, 43, XChainClaim, Delegation::delegatable, ({ })) /** This transaction initiates a crosschain account create transaction */ -TRANSACTION(ttXCHAIN_ACCOUNT_CREATE_COMMIT, 44, XChainAccountCreateCommit, Delegation::delegatable, ({ +TRANSACTION(ttXCHAIN_ACCOUNT_CREATE_COMMIT, 44, XChainAccountCreateCommit, + Delegation::delegatable, + noPriv, ({ {sfXChainBridge, soeREQUIRED}, {sfDestination, soeREQUIRED}, {sfAmount, soeREQUIRED}, @@ -324,7 +514,9 @@ TRANSACTION(ttXCHAIN_ACCOUNT_CREATE_COMMIT, 44, XChainAccountCreateCommit, Deleg })) /** This transaction adds an attestation to a claim */ -TRANSACTION(ttXCHAIN_ADD_CLAIM_ATTESTATION, 45, XChainAddClaimAttestation, Delegation::delegatable, ({ +TRANSACTION(ttXCHAIN_ADD_CLAIM_ATTESTATION, 45, XChainAddClaimAttestation, + Delegation::delegatable, + createAcct, ({ {sfXChainBridge, soeREQUIRED}, {sfAttestationSignerAccount, soeREQUIRED}, @@ -340,7 +532,10 @@ TRANSACTION(ttXCHAIN_ADD_CLAIM_ATTESTATION, 45, XChainAddClaimAttestation, Deleg })) /** This transaction adds an attestation to an account */ -TRANSACTION(ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION, 46, XChainAddAccountCreateAttestation, Delegation::delegatable, ({ +TRANSACTION(ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION, 46, + XChainAddAccountCreateAttestation, + Delegation::delegatable, + createAcct, ({ {sfXChainBridge, soeREQUIRED}, {sfAttestationSignerAccount, soeREQUIRED}, @@ -357,31 +552,47 @@ TRANSACTION(ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION, 46, XChainAddAccountCreateA })) /** This transaction modifies a sidechain */ -TRANSACTION(ttXCHAIN_MODIFY_BRIDGE, 47, XChainModifyBridge, Delegation::delegatable, ({ +TRANSACTION(ttXCHAIN_MODIFY_BRIDGE, 47, XChainModifyBridge, + Delegation::delegatable, + noPriv, ({ {sfXChainBridge, soeREQUIRED}, {sfSignatureReward, soeOPTIONAL}, {sfMinAccountCreateAmount, soeOPTIONAL}, })) /** This transactions creates a sidechain */ -TRANSACTION(ttXCHAIN_CREATE_BRIDGE, 48, XChainCreateBridge, Delegation::delegatable, ({ +TRANSACTION(ttXCHAIN_CREATE_BRIDGE, 48, XChainCreateBridge, + Delegation::delegatable, + noPriv, ({ {sfXChainBridge, soeREQUIRED}, {sfSignatureReward, soeREQUIRED}, {sfMinAccountCreateAmount, soeOPTIONAL}, })) /** This transaction type creates or updates a DID */ -TRANSACTION(ttDID_SET, 49, DIDSet, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttDID_SET, 49, DIDSet, + Delegation::delegatable, + noPriv, ({ {sfDIDDocument, soeOPTIONAL}, {sfURI, soeOPTIONAL}, {sfData, soeOPTIONAL}, })) /** This transaction type deletes a DID */ -TRANSACTION(ttDID_DELETE, 50, DIDDelete, Delegation::delegatable, ({})) +TRANSACTION(ttDID_DELETE, 50, DIDDelete, + Delegation::delegatable, + noPriv, ({})) /** This transaction type creates an Oracle instance */ -TRANSACTION(ttORACLE_SET, 51, OracleSet, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttORACLE_SET, 51, OracleSet, + Delegation::delegatable, + noPriv, ({ {sfOracleDocumentID, soeREQUIRED}, {sfProvider, soeOPTIONAL}, {sfURI, soeOPTIONAL}, @@ -391,18 +602,33 @@ TRANSACTION(ttORACLE_SET, 51, OracleSet, Delegation::delegatable, ({ })) /** This transaction type deletes an Oracle instance */ -TRANSACTION(ttORACLE_DELETE, 52, OracleDelete, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttORACLE_DELETE, 52, OracleDelete, + Delegation::delegatable, + noPriv, ({ {sfOracleDocumentID, soeREQUIRED}, })) /** This transaction type fixes a problem in the ledger state */ -TRANSACTION(ttLEDGER_STATE_FIX, 53, LedgerStateFix, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttLEDGER_STATE_FIX, 53, LedgerStateFix, + Delegation::delegatable, + noPriv, ({ {sfLedgerFixType, soeREQUIRED}, {sfOwner, soeOPTIONAL}, })) /** This transaction type creates a MPTokensIssuance instance */ -TRANSACTION(ttMPTOKEN_ISSUANCE_CREATE, 54, MPTokenIssuanceCreate, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttMPTOKEN_ISSUANCE_CREATE, 54, MPTokenIssuanceCreate, + Delegation::delegatable, + createMPTIssuance, ({ {sfAssetScale, soeOPTIONAL}, {sfTransferFee, soeOPTIONAL}, {sfMaximumAmount, soeOPTIONAL}, @@ -410,24 +636,44 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_CREATE, 54, MPTokenIssuanceCreate, Delegation::de })) /** This transaction type destroys a MPTokensIssuance instance */ -TRANSACTION(ttMPTOKEN_ISSUANCE_DESTROY, 55, MPTokenIssuanceDestroy, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttMPTOKEN_ISSUANCE_DESTROY, 55, MPTokenIssuanceDestroy, + Delegation::delegatable, + destroyMPTIssuance, ({ {sfMPTokenIssuanceID, soeREQUIRED}, })) /** This transaction type sets flags on a MPTokensIssuance or MPToken instance */ -TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet, + Delegation::delegatable, + noPriv, ({ {sfMPTokenIssuanceID, soeREQUIRED}, {sfHolder, soeOPTIONAL}, })) /** This transaction type authorizes a MPToken instance */ -TRANSACTION(ttMPTOKEN_AUTHORIZE, 57, MPTokenAuthorize, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttMPTOKEN_AUTHORIZE, 57, MPTokenAuthorize, + Delegation::delegatable, + mustAuthorizeMPT, ({ {sfMPTokenIssuanceID, soeREQUIRED}, {sfHolder, soeOPTIONAL}, })) /** This transaction type create an Credential instance */ -TRANSACTION(ttCREDENTIAL_CREATE, 58, CredentialCreate, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttCREDENTIAL_CREATE, 58, CredentialCreate, + Delegation::delegatable, + noPriv, ({ {sfSubject, soeREQUIRED}, {sfCredentialType, soeREQUIRED}, {sfExpiration, soeOPTIONAL}, @@ -435,44 +681,73 @@ TRANSACTION(ttCREDENTIAL_CREATE, 58, CredentialCreate, Delegation::delegatable, })) /** This transaction type accept an Credential object */ -TRANSACTION(ttCREDENTIAL_ACCEPT, 59, CredentialAccept, Delegation::delegatable, ({ +TRANSACTION(ttCREDENTIAL_ACCEPT, 59, CredentialAccept, + Delegation::delegatable, + noPriv, ({ {sfIssuer, soeREQUIRED}, {sfCredentialType, soeREQUIRED}, })) /** This transaction type delete an Credential object */ -TRANSACTION(ttCREDENTIAL_DELETE, 60, CredentialDelete, Delegation::delegatable, ({ +TRANSACTION(ttCREDENTIAL_DELETE, 60, CredentialDelete, + Delegation::delegatable, + noPriv, ({ {sfSubject, soeOPTIONAL}, {sfIssuer, soeOPTIONAL}, {sfCredentialType, soeREQUIRED}, })) /** This transaction type modify a NFToken */ -TRANSACTION(ttNFTOKEN_MODIFY, 61, NFTokenModify, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttNFTOKEN_MODIFY, 61, NFTokenModify, + Delegation::delegatable, + noPriv, ({ {sfNFTokenID, soeREQUIRED}, {sfOwner, soeOPTIONAL}, {sfURI, soeOPTIONAL}, })) /** This transaction type creates or modifies a Permissioned Domain */ -TRANSACTION(ttPERMISSIONED_DOMAIN_SET, 62, PermissionedDomainSet, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttPERMISSIONED_DOMAIN_SET, 62, PermissionedDomainSet, + Delegation::delegatable, + noPriv, ({ {sfDomainID, soeOPTIONAL}, {sfAcceptedCredentials, soeREQUIRED}, })) /** This transaction type deletes a Permissioned Domain */ -TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 63, PermissionedDomainDelete, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 63, PermissionedDomainDelete, + Delegation::delegatable, + noPriv, ({ {sfDomainID, soeREQUIRED}, })) /** This transaction type delegates authorized account specified permissions */ -TRANSACTION(ttDELEGATE_SET, 64, DelegateSet, Delegation::notDelegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttDELEGATE_SET, 64, DelegateSet, + Delegation::notDelegatable, + noPriv, ({ {sfAuthorize, soeREQUIRED}, {sfPermissions, soeREQUIRED}, })) /** This transaction creates a single asset vault. */ -TRANSACTION(ttVAULT_CREATE, 65, VaultCreate, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttVAULT_CREATE, 65, VaultCreate, + Delegation::delegatable, + createPseudoAcct | createMPTIssuance, ({ {sfAsset, soeREQUIRED, soeMPTSupported}, {sfAssetsMaximum, soeOPTIONAL}, {sfMPTokenMetadata, soeOPTIONAL}, @@ -482,7 +757,12 @@ TRANSACTION(ttVAULT_CREATE, 65, VaultCreate, Delegation::delegatable, ({ })) /** This transaction updates a single asset vault. */ -TRANSACTION(ttVAULT_SET, 66, VaultSet, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttVAULT_SET, 66, VaultSet, + Delegation::delegatable, + noPriv, ({ {sfVaultID, soeREQUIRED}, {sfAssetsMaximum, soeOPTIONAL}, {sfDomainID, soeOPTIONAL}, // PermissionedDomainID @@ -490,25 +770,45 @@ TRANSACTION(ttVAULT_SET, 66, VaultSet, Delegation::delegatable, ({ })) /** This transaction deletes a single asset vault. */ -TRANSACTION(ttVAULT_DELETE, 67, VaultDelete, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttVAULT_DELETE, 67, VaultDelete, + Delegation::delegatable, + mustDeleteAcct | destroyMPTIssuance, ({ {sfVaultID, soeREQUIRED}, })) /** This transaction trades assets for shares with a vault. */ -TRANSACTION(ttVAULT_DEPOSIT, 68, VaultDeposit, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttVAULT_DEPOSIT, 68, VaultDeposit, + Delegation::delegatable, + mayAuthorizeMPT, ({ {sfVaultID, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, })) /** This transaction trades shares for assets with a vault. */ -TRANSACTION(ttVAULT_WITHDRAW, 69, VaultWithdraw, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttVAULT_WITHDRAW, 69, VaultWithdraw, + Delegation::delegatable, + noPriv, ({ {sfVaultID, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, {sfDestination, soeOPTIONAL}, })) /** This transaction claws back tokens from a vault. */ -TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback, Delegation::delegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback, + Delegation::delegatable, + noPriv, ({ {sfVaultID, soeREQUIRED}, {sfHolder, soeREQUIRED}, {sfAmount, soeOPTIONAL, soeMPTSupported}, @@ -518,7 +818,12 @@ TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback, Delegation::delegatable, ({ For details, see: https://xrpl.org/amendments.html */ -TRANSACTION(ttAMENDMENT, 100, EnableAmendment, Delegation::notDelegatable, ({ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttAMENDMENT, 100, EnableAmendment, + Delegation::notDelegatable, + noPriv, ({ {sfLedgerSequence, soeREQUIRED}, {sfAmendment, soeREQUIRED}, })) @@ -526,7 +831,9 @@ TRANSACTION(ttAMENDMENT, 100, EnableAmendment, Delegation::notDelegatable, ({ /** This system-generated transaction type is used to update the network's fee settings. For details, see: https://xrpl.org/fee-voting.html */ -TRANSACTION(ttFEE, 101, SetFee, Delegation::notDelegatable, ({ +TRANSACTION(ttFEE, 101, SetFee, + Delegation::notDelegatable, + noPriv, ({ {sfLedgerSequence, soeOPTIONAL}, // Old version uses raw numbers {sfBaseFee, soeOPTIONAL}, @@ -543,7 +850,9 @@ TRANSACTION(ttFEE, 101, SetFee, Delegation::notDelegatable, ({ For details, see: https://xrpl.org/negative-unl.html */ -TRANSACTION(ttUNL_MODIFY, 102, UNLModify, Delegation::notDelegatable, ({ +TRANSACTION(ttUNL_MODIFY, 102, UNLModify, + Delegation::notDelegatable, + noPriv, ({ {sfUNLModifyDisabling, soeREQUIRED}, {sfLedgerSequence, soeREQUIRED}, {sfUNLModifyValidator, soeREQUIRED}, diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index de3560d7f9..fc7f367562 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -703,7 +703,7 @@ JSS(write_load); // out: GetCounts #pragma push_macro("TRANSACTION") #undef TRANSACTION -#define TRANSACTION(tag, value, name, delegatable, fields) JSS(name); +#define TRANSACTION(tag, value, name, ...) JSS(name); #include diff --git a/src/libxrpl/protocol/Permissions.cpp b/src/libxrpl/protocol/Permissions.cpp index dbe5325a4e..35f788e9eb 100644 --- a/src/libxrpl/protocol/Permissions.cpp +++ b/src/libxrpl/protocol/Permissions.cpp @@ -29,7 +29,7 @@ Permission::Permission() #pragma push_macro("TRANSACTION") #undef TRANSACTION -#define TRANSACTION(tag, value, name, delegatable, fields) {value, delegatable}, +#define TRANSACTION(tag, value, name, delegatable, ...) {value, delegatable}, #include @@ -145,4 +145,4 @@ Permission::permissionToTxType(uint32_t const& value) const return static_cast(value - 1); } -} // namespace ripple \ No newline at end of file +} // namespace ripple diff --git a/src/libxrpl/protocol/SField.cpp b/src/libxrpl/protocol/SField.cpp index 1ffce099b8..478e6a4066 100644 --- a/src/libxrpl/protocol/SField.cpp +++ b/src/libxrpl/protocol/SField.cpp @@ -17,6 +17,7 @@ */ //============================================================================== +#include #include #include @@ -28,6 +29,7 @@ namespace ripple { SField::IsSigning const SField::notSigning; int SField::num = 0; std::map SField::knownCodeToField; +std::map SField::knownNameToField; // Give only this translation unit permission to construct SFields struct SField::private_access_tag_t @@ -45,7 +47,7 @@ TypedField::TypedField(private_access_tag_t pat, Args&&... args) } // Construct all compile-time SFields, and register them in the knownCodeToField -// database: +// and knownNameToField databases: // Use macros for most SField construction to enforce naming conventions. #pragma push_macro("UNTYPED_SFIELD") @@ -99,7 +101,14 @@ SField::SField( , signingField(signing) , jsonName(fieldName.c_str()) { + XRPL_ASSERT( + !knownCodeToField.contains(fieldCode), + "ripple::SField::SField(tid,fv,fn,meta,signing) : fieldCode is unique"); + XRPL_ASSERT( + !knownNameToField.contains(fieldName), + "ripple::SField::SField(tid,fv,fn,meta,signing) : fieldName is unique"); knownCodeToField[fieldCode] = this; + knownNameToField[fieldName] = this; } SField::SField(private_access_tag_t, int fc) @@ -111,6 +120,9 @@ SField::SField(private_access_tag_t, int fc) , signingField(IsSigning::yes) , jsonName(fieldName.c_str()) { + XRPL_ASSERT( + !knownCodeToField.contains(fieldCode), + "ripple::SField::SField(fc) : fieldCode is unique"); knownCodeToField[fieldCode] = this; } @@ -145,11 +157,11 @@ SField::compare(SField const& f1, SField const& f2) SField const& SField::getField(std::string const& fieldName) { - for (auto const& [_, f] : knownCodeToField) + auto it = knownNameToField.find(fieldName); + + if (it != knownNameToField.end()) { - (void)_; - if (f->fieldName == fieldName) - return *f; + return *(it->second); } return sfInvalid; } diff --git a/src/libxrpl/protocol/TxFormats.cpp b/src/libxrpl/protocol/TxFormats.cpp index 5edffeb666..f5e2125b3c 100644 --- a/src/libxrpl/protocol/TxFormats.cpp +++ b/src/libxrpl/protocol/TxFormats.cpp @@ -55,7 +55,7 @@ TxFormats::TxFormats() #undef TRANSACTION #define UNWRAP(...) __VA_ARGS__ -#define TRANSACTION(tag, value, name, delegatable, fields) \ +#define TRANSACTION(tag, value, name, delegatable, privileges, fields) \ add(jss::name, tag, UNWRAP fields, commonFields); #include diff --git a/src/test/app/AMMCalc_test.cpp b/src/test/app/AMMCalc_test.cpp index bebf2844b6..7349b38766 100644 --- a/src/test/app/AMMCalc_test.cpp +++ b/src/test/app/AMMCalc_test.cpp @@ -67,7 +67,7 @@ class AMMCalc_test : public beast::unit_test::suite // drops else if (match[1] == "XRPA") return XRPAmount{std::stoll(match[2])}; - return amountFromString(gw[match[1]], match[2]); + return amountFromString(gw[match[1]].asset(), match[2]); } return std::nullopt; } diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index e0b3dc1ec7..9d926fdcc1 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -7161,7 +7161,7 @@ private: using namespace test::jtx; auto const testCase = [&](std::string suffix, FeatureBitset features) { - testcase("Failed pseudo-account allocation " + suffix); + testcase("Fail pseudo-account allocation " + suffix); Env env{*this, features}; env.fund(XRP(30'000), gw, alice); env.close(); diff --git a/src/test/app/Credentials_test.cpp b/src/test/app/Credentials_test.cpp index fa6505e926..005ab0cc20 100644 --- a/src/test/app/Credentials_test.cpp +++ b/src/test/app/Credentials_test.cpp @@ -34,15 +34,6 @@ namespace ripple { namespace test { -static inline bool -checkVL( - std::shared_ptr const& sle, - SField const& field, - std::string const& expected) -{ - return strHex(expected) == strHex(sle->getFieldVL(field)); -} - struct Credentials_test : public beast::unit_test::suite { void diff --git a/src/test/app/DID_test.cpp b/src/test/app/DID_test.cpp index c885ed0861..94c0ced162 100644 --- a/src/test/app/DID_test.cpp +++ b/src/test/app/DID_test.cpp @@ -27,14 +27,6 @@ namespace ripple { namespace test { -bool -checkVL(Slice const& result, std::string expected) -{ - Serializer s; - s.addRaw(result); - return s.getString() == expected; -} - struct DID_test : public beast::unit_test::suite { void diff --git a/src/test/app/ValidatorSite_test.cpp b/src/test/app/ValidatorSite_test.cpp index 7a7511e6f0..840a6cdb43 100644 --- a/src/test/app/ValidatorSite_test.cpp +++ b/src/test/app/ValidatorSite_test.cpp @@ -37,7 +37,6 @@ #include namespace ripple { -namespace test { namespace detail { constexpr char const* realValidatorContents() @@ -56,6 +55,7 @@ auto constexpr default_expires = std::chrono::seconds{3600}; auto constexpr default_effective_overlap = std::chrono::seconds{30}; } // namespace detail +namespace test { class ValidatorSite_test : public beast::unit_test::suite { private: diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 67cc3812df..d6e1dfc73f 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -1111,7 +1111,7 @@ class Vault_test : public beast::unit_test::suite env.close(); Vault vault{env}; - Asset asset = issuer["IOU"]; + Asset asset = issuer["IOU"].asset(); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); @@ -1131,7 +1131,7 @@ class Vault_test : public beast::unit_test::suite env.close(); Vault vault{env}; - Asset asset = issuer["IOU"]; + Asset asset = issuer["IOU"].asset(); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx, ter(terNO_RIPPLE)); @@ -1148,7 +1148,7 @@ class Vault_test : public beast::unit_test::suite env.close(); Vault vault{env}; - Asset asset = issuer["IOU"]; + Asset asset = issuer["IOU"].asset(); { auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); @@ -2559,7 +2559,7 @@ class Vault_test : public beast::unit_test::suite { using namespace test::jtx; - testcase("failed pseudo-account allocation"); + testcase("fail pseudo-account allocation"); Env env{*this, supported_amendments() | featureSingleAssetVault}; Account const owner{"owner"}; Vault vault{env}; diff --git a/src/test/basics/FileUtilities_test.cpp b/src/test/basics/FileUtilities_test.cpp index 4b4cbe70c8..054a6bc0f7 100644 --- a/src/test/basics/FileUtilities_test.cpp +++ b/src/test/basics/FileUtilities_test.cpp @@ -31,7 +31,7 @@ public: void testGetFileContents() { - using namespace ripple::test::detail; + using namespace ripple::detail; using namespace boost::system; constexpr char const* expectedContents = diff --git a/src/test/basics/FeeUnits_test.cpp b/src/test/basics/Units_test.cpp similarity index 92% rename from src/test/basics/FeeUnits_test.cpp rename to src/test/basics/Units_test.cpp index 6266288896..d22ff018dd 100644 --- a/src/test/basics/FeeUnits_test.cpp +++ b/src/test/basics/Units_test.cpp @@ -17,13 +17,13 @@ */ #include -#include #include +#include namespace ripple { namespace test { -class feeunits_test : public beast::unit_test::suite +class units_test : public beast::unit_test::suite { private: void @@ -35,16 +35,16 @@ private: XRPAmount x{100}; BEAST_EXPECT(x.drops() == 100); BEAST_EXPECT( - (std::is_same_v)); + (std::is_same_v)); auto y = 4u * x; BEAST_EXPECT(y.value() == 400); BEAST_EXPECT( - (std::is_same_v)); + (std::is_same_v)); auto z = 4 * y; BEAST_EXPECT(z.value() == 1600); BEAST_EXPECT( - (std::is_same_v)); + (std::is_same_v)); FeeLevel32 f{10}; FeeLevel32 baseFee{100}; @@ -55,7 +55,7 @@ private: BEAST_EXPECT(drops.value() == 1000); BEAST_EXPECT((std::is_same_v< std::remove_reference_t::unit_type, - feeunit::dropTag>)); + unit::dropTag>)); BEAST_EXPECT((std::is_same_v< std::remove_reference_t, @@ -65,11 +65,11 @@ private: XRPAmount x{100}; BEAST_EXPECT(x.value() == 100); BEAST_EXPECT( - (std::is_same_v)); + (std::is_same_v)); auto y = 4u * x; BEAST_EXPECT(y.value() == 400); BEAST_EXPECT( - (std::is_same_v)); + (std::is_same_v)); FeeLevel64 f{10}; FeeLevel64 baseFee{100}; @@ -80,7 +80,7 @@ private: BEAST_EXPECT(drops.value() == 1000); BEAST_EXPECT((std::is_same_v< std::remove_reference_t::unit_type, - feeunit::dropTag>)); + unit::dropTag>)); BEAST_EXPECT((std::is_same_v< std::remove_reference_t, XRPAmount>)); @@ -89,12 +89,12 @@ private: FeeLevel64 x{1024}; BEAST_EXPECT(x.value() == 1024); BEAST_EXPECT( - (std::is_same_v)); + (std::is_same_v)); std::uint64_t m = 4; auto y = m * x; BEAST_EXPECT(y.value() == 4096); BEAST_EXPECT( - (std::is_same_v)); + (std::is_same_v)); XRPAmount basefee{10}; FeeLevel64 referencefee{256}; @@ -105,7 +105,7 @@ private: BEAST_EXPECT(drops.value() == 40); BEAST_EXPECT((std::is_same_v< std::remove_reference_t::unit_type, - feeunit::dropTag>)); + unit::dropTag>)); BEAST_EXPECT((std::is_same_v< std::remove_reference_t, XRPAmount>)); @@ -181,7 +181,7 @@ private: void testFunctions() { - // Explicitly test every defined function for the TaggedFee class + // Explicitly test every defined function for the ValueUnit class // since some of them are templated, but not used anywhere else. using FeeLevel32 = FeeLevel; @@ -191,8 +191,8 @@ private: return FeeLevel64{x}; }; + [[maybe_unused]] FeeLevel64 defaulted; - (void)defaulted; FeeLevel64 test{0}; BEAST_EXPECT(test.fee() == 0); @@ -278,8 +278,8 @@ private: return FeeLevelDouble{x}; }; + [[maybe_unused]] FeeLevelDouble defaulted; - (void)defaulted; FeeLevelDouble test{0}; BEAST_EXPECT(test.fee() == 0); @@ -371,7 +371,7 @@ public: } }; -BEAST_DEFINE_TESTSUITE(feeunits, ripple_basics, ripple); +BEAST_DEFINE_TESTSUITE(units, ripple_basics, ripple); } // namespace test } // namespace ripple diff --git a/src/test/basics/base_uint_test.cpp b/src/test/basics/base_uint_test.cpp index 8058e0d6f0..a6be1f327d 100644 --- a/src/test/basics/base_uint_test.cpp +++ b/src/test/basics/base_uint_test.cpp @@ -152,6 +152,7 @@ struct base_uint_test : beast::unit_test::suite uset.insert(u); BEAST_EXPECT(raw.size() == u.size()); BEAST_EXPECT(to_string(u) == "0102030405060708090A0B0C"); + BEAST_EXPECT(to_short_string(u) == "01020304..."); BEAST_EXPECT(*u.data() == 1); BEAST_EXPECT(u.signum() == 1); BEAST_EXPECT(!!u); @@ -174,6 +175,7 @@ struct base_uint_test : beast::unit_test::suite test96 v{~u}; uset.insert(v); BEAST_EXPECT(to_string(v) == "FEFDFCFBFAF9F8F7F6F5F4F3"); + BEAST_EXPECT(to_short_string(v) == "FEFDFCFB..."); BEAST_EXPECT(*v.data() == 0xfe); BEAST_EXPECT(v.signum() == 1); BEAST_EXPECT(!!v); @@ -194,6 +196,7 @@ struct base_uint_test : beast::unit_test::suite test96 z{beast::zero}; uset.insert(z); BEAST_EXPECT(to_string(z) == "000000000000000000000000"); + BEAST_EXPECT(to_short_string(z) == "00000000..."); BEAST_EXPECT(*z.data() == 0); BEAST_EXPECT(*z.begin() == 0); BEAST_EXPECT(*std::prev(z.end(), 1) == 0); @@ -214,6 +217,7 @@ struct base_uint_test : beast::unit_test::suite BEAST_EXPECT(n == z); n--; BEAST_EXPECT(to_string(n) == "FFFFFFFFFFFFFFFFFFFFFFFF"); + BEAST_EXPECT(to_short_string(n) == "FFFFFFFF..."); n = beast::zero; BEAST_EXPECT(n == z); @@ -224,6 +228,7 @@ struct base_uint_test : beast::unit_test::suite test96 x{zm1 ^ zp1}; uset.insert(x); BEAST_EXPECTS(to_string(x) == "FFFFFFFFFFFFFFFFFFFFFFFE", to_string(x)); + BEAST_EXPECTS(to_short_string(x) == "FFFFFFFF...", to_short_string(x)); BEAST_EXPECT(uset.size() == 4); diff --git a/src/test/core/Config_test.cpp b/src/test/core/Config_test.cpp index 8b0fce1e20..a1a6a079cc 100644 --- a/src/test/core/Config_test.cpp +++ b/src/test/core/Config_test.cpp @@ -128,7 +128,7 @@ backend=sqlite /** Write a rippled config file and remove when done. */ -class RippledCfgGuard : public ripple::test::detail::FileDirGuard +class RippledCfgGuard : public ripple::detail::FileDirGuard { private: path dataDir_; @@ -239,7 +239,7 @@ moreripplevalidators.net /** Write a validators.txt file and remove when done. */ -class ValidatorsTxtGuard : public test::detail::FileDirGuard +class ValidatorsTxtGuard : public detail::FileDirGuard { public: ValidatorsTxtGuard( @@ -345,7 +345,7 @@ port_wss_admin { // read from file absolute path auto const cwd = current_path(); - ripple::test::detail::DirGuard const g0(*this, "test_db"); + ripple::detail::DirGuard const g0(*this, "test_db"); path const dataDirRel("test_data_dir"); path const dataDirAbs(cwd / g0.subdir() / dataDirRel); detail::RippledCfgGuard const g( diff --git a/src/test/csf/Digraph.h b/src/test/csf/Digraph.h index 3f079eac17..e65a7af913 100644 --- a/src/test/csf/Digraph.h +++ b/src/test/csf/Digraph.h @@ -30,9 +30,6 @@ #include namespace ripple { -namespace test { -namespace csf { - namespace detail { // Dummy class when no edge data needed for graph struct NoEdgeData @@ -41,6 +38,9 @@ struct NoEdgeData } // namespace detail +namespace test { +namespace csf { + /** Directed graph Basic directed graph that uses an adjacency list to represent out edges. diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index de6b83362d..3a2171a420 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -469,9 +469,15 @@ public: Returns 0 if the trust line does not exist. */ // VFALCO NOTE This should return a unit-less amount + PrettyAmount + balance(Account const& account, Asset const& asset) const; + PrettyAmount balance(Account const& account, Issue const& issue) const; + PrettyAmount + balance(Account const& account, MPTIssue const& mptIssue) const; + /** Return the number of objects owned by an account. * Returns 0 if the account does not exist. */ diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index 534419494d..11c13543e4 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -26,7 +26,9 @@ #include #include #include +#include #include +#include #include #include @@ -35,6 +37,252 @@ namespace ripple { namespace test { namespace jtx { +/** Generic helper class for helper clases that set a field on a JTx. + + Not every helper will be able to use this because of conversions and other + issues, but for classes where it's straightforward, this can simplify things. +*/ +template < + class SField, + class StoredValue = typename SField::type::value_type, + class OutputValue = StoredValue> +struct JTxField +{ + using SF = SField; + using SV = StoredValue; + using OV = OutputValue; + +protected: + SF const& sfield_; + SV value_; + +public: + explicit JTxField(SF const& sfield, SV const& value) + : sfield_(sfield), value_(value) + { + } + + virtual ~JTxField() = default; + + virtual OV + value() const = 0; + + virtual void + operator()(Env&, JTx& jt) const + { + jt.jv[sfield_.jsonName] = value(); + } +}; + +template +struct JTxField +{ + using SF = SField; + using SV = StoredValue; + using OV = SV; + +protected: + SF const& sfield_; + SV value_; + +public: + explicit JTxField(SF const& sfield, SV const& value) + : sfield_(sfield), value_(value) + { + } + + void + operator()(Env&, JTx& jt) const + { + jt.jv[sfield_.jsonName] = value_; + } +}; + +struct timePointField + : public JTxField +{ + using SF = SF_UINT32; + using SV = NetClock::time_point; + using OV = NetClock::rep; + using base = JTxField; + +protected: + using base::value_; + +public: + explicit timePointField(SF const& sfield, SV const& value) + : JTxField(sfield, value) + { + } + + OV + value() const override + { + return value_.time_since_epoch().count(); + } +}; + +struct uint256Field : public JTxField +{ + using SF = SF_UINT256; + using SV = uint256; + using OV = std::string; + using base = JTxField; + +protected: + using base::value_; + +public: + explicit uint256Field(SF const& sfield, SV const& value) + : JTxField(sfield, value) + { + } + + OV + value() const override + { + return to_string(value_); + } +}; + +struct accountIDField : public JTxField +{ + using SF = SF_ACCOUNT; + using SV = AccountID; + using OV = std::string; + using base = JTxField; + +protected: + using base::value_; + +public: + explicit accountIDField(SF const& sfield, SV const& value) + : JTxField(sfield, value) + { + } + + OV + value() const override + { + return toBase58(value_); + } +}; + +struct blobField : public JTxField +{ + using SF = SF_VL; + using SV = std::string; + using base = JTxField; + + using JTxField::JTxField; + + explicit blobField(SF const& sfield, Slice const& cond) + : JTxField(sfield, strHex(cond)) + { + } + + template + explicit blobField(SF const& sfield, std::array const& c) + : blobField(sfield, makeSlice(c)) + { + } +}; + +template +struct valueUnitField + : public JTxField, ValueType> +{ + using SF = SField; + using SV = unit::ValueUnit; + using OV = ValueType; + using base = JTxField; + + static_assert(std::is_same_v); + +protected: + using base::value_; + +public: + using JTxField::JTxField; + + OV + value() const override + { + return value_.value(); + } +}; + +template +struct JTxFieldWrapper +{ + using JF = JTxField; + using SF = typename JF::SF; + using SV = typename JF::SV; + +protected: + SF const& sfield_; + +public: + explicit JTxFieldWrapper(SF const& sfield) : sfield_(sfield) + { + } + + JF + operator()(SV const& value) const + { + return JTxField(sfield_, value); + } +}; + +template <> +struct JTxFieldWrapper +{ + using JF = blobField; + using SF = JF::SF; + using SV = JF::SV; + +protected: + SF const& sfield_; + +public: + explicit JTxFieldWrapper(SF const& sfield) : sfield_(sfield) + { + } + + JF + operator()(SV const& cond) const + { + return JF(sfield_, makeSlice(cond)); + } + + JF + operator()(Slice const& cond) const + { + return JF(sfield_, cond); + } + + template + JF + operator()(std::array const& c) const + { + return operator()(makeSlice(c)); + } +}; + +template < + class SField, + class UnitTag, + class ValueType = typename SField::type::value_type> +using valueUnitWrapper = + JTxFieldWrapper>; + +template +using simpleField = JTxFieldWrapper>; + +/** General field definitions, or fields used in multiple transaction namespaces + */ +auto const data = JTxFieldWrapper(sfData); + // TODO We only need this long "requires" clause as polyfill, for C++20 // implementations which are missing header. Replace with // `std::ranges::range`, and accordingly use std::ranges::begin/end @@ -102,6 +350,25 @@ checkArraySize(Json::Value const& val, unsigned int size); std::uint32_t ownerCount(test::jtx::Env const& env, test::jtx::Account const& account); +[[nodiscard]] +inline bool +checkVL(Slice const& result, std::string expected) +{ + Serializer s; + s.addRaw(result); + return s.getString() == expected; +} + +[[nodiscard]] +inline bool +checkVL( + std::shared_ptr const& sle, + SField const& field, + std::string const& expected) +{ + return strHex(expected) == strHex(sle->getFieldVL(field)); +} + /* Path finding */ /******************************************************************************/ void @@ -264,86 +531,14 @@ std::array constexpr cb1 = { std::array const fb1 = {{0xA0, 0x02, 0x80, 0x00}}; /** Set the "FinishAfter" time tag on a JTx */ -struct finish_time -{ -private: - NetClock::time_point value_; - -public: - explicit finish_time(NetClock::time_point const& value) : value_(value) - { - } - - void - operator()(Env&, JTx& jt) const - { - jt.jv[sfFinishAfter.jsonName] = value_.time_since_epoch().count(); - } -}; +auto const finish_time = JTxFieldWrapper(sfFinishAfter); /** Set the "CancelAfter" time tag on a JTx */ -struct cancel_time -{ -private: - NetClock::time_point value_; +auto const cancel_time = JTxFieldWrapper(sfCancelAfter); -public: - explicit cancel_time(NetClock::time_point const& value) : value_(value) - { - } +auto const condition = JTxFieldWrapper(sfCondition); - void - operator()(jtx::Env&, jtx::JTx& jt) const - { - jt.jv[sfCancelAfter.jsonName] = value_.time_since_epoch().count(); - } -}; - -struct condition -{ -private: - std::string value_; - -public: - explicit condition(Slice const& cond) : value_(strHex(cond)) - { - } - - template - explicit condition(std::array const& c) - : condition(makeSlice(c)) - { - } - - void - operator()(Env&, JTx& jt) const - { - jt.jv[sfCondition.jsonName] = value_; - } -}; - -struct fulfillment -{ -private: - std::string value_; - -public: - explicit fulfillment(Slice condition) : value_(strHex(condition)) - { - } - - template - explicit fulfillment(std::array f) - : fulfillment(makeSlice(f)) - { - } - - void - operator()(Env&, JTx& jt) const - { - jt.jv[sfFulfillment.jsonName] = value_; - } -}; +auto const fulfillment = JTxFieldWrapper(sfFulfillment); /* Payment Channel */ /******************************************************************************/ diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h index 344a2ab73c..d0b21d31ce 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -24,9 +24,9 @@ #include #include -#include #include #include +#include #include #include @@ -34,6 +34,16 @@ #include namespace ripple { + +namespace detail { + +struct epsilon_multiple +{ + std::size_t n; +}; + +} // namespace detail + namespace test { namespace jtx { @@ -57,7 +67,7 @@ struct AnyAmount; // struct None { - Issue issue; + Asset asset; }; //------------------------------------------------------------------------------ @@ -133,6 +143,12 @@ public: return amount_; } + inline int + signum() const + { + return amount_.signum(); + } + operator STAmount const&() const { return amount_; @@ -165,17 +181,17 @@ struct PrettyAsset { private: Asset asset_; - unsigned int scale_; + std::uint64_t scale_; public: template requires std::convertible_to - PrettyAsset(A const& asset, unsigned int scale = 1) + PrettyAsset(A const& asset, std::uint32_t scale = 1) : PrettyAsset{Asset{asset}, scale} { } - PrettyAsset(Asset const& asset, unsigned int scale = 1) + PrettyAsset(Asset const& asset, std::uint32_t scale = 1) : asset_(asset), scale_(scale) { } @@ -199,10 +215,22 @@ public: template PrettyAmount operator()(T v) const + { + return operator()(Number(v)); + } + + PrettyAmount + operator()(Number v) const { STAmount amount{asset_, v * scale_}; return {amount, ""}; } + + None + operator()(none_t) const + { + return {asset_}; + } }; //------------------------------------------------------------------------------ @@ -312,15 +340,6 @@ drops(XRPAmount i) //------------------------------------------------------------------------------ -namespace detail { - -struct epsilon_multiple -{ - std::size_t n; -}; - -} // namespace detail - // The smallest possible IOU STAmount struct epsilon_t { @@ -360,6 +379,11 @@ public: { return {currency, account.id()}; } + Asset + asset() const + { + return issue(); + } /** Implicit conversion to Issue or Asset. @@ -370,9 +394,9 @@ public: { return issue(); } - operator Asset() const + operator PrettyAsset() const { - return issue(); + return asset(); } template < @@ -438,14 +462,32 @@ public: return issuanceID; } - /** Implicit conversion to MPTIssue. + /** Explicit conversion to MPTIssue or asset. + */ + ripple::MPTIssue + mptIssue() const + { + return MPTIssue{issuanceID}; + } + Asset + asset() const + { + return mptIssue(); + } + + /** Implicit conversion to MPTIssue or asset. This allows passing an MPT value where an MPTIssue is expected. */ operator ripple::MPTIssue() const { - return MPTIssue{issuanceID}; + return mptIssue(); + } + + operator PrettyAsset() const + { + return asset(); } template @@ -461,6 +503,13 @@ public: PrettyAmount operator()(detail::epsilon_multiple) const; + /** Returns None-of-Issue */ + None + operator()(none_t) const + { + return {mptIssue()}; + } + friend BookSpec operator~(MPT const& mpt) { diff --git a/src/test/jtx/balance.h b/src/test/jtx/balance.h index 3a2cf0423f..0c4a6cca1d 100644 --- a/src/test/jtx/balance.h +++ b/src/test/jtx/balance.h @@ -38,9 +38,9 @@ namespace jtx { class balance { private: - bool none_; - Account account_; - STAmount value_; + bool const none_; + Account const account_; + STAmount const value_; public: balance(Account const& account, none_t) @@ -49,7 +49,7 @@ public: } balance(Account const& account, None const& value) - : none_(true), account_(account), value_(value.issue) + : none_(true), account_(account), value_(value.asset) { } diff --git a/src/test/jtx/fee.h b/src/test/jtx/fee.h index 7d54804f87..3e3740b80d 100644 --- a/src/test/jtx/fee.h +++ b/src/test/jtx/fee.h @@ -37,6 +37,7 @@ class fee { private: bool manual_ = true; + bool increment_ = false; std::optional amount_; public: @@ -44,6 +45,10 @@ public: { } + explicit fee(increment_t) : increment_(true) + { + } + explicit fee(none_t) { } diff --git a/src/test/jtx/flags.h b/src/test/jtx/flags.h index 09e5dac52f..4adc75c6a8 100644 --- a/src/test/jtx/flags.h +++ b/src/test/jtx/flags.h @@ -27,22 +27,6 @@ #include namespace ripple { -namespace test { -namespace jtx { - -// JSON generators - -/** Add and/or remove flag. */ -Json::Value -fset(Account const& account, std::uint32_t on, std::uint32_t off = 0); - -/** Remove account flag. */ -inline Json::Value -fclear(Account const& account, std::uint32_t off) -{ - return fset(account, 0, off); -} - namespace detail { class flags_helper @@ -120,6 +104,22 @@ protected: } // namespace detail +namespace test { +namespace jtx { + +// JSON generators + +/** Add and/or remove flag. */ +Json::Value +fset(Account const& account, std::uint32_t on, std::uint32_t off = 0); + +/** Remove account flag. */ +inline Json::Value +fclear(Account const& account, std::uint32_t off) +{ + return fset(account, 0, off); +} + /** Match set account flags */ class flags : private detail::flags_helper { diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index ac00d3eed1..96b63bd927 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -199,6 +199,26 @@ Env::balance(Account const& account, Issue const& issue) const return {amount, lookup(issue.account).name()}; } +PrettyAmount +Env::balance(Account const& account, MPTIssue const& mptIssue) const +{ + auto const sle = le(keylet::mptoken(mptIssue.getMptID(), account)); + if (!sle) + { + return {STAmount(mptIssue, 0), account.name()}; + } + STAmount const amount{mptIssue, sle->getFieldU64(sfMPTAmount)}; + return {amount, lookup(mptIssue.getIssuer()).name()}; +} + +PrettyAmount +Env::balance(Account const& account, Asset const& asset) const +{ + return std::visit( + [&](auto const& issue) { return balance(account, issue); }, + asset.value()); +} + std::uint32_t Env::ownerCount(Account const& account) const { diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index e5b136e9c0..f5f0368d47 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -136,10 +136,30 @@ expectLine( return false; } +[[nodiscard]] bool +expectLine(Env& env, AccountID const& account, None const&, Issue const& issue) +{ + return !env.le(keylet::line(account, issue)); +} + +[[nodiscard]] bool +expectLine( + Env& env, + AccountID const& account, + None const&, + MPTIssue const& mptIssue) +{ + return !env.le(keylet::mptoken(mptIssue.getMptID(), account)); +} + [[nodiscard]] bool expectLine(Env& env, AccountID const& account, None const& value) { - return !env.le(keylet::line(account, value.issue)); + return std::visit( + [&](auto const& issue) { + return expectLine(env, account, value, issue); + }, + value.asset.value()); } [[nodiscard]] bool diff --git a/src/test/jtx/impl/balance.cpp b/src/test/jtx/impl/balance.cpp index 42330658eb..decbd816e1 100644 --- a/src/test/jtx/impl/balance.cpp +++ b/src/test/jtx/impl/balance.cpp @@ -24,38 +24,73 @@ namespace test { namespace jtx { void -balance::operator()(Env& env) const +doBalance( + Env& env, + AccountID const& account, + bool none, + STAmount const& value, + Issue const& issue) { - if (isXRP(value_.issue())) + if (isXRP(issue)) { - auto const sle = env.le(account_); - if (none_) + auto const sle = env.le(keylet::account(account)); + if (none) { env.test.expect(!sle); } else if (env.test.expect(sle)) { - env.test.expect(sle->getFieldAmount(sfBalance) == value_); + env.test.expect(sle->getFieldAmount(sfBalance) == value); } } else { - auto const sle = env.le(keylet::line(account_.id(), value_.issue())); - if (none_) + auto const sle = env.le(keylet::line(account, issue)); + if (none) { env.test.expect(!sle); } else if (env.test.expect(sle)) { auto amount = sle->getFieldAmount(sfBalance); - amount.setIssuer(value_.issue().account); - if (account_.id() > value_.issue().account) + amount.setIssuer(issue.account); + if (account > issue.account) amount.negate(); - env.test.expect(amount == value_); + env.test.expect(amount == value); } } } +void +doBalance( + Env& env, + AccountID const& account, + bool none, + STAmount const& value, + MPTIssue const& mptIssue) +{ + auto const sle = env.le(keylet::mptoken(mptIssue.getMptID(), account)); + if (none) + { + env.test.expect(!sle); + } + else if (env.test.expect(sle)) + { + STAmount const amount{mptIssue, sle->getFieldU64(sfMPTAmount)}; + env.test.expect(amount == value); + } +} + +void +balance::operator()(Env& env) const +{ + return std::visit( + [&](auto const& issue) { + doBalance(env, account_.id(), none_, value_, issue); + }, + value_.asset().value()); +} + } // namespace jtx } // namespace test } // namespace ripple diff --git a/src/test/jtx/impl/fee.cpp b/src/test/jtx/impl/fee.cpp index 71e3dd089a..b887849946 100644 --- a/src/test/jtx/impl/fee.cpp +++ b/src/test/jtx/impl/fee.cpp @@ -26,13 +26,15 @@ namespace test { namespace jtx { void -fee::operator()(Env&, JTx& jt) const +fee::operator()(Env& env, JTx& jt) const { if (!manual_) return; jt.fill_fee = false; + if (increment_) + jt[sfFee] = STAmount(env.current()->fees().increment).getJson(); if (amount_) - jt[jss::Fee] = amount_->getJson(JsonOptions::none); + jt[sfFee] = amount_->getJson(JsonOptions::none); } } // namespace jtx diff --git a/src/test/jtx/impl/owners.cpp b/src/test/jtx/impl/owners.cpp index 386ec29a37..b55986fccb 100644 --- a/src/test/jtx/impl/owners.cpp +++ b/src/test/jtx/impl/owners.cpp @@ -20,9 +20,6 @@ #include namespace ripple { -namespace test { -namespace jtx { - namespace detail { std::uint32_t @@ -39,7 +36,7 @@ owned_count_of(ReadView const& view, AccountID const& id, LedgerEntryType type) void owned_count_helper( - Env& env, + test::jtx::Env& env, AccountID const& id, LedgerEntryType type, std::uint32_t value) @@ -49,6 +46,9 @@ owned_count_helper( } // namespace detail +namespace test { +namespace jtx { + void owners::operator()(Env& env) const { diff --git a/src/test/jtx/owners.h b/src/test/jtx/owners.h index fc904f9e87..9408d67a9c 100644 --- a/src/test/jtx/owners.h +++ b/src/test/jtx/owners.h @@ -30,8 +30,6 @@ #include namespace ripple { -namespace test { -namespace jtx { namespace detail { @@ -40,13 +38,16 @@ owned_count_of(ReadView const& view, AccountID const& id, LedgerEntryType type); void owned_count_helper( - Env& env, + test::jtx::Env& env, AccountID const& id, LedgerEntryType type, std::uint32_t value); } // namespace detail +namespace test { +namespace jtx { + // Helper for aliases template class owner_count diff --git a/src/test/jtx/require.h b/src/test/jtx/require.h index bec21235a6..3215ac0abb 100644 --- a/src/test/jtx/require.h +++ b/src/test/jtx/require.h @@ -26,14 +26,12 @@ #include namespace ripple { -namespace test { -namespace jtx { namespace detail { template inline void -require_args(requires_t& vec, Cond const& cond, Args const&... args) +require_args(test::jtx::requires_t& vec, Cond const& cond, Args const&... args) { vec.push_back(cond); if constexpr (sizeof...(args) > 0) @@ -42,6 +40,9 @@ require_args(requires_t& vec, Cond const& cond, Args const&... args) } // namespace detail +namespace test { +namespace jtx { + /** Compose many condition functors into one */ template require_t diff --git a/src/test/jtx/tags.h b/src/test/jtx/tags.h index bb64295f05..4d55929d69 100644 --- a/src/test/jtx/tags.h +++ b/src/test/jtx/tags.h @@ -49,6 +49,16 @@ struct disabled_t }; static disabled_t const disabled; +/** Used for fee() calls that use an owner reserve increment */ +struct increment_t +{ + increment_t() + { + } +}; + +static increment_t const increment; + } // namespace jtx } // namespace test diff --git a/src/test/ledger/Invariants_test.cpp b/src/test/ledger/Invariants_test.cpp index 7ceb76504d..5bb9feb070 100644 --- a/src/test/ledger/Invariants_test.cpp +++ b/src/test/ledger/Invariants_test.cpp @@ -31,6 +31,7 @@ #include namespace ripple { +namespace test { class Invariants_test : public beast::unit_test::suite { @@ -112,13 +113,13 @@ class Invariants_test : public beast::unit_test::suite { terActual = ac.checkInvariants(terActual, fee); BEAST_EXPECT(terExpect == terActual); + auto const messages = sink.messages().str(); BEAST_EXPECT( - sink.messages().str().starts_with("Invariant failed:") || - sink.messages().str().starts_with( - "Transaction caused an exception")); + messages.starts_with("Invariant failed:") || + messages.starts_with("Transaction caused an exception")); for (auto const& m : expect_logs) { - if (sink.messages().str().find(m) == std::string::npos) + if (messages.find(m) == std::string::npos) { // uncomment if you want to log the invariant failure // message log << " --> " << m << std::endl; @@ -1300,6 +1301,118 @@ class Invariants_test : public beast::unit_test::suite {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); } + void + testValidPseudoAccounts() + { + testcase << "valid pseudo accounts"; + + using namespace jtx; + + AccountID pseudoAccountID; + Preclose createPseudo = + [&, this](Account const& a, Account const& b, Env& env) { + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + + // Create vault + Vault vault{env}; + auto [tx, vKeylet] = + vault.create({.owner = a, .asset = xrpAsset}); + env(tx); + env.close(); + if (auto const vSle = env.le(vKeylet); BEAST_EXPECT(vSle)) + { + pseudoAccountID = vSle->at(sfAccount); + } + + return BEAST_EXPECT(env.le(keylet::account(pseudoAccountID))); + }; + + /* Cases to check + "pseudo-account has 0 pseudo-account fields set" + "pseudo-account has 2 pseudo-account fields set" + "pseudo-account sequence changed" + "pseudo-account flags are not set" + "pseudo-account has a regular key" + */ + struct Mod + { + std::string expectedFailure; + std::function func; + }; + auto const mods = std::to_array({ + { + "pseudo-account has 0 pseudo-account fields set", + [this](SLE::pointer& sle) { + BEAST_EXPECT(sle->at(~sfVaultID)); + sle->at(~sfVaultID) = std::nullopt; + }, + }, + { + "pseudo-account has 2 pseudo-account fields set", + [this](SLE::pointer& sle) { + BEAST_EXPECT(sle->at(~sfVaultID) && !sle->at(~sfAMMID)); + sle->at(~sfAMMID) = ~sle->at(~sfVaultID); + }, + }, + /* + { + "pseudo-account has 2 pseudo-account fields set", + [this](SLE::pointer& sle) { + BEAST_EXPECT( + sle->at(~sfVaultID) && !sle->at(~sfLoanBrokerID)); + sle->at(~sfLoanBrokerID) = ~sle->at(~sfVaultID); + }, + }, + */ + { + "pseudo-account sequence changed", + [](SLE::pointer& sle) { sle->at(sfSequence) = 12345; }, + }, + { + "pseudo-account flags are not set", + [](SLE::pointer& sle) { sle->at(sfFlags) = lsfNoFreeze; }, + }, + { + "pseudo-account has a regular key", + [](SLE::pointer& sle) { + sle->at(sfRegularKey) = Account("regular").id(); + }, + }, + }); + + for (auto const& mod : mods) + { + doInvariantCheck( + {{mod.expectedFailure}}, + [&](Account const& A1, Account const&, ApplyContext& ac) { + auto sle = ac.view().peek(keylet::account(pseudoAccountID)); + if (!sle) + return false; + mod.func(sle); + ac.view().update(sle); + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + createPseudo); + } + + // Take one of the regular accounts and set the sequence to 0, which + // will make it look like a pseudo-account + doInvariantCheck( + {{"pseudo-account has 0 pseudo-account fields set"}, + {"pseudo-account sequence changed"}, + {"pseudo-account flags are not set"}}, + [&](Account const& A1, Account const&, ApplyContext& ac) { + auto sle = ac.view().peek(keylet::account(A1.id())); + if (!sle) + return false; + sle->at(sfSequence) = 0; + ac.view().update(sle); + return true; + }); + } public: void run() override @@ -1318,9 +1431,11 @@ public: testValidNewAccountRoot(); testNFTokenPageInvariants(); testPermissionedDomainInvariants(); + testValidPseudoAccounts(); } }; BEAST_DEFINE_TESTSUITE(Invariants, ledger, ripple); +} // namespace test } // namespace ripple diff --git a/src/test/nodestore/import_test.cpp b/src/test/nodestore/import_test.cpp index d7865a20fc..fd52d07c4c 100644 --- a/src/test/nodestore/import_test.cpp +++ b/src/test/nodestore/import_test.cpp @@ -61,7 +61,6 @@ multi(32gb): */ namespace ripple { -namespace NodeStore { namespace detail { @@ -191,6 +190,8 @@ fmtdur(std::chrono::duration const& d) } // namespace detail +namespace NodeStore { + //------------------------------------------------------------------------------ class progress diff --git a/src/test/unit_test/FileDirGuard.h b/src/test/unit_test/FileDirGuard.h index d247ae3015..e7d5e1ab96 100644 --- a/src/test/unit_test/FileDirGuard.h +++ b/src/test/unit_test/FileDirGuard.h @@ -27,7 +27,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. #include namespace ripple { -namespace test { + namespace detail { /** @@ -176,7 +176,6 @@ public: }; } // namespace detail -} // namespace test } // namespace ripple #endif // TEST_UNIT_TEST_DIRGUARD_H diff --git a/src/test/unit_test/multi_runner.cpp b/src/test/unit_test/multi_runner.cpp index 087e37dac2..3755428ee3 100644 --- a/src/test/unit_test/multi_runner.cpp +++ b/src/test/unit_test/multi_runner.cpp @@ -30,7 +30,6 @@ #include namespace ripple { -namespace test { namespace detail { @@ -388,6 +387,8 @@ multi_runner_base::add_failures(std::size_t failures) } // namespace detail +namespace test { + //------------------------------------------------------------------------------ multi_runner_parent::multi_runner_parent() : os_(std::cout) @@ -645,10 +646,11 @@ multi_runner_child::on_log(std::string const& msg) message_queue_send(MessageType::log, s.str()); } +} // namespace test + namespace detail { template class multi_runner_base; template class multi_runner_base; } // namespace detail -} // namespace test } // namespace ripple diff --git a/src/test/unit_test/multi_runner.h b/src/test/unit_test/multi_runner.h index 08512d1882..bce62fb131 100644 --- a/src/test/unit_test/multi_runner.h +++ b/src/test/unit_test/multi_runner.h @@ -40,7 +40,6 @@ #include namespace ripple { -namespace test { namespace detail { @@ -212,6 +211,8 @@ public: } // namespace detail +namespace test { + //------------------------------------------------------------------------------ /** Manager for children running unit tests diff --git a/src/xrpld/app/misc/detail/LoadFeeTrack.cpp b/src/xrpld/app/misc/detail/LoadFeeTrack.cpp index 96e7555401..90c084c44b 100644 --- a/src/xrpld/app/misc/detail/LoadFeeTrack.cpp +++ b/src/xrpld/app/misc/detail/LoadFeeTrack.cpp @@ -23,7 +23,7 @@ #include #include #include -#include +#include #include diff --git a/src/xrpld/app/tx/detail/DeleteAccount.cpp b/src/xrpld/app/tx/detail/DeleteAccount.cpp index 7aa47e05f3..a2a9769d43 100644 --- a/src/xrpld/app/tx/detail/DeleteAccount.cpp +++ b/src/xrpld/app/tx/detail/DeleteAccount.cpp @@ -31,10 +31,10 @@ #include #include #include -#include #include #include #include +#include namespace ripple { diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index aa1464ec2a..1a707e8496 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -26,14 +26,67 @@ #include #include -#include #include +#include #include #include +#include #include namespace ripple { +enum Privilege { + noPriv = + 0x0000, // The transaction can not do any of the enumerated operations + createAcct = + 0x0001, // The transaction can create a new ACCOUNT_ROOT object. + createPseudoAcct = 0x0002, // The transaction can create a pseudo account, + // which implies createAcct + mustDeleteAcct = + 0x0004, // The transaction must delete an ACCOUNT_ROOT object + mayDeleteAcct = 0x0008, // The transaction may delete an ACCOUNT_ROOT + // object, but does not have to + overrideFreeze = 0x0010, // The transaction can override some freeze rules + changeNFTCounts = 0x0020, // The transaction can mint or burn an NFT + createMPTIssuance = + 0x0040, // The transaction can create a new MPT issuance + destroyMPTIssuance = 0x0080, // The transaction can destroy an MPT issuance + mustAuthorizeMPT = 0x0100, // The transaction MUST create or delete an MPT + // object (except by issuer) + mayAuthorizeMPT = 0x0200, // The transaction MAY create or delete an MPT + // object (except by issuer) +}; +constexpr Privilege +operator|(Privilege lhs, Privilege rhs) +{ + return safe_cast( + safe_cast>(lhs) | + safe_cast>(rhs)); +} + +#pragma push_macro("TRANSACTION") +#undef TRANSACTION + +#define TRANSACTION(tag, value, name, delegatable, privileges, ...) \ + case tag: { \ + return (privileges) & priv; \ + } + +bool +checkMyPrivilege(STTx const& tx, Privilege priv) +{ + switch (tx.getTxnType()) + { +#include + // Deprecated types + default: + return false; + } +}; + +#undef TRANSACTION +#pragma pop_macro("TRANSACTION") + void TransactionFeeCheck::visitEntry( bool, @@ -328,10 +381,7 @@ AccountRootsNotDeleted::finalize( // transaction when the total AMM LP Tokens balance goes to 0. // A successful AccountDelete or AMMDelete MUST delete exactly // one account root. - if ((tx.getTxnType() == ttACCOUNT_DELETE || - tx.getTxnType() == ttAMM_DELETE || - tx.getTxnType() == ttVAULT_DELETE) && - result == tesSUCCESS) + if (checkMyPrivilege(tx, mustDeleteAcct) && result == tesSUCCESS) { if (accountsDeleted_ == 1) return true; @@ -348,9 +398,8 @@ AccountRootsNotDeleted::finalize( // A successful AMMWithdraw/AMMClawback MAY delete one account root // when the total AMM LP Tokens balance goes to 0. Not every AMM withdraw // deletes the AMM account, accountsDeleted_ is set if it is deleted. - if ((tx.getTxnType() == ttAMM_WITHDRAW || - tx.getTxnType() == ttAMM_CLAWBACK) && - result == tesSUCCESS && accountsDeleted_ == 1) + if (checkMyPrivilege(tx, mayDeleteAcct) && result == tesSUCCESS && + accountsDeleted_ == 1) return true; if (accountsDeleted_ == 0) @@ -439,10 +488,14 @@ AccountRootsDeletedClean::finalize( } // Keys directly stored in the AccountRoot object - if (auto const ammKey = accountSLE->at(~sfAMMID)) + for (auto const& field : getPseudoAccountFields()) { - if (objectExists(keylet::amm(*ammKey)) && enforce) - return false; + if (accountSLE->isFieldPresent(*field)) + { + auto const key = accountSLE->getFieldH256(*field); + if (objectExists(keylet::unchecked(key)) && enforce) + return false; + } } } @@ -462,41 +515,23 @@ LedgerEntryTypesMatch::visitEntry( if (after) { +#pragma push_macro("LEDGER_ENTRY") +#undef LEDGER_ENTRY + +#define LEDGER_ENTRY(tag, value, name, rpcName, fields) case tag: + switch (after->getType()) { - case ltACCOUNT_ROOT: - case ltDELEGATE: - case ltDIR_NODE: - case ltRIPPLE_STATE: - case ltTICKET: - case ltSIGNER_LIST: - case ltOFFER: - case ltLEDGER_HASHES: - case ltAMENDMENTS: - case ltFEE_SETTINGS: - case ltESCROW: - case ltPAYCHAN: - case ltCHECK: - case ltDEPOSIT_PREAUTH: - case ltNEGATIVE_UNL: - case ltNFTOKEN_PAGE: - case ltNFTOKEN_OFFER: - case ltAMM: - case ltBRIDGE: - case ltXCHAIN_OWNED_CLAIM_ID: - case ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID: - case ltDID: - case ltORACLE: - case ltMPTOKEN_ISSUANCE: - case ltMPTOKEN: - case ltCREDENTIAL: - case ltPERMISSIONED_DOMAIN: - case ltVAULT: - break; +#include + + break; default: invalidTypeAdded_ = true; break; } + +#undef LEDGER_ENTRY +#pragma pop_macro("LEDGER_ENTRY") } } @@ -850,7 +885,7 @@ TransfersNotFrozen::validateFrozenState( } // AMMClawbacks are allowed to override some freeze rules - if ((!isAMMLine || globalFreeze) && tx.getTxnType() == ttAMM_CLAWBACK) + if ((!isAMMLine || globalFreeze) && checkMyPrivilege(tx, overrideFreeze)) { JLOG(j.debug()) << "Invariant check allowing funds to be moved " << (change.balanceChangeSign > 0 ? "to" : "from") @@ -910,17 +945,13 @@ ValidNewAccountRoot::finalize( } // From this point on we know exactly one account was created. - if ((tx.getTxnType() == ttPAYMENT || tx.getTxnType() == ttAMM_CREATE || - tx.getTxnType() == ttVAULT_CREATE || - tx.getTxnType() == ttXCHAIN_ADD_CLAIM_ATTESTATION || - tx.getTxnType() == ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION) && + if (checkMyPrivilege(tx, createAcct | createPseudoAcct) && result == tesSUCCESS) { bool const pseudoAccount = (pseudoAccount_ && view.rules().enabled(featureSingleAssetVault)); - if (pseudoAccount && tx.getTxnType() != ttAMM_CREATE && - tx.getTxnType() != ttVAULT_CREATE) + if (pseudoAccount && !checkMyPrivilege(tx, createPseudoAcct)) { JLOG(j.fatal()) << "Invariant failed: pseudo-account created by a " "wrong transaction type"; @@ -959,7 +990,7 @@ ValidNewAccountRoot::finalize( JLOG(j.fatal()) << "Invariant failed: account root created illegally"; return false; -} +} // namespace ripple //------------------------------------------------------------------------------ @@ -1154,8 +1185,7 @@ NFTokenCountTracking::finalize( ReadView const& view, beast::Journal const& j) { - if (TxType const txType = tx.getTxnType(); - txType != ttNFTOKEN_MINT && txType != ttNFTOKEN_BURN) + if (!checkMyPrivilege(tx, changeNFTCounts)) { if (beforeMintedTotal != afterMintedTotal) { @@ -1345,8 +1375,7 @@ ValidMPTIssuance::finalize( { if (result == tesSUCCESS) { - if (tx.getTxnType() == ttMPTOKEN_ISSUANCE_CREATE || - tx.getTxnType() == ttVAULT_CREATE) + if (checkMyPrivilege(tx, createMPTIssuance)) { if (mptIssuancesCreated_ == 0) { @@ -1367,8 +1396,7 @@ ValidMPTIssuance::finalize( return mptIssuancesCreated_ == 1 && mptIssuancesDeleted_ == 0; } - if (tx.getTxnType() == ttMPTOKEN_ISSUANCE_DESTROY || - tx.getTxnType() == ttVAULT_DELETE) + if (checkMyPrivilege(tx, destroyMPTIssuance)) { if (mptIssuancesDeleted_ == 0) { @@ -1389,8 +1417,7 @@ ValidMPTIssuance::finalize( return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 1; } - if (tx.getTxnType() == ttMPTOKEN_AUTHORIZE || - tx.getTxnType() == ttVAULT_DEPOSIT) + if (checkMyPrivilege(tx, mustAuthorizeMPT | mayAuthorizeMPT)) { bool const submittedByIssuer = tx.isFieldPresent(sfHolder); @@ -1416,7 +1443,7 @@ ValidMPTIssuance::finalize( return false; } else if ( - !submittedByIssuer && (tx.getTxnType() != ttVAULT_DEPOSIT) && + !submittedByIssuer && !checkMyPrivilege(tx, mayAuthorizeMPT) && (mptokensCreated_ + mptokensDeleted_ != 1)) { // if the holder submitted this tx, then a mptoken must be @@ -1580,4 +1607,98 @@ ValidPermissionedDomain::finalize( (sleStatus_[1] ? check(*sleStatus_[1], j) : true); } +//------------------------------------------------------------------------------ + +void +ValidPseudoAccounts::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (isDelete) + // Deletion is ignored + return; + + if (after && after->getType() == ltACCOUNT_ROOT) + { + bool const isPseudo = [&]() { + // isPseudoAccount checks that any of the pseudo-account fields are + // set. + if (isPseudoAccount(after)) + return true; + // Not all pseudo-accounts have a zero sequence, but all accounts + // with a zero sequence had better be pseudo-accounts. + if (after->at(sfSequence) == 0) + return true; + + return false; + }(); + if (isPseudo) + { + // Pseudo accounts must have the following properties: + // 1. Exactly one of the pseudo-account fields is set. + // 2. The sequence number is not changed. + // 3. The lsfDisableMaster, lsfDefaultRipple, and lsfDepositAuth + // flags are set. + // 4. The RegularKey is not set. + { + std::vector const& fields = + getPseudoAccountFields(); + + auto const numFields = std::count_if( + fields.begin(), + fields.end(), + [&after](SField const* sf) -> bool { + return after->isFieldPresent(*sf); + }); + if (numFields != 1) + { + std::stringstream error; + error << "pseudo-account has " << numFields + << " pseudo-account fields set"; + errors_.emplace_back(error.str()); + } + } + if (before && before->at(sfSequence) != after->at(sfSequence)) + { + errors_.emplace_back("pseudo-account sequence changed"); + } + if (!after->isFlag( + lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth)) + { + errors_.emplace_back("pseudo-account flags are not set"); + } + if (after->isFieldPresent(sfRegularKey)) + { + errors_.emplace_back("pseudo-account has a regular key"); + } + } + } +} + +bool +ValidPseudoAccounts::finalize( + STTx const& tx, + TER const, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + bool const enforce = view.rules().enabled(featureSingleAssetVault); + XRPL_ASSERT( + errors_.empty() || enforce, + "ripple::ValidPseudoAccounts::finalize : no bad " + "changes or enforce invariant"); + if (!errors_.empty()) + { + for (auto const& error : errors_) + { + JLOG(j.fatal()) << "Invariant failed: " << error; + } + 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..58dbc23066 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.h +++ b/src/xrpld/app/tx/detail/InvariantCheck.h @@ -618,6 +618,34 @@ public: beast::Journal const&); }; +/** + * @brief Invariants: Pseudo-accounts have valid and consisent properties + * + * Pseudo-accounts have certain properties, and some of those properties are + * unique to pseudo-accounts. Check that all pseudo-accounts are following the + * rules, and that only pseudo-accounts look like pseudo-accounts. + * + */ +class ValidPseudoAccounts +{ + std::vector errors_; + +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 +665,8 @@ using InvariantChecks = std::tuple< NFTokenCountTracking, ValidClawback, ValidMPTIssuance, - ValidPermissionedDomain>; + ValidPermissionedDomain, + ValidPseudoAccounts>; /** * @brief get a tuple of all invariant checks diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index baba7d131e..56d3302f46 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -196,7 +196,10 @@ PreflightContext::PreflightContext( //------------------------------------------------------------------------------ Transactor::Transactor(ApplyContext& ctx) - : ctx_(ctx), j_(ctx.journal), account_(ctx.tx.getAccountID(sfAccount)) + : ctx_(ctx) + , sink_(ctx.journal, to_short_string(ctx.tx.getTransactionID()) + " ") + , j_(sink_) + , account_(ctx.tx.getAccountID(sfAccount)) { } diff --git a/src/xrpld/app/tx/detail/Transactor.h b/src/xrpld/app/tx/detail/Transactor.h index 4956f021df..88ccdb8db7 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -24,6 +24,7 @@ #include #include +#include #include #include @@ -88,6 +89,7 @@ class Transactor { protected: ApplyContext& ctx_; + beast::WrappedSink sink_; beast::Journal const j_; AccountID const account_; diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index 5e8c125e83..32745f703d 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -18,56 +18,20 @@ //============================================================================== #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#pragma push_macro("TRANSACTION") +#undef TRANSACTION + +// Do nothing +#define TRANSACTION(...) +#define TRANSACTION_INCLUDE 1 + +#include + +#undef TRANSACTION +#pragma pop_macro("TRANSACTION") + +// DO NOT INCLUDE TRANSACTOR HEADER FILES HERE. +// See the instructions at the top of transactions.macro instead. #include @@ -96,8 +60,8 @@ with_txn_type(TxType txnType, F&& f) #pragma push_macro("TRANSACTION") #undef TRANSACTION -#define TRANSACTION(tag, value, name, delegatable, fields) \ - case tag: \ +#define TRANSACTION(tag, value, name, ...) \ + case tag: \ return f.template operator()(); #include diff --git a/src/xrpld/ledger/View.h b/src/xrpld/ledger/View.h index 387aedecfc..5aa8b7216d 100644 --- a/src/xrpld/ledger/View.h +++ b/src/xrpld/ledger/View.h @@ -532,12 +532,28 @@ createPseudoAccount( [[nodiscard]] bool isPseudoAccount(std::shared_ptr sleAcct); +// Returns the list of fields that define an ACCOUNT_ROOT as a pseudo-account if +// set +// Pseudo-account designator fields MUST be maintained by including the +// SField::sMD_PseudoAccount flag in the SField definition. (Don't forget to +// "| SField::sMD_Default"!) The fields do NOT need to be amendment-gated, +// since a non-active amendment will not set any field, by definition. +// Specific properties of a pseudo-account are NOT checked here, that's what +// InvariantCheck is for. +[[nodiscard]] std::vector const& +getPseudoAccountFields(); + [[nodiscard]] inline bool isPseudoAccount(ReadView const& view, AccountID accountId) { return isPseudoAccount(view.read(keylet::account(accountId))); } +[[nodiscard]] TER +canAddHolding(ReadView const& view, Asset const& asset); + +/// Any transactors that call addEmptyHolding() in doApply must call +/// canAddHolding() in preflight with the same View and Asset [[nodiscard]] TER addEmptyHolding( ApplyView& view, diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index d248d37e18..077b1a172a 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -629,8 +629,8 @@ xrpLiquid( std::uint32_t const ownerCount = confineOwnerCount( view.ownerCountHook(id, sle->getFieldU32(sfOwnerCount)), ownerCountAdj); - // AMMs have no reserve requirement - auto const reserve = sle->isFieldPresent(sfAMMID) + // Pseudo-accounts have no reserve requirement + auto const reserve = isPseudoAccount(sle) ? XRPAmount{0} : view.fees().accountReserve(ownerCount); @@ -1029,7 +1029,7 @@ adjustOwnerCount( AccountID const id = (*sle)[sfAccount]; std::uint32_t const adjusted = confineOwnerCount(current, amount, id, j); view.adjustOwnerCountHook(id, current, adjusted); - sle->setFieldU32(sfOwnerCount, adjusted); + sle->at(sfOwnerCount) = adjusted; view.update(sle); } @@ -1069,15 +1069,47 @@ pseudoAccountAddress(ReadView const& view, uint256 const& pseudoOwnerKey) return beast::zero; } -// Note, the list of the pseudo-account designator fields below MUST be -// maintained but it does NOT need to be amendment-gated, since a -// non-active amendment will not set any field, by definition. Specific -// properties of a pseudo-account are NOT checked here, that's what +// Pseudo-account designator fields MUST be maintained by including the +// SField::sMD_PseudoAccount flag in the SField definition. (Don't forget to +// "| SField::sMD_Default"!) The fields do NOT need to be amendment-gated, +// since a non-active amendment will not set any field, by definition. +// Specific properties of a pseudo-account are NOT checked here, that's what // InvariantCheck is for. -static std::array const pseudoAccountOwnerFields = { - &sfAMMID, // - &sfVaultID, // -}; +[[nodiscard]] std::vector const& +getPseudoAccountFields() +{ + static std::vector const pseudoFields = []() { + auto const ar = LedgerFormats::getInstance().findByType(ltACCOUNT_ROOT); + if (!ar) + LogicError( + "ripple::isPseudoAccount : unable to find account root ledger " + "format"); + auto const& soTemplate = ar->getSOTemplate(); + + std::vector pseudoFields; + for (auto const& field : soTemplate) + { + if (field.sField().shouldMeta(SField::sMD_PseudoAccount)) + pseudoFields.emplace_back(&field.sField()); + } + return pseudoFields; + }(); + return pseudoFields; +} + +[[nodiscard]] bool +isPseudoAccount(std::shared_ptr sleAcct) +{ + auto const& fields = getPseudoAccountFields(); + + // Intentionally use defensive coding here because it's cheap and makes the + // semantics of true return value clean. + return sleAcct && sleAcct->getType() == ltACCOUNT_ROOT && + std::count_if( + fields.begin(), fields.end(), [&sleAcct](SField const* sf) -> bool { + return sleAcct->isFieldPresent(*sf); + }) > 0; +} Expected, TER> createPseudoAccount( @@ -1085,10 +1117,11 @@ createPseudoAccount( uint256 const& pseudoOwnerKey, SField const& ownerField) { + auto const& fields = getPseudoAccountFields(); XRPL_ASSERT( std::count_if( - pseudoAccountOwnerFields.begin(), - pseudoAccountOwnerFields.end(), + fields.begin(), + fields.end(), [&ownerField](SField const* sf) -> bool { return *sf == ownerField; }) == 1, @@ -1124,18 +1157,42 @@ createPseudoAccount( return account; } -[[nodiscard]] bool -isPseudoAccount(std::shared_ptr sleAcct) +[[nodiscard]] TER +canAddHolding(ReadView const& view, Issue const& issue) { - // Intentionally use defensive coding here because it's cheap and makes the - // semantics of true return value clean. - return sleAcct && sleAcct->getType() == ltACCOUNT_ROOT && - std::count_if( - pseudoAccountOwnerFields.begin(), - pseudoAccountOwnerFields.end(), - [&sleAcct](SField const* sf) -> bool { - return sleAcct->isFieldPresent(*sf); - }) > 0; + if (issue.native()) + return tesSUCCESS; // No special checks for XRP + + auto const issuer = view.read(keylet::account(issue.getIssuer())); + if (!issuer) + return terNO_ACCOUNT; + else if (!issuer->isFlag(lsfDefaultRipple)) + return terNO_RIPPLE; + + return tesSUCCESS; +} + +[[nodiscard]] TER +canAddHolding(ReadView const& view, MPTIssue const& mptIssue) +{ + auto mptID = mptIssue.getMptID(); + auto issuance = view.read(keylet::mptIssuance(mptID)); + if (!issuance) + return tecOBJECT_NOT_FOUND; + if (!issuance->isFlag(lsfMPTCanTransfer)) + return tecNO_AUTH; + + return tesSUCCESS; +} + +[[nodiscard]] TER +canAddHolding(ReadView const& view, Asset const& asset) +{ + return std::visit( + [&](TIss const& issue) -> TER { + return canAddHolding(view, issue); + }, + asset.value()); } [[nodiscard]] TER From 937b67cbc0e7564652042f020399b53eb2342fe1 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 16 May 2025 15:40:29 +0100 Subject: [PATCH 002/291] Refactor 2: STParsed Json --- src/libxrpl/protocol/STParsedJSON.cpp | 291 +++++++++++++++----------- 1 file changed, 173 insertions(+), 118 deletions(-) diff --git a/src/libxrpl/protocol/STParsedJSON.cpp b/src/libxrpl/protocol/STParsedJSON.cpp index bc9aad0a13..db199a73ed 100644 --- a/src/libxrpl/protocol/STParsedJSON.cpp +++ b/src/libxrpl/protocol/STParsedJSON.cpp @@ -202,6 +202,173 @@ non_object_in_array(std::string const& item, Json::UInt index) " is not an object. Arrays may only contain objects."); } +template +static std::optional +parseUnsigned( + SField const& field, + std::string const& json_name, + std::string const& fieldName, + SField const* name, + Json::Value const& value, + Json::Value& error) +{ + std::optional ret; + + try + { + if (value.isString()) + { + ret = detail::make_stvar( + field, + safe_cast( + beast::lexicalCastThrow(value.asString()))); + } + else if (value.isInt()) + { + ret = detail::make_stvar( + field, + to_unsigned(value.asInt())); + } + else if (value.isUInt()) + { + ret = detail::make_stvar( + field, + to_unsigned(value.asUInt())); + } + else + { + error = bad_type(json_name, fieldName); + return ret; + } + } + catch (std::exception const&) + { + error = invalid_data(json_name, fieldName); + return ret; + } + + return ret; +} + +template +static std::optional +parseUint16( + SField const& field, + std::string const& json_name, + std::string const& fieldName, + SField const* name, + Json::Value const& value, + Json::Value& error) +{ + std::optional ret; + + try + { + if (value.isString()) + { + std::string const strValue = value.asString(); + + if (!strValue.empty() && + ((strValue[0] < '0') || (strValue[0] > '9'))) + { + if (field == sfTransactionType) + { + ret = detail::make_stvar( + field, + safe_cast( + static_cast( + TxFormats::getInstance().findTypeByName( + strValue)))); + + if (*name == sfGeneric) + name = &sfTransaction; + } + else if (field == sfLedgerEntryType) + { + ret = detail::make_stvar( + field, + safe_cast( + static_cast( + LedgerFormats::getInstance().findTypeByName( + strValue)))); + + if (*name == sfGeneric) + name = &sfLedgerEntry; + } + else + { + error = invalid_data(json_name, fieldName); + return ret; + } + } + } + if (!ret) + return parseUnsigned( + field, json_name, fieldName, name, value, error); + } + catch (std::exception const&) + { + error = invalid_data(json_name, fieldName); + return ret; + } + + return ret; +} + +template +static std::optional +parseUint32( + SField const& field, + std::string const& json_name, + std::string const& fieldName, + SField const* name, + Json::Value const& value, + Json::Value& error) +{ + std::optional ret; + + try + { + if (value.isString()) + { + if (field == sfPermissionValue) + { + std::string const strValue = value.asString(); + auto const granularPermission = + Permission::getInstance().getGranularValue(strValue); + if (granularPermission) + { + ret = detail::make_stvar( + field, *granularPermission); + } + else + { + auto const& txType = + TxFormats::getInstance().findTypeByName(strValue); + ret = detail::make_stvar( + field, + Permission::getInstance().txToPermissionType(txType)); + } + } + else + { + ret = detail::make_stvar( + field, beast::lexicalCastThrow(value.asString())); + } + } + if (!ret) + return parseUnsigned( + field, json_name, fieldName, name, value, error); + } + catch (std::exception const&) + { + error = invalid_data(json_name, fieldName); + return ret; + } + + return ret; +} + // This function is used by parseObject to parse any JSON type that doesn't // recurse. Everything represented here is a leaf-type. static std::optional @@ -302,130 +469,18 @@ parseLeaf( break; case STI_UINT16: - try - { - if (value.isString()) - { - std::string const strValue = value.asString(); - - if (!strValue.empty() && - ((strValue[0] < '0') || (strValue[0] > '9'))) - { - if (field == sfTransactionType) - { - ret = detail::make_stvar( - field, - static_cast( - TxFormats::getInstance().findTypeByName( - strValue))); - - if (*name == sfGeneric) - name = &sfTransaction; - } - else if (field == sfLedgerEntryType) - { - ret = detail::make_stvar( - field, - static_cast( - LedgerFormats::getInstance().findTypeByName( - strValue))); - - if (*name == sfGeneric) - name = &sfLedgerEntry; - } - else - { - error = invalid_data(json_name, fieldName); - return ret; - } - } - else - { - ret = detail::make_stvar( - field, - beast::lexicalCastThrow(strValue)); - } - } - else if (value.isInt()) - { - ret = detail::make_stvar( - field, to_unsigned(value.asInt())); - } - else if (value.isUInt()) - { - ret = detail::make_stvar( - field, to_unsigned(value.asUInt())); - } - else - { - error = bad_type(json_name, fieldName); - return ret; - } - } - catch (std::exception const&) - { - error = invalid_data(json_name, fieldName); + ret = parseUint16( + field, json_name, fieldName, name, value, error); + if (!ret) return ret; - } break; case STI_UINT32: - try - { - if (value.isString()) - { - if (field == sfPermissionValue) - { - std::string const strValue = value.asString(); - auto const granularPermission = - Permission::getInstance().getGranularValue( - strValue); - if (granularPermission) - { - ret = detail::make_stvar( - field, *granularPermission); - } - else - { - auto const& txType = - TxFormats::getInstance().findTypeByName( - strValue); - ret = detail::make_stvar( - field, - Permission::getInstance().txToPermissionType( - txType)); - } - } - else - { - ret = detail::make_stvar( - field, - beast::lexicalCastThrow( - value.asString())); - } - } - else if (value.isInt()) - { - ret = detail::make_stvar( - field, to_unsigned(value.asInt())); - } - else if (value.isUInt()) - { - ret = detail::make_stvar( - field, safe_cast(value.asUInt())); - } - else - { - error = bad_type(json_name, fieldName); - return ret; - } - } - catch (std::exception const&) - { - error = invalid_data(json_name, fieldName); + ret = parseUint32( + field, json_name, fieldName, name, value, error); + if (!ret) return ret; - } break; From 4fe3ec8a08dcf7c5ebc8fb3ee499fa18c57187e8 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 16 May 2025 16:08:20 +0100 Subject: [PATCH 003/291] Refactor 3: Transactors --- src/test/app/AMM_test.cpp | 6 +- src/xrpld/app/tx/detail/AMMBid.cpp | 22 +-- src/xrpld/app/tx/detail/AMMBid.h | 5 +- src/xrpld/app/tx/detail/AMMClawback.cpp | 26 +-- src/xrpld/app/tx/detail/AMMClawback.h | 8 +- src/xrpld/app/tx/detail/AMMCreate.cpp | 24 +-- src/xrpld/app/tx/detail/AMMCreate.h | 5 +- src/xrpld/app/tx/detail/AMMDelete.cpp | 22 +-- src/xrpld/app/tx/detail/AMMDelete.h | 5 +- src/xrpld/app/tx/detail/AMMDeposit.cpp | 26 +-- src/xrpld/app/tx/detail/AMMDeposit.h | 8 +- src/xrpld/app/tx/detail/AMMVote.cpp | 22 +-- src/xrpld/app/tx/detail/AMMVote.h | 5 +- src/xrpld/app/tx/detail/AMMWithdraw.cpp | 25 +-- src/xrpld/app/tx/detail/AMMWithdraw.h | 8 +- src/xrpld/app/tx/detail/CancelCheck.cpp | 24 +-- src/xrpld/app/tx/detail/CancelCheck.h | 5 +- src/xrpld/app/tx/detail/CancelOffer.cpp | 16 +- src/xrpld/app/tx/detail/CancelOffer.h | 2 +- src/xrpld/app/tx/detail/CashCheck.cpp | 24 +-- src/xrpld/app/tx/detail/CashCheck.h | 5 +- src/xrpld/app/tx/detail/Change.cpp | 7 +- src/xrpld/app/tx/detail/Change.h | 3 - src/xrpld/app/tx/detail/Clawback.cpp | 23 +-- src/xrpld/app/tx/detail/Clawback.h | 8 +- src/xrpld/app/tx/detail/CreateCheck.cpp | 23 +-- src/xrpld/app/tx/detail/CreateCheck.h | 5 +- src/xrpld/app/tx/detail/CreateOffer.cpp | 19 +-- src/xrpld/app/tx/detail/CreateOffer.h | 5 +- src/xrpld/app/tx/detail/CreateTicket.cpp | 19 +-- src/xrpld/app/tx/detail/CreateTicket.h | 5 +- src/xrpld/app/tx/detail/Credentials.cpp | 93 +++++------ src/xrpld/app/tx/detail/Credentials.h | 24 ++- src/xrpld/app/tx/detail/DID.cpp | 38 ++--- src/xrpld/app/tx/detail/DID.h | 10 +- src/xrpld/app/tx/detail/DelegateSet.cpp | 18 +- src/xrpld/app/tx/detail/DelegateSet.h | 7 +- src/xrpld/app/tx/detail/DeleteAccount.cpp | 25 ++- src/xrpld/app/tx/detail/DeleteAccount.h | 5 +- src/xrpld/app/tx/detail/DeleteOracle.cpp | 22 +-- src/xrpld/app/tx/detail/DeleteOracle.h | 5 +- src/xrpld/app/tx/detail/DepositPreauth.cpp | 30 ++-- src/xrpld/app/tx/detail/DepositPreauth.h | 5 +- src/xrpld/app/tx/detail/Escrow.cpp | 60 ++++--- src/xrpld/app/tx/detail/Escrow.h | 18 +- src/xrpld/app/tx/detail/LedgerStateFix.cpp | 21 +-- src/xrpld/app/tx/detail/LedgerStateFix.h | 5 +- src/xrpld/app/tx/detail/MPTokenAuthorize.cpp | 23 +-- src/xrpld/app/tx/detail/MPTokenAuthorize.h | 8 +- .../app/tx/detail/MPTokenIssuanceCreate.cpp | 23 +-- .../app/tx/detail/MPTokenIssuanceCreate.h | 8 +- .../app/tx/detail/MPTokenIssuanceDestroy.cpp | 24 +-- .../app/tx/detail/MPTokenIssuanceDestroy.h | 8 +- .../app/tx/detail/MPTokenIssuanceSet.cpp | 25 +-- src/xrpld/app/tx/detail/MPTokenIssuanceSet.h | 8 +- .../app/tx/detail/NFTokenAcceptOffer.cpp | 23 +-- src/xrpld/app/tx/detail/NFTokenAcceptOffer.h | 8 +- src/xrpld/app/tx/detail/NFTokenBurn.cpp | 19 +-- src/xrpld/app/tx/detail/NFTokenBurn.h | 5 +- .../app/tx/detail/NFTokenCancelOffer.cpp | 23 +-- src/xrpld/app/tx/detail/NFTokenCancelOffer.h | 8 +- .../app/tx/detail/NFTokenCreateOffer.cpp | 23 +-- src/xrpld/app/tx/detail/NFTokenCreateOffer.h | 8 +- src/xrpld/app/tx/detail/NFTokenMint.cpp | 37 +++-- src/xrpld/app/tx/detail/NFTokenMint.h | 8 +- src/xrpld/app/tx/detail/NFTokenModify.cpp | 21 +-- src/xrpld/app/tx/detail/NFTokenModify.h | 5 +- src/xrpld/app/tx/detail/PayChan.cpp | 60 ++++--- src/xrpld/app/tx/detail/PayChan.h | 18 +- src/xrpld/app/tx/detail/Payment.cpp | 35 ++-- src/xrpld/app/tx/detail/Payment.h | 8 +- .../tx/detail/PermissionedDomainDelete.cpp | 22 +-- .../app/tx/detail/PermissionedDomainDelete.h | 5 +- .../app/tx/detail/PermissionedDomainSet.cpp | 24 +-- .../app/tx/detail/PermissionedDomainSet.h | 5 +- src/xrpld/app/tx/detail/SetAccount.cpp | 19 +-- src/xrpld/app/tx/detail/SetAccount.h | 5 +- src/xrpld/app/tx/detail/SetOracle.cpp | 19 +-- src/xrpld/app/tx/detail/SetOracle.h | 5 +- src/xrpld/app/tx/detail/SetRegularKey.cpp | 16 +- src/xrpld/app/tx/detail/SetRegularKey.h | 2 +- src/xrpld/app/tx/detail/SetSignerList.cpp | 21 +-- src/xrpld/app/tx/detail/SetSignerList.h | 5 +- src/xrpld/app/tx/detail/SetTrust.cpp | 19 +-- src/xrpld/app/tx/detail/SetTrust.h | 5 +- src/xrpld/app/tx/detail/Transactor.cpp | 73 +++++++-- src/xrpld/app/tx/detail/Transactor.h | 115 ++++++++++++- src/xrpld/app/tx/detail/VaultClawback.cpp | 19 +-- src/xrpld/app/tx/detail/VaultClawback.h | 5 +- src/xrpld/app/tx/detail/VaultCreate.cpp | 64 +++----- src/xrpld/app/tx/detail/VaultCreate.h | 8 +- src/xrpld/app/tx/detail/VaultDelete.cpp | 19 +-- src/xrpld/app/tx/detail/VaultDelete.h | 5 +- src/xrpld/app/tx/detail/VaultDeposit.cpp | 19 +-- src/xrpld/app/tx/detail/VaultDeposit.h | 5 +- src/xrpld/app/tx/detail/VaultSet.cpp | 23 ++- src/xrpld/app/tx/detail/VaultSet.h | 5 +- src/xrpld/app/tx/detail/VaultWithdraw.cpp | 19 +-- src/xrpld/app/tx/detail/VaultWithdraw.h | 5 +- src/xrpld/app/tx/detail/XChainBridge.cpp | 155 +++++++++--------- src/xrpld/app/tx/detail/XChainBridge.h | 43 ++++- src/xrpld/app/tx/detail/applySteps.cpp | 2 +- 102 files changed, 1125 insertions(+), 863 deletions(-) diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index 9d926fdcc1..a07a5eb1a2 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -3328,7 +3328,7 @@ private: env.current()->rules(), tapNONE, env.journal); - auto pf = AMMBid::preflight(pfctx); + auto pf = Transactor::preflight(pfctx); BEAST_EXPECT(pf == temDISABLED); env.app().config().features.insert(featureAMM); } @@ -3343,7 +3343,7 @@ private: env.current()->rules(), tapNONE, env.journal); - auto pf = AMMBid::preflight(pfctx); + auto pf = Transactor::preflight(pfctx); BEAST_EXPECT(pf != tesSUCCESS); } @@ -3358,7 +3358,7 @@ private: env.current()->rules(), tapNONE, env.journal); - auto pf = AMMBid::preflight(pfctx); + auto pf = Transactor::preflight(pfctx); BEAST_EXPECT(pf == temBAD_AMM_TOKENS); } } diff --git a/src/xrpld/app/tx/detail/AMMBid.cpp b/src/xrpld/app/tx/detail/AMMBid.cpp index 6fec46be90..f64e90013b 100644 --- a/src/xrpld/app/tx/detail/AMMBid.cpp +++ b/src/xrpld/app/tx/detail/AMMBid.cpp @@ -30,21 +30,15 @@ namespace ripple { -NotTEC -AMMBid::preflight(PreflightContext const& ctx) +bool +AMMBid::isEnabled(PreflightContext const& ctx) { - if (!ammEnabled(ctx.rules)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - { - JLOG(ctx.j.debug()) << "AMM Bid: invalid flags."; - return temINVALID_FLAG; - } + return ammEnabled(ctx.rules); +} +NotTEC +AMMBid::doPreflight(PreflightContext const& ctx) +{ if (auto const res = invalidAMMAssetPair( ctx.tx[sfAsset].get(), ctx.tx[sfAsset2].get())) { @@ -80,7 +74,7 @@ AMMBid::preflight(PreflightContext const& ctx) } } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/AMMBid.h b/src/xrpld/app/tx/detail/AMMBid.h index 4bb3a2adfd..9260ae78f2 100644 --- a/src/xrpld/app/tx/detail/AMMBid.h +++ b/src/xrpld/app/tx/detail/AMMBid.h @@ -71,8 +71,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/AMMClawback.cpp b/src/xrpld/app/tx/detail/AMMClawback.cpp index 64a42374ec..527af9b471 100644 --- a/src/xrpld/app/tx/detail/AMMClawback.cpp +++ b/src/xrpld/app/tx/detail/AMMClawback.cpp @@ -33,19 +33,21 @@ namespace ripple { -NotTEC -AMMClawback::preflight(PreflightContext const& ctx) +bool +AMMClawback::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureAMMClawback)) - return temDISABLED; + return ctx.rules.enabled(featureAMMClawback); +} - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; // LCOV_EXCL_LINE - - auto const flags = ctx.tx.getFlags(); - if (flags & tfAMMClawbackMask) - return temINVALID_FLAG; +std::uint32_t +AMMClawback::getFlagsMask(PreflightContext const& ctx) +{ + return tfAMMClawbackMask; +} +NotTEC +AMMClawback::doPreflight(PreflightContext const& ctx) +{ AccountID const issuer = ctx.tx[sfAccount]; AccountID const holder = ctx.tx[sfHolder]; @@ -63,6 +65,8 @@ AMMClawback::preflight(PreflightContext const& ctx) if (isXRP(asset)) return temMALFORMED; + auto const flags = ctx.tx.getFlags(); + if (flags & tfClawTwoAssets && asset.account != asset2.account) { JLOG(ctx.j.trace()) @@ -88,7 +92,7 @@ AMMClawback::preflight(PreflightContext const& ctx) if (clawAmount && *clawAmount <= beast::zero) return temBAD_AMOUNT; - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/AMMClawback.h b/src/xrpld/app/tx/detail/AMMClawback.h index fdcfc53e2c..828d07343b 100644 --- a/src/xrpld/app/tx/detail/AMMClawback.h +++ b/src/xrpld/app/tx/detail/AMMClawback.h @@ -33,8 +33,14 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/AMMCreate.cpp b/src/xrpld/app/tx/detail/AMMCreate.cpp index 95cb5bf2e6..0f14fb3ac6 100644 --- a/src/xrpld/app/tx/detail/AMMCreate.cpp +++ b/src/xrpld/app/tx/detail/AMMCreate.cpp @@ -31,21 +31,15 @@ namespace ripple { -NotTEC -AMMCreate::preflight(PreflightContext const& ctx) +bool +AMMCreate::isEnabled(PreflightContext const& ctx) { - if (!ammEnabled(ctx.rules)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - { - JLOG(ctx.j.debug()) << "AMM Instance: invalid flags."; - return temINVALID_FLAG; - } + return ammEnabled(ctx.rules); +} +NotTEC +AMMCreate::doPreflight(PreflightContext const& ctx) +{ auto const amount = ctx.tx[sfAmount]; auto const amount2 = ctx.tx[sfAmount2]; @@ -74,14 +68,14 @@ AMMCreate::preflight(PreflightContext const& ctx) return temBAD_FEE; } - return preflight2(ctx); + return tesSUCCESS; } XRPAmount AMMCreate::calculateBaseFee(ReadView const& view, STTx const& tx) { // The fee required for AMMCreate is one owner reserve. - return view.fees().increment; + return calculateOwnerReserveFee(view, tx); } TER diff --git a/src/xrpld/app/tx/detail/AMMCreate.h b/src/xrpld/app/tx/detail/AMMCreate.h index 189d66a55a..f6313dd8af 100644 --- a/src/xrpld/app/tx/detail/AMMCreate.h +++ b/src/xrpld/app/tx/detail/AMMCreate.h @@ -63,8 +63,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); diff --git a/src/xrpld/app/tx/detail/AMMDelete.cpp b/src/xrpld/app/tx/detail/AMMDelete.cpp index 28d56eab98..172a33073e 100644 --- a/src/xrpld/app/tx/detail/AMMDelete.cpp +++ b/src/xrpld/app/tx/detail/AMMDelete.cpp @@ -27,22 +27,16 @@ namespace ripple { -NotTEC -AMMDelete::preflight(PreflightContext const& ctx) +bool +AMMDelete::isEnabled(PreflightContext const& ctx) { - if (!ammEnabled(ctx.rules)) - return temDISABLED; + return ammEnabled(ctx.rules); +} - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - { - JLOG(ctx.j.debug()) << "AMM Delete: invalid flags."; - return temINVALID_FLAG; - } - - return preflight2(ctx); +NotTEC +AMMDelete::doPreflight(PreflightContext const& ctx) +{ + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/AMMDelete.h b/src/xrpld/app/tx/detail/AMMDelete.h index 19885b1dad..87abf92c27 100644 --- a/src/xrpld/app/tx/detail/AMMDelete.h +++ b/src/xrpld/app/tx/detail/AMMDelete.h @@ -39,8 +39,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/AMMDeposit.cpp b/src/xrpld/app/tx/detail/AMMDeposit.cpp index 6a718a3f04..cdd36c80fe 100644 --- a/src/xrpld/app/tx/detail/AMMDeposit.cpp +++ b/src/xrpld/app/tx/detail/AMMDeposit.cpp @@ -29,21 +29,23 @@ namespace ripple { -NotTEC -AMMDeposit::preflight(PreflightContext const& ctx) +bool +AMMDeposit::isEnabled(PreflightContext const& ctx) { - if (!ammEnabled(ctx.rules)) - return temDISABLED; + return ammEnabled(ctx.rules); +} - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; +std::uint32_t +AMMDeposit::getFlagsMask(PreflightContext const& ctx) +{ + return tfDepositMask; +} + +NotTEC +AMMDeposit::doPreflight(PreflightContext const& ctx) +{ auto const flags = ctx.tx.getFlags(); - if (flags & tfDepositMask) - { - JLOG(ctx.j.debug()) << "AMM Deposit: invalid flags."; - return temINVALID_FLAG; - } auto const amount = ctx.tx[~sfAmount]; auto const amount2 = ctx.tx[~sfAmount2]; @@ -159,7 +161,7 @@ AMMDeposit::preflight(PreflightContext const& ctx) return temBAD_FEE; } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/AMMDeposit.h b/src/xrpld/app/tx/detail/AMMDeposit.h index 0acb1dd9ab..982c2d8abd 100644 --- a/src/xrpld/app/tx/detail/AMMDeposit.h +++ b/src/xrpld/app/tx/detail/AMMDeposit.h @@ -68,8 +68,14 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/AMMVote.cpp b/src/xrpld/app/tx/detail/AMMVote.cpp index 84d0905a22..ca3435449d 100644 --- a/src/xrpld/app/tx/detail/AMMVote.cpp +++ b/src/xrpld/app/tx/detail/AMMVote.cpp @@ -27,15 +27,15 @@ namespace ripple { -NotTEC -AMMVote::preflight(PreflightContext const& ctx) +bool +AMMVote::isEnabled(PreflightContext const& ctx) { - if (!ammEnabled(ctx.rules)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; + return ammEnabled(ctx.rules); +} +NotTEC +AMMVote::doPreflight(PreflightContext const& ctx) +{ if (auto const res = invalidAMMAssetPair( ctx.tx[sfAsset].get(), ctx.tx[sfAsset2].get())) { @@ -43,19 +43,13 @@ AMMVote::preflight(PreflightContext const& ctx) return res; } - if (ctx.tx.getFlags() & tfUniversalMask) - { - JLOG(ctx.j.debug()) << "AMM Vote: invalid flags."; - return temINVALID_FLAG; - } - if (ctx.tx[sfTradingFee] > TRADING_FEE_THRESHOLD) { JLOG(ctx.j.debug()) << "AMM Vote: invalid trading fee."; return temBAD_FEE; } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/AMMVote.h b/src/xrpld/app/tx/detail/AMMVote.h index 2bee01aff5..bc94517961 100644 --- a/src/xrpld/app/tx/detail/AMMVote.h +++ b/src/xrpld/app/tx/detail/AMMVote.h @@ -56,8 +56,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/AMMWithdraw.cpp b/src/xrpld/app/tx/detail/AMMWithdraw.cpp index 586f453c6f..68e46cfb9b 100644 --- a/src/xrpld/app/tx/detail/AMMWithdraw.cpp +++ b/src/xrpld/app/tx/detail/AMMWithdraw.cpp @@ -28,21 +28,22 @@ namespace ripple { -NotTEC -AMMWithdraw::preflight(PreflightContext const& ctx) +bool +AMMWithdraw::isEnabled(PreflightContext const& ctx) { - if (!ammEnabled(ctx.rules)) - return temDISABLED; + return ammEnabled(ctx.rules); +} - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; +std::uint32_t +AMMWithdraw::getFlagsMask(PreflightContext const& ctx) +{ + return tfWithdrawMask; +} +NotTEC +AMMWithdraw::doPreflight(PreflightContext const& ctx) +{ auto const flags = ctx.tx.getFlags(); - if (flags & tfWithdrawMask) - { - JLOG(ctx.j.debug()) << "AMM Withdraw: invalid flags."; - return temINVALID_FLAG; - } auto const amount = ctx.tx[~sfAmount]; auto const amount2 = ctx.tx[~sfAmount2]; @@ -150,7 +151,7 @@ AMMWithdraw::preflight(PreflightContext const& ctx) } } - return preflight2(ctx); + return tesSUCCESS; } static std::optional diff --git a/src/xrpld/app/tx/detail/AMMWithdraw.h b/src/xrpld/app/tx/detail/AMMWithdraw.h index ae9328cb05..7e6397f535 100644 --- a/src/xrpld/app/tx/detail/AMMWithdraw.h +++ b/src/xrpld/app/tx/detail/AMMWithdraw.h @@ -75,8 +75,14 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/CancelCheck.cpp b/src/xrpld/app/tx/detail/CancelCheck.cpp index cfa3bd10e2..15b647446b 100644 --- a/src/xrpld/app/tx/detail/CancelCheck.cpp +++ b/src/xrpld/app/tx/detail/CancelCheck.cpp @@ -29,24 +29,16 @@ namespace ripple { -NotTEC -CancelCheck::preflight(PreflightContext const& ctx) +bool +CancelCheck::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureChecks)) - return temDISABLED; + return ctx.rules.enabled(featureChecks); +} - NotTEC const ret{preflight1(ctx)}; - if (!isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - { - // There are no flags (other than universal) for CreateCheck yet. - JLOG(ctx.j.warn()) << "Malformed transaction: Invalid flags set."; - return temINVALID_FLAG; - } - - return preflight2(ctx); +NotTEC +CancelCheck::doPreflight(PreflightContext const& ctx) +{ + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/CancelCheck.h b/src/xrpld/app/tx/detail/CancelCheck.h index d9b70f919c..3b40d24008 100644 --- a/src/xrpld/app/tx/detail/CancelCheck.h +++ b/src/xrpld/app/tx/detail/CancelCheck.h @@ -33,8 +33,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/CancelOffer.cpp b/src/xrpld/app/tx/detail/CancelOffer.cpp index 004ae1e8b9..6141f63e48 100644 --- a/src/xrpld/app/tx/detail/CancelOffer.cpp +++ b/src/xrpld/app/tx/detail/CancelOffer.cpp @@ -26,27 +26,15 @@ namespace ripple { NotTEC -CancelOffer::preflight(PreflightContext const& ctx) +CancelOffer::doPreflight(PreflightContext const& ctx) { - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - auto const uTxFlags = ctx.tx.getFlags(); - - if (uTxFlags & tfUniversalMask) - { - JLOG(ctx.j.trace()) << "Malformed transaction: " - << "Invalid flags set."; - return temINVALID_FLAG; - } - if (!ctx.tx[sfOfferSequence]) { JLOG(ctx.j.trace()) << "CancelOffer::preflight: missing sequence"; return temBAD_SEQUENCE; } - return preflight2(ctx); + return tesSUCCESS; } //------------------------------------------------------------------------------ diff --git a/src/xrpld/app/tx/detail/CancelOffer.h b/src/xrpld/app/tx/detail/CancelOffer.h index 0813dd0aef..8632115026 100644 --- a/src/xrpld/app/tx/detail/CancelOffer.h +++ b/src/xrpld/app/tx/detail/CancelOffer.h @@ -36,7 +36,7 @@ public: } static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/CashCheck.cpp b/src/xrpld/app/tx/detail/CashCheck.cpp index cccda83a68..5ab84f0524 100644 --- a/src/xrpld/app/tx/detail/CashCheck.cpp +++ b/src/xrpld/app/tx/detail/CashCheck.cpp @@ -32,23 +32,15 @@ namespace ripple { -NotTEC -CashCheck::preflight(PreflightContext const& ctx) +bool +CashCheck::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureChecks)) - return temDISABLED; - - NotTEC const ret{preflight1(ctx)}; - if (!isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - { - // There are no flags (other than universal) for CashCheck yet. - JLOG(ctx.j.warn()) << "Malformed transaction: Invalid flags set."; - return temINVALID_FLAG; - } + return ctx.rules.enabled(featureChecks); +} +NotTEC +CashCheck::doPreflight(PreflightContext const& ctx) +{ // Exactly one of Amount or DeliverMin must be present. auto const optAmount = ctx.tx[~sfAmount]; auto const optDeliverMin = ctx.tx[~sfDeliverMin]; @@ -76,7 +68,7 @@ CashCheck::preflight(PreflightContext const& ctx) return temBAD_CURRENCY; } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/CashCheck.h b/src/xrpld/app/tx/detail/CashCheck.h index da7ef22e3c..5dbd21a8e3 100644 --- a/src/xrpld/app/tx/detail/CashCheck.h +++ b/src/xrpld/app/tx/detail/CashCheck.h @@ -33,8 +33,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/Change.cpp b/src/xrpld/app/tx/detail/Change.cpp index 1392d84c08..f1e95a376f 100644 --- a/src/xrpld/app/tx/detail/Change.cpp +++ b/src/xrpld/app/tx/detail/Change.cpp @@ -33,11 +33,12 @@ namespace ripple { +template <> NotTEC -Change::preflight(PreflightContext const& ctx) +Transactor::preflight(PreflightContext const& ctx) { - auto const ret = preflight0(ctx); - if (!isTesSuccess(ret)) + // 0 means "Allow any flags" + if (auto const ret = preflight0(ctx, 0)) return ret; auto account = ctx.tx.getAccountID(sfAccount); diff --git a/src/xrpld/app/tx/detail/Change.h b/src/xrpld/app/tx/detail/Change.h index d710827dd6..7b8fbf3421 100644 --- a/src/xrpld/app/tx/detail/Change.h +++ b/src/xrpld/app/tx/detail/Change.h @@ -33,9 +33,6 @@ public: { } - static NotTEC - preflight(PreflightContext const& ctx); - TER doApply() override; void diff --git a/src/xrpld/app/tx/detail/Clawback.cpp b/src/xrpld/app/tx/detail/Clawback.cpp index 41ab1256fb..0375b6b718 100644 --- a/src/xrpld/app/tx/detail/Clawback.cpp +++ b/src/xrpld/app/tx/detail/Clawback.cpp @@ -75,25 +75,28 @@ preflightHelper(PreflightContext const& ctx) return tesSUCCESS; } -NotTEC -Clawback::preflight(PreflightContext const& ctx) +bool +Clawback::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureClawback)) - return temDISABLED; + return ctx.rules.enabled(featureClawback); +} - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfClawbackMask) - return temINVALID_FLAG; +std::uint32_t +Clawback::getFlagsMask(PreflightContext const& ctx) +{ + return tfClawbackMask; +} +NotTEC +Clawback::doPreflight(PreflightContext const& ctx) +{ if (auto const ret = std::visit( [&](T const&) { return preflightHelper(ctx); }, ctx.tx[sfAmount].asset().value()); !isTesSuccess(ret)) return ret; - return preflight2(ctx); + return tesSUCCESS; } template diff --git a/src/xrpld/app/tx/detail/Clawback.h b/src/xrpld/app/tx/detail/Clawback.h index d908a2e4ef..6353c79a55 100644 --- a/src/xrpld/app/tx/detail/Clawback.h +++ b/src/xrpld/app/tx/detail/Clawback.h @@ -33,8 +33,14 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/CreateCheck.cpp b/src/xrpld/app/tx/detail/CreateCheck.cpp index 9baceef944..7b19629230 100644 --- a/src/xrpld/app/tx/detail/CreateCheck.cpp +++ b/src/xrpld/app/tx/detail/CreateCheck.cpp @@ -28,22 +28,15 @@ namespace ripple { -NotTEC -CreateCheck::preflight(PreflightContext const& ctx) +bool +CreateCheck::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureChecks)) - return temDISABLED; + return ctx.rules.enabled(featureChecks); +} - NotTEC const ret{preflight1(ctx)}; - if (!isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - { - // There are no flags (other than universal) for CreateCheck yet. - JLOG(ctx.j.warn()) << "Malformed transaction: Invalid flags set."; - return temINVALID_FLAG; - } +NotTEC +CreateCheck::doPreflight(PreflightContext const& ctx) +{ if (ctx.tx[sfAccount] == ctx.tx[sfDestination]) { // They wrote a check to themselves. @@ -76,7 +69,7 @@ CreateCheck::preflight(PreflightContext const& ctx) } } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/CreateCheck.h b/src/xrpld/app/tx/detail/CreateCheck.h index 0e414ce012..cb794332ca 100644 --- a/src/xrpld/app/tx/detail/CreateCheck.h +++ b/src/xrpld/app/tx/detail/CreateCheck.h @@ -33,8 +33,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/CreateOffer.cpp b/src/xrpld/app/tx/detail/CreateOffer.cpp index d9bd57ec3c..ab6c0c1fbb 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.cpp +++ b/src/xrpld/app/tx/detail/CreateOffer.cpp @@ -39,23 +39,20 @@ CreateOffer::makeTxConsequences(PreflightContext const& ctx) return TxConsequences{ctx.tx, calculateMaxXRPSpend(ctx.tx)}; } -NotTEC -CreateOffer::preflight(PreflightContext const& ctx) +std::uint32_t +CreateOffer::getFlagsMask(PreflightContext const& ctx) { - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; + return tfOfferCreateMask; +} +NotTEC +CreateOffer::doPreflight(PreflightContext const& ctx) +{ auto& tx = ctx.tx; auto& j = ctx.j; std::uint32_t const uTxFlags = tx.getFlags(); - if (uTxFlags & tfOfferCreateMask) - { - JLOG(j.debug()) << "Malformed transaction: Invalid flags set."; - return temINVALID_FLAG; - } - bool const bImmediateOrCancel(uTxFlags & tfImmediateOrCancel); bool const bFillOrKill(uTxFlags & tfFillOrKill); @@ -122,7 +119,7 @@ CreateOffer::preflight(PreflightContext const& ctx) return temBAD_ISSUER; } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/CreateOffer.h b/src/xrpld/app/tx/detail/CreateOffer.h index 35808c78fe..d654c14042 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.h +++ b/src/xrpld/app/tx/detail/CreateOffer.h @@ -44,9 +44,12 @@ public: static TxConsequences makeTxConsequences(PreflightContext const& ctx); + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + /** Enforce constraints beyond those of the Transactor base class. */ static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); /** Enforce constraints beyond those of the Transactor base class. */ static TER diff --git a/src/xrpld/app/tx/detail/CreateTicket.cpp b/src/xrpld/app/tx/detail/CreateTicket.cpp index 594335f489..254cf68555 100644 --- a/src/xrpld/app/tx/detail/CreateTicket.cpp +++ b/src/xrpld/app/tx/detail/CreateTicket.cpp @@ -33,23 +33,20 @@ CreateTicket::makeTxConsequences(PreflightContext const& ctx) return TxConsequences{ctx.tx, ctx.tx[sfTicketCount]}; } -NotTEC -CreateTicket::preflight(PreflightContext const& ctx) +bool +CreateTicket::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureTicketBatch)) - return temDISABLED; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; + return ctx.rules.enabled(featureTicketBatch); +} +NotTEC +CreateTicket::doPreflight(PreflightContext const& ctx) +{ if (std::uint32_t const count = ctx.tx[sfTicketCount]; count < minValidCount || count > maxValidCount) return temINVALID_COUNT; - if (NotTEC const ret{preflight1(ctx)}; !isTesSuccess(ret)) - return ret; - - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/CreateTicket.h b/src/xrpld/app/tx/detail/CreateTicket.h index 86cf91bb5a..746daf7c56 100644 --- a/src/xrpld/app/tx/detail/CreateTicket.h +++ b/src/xrpld/app/tx/detail/CreateTicket.h @@ -69,9 +69,12 @@ public: static TxConsequences makeTxConsequences(PreflightContext const& ctx); + static bool + isEnabled(PreflightContext const& ctx); + /** Enforce constraints beyond those of the Transactor base class. */ static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); /** Enforce constraints beyond those of the Transactor base class. */ static TER diff --git a/src/xrpld/app/tx/detail/Credentials.cpp b/src/xrpld/app/tx/detail/Credentials.cpp index 73c397cf37..83873a9612 100644 --- a/src/xrpld/app/tx/detail/Credentials.cpp +++ b/src/xrpld/app/tx/detail/Credentials.cpp @@ -48,28 +48,25 @@ using namespace credentials; // ------- CREATE -------------------------- -NotTEC -CredentialCreate::preflight(PreflightContext const& ctx) +bool +CredentialCreate::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureCredentials)) - { - JLOG(ctx.j.trace()) << "featureCredentials is disabled."; - return temDISABLED; - } + return ctx.rules.enabled(featureCredentials); +} - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; +std::uint32_t +CredentialCreate::getFlagsMask(PreflightContext const& ctx) +{ + // 0 means "Allow any flags" + return ctx.rules.enabled(fixInvalidTxFlags) ? tfUniversalMask : 0; +} +NotTEC +CredentialCreate::doPreflight(PreflightContext const& ctx) +{ auto const& tx = ctx.tx; auto& j = ctx.j; - if (ctx.rules.enabled(fixInvalidTxFlags) && - (tx.getFlags() & tfUniversalMask)) - { - JLOG(ctx.j.debug()) << "CredentialCreate: invalid flags."; - return temINVALID_FLAG; - } - if (!tx[sfSubject]) { JLOG(j.trace()) << "Malformed transaction: Invalid Subject"; @@ -91,7 +88,7 @@ CredentialCreate::preflight(PreflightContext const& ctx) return temMALFORMED; } - return preflight2(ctx); + return tesSUCCESS; } TER @@ -202,25 +199,22 @@ CredentialCreate::doApply() } // ------- DELETE -------------------------- -NotTEC -CredentialDelete::preflight(PreflightContext const& ctx) +bool +CredentialDelete::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureCredentials)) - { - JLOG(ctx.j.trace()) << "featureCredentials is disabled."; - return temDISABLED; - } + return ctx.rules.enabled(featureCredentials); +} - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.rules.enabled(fixInvalidTxFlags) && - (ctx.tx.getFlags() & tfUniversalMask)) - { - JLOG(ctx.j.debug()) << "CredentialDelete: invalid flags."; - return temINVALID_FLAG; - } +std::uint32_t +CredentialDelete::getFlagsMask(PreflightContext const& ctx) +{ + // 0 means "Allow any flags" + return ctx.rules.enabled(fixInvalidTxFlags) ? tfUniversalMask : 0; +} +NotTEC +CredentialDelete::doPreflight(PreflightContext const& ctx) +{ auto const subject = ctx.tx[~sfSubject]; auto const issuer = ctx.tx[~sfIssuer]; @@ -248,7 +242,7 @@ CredentialDelete::preflight(PreflightContext const& ctx) return temMALFORMED; } - return preflight2(ctx); + return tesSUCCESS; } TER @@ -289,25 +283,22 @@ CredentialDelete::doApply() // ------- APPLY -------------------------- -NotTEC -CredentialAccept::preflight(PreflightContext const& ctx) +bool +CredentialAccept::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureCredentials)) - { - JLOG(ctx.j.trace()) << "featureCredentials is disabled."; - return temDISABLED; - } + return ctx.rules.enabled(featureCredentials); +} - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.rules.enabled(fixInvalidTxFlags) && - (ctx.tx.getFlags() & tfUniversalMask)) - { - JLOG(ctx.j.debug()) << "CredentialAccept: invalid flags."; - return temINVALID_FLAG; - } +std::uint32_t +CredentialAccept::getFlagsMask(PreflightContext const& ctx) +{ + // 0 means "Allow any flags" + return ctx.rules.enabled(fixInvalidTxFlags) ? tfUniversalMask : 0; +} +NotTEC +CredentialAccept::doPreflight(PreflightContext const& ctx) +{ if (!ctx.tx[sfIssuer]) { JLOG(ctx.j.trace()) << "Malformed transaction: Issuer field zeroed."; @@ -322,7 +313,7 @@ CredentialAccept::preflight(PreflightContext const& ctx) return temMALFORMED; } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/Credentials.h b/src/xrpld/app/tx/detail/Credentials.h index 5b4acb3998..ee19c5c0a3 100644 --- a/src/xrpld/app/tx/detail/Credentials.h +++ b/src/xrpld/app/tx/detail/Credentials.h @@ -33,8 +33,14 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -54,8 +60,14 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -75,8 +87,14 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/DID.cpp b/src/xrpld/app/tx/detail/DID.cpp index 31ce7c8770..d74c9733ae 100644 --- a/src/xrpld/app/tx/detail/DID.cpp +++ b/src/xrpld/app/tx/detail/DID.cpp @@ -42,18 +42,15 @@ namespace ripple { //------------------------------------------------------------------------------ -NotTEC -DIDSet::preflight(PreflightContext const& ctx) +bool +DIDSet::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureDID)) - return temDISABLED; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; + return ctx.rules.enabled(featureDID); +} +NotTEC +DIDSet::doPreflight(PreflightContext const& ctx) +{ if (!ctx.tx.isFieldPresent(sfURI) && !ctx.tx.isFieldPresent(sfDIDDocument) && !ctx.tx.isFieldPresent(sfData)) return temEMPTY_DID; @@ -74,7 +71,7 @@ DIDSet::preflight(PreflightContext const& ctx) isTooLong(sfData, maxDIDAttestationLength)) return temMALFORMED; - return preflight2(ctx); + return tesSUCCESS; } TER @@ -171,19 +168,16 @@ DIDSet::doApply() return addSLE(ctx_, sleDID, account_); } -NotTEC -DIDDelete::preflight(PreflightContext const& ctx) +bool +DIDDelete::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureDID)) - return temDISABLED; + return ctx.rules.enabled(featureDID); +} - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - return preflight2(ctx); +NotTEC +DIDDelete::doPreflight(PreflightContext const& ctx) +{ + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/DID.h b/src/xrpld/app/tx/detail/DID.h index 54ae6630f2..1995176d17 100644 --- a/src/xrpld/app/tx/detail/DID.h +++ b/src/xrpld/app/tx/detail/DID.h @@ -33,8 +33,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); TER doApply() override; @@ -51,8 +54,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER deleteSLE(ApplyContext& ctx, Keylet sleKeylet, AccountID const owner); diff --git a/src/xrpld/app/tx/detail/DelegateSet.cpp b/src/xrpld/app/tx/detail/DelegateSet.cpp index d93ed6fa96..07ebc83893 100644 --- a/src/xrpld/app/tx/detail/DelegateSet.cpp +++ b/src/xrpld/app/tx/detail/DelegateSet.cpp @@ -28,15 +28,15 @@ namespace ripple { -NotTEC -DelegateSet::preflight(PreflightContext const& ctx) +bool +DelegateSet::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featurePermissionDelegation)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; + return ctx.rules.enabled(featurePermissionDelegation); +} +NotTEC +DelegateSet::doPreflight(PreflightContext const& ctx) +{ auto const& permissions = ctx.tx.getFieldArray(sfPermissions); if (permissions.size() > permissionMaxSize) return temARRAY_TOO_LARGE; @@ -53,7 +53,7 @@ DelegateSet::preflight(PreflightContext const& ctx) return temMALFORMED; } - return preflight2(ctx); + return tesSUCCESS; } TER @@ -159,4 +159,4 @@ DelegateSet::deleteDelegate( return tesSUCCESS; } -} // namespace ripple \ No newline at end of file +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/DelegateSet.h b/src/xrpld/app/tx/detail/DelegateSet.h index 6b01d63281..87166ab5fb 100644 --- a/src/xrpld/app/tx/detail/DelegateSet.h +++ b/src/xrpld/app/tx/detail/DelegateSet.h @@ -33,8 +33,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -53,4 +56,4 @@ public: } // namespace ripple -#endif \ No newline at end of file +#endif diff --git a/src/xrpld/app/tx/detail/DeleteAccount.cpp b/src/xrpld/app/tx/detail/DeleteAccount.cpp index a2a9769d43..e27c31b670 100644 --- a/src/xrpld/app/tx/detail/DeleteAccount.cpp +++ b/src/xrpld/app/tx/detail/DeleteAccount.cpp @@ -38,22 +38,19 @@ namespace ripple { -NotTEC -DeleteAccount::preflight(PreflightContext const& ctx) +bool +DeleteAccount::isEnabled(PreflightContext const& ctx) { if (!ctx.rules.enabled(featureDeletableAccounts)) - return temDISABLED; + return false; - if (ctx.tx.isFieldPresent(sfCredentialIDs) && - !ctx.rules.enabled(featureCredentials)) - return temDISABLED; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; + return !ctx.tx.isFieldPresent(sfCredentialIDs) || + ctx.rules.enabled(featureCredentials); +} +NotTEC +DeleteAccount::doPreflight(PreflightContext const& ctx) +{ if (ctx.tx[sfAccount] == ctx.tx[sfDestination]) // An account cannot be deleted and give itself the resulting XRP. return temDST_IS_SRC; @@ -61,14 +58,14 @@ DeleteAccount::preflight(PreflightContext const& ctx) if (auto const err = credentials::checkFields(ctx); !isTesSuccess(err)) return err; - return preflight2(ctx); + return tesSUCCESS; } XRPAmount DeleteAccount::calculateBaseFee(ReadView const& view, STTx const& tx) { // The fee required for AccountDelete is one owner reserve. - return view.fees().increment; + return calculateOwnerReserveFee(view, tx); } namespace { diff --git a/src/xrpld/app/tx/detail/DeleteAccount.h b/src/xrpld/app/tx/detail/DeleteAccount.h index c9d3305562..42946bfc27 100644 --- a/src/xrpld/app/tx/detail/DeleteAccount.h +++ b/src/xrpld/app/tx/detail/DeleteAccount.h @@ -33,8 +33,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); diff --git a/src/xrpld/app/tx/detail/DeleteOracle.cpp b/src/xrpld/app/tx/detail/DeleteOracle.cpp index 78e3d55230..2e49877d9b 100644 --- a/src/xrpld/app/tx/detail/DeleteOracle.cpp +++ b/src/xrpld/app/tx/detail/DeleteOracle.cpp @@ -26,22 +26,16 @@ namespace ripple { -NotTEC -DeleteOracle::preflight(PreflightContext const& ctx) +bool +DeleteOracle::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featurePriceOracle)) - return temDISABLED; + return ctx.rules.enabled(featurePriceOracle); +} - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - { - JLOG(ctx.j.debug()) << "Oracle Delete: invalid flags."; - return temINVALID_FLAG; - } - - return preflight2(ctx); +NotTEC +DeleteOracle::doPreflight(PreflightContext const& ctx) +{ + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/DeleteOracle.h b/src/xrpld/app/tx/detail/DeleteOracle.h index bbbfc6f525..87183268ff 100644 --- a/src/xrpld/app/tx/detail/DeleteOracle.h +++ b/src/xrpld/app/tx/detail/DeleteOracle.h @@ -42,8 +42,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/DepositPreauth.cpp b/src/xrpld/app/tx/detail/DepositPreauth.cpp index f10f09b38f..a1bdafa852 100644 --- a/src/xrpld/app/tx/detail/DepositPreauth.cpp +++ b/src/xrpld/app/tx/detail/DepositPreauth.cpp @@ -30,11 +30,11 @@ namespace ripple { -NotTEC -DepositPreauth::preflight(PreflightContext const& ctx) +bool +DepositPreauth::isEnabled(PreflightContext const& ctx) { if (!ctx.rules.enabled(featureDepositPreauth)) - return temDISABLED; + return false; bool const authArrPresent = ctx.tx.isFieldPresent(sfAuthorizeCredentials); bool const unauthArrPresent = @@ -42,19 +42,17 @@ DepositPreauth::preflight(PreflightContext const& ctx) int const authCredPresent = static_cast(authArrPresent) + static_cast(unauthArrPresent); - if (authCredPresent && !ctx.rules.enabled(featureCredentials)) - return temDISABLED; + return !authCredPresent || ctx.rules.enabled(featureCredentials); +} - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - auto& tx = ctx.tx; - - if (tx.getFlags() & tfUniversalMask) - { - JLOG(ctx.j.trace()) << "Malformed transaction: Invalid flags set."; - return temINVALID_FLAG; - } +NotTEC +DepositPreauth::doPreflight(PreflightContext const& ctx) +{ + bool const authArrPresent = ctx.tx.isFieldPresent(sfAuthorizeCredentials); + bool const unauthArrPresent = + ctx.tx.isFieldPresent(sfUnauthorizeCredentials); + int const authCredPresent = + static_cast(authArrPresent) + static_cast(unauthArrPresent); auto const optAuth = ctx.tx[~sfAuthorize]; auto const optUnauth = ctx.tx[~sfUnauthorize]; @@ -102,7 +100,7 @@ DepositPreauth::preflight(PreflightContext const& ctx) return err; } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/DepositPreauth.h b/src/xrpld/app/tx/detail/DepositPreauth.h index 76a7c08073..3d969b8df4 100644 --- a/src/xrpld/app/tx/detail/DepositPreauth.h +++ b/src/xrpld/app/tx/detail/DepositPreauth.h @@ -33,8 +33,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/Escrow.cpp b/src/xrpld/app/tx/detail/Escrow.cpp index 0b58957fcf..a3491f479b 100644 --- a/src/xrpld/app/tx/detail/Escrow.cpp +++ b/src/xrpld/app/tx/detail/Escrow.cpp @@ -82,15 +82,16 @@ EscrowCreate::makeTxConsequences(PreflightContext const& ctx) return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; } -NotTEC -EscrowCreate::preflight(PreflightContext const& ctx) +std::uint32_t +EscrowCreate::getFlagsMask(PreflightContext const& ctx) { - if (ctx.rules.enabled(fix1543) && ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; + // 0 means "Allow any flags" + return ctx.rules.enabled(fix1543) ? tfUniversalMask : 0; +} +NotTEC +EscrowCreate::doPreflight(PreflightContext const& ctx) +{ if (!isXRP(ctx.tx[sfAmount])) return temBAD_AMOUNT; @@ -139,7 +140,7 @@ EscrowCreate::preflight(PreflightContext const& ctx) return temDISABLED; } - return preflight2(ctx); + return tesSUCCESS; } TER @@ -293,19 +294,23 @@ checkCondition(Slice f, Slice c) return validate(*fulfillment, *condition); } -NotTEC -EscrowFinish::preflight(PreflightContext const& ctx) +bool +EscrowFinish::isEnabled(PreflightContext const& ctx) { - if (ctx.rules.enabled(fix1543) && ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; + return !ctx.tx.isFieldPresent(sfCredentialIDs) || + ctx.rules.enabled(featureCredentials); +} - if (ctx.tx.isFieldPresent(sfCredentialIDs) && - !ctx.rules.enabled(featureCredentials)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; +std::uint32_t +EscrowFinish::getFlagsMask(PreflightContext const& ctx) +{ + // 0 means "Allow any flags" + return ctx.rules.enabled(fix1543) ? tfUniversalMask : 0; +} +NotTEC +EscrowFinish::doPreflight(PreflightContext const& ctx) +{ auto const cb = ctx.tx[~sfCondition]; auto const fb = ctx.tx[~sfFulfillment]; @@ -317,7 +322,7 @@ EscrowFinish::preflight(PreflightContext const& ctx) // Verify the transaction signature. If it doesn't work // then don't do any more work. { - auto const ret = preflight2(ctx); + auto const ret = detail::preflight2(ctx); if (!isTesSuccess(ret)) return ret; } @@ -512,16 +517,17 @@ EscrowFinish::doApply() //------------------------------------------------------------------------------ -NotTEC -EscrowCancel::preflight(PreflightContext const& ctx) +std::uint32_t +EscrowCancel::getFlagsMask(PreflightContext const& ctx) { - if (ctx.rules.enabled(fix1543) && ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; + // 0 means "Allow any flags" + return ctx.rules.enabled(fix1543) ? tfUniversalMask : 0; +} - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - return preflight2(ctx); +NotTEC +EscrowCancel::doPreflight(PreflightContext const& ctx) +{ + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/Escrow.h b/src/xrpld/app/tx/detail/Escrow.h index 78acdbee00..f857fea9cb 100644 --- a/src/xrpld/app/tx/detail/Escrow.h +++ b/src/xrpld/app/tx/detail/Escrow.h @@ -36,8 +36,11 @@ public: static TxConsequences makeTxConsequences(PreflightContext const& ctx); + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -57,8 +60,14 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); @@ -81,8 +90,11 @@ public: { } + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); TER doApply() override; diff --git a/src/xrpld/app/tx/detail/LedgerStateFix.cpp b/src/xrpld/app/tx/detail/LedgerStateFix.cpp index 008d9b6ae7..680cc30d95 100644 --- a/src/xrpld/app/tx/detail/LedgerStateFix.cpp +++ b/src/xrpld/app/tx/detail/LedgerStateFix.cpp @@ -27,18 +27,15 @@ namespace ripple { -NotTEC -LedgerStateFix::preflight(PreflightContext const& ctx) +bool +LedgerStateFix::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(fixNFTokenPageLinks)) - return temDISABLED; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; + return ctx.rules.enabled(fixNFTokenPageLinks); +} +NotTEC +LedgerStateFix::doPreflight(PreflightContext const& ctx) +{ switch (ctx.tx[sfLedgerFixType]) { case FixType::nfTokenPageLink: @@ -50,7 +47,7 @@ LedgerStateFix::preflight(PreflightContext const& ctx) return tefINVALID_LEDGER_FIX_TYPE; } - return preflight2(ctx); + return tesSUCCESS; } XRPAmount @@ -58,7 +55,7 @@ LedgerStateFix::calculateBaseFee(ReadView const& view, STTx const& tx) { // The fee required for LedgerStateFix is one owner reserve, just like // the fee for AccountDelete. - return view.fees().increment; + return calculateOwnerReserveFee(view, tx); } TER diff --git a/src/xrpld/app/tx/detail/LedgerStateFix.h b/src/xrpld/app/tx/detail/LedgerStateFix.h index bff51a723c..e67dfd26c9 100644 --- a/src/xrpld/app/tx/detail/LedgerStateFix.h +++ b/src/xrpld/app/tx/detail/LedgerStateFix.h @@ -37,8 +37,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp index 748c05869f..1fec7d9c3e 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp @@ -26,22 +26,25 @@ namespace ripple { -NotTEC -MPTokenAuthorize::preflight(PreflightContext const& ctx) +bool +MPTokenAuthorize::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureMPTokensV1)) - return temDISABLED; + return ctx.rules.enabled(featureMPTokensV1); +} - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfMPTokenAuthorizeMask) - return temINVALID_FLAG; +std::uint32_t +MPTokenAuthorize::getFlagsMask(PreflightContext const& ctx) +{ + return tfMPTokenAuthorizeMask; +} +NotTEC +MPTokenAuthorize::doPreflight(PreflightContext const& ctx) +{ if (ctx.tx[sfAccount] == ctx.tx[~sfHolder]) return temMALFORMED; - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.h b/src/xrpld/app/tx/detail/MPTokenAuthorize.h index e2b135a22a..5651dbf075 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.h +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.h @@ -42,8 +42,14 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp index 1b96b27f24..8f751015d8 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp @@ -25,18 +25,21 @@ namespace ripple { -NotTEC -MPTokenIssuanceCreate::preflight(PreflightContext const& ctx) +bool +MPTokenIssuanceCreate::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureMPTokensV1)) - return temDISABLED; + return ctx.rules.enabled(featureMPTokensV1); +} - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfMPTokenIssuanceCreateMask) - return temINVALID_FLAG; +std::uint32_t +MPTokenIssuanceCreate::getFlagsMask(PreflightContext const& ctx) +{ + return tfMPTokenIssuanceCreateMask; +} +NotTEC +MPTokenIssuanceCreate::doPreflight(PreflightContext const& ctx) +{ if (auto const fee = ctx.tx[~sfTransferFee]) { if (fee > maxTransferFee) @@ -64,7 +67,7 @@ MPTokenIssuanceCreate::preflight(PreflightContext const& ctx) if (maxAmt > maxMPTokenAmount) return temMALFORMED; } - return preflight2(ctx); + return tesSUCCESS; } Expected diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h index ea01908dff..3df61d52ce 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h @@ -49,8 +49,14 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); TER doApply() override; diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp index d06ea3473e..3705f8dd6e 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp @@ -25,20 +25,22 @@ namespace ripple { -NotTEC -MPTokenIssuanceDestroy::preflight(PreflightContext const& ctx) +bool +MPTokenIssuanceDestroy::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureMPTokensV1)) - return temDISABLED; + return ctx.rules.enabled(featureMPTokensV1); +} - // check flags - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; +std::uint32_t +MPTokenIssuanceDestroy::getFlagsMask(PreflightContext const& ctx) +{ + return tfMPTokenIssuanceDestroyMask; +} - if (ctx.tx.getFlags() & tfMPTokenIssuanceDestroyMask) - return temINVALID_FLAG; - - return preflight2(ctx); +NotTEC +MPTokenIssuanceDestroy::doPreflight(PreflightContext const& ctx) +{ + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h index 69abb99feb..8718caf5b5 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h @@ -33,8 +33,14 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp index 85a1f6cf1a..399d24ccd8 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp @@ -25,22 +25,25 @@ namespace ripple { -NotTEC -MPTokenIssuanceSet::preflight(PreflightContext const& ctx) +bool +MPTokenIssuanceSet::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureMPTokensV1)) - return temDISABLED; + return ctx.rules.enabled(featureMPTokensV1); +} - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; +std::uint32_t +MPTokenIssuanceSet::getFlagsMask(PreflightContext const& ctx) +{ + return tfMPTokenIssuanceSetMask; +} +NotTEC +MPTokenIssuanceSet::doPreflight(PreflightContext const& ctx) +{ auto const txFlags = ctx.tx.getFlags(); - // check flags - if (txFlags & tfMPTokenIssuanceSetMask) - return temINVALID_FLAG; // fails if both flags are set - else if ((txFlags & tfMPTLock) && (txFlags & tfMPTUnlock)) + if ((txFlags & tfMPTLock) && (txFlags & tfMPTUnlock)) return temINVALID_FLAG; auto const accountID = ctx.tx[sfAccount]; @@ -48,7 +51,7 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx) if (holderID && accountID == holderID) return temMALFORMED; - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h index 5b3db0e75b..ac2b2fa6da 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h @@ -33,8 +33,14 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER checkPermission(ReadView const& view, STTx const& tx); diff --git a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp index 4c5fdb7683..194d6f9b52 100644 --- a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp +++ b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp @@ -27,18 +27,21 @@ namespace ripple { -NotTEC -NFTokenAcceptOffer::preflight(PreflightContext const& ctx) +bool +NFTokenAcceptOffer::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureNonFungibleTokensV1)) - return temDISABLED; + return ctx.rules.enabled(featureNonFungibleTokensV1); +} - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfNFTokenAcceptOfferMask) - return temINVALID_FLAG; +std::uint32_t +NFTokenAcceptOffer::getFlagsMask(PreflightContext const& ctx) +{ + return tfNFTokenAcceptOfferMask; +} +NotTEC +NFTokenAcceptOffer::doPreflight(PreflightContext const& ctx) +{ auto const bo = ctx.tx[~sfNFTokenBuyOffer]; auto const so = ctx.tx[~sfNFTokenSellOffer]; @@ -57,7 +60,7 @@ NFTokenAcceptOffer::preflight(PreflightContext const& ctx) return temMALFORMED; } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.h b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.h index 6a594e2b2c..7effde2d45 100644 --- a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.h +++ b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.h @@ -59,8 +59,14 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/NFTokenBurn.cpp b/src/xrpld/app/tx/detail/NFTokenBurn.cpp index 947a663f92..4948ef7f2a 100644 --- a/src/xrpld/app/tx/detail/NFTokenBurn.cpp +++ b/src/xrpld/app/tx/detail/NFTokenBurn.cpp @@ -26,19 +26,16 @@ namespace ripple { -NotTEC -NFTokenBurn::preflight(PreflightContext const& ctx) +bool +NFTokenBurn::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureNonFungibleTokensV1)) - return temDISABLED; + return ctx.rules.enabled(featureNonFungibleTokensV1); +} - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - - return preflight2(ctx); +NotTEC +NFTokenBurn::doPreflight(PreflightContext const& ctx) +{ + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/NFTokenBurn.h b/src/xrpld/app/tx/detail/NFTokenBurn.h index 3f5296c217..ff930c1be7 100644 --- a/src/xrpld/app/tx/detail/NFTokenBurn.h +++ b/src/xrpld/app/tx/detail/NFTokenBurn.h @@ -33,8 +33,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/NFTokenCancelOffer.cpp b/src/xrpld/app/tx/detail/NFTokenCancelOffer.cpp index f6072bc953..5c6c406d2d 100644 --- a/src/xrpld/app/tx/detail/NFTokenCancelOffer.cpp +++ b/src/xrpld/app/tx/detail/NFTokenCancelOffer.cpp @@ -28,18 +28,21 @@ namespace ripple { -NotTEC -NFTokenCancelOffer::preflight(PreflightContext const& ctx) +bool +NFTokenCancelOffer::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureNonFungibleTokensV1)) - return temDISABLED; + return ctx.rules.enabled(featureNonFungibleTokensV1); +} - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfNFTokenCancelOfferMask) - return temINVALID_FLAG; +std::uint32_t +NFTokenCancelOffer::getFlagsMask(PreflightContext const& ctx) +{ + return tfNFTokenCancelOfferMask; +} +NotTEC +NFTokenCancelOffer::doPreflight(PreflightContext const& ctx) +{ if (auto const& ids = ctx.tx[sfNFTokenOffers]; ids.empty() || (ids.size() > maxTokenOfferCancelCount)) return temMALFORMED; @@ -51,7 +54,7 @@ NFTokenCancelOffer::preflight(PreflightContext const& ctx) if (std::adjacent_find(ids.begin(), ids.end()) != ids.end()) return temMALFORMED; - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/NFTokenCancelOffer.h b/src/xrpld/app/tx/detail/NFTokenCancelOffer.h index d460675711..e112dce6f8 100644 --- a/src/xrpld/app/tx/detail/NFTokenCancelOffer.h +++ b/src/xrpld/app/tx/detail/NFTokenCancelOffer.h @@ -33,8 +33,14 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp b/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp index 8e1a026415..46baaf7f49 100644 --- a/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp +++ b/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp @@ -26,20 +26,23 @@ namespace ripple { -NotTEC -NFTokenCreateOffer::preflight(PreflightContext const& ctx) +bool +NFTokenCreateOffer::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureNonFungibleTokensV1)) - return temDISABLED; + return ctx.rules.enabled(featureNonFungibleTokensV1); +} - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; +std::uint32_t +NFTokenCreateOffer::getFlagsMask(PreflightContext const& ctx) +{ + return tfNFTokenCreateOfferMask; +} +NotTEC +NFTokenCreateOffer::doPreflight(PreflightContext const& ctx) +{ auto const txFlags = ctx.tx.getFlags(); - if (txFlags & tfNFTokenCreateOfferMask) - return temINVALID_FLAG; - auto const nftFlags = nft::getFlags(ctx.tx[sfNFTokenID]); // Use implementation shared with NFTokenMint @@ -55,7 +58,7 @@ NFTokenCreateOffer::preflight(PreflightContext const& ctx) !isTesSuccess(notTec)) return notTec; - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/NFTokenCreateOffer.h b/src/xrpld/app/tx/detail/NFTokenCreateOffer.h index 075a5a712f..fb1b6a575f 100644 --- a/src/xrpld/app/tx/detail/NFTokenCreateOffer.h +++ b/src/xrpld/app/tx/detail/NFTokenCreateOffer.h @@ -33,8 +33,14 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/NFTokenMint.cpp b/src/xrpld/app/tx/detail/NFTokenMint.cpp index 42b551b3a4..3dcad38ee1 100644 --- a/src/xrpld/app/tx/detail/NFTokenMint.cpp +++ b/src/xrpld/app/tx/detail/NFTokenMint.cpp @@ -38,22 +38,26 @@ extractNFTokenFlagsFromTxFlags(std::uint32_t txFlags) return static_cast(txFlags & 0x0000FFFF); } -NotTEC -NFTokenMint::preflight(PreflightContext const& ctx) +static bool +hasOfferFields(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureNonFungibleTokensV1)) - return temDISABLED; - - bool const hasOfferFields = ctx.tx.isFieldPresent(sfAmount) || + return ctx.tx.isFieldPresent(sfAmount) || ctx.tx.isFieldPresent(sfDestination) || ctx.tx.isFieldPresent(sfExpiration); +} - if (!ctx.rules.enabled(featureNFTokenMintOffer) && hasOfferFields) - return temDISABLED; +bool +NFTokenMint::isEnabled(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureNonFungibleTokensV1)) + return false; - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; + return ctx.rules.enabled(featureNFTokenMintOffer) || !hasOfferFields(ctx); +} +std::uint32_t +NFTokenMint::getFlagsMask(PreflightContext const& ctx) +{ // Prior to fixRemoveNFTokenAutoTrustLine, transfer of an NFToken between // accounts allowed a TrustLine to be added to the issuer of that token // without explicit permission from that issuer. This was enabled by @@ -67,7 +71,7 @@ NFTokenMint::preflight(PreflightContext const& ctx) // The fixRemoveNFTokenAutoTrustLine amendment disables minting with the // tfTrustLine flag as a way to prevent the attack. But until the // amendment passes we still need to keep the old behavior available. - std::uint32_t const NFTokenMintMask = + std::uint32_t const nfTokenMintMask = ctx.rules.enabled(fixRemoveNFTokenAutoTrustLine) // if featureDynamicNFT enabled then new flag allowing mutable URI // available @@ -76,9 +80,12 @@ NFTokenMint::preflight(PreflightContext const& ctx) : ctx.rules.enabled(featureDynamicNFT) ? tfNFTokenMintOldMaskWithMutable : tfNFTokenMintOldMask; - if (ctx.tx.getFlags() & NFTokenMintMask) - return temINVALID_FLAG; + return nfTokenMintMask; +} +NotTEC +NFTokenMint::doPreflight(PreflightContext const& ctx) +{ if (auto const f = ctx.tx[~sfTransferFee]) { if (f > maxTransferFee) @@ -100,7 +107,7 @@ NFTokenMint::preflight(PreflightContext const& ctx) return temMALFORMED; } - if (hasOfferFields) + if (hasOfferFields(ctx)) { // The Amount field must be present if either the Destination or // Expiration fields are present. @@ -123,7 +130,7 @@ NFTokenMint::preflight(PreflightContext const& ctx) } } - return preflight2(ctx); + return tesSUCCESS; } uint256 diff --git a/src/xrpld/app/tx/detail/NFTokenMint.h b/src/xrpld/app/tx/detail/NFTokenMint.h index f606120c54..4af7448532 100644 --- a/src/xrpld/app/tx/detail/NFTokenMint.h +++ b/src/xrpld/app/tx/detail/NFTokenMint.h @@ -36,8 +36,14 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/NFTokenModify.cpp b/src/xrpld/app/tx/detail/NFTokenModify.cpp index a3803c423b..54f26387b1 100644 --- a/src/xrpld/app/tx/detail/NFTokenModify.cpp +++ b/src/xrpld/app/tx/detail/NFTokenModify.cpp @@ -25,19 +25,16 @@ namespace ripple { -NotTEC -NFTokenModify::preflight(PreflightContext const& ctx) +bool +NFTokenModify::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureNonFungibleTokensV1_1) || - !ctx.rules.enabled(featureDynamicNFT)) - return temDISABLED; - - if (NotTEC const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; + return ctx.rules.enabled(featureNonFungibleTokensV1_1) && + ctx.rules.enabled(featureDynamicNFT); +} +NotTEC +NFTokenModify::doPreflight(PreflightContext const& ctx) +{ if (auto owner = ctx.tx[~sfOwner]; owner == ctx.tx[sfAccount]) return temMALFORMED; @@ -47,7 +44,7 @@ NFTokenModify::preflight(PreflightContext const& ctx) return temMALFORMED; } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/NFTokenModify.h b/src/xrpld/app/tx/detail/NFTokenModify.h index 0d1e72ade1..a0936c6af1 100644 --- a/src/xrpld/app/tx/detail/NFTokenModify.h +++ b/src/xrpld/app/tx/detail/NFTokenModify.h @@ -33,8 +33,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/PayChan.cpp b/src/xrpld/app/tx/detail/PayChan.cpp index a42902f6ac..47fcae6ef5 100644 --- a/src/xrpld/app/tx/detail/PayChan.cpp +++ b/src/xrpld/app/tx/detail/PayChan.cpp @@ -171,15 +171,16 @@ PayChanCreate::makeTxConsequences(PreflightContext const& ctx) return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; } -NotTEC -PayChanCreate::preflight(PreflightContext const& ctx) +std::uint32_t +PayChanCreate::getFlagsMask(PreflightContext const& ctx) { - if (ctx.rules.enabled(fix1543) && ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; + // 0 means "Allow any flags" + return ctx.rules.enabled(fix1543) ? tfUniversalMask : 0; +} +NotTEC +PayChanCreate::doPreflight(PreflightContext const& ctx) +{ if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero)) return temBAD_AMOUNT; @@ -189,7 +190,7 @@ PayChanCreate::preflight(PreflightContext const& ctx) if (!publicKeyType(ctx.tx[sfPublicKey])) return temMALFORMED; - return preflight2(ctx); + return tesSUCCESS; } TER @@ -326,19 +327,20 @@ PayChanFund::makeTxConsequences(PreflightContext const& ctx) return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; } -NotTEC -PayChanFund::preflight(PreflightContext const& ctx) +std::uint32_t +PayChanFund::getFlagsMask(PreflightContext const& ctx) { - if (ctx.rules.enabled(fix1543) && ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; + // 0 means "Allow any flags" + return ctx.rules.enabled(fix1543) ? tfUniversalMask : 0; +} +NotTEC +PayChanFund::doPreflight(PreflightContext const& ctx) +{ if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero)) return temBAD_AMOUNT; - return preflight2(ctx); + return tesSUCCESS; } TER @@ -416,16 +418,23 @@ PayChanFund::doApply() //------------------------------------------------------------------------------ -NotTEC -PayChanClaim::preflight(PreflightContext const& ctx) +bool +PayChanClaim::isEnabled(PreflightContext const& ctx) { - if (ctx.tx.isFieldPresent(sfCredentialIDs) && - !ctx.rules.enabled(featureCredentials)) - return temDISABLED; + return !ctx.tx.isFieldPresent(sfCredentialIDs) || + ctx.rules.enabled(featureCredentials); +} - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; +std::uint32_t +PayChanClaim::getFlagsMask(PreflightContext const& ctx) +{ + // 0 means "Allow any flags" + return ctx.rules.enabled(fix1543) ? tfPayChanClaimMask : 0; +} +NotTEC +PayChanClaim::doPreflight(PreflightContext const& ctx) +{ auto const bal = ctx.tx[~sfBalance]; if (bal && (!isXRP(*bal) || *bal <= beast::zero)) return temBAD_AMOUNT; @@ -440,9 +449,6 @@ PayChanClaim::preflight(PreflightContext const& ctx) { auto const flags = ctx.tx.getFlags(); - if (ctx.rules.enabled(fix1543) && (flags & tfPayChanClaimMask)) - return temINVALID_FLAG; - if ((flags & tfClose) && (flags & tfRenew)) return temMALFORMED; } @@ -476,7 +482,7 @@ PayChanClaim::preflight(PreflightContext const& ctx) if (auto const err = credentials::checkFields(ctx); !isTesSuccess(err)) return err; - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/PayChan.h b/src/xrpld/app/tx/detail/PayChan.h index 2e09c473dc..1faa34499b 100644 --- a/src/xrpld/app/tx/detail/PayChan.h +++ b/src/xrpld/app/tx/detail/PayChan.h @@ -36,8 +36,11 @@ public: static TxConsequences makeTxConsequences(PreflightContext const& ctx); + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -62,8 +65,11 @@ public: static TxConsequences makeTxConsequences(PreflightContext const& ctx); + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); TER doApply() override; @@ -82,8 +88,14 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index a97e472841..1b6573181f 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -64,16 +64,27 @@ getMaxSourceAmount( dstAmount < beast::zero); } -NotTEC -Payment::preflight(PreflightContext const& ctx) +bool +Payment::isEnabled(PreflightContext const& ctx) { - if (ctx.tx.isFieldPresent(sfCredentialIDs) && - !ctx.rules.enabled(featureCredentials)) - return temDISABLED; + return !ctx.tx.isFieldPresent(sfCredentialIDs) || + ctx.rules.enabled(featureCredentials); +} - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; +std::uint32_t +Payment::getFlagsMask(PreflightContext const& ctx) +{ + auto& tx = ctx.tx; + STAmount const dstAmount(tx.getFieldAmount(sfAmount)); + bool const mptDirect = dstAmount.holds(); + + return mptDirect ? tfMPTPaymentMask : tfPaymentMask; +} + +NotTEC +Payment::doPreflight(PreflightContext const& ctx) +{ auto& tx = ctx.tx; auto& j = ctx.j; @@ -85,14 +96,6 @@ Payment::preflight(PreflightContext const& ctx) std::uint32_t const txFlags = tx.getFlags(); - std::uint32_t paymentMask = mptDirect ? tfMPTPaymentMask : tfPaymentMask; - - if (txFlags & paymentMask) - { - JLOG(j.trace()) << "Malformed transaction: Invalid flags set."; - return temINVALID_FLAG; - } - if (mptDirect && ctx.tx.isFieldPresent(sfPaths)) return temMALFORMED; @@ -236,7 +239,7 @@ Payment::preflight(PreflightContext const& ctx) if (auto const err = credentials::checkFields(ctx); !isTesSuccess(err)) return err; - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/Payment.h b/src/xrpld/app/tx/detail/Payment.h index 010a2453cf..577b7827ae 100644 --- a/src/xrpld/app/tx/detail/Payment.h +++ b/src/xrpld/app/tx/detail/Payment.h @@ -42,8 +42,14 @@ public: static TxConsequences makeTxConsequences(PreflightContext const& ctx); + static bool + isEnabled(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER checkPermission(ReadView const& view, STTx const& tx); diff --git a/src/xrpld/app/tx/detail/PermissionedDomainDelete.cpp b/src/xrpld/app/tx/detail/PermissionedDomainDelete.cpp index 64c498b68b..d89831dfe0 100644 --- a/src/xrpld/app/tx/detail/PermissionedDomainDelete.cpp +++ b/src/xrpld/app/tx/detail/PermissionedDomainDelete.cpp @@ -24,26 +24,20 @@ namespace ripple { -NotTEC -PermissionedDomainDelete::preflight(PreflightContext const& ctx) +bool +PermissionedDomainDelete::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featurePermissionedDomains)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - { - JLOG(ctx.j.debug()) << "PermissionedDomainDelete: invalid flags."; - return temINVALID_FLAG; - } + return ctx.rules.enabled(featurePermissionedDomains); +} +NotTEC +PermissionedDomainDelete::doPreflight(PreflightContext const& ctx) +{ auto const domain = ctx.tx.getFieldH256(sfDomainID); if (domain == beast::zero) return temMALFORMED; - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/PermissionedDomainDelete.h b/src/xrpld/app/tx/detail/PermissionedDomainDelete.h index 8b5fa68e59..574a99bffa 100644 --- a/src/xrpld/app/tx/detail/PermissionedDomainDelete.h +++ b/src/xrpld/app/tx/detail/PermissionedDomainDelete.h @@ -33,8 +33,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/PermissionedDomainSet.cpp b/src/xrpld/app/tx/detail/PermissionedDomainSet.cpp index 6e2df2a082..98aedb7304 100644 --- a/src/xrpld/app/tx/detail/PermissionedDomainSet.cpp +++ b/src/xrpld/app/tx/detail/PermissionedDomainSet.cpp @@ -28,22 +28,16 @@ namespace ripple { -NotTEC -PermissionedDomainSet::preflight(PreflightContext const& ctx) +bool +PermissionedDomainSet::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featurePermissionedDomains) || - !ctx.rules.enabled(featureCredentials)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - { - JLOG(ctx.j.debug()) << "PermissionedDomainSet: invalid flags."; - return temINVALID_FLAG; - } + return ctx.rules.enabled(featurePermissionedDomains) && + ctx.rules.enabled(featureCredentials); +} +NotTEC +PermissionedDomainSet::doPreflight(PreflightContext const& ctx) +{ if (auto err = credentials::checkArray( ctx.tx.getFieldArray(sfAcceptedCredentials), maxPermissionedDomainCredentialsArraySize, @@ -55,7 +49,7 @@ PermissionedDomainSet::preflight(PreflightContext const& ctx) if (domain && *domain == beast::zero) return temMALFORMED; - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/PermissionedDomainSet.h b/src/xrpld/app/tx/detail/PermissionedDomainSet.h index 502d576e32..94761bae26 100644 --- a/src/xrpld/app/tx/detail/PermissionedDomainSet.h +++ b/src/xrpld/app/tx/detail/PermissionedDomainSet.h @@ -33,8 +33,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/SetAccount.cpp b/src/xrpld/app/tx/detail/SetAccount.cpp index 599819151a..f24ddd38cd 100644 --- a/src/xrpld/app/tx/detail/SetAccount.cpp +++ b/src/xrpld/app/tx/detail/SetAccount.cpp @@ -57,23 +57,20 @@ SetAccount::makeTxConsequences(PreflightContext const& ctx) return TxConsequences{ctx.tx, getTxConsequencesCategory(ctx.tx)}; } -NotTEC -SetAccount::preflight(PreflightContext const& ctx) +std::uint32_t +SetAccount::getFlagsMask(PreflightContext const& ctx) { - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; + return tfAccountSetMask; +} +NotTEC +SetAccount::doPreflight(PreflightContext const& ctx) +{ auto& tx = ctx.tx; auto& j = ctx.j; std::uint32_t const uTxFlags = tx.getFlags(); - if (uTxFlags & tfAccountSetMask) - { - JLOG(j.trace()) << "Malformed transaction: Invalid flags set."; - return temINVALID_FLAG; - } - std::uint32_t const uSetFlag = tx.getFieldU32(sfSetFlag); std::uint32_t const uClearFlag = tx.getFieldU32(sfClearFlag); @@ -186,7 +183,7 @@ SetAccount::preflight(PreflightContext const& ctx) return temMALFORMED; } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/SetAccount.h b/src/xrpld/app/tx/detail/SetAccount.h index ed4242c250..a16d2e0cdf 100644 --- a/src/xrpld/app/tx/detail/SetAccount.h +++ b/src/xrpld/app/tx/detail/SetAccount.h @@ -38,8 +38,11 @@ public: static TxConsequences makeTxConsequences(PreflightContext const& ctx); + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER checkPermission(ReadView const& view, STTx const& tx); diff --git a/src/xrpld/app/tx/detail/SetOracle.cpp b/src/xrpld/app/tx/detail/SetOracle.cpp index 8559c3e7b9..4f6b9b700e 100644 --- a/src/xrpld/app/tx/detail/SetOracle.cpp +++ b/src/xrpld/app/tx/detail/SetOracle.cpp @@ -36,18 +36,15 @@ tokenPairKey(STObject const& pair) pair.getFieldCurrency(sfQuoteAsset).currency()); } -NotTEC -SetOracle::preflight(PreflightContext const& ctx) +bool +SetOracle::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featurePriceOracle)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; + return ctx.rules.enabled(featurePriceOracle); +} +NotTEC +SetOracle::doPreflight(PreflightContext const& ctx) +{ auto const& dataSeries = ctx.tx.getFieldArray(sfPriceDataSeries); if (dataSeries.empty()) return temARRAY_EMPTY; @@ -64,7 +61,7 @@ SetOracle::preflight(PreflightContext const& ctx) isInvalidLength(sfAssetClass, maxOracleSymbolClass)) return temMALFORMED; - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/SetOracle.h b/src/xrpld/app/tx/detail/SetOracle.h index 656b656019..a35ef74c4c 100644 --- a/src/xrpld/app/tx/detail/SetOracle.h +++ b/src/xrpld/app/tx/detail/SetOracle.h @@ -42,8 +42,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/SetRegularKey.cpp b/src/xrpld/app/tx/detail/SetRegularKey.cpp index 92d130a15a..f4d04ec1c1 100644 --- a/src/xrpld/app/tx/detail/SetRegularKey.cpp +++ b/src/xrpld/app/tx/detail/SetRegularKey.cpp @@ -49,20 +49,8 @@ SetRegularKey::calculateBaseFee(ReadView const& view, STTx const& tx) } NotTEC -SetRegularKey::preflight(PreflightContext const& ctx) +SetRegularKey::doPreflight(PreflightContext const& ctx) { - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - std::uint32_t const uTxFlags = ctx.tx.getFlags(); - - if (uTxFlags & tfUniversalMask) - { - JLOG(ctx.j.trace()) << "Malformed transaction: Invalid flags set."; - - return temINVALID_FLAG; - } - if (ctx.rules.enabled(fixMasterKeyAsRegularKey) && ctx.tx.isFieldPresent(sfRegularKey) && (ctx.tx.getAccountID(sfRegularKey) == ctx.tx.getAccountID(sfAccount))) @@ -70,7 +58,7 @@ SetRegularKey::preflight(PreflightContext const& ctx) return temBAD_REGKEY; } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/SetRegularKey.h b/src/xrpld/app/tx/detail/SetRegularKey.h index 055748aa25..00b9e47169 100644 --- a/src/xrpld/app/tx/detail/SetRegularKey.h +++ b/src/xrpld/app/tx/detail/SetRegularKey.h @@ -34,7 +34,7 @@ public: } static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); diff --git a/src/xrpld/app/tx/detail/SetSignerList.cpp b/src/xrpld/app/tx/detail/SetSignerList.cpp index 4a1ee703a0..191b947560 100644 --- a/src/xrpld/app/tx/detail/SetSignerList.cpp +++ b/src/xrpld/app/tx/detail/SetSignerList.cpp @@ -77,19 +77,16 @@ SetSignerList::determineOperation( return std::make_tuple(tesSUCCESS, quorum, sign, op); } -NotTEC -SetSignerList::preflight(PreflightContext const& ctx) +std::uint32_t +SetSignerList::getFlagsMask(PreflightContext const& ctx) { - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.rules.enabled(fixInvalidTxFlags) && - (ctx.tx.getFlags() & tfUniversalMask)) - { - JLOG(ctx.j.debug()) << "SetSignerList: invalid flags."; - return temINVALID_FLAG; - } + // 0 means "Allow any flags" + return ctx.rules.enabled(fixInvalidTxFlags) ? tfUniversalMask : 0; +} +NotTEC +SetSignerList::doPreflight(PreflightContext const& ctx) +{ auto const result = determineOperation(ctx.tx, ctx.flags, ctx.j); if (std::get<0>(result) != tesSUCCESS) @@ -119,7 +116,7 @@ SetSignerList::preflight(PreflightContext const& ctx) } } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/SetSignerList.h b/src/xrpld/app/tx/detail/SetSignerList.h index 1827aca975..575db5a127 100644 --- a/src/xrpld/app/tx/detail/SetSignerList.h +++ b/src/xrpld/app/tx/detail/SetSignerList.h @@ -51,8 +51,11 @@ public: { } + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); TER doApply() override; diff --git a/src/xrpld/app/tx/detail/SetTrust.cpp b/src/xrpld/app/tx/detail/SetTrust.cpp index 5e83c201fa..ed9528036a 100644 --- a/src/xrpld/app/tx/detail/SetTrust.cpp +++ b/src/xrpld/app/tx/detail/SetTrust.cpp @@ -67,23 +67,20 @@ computeFreezeFlags( namespace ripple { -NotTEC -SetTrust::preflight(PreflightContext const& ctx) +std::uint32_t +SetTrust::getFlagsMask(PreflightContext const& ctx) { - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; + return tfTrustSetMask; +} +NotTEC +SetTrust::doPreflight(PreflightContext const& ctx) +{ auto& tx = ctx.tx; auto& j = ctx.j; std::uint32_t const uTxFlags = tx.getFlags(); - if (uTxFlags & tfTrustSetMask) - { - JLOG(j.trace()) << "Malformed transaction: Invalid flags set."; - return temINVALID_FLAG; - } - if (!ctx.rules.enabled(featureDeepFreeze)) { // Even though the deep freeze flags are included in the @@ -127,7 +124,7 @@ SetTrust::preflight(PreflightContext const& ctx) return temDST_NEEDED; } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/SetTrust.h b/src/xrpld/app/tx/detail/SetTrust.h index a0476918ac..d7ca54d1dc 100644 --- a/src/xrpld/app/tx/detail/SetTrust.h +++ b/src/xrpld/app/tx/detail/SetTrust.h @@ -35,8 +35,11 @@ public: { } + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER checkPermission(ReadView const& view, STTx const& tx); diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 56d3302f46..33b899d823 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -34,13 +34,14 @@ #include #include #include +#include #include namespace ripple { /** Performs early sanity checks on the txid */ NotTEC -preflight0(PreflightContext const& ctx) +preflight0(PreflightContext const& ctx, std::uint32_t flagMask) { if (!isPseudoTx(ctx.tx) || ctx.tx.isFieldPresent(sfNetworkID)) { @@ -75,12 +76,38 @@ preflight0(PreflightContext const& ctx) return temINVALID; } + if (ctx.tx.getFlags() & flagMask) + { + JLOG(ctx.j.debug()) + << ctx.tx.peekAtField(sfTransactionType).getFullText() + << ": invalid flags."; + return temINVALID_FLAG; + } + + return tesSUCCESS; +} + +namespace detail { + +/** Checks the validity of the transactor signing key. + * + * Normally called from preflight1. + */ +NotTEC +preflightCheckSigningKey(STObject const& sigObject, beast::Journal j) +{ + if (auto const spk = sigObject.getFieldVL(sfSigningPubKey); + !spk.empty() && !publicKeyType(makeSlice(spk))) + { + JLOG(j.debug()) << "preflightCheckSigningKey: invalid signing key"; + return temBAD_SIGNATURE; + } return tesSUCCESS; } /** Performs early sanity checks on the account and fee fields */ NotTEC -preflight1(PreflightContext const& ctx) +preflight1(PreflightContext const& ctx, std::uint32_t flagMask) { // This is inappropriate in preflight0, because only Change transactions // skip this function, and those do not allow an sfTicketSequence field. @@ -99,8 +126,7 @@ preflight1(PreflightContext const& ctx) return temBAD_SIGNER; } - auto const ret = preflight0(ctx); - if (!isTesSuccess(ret)) + if (auto const ret = preflight0(ctx, flagMask)) return ret; auto const id = ctx.tx.getAccountID(sfAccount); @@ -118,13 +144,8 @@ preflight1(PreflightContext const& ctx) return temBAD_FEE; } - auto const spk = ctx.tx.getSigningPubKey(); - - if (!spk.empty() && !publicKeyType(makeSlice(spk))) - { - JLOG(ctx.j.debug()) << "preflight1: invalid signing key"; - return temBAD_SIGNATURE; - } + if (auto const ret = preflightCheckSigningKey(ctx.tx, ctx.j)) + return ret; // An AccountTxnID field constrains transaction ordering more than the // Sequence field. Tickets, on the other hand, reduce ordering @@ -181,6 +202,8 @@ preflight2(PreflightContext const& ctx) return tesSUCCESS; } +} // namespace detail + //------------------------------------------------------------------------------ PreflightContext::PreflightContext( @@ -203,6 +226,22 @@ Transactor::Transactor(ApplyContext& ctx) { } +bool +Transactor::validDataLength( + std::optional const& slice, + std::size_t maxLength) +{ + if (!slice) + return true; + return !slice->empty() && slice->length() <= maxLength; +} + +std::uint32_t +Transactor::getFlagsMask(PreflightContext const& ctx) +{ + return tfUniversalMask; +} + TER Transactor::checkPermission(ReadView const& view, STTx const& tx) { @@ -237,6 +276,18 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx) return baseFee + (signerCount * baseFee); } +// Returns the fee in fee units, not scaled for load. +XRPAmount +Transactor::calculateOwnerReserveFee(ReadView const& view, STTx const& tx) +{ + // One reserve increment is typically much greater than one base fee. + XRPL_ASSERT( + view.fees().increment > view.fees().base * 100, + "ripple::Transactor::calculateOwnerReserveFee : Owner reserve is much " + "greater than base fee"); + return view.fees().increment; +} + XRPAmount Transactor::minimumFee( Application& app, diff --git a/src/xrpld/app/tx/detail/Transactor.h b/src/xrpld/app/tx/detail/Transactor.h index 88ccdb8db7..ba33e0cd13 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -84,6 +84,8 @@ public: class TxConsequences; struct PreflightResult; +// Needed for preflight specialization +class Change; class Transactor { @@ -145,6 +147,24 @@ public: static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); + /* Do NOT define a preflight function in a derived class. + Instead, define + + // Optional if the transaction is gated on an amendment + static bool + isEnabled(PreflightContext const& ctx); + + // Optional if the transaction uses any flags other than tfUniversal + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + + static NotTEC + doPreflight(PreflightContext const& ctx); + */ + template + static NotTEC + preflight(PreflightContext const& ctx); + static TER preclaim(PreclaimContext const& ctx) { @@ -193,6 +213,32 @@ protected: Fees const& fees, ApplyFlags flags); + // Returns the fee in fee units, not scaled for load. + static XRPAmount + calculateOwnerReserveFee(ReadView const& view, STTx const& tx); + + // Base class always returns true + static bool + isEnabled(PreflightContext const& ctx); + + // Base class always returns tfUniversalMask + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + + static bool + validDataLength(std::optional const& slice, std::size_t maxLength); + + template + static bool + validNumericRange(std::optional value, T max, T min = {}); + + template + static bool + validNumericRange( + std::optional value, + unit::ValueUnit max, + unit::ValueUnit min = {}); + private: std::pair reset(XRPAmount fee); @@ -209,17 +255,76 @@ private: void trapTransaction(uint256) const; }; -/** Performs early sanity checks on the txid */ -NotTEC -preflight0(PreflightContext const& ctx); +inline bool +Transactor::isEnabled(PreflightContext const& ctx) +{ + return true; +} -/** Performs early sanity checks on the account and fee fields */ +/** Performs early sanity checks on the txid and flags */ NotTEC -preflight1(PreflightContext const& ctx); +preflight0(PreflightContext const& ctx, std::uint32_t flagMask); + +namespace detail { + +/** Checks the validity of the transactor signing key. + * + * Normally called from preflight1 with ctx.tx. + */ +NotTEC +preflightCheckSigningKey(STObject const& sigObject, beast::Journal j); + +/** Performs early sanity checks on the account and fee fields. + + (And passes flagMask to preflight0) +*/ +NotTEC +preflight1(PreflightContext const& ctx, std::uint32_t flagMask); /** Checks whether the signature appears valid */ NotTEC preflight2(PreflightContext const& ctx); +} // namespace detail + +// Defined in Change.cpp +template <> +NotTEC +Transactor::preflight(PreflightContext const& ctx); + +template +NotTEC +Transactor::preflight(PreflightContext const& ctx) +{ + if (!T::isEnabled(ctx)) + return temDISABLED; + + if (auto const ret = ripple::detail::preflight1(ctx, T::getFlagsMask(ctx))) + return ret; + + if (auto const ret = T::doPreflight(ctx)) + return ret; + + return ripple::detail::preflight2(ctx); +} + +template +bool +Transactor::validNumericRange(std::optional value, T max, T min) +{ + if (!value) + return true; + return value >= min && value <= max; +} + +template +bool +Transactor::validNumericRange( + std::optional value, + unit::ValueUnit max, + unit::ValueUnit min) +{ + return validNumericRange(value, max.value(), min.value()); +} } // namespace ripple diff --git a/src/xrpld/app/tx/detail/VaultClawback.cpp b/src/xrpld/app/tx/detail/VaultClawback.cpp index f9bd0c7629..39d068e58d 100644 --- a/src/xrpld/app/tx/detail/VaultClawback.cpp +++ b/src/xrpld/app/tx/detail/VaultClawback.cpp @@ -30,18 +30,15 @@ namespace ripple { -NotTEC -VaultClawback::preflight(PreflightContext const& ctx) +bool +VaultClawback::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureSingleAssetVault)) - return temDISABLED; - - if (auto const ter = preflight1(ctx)) - return ter; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; + return ctx.rules.enabled(featureSingleAssetVault); +} +NotTEC +VaultClawback::doPreflight(PreflightContext const& ctx) +{ if (ctx.tx[sfVaultID] == beast::zero) { JLOG(ctx.j.debug()) << "VaultClawback: zero/empty vault ID."; @@ -76,7 +73,7 @@ VaultClawback::preflight(PreflightContext const& ctx) } } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/VaultClawback.h b/src/xrpld/app/tx/detail/VaultClawback.h index 65f0164686..58ee97db46 100644 --- a/src/xrpld/app/tx/detail/VaultClawback.h +++ b/src/xrpld/app/tx/detail/VaultClawback.h @@ -33,8 +33,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/VaultCreate.cpp b/src/xrpld/app/tx/detail/VaultCreate.cpp index cb6a994e7e..835201cb5f 100644 --- a/src/xrpld/app/tx/detail/VaultCreate.cpp +++ b/src/xrpld/app/tx/detail/VaultCreate.cpp @@ -33,28 +33,28 @@ namespace ripple { -NotTEC -VaultCreate::preflight(PreflightContext const& ctx) +bool +VaultCreate::isEnabled(PreflightContext const& ctx) { if (!ctx.rules.enabled(featureSingleAssetVault) || !ctx.rules.enabled(featureMPTokensV1)) - return temDISABLED; + return false; - if (ctx.tx.isFieldPresent(sfDomainID) && - !ctx.rules.enabled(featurePermissionedDomains)) - return temDISABLED; + return !ctx.tx.isFieldPresent(sfDomainID) || + ctx.rules.enabled(featurePermissionedDomains); +} - if (auto const ter = preflight1(ctx)) - return ter; +std::uint32_t +VaultCreate::getFlagsMask(PreflightContext const& ctx) +{ + return tfVaultCreateMask; +} - if (ctx.tx.getFlags() & tfVaultCreateMask) - return temINVALID_FLAG; - - if (auto const data = ctx.tx[~sfData]) - { - if (data->empty() || data->length() > maxDataPayloadLength) - return temMALFORMED; - } +NotTEC +VaultCreate::doPreflight(PreflightContext const& ctx) +{ + if (!validDataLength(ctx.tx[~sfData], maxDataPayloadLength)) + return temMALFORMED; if (auto const withdrawalPolicy = ctx.tx[~sfWithdrawalPolicy]) { @@ -84,14 +84,14 @@ VaultCreate::preflight(PreflightContext const& ctx) return temMALFORMED; } - return preflight2(ctx); + return tesSUCCESS; } XRPAmount VaultCreate::calculateBaseFee(ReadView const& view, STTx const& tx) { // One reserve increment is typically much greater than one base fee. - return view.fees().increment; + return calculateOwnerReserveFee(view, tx); } TER @@ -100,32 +100,8 @@ VaultCreate::preclaim(PreclaimContext const& ctx) auto vaultAsset = ctx.tx[sfAsset]; auto account = ctx.tx[sfAccount]; - if (vaultAsset.native()) - ; // No special checks for XRP - else if (vaultAsset.holds()) - { - auto mptID = vaultAsset.get().getMptID(); - auto issuance = ctx.view.read(keylet::mptIssuance(mptID)); - if (!issuance) - return tecOBJECT_NOT_FOUND; - if (!issuance->isFlag(lsfMPTCanTransfer)) - { - // NOTE: flag lsfMPTCanTransfer is immutable, so this is debug in - // VaultCreate only; in other vault function it's an error. - JLOG(ctx.j.debug()) - << "VaultCreate: vault assets are non-transferable."; - return tecNO_AUTH; - } - } - else if (vaultAsset.holds()) - { - auto const issuer = - ctx.view.read(keylet::account(vaultAsset.getIssuer())); - if (!issuer) - return terNO_ACCOUNT; - else if (!issuer->isFlag(lsfDefaultRipple)) - return terNO_RIPPLE; - } + if (auto const ter = canAddHolding(ctx.view, vaultAsset)) + return ter; // Check for pseudo-account issuers - we do not want a vault to hold such // assets (e.g. MPT shares to other vaults or AMM LPTokens) as they would be diff --git a/src/xrpld/app/tx/detail/VaultCreate.h b/src/xrpld/app/tx/detail/VaultCreate.h index 5555644629..effb7cd475 100644 --- a/src/xrpld/app/tx/detail/VaultCreate.h +++ b/src/xrpld/app/tx/detail/VaultCreate.h @@ -33,8 +33,14 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); diff --git a/src/xrpld/app/tx/detail/VaultDelete.cpp b/src/xrpld/app/tx/detail/VaultDelete.cpp index 7861e9e9b6..71f1f86bae 100644 --- a/src/xrpld/app/tx/detail/VaultDelete.cpp +++ b/src/xrpld/app/tx/detail/VaultDelete.cpp @@ -27,25 +27,22 @@ namespace ripple { -NotTEC -VaultDelete::preflight(PreflightContext const& ctx) +bool +VaultDelete::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureSingleAssetVault)) - return temDISABLED; - - if (auto const ter = preflight1(ctx)) - return ter; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; + return ctx.rules.enabled(featureSingleAssetVault); +} +NotTEC +VaultDelete::doPreflight(PreflightContext const& ctx) +{ if (ctx.tx[sfVaultID] == beast::zero) { JLOG(ctx.j.debug()) << "VaultDelete: zero/empty vault ID."; return temMALFORMED; } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/VaultDelete.h b/src/xrpld/app/tx/detail/VaultDelete.h index 2b77e84469..aa32db48ab 100644 --- a/src/xrpld/app/tx/detail/VaultDelete.h +++ b/src/xrpld/app/tx/detail/VaultDelete.h @@ -33,8 +33,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/VaultDeposit.cpp b/src/xrpld/app/tx/detail/VaultDeposit.cpp index 0efddb0ff7..847ba06954 100644 --- a/src/xrpld/app/tx/detail/VaultDeposit.cpp +++ b/src/xrpld/app/tx/detail/VaultDeposit.cpp @@ -32,18 +32,15 @@ namespace ripple { -NotTEC -VaultDeposit::preflight(PreflightContext const& ctx) +bool +VaultDeposit::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureSingleAssetVault)) - return temDISABLED; - - if (auto const ter = preflight1(ctx)) - return ter; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; + return ctx.rules.enabled(featureSingleAssetVault); +} +NotTEC +VaultDeposit::doPreflight(PreflightContext const& ctx) +{ if (ctx.tx[sfVaultID] == beast::zero) { JLOG(ctx.j.debug()) << "VaultDeposit: zero/empty vault ID."; @@ -53,7 +50,7 @@ VaultDeposit::preflight(PreflightContext const& ctx) if (ctx.tx[sfAmount] <= beast::zero) return temBAD_AMOUNT; - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/VaultDeposit.h b/src/xrpld/app/tx/detail/VaultDeposit.h index 50515ce3d8..79cd8f4a02 100644 --- a/src/xrpld/app/tx/detail/VaultDeposit.h +++ b/src/xrpld/app/tx/detail/VaultDeposit.h @@ -33,8 +33,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/VaultSet.cpp b/src/xrpld/app/tx/detail/VaultSet.cpp index a13ce6d10e..feec2eb160 100644 --- a/src/xrpld/app/tx/detail/VaultSet.cpp +++ b/src/xrpld/app/tx/detail/VaultSet.cpp @@ -30,28 +30,25 @@ namespace ripple { -NotTEC -VaultSet::preflight(PreflightContext const& ctx) +bool +VaultSet::isEnabled(PreflightContext const& ctx) { if (!ctx.rules.enabled(featureSingleAssetVault)) - return temDISABLED; + return false; - if (ctx.tx.isFieldPresent(sfDomainID) && - !ctx.rules.enabled(featurePermissionedDomains)) - return temDISABLED; - - if (auto const ter = preflight1(ctx)) - return ter; + return !ctx.tx.isFieldPresent(sfDomainID) || + ctx.rules.enabled(featurePermissionedDomains); +} +NotTEC +VaultSet::doPreflight(PreflightContext const& ctx) +{ if (ctx.tx[sfVaultID] == beast::zero) { JLOG(ctx.j.debug()) << "VaultSet: zero/empty vault ID."; return temMALFORMED; } - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - if (auto const data = ctx.tx[~sfData]) { if (data->empty() || data->length() > maxDataPayloadLength) @@ -78,7 +75,7 @@ VaultSet::preflight(PreflightContext const& ctx) return temMALFORMED; } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/VaultSet.h b/src/xrpld/app/tx/detail/VaultSet.h index f16aa6c284..2e8004bec1 100644 --- a/src/xrpld/app/tx/detail/VaultSet.h +++ b/src/xrpld/app/tx/detail/VaultSet.h @@ -33,8 +33,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.cpp b/src/xrpld/app/tx/detail/VaultWithdraw.cpp index 7a8605cdbd..32ae9fce1c 100644 --- a/src/xrpld/app/tx/detail/VaultWithdraw.cpp +++ b/src/xrpld/app/tx/detail/VaultWithdraw.cpp @@ -30,18 +30,15 @@ namespace ripple { -NotTEC -VaultWithdraw::preflight(PreflightContext const& ctx) +bool +VaultWithdraw::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureSingleAssetVault)) - return temDISABLED; - - if (auto const ter = preflight1(ctx)) - return ter; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; + return ctx.rules.enabled(featureSingleAssetVault); +} +NotTEC +VaultWithdraw::doPreflight(PreflightContext const& ctx) +{ if (ctx.tx[sfVaultID] == beast::zero) { JLOG(ctx.j.debug()) << "VaultWithdraw: zero/empty vault ID."; @@ -58,7 +55,7 @@ VaultWithdraw::preflight(PreflightContext const& ctx) return temMALFORMED; } - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.h b/src/xrpld/app/tx/detail/VaultWithdraw.h index 0b713d403b..9ac9e420ea 100644 --- a/src/xrpld/app/tx/detail/VaultWithdraw.h +++ b/src/xrpld/app/tx/detail/VaultWithdraw.h @@ -33,8 +33,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/XChainBridge.cpp b/src/xrpld/app/tx/detail/XChainBridge.cpp index 5fa03557e5..fa188c45c5 100644 --- a/src/xrpld/app/tx/detail/XChainBridge.cpp +++ b/src/xrpld/app/tx/detail/XChainBridge.cpp @@ -1209,17 +1209,8 @@ toClaim(STTx const& tx) template NotTEC -attestationPreflight(PreflightContext const& ctx) +attestationDoPreflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureXChainBridge)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - if (!publicKeyType(ctx.tx[sfPublicKey])) return temMALFORMED; @@ -1240,7 +1231,7 @@ attestationPreflight(PreflightContext const& ctx) if (att->sendingAmount.issue() != expectedIssue) return temXCHAIN_BAD_PROOF; - return preflight2(ctx); + return tesSUCCESS; } template @@ -1375,18 +1366,15 @@ attestationDoApply(ApplyContext& ctx) } // namespace //------------------------------------------------------------------------------ -NotTEC -XChainCreateBridge::preflight(PreflightContext const& ctx) +bool +XChainCreateBridge::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureXChainBridge)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; + return ctx.rules.enabled(featureXChainBridge); +} +NotTEC +XChainCreateBridge::doPreflight(PreflightContext const& ctx) +{ auto const account = ctx.tx[sfAccount]; auto const reward = ctx.tx[sfSignatureReward]; auto const minAccountCreate = ctx.tx[~sfMinAccountCreateAmount]; @@ -1456,7 +1444,7 @@ XChainCreateBridge::preflight(PreflightContext const& ctx) return temXCHAIN_BRIDGE_BAD_ISSUES; } - return preflight2(ctx); + return tesSUCCESS; } TER @@ -1556,18 +1544,21 @@ XChainCreateBridge::doApply() //------------------------------------------------------------------------------ -NotTEC -BridgeModify::preflight(PreflightContext const& ctx) +bool +BridgeModify::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureXChainBridge)) - return temDISABLED; + return ctx.rules.enabled(featureXChainBridge); +} - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfBridgeModifyMask) - return temINVALID_FLAG; +std::uint32_t +BridgeModify::getFlagsMask(PreflightContext const& ctx) +{ + return tfBridgeModifyMask; +} +NotTEC +BridgeModify::doPreflight(PreflightContext const& ctx) +{ auto const account = ctx.tx[sfAccount]; auto const reward = ctx.tx[~sfSignatureReward]; auto const minAccountCreate = ctx.tx[~sfMinAccountCreateAmount]; @@ -1606,7 +1597,7 @@ BridgeModify::preflight(PreflightContext const& ctx) return temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT; } - return preflight2(ctx); + return tesSUCCESS; } TER @@ -1666,18 +1657,15 @@ BridgeModify::doApply() //------------------------------------------------------------------------------ -NotTEC -XChainClaim::preflight(PreflightContext const& ctx) +bool +XChainClaim::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureXChainBridge)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; + return ctx.rules.enabled(featureXChainBridge); +} +NotTEC +XChainClaim::doPreflight(PreflightContext const& ctx) +{ STXChainBridge const bridgeSpec = ctx.tx[sfXChainBridge]; auto const amount = ctx.tx[sfAmount]; @@ -1688,7 +1676,7 @@ XChainClaim::preflight(PreflightContext const& ctx) return temBAD_AMOUNT; } - return preflight2(ctx); + return tesSUCCESS; } TER @@ -1904,18 +1892,15 @@ XChainCommit::makeTxConsequences(PreflightContext const& ctx) return TxConsequences{ctx.tx, maxSpend}; } -NotTEC -XChainCommit::preflight(PreflightContext const& ctx) +bool +XChainCommit::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureXChainBridge)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; + return ctx.rules.enabled(featureXChainBridge); +} +NotTEC +XChainCommit::doPreflight(PreflightContext const& ctx) +{ auto const amount = ctx.tx[sfAmount]; auto const bridgeSpec = ctx.tx[sfXChainBridge]; @@ -1926,7 +1911,7 @@ XChainCommit::preflight(PreflightContext const& ctx) amount.issue() != bridgeSpec.issuingChainIssue()) return temBAD_ISSUER; - return preflight2(ctx); + return tesSUCCESS; } TER @@ -2018,24 +2003,21 @@ XChainCommit::doApply() //------------------------------------------------------------------------------ -NotTEC -XChainCreateClaimID::preflight(PreflightContext const& ctx) +bool +XChainCreateClaimID::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureXChainBridge)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; + return ctx.rules.enabled(featureXChainBridge); +} +NotTEC +XChainCreateClaimID::doPreflight(PreflightContext const& ctx) +{ auto const reward = ctx.tx[sfSignatureReward]; if (!isXRP(reward) || reward.signum() < 0 || !isLegalNet(reward)) return temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT; - return preflight2(ctx); + return tesSUCCESS; } TER @@ -2133,10 +2115,16 @@ XChainCreateClaimID::doApply() //------------------------------------------------------------------------------ -NotTEC -XChainAddClaimAttestation::preflight(PreflightContext const& ctx) +bool +XChainAddClaimAttestation::isEnabled(PreflightContext const& ctx) { - return attestationPreflight(ctx); + return ctx.rules.enabled(featureXChainBridge); +} + +NotTEC +XChainAddClaimAttestation::doPreflight(PreflightContext const& ctx) +{ + return attestationDoPreflight(ctx); } TER @@ -2153,10 +2141,16 @@ XChainAddClaimAttestation::doApply() //------------------------------------------------------------------------------ -NotTEC -XChainAddAccountCreateAttestation::preflight(PreflightContext const& ctx) +bool +XChainAddAccountCreateAttestation::isEnabled(PreflightContext const& ctx) { - return attestationPreflight(ctx); + return ctx.rules.enabled(featureXChainBridge); +} + +NotTEC +XChainAddAccountCreateAttestation::doPreflight(PreflightContext const& ctx) +{ + return attestationDoPreflight(ctx); } TER @@ -2173,18 +2167,15 @@ XChainAddAccountCreateAttestation::doApply() //------------------------------------------------------------------------------ -NotTEC -XChainCreateAccountCommit::preflight(PreflightContext const& ctx) +bool +XChainCreateAccountCommit::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureXChainBridge)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; + return ctx.rules.enabled(featureXChainBridge); +} +NotTEC +XChainCreateAccountCommit::doPreflight(PreflightContext const& ctx) +{ auto const amount = ctx.tx[sfAmount]; if (amount.signum() <= 0 || !amount.native()) @@ -2197,7 +2188,7 @@ XChainCreateAccountCommit::preflight(PreflightContext const& ctx) if (reward.issue() != amount.issue()) return temBAD_AMOUNT; - return preflight2(ctx); + return tesSUCCESS; } TER diff --git a/src/xrpld/app/tx/detail/XChainBridge.h b/src/xrpld/app/tx/detail/XChainBridge.h index 82b64cc0e3..b116418023 100644 --- a/src/xrpld/app/tx/detail/XChainBridge.h +++ b/src/xrpld/app/tx/detail/XChainBridge.h @@ -39,8 +39,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -58,8 +61,14 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -91,8 +100,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -118,8 +130,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -147,8 +162,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -176,8 +194,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -197,8 +218,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -241,8 +265,11 @@ public: { } + static bool + isEnabled(PreflightContext const& ctx); + static NotTEC - preflight(PreflightContext const& ctx); + doPreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index 32745f703d..476021cbd6 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -120,7 +120,7 @@ invoke_preflight(PreflightContext const& ctx) try { return with_txn_type(ctx.tx.getTxnType(), [&]() { - auto const tec = T::preflight(ctx); + auto const tec = Transactor::preflight(ctx); return std::make_pair( tec, isTesSuccess(tec) ? consequences_helper(ctx) From fb5d94bbefcd09bf64be711c8bf534b42850edd4 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sat, 17 May 2025 15:12:54 +0100 Subject: [PATCH 004/291] Refactor 4: Transactor extra signing support --- include/xrpl/protocol/SField.h | 4 +- src/test/jtx/Account.h | 4 ++ src/test/jtx/impl/Account.cpp | 8 +++ src/xrpld/app/tx/detail/Transactor.cpp | 77 +++++++++++++++++--------- src/xrpld/app/tx/detail/Transactor.h | 26 ++++++++- 5 files changed, 90 insertions(+), 29 deletions(-) diff --git a/include/xrpl/protocol/SField.h b/include/xrpl/protocol/SField.h index a72332b888..c8e5d6dca3 100644 --- a/include/xrpl/protocol/SField.h +++ b/include/xrpl/protocol/SField.h @@ -149,8 +149,8 @@ public: sMD_ChangeNew = 0x02, // new value when it changes sMD_DeleteFinal = 0x04, // final value when it is deleted sMD_Create = 0x08, // value when it's created - sMD_Always = 0x10, // value when node containing it is affected at all - sMD_BaseTen = 0x20, // value is treated as base 10, overriding behavior + sMD_Always = 0x10, // value when node containing it is affected at all + sMD_BaseTen = 0x20, // value is treated as base 10, overriding behavior sMD_PseudoAccount = 0x40, // if this field is set in an ACCOUNT_ROOT // _only_, then it is a pseudo-account sMD_Default = diff --git a/src/test/jtx/Account.h b/src/test/jtx/Account.h index d91bb4a383..2e6987d32b 100644 --- a/src/test/jtx/Account.h +++ b/src/test/jtx/Account.h @@ -74,6 +74,10 @@ public: /** @} */ + /** Create an amount from an account ID. Should only be used when the secret + * key is unavailable, such as for pseudo-accounts. */ + explicit Account(std::string name, AccountID const& id); + enum AcctStringType { base58Seed, other }; /** Create an account from a base58 seed string. Throws on invalid seed. */ Account(AcctStringType stringType, std::string base58SeedStr); diff --git a/src/test/jtx/impl/Account.cpp b/src/test/jtx/impl/Account.cpp index b61048e66f..fe901848f8 100644 --- a/src/test/jtx/impl/Account.cpp +++ b/src/test/jtx/impl/Account.cpp @@ -86,6 +86,14 @@ Account::Account(AcctStringType stringType, std::string base58SeedStr) { } +Account::Account(std::string name, AccountID const& id) + : Account(name, randomKeyPair(KeyType::secp256k1), privateCtorTag{}) +{ + // override the randomly generated values + id_ = id; + human_ = toBase58(id_); +} + IOU Account::operator[](std::string const& s) const { diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 33b899d823..1a93476e0d 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -160,26 +160,28 @@ preflight1(PreflightContext const& ctx, std::uint32_t flagMask) return tesSUCCESS; } -/** Checks whether the signature appears valid */ -NotTEC -preflight2(PreflightContext const& ctx) +std::optional +preflightCheckSimulateKeys( + ApplyFlags flags, + STObject const& sigObject, + beast::Journal j) { - if (ctx.flags & tapDRY_RUN) // simulation + if (flags & tapDRY_RUN) // simulation { - if (!ctx.tx.getSignature().empty()) + if (!sigObject.getFieldVL(sfTxnSignature).empty()) { // NOTE: This code should never be hit because it's checked in the // `simulate` RPC return temINVALID; // LCOV_EXCL_LINE } - if (!ctx.tx.isFieldPresent(sfSigners)) + if (!sigObject.isFieldPresent(sfSigners)) { // no signers, no signature - a valid simulation return tesSUCCESS; } - for (auto const& signer : ctx.tx.getFieldArray(sfSigners)) + for (auto const& signer : sigObject.getFieldArray(sfSigners)) { if (signer.isFieldPresent(sfTxnSignature) && !signer[sfTxnSignature].empty()) @@ -191,6 +193,17 @@ preflight2(PreflightContext const& ctx) } return tesSUCCESS; } + return {}; +} + +/** Checks whether the signature appears valid */ +NotTEC +preflight2(PreflightContext const& ctx) +{ + if (auto const ret = preflightCheckSimulateKeys(ctx.flags, ctx.tx, ctx.j)) + // Skips following checks if the transaction is being simulated, + // regardless of success or failure + return *ret; auto const sigValid = checkValidity( ctx.app.getHashRouter(), ctx.tx, ctx.rules, ctx.app.config()); @@ -609,28 +622,44 @@ Transactor::apply() } NotTEC -Transactor::checkSign(PreclaimContext const& ctx) +Transactor::checkSign( + PreclaimContext const& ctx, + AccountID const& id, + STObject const& sigObject) { if (ctx.flags & tapDRY_RUN) { // This code must be different for `simulate` // Since the public key may be empty even for single signing - if (ctx.tx.isFieldPresent(sfSigners)) - return checkMultiSign(ctx); - return checkSingleSign(ctx); + if (sigObject.isFieldPresent(sfSigners)) + return checkMultiSign(ctx, id, sigObject); + return checkSingleSign(ctx, id, sigObject); } // If the pk is empty, then we must be multi-signing. - if (ctx.tx.getSigningPubKey().empty()) - return checkMultiSign(ctx); + if (sigObject.getFieldVL(sfSigningPubKey).empty()) + return checkMultiSign(ctx, id, sigObject); - return checkSingleSign(ctx); + return checkSingleSign(ctx, id, sigObject); } NotTEC -Transactor::checkSingleSign(PreclaimContext const& ctx) +Transactor::checkSign(PreclaimContext const& ctx) +{ + auto const idAccount = ctx.tx.isFieldPresent(sfDelegate) + ? ctx.tx.getAccountID(sfDelegate) + : ctx.tx.getAccountID(sfAccount); + return checkSign(ctx, idAccount, ctx.tx); +} + +// TODO generalize +NotTEC +Transactor::checkSingleSign( + PreclaimContext const& ctx, + AccountID const& idAccount, + STObject const& sigObject) { // Check that the value in the signing key slot is a public key. - auto const pkSigner = ctx.tx.getSigningPubKey(); + auto const pkSigner = sigObject.getFieldVL(sfSigningPubKey); if (!(ctx.flags & tapDRY_RUN) && !publicKeyType(makeSlice(pkSigner))) { JLOG(ctx.j.trace()) @@ -639,9 +668,6 @@ Transactor::checkSingleSign(PreclaimContext const& ctx) } // Look up the account. - auto const idAccount = ctx.tx.isFieldPresent(sfDelegate) - ? ctx.tx.getAccountID(sfDelegate) - : ctx.tx.getAccountID(sfAccount); auto const sleAccount = ctx.view.read(keylet::account(idAccount)); if (!sleAccount) return terNO_ACCOUNT; @@ -708,13 +734,14 @@ Transactor::checkSingleSign(PreclaimContext const& ctx) return tesSUCCESS; } +// TODO generalize NotTEC -Transactor::checkMultiSign(PreclaimContext const& ctx) +Transactor::checkMultiSign( + PreclaimContext const& ctx, + AccountID const& id, + STObject const& sigObject) { - auto const id = ctx.tx.isFieldPresent(sfDelegate) - ? ctx.tx.getAccountID(sfDelegate) - : ctx.tx.getAccountID(sfAccount); - // Get mTxnAccountID's SignerList and Quorum. + // Get id's SignerList and Quorum. std::shared_ptr sleAccountSigners = ctx.view.read(keylet::signers(id)); // If the signer list doesn't exist the account is not multi-signing. @@ -740,7 +767,7 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) return accountSigners.error(); // Get the array of transaction signers. - STArray const& txSigners(ctx.tx.getFieldArray(sfSigners)); + STArray const& txSigners(sigObject.getFieldArray(sfSigners)); // Walk the accountSigners performing a variety of checks and see if // the quorum is met. diff --git a/src/xrpld/app/tx/detail/Transactor.h b/src/xrpld/app/tx/detail/Transactor.h index ba33e0cd13..cf57017e3e 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -217,6 +217,12 @@ protected: static XRPAmount calculateOwnerReserveFee(ReadView const& view, STTx const& tx); + static NotTEC + checkSign( + PreclaimContext const& ctx, + AccountID const& id, + STObject const& sigObject); + // Base class always returns true static bool isEnabled(PreflightContext const& ctx); @@ -248,9 +254,15 @@ private: TER payFee(); static NotTEC - checkSingleSign(PreclaimContext const& ctx); + checkSingleSign( + PreclaimContext const& ctx, + AccountID const& id, + STObject const& sigObject); static NotTEC - checkMultiSign(PreclaimContext const& ctx); + checkMultiSign( + PreclaimContext const& ctx, + AccountID const& id, + STObject const& sigObject); void trapTransaction(uint256) const; }; @@ -281,6 +293,16 @@ preflightCheckSigningKey(STObject const& sigObject, beast::Journal j); NotTEC preflight1(PreflightContext const& ctx, std::uint32_t flagMask); +/** Checks the special signing key state needed for simulation + * + * Normally called from preflight2 with ctx.tx. + */ +std::optional +preflightCheckSimulateKeys( + ApplyFlags flags, + STObject const& sigObject, + beast::Journal j); + /** Checks whether the signature appears valid */ NotTEC preflight2(PreflightContext const& ctx); From 527e0c916fc31893b476c2cd3bd785e267af4a1c Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 16 May 2025 16:10:04 +0100 Subject: [PATCH 005/291] Lending protocol implementation (XLS-0066) - Add the LendingProtocol amendment - Add Loan Broker and Loan ledger objects: - Also add new SFields, Keylet functions, and an Invariant to verify no illegal field modification - Update list of "constant" fields from spec - Also add a general check for all object types for the type and index fields - refactor: Check transaction flags in preflight0 - Adds a flagMask parameter to preflight1 so that it's impossible to forget to check flags. - Also adds a short hash prefix to all Transactor log messages. - refactor: Generalize Transactor preflight: - Derived classes no longer need to explicitly check amendments, nor call into preflight1 or preflight2. - implemeng LoanBrokerSet - Transactions: LoanDelete, LoanManage, LoanDraw, LoanPay - LoanBrokerSet creation mostly done. Need update. - Also added a lookup table for pseudo account fields. - Update changed field name. - Modify modifiable fields in an update. Note there are only two. - Add a node field to dirLink, defaulting sfOwnerNode, so other relationships can be updated. - Create some helper classes for transaction fields - Test that they work by converting some of the existing classes - Finish creating helper classes for JTx fields - Also change the pseudo account field lookup to a function that uses a switch - Update tests, update pseudo-account checking - Generalize some of the Invariant checks using macro files - Valid ledger entry type - Valid new account root and pseudo account check - Enumerate transaction privileges for invariants - Allows them to be defined in transactions.macro instead of needing to scrutinize every existing Invariant class. - List is not necessarily comprehensive, but does cover every check where more than one transaction type is involved. - Reserve a few values between Vault and Lending for future use - Pseudo-account improvements - Define pseudo-account fields with an sfield flag - Pseudo-account invariant checks rules whenever a pseudo-account is created or modified. - Move some helper functions. - Check the regular key in the pseudo-transaction invariant check. - Transactor::checkSign will always fail for a pseudo-account, so even if someone figures out how to get a good signature, it won't work. - Fix account creation to check both amendments - Add a validity range for sfDebtMaximum - Change more "failed" messages. The goal here is to be able to search the log for "failed" and ONLY get test failures. - NoModifiedUnmodifiableFields and ValidPseudoAccounts - Move the Invariants_test class into the test namespace - Clang wants an explicit ctor to emplace in a vector - Refactor: Add a Transactor base function to make it easier to get the owner reserve increment as a fee. - Refactor: Add an overload jtx::fee(increment) to pay an owner reserve. - Initial implementation of LoanBrokerDelete - Generalize the LoanBroker lifecycle test - Refactor ApplyView::dirAdd to give access to low-level operations - Takes a page from #5362, which may turn out to be useful! - Start writing Loan Broker invariants and tests - Specifically those mentioned for LoanBrokerDelete - Move all detail namespaces to be under ripple - Avoids problems with namespace collisions / ambiguous symbol issues with unity builds, especially when adding or removing files. - Add LoanBrokerCoverDeposit transaction - Add LoanBrokerCoverWithdraw transaction - Start writing tests for LoanBrokerCover* - Add support for `Asset` and `MPTIssue` to some `jtx` helper classes and functions (`balance`, `expectLine`) - Add support for pseudo-accounts to `jtx::Account` by allowing directly setting the AccountID without a matching key. - Add Asset and MPTIssue support to more jtx objects / functions - Unfortunately, to work around some ambiguous symbol compilation errors, I had to change the implicit conversion from IOU to Asset to a conversion from IOU to PrettyAsset, and add a more explicit `asset()` function. This workaround only required changing two existing tests, so seems acceptable. - Ensure that an account is not deleted with an XRP balance - Updates the AccountRootsDeletedClean invariant - Finish up the Loan Broker tests - Move inclusion of Transactor headers to transactions.macro - Only need to update in one place when adding a new transaction. - Start implementing LoanSet transactor - Add some more values and functions to make it easier to work with basis point values / bips. - Fix several earlier mistakes. - Generalize the check*Sign functions to support CounterParty - checkSign, checkSingleSign, and checkMultiSign in STTx and Transactor - Start writing Loan tests - Required adding support for counterparty signature to jtx framework: arbitrary signature field destination, multiple signer callbacks - Get Counterparty signing working - Add more LoanSet unit tests, added LoanBroker LoanSequence field - LoanSequence will prevent loan key collisions - Change Loan object indexing, fix several broken LoanSet unit tests - Loan objects will now only be indexed by LoanBrokerID and LoanSequence, which is a new field in LoanBroker. Also changes Loan.Sequence to Loan.LoanSequence to match up. - Several tests weren't working because of `PrettyAsset` scaling. Also, `PrettyAsset` calculations could overflow. Made that less likely by changing the type of `scale_`. - LoanSet will fail if an account tries to loan to itself. - Ensure that an account is not deleted with a non-zero owner count - Updates the AccountRootsDeletedClean invariant - Add unit tests to create a Loan successfully - Fix a few field initializations in LoanSet - Refactor issuance validity check in VaultCreate - Utility function: canAddHolding - Call canAddHolding from any transactor that call addEmptyHolding (LoanBrokerSet, LoanSet) - Start implementing LoanManage transaction - Also add a ValidLoan invariant - Finish `LoanManage` functionality and tests, modulo LoanDraw/Pay - Allow existing trust lines to loan brokers to be managed (by issuer) - Implement LoanDelete, and fix a bunch of math errors in LoanManage - Update to match latest spec: compute interest, LoanBroker reserves - refactor: Define getFlagsMask in the base Transactor class - Returns tfUniversalMask for most transactors - Only transactors that use other flags need to override - Implement LoanDraw, and made good progress on related tests - Start implementing LoanPay transaction - Implement LoanPay & most tests - Also add an XRPL_ASSERT_PARTS, which splits the parts of the assert message so I don't have to remember the proper formatting. Start writing LoanPay transaction tests --- include/xrpl/protocol/Indexes.h | 18 + include/xrpl/protocol/LedgerFormats.h | 5 + include/xrpl/protocol/Protocol.h | 84 + include/xrpl/protocol/SField.h | 4 +- include/xrpl/protocol/STAmount.h | 38 +- include/xrpl/protocol/STObject.h | 28 +- include/xrpl/protocol/STTx.h | 35 +- include/xrpl/protocol/TxFlags.h | 12 + include/xrpl/protocol/detail/features.macro | 7 +- .../xrpl/protocol/detail/ledger_entries.macro | 61 + include/xrpl/protocol/detail/sfields.macro | 37 + .../xrpl/protocol/detail/transactions.macro | 119 ++ src/libxrpl/basics/Number.cpp | 2 + src/libxrpl/protocol/Indexes.cpp | 14 + src/libxrpl/protocol/InnerObjectFormats.cpp | 8 + src/libxrpl/protocol/STAmount.cpp | 24 + src/libxrpl/protocol/STObject.cpp | 10 + src/libxrpl/protocol/STTx.cpp | 60 +- src/test/app/LoanBroker_test.cpp | 594 ++++++ src/test/app/Loan_test.cpp | 1702 +++++++++++++++++ src/test/jtx/JTx.h | 6 +- src/test/jtx/TestHelpers.h | 108 ++ src/test/jtx/impl/Env.cpp | 19 +- src/test/jtx/impl/TestHelpers.cpp | 136 ++ src/test/jtx/impl/multisign.cpp | 20 +- src/test/jtx/impl/sig.cpp | 14 +- src/test/jtx/impl/utility.cpp | 12 +- src/test/jtx/multisign.h | 45 +- src/test/jtx/sig.h | 15 +- src/test/jtx/utility.h | 6 + src/test/ledger/Invariants_test.cpp | 421 +++- src/xrpld/app/misc/LendingHelpers.h | 618 ++++++ src/xrpld/app/misc/detail/LendingHelpers.cpp | 111 ++ src/xrpld/app/tx/detail/Change.cpp | 6 +- src/xrpld/app/tx/detail/InvariantCheck.cpp | 331 +++- src/xrpld/app/tx/detail/InvariantCheck.h | 106 +- .../app/tx/detail/LoanBrokerCoverDeposit.cpp | 147 ++ .../app/tx/detail/LoanBrokerCoverDeposit.h | 53 + .../app/tx/detail/LoanBrokerCoverWithdraw.cpp | 164 ++ .../app/tx/detail/LoanBrokerCoverWithdraw.h | 53 + src/xrpld/app/tx/detail/LoanBrokerDelete.cpp | 179 ++ src/xrpld/app/tx/detail/LoanBrokerDelete.h | 53 + src/xrpld/app/tx/detail/LoanBrokerSet.cpp | 210 ++ src/xrpld/app/tx/detail/LoanBrokerSet.h | 53 + src/xrpld/app/tx/detail/LoanDelete.cpp | 167 ++ src/xrpld/app/tx/detail/LoanDelete.h | 53 + src/xrpld/app/tx/detail/LoanDraw.cpp | 194 ++ src/xrpld/app/tx/detail/LoanDraw.h | 53 + src/xrpld/app/tx/detail/LoanManage.cpp | 442 +++++ src/xrpld/app/tx/detail/LoanManage.h | 56 + src/xrpld/app/tx/detail/LoanPay.cpp | 297 +++ src/xrpld/app/tx/detail/LoanPay.h | 53 + src/xrpld/app/tx/detail/LoanSet.cpp | 418 ++++ src/xrpld/app/tx/detail/LoanSet.h | 74 + src/xrpld/app/tx/detail/SetTrust.cpp | 4 +- src/xrpld/app/tx/detail/Transactor.cpp | 10 + src/xrpld/ledger/ApplyView.h | 39 + src/xrpld/ledger/View.h | 6 +- src/xrpld/ledger/detail/ApplyView.cpp | 204 +- src/xrpld/ledger/detail/View.cpp | 15 +- 60 files changed, 7684 insertions(+), 149 deletions(-) create mode 100644 src/test/app/LoanBroker_test.cpp create mode 100644 src/test/app/Loan_test.cpp create mode 100644 src/xrpld/app/misc/LendingHelpers.h create mode 100644 src/xrpld/app/misc/detail/LendingHelpers.cpp create mode 100644 src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp create mode 100644 src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.h create mode 100644 src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp create mode 100644 src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h create mode 100644 src/xrpld/app/tx/detail/LoanBrokerDelete.cpp create mode 100644 src/xrpld/app/tx/detail/LoanBrokerDelete.h create mode 100644 src/xrpld/app/tx/detail/LoanBrokerSet.cpp create mode 100644 src/xrpld/app/tx/detail/LoanBrokerSet.h create mode 100644 src/xrpld/app/tx/detail/LoanDelete.cpp create mode 100644 src/xrpld/app/tx/detail/LoanDelete.h create mode 100644 src/xrpld/app/tx/detail/LoanDraw.cpp create mode 100644 src/xrpld/app/tx/detail/LoanDraw.h create mode 100644 src/xrpld/app/tx/detail/LoanManage.cpp create mode 100644 src/xrpld/app/tx/detail/LoanManage.h create mode 100644 src/xrpld/app/tx/detail/LoanPay.cpp create mode 100644 src/xrpld/app/tx/detail/LoanPay.h create mode 100644 src/xrpld/app/tx/detail/LoanSet.cpp create mode 100644 src/xrpld/app/tx/detail/LoanSet.h diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index 57c8727ae6..d3d2adbe92 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -343,6 +343,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(uint256 const& loanBrokerID, std::uint32_t loanSeq) 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/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index 3edd656213..d78d3e9dd0 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -194,6 +194,11 @@ enum LedgerSpecificFlags { // ltVAULT lsfVaultPrivate = 0x00010000, + + // ltLOAN + lsfLoanDefault = 0x00010000, + lsfLoanImpaired = 0x00020000, + lsfLoanOverpayment = 0x00040000, // True, loan allows overpayments }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 1a49d9d09e..fb8e7ce9d1 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -84,6 +84,90 @@ std::size_t constexpr maxDeletableTokenOfferEntries = 500; */ std::uint16_t constexpr maxTransferFee = 50000; +/** There are 10,000 basis points (bips) in 100%. + * + * Basis points represent 0.01%. + * + * Given a value X, to find the amount for B bps, + * use X * B / bipsPerUnity + * + * Example: If a loan broker has 999 XRP of debt, and must maintain 1,000 bps of + * that debt as cover (10%), then the minimum cover amount is 999,000,000 drops + * * 1000 / bipsPerUnity = 99,900,00 drops or 99.9 XRP. + * + * Given a percentage P, to find the number of bps that percentage represents, + * use P * bipsPerUnity. + * + * Example: 50% is 0.50 * bipsPerUnity = 5,000 bps. + */ +Bips32 constexpr bipsPerUnity(100 * 100); +TenthBips32 constexpr tenthBipsPerUnity(bipsPerUnity.value() * 10); + +constexpr Bips32 +percentageToBips(std::uint32_t percentage) +{ + return Bips32(percentage * bipsPerUnity.value() / 100); +} +constexpr TenthBips32 +percentageToTenthBips(std::uint32_t percentage) +{ + return TenthBips32(percentage * tenthBipsPerUnity.value() / 100); +} +template +constexpr T +bipsOfValue(T value, Bips bips) +{ + return value * bips.value() / bipsPerUnity.value(); +} +template +constexpr T +tenthBipsOfValue(T value, TenthBips bips) +{ + return value * bips.value() / tenthBipsPerUnity.value(); +} + +/** The maximum management fee rate allowed by a loan broker in 1/10 bips. + + Valid values are between 0 and 10% inclusive. +*/ +TenthBips16 constexpr maxManagementFeeRate( + unsafe_cast(percentageToTenthBips(10).value())); +static_assert(maxManagementFeeRate == TenthBips16(std::uint16_t(10'000u))); + +/** The maximum coverage rate required of a loan broker in 1/10 bips. + + Valid values are between 0 and 100% inclusive. +*/ +TenthBips32 constexpr maxCoverRate = percentageToTenthBips(100); +static_assert(maxCoverRate == TenthBips32(100'000u)); + +/** The maximum overpayment fee on a loan in 1/10 bips. +* + Valid values are between 0 and 100% inclusive. +*/ +TenthBips32 constexpr maxOverpaymentFee = percentageToTenthBips(100); + +/** The maximum premium added to the interest rate for late payments on a loan + * in 1/10 bips. + * + * Valid values are between 0 and 100% inclusive. + */ +TenthBips32 constexpr maxLateInterestRate = percentageToTenthBips(100); + +/** The maximum close interest rate charged for repaying a loan early in 1/10 + * bips. + * + * Valid values are between 0 and 100% inclusive. + */ +TenthBips32 constexpr maxCloseInterestRate = percentageToTenthBips(100); + +/** The maximum overpayment interest rate charged on loan overpayments in 1/10 + * bips. + * + * Valid values are between 0 and 100% inclusive. + */ +TenthBips32 constexpr maxOverpaymentInterestRate = percentageToTenthBips(100); + /** The maximum length of a URI inside an NFT */ std::size_t constexpr maxTokenURILength = 256; diff --git a/include/xrpl/protocol/SField.h b/include/xrpl/protocol/SField.h index c8e5d6dca3..7a523742d6 100644 --- a/include/xrpl/protocol/SField.h +++ b/include/xrpl/protocol/SField.h @@ -137,8 +137,8 @@ field_code(int id, int index) SFields are created at compile time. Each SField, once constructed, lives until program termination, and there - is only one instance per fieldType/fieldValue pair which serves the entire - application. + is only one instance per fieldType/fieldValue pair which serves the + entire application. */ class SField { diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index c66d273254..b7b8966a86 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -695,6 +695,36 @@ divRoundStrict( std::uint64_t getRate(STAmount const& offerOut, STAmount const& offerIn); +STAmount +roundToReference( + STAmount const value, + STAmount referenceValue, + Number::rounding_mode rounding = Number::getround()); + +/** Round an arbitrary precision Number to the precision of a given Asset. + * + * @param asset The relevant asset + * @param value The value to be rounded + * @param referenceValue Only relevant to IOU assets. A reference value to + * establish the precision limit of `value`. Should be larger than + * `value`. + * @param rounding Optional Number rounding mode + */ +template +Number +roundToAsset( + A const& asset, + Number const& value, + Number const& referenceValue, + Number::rounding_mode rounding = Number::getround()) +{ + NumberRoundModeGuard mg(rounding); + STAmount const ret{asset, value}; + if (ret.asset().native() || !ret.asset().holds()) + return ret; + return roundToReference(ret, STAmount{asset, referenceValue}); +} + //------------------------------------------------------------------------------ inline bool @@ -703,10 +733,10 @@ isXRP(STAmount const& amount) return amount.native(); } -// Since `canonicalize` does not have access to a ledger, this is needed to put -// the low-level routine stAmountCanonicalize on an amendment switch. Only -// transactions need to use this switchover. Outside of a transaction it's safe -// to unconditionally use the new behavior. +// Since `canonicalize` does not have access to a ledger, this is needed to +// put the low-level routine stAmountCanonicalize on an amendment switch. +// Only transactions need to use this switchover. Outside of a transaction +// it's safe to unconditionally use the new behavior. bool getSTAmountCanonicalizeSwitchover(); diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index b3cb561390..ef88fd08ae 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -242,6 +242,9 @@ public: getFieldPathSet(SField const& field) const; STVector256 const& getFieldV256(SField const& field) const; + // If not found, returns an object constructed with the given field + STObject + getFieldObject(SField const& field) const; STArray const& getFieldArray(SField const& field) const; STCurrency const& @@ -515,7 +518,26 @@ protected: // Constraint += and -= ValueProxy operators // to value types that support arithmetic operations template -concept IsArithmetic = std::is_arithmetic_v || std::is_same_v; +concept IsArithmeticNumber = std::is_arithmetic_v || + std::is_same_v || std::is_same_v; +template < + typename U, + typename Value = typename U::value_type, + typename Unit = typename U::unit_type> +concept IsArithmeticValueUnit = + std::is_same_v> && + IsArithmeticNumber && std::is_class_v; +template +concept IsArithmeticST = !IsArithmeticValueUnit && IsArithmeticNumber; +template +concept IsArithmetic = + IsArithmeticNumber || IsArithmeticST || IsArithmeticValueUnit; + +template +concept Addable = requires(T t, U u) { t = t + u; }; +template +concept IsArithmeticCompatible = + IsArithmetic && Addable; template class STObject::ValueProxy : public Proxy @@ -535,10 +557,12 @@ public: // Convenience operators for value types supporting // arithmetic operations template + requires IsArithmeticCompatible ValueProxy& operator+=(U const& u); template + requires IsArithmeticCompatible ValueProxy& operator-=(U const& u); @@ -774,6 +798,7 @@ STObject::ValueProxy::operator=(U&& u) template template + requires IsArithmeticCompatible STObject::ValueProxy& STObject::ValueProxy::operator+=(U const& u) { @@ -783,6 +808,7 @@ STObject::ValueProxy::operator+=(U const& u) template template + requires IsArithmeticCompatible STObject::ValueProxy& STObject::ValueProxy::operator-=(U const& u) { diff --git a/include/xrpl/protocol/STTx.h b/include/xrpl/protocol/STTx.h index b00495bf76..e89a780655 100644 --- a/include/xrpl/protocol/STTx.h +++ b/include/xrpl/protocol/STTx.h @@ -88,7 +88,13 @@ public: // Outer transaction functions / signature functions. Blob - getSignature() const; + getSignature(STObject const& sigObject) const; + + Blob + getSignature() const + { + return getSignature(*this); + } uint256 getSigningHash() const; @@ -121,10 +127,28 @@ public: void sign(PublicKey const& publicKey, SecretKey const& secretKey); + enum class RequireFullyCanonicalSig : bool { no, yes }; + /** Check the signature. + @param requireCanonicalSig If `true`, check that the signature is fully + canonical. If `false`, only check that the signature is valid. + @param rules The current ledger rules. + @param pSig Pointer to object that contains the signature fields, if not + using "this". Will most often be null + @return `true` if valid signature. If invalid, the error message string. + */ + Expected + checkSign( + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules, + STObject const* pSig) const; + + /** Check the signature. + @param requireCanonicalSig If `true`, check that the signature is fully + canonical. If `false`, only check that the signature is valid. + @param rules The current ledger rules. @return `true` if valid signature. If invalid, the error message string. */ - enum class RequireFullyCanonicalSig : bool { no, yes }; Expected checkSign(RequireFullyCanonicalSig requireCanonicalSig, Rules const& rules) const; @@ -146,12 +170,15 @@ public: private: Expected - checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const; + checkSingleSign( + RequireFullyCanonicalSig requireCanonicalSig, + STObject const* pSig) const; Expected checkMultiSign( RequireFullyCanonicalSig requireCanonicalSig, - Rules const& rules) const; + Rules const& rules, + STObject const* pSig) const; STBase* copy(std::size_t n, void* buf) const override; diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index b2da99594a..24aa92399c 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -242,6 +242,18 @@ constexpr std::uint32_t const tfVaultPrivate = 0x00010000; static_assert(tfVaultPrivate == lsfVaultPrivate); constexpr std::uint32_t const tfVaultShareNonTransferable = 0x00020000; constexpr std::uint32_t const tfVaultCreateMask = ~(tfUniversal | tfVaultPrivate | tfVaultShareNonTransferable); + +// LoanSet flags: +// True, indicates the loan supports overpayments +constexpr std::uint32_t const tfLoanOverpayment = 0x00010000; +constexpr std::uint32_t const tfLoanSetMask = ~(tfUniversal | tfLoanOverpayment); + +// LoanManage flags: +constexpr std::uint32_t const tfLoanDefault = 0x00010000; +constexpr std::uint32_t const tfLoanImpair = 0x00020000; +constexpr std::uint32_t const tfLoanUnimpair = 0x00040000; +constexpr std::uint32_t const tfLoanManageMask = ~(tfUniversal | tfLoanDefault | tfLoanImpair | tfLoanUnimpair); + // clang-format on } // namespace ripple diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 3be0fd426c..0b71de3a8d 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -27,11 +27,12 @@ #error "undefined macro: XRPL_RETIRE" #endif +// clang-format off + // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. -// If you add an amendment here, then do not forget to increment `numFeatures` -// in include/xrpl/protocol/Feature.h. +XRPL_FEATURE(LendingProtocol, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(SingleAssetVault, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(PermissionDelegation, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (PayChanCancelAfter, Supported::yes, VoteBehavior::DefaultNo) @@ -146,3 +147,5 @@ XRPL_RETIRE(fix1201) XRPL_RETIRE(fix1512) XRPL_RETIRE(fix1523) XRPL_RETIRE(fix1528) + +// clang-format on \ No newline at end of file diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index a902b32026..d009a67bf5 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. @@ -497,6 +498,66 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({ // no PermissionedDomainID ever (use MPTIssuance.sfDomainID) })) +/** Reserve 0x0084-0x0087 for future Vault-related objects. */ + +/** A ledger object representing a loan broker + + \sa keylet::loanbroker + */ +LEDGER_ENTRY(ltLOAN_BROKER, 0x0088, LoanBroker, loan_broker, ({ + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED}, + {sfSequence, soeREQUIRED}, + {sfOwnerNode, soeREQUIRED}, + {sfVaultNode, soeREQUIRED}, + {sfVaultID, soeREQUIRED}, + {sfAccount, soeREQUIRED}, + {sfOwner, soeREQUIRED}, + {sfLoanSequence, soeREQUIRED}, + {sfData, soeDEFAULT}, + {sfManagementFeeRate, soeDEFAULT}, + {sfOwnerCount, soeDEFAULT}, + {sfDebtTotal, soeDEFAULT}, + {sfDebtMaximum, soeDEFAULT}, + {sfCoverAvailable, soeDEFAULT}, + {sfCoverRateMinimum, soeDEFAULT}, + {sfCoverRateLiquidation, soeDEFAULT}, +})) + +/** A ledger object representing a loan between a Borrower and a Loan Broker + + \sa keylet::loan + */ +LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({ + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED}, + {sfOwnerNode, soeREQUIRED}, + {sfLoanBrokerNode, soeREQUIRED}, + {sfLoanBrokerID, soeREQUIRED}, + {sfLoanSequence, 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}, + {sfAssetsAvailable, soeREQUIRED}, + {sfPrincipalOutstanding, soeREQUIRED}, + // Save the original request amount for rounding / scaling of + // other computations, particularly for IOUs + {sfPrincipalRequested, soeREQUIRED}, +})) + #undef EXPAND #undef LEDGER_ENTRY_DUPLICATE diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 40d118c684..29771944d2 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -24,6 +24,8 @@ #error "undefined macro: TYPED_SFIELD" #endif +// clang-format off + // untyped UNTYPED_SFIELD(sfLedgerEntry, LEDGERENTRY, 257) UNTYPED_SFIELD(sfTransaction, TRANSACTION, 257) @@ -59,6 +61,7 @@ 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) // 1/10 basis points (bips) // 32-bit integers (common) TYPED_SFIELD(sfNetworkID, UINT32, 1) @@ -114,6 +117,21 @@ TYPED_SFIELD(sfVoteWeight, UINT32, 48) TYPED_SFIELD(sfFirstNFTokenSequence, UINT32, 50) TYPED_SFIELD(sfOracleDocumentID, UINT32, 51) TYPED_SFIELD(sfPermissionValue, UINT32, 52) +TYPED_SFIELD(sfStartDate, UINT32, 53) +TYPED_SFIELD(sfPaymentInterval, UINT32, 54) +TYPED_SFIELD(sfGracePeriod, UINT32, 55) +TYPED_SFIELD(sfPreviousPaymentDate, UINT32, 56) +TYPED_SFIELD(sfNextPaymentDueDate, UINT32, 57) +TYPED_SFIELD(sfPaymentRemaining, UINT32, 58) +TYPED_SFIELD(sfPaymentTotal, UINT32, 59) +TYPED_SFIELD(sfLoanSequence, UINT32, 60) +TYPED_SFIELD(sfCoverRateMinimum, UINT32, 61) // 1/10 basis points (bips) +TYPED_SFIELD(sfCoverRateLiquidation, UINT32, 62) // 1/10 basis points (bips) +TYPED_SFIELD(sfOverpaymentFee, UINT32, 63) // 1/10 basis points (bips) +TYPED_SFIELD(sfInterestRate, UINT32, 64) // 1/10 basis points (bips) +TYPED_SFIELD(sfLateInterestRate, UINT32, 65) // 1/10 basis points (bips) +TYPED_SFIELD(sfCloseInterestRate, UINT32, 66) // 1/10 basis points (bips) +TYPED_SFIELD(sfOverpaymentInterestRate, UINT32, 67) // 1/10 basis points (bips) // 64-bit integers (common) TYPED_SFIELD(sfIndexNext, UINT64, 1) @@ -144,6 +162,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) @@ -197,6 +217,9 @@ TYPED_SFIELD(sfHookSetTxnID, UINT256, 33) TYPED_SFIELD(sfDomainID, UINT256, 34) TYPED_SFIELD(sfVaultID, UINT256, 35, SField::sMD_PseudoAccount | SField::sMD_Default) +TYPED_SFIELD(sfLoanBrokerID, UINT256, 36, + SField::sMD_PseudoAccount | SField::sMD_Default) +TYPED_SFIELD(sfLoanID, UINT256, 37) // number (common) TYPED_SFIELD(sfNumber, NUMBER, 1) @@ -204,6 +227,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, 10) +TYPED_SFIELD(sfLatePaymentFee, NUMBER, 11) +TYPED_SFIELD(sfClosePaymentFee, NUMBER, 12) +TYPED_SFIELD(sfPrincipalOutstanding, NUMBER, 13) +TYPED_SFIELD(sfPrincipalRequested, NUMBER, 14) // currency amount (common) TYPED_SFIELD(sfAmount, AMOUNT, 1) @@ -299,6 +331,8 @@ 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) +TYPED_SFIELD(sfCounterparty, ACCOUNT, 26) // vector of 256-bit TYPED_SFIELD(sfIndexes, VECTOR256, 1, SField::sMD_Never) @@ -359,6 +393,7 @@ UNTYPED_SFIELD(sfXChainClaimAttestationCollectionElement, OBJECT, 30) UNTYPED_SFIELD(sfXChainCreateAccountAttestationCollectionElement, OBJECT, 31) UNTYPED_SFIELD(sfPriceData, OBJECT, 32) UNTYPED_SFIELD(sfCredential, OBJECT, 33) +UNTYPED_SFIELD(sfCounterpartySignature, OBJECT, 34, SField::sMD_Default, SField::notSigning) // array of objects (common) // ARRAY/1 is reserved for end of array @@ -390,3 +425,5 @@ UNTYPED_SFIELD(sfAuthorizeCredentials, ARRAY, 26) UNTYPED_SFIELD(sfUnauthorizeCredentials, ARRAY, 27) UNTYPED_SFIELD(sfAcceptedCredentials, ARRAY, 28) UNTYPED_SFIELD(sfPermissions, ARRAY, 29) + +// clang-format on diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index b61bf3135f..5a07fcebbf 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -814,6 +814,125 @@ TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback, {sfAmount, soeOPTIONAL, soeMPTSupported}, })) +/** Reserve 71-73 for future Vault-related transactions */ + +/** This transaction creates and updates a Loan Broker */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttLOAN_BROKER_SET, 74, LoanBrokerSet, + Delegation::delegatable, + createPseudoAcct | mayAuthorizeMPT, ({ + {sfVaultID, soeREQUIRED}, + {sfLoanBrokerID, soeOPTIONAL}, + {sfData, soeOPTIONAL}, + {sfManagementFeeRate, soeOPTIONAL}, + {sfDebtMaximum, soeOPTIONAL}, + {sfCoverRateMinimum, soeOPTIONAL}, + {sfCoverRateLiquidation, soeOPTIONAL}, +})) + +/** This transaction deletes a Loan Broker */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttLOAN_BROKER_DELETE, 75, LoanBrokerDelete, + Delegation::delegatable, + mustDeleteAcct | mayAuthorizeMPT, ({ + {sfLoanBrokerID, soeREQUIRED}, +})) + +/** This transaction deposits First Loss Capital into a Loan Broker */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttLOAN_BROKER_COVER_DEPOSIT, 76, LoanBrokerCoverDeposit, + Delegation::delegatable, + noPriv, ({ + {sfLoanBrokerID, soeREQUIRED}, + {sfAmount, soeREQUIRED, soeMPTSupported}, +})) + +/** This transaction withdraws First Loss Capital from a Loan Broker */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttLOAN_BROKER_COVER_WITHDRAW, 77, LoanBrokerCoverWithdraw, + Delegation::delegatable, + noPriv, ({ + {sfLoanBrokerID, soeREQUIRED}, + {sfAmount, soeREQUIRED, soeMPTSupported}, +})) + +/** This transaction creates a Loan */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttLOAN_SET, 78, LoanSet, + Delegation::delegatable, + noPriv, ({ + {sfLoanBrokerID, soeREQUIRED}, + {sfData, soeOPTIONAL}, + {sfCounterparty, soeOPTIONAL}, + {sfCounterpartySignature, soeREQUIRED}, + {sfLoanOriginationFee, soeOPTIONAL}, + {sfLoanServiceFee, soeOPTIONAL}, + {sfLatePaymentFee, soeOPTIONAL}, + {sfClosePaymentFee, soeOPTIONAL}, + {sfOverpaymentFee, soeOPTIONAL}, + {sfInterestRate, soeOPTIONAL}, + {sfLateInterestRate, soeOPTIONAL}, + {sfCloseInterestRate, soeOPTIONAL}, + {sfOverpaymentInterestRate, soeOPTIONAL}, + {sfPrincipalRequested, soeREQUIRED}, + {sfStartDate, soeREQUIRED}, + {sfPaymentTotal, soeOPTIONAL}, + {sfPaymentInterval, soeOPTIONAL}, + {sfGracePeriod, soeOPTIONAL}, +})) + +/** This transaction deletes an existing Loan */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttLOAN_DELETE, 79, LoanDelete, + Delegation::delegatable, + noPriv, ({ + {sfLoanID, soeREQUIRED}, +})) + +/** This transaction is used to change the delinquency status of an existing Loan */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttLOAN_MANAGE, 80, LoanManage, + Delegation::delegatable, + noPriv, ({ + {sfLoanID, soeREQUIRED}, +})) + +/** The Borrower uses this transaction to draws funds from the Loan. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttLOAN_DRAW, 81, LoanDraw, + Delegation::delegatable, + noPriv, ({ + {sfLoanID, soeREQUIRED}, + {sfAmount, soeREQUIRED, soeMPTSupported}, +})) + +/** The Borrower uses this transaction to make a Payment on the Loan. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttLOAN_PAY, 82, LoanPay, + Delegation::delegatable, + noPriv, ({ + {sfLoanID, soeREQUIRED}, + {sfAmount, soeREQUIRED, soeMPTSupported}, +})) + /** This system-generated transaction type is used to update the status of the various amendments. For details, see: https://xrpl.org/amendments.html diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index f43288b57b..26bdf68918 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -20,6 +20,8 @@ #include #include +#include + #include #include #include diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index 2426092d13..73ef3e8a3e 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -96,6 +96,8 @@ enum class LedgerNameSpace : std::uint16_t { PERMISSIONED_DOMAIN = 'm', DELEGATE = 'E', 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. @@ -559,6 +561,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(uint256 const& loanBrokerID, std::uint32_t loanSeq) noexcept +{ + return loan(indexHash(LedgerNameSpace::LOAN, loanBrokerID, loanSeq)); +} + Keylet permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept { diff --git a/src/libxrpl/protocol/InnerObjectFormats.cpp b/src/libxrpl/protocol/InnerObjectFormats.cpp index ecfca9743d..fdbc9d7772 100644 --- a/src/libxrpl/protocol/InnerObjectFormats.cpp +++ b/src/libxrpl/protocol/InnerObjectFormats.cpp @@ -158,6 +158,14 @@ InnerObjectFormats::InnerObjectFormats() add(sfPermission.jsonName.c_str(), sfPermission.getCode(), {{sfPermissionValue, soeREQUIRED}}); + + add(sfCounterpartySignature.jsonName, + sfCounterpartySignature.getCode(), + { + {sfSigningPubKey, soeOPTIONAL}, + {sfTxnSignature, soeOPTIONAL}, + {sfSigners, soeOPTIONAL}, + }); } InnerObjectFormats const& diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 02de5d4c58..8ba4c5f297 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -1353,6 +1353,30 @@ canonicalizeRoundStrict( } } +STAmount +roundToReference( + STAmount const value, + STAmount referenceValue, + Number::rounding_mode rounding) +{ + if (value.asset().native() || !value.asset().holds()) + return value; + + NumberRoundModeGuard mg(rounding); + if (referenceValue.negative() != value.negative()) + referenceValue.negate(); + + if (value.exponent() > referenceValue.exponent() && + (value.exponent() == referenceValue.exponent() && + value.mantissa() >= referenceValue.mantissa())) + return value; + // With an IOU, the total will be truncated to the precision of the + // larger value: referenceValue + STAmount const total = referenceValue + value; + STAmount const result = total - referenceValue; + return result; +} + namespace { // We need a class that has an interface similar to NumberRoundModeGuard diff --git a/src/libxrpl/protocol/STObject.cpp b/src/libxrpl/protocol/STObject.cpp index 9c23898a74..f7e9af7417 100644 --- a/src/libxrpl/protocol/STObject.cpp +++ b/src/libxrpl/protocol/STObject.cpp @@ -682,6 +682,16 @@ STObject::getFieldV256(SField const& field) const return getFieldByConstRef(field, empty); } +STObject +STObject::getFieldObject(SField const& field) const +{ + STObject const empty{field}; + auto ret = getFieldByConstRef(field, empty); + if (ret != empty) + ret.applyTemplateFromSField(field); + return ret; +} + STArray const& STObject::getFieldArray(SField const& field) const { diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index 7b6b4c1ee2..7b00a05790 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -197,11 +197,11 @@ STTx::getSigningHash() const } Blob -STTx::getSignature() const +STTx::getSignature(STObject const& sigObject) const { try { - return getFieldVL(sfTxnSignature); + return sigObject.getFieldVL(sfTxnSignature); } catch (std::exception const&) { @@ -244,17 +244,20 @@ STTx::sign(PublicKey const& publicKey, SecretKey const& secretKey) Expected STTx::checkSign( RequireFullyCanonicalSig requireCanonicalSig, - Rules const& rules) const + Rules const& rules, + STObject const* pSig) const { try { // Determine whether we're single- or multi-signing by looking // at the SigningPubKey. If it's empty we must be // multi-signing. Otherwise we're single-signing. - Blob const& signingPubKey = getFieldVL(sfSigningPubKey); + STObject const& sigObject{pSig ? *pSig : *this}; + + Blob const& signingPubKey = sigObject.getFieldVL(sfSigningPubKey); return signingPubKey.empty() - ? checkMultiSign(requireCanonicalSig, rules) - : checkSingleSign(requireCanonicalSig); + ? checkMultiSign(requireCanonicalSig, rules, pSig) + : checkSingleSign(requireCanonicalSig, pSig); } catch (std::exception const&) { @@ -262,6 +265,24 @@ STTx::checkSign( return Unexpected("Internal signature check failure."); } +Expected +STTx::checkSign( + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules) const +{ + if (auto const ret = checkSign(requireCanonicalSig, rules, nullptr); !ret) + return ret; + + if (isFieldPresent(sfCounterpartySignature)) + { + auto const counterSig = getFieldObject(sfCounterpartySignature); + if (auto const ret = checkSign(requireCanonicalSig, rules, &counterSig); + !ret) + return Unexpected("Counterparty: " + ret.error()); + } + return {}; +} + Json::Value STTx::getJson(JsonOptions options) const { @@ -342,12 +363,16 @@ STTx::getMetaSQL( } Expected -STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const +STTx::checkSingleSign( + RequireFullyCanonicalSig requireCanonicalSig, + STObject const* pSig) const { + STObject const& sigObject{pSig ? *pSig : *this}; + // We don't allow both a non-empty sfSigningPubKey and an sfSigners. // That would allow the transaction to be signed two ways. So if both // fields are present the signature is invalid. - if (isFieldPresent(sfSigners)) + if (sigObject.isFieldPresent(sfSigners)) return Unexpected("Cannot both single- and multi-sign."); bool validSig = false; @@ -356,11 +381,11 @@ STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || (requireCanonicalSig == RequireFullyCanonicalSig::yes); - auto const spk = getFieldVL(sfSigningPubKey); + auto const spk = sigObject.getFieldVL(sfSigningPubKey); if (publicKeyType(makeSlice(spk))) { - Blob const signature = getFieldVL(sfTxnSignature); + Blob const signature = sigObject.getFieldVL(sfTxnSignature); Blob const data = getSigningData(*this); validSig = verify( @@ -384,19 +409,22 @@ STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const Expected STTx::checkMultiSign( RequireFullyCanonicalSig requireCanonicalSig, - Rules const& rules) const + Rules const& rules, + STObject const* pSig) const { + STObject const& sigObject{pSig ? *pSig : *this}; + // Make sure the MultiSigners are present. Otherwise they are not // attempting multi-signing and we just have a bad SigningPubKey. - if (!isFieldPresent(sfSigners)) + if (!sigObject.isFieldPresent(sfSigners)) return Unexpected("Empty SigningPubKey."); // We don't allow both an sfSigners and an sfTxnSignature. Both fields // being present would indicate that the transaction is signed both ways. - if (isFieldPresent(sfTxnSignature)) + if (sigObject.isFieldPresent(sfTxnSignature)) return Unexpected("Cannot both single- and multi-sign."); - STArray const& signers{getFieldArray(sfSigners)}; + STArray const& signers{sigObject.getFieldArray(sfSigners)}; // There are well known bounds that the number of signers must be within. if (signers.size() < minMultiSigners || @@ -422,8 +450,8 @@ STTx::checkMultiSign( { auto const accountID = signer.getAccountID(sfAccount); - // The account owner may not multisign for themselves. - if (accountID == txnAccountID) + // The account owner may not usually multisign for themselves. + if (!pSig && accountID == txnAccountID) return Unexpected("Invalid multisigner."); // No duplicate signers allowed. diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp new file mode 100644 index 0000000000..941d33103e --- /dev/null +++ b/src/test/app/LoanBroker_test.cpp @@ -0,0 +1,594 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace test { + +class LoanBroker_test : public beast::unit_test::suite +{ + // Ensure that all the features needed for Lending Protocol are included, + // even if they are set to unsupported. + FeatureBitset const all{ + jtx::supported_amendments() | featureMPTokensV1 | + featureSingleAssetVault | featureLendingProtocol}; + + void + testDisabled() + { + testcase("Disabled"); + // Lending Protocol depends on Single Asset Vault (SAV). Test + // combinations of the two amendments. + // Single Asset Vault depends on MPTokensV1, but don't test every combo + // of that. + using namespace jtx; + auto failAll = [this](FeatureBitset features, bool goodVault = false) { + Env env(*this, features); + + Account const alice{"alice"}; + env.fund(XRP(10000), alice); + + // Try to create a vault + PrettyAsset const asset{xrpIssue(), 1'000'000}; + Vault vault{env}; + auto const [tx, keylet] = + vault.create({.owner = alice, .asset = asset}); + env(tx, ter(goodVault ? ter(tesSUCCESS) : ter(temDISABLED))); + env.close(); + BEAST_EXPECT(static_cast(env.le(keylet)) == goodVault); + + using namespace loanBroker; + // Can't create a loan broker regardless of whether the vault exists + env(set(alice, keylet.key), ter(temDISABLED)); + auto const brokerKeylet = + keylet::loanbroker(alice.id(), env.seq(alice)); + // Other LoanBroker transactions are disabled, too. + // 1. LoanBrokerCoverDeposit + env(coverDeposit(alice, brokerKeylet.key, asset(1000)), + ter(temDISABLED)); + // 2. LoanBrokerCoverWithdraw + env(coverWithdraw(alice, brokerKeylet.key, asset(1000)), + ter(temDISABLED)); + // 3. LoanBrokerDelete + env(del(alice, brokerKeylet.key), ter(temDISABLED)); + }; + failAll(all - featureMPTokensV1); + failAll(all - featureSingleAssetVault - featureLendingProtocol); + failAll(all - featureSingleAssetVault); + failAll(all - featureLendingProtocol, true); + } + + struct VaultInfo + { + jtx::PrettyAsset asset; + uint256 vaultID; + VaultInfo(jtx::PrettyAsset const& asset_, uint256 const& vaultID_) + : asset(asset_), vaultID(vaultID_) + { + } + }; + + void + lifecycle( + char const* label, + jtx::Env& env, + jtx::Account const& alice, + jtx::Account const& evan, + VaultInfo const& vault, + std::function modifyJTx, + std::function checkBroker, + std::function changeBroker, + std::function checkChangedBroker) + { + auto const keylet = keylet::loanbroker(alice.id(), env.seq(alice)); + { + auto const& asset = vault.asset.raw(); + testcase << "Lifecycle: " + << (asset.native() ? "XRP " + : asset.holds() ? "IOU " + : asset.holds() ? "MPT " + : "Unknown ") + << label; + } + + using namespace jtx; + using namespace loanBroker; + + { + // Start with default values + auto jtx = env.jt(set(alice, vault.vaultID)); + // Modify as desired + if (modifyJTx) + jtx = modifyJTx(jtx); + // Successfully create a Loan Broker + env(jtx); + } + + env.close(); + if (auto broker = env.le(keylet); BEAST_EXPECT(broker)) + { + // log << "Broker after create: " << to_string(broker->getJson()) + // << std::endl; + BEAST_EXPECT(broker->at(sfVaultID) == vault.vaultID); + BEAST_EXPECT(broker->at(sfAccount) != alice.id()); + BEAST_EXPECT(broker->at(sfOwner) == alice.id()); + BEAST_EXPECT(broker->at(sfFlags) == 0); + BEAST_EXPECT(broker->at(sfSequence) == env.seq(alice) - 1); + BEAST_EXPECT(broker->at(sfOwnerCount) == 0); + BEAST_EXPECT(broker->at(sfLoanSequence) == 1); + BEAST_EXPECT(broker->at(sfDebtTotal) == 0); + BEAST_EXPECT(broker->at(sfCoverAvailable) == 0); + if (checkBroker) + checkBroker(broker); + + // if (auto const vaultSLE = env.le(keylet::vault(vault.vaultID))) + //{ + // log << "Vault: " << to_string(vaultSLE->getJson()) << + // std::endl; + // } + // Load the pseudo-account + Account const pseudoAccount{ + "Broker pseudo-account", broker->at(sfAccount)}; + auto const pseudoKeylet = keylet::account(pseudoAccount); + if (auto const pseudo = env.le(pseudoKeylet); BEAST_EXPECT(pseudo)) + { + // log << "Pseudo-account after create: " + // << to_string(pseudo->getJson()) << std::endl + // << std::endl; + BEAST_EXPECT( + pseudo->at(sfFlags) == + (lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth)); + BEAST_EXPECT(pseudo->at(sfSequence) == 0); + BEAST_EXPECT(pseudo->at(sfBalance) == beast::zero); + BEAST_EXPECT( + pseudo->at(sfOwnerCount) == + (vault.asset.raw().native() ? 0 : 1)); + BEAST_EXPECT(!pseudo->isFieldPresent(sfAccountTxnID)); + BEAST_EXPECT(!pseudo->isFieldPresent(sfRegularKey)); + BEAST_EXPECT(!pseudo->isFieldPresent(sfEmailHash)); + BEAST_EXPECT(!pseudo->isFieldPresent(sfWalletLocator)); + BEAST_EXPECT(!pseudo->isFieldPresent(sfWalletSize)); + BEAST_EXPECT(!pseudo->isFieldPresent(sfMessageKey)); + BEAST_EXPECT(!pseudo->isFieldPresent(sfTransferRate)); + BEAST_EXPECT(!pseudo->isFieldPresent(sfDomain)); + BEAST_EXPECT(!pseudo->isFieldPresent(sfTickSize)); + BEAST_EXPECT(!pseudo->isFieldPresent(sfTicketCount)); + BEAST_EXPECT(!pseudo->isFieldPresent(sfNFTokenMinter)); + BEAST_EXPECT(!pseudo->isFieldPresent(sfMintedNFTokens)); + BEAST_EXPECT(!pseudo->isFieldPresent(sfBurnedNFTokens)); + BEAST_EXPECT(!pseudo->isFieldPresent(sfFirstNFTokenSequence)); + BEAST_EXPECT(!pseudo->isFieldPresent(sfAMMID)); + BEAST_EXPECT(!pseudo->isFieldPresent(sfVaultID)); + BEAST_EXPECT(pseudo->at(sfLoanBrokerID) == keylet.key); + } + + auto verifyCoverAmount = + [&env, &vault, &broker, &pseudoAccount, this](auto n) { + auto const amount = vault.asset(n); + BEAST_EXPECT( + broker->at(sfCoverAvailable) == amount.number()); + env.require(balance(pseudoAccount, amount)); + }; + + // Test Cover funding before allowing alterations + env(coverDeposit(alice, uint256(0), vault.asset(10)), + ter(temINVALID)); + env(coverDeposit(evan, keylet.key, vault.asset(10)), + ter(tecNO_PERMISSION)); + env(coverDeposit(evan, keylet.key, vault.asset(0)), + ter(temBAD_AMOUNT)); + env(coverDeposit(evan, keylet.key, vault.asset(-10)), + ter(temBAD_AMOUNT)); + env(coverDeposit(alice, vault.vaultID, vault.asset(10)), + ter(tecNO_ENTRY)); + + verifyCoverAmount(0); + + // Fund the cover deposit + env(coverDeposit(alice, keylet.key, vault.asset(10))); + if (BEAST_EXPECT(broker = env.le(keylet))) + { + verifyCoverAmount(10); + } + + // Test withdrawal failure cases + env(coverWithdraw(alice, uint256(0), vault.asset(10)), + ter(temINVALID)); + env(coverWithdraw(evan, keylet.key, vault.asset(10)), + ter(tecNO_PERMISSION)); + env(coverWithdraw(evan, keylet.key, vault.asset(0)), + ter(temBAD_AMOUNT)); + env(coverWithdraw(evan, keylet.key, vault.asset(-10)), + ter(temBAD_AMOUNT)); + env(coverWithdraw(alice, vault.vaultID, vault.asset(10)), + ter(tecNO_ENTRY)); + env(coverWithdraw(alice, keylet.key, vault.asset(900)), + ter(tecINSUFFICIENT_FUNDS)); + + // Withdraw some of the cover amount + env(coverWithdraw(alice, keylet.key, vault.asset(7))); + if (BEAST_EXPECT(broker = env.le(keylet))) + { + verifyCoverAmount(3); + } + + // Add some more cover + env(coverDeposit(alice, keylet.key, vault.asset(5))); + if (BEAST_EXPECT(broker = env.le(keylet))) + { + verifyCoverAmount(8); + } + + // Withdraw some more + env(coverWithdraw(alice, keylet.key, vault.asset(2))); + if (BEAST_EXPECT(broker = env.le(keylet))) + { + verifyCoverAmount(6); + } + + env.close(); + + // no-op + env(set(alice, vault.vaultID), loanBrokerID(keylet.key)); + + // Make modifications to the broker + if (changeBroker) + changeBroker(broker); + + env.close(); + + // Check the results of modifications + if (BEAST_EXPECT(broker = env.le(keylet)) && checkChangedBroker) + checkChangedBroker(broker); + + // Verify that fields get removed when set to default values + // Debt maximum: explicit 0 + // Data: explicit empty + env(set(alice, vault.vaultID), + loanBrokerID(broker->key()), + debtMaximum(Number(0)), + data("")); + + // Check the updated fields + if (BEAST_EXPECT(broker = env.le(keylet))) + { + BEAST_EXPECT(!broker->isFieldPresent(sfDebtMaximum)); + BEAST_EXPECT(!broker->isFieldPresent(sfData)); + } + + ///////////////////////////////////// + // try to delete the wrong broker object + env(del(alice, vault.vaultID), ter(tecNO_ENTRY)); + // evan tries to delete the broker + env(del(evan, keylet.key), ter(tecNO_PERMISSION)); + + // Note alice's balance of the asset and the broker account's cover + // funds + auto const aliceBalance = env.balance(alice, vault.asset); + auto const coverFunds = env.balance(pseudoAccount, vault.asset); + BEAST_EXPECT(coverFunds.number() == broker->at(sfCoverAvailable)); + BEAST_EXPECT(coverFunds != beast::zero); + verifyCoverAmount(6); + + // delete the broker + // log << "Broker before delete: " << to_string(broker->getJson()) + // << std::endl; + // if (auto const pseudo = env.le(pseudoKeylet); + // BEAST_EXPECT(pseudo)) + //{ + // log << "Pseudo-account before delete: " + // << to_string(pseudo->getJson()) << std::endl + // << std::endl; + //} + + env(del(alice, keylet.key)); + env.close(); + { + broker = env.le(keylet); + BEAST_EXPECT(!broker); + auto pseudo = env.le(pseudoKeylet); + BEAST_EXPECT(!pseudo); + } + auto const expectedBalance = aliceBalance + coverFunds - + (aliceBalance.value().native() + ? STAmount(env.current()->fees().base.value()) + : vault.asset(0)); + env.require(balance(alice, expectedBalance)); + env.require(balance(pseudoAccount, vault.asset(none))); + } + } + + void + testLifecycle() + { + testcase("Lifecycle"); + using namespace jtx; + + // Create 3 loan brokers: one for XRP, one for an IOU, and one for an + // MPT. That'll require three corresponding SAVs. + Env env(*this, all); + + Account issuer{"issuer"}; + // For simplicity, alice will be the sole actor for the vault & brokers. + Account alice{"alice"}; + // Evan will attempt to be naughty + Account evan{"evan"}; + Vault vault{env}; + + // Fund the accounts and trust lines with the same amount so that tests + // can use the same values regardless of the asset. + env.fund(XRP(100'000), issuer, noripple(alice, evan)); + env.close(); + + // Create assets + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + PrettyAsset const iouAsset = issuer["IOU"]; + env(trust(alice, iouAsset(1'000'000))); + env(trust(evan, iouAsset(1'000'000))); + env(pay(issuer, evan, iouAsset(100'000))); + env(pay(issuer, alice, iouAsset(100'000))); + env.close(); + + MPTTester mptt{env, issuer, mptInitNoFund}; + mptt.create( + {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); + PrettyAsset const mptAsset = mptt["MPT"]; + mptt.authorize({.account = alice}); + mptt.authorize({.account = evan}); + env(pay(issuer, alice, mptAsset(100'000))); + env(pay(issuer, evan, mptAsset(100'000))); + env.close(); + + std::array const assets{xrpAsset, iouAsset, mptAsset}; + + // Create vaults + std::vector vaults; + for (auto const& asset : assets) + { + auto [tx, keylet] = vault.create({.owner = alice, .asset = asset}); + env(tx); + env.close(); + BEAST_EXPECT(env.le(keylet)); + + vaults.emplace_back(asset, keylet.key); + + env(vault.deposit( + {.depositor = alice, .id = keylet.key, .amount = asset(50)})); + env.close(); + } + + auto const aliceOriginalCount = env.ownerCount(alice); + + // Create and update Loan Brokers + for (auto const& vault : vaults) + { + using namespace loanBroker; + + auto badKeylet = keylet::vault(alice.id(), env.seq(alice)); + // Try some failure cases + // not the vault owner + env(set(evan, vault.vaultID), ter(tecNO_PERMISSION)); + // not a vault + env(set(alice, badKeylet.key), ter(tecNO_ENTRY)); + // flags are checked first + env(set(evan, vault.vaultID, ~tfUniversal), ter(temINVALID_FLAG)); + // field length validation + // sfData: good length, bad account + env(set(evan, vault.vaultID), + data(std::string(maxDataPayloadLength, 'X')), + ter(tecNO_PERMISSION)); + // sfData: too long + env(set(evan, vault.vaultID), + data(std::string(maxDataPayloadLength + 1, 'Y')), + ter(temINVALID)); + // sfManagementFeeRate: good value, bad account + env(set(evan, vault.vaultID), + managementFeeRate(maxManagementFeeRate), + ter(tecNO_PERMISSION)); + // sfManagementFeeRate: too big + env(set(evan, vault.vaultID), + managementFeeRate(maxManagementFeeRate + TenthBips16(10)), + ter(temINVALID)); + // sfCoverRateMinimum: good value, bad account + env(set(evan, vault.vaultID), + coverRateMinimum(maxCoverRate), + ter(tecNO_PERMISSION)); + // sfCoverRateMinimum: too big + env(set(evan, vault.vaultID), + coverRateMinimum(maxCoverRate + 1), + ter(temINVALID)); + // sfCoverRateLiquidation: good value, bad account + env(set(evan, vault.vaultID), + coverRateLiquidation(maxCoverRate), + ter(tecNO_PERMISSION)); + // sfCoverRateLiquidation: too big + env(set(evan, vault.vaultID), + coverRateLiquidation(maxCoverRate + 1), + ter(temINVALID)); + // sfDebtMaximum: good value, bad account + env(set(evan, vault.vaultID), + debtMaximum(Number(0)), + ter(tecNO_PERMISSION)); + // sfDebtMaximum: overflow + env(set(evan, vault.vaultID), + debtMaximum(Number(1, 100)), + ter(temINVALID)); + // sfDebtMaximum: negative + env(set(evan, vault.vaultID), + debtMaximum(Number(-1)), + ter(temINVALID)); + + std::string testData; + lifecycle( + "default fields", + env, + alice, + evan, + vault, + // No modifications + {}, + [&](SLE::const_ref broker) { + // Extra checks + BEAST_EXPECT(!broker->isFieldPresent(sfManagementFeeRate)); + BEAST_EXPECT(!broker->isFieldPresent(sfCoverRateMinimum)); + BEAST_EXPECT( + !broker->isFieldPresent(sfCoverRateLiquidation)); + BEAST_EXPECT(!broker->isFieldPresent(sfData)); + BEAST_EXPECT(!broker->isFieldPresent(sfDebtMaximum)); + BEAST_EXPECT(broker->at(sfDebtMaximum) == 0); + BEAST_EXPECT(broker->at(sfCoverRateMinimum) == 0); + BEAST_EXPECT(broker->at(sfCoverRateLiquidation) == 0); + + BEAST_EXPECT( + env.ownerCount(alice) == aliceOriginalCount + 2); + }, + [&](SLE::const_ref broker) { + // Modifications + + // Update the fields + auto const nextKeylet = + keylet::loanbroker(alice.id(), env.seq(alice)); + + // fields that can't be changed + // LoanBrokerID + env(set(alice, vault.vaultID), + loanBrokerID(nextKeylet.key), + ter(tecNO_ENTRY)); + // VaultID + env(set(alice, nextKeylet.key), + loanBrokerID(broker->key()), + ter(tecNO_PERMISSION)); + // Owner + env(set(evan, vault.vaultID), + loanBrokerID(broker->key()), + ter(tecNO_PERMISSION)); + // ManagementFeeRate + env(set(alice, vault.vaultID), + loanBrokerID(broker->key()), + managementFeeRate(maxManagementFeeRate), + ter(temINVALID)); + // CoverRateMinimum + env(set(alice, vault.vaultID), + loanBrokerID(broker->key()), + coverRateMinimum(maxManagementFeeRate), + ter(temINVALID)); + // CoverRateLiquidation + env(set(alice, vault.vaultID), + loanBrokerID(broker->key()), + coverRateLiquidation(maxManagementFeeRate), + ter(temINVALID)); + + // fields that can be changed + testData = "Test Data 1234"; + // Bad data: too long + env(set(alice, vault.vaultID), + loanBrokerID(broker->key()), + data(std::string(maxDataPayloadLength + 1, 'W')), + ter(temINVALID)); + + // Bad debt maximum + env(set(alice, vault.vaultID), + loanBrokerID(broker->key()), + debtMaximum(Number(-175, -1)), + ter(temINVALID)); + // Data & Debt maximum + env(set(alice, vault.vaultID), + loanBrokerID(broker->key()), + data(testData), + debtMaximum(Number(175, -1))); + }, + [&](SLE::const_ref broker) { + // Check the updated fields + BEAST_EXPECT(checkVL(broker->at(sfData), testData)); + BEAST_EXPECT(broker->at(sfDebtMaximum) == Number(175, -1)); + }); + + lifecycle( + "non-default fields", + env, + alice, + evan, + vault, + [&](jtx::JTx const& jv) { + testData = "spam spam spam spam"; + // Finally, create another Loan Broker with none of the + // values at default + return env.jt( + jv, + data(testData), + managementFeeRate(TenthBips16(123)), + debtMaximum(Number(9)), + coverRateMinimum(TenthBips32(100)), + coverRateLiquidation(TenthBips32(200))); + }, + [&](SLE::const_ref broker) { + // Extra checks + BEAST_EXPECT(broker->at(sfManagementFeeRate) == 123); + BEAST_EXPECT(broker->at(sfCoverRateMinimum) == 100); + BEAST_EXPECT(broker->at(sfCoverRateLiquidation) == 200); + BEAST_EXPECT(broker->at(sfDebtMaximum) == Number(9)); + BEAST_EXPECT(checkVL(broker->at(sfData), testData)); + }, + [&](SLE::const_ref broker) { + // Reset Data & Debt maximum to default values + env(set(alice, vault.vaultID), + loanBrokerID(broker->key()), + data(""), + debtMaximum(Number(0))); + }, + [&](SLE::const_ref broker) { + // Check the updated fields + BEAST_EXPECT(!broker->isFieldPresent(sfData)); + BEAST_EXPECT(!broker->isFieldPresent(sfDebtMaximum)); + }); + } + + BEAST_EXPECT(env.ownerCount(alice) == aliceOriginalCount); + } + +public: + void + run() override + { + testDisabled(); + testLifecycle(); + } +}; + +BEAST_DEFINE_TESTSUITE(LoanBroker, tx, ripple); + +} // namespace test +} // namespace ripple diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp new file mode 100644 index 0000000000..86088f92db --- /dev/null +++ b/src/test/app/Loan_test.cpp @@ -0,0 +1,1702 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace test { + +class Loan_test : public beast::unit_test::suite +{ + // Ensure that all the features needed for Lending Protocol are included, + // even if they are set to unsupported. + FeatureBitset const all{ + jtx::supported_amendments() | featureMPTokensV1 | + featureSingleAssetVault | featureLendingProtocol}; + + void + testDisabled() + { + testcase("Disabled"); + // Lending Protocol depends on Single Asset Vault (SAV). Test + // combinations of the two amendments. + // Single Asset Vault depends on MPTokensV1, but don't test every combo + // of that. + using namespace jtx; + auto failAll = [this](FeatureBitset features, bool goodVault = false) { + Env env(*this, features); + + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10000), alice, bob); + + auto const keylet = keylet::loanbroker(alice, env.seq(alice)); + + using namespace std::chrono_literals; + using namespace loan; + + // counter party signature is required on LoanSet + auto setTx = env.jt( + set(alice, keylet.key, Number(10000), env.now() + 720h), + ter(temMALFORMED)); + env(setTx); + + // All loan transactions are disabled. + // 1. LoanSet + setTx = env.jt( + setTx, sig(sfCounterpartySignature, bob), ter(temDISABLED)); + env(setTx); + // Actual sequence will be based off the loan broker, but we + // obviously don't have one of those if the amendment is disabled + auto const loanKeylet = keylet::loan(keylet.key, env.seq(alice)); + // Other Loan transactions are disabled, too. + // 2. LoanDelete + env(del(alice, loanKeylet.key), ter(temDISABLED)); + // 3. LoanManage + env(manage(alice, loanKeylet.key, tfLoanImpair), ter(temDISABLED)); + // 4. LoanDraw + env(draw(alice, loanKeylet.key, XRP(500)), ter(temDISABLED)); + // 5. LoanPay + env(pay(alice, loanKeylet.key, XRP(500)), ter(temDISABLED)); + }; + failAll(all - featureMPTokensV1); + failAll(all - featureSingleAssetVault - featureLendingProtocol); + failAll(all - featureSingleAssetVault); + failAll(all - featureLendingProtocol, true); + } + + struct BrokerInfo + { + jtx::PrettyAsset asset; + uint256 brokerID; + BrokerInfo(jtx::PrettyAsset const& asset_, uint256 const& brokerID_) + : asset(asset_), brokerID(brokerID_) + { + } + }; + + struct LoanState + { + std::uint32_t previousPaymentDate = 0; + NetClock::time_point startDate = {}; + std::uint32_t nextPaymentDate = 0; + std::uint32_t paymentRemaining = 0; + Number assetsAvailable = 0; + Number const principalRequested; + Number principalOutstanding = 0; + std::uint32_t flags = 0; + std::uint32_t paymentInterval = 0; + }; + + struct VerifyLoanStatus + { + public: + jtx::Env const& env; + BrokerInfo const& broker; + jtx::Account const& pseudoAccount; + Keylet const& keylet; + + VerifyLoanStatus( + jtx::Env const& env_, + BrokerInfo const& broker_, + jtx::Account const& pseudo_, + Keylet const& keylet_) + : env(env_) + , broker(broker_) + , pseudoAccount(pseudo_) + , keylet(keylet_) + { + } + + void + checkBroker( + Number const& assetsAvailable, + Number const& principalRequested, + Number const& principalOutstanding, + TenthBips32 interestRate, + std::uint32_t paymentInterval, + std::uint32_t paymentsRemaining, + std::uint32_t ownerCount) const + { + using namespace jtx; + if (auto brokerSle = env.le(keylet::loanbroker(broker.brokerID)); + env.test.BEAST_EXPECT(brokerSle)) + { + TenthBips16 const managementFeeRate{ + brokerSle->at(sfManagementFeeRate)}; + auto const loanInterest = loanInterestOutstandingMinusFee( + broker.asset, + principalRequested, + principalOutstanding, + interestRate, + paymentInterval, + paymentsRemaining, + managementFeeRate); + auto const brokerDebt = brokerSle->at(sfDebtTotal); + auto const expectedDebt = principalOutstanding + loanInterest; + env.test.BEAST_EXPECT( + // Allow some slop for rounding + brokerDebt == expectedDebt || + (expectedDebt != Number(0) && + ((brokerDebt - expectedDebt) / expectedDebt < + Number(1, -8)))); + env.test.BEAST_EXPECT( + env.balance(pseudoAccount, broker.asset).number() == + brokerSle->at(sfCoverAvailable) + assetsAvailable); + env.test.BEAST_EXPECT( + brokerSle->at(sfOwnerCount) == ownerCount); + + if (auto vaultSle = + env.le(keylet::vault(brokerSle->at(sfVaultID))); + env.test.BEAST_EXPECT(vaultSle)) + { + Account const vaultPseudo{ + "vaultPseudoAccount", vaultSle->at(sfAccount)}; + env.test.BEAST_EXPECT( + vaultSle->at(sfAssetsAvailable) == + env.balance(vaultPseudo, broker.asset).number()); + if (ownerCount == 0) + { + // Allow some slop for rounding IOUs + auto const total = vaultSle->at(sfAssetsTotal); + auto const available = vaultSle->at(sfAssetsAvailable); + env.test.BEAST_EXPECT( + total == available || + (!broker.asset.raw().native() && + broker.asset.raw().holds() && + available != 0 && + ((total - available) / available < + Number(1, -6)))); + env.test.BEAST_EXPECT( + vaultSle->at(sfLossUnrealized) == 0); + } + } + } + } + + void + checkBroker( + LoanState const& state, + TenthBips32 interestRate, + std::uint32_t ownerCount) const + { + checkBroker( + state.assetsAvailable, + state.principalRequested, + state.principalOutstanding, + interestRate, + state.paymentInterval, + state.paymentRemaining, + ownerCount); + } + + void + operator()( + std::uint32_t previousPaymentDate, + std::uint32_t nextPaymentDate, + std::uint32_t paymentRemaining, + Number const& assetsAvailable, + Number const& principalRequested, + Number const& principalOutstanding, + std::uint32_t flags) const + { + using namespace jtx; + if (auto loan = env.le(keylet); env.test.BEAST_EXPECT(loan)) + { + env.test.BEAST_EXPECT( + loan->at(sfPreviousPaymentDate) == previousPaymentDate); + env.test.BEAST_EXPECT( + loan->at(sfNextPaymentDueDate) == nextPaymentDate); + env.test.BEAST_EXPECT( + loan->at(sfPaymentRemaining) == paymentRemaining); + env.test.BEAST_EXPECT( + loan->at(sfAssetsAvailable) == assetsAvailable); + env.test.BEAST_EXPECT( + loan->at(sfPrincipalRequested) == principalRequested); + env.test.BEAST_EXPECT( + loan->at(sfPrincipalOutstanding) == principalOutstanding); + env.test.BEAST_EXPECT( + loan->at(sfPrincipalRequested) == + broker.asset(1000).value()); + env.test.BEAST_EXPECT(loan->at(sfFlags) == flags); + + auto const interestRate = TenthBips32{loan->at(sfInterestRate)}; + auto const paymentInterval = loan->at(sfPaymentInterval); + checkBroker( + assetsAvailable, + principalRequested, + principalOutstanding, + interestRate, + paymentInterval, + paymentRemaining, + 1); + + if (auto brokerSle = + env.le(keylet::loanbroker(broker.brokerID)); + env.test.BEAST_EXPECT(brokerSle)) + { + if (auto vaultSle = + env.le(keylet::vault(brokerSle->at(sfVaultID))); + env.test.BEAST_EXPECT(vaultSle)) + { + if ((flags & lsfLoanImpaired) && + !(flags & lsfLoanDefault)) + { + TenthBips32 const managementFeeRate{ + brokerSle->at(sfManagementFeeRate)}; + env.test.BEAST_EXPECT( + vaultSle->at(sfLossUnrealized) == + principalOutstanding + + loanInterestOutstandingMinusFee( + broker.asset, + principalRequested, + principalOutstanding, + interestRate, + paymentInterval, + paymentRemaining, + managementFeeRate)); + } + else + { + env.test.BEAST_EXPECT( + vaultSle->at(sfLossUnrealized) == 0); + } + } + } + } + } + + void + operator()(LoanState const& state) const + { + operator()( + state.previousPaymentDate, + state.nextPaymentDate, + state.paymentRemaining, + state.assetsAvailable, + state.principalRequested, + state.principalOutstanding, + state.flags); + }; + }; + + void + lifecycle( + char const* label, + jtx::Env& env, + jtx::Account const& lender, + jtx::Account const& borrower, + jtx::Account const& evan, + BrokerInfo const& broker, + jtx::Account const& pseudoAcct, + std::uint32_t flags, + // The end of life callback is expected to take the loan to 0 payments + // remaining, one way or another + std::function toEndOfLife) + { + auto const [keylet, loanSequence] = [&]() { + auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); + if (!BEAST_EXPECT(brokerSle)) + // will be invalid + return std::make_pair( + keylet::loan(broker.brokerID), std::uint32_t(0)); + + // Broker has no loans + BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); + + // The loan keylet is based on the LoanSequence of the _LOAN_BROKER_ + // object. + auto const loanSequence = brokerSle->at(sfLoanSequence); + return std::make_pair( + keylet::loan(broker.brokerID, loanSequence), loanSequence); + }(); + + VerifyLoanStatus const verifyLoanStatus( + env, broker, pseudoAcct, keylet); + + // No loans yet + verifyLoanStatus.checkBroker( + 0, broker.asset(1000).value(), 0, TenthBips32{0}, 1, 0, 0); + + if (!BEAST_EXPECT(loanSequence != 0)) + return; + { + auto const& asset = broker.asset.raw(); + testcase << "Lifecycle: " + << (asset.native() ? "XRP " + : asset.holds() ? "IOU " + : asset.holds() ? "MPT " + : "Unknown ") + << label; + } + + using namespace jtx; + using namespace loan; + using namespace std::chrono_literals; + + auto const borrowerOwnerCount = env.ownerCount(borrower); + + auto const loanSetFee = fee(env.current()->fees().base * 2); + Number const principalRequest = broker.asset(1000).value(); + auto const startDate = env.now() + 3600s; + auto const originationFee = broker.asset(1).value(); + auto const serviceFee = broker.asset(2).value(); + auto const lateFee = broker.asset(3).value(); + auto const closeFee = broker.asset(4).value(); + auto const overFee = percentageToTenthBips(5) / 10; + auto const interest = percentageToTenthBips(12); + // 2.4% + auto const lateInterest = percentageToTenthBips(24) / 10; + auto const closeInterest = percentageToTenthBips(36) / 10; + auto const overpaymentInterest = percentageToTenthBips(48) / 10; + auto const total = 12; + auto const interval = 600; + auto const grace = 60; + + // Use the defined values + auto createJtx = env.jt( + set(borrower, broker.brokerID, principalRequest, startDate, flags), + sig(sfCounterpartySignature, lender), + loanOriginationFee(originationFee), + loanServiceFee(serviceFee), + latePaymentFee(lateFee), + closePaymentFee(closeFee), + overpaymentFee(overFee), + interestRate(interest), + lateInterestRate(lateInterest), + closeInterestRate(closeInterest), + overpaymentInterestRate(overpaymentInterest), + paymentTotal(total), + paymentInterval(interval), + gracePeriod(grace), + fee(loanSetFee)); + // Successfully create a Loan + env(createJtx); + + env.close(); + + if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); + BEAST_EXPECT(brokerSle)) + { + BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 1); + } + + auto const loanFlags = createJtx.stx->isFlag(tfLoanOverpayment) + ? lsfLoanOverpayment + : LedgerSpecificFlags(0); + + if (auto loan = env.le(keylet); BEAST_EXPECT(loan)) + { + // log << "loan after create: " << to_string(loan->getJson()) + // << std::endl; + BEAST_EXPECT( + loan->isFlag(lsfLoanOverpayment) == + createJtx.stx->isFlag(tfLoanOverpayment)); + BEAST_EXPECT(loan->at(sfLoanSequence) == loanSequence); + BEAST_EXPECT(loan->at(sfBorrower) == borrower.id()); + BEAST_EXPECT(loan->at(sfLoanBrokerID) == broker.brokerID); + BEAST_EXPECT(loan->at(sfLoanOriginationFee) == originationFee); + BEAST_EXPECT(loan->at(sfLoanServiceFee) == serviceFee); + BEAST_EXPECT(loan->at(sfLatePaymentFee) == lateFee); + BEAST_EXPECT(loan->at(sfClosePaymentFee) == closeFee); + BEAST_EXPECT(loan->at(sfOverpaymentFee) == overFee); + BEAST_EXPECT(loan->at(sfInterestRate) == interest); + BEAST_EXPECT(loan->at(sfLateInterestRate) == lateInterest); + BEAST_EXPECT(loan->at(sfCloseInterestRate) == closeInterest); + BEAST_EXPECT( + loan->at(sfOverpaymentInterestRate) == overpaymentInterest); + BEAST_EXPECT( + loan->at(sfStartDate) == startDate.time_since_epoch().count()); + BEAST_EXPECT(loan->at(sfPaymentInterval) == interval); + BEAST_EXPECT(loan->at(sfGracePeriod) == grace); + BEAST_EXPECT(loan->at(sfPreviousPaymentDate) == 0); + BEAST_EXPECT( + loan->at(sfNextPaymentDueDate) == + startDate.time_since_epoch().count() + interval); + BEAST_EXPECT(loan->at(sfPaymentRemaining) == total); + BEAST_EXPECT( + loan->at(sfAssetsAvailable) == + principalRequest - originationFee); + BEAST_EXPECT(loan->at(sfPrincipalRequested) == principalRequest); + BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == principalRequest); + } + + verifyLoanStatus( + 0, + startDate.time_since_epoch().count() + interval, + total, + principalRequest - originationFee, + principalRequest, + principalRequest, + loanFlags | 0); + + // Manage the loan + // no-op + env(manage(lender, keylet.key, 0)); + // Only the lender can manage + env(manage(evan, keylet.key, 0), ter(tecNO_PERMISSION)); + // unknown flags + env(manage(lender, keylet.key, tfLoanManageMask), ter(temINVALID_FLAG)); + // combinations of flags are not allowed + env(manage(lender, keylet.key, tfLoanUnimpair | tfLoanImpair), + ter(temINVALID_FLAG)); + env(manage(lender, keylet.key, tfLoanImpair | tfLoanDefault), + ter(temINVALID_FLAG)); + env(manage(lender, keylet.key, tfLoanUnimpair | tfLoanDefault), + ter(temINVALID_FLAG)); + env(manage( + lender, + keylet.key, + tfLoanUnimpair | tfLoanImpair | tfLoanDefault), + ter(temINVALID_FLAG)); + // invalid loan ID + env(manage(lender, broker.brokerID, tfLoanImpair), ter(tecNO_ENTRY)); + // Loan is unimpaired, can't unimpair it again + env(manage(lender, keylet.key, tfLoanUnimpair), ter(tecNO_PERMISSION)); + // Loan is unimpaired, it can go into default, but only after it's past + // due + env(manage(lender, keylet.key, tfLoanDefault), ter(tecTOO_SOON)); + + // Impair the loan + env(manage(lender, keylet.key, tfLoanImpair)); + // Unimpair the loan + env(manage(lender, keylet.key, tfLoanUnimpair)); + + auto const nextDueDate = + startDate.time_since_epoch().count() + interval; + + env.close(); + + verifyLoanStatus( + 0, + nextDueDate, + total, + principalRequest - originationFee, + principalRequest, + principalRequest, + loanFlags | 0); + + // Can't delete the loan yet. It has payments remaining. + env(del(lender, keylet.key), ter(tecHAS_OBLIGATIONS)); + + if (BEAST_EXPECT(toEndOfLife)) + toEndOfLife(keylet, verifyLoanStatus); + env.close(); + + // Verify the loan is at EOL + auto const assetsAvailable = [&, &keylet = keylet]() { + if (auto loan = env.le(keylet); BEAST_EXPECT(loan)) + { + BEAST_EXPECT(loan->at(sfPaymentRemaining) == 0); + BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == 0); + return loan->at(sfAssetsAvailable); + } + return Number(0); + }(); + auto const borrowerStartingBalance = + env.balance(borrower, broker.asset); + + // Try to delete the loan broker with an active loan + env(loanBroker::del(lender, broker.brokerID), ter(tecHAS_OBLIGATIONS)); + // Ensure the above tx doesn't get ordered after the LoanDelete and + // delete our broker! + env.close(); + + // Test failure cases + env(del(lender, keylet.key, tfLoanOverpayment), ter(temINVALID_FLAG)); + env(del(evan, keylet.key), ter(tecNO_PERMISSION)); + env(del(lender, broker.brokerID), ter(tecNO_ENTRY)); + + // Delete the loan + env(del(lender, keylet.key)); + env.close(); + + // No loans left + verifyLoanStatus.checkBroker( + 0, broker.asset(1000).value(), 0, interest, 1, 0, 0); + + BEAST_EXPECT( + env.balance(borrower, broker.asset).value() == + borrowerStartingBalance.value() + assetsAvailable); + BEAST_EXPECT(env.ownerCount(borrower) == borrowerOwnerCount); + + if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); + BEAST_EXPECT(brokerSle)) + { + BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); + } + } + + void + testLifecycle() + { + testcase("Lifecycle"); + using namespace jtx; + + // Create 3 loan brokers: one for XRP, one for an IOU, and one for an + // MPT. That'll require three corresponding SAVs. + Env env(*this, all); + + Account const issuer{"issuer"}; + // For simplicity, lender will be the sole actor for the vault & + // brokers. + Account const lender{"lender"}; + // Borrower only wants to borrow + Account const borrower{"borrower"}; + // Evan will attempt to be naughty + Account const evan{"evan"}; + // Do not fund alice + Account const alice{"alice"}; + Vault vault{env}; + + // Fund the accounts and trust lines with the same amount so that tests + // can use the same values regardless of the asset. + env.fund(XRP(100'000), issuer, noripple(lender, borrower, evan)); + env.close(); + + // Create assets + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + std::string const iouCurrency{"IOU"}; + PrettyAsset const iouAsset = issuer[iouCurrency]; + env(trust(lender, iouAsset(1'000'000))); + env(trust(borrower, iouAsset(1'000'000))); + env(trust(evan, iouAsset(1'000'000))); + env(pay(issuer, evan, iouAsset(100'000))); + env(pay(issuer, lender, iouAsset(100'000))); + // Fund the borrower with enough to cover interest and fees + env(pay(issuer, borrower, iouAsset(1'000))); + env.close(); + + MPTTester mptt{env, issuer, mptInitNoFund}; + mptt.create( + {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); + PrettyAsset const mptAsset = mptt.issuanceID(); + mptt.authorize({.account = lender}); + mptt.authorize({.account = borrower}); + mptt.authorize({.account = evan}); + env(pay(issuer, lender, mptAsset(100'000))); + env(pay(issuer, evan, mptAsset(100'000))); + // Fund the borrower with enough to cover interest and fees + env(pay(issuer, borrower, mptAsset(1'000))); + env.close(); + + std::array const assets{xrpAsset, mptAsset, iouAsset}; + + auto const coverDepositParameter = 1000; + auto const coverRateMinParameter = percentageToTenthBips(10); + auto const maxCoveredLoanValue = 1000 * 100 / 10; + auto const vaultDeposit = 50'000; + auto const debtMaximumParameter = 25'000; + + // Create vaults and loan brokers + std::vector brokers; + for (auto const& asset : assets) + { + auto const deposit = asset(vaultDeposit); + auto const debtMaximumValue = asset(debtMaximumParameter).value(); + auto const coverDepositValue = asset(coverDepositParameter).value(); + + auto [tx, vaultKeylet] = + vault.create({.owner = lender, .asset = asset}); + env(tx); + env.close(); + BEAST_EXPECT(env.le(vaultKeylet)); + + env(vault.deposit( + {.depositor = lender, + .id = vaultKeylet.key, + .amount = deposit})); + env.close(); + if (auto const vault = env.le(keylet::vault(vaultKeylet.key)); + BEAST_EXPECT(vault)) + { + BEAST_EXPECT(vault->at(sfAssetsAvailable) == deposit.value()); + } + + auto const keylet = + keylet::loanbroker(lender.id(), env.seq(lender)); + auto const testData = "spam spam spam spam"; + + using namespace loanBroker; + env(set(lender, vaultKeylet.key), + fee(increment), + data(testData), + managementFeeRate(TenthBips16(100)), + debtMaximum(debtMaximumValue), + coverRateMinimum(TenthBips32(coverRateMinParameter)), + coverRateLiquidation(TenthBips32(percentageToTenthBips(25)))); + + env(coverDeposit(lender, keylet.key, coverDepositValue)); + + brokers.emplace_back(asset, keylet.key); + } + + // Create and update Loans + for (auto const& broker : brokers) + { + auto const& asset = broker.asset.raw(); + testcase << "Lifecycle: " + << (asset.native() ? "XRP " + : asset.holds() ? "IOU " + : asset.holds() ? "MPT " + : "Unknown "); + + using namespace loan; + using namespace std::chrono_literals; + using d = NetClock::duration; + using tp = NetClock::time_point; + + Number const principalRequest = broker.asset(1000).value(); + Number const maxCoveredLoanRequest = + broker.asset(maxCoveredLoanValue).value(); + Number const totalVaultRequest = broker.asset(vaultDeposit).value(); + Number const debtMaximumRequest = + broker.asset(debtMaximumParameter).value(); + + auto const startDate = env.now() + 3600s; + auto const loanSetFee = fee(env.current()->fees().base * 2); + + auto const pseudoAcct = [&]() { + auto const brokerSle = + env.le(keylet::loanbroker(broker.brokerID)); + if (!BEAST_EXPECT(brokerSle)) + return lender; + auto const brokerPseudo = brokerSle->at(sfAccount); + return Account("Broker pseudo-account", brokerPseudo); + }(); + + auto badKeylet = keylet::vault(lender.id(), env.seq(lender)); + // Try some failure cases + // flags are checked first + env(set(evan, + broker.brokerID, + principalRequest, + startDate, + tfLoanSetMask), + sig(sfCounterpartySignature, lender), + loanSetFee, + ter(temINVALID_FLAG)); + + // field length validation + // sfData: good length, bad account + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, borrower), + data(std::string(maxDataPayloadLength, 'X')), + loanSetFee, + ter(tefBAD_AUTH)); + // sfData: too long + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, lender), + data(std::string(maxDataPayloadLength + 1, 'Y')), + loanSetFee, + ter(temINVALID)); + + // field range validation + // sfOverpaymentFee: good value, bad account + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, borrower), + overpaymentFee(maxOverpaymentFee), + loanSetFee, + ter(tefBAD_AUTH)); + // sfOverpaymentFee: too big + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, lender), + overpaymentFee(maxOverpaymentFee + 1), + loanSetFee, + ter(temINVALID)); + + // sfLateInterestRate: good value, bad account + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, borrower), + lateInterestRate(maxLateInterestRate), + loanSetFee, + ter(tefBAD_AUTH)); + // sfLateInterestRate: too big + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, lender), + lateInterestRate(maxLateInterestRate + 1), + loanSetFee, + ter(temINVALID)); + + // sfCloseInterestRate: good value, bad account + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, borrower), + closeInterestRate(maxCloseInterestRate), + loanSetFee, + ter(tefBAD_AUTH)); + // sfCloseInterestRate: too big + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, lender), + closeInterestRate(maxCloseInterestRate + 1), + loanSetFee, + ter(temINVALID)); + + // sfOverpaymentInterestRate: good value, bad account + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, borrower), + overpaymentInterestRate(maxOverpaymentInterestRate), + loanSetFee, + ter(tefBAD_AUTH)); + // sfOverpaymentInterestRate: too big + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, lender), + overpaymentInterestRate(maxOverpaymentInterestRate + 1), + loanSetFee, + ter(temINVALID)); + + // sfPaymentTotal: good value, bad account + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, borrower), + paymentTotal(LoanSet::minPaymentTotal), + loanSetFee, + ter(tefBAD_AUTH)); + // sfPaymentTotal: too small (there is no max) + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, lender), + paymentTotal(LoanSet::minPaymentTotal - 1), + loanSetFee, + ter(temINVALID)); + + // sfPaymentInterval: good value, bad account + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, borrower), + paymentInterval(LoanSet::minPaymentInterval), + loanSetFee, + ter(tefBAD_AUTH)); + // sfPaymentInterval: too small (there is no max) + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, lender), + paymentInterval(LoanSet::minPaymentInterval - 1), + loanSetFee, + ter(temINVALID)); + + // sfGracePeriod: good value, bad account + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, borrower), + paymentInterval(LoanSet::minPaymentInterval * 2), + gracePeriod(LoanSet::minPaymentInterval * 2), + loanSetFee, + ter(tefBAD_AUTH)); + // sfGracePeriod: larger than paymentInterval + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, lender), + paymentInterval(LoanSet::minPaymentInterval * 2), + gracePeriod(LoanSet::minPaymentInterval * 3), + loanSetFee, + ter(temINVALID)); + + // insufficient fee - single sign + env(set(borrower, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, lender), + ter(telINSUF_FEE_P)); + // insufficient fee - multisign + env(set(borrower, broker.brokerID, principalRequest, startDate), + counterparty(lender), + msig(evan, lender), + msig(sfCounterpartySignature, evan, borrower), + fee(env.current()->fees().base * 5 - 1), + ter(telINSUF_FEE_P)); + // multisign sufficient fee, but no signers set up + env(set(borrower, broker.brokerID, principalRequest, startDate), + counterparty(lender), + msig(evan, lender), + msig(sfCounterpartySignature, evan, borrower), + fee(env.current()->fees().base * 5), + ter(tefNOT_MULTI_SIGNING)); + // not the broker owner, no counterparty, not signed by broker + // owner + env(set(borrower, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, evan), + loanSetFee, + ter(tefBAD_AUTH)); + // bad start date - in the past + env(set(evan, + broker.brokerID, + principalRequest, + env.closed()->info().closeTime - 1s), + sig(sfCounterpartySignature, lender), + loanSetFee, + ter(tecEXPIRED)); + // not the broker owner, counterparty is borrower + env(set(evan, broker.brokerID, principalRequest, startDate), + counterparty(borrower), + sig(sfCounterpartySignature, borrower), + loanSetFee, + ter(tecNO_PERMISSION)); + // can not lend money to yourself + env(set(lender, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, lender), + loanSetFee, + ter(tecNO_PERMISSION)); + // not a LoanBroker object, no counterparty + env(set(lender, badKeylet.key, principalRequest, startDate), + sig(sfCounterpartySignature, evan), + loanSetFee, + ter(temBAD_SIGNER)); + // not a LoanBroker object, counterparty is valid + env(set(lender, badKeylet.key, principalRequest, startDate), + counterparty(borrower), + sig(sfCounterpartySignature, borrower), + loanSetFee, + ter(tecNO_ENTRY)); + // borrower doesn't exist + env(set(lender, broker.brokerID, principalRequest, startDate), + counterparty(alice), + sig(sfCounterpartySignature, alice), + loanSetFee, + ter(terNO_ACCOUNT)); + + // Request more funds than the vault has available + env(set(evan, broker.brokerID, totalVaultRequest + 1, startDate), + sig(sfCounterpartySignature, lender), + loanSetFee, + ter(tecINSUFFICIENT_FUNDS)); + + // Request more funds than the broker's first-loss capital can + // cover. + env(set(evan, + broker.brokerID, + maxCoveredLoanRequest + 1, + startDate), + sig(sfCounterpartySignature, lender), + loanSetFee, + ter(tecINSUFFICIENT_FUNDS)); + + // Frozen trust line / locked MPT issuance + // XRP can not be frozen, but run through the loop anyway to test + // the tecLIMIT_EXCEEDED case + { + auto const brokerSle = + env.le(keylet::loanbroker(broker.brokerID)); + if (!BEAST_EXPECT(brokerSle)) + return; + + auto const [freeze, deepfreeze, unfreeze, expectedResult] = + [&]() -> std::tuple< + std::function, + std::function, + std::function, + TER> { + // Freeze / lock the asset + std::function empty; + if (broker.asset.raw().native()) + { + // XRP can't be frozen + return std::make_tuple(empty, empty, empty, tesSUCCESS); + } + else if (broker.asset.raw().holds()) + { + auto freeze = [&](Account const& holder) { + env(trust( + issuer, holder[iouCurrency](0), tfSetFreeze)); + }; + auto deepfreeze = [&](Account const& holder) { + env(trust( + issuer, + holder[iouCurrency](0), + tfSetFreeze | tfSetDeepFreeze)); + }; + auto unfreeze = [&](Account const& holder) { + env(trust( + issuer, + holder[iouCurrency](0), + tfClearFreeze | tfClearDeepFreeze)); + }; + return std::make_tuple( + freeze, deepfreeze, unfreeze, tecFROZEN); + } + else + { + auto freeze = [&](Account const& holder) { + mptt.set( + {.account = issuer, + .holder = holder, + .flags = tfMPTLock}); + }; + auto unfreeze = [&](Account const& holder) { + mptt.set( + {.account = issuer, + .holder = holder, + .flags = tfMPTUnlock}); + }; + return std::make_tuple( + freeze, empty, unfreeze, tecLOCKED); + } + }(); + + // Try freezing both the lender and the pseudo-account + for (auto const& account : {lender, pseudoAcct}) + { + if (freeze) + { + // Freeze the account + freeze(account); + + // Try to create a loan with a frozen line + env(set(evan, + broker.brokerID, + debtMaximumRequest, + startDate), + sig(sfCounterpartySignature, lender), + loanSetFee, + ter(expectedResult)); + + // Unfreeze the account + BEAST_EXPECT(unfreeze); + unfreeze(account); + } + + // Ensure the line is unfrozen with a request that is fine + // except too it requests more principal than the broker can + // carry + env(set(evan, + broker.brokerID, + debtMaximumRequest + 1, + startDate), + sig(sfCounterpartySignature, lender), + loanSetFee, + ter(tecLIMIT_EXCEEDED)); + } + + // Deep freeze the borrower, which prevents them from receiving + // funds + if (deepfreeze) + { + // Make sure evan has a trust line that so the issuer can + // freeze it. (Don't need to do this for the borrower, + // because LoanDraw will create a line to the borrower + // automatically.) + env(trust(evan, issuer[iouCurrency](100'000))); + + // Freeze evan + deepfreeze(evan); + + // Try to create a loan with a deep frozen line + env(set(evan, + broker.brokerID, + debtMaximumRequest, + startDate), + sig(sfCounterpartySignature, lender), + loanSetFee, + ter(expectedResult)); + + // Unfreeze evan + BEAST_EXPECT(unfreeze); + unfreeze(evan); + + // Ensure the line is unfrozen with a request that is fine + // except too it requests more principal than the broker can + // carry + env(set(evan, + broker.brokerID, + debtMaximumRequest + 1, + startDate), + sig(sfCounterpartySignature, lender), + loanSetFee, + ter(tecLIMIT_EXCEEDED)); + } + } + + // Finally! Create a loan + std::string testData; + + auto currentState = [&](Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus) { + // Lookup the current loan state + if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan)) + { + LoanState state{ + .previousPaymentDate = loan->at(sfPreviousPaymentDate), + .startDate = tp{d{loan->at(sfStartDate)}}, + .nextPaymentDate = loan->at(sfNextPaymentDueDate), + .paymentRemaining = loan->at(sfPaymentRemaining), + .assetsAvailable = loan->at(sfAssetsAvailable), + .principalRequested = loan->at(sfPrincipalRequested), + .principalOutstanding = + loan->at(sfPrincipalOutstanding), + .flags = loan->at(sfFlags), + .paymentInterval = loan->at(sfPaymentInterval), + }; + BEAST_EXPECT(state.previousPaymentDate == 0); + BEAST_EXPECT( + tp{d{state.nextPaymentDate}} == state.startDate + 600s); + BEAST_EXPECT(state.paymentRemaining == 12); + BEAST_EXPECT( + state.assetsAvailable == broker.asset(999).value()); + BEAST_EXPECT( + state.principalOutstanding == + broker.asset(1000).value()); + BEAST_EXPECT( + state.principalOutstanding == state.principalRequested); + BEAST_EXPECT(state.paymentInterval == 600); + + verifyLoanStatus(state); + + return state; + } + + return LoanState{ + .previousPaymentDate = 0, + .startDate = tp{d{0}}, + .nextPaymentDate = 0, + .paymentRemaining = 0, + .assetsAvailable = 0, + .principalRequested = 0, + .principalOutstanding = 0, + .flags = 0, + .paymentInterval = 0, + }; + }; + + auto defaultBeforeStartDate = [&](std::uint32_t baseFlag, + bool impair = true) { + return [&, impair, baseFlag]( + Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus) { + // toEndOfLife + // + // Default the loan + + // Initialize values with the current state + auto state = currentState(loanKeylet, verifyLoanStatus); + BEAST_EXPECT(state.flags == baseFlag); + + if (impair) + { + // Impair the loan + env(manage(lender, loanKeylet.key, tfLoanImpair)); + + state.flags |= tfLoanImpair; + state.nextPaymentDate = + env.now().time_since_epoch().count(); + verifyLoanStatus(state); + + // Once the loan is impaired, it can't be impaired again + env(manage(lender, loanKeylet.key, tfLoanImpair), + ter(tecNO_PERMISSION)); + } + + auto const nextDueDate = tp{d{state.nextPaymentDate}}; + + // Can't default the loan yet. The grace period hasn't + // expired + env(manage(lender, loanKeylet.key, tfLoanDefault), + ter(tecTOO_SOON)); + + // Let some time pass so that the loan can be + // defaulted + env.close(nextDueDate + 60s); + + if (impair) + { + // Impaired loans can't be drawn against + env(draw(borrower, loanKeylet.key, broker.asset(100)), + ter(tecNO_PERMISSION)); + } + + // Default the loan + env(manage(lender, loanKeylet.key, tfLoanDefault)); + + state.flags |= tfLoanDefault; + state.paymentRemaining = 0; + state.assetsAvailable = 0; + state.principalOutstanding = 0; + verifyLoanStatus(state); + + // Defaulted loans can't be drawn against, either + env(draw(borrower, loanKeylet.key, broker.asset(100)), + ter(tecNO_PERMISSION)); + + // Once a loan is defaulted, it can't be managed + env(manage(lender, loanKeylet.key, tfLoanUnimpair), + ter(tecNO_PERMISSION)); + env(manage(lender, loanKeylet.key, tfLoanImpair), + ter(tecNO_PERMISSION)); + }; + }; + + auto immediatePayoff = [&](std::uint32_t baseFlag) { + return [&, baseFlag]( + Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus) { + // toEndOfLife + // + auto state = currentState(loanKeylet, verifyLoanStatus); + BEAST_EXPECT(state.flags == baseFlag); + auto const borrowerStartingBalance = + env.balance(borrower, broker.asset); + + // Try to make a payment before the loan starts + env(pay(borrower, loanKeylet.key, broker.asset(500)), + ter(tecTOO_SOON)); + + // Advance to the start date of the loan + env.close(state.startDate + 5s); + + verifyLoanStatus(state); + + // Need to account for fees if the loan is in XRP + PrettyAmount adjustment = broker.asset(0); + if (broker.asset.raw().native()) + { + adjustment = 2 * env.current()->fees().base; + } + + // Draw the entire available balance + // Need to create the STAmount directly to avoid + // PrettyAsset scaling. + STAmount const drawAmount{ + broker.asset, state.assetsAvailable}; + env(draw(borrower, loanKeylet.key, drawAmount)); + env.close(state.startDate + 20s); + auto const loanAge = (env.now() - state.startDate).count(); + BEAST_EXPECT(loanAge == 30); + + state.assetsAvailable -= drawAmount; + verifyLoanStatus(state); + BEAST_EXPECT( + env.balance(borrower, broker.asset) == + borrowerStartingBalance + drawAmount - adjustment); + + // Send some bogus pay transactions + env(pay(borrower, + keylet::loan(uint256(0)).key, + broker.asset(10)), + ter(temINVALID)); + env(pay(borrower, loanKeylet.key, broker.asset(-100)), + ter(temBAD_AMOUNT)); + env(pay(borrower, broker.brokerID, broker.asset(100)), + ter(tecNO_ENTRY)); + env(pay(evan, loanKeylet.key, broker.asset(500)), + ter(tecNO_PERMISSION)); + + { + auto const otherAsset = + broker.asset.raw() == assets[0].raw() ? assets[1] + : assets[0]; + env(pay(borrower, loanKeylet.key, otherAsset(100)), + ter(tecWRONG_ASSET)); + } + + // Amount doesn't cover a single payment + env(pay(borrower, + loanKeylet.key, + STAmount{broker.asset, 1}), + ter(tecINSUFFICIENT_PAYMENT)); + + // Get the balance after these failed transactions take + // fees + auto const borrowerBalanceBeforePayment = + env.balance(borrower, broker.asset); + + // Full payoff amount will consist of + // 1. principal outstanding (1000) + // 2. accrued interest (at 12%) + // 3. prepayment penalty (closeInterest at 3.6%) + // 4. close payment fee (4) + // Calculate these values without the helper functions + // to verify they're working correctly The numbers in + // the below BEAST_EXPECTs may not hold across assets. + Number const interval = state.paymentInterval; + auto const periodicRate = + interval * Number(12, -2) / (365 * 24 * 60 * 60); + BEAST_EXPECT( + periodicRate == + Number(2283105022831050, -21, Number::unchecked{})); + STAmount const accruedInterest{ + broker.asset, + state.principalOutstanding * periodicRate * loanAge / + interval}; + BEAST_EXPECT( + accruedInterest == + broker.asset(Number(1141552511415525, -19))); + STAmount const prepaymentPenalty{ + broker.asset, + state.principalOutstanding * Number(36, -3)}; + BEAST_EXPECT(prepaymentPenalty == broker.asset(36)); + STAmount const closePaymentFee = broker.asset(4); + auto const payoffAmount = + STAmount{broker.asset, state.principalOutstanding} + + accruedInterest + prepaymentPenalty + closePaymentFee; + BEAST_EXPECT( + payoffAmount == + broker.asset(Number(1040000114155251, -12))); + BEAST_EXPECT(payoffAmount > drawAmount); + // Try to pay a little extra to show that it's _not_ + // taken + auto const transactionAmount = + payoffAmount + broker.asset(10); + BEAST_EXPECT( + transactionAmount == + broker.asset(Number(1050000114155251, -12))); + env(pay(borrower, loanKeylet.key, transactionAmount)); + + env.close(); + + // Need to account for fees if the loan is in XRP + adjustment = broker.asset(0); + if (broker.asset.raw().native()) + { + adjustment = env.current()->fees().base; + } + + state.paymentRemaining = 0; + state.principalOutstanding = 0; + verifyLoanStatus(state); + + BEAST_EXPECT( + env.balance(borrower, broker.asset) == + borrowerBalanceBeforePayment - payoffAmount - + adjustment); + + // Can't impair or default a paid off loan + env(manage(lender, loanKeylet.key, tfLoanImpair), + ter(tecNO_PERMISSION)); + env(manage(lender, loanKeylet.key, tfLoanDefault), + ter(tecNO_PERMISSION)); + }; + }; + + // There are a lot of fields that can be set on a loan, but most of + // them only affect the "math" when a payment is made. The only one + // that really affects behavior is the `tfLoanOverpayment` flag. + lifecycle( + "Loan overpayment allowed - Impair and Default before start " + "date", + env, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + defaultBeforeStartDate(lsfLoanOverpayment)); + + lifecycle( + "Loan overpayment prohibited - Impair and Default before start " + "date", + env, + lender, + borrower, + evan, + broker, + pseudoAcct, + 0, + defaultBeforeStartDate(0)); + + lifecycle( + "Loan overpayment allowed - Default without Impair before " + "start " + "date", + env, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + defaultBeforeStartDate(lsfLoanOverpayment, false)); + + lifecycle( + "Loan overpayment prohibited - Default without Impair before " + "start " + "date", + env, + lender, + borrower, + evan, + broker, + pseudoAcct, + 0, + defaultBeforeStartDate(0, false)); + + lifecycle( + "Loan overpayment allowed - Draw then default", + env, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + [&](Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus) { + // toEndOfLife + // + // Initialize values with the current state + auto state = currentState(loanKeylet, verifyLoanStatus); + BEAST_EXPECT(state.flags == lsfLoanOverpayment); + auto const borrowerStartingBalance = + env.balance(borrower, broker.asset); + + // Draw the balance + env(draw( + borrower, + keylet::loan(uint256(0)).key, + broker.asset(10)), + ter(temINVALID)); + env(draw(borrower, loanKeylet.key, broker.asset(-100)), + ter(temBAD_AMOUNT)); + env(draw(borrower, broker.brokerID, broker.asset(100)), + ter(tecNO_ENTRY)); + env(draw(evan, loanKeylet.key, broker.asset(500)), + ter(tecNO_PERMISSION)); + env(draw(borrower, loanKeylet.key, broker.asset(500)), + ter(tecTOO_SOON)); + + // Advance to the start date of the loan + env.close(state.startDate + 5s); + env(draw(borrower, loanKeylet.key, broker.asset(10000)), + ter(tecINSUFFICIENT_FUNDS)); + { + auto const otherAsset = + broker.asset.raw() == assets[0].raw() ? assets[1] + : assets[0]; + env(draw(borrower, loanKeylet.key, otherAsset(100)), + ter(tecWRONG_ASSET)); + } + + verifyLoanStatus(state); + + // Need to account for fees if the loan is in XRP + PrettyAmount adjustment = broker.asset(0); + if (broker.asset.raw().native()) + { + adjustment = 5 * env.current()->fees().base; + } + + // Draw about half the balance + auto const drawAmount = broker.asset(500); + env(draw(borrower, loanKeylet.key, drawAmount)); + + state.assetsAvailable -= drawAmount.number(); + verifyLoanStatus(state); + BEAST_EXPECT( + env.balance(borrower, broker.asset) == + borrowerStartingBalance + drawAmount - adjustment); + + // move past the due date + grace period (60s) + env.close(tp{d{state.nextPaymentDate}} + 60s + 20s); + // Try to draw + env(draw(borrower, loanKeylet.key, broker.asset(100)), + ter(tecNO_PERMISSION)); + + // default the loan + env(manage(lender, loanKeylet.key, tfLoanDefault)); + state.paymentRemaining = 0; + state.assetsAvailable = 0; + state.principalOutstanding = 0; + state.flags |= tfLoanDefault; + + verifyLoanStatus(state); + + // Same error, different check + env(draw(borrower, loanKeylet.key, broker.asset(100)), + ter(tecNO_PERMISSION)); + + // Can't make a payment on it either + env(pay(borrower, loanKeylet.key, broker.asset(300)), + ter(tecKILLED)); + + // Default + }); + + lifecycle( + "Loan overpayment prohibited - Pay off immediately", + env, + lender, + borrower, + evan, + broker, + pseudoAcct, + 0, + immediatePayoff(0)); + + lifecycle( + "Loan overpayment allowed - Pay off immediately", + env, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + immediatePayoff(lsfLoanOverpayment)); + + lifecycle( + "Loan overpayment prohibited - Make payments", + env, + lender, + borrower, + evan, + broker, + pseudoAcct, + 0, + [&](Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus) { + // toEndOfLife + // + // Draw and make multiple payments + auto state = currentState(loanKeylet, verifyLoanStatus); + BEAST_EXPECT(state.flags == 0); + // Advance to the start date of the loan + env.close(state.startDate + 5s); + + verifyLoanStatus(state); + + auto const borrowerStartingBalance = + env.balance(borrower, broker.asset); + + // Need to account for fees if the loan is in XRP + PrettyAmount adjustment = broker.asset(0); + if (broker.asset.raw().native()) + { + adjustment = env.current()->fees().base; + } + + // Draw the entire available balance + // Need to create the STAmount directly to avoid + // PrettyAsset scaling. + STAmount const drawAmount{ + broker.asset, state.assetsAvailable}; + env(draw(borrower, loanKeylet.key, drawAmount)); + env.close(state.startDate + 20s); + auto const loanAge = (env.now() - state.startDate).count(); + BEAST_EXPECT(loanAge == 30); + + state.assetsAvailable -= drawAmount; + verifyLoanStatus(state); + BEAST_EXPECT( + env.balance(borrower, broker.asset) == + borrowerStartingBalance + drawAmount - adjustment); + + // Periodic payment amount will consist of + // 1. principal outstanding (1000) + // 2. interest interest rate (at 12%) + // 3. payment interval (600s) + // 4. loan service fee (2) + // Calculate these values without the helper functions + // to verify they're working correctly The numbers in + // the below BEAST_EXPECTs may not hold across assets. + Number const interval = state.paymentInterval; + auto const periodicRate = + interval * Number(12, -2) / (365 * 24 * 60 * 60); + BEAST_EXPECT( + periodicRate == + Number(2283105022831050, -21, Number::unchecked{})); + + while (state.paymentRemaining > 0) + { + testcase << "Payments remaining: " + << state.paymentRemaining; + + STAmount const principalRequestedAmount{ + broker.asset, state.principalRequested}; + // Compute the payment based on the number of payments + // remaining + auto const rateFactor = + power(1 + periodicRate, state.paymentRemaining); + Number const rawPeriodicPayment = + state.principalOutstanding * periodicRate * + rateFactor / (rateFactor - 1); + STAmount const periodicPayment = roundToReference( + STAmount{broker.asset, rawPeriodicPayment}, + principalRequestedAmount); + // Only check the first payment since the rounding may + // drift as payments are made + BEAST_EXPECT( + state.paymentRemaining < 12 || + STAmount(broker.asset, rawPeriodicPayment) == + broker.asset(Number(8333457001162141, -14))); + // Include the service fee + STAmount const totalDue = roundToReference( + periodicPayment + broker.asset(2), + principalRequestedAmount); + // Only check the first payment since the rounding may + // drift as payments are made + BEAST_EXPECT( + state.paymentRemaining < 12 || + totalDue == + roundToReference( + broker.asset(Number(8533457001162141, -14)), + principalRequestedAmount)); + + // Try to pay a little extra to show that it's _not_ + // taken + STAmount const transactionAmount = + STAmount{broker.asset, totalDue} + broker.asset(10); + // Only check the first payment since the rounding may + // drift as payments are made + BEAST_EXPECT( + state.paymentRemaining < 12 || + transactionAmount == + roundToReference( + broker.asset(Number(9533457001162141, -14)), + principalRequestedAmount)); + + auto const totalDueAmount = + STAmount{broker.asset, totalDue}; + + // Compute the expected principal amount + Number const rawInterest = state.paymentRemaining == 1 + ? rawPeriodicPayment - state.principalOutstanding + : state.principalOutstanding * periodicRate; + STAmount const interest = roundToReference( + STAmount{broker.asset, rawInterest}, + principalRequestedAmount); + BEAST_EXPECT( + state.paymentRemaining < 12 || + roundToReference( + STAmount{broker.asset, rawInterest}, + principalRequestedAmount) == + roundToReference( + broker.asset(Number(2283105022831050, -18)), + principalRequestedAmount)); + BEAST_EXPECT(interest >= Number(0)); + + auto const rawPrincipal = + rawPeriodicPayment - rawInterest; + BEAST_EXPECT( + state.paymentRemaining < 12 || + roundToReference( + STAmount{broker.asset, rawPrincipal}, + principalRequestedAmount) == + roundToReference( + broker.asset(Number(8333228690659858, -14)), + principalRequestedAmount)); + BEAST_EXPECT( + state.paymentRemaining > 1 || + rawPrincipal == state.principalOutstanding); + auto const principal = roundToReference( + STAmount{broker.asset, periodicPayment - interest}, + principalRequestedAmount); + BEAST_EXPECT( + principal > Number(0) && + principal <= state.principalOutstanding); + BEAST_EXPECT( + state.paymentRemaining > 1 || + principal == state.principalOutstanding); + BEAST_EXPECT( + rawPrincipal + rawInterest == rawPeriodicPayment); + BEAST_EXPECT(principal + interest == periodicPayment); + + auto const borrowerBalanceBeforePayment = + env.balance(borrower, broker.asset); + + // Make the payment + env(pay(borrower, loanKeylet.key, transactionAmount)); + + env.close(); + + // Need to account for fees if the loan is in XRP + adjustment = broker.asset(0); + if (broker.asset.raw().native()) + { + adjustment = env.current()->fees().base; + } + + // Check the result + auto const borrowerBalance = + env.balance(borrower, broker.asset); + auto const expectedBalance = + borrowerBalanceBeforePayment - totalDueAmount - + adjustment; + BEAST_EXPECT( + borrowerBalance == expectedBalance || + (!broker.asset.raw().native() && + broker.asset.raw().holds() && + ((borrowerBalance - expectedBalance) / + expectedBalance < + Number(1, -4)))); + + --state.paymentRemaining; + state.previousPaymentDate = state.nextPaymentDate; + state.nextPaymentDate += state.paymentInterval; + state.principalOutstanding -= principal; + + verifyLoanStatus(state); + } + + // Loan is paid off + BEAST_EXPECT(state.paymentRemaining == 0); + BEAST_EXPECT(state.principalOutstanding == 0); + + // Can't impair or default a paid off loan + env(manage(lender, loanKeylet.key, tfLoanImpair), + ter(tecNO_PERMISSION)); + env(manage(lender, loanKeylet.key, tfLoanDefault), + ter(tecNO_PERMISSION)); + }); + + if (auto brokerSle = env.le(keylet::loanbroker(broker.brokerID)); + BEAST_EXPECT(brokerSle)) + { + BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); + BEAST_EXPECT(brokerSle->at(sfDebtTotal) == 0); + + auto const coverAvailable = brokerSle->at(sfCoverAvailable); + env(loanBroker::coverWithdraw( + lender, + broker.brokerID, + STAmount(broker.asset, coverAvailable))); + env.close(); + + brokerSle = env.le(keylet::loanbroker(broker.brokerID)); + BEAST_EXPECT(brokerSle && brokerSle->at(sfCoverAvailable) == 0); + } + // Verify we can delete the loan broker + env(loanBroker::del(lender, broker.brokerID)); + env.close(); + } + } + +public: + void + run() override + { + testDisabled(); + testLifecycle(); + } +}; + +BEAST_DEFINE_TESTSUITE(Loan, tx, ripple); + +} // namespace test +} // namespace ripple diff --git a/src/test/jtx/JTx.h b/src/test/jtx/JTx.h index 198839dd28..36127f1843 100644 --- a/src/test/jtx/JTx.h +++ b/src/test/jtx/JTx.h @@ -54,7 +54,11 @@ struct JTx bool fill_sig = true; bool fill_netid = true; std::shared_ptr stx; - std::function signer; + // Functions that sign the transaction from the Account + std::vector> mainSigners; + // Functions that sign something else after the mainSigners, such as + // sfCounterpartySignature + std::vector> postSigners; JTx() = default; JTx(JTx const&) = default; diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index 11c13543e4..0ff8e404fd 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -656,6 +656,114 @@ create( } // namespace check +/* LoanBroker */ +/******************************************************************************/ + +namespace loanBroker { + +Json::Value +set(AccountID const& account, uint256 const& vaultId, std::uint32_t flags = 0); + +// Use "del" because "delete" is a reserved word in C++. +Json::Value +del(AccountID const& account, + uint256 const& loanBrokerID, + std::uint32_t flags = 0); + +Json::Value +coverDeposit( + AccountID const& account, + uint256 const& loanBrokerID, + STAmount const& amount, + std::uint32_t flags = 0); + +Json::Value +coverWithdraw( + AccountID const& account, + uint256 const& loanBrokerID, + STAmount const& amount, + std::uint32_t flags = 0); + +auto const loanBrokerID = JTxFieldWrapper(sfLoanBrokerID); + +auto const managementFeeRate = + valueUnitWrapper(sfManagementFeeRate); + +auto const debtMaximum = simpleField(sfDebtMaximum); + +auto const coverRateMinimum = + valueUnitWrapper(sfCoverRateMinimum); + +auto const coverRateLiquidation = + valueUnitWrapper(sfCoverRateLiquidation); + +} // namespace loanBroker + +/* Loan */ +/******************************************************************************/ +namespace loan { + +Json::Value +set(AccountID const& account, + uint256 const& loanBrokerID, + Number principalRequested, + NetClock::time_point const& startDate, + std::uint32_t flags = 0); + +auto const counterparty = JTxFieldWrapper(sfCounterparty); + +// For `CounterPartySignature`, use `sig(sfCounterpartySignature, ...)` + +auto const loanOriginationFee = simpleField(sfLoanOriginationFee); + +auto const loanServiceFee = simpleField(sfLoanServiceFee); + +auto const latePaymentFee = simpleField(sfLatePaymentFee); + +auto const closePaymentFee = simpleField(sfClosePaymentFee); + +auto const overpaymentFee = + valueUnitWrapper(sfOverpaymentFee); + +auto const interestRate = + valueUnitWrapper(sfInterestRate); + +auto const lateInterestRate = + valueUnitWrapper(sfLateInterestRate); + +auto const closeInterestRate = + valueUnitWrapper(sfCloseInterestRate); + +auto const overpaymentInterestRate = + valueUnitWrapper(sfOverpaymentInterestRate); + +auto const paymentTotal = simpleField(sfPaymentTotal); + +auto const paymentInterval = simpleField(sfPaymentInterval); + +auto const gracePeriod = simpleField(sfGracePeriod); + +Json::Value +manage(AccountID const& account, uint256 const& loanID, std::uint32_t flags); + +Json::Value +del(AccountID const& account, uint256 const& loanID, std::uint32_t flags = 0); + +Json::Value +draw( + AccountID const& account, + uint256 const& loanID, + STAmount const& amount, + std::uint32_t flags = 0); + +Json::Value +pay(AccountID const& account, + uint256 const& loanID, + STAmount const& amount, + std::uint32_t flags = 0); + +} // namespace loan + } // namespace jtx } // namespace test } // namespace ripple diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index 96b63bd927..60ed546059 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -35,6 +35,7 @@ #include #include +#include #include #include #include @@ -481,8 +482,22 @@ void Env::autofill_sig(JTx& jt) { auto& jv = jt.jv; - if (jt.signer) - return jt.signer(*this, jt); + + scope_success success([&]() { + // Call all the post-signers after the main signers or autofill are done + for (auto const& signer : jt.postSigners) + signer(*this, jt); + }); + + // Call all the main signers + if (!jt.mainSigners.empty()) + { + for (auto const& signer : jt.mainSigners) + signer(*this, jt); + return; + } + + // If the sig is still needed, get it here. if (!jt.fill_sig) return; auto const account = jv.isMember(sfDelegate.jsonName) diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index f5f0368d47..500d30fc1c 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -409,6 +409,142 @@ allpe(AccountID const& a, Issue const& iss) iss.account); }; +/* LoanBroker */ +/******************************************************************************/ + +namespace loanBroker { + +Json::Value +set(AccountID const& account, uint256 const& vaultId, uint32_t flags) +{ + Json::Value jv; + jv[sfTransactionType] = jss::LoanBrokerSet; + jv[sfAccount] = to_string(account); + jv[sfVaultID] = to_string(vaultId); + jv[sfFlags] = flags; + return jv; +} + +Json::Value +del(AccountID const& account, uint256 const& loanBrokerID, uint32_t flags) +{ + Json::Value jv; + jv[sfTransactionType] = jss::LoanBrokerDelete; + jv[sfAccount] = to_string(account); + jv[sfLoanBrokerID] = to_string(loanBrokerID); + jv[sfFlags] = flags; + return jv; +} + +Json::Value +coverDeposit( + AccountID const& account, + uint256 const& loanBrokerID, + STAmount const& amount, + uint32_t flags) +{ + Json::Value jv; + jv[sfTransactionType] = jss::LoanBrokerCoverDeposit; + jv[sfAccount] = to_string(account); + jv[sfLoanBrokerID] = to_string(loanBrokerID); + jv[sfAmount] = amount.getJson(JsonOptions::none); + jv[sfFlags] = flags; + return jv; +} + +Json::Value +coverWithdraw( + AccountID const& account, + uint256 const& loanBrokerID, + STAmount const& amount, + uint32_t flags) +{ + Json::Value jv; + jv[sfTransactionType] = jss::LoanBrokerCoverWithdraw; + jv[sfAccount] = to_string(account); + jv[sfLoanBrokerID] = to_string(loanBrokerID); + jv[sfAmount] = amount.getJson(JsonOptions::none); + jv[sfFlags] = flags; + return jv; +} + +} // namespace loanBroker + +/* Loan */ +/******************************************************************************/ +namespace loan { + +Json::Value +set(AccountID const& account, + uint256 const& loanBrokerID, + Number principalRequested, + NetClock::time_point const& startDate, + std::uint32_t flags) +{ + Json::Value jv; + jv[sfTransactionType] = jss::LoanSet; + jv[sfAccount] = to_string(account); + jv[sfLoanBrokerID] = to_string(loanBrokerID); + jv[sfPrincipalRequested] = to_string(principalRequested); + jv[sfFlags] = flags; + jv[sfStartDate] = startDate.time_since_epoch().count(); + return jv; +} + +Json::Value +manage(AccountID const& account, uint256 const& loanID, std::uint32_t flags) +{ + Json::Value jv; + jv[sfTransactionType] = jss::LoanManage; + jv[sfAccount] = to_string(account); + jv[sfLoanID] = to_string(loanID); + jv[sfFlags] = flags; + return jv; +} + +Json::Value +del(AccountID const& account, uint256 const& loanID, std::uint32_t flags) +{ + Json::Value jv; + jv[sfTransactionType] = jss::LoanDelete; + jv[sfAccount] = to_string(account); + jv[sfLoanID] = to_string(loanID); + jv[sfFlags] = flags; + return jv; +} + +Json::Value +draw( + AccountID const& account, + uint256 const& loanID, + STAmount const& amount, + std::uint32_t flags) +{ + Json::Value jv; + jv[sfTransactionType] = jss::LoanDraw; + jv[sfAccount] = to_string(account); + jv[sfLoanID] = to_string(loanID); + jv[sfAmount] = amount.getJson(); + jv[sfFlags] = flags; + return jv; +} + +Json::Value +pay(AccountID const& account, + uint256 const& loanID, + STAmount const& amount, + std::uint32_t flags) +{ + Json::Value jv; + jv[sfTransactionType] = jss::LoanPay; + jv[sfAccount] = to_string(account); + jv[sfLoanID] = to_string(loanID); + jv[sfAmount] = amount.getJson(); + jv[sfFlags] = flags; + return jv; +} + +} // namespace loan } // namespace jtx } // namespace test } // namespace ripple diff --git a/src/test/jtx/impl/multisign.cpp b/src/test/jtx/impl/multisign.cpp index a802528247..eb2e16f8c3 100644 --- a/src/test/jtx/impl/multisign.cpp +++ b/src/test/jtx/impl/multisign.cpp @@ -65,7 +65,8 @@ signers(Account const& account, none_t) //------------------------------------------------------------------------------ -msig::msig(std::vector signers_) : signers(std::move(signers_)) +msig::msig(SField const* subField_, std::vector signers_) + : signers(std::move(signers_)), subField(subField_) { // Signatures must be applied in sorted order. std::sort( @@ -80,8 +81,15 @@ void msig::operator()(Env& env, JTx& jt) const { auto const mySigners = signers; - jt.signer = [mySigners, &env](Env&, JTx& jtx) { - jtx[sfSigningPubKey.getJsonName()] = ""; + auto callback = [subField = subField, mySigners, &env](Env&, JTx& jtx) { + // Where to put the signature. Supports sfCounterPartySignature. + auto& sigObject = subField ? jtx[*subField] : jtx.jv; + + // The signing pub key is only required at the top level. + if (!subField) + sigObject[sfSigningPubKey] = ""; + else if (sigObject.isNull()) + sigObject = Json::Value(Json::objectValue); std::optional st; try { @@ -92,7 +100,7 @@ msig::operator()(Env& env, JTx& jt) const env.test.log << pretty(jtx.jv) << std::endl; Rethrow(); } - auto& js = jtx[sfSigners.getJsonName()]; + auto& js = sigObject[sfSigners]; for (std::size_t i = 0; i < mySigners.size(); ++i) { auto const& e = mySigners[i]; @@ -107,6 +115,10 @@ msig::operator()(Env& env, JTx& jt) const strHex(Slice{sig.data(), sig.size()}); } }; + if (!subField) + jt.mainSigners.emplace_back(callback); + else + jt.postSigners.emplace_back(callback); } } // namespace jtx diff --git a/src/test/jtx/impl/sig.cpp b/src/test/jtx/impl/sig.cpp index fa1977fe08..2763df5271 100644 --- a/src/test/jtx/impl/sig.cpp +++ b/src/test/jtx/impl/sig.cpp @@ -29,12 +29,22 @@ sig::operator()(Env&, JTx& jt) const { if (!manual_) return; - jt.fill_sig = false; + if (!subField) + jt.fill_sig = false; if (account_) { // VFALCO Inefficient pre-C++14 auto const account = *account_; - jt.signer = [account](Env&, JTx& jtx) { jtx::sign(jtx.jv, account); }; + auto callback = [subField = subField, account](Env&, JTx& jtx) { + // Where to put the signature. Supports sfCounterPartySignature. + auto& sigObject = subField ? jtx[*subField] : jtx.jv; + + jtx::sign(jtx.jv, account, sigObject); + }; + if (!subField) + jt.mainSigners.emplace_back(callback); + else + jt.postSigners.emplace_back(callback); } } diff --git a/src/test/jtx/impl/utility.cpp b/src/test/jtx/impl/utility.cpp index afa7ee8f35..c2bbceb9f2 100644 --- a/src/test/jtx/impl/utility.cpp +++ b/src/test/jtx/impl/utility.cpp @@ -44,14 +44,20 @@ parse(Json::Value const& jv) } void -sign(Json::Value& jv, Account const& account) +sign(Json::Value& jv, Account const& account, Json::Value& sigObject) { - jv[jss::SigningPubKey] = strHex(account.pk().slice()); + sigObject[jss::SigningPubKey] = strHex(account.pk().slice()); Serializer ss; ss.add32(HashPrefix::txSign); parse(jv).addWithoutSigningFields(ss); auto const sig = ripple::sign(account.pk(), account.sk(), ss.slice()); - jv[jss::TxnSignature] = strHex(Slice{sig.data(), sig.size()}); + sigObject[jss::TxnSignature] = strHex(Slice{sig.data(), sig.size()}); +} + +void +sign(Json::Value& jv, Account const& account) +{ + sign(jv, account, jv); } void diff --git a/src/test/jtx/multisign.h b/src/test/jtx/multisign.h index 6bcb1a671c..a364d07ca6 100644 --- a/src/test/jtx/multisign.h +++ b/src/test/jtx/multisign.h @@ -96,16 +96,53 @@ public: }; std::vector signers; + SField const* const subField = nullptr; + static constexpr SField* const topLevel = nullptr; public: - msig(std::vector signers_); + msig(SField const* subField_, std::vector signers_); + + msig(SField const& subField_, std::vector signers_) + : msig{&subField_, signers_} + { + } + + msig(std::vector signers_) : msig(topLevel, signers_) + { + } template requires std::convertible_to + explicit msig(SField const* subField_, AccountType&& a0, Accounts&&... aN) + : msig{ + subField_, + std::vector{ + std::forward(a0), + std::forward(aN)...}} + { + } + + template + requires std::convertible_to + explicit msig(SField const& subField_, AccountType&& a0, Accounts&&... aN) + : msig{ + &subField_, + std::vector{ + std::forward(a0), + std::forward(aN)...}} + { + } + + template + requires( + std::convertible_to && + !std::is_same_v) explicit msig(AccountType&& a0, Accounts&&... aN) - : msig{std::vector{ - std::forward(a0), - std::forward(aN)...}} + : msig{ + topLevel, + std::vector{ + std::forward(a0), + std::forward(aN)...}} { } diff --git a/src/test/jtx/sig.h b/src/test/jtx/sig.h index aa65a4f697..c101d9e491 100644 --- a/src/test/jtx/sig.h +++ b/src/test/jtx/sig.h @@ -35,7 +35,10 @@ class sig { private: bool manual_ = true; + /// subField only supported with explicit account + SField const* const subField = nullptr; std::optional account_; + static constexpr SField* const topLevel = nullptr; public: explicit sig(autofill_t) : manual_(false) @@ -46,7 +49,17 @@ public: { } - explicit sig(Account const& account) : account_(account) + explicit sig(SField const* subField_, Account const& account) + : subField(subField_), account_(account) + { + } + + explicit sig(SField const& subField_, Account const& account) + : sig(&subField_, account) + { + } + + explicit sig(Account const& account) : sig(topLevel, account) { } diff --git a/src/test/jtx/utility.h b/src/test/jtx/utility.h index 2c21fbd3ff..9e89c3bb93 100644 --- a/src/test/jtx/utility.h +++ b/src/test/jtx/utility.h @@ -51,6 +51,12 @@ struct parse_error : std::logic_error STObject parse(Json::Value const& jv); +/** Sign automatically into a specific Json field of the jv object. + @note This only works on accounts with multi-signing off. +*/ +void +sign(Json::Value& jv, Account const& account, Json::Value& sigObject); + /** Sign automatically. @note This only works on accounts with multi-signing off. */ diff --git a/src/test/ledger/Invariants_test.cpp b/src/test/ledger/Invariants_test.cpp index 5bb9feb070..10e36f2f21 100644 --- a/src/test/ledger/Invariants_test.cpp +++ b/src/test/ledger/Invariants_test.cpp @@ -159,9 +159,10 @@ class Invariants_test : public beast::unit_test::suite {{"an account root was deleted"}}, [](Account const& A1, Account const&, ApplyContext& ac) { // remove an account from the view - auto const sle = ac.view().peek(keylet::account(A1.id())); + auto sle = ac.view().peek(keylet::account(A1.id())); if (!sle) return false; + sle->at(sfBalance) = beast::zero; ac.view().erase(sle); return true; }); @@ -185,10 +186,12 @@ class Invariants_test : public beast::unit_test::suite {{"account deletion succeeded but deleted multiple accounts"}}, [](Account const& A1, Account const& A2, ApplyContext& ac) { // remove two accounts from the view - auto const sleA1 = ac.view().peek(keylet::account(A1.id())); - auto const sleA2 = ac.view().peek(keylet::account(A2.id())); + auto sleA1 = ac.view().peek(keylet::account(A1.id())); + auto sleA2 = ac.view().peek(keylet::account(A2.id())); if (!sleA1 || !sleA2) return false; + sleA1->at(sfBalance) = beast::zero; + sleA2->at(sfBalance) = beast::zero; ac.view().erase(sleA1); ac.view().erase(sleA2); return true; @@ -203,6 +206,43 @@ class Invariants_test : public beast::unit_test::suite using namespace test::jtx; testcase << "account root deletion left artifact"; + doInvariantCheck( + {{"account deletion left behind a non-zero balance"}}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + // A1 has a balance. Delete A1 + auto const a1 = A1.id(); + auto const sleA1 = ac.view().peek(keylet::account(a1)); + if (!sleA1) + return false; + if (!BEAST_EXPECT(*sleA1->at(sfBalance) != beast::zero)) + return false; + + ac.view().erase(sleA1); + + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_DELETE, [](STObject& tx) {}}); + + doInvariantCheck( + {{"account deletion left behind a non-zero owner count"}}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + // Increment A1's owner count, then delete A1 + auto const a1 = A1.id(); + auto const sleA1 = ac.view().peek(keylet::account(a1)); + if (!sleA1) + return false; + sleA1->at(sfBalance) = beast::zero; + BEAST_EXPECT(sleA1->at(sfOwnerCount) == 0); + adjustOwnerCount(ac.view(), sleA1, 1, ac.journal); + + ac.view().erase(sleA1); + + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_DELETE, [](STObject& tx) {}}); + for (auto const& keyletInfo : directAccountKeylets) { // TODO: Use structured binding once LLVM 16 is the minimum @@ -223,29 +263,32 @@ class Invariants_test : public beast::unit_test::suite // Add an object to the ledger for account A1, then delete // A1 auto const a1 = A1.id(); - auto const sleA1 = ac.view().peek(keylet::account(a1)); + auto sleA1 = ac.view().peek(keylet::account(a1)); if (!sleA1) return false; auto const key = std::invoke(keyletfunc, a1); auto const newSLE = std::make_shared(key); ac.view().insert(newSLE); + sleA1->at(sfBalance) = beast::zero; ac.view().erase(sleA1); return true; }, XRPAmount{}, STTx{ttACCOUNT_DELETE, [](STObject& tx) {}}); - }; + } // NFT special case doInvariantCheck( {{"account deletion left behind a NFTokenPage object"}}, [&](Account const& A1, Account const&, ApplyContext& ac) { // remove an account from the view - auto const sle = ac.view().peek(keylet::account(A1.id())); + auto sle = ac.view().peek(keylet::account(A1.id())); if (!sle) return false; + sle->at(sfBalance) = beast::zero; + sle->at(sfOwnerCount) = 0; ac.view().erase(sle); return true; }, @@ -269,13 +312,15 @@ class Invariants_test : public beast::unit_test::suite [&](Account const& A1, Account const& A2, ApplyContext& ac) { // Delete the AMM account without cleaning up the directory or // deleting the AMM object - auto const sle = ac.view().peek(keylet::account(ammAcctID)); + auto sle = ac.view().peek(keylet::account(ammAcctID)); if (!sle) return false; BEAST_EXPECT(sle->at(~sfAMMID)); BEAST_EXPECT(sle->at(~sfAMMID) == ammKey); + sle->at(sfBalance) = beast::zero; + sle->at(sfOwnerCount) = 0; ac.view().erase(sle); return true; @@ -298,7 +343,7 @@ class Invariants_test : public beast::unit_test::suite // Delete all the AMM's trust lines, remove the AMM from the AMM // account's directory (this deletes the directory), and delete // the AMM account. Do not delete the AMM object. - auto const sle = ac.view().peek(keylet::account(ammAcctID)); + auto sle = ac.view().peek(keylet::account(ammAcctID)); if (!sle) return false; @@ -338,6 +383,8 @@ class Invariants_test : public beast::unit_test::suite !ac.view().exists(ownerDirKeylet) || ac.view().emptyDirDelete(ownerDirKeylet)); + sle->at(sfBalance) = beast::zero; + sle->at(sfOwnerCount) = 0; ac.view().erase(sle); return true; @@ -1301,6 +1348,121 @@ class Invariants_test : public beast::unit_test::suite {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); } + Keylet + createLoanBroker( + jtx::Account const& a, + jtx::Env& env, + jtx::PrettyAsset const& asset) + { + using namespace jtx; + + // Create vault + uint256 vaultID; + Vault vault{env}; + auto [tx, vKeylet] = vault.create({.owner = a, .asset = asset}); + env(tx); + BEAST_EXPECT(env.le(vKeylet)); + + vaultID = vKeylet.key; + + // Create Loan Broker + using namespace loanBroker; + + auto const loanBrokerKeylet = keylet::loanbroker(a.id(), env.seq(a)); + // Create a Loan Broker with all default values. + env(set(a, vaultID), fee(increment)); + + return loanBrokerKeylet; + }; + + void + testNoModifiedUnmodifiableFields() + { + testcase("no modified unmodifiable fields"); + using namespace jtx; + + // Initialize with a placeholder value because there's no default ctor + Keylet loanBrokerKeylet = keylet::amendments(); + Preclose createLoanBroker = + [&, this](Account const& a, Account const& b, Env& env) { + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + + loanBrokerKeylet = this->createLoanBroker(a, env, xrpAsset); + return BEAST_EXPECT(env.le(loanBrokerKeylet)); + }; + + { + auto const mods = + std::to_array>({ + [](SLE::pointer& sle) { sle->at(sfSequence) += 1; }, + [](SLE::pointer& sle) { sle->at(sfOwnerNode) += 1; }, + [](SLE::pointer& sle) { sle->at(sfVaultNode) += 1; }, + [](SLE::pointer& sle) { sle->at(sfVaultID) = uint256(1u); }, + [](SLE::pointer& sle) { + sle->at(sfAccount) = sle->at(sfOwner); + }, + [](SLE::pointer& sle) { + sle->at(sfOwner) = sle->at(sfAccount); + }, + [](SLE::pointer& sle) { + sle->at(sfManagementFeeRate) += 1; + }, + [](SLE::pointer& sle) { sle->at(sfCoverRateMinimum) += 1; }, + [](SLE::pointer& sle) { + sle->at(sfCoverRateLiquidation) += 1; + }, + [](SLE::pointer& sle) { sle->at(sfLedgerEntryType) += 1; }, + [](SLE::pointer& sle) { + sle->at(sfLedgerIndex) = sle->at(sfVaultID).value(); + }, + }); + + for (auto const& mod : mods) + { + doInvariantCheck( + {{"changed an unchangable field"}}, + [&](Account const& A1, Account const&, ApplyContext& ac) { + auto sle = ac.view().peek(loanBrokerKeylet); + if (!sle) + return false; + mod(sle); + ac.view().update(sle); + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + createLoanBroker); + } + } + + // TODO: Loan Object + + { + auto const mods = + std::to_array>({ + [](SLE::pointer& sle) { sle->at(sfLedgerEntryType) += 1; }, + [](SLE::pointer& sle) { + sle->at(sfLedgerIndex) = uint256(1u); + }, + }); + + for (auto const& mod : mods) + { + doInvariantCheck( + {{"changed an unchangable field"}}, + [&](Account const& A1, Account const&, ApplyContext& ac) { + auto sle = ac.view().peek(keylet::account(A1.id())); + if (!sle) + return false; + mod(sle); + ac.view().update(sle); + return true; + }); + } + } + } + void testValidPseudoAccounts() { @@ -1354,7 +1516,6 @@ class Invariants_test : public beast::unit_test::suite sle->at(~sfAMMID) = ~sle->at(~sfVaultID); }, }, - /* { "pseudo-account has 2 pseudo-account fields set", [this](SLE::pointer& sle) { @@ -1363,7 +1524,6 @@ class Invariants_test : public beast::unit_test::suite sle->at(~sfLoanBrokerID) = ~sle->at(~sfVaultID); }, }, - */ { "pseudo-account sequence changed", [](SLE::pointer& sle) { sle->at(sfSequence) = 12345; }, @@ -1413,6 +1573,245 @@ class Invariants_test : public beast::unit_test::suite return true; }); } + + void + testValidLoanBroker() + { + testcase << "valid loan broker"; + + using namespace jtx; + + enum class Asset { XRP, IOU, MPT }; + auto const assetTypes = + std::to_array({Asset::XRP, Asset::IOU, Asset::MPT}); + + for (auto const assetType : assetTypes) + { + // Initialize with a placeholder value because there's no default + // ctor + Keylet loanBrokerKeylet = keylet::amendments(); + Preclose createLoanBroker = [&, this]( + Account const& alice, + Account const& issuer, + Env& env) { + PrettyAsset const asset = [&]() { + switch (assetType) + { + case Asset::IOU: { + PrettyAsset const iouAsset = issuer["IOU"]; + env(trust(alice, iouAsset(1000))); + env(pay(issuer, alice, iouAsset(1000))); + env.close(); + return iouAsset; + } + + case Asset::MPT: { + MPTTester mptt{env, issuer, mptInitNoFund}; + mptt.create( + {.flags = tfMPTCanClawback | tfMPTCanTransfer | + tfMPTCanLock}); + PrettyAsset const mptAsset = mptt.issuanceID(); + mptt.authorize({.account = alice}); + env(pay(issuer, alice, mptAsset(1000))); + env.close(); + return mptAsset; + } + + case Asset::XRP: + default: + return PrettyAsset{xrpIssue(), 1'000'000}; + } + }(); + loanBrokerKeylet = this->createLoanBroker(alice, env, asset); + return BEAST_EXPECT(env.le(loanBrokerKeylet)); + }; + + // Ensure the test scenarios are set up completely. The test cases + // will need to recompute any of these values it needs for itself + // rather than trying to return a bunch of items + auto setupTest = + [&, this](Account const& A1, Account const&, ApplyContext& ac) + -> std::optional> { + if (loanBrokerKeylet.type != ltLOAN_BROKER) + return {}; + auto sleBroker = ac.view().peek(loanBrokerKeylet); + if (!sleBroker) + return {}; + if (!BEAST_EXPECT(sleBroker->at(sfOwnerCount) == 0)) + return {}; + // Need to touch sleBroker so that it is included in the + // modified entries for the invariant to find + ac.view().update(sleBroker); + + // The pseudo-account holds the directory, so get it + auto const pseudoAccountID = sleBroker->at(sfAccount); + auto const pseudoAccountKeylet = + keylet::account(pseudoAccountID); + // Strictly speaking, we don't need to load the + // ACCOUNT_ROOT, but check anyway + auto slePseudo = ac.view().peek(pseudoAccountKeylet); + if (!BEAST_EXPECT(slePseudo)) + return {}; + // Make sure the directory doesn't already exist + auto const dirKeylet = keylet::ownerDir(pseudoAccountID); + auto sleDir = ac.view().peek(dirKeylet); + auto const describe = describeOwnerDir(pseudoAccountID); + if (!sleDir) + { + // Create the directory + BEAST_EXPECT( + directory::createRoot( + ac.view(), + dirKeylet, + loanBrokerKeylet.key, + describe) == 0); + + sleDir = ac.view().peek(dirKeylet); + } + + return std::make_pair(slePseudo, sleDir); + }; + + doInvariantCheck( + {{"Loan Broker with zero OwnerCount has multiple directory " + "pages"}}, + [&setupTest, this]( + Account const& A1, Account const& A2, ApplyContext& ac) { + auto test = setupTest(A1, A2, ac); + if (!test || !test->first || !test->second) + return false; + + auto slePseudo = test->first; + auto sleDir = test->second; + auto const describe = + describeOwnerDir(slePseudo->at(sfAccount)); + + BEAST_EXPECT( + directory::insertPage( + ac.view(), + 0, + sleDir, + 0, + sleDir, + slePseudo->key(), + keylet::page(sleDir->key(), 0), + describe) == 1); + + return true; + }, + XRPAmount{}, + STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + createLoanBroker); + + doInvariantCheck( + {{"Loan Broker with zero OwnerCount has multiple indexes in " + "the Directory root"}}, + [&setupTest]( + Account const& A1, Account const& A2, ApplyContext& ac) { + auto test = setupTest(A1, A2, ac); + if (!test || !test->first || !test->second) + return false; + + auto slePseudo = test->first; + auto sleDir = test->second; + auto indexes = sleDir->getFieldV256(sfIndexes); + + // Put some extra garbage into the directory + for (auto const& key : {slePseudo->key(), sleDir->key()}) + { + directory::insertKey( + ac.view(), sleDir, 0, false, indexes, key); + } + + return true; + }, + XRPAmount{}, + STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + createLoanBroker); + + doInvariantCheck( + {{"Loan Broker directory corrupt"}}, + [&setupTest]( + Account const& A1, Account const& A2, ApplyContext& ac) { + auto test = setupTest(A1, A2, ac); + if (!test || !test->first || !test->second) + return false; + + auto slePseudo = test->first; + auto sleDir = test->second; + auto const describe = + describeOwnerDir(slePseudo->at(sfAccount)); + // Empty vector will overwrite the existing entry for the + // holding, if any, avoiding the "has multiple indexes" + // failure. + STVector256 indexes; + + // Put one meaningless key into the directory + auto const key = + keylet::account(Account("random").id()).key; + directory::insertKey( + ac.view(), sleDir, 0, false, indexes, key); + + return true; + }, + XRPAmount{}, + STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + createLoanBroker); + + doInvariantCheck( + {{"Loan Broker with zero OwnerCount has an unexpected entry in " + "the directory"}}, + [&setupTest]( + Account const& A1, Account const& A2, ApplyContext& ac) { + auto test = setupTest(A1, A2, ac); + if (!test || !test->first || !test->second) + return false; + + auto slePseudo = test->first; + auto sleDir = test->second; + // Empty vector will overwrite the existing entry for the + // holding, if any, avoiding the "has multiple indexes" + // failure. + STVector256 indexes; + + directory::insertKey( + ac.view(), sleDir, 0, false, indexes, slePseudo->key()); + + return true; + }, + XRPAmount{}, + STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + createLoanBroker); + + doInvariantCheck( + {{"Loan Broker sequence number decreased"}}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + if (loanBrokerKeylet.type != ltLOAN_BROKER) + return false; + auto sleBroker = ac.view().peek(loanBrokerKeylet); + if (!sleBroker) + return false; + if (!BEAST_EXPECT(sleBroker->at(sfLoanSequence) > 0)) + return false; + // Need to touch sleBroker so that it is included in the + // modified entries for the invariant to find + ac.view().update(sleBroker); + + sleBroker->at(sfLoanSequence) -= 1; + + return true; + }, + XRPAmount{}, + STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + createLoanBroker); + } + } + public: void run() override @@ -1431,7 +1830,9 @@ public: testValidNewAccountRoot(); testNFTokenPageInvariants(); testPermissionedDomainInvariants(); + testNoModifiedUnmodifiableFields(); testValidPseudoAccounts(); + testValidLoanBroker(); } }; diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h new file mode 100644 index 0000000000..2b51f3d14d --- /dev/null +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -0,0 +1,618 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_APP_MISC_LENDINGHELPERS_H_INCLUDED +#define RIPPLE_APP_MISC_LENDINGHELPERS_H_INCLUDED + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace ripple { + +struct PreflightContext; + +// Lending protocol has dependencies, so capture them here. +bool +lendingProtocolEnabled(PreflightContext const& ctx); + +namespace detail { +// These functions should rarely be used directly. More often, the ultimate +// result needs to be roundToAsset'd. + +struct LoanPaymentParts +{ + Number principalPaid; + Number interestPaid; + Number valueChange; + Number feePaid; +}; + +Number +loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval); + +Number +loanPeriodicPayment( + Number principalOutstanding, + Number periodicRate, + std::uint32_t paymentsRemaining); + +Number +loanPeriodicPayment( + Number principalOutstanding, + TenthBips32 interestRate, + std::uint32_t paymentInterval, + std::uint32_t paymentsRemaining); + +template +Number +loanTotalValueOutstanding( + A asset, + Number const& originalPrincipal, + Number const& periodicPayment, + std::uint32_t paymentsRemaining) +{ + return roundToAsset( + asset, + periodicPayment * paymentsRemaining, + originalPrincipal, + Number::upward); +} + +template +Number +loanTotalValueOutstanding( + A asset, + Number const& originalPrincipal, + Number const& principalOutstanding, + TenthBips32 interestRate, + std::uint32_t paymentInterval, + std::uint32_t paymentsRemaining) +{ + return loanTotalValueOutstanding( + asset, + originalPrincipal, + loanPeriodicPayment( + principalOutstanding, + interestRate, + paymentInterval, + paymentsRemaining), + paymentsRemaining); +} + +inline Number +loanTotalInterestOutstanding( + Number principalOutstanding, + Number totalValueOutstanding) +{ + return totalValueOutstanding - principalOutstanding; +} + +template +Number +loanTotalInterestOutstanding( + A asset, + Number const& originalPrincipal, + Number const& principalOutstanding, + TenthBips32 interestRate, + std::uint32_t paymentInterval, + std::uint32_t paymentsRemaining) +{ + return loanTotalInterestOutstanding( + principalOutstanding, + loanTotalValueOutstanding( + asset, + originalPrincipal, + principalOutstanding, + interestRate, + paymentInterval, + paymentsRemaining)); +} + +Number +loanLatePaymentInterest( + Number principalOutstanding, + TenthBips32 lateInterestRate, + NetClock::time_point parentCloseTime, + std::uint32_t startDate, + std::uint32_t prevPaymentDate); + +Number +loanAccruedInterest( + Number principalOutstanding, + Number periodicRate, + NetClock::time_point parentCloseTime, + std::uint32_t startDate, + std::uint32_t prevPaymentDate, + std::uint32_t paymentInterval); + +struct PeriodicPayment +{ + Number interest; + Number principal; +}; + +template +PeriodicPayment +computePeriodicPaymentParts( + A const& asset, + Number const& originalPrincipal, + Number const& principalOutstanding, + Number const& periodicPaymentAmount, + Number const& periodicRate, + std::uint32_t paymentRemaining) +{ + if (paymentRemaining == 1) + { + // If there's only one payment left, we need to pay off the principal. + Number const interest = roundToAsset( + asset, + periodicPaymentAmount - principalOutstanding, + originalPrincipal); + return {interest, principalOutstanding}; + } + Number const interest = roundToAsset( + asset, principalOutstanding * periodicRate, originalPrincipal); + XRPL_ASSERT( + interest >= 0, + "ripple::detail::computePeriodicPayment : valid interest"); + + auto const roundedPayment = + roundToAsset(asset, periodicPaymentAmount, originalPrincipal); + Number const principal = + roundToAsset(asset, roundedPayment - interest, originalPrincipal); + XRPL_ASSERT( + principal > 0 && principal <= principalOutstanding, + "ripple::detail::computePeriodicPayment : valid principal"); + + return {interest, principal}; +} + +inline Number +minusManagementFee(Number value, TenthBips32 managementFeeRate) +{ + return tenthBipsOfValue(value, tenthBipsPerUnity - managementFeeRate); +} + +} // namespace detail + +template +Number +valueMinusManagementFee( + A const& asset, + Number const& value, + TenthBips32 managementFeeRate, + Number const& originalPrincipal) +{ + return roundToAsset( + asset, + detail::minusManagementFee(value, managementFeeRate), + originalPrincipal); +} + +template +Number +loanInterestOutstandingMinusFee( + A const& asset, + Number const& originalPrincipal, + Number const& principalOutstanding, + TenthBips32 interestRate, + std::uint32_t paymentInterval, + std::uint32_t paymentsRemaining, + TenthBips32 managementFeeRate) +{ + return valueMinusManagementFee( + asset, + detail::loanTotalInterestOutstanding( + asset, + originalPrincipal, + principalOutstanding, + interestRate, + paymentInterval, + paymentsRemaining), + managementFeeRate, + originalPrincipal); +} + +template +Number +loanPeriodicPayment( + A const& asset, + Number const& principalOutstanding, + TenthBips32 interestRate, + std::uint32_t paymentInterval, + std::uint32_t paymentsRemaining, + Number const& originalPrincipal) +{ + return roundToAsset( + asset, + detail::loanPeriodicPayment( + principalOutstanding, + interestRate, + paymentInterval, + paymentsRemaining), + originalPrincipal); +} + +template +Number +loanLatePaymentInterest( + A const& asset, + Number const& principalOutstanding, + TenthBips32 lateInterestRate, + NetClock::time_point parentCloseTime, + std::uint32_t startDate, + std::uint32_t prevPaymentDate, + Number const& originalPrincipal) +{ + return roundToAsset( + asset, + detail::loanLatePaymentInterest( + principalOutstanding, + lateInterestRate, + parentCloseTime, + startDate, + prevPaymentDate), + originalPrincipal); +} + +struct LoanPaymentParts +{ + Number principalPaid; + Number interestPaid; + Number valueChange; + Number feePaid; +}; + +template +Expected +loanComputePaymentParts( + A const& asset, + ApplyView& view, + SLE::ref loan, + STAmount const& amount, + beast::Journal j) +{ + Number const originalPrincipalRequested = loan->at(sfPrincipalRequested); + auto principalOutstandingField = loan->at(sfPrincipalOutstanding); + bool const allowOverpayment = loan->isFlag(lsfLoanOverpayment); + + TenthBips32 const interestRate{loan->at(sfInterestRate)}; + TenthBips32 const lateInterestRate{loan->at(sfLateInterestRate)}; + TenthBips32 const closeInterestRate{loan->at(sfCloseInterestRate)}; + TenthBips32 const overpaymentInterestRate{ + loan->at(sfOverpaymentInterestRate)}; + + Number const serviceFee = loan->at(sfLoanServiceFee); + Number const latePaymentFee = loan->at(sfLatePaymentFee); + Number const closePaymentFee = roundToAsset( + asset, loan->at(sfClosePaymentFee), originalPrincipalRequested); + TenthBips32 const overpaymentFee{loan->at(sfOverpaymentFee)}; + + std::uint32_t const paymentInterval = loan->at(sfPaymentInterval); + auto paymentRemainingField = loan->at(sfPaymentRemaining); + + auto prevPaymentDateField = loan->at(sfPreviousPaymentDate); + std::uint32_t const startDate = loan->at(sfStartDate); + auto nextDueDateField = loan->at(sfNextPaymentDueDate); + + if (paymentRemainingField == 0 || principalOutstandingField == 0) + { + // Loan complete + JLOG(j.warn()) << "Loan is already paid off."; + return Unexpected(tecKILLED); + } + + // Compute the normal periodic rate, payment, etc. + // We'll need it in the remaining calculations + Number const periodicRate = + detail::loanPeriodicRate(interestRate, paymentInterval); + XRPL_ASSERT( + periodicRate > 0, "ripple::loanComputePaymentParts : valid rate"); + + // Don't round the payment amount. Only round the final computations using + // it. + Number const periodicPaymentAmount = detail::loanPeriodicPayment( + principalOutstandingField, periodicRate, paymentRemainingField); + XRPL_ASSERT( + periodicPaymentAmount > 0, + "ripple::computePeriodicPayment : valid payment"); + + auto const periodic = detail::computePeriodicPaymentParts( + asset, + originalPrincipalRequested, + principalOutstandingField, + periodicPaymentAmount, + periodicRate, + paymentRemainingField); + + Number const totalValueOutstanding = detail::loanTotalValueOutstanding( + asset, + originalPrincipalRequested, + periodicPaymentAmount, + paymentRemainingField); + XRPL_ASSERT( + totalValueOutstanding > 0, + "ripple::loanComputePaymentParts : valid total value"); + Number const totalInterestOutstanding = + detail::loanTotalInterestOutstanding( + principalOutstandingField, totalValueOutstanding); + XRPL_ASSERT_PARTS( + totalInterestOutstanding >= 0, + "ripple::loanComputePaymentParts", + "valid total interest"); + XRPL_ASSERT_PARTS( + totalValueOutstanding - totalInterestOutstanding == + principalOutstandingField, + "ripple::loanComputePaymentParts", + "valid principal computation"); + + view.update(loan); + + // ------------------------------------------------------------- + // late payment handling + if (hasExpired(view, nextDueDateField)) + { + // the payment is late + auto const latePaymentInterest = loanLatePaymentInterest( + asset, + principalOutstandingField, + lateInterestRate, + view.parentCloseTime(), + startDate, + prevPaymentDateField, + originalPrincipalRequested); + XRPL_ASSERT( + latePaymentInterest >= 0, + "ripple::loanComputePaymentParts : valid late interest"); + auto const latePaymentAmount = + periodicPaymentAmount + latePaymentInterest + latePaymentFee; + + if (amount < latePaymentAmount) + { + JLOG(j.warn()) << "Late loan payment amount is insufficient. Due: " + << latePaymentAmount << ", paid: " << amount; + return Unexpected(tecINSUFFICIENT_PAYMENT); + } + + paymentRemainingField -= 1; + // A single payment always pays the same amount of principal. Only the + // interest and fees are extra for a late payment + principalOutstandingField -= periodic.principal; + + // Make sure this does an assignment + prevPaymentDateField = nextDueDateField; + nextDueDateField += paymentInterval; + + // A late payment increases the value of the loan by the difference + // between periodic and late payment interest + return LoanPaymentParts{ + periodic.principal, + latePaymentInterest + periodic.interest, + latePaymentInterest, + latePaymentFee}; + } + + // ------------------------------------------------------------- + // full payment handling + if (paymentRemainingField > 1) + { + // If there is more than one payment remaining, see if enough was paid + // for a full payment + auto const accruedInterest = roundToAsset( + asset, + detail::loanAccruedInterest( + principalOutstandingField, + periodicRate, + view.parentCloseTime(), + startDate, + prevPaymentDateField, + paymentInterval), + originalPrincipalRequested); + XRPL_ASSERT( + accruedInterest >= 0, + "ripple::loanComputePaymentParts : valid accrued interest"); + auto const closePrepaymentInterest = roundToAsset( + asset, + tenthBipsOfValue( + principalOutstandingField.value(), closeInterestRate), + originalPrincipalRequested); + XRPL_ASSERT( + closePrepaymentInterest >= 0, + "ripple::loanComputePaymentParts : valid prepayment " + "interest"); + auto const totalInterest = accruedInterest + closePrepaymentInterest; + auto const closeFullPayment = + principalOutstandingField + totalInterest + closePaymentFee; + + // if the payment is equal or higher than full payment amount, make a + // full payment + if (amount >= closeFullPayment) + { + // A full payment decreases the value of the loan by the + // difference between the interest paid and the expected + // outstanding interest return + auto const valueChange = totalInterest - totalInterestOutstanding; + + LoanPaymentParts const result{ + principalOutstandingField, + totalInterest, + valueChange, + closePaymentFee}; + + paymentRemainingField = 0; + principalOutstandingField = 0; + + return result; + } + } + + // ------------------------------------------------------------- + // normal payment handling + + // if the payment is not late nor if it's a full payment, then it must be a + // periodic one, with possible overpayments + + auto const totalDue = roundToAsset( + asset, + periodicPaymentAmount + serviceFee, + originalPrincipalRequested, + Number::upward); + + std::optional mg(Number::downward); + std::int64_t const fullPeriodicPayments = [&]() { + std::int64_t const full{amount / totalDue}; + return full < paymentRemainingField ? full : paymentRemainingField; + }(); + mg.reset(); + // Temporary asserts + XRPL_ASSERT( + amount >= totalDue || fullPeriodicPayments == 0, + "temp full periodic rounding"); + XRPL_ASSERT( + amount < totalDue || fullPeriodicPayments >= 1, + "temp full periodic rounding"); + + if (fullPeriodicPayments < 1) + { + JLOG(j.warn()) << "Periodic loan payment amount is insufficient. Due: " + << periodicPaymentAmount << ", paid: " << amount; + return Unexpected(tecINSUFFICIENT_PAYMENT); + } + + nextDueDateField += paymentInterval * fullPeriodicPayments; + prevPaymentDateField = nextDueDateField - paymentInterval; + + Number totalPrincipalPaid = 0; + Number totalInterestPaid = 0; + Number loanValueChange = 0; + + std::optional future = periodic; + for (int i = 0; i < fullPeriodicPayments; ++i) + { + // Only do the work if we need to + if (!future) + future = detail::computePeriodicPaymentParts( + asset, + originalPrincipalRequested, + principalOutstandingField, + periodicPaymentAmount, + periodicRate, + paymentRemainingField); + XRPL_ASSERT( + future->interest <= periodic.interest, + "ripple::loanComputePaymentParts : decreasing interest"); + XRPL_ASSERT( + future->principal >= periodic.principal, + "ripple::loanComputePaymentParts : increasing principal"); + + totalPrincipalPaid += future->principal; + totalInterestPaid += future->interest; + paymentRemainingField -= 1; + principalOutstandingField -= future->principal; + + future.reset(); + } + + Number totalFeePaid = serviceFee * fullPeriodicPayments; + + Number const newInterest = detail::loanTotalInterestOutstanding( + asset, + originalPrincipalRequested, + principalOutstandingField, + interestRate, + paymentInterval, + paymentRemainingField) + + totalInterestPaid; + + Number overpaymentInterestPortion = 0; + if (allowOverpayment) + { + Number const overpayment = std::min( + principalOutstandingField.value(), + amount - (totalPrincipalPaid + totalInterestPaid + totalFeePaid)); + + if (roundToAsset(asset, overpayment, originalPrincipalRequested) > 0) + { + Number const interestPortion = roundToAsset( + asset, + tenthBipsOfValue(overpayment, overpaymentInterestRate), + originalPrincipalRequested); + Number const feePortion = roundToAsset( + asset, + tenthBipsOfValue(overpayment, overpaymentFee), + originalPrincipalRequested); + Number const remainder = roundToAsset( + asset, + overpayment - interestPortion - feePortion, + originalPrincipalRequested); + + // Don't process an overpayment if the whole amount (or more!) gets + // eaten by fees + if (remainder > 0) + { + overpaymentInterestPortion = interestPortion; + totalPrincipalPaid += remainder; + totalInterestPaid += interestPortion; + totalFeePaid += feePortion; + + principalOutstandingField -= remainder; + } + } + } + + loanValueChange = + (newInterest - totalInterestOutstanding) + overpaymentInterestPortion; + + // Check the final results are rounded, to double-check that the + // intermediate steps were rounded. + XRPL_ASSERT( + roundToAsset(asset, totalPrincipalPaid, originalPrincipalRequested) == + totalPrincipalPaid, + "ripple::loanComputePaymentParts : totalPrincipalPaid rounded"); + XRPL_ASSERT( + roundToAsset(asset, totalInterestPaid, originalPrincipalRequested) == + totalInterestPaid, + "ripple::loanComputePaymentParts : totalInterestPaid rounded"); + XRPL_ASSERT( + roundToAsset(asset, loanValueChange, originalPrincipalRequested) == + loanValueChange, + "ripple::loanComputePaymentParts : loanValueChange rounded"); + XRPL_ASSERT( + roundToAsset(asset, totalFeePaid, originalPrincipalRequested) == + totalFeePaid, + "ripple::loanComputePaymentParts : totalFeePaid rounded"); + return LoanPaymentParts{ + totalPrincipalPaid, totalInterestPaid, loanValueChange, totalFeePaid}; +} + +} // namespace ripple + +#endif // RIPPLE_APP_MISC_LENDINGHELPERS_H_INCLUDED diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp new file mode 100644 index 0000000000..ecdcaa13ee --- /dev/null +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -0,0 +1,111 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +// +#include +#include + +namespace ripple { + +bool +lendingProtocolEnabled(PreflightContext const& ctx) +{ + return ctx.rules.enabled(featureLendingProtocol) && + VaultCreate::isEnabled(ctx); +} + +namespace detail { + +Number +loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval) +{ + // Need floating point math for this one, since we're dividing by some + // large numbers + return tenthBipsOfValue(Number(paymentInterval), interestRate) / + (365 * 24 * 60 * 60); +} + +Number +loanPeriodicPayment( + Number principalOutstanding, + Number periodicRate, + std::uint32_t paymentsRemaining) +{ + // TODO: Need a better name + Number const timeFactor = power(1 + periodicRate, paymentsRemaining); + + return principalOutstanding * periodicRate * timeFactor / (timeFactor - 1); +} + +Number +loanPeriodicPayment( + Number principalOutstanding, + TenthBips32 interestRate, + std::uint32_t paymentInterval, + std::uint32_t paymentsRemaining) +{ + if (principalOutstanding == 0 || paymentsRemaining == 0) + return 0; + Number const periodicRate = loanPeriodicRate(interestRate, paymentInterval); + + return loanPeriodicPayment( + principalOutstanding, periodicRate, paymentsRemaining); +} + +Number +loanLatePaymentInterest( + Number principalOutstanding, + TenthBips32 lateInterestRate, + NetClock::time_point parentCloseTime, + std::uint32_t startDate, + std::uint32_t prevPaymentDate) +{ + auto const lastPaymentDate = std::max(prevPaymentDate, startDate); + + auto const secondsSinceLastPayment = + parentCloseTime.time_since_epoch().count() - lastPaymentDate; + + auto const rate = + loanPeriodicRate(lateInterestRate, secondsSinceLastPayment); + + return principalOutstanding * rate; +} + +Number +loanAccruedInterest( + Number principalOutstanding, + Number periodicRate, + NetClock::time_point parentCloseTime, + std::uint32_t startDate, + std::uint32_t prevPaymentDate, + std::uint32_t paymentInterval) +{ + auto const lastPaymentDate = std::max(prevPaymentDate, startDate); + + auto const secondsSinceLastPayment = + parentCloseTime.time_since_epoch().count() - lastPaymentDate; + + return principalOutstanding * periodicRate * secondsSinceLastPayment / + paymentInterval; +} + +} // namespace detail + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/Change.cpp b/src/xrpld/app/tx/detail/Change.cpp index f1e95a376f..8d4a42e50d 100644 --- a/src/xrpld/app/tx/detail/Change.cpp +++ b/src/xrpld/app/tx/detail/Change.cpp @@ -38,7 +38,11 @@ NotTEC Transactor::preflight(PreflightContext const& ctx) { // 0 means "Allow any flags" - if (auto const ret = preflight0(ctx, 0)) + // The check for tfChangeMask is gated by LendingProtocol because that + // feature introduced this parameter, and it's not worth adding another + // amendment just for this. + if (auto const ret = preflight0( + ctx, ctx.rules.enabled(featureLendingProtocol) ? tfChangeMask : 0)) return ret; auto account = ctx.tx.getAccountID(sfAccount); diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 1a707e8496..284c5f27eb 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -415,10 +415,10 @@ void AccountRootsDeletedClean::visitEntry( bool isDelete, std::shared_ptr const& before, - std::shared_ptr const&) + std::shared_ptr const& after) { if (isDelete && before && before->getType() == ltACCOUNT_ROOT) - accountsDeleted_.emplace_back(before); + accountsDeleted_.emplace_back(before, after); } bool @@ -434,7 +434,8 @@ AccountRootsDeletedClean::finalize( // feature is enabled. Enabled, or not, though, a fatal-level message will // be logged [[maybe_unused]] bool const enforce = - view.rules().enabled(featureInvariantsV1_1); + view.rules().enabled(featureInvariantsV1_1) || + view.rules().enabled(featureLendingProtocol); auto const objectExists = [&view, enforce, &j](auto const& keylet) { (void)enforce; @@ -462,9 +463,33 @@ AccountRootsDeletedClean::finalize( return false; }; - for (auto const& accountSLE : accountsDeleted_) + for (auto const& [before, after] : accountsDeleted_) { - auto const accountID = accountSLE->getAccountID(sfAccount); + auto const accountID = before->getAccountID(sfAccount); + // An account should not be deleted with a balance + if (after->at(sfBalance) != beast::zero) + { + JLOG(j.fatal()) << "Invariant failed: account deletion left " + "behind a non-zero balance"; + XRPL_ASSERT( + enforce, + "ripple::AccountRootsDeletedClean::finalize : " + "deleted account has zero balance"); + if (enforce) + return false; + } + // An account should not be deleted with a non-zero owner count + if (after->at(sfOwnerCount) != 0) + { + JLOG(j.fatal()) << "Invariant failed: account deletion left " + "behind a non-zero owner count"; + XRPL_ASSERT( + enforce, + "ripple::AccountRootsDeletedClean::finalize : " + "deleted account has zero owner count"); + if (enforce) + return false; + } // Simple types for (auto const& [keyletfunc, _, __] : directAccountKeylets) { @@ -490,9 +515,9 @@ AccountRootsDeletedClean::finalize( // Keys directly stored in the AccountRoot object for (auto const& field : getPseudoAccountFields()) { - if (accountSLE->isFieldPresent(*field)) + if (before->isFieldPresent(*field)) { - auto const key = accountSLE->getFieldH256(*field); + auto const key = before->getFieldH256(*field); if (objectExists(keylet::unchecked(key)) && enforce) return false; } @@ -949,7 +974,9 @@ ValidNewAccountRoot::finalize( result == tesSUCCESS) { bool const pseudoAccount = - (pseudoAccount_ && view.rules().enabled(featureSingleAssetVault)); + (pseudoAccount_ && + (view.rules().enabled(featureSingleAssetVault) || + view.rules().enabled(featureLendingProtocol))); if (pseudoAccount && !checkMyPrivilege(tx, createPseudoAcct)) { @@ -1433,6 +1460,12 @@ ValidMPTIssuance::finalize( "succeeded but deleted issuances"; return false; } + else if (mptokensCreated_ + mptokensDeleted_ > 1) + { + JLOG(j.fatal()) << "Invariant failed: MPT authorize succeeded " + "but created/deleted bad number mptokens"; + return false; + } else if ( submittedByIssuer && (mptokensCreated_ > 0 || mptokensDeleted_ > 0)) @@ -1609,6 +1642,122 @@ ValidPermissionedDomain::finalize( //------------------------------------------------------------------------------ +void +NoModifiedUnmodifiableFields::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (isDelete || !before) + // Creation and deletion are ignored + return; + + 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) { + bool const beforeField = before->isFieldPresent(field); + bool const afterField = after->isFieldPresent(field); + return beforeField != afterField || + (afterField && 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, sfLedgerEntryType) || + fieldChanged(before, after, sfLedgerIndex) || + fieldChanged(before, after, sfSequence) || + fieldChanged(before, after, sfOwnerNode) || + fieldChanged(before, after, sfVaultNode) || + 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, sfLedgerEntryType) || + fieldChanged(before, after, sfLedgerIndex) || + fieldChanged(before, after, sfSequence) || + fieldChanged(before, after, sfOwnerNode) || + fieldChanged(before, after, sfLoanBrokerNode) || + 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, sfStartDate) || + fieldChanged(before, after, sfPaymentInterval) || + fieldChanged(before, after, sfGracePeriod) || + fieldChanged(before, after, sfPrincipalRequested); + break; + default: + /* + * We check this invariant regardless of lending protocol + * amendment status, allowing for detection and logging of + * potential issues even when the amendment is disabled. + * + * We use the lending protocol as a gate, even though + * all transactions are affected because that's when it + * was added. + */ + enforce = view.rules().enabled(featureLendingProtocol); + bad = fieldChanged(before, after, sfLedgerEntryType) || + fieldChanged(before, after, sfLedgerIndex); + } + 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; +} + +//------------------------------------------------------------------------------ + void ValidPseudoAccounts::visitEntry( bool isDelete, @@ -1701,4 +1850,170 @@ ValidPseudoAccounts::finalize( return true; } +//------------------------------------------------------------------------------ + +void +ValidLoanBroker::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (after && after->getType() == ltLOAN_BROKER) + { + brokers_.emplace_back(before, after); + } +} + +bool +ValidLoanBroker::goodZeroDirectory( + ReadView const& view, + SLE::const_ref dir, + beast::Journal const& j) const +{ + auto const next = dir->at(~sfIndexNext); + auto const prev = dir->at(~sfIndexPrevious); + if ((prev && *prev) || (next && *next)) + { + JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero " + "OwnerCount has multiple directory pages"; + return false; + } + auto indexes = dir->getFieldV256(sfIndexes); + if (indexes.size() > 1) + { + JLOG(j.fatal()) + << "Invariant failed: Loan Broker with zero " + "OwnerCount has multiple indexes in the Directory root"; + return false; + } + if (indexes.size() == 1) + { + auto const index = indexes.value().front(); + auto const sle = view.read(keylet::unchecked(index)); + if (!sle) + { + JLOG(j.fatal()) + << "Invariant failed: Loan Broker directory corrupt"; + return false; + } + if (sle->getType() != ltRIPPLE_STATE && sle->getType() != ltMPTOKEN) + { + JLOG(j.fatal()) + << "Invariant failed: Loan Broker with zero " + "OwnerCount has an unexpected entry in the directory"; + return false; + } + } + + return true; +} + +bool +ValidLoanBroker::finalize( + STTx const& tx, + TER const, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + // Loan Brokers will not exist on ledger if the Lending Protocol amendment + // is not enabled, so there's no need to check it. + + for (auto const& [before, after] : brokers_) + { + // https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3123-invariants + // If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most + // one node (the root), which will only hold entries for `RippleState` + // or `MPToken` objects. + if (after->at(sfOwnerCount) == 0) + { + auto const dir = view.read(keylet::ownerDir(after->at(sfAccount))); + if (dir) + { + if (!goodZeroDirectory(view, dir, j)) + { + return false; + } + } + } + if (before && before->at(sfLoanSequence) > after->at(sfLoanSequence)) + { + JLOG(j.fatal()) << "Invariant failed: Loan Broker sequence number " + "decreased"; + return false; + } + if (after->at(sfDebtTotal) < 0) + { + JLOG(j.fatal()) + << "Invariant failed: Loan Broker debt total is negative"; + return false; + } + if (after->at(sfCoverAvailable) < 0) + { + JLOG(j.fatal()) + << "Invariant failed: Loan Broker cover available is negative"; + return false; + } + } + return true; +} + +//------------------------------------------------------------------------------ + +void +ValidLoan::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (after && after->getType() == ltLOAN) + { + loans_.emplace_back(before, after); + } +} + +bool +ValidLoan::finalize( + STTx const& tx, + TER const, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + // Loan Brokers will not exist on ledger if the Lending Protocol amendment + // is not enabled, so there's no need to check it. + + for (auto const& [before, after] : loans_) + { + // https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3223-invariants + // If `Loan.PaymentRemaining = 0` then `Loan.PrincipalOutstanding = 0` + if (after->at(sfPaymentRemaining) == 0 && + after->at(sfPrincipalOutstanding) != 0) + { + return false; + } + if (before && + (before->isFlag(lsfLoanOverpayment) != + after->isFlag(lsfLoanOverpayment))) + { + JLOG(j.fatal()) + << "Invariant failed: Loan Overpayment flag changed"; + return false; + } + if (after->at(sfAssetsAvailable) < 0) + { + JLOG(j.fatal()) + << "Invariant failed: Loan assets available is negative"; + return false; + } + if (after->at(sfPrincipalOutstanding) < 0) + { + JLOG(j.fatal()) + << "Invariant failed: Loan principal outstanding is negative"; + 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 58dbc23066..7f55cd73a6 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.h +++ b/src/xrpld/app/tx/detail/InvariantCheck.h @@ -174,7 +174,14 @@ public: */ class AccountRootsDeletedClean { - std::vector> accountsDeleted_; + // Pair is . Before is used for most of the checks, so that + // if, for example, and object ID field is cleared, but the object is not + // deleted, it can still be found. After is used specifically for any checks + // that are expected as part of the deletion, such as zeroing out the + // balance. + std::vector< + std::pair, std::shared_ptr>> + accountsDeleted_; public: void @@ -618,6 +625,34 @@ 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 +{ + // Pair is . + 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&); +}; + /** * @brief Invariants: Pseudo-accounts have valid and consisent properties * @@ -646,6 +681,70 @@ public: beast::Journal const&); }; +/** + * @brief Invariants: Loan brokers are internally consistent + * + * 1. If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most one + * node (the root), which will only hold entries for `RippleState` or + * `MPToken` objects. + * + */ +class ValidLoanBroker +{ + // Pair is . After is used for most of the checks, except + // those that check changed values. + std::vector> brokers_; + + bool + goodZeroDirectory( + ReadView const& view, + SLE::const_ref dir, + beast::Journal const& j) const; + +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&); +}; + +/** + * @brief Invariants: Loans are internally consistent + * + * 1. If `Loan.PaymentRemaining = 0` then `Loan.PrincipalOutstanding = 0` + * + */ +class ValidLoan +{ + // Pair is . After is used for most of the checks, except + // those that check changed values. + std::vector> loans_; + +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< @@ -666,7 +765,10 @@ using InvariantChecks = std::tuple< ValidClawback, ValidMPTIssuance, ValidPermissionedDomain, - ValidPseudoAccounts>; + NoModifiedUnmodifiableFields, + ValidPseudoAccounts, + ValidLoanBroker, + ValidLoan>; /** * @brief get a tuple of all invariant checks diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp new file mode 100644 index 0000000000..d82a59ed0d --- /dev/null +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp @@ -0,0 +1,147 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +// +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +bool +LoanBrokerCoverDeposit::isEnabled(PreflightContext const& ctx) +{ + return lendingProtocolEnabled(ctx); +} + +NotTEC +LoanBrokerCoverDeposit::doPreflight(PreflightContext const& ctx) +{ + if (ctx.tx[sfLoanBrokerID] == beast::zero) + return temINVALID; + + if (ctx.tx[sfAmount] <= beast::zero) + return temBAD_AMOUNT; + + return tesSUCCESS; +} + +TER +LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx) +{ + auto const& tx = ctx.tx; + + auto const account = tx[sfAccount]; + auto const brokerID = tx[sfLoanBrokerID]; + auto const amount = tx[sfAmount]; + + auto const sleBroker = ctx.view.read(keylet::loanbroker(brokerID)); + if (!sleBroker) + { + JLOG(ctx.j.warn()) << "LoanBroker does not exist."; + return tecNO_ENTRY; + } + if (account != sleBroker->at(sfOwner)) + { + JLOG(ctx.j.warn()) << "Account is not the owner of the LoanBroker."; + return tecNO_PERMISSION; + } + auto const vault = ctx.view.read(keylet::vault(sleBroker->at(sfVaultID))); + auto const vaultAsset = vault->at(sfAsset); + + if (amount.asset() != vaultAsset) + return tecWRONG_ASSET; + + auto const pseudoAccountID = sleBroker->at(sfAccount); + + // Cannot transfer a frozen Asset + if (isFrozen(ctx.view, account, vaultAsset)) + return vaultAsset.holds() ? tecFROZEN : tecLOCKED; + if (vaultAsset.holds()) + { + auto const issue = vaultAsset.get(); + if (isDeepFrozen( + ctx.view, pseudoAccountID, issue.currency, issue.account)) + return tecFROZEN; + } + + if (accountHolds( + ctx.view, + account, + vaultAsset, + FreezeHandling::fhZERO_IF_FROZEN, + AuthHandling::ahZERO_IF_UNAUTHORIZED, + ctx.j) < amount) + return tecINSUFFICIENT_FUNDS; + + return tesSUCCESS; +} + +TER +LoanBrokerCoverDeposit::doApply() +{ + auto const& tx = ctx_.tx; + + auto const brokerID = tx[sfLoanBrokerID]; + auto const amount = tx[sfAmount]; + + auto broker = view().peek(keylet::loanbroker(brokerID)); + if (!broker) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const brokerPseudoID = broker->at(sfAccount); + + // Transfer assets from depositor to pseudo-account. + if (auto ter = accountSend( + view(), + account_, + brokerPseudoID, + amount, + j_, + WaiveTransferFee::Yes)) + return ter; + + // Increase the LoanBroker's CoverAvailable by Amount + broker->at(sfCoverAvailable) += amount; + view().update(broker); + + return tesSUCCESS; +} + +//------------------------------------------------------------------------------ + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.h b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.h new file mode 100644 index 0000000000..7e9a10119d --- /dev/null +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.h @@ -0,0 +1,53 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_LOANBROKERCOVERDEPOSIT_H_INCLUDED +#define RIPPLE_TX_LOANBROKERCOVERDEPOSIT_H_INCLUDED + +#include + +namespace ripple { + +class LoanBrokerCoverDeposit : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit LoanBrokerCoverDeposit(ApplyContext& ctx) : Transactor(ctx) + { + } + + static bool + isEnabled(PreflightContext const& ctx); + + static NotTEC + doPreflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +//------------------------------------------------------------------------------ + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp new file mode 100644 index 0000000000..b776eea535 --- /dev/null +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp @@ -0,0 +1,164 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +// +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +bool +LoanBrokerCoverWithdraw::isEnabled(PreflightContext const& ctx) +{ + return lendingProtocolEnabled(ctx); +} + +NotTEC +LoanBrokerCoverWithdraw::doPreflight(PreflightContext const& ctx) +{ + if (ctx.tx[sfLoanBrokerID] == beast::zero) + return temINVALID; + + if (ctx.tx[sfAmount] <= beast::zero) + return temBAD_AMOUNT; + + return tesSUCCESS; +} + +TER +LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) +{ + auto const& tx = ctx.tx; + + auto const account = tx[sfAccount]; + auto const brokerID = tx[sfLoanBrokerID]; + auto const amount = tx[sfAmount]; + + auto const sleBroker = ctx.view.read(keylet::loanbroker(brokerID)); + if (!sleBroker) + { + JLOG(ctx.j.warn()) << "LoanBroker does not exist."; + return tecNO_ENTRY; + } + if (account != sleBroker->at(sfOwner)) + { + JLOG(ctx.j.warn()) << "Account is not the owner of the LoanBroker."; + return tecNO_PERMISSION; + } + auto const vault = ctx.view.read(keylet::vault(sleBroker->at(sfVaultID))); + auto const vaultAsset = vault->at(sfAsset); + + if (amount.asset() != vaultAsset) + return tecWRONG_ASSET; + + // Cannot transfer a frozen Asset + auto const pseudoAccountID = sleBroker->at(sfAccount); + + // Cannot transfer a frozen Asset + /* + if (isFrozen(ctx.view, account, vaultAsset)) + return vaultAsset.holds() ? tecFROZEN : tecLOCKED; + */ + if (isFrozen(ctx.view, pseudoAccountID, vaultAsset)) + return vaultAsset.holds() ? tecFROZEN : tecLOCKED; + if (vaultAsset.holds()) + { + auto const issue = vaultAsset.get(); + if (isDeepFrozen(ctx.view, account, issue.currency, issue.account)) + return tecFROZEN; + } + + auto const coverAvail = sleBroker->at(sfCoverAvailable); + // Cover Rate is in 1/10 bips units + auto const currentDebtTotal = sleBroker->at(sfDebtTotal); + auto const minimumCover = roundToAsset( + vaultAsset, + tenthBipsOfValue( + currentDebtTotal, TenthBips32(sleBroker->at(sfCoverRateMinimum))), + currentDebtTotal); + if (coverAvail < amount) + return tecINSUFFICIENT_FUNDS; + if ((coverAvail - amount) < minimumCover) + return tecINSUFFICIENT_FUNDS; + + if (accountHolds( + ctx.view, + pseudoAccountID, + vaultAsset, + FreezeHandling::fhZERO_IF_FROZEN, + AuthHandling::ahZERO_IF_UNAUTHORIZED, + ctx.j) < amount) + return tecINSUFFICIENT_FUNDS; + + return tesSUCCESS; +} + +TER +LoanBrokerCoverWithdraw::doApply() +{ + auto const& tx = ctx_.tx; + + auto const brokerID = tx[sfLoanBrokerID]; + auto const amount = tx[sfAmount]; + + auto broker = view().peek(keylet::loanbroker(brokerID)); + if (!broker) + return tefBAD_LEDGER; // LCOV_EXCL_LINE + + auto const brokerPseudoID = broker->at(sfAccount); + + // Transfer assets from pseudo-account to depositor. + if (auto ter = accountSend( + view(), + brokerPseudoID, + account_, + amount, + j_, + WaiveTransferFee::Yes)) + return ter; + + // Increase the LoanBroker's CoverAvailable by Amount + broker->at(sfCoverAvailable) -= amount; + view().update(broker); + + return tesSUCCESS; +} + +//------------------------------------------------------------------------------ + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h new file mode 100644 index 0000000000..f7a03f7bb8 --- /dev/null +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h @@ -0,0 +1,53 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_LOANBROKERCOVERWITHDRAW_H_INCLUDED +#define RIPPLE_TX_LOANBROKERCOVERWITHDRAW_H_INCLUDED + +#include + +namespace ripple { + +class LoanBrokerCoverWithdraw : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit LoanBrokerCoverWithdraw(ApplyContext& ctx) : Transactor(ctx) + { + } + + static bool + isEnabled(PreflightContext const& ctx); + + static NotTEC + doPreflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +//------------------------------------------------------------------------------ + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp b/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp new file mode 100644 index 0000000000..f76b1e8a0f --- /dev/null +++ b/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp @@ -0,0 +1,179 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +// +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +bool +LoanBrokerDelete::isEnabled(PreflightContext const& ctx) +{ + return lendingProtocolEnabled(ctx); +} + +NotTEC +LoanBrokerDelete::doPreflight(PreflightContext const& ctx) +{ + return tesSUCCESS; +} + +TER +LoanBrokerDelete::preclaim(PreclaimContext const& ctx) +{ + auto const& tx = ctx.tx; + + auto const account = tx[sfAccount]; + auto const brokerID = tx[sfLoanBrokerID]; + + auto const sleBroker = ctx.view.read(keylet::loanbroker(brokerID)); + if (!sleBroker) + { + JLOG(ctx.j.warn()) << "LoanBroker does not exist."; + return tecNO_ENTRY; + } + if (account != sleBroker->at(sfOwner)) + { + JLOG(ctx.j.warn()) << "Account is not the owner of the LoanBroker."; + return tecNO_PERMISSION; + } + if (auto const ownerCount = sleBroker->at(sfOwnerCount); ownerCount != 0) + { + JLOG(ctx.j.warn()) << "LoanBrokerDelete: Owner count is " << ownerCount; + return tecHAS_OBLIGATIONS; + } + + return tesSUCCESS; +} + +TER +LoanBrokerDelete::doApply() +{ + auto const& tx = ctx_.tx; + + auto const brokerID = tx[sfLoanBrokerID]; + + // Delete the loan broker + auto broker = view().peek(keylet::loanbroker(brokerID)); + if (!broker) + return tefBAD_LEDGER; // LCOV_EXCL_LINE + auto const vaultID = broker->at(sfVaultID); + auto const sleVault = view().read(keylet::vault(vaultID)); + if (!sleVault) + return tefBAD_LEDGER; // LCOV_EXCL_LINE + auto const vaultPseudoID = sleVault->at(sfAccount); + auto const vaultAsset = sleVault->at(sfAsset); + + auto const brokerPseudoID = broker->at(sfAccount); + + if (!view().dirRemove( + keylet::ownerDir(account_), + broker->at(sfOwnerNode), + broker->key(), + false)) + { + return tefBAD_LEDGER; + } + if (!view().dirRemove( + keylet::ownerDir(vaultPseudoID), + broker->at(sfVaultNode), + broker->key(), + false)) + { + return tefBAD_LEDGER; + } + + { + auto const coverAvailable = + STAmount{vaultAsset, broker->at(sfCoverAvailable)}; + if (auto const ter = accountSend( + view(), + brokerPseudoID, + account_, + coverAvailable, + j_, + WaiveTransferFee::Yes)) + return ter; + } + + if (auto ter = removeEmptyHolding(view(), brokerPseudoID, vaultAsset, j_)) + return ter; + + auto brokerPseudoSLE = view().peek(keylet::account(brokerPseudoID)); + if (!brokerPseudoSLE) + return tefBAD_LEDGER; + + // Making the payment and removing the empty holding should have deleted any + // obligations associated with the broker or broker pseudo-account. + if (*brokerPseudoSLE->at(sfBalance)) + { + JLOG(j_.warn()) << "LoanBrokerDelete: Pseudo-account has a balance"; + return tecHAS_OBLIGATIONS; + } + if (brokerPseudoSLE->at(sfOwnerCount) != 0) + { + JLOG(j_.warn()) + << "LoanBrokerDelete: Pseudo-account still owns objects"; + return tecHAS_OBLIGATIONS; + } + if (auto const directory = keylet::ownerDir(brokerPseudoID); + view().read(directory)) + { + JLOG(j_.warn()) << "LoanBrokerDelete: Pseudo-account has a directory"; + return tecHAS_OBLIGATIONS; + } + + view().erase(brokerPseudoSLE); + + view().erase(broker); + + { + auto owner = view().peek(keylet::account(account_)); + if (!owner) + return tefBAD_LEDGER; // LCOV_EXCL_LINE + + adjustOwnerCount(view(), owner, -2, j_); + } + + return tesSUCCESS; +} + +//------------------------------------------------------------------------------ + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/LoanBrokerDelete.h b/src/xrpld/app/tx/detail/LoanBrokerDelete.h new file mode 100644 index 0000000000..7382106c40 --- /dev/null +++ b/src/xrpld/app/tx/detail/LoanBrokerDelete.h @@ -0,0 +1,53 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_LOANBROKERDELETE_H_INCLUDED +#define RIPPLE_TX_LOANBROKERDELETE_H_INCLUDED + +#include + +namespace ripple { + +class LoanBrokerDelete : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit LoanBrokerDelete(ApplyContext& ctx) : Transactor(ctx) + { + } + + static bool + isEnabled(PreflightContext const& ctx); + + static NotTEC + doPreflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +//------------------------------------------------------------------------------ + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp new file mode 100644 index 0000000000..7589a5817a --- /dev/null +++ b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp @@ -0,0 +1,210 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +// +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +bool +LoanBrokerSet::isEnabled(PreflightContext const& ctx) +{ + return lendingProtocolEnabled(ctx); +} + +NotTEC +LoanBrokerSet::doPreflight(PreflightContext const& ctx) +{ + auto const& tx = ctx.tx; + if (auto const data = tx[~sfData]; data && !data->empty() && + !validDataLength(tx[~sfData], maxDataPayloadLength)) + return temINVALID; + if (!validNumericRange(tx[~sfManagementFeeRate], maxManagementFeeRate)) + return temINVALID; + if (!validNumericRange(tx[~sfCoverRateMinimum], maxCoverRate)) + return temINVALID; + if (!validNumericRange(tx[~sfCoverRateLiquidation], maxCoverRate)) + return temINVALID; + if (!validNumericRange( + tx[~sfDebtMaximum], Number(maxMPTokenAmount), Number(0))) + return temINVALID; + + if (tx.isFieldPresent(sfLoanBrokerID)) + { + // Fixed fields can not be specified if we're modifying an existing + // LoanBroker Object + if (tx.isFieldPresent(sfManagementFeeRate) || + tx.isFieldPresent(sfCoverRateMinimum) || + tx.isFieldPresent(sfCoverRateLiquidation)) + return temINVALID; + } + + return tesSUCCESS; +} + +TER +LoanBrokerSet::preclaim(PreclaimContext const& ctx) +{ + auto const& tx = ctx.tx; + + auto const account = tx[sfAccount]; + if (auto const brokerID = tx[~sfLoanBrokerID]) + { + auto const sleBroker = ctx.view.read(keylet::loanbroker(*brokerID)); + if (!sleBroker) + { + JLOG(ctx.j.warn()) << "LoanBroker does not exist."; + return tecNO_ENTRY; + } + if (tx[sfVaultID] != sleBroker->at(sfVaultID)) + { + JLOG(ctx.j.warn()) + << "Can not change VaultID on an existing LoanBroker."; + return tecNO_PERMISSION; + } + if (account != sleBroker->at(sfOwner)) + { + JLOG(ctx.j.warn()) << "Account is not the owner of the LoanBroker."; + return tecNO_PERMISSION; + } + } + else + { + auto const vaultID = tx[sfVaultID]; + auto const sleVault = ctx.view.read(keylet::vault(vaultID)); + if (!sleVault) + { + JLOG(ctx.j.warn()) << "Vault does not exist."; + return tecNO_ENTRY; + } + if (account != sleVault->at(sfOwner)) + { + JLOG(ctx.j.warn()) << "Account is not the owner of the Vault."; + return tecNO_PERMISSION; + } + if (auto const ter = canAddHolding(ctx.view, sleVault->at(sfAsset))) + return ter; + } + return tesSUCCESS; +} + +TER +LoanBrokerSet::doApply() +{ + auto const& tx = ctx_.tx; + auto& view = ctx_.view(); + + if (auto const brokerID = tx[~sfLoanBrokerID]) + { + // Modify an existing LoanBroker + auto broker = view.peek(keylet::loanbroker(*brokerID)); + if (!broker) + return tefBAD_LEDGER; // LCOV_EXCL_LINE + + if (auto const data = tx[~sfData]) + broker->at(sfData) = *data; + if (auto const debtMax = tx[~sfDebtMaximum]) + broker->at(sfDebtMaximum) = *debtMax; + + view.update(broker); + } + else + { + // Create a new LoanBroker pointing back to the given Vault + auto const vaultID = tx[sfVaultID]; + auto const sleVault = view.read(keylet::vault(vaultID)); + auto const vaultPseudoID = sleVault->at(sfAccount); + auto const sequence = tx.getSeqValue(); + + auto owner = view.peek(keylet::account(account_)); + if (!owner) + return tefBAD_LEDGER; // LCOV_EXCL_LINE + auto broker = + std::make_shared(keylet::loanbroker(account_, sequence)); + + if (auto const ter = dirLink(view, account_, broker)) + return ter; + if (auto const ter = dirLink(view, vaultPseudoID, broker, sfVaultNode)) + return ter; + + adjustOwnerCount(view, owner, 2, j_); + auto const ownerCount = owner->at(sfOwnerCount); + if (mPriorBalance < view.fees().accountReserve(ownerCount)) + return tecINSUFFICIENT_RESERVE; + + auto maybePseudo = + createPseudoAccount(view, broker->key(), sfLoanBrokerID); + if (!maybePseudo) + return maybePseudo.error(); + auto& pseudo = *maybePseudo; + auto pseudoId = pseudo->at(sfAccount); + + if (auto ter = addEmptyHolding( + view, pseudoId, mPriorBalance, sleVault->at(sfAsset), j_)) + return ter; + + // Initialize data fields: + broker->at(sfSequence) = sequence; + broker->at(sfVaultID) = vaultID; + broker->at(sfOwner) = account_; + broker->at(sfAccount) = pseudoId; + broker->at(sfLoanSequence) = 1; + if (auto const data = tx[~sfData]) + broker->at(sfData) = *data; + if (auto const rate = tx[~sfManagementFeeRate]) + broker->at(sfManagementFeeRate) = *rate; + if (auto const debtMax = tx[~sfDebtMaximum]) + broker->at(sfDebtMaximum) = *debtMax; + if (auto const coverMin = tx[~sfCoverRateMinimum]) + broker->at(sfCoverRateMinimum) = *coverMin; + if (auto const coverLiq = tx[~sfCoverRateLiquidation]) + broker->at(sfCoverRateLiquidation) = *coverLiq; + + view.insert(broker); + } + + return tesSUCCESS; +} + +//------------------------------------------------------------------------------ + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/LoanBrokerSet.h b/src/xrpld/app/tx/detail/LoanBrokerSet.h new file mode 100644 index 0000000000..018833132b --- /dev/null +++ b/src/xrpld/app/tx/detail/LoanBrokerSet.h @@ -0,0 +1,53 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_LOANBROKERSET_H_INCLUDED +#define RIPPLE_TX_LOANBROKERSET_H_INCLUDED + +#include + +namespace ripple { + +class LoanBrokerSet : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit LoanBrokerSet(ApplyContext& ctx) : Transactor(ctx) + { + } + + static bool + isEnabled(PreflightContext const& ctx); + + static NotTEC + doPreflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +//------------------------------------------------------------------------------ + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/LoanDelete.cpp b/src/xrpld/app/tx/detail/LoanDelete.cpp new file mode 100644 index 0000000000..d29c03e969 --- /dev/null +++ b/src/xrpld/app/tx/detail/LoanDelete.cpp @@ -0,0 +1,167 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +// +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +bool +LoanDelete::isEnabled(PreflightContext const& ctx) +{ + return lendingProtocolEnabled(ctx); +} + +NotTEC +LoanDelete::doPreflight(PreflightContext const& ctx) +{ + if (ctx.tx[sfLoanID] == beast::zero) + return temINVALID; + + return tesSUCCESS; +} + +TER +LoanDelete::preclaim(PreclaimContext const& ctx) +{ + auto const& tx = ctx.tx; + + auto const account = tx[sfAccount]; + auto const loanID = tx[sfLoanID]; + + auto const loanSle = ctx.view.read(keylet::loan(loanID)); + if (!loanSle) + { + JLOG(ctx.j.warn()) << "Loan does not exist."; + return tecNO_ENTRY; + } + if (loanSle->at(sfPaymentRemaining) > 0) + { + JLOG(ctx.j.warn()) << "Active loan can not be deleted."; + return tecHAS_OBLIGATIONS; + } + + auto const loanBrokerID = loanSle->at(sfLoanBrokerID); + auto const loanBrokerSle = ctx.view.read(keylet::loanbroker(loanBrokerID)); + if (!loanBrokerSle) + { + // should be impossible + return tecINTERNAL; // LCOV_EXCL_LINE + } + if (loanBrokerSle->at(sfOwner) != account) + { + JLOG(ctx.j.warn()) + << "LoanBroker for Loan does not belong to the account."; + return tecNO_PERMISSION; + } + + return tesSUCCESS; +} + +TER +LoanDelete::doApply() +{ + auto const& tx = ctx_.tx; + auto& view = ctx_.view(); + + auto const loanID = tx[sfLoanID]; + auto const loanSle = view.peek(keylet::loan(loanID)); + if (!loanSle) + return tefBAD_LEDGER; // LCOV_EXCL_LINE + auto const borrower = loanSle->at(sfBorrower); + auto const borrowerSle = view.peek(keylet::account(borrower)); + if (!borrowerSle) + return tefBAD_LEDGER; // LCOV_EXCL_LINE + + auto const brokerID = loanSle->at(sfLoanBrokerID); + auto const brokerSle = view.peek(keylet::loanbroker(brokerID)); + if (!brokerSle) + return tefBAD_LEDGER; // LCOV_EXCL_LINE + auto const brokerPseudoAccount = brokerSle->at(sfAccount); + + auto const vaultSle = view.peek(keylet ::vault(brokerSle->at(sfVaultID))); + if (!vaultSle) + return tefBAD_LEDGER; // LCOV_EXCL_LINE + auto const vaultAsset = vaultSle->at(sfAsset); + + // transfer any remaining funds to the borrower + auto assetsAvailableProxy = loanSle->at(sfAssetsAvailable); + if (assetsAvailableProxy != 0) + { + if (auto const ter = accountSend( + view, + brokerPseudoAccount, + borrower, + STAmount{vaultAsset, assetsAvailableProxy}, + j_, + WaiveTransferFee::Yes)) + return ter; + } + + // Remove LoanID from Directory of the LoanBroker pseudo-account. + if (!view.dirRemove( + keylet::ownerDir(brokerPseudoAccount), + loanSle->at(sfLoanBrokerNode), + loanID, + false)) + return tefBAD_LEDGER; + // Remove LoanID from Directory of the Borrower. + if (!view.dirRemove( + keylet::ownerDir(borrower), + loanSle->at(sfOwnerNode), + loanID, + false)) + return tefBAD_LEDGER; + + // Delete the Loan object + view.erase(loanSle); + + // Decrement the LoanBroker's owner count. + adjustOwnerCount(view, brokerSle, -1, j_); + // Decrement the borrower's owner count + adjustOwnerCount(view, borrowerSle, -1, j_); + + return tesSUCCESS; +} + +//------------------------------------------------------------------------------ + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/LoanDelete.h b/src/xrpld/app/tx/detail/LoanDelete.h new file mode 100644 index 0000000000..6909953099 --- /dev/null +++ b/src/xrpld/app/tx/detail/LoanDelete.h @@ -0,0 +1,53 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_LOANDELETE_H_INCLUDED +#define RIPPLE_TX_LOANDELETE_H_INCLUDED + +#include + +namespace ripple { + +class LoanDelete : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit LoanDelete(ApplyContext& ctx) : Transactor(ctx) + { + } + + static bool + isEnabled(PreflightContext const& ctx); + + static NotTEC + doPreflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +//------------------------------------------------------------------------------ + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/LoanDraw.cpp b/src/xrpld/app/tx/detail/LoanDraw.cpp new file mode 100644 index 0000000000..1f01639fe0 --- /dev/null +++ b/src/xrpld/app/tx/detail/LoanDraw.cpp @@ -0,0 +1,194 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +// +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +bool +LoanDraw::isEnabled(PreflightContext const& ctx) +{ + return lendingProtocolEnabled(ctx); +} + +NotTEC +LoanDraw::doPreflight(PreflightContext const& ctx) +{ + if (ctx.tx[sfLoanID] == beast::zero) + return temINVALID; + + if (ctx.tx[sfAmount] <= beast::zero) + return temBAD_AMOUNT; + + return tesSUCCESS; +} + +TER +LoanDraw::preclaim(PreclaimContext const& ctx) +{ + auto const& tx = ctx.tx; + + auto const account = tx[sfAccount]; + auto const loanID = tx[sfLoanID]; + auto const amount = tx[sfAmount]; + + auto const loanSle = ctx.view.read(keylet::loan(loanID)); + if (!loanSle) + { + JLOG(ctx.j.warn()) << "Loan does not exist."; + return tecNO_ENTRY; + } + + if (loanSle->at(sfBorrower) != account) + { + JLOG(ctx.j.warn()) << "Loan does not belong to the account."; + return tecNO_PERMISSION; + } + + if (loanSle->isFlag(lsfLoanImpaired) || loanSle->isFlag(lsfLoanDefault)) + { + JLOG(ctx.j.warn()) << "Loan is impaired or in default."; + return tecNO_PERMISSION; + } + + if (!hasExpired(ctx.view, loanSle->at(sfStartDate))) + { + JLOG(ctx.j.warn()) << "Loan has not started yet."; + return tecTOO_SOON; + } + + auto const loanBrokerID = loanSle->at(sfLoanBrokerID); + auto const loanBrokerSle = ctx.view.read(keylet::loanbroker(loanBrokerID)); + if (!loanBrokerSle) + { + // This should be impossible + // LCOV_EXCL_START + JLOG(ctx.j.fatal()) << "LoanBroker does not exist."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + auto const brokerPseudoAccount = loanBrokerSle->at(sfAccount); + auto const vaultID = loanBrokerSle->at(sfVaultID); + auto const vaultSle = ctx.view.read(keylet::vault(vaultID)); + if (!vaultSle) + { + // This should be impossible + // LCOV_EXCL_START + JLOG(ctx.j.fatal()) << "Vault does not exist."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + auto const asset = vaultSle->at(sfAsset); + + if (amount.asset() != asset) + { + JLOG(ctx.j.warn()) << "Loan amount does not match the Vault asset."; + return tecWRONG_ASSET; + } + + if (loanSle->at(sfAssetsAvailable) < amount) + { + JLOG(ctx.j.warn()) << "Loan does not have enough assets available."; + return tecINSUFFICIENT_FUNDS; + } + + if (isFrozen(ctx.view, brokerPseudoAccount, asset)) + { + JLOG(ctx.j.warn()) << "Loan Broker pseudo-account is frozen."; + return asset.holds() ? tecFROZEN : tecLOCKED; + } + if (asset.holds()) + { + auto const issue = asset.get(); + if (isDeepFrozen(ctx.view, account, issue.currency, issue.account)) + { + JLOG(ctx.j.warn()) << "Borrower account is frozen."; + return tecFROZEN; + } + } + + if (hasExpired(ctx.view, loanSle->at(sfNextPaymentDueDate))) + { + JLOG(ctx.j.warn()) << "Loan payment is overdue."; + return tecNO_PERMISSION; + } + + return tesSUCCESS; +} + +TER +LoanDraw::doApply() +{ + auto const& tx = ctx_.tx; + auto& view = ctx_.view(); + + auto const amount = tx[sfAmount]; + + auto const loanID = tx[sfLoanID]; + auto const loanSle = view.peek(keylet::loan(loanID)); + if (!loanSle) + return tefBAD_LEDGER; // LCOV_EXCL_LINE + + auto const brokerID = loanSle->at(sfLoanBrokerID); + auto const brokerSle = view.peek(keylet::loanbroker(brokerID)); + if (!brokerSle) + return tefBAD_LEDGER; // LCOV_EXCL_LINE + auto const brokerPseudoAccount = brokerSle->at(sfAccount); + + if (auto const ter = accountSend( + view, + brokerPseudoAccount, + account_, + amount, + j_, + WaiveTransferFee::Yes)) + return ter; + + loanSle->at(sfAssetsAvailable) -= amount; + view.update(loanSle); + + return tesSUCCESS; +} + +//------------------------------------------------------------------------------ + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/LoanDraw.h b/src/xrpld/app/tx/detail/LoanDraw.h new file mode 100644 index 0000000000..bb8c5dcf22 --- /dev/null +++ b/src/xrpld/app/tx/detail/LoanDraw.h @@ -0,0 +1,53 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_LOANDRAW_H_INCLUDED +#define RIPPLE_TX_LOANDRAW_H_INCLUDED + +#include + +namespace ripple { + +class LoanDraw : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit LoanDraw(ApplyContext& ctx) : Transactor(ctx) + { + } + + static bool + isEnabled(PreflightContext const& ctx); + + static NotTEC + doPreflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +//------------------------------------------------------------------------------ + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp new file mode 100644 index 0000000000..d44bb5ae7b --- /dev/null +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -0,0 +1,442 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +// +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +bool +LoanManage::isEnabled(PreflightContext const& ctx) +{ + return lendingProtocolEnabled(ctx); +} + +std::uint32_t +LoanManage::getFlagsMask(PreflightContext const& ctx) +{ + return tfLoanManageMask; +} + +NotTEC +LoanManage::doPreflight(PreflightContext const& ctx) +{ + if (ctx.tx[sfLoanID] == beast::zero) + return temINVALID; + + // Flags are mutually exclusive + int numFlags = 0; + for (auto const flag : { + tfLoanDefault, + tfLoanImpair, + tfLoanUnimpair, + }) + { + if (ctx.tx.isFlag(flag)) + ++numFlags; + } + if (numFlags > 1) + { + JLOG(ctx.j.warn()) + << "LoanManage: Only one of tfLoanDefault, tfLoanImpair, or " + "tfLoanUnimpair can be set."; + return temINVALID_FLAG; + } + + return tesSUCCESS; +} + +TER +LoanManage::preclaim(PreclaimContext const& ctx) +{ + auto const& tx = ctx.tx; + + auto const account = tx[sfAccount]; + auto const loanID = tx[sfLoanID]; + + auto const loanSle = ctx.view.read(keylet::loan(loanID)); + if (!loanSle) + { + JLOG(ctx.j.warn()) << "Loan does not exist."; + return tecNO_ENTRY; + } + // Impairment only allows certain transitions. + // 1. Once it's in default, it can't be changed. + // 2. It can get worse: unimpaired -> impaired -> default + // or unimpaired -> default + // 3. It can get better: impaired -> unimpaired + // 4. If it's in a state, it can't be put in that state again. + if (loanSle->isFlag(lsfLoanDefault)) + { + JLOG(ctx.j.warn()) + << "Loan is in default. A defaulted loan can not be modified."; + return tecNO_PERMISSION; + } + if (loanSle->isFlag(lsfLoanImpaired) && tx.isFlag(tfLoanImpair)) + { + JLOG(ctx.j.warn()) + << "Loan is impaired. A loan can not be impaired twice."; + return tecNO_PERMISSION; + } + if (!(loanSle->isFlag(lsfLoanImpaired) || + loanSle->isFlag(lsfLoanDefault)) && + (tx.isFlag(tfLoanUnimpair))) + { + JLOG(ctx.j.warn()) + << "Loan is unimpaired. Can not be unimpaired again."; + return tecNO_PERMISSION; + } + if (loanSle->at(sfPaymentRemaining) == 0) + { + JLOG(ctx.j.warn()) << "Loan is fully paid. A loan can not be modified " + "after it is fully paid."; + return tecNO_PERMISSION; + } + if (tx.isFlag(tfLoanDefault) && + !hasExpired( + ctx.view, + loanSle->at(sfNextPaymentDueDate) + loanSle->at(sfGracePeriod))) + { + JLOG(ctx.j.warn()) + << "A loan can not be defaulted before the next payment due date."; + return tecTOO_SOON; + } + + auto const loanBrokerID = loanSle->at(sfLoanBrokerID); + auto const loanBrokerSle = ctx.view.read(keylet::loanbroker(loanBrokerID)); + if (!loanBrokerSle) + { + // should be impossible + return tecINTERNAL; // LCOV_EXCL_LINE + } + if (loanBrokerSle->at(sfOwner) != account) + { + JLOG(ctx.j.warn()) + << "LoanBroker for Loan does not belong to the account."; + return tecNO_PERMISSION; + } + + return tesSUCCESS; +} + +TER +defaultLoan( + ApplyView& view, + SLE::ref loanSle, + SLE::ref brokerSle, + SLE::ref vaultSle, + Number const& principalOutstanding, + Number const& interestOutstanding, + std::uint32_t paymentInterval, + Asset const& vaultAsset, + beast::Journal j) +{ + // Calculate the amount of the Default that First-Loss Capital covers: + + Number const originalPrincipalRequested = loanSle->at(sfPrincipalRequested); + TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; + auto brokerDebtTotalProxy = brokerSle->at(sfDebtTotal); + auto const totalDefaultAmount = principalOutstanding + interestOutstanding; + + // The default Amount equals the outstanding principal and interest, + // excluding any funds unclaimed by the Borrower. + auto loanAssetsAvailableProxy = loanSle->at(sfAssetsAvailable); + auto const defaultAmount = totalDefaultAmount - loanAssetsAvailableProxy; + // Apply the First-Loss Capital to the Default Amount + TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)}; + TenthBips32 const coverRateLiquidation{ + brokerSle->at(sfCoverRateLiquidation)}; + auto const defaultCovered = roundToAsset( + vaultAsset, + std::min( + tenthBipsOfValue( + tenthBipsOfValue( + brokerDebtTotalProxy.value(), coverRateMinimum), + coverRateLiquidation), + defaultAmount), + originalPrincipalRequested); + auto const returnToVault = defaultCovered + loanAssetsAvailableProxy; + auto const vaultDefaultAmount = defaultAmount - defaultCovered; + + // Update the Vault object: + + { + // Decrease the Total Value of the Vault: + auto vaultAssetsTotalProxy = vaultSle->at(sfAssetsTotal); + if (vaultAssetsTotalProxy < vaultDefaultAmount) + { + // LCOV_EXCL_START + JLOG(j.warn()) + << "Vault total assets is less than the vault default amount"; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + vaultAssetsTotalProxy -= vaultDefaultAmount; + // Increase the Asset Available of the Vault by liquidated First-Loss + // Capital and any unclaimed funds amount: + vaultSle->at(sfAssetsAvailable) += returnToVault; + // The loss has been realized + if (loanSle->isFlag(lsfLoanImpaired)) + { + auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized); + if (vaultLossUnrealizedProxy < totalDefaultAmount) + { + JLOG(j.warn()) + << "Vault unrealized loss is less than the default amount"; + return tefBAD_LEDGER; + } + vaultLossUnrealizedProxy -= totalDefaultAmount; + } + view.update(vaultSle); + } + + // Update the LoanBroker object: + + { + // Decrease the Debt of the LoanBroker: + if (brokerDebtTotalProxy < totalDefaultAmount) + { + // LCOV_EXCL_START + JLOG(j.warn()) + << "LoanBroker debt total is less than the default amount"; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + brokerDebtTotalProxy -= totalDefaultAmount; + // Decrease the First-Loss Capital Cover Available: + auto coverAvailableProxy = brokerSle->at(sfCoverAvailable); + if (coverAvailableProxy < defaultCovered) + { + // LCOV_EXCL_START + JLOG(j.warn()) + << "LoanBroker cover available is less than amount covered"; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + coverAvailableProxy -= defaultCovered; + view.update(brokerSle); + } + + // Update the Loan object: + loanSle->setFlag(lsfLoanDefault); + loanSle->at(sfPaymentRemaining) = 0; + loanAssetsAvailableProxy = 0; + loanSle->at(sfPrincipalOutstanding) = 0; + view.update(loanSle); + + // Return funds from the LoanBroker pseudo-account to the + // Vault pseudo-account: + return accountSend( + view, + brokerSle->at(sfAccount), + vaultSle->at(sfAccount), + STAmount{vaultAsset, returnToVault}, + j, + WaiveTransferFee::Yes); +} + +TER +impairLoan( + ApplyView& view, + SLE::ref loanSle, + SLE::ref brokerSle, + SLE::ref vaultSle, + Number const& principalOutstanding, + Number const& interestOutstanding, + std::uint32_t paymentInterval, + Asset const& vaultAsset, + beast::Journal j) +{ + // Update the Vault object(set "paper loss") + vaultSle->at(sfLossUnrealized) += + principalOutstanding + interestOutstanding; + view.update(vaultSle); + + // Update the Loan object + loanSle->setFlag(lsfLoanImpaired); + auto loanNextDueProxy = loanSle->at(sfNextPaymentDueDate); + if (!hasExpired(view, loanNextDueProxy)) + { + // loan payment is not yet late - + // move the next payment due date to now + loanNextDueProxy = view.parentCloseTime().time_since_epoch().count(); + } + view.update(loanSle); + + return tesSUCCESS; +} + +TER +unimpairLoan( + ApplyView& view, + SLE::ref loanSle, + SLE::ref brokerSle, + SLE::ref vaultSle, + Number const& principalOutstanding, + Number const& interestOutstanding, + std::uint32_t paymentInterval, + Asset const& vaultAsset, + beast::Journal j) +{ + // Update the Vault object(clear "paper loss") + auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized); + auto const lossReversed = principalOutstanding + interestOutstanding; + if (vaultLossUnrealizedProxy < lossReversed) + { + // LCOV_EXCL_START + JLOG(j.warn()) + << "Vault unrealized loss is less than the amount to be cleared"; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + vaultLossUnrealizedProxy -= lossReversed; + view.update(vaultSle); + + // Update the Loan object + loanSle->clearFlag(lsfLoanImpaired); + auto const normalPaymentDueDate = + std::max(loanSle->at(sfPreviousPaymentDate), loanSle->at(sfStartDate)) + + paymentInterval; + if (!hasExpired(view, normalPaymentDueDate)) + { + // loan was unimpaired within the payment interval + loanSle->at(sfNextPaymentDueDate) = normalPaymentDueDate; + } + else + { + // loan was unimpaired after the original payment due date + loanSle->at(sfNextPaymentDueDate) = + view.parentCloseTime().time_since_epoch().count() + paymentInterval; + } + view.update(loanSle); + + return tesSUCCESS; +} + +TER +LoanManage::doApply() +{ + auto const& tx = ctx_.tx; + auto& view = ctx_.view(); + + auto const loanID = tx[sfLoanID]; + auto const loanSle = view.peek(keylet::loan(loanID)); + if (!loanSle) + return tefBAD_LEDGER; // LCOV_EXCL_LINE + + auto const brokerID = loanSle->at(sfLoanBrokerID); + auto const brokerSle = view.peek(keylet::loanbroker(brokerID)); + if (!brokerSle) + return tefBAD_LEDGER; // LCOV_EXCL_LINE + + auto const vaultSle = view.peek(keylet ::vault(brokerSle->at(sfVaultID))); + if (!vaultSle) + return tefBAD_LEDGER; // LCOV_EXCL_LINE + auto const vaultAsset = vaultSle->at(sfAsset); + + TenthBips32 const interestRate{loanSle->at(sfInterestRate)}; + Number const originalPrincipalRequested = loanSle->at(sfPrincipalRequested); + auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding); + + TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; + auto const paymentInterval = loanSle->at(sfPaymentInterval); + auto const paymentsRemaining = loanSle->at(sfPaymentRemaining); + auto const interestOutstanding = loanInterestOutstandingMinusFee( + vaultAsset, + originalPrincipalRequested, + principalOutstanding.value(), + interestRate, + paymentInterval, + paymentsRemaining, + managementFeeRate); + + // Valid flag combinations are checked in preflight. No flags is valid - + // just a noop. + if (tx.isFlag(tfLoanDefault)) + { + if (auto const ter = defaultLoan( + view, + loanSle, + brokerSle, + vaultSle, + principalOutstanding, + interestOutstanding, + paymentInterval, + vaultAsset, + j_)) + return ter; + } + if (tx.isFlag(tfLoanImpair)) + { + if (auto const ter = impairLoan( + view, + loanSle, + brokerSle, + vaultSle, + principalOutstanding, + interestOutstanding, + paymentInterval, + vaultAsset, + j_)) + return ter; + } + if (tx.isFlag(tfLoanUnimpair)) + { + if (auto const ter = unimpairLoan( + view, + loanSle, + brokerSle, + vaultSle, + principalOutstanding, + interestOutstanding, + paymentInterval, + vaultAsset, + j_)) + return ter; + } + + return tesSUCCESS; +} + +//------------------------------------------------------------------------------ + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/LoanManage.h b/src/xrpld/app/tx/detail/LoanManage.h new file mode 100644 index 0000000000..74b8a7299f --- /dev/null +++ b/src/xrpld/app/tx/detail/LoanManage.h @@ -0,0 +1,56 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_LOANMANAGE_H_INCLUDED +#define RIPPLE_TX_LOANMANAGE_H_INCLUDED + +#include + +namespace ripple { + +class LoanManage : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit LoanManage(ApplyContext& ctx) : Transactor(ctx) + { + } + + static bool + isEnabled(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + + static NotTEC + doPreflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +//------------------------------------------------------------------------------ + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp new file mode 100644 index 0000000000..5ff03b267b --- /dev/null +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -0,0 +1,297 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +// +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +bool +LoanPay::isEnabled(PreflightContext const& ctx) +{ + return lendingProtocolEnabled(ctx); +} + +NotTEC +LoanPay::doPreflight(PreflightContext const& ctx) +{ + if (ctx.tx[sfLoanID] == beast::zero) + return temINVALID; + + if (ctx.tx[sfAmount] <= beast::zero) + return temBAD_AMOUNT; + + return tesSUCCESS; +} + +TER +LoanPay::preclaim(PreclaimContext const& ctx) +{ + auto const& tx = ctx.tx; + + auto const account = tx[sfAccount]; + auto const loanID = tx[sfLoanID]; + auto const amount = tx[sfAmount]; + + auto const loanSle = ctx.view.read(keylet::loan(loanID)); + if (!loanSle) + { + JLOG(ctx.j.warn()) << "Loan does not exist."; + return tecNO_ENTRY; + } + + auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding); + TenthBips32 const interestRate{loanSle->at(sfInterestRate)}; + auto const paymentRemaining = loanSle->at(sfPaymentRemaining); + TenthBips32 const lateInterestRate{loanSle->at(sfLateInterestRate)}; + auto const startDate = loanSle->at(sfStartDate); + + if (loanSle->at(sfBorrower) != account) + { + JLOG(ctx.j.warn()) << "Loan does not belong to the account."; + return tecNO_PERMISSION; + } + + if (!hasExpired(ctx.view, startDate)) + { + JLOG(ctx.j.warn()) << "Loan has not started yet."; + return tecTOO_SOON; + } + + if (paymentRemaining == 0 || principalOutstanding == 0) + { + JLOG(ctx.j.warn()) << "Loan is already paid off."; + return tecKILLED; + } + + auto const loanBrokerID = loanSle->at(sfLoanBrokerID); + auto const loanBrokerSle = ctx.view.read(keylet::loanbroker(loanBrokerID)); + if (!loanBrokerSle) + { + // This should be impossible + // LCOV_EXCL_START + JLOG(ctx.j.fatal()) << "LoanBroker does not exist."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + auto const brokerPseudoAccount = loanBrokerSle->at(sfAccount); + auto const vaultID = loanBrokerSle->at(sfVaultID); + auto const vaultSle = ctx.view.read(keylet::vault(vaultID)); + if (!vaultSle) + { + // This should be impossible + // LCOV_EXCL_START + JLOG(ctx.j.fatal()) << "Vault does not exist."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + auto const asset = vaultSle->at(sfAsset); + + if (amount.asset() != asset) + { + JLOG(ctx.j.warn()) << "Loan amount does not match the Vault asset."; + return tecWRONG_ASSET; + } + + if (isFrozen(ctx.view, brokerPseudoAccount, asset)) + { + JLOG(ctx.j.warn()) << "Loan Broker pseudo-account is frozen."; + return asset.holds() ? tecFROZEN : tecLOCKED; + } + if (asset.holds()) + { + auto const issue = asset.get(); + if (isDeepFrozen(ctx.view, account, issue.currency, issue.account)) + { + JLOG(ctx.j.warn()) << "Borrower account is frozen."; + return tecFROZEN; + } + } + + return tesSUCCESS; +} + +TER +LoanPay::doApply() +{ + auto const& tx = ctx_.tx; + auto& view = ctx_.view(); + + auto const amount = tx[sfAmount]; + + auto const loanID = tx[sfLoanID]; + auto const loanSle = view.peek(keylet::loan(loanID)); + if (!loanSle) + return tefBAD_LEDGER; // LCOV_EXCL_LINE + + auto const brokerID = loanSle->at(sfLoanBrokerID); + auto const brokerSle = view.peek(keylet::loanbroker(brokerID)); + if (!brokerSle) + return tefBAD_LEDGER; // LCOV_EXCL_LINE + auto const brokerOwner = brokerSle->at(sfOwner); + auto const brokerPseudoAccount = brokerSle->at(sfAccount); + auto const vaultID = brokerSle->at(sfVaultID); + auto const vaultSle = view.peek(keylet::vault(vaultID)); + if (!vaultSle) + return tefBAD_LEDGER; // LCOV_EXCL_LINE + auto const vaultPseudoAccount = vaultSle->at(sfAccount); + auto const asset = vaultSle->at(sfAsset); + + //------------------------------------------------------ + // Loan object state changes + Number const originalPrincipalRequested = loanSle->at(sfPrincipalRequested); + + view.update(loanSle); + + Expected paymentParts = + loanComputePaymentParts(asset, view, loanSle, amount, j_); + + if (!paymentParts) + return paymentParts.error(); + + // If the loan was impaired, it isn't anymore. + loanSle->clearFlag(lsfLoanImpaired); + + XRPL_ASSERT_PARTS( + paymentParts->principalPaid > 0, + "ripple::LoanPay::doApply", + "valid principal paid"); + XRPL_ASSERT_PARTS( + paymentParts->interestPaid >= 0, + "ripple::LoanPay::doApply", + "valid interest paid"); + XRPL_ASSERT_PARTS( + paymentParts->feePaid >= 0, + "ripple::LoanPay::doApply", + "valid fee paid"); + + //------------------------------------------------------ + // LoanBroker object state changes + view.update(brokerSle); + + TenthBips32 managementFeeRate{brokerSle->at(sfManagementFeeRate)}; + auto const managementFee = roundToAsset( + asset, + tenthBipsOfValue(paymentParts->interestPaid, managementFeeRate), + originalPrincipalRequested); + + auto const totalPaidToVault = paymentParts->principalPaid + + paymentParts->interestPaid - managementFee; + + auto const totalPaidToBroker = paymentParts->feePaid + managementFee; + + // If there is not enough first-loss capital + auto coverAvailableField = brokerSle->at(sfCoverAvailable); + auto debtTotalField = brokerSle->at(sfDebtTotal); + TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)}; + + bool const sufficientCover = coverAvailableField >= + roundToAsset(asset, + tenthBipsOfValue(debtTotalField.value(), coverRateMinimum), + originalPrincipalRequested); + if (!sufficientCover) + { + // Add the fee to to First Loss Cover Pool + coverAvailableField += totalPaidToBroker; + } + + // Decrease LoanBroker Debt by the amount paid, add the Loan value change, + // and subtract the change in the management fee + auto const vaultValueChange = valueMinusManagementFee( + asset, + paymentParts->valueChange, + managementFeeRate, + originalPrincipalRequested); + // debtDecrease may be negative, increasing the debt + auto const debtDecrease = totalPaidToVault - vaultValueChange; + XRPL_ASSERT_PARTS( + roundToAsset(asset, debtDecrease, originalPrincipalRequested) == + debtDecrease, + "ripple::LoanPay::doApply", + "debtDecrease rounding good"); + if (debtDecrease >= debtTotalField) + debtTotalField = 0; + else + debtTotalField -= debtDecrease; + + //------------------------------------------------------ + // Vault object state changes + view.update(vaultSle); + + vaultSle->at(sfAssetsAvailable) += totalPaidToVault; + vaultSle->at(sfAssetsTotal) += vaultValueChange; + + // Move funds + STAmount const paidToVault(asset, totalPaidToVault); + STAmount const paidToBroker(asset, totalPaidToBroker); + XRPL_ASSERT_PARTS( + paidToVault + paidToBroker <= amount, + "ripple::LoanPay::doApply", + "amount is sufficient"); + XRPL_ASSERT_PARTS( + paidToVault + paidToBroker <= paymentParts->principalPaid + + paymentParts->interestPaid + paymentParts->feePaid, + "ripple::LoanPay::doApply", + "payment agreement"); + + if (auto const ter = accountSend( + view, + account_, + vaultPseudoAccount, + paidToVault, + j_, + WaiveTransferFee::Yes)) + return ter; + if (auto const ter = accountSend( + view, + account_, + sufficientCover ? brokerOwner : brokerPseudoAccount, + paidToBroker, + j_, + WaiveTransferFee::Yes)) + return ter; + + return tesSUCCESS; +} + +//------------------------------------------------------------------------------ + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/LoanPay.h b/src/xrpld/app/tx/detail/LoanPay.h new file mode 100644 index 0000000000..f608a7fb38 --- /dev/null +++ b/src/xrpld/app/tx/detail/LoanPay.h @@ -0,0 +1,53 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_LOANPAY_H_INCLUDED +#define RIPPLE_TX_LOANPAY_H_INCLUDED + +#include + +namespace ripple { + +class LoanPay : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit LoanPay(ApplyContext& ctx) : Transactor(ctx) + { + } + + static bool + isEnabled(PreflightContext const& ctx); + + static NotTEC + doPreflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +//------------------------------------------------------------------------------ + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp new file mode 100644 index 0000000000..3ce1c2cd73 --- /dev/null +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -0,0 +1,418 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +// +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +bool +LoanSet::isEnabled(PreflightContext const& ctx) +{ + return lendingProtocolEnabled(ctx); +} + +std::uint32_t +LoanSet::getFlagsMask(PreflightContext const& ctx) +{ + return tfLoanSetMask; +} + +NotTEC +LoanSet::doPreflight(PreflightContext const& ctx) +{ + auto const& tx = ctx.tx; + auto const counterPartySig = ctx.tx.getFieldObject(sfCounterpartySignature); + + if (auto const ret = + ripple::detail::preflightCheckSigningKey(counterPartySig, ctx.j)) + return ret; + + if (auto const data = tx[~sfData]; data && !data->empty() && + !validDataLength(tx[~sfData], maxDataPayloadLength)) + return temINVALID; + if (!validNumericRange(tx[~sfOverpaymentFee], maxOverpaymentFee)) + return temINVALID; + if (!validNumericRange(tx[~sfLateInterestRate], maxLateInterestRate)) + return temINVALID; + if (!validNumericRange(tx[~sfCloseInterestRate], maxCloseInterestRate)) + return temINVALID; + if (!validNumericRange( + tx[~sfOverpaymentInterestRate], maxOverpaymentInterestRate)) + return temINVALID; + + if (auto const paymentTotal = tx[~sfPaymentTotal]; + paymentTotal && *paymentTotal == 0) + return temINVALID; + + if (auto const paymentInterval = + tx[~sfPaymentInterval].value_or(LoanSet::defaultPaymentInterval); + paymentInterval < LoanSet::minPaymentInterval) + return temINVALID; + + else if (auto const gracePeriod = + tx[~sfGracePeriod].value_or(LoanSet::defaultGracePeriod); + gracePeriod > paymentInterval) + return temINVALID; + + // Copied from preflight2 + if (auto const ret = ripple::detail::preflightCheckSimulateKeys( + ctx.flags, counterPartySig, ctx.j)) + return *ret; + + return tesSUCCESS; +} + +NotTEC +LoanSet::checkSign(PreclaimContext const& ctx) +{ + if (auto ret = Transactor::checkSign(ctx)) + return ret; + + // Counter signer is optional. If it's not specified, it's assumed to be + // `LoanBroker.Owner`. Note that we have not checked whether the + // loanbroker exists at this point. + auto const counterSigner = [&]() -> std::optional { + if (auto const c = ctx.tx.at(~sfCounterparty)) + return c; + + if (auto const broker = + ctx.view.read(keylet::loanbroker(ctx.tx[sfLoanBrokerID]))) + return broker->at(sfOwner); + return std::nullopt; + }(); + if (!counterSigner) + return temBAD_SIGNER; + // Counterparty signature is required + auto const counterSig = ctx.tx.getFieldObject(sfCounterpartySignature); + return Transactor::checkSign(ctx, *counterSigner, counterSig); +} + +XRPAmount +LoanSet::calculateBaseFee(ReadView const& view, STTx const& tx) +{ + auto const normalCost = Transactor::calculateBaseFee(view, tx); + + // Compute the additional cost of each signature in the + // CounterpartySignature, whether a single signature or a multisignature + XRPAmount const baseFee = view.fees().base; + + auto const counterSig = tx.getFieldObject(sfCounterpartySignature); + // Each signer adds one more baseFee to the minimum required fee + // for the transaction. Note that unlike the base class, if there are no + // signers, 1 extra signature is still counted for the single signer. + std::size_t const signerCount = + tx.isFieldPresent(sfSigners) ? tx.getFieldArray(sfSigners).size() : 1; + + return normalCost + (signerCount * baseFee); +} + +TER +LoanSet::preclaim(PreclaimContext const& ctx) +{ + auto const& tx = ctx.tx; + + if (auto const startDate(tx[sfStartDate]); hasExpired(ctx.view, startDate)) + { + JLOG(ctx.j.warn()) << "Start date is in the past."; + return tecEXPIRED; + } + + auto const account = tx[sfAccount]; + auto const brokerID = tx[sfLoanBrokerID]; + + auto const brokerSle = ctx.view.read(keylet::loanbroker(brokerID)); + if (!brokerSle) + { + // This can only be hit if there's a counterparty specified, otherwise + // it'll fail in the signature check + JLOG(ctx.j.warn()) << "LoanBroker does not exist."; + return tecNO_ENTRY; + } + auto const brokerOwner = brokerSle->at(sfOwner); + auto const counterparty = tx[~sfCounterparty].value_or(brokerOwner); + if (account != brokerOwner && counterparty != brokerOwner) + { + JLOG(ctx.j.warn()) << "Neither Account nor Counterparty are the owner " + "of the LoanBroker."; + return tecNO_PERMISSION; + } + if (account == counterparty) + { + JLOG(ctx.j.warn()) << "Account and Counterparty are the same. Can not " + "loan money to yourself."; + return tecNO_PERMISSION; + } + + auto const borrower = counterparty == brokerOwner ? account : counterparty; + if (auto const borrowerSle = ctx.view.read(keylet::account(borrower)); + !borrowerSle) + { + // It may not be possible to hit this case, because it'll fail the + // signature check with terNO_ACCOUNT. + JLOG(ctx.j.warn()) << "Borrower does not exist."; + return terNO_ACCOUNT; + } + + auto const brokerPseudo = brokerSle->at(sfAccount); + auto const vault = ctx.view.read(keylet::vault(brokerSle->at(sfVaultID))); + if (!vault) + // Should be impossible + return tefBAD_LEDGER; // LCOV_EXCL_LINE + Asset const asset = vault->at(sfAsset); + + if (auto const ter = canAddHolding(ctx.view, asset)) + return ter; + + if (isFrozen(ctx.view, brokerOwner, asset) || + isFrozen(ctx.view, brokerPseudo, asset)) + { + JLOG(ctx.j.warn()) << "One of the affected accounts is frozen."; + return asset.holds() ? tecFROZEN : tecLOCKED; + } + if (asset.holds()) + { + auto const issue = asset.get(); + if (isDeepFrozen(ctx.view, borrower, issue.currency, issue.account)) + return tecFROZEN; + if (isDeepFrozen(ctx.view, brokerPseudo, issue.currency, issue.account)) + return tecFROZEN; + } + + auto const principalRequested = tx[sfPrincipalRequested]; + if (auto const assetsAvailable = vault->at(sfAssetsAvailable); + assetsAvailable < principalRequested) + { + JLOG(ctx.j.warn()) + << "Insufficient assets available in the Vault to fund the loan."; + return tecINSUFFICIENT_FUNDS; + } + auto const newDebtTotal = brokerSle->at(sfDebtTotal) + principalRequested; + if (brokerSle->at(sfDebtMaximum) < newDebtTotal) + { + JLOG(ctx.j.warn()) + << "Loan would exceed the maximum debt limit of the LoanBroker."; + return tecLIMIT_EXCEEDED; + } + TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)}; + if (brokerSle->at(sfCoverAvailable) < + tenthBipsOfValue(newDebtTotal, coverRateMinimum)) + { + JLOG(ctx.j.warn()) + << "Insufficient first-loss capital to cover the loan."; + return tecINSUFFICIENT_FUNDS; + } + + return tesSUCCESS; +} + +TER +LoanSet::doApply() +{ + auto const& tx = ctx_.tx; + auto& view = ctx_.view(); + + auto const brokerID = tx[sfLoanBrokerID]; + + auto const brokerSle = view.peek(keylet::loanbroker(brokerID)); + if (!brokerSle) + return tefBAD_LEDGER; // LCOV_EXCL_LINE + auto const brokerOwner = brokerSle->at(sfOwner); + auto const brokerOwnerSle = view.peek(keylet::account(brokerOwner)); + if (!brokerOwnerSle) + return tefBAD_LEDGER; // LCOV_EXCL_LINE + + auto const vaultSle = view.peek(keylet ::vault(brokerSle->at(sfVaultID))); + if (!vaultSle) + return tefBAD_LEDGER; // LCOV_EXCL_LINE + auto const vaultPseudo = vaultSle->at(sfAccount); + Asset const vaultAsset = vaultSle->at(sfAsset); + + auto const counterparty = tx[~sfCounterparty].value_or(brokerOwner); + auto const borrower = counterparty == brokerOwner ? account_ : counterparty; + auto const borrowerSle = view.peek(keylet::account(borrower)); + if (!borrowerSle) + { + return tefBAD_LEDGER; // LCOV_EXCL_LINE + } + + auto const brokerPseudo = brokerSle->at(sfAccount); + auto const brokerPseudoSle = view.peek(keylet::account(brokerPseudo)); + if (!brokerPseudoSle) + { + return tefBAD_LEDGER; // LCOV_EXCL_LINE + } + auto const principalRequested = roundToAsset( + vaultAsset, tx[sfPrincipalRequested], tx[sfPrincipalRequested]); + TenthBips32 const interestRate{tx[~sfInterestRate].value_or(0)}; + auto const originationFee = tx[~sfLoanOriginationFee]; + auto const loanAssetsAvailable = + principalRequested - originationFee.value_or(Number{}); + + adjustOwnerCount(view, borrowerSle, 1, j_); + auto ownerCount = borrowerSle->at(sfOwnerCount); + if (mPriorBalance < view.fees().accountReserve(ownerCount)) + return tecINSUFFICIENT_RESERVE; + + // Create a holding for the borrower if one does not already exist. + + // Account for the origination fee using two payments + // + // 1. Transfer loanAssetsAvailable (principalRequested - originationFee) + // from vault pseudo-account to LoanBroker pseudo-account. + if (auto const ter = accountSend( + view, + vaultPseudo, + brokerPseudo, + STAmount{vaultAsset, loanAssetsAvailable}, + j_, + WaiveTransferFee::Yes)) + return ter; + // 2. Transfer originationFee, if any, from vault pseudo-account to + // LoanBroker owner. + if (originationFee) + { + // Create the holding if it doesn't already exist (necessary for MPTs). + // The owner may have deleted their MPT / line at some point. + if (auto const ter = addEmptyHolding( + view, + brokerOwner, + brokerOwnerSle->at(sfBalance).value().xrp(), + vaultAsset, + j_); + ter != tesSUCCESS && ter != tecDUPLICATE) + // ignore tecDUPLICATE. That means the holding already exists, and + // is fine here + return ter; + if (auto const ter = accountSend( + view, + vaultPseudo, + brokerOwner, + STAmount{vaultAsset, *originationFee}, + j_, + WaiveTransferFee::Yes)) + return ter; + } + + auto const paymentInterval = + tx[~sfPaymentInterval].value_or(defaultPaymentInterval); + auto const paymentTotal = tx[~sfPaymentTotal].value_or(defaultPaymentTotal); + TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; + // The portion of the loan interest that will go to the vault (total + // interest minus the management fee) + auto const loanInterestToVault = loanInterestOutstandingMinusFee( + vaultAsset, + principalRequested, + principalRequested, + interestRate, + paymentInterval, + paymentTotal, + managementFeeRate); + auto const startDate = tx[sfStartDate]; + auto loanSequence = brokerSle->at(sfLoanSequence); + + // Create the loan + auto loan = std::make_shared(keylet::loan(brokerID, *loanSequence)); + + // Prevent copy/paste errors + auto setLoanField = + [&loan, &tx](auto const& field, std::uint32_t const defValue = 0) { + // at() is smart enough to unseat a default field set to the default + // value + loan->at(field) = tx[field].value_or(defValue); + }; + + // Set required tx fields and pre-computed fields + loan->at(sfPrincipalRequested) = principalRequested; + loan->at(sfPrincipalOutstanding) = principalRequested; + loan->at(sfStartDate) = startDate; + loan->at(sfPaymentInterval) = paymentInterval; + loan->at(sfLoanSequence) = loanSequence; + loan->at(sfLoanBrokerID) = brokerID; + loan->at(sfBorrower) = borrower; + // Set all other transaction fields directly from the transaction + if (tx.isFlag(tfLoanOverpayment)) + loan->setFlag(lsfLoanOverpayment); + setLoanField(~sfLoanOriginationFee); + setLoanField(~sfLoanServiceFee); + setLoanField(~sfLatePaymentFee); + setLoanField(~sfClosePaymentFee); + setLoanField(~sfOverpaymentFee); + setLoanField(~sfInterestRate); + setLoanField(~sfLateInterestRate); + setLoanField(~sfCloseInterestRate); + setLoanField(~sfOverpaymentInterestRate); + setLoanField(~sfGracePeriod, defaultGracePeriod); + // Set dynamic fields to their initial values + loan->at(sfPreviousPaymentDate) = 0; + loan->at(sfNextPaymentDueDate) = startDate + paymentInterval; + loan->at(sfPaymentRemaining) = paymentTotal; + loan->at(sfAssetsAvailable) = loanAssetsAvailable; + view.insert(loan); + + // Update the balances in the vault + vaultSle->at(sfAssetsAvailable) -= principalRequested; + vaultSle->at(sfAssetsTotal) += loanInterestToVault; + view.update(vaultSle); + + // Update the balances in the loan broker + brokerSle->at(sfDebtTotal) += principalRequested + loanInterestToVault; + // The broker's owner count is solely for the number of outstanding loans, + // and is distinct from the broker's pseudo-account's owner count + adjustOwnerCount(view, brokerSle, 1, j_); + loanSequence += 1; + view.update(brokerSle); + + // Put the loan into the pseudo-account's directory + if (auto const ter = dirLink(view, brokerPseudo, loan, sfLoanBrokerNode)) + return ter; + // Borrower is the owner of the loan + if (auto const ter = dirLink(view, borrower, loan, sfOwnerNode)) + return ter; + + return tesSUCCESS; +} + +//------------------------------------------------------------------------------ + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/LoanSet.h b/src/xrpld/app/tx/detail/LoanSet.h new file mode 100644 index 0000000000..888c1343ff --- /dev/null +++ b/src/xrpld/app/tx/detail/LoanSet.h @@ -0,0 +1,74 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_LOANSET_H_INCLUDED +#define RIPPLE_TX_LOANSET_H_INCLUDED + +#include + +namespace ripple { + +class LoanSet : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit LoanSet(ApplyContext& ctx) : Transactor(ctx) + { + } + + static bool + isEnabled(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + + static NotTEC + doPreflight(PreflightContext const& ctx); + + static NotTEC + checkSign(PreclaimContext const& ctx); + + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; + +public: + static std::uint32_t constexpr minPaymentTotal = 1; + static std::uint32_t constexpr defaultPaymentTotal = 1; + static_assert(defaultPaymentTotal >= minPaymentTotal); + + static std::uint32_t constexpr minPaymentInterval = 60; + static std::uint32_t constexpr defaultPaymentInterval = 60; + static_assert(defaultPaymentInterval >= minPaymentInterval); + + static std::uint32_t constexpr defaultGracePeriod = 60; + static_assert(defaultGracePeriod >= minPaymentInterval); +}; + +//------------------------------------------------------------------------------ + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/SetTrust.cpp b/src/xrpld/app/tx/detail/SetTrust.cpp index ed9528036a..01bbff3ba5 100644 --- a/src/xrpld/app/tx/detail/SetTrust.cpp +++ b/src/xrpld/app/tx/detail/SetTrust.cpp @@ -295,7 +295,9 @@ SetTrust::preclaim(PreclaimContext const& ctx) else return tecINTERNAL; // LCOV_EXCL_LINE } - else if (sleDst->isFieldPresent(sfVaultID)) + else if ( + sleDst->isFieldPresent(sfVaultID) || + sleDst->isFieldPresent(sfLoanBrokerID)) { if (!ctx.view.exists(keylet::line(id, uDstAccountID, currency))) return tecNO_PERMISSION; diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 1a93476e0d..0cbd3c0fcc 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -627,6 +627,16 @@ Transactor::checkSign( AccountID const& id, STObject const& sigObject) { + { + auto const sle = ctx.view.read(keylet::account(id)); + + if (ctx.view.rules().enabled(featureLendingProtocol) && + isPseudoAccount(sle)) + // Pseudo-accounts can't sign transactions. This check is gated on + // the Lending Protocol amendment because that's the project it was + // added under, and it doesn't justify another amendment + return tefBAD_AUTH; + } if (ctx.flags & tapDRY_RUN) { // This code must be different for `simulate` diff --git a/src/xrpld/ledger/ApplyView.h b/src/xrpld/ledger/ApplyView.h index 1abff33be0..b55a2b4e8e 100644 --- a/src/xrpld/ledger/ApplyView.h +++ b/src/xrpld/ledger/ApplyView.h @@ -383,6 +383,45 @@ public: emptyDirDelete(Keylet const& directory); }; +namespace directory { +/** Helper functions for managing low-level directory operations. + These are not part of the ApplyView interface. + + Don't use them unless you really, really know what you're doing. + Instead use dirAdd, dirInsert, etc. + */ + +std::uint64_t +createRoot( + ApplyView& view, + Keylet const& directory, + uint256 const& key, + std::function const&)> const& describe); + +auto +findPreviousPage(ApplyView& view, Keylet const& directory, SLE::ref start); + +std::uint64_t +insertKey( + ApplyView& view, + SLE::ref node, + std::uint64_t page, + bool preserveOrder, + STVector256& indexes, + uint256 const& key); + +std::optional +insertPage( + ApplyView& view, + std::uint64_t page, + SLE::pointer node, + std::uint64_t nextPage, + SLE::ref next, + uint256 const& key, + Keylet const& directory, + std::function const&)> const& describe); + +} // namespace directory } // namespace ripple #endif diff --git a/src/xrpld/ledger/View.h b/src/xrpld/ledger/View.h index 5aa8b7216d..5642047efc 100644 --- a/src/xrpld/ledger/View.h +++ b/src/xrpld/ledger/View.h @@ -504,7 +504,11 @@ dirNext( describeOwnerDir(AccountID const& account); [[nodiscard]] TER -dirLink(ApplyView& view, AccountID const& owner, std::shared_ptr& object); +dirLink( + ApplyView& view, + AccountID const& owner, + std::shared_ptr& object, + SF_UINT64 const& node = sfOwnerNode); AccountID pseudoAccountAddress(ReadView const& view, uint256 const& pseudoOwnerKey); diff --git a/src/xrpld/ledger/detail/ApplyView.cpp b/src/xrpld/ledger/detail/ApplyView.cpp index 3191b47cbb..b4e7da21df 100644 --- a/src/xrpld/ledger/detail/ApplyView.cpp +++ b/src/xrpld/ledger/detail/ApplyView.cpp @@ -25,6 +25,128 @@ namespace ripple { +namespace directory { + +std::uint64_t +createRoot( + ApplyView& view, + Keylet const& directory, + uint256 const& key, + std::function const&)> const& describe) +{ + auto newRoot = std::make_shared(directory); + newRoot->setFieldH256(sfRootIndex, directory.key); + describe(newRoot); + + STVector256 v; + v.push_back(key); + newRoot->setFieldV256(sfIndexes, v); + + view.insert(newRoot); + return std::uint64_t{0}; +} + +auto +findPreviousPage(ApplyView& view, Keylet const& directory, SLE::ref start) +{ + std::uint64_t page = start->getFieldU64(sfIndexPrevious); + + auto node = start; + + if (page) + { + node = view.peek(keylet::page(directory, page)); + if (!node) + LogicError("Directory chain: root back-pointer broken."); + } + + auto indexes = node->getFieldV256(sfIndexes); + return std::make_tuple(page, node, indexes); +} + +std::uint64_t +insertKey( + ApplyView& view, + SLE::ref node, + std::uint64_t page, + bool preserveOrder, + STVector256& indexes, + uint256 const& key) +{ + if (preserveOrder) + { + if (std::find(indexes.begin(), indexes.end(), key) != indexes.end()) + LogicError("dirInsert: double insertion"); + + indexes.push_back(key); + } + else + { + // We can't be sure if this page is already sorted because + // it may be a legacy page we haven't yet touched. Take + // the time to sort it. + std::sort(indexes.begin(), indexes.end()); + + auto pos = std::lower_bound(indexes.begin(), indexes.end(), key); + + if (pos != indexes.end() && key == *pos) + LogicError("dirInsert: double insertion"); + + indexes.insert(pos, key); + } + + node->setFieldV256(sfIndexes, indexes); + view.update(node); + return page; +} + +std::optional +insertPage( + ApplyView& view, + std::uint64_t page, + SLE::pointer node, + std::uint64_t nextPage, + SLE::ref next, + uint256 const& key, + Keylet const& directory, + std::function const&)> const& describe) +{ + // Check whether we're out of pages. + if (++page >= dirNodeMaxPages) + { + return std::nullopt; + } + + // We are about to create a new node; we'll link it to + // the chain first: + node->setFieldU64(sfIndexNext, page); + view.update(node); + + next->setFieldU64(sfIndexPrevious, page); + view.update(next); + + // Insert the new key: + STVector256 indexes; + indexes.push_back(key); + + node = std::make_shared(keylet::page(directory, page)); + node->setFieldH256(sfRootIndex, directory.key); + node->setFieldV256(sfIndexes, indexes); + + // Save some space by not specifying the value 0 since + // it's the default. + if (page != 1) + node->setFieldU64(sfIndexPrevious, page - 1); + if (nextPage) + node->setFieldU64(sfIndexNext, nextPage); + describe(node); + view.insert(node); + + return page; +} + +} // namespace directory + std::optional ApplyView::dirAdd( bool preserveOrder, @@ -37,89 +159,21 @@ ApplyView::dirAdd( if (!root) { // No root, make it. - root = std::make_shared(directory); - root->setFieldH256(sfRootIndex, directory.key); - describe(root); - - STVector256 v; - v.push_back(key); - root->setFieldV256(sfIndexes, v); - - insert(root); - return std::uint64_t{0}; + return directory::createRoot(*this, directory, key, describe); } - std::uint64_t page = root->getFieldU64(sfIndexPrevious); - - auto node = root; - - if (page) - { - node = peek(keylet::page(directory, page)); - if (!node) - LogicError("Directory chain: root back-pointer broken."); - } - - auto indexes = node->getFieldV256(sfIndexes); + auto [page, node, indexes] = + directory::findPreviousPage(*this, directory, root); // If there's space, we use it: if (indexes.size() < dirNodeMaxEntries) { - if (preserveOrder) - { - if (std::find(indexes.begin(), indexes.end(), key) != indexes.end()) - LogicError("dirInsert: double insertion"); - - indexes.push_back(key); - } - else - { - // We can't be sure if this page is already sorted because - // it may be a legacy page we haven't yet touched. Take - // the time to sort it. - std::sort(indexes.begin(), indexes.end()); - - auto pos = std::lower_bound(indexes.begin(), indexes.end(), key); - - if (pos != indexes.end() && key == *pos) - LogicError("dirInsert: double insertion"); - - indexes.insert(pos, key); - } - - node->setFieldV256(sfIndexes, indexes); - update(node); - return page; + return directory::insertKey( + *this, node, page, preserveOrder, indexes, key); } - // Check whether we're out of pages. - if (++page >= dirNodeMaxPages) - return std::nullopt; - - // We are about to create a new node; we'll link it to - // the chain first: - node->setFieldU64(sfIndexNext, page); - update(node); - - root->setFieldU64(sfIndexPrevious, page); - update(root); - - // Insert the new key: - indexes.clear(); - indexes.push_back(key); - - node = std::make_shared(keylet::page(directory, page)); - node->setFieldH256(sfRootIndex, directory.key); - node->setFieldV256(sfIndexes, indexes); - - // Save some space by not specifying the value 0 since - // it's the default. - if (page != 1) - node->setFieldU64(sfIndexPrevious, page - 1); - describe(node); - insert(node); - - return page; + return directory::insertPage( + *this, page, node, 0, root, key, directory, describe); } bool diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index 077b1a172a..d90f7a1ced 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -1042,13 +1042,17 @@ describeOwnerDir(AccountID const& account) } TER -dirLink(ApplyView& view, AccountID const& owner, std::shared_ptr& object) +dirLink( + ApplyView& view, + AccountID const& owner, + std::shared_ptr& object, + SF_UINT64 const& node) { auto const page = view.dirInsert( keylet::ownerDir(owner), object->key(), describeOwnerDir(owner)); if (!page) return tecDIR_FULL; // LCOV_EXCL_LINE - object->setFieldU64(sfOwnerNode, *page); + object->setFieldU64(node, *page); return tesSUCCESS; } @@ -1139,9 +1143,10 @@ createPseudoAccount( // Pseudo-accounts can't submit transactions, so set the sequence number // to 0 to make them easier to spot and verify, and add an extra level // of protection. - std::uint32_t const seqno = // - view.rules().enabled(featureSingleAssetVault) // - ? 0 // + std::uint32_t const seqno = // + view.rules().enabled(featureSingleAssetVault) || // + view.rules().enabled(featureLendingProtocol) // + ? 0 // : view.seq(); account->setFieldU32(sfSequence, seqno); // Ignore reserves requirement, disable the master key, allow default From ad5d28bc485e773b61538e59fc197b084e34d9b4 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 11 Jul 2025 16:14:10 -0400 Subject: [PATCH 006/291] Make a few tweaks to the changes in merge cc83ea8 - Not necessarily wrong, just not how I want it. - Fixes a build error in LoanBroker_test - In CreateOffer, fixes a logic error where _no_ offers will work if PermissionedDex amendment is enabled, and FlowCross is not --- src/libxrpl/protocol/STTx.cpp | 17 ++++++++++------- src/test/app/LoanBroker_test.cpp | 2 ++ src/test/jtx/TestHelpers.h | 2 +- src/xrpld/app/tx/detail/CreateOffer.cpp | 7 ++----- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index 162a9a756c..a1d30c63e5 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -495,6 +495,8 @@ multiSignHelper( auto const accountID = signer.getAccountID(sfAccount); // The account owner may not usually multisign for themselves. + // If they can, txnAccountID will be unseated, which is not equal to any + // value. if (txnAccountID == accountID) return Unexpected("Invalid multisigner."); @@ -556,7 +558,7 @@ STTx::checkBatchMultiSign( batchSigner, std::nullopt, fullyCanonical, - [&dataStart](AccountID const& accountID) mutable -> Serializer { + [&dataStart](AccountID const& accountID) -> Serializer { Serializer s = dataStart; finishMultiSigningData(accountID, s); return s; @@ -588,7 +590,7 @@ STTx::checkMultiSign( sigObject, txnAccountID, fullyCanonical, - [&dataStart](AccountID const& accountID) mutable -> Serializer { + [&dataStart](AccountID const& accountID) -> Serializer { Serializer s = dataStart; finishMultiSigningData(accountID, s); return s; @@ -620,11 +622,12 @@ STTx::getBatchTransactionIDs() const XRPL_ASSERT( getFieldArray(sfRawTransactions).size() != 0, "STTx::getBatchTransactionIDs : empty raw transactions"); - if (batch_txn_ids_.size() != 0) - return batch_txn_ids_; - - for (STObject const& rb : getFieldArray(sfRawTransactions)) - batch_txn_ids_.push_back(rb.getHash(HashPrefix::transactionID)); + // Don't early return so that the size check is always done. + if (batch_txn_ids_.size() == 0) + { + for (STObject const& rb : getFieldArray(sfRawTransactions)) + batch_txn_ids_.push_back(rb.getHash(HashPrefix::transactionID)); + } XRPL_ASSERT( batch_txn_ids_.size() == getFieldArray(sfRawTransactions).size(), diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 941d33103e..d52fc759a9 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -197,6 +197,8 @@ class LoanBroker_test : public beast::unit_test::suite auto verifyCoverAmount = [&env, &vault, &broker, &pseudoAccount, this](auto n) { + using namespace jtx; + auto const amount = vault.asset(n); BEAST_EXPECT( broker->at(sfCoverAvailable) == amount.number()); diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index f7f7a601fc..85a46ad76a 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -615,7 +615,7 @@ create( } // namespace check -static constexpr FeeLevel64 baseFeeLevel{256}; +static constexpr FeeLevel64 baseFeeLevel{TxQ::baseLevel}; static constexpr FeeLevel64 minEscalationFeeLevel = baseFeeLevel * 500; template diff --git a/src/xrpld/app/tx/detail/CreateOffer.cpp b/src/xrpld/app/tx/detail/CreateOffer.cpp index 815bad994d..3734f0787e 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.cpp +++ b/src/xrpld/app/tx/detail/CreateOffer.cpp @@ -48,12 +48,9 @@ CreateOffer::isEnabled(PreflightContext const& ctx) { // Permissioned offers should use the PE (which must be enabled by // featureFlowCross amendment) - if (ctx.rules.enabled(featurePermissionedDEX) && - !ctx.rules.enabled(featureFlowCross)) - return false; - return (!ctx.tx.isFieldPresent(sfDomainID)) || - ctx.rules.enabled(featurePermissionedDEX); + (ctx.rules.enabled(featurePermissionedDEX) && + ctx.rules.enabled(featureFlowCross)); } std::uint32_t From 9d052dc86c6ae0303a6fa1a02e0a19ba0f19ca7e Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 11 Jul 2025 17:06:26 -0400 Subject: [PATCH 007/291] Make a few tweaks to the changes in 907cc19a --- src/test/jtx/impl/Env.cpp | 4 +++- src/xrpld/app/tx/detail/Escrow.cpp | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index ca947558bc..eefe2bd94b 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -215,7 +215,9 @@ Env::balance(Account const& account, MPTIssue const& mptIssue) const if (!sle) return {STAmount(mptIssue, 0), account.name()}; - STAmount const amount{mptIssue, sle->getFieldU64(sfOutstandingAmount)}; + // Make it negative + STAmount const amount{ + mptIssue, sle->getFieldU64(sfOutstandingAmount), 0, true}; return {amount, lookup(issuer).name()}; } else diff --git a/src/xrpld/app/tx/detail/Escrow.cpp b/src/xrpld/app/tx/detail/Escrow.cpp index 85711e2a7c..b8532a6efe 100644 --- a/src/xrpld/app/tx/detail/Escrow.cpp +++ b/src/xrpld/app/tx/detail/Escrow.cpp @@ -81,8 +81,8 @@ namespace ripple { TxConsequences EscrowCreate::makeTxConsequences(PreflightContext const& ctx) { - return TxConsequences{ - ctx.tx, isXRP(ctx.tx[sfAmount]) ? ctx.tx[sfAmount].xrp() : beast::zero}; + auto const amount = ctx.tx[sfAmount]; + return TxConsequences{ctx.tx, isXRP(amount) ? amount.xrp() : beast::zero}; } template From e9d2dfe329c573c80e5c1a030f3c11d5235ead8e Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 11 Jul 2025 17:14:53 -0400 Subject: [PATCH 008/291] fixup! Make a few tweaks to the changes in merge cc83ea8 --- src/libxrpl/protocol/STTx.cpp | 2 +- src/xrpld/app/tx/detail/CreateOffer.cpp | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index a1d30c63e5..d51a63a092 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -622,7 +622,7 @@ STTx::getBatchTransactionIDs() const XRPL_ASSERT( getFieldArray(sfRawTransactions).size() != 0, "STTx::getBatchTransactionIDs : empty raw transactions"); - // Don't early return so that the size check is always done. + // Don't early return so that the size assert is always hit. if (batch_txn_ids_.size() == 0) { for (STObject const& rb : getFieldArray(sfRawTransactions)) diff --git a/src/xrpld/app/tx/detail/CreateOffer.cpp b/src/xrpld/app/tx/detail/CreateOffer.cpp index 3734f0787e..aa54cc81c9 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.cpp +++ b/src/xrpld/app/tx/detail/CreateOffer.cpp @@ -46,11 +46,18 @@ CreateOffer::makeTxConsequences(PreflightContext const& ctx) bool CreateOffer::isEnabled(PreflightContext const& ctx) { + // TODO: Remove or update this check. Even though FlowCross was enabled a + // long time ago, if a network did manage to enable PermissionedDex and not + // FlowCross, then no offers of any kind would be allowed. + // // Permissioned offers should use the PE (which must be enabled by // featureFlowCross amendment) + if (ctx.rules.enabled(featurePermissionedDEX) && + !ctx.rules.enabled(featureFlowCross)) + return false; + return (!ctx.tx.isFieldPresent(sfDomainID)) || - (ctx.rules.enabled(featurePermissionedDEX) && - ctx.rules.enabled(featureFlowCross)); + ctx.rules.enabled(featurePermissionedDEX); } std::uint32_t From d82461ea70cae48197079fb3bcc8c6210573786b Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 11 Jul 2025 18:42:39 -0400 Subject: [PATCH 009/291] Make preflight1 and preflight2 private static Transactor functions - They should never be called by derived classes. --- src/xrpld/app/tx/detail/Batch.cpp | 5 - src/xrpld/app/tx/detail/Escrow.cpp | 8 -- src/xrpld/app/tx/detail/Transactor.cpp | 135 +++++++++++++------------ src/xrpld/app/tx/detail/Transactor.h | 27 ++--- 4 files changed, 82 insertions(+), 93 deletions(-) diff --git a/src/xrpld/app/tx/detail/Batch.cpp b/src/xrpld/app/tx/detail/Batch.cpp index 601f755ff8..00595f55a1 100644 --- a/src/xrpld/app/tx/detail/Batch.cpp +++ b/src/xrpld/app/tx/detail/Batch.cpp @@ -378,11 +378,6 @@ Batch::doPreflight(PreflightContext const& ctx) requiredSigners.insert(innerAccount); } - // LCOV_EXCL_START - if (auto const ret = detail::preflight2(ctx); !isTesSuccess(ret)) - return ret; - // LCOV_EXCL_STOP - // Validation Batch Signers std::unordered_set batchSigners; if (ctx.tx.isFieldPresent(sfBatchSigners)) diff --git a/src/xrpld/app/tx/detail/Escrow.cpp b/src/xrpld/app/tx/detail/Escrow.cpp index b8532a6efe..e2601dbb76 100644 --- a/src/xrpld/app/tx/detail/Escrow.cpp +++ b/src/xrpld/app/tx/detail/Escrow.cpp @@ -650,14 +650,6 @@ EscrowFinish::doPreflight(PreflightContext const& ctx) if (static_cast(cb) != static_cast(fb)) return temMALFORMED; - // Verify the transaction signature. If it doesn't work - // then don't do any more work. - { - auto const ret = detail::preflight2(ctx); - if (!isTesSuccess(ret)) - return ret; - } - if (cb && fb) { auto& router = ctx.app.getHashRouter(); diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index b27400767b..8f0ed78b88 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -112,69 +112,6 @@ preflightCheckSigningKey(STObject const& sigObject, beast::Journal j) return tesSUCCESS; } -/** Performs early sanity checks on the account and fee fields */ -NotTEC -preflight1(PreflightContext const& ctx, std::uint32_t flagMask) -{ - // This is inappropriate in preflight0, because only Change transactions - // skip this function, and those do not allow an sfTicketSequence field. - if (ctx.tx.isFieldPresent(sfTicketSequence) && - !ctx.rules.enabled(featureTicketBatch)) - { - return temMALFORMED; - } - - if (ctx.tx.isFieldPresent(sfDelegate)) - { - if (!ctx.rules.enabled(featurePermissionDelegation)) - return temDISABLED; - - if (ctx.tx[sfDelegate] == ctx.tx[sfAccount]) - return temBAD_SIGNER; - } - - if (auto const ret = preflight0(ctx, flagMask)) - return ret; - - auto const id = ctx.tx.getAccountID(sfAccount); - if (id == beast::zero) - { - JLOG(ctx.j.warn()) << "preflight1: bad account id"; - return temBAD_SRC_ACCOUNT; - } - - // No point in going any further if the transaction fee is malformed. - auto const fee = ctx.tx.getFieldAmount(sfFee); - if (!fee.native() || fee.negative() || !isLegalAmount(fee.xrp())) - { - JLOG(ctx.j.debug()) << "preflight1: invalid fee"; - return temBAD_FEE; - } - - if (auto const ret = preflightCheckSigningKey(ctx.tx, ctx.j)) - return ret; - - // An AccountTxnID field constrains transaction ordering more than the - // Sequence field. Tickets, on the other hand, reduce ordering - // constraints. Because Tickets and AccountTxnID work against one - // another the combination is unsupported and treated as malformed. - // - // We return temINVALID for such transactions. - if (ctx.tx.getSeqProxy().isTicket() && - ctx.tx.isFieldPresent(sfAccountTxnID)) - return temINVALID; - - if (ctx.tx.isFlag(tfInnerBatchTxn) && !ctx.rules.enabled(featureBatch)) - return temINVALID_FLAG; - - XRPL_ASSERT( - ctx.tx.isFlag(tfInnerBatchTxn) == ctx.parentBatchId.has_value() || - !ctx.rules.enabled(featureBatch), - "Inner batch transaction must have a parent batch ID."); - - return tesSUCCESS; -} - std::optional preflightCheckSimulateKeys( ApplyFlags flags, @@ -217,11 +154,77 @@ preflightCheckSimulateKeys( return {}; } +} // namespace detail + +/** Performs early sanity checks on the account and fee fields */ +NotTEC +Transactor::preflight1(PreflightContext const& ctx, std::uint32_t flagMask) +{ + // This is inappropriate in preflight0, because only Change transactions + // skip this function, and those do not allow an sfTicketSequence field. + if (ctx.tx.isFieldPresent(sfTicketSequence) && + !ctx.rules.enabled(featureTicketBatch)) + { + return temMALFORMED; + } + + if (ctx.tx.isFieldPresent(sfDelegate)) + { + if (!ctx.rules.enabled(featurePermissionDelegation)) + return temDISABLED; + + if (ctx.tx[sfDelegate] == ctx.tx[sfAccount]) + return temBAD_SIGNER; + } + + if (auto const ret = preflight0(ctx, flagMask)) + return ret; + + auto const id = ctx.tx.getAccountID(sfAccount); + if (id == beast::zero) + { + JLOG(ctx.j.warn()) << "preflight1: bad account id"; + return temBAD_SRC_ACCOUNT; + } + + // No point in going any further if the transaction fee is malformed. + auto const fee = ctx.tx.getFieldAmount(sfFee); + if (!fee.native() || fee.negative() || !isLegalAmount(fee.xrp())) + { + JLOG(ctx.j.debug()) << "preflight1: invalid fee"; + return temBAD_FEE; + } + + if (auto const ret = detail::preflightCheckSigningKey(ctx.tx, ctx.j)) + return ret; + + // An AccountTxnID field constrains transaction ordering more than the + // Sequence field. Tickets, on the other hand, reduce ordering + // constraints. Because Tickets and AccountTxnID work against one + // another the combination is unsupported and treated as malformed. + // + // We return temINVALID for such transactions. + if (ctx.tx.getSeqProxy().isTicket() && + ctx.tx.isFieldPresent(sfAccountTxnID)) + return temINVALID; + + if (ctx.tx.isFlag(tfInnerBatchTxn) && !ctx.rules.enabled(featureBatch)) + return temINVALID_FLAG; + + XRPL_ASSERT( + ctx.tx.isFlag(tfInnerBatchTxn) == ctx.parentBatchId.has_value() || + !ctx.rules.enabled(featureBatch), + "Inner batch transaction must have a parent batch ID."); + + return tesSUCCESS; +} + /** Checks whether the signature appears valid */ NotTEC -preflight2(PreflightContext const& ctx) +Transactor::preflight2(PreflightContext const& ctx) { - if (auto const ret = preflightCheckSimulateKeys(ctx.flags, ctx.tx, ctx.j)) + if (auto const ret = + detail::preflightCheckSimulateKeys(ctx.flags, ctx.tx, ctx.j)) // Skips following checks if the transaction is being simulated, // regardless of success or failure return *ret; @@ -236,8 +239,6 @@ preflight2(PreflightContext const& ctx) return tesSUCCESS; } -} // namespace detail - //------------------------------------------------------------------------------ Transactor::Transactor(ApplyContext& ctx) diff --git a/src/xrpld/app/tx/detail/Transactor.h b/src/xrpld/app/tx/detail/Transactor.h index e5e932f566..b2ed320c1b 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -319,6 +319,18 @@ private: STObject const& sigObject); void trapTransaction(uint256) const; + + // Helper functions for preflight checks. Do not use directly. + /** Performs early sanity checks on the account and fee fields. + + (And passes flagMask to preflight0) + */ + static NotTEC + preflight1(PreflightContext const& ctx, std::uint32_t flagMask); + + /** Checks whether the signature appears valid */ + static NotTEC + preflight2(PreflightContext const& ctx); }; inline bool @@ -340,13 +352,6 @@ namespace detail { NotTEC preflightCheckSigningKey(STObject const& sigObject, beast::Journal j); -/** Performs early sanity checks on the account and fee fields. - - (And passes flagMask to preflight0) -*/ -NotTEC -preflight1(PreflightContext const& ctx, std::uint32_t flagMask); - /** Checks the special signing key state needed for simulation * * Normally called from preflight2 with ctx.tx. @@ -356,10 +361,6 @@ preflightCheckSimulateKeys( ApplyFlags flags, STObject const& sigObject, beast::Journal j); - -/** Checks whether the signature appears valid */ -NotTEC -preflight2(PreflightContext const& ctx); } // namespace detail // Defined in Change.cpp @@ -374,13 +375,13 @@ Transactor::preflight(PreflightContext const& ctx) if (!T::isEnabled(ctx)) return temDISABLED; - if (auto const ret = ripple::detail::preflight1(ctx, T::getFlagsMask(ctx))) + if (auto const ret = preflight1(ctx, T::getFlagsMask(ctx))) return ret; if (auto const ret = T::doPreflight(ctx)) return ret; - return ripple::detail::preflight2(ctx); + return preflight2(ctx); } template From 269eac9a154656ce7908eb53b234741d0ec44cdd Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 11 Jul 2025 18:55:08 -0400 Subject: [PATCH 010/291] Mark the LendingProtocol amendment as unsupported --- include/xrpl/protocol/detail/features.macro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 9905159b80..09a05fcb00 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -35,7 +35,7 @@ // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. -XRPL_FEATURE(LendingProtocol, Supported::yes, VoteBehavior::DefaultNo) +XRPL_FEATURE(LendingProtocol, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(TokenEscrow, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (EnforceNFTokenTrustlineV2, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (AMMv1_3, Supported::yes, VoteBehavior::DefaultNo) From 37745cb5b2ce01de5c63b89629d251c545726ade Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 11 Jul 2025 19:17:11 -0400 Subject: [PATCH 011/291] Rename Transactor preflight functions - Rename Transactor::preflight to invokePreflight. - Rename doPreflight back to preflight. - Update instructions. - With preflight1 & 2 now uncallable, in-flight code in other branches should be easier to convert. --- src/test/app/AMM_test.cpp | 6 ++-- src/xrpld/app/tx/detail/AMMBid.cpp | 2 +- src/xrpld/app/tx/detail/AMMBid.h | 2 +- src/xrpld/app/tx/detail/AMMClawback.cpp | 2 +- src/xrpld/app/tx/detail/AMMClawback.h | 2 +- src/xrpld/app/tx/detail/AMMCreate.cpp | 2 +- src/xrpld/app/tx/detail/AMMCreate.h | 2 +- src/xrpld/app/tx/detail/AMMDelete.cpp | 2 +- src/xrpld/app/tx/detail/AMMDelete.h | 2 +- src/xrpld/app/tx/detail/AMMDeposit.cpp | 2 +- src/xrpld/app/tx/detail/AMMDeposit.h | 2 +- src/xrpld/app/tx/detail/AMMVote.cpp | 2 +- src/xrpld/app/tx/detail/AMMVote.h | 2 +- src/xrpld/app/tx/detail/AMMWithdraw.cpp | 2 +- src/xrpld/app/tx/detail/AMMWithdraw.h | 2 +- src/xrpld/app/tx/detail/Batch.cpp | 2 +- src/xrpld/app/tx/detail/Batch.h | 2 +- src/xrpld/app/tx/detail/CancelCheck.cpp | 2 +- src/xrpld/app/tx/detail/CancelCheck.h | 2 +- src/xrpld/app/tx/detail/CancelOffer.cpp | 2 +- src/xrpld/app/tx/detail/CancelOffer.h | 2 +- src/xrpld/app/tx/detail/CashCheck.cpp | 2 +- src/xrpld/app/tx/detail/CashCheck.h | 2 +- src/xrpld/app/tx/detail/Change.cpp | 2 +- src/xrpld/app/tx/detail/Clawback.cpp | 2 +- src/xrpld/app/tx/detail/Clawback.h | 2 +- src/xrpld/app/tx/detail/CreateCheck.cpp | 2 +- src/xrpld/app/tx/detail/CreateCheck.h | 2 +- src/xrpld/app/tx/detail/CreateOffer.cpp | 2 +- src/xrpld/app/tx/detail/CreateOffer.h | 2 +- src/xrpld/app/tx/detail/CreateTicket.cpp | 2 +- src/xrpld/app/tx/detail/CreateTicket.h | 2 +- src/xrpld/app/tx/detail/Credentials.cpp | 6 ++-- src/xrpld/app/tx/detail/Credentials.h | 6 ++-- src/xrpld/app/tx/detail/DID.cpp | 4 +-- src/xrpld/app/tx/detail/DID.h | 4 +-- src/xrpld/app/tx/detail/DelegateSet.cpp | 2 +- src/xrpld/app/tx/detail/DelegateSet.h | 2 +- src/xrpld/app/tx/detail/DeleteAccount.cpp | 2 +- src/xrpld/app/tx/detail/DeleteAccount.h | 2 +- src/xrpld/app/tx/detail/DeleteOracle.cpp | 2 +- src/xrpld/app/tx/detail/DeleteOracle.h | 2 +- src/xrpld/app/tx/detail/DepositPreauth.cpp | 2 +- src/xrpld/app/tx/detail/DepositPreauth.h | 2 +- src/xrpld/app/tx/detail/Escrow.cpp | 6 ++-- src/xrpld/app/tx/detail/Escrow.h | 6 ++-- src/xrpld/app/tx/detail/LedgerStateFix.cpp | 2 +- src/xrpld/app/tx/detail/LedgerStateFix.h | 2 +- .../app/tx/detail/LoanBrokerCoverDeposit.cpp | 2 +- .../app/tx/detail/LoanBrokerCoverDeposit.h | 2 +- .../app/tx/detail/LoanBrokerCoverWithdraw.cpp | 2 +- .../app/tx/detail/LoanBrokerCoverWithdraw.h | 2 +- src/xrpld/app/tx/detail/LoanBrokerDelete.cpp | 2 +- src/xrpld/app/tx/detail/LoanBrokerDelete.h | 2 +- src/xrpld/app/tx/detail/LoanBrokerSet.cpp | 2 +- src/xrpld/app/tx/detail/LoanBrokerSet.h | 2 +- src/xrpld/app/tx/detail/LoanDelete.cpp | 2 +- src/xrpld/app/tx/detail/LoanDelete.h | 2 +- src/xrpld/app/tx/detail/LoanDraw.cpp | 2 +- src/xrpld/app/tx/detail/LoanDraw.h | 2 +- src/xrpld/app/tx/detail/LoanManage.cpp | 2 +- src/xrpld/app/tx/detail/LoanManage.h | 2 +- src/xrpld/app/tx/detail/LoanPay.cpp | 2 +- src/xrpld/app/tx/detail/LoanPay.h | 2 +- src/xrpld/app/tx/detail/LoanSet.cpp | 2 +- src/xrpld/app/tx/detail/LoanSet.h | 2 +- src/xrpld/app/tx/detail/MPTokenAuthorize.cpp | 2 +- src/xrpld/app/tx/detail/MPTokenAuthorize.h | 2 +- .../app/tx/detail/MPTokenIssuanceCreate.cpp | 2 +- .../app/tx/detail/MPTokenIssuanceCreate.h | 2 +- .../app/tx/detail/MPTokenIssuanceDestroy.cpp | 2 +- .../app/tx/detail/MPTokenIssuanceDestroy.h | 2 +- .../app/tx/detail/MPTokenIssuanceSet.cpp | 2 +- src/xrpld/app/tx/detail/MPTokenIssuanceSet.h | 2 +- .../app/tx/detail/NFTokenAcceptOffer.cpp | 2 +- src/xrpld/app/tx/detail/NFTokenAcceptOffer.h | 2 +- src/xrpld/app/tx/detail/NFTokenBurn.cpp | 2 +- src/xrpld/app/tx/detail/NFTokenBurn.h | 2 +- .../app/tx/detail/NFTokenCancelOffer.cpp | 2 +- src/xrpld/app/tx/detail/NFTokenCancelOffer.h | 2 +- .../app/tx/detail/NFTokenCreateOffer.cpp | 2 +- src/xrpld/app/tx/detail/NFTokenCreateOffer.h | 2 +- src/xrpld/app/tx/detail/NFTokenMint.cpp | 2 +- src/xrpld/app/tx/detail/NFTokenMint.h | 2 +- src/xrpld/app/tx/detail/NFTokenModify.cpp | 2 +- src/xrpld/app/tx/detail/NFTokenModify.h | 2 +- src/xrpld/app/tx/detail/PayChan.cpp | 6 ++-- src/xrpld/app/tx/detail/PayChan.h | 6 ++-- src/xrpld/app/tx/detail/Payment.cpp | 2 +- src/xrpld/app/tx/detail/Payment.h | 2 +- .../tx/detail/PermissionedDomainDelete.cpp | 2 +- .../app/tx/detail/PermissionedDomainDelete.h | 2 +- .../app/tx/detail/PermissionedDomainSet.cpp | 2 +- .../app/tx/detail/PermissionedDomainSet.h | 2 +- src/xrpld/app/tx/detail/SetAccount.cpp | 2 +- src/xrpld/app/tx/detail/SetAccount.h | 2 +- src/xrpld/app/tx/detail/SetOracle.cpp | 2 +- src/xrpld/app/tx/detail/SetOracle.h | 2 +- src/xrpld/app/tx/detail/SetRegularKey.cpp | 2 +- src/xrpld/app/tx/detail/SetRegularKey.h | 2 +- src/xrpld/app/tx/detail/SetSignerList.cpp | 2 +- src/xrpld/app/tx/detail/SetSignerList.h | 2 +- src/xrpld/app/tx/detail/SetTrust.cpp | 2 +- src/xrpld/app/tx/detail/SetTrust.h | 2 +- src/xrpld/app/tx/detail/Transactor.h | 30 +++++++++++++------ src/xrpld/app/tx/detail/VaultClawback.cpp | 2 +- src/xrpld/app/tx/detail/VaultClawback.h | 2 +- src/xrpld/app/tx/detail/VaultCreate.cpp | 2 +- src/xrpld/app/tx/detail/VaultCreate.h | 2 +- src/xrpld/app/tx/detail/VaultDelete.cpp | 2 +- src/xrpld/app/tx/detail/VaultDelete.h | 2 +- src/xrpld/app/tx/detail/VaultDeposit.cpp | 2 +- src/xrpld/app/tx/detail/VaultDeposit.h | 2 +- src/xrpld/app/tx/detail/VaultSet.cpp | 2 +- src/xrpld/app/tx/detail/VaultSet.h | 2 +- src/xrpld/app/tx/detail/VaultWithdraw.cpp | 2 +- src/xrpld/app/tx/detail/VaultWithdraw.h | 2 +- src/xrpld/app/tx/detail/XChainBridge.cpp | 22 +++++++------- src/xrpld/app/tx/detail/XChainBridge.h | 16 +++++----- src/xrpld/app/tx/detail/applySteps.cpp | 2 +- 120 files changed, 173 insertions(+), 161 deletions(-) diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index 286d1ca425..960f92c071 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -3571,7 +3571,7 @@ private: env.current()->rules(), tapNONE, env.journal); - auto pf = Transactor::preflight(pfctx); + auto pf = Transactor::invokePreflight(pfctx); BEAST_EXPECT(pf == temDISABLED); env.app().config().features.insert(featureAMM); } @@ -3586,7 +3586,7 @@ private: env.current()->rules(), tapNONE, env.journal); - auto pf = Transactor::preflight(pfctx); + auto pf = Transactor::invokePreflight(pfctx); BEAST_EXPECT(pf != tesSUCCESS); } @@ -3601,7 +3601,7 @@ private: env.current()->rules(), tapNONE, env.journal); - auto pf = Transactor::preflight(pfctx); + auto pf = Transactor::invokePreflight(pfctx); BEAST_EXPECT(pf == temBAD_AMM_TOKENS); } } diff --git a/src/xrpld/app/tx/detail/AMMBid.cpp b/src/xrpld/app/tx/detail/AMMBid.cpp index d14b668790..2fc420974d 100644 --- a/src/xrpld/app/tx/detail/AMMBid.cpp +++ b/src/xrpld/app/tx/detail/AMMBid.cpp @@ -37,7 +37,7 @@ AMMBid::isEnabled(PreflightContext const& ctx) } NotTEC -AMMBid::doPreflight(PreflightContext const& ctx) +AMMBid::preflight(PreflightContext const& ctx) { if (auto const res = invalidAMMAssetPair( ctx.tx[sfAsset].get(), ctx.tx[sfAsset2].get())) diff --git a/src/xrpld/app/tx/detail/AMMBid.h b/src/xrpld/app/tx/detail/AMMBid.h index 9260ae78f2..9e69aca471 100644 --- a/src/xrpld/app/tx/detail/AMMBid.h +++ b/src/xrpld/app/tx/detail/AMMBid.h @@ -75,7 +75,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/AMMClawback.cpp b/src/xrpld/app/tx/detail/AMMClawback.cpp index 527af9b471..74cfb673ee 100644 --- a/src/xrpld/app/tx/detail/AMMClawback.cpp +++ b/src/xrpld/app/tx/detail/AMMClawback.cpp @@ -46,7 +46,7 @@ AMMClawback::getFlagsMask(PreflightContext const& ctx) } NotTEC -AMMClawback::doPreflight(PreflightContext const& ctx) +AMMClawback::preflight(PreflightContext const& ctx) { AccountID const issuer = ctx.tx[sfAccount]; AccountID const holder = ctx.tx[sfHolder]; diff --git a/src/xrpld/app/tx/detail/AMMClawback.h b/src/xrpld/app/tx/detail/AMMClawback.h index 828d07343b..0e354ebdc7 100644 --- a/src/xrpld/app/tx/detail/AMMClawback.h +++ b/src/xrpld/app/tx/detail/AMMClawback.h @@ -40,7 +40,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/AMMCreate.cpp b/src/xrpld/app/tx/detail/AMMCreate.cpp index 14bc8f7cda..c2e1503888 100644 --- a/src/xrpld/app/tx/detail/AMMCreate.cpp +++ b/src/xrpld/app/tx/detail/AMMCreate.cpp @@ -38,7 +38,7 @@ AMMCreate::isEnabled(PreflightContext const& ctx) } NotTEC -AMMCreate::doPreflight(PreflightContext const& ctx) +AMMCreate::preflight(PreflightContext const& ctx) { auto const amount = ctx.tx[sfAmount]; auto const amount2 = ctx.tx[sfAmount2]; diff --git a/src/xrpld/app/tx/detail/AMMCreate.h b/src/xrpld/app/tx/detail/AMMCreate.h index f6313dd8af..aab4a4fb27 100644 --- a/src/xrpld/app/tx/detail/AMMCreate.h +++ b/src/xrpld/app/tx/detail/AMMCreate.h @@ -67,7 +67,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); diff --git a/src/xrpld/app/tx/detail/AMMDelete.cpp b/src/xrpld/app/tx/detail/AMMDelete.cpp index 172a33073e..31e18573fc 100644 --- a/src/xrpld/app/tx/detail/AMMDelete.cpp +++ b/src/xrpld/app/tx/detail/AMMDelete.cpp @@ -34,7 +34,7 @@ AMMDelete::isEnabled(PreflightContext const& ctx) } NotTEC -AMMDelete::doPreflight(PreflightContext const& ctx) +AMMDelete::preflight(PreflightContext const& ctx) { return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/AMMDelete.h b/src/xrpld/app/tx/detail/AMMDelete.h index 87abf92c27..1c44571021 100644 --- a/src/xrpld/app/tx/detail/AMMDelete.h +++ b/src/xrpld/app/tx/detail/AMMDelete.h @@ -43,7 +43,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/AMMDeposit.cpp b/src/xrpld/app/tx/detail/AMMDeposit.cpp index 2aad1e05b9..9df16ef12a 100644 --- a/src/xrpld/app/tx/detail/AMMDeposit.cpp +++ b/src/xrpld/app/tx/detail/AMMDeposit.cpp @@ -43,7 +43,7 @@ AMMDeposit::getFlagsMask(PreflightContext const& ctx) } NotTEC -AMMDeposit::doPreflight(PreflightContext const& ctx) +AMMDeposit::preflight(PreflightContext const& ctx) { auto const flags = ctx.tx.getFlags(); diff --git a/src/xrpld/app/tx/detail/AMMDeposit.h b/src/xrpld/app/tx/detail/AMMDeposit.h index 982c2d8abd..09e1f040ff 100644 --- a/src/xrpld/app/tx/detail/AMMDeposit.h +++ b/src/xrpld/app/tx/detail/AMMDeposit.h @@ -75,7 +75,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/AMMVote.cpp b/src/xrpld/app/tx/detail/AMMVote.cpp index ca3435449d..1c5cd543eb 100644 --- a/src/xrpld/app/tx/detail/AMMVote.cpp +++ b/src/xrpld/app/tx/detail/AMMVote.cpp @@ -34,7 +34,7 @@ AMMVote::isEnabled(PreflightContext const& ctx) } NotTEC -AMMVote::doPreflight(PreflightContext const& ctx) +AMMVote::preflight(PreflightContext const& ctx) { if (auto const res = invalidAMMAssetPair( ctx.tx[sfAsset].get(), ctx.tx[sfAsset2].get())) diff --git a/src/xrpld/app/tx/detail/AMMVote.h b/src/xrpld/app/tx/detail/AMMVote.h index bc94517961..af7691a3b4 100644 --- a/src/xrpld/app/tx/detail/AMMVote.h +++ b/src/xrpld/app/tx/detail/AMMVote.h @@ -60,7 +60,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/AMMWithdraw.cpp b/src/xrpld/app/tx/detail/AMMWithdraw.cpp index 712695fd61..6f16052de2 100644 --- a/src/xrpld/app/tx/detail/AMMWithdraw.cpp +++ b/src/xrpld/app/tx/detail/AMMWithdraw.cpp @@ -41,7 +41,7 @@ AMMWithdraw::getFlagsMask(PreflightContext const& ctx) } NotTEC -AMMWithdraw::doPreflight(PreflightContext const& ctx) +AMMWithdraw::preflight(PreflightContext const& ctx) { auto const flags = ctx.tx.getFlags(); diff --git a/src/xrpld/app/tx/detail/AMMWithdraw.h b/src/xrpld/app/tx/detail/AMMWithdraw.h index 7afda43e1e..e70f74bad2 100644 --- a/src/xrpld/app/tx/detail/AMMWithdraw.h +++ b/src/xrpld/app/tx/detail/AMMWithdraw.h @@ -82,7 +82,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/Batch.cpp b/src/xrpld/app/tx/detail/Batch.cpp index 00595f55a1..5f05d9919e 100644 --- a/src/xrpld/app/tx/detail/Batch.cpp +++ b/src/xrpld/app/tx/detail/Batch.cpp @@ -210,7 +210,7 @@ Batch::getFlagsMask(PreflightContext const& ctx) * code otherwise. */ NotTEC -Batch::doPreflight(PreflightContext const& ctx) +Batch::preflight(PreflightContext const& ctx) { auto const parentBatchId = ctx.tx.getTransactionID(); auto const outerAccount = ctx.tx.getAccountID(sfAccount); diff --git a/src/xrpld/app/tx/detail/Batch.h b/src/xrpld/app/tx/detail/Batch.h index 858a3d9c3d..c8a264f328 100644 --- a/src/xrpld/app/tx/detail/Batch.h +++ b/src/xrpld/app/tx/detail/Batch.h @@ -47,7 +47,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static NotTEC checkSign(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/CancelCheck.cpp b/src/xrpld/app/tx/detail/CancelCheck.cpp index 15b647446b..82ec1bbd1e 100644 --- a/src/xrpld/app/tx/detail/CancelCheck.cpp +++ b/src/xrpld/app/tx/detail/CancelCheck.cpp @@ -36,7 +36,7 @@ CancelCheck::isEnabled(PreflightContext const& ctx) } NotTEC -CancelCheck::doPreflight(PreflightContext const& ctx) +CancelCheck::preflight(PreflightContext const& ctx) { return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/CancelCheck.h b/src/xrpld/app/tx/detail/CancelCheck.h index 3b40d24008..44cf3e0721 100644 --- a/src/xrpld/app/tx/detail/CancelCheck.h +++ b/src/xrpld/app/tx/detail/CancelCheck.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/CancelOffer.cpp b/src/xrpld/app/tx/detail/CancelOffer.cpp index 6141f63e48..99ca792b3b 100644 --- a/src/xrpld/app/tx/detail/CancelOffer.cpp +++ b/src/xrpld/app/tx/detail/CancelOffer.cpp @@ -26,7 +26,7 @@ namespace ripple { NotTEC -CancelOffer::doPreflight(PreflightContext const& ctx) +CancelOffer::preflight(PreflightContext const& ctx) { if (!ctx.tx[sfOfferSequence]) { diff --git a/src/xrpld/app/tx/detail/CancelOffer.h b/src/xrpld/app/tx/detail/CancelOffer.h index 8632115026..0813dd0aef 100644 --- a/src/xrpld/app/tx/detail/CancelOffer.h +++ b/src/xrpld/app/tx/detail/CancelOffer.h @@ -36,7 +36,7 @@ public: } static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/CashCheck.cpp b/src/xrpld/app/tx/detail/CashCheck.cpp index 02babcfbe5..7768db31c9 100644 --- a/src/xrpld/app/tx/detail/CashCheck.cpp +++ b/src/xrpld/app/tx/detail/CashCheck.cpp @@ -39,7 +39,7 @@ CashCheck::isEnabled(PreflightContext const& ctx) } NotTEC -CashCheck::doPreflight(PreflightContext const& ctx) +CashCheck::preflight(PreflightContext const& ctx) { // Exactly one of Amount or DeliverMin must be present. auto const optAmount = ctx.tx[~sfAmount]; diff --git a/src/xrpld/app/tx/detail/CashCheck.h b/src/xrpld/app/tx/detail/CashCheck.h index 5dbd21a8e3..cc3d564efe 100644 --- a/src/xrpld/app/tx/detail/CashCheck.h +++ b/src/xrpld/app/tx/detail/CashCheck.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/Change.cpp b/src/xrpld/app/tx/detail/Change.cpp index 8d4a42e50d..6a816d2b61 100644 --- a/src/xrpld/app/tx/detail/Change.cpp +++ b/src/xrpld/app/tx/detail/Change.cpp @@ -35,7 +35,7 @@ namespace ripple { template <> NotTEC -Transactor::preflight(PreflightContext const& ctx) +Transactor::invokePreflight(PreflightContext const& ctx) { // 0 means "Allow any flags" // The check for tfChangeMask is gated by LendingProtocol because that diff --git a/src/xrpld/app/tx/detail/Clawback.cpp b/src/xrpld/app/tx/detail/Clawback.cpp index 0375b6b718..012565ad95 100644 --- a/src/xrpld/app/tx/detail/Clawback.cpp +++ b/src/xrpld/app/tx/detail/Clawback.cpp @@ -88,7 +88,7 @@ Clawback::getFlagsMask(PreflightContext const& ctx) } NotTEC -Clawback::doPreflight(PreflightContext const& ctx) +Clawback::preflight(PreflightContext const& ctx) { if (auto const ret = std::visit( [&](T const&) { return preflightHelper(ctx); }, diff --git a/src/xrpld/app/tx/detail/Clawback.h b/src/xrpld/app/tx/detail/Clawback.h index 6353c79a55..8300d98fde 100644 --- a/src/xrpld/app/tx/detail/Clawback.h +++ b/src/xrpld/app/tx/detail/Clawback.h @@ -40,7 +40,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/CreateCheck.cpp b/src/xrpld/app/tx/detail/CreateCheck.cpp index 7b19629230..a8ebce4204 100644 --- a/src/xrpld/app/tx/detail/CreateCheck.cpp +++ b/src/xrpld/app/tx/detail/CreateCheck.cpp @@ -35,7 +35,7 @@ CreateCheck::isEnabled(PreflightContext const& ctx) } NotTEC -CreateCheck::doPreflight(PreflightContext const& ctx) +CreateCheck::preflight(PreflightContext const& ctx) { if (ctx.tx[sfAccount] == ctx.tx[sfDestination]) { diff --git a/src/xrpld/app/tx/detail/CreateCheck.h b/src/xrpld/app/tx/detail/CreateCheck.h index cb794332ca..1d9a7ce168 100644 --- a/src/xrpld/app/tx/detail/CreateCheck.h +++ b/src/xrpld/app/tx/detail/CreateCheck.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/CreateOffer.cpp b/src/xrpld/app/tx/detail/CreateOffer.cpp index aa54cc81c9..9ddc4d2502 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.cpp +++ b/src/xrpld/app/tx/detail/CreateOffer.cpp @@ -70,7 +70,7 @@ CreateOffer::getFlagsMask(PreflightContext const& ctx) } NotTEC -CreateOffer::doPreflight(PreflightContext const& ctx) +CreateOffer::preflight(PreflightContext const& ctx) { auto& tx = ctx.tx; auto& j = ctx.j; diff --git a/src/xrpld/app/tx/detail/CreateOffer.h b/src/xrpld/app/tx/detail/CreateOffer.h index cff1022a6b..27d0107e41 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.h +++ b/src/xrpld/app/tx/detail/CreateOffer.h @@ -52,7 +52,7 @@ public: /** Enforce constraints beyond those of the Transactor base class. */ static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); /** Enforce constraints beyond those of the Transactor base class. */ static TER diff --git a/src/xrpld/app/tx/detail/CreateTicket.cpp b/src/xrpld/app/tx/detail/CreateTicket.cpp index 254cf68555..8d88dd7bf8 100644 --- a/src/xrpld/app/tx/detail/CreateTicket.cpp +++ b/src/xrpld/app/tx/detail/CreateTicket.cpp @@ -40,7 +40,7 @@ CreateTicket::isEnabled(PreflightContext const& ctx) } NotTEC -CreateTicket::doPreflight(PreflightContext const& ctx) +CreateTicket::preflight(PreflightContext const& ctx) { if (std::uint32_t const count = ctx.tx[sfTicketCount]; count < minValidCount || count > maxValidCount) diff --git a/src/xrpld/app/tx/detail/CreateTicket.h b/src/xrpld/app/tx/detail/CreateTicket.h index 746daf7c56..04ad6711c7 100644 --- a/src/xrpld/app/tx/detail/CreateTicket.h +++ b/src/xrpld/app/tx/detail/CreateTicket.h @@ -74,7 +74,7 @@ public: /** Enforce constraints beyond those of the Transactor base class. */ static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); /** Enforce constraints beyond those of the Transactor base class. */ static TER diff --git a/src/xrpld/app/tx/detail/Credentials.cpp b/src/xrpld/app/tx/detail/Credentials.cpp index 83873a9612..abd5539100 100644 --- a/src/xrpld/app/tx/detail/Credentials.cpp +++ b/src/xrpld/app/tx/detail/Credentials.cpp @@ -62,7 +62,7 @@ CredentialCreate::getFlagsMask(PreflightContext const& ctx) } NotTEC -CredentialCreate::doPreflight(PreflightContext const& ctx) +CredentialCreate::preflight(PreflightContext const& ctx) { auto const& tx = ctx.tx; auto& j = ctx.j; @@ -213,7 +213,7 @@ CredentialDelete::getFlagsMask(PreflightContext const& ctx) } NotTEC -CredentialDelete::doPreflight(PreflightContext const& ctx) +CredentialDelete::preflight(PreflightContext const& ctx) { auto const subject = ctx.tx[~sfSubject]; auto const issuer = ctx.tx[~sfIssuer]; @@ -297,7 +297,7 @@ CredentialAccept::getFlagsMask(PreflightContext const& ctx) } NotTEC -CredentialAccept::doPreflight(PreflightContext const& ctx) +CredentialAccept::preflight(PreflightContext const& ctx) { if (!ctx.tx[sfIssuer]) { diff --git a/src/xrpld/app/tx/detail/Credentials.h b/src/xrpld/app/tx/detail/Credentials.h index ee19c5c0a3..31b1ee8cf7 100644 --- a/src/xrpld/app/tx/detail/Credentials.h +++ b/src/xrpld/app/tx/detail/Credentials.h @@ -40,7 +40,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -67,7 +67,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -94,7 +94,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/DID.cpp b/src/xrpld/app/tx/detail/DID.cpp index d74c9733ae..8c1e23f4d5 100644 --- a/src/xrpld/app/tx/detail/DID.cpp +++ b/src/xrpld/app/tx/detail/DID.cpp @@ -49,7 +49,7 @@ DIDSet::isEnabled(PreflightContext const& ctx) } NotTEC -DIDSet::doPreflight(PreflightContext const& ctx) +DIDSet::preflight(PreflightContext const& ctx) { if (!ctx.tx.isFieldPresent(sfURI) && !ctx.tx.isFieldPresent(sfDIDDocument) && !ctx.tx.isFieldPresent(sfData)) @@ -175,7 +175,7 @@ DIDDelete::isEnabled(PreflightContext const& ctx) } NotTEC -DIDDelete::doPreflight(PreflightContext const& ctx) +DIDDelete::preflight(PreflightContext const& ctx) { return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/DID.h b/src/xrpld/app/tx/detail/DID.h index 1995176d17..c9fbb199b9 100644 --- a/src/xrpld/app/tx/detail/DID.h +++ b/src/xrpld/app/tx/detail/DID.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); TER doApply() override; @@ -58,7 +58,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER deleteSLE(ApplyContext& ctx, Keylet sleKeylet, AccountID const owner); diff --git a/src/xrpld/app/tx/detail/DelegateSet.cpp b/src/xrpld/app/tx/detail/DelegateSet.cpp index ebd52c113e..5e6b17f187 100644 --- a/src/xrpld/app/tx/detail/DelegateSet.cpp +++ b/src/xrpld/app/tx/detail/DelegateSet.cpp @@ -35,7 +35,7 @@ DelegateSet::isEnabled(PreflightContext const& ctx) } NotTEC -DelegateSet::doPreflight(PreflightContext const& ctx) +DelegateSet::preflight(PreflightContext const& ctx) { auto const& permissions = ctx.tx.getFieldArray(sfPermissions); if (permissions.size() > permissionMaxSize) diff --git a/src/xrpld/app/tx/detail/DelegateSet.h b/src/xrpld/app/tx/detail/DelegateSet.h index 87166ab5fb..c95b0f8fd4 100644 --- a/src/xrpld/app/tx/detail/DelegateSet.h +++ b/src/xrpld/app/tx/detail/DelegateSet.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/DeleteAccount.cpp b/src/xrpld/app/tx/detail/DeleteAccount.cpp index 0aa3031130..4f49384b87 100644 --- a/src/xrpld/app/tx/detail/DeleteAccount.cpp +++ b/src/xrpld/app/tx/detail/DeleteAccount.cpp @@ -49,7 +49,7 @@ DeleteAccount::isEnabled(PreflightContext const& ctx) } NotTEC -DeleteAccount::doPreflight(PreflightContext const& ctx) +DeleteAccount::preflight(PreflightContext const& ctx) { if (ctx.tx[sfAccount] == ctx.tx[sfDestination]) // An account cannot be deleted and give itself the resulting XRP. diff --git a/src/xrpld/app/tx/detail/DeleteAccount.h b/src/xrpld/app/tx/detail/DeleteAccount.h index 42946bfc27..a67267ad7b 100644 --- a/src/xrpld/app/tx/detail/DeleteAccount.h +++ b/src/xrpld/app/tx/detail/DeleteAccount.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); diff --git a/src/xrpld/app/tx/detail/DeleteOracle.cpp b/src/xrpld/app/tx/detail/DeleteOracle.cpp index 2e49877d9b..ac078e5c9d 100644 --- a/src/xrpld/app/tx/detail/DeleteOracle.cpp +++ b/src/xrpld/app/tx/detail/DeleteOracle.cpp @@ -33,7 +33,7 @@ DeleteOracle::isEnabled(PreflightContext const& ctx) } NotTEC -DeleteOracle::doPreflight(PreflightContext const& ctx) +DeleteOracle::preflight(PreflightContext const& ctx) { return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/DeleteOracle.h b/src/xrpld/app/tx/detail/DeleteOracle.h index 87183268ff..f55b5d0f50 100644 --- a/src/xrpld/app/tx/detail/DeleteOracle.h +++ b/src/xrpld/app/tx/detail/DeleteOracle.h @@ -46,7 +46,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/DepositPreauth.cpp b/src/xrpld/app/tx/detail/DepositPreauth.cpp index a1bdafa852..3d1cbf7381 100644 --- a/src/xrpld/app/tx/detail/DepositPreauth.cpp +++ b/src/xrpld/app/tx/detail/DepositPreauth.cpp @@ -46,7 +46,7 @@ DepositPreauth::isEnabled(PreflightContext const& ctx) } NotTEC -DepositPreauth::doPreflight(PreflightContext const& ctx) +DepositPreauth::preflight(PreflightContext const& ctx) { bool const authArrPresent = ctx.tx.isFieldPresent(sfAuthorizeCredentials); bool const unauthArrPresent = diff --git a/src/xrpld/app/tx/detail/DepositPreauth.h b/src/xrpld/app/tx/detail/DepositPreauth.h index 3d969b8df4..384b098aef 100644 --- a/src/xrpld/app/tx/detail/DepositPreauth.h +++ b/src/xrpld/app/tx/detail/DepositPreauth.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/Escrow.cpp b/src/xrpld/app/tx/detail/Escrow.cpp index e2601dbb76..501fce3d73 100644 --- a/src/xrpld/app/tx/detail/Escrow.cpp +++ b/src/xrpld/app/tx/detail/Escrow.cpp @@ -126,7 +126,7 @@ EscrowCreate::getFlagsMask(PreflightContext const& ctx) } NotTEC -EscrowCreate::doPreflight(PreflightContext const& ctx) +EscrowCreate::preflight(PreflightContext const& ctx) { STAmount const amount{ctx.tx[sfAmount]}; if (!isXRP(amount)) @@ -640,7 +640,7 @@ EscrowFinish::getFlagsMask(PreflightContext const& ctx) } NotTEC -EscrowFinish::doPreflight(PreflightContext const& ctx) +EscrowFinish::preflight(PreflightContext const& ctx) { auto const cb = ctx.tx[~sfCondition]; auto const fb = ctx.tx[~sfFulfillment]; @@ -1202,7 +1202,7 @@ EscrowCancel::getFlagsMask(PreflightContext const& ctx) } NotTEC -EscrowCancel::doPreflight(PreflightContext const& ctx) +EscrowCancel::preflight(PreflightContext const& ctx) { return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/Escrow.h b/src/xrpld/app/tx/detail/Escrow.h index 86ae208b67..9696515963 100644 --- a/src/xrpld/app/tx/detail/Escrow.h +++ b/src/xrpld/app/tx/detail/Escrow.h @@ -40,7 +40,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -67,7 +67,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); @@ -94,7 +94,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/LedgerStateFix.cpp b/src/xrpld/app/tx/detail/LedgerStateFix.cpp index 680cc30d95..eeb67bbc91 100644 --- a/src/xrpld/app/tx/detail/LedgerStateFix.cpp +++ b/src/xrpld/app/tx/detail/LedgerStateFix.cpp @@ -34,7 +34,7 @@ LedgerStateFix::isEnabled(PreflightContext const& ctx) } NotTEC -LedgerStateFix::doPreflight(PreflightContext const& ctx) +LedgerStateFix::preflight(PreflightContext const& ctx) { switch (ctx.tx[sfLedgerFixType]) { diff --git a/src/xrpld/app/tx/detail/LedgerStateFix.h b/src/xrpld/app/tx/detail/LedgerStateFix.h index e67dfd26c9..b93dcd6a00 100644 --- a/src/xrpld/app/tx/detail/LedgerStateFix.h +++ b/src/xrpld/app/tx/detail/LedgerStateFix.h @@ -41,7 +41,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp index d82a59ed0d..923703a6a0 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp @@ -49,7 +49,7 @@ LoanBrokerCoverDeposit::isEnabled(PreflightContext const& ctx) } NotTEC -LoanBrokerCoverDeposit::doPreflight(PreflightContext const& ctx) +LoanBrokerCoverDeposit::preflight(PreflightContext const& ctx) { if (ctx.tx[sfLoanBrokerID] == beast::zero) return temINVALID; diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.h b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.h index 7e9a10119d..548b873dec 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.h +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp index b776eea535..a1e1233b89 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp @@ -49,7 +49,7 @@ LoanBrokerCoverWithdraw::isEnabled(PreflightContext const& ctx) } NotTEC -LoanBrokerCoverWithdraw::doPreflight(PreflightContext const& ctx) +LoanBrokerCoverWithdraw::preflight(PreflightContext const& ctx) { if (ctx.tx[sfLoanBrokerID] == beast::zero) return temINVALID; diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h index f7a03f7bb8..ce7eb79abe 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp b/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp index f76b1e8a0f..d5eeb9c152 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp @@ -49,7 +49,7 @@ LoanBrokerDelete::isEnabled(PreflightContext const& ctx) } NotTEC -LoanBrokerDelete::doPreflight(PreflightContext const& ctx) +LoanBrokerDelete::preflight(PreflightContext const& ctx) { return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/LoanBrokerDelete.h b/src/xrpld/app/tx/detail/LoanBrokerDelete.h index 7382106c40..41e1ff6853 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerDelete.h +++ b/src/xrpld/app/tx/detail/LoanBrokerDelete.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp index 7589a5817a..70f329af16 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp @@ -52,7 +52,7 @@ LoanBrokerSet::isEnabled(PreflightContext const& ctx) } NotTEC -LoanBrokerSet::doPreflight(PreflightContext const& ctx) +LoanBrokerSet::preflight(PreflightContext const& ctx) { auto const& tx = ctx.tx; if (auto const data = tx[~sfData]; data && !data->empty() && diff --git a/src/xrpld/app/tx/detail/LoanBrokerSet.h b/src/xrpld/app/tx/detail/LoanBrokerSet.h index 018833132b..7e4e02211e 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerSet.h +++ b/src/xrpld/app/tx/detail/LoanBrokerSet.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/LoanDelete.cpp b/src/xrpld/app/tx/detail/LoanDelete.cpp index d29c03e969..4cf9e9a929 100644 --- a/src/xrpld/app/tx/detail/LoanDelete.cpp +++ b/src/xrpld/app/tx/detail/LoanDelete.cpp @@ -51,7 +51,7 @@ LoanDelete::isEnabled(PreflightContext const& ctx) } NotTEC -LoanDelete::doPreflight(PreflightContext const& ctx) +LoanDelete::preflight(PreflightContext const& ctx) { if (ctx.tx[sfLoanID] == beast::zero) return temINVALID; diff --git a/src/xrpld/app/tx/detail/LoanDelete.h b/src/xrpld/app/tx/detail/LoanDelete.h index 6909953099..74825b5bcd 100644 --- a/src/xrpld/app/tx/detail/LoanDelete.h +++ b/src/xrpld/app/tx/detail/LoanDelete.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/LoanDraw.cpp b/src/xrpld/app/tx/detail/LoanDraw.cpp index 1f01639fe0..2e957c6f79 100644 --- a/src/xrpld/app/tx/detail/LoanDraw.cpp +++ b/src/xrpld/app/tx/detail/LoanDraw.cpp @@ -51,7 +51,7 @@ LoanDraw::isEnabled(PreflightContext const& ctx) } NotTEC -LoanDraw::doPreflight(PreflightContext const& ctx) +LoanDraw::preflight(PreflightContext const& ctx) { if (ctx.tx[sfLoanID] == beast::zero) return temINVALID; diff --git a/src/xrpld/app/tx/detail/LoanDraw.h b/src/xrpld/app/tx/detail/LoanDraw.h index bb8c5dcf22..a4d0a29b79 100644 --- a/src/xrpld/app/tx/detail/LoanDraw.h +++ b/src/xrpld/app/tx/detail/LoanDraw.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index d44bb5ae7b..a218059ade 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -58,7 +58,7 @@ LoanManage::getFlagsMask(PreflightContext const& ctx) } NotTEC -LoanManage::doPreflight(PreflightContext const& ctx) +LoanManage::preflight(PreflightContext const& ctx) { if (ctx.tx[sfLoanID] == beast::zero) return temINVALID; diff --git a/src/xrpld/app/tx/detail/LoanManage.h b/src/xrpld/app/tx/detail/LoanManage.h index 74b8a7299f..7f61008c57 100644 --- a/src/xrpld/app/tx/detail/LoanManage.h +++ b/src/xrpld/app/tx/detail/LoanManage.h @@ -40,7 +40,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 5ff03b267b..e83e4ae27e 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -51,7 +51,7 @@ LoanPay::isEnabled(PreflightContext const& ctx) } NotTEC -LoanPay::doPreflight(PreflightContext const& ctx) +LoanPay::preflight(PreflightContext const& ctx) { if (ctx.tx[sfLoanID] == beast::zero) return temINVALID; diff --git a/src/xrpld/app/tx/detail/LoanPay.h b/src/xrpld/app/tx/detail/LoanPay.h index f608a7fb38..3f4b208aa2 100644 --- a/src/xrpld/app/tx/detail/LoanPay.h +++ b/src/xrpld/app/tx/detail/LoanPay.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 3ce1c2cd73..c6cfed0335 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -59,7 +59,7 @@ LoanSet::getFlagsMask(PreflightContext const& ctx) } NotTEC -LoanSet::doPreflight(PreflightContext const& ctx) +LoanSet::preflight(PreflightContext const& ctx) { auto const& tx = ctx.tx; auto const counterPartySig = ctx.tx.getFieldObject(sfCounterpartySignature); diff --git a/src/xrpld/app/tx/detail/LoanSet.h b/src/xrpld/app/tx/detail/LoanSet.h index 888c1343ff..342f822168 100644 --- a/src/xrpld/app/tx/detail/LoanSet.h +++ b/src/xrpld/app/tx/detail/LoanSet.h @@ -40,7 +40,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static NotTEC checkSign(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp index f4a93ffb3e..f9dbb0995f 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp @@ -39,7 +39,7 @@ MPTokenAuthorize::getFlagsMask(PreflightContext const& ctx) } NotTEC -MPTokenAuthorize::doPreflight(PreflightContext const& ctx) +MPTokenAuthorize::preflight(PreflightContext const& ctx) { if (ctx.tx[sfAccount] == ctx.tx[~sfHolder]) return temMALFORMED; diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.h b/src/xrpld/app/tx/detail/MPTokenAuthorize.h index 63ca2c8b01..5c386db530 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.h +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.h @@ -49,7 +49,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp index 8f751015d8..f34b97a6b4 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp @@ -38,7 +38,7 @@ MPTokenIssuanceCreate::getFlagsMask(PreflightContext const& ctx) } NotTEC -MPTokenIssuanceCreate::doPreflight(PreflightContext const& ctx) +MPTokenIssuanceCreate::preflight(PreflightContext const& ctx) { if (auto const fee = ctx.tx[~sfTransferFee]) { diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h index 3df61d52ce..ce8f30fda9 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h @@ -56,7 +56,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); TER doApply() override; diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp index 7c0d5baf31..d52dcca8bf 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp @@ -38,7 +38,7 @@ MPTokenIssuanceDestroy::getFlagsMask(PreflightContext const& ctx) } NotTEC -MPTokenIssuanceDestroy::doPreflight(PreflightContext const& ctx) +MPTokenIssuanceDestroy::preflight(PreflightContext const& ctx) { return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h index 8718caf5b5..dd2131713b 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h @@ -40,7 +40,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp index a36df80d21..ca09ab9fe1 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp @@ -38,7 +38,7 @@ MPTokenIssuanceSet::getFlagsMask(PreflightContext const& ctx) } NotTEC -MPTokenIssuanceSet::doPreflight(PreflightContext const& ctx) +MPTokenIssuanceSet::preflight(PreflightContext const& ctx) { auto const txFlags = ctx.tx.getFlags(); diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h index ac2b2fa6da..55d6110fcd 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h @@ -40,7 +40,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER checkPermission(ReadView const& view, STTx const& tx); diff --git a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp index df56b0c763..6cb8e89c40 100644 --- a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp +++ b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp @@ -40,7 +40,7 @@ NFTokenAcceptOffer::getFlagsMask(PreflightContext const& ctx) } NotTEC -NFTokenAcceptOffer::doPreflight(PreflightContext const& ctx) +NFTokenAcceptOffer::preflight(PreflightContext const& ctx) { auto const bo = ctx.tx[~sfNFTokenBuyOffer]; auto const so = ctx.tx[~sfNFTokenSellOffer]; diff --git a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.h b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.h index 637944af92..b049aad46e 100644 --- a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.h +++ b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.h @@ -58,7 +58,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/NFTokenBurn.cpp b/src/xrpld/app/tx/detail/NFTokenBurn.cpp index 4948ef7f2a..80aec56af6 100644 --- a/src/xrpld/app/tx/detail/NFTokenBurn.cpp +++ b/src/xrpld/app/tx/detail/NFTokenBurn.cpp @@ -33,7 +33,7 @@ NFTokenBurn::isEnabled(PreflightContext const& ctx) } NotTEC -NFTokenBurn::doPreflight(PreflightContext const& ctx) +NFTokenBurn::preflight(PreflightContext const& ctx) { return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/NFTokenBurn.h b/src/xrpld/app/tx/detail/NFTokenBurn.h index ff930c1be7..f1ae0752db 100644 --- a/src/xrpld/app/tx/detail/NFTokenBurn.h +++ b/src/xrpld/app/tx/detail/NFTokenBurn.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/NFTokenCancelOffer.cpp b/src/xrpld/app/tx/detail/NFTokenCancelOffer.cpp index 5c6c406d2d..66edddcc82 100644 --- a/src/xrpld/app/tx/detail/NFTokenCancelOffer.cpp +++ b/src/xrpld/app/tx/detail/NFTokenCancelOffer.cpp @@ -41,7 +41,7 @@ NFTokenCancelOffer::getFlagsMask(PreflightContext const& ctx) } NotTEC -NFTokenCancelOffer::doPreflight(PreflightContext const& ctx) +NFTokenCancelOffer::preflight(PreflightContext const& ctx) { if (auto const& ids = ctx.tx[sfNFTokenOffers]; ids.empty() || (ids.size() > maxTokenOfferCancelCount)) diff --git a/src/xrpld/app/tx/detail/NFTokenCancelOffer.h b/src/xrpld/app/tx/detail/NFTokenCancelOffer.h index e112dce6f8..bdd628e58d 100644 --- a/src/xrpld/app/tx/detail/NFTokenCancelOffer.h +++ b/src/xrpld/app/tx/detail/NFTokenCancelOffer.h @@ -40,7 +40,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp b/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp index 46baaf7f49..76ebc9e24d 100644 --- a/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp +++ b/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp @@ -39,7 +39,7 @@ NFTokenCreateOffer::getFlagsMask(PreflightContext const& ctx) } NotTEC -NFTokenCreateOffer::doPreflight(PreflightContext const& ctx) +NFTokenCreateOffer::preflight(PreflightContext const& ctx) { auto const txFlags = ctx.tx.getFlags(); diff --git a/src/xrpld/app/tx/detail/NFTokenCreateOffer.h b/src/xrpld/app/tx/detail/NFTokenCreateOffer.h index fb1b6a575f..58946bad55 100644 --- a/src/xrpld/app/tx/detail/NFTokenCreateOffer.h +++ b/src/xrpld/app/tx/detail/NFTokenCreateOffer.h @@ -40,7 +40,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/NFTokenMint.cpp b/src/xrpld/app/tx/detail/NFTokenMint.cpp index 3dcad38ee1..db49e122e2 100644 --- a/src/xrpld/app/tx/detail/NFTokenMint.cpp +++ b/src/xrpld/app/tx/detail/NFTokenMint.cpp @@ -84,7 +84,7 @@ NFTokenMint::getFlagsMask(PreflightContext const& ctx) } NotTEC -NFTokenMint::doPreflight(PreflightContext const& ctx) +NFTokenMint::preflight(PreflightContext const& ctx) { if (auto const f = ctx.tx[~sfTransferFee]) { diff --git a/src/xrpld/app/tx/detail/NFTokenMint.h b/src/xrpld/app/tx/detail/NFTokenMint.h index 4af7448532..696ad856ab 100644 --- a/src/xrpld/app/tx/detail/NFTokenMint.h +++ b/src/xrpld/app/tx/detail/NFTokenMint.h @@ -43,7 +43,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/NFTokenModify.cpp b/src/xrpld/app/tx/detail/NFTokenModify.cpp index 54f26387b1..98cbd5760a 100644 --- a/src/xrpld/app/tx/detail/NFTokenModify.cpp +++ b/src/xrpld/app/tx/detail/NFTokenModify.cpp @@ -33,7 +33,7 @@ NFTokenModify::isEnabled(PreflightContext const& ctx) } NotTEC -NFTokenModify::doPreflight(PreflightContext const& ctx) +NFTokenModify::preflight(PreflightContext const& ctx) { if (auto owner = ctx.tx[~sfOwner]; owner == ctx.tx[sfAccount]) return temMALFORMED; diff --git a/src/xrpld/app/tx/detail/NFTokenModify.h b/src/xrpld/app/tx/detail/NFTokenModify.h index a0936c6af1..8a80e422c5 100644 --- a/src/xrpld/app/tx/detail/NFTokenModify.h +++ b/src/xrpld/app/tx/detail/NFTokenModify.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/PayChan.cpp b/src/xrpld/app/tx/detail/PayChan.cpp index 40c3a22871..d90a661321 100644 --- a/src/xrpld/app/tx/detail/PayChan.cpp +++ b/src/xrpld/app/tx/detail/PayChan.cpp @@ -179,7 +179,7 @@ PayChanCreate::getFlagsMask(PreflightContext const& ctx) } NotTEC -PayChanCreate::doPreflight(PreflightContext const& ctx) +PayChanCreate::preflight(PreflightContext const& ctx) { if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero)) return temBAD_AMOUNT; @@ -335,7 +335,7 @@ PayChanFund::getFlagsMask(PreflightContext const& ctx) } NotTEC -PayChanFund::doPreflight(PreflightContext const& ctx) +PayChanFund::preflight(PreflightContext const& ctx) { if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero)) return temBAD_AMOUNT; @@ -433,7 +433,7 @@ PayChanClaim::getFlagsMask(PreflightContext const& ctx) } NotTEC -PayChanClaim::doPreflight(PreflightContext const& ctx) +PayChanClaim::preflight(PreflightContext const& ctx) { auto const bal = ctx.tx[~sfBalance]; if (bal && (!isXRP(*bal) || *bal <= beast::zero)) diff --git a/src/xrpld/app/tx/detail/PayChan.h b/src/xrpld/app/tx/detail/PayChan.h index 1faa34499b..9390aa52b9 100644 --- a/src/xrpld/app/tx/detail/PayChan.h +++ b/src/xrpld/app/tx/detail/PayChan.h @@ -40,7 +40,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -69,7 +69,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); TER doApply() override; @@ -95,7 +95,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index 34ba3b94f6..da5e185770 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -90,7 +90,7 @@ Payment::getFlagsMask(PreflightContext const& ctx) } NotTEC -Payment::doPreflight(PreflightContext const& ctx) +Payment::preflight(PreflightContext const& ctx) { auto& tx = ctx.tx; auto& j = ctx.j; diff --git a/src/xrpld/app/tx/detail/Payment.h b/src/xrpld/app/tx/detail/Payment.h index 577b7827ae..3f16dd97bc 100644 --- a/src/xrpld/app/tx/detail/Payment.h +++ b/src/xrpld/app/tx/detail/Payment.h @@ -49,7 +49,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER checkPermission(ReadView const& view, STTx const& tx); diff --git a/src/xrpld/app/tx/detail/PermissionedDomainDelete.cpp b/src/xrpld/app/tx/detail/PermissionedDomainDelete.cpp index d89831dfe0..6087bae8ad 100644 --- a/src/xrpld/app/tx/detail/PermissionedDomainDelete.cpp +++ b/src/xrpld/app/tx/detail/PermissionedDomainDelete.cpp @@ -31,7 +31,7 @@ PermissionedDomainDelete::isEnabled(PreflightContext const& ctx) } NotTEC -PermissionedDomainDelete::doPreflight(PreflightContext const& ctx) +PermissionedDomainDelete::preflight(PreflightContext const& ctx) { auto const domain = ctx.tx.getFieldH256(sfDomainID); if (domain == beast::zero) diff --git a/src/xrpld/app/tx/detail/PermissionedDomainDelete.h b/src/xrpld/app/tx/detail/PermissionedDomainDelete.h index 574a99bffa..403d0786e3 100644 --- a/src/xrpld/app/tx/detail/PermissionedDomainDelete.h +++ b/src/xrpld/app/tx/detail/PermissionedDomainDelete.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/PermissionedDomainSet.cpp b/src/xrpld/app/tx/detail/PermissionedDomainSet.cpp index 98aedb7304..8e27fdddd6 100644 --- a/src/xrpld/app/tx/detail/PermissionedDomainSet.cpp +++ b/src/xrpld/app/tx/detail/PermissionedDomainSet.cpp @@ -36,7 +36,7 @@ PermissionedDomainSet::isEnabled(PreflightContext const& ctx) } NotTEC -PermissionedDomainSet::doPreflight(PreflightContext const& ctx) +PermissionedDomainSet::preflight(PreflightContext const& ctx) { if (auto err = credentials::checkArray( ctx.tx.getFieldArray(sfAcceptedCredentials), diff --git a/src/xrpld/app/tx/detail/PermissionedDomainSet.h b/src/xrpld/app/tx/detail/PermissionedDomainSet.h index 94761bae26..01e4bb86d0 100644 --- a/src/xrpld/app/tx/detail/PermissionedDomainSet.h +++ b/src/xrpld/app/tx/detail/PermissionedDomainSet.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/SetAccount.cpp b/src/xrpld/app/tx/detail/SetAccount.cpp index 9d39670130..1641027a8e 100644 --- a/src/xrpld/app/tx/detail/SetAccount.cpp +++ b/src/xrpld/app/tx/detail/SetAccount.cpp @@ -64,7 +64,7 @@ SetAccount::getFlagsMask(PreflightContext const& ctx) } NotTEC -SetAccount::doPreflight(PreflightContext const& ctx) +SetAccount::preflight(PreflightContext const& ctx) { auto& tx = ctx.tx; auto& j = ctx.j; diff --git a/src/xrpld/app/tx/detail/SetAccount.h b/src/xrpld/app/tx/detail/SetAccount.h index a16d2e0cdf..bcc0a61b1b 100644 --- a/src/xrpld/app/tx/detail/SetAccount.h +++ b/src/xrpld/app/tx/detail/SetAccount.h @@ -42,7 +42,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER checkPermission(ReadView const& view, STTx const& tx); diff --git a/src/xrpld/app/tx/detail/SetOracle.cpp b/src/xrpld/app/tx/detail/SetOracle.cpp index 4f6b9b700e..411f4b6e49 100644 --- a/src/xrpld/app/tx/detail/SetOracle.cpp +++ b/src/xrpld/app/tx/detail/SetOracle.cpp @@ -43,7 +43,7 @@ SetOracle::isEnabled(PreflightContext const& ctx) } NotTEC -SetOracle::doPreflight(PreflightContext const& ctx) +SetOracle::preflight(PreflightContext const& ctx) { auto const& dataSeries = ctx.tx.getFieldArray(sfPriceDataSeries); if (dataSeries.empty()) diff --git a/src/xrpld/app/tx/detail/SetOracle.h b/src/xrpld/app/tx/detail/SetOracle.h index a35ef74c4c..2da10105b2 100644 --- a/src/xrpld/app/tx/detail/SetOracle.h +++ b/src/xrpld/app/tx/detail/SetOracle.h @@ -46,7 +46,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/SetRegularKey.cpp b/src/xrpld/app/tx/detail/SetRegularKey.cpp index f4d04ec1c1..4e063e7d1f 100644 --- a/src/xrpld/app/tx/detail/SetRegularKey.cpp +++ b/src/xrpld/app/tx/detail/SetRegularKey.cpp @@ -49,7 +49,7 @@ SetRegularKey::calculateBaseFee(ReadView const& view, STTx const& tx) } NotTEC -SetRegularKey::doPreflight(PreflightContext const& ctx) +SetRegularKey::preflight(PreflightContext const& ctx) { if (ctx.rules.enabled(fixMasterKeyAsRegularKey) && ctx.tx.isFieldPresent(sfRegularKey) && diff --git a/src/xrpld/app/tx/detail/SetRegularKey.h b/src/xrpld/app/tx/detail/SetRegularKey.h index 00b9e47169..055748aa25 100644 --- a/src/xrpld/app/tx/detail/SetRegularKey.h +++ b/src/xrpld/app/tx/detail/SetRegularKey.h @@ -34,7 +34,7 @@ public: } static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); diff --git a/src/xrpld/app/tx/detail/SetSignerList.cpp b/src/xrpld/app/tx/detail/SetSignerList.cpp index 191b947560..a2b310bbd6 100644 --- a/src/xrpld/app/tx/detail/SetSignerList.cpp +++ b/src/xrpld/app/tx/detail/SetSignerList.cpp @@ -85,7 +85,7 @@ SetSignerList::getFlagsMask(PreflightContext const& ctx) } NotTEC -SetSignerList::doPreflight(PreflightContext const& ctx) +SetSignerList::preflight(PreflightContext const& ctx) { auto const result = determineOperation(ctx.tx, ctx.flags, ctx.j); diff --git a/src/xrpld/app/tx/detail/SetSignerList.h b/src/xrpld/app/tx/detail/SetSignerList.h index 575db5a127..be2df8152e 100644 --- a/src/xrpld/app/tx/detail/SetSignerList.h +++ b/src/xrpld/app/tx/detail/SetSignerList.h @@ -55,7 +55,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); TER doApply() override; diff --git a/src/xrpld/app/tx/detail/SetTrust.cpp b/src/xrpld/app/tx/detail/SetTrust.cpp index f7ae39c1ae..55e10b2317 100644 --- a/src/xrpld/app/tx/detail/SetTrust.cpp +++ b/src/xrpld/app/tx/detail/SetTrust.cpp @@ -74,7 +74,7 @@ SetTrust::getFlagsMask(PreflightContext const& ctx) } NotTEC -SetTrust::doPreflight(PreflightContext const& ctx) +SetTrust::preflight(PreflightContext const& ctx) { auto& tx = ctx.tx; auto& j = ctx.j; diff --git a/src/xrpld/app/tx/detail/SetTrust.h b/src/xrpld/app/tx/detail/SetTrust.h index d7ca54d1dc..443080bf74 100644 --- a/src/xrpld/app/tx/detail/SetTrust.h +++ b/src/xrpld/app/tx/detail/SetTrust.h @@ -39,7 +39,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER checkPermission(ReadView const& view, STTx const& tx); diff --git a/src/xrpld/app/tx/detail/Transactor.h b/src/xrpld/app/tx/detail/Transactor.h index b2ed320c1b..6eda07a387 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -200,8 +200,8 @@ public: static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); - /* Do NOT define a preflight function in a derived class. - Instead, define + /* Do NOT define an invokePreflight function in a derived class. + Instead, define: // Optional if the transaction is gated on an amendment static bool @@ -211,12 +211,18 @@ public: static std::uint32_t getFlagsMask(PreflightContext const& ctx); + // Required, even if it just returns tesSUCCESS. static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); + + * Do not try to call preflight1 or preflight2 directly. + * Do not check whether relevant amendments are enabled in preflight. + Instead, define isEnabled. + * Do not check flags in preflight. Instead, define getFlagsMask. */ template static NotTEC - preflight(PreflightContext const& ctx); + invokePreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx) @@ -320,15 +326,21 @@ private: void trapTransaction(uint256) const; - // Helper functions for preflight checks. Do not use directly. /** Performs early sanity checks on the account and fee fields. (And passes flagMask to preflight0) + + Do not try to call preflight1 from preflight() in derived classes. See + the description of invokePreflight for details. */ static NotTEC preflight1(PreflightContext const& ctx, std::uint32_t flagMask); - /** Checks whether the signature appears valid */ + /** Checks whether the signature appears valid + + Do not try to call preflight2 from preflight() in derived classes. See + the description of invokePreflight for details. + */ static NotTEC preflight2(PreflightContext const& ctx); }; @@ -366,11 +378,11 @@ preflightCheckSimulateKeys( // Defined in Change.cpp template <> NotTEC -Transactor::preflight(PreflightContext const& ctx); +Transactor::invokePreflight(PreflightContext const& ctx); template NotTEC -Transactor::preflight(PreflightContext const& ctx) +Transactor::invokePreflight(PreflightContext const& ctx) { if (!T::isEnabled(ctx)) return temDISABLED; @@ -378,7 +390,7 @@ Transactor::preflight(PreflightContext const& ctx) if (auto const ret = preflight1(ctx, T::getFlagsMask(ctx))) return ret; - if (auto const ret = T::doPreflight(ctx)) + if (auto const ret = T::preflight(ctx)) return ret; return preflight2(ctx); diff --git a/src/xrpld/app/tx/detail/VaultClawback.cpp b/src/xrpld/app/tx/detail/VaultClawback.cpp index 39d068e58d..7b4d15c592 100644 --- a/src/xrpld/app/tx/detail/VaultClawback.cpp +++ b/src/xrpld/app/tx/detail/VaultClawback.cpp @@ -37,7 +37,7 @@ VaultClawback::isEnabled(PreflightContext const& ctx) } NotTEC -VaultClawback::doPreflight(PreflightContext const& ctx) +VaultClawback::preflight(PreflightContext const& ctx) { if (ctx.tx[sfVaultID] == beast::zero) { diff --git a/src/xrpld/app/tx/detail/VaultClawback.h b/src/xrpld/app/tx/detail/VaultClawback.h index 58ee97db46..58a7e738c3 100644 --- a/src/xrpld/app/tx/detail/VaultClawback.h +++ b/src/xrpld/app/tx/detail/VaultClawback.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/VaultCreate.cpp b/src/xrpld/app/tx/detail/VaultCreate.cpp index 835201cb5f..569c462e7a 100644 --- a/src/xrpld/app/tx/detail/VaultCreate.cpp +++ b/src/xrpld/app/tx/detail/VaultCreate.cpp @@ -51,7 +51,7 @@ VaultCreate::getFlagsMask(PreflightContext const& ctx) } NotTEC -VaultCreate::doPreflight(PreflightContext const& ctx) +VaultCreate::preflight(PreflightContext const& ctx) { if (!validDataLength(ctx.tx[~sfData], maxDataPayloadLength)) return temMALFORMED; diff --git a/src/xrpld/app/tx/detail/VaultCreate.h b/src/xrpld/app/tx/detail/VaultCreate.h index effb7cd475..54074c4bce 100644 --- a/src/xrpld/app/tx/detail/VaultCreate.h +++ b/src/xrpld/app/tx/detail/VaultCreate.h @@ -40,7 +40,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); diff --git a/src/xrpld/app/tx/detail/VaultDelete.cpp b/src/xrpld/app/tx/detail/VaultDelete.cpp index 71f1f86bae..02b63c145d 100644 --- a/src/xrpld/app/tx/detail/VaultDelete.cpp +++ b/src/xrpld/app/tx/detail/VaultDelete.cpp @@ -34,7 +34,7 @@ VaultDelete::isEnabled(PreflightContext const& ctx) } NotTEC -VaultDelete::doPreflight(PreflightContext const& ctx) +VaultDelete::preflight(PreflightContext const& ctx) { if (ctx.tx[sfVaultID] == beast::zero) { diff --git a/src/xrpld/app/tx/detail/VaultDelete.h b/src/xrpld/app/tx/detail/VaultDelete.h index aa32db48ab..f8b838e02e 100644 --- a/src/xrpld/app/tx/detail/VaultDelete.h +++ b/src/xrpld/app/tx/detail/VaultDelete.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/VaultDeposit.cpp b/src/xrpld/app/tx/detail/VaultDeposit.cpp index 847ba06954..162a314ff2 100644 --- a/src/xrpld/app/tx/detail/VaultDeposit.cpp +++ b/src/xrpld/app/tx/detail/VaultDeposit.cpp @@ -39,7 +39,7 @@ VaultDeposit::isEnabled(PreflightContext const& ctx) } NotTEC -VaultDeposit::doPreflight(PreflightContext const& ctx) +VaultDeposit::preflight(PreflightContext const& ctx) { if (ctx.tx[sfVaultID] == beast::zero) { diff --git a/src/xrpld/app/tx/detail/VaultDeposit.h b/src/xrpld/app/tx/detail/VaultDeposit.h index 79cd8f4a02..e804520595 100644 --- a/src/xrpld/app/tx/detail/VaultDeposit.h +++ b/src/xrpld/app/tx/detail/VaultDeposit.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/VaultSet.cpp b/src/xrpld/app/tx/detail/VaultSet.cpp index feec2eb160..30a99fe153 100644 --- a/src/xrpld/app/tx/detail/VaultSet.cpp +++ b/src/xrpld/app/tx/detail/VaultSet.cpp @@ -41,7 +41,7 @@ VaultSet::isEnabled(PreflightContext const& ctx) } NotTEC -VaultSet::doPreflight(PreflightContext const& ctx) +VaultSet::preflight(PreflightContext const& ctx) { if (ctx.tx[sfVaultID] == beast::zero) { diff --git a/src/xrpld/app/tx/detail/VaultSet.h b/src/xrpld/app/tx/detail/VaultSet.h index 2e8004bec1..bce0077802 100644 --- a/src/xrpld/app/tx/detail/VaultSet.h +++ b/src/xrpld/app/tx/detail/VaultSet.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.cpp b/src/xrpld/app/tx/detail/VaultWithdraw.cpp index 32ae9fce1c..f628c0aa32 100644 --- a/src/xrpld/app/tx/detail/VaultWithdraw.cpp +++ b/src/xrpld/app/tx/detail/VaultWithdraw.cpp @@ -37,7 +37,7 @@ VaultWithdraw::isEnabled(PreflightContext const& ctx) } NotTEC -VaultWithdraw::doPreflight(PreflightContext const& ctx) +VaultWithdraw::preflight(PreflightContext const& ctx) { if (ctx.tx[sfVaultID] == beast::zero) { diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.h b/src/xrpld/app/tx/detail/VaultWithdraw.h index 9ac9e420ea..f01a8e4c9d 100644 --- a/src/xrpld/app/tx/detail/VaultWithdraw.h +++ b/src/xrpld/app/tx/detail/VaultWithdraw.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/XChainBridge.cpp b/src/xrpld/app/tx/detail/XChainBridge.cpp index 93619e620c..fb35f9b2ad 100644 --- a/src/xrpld/app/tx/detail/XChainBridge.cpp +++ b/src/xrpld/app/tx/detail/XChainBridge.cpp @@ -1210,7 +1210,7 @@ toClaim(STTx const& tx) template NotTEC -attestationDoPreflight(PreflightContext const& ctx) +attestationpreflight(PreflightContext const& ctx) { if (!publicKeyType(ctx.tx[sfPublicKey])) return temMALFORMED; @@ -1374,7 +1374,7 @@ XChainCreateBridge::isEnabled(PreflightContext const& ctx) } NotTEC -XChainCreateBridge::doPreflight(PreflightContext const& ctx) +XChainCreateBridge::preflight(PreflightContext const& ctx) { auto const account = ctx.tx[sfAccount]; auto const reward = ctx.tx[sfSignatureReward]; @@ -1558,7 +1558,7 @@ BridgeModify::getFlagsMask(PreflightContext const& ctx) } NotTEC -BridgeModify::doPreflight(PreflightContext const& ctx) +BridgeModify::preflight(PreflightContext const& ctx) { auto const account = ctx.tx[sfAccount]; auto const reward = ctx.tx[~sfSignatureReward]; @@ -1665,7 +1665,7 @@ XChainClaim::isEnabled(PreflightContext const& ctx) } NotTEC -XChainClaim::doPreflight(PreflightContext const& ctx) +XChainClaim::preflight(PreflightContext const& ctx) { STXChainBridge const bridgeSpec = ctx.tx[sfXChainBridge]; auto const amount = ctx.tx[sfAmount]; @@ -1900,7 +1900,7 @@ XChainCommit::isEnabled(PreflightContext const& ctx) } NotTEC -XChainCommit::doPreflight(PreflightContext const& ctx) +XChainCommit::preflight(PreflightContext const& ctx) { auto const amount = ctx.tx[sfAmount]; auto const bridgeSpec = ctx.tx[sfXChainBridge]; @@ -2011,7 +2011,7 @@ XChainCreateClaimID::isEnabled(PreflightContext const& ctx) } NotTEC -XChainCreateClaimID::doPreflight(PreflightContext const& ctx) +XChainCreateClaimID::preflight(PreflightContext const& ctx) { auto const reward = ctx.tx[sfSignatureReward]; @@ -2123,9 +2123,9 @@ XChainAddClaimAttestation::isEnabled(PreflightContext const& ctx) } NotTEC -XChainAddClaimAttestation::doPreflight(PreflightContext const& ctx) +XChainAddClaimAttestation::preflight(PreflightContext const& ctx) { - return attestationDoPreflight(ctx); + return attestationpreflight(ctx); } TER @@ -2149,9 +2149,9 @@ XChainAddAccountCreateAttestation::isEnabled(PreflightContext const& ctx) } NotTEC -XChainAddAccountCreateAttestation::doPreflight(PreflightContext const& ctx) +XChainAddAccountCreateAttestation::preflight(PreflightContext const& ctx) { - return attestationDoPreflight(ctx); + return attestationpreflight(ctx); } TER @@ -2175,7 +2175,7 @@ XChainCreateAccountCommit::isEnabled(PreflightContext const& ctx) } NotTEC -XChainCreateAccountCommit::doPreflight(PreflightContext const& ctx) +XChainCreateAccountCommit::preflight(PreflightContext const& ctx) { auto const amount = ctx.tx[sfAmount]; diff --git a/src/xrpld/app/tx/detail/XChainBridge.h b/src/xrpld/app/tx/detail/XChainBridge.h index b116418023..e210169bcf 100644 --- a/src/xrpld/app/tx/detail/XChainBridge.h +++ b/src/xrpld/app/tx/detail/XChainBridge.h @@ -43,7 +43,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -68,7 +68,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -104,7 +104,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -134,7 +134,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -166,7 +166,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -198,7 +198,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -222,7 +222,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -269,7 +269,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index fbb117c372..c2e4e13f08 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -119,7 +119,7 @@ invoke_preflight(PreflightContext const& ctx) try { return with_txn_type(ctx.tx.getTxnType(), [&]() { - auto const tec = Transactor::preflight(ctx); + auto const tec = Transactor::invokePreflight(ctx); return std::make_pair( tec, isTesSuccess(tec) ? consequences_helper(ctx) From 06906a8bd3b3da7e9ff6a68628c4acf1eec939e2 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 14 Jul 2025 18:54:48 -0400 Subject: [PATCH 012/291] Refactor the bulk of testLifecycle into a function - Allows tweaking loan properties --- src/test/app/Loan_test.cpp | 2100 +++++++++++++++++++----------------- 1 file changed, 1086 insertions(+), 1014 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 86088f92db..8212a8f617 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -49,6 +49,14 @@ class Loan_test : public beast::unit_test::suite jtx::supported_amendments() | featureMPTokensV1 | featureSingleAssetVault | featureLendingProtocol}; + static constexpr auto const coverDepositParameter = 1000; + static constexpr auto const coverRateMinParameter = + percentageToTenthBips(10); + static constexpr auto const maxCoveredLoanValue = 1000 * 100 / 10; + static constexpr auto const vaultDeposit = 50'000; + static constexpr auto const debtMaximumParameter = 25'000; + std::string const iouCurrency{"IOU"}; + void testDisabled() { @@ -128,16 +136,19 @@ class Loan_test : public beast::unit_test::suite public: jtx::Env const& env; BrokerInfo const& broker; + Number const& loanAmount; jtx::Account const& pseudoAccount; Keylet const& keylet; VerifyLoanStatus( jtx::Env const& env_, BrokerInfo const& broker_, + Number const& loanAmount_, jtx::Account const& pseudo_, Keylet const& keylet_) : env(env_) , broker(broker_) + , loanAmount(loanAmount_) , pseudoAccount(pseudo_) , keylet(keylet_) { @@ -252,7 +263,7 @@ class Loan_test : public beast::unit_test::suite loan->at(sfPrincipalOutstanding) == principalOutstanding); env.test.BEAST_EXPECT( loan->at(sfPrincipalRequested) == - broker.asset(1000).value()); + broker.asset(loanAmount).value()); env.test.BEAST_EXPECT(loan->at(sfFlags) == flags); auto const interestRate = TenthBips32{loan->at(sfInterestRate)}; @@ -317,8 +328,11 @@ class Loan_test : public beast::unit_test::suite void lifecycle( + std::string const& caseLabel, char const* label, jtx::Env& env, + Number const& loanAmount, + int interestExponent, jtx::Account const& lender, jtx::Account const& borrower, jtx::Account const& evan, @@ -349,23 +363,16 @@ class Loan_test : public beast::unit_test::suite }(); VerifyLoanStatus const verifyLoanStatus( - env, broker, pseudoAcct, keylet); + env, broker, loanAmount, pseudoAcct, keylet); // No loans yet verifyLoanStatus.checkBroker( - 0, broker.asset(1000).value(), 0, TenthBips32{0}, 1, 0, 0); + 0, broker.asset(loanAmount).value(), 0, TenthBips32{0}, 1, 0, 0); if (!BEAST_EXPECT(loanSequence != 0)) return; - { - auto const& asset = broker.asset.raw(); - testcase << "Lifecycle: " - << (asset.native() ? "XRP " - : asset.holds() ? "IOU " - : asset.holds() ? "MPT " - : "Unknown ") - << label; - } + + testcase << caseLabel << " " << label; using namespace jtx; using namespace loan; @@ -374,18 +381,41 @@ class Loan_test : public beast::unit_test::suite auto const borrowerOwnerCount = env.ownerCount(borrower); auto const loanSetFee = fee(env.current()->fees().base * 2); - Number const principalRequest = broker.asset(1000).value(); + Number const principalRequest = broker.asset(loanAmount).value(); auto const startDate = env.now() + 3600s; auto const originationFee = broker.asset(1).value(); auto const serviceFee = broker.asset(2).value(); auto const lateFee = broker.asset(3).value(); auto const closeFee = broker.asset(4).value(); - auto const overFee = percentageToTenthBips(5) / 10; - auto const interest = percentageToTenthBips(12); + + auto applyExponent = [interestExponent, + this](TenthBips32 value) mutable { + BEAST_EXPECT(value > TenthBips32(0)); + while (interestExponent > 0) + { + auto const oldValue = value; + value *= 10; + --interestExponent; + BEAST_EXPECT(value / 10 == oldValue); + } + while (interestExponent < 0) + { + auto const oldValue = value; + value /= 10; + ++interestExponent; + BEAST_EXPECT(value * 10 == oldValue); + } + return value; + }; + + auto const overFee = applyExponent(percentageToTenthBips(5) / 10); + auto const interest = applyExponent(percentageToTenthBips(12)); // 2.4% - auto const lateInterest = percentageToTenthBips(24) / 10; - auto const closeInterest = percentageToTenthBips(36) / 10; - auto const overpaymentInterest = percentageToTenthBips(48) / 10; + auto const lateInterest = applyExponent(percentageToTenthBips(24) / 10); + auto const closeInterest = + applyExponent(percentageToTenthBips(36) / 10); + auto const overpaymentInterest = + applyExponent(percentageToTenthBips(48) / 10); auto const total = 12; auto const interval = 600; auto const grace = 60; @@ -564,6 +594,1033 @@ class Loan_test : public beast::unit_test::suite } } + template + void + testCaseWrapper( + jtx::Env& env, + jtx::MPTTester& mptt, + std::array const& assets, + BrokerInfo const& broker, + Number const& loanAmount, + int interestExponent) + { + using namespace jtx; + + auto const& asset = broker.asset.raw(); + auto const caseLabel = [&]() { + std::stringstream ss; + ss << "Lifecycle: " << loanAmount << " " + << (asset.native() ? "XRP" + : asset.holds() ? "IOU" + : asset.holds() ? "MPT" + : "Unknown") + << " Scale interest to: " << interestExponent << " "; + return ss.str(); + }(); + testcase << caseLabel; + + using namespace loan; + using namespace std::chrono_literals; + using d = NetClock::duration; + using tp = NetClock::time_point; + + Account const issuer{"issuer"}; + // For simplicity, lender will be the sole actor for the vault & + // brokers. + Account const lender{"lender"}; + // Borrower only wants to borrow + Account const borrower{"borrower"}; + // Evan will attempt to be naughty + Account const evan{"evan"}; + // Do not fund alice + Account const alice{"alice"}; + + Number const principalRequest = broker.asset(loanAmount).value(); + Number const maxCoveredLoanRequest = + broker.asset(maxCoveredLoanValue).value(); + Number const totalVaultRequest = broker.asset(vaultDeposit).value(); + Number const debtMaximumRequest = + broker.asset(debtMaximumParameter).value(); + + auto const startDate = env.now() + 3600s; + auto const loanSetFee = fee(env.current()->fees().base * 2); + + auto const pseudoAcct = [&]() { + auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); + if (!BEAST_EXPECT(brokerSle)) + return lender; + auto const brokerPseudo = brokerSle->at(sfAccount); + return Account("Broker pseudo-account", brokerPseudo); + }(); + + auto badKeylet = keylet::vault(lender.id(), env.seq(lender)); + // Try some failure cases + // flags are checked first + env(set(evan, + broker.brokerID, + principalRequest, + startDate, + tfLoanSetMask), + sig(sfCounterpartySignature, lender), + loanSetFee, + ter(temINVALID_FLAG)); + + // field length validation + // sfData: good length, bad account + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, borrower), + data(std::string(maxDataPayloadLength, 'X')), + loanSetFee, + ter(tefBAD_AUTH)); + // sfData: too long + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, lender), + data(std::string(maxDataPayloadLength + 1, 'Y')), + loanSetFee, + ter(temINVALID)); + + // field range validation + // sfOverpaymentFee: good value, bad account + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, borrower), + overpaymentFee(maxOverpaymentFee), + loanSetFee, + ter(tefBAD_AUTH)); + // sfOverpaymentFee: too big + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, lender), + overpaymentFee(maxOverpaymentFee + 1), + loanSetFee, + ter(temINVALID)); + + // sfLateInterestRate: good value, bad account + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, borrower), + lateInterestRate(maxLateInterestRate), + loanSetFee, + ter(tefBAD_AUTH)); + // sfLateInterestRate: too big + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, lender), + lateInterestRate(maxLateInterestRate + 1), + loanSetFee, + ter(temINVALID)); + + // sfCloseInterestRate: good value, bad account + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, borrower), + closeInterestRate(maxCloseInterestRate), + loanSetFee, + ter(tefBAD_AUTH)); + // sfCloseInterestRate: too big + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, lender), + closeInterestRate(maxCloseInterestRate + 1), + loanSetFee, + ter(temINVALID)); + + // sfOverpaymentInterestRate: good value, bad account + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, borrower), + overpaymentInterestRate(maxOverpaymentInterestRate), + loanSetFee, + ter(tefBAD_AUTH)); + // sfOverpaymentInterestRate: too big + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, lender), + overpaymentInterestRate(maxOverpaymentInterestRate + 1), + loanSetFee, + ter(temINVALID)); + + // sfPaymentTotal: good value, bad account + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, borrower), + paymentTotal(LoanSet::minPaymentTotal), + loanSetFee, + ter(tefBAD_AUTH)); + // sfPaymentTotal: too small (there is no max) + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, lender), + paymentTotal(LoanSet::minPaymentTotal - 1), + loanSetFee, + ter(temINVALID)); + + // sfPaymentInterval: good value, bad account + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, borrower), + paymentInterval(LoanSet::minPaymentInterval), + loanSetFee, + ter(tefBAD_AUTH)); + // sfPaymentInterval: too small (there is no max) + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, lender), + paymentInterval(LoanSet::minPaymentInterval - 1), + loanSetFee, + ter(temINVALID)); + + // sfGracePeriod: good value, bad account + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, borrower), + paymentInterval(LoanSet::minPaymentInterval * 2), + gracePeriod(LoanSet::minPaymentInterval * 2), + loanSetFee, + ter(tefBAD_AUTH)); + // sfGracePeriod: larger than paymentInterval + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, lender), + paymentInterval(LoanSet::minPaymentInterval * 2), + gracePeriod(LoanSet::minPaymentInterval * 3), + loanSetFee, + ter(temINVALID)); + + // insufficient fee - single sign + env(set(borrower, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, lender), + ter(telINSUF_FEE_P)); + // insufficient fee - multisign + env(set(borrower, broker.brokerID, principalRequest, startDate), + counterparty(lender), + msig(evan, lender), + msig(sfCounterpartySignature, evan, borrower), + fee(env.current()->fees().base * 5 - 1), + ter(telINSUF_FEE_P)); + // multisign sufficient fee, but no signers set up + env(set(borrower, broker.brokerID, principalRequest, startDate), + counterparty(lender), + msig(evan, lender), + msig(sfCounterpartySignature, evan, borrower), + fee(env.current()->fees().base * 5), + ter(tefNOT_MULTI_SIGNING)); + // not the broker owner, no counterparty, not signed by broker + // owner + env(set(borrower, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, evan), + loanSetFee, + ter(tefBAD_AUTH)); + // bad start date - in the past + env(set(evan, + broker.brokerID, + principalRequest, + env.closed()->info().closeTime - 1s), + sig(sfCounterpartySignature, lender), + loanSetFee, + ter(tecEXPIRED)); + // not the broker owner, counterparty is borrower + env(set(evan, broker.brokerID, principalRequest, startDate), + counterparty(borrower), + sig(sfCounterpartySignature, borrower), + loanSetFee, + ter(tecNO_PERMISSION)); + // can not lend money to yourself + env(set(lender, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, lender), + loanSetFee, + ter(tecNO_PERMISSION)); + // not a LoanBroker object, no counterparty + env(set(lender, badKeylet.key, principalRequest, startDate), + sig(sfCounterpartySignature, evan), + loanSetFee, + ter(temBAD_SIGNER)); + // not a LoanBroker object, counterparty is valid + env(set(lender, badKeylet.key, principalRequest, startDate), + counterparty(borrower), + sig(sfCounterpartySignature, borrower), + loanSetFee, + ter(tecNO_ENTRY)); + // borrower doesn't exist + env(set(lender, broker.brokerID, principalRequest, startDate), + counterparty(alice), + sig(sfCounterpartySignature, alice), + loanSetFee, + ter(terNO_ACCOUNT)); + + // Request more funds than the vault has available + env(set(evan, broker.brokerID, totalVaultRequest + 1, startDate), + sig(sfCounterpartySignature, lender), + loanSetFee, + ter(tecINSUFFICIENT_FUNDS)); + + // Request more funds than the broker's first-loss capital can + // cover. + env(set(evan, broker.brokerID, maxCoveredLoanRequest + 1, startDate), + sig(sfCounterpartySignature, lender), + loanSetFee, + ter(tecINSUFFICIENT_FUNDS)); + + // Frozen trust line / locked MPT issuance + // XRP can not be frozen, but run through the loop anyway to test + // the tecLIMIT_EXCEEDED case + { + auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); + if (!BEAST_EXPECT(brokerSle)) + return; + + auto const [freeze, deepfreeze, unfreeze, expectedResult] = + [&]() -> std::tuple< + std::function, + std::function, + std::function, + TER> { + // Freeze / lock the asset + std::function empty; + if (broker.asset.raw().native()) + { + // XRP can't be frozen + return std::make_tuple(empty, empty, empty, tesSUCCESS); + } + else if (broker.asset.raw().holds()) + { + auto freeze = [&](Account const& holder) { + env(trust(issuer, holder[iouCurrency](0), tfSetFreeze)); + }; + auto deepfreeze = [&](Account const& holder) { + env(trust( + issuer, + holder[iouCurrency](0), + tfSetFreeze | tfSetDeepFreeze)); + }; + auto unfreeze = [&](Account const& holder) { + env(trust( + issuer, + holder[iouCurrency](0), + tfClearFreeze | tfClearDeepFreeze)); + }; + return std::make_tuple( + freeze, deepfreeze, unfreeze, tecFROZEN); + } + else + { + auto freeze = [&](Account const& holder) { + mptt.set( + {.account = issuer, + .holder = holder, + .flags = tfMPTLock}); + }; + auto unfreeze = [&](Account const& holder) { + mptt.set( + {.account = issuer, + .holder = holder, + .flags = tfMPTUnlock}); + }; + return std::make_tuple(freeze, empty, unfreeze, tecLOCKED); + } + }(); + + // Try freezing both the lender and the pseudo-account + for (auto const& account : {lender, pseudoAcct}) + { + if (freeze) + { + // Freeze the account + freeze(account); + + // Try to create a loan with a frozen line + env(set(evan, + broker.brokerID, + debtMaximumRequest, + startDate), + sig(sfCounterpartySignature, lender), + loanSetFee, + ter(expectedResult)); + + // Unfreeze the account + BEAST_EXPECT(unfreeze); + unfreeze(account); + } + + // Ensure the line is unfrozen with a request that is fine + // except too it requests more principal than the broker can + // carry + env(set(evan, + broker.brokerID, + debtMaximumRequest + 1, + startDate), + sig(sfCounterpartySignature, lender), + loanSetFee, + ter(tecLIMIT_EXCEEDED)); + } + + // Deep freeze the borrower, which prevents them from receiving + // funds + if (deepfreeze) + { + // Make sure evan has a trust line that so the issuer can + // freeze it. (Don't need to do this for the borrower, + // because LoanDraw will create a line to the borrower + // automatically.) + env(trust(evan, issuer[iouCurrency](100'000))); + + // Freeze evan + deepfreeze(evan); + + // Try to create a loan with a deep frozen line + env(set(evan, broker.brokerID, debtMaximumRequest, startDate), + sig(sfCounterpartySignature, lender), + loanSetFee, + ter(expectedResult)); + + // Unfreeze evan + BEAST_EXPECT(unfreeze); + unfreeze(evan); + + // Ensure the line is unfrozen with a request that is fine + // except too it requests more principal than the broker can + // carry + env(set(evan, + broker.brokerID, + debtMaximumRequest + 1, + startDate), + sig(sfCounterpartySignature, lender), + loanSetFee, + ter(tecLIMIT_EXCEEDED)); + } + } + + // Finally! Create a loan + std::string testData; + + auto currentState = [&](Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus) { + // Lookup the current loan state + if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan)) + { + LoanState state{ + .previousPaymentDate = loan->at(sfPreviousPaymentDate), + .startDate = tp{d{loan->at(sfStartDate)}}, + .nextPaymentDate = loan->at(sfNextPaymentDueDate), + .paymentRemaining = loan->at(sfPaymentRemaining), + .assetsAvailable = loan->at(sfAssetsAvailable), + .principalRequested = loan->at(sfPrincipalRequested), + .principalOutstanding = loan->at(sfPrincipalOutstanding), + .flags = loan->at(sfFlags), + .paymentInterval = loan->at(sfPaymentInterval), + }; + BEAST_EXPECT(state.previousPaymentDate == 0); + BEAST_EXPECT( + tp{d{state.nextPaymentDate}} == state.startDate + 600s); + BEAST_EXPECT(state.paymentRemaining == 12); + BEAST_EXPECT( + state.assetsAvailable == broker.asset(999).value()); + BEAST_EXPECT( + state.principalOutstanding == broker.asset(1000).value()); + BEAST_EXPECT( + state.principalOutstanding == state.principalRequested); + BEAST_EXPECT(state.paymentInterval == 600); + + verifyLoanStatus(state); + + return state; + } + + return LoanState{ + .previousPaymentDate = 0, + .startDate = tp{d{0}}, + .nextPaymentDate = 0, + .paymentRemaining = 0, + .assetsAvailable = 0, + .principalRequested = 0, + .principalOutstanding = 0, + .flags = 0, + .paymentInterval = 0, + }; + }; + + auto defaultBeforeStartDate = [&](std::uint32_t baseFlag, + bool impair = true) { + return [&, impair, baseFlag]( + Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus) { + // toEndOfLife + // + // Default the loan + + // Initialize values with the current state + auto state = currentState(loanKeylet, verifyLoanStatus); + BEAST_EXPECT(state.flags == baseFlag); + + if (impair) + { + // Impair the loan + env(manage(lender, loanKeylet.key, tfLoanImpair)); + + state.flags |= tfLoanImpair; + state.nextPaymentDate = + env.now().time_since_epoch().count(); + verifyLoanStatus(state); + + // Once the loan is impaired, it can't be impaired again + env(manage(lender, loanKeylet.key, tfLoanImpair), + ter(tecNO_PERMISSION)); + } + + auto const nextDueDate = tp{d{state.nextPaymentDate}}; + + // Can't default the loan yet. The grace period hasn't + // expired + env(manage(lender, loanKeylet.key, tfLoanDefault), + ter(tecTOO_SOON)); + + // Let some time pass so that the loan can be + // defaulted + env.close(nextDueDate + 60s); + + if (impair) + { + // Impaired loans can't be drawn against + env(draw(borrower, loanKeylet.key, broker.asset(100)), + ter(tecNO_PERMISSION)); + } + + // Default the loan + env(manage(lender, loanKeylet.key, tfLoanDefault)); + + state.flags |= tfLoanDefault; + state.paymentRemaining = 0; + state.assetsAvailable = 0; + state.principalOutstanding = 0; + verifyLoanStatus(state); + + // Defaulted loans can't be drawn against, either + env(draw(borrower, loanKeylet.key, broker.asset(100)), + ter(tecNO_PERMISSION)); + + // Once a loan is defaulted, it can't be managed + env(manage(lender, loanKeylet.key, tfLoanUnimpair), + ter(tecNO_PERMISSION)); + env(manage(lender, loanKeylet.key, tfLoanImpair), + ter(tecNO_PERMISSION)); + }; + }; + + auto immediatePayoff = [&](std::uint32_t baseFlag) { + return [&, baseFlag]( + Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus) { + // toEndOfLife + // + auto state = currentState(loanKeylet, verifyLoanStatus); + BEAST_EXPECT(state.flags == baseFlag); + auto const borrowerStartingBalance = + env.balance(borrower, broker.asset); + + // Try to make a payment before the loan starts + env(pay(borrower, loanKeylet.key, broker.asset(500)), + ter(tecTOO_SOON)); + + // Advance to the start date of the loan + env.close(state.startDate + 5s); + + verifyLoanStatus(state); + + // Need to account for fees if the loan is in XRP + PrettyAmount adjustment = broker.asset(0); + if (broker.asset.raw().native()) + { + adjustment = 2 * env.current()->fees().base; + } + + // Draw the entire available balance + // Need to create the STAmount directly to avoid + // PrettyAsset scaling. + STAmount const drawAmount{broker.asset, state.assetsAvailable}; + env(draw(borrower, loanKeylet.key, drawAmount)); + env.close(state.startDate + 20s); + auto const loanAge = (env.now() - state.startDate).count(); + BEAST_EXPECT(loanAge == 30); + + state.assetsAvailable -= drawAmount; + verifyLoanStatus(state); + BEAST_EXPECT( + env.balance(borrower, broker.asset) == + borrowerStartingBalance + drawAmount - adjustment); + + // Send some bogus pay transactions + env(pay(borrower, + keylet::loan(uint256(0)).key, + broker.asset(10)), + ter(temINVALID)); + env(pay(borrower, loanKeylet.key, broker.asset(-100)), + ter(temBAD_AMOUNT)); + env(pay(borrower, broker.brokerID, broker.asset(100)), + ter(tecNO_ENTRY)); + env(pay(evan, loanKeylet.key, broker.asset(500)), + ter(tecNO_PERMISSION)); + + { + auto const otherAsset = + broker.asset.raw() == assets[0].raw() ? assets[1] + : assets[0]; + env(pay(borrower, loanKeylet.key, otherAsset(100)), + ter(tecWRONG_ASSET)); + } + + // Amount doesn't cover a single payment + env(pay(borrower, loanKeylet.key, STAmount{broker.asset, 1}), + ter(tecINSUFFICIENT_PAYMENT)); + + // Get the balance after these failed transactions take + // fees + auto const borrowerBalanceBeforePayment = + env.balance(borrower, broker.asset); + + // Full payoff amount will consist of + // 1. principal outstanding (1000) + // 2. accrued interest (at 12%) + // 3. prepayment penalty (closeInterest at 3.6%) + // 4. close payment fee (4) + // Calculate these values without the helper functions + // to verify they're working correctly The numbers in + // the below BEAST_EXPECTs may not hold across assets. + Number const interval = state.paymentInterval; + auto const periodicRate = + interval * Number(12, -2) / (365 * 24 * 60 * 60); + BEAST_EXPECT( + periodicRate == + Number(2283105022831050, -21, Number::unchecked{})); + STAmount const accruedInterest{ + broker.asset, + state.principalOutstanding * periodicRate * loanAge / + interval}; + BEAST_EXPECT( + accruedInterest == + broker.asset(Number(1141552511415525, -19))); + STAmount const prepaymentPenalty{ + broker.asset, state.principalOutstanding * Number(36, -3)}; + BEAST_EXPECT(prepaymentPenalty == broker.asset(36)); + STAmount const closePaymentFee = broker.asset(4); + auto const payoffAmount = + STAmount{broker.asset, state.principalOutstanding} + + accruedInterest + prepaymentPenalty + closePaymentFee; + BEAST_EXPECT( + payoffAmount == + broker.asset(Number(1040000114155251, -12))); + BEAST_EXPECT(payoffAmount > drawAmount); + // Try to pay a little extra to show that it's _not_ + // taken + auto const transactionAmount = payoffAmount + broker.asset(10); + BEAST_EXPECT( + transactionAmount == + broker.asset(Number(1050000114155251, -12))); + env(pay(borrower, loanKeylet.key, transactionAmount)); + + env.close(); + + // Need to account for fees if the loan is in XRP + adjustment = broker.asset(0); + if (broker.asset.raw().native()) + { + adjustment = env.current()->fees().base; + } + + state.paymentRemaining = 0; + state.principalOutstanding = 0; + verifyLoanStatus(state); + + BEAST_EXPECT( + env.balance(borrower, broker.asset) == + borrowerBalanceBeforePayment - payoffAmount - adjustment); + + // Can't impair or default a paid off loan + env(manage(lender, loanKeylet.key, tfLoanImpair), + ter(tecNO_PERMISSION)); + env(manage(lender, loanKeylet.key, tfLoanDefault), + ter(tecNO_PERMISSION)); + }; + }; + + // There are a lot of fields that can be set on a loan, but most of + // them only affect the "math" when a payment is made. The only one + // that really affects behavior is the `tfLoanOverpayment` flag. + lifecycle( + caseLabel, + "Loan overpayment allowed - Impair and Default before start " + "date", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + defaultBeforeStartDate(lsfLoanOverpayment)); + + lifecycle( + caseLabel, + "Loan overpayment prohibited - Impair and Default before start " + "date", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + 0, + defaultBeforeStartDate(0)); + + lifecycle( + caseLabel, + "Loan overpayment allowed - Default without Impair before " + "start " + "date", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + defaultBeforeStartDate(lsfLoanOverpayment, false)); + + lifecycle( + caseLabel, + "Loan overpayment prohibited - Default without Impair before " + "start " + "date", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + 0, + defaultBeforeStartDate(0, false)); + + lifecycle( + caseLabel, + "Loan overpayment allowed - Draw then default", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + [&](Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus) { + // toEndOfLife + // + // Initialize values with the current state + auto state = currentState(loanKeylet, verifyLoanStatus); + BEAST_EXPECT(state.flags == lsfLoanOverpayment); + auto const borrowerStartingBalance = + env.balance(borrower, broker.asset); + + // Draw the balance + env(draw( + borrower, + keylet::loan(uint256(0)).key, + broker.asset(10)), + ter(temINVALID)); + env(draw(borrower, loanKeylet.key, broker.asset(-100)), + ter(temBAD_AMOUNT)); + env(draw(borrower, broker.brokerID, broker.asset(100)), + ter(tecNO_ENTRY)); + env(draw(evan, loanKeylet.key, broker.asset(500)), + ter(tecNO_PERMISSION)); + env(draw(borrower, loanKeylet.key, broker.asset(500)), + ter(tecTOO_SOON)); + + // Advance to the start date of the loan + env.close(state.startDate + 5s); + env(draw(borrower, loanKeylet.key, broker.asset(10000)), + ter(tecINSUFFICIENT_FUNDS)); + { + auto const otherAsset = + broker.asset.raw() == assets[0].raw() ? assets[1] + : assets[0]; + env(draw(borrower, loanKeylet.key, otherAsset(100)), + ter(tecWRONG_ASSET)); + } + + verifyLoanStatus(state); + + // Need to account for fees if the loan is in XRP + PrettyAmount adjustment = broker.asset(0); + if (broker.asset.raw().native()) + { + adjustment = 5 * env.current()->fees().base; + } + + // Draw about half the balance + auto const drawAmount = broker.asset(500); + env(draw(borrower, loanKeylet.key, drawAmount)); + + state.assetsAvailable -= drawAmount.number(); + verifyLoanStatus(state); + BEAST_EXPECT( + env.balance(borrower, broker.asset) == + borrowerStartingBalance + drawAmount - adjustment); + + // move past the due date + grace period (60s) + env.close(tp{d{state.nextPaymentDate}} + 60s + 20s); + // Try to draw + env(draw(borrower, loanKeylet.key, broker.asset(100)), + ter(tecNO_PERMISSION)); + + // default the loan + env(manage(lender, loanKeylet.key, tfLoanDefault)); + state.paymentRemaining = 0; + state.assetsAvailable = 0; + state.principalOutstanding = 0; + state.flags |= tfLoanDefault; + + verifyLoanStatus(state); + + // Same error, different check + env(draw(borrower, loanKeylet.key, broker.asset(100)), + ter(tecNO_PERMISSION)); + + // Can't make a payment on it either + env(pay(borrower, loanKeylet.key, broker.asset(300)), + ter(tecKILLED)); + + // Default + }); + + lifecycle( + caseLabel, + "Loan overpayment prohibited - Pay off immediately", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + 0, + immediatePayoff(0)); + + lifecycle( + caseLabel, + "Loan overpayment allowed - Pay off immediately", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + immediatePayoff(lsfLoanOverpayment)); + + lifecycle( + caseLabel, + "Loan overpayment prohibited - Make payments", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + 0, + [&](Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus) { + // toEndOfLife + // + // Draw and make multiple payments + auto state = currentState(loanKeylet, verifyLoanStatus); + BEAST_EXPECT(state.flags == 0); + // Advance to the start date of the loan + env.close(state.startDate + 5s); + + verifyLoanStatus(state); + + auto const borrowerStartingBalance = + env.balance(borrower, broker.asset); + + // Need to account for fees if the loan is in XRP + PrettyAmount adjustment = broker.asset(0); + if (broker.asset.raw().native()) + { + adjustment = env.current()->fees().base; + } + + // Draw the entire available balance + // Need to create the STAmount directly to avoid + // PrettyAsset scaling. + STAmount const drawAmount{broker.asset, state.assetsAvailable}; + env(draw(borrower, loanKeylet.key, drawAmount)); + env.close(state.startDate + 20s); + auto const loanAge = (env.now() - state.startDate).count(); + BEAST_EXPECT(loanAge == 30); + + state.assetsAvailable -= drawAmount; + verifyLoanStatus(state); + BEAST_EXPECT( + env.balance(borrower, broker.asset) == + borrowerStartingBalance + drawAmount - adjustment); + + // Periodic payment amount will consist of + // 1. principal outstanding (1000) + // 2. interest interest rate (at 12%) + // 3. payment interval (600s) + // 4. loan service fee (2) + // Calculate these values without the helper functions + // to verify they're working correctly The numbers in + // the below BEAST_EXPECTs may not hold across assets. + Number const interval = state.paymentInterval; + auto const periodicRate = + interval * Number(12, -2) / (365 * 24 * 60 * 60); + BEAST_EXPECT( + periodicRate == + Number(2283105022831050, -21, Number::unchecked{})); + + while (state.paymentRemaining > 0) + { + testcase << "Payments remaining: " + << state.paymentRemaining; + + STAmount const principalRequestedAmount{ + broker.asset, state.principalRequested}; + // Compute the payment based on the number of payments + // remaining + auto const rateFactor = + power(1 + periodicRate, state.paymentRemaining); + Number const rawPeriodicPayment = + state.principalOutstanding * periodicRate * rateFactor / + (rateFactor - 1); + STAmount const periodicPayment = roundToReference( + STAmount{broker.asset, rawPeriodicPayment}, + principalRequestedAmount); + // Only check the first payment since the rounding may + // drift as payments are made + BEAST_EXPECT( + state.paymentRemaining < 12 || + STAmount(broker.asset, rawPeriodicPayment) == + broker.asset(Number(8333457001162141, -14))); + // Include the service fee + STAmount const totalDue = roundToReference( + periodicPayment + broker.asset(2), + principalRequestedAmount); + // Only check the first payment since the rounding may + // drift as payments are made + BEAST_EXPECT( + state.paymentRemaining < 12 || + totalDue == + roundToReference( + broker.asset(Number(8533457001162141, -14)), + principalRequestedAmount)); + + // Try to pay a little extra to show that it's _not_ + // taken + STAmount const transactionAmount = + STAmount{broker.asset, totalDue} + broker.asset(10); + // Only check the first payment since the rounding may + // drift as payments are made + BEAST_EXPECT( + state.paymentRemaining < 12 || + transactionAmount == + roundToReference( + broker.asset(Number(9533457001162141, -14)), + principalRequestedAmount)); + + auto const totalDueAmount = + STAmount{broker.asset, totalDue}; + + // Compute the expected principal amount + Number const rawInterest = state.paymentRemaining == 1 + ? rawPeriodicPayment - state.principalOutstanding + : state.principalOutstanding * periodicRate; + STAmount const interest = roundToReference( + STAmount{broker.asset, rawInterest}, + principalRequestedAmount); + BEAST_EXPECT( + state.paymentRemaining < 12 || + roundToReference( + STAmount{broker.asset, rawInterest}, + principalRequestedAmount) == + roundToReference( + broker.asset(Number(2283105022831050, -18)), + principalRequestedAmount)); + BEAST_EXPECT(interest >= Number(0)); + + auto const rawPrincipal = rawPeriodicPayment - rawInterest; + BEAST_EXPECT( + state.paymentRemaining < 12 || + roundToReference( + STAmount{broker.asset, rawPrincipal}, + principalRequestedAmount) == + roundToReference( + broker.asset(Number(8333228690659858, -14)), + principalRequestedAmount)); + BEAST_EXPECT( + state.paymentRemaining > 1 || + rawPrincipal == state.principalOutstanding); + auto const principal = roundToReference( + STAmount{broker.asset, periodicPayment - interest}, + principalRequestedAmount); + BEAST_EXPECT( + principal > Number(0) && + principal <= state.principalOutstanding); + BEAST_EXPECT( + state.paymentRemaining > 1 || + principal == state.principalOutstanding); + BEAST_EXPECT( + rawPrincipal + rawInterest == rawPeriodicPayment); + BEAST_EXPECT(principal + interest == periodicPayment); + + auto const borrowerBalanceBeforePayment = + env.balance(borrower, broker.asset); + + // Make the payment + env(pay(borrower, loanKeylet.key, transactionAmount)); + + env.close(); + + // Need to account for fees if the loan is in XRP + adjustment = broker.asset(0); + if (broker.asset.raw().native()) + { + adjustment = env.current()->fees().base; + } + + // Check the result + auto const borrowerBalance = + env.balance(borrower, broker.asset); + auto const expectedBalance = borrowerBalanceBeforePayment - + totalDueAmount - adjustment; + BEAST_EXPECT( + borrowerBalance == expectedBalance || + (!broker.asset.raw().native() && + broker.asset.raw().holds() && + ((borrowerBalance - expectedBalance) / + expectedBalance < + Number(1, -4)))); + + --state.paymentRemaining; + state.previousPaymentDate = state.nextPaymentDate; + state.nextPaymentDate += state.paymentInterval; + state.principalOutstanding -= principal; + + verifyLoanStatus(state); + } + + // Loan is paid off + BEAST_EXPECT(state.paymentRemaining == 0); + BEAST_EXPECT(state.principalOutstanding == 0); + + // Can't impair or default a paid off loan + env(manage(lender, loanKeylet.key, tfLoanImpair), + ter(tecNO_PERMISSION)); + env(manage(lender, loanKeylet.key, tfLoanDefault), + ter(tecNO_PERMISSION)); + }); + } + void testLifecycle() { @@ -593,7 +1650,6 @@ class Loan_test : public beast::unit_test::suite // Create assets PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; - std::string const iouCurrency{"IOU"}; PrettyAsset const iouAsset = issuer[iouCurrency]; env(trust(lender, iouAsset(1'000'000))); env(trust(borrower, iouAsset(1'000'000))); @@ -619,12 +1675,6 @@ class Loan_test : public beast::unit_test::suite std::array const assets{xrpAsset, mptAsset, iouAsset}; - auto const coverDepositParameter = 1000; - auto const coverRateMinParameter = percentageToTenthBips(10); - auto const maxCoveredLoanValue = 1000 * 100 / 10; - auto const vaultDeposit = 50'000; - auto const debtMaximumParameter = 25'000; - // Create vaults and loan brokers std::vector brokers; for (auto const& asset : assets) @@ -671,1000 +1721,22 @@ class Loan_test : public beast::unit_test::suite // Create and update Loans for (auto const& broker : brokers) { - auto const& asset = broker.asset.raw(); - testcase << "Lifecycle: " - << (asset.native() ? "XRP " - : asset.holds() ? "IOU " - : asset.holds() ? "MPT " - : "Unknown "); - - using namespace loan; - using namespace std::chrono_literals; - using d = NetClock::duration; - using tp = NetClock::time_point; - - Number const principalRequest = broker.asset(1000).value(); - Number const maxCoveredLoanRequest = - broker.asset(maxCoveredLoanValue).value(); - Number const totalVaultRequest = broker.asset(vaultDeposit).value(); - Number const debtMaximumRequest = - broker.asset(debtMaximumParameter).value(); - - auto const startDate = env.now() + 3600s; - auto const loanSetFee = fee(env.current()->fees().base * 2); - - auto const pseudoAcct = [&]() { - auto const brokerSle = - env.le(keylet::loanbroker(broker.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - return lender; - auto const brokerPseudo = brokerSle->at(sfAccount); - return Account("Broker pseudo-account", brokerPseudo); - }(); - - auto badKeylet = keylet::vault(lender.id(), env.seq(lender)); - // Try some failure cases - // flags are checked first - env(set(evan, - broker.brokerID, - principalRequest, - startDate, - tfLoanSetMask), - sig(sfCounterpartySignature, lender), - loanSetFee, - ter(temINVALID_FLAG)); - - // field length validation - // sfData: good length, bad account - env(set(evan, broker.brokerID, principalRequest, startDate), - sig(sfCounterpartySignature, borrower), - data(std::string(maxDataPayloadLength, 'X')), - loanSetFee, - ter(tefBAD_AUTH)); - // sfData: too long - env(set(evan, broker.brokerID, principalRequest, startDate), - sig(sfCounterpartySignature, lender), - data(std::string(maxDataPayloadLength + 1, 'Y')), - loanSetFee, - ter(temINVALID)); - - // field range validation - // sfOverpaymentFee: good value, bad account - env(set(evan, broker.brokerID, principalRequest, startDate), - sig(sfCounterpartySignature, borrower), - overpaymentFee(maxOverpaymentFee), - loanSetFee, - ter(tefBAD_AUTH)); - // sfOverpaymentFee: too big - env(set(evan, broker.brokerID, principalRequest, startDate), - sig(sfCounterpartySignature, lender), - overpaymentFee(maxOverpaymentFee + 1), - loanSetFee, - ter(temINVALID)); - - // sfLateInterestRate: good value, bad account - env(set(evan, broker.brokerID, principalRequest, startDate), - sig(sfCounterpartySignature, borrower), - lateInterestRate(maxLateInterestRate), - loanSetFee, - ter(tefBAD_AUTH)); - // sfLateInterestRate: too big - env(set(evan, broker.brokerID, principalRequest, startDate), - sig(sfCounterpartySignature, lender), - lateInterestRate(maxLateInterestRate + 1), - loanSetFee, - ter(temINVALID)); - - // sfCloseInterestRate: good value, bad account - env(set(evan, broker.brokerID, principalRequest, startDate), - sig(sfCounterpartySignature, borrower), - closeInterestRate(maxCloseInterestRate), - loanSetFee, - ter(tefBAD_AUTH)); - // sfCloseInterestRate: too big - env(set(evan, broker.brokerID, principalRequest, startDate), - sig(sfCounterpartySignature, lender), - closeInterestRate(maxCloseInterestRate + 1), - loanSetFee, - ter(temINVALID)); - - // sfOverpaymentInterestRate: good value, bad account - env(set(evan, broker.brokerID, principalRequest, startDate), - sig(sfCounterpartySignature, borrower), - overpaymentInterestRate(maxOverpaymentInterestRate), - loanSetFee, - ter(tefBAD_AUTH)); - // sfOverpaymentInterestRate: too big - env(set(evan, broker.brokerID, principalRequest, startDate), - sig(sfCounterpartySignature, lender), - overpaymentInterestRate(maxOverpaymentInterestRate + 1), - loanSetFee, - ter(temINVALID)); - - // sfPaymentTotal: good value, bad account - env(set(evan, broker.brokerID, principalRequest, startDate), - sig(sfCounterpartySignature, borrower), - paymentTotal(LoanSet::minPaymentTotal), - loanSetFee, - ter(tefBAD_AUTH)); - // sfPaymentTotal: too small (there is no max) - env(set(evan, broker.brokerID, principalRequest, startDate), - sig(sfCounterpartySignature, lender), - paymentTotal(LoanSet::minPaymentTotal - 1), - loanSetFee, - ter(temINVALID)); - - // sfPaymentInterval: good value, bad account - env(set(evan, broker.brokerID, principalRequest, startDate), - sig(sfCounterpartySignature, borrower), - paymentInterval(LoanSet::minPaymentInterval), - loanSetFee, - ter(tefBAD_AUTH)); - // sfPaymentInterval: too small (there is no max) - env(set(evan, broker.brokerID, principalRequest, startDate), - sig(sfCounterpartySignature, lender), - paymentInterval(LoanSet::minPaymentInterval - 1), - loanSetFee, - ter(temINVALID)); - - // sfGracePeriod: good value, bad account - env(set(evan, broker.brokerID, principalRequest, startDate), - sig(sfCounterpartySignature, borrower), - paymentInterval(LoanSet::minPaymentInterval * 2), - gracePeriod(LoanSet::minPaymentInterval * 2), - loanSetFee, - ter(tefBAD_AUTH)); - // sfGracePeriod: larger than paymentInterval - env(set(evan, broker.brokerID, principalRequest, startDate), - sig(sfCounterpartySignature, lender), - paymentInterval(LoanSet::minPaymentInterval * 2), - gracePeriod(LoanSet::minPaymentInterval * 3), - loanSetFee, - ter(temINVALID)); - - // insufficient fee - single sign - env(set(borrower, broker.brokerID, principalRequest, startDate), - sig(sfCounterpartySignature, lender), - ter(telINSUF_FEE_P)); - // insufficient fee - multisign - env(set(borrower, broker.brokerID, principalRequest, startDate), - counterparty(lender), - msig(evan, lender), - msig(sfCounterpartySignature, evan, borrower), - fee(env.current()->fees().base * 5 - 1), - ter(telINSUF_FEE_P)); - // multisign sufficient fee, but no signers set up - env(set(borrower, broker.brokerID, principalRequest, startDate), - counterparty(lender), - msig(evan, lender), - msig(sfCounterpartySignature, evan, borrower), - fee(env.current()->fees().base * 5), - ter(tefNOT_MULTI_SIGNING)); - // not the broker owner, no counterparty, not signed by broker - // owner - env(set(borrower, broker.brokerID, principalRequest, startDate), - sig(sfCounterpartySignature, evan), - loanSetFee, - ter(tefBAD_AUTH)); - // bad start date - in the past - env(set(evan, - broker.brokerID, - principalRequest, - env.closed()->info().closeTime - 1s), - sig(sfCounterpartySignature, lender), - loanSetFee, - ter(tecEXPIRED)); - // not the broker owner, counterparty is borrower - env(set(evan, broker.brokerID, principalRequest, startDate), - counterparty(borrower), - sig(sfCounterpartySignature, borrower), - loanSetFee, - ter(tecNO_PERMISSION)); - // can not lend money to yourself - env(set(lender, broker.brokerID, principalRequest, startDate), - sig(sfCounterpartySignature, lender), - loanSetFee, - ter(tecNO_PERMISSION)); - // not a LoanBroker object, no counterparty - env(set(lender, badKeylet.key, principalRequest, startDate), - sig(sfCounterpartySignature, evan), - loanSetFee, - ter(temBAD_SIGNER)); - // not a LoanBroker object, counterparty is valid - env(set(lender, badKeylet.key, principalRequest, startDate), - counterparty(borrower), - sig(sfCounterpartySignature, borrower), - loanSetFee, - ter(tecNO_ENTRY)); - // borrower doesn't exist - env(set(lender, broker.brokerID, principalRequest, startDate), - counterparty(alice), - sig(sfCounterpartySignature, alice), - loanSetFee, - ter(terNO_ACCOUNT)); - - // Request more funds than the vault has available - env(set(evan, broker.brokerID, totalVaultRequest + 1, startDate), - sig(sfCounterpartySignature, lender), - loanSetFee, - ter(tecINSUFFICIENT_FUNDS)); - - // Request more funds than the broker's first-loss capital can - // cover. - env(set(evan, - broker.brokerID, - maxCoveredLoanRequest + 1, - startDate), - sig(sfCounterpartySignature, lender), - loanSetFee, - ter(tecINSUFFICIENT_FUNDS)); - - // Frozen trust line / locked MPT issuance - // XRP can not be frozen, but run through the loop anyway to test - // the tecLIMIT_EXCEEDED case + for (int amountExponent = 3; amountExponent >= 3; --amountExponent) { - auto const brokerSle = - env.le(keylet::loanbroker(broker.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - return; - - auto const [freeze, deepfreeze, unfreeze, expectedResult] = - [&]() -> std::tuple< - std::function, - std::function, - std::function, - TER> { - // Freeze / lock the asset - std::function empty; - if (broker.asset.raw().native()) - { - // XRP can't be frozen - return std::make_tuple(empty, empty, empty, tesSUCCESS); - } - else if (broker.asset.raw().holds()) - { - auto freeze = [&](Account const& holder) { - env(trust( - issuer, holder[iouCurrency](0), tfSetFreeze)); - }; - auto deepfreeze = [&](Account const& holder) { - env(trust( - issuer, - holder[iouCurrency](0), - tfSetFreeze | tfSetDeepFreeze)); - }; - auto unfreeze = [&](Account const& holder) { - env(trust( - issuer, - holder[iouCurrency](0), - tfClearFreeze | tfClearDeepFreeze)); - }; - return std::make_tuple( - freeze, deepfreeze, unfreeze, tecFROZEN); - } - else - { - auto freeze = [&](Account const& holder) { - mptt.set( - {.account = issuer, - .holder = holder, - .flags = tfMPTLock}); - }; - auto unfreeze = [&](Account const& holder) { - mptt.set( - {.account = issuer, - .holder = holder, - .flags = tfMPTUnlock}); - }; - return std::make_tuple( - freeze, empty, unfreeze, tecLOCKED); - } - }(); - - // Try freezing both the lender and the pseudo-account - for (auto const& account : {lender, pseudoAcct}) + Number const loanAmount{1, amountExponent}; + for (int interestExponent = 0; interestExponent >= 0; + --interestExponent) { - if (freeze) - { - // Freeze the account - freeze(account); - - // Try to create a loan with a frozen line - env(set(evan, - broker.brokerID, - debtMaximumRequest, - startDate), - sig(sfCounterpartySignature, lender), - loanSetFee, - ter(expectedResult)); - - // Unfreeze the account - BEAST_EXPECT(unfreeze); - unfreeze(account); - } - - // Ensure the line is unfrozen with a request that is fine - // except too it requests more principal than the broker can - // carry - env(set(evan, - broker.brokerID, - debtMaximumRequest + 1, - startDate), - sig(sfCounterpartySignature, lender), - loanSetFee, - ter(tecLIMIT_EXCEEDED)); - } - - // Deep freeze the borrower, which prevents them from receiving - // funds - if (deepfreeze) - { - // Make sure evan has a trust line that so the issuer can - // freeze it. (Don't need to do this for the borrower, - // because LoanDraw will create a line to the borrower - // automatically.) - env(trust(evan, issuer[iouCurrency](100'000))); - - // Freeze evan - deepfreeze(evan); - - // Try to create a loan with a deep frozen line - env(set(evan, - broker.brokerID, - debtMaximumRequest, - startDate), - sig(sfCounterpartySignature, lender), - loanSetFee, - ter(expectedResult)); - - // Unfreeze evan - BEAST_EXPECT(unfreeze); - unfreeze(evan); - - // Ensure the line is unfrozen with a request that is fine - // except too it requests more principal than the broker can - // carry - env(set(evan, - broker.brokerID, - debtMaximumRequest + 1, - startDate), - sig(sfCounterpartySignature, lender), - loanSetFee, - ter(tecLIMIT_EXCEEDED)); + testCaseWrapper( + env, + mptt, + assets, + broker, + loanAmount, + interestExponent); } } - // Finally! Create a loan - std::string testData; - - auto currentState = [&](Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus) { - // Lookup the current loan state - if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan)) - { - LoanState state{ - .previousPaymentDate = loan->at(sfPreviousPaymentDate), - .startDate = tp{d{loan->at(sfStartDate)}}, - .nextPaymentDate = loan->at(sfNextPaymentDueDate), - .paymentRemaining = loan->at(sfPaymentRemaining), - .assetsAvailable = loan->at(sfAssetsAvailable), - .principalRequested = loan->at(sfPrincipalRequested), - .principalOutstanding = - loan->at(sfPrincipalOutstanding), - .flags = loan->at(sfFlags), - .paymentInterval = loan->at(sfPaymentInterval), - }; - BEAST_EXPECT(state.previousPaymentDate == 0); - BEAST_EXPECT( - tp{d{state.nextPaymentDate}} == state.startDate + 600s); - BEAST_EXPECT(state.paymentRemaining == 12); - BEAST_EXPECT( - state.assetsAvailable == broker.asset(999).value()); - BEAST_EXPECT( - state.principalOutstanding == - broker.asset(1000).value()); - BEAST_EXPECT( - state.principalOutstanding == state.principalRequested); - BEAST_EXPECT(state.paymentInterval == 600); - - verifyLoanStatus(state); - - return state; - } - - return LoanState{ - .previousPaymentDate = 0, - .startDate = tp{d{0}}, - .nextPaymentDate = 0, - .paymentRemaining = 0, - .assetsAvailable = 0, - .principalRequested = 0, - .principalOutstanding = 0, - .flags = 0, - .paymentInterval = 0, - }; - }; - - auto defaultBeforeStartDate = [&](std::uint32_t baseFlag, - bool impair = true) { - return [&, impair, baseFlag]( - Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus) { - // toEndOfLife - // - // Default the loan - - // Initialize values with the current state - auto state = currentState(loanKeylet, verifyLoanStatus); - BEAST_EXPECT(state.flags == baseFlag); - - if (impair) - { - // Impair the loan - env(manage(lender, loanKeylet.key, tfLoanImpair)); - - state.flags |= tfLoanImpair; - state.nextPaymentDate = - env.now().time_since_epoch().count(); - verifyLoanStatus(state); - - // Once the loan is impaired, it can't be impaired again - env(manage(lender, loanKeylet.key, tfLoanImpair), - ter(tecNO_PERMISSION)); - } - - auto const nextDueDate = tp{d{state.nextPaymentDate}}; - - // Can't default the loan yet. The grace period hasn't - // expired - env(manage(lender, loanKeylet.key, tfLoanDefault), - ter(tecTOO_SOON)); - - // Let some time pass so that the loan can be - // defaulted - env.close(nextDueDate + 60s); - - if (impair) - { - // Impaired loans can't be drawn against - env(draw(borrower, loanKeylet.key, broker.asset(100)), - ter(tecNO_PERMISSION)); - } - - // Default the loan - env(manage(lender, loanKeylet.key, tfLoanDefault)); - - state.flags |= tfLoanDefault; - state.paymentRemaining = 0; - state.assetsAvailable = 0; - state.principalOutstanding = 0; - verifyLoanStatus(state); - - // Defaulted loans can't be drawn against, either - env(draw(borrower, loanKeylet.key, broker.asset(100)), - ter(tecNO_PERMISSION)); - - // Once a loan is defaulted, it can't be managed - env(manage(lender, loanKeylet.key, tfLoanUnimpair), - ter(tecNO_PERMISSION)); - env(manage(lender, loanKeylet.key, tfLoanImpair), - ter(tecNO_PERMISSION)); - }; - }; - - auto immediatePayoff = [&](std::uint32_t baseFlag) { - return [&, baseFlag]( - Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus) { - // toEndOfLife - // - auto state = currentState(loanKeylet, verifyLoanStatus); - BEAST_EXPECT(state.flags == baseFlag); - auto const borrowerStartingBalance = - env.balance(borrower, broker.asset); - - // Try to make a payment before the loan starts - env(pay(borrower, loanKeylet.key, broker.asset(500)), - ter(tecTOO_SOON)); - - // Advance to the start date of the loan - env.close(state.startDate + 5s); - - verifyLoanStatus(state); - - // Need to account for fees if the loan is in XRP - PrettyAmount adjustment = broker.asset(0); - if (broker.asset.raw().native()) - { - adjustment = 2 * env.current()->fees().base; - } - - // Draw the entire available balance - // Need to create the STAmount directly to avoid - // PrettyAsset scaling. - STAmount const drawAmount{ - broker.asset, state.assetsAvailable}; - env(draw(borrower, loanKeylet.key, drawAmount)); - env.close(state.startDate + 20s); - auto const loanAge = (env.now() - state.startDate).count(); - BEAST_EXPECT(loanAge == 30); - - state.assetsAvailable -= drawAmount; - verifyLoanStatus(state); - BEAST_EXPECT( - env.balance(borrower, broker.asset) == - borrowerStartingBalance + drawAmount - adjustment); - - // Send some bogus pay transactions - env(pay(borrower, - keylet::loan(uint256(0)).key, - broker.asset(10)), - ter(temINVALID)); - env(pay(borrower, loanKeylet.key, broker.asset(-100)), - ter(temBAD_AMOUNT)); - env(pay(borrower, broker.brokerID, broker.asset(100)), - ter(tecNO_ENTRY)); - env(pay(evan, loanKeylet.key, broker.asset(500)), - ter(tecNO_PERMISSION)); - - { - auto const otherAsset = - broker.asset.raw() == assets[0].raw() ? assets[1] - : assets[0]; - env(pay(borrower, loanKeylet.key, otherAsset(100)), - ter(tecWRONG_ASSET)); - } - - // Amount doesn't cover a single payment - env(pay(borrower, - loanKeylet.key, - STAmount{broker.asset, 1}), - ter(tecINSUFFICIENT_PAYMENT)); - - // Get the balance after these failed transactions take - // fees - auto const borrowerBalanceBeforePayment = - env.balance(borrower, broker.asset); - - // Full payoff amount will consist of - // 1. principal outstanding (1000) - // 2. accrued interest (at 12%) - // 3. prepayment penalty (closeInterest at 3.6%) - // 4. close payment fee (4) - // Calculate these values without the helper functions - // to verify they're working correctly The numbers in - // the below BEAST_EXPECTs may not hold across assets. - Number const interval = state.paymentInterval; - auto const periodicRate = - interval * Number(12, -2) / (365 * 24 * 60 * 60); - BEAST_EXPECT( - periodicRate == - Number(2283105022831050, -21, Number::unchecked{})); - STAmount const accruedInterest{ - broker.asset, - state.principalOutstanding * periodicRate * loanAge / - interval}; - BEAST_EXPECT( - accruedInterest == - broker.asset(Number(1141552511415525, -19))); - STAmount const prepaymentPenalty{ - broker.asset, - state.principalOutstanding * Number(36, -3)}; - BEAST_EXPECT(prepaymentPenalty == broker.asset(36)); - STAmount const closePaymentFee = broker.asset(4); - auto const payoffAmount = - STAmount{broker.asset, state.principalOutstanding} + - accruedInterest + prepaymentPenalty + closePaymentFee; - BEAST_EXPECT( - payoffAmount == - broker.asset(Number(1040000114155251, -12))); - BEAST_EXPECT(payoffAmount > drawAmount); - // Try to pay a little extra to show that it's _not_ - // taken - auto const transactionAmount = - payoffAmount + broker.asset(10); - BEAST_EXPECT( - transactionAmount == - broker.asset(Number(1050000114155251, -12))); - env(pay(borrower, loanKeylet.key, transactionAmount)); - - env.close(); - - // Need to account for fees if the loan is in XRP - adjustment = broker.asset(0); - if (broker.asset.raw().native()) - { - adjustment = env.current()->fees().base; - } - - state.paymentRemaining = 0; - state.principalOutstanding = 0; - verifyLoanStatus(state); - - BEAST_EXPECT( - env.balance(borrower, broker.asset) == - borrowerBalanceBeforePayment - payoffAmount - - adjustment); - - // Can't impair or default a paid off loan - env(manage(lender, loanKeylet.key, tfLoanImpair), - ter(tecNO_PERMISSION)); - env(manage(lender, loanKeylet.key, tfLoanDefault), - ter(tecNO_PERMISSION)); - }; - }; - - // There are a lot of fields that can be set on a loan, but most of - // them only affect the "math" when a payment is made. The only one - // that really affects behavior is the `tfLoanOverpayment` flag. - lifecycle( - "Loan overpayment allowed - Impair and Default before start " - "date", - env, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - defaultBeforeStartDate(lsfLoanOverpayment)); - - lifecycle( - "Loan overpayment prohibited - Impair and Default before start " - "date", - env, - lender, - borrower, - evan, - broker, - pseudoAcct, - 0, - defaultBeforeStartDate(0)); - - lifecycle( - "Loan overpayment allowed - Default without Impair before " - "start " - "date", - env, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - defaultBeforeStartDate(lsfLoanOverpayment, false)); - - lifecycle( - "Loan overpayment prohibited - Default without Impair before " - "start " - "date", - env, - lender, - borrower, - evan, - broker, - pseudoAcct, - 0, - defaultBeforeStartDate(0, false)); - - lifecycle( - "Loan overpayment allowed - Draw then default", - env, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - [&](Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus) { - // toEndOfLife - // - // Initialize values with the current state - auto state = currentState(loanKeylet, verifyLoanStatus); - BEAST_EXPECT(state.flags == lsfLoanOverpayment); - auto const borrowerStartingBalance = - env.balance(borrower, broker.asset); - - // Draw the balance - env(draw( - borrower, - keylet::loan(uint256(0)).key, - broker.asset(10)), - ter(temINVALID)); - env(draw(borrower, loanKeylet.key, broker.asset(-100)), - ter(temBAD_AMOUNT)); - env(draw(borrower, broker.brokerID, broker.asset(100)), - ter(tecNO_ENTRY)); - env(draw(evan, loanKeylet.key, broker.asset(500)), - ter(tecNO_PERMISSION)); - env(draw(borrower, loanKeylet.key, broker.asset(500)), - ter(tecTOO_SOON)); - - // Advance to the start date of the loan - env.close(state.startDate + 5s); - env(draw(borrower, loanKeylet.key, broker.asset(10000)), - ter(tecINSUFFICIENT_FUNDS)); - { - auto const otherAsset = - broker.asset.raw() == assets[0].raw() ? assets[1] - : assets[0]; - env(draw(borrower, loanKeylet.key, otherAsset(100)), - ter(tecWRONG_ASSET)); - } - - verifyLoanStatus(state); - - // Need to account for fees if the loan is in XRP - PrettyAmount adjustment = broker.asset(0); - if (broker.asset.raw().native()) - { - adjustment = 5 * env.current()->fees().base; - } - - // Draw about half the balance - auto const drawAmount = broker.asset(500); - env(draw(borrower, loanKeylet.key, drawAmount)); - - state.assetsAvailable -= drawAmount.number(); - verifyLoanStatus(state); - BEAST_EXPECT( - env.balance(borrower, broker.asset) == - borrowerStartingBalance + drawAmount - adjustment); - - // move past the due date + grace period (60s) - env.close(tp{d{state.nextPaymentDate}} + 60s + 20s); - // Try to draw - env(draw(borrower, loanKeylet.key, broker.asset(100)), - ter(tecNO_PERMISSION)); - - // default the loan - env(manage(lender, loanKeylet.key, tfLoanDefault)); - state.paymentRemaining = 0; - state.assetsAvailable = 0; - state.principalOutstanding = 0; - state.flags |= tfLoanDefault; - - verifyLoanStatus(state); - - // Same error, different check - env(draw(borrower, loanKeylet.key, broker.asset(100)), - ter(tecNO_PERMISSION)); - - // Can't make a payment on it either - env(pay(borrower, loanKeylet.key, broker.asset(300)), - ter(tecKILLED)); - - // Default - }); - - lifecycle( - "Loan overpayment prohibited - Pay off immediately", - env, - lender, - borrower, - evan, - broker, - pseudoAcct, - 0, - immediatePayoff(0)); - - lifecycle( - "Loan overpayment allowed - Pay off immediately", - env, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - immediatePayoff(lsfLoanOverpayment)); - - lifecycle( - "Loan overpayment prohibited - Make payments", - env, - lender, - borrower, - evan, - broker, - pseudoAcct, - 0, - [&](Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus) { - // toEndOfLife - // - // Draw and make multiple payments - auto state = currentState(loanKeylet, verifyLoanStatus); - BEAST_EXPECT(state.flags == 0); - // Advance to the start date of the loan - env.close(state.startDate + 5s); - - verifyLoanStatus(state); - - auto const borrowerStartingBalance = - env.balance(borrower, broker.asset); - - // Need to account for fees if the loan is in XRP - PrettyAmount adjustment = broker.asset(0); - if (broker.asset.raw().native()) - { - adjustment = env.current()->fees().base; - } - - // Draw the entire available balance - // Need to create the STAmount directly to avoid - // PrettyAsset scaling. - STAmount const drawAmount{ - broker.asset, state.assetsAvailable}; - env(draw(borrower, loanKeylet.key, drawAmount)); - env.close(state.startDate + 20s); - auto const loanAge = (env.now() - state.startDate).count(); - BEAST_EXPECT(loanAge == 30); - - state.assetsAvailable -= drawAmount; - verifyLoanStatus(state); - BEAST_EXPECT( - env.balance(borrower, broker.asset) == - borrowerStartingBalance + drawAmount - adjustment); - - // Periodic payment amount will consist of - // 1. principal outstanding (1000) - // 2. interest interest rate (at 12%) - // 3. payment interval (600s) - // 4. loan service fee (2) - // Calculate these values without the helper functions - // to verify they're working correctly The numbers in - // the below BEAST_EXPECTs may not hold across assets. - Number const interval = state.paymentInterval; - auto const periodicRate = - interval * Number(12, -2) / (365 * 24 * 60 * 60); - BEAST_EXPECT( - periodicRate == - Number(2283105022831050, -21, Number::unchecked{})); - - while (state.paymentRemaining > 0) - { - testcase << "Payments remaining: " - << state.paymentRemaining; - - STAmount const principalRequestedAmount{ - broker.asset, state.principalRequested}; - // Compute the payment based on the number of payments - // remaining - auto const rateFactor = - power(1 + periodicRate, state.paymentRemaining); - Number const rawPeriodicPayment = - state.principalOutstanding * periodicRate * - rateFactor / (rateFactor - 1); - STAmount const periodicPayment = roundToReference( - STAmount{broker.asset, rawPeriodicPayment}, - principalRequestedAmount); - // Only check the first payment since the rounding may - // drift as payments are made - BEAST_EXPECT( - state.paymentRemaining < 12 || - STAmount(broker.asset, rawPeriodicPayment) == - broker.asset(Number(8333457001162141, -14))); - // Include the service fee - STAmount const totalDue = roundToReference( - periodicPayment + broker.asset(2), - principalRequestedAmount); - // Only check the first payment since the rounding may - // drift as payments are made - BEAST_EXPECT( - state.paymentRemaining < 12 || - totalDue == - roundToReference( - broker.asset(Number(8533457001162141, -14)), - principalRequestedAmount)); - - // Try to pay a little extra to show that it's _not_ - // taken - STAmount const transactionAmount = - STAmount{broker.asset, totalDue} + broker.asset(10); - // Only check the first payment since the rounding may - // drift as payments are made - BEAST_EXPECT( - state.paymentRemaining < 12 || - transactionAmount == - roundToReference( - broker.asset(Number(9533457001162141, -14)), - principalRequestedAmount)); - - auto const totalDueAmount = - STAmount{broker.asset, totalDue}; - - // Compute the expected principal amount - Number const rawInterest = state.paymentRemaining == 1 - ? rawPeriodicPayment - state.principalOutstanding - : state.principalOutstanding * periodicRate; - STAmount const interest = roundToReference( - STAmount{broker.asset, rawInterest}, - principalRequestedAmount); - BEAST_EXPECT( - state.paymentRemaining < 12 || - roundToReference( - STAmount{broker.asset, rawInterest}, - principalRequestedAmount) == - roundToReference( - broker.asset(Number(2283105022831050, -18)), - principalRequestedAmount)); - BEAST_EXPECT(interest >= Number(0)); - - auto const rawPrincipal = - rawPeriodicPayment - rawInterest; - BEAST_EXPECT( - state.paymentRemaining < 12 || - roundToReference( - STAmount{broker.asset, rawPrincipal}, - principalRequestedAmount) == - roundToReference( - broker.asset(Number(8333228690659858, -14)), - principalRequestedAmount)); - BEAST_EXPECT( - state.paymentRemaining > 1 || - rawPrincipal == state.principalOutstanding); - auto const principal = roundToReference( - STAmount{broker.asset, periodicPayment - interest}, - principalRequestedAmount); - BEAST_EXPECT( - principal > Number(0) && - principal <= state.principalOutstanding); - BEAST_EXPECT( - state.paymentRemaining > 1 || - principal == state.principalOutstanding); - BEAST_EXPECT( - rawPrincipal + rawInterest == rawPeriodicPayment); - BEAST_EXPECT(principal + interest == periodicPayment); - - auto const borrowerBalanceBeforePayment = - env.balance(borrower, broker.asset); - - // Make the payment - env(pay(borrower, loanKeylet.key, transactionAmount)); - - env.close(); - - // Need to account for fees if the loan is in XRP - adjustment = broker.asset(0); - if (broker.asset.raw().native()) - { - adjustment = env.current()->fees().base; - } - - // Check the result - auto const borrowerBalance = - env.balance(borrower, broker.asset); - auto const expectedBalance = - borrowerBalanceBeforePayment - totalDueAmount - - adjustment; - BEAST_EXPECT( - borrowerBalance == expectedBalance || - (!broker.asset.raw().native() && - broker.asset.raw().holds() && - ((borrowerBalance - expectedBalance) / - expectedBalance < - Number(1, -4)))); - - --state.paymentRemaining; - state.previousPaymentDate = state.nextPaymentDate; - state.nextPaymentDate += state.paymentInterval; - state.principalOutstanding -= principal; - - verifyLoanStatus(state); - } - - // Loan is paid off - BEAST_EXPECT(state.paymentRemaining == 0); - BEAST_EXPECT(state.principalOutstanding == 0); - - // Can't impair or default a paid off loan - env(manage(lender, loanKeylet.key, tfLoanImpair), - ter(tecNO_PERMISSION)); - env(manage(lender, loanKeylet.key, tfLoanDefault), - ter(tecNO_PERMISSION)); - }); - if (auto brokerSle = env.le(keylet::loanbroker(broker.brokerID)); BEAST_EXPECT(brokerSle)) { From 3ef72d28bd78de83a268f0f0a2f3e3588bf77aa7 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 15 Jul 2025 14:20:44 -0400 Subject: [PATCH 013/291] fixup! Make a few tweaks to the changes in 907cc19a --- src/test/app/EscrowToken_test.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/app/EscrowToken_test.cpp b/src/test/app/EscrowToken_test.cpp index 6ba8c48c93..3265a1cd0c 100644 --- a/src/test/app/EscrowToken_test.cpp +++ b/src/test/app/EscrowToken_test.cpp @@ -3426,7 +3426,7 @@ struct EscrowToken_test : public beast::unit_test::suite auto const preAliceMPT = env.balance(alice, MPT); auto const preOutstanding = env.balance(gw, MPT); auto const preEscrowed = issuerMPTEscrowed(env, MPT); - BEAST_EXPECT(preOutstanding == MPT(10'000)); + BEAST_EXPECT(preOutstanding == MPT(-10'000)); BEAST_EXPECT(preEscrowed == 0); env(escrow::create(alice, gw, MPT(1'000)), @@ -3449,7 +3449,7 @@ struct EscrowToken_test : public beast::unit_test::suite BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000)); BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); - BEAST_EXPECT(env.balance(gw, MPT) == preOutstanding - MPT(1'000)); + BEAST_EXPECT(env.balance(gw, MPT) == preOutstanding + MPT(1'000)); BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == preEscrowed); } } @@ -3759,7 +3759,7 @@ struct EscrowToken_test : public beast::unit_test::suite BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 10); BEAST_EXPECT(env.balance(bob, MPT) == MPT(0)); BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); - BEAST_EXPECT(env.balance(gw, MPT) == MPT(10)); + BEAST_EXPECT(env.balance(gw, MPT) == MPT(-10)); mptGw.authorize({.account = bob, .flags = tfMPTUnauthorize}); mptGw.destroy( {.id = mptGw.issuanceID(), From 5199c5e073288f64885cd123dbdb267d35904598 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 15 Jul 2025 15:16:02 -0400 Subject: [PATCH 014/291] Improve / add freeze checking helper functions - Add an override of isDeepFrozen that can take an Asset, and thus an MPTIssue. The MPT version just calls isFrozen, since they're equivalent for MPTs. - Add wrappers checkFrozen and checkDeepFrozen that return the appropriate TER code, so the Asset type doesn't have to be checked at every #*%@ing caller. - Convert the Loan* transactors to use these functions. --- .../app/tx/detail/LoanBrokerCoverDeposit.cpp | 14 +-- .../app/tx/detail/LoanBrokerCoverWithdraw.cpp | 17 +--- src/xrpld/app/tx/detail/LoanDraw.cpp | 15 ++- src/xrpld/app/tx/detail/LoanPay.cpp | 17 ++-- src/xrpld/app/tx/detail/LoanSet.cpp | 26 +++-- src/xrpld/ledger/View.h | 97 +++++++++++++++++++ 6 files changed, 136 insertions(+), 50 deletions(-) diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp index 923703a6a0..5b9e8bba0e 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp @@ -89,15 +89,11 @@ LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx) auto const pseudoAccountID = sleBroker->at(sfAccount); // Cannot transfer a frozen Asset - if (isFrozen(ctx.view, account, vaultAsset)) - return vaultAsset.holds() ? tecFROZEN : tecLOCKED; - if (vaultAsset.holds()) - { - auto const issue = vaultAsset.get(); - if (isDeepFrozen( - ctx.view, pseudoAccountID, issue.currency, issue.account)) - return tecFROZEN; - } + if (auto const ret = checkFrozen(ctx.view, account, vaultAsset)) + return ret; + // Pseudo-account cannot receive if asset is deep frozen + if (auto const ret = checkDeepFrozen(ctx.view, pseudoAccountID, vaultAsset)) + return ret; if (accountHolds( ctx.view, diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp index a1e1233b89..d1705082d6 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp @@ -90,18 +90,11 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) auto const pseudoAccountID = sleBroker->at(sfAccount); // Cannot transfer a frozen Asset - /* - if (isFrozen(ctx.view, account, vaultAsset)) - return vaultAsset.holds() ? tecFROZEN : tecLOCKED; - */ - if (isFrozen(ctx.view, pseudoAccountID, vaultAsset)) - return vaultAsset.holds() ? tecFROZEN : tecLOCKED; - if (vaultAsset.holds()) - { - auto const issue = vaultAsset.get(); - if (isDeepFrozen(ctx.view, account, issue.currency, issue.account)) - return tecFROZEN; - } + if (auto const ret = checkFrozen(ctx.view, pseudoAccountID, vaultAsset)) + return ret; + // Account cannot receive if asset is deep frozen + if (auto const ret = checkDeepFrozen(ctx.view, account, vaultAsset)) + return ret; auto const coverAvail = sleBroker->at(sfCoverAvailable); // Cover Rate is in 1/10 bips units diff --git a/src/xrpld/app/tx/detail/LoanDraw.cpp b/src/xrpld/app/tx/detail/LoanDraw.cpp index 2e957c6f79..3b99ccf900 100644 --- a/src/xrpld/app/tx/detail/LoanDraw.cpp +++ b/src/xrpld/app/tx/detail/LoanDraw.cpp @@ -131,19 +131,16 @@ LoanDraw::preclaim(PreclaimContext const& ctx) return tecINSUFFICIENT_FUNDS; } - if (isFrozen(ctx.view, brokerPseudoAccount, asset)) + if (auto const ret = checkFrozen(ctx.view, brokerPseudoAccount, asset)) { JLOG(ctx.j.warn()) << "Loan Broker pseudo-account is frozen."; - return asset.holds() ? tecFROZEN : tecLOCKED; + return ret; } - if (asset.holds()) + if (auto const ret = checkDeepFrozen(ctx.view, account, asset)) { - auto const issue = asset.get(); - if (isDeepFrozen(ctx.view, account, issue.currency, issue.account)) - { - JLOG(ctx.j.warn()) << "Borrower account is frozen."; - return tecFROZEN; - } + JLOG(ctx.j.warn()) + << "Borrower account cannot receive funds (deep frozen)."; + return ret; } if (hasExpired(ctx.view, loanSle->at(sfNextPaymentDueDate))) diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index e83e4ae27e..866a5d9914 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -131,19 +131,16 @@ LoanPay::preclaim(PreclaimContext const& ctx) return tecWRONG_ASSET; } - if (isFrozen(ctx.view, brokerPseudoAccount, asset)) + if (auto const ret = checkFrozen(ctx.view, account, asset)) { - JLOG(ctx.j.warn()) << "Loan Broker pseudo-account is frozen."; - return asset.holds() ? tecFROZEN : tecLOCKED; + JLOG(ctx.j.warn()) << "Borrower account is frozen."; + return ret; } - if (asset.holds()) + if (auto const ret = checkDeepFrozen(ctx.view, brokerPseudoAccount, asset)) { - auto const issue = asset.get(); - if (isDeepFrozen(ctx.view, account, issue.currency, issue.account)) - { - JLOG(ctx.j.warn()) << "Borrower account is frozen."; - return tecFROZEN; - } + JLOG(ctx.j.warn()) << "Loan Broker pseudo-account can not receive " + "funds (deep frozen)."; + return ret; } return tesSUCCESS; diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index c6cfed0335..b42cdb1cbc 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -204,19 +204,25 @@ LoanSet::preclaim(PreclaimContext const& ctx) if (auto const ter = canAddHolding(ctx.view, asset)) return ter; - if (isFrozen(ctx.view, brokerOwner, asset) || - isFrozen(ctx.view, brokerPseudo, asset)) + if (auto const ret = checkFrozen(ctx.view, brokerOwner, asset)) { - JLOG(ctx.j.warn()) << "One of the affected accounts is frozen."; - return asset.holds() ? tecFROZEN : tecLOCKED; + JLOG(ctx.j.warn()) << "Broker owner account is frozen."; + return ret; } - if (asset.holds()) + if (auto const ret = checkFrozen(ctx.view, brokerPseudo, asset)) { - auto const issue = asset.get(); - if (isDeepFrozen(ctx.view, borrower, issue.currency, issue.account)) - return tecFROZEN; - if (isDeepFrozen(ctx.view, brokerPseudo, issue.currency, issue.account)) - return tecFROZEN; + JLOG(ctx.j.warn()) << "Broker pseudo-account account is frozen."; + return ret; + } + if (auto const ret = checkDeepFrozen(ctx.view, borrower, asset)) + { + JLOG(ctx.j.warn()) << "Borrower account is deep frozen."; + return ret; + } + if (auto const ret = checkDeepFrozen(ctx.view, brokerPseudo, asset)) + { + JLOG(ctx.j.warn()) << "Broker pseudo-account account is deep frozen."; + return ret; } auto const principalRequested = tx[sfPrincipalRequested]; diff --git a/src/xrpld/ledger/View.h b/src/xrpld/ledger/View.h index 4f19e34b97..3a01638591 100644 --- a/src/xrpld/ledger/View.h +++ b/src/xrpld/ledger/View.h @@ -175,6 +175,29 @@ isFrozen( asset.value()); } +[[nodiscard]] inline TER +checkFrozen(ReadView const& view, AccountID const& account, Issue const& issue) +{ + return isFrozen(view, account, issue) ? (TER)tecFROZEN : (TER)tesSUCCESS; +} + +[[nodiscard]] inline TER +checkFrozen( + ReadView const& view, + AccountID const& account, + MPTIssue const& mptIssue) +{ + return isFrozen(view, account, mptIssue) ? (TER)tecLOCKED : (TER)tesSUCCESS; +} + +[[nodiscard]] inline TER +checkFrozen(ReadView const& view, AccountID const& account, Asset const& asset) +{ + return std::visit( + [&](auto const& issue) { return checkFrozen(view, account, issue); }, + asset.value()); +} + [[nodiscard]] bool isAnyFrozen( ReadView const& view, @@ -220,6 +243,80 @@ isDeepFrozen( Currency const& currency, AccountID const& issuer); +[[nodiscard]] inline bool +isDeepFrozen( + ReadView const& view, + AccountID const& account, + Issue const& issue, + int = 0 /*ignored*/) +{ + return isDeepFrozen(view, account, issue.currency, issue.account); +} + +[[nodiscard]] inline bool +isDeepFrozen( + ReadView const& view, + AccountID const& account, + MPTIssue const& mptIssue, + int depth = 0) +{ + // Unlike IOUs, frozen / locked MPTs are not allowed to send or receive + // funds, so checking "deep frozen" is the same as checking "frozen". + return isFrozen(view, account, mptIssue, depth); +} + +/** + * isFrozen check is recursive for MPT shares in a vault, descending to + * assets in the vault, up to maxAssetCheckDepth recursion depth. This is + * purely defensive, as we currently do not allow such vaults to be created. + */ +[[nodiscard]] inline bool +isDeepFrozen( + ReadView const& view, + AccountID const& account, + Asset const& asset, + int depth = 0) +{ + return std::visit( + [&](auto const& issue) { + return isDeepFrozen(view, account, issue, depth); + }, + asset.value()); +} + +[[nodiscard]] inline TER +checkDeepFrozen( + ReadView const& view, + AccountID const& account, + Issue const& issue) +{ + return isDeepFrozen(view, account, issue) ? (TER)tecFROZEN + : (TER)tesSUCCESS; +} + +[[nodiscard]] inline TER +checkDeepFrozen( + ReadView const& view, + AccountID const& account, + MPTIssue const& mptIssue) +{ + return isDeepFrozen(view, account, mptIssue) ? (TER)tecLOCKED + : (TER)tesSUCCESS; +} + +[[nodiscard]] inline TER +checkDeepFrozen( + ReadView const& view, + AccountID const& account, + Asset const& asset) +{ + return std::visit( + [&](auto const& issue) { + return checkDeepFrozen(view, account, issue); + }, + asset.value()); +} + [[nodiscard]] bool isLPTokenFrozen( ReadView const& view, From 38cb371c72a3d55d81c8dbac05d472775e152e7e Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 15 Jul 2025 17:48:14 -0400 Subject: [PATCH 015/291] Make requireAuth work more similarly between IOUs and MPTs - Unfortunately, to not change behavior, a new "Legacy" authorization type was created. It acts like "StrongAuth" for MPTs and "WeakAuth" for IOUs. --- src/xrpld/app/tx/detail/Escrow.cpp | 8 ++--- src/xrpld/ledger/View.h | 47 ++++++++++++++++++++---------- src/xrpld/ledger/detail/View.cpp | 29 +++++++++++++----- 3 files changed, 57 insertions(+), 27 deletions(-) diff --git a/src/xrpld/app/tx/detail/Escrow.cpp b/src/xrpld/app/tx/detail/Escrow.cpp index 501fce3d73..68ee111b76 100644 --- a/src/xrpld/app/tx/detail/Escrow.cpp +++ b/src/xrpld/app/tx/detail/Escrow.cpp @@ -316,14 +316,14 @@ escrowCreatePreclaimHelper( // authorized auto const& mptIssue = amount.get(); if (auto const ter = - requireAuth(ctx.view, mptIssue, account, MPTAuthType::WeakAuth); + requireAuth(ctx.view, mptIssue, account, AuthType::WeakAuth); ter != tesSUCCESS) return ter; // If the issuer has requireAuth set, check if the destination is // authorized if (auto const ter = - requireAuth(ctx.view, mptIssue, dest, MPTAuthType::WeakAuth); + requireAuth(ctx.view, mptIssue, dest, AuthType::WeakAuth); ter != tesSUCCESS) return ter; @@ -743,7 +743,7 @@ escrowFinishPreclaimHelper( // authorized auto const& mptIssue = amount.get(); if (auto const ter = - requireAuth(ctx.view, mptIssue, dest, MPTAuthType::WeakAuth); + requireAuth(ctx.view, mptIssue, dest, AuthType::WeakAuth); ter != tesSUCCESS) return ter; @@ -1257,7 +1257,7 @@ escrowCancelPreclaimHelper( // authorized auto const& mptIssue = amount.get(); if (auto const ter = - requireAuth(ctx.view, mptIssue, account, MPTAuthType::WeakAuth); + requireAuth(ctx.view, mptIssue, account, AuthType::WeakAuth); ter != tesSUCCESS) return ter; diff --git a/src/xrpld/ledger/View.h b/src/xrpld/ledger/View.h index 3a01638591..112dcbfb0c 100644 --- a/src/xrpld/ledger/View.h +++ b/src/xrpld/ledger/View.h @@ -846,15 +846,31 @@ transferXRP( * - StrongAuth - before checking lsfMPTRequireAuth is set * - WeakAuth - after checking if lsfMPTRequireAuth is set */ -enum class MPTAuthType : bool { StrongAuth = true, WeakAuth = false }; +enum class AuthType { StrongAuth, WeakAuth, Legacy }; /** Check if the account lacks required authorization. * - * Return tecNO_AUTH or tecNO_LINE if it does - * and tesSUCCESS otherwise. + * Return tecNO_AUTH or tecNO_LINE if it does + * and tesSUCCESS otherwise. + * + * If StrongAuth then return tecNO_LINE if the RippleState doesn't exist. Return + * tecNO_AUTH if lsfRequireAuth is set on the issuer's AccountRoot, and the + * RippleState does exist, and the RippleState is not authorized. + * + * If WeakAuth then return tecNO_AUTH if lsfRequireAuth is set, and the + * RippleState exists, and is not authorized. Return tecNO_LINE if + * lsfRequireAuth is set and the RippleState doesn't exist. Consequently, if + * WeakAuth and lsfRequireAuth is *not* set, this function will return + * tesSUCCESS even if RippleState does *not* exist. + * + * The default "Legacy" auth type is equivalent to WeakAuth. */ [[nodiscard]] TER -requireAuth(ReadView const& view, Issue const& issue, AccountID const& account); +requireAuth( + ReadView const& view, + Issue const& issue, + AccountID const& account, + AuthType authType = AuthType::Legacy); /** Check if the account lacks required authorization. * @@ -868,32 +884,33 @@ requireAuth(ReadView const& view, Issue const& issue, AccountID const& account); * purely defensive, as we currently do not allow such vaults to be created. * * If StrongAuth then return tecNO_AUTH if MPToken doesn't exist or - * lsfMPTRequireAuth is set and MPToken is not authorized. If WeakAuth then - * return tecNO_AUTH if lsfMPTRequireAuth is set and MPToken doesn't exist or is - * not authorized (explicitly or via credentials, if DomainID is set in - * MPTokenIssuance). Consequently, if WeakAuth and lsfMPTRequireAuth is *not* - * set, this function will return true even if MPToken does *not* exist. + * lsfMPTRequireAuth is set and MPToken is not authorized. + * + * If WeakAuth then return tecNO_AUTH if lsfMPTRequireAuth is set and MPToken + * doesn't exist or is not authorized (explicitly or via credentials, if + * DomainID is set in MPTokenIssuance). Consequently, if WeakAuth and + * lsfMPTRequireAuth is *not* set, this function will return true even if + * MPToken does *not* exist. + * + * The default "Legacy" auth type is equivalent to StrongAuth. */ [[nodiscard]] TER requireAuth( ReadView const& view, MPTIssue const& mptIssue, AccountID const& account, - MPTAuthType authType = MPTAuthType::StrongAuth, + AuthType authType = AuthType::Legacy, int depth = 0); [[nodiscard]] TER inline requireAuth( ReadView const& view, Asset const& asset, AccountID const& account, - MPTAuthType authType = MPTAuthType::StrongAuth) + AuthType authType = AuthType::Legacy) { return std::visit( [&](TIss const& issue_) { - if constexpr (std::is_same_v) - return requireAuth(view, issue_, account); - else - return requireAuth(view, issue_, account, authType); + return requireAuth(view, issue_, account, authType); }, asset.value()); } diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index 38c1cde689..a7e7f2e7f8 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -505,8 +505,8 @@ accountHolds( if (zeroIfUnauthorized == ahZERO_IF_UNAUTHORIZED && view.rules().enabled(featureSingleAssetVault)) { - if (auto const err = requireAuth( - view, mptIssue, account, MPTAuthType::StrongAuth); + if (auto const err = + requireAuth(view, mptIssue, account, AuthType::StrongAuth); !isTesSuccess(err)) amount.clear(mptIssue); } @@ -2360,15 +2360,27 @@ transferXRP( } TER -requireAuth(ReadView const& view, Issue const& issue, AccountID const& account) +requireAuth( + ReadView const& view, + Issue const& issue, + AccountID const& account, + AuthType authType) { if (isXRP(issue) || issue.account == account) return tesSUCCESS; + + auto const trustLine = + view.read(keylet::line(account, issue.account, issue.currency)); + // If account has no line, and this is a strong check, fail + if (!trustLine && authType == AuthType::StrongAuth) + return tecNO_LINE; + + // If this is a weak or legacy check, or if the account has a line, fail if + // auth is required and not set on the line if (auto const issuerAccount = view.read(keylet::account(issue.account)); issuerAccount && (*issuerAccount)[sfFlags] & lsfRequireAuth) { - if (auto const trustLine = - view.read(keylet::line(account, issue.account, issue.currency))) + if (trustLine) return ((*trustLine)[sfFlags] & ((account > issue.account) ? lsfLowAuth : lsfHighAuth)) ? tesSUCCESS @@ -2384,7 +2396,7 @@ requireAuth( ReadView const& view, MPTIssue const& mptIssue, AccountID const& account, - MPTAuthType authType, + AuthType authType, int depth) { auto const mptID = keylet::mptIssuance(mptIssue.getMptID()); @@ -2419,7 +2431,7 @@ requireAuth( if (auto const err = std::visit( [&](TIss const& issue) { if constexpr (std::is_same_v) - return requireAuth(view, issue, account); + return requireAuth(view, issue, account, authType); else return requireAuth( view, issue, account, authType, depth + 1); @@ -2434,7 +2446,8 @@ requireAuth( auto const sleToken = view.read(mptokenID); // if account has no MPToken, fail - if (!sleToken && authType == MPTAuthType::StrongAuth) + if (!sleToken && + (authType == AuthType::StrongAuth || authType == AuthType::Legacy)) return tecNO_AUTH; // Note, this check is not amendment-gated because DomainID will be always From b34f59eafcdeb8768b22a1d877cb6de35e957160 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 15 Jul 2025 19:30:58 -0400 Subject: [PATCH 016/291] Start implementing LoanBrokerCoverWithdraw.Destination field - Refactor the IOU payment code out of the Payment transactor and use it for different destinations. This should enforce all of the trust line rules without having to reinvent a dozen wheels. - TODO: Same for MPTs. --- .../xrpl/protocol/detail/transactions.macro | 3 + src/test/app/LoanBroker_test.cpp | 19 ++- src/test/jtx/TestHelpers.h | 2 + .../app/tx/detail/LoanBrokerCoverWithdraw.cpp | 109 ++++++++++-- src/xrpld/app/tx/detail/Payment.cpp | 156 ++++++++++-------- src/xrpld/app/tx/detail/Payment.h | 19 +++ src/xrpld/app/tx/detail/VaultWithdraw.cpp | 19 ++- 7 files changed, 232 insertions(+), 95 deletions(-) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 32d4335c8b..73d9a56ab0 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -802,6 +802,7 @@ TRANSACTION(ttVAULT_WITHDRAW, 69, VaultWithdraw, {sfVaultID, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, {sfDestination, soeOPTIONAL}, + {sfDestinationTag, soeOPTIONAL}, })) /** This transaction claws back tokens from a vault. */ @@ -875,6 +876,8 @@ TRANSACTION(ttLOAN_BROKER_COVER_WITHDRAW, 77, LoanBrokerCoverWithdraw, noPriv, ({ {sfLoanBrokerID, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, + {sfDestination, soeOPTIONAL}, + {sfDestinationTag, soeOPTIONAL}, })) /** This transaction creates a Loan */ diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index d52fc759a9..e06581199b 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -107,6 +107,7 @@ class LoanBroker_test : public beast::unit_test::suite jtx::Env& env, jtx::Account const& alice, jtx::Account const& evan, + jtx::Account const& bystander, VaultInfo const& vault, std::function modifyJTx, std::function checkBroker, @@ -240,6 +241,11 @@ class LoanBroker_test : public beast::unit_test::suite env(coverWithdraw(alice, keylet.key, vault.asset(900)), ter(tecINSUFFICIENT_FUNDS)); + env(coverWithdraw(alice, keylet.key, vault.asset(1)), + destination(bystander), + ter(tecNO_LINE)); + verifyCoverAmount(10); + // Withdraw some of the cover amount env(coverWithdraw(alice, keylet.key, vault.asset(7))); if (BEAST_EXPECT(broker = env.le(keylet))) @@ -254,8 +260,10 @@ class LoanBroker_test : public beast::unit_test::suite verifyCoverAmount(8); } - // Withdraw some more - env(coverWithdraw(alice, keylet.key, vault.asset(2))); + // Withdraw some more. Send it to Evan. Very generous, considering + // how much trouble he's been. + env(coverWithdraw(alice, keylet.key, vault.asset(2)), + destination(evan)); if (BEAST_EXPECT(broker = env.le(keylet))) { verifyCoverAmount(6); @@ -348,11 +356,14 @@ class LoanBroker_test : public beast::unit_test::suite Account alice{"alice"}; // Evan will attempt to be naughty Account evan{"evan"}; + // Bystander doesn't have anything to do with the SAV or Broker, or any + // of the relevant tokens + Account bystander{"bystander"}; Vault vault{env}; // Fund the accounts and trust lines with the same amount so that tests // can use the same values regardless of the asset. - env.fund(XRP(100'000), issuer, noripple(alice, evan)); + env.fund(XRP(100'000), issuer, noripple(alice, evan, bystander)); env.close(); // Create assets @@ -459,6 +470,7 @@ class LoanBroker_test : public beast::unit_test::suite env, alice, evan, + bystander, vault, // No modifications {}, @@ -543,6 +555,7 @@ class LoanBroker_test : public beast::unit_test::suite env, alice, evan, + bystander, vault, [&](jtx::JTx const& jv) { testData = "spam spam spam spam"; diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index 85a46ad76a..4240a1e387 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -752,6 +752,8 @@ auto const coverRateMinimum = auto const coverRateLiquidation = valueUnitWrapper(sfCoverRateLiquidation); +auto const destination = JTxFieldWrapper(sfDestination); + } // namespace loanBroker /* Loan */ diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp index d1705082d6..ed30799b93 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp @@ -20,6 +20,7 @@ #include // #include +#include #include #include @@ -57,6 +58,14 @@ LoanBrokerCoverWithdraw::preflight(PreflightContext const& ctx) if (ctx.tx[sfAmount] <= beast::zero) return temBAD_AMOUNT; + if (auto const destination = ctx.tx[~sfDestination]; + destination && *destination == beast::zero) + { + JLOG(ctx.j.debug()) + << "LoanBrokerCoverWithdraw: zero/empty destination account."; + return temMALFORMED; + } + return tesSUCCESS; } @@ -69,6 +78,12 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) auto const brokerID = tx[sfLoanBrokerID]; auto const amount = tx[sfAmount]; + auto const dstAcct = [&]() -> AccountID { + if (auto const dst = tx[~sfDestination]) + return *dst; + return account; + }(); + auto const sleBroker = ctx.view.read(keylet::loanbroker(brokerID)); if (!sleBroker) { @@ -86,15 +101,45 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) if (amount.asset() != vaultAsset) return tecWRONG_ASSET; - // Cannot transfer a frozen Asset + // Withdrawal to a 3rd party destination account is essentially a transfer. + // Enforce all the usual asset transfer checks. + AuthType authType = AuthType::Legacy; + if (account != dstAcct) + { + auto const sleDst = ctx.view.read(keylet::account(dstAcct)); + if (sleDst == nullptr) + return tecNO_DST; + + if (sleDst->isFlag(lsfRequireDestTag) && + !tx.isFieldPresent(sfDestinationTag)) + return tecDST_TAG_NEEDED; // Cannot send without a tag + + if (sleDst->isFlag(lsfDepositAuth)) + { + if (!ctx.view.exists(keylet::depositPreauth(dstAcct, account))) + return tecNO_PERMISSION; + } + // The destination account must have consented to receive the asset by + // creating a RippleState or MPToken + authType = AuthType::StrongAuth; + } + + // Destination MPToken must exist (if asset is an MPT) + if (auto const ter = requireAuth(ctx.view, vaultAsset, dstAcct)) + return ter; + + // The broker's pseudo-account is the source of funds. auto const pseudoAccountID = sleBroker->at(sfAccount); - // Cannot transfer a frozen Asset + // Cannot send a frozen Asset if (auto const ret = checkFrozen(ctx.view, pseudoAccountID, vaultAsset)) return ret; - // Account cannot receive if asset is deep frozen - if (auto const ret = checkDeepFrozen(ctx.view, account, vaultAsset)) - return ret; + if (dstAcct != vaultAsset.getIssuer()) + { + // Destination account cannot receive if asset is deep frozen + if (auto const ret = checkDeepFrozen(ctx.view, dstAcct, vaultAsset)) + return ret; + } auto const coverAvail = sleBroker->at(sfCoverAvailable); // Cover Rate is in 1/10 bips units @@ -128,28 +173,56 @@ LoanBrokerCoverWithdraw::doApply() auto const brokerID = tx[sfLoanBrokerID]; auto const amount = tx[sfAmount]; + auto const dstAcct = [&]() -> AccountID { + if (auto const dst = tx[~sfDestination]) + return *dst; + return account_; + }(); auto broker = view().peek(keylet::loanbroker(brokerID)); if (!broker) - return tefBAD_LEDGER; // LCOV_EXCL_LINE + return tecINTERNAL; // LCOV_EXCL_LINE auto const brokerPseudoID = broker->at(sfAccount); - // Transfer assets from pseudo-account to depositor. - if (auto ter = accountSend( - view(), - brokerPseudoID, - account_, - amount, - j_, - WaiveTransferFee::Yes)) - return ter; - - // Increase the LoanBroker's CoverAvailable by Amount + // Decrease the LoanBroker's CoverAvailable by Amount broker->at(sfCoverAvailable) -= amount; view().update(broker); - return tesSUCCESS; + if (dstAcct != account_ && !amount.native() && !amount.holds()) + { + STAmount const maxSourceAmount( + Issue{amount.get().currency, brokerPseudoID}, + amount.mantissa(), + amount.exponent(), + amount < beast::zero); + SLE::pointer sleDst = view().peek(keylet::account(dstAcct)); + + auto ret = Payment::makeRipplePayment(Payment::RipplePaymentParams{ + .ctx = ctx_, + .maxSourceAmount = maxSourceAmount, + .srcAccountID = account_, + .dstAccountID = dstAcct, + .sleDst = sleDst, + .dstAmount = amount, + .deliverMin = std::nullopt, + .j = j_}); + + // Always claim a fee + if (!isTesSuccess(ret) && !isTecClaim(ret)) + { + JLOG(j_.info()) + << "LoanBrokerCoverWithdraw: changing result from " + << transToken(ret) + << " to tecPATH_DRY for IOU payment with Destination"; + ret = tecPATH_DRY; + } + return ret; + } + + // Transfer assets from pseudo-account to depositor. + return accountSend( + view(), brokerPseudoID, dstAcct, amount, j_, WaiveTransferFee::Yes); } //------------------------------------------------------------------------------ diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index da5e185770..bd20ba36ed 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -447,73 +447,18 @@ Payment::doApply() if (ripple) { - // Ripple payment with at least one intermediate step and uses - // transitive balances. - - if (depositPreauth && depositAuth) - { - // If depositPreauth is enabled, then an account that requires - // authorization has two ways to get an IOU Payment in: - // 1. If Account == Destination, or - // 2. If Account is deposit preauthorized by destination. - - if (auto err = verifyDepositPreauth( - ctx_.tx, - ctx_.view(), - account_, - dstAccountID, - sleDst, - ctx_.journal); - !isTesSuccess(err)) - return err; - } - - path::RippleCalc::Input rcInput; - rcInput.partialPaymentAllowed = partialPaymentAllowed; - rcInput.defaultPathsAllowed = defaultPathsAllowed; - rcInput.limitQuality = limitQuality; - rcInput.isLedgerOpen = view().open(); - - path::RippleCalc::Output rc; - { - PaymentSandbox pv(&view()); - JLOG(j_.debug()) << "Entering RippleCalc in payment: " - << ctx_.tx.getTransactionID(); - rc = path::RippleCalc::rippleCalculate( - pv, - maxSourceAmount, - dstAmount, - dstAccountID, - account_, - ctx_.tx.getFieldPathSet(sfPaths), - ctx_.tx[~sfDomainID], - ctx_.app.logs(), - &rcInput); - // VFALCO NOTE We might not need to apply, depending - // on the TER. But always applying *should* - // be safe. - pv.apply(ctx_.rawView()); - } - - // TODO: is this right? If the amount is the correct amount, was - // the delivered amount previously set? - if (rc.result() == tesSUCCESS && rc.actualAmountOut != dstAmount) - { - if (deliverMin && rc.actualAmountOut < *deliverMin) - rc.setResult(tecPATH_PARTIAL); - else - ctx_.deliver(rc.actualAmountOut); - } - - auto terResult = rc.result(); - - // Because of its overhead, if RippleCalc - // fails with a retry code, claim a fee - // instead. Maybe the user will be more - // careful with their path spec next time. - if (isTerRetry(terResult)) - terResult = tecPATH_DRY; - return terResult; + return makeRipplePayment(RipplePaymentParams{ + .ctx = ctx_, + .maxSourceAmount = maxSourceAmount, + .srcAccountID = account_, + .dstAccountID = dstAccountID, + .sleDst = sleDst, + .dstAmount = dstAmount, + .deliverMin = deliverMin, + .partialPaymentAllowed = partialPaymentAllowed, + .defaultPathsAllowed = defaultPathsAllowed, + .limitQuality = limitQuality, + .j = j_}); } else if (mptDirect) { @@ -685,4 +630,81 @@ Payment::doApply() return tesSUCCESS; } +// Reusable helpers +TER +Payment::makeRipplePayment(Payment::RipplePaymentParams const& p) +{ + // Ripple payment with at least one intermediate step and uses + // transitive balances. + + bool const depositPreauth = + p.ctx.view().rules().enabled(featureDepositPreauth); + bool const depositAuth = p.ctx.view().rules().enabled(featureDepositAuth); + + if (depositPreauth && depositAuth) + { + // If depositPreauth is enabled, then an account that requires + // authorization has two ways to get an IOU Payment in: + // 1. If Account == Destination, or + // 2. If Account is deposit preauthorized by destination. + + if (auto err = verifyDepositPreauth( + p.ctx.tx, + p.ctx.view(), + p.srcAccountID, + p.dstAccountID, + p.sleDst, + p.ctx.journal); + !isTesSuccess(err)) + return err; + } + + path::RippleCalc::Input rcInput; + rcInput.partialPaymentAllowed = p.partialPaymentAllowed; + rcInput.defaultPathsAllowed = p.defaultPathsAllowed; + rcInput.limitQuality = p.limitQuality; + rcInput.isLedgerOpen = p.ctx.view().open(); + + path::RippleCalc::Output rc; + { + PaymentSandbox pv(&p.ctx.view()); + JLOG(p.j.debug()) << "Entering RippleCalc in payment: " + << p.ctx.tx.getTransactionID(); + rc = path::RippleCalc::rippleCalculate( + pv, + p.maxSourceAmount, + p.dstAmount, + p.dstAccountID, + p.srcAccountID, + p.ctx.tx.getFieldPathSet(sfPaths), + p.ctx.tx[~sfDomainID], + p.ctx.app.logs(), + &rcInput); + // VFALCO NOTE We might not need to apply, depending + // on the TER. But always applying *should* + // be safe. + pv.apply(p.ctx.rawView()); + } + + // TODO: is this right? If the amount is the correct + // amount, was the delivered amount previously set? + if (rc.result() == tesSUCCESS && rc.actualAmountOut != p.dstAmount) + { + if (p.deliverMin && rc.actualAmountOut < *p.deliverMin) + rc.setResult(tecPATH_PARTIAL); + else + p.ctx.deliver(rc.actualAmountOut); + } + + auto terResult = rc.result(); + + // Because of its overhead, if RippleCalc + // fails with a retry code, claim a fee + // instead. Maybe the user will be more + // careful with their path spec next time. + if (isTerRetry(terResult)) + terResult = tecPATH_DRY; + return terResult; +} + } // namespace ripple diff --git a/src/xrpld/app/tx/detail/Payment.h b/src/xrpld/app/tx/detail/Payment.h index 3f16dd97bc..f201f3f9be 100644 --- a/src/xrpld/app/tx/detail/Payment.h +++ b/src/xrpld/app/tx/detail/Payment.h @@ -59,6 +59,25 @@ public: TER doApply() override; + + // Helpers for this class and other transactors that make "Payments" + struct RipplePaymentParams + { + ApplyContext& ctx; + STAmount const& maxSourceAmount; + AccountID const& srcAccountID; + AccountID const& dstAccountID; + SLE::pointer sleDst; + STAmount const& dstAmount; + std::optional const& deliverMin; + bool partialPaymentAllowed = false; + bool defaultPathsAllowed = true; + bool limitQuality = false; + beast::Journal j; + }; + + static TER + makeRipplePayment(RipplePaymentParams const& p); }; } // namespace ripple diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.cpp b/src/xrpld/app/tx/detail/VaultWithdraw.cpp index f628c0aa32..decc82407b 100644 --- a/src/xrpld/app/tx/detail/VaultWithdraw.cpp +++ b/src/xrpld/app/tx/detail/VaultWithdraw.cpp @@ -120,33 +120,38 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx) // Withdrawal to a 3rd party destination account is essentially a transfer, // via shares in the vault. Enforce all the usual asset transfer checks. + AuthType authType = AuthType::Legacy; if (account != dstAcct) { auto const sleDst = ctx.view.read(keylet::account(dstAcct)); if (sleDst == nullptr) return tecNO_DST; - if (sleDst->getFlags() & lsfRequireDestTag) + if (sleDst->isFlag(lsfRequireDestTag) && + !ctx.tx.isFieldPresent(sfDestinationTag)) return tecDST_TAG_NEEDED; // Cannot send without a tag - if (sleDst->getFlags() & lsfDepositAuth) + if (sleDst->isFlag(lsfDepositAuth)) { if (!ctx.view.exists(keylet::depositPreauth(dstAcct, account))) return tecNO_PERMISSION; } + // The destination account must have consented to receive the asset by + // creating a RippleState or MPToken + authType = AuthType::StrongAuth; } // Destination MPToken must exist (if asset is an MPT) - if (auto const ter = requireAuth(ctx.view, vaultAsset, dstAcct); + if (auto const ter = requireAuth(ctx.view, vaultAsset, dstAcct, authType); !isTesSuccess(ter)) return ter; // Cannot withdraw from a Vault an Asset frozen for the destination account - if (isFrozen(ctx.view, dstAcct, vaultAsset)) - return vaultAsset.holds() ? tecFROZEN : tecLOCKED; + if (auto const ret = checkFrozen(ctx.view, dstAcct, vaultAsset)) + return ret; - if (isFrozen(ctx.view, account, vaultShare)) - return tecLOCKED; + if (auto const ret = checkFrozen(ctx.view, account, vaultShare)) + return ret; return tesSUCCESS; } From 10b73525be4ca5a11dd0b7d5d00fa9e189af0483 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 16 Jul 2025 12:55:23 -0400 Subject: [PATCH 017/291] fixup! Start implementing LoanBrokerCoverWithdraw.Destination field --- src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp index ed30799b93..3dff73ebd5 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp @@ -125,7 +125,7 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) } // Destination MPToken must exist (if asset is an MPT) - if (auto const ter = requireAuth(ctx.view, vaultAsset, dstAcct)) + if (auto const ter = requireAuth(ctx.view, vaultAsset, dstAcct, authType)) return ter; // The broker's pseudo-account is the source of funds. From b83af2d54aac5a515513238b67f32bee038b1f00 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 16 Jul 2025 13:43:21 -0400 Subject: [PATCH 018/291] Fix formatting --- src/test/app/Vault_test.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 22814b3086..31fe2961ff 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -943,14 +943,13 @@ class Vault_test : public beast::unit_test::suite { using namespace test::jtx; - auto testCase = [this]( - std::function test) { + auto testCase = [this](std::function test) { Env env{*this, testable_amendments() | featureSingleAssetVault}; Account issuer{"issuer"}; Account owner{"owner"}; From 4d7492a00df6c013250ae76190a90bee91f89ce7 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 16 Jul 2025 14:03:50 -0400 Subject: [PATCH 019/291] Use new "testable_amendments" function in new tests --- src/test/app/LoanBroker_test.cpp | 2 +- src/test/app/Loan_test.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index e06581199b..08adc5d3fa 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -43,7 +43,7 @@ class LoanBroker_test : public beast::unit_test::suite // Ensure that all the features needed for Lending Protocol are included, // even if they are set to unsupported. FeatureBitset const all{ - jtx::supported_amendments() | featureMPTokensV1 | + jtx::testable_amendments() | featureMPTokensV1 | featureSingleAssetVault | featureLendingProtocol}; void diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 8212a8f617..2abe333c46 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -46,7 +46,7 @@ class Loan_test : public beast::unit_test::suite // Ensure that all the features needed for Lending Protocol are included, // even if they are set to unsupported. FeatureBitset const all{ - jtx::supported_amendments() | featureMPTokensV1 | + jtx::testable_amendments() | featureMPTokensV1 | featureSingleAssetVault | featureLendingProtocol}; static constexpr auto const coverDepositParameter = 1000; From 45abab3a78a1f0df8c947c45eb8855a119b94874 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 16 Jul 2025 15:29:59 -0400 Subject: [PATCH 020/291] Add missing headers --- src/test/app/LoanBroker_test.cpp | 3 +++ src/test/app/Loan_test.cpp | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 08adc5d3fa..69908c4b52 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -20,7 +20,10 @@ #include #include #include +#include #include +#include +#include #include #include diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 2abe333c46..497329e66d 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -20,7 +20,12 @@ #include #include #include +#include #include +#include +#include +#include +#include #include #include From 6b4236343aaa66ccfd8049447947d50ab60ee1c7 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 16 Jul 2025 17:41:23 -0400 Subject: [PATCH 021/291] Get CoverWithdraw IOU payments working - Clean up some of the payment parameters - Also factor out Payment::makeMPTDirectPayment for future use --- src/test/app/LoanBroker_test.cpp | 13 +- .../app/tx/detail/LoanBrokerCoverWithdraw.cpp | 69 ++++--- src/xrpld/app/tx/detail/Payment.cpp | 174 ++++++++++-------- src/xrpld/app/tx/detail/Payment.h | 12 ++ 4 files changed, 162 insertions(+), 106 deletions(-) diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 69908c4b52..eb133628ef 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -244,9 +244,16 @@ class LoanBroker_test : public beast::unit_test::suite env(coverWithdraw(alice, keylet.key, vault.asset(900)), ter(tecINSUFFICIENT_FUNDS)); - env(coverWithdraw(alice, keylet.key, vault.asset(1)), - destination(bystander), - ter(tecNO_LINE)); + // Skip this test for XRP, because that can always be sent + if (!vault.asset.raw().native()) + { + TER const expected = vault.asset.raw().holds() + ? tecNO_AUTH + : tecNO_LINE; + env(coverWithdraw(alice, keylet.key, vault.asset(1)), + destination(bystander), + ter(expected)); + } verifyCoverAmount(10); // Withdraw some of the cover amount diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp index 3dff73ebd5..9b155b32c2 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp @@ -55,7 +55,11 @@ LoanBrokerCoverWithdraw::preflight(PreflightContext const& ctx) if (ctx.tx[sfLoanBrokerID] == beast::zero) return temINVALID; - if (ctx.tx[sfAmount] <= beast::zero) + auto const dstAmount = ctx.tx[sfAmount]; + if (dstAmount <= beast::zero) + return temBAD_AMOUNT; + + if (!isLegalNet(dstAmount)) return temBAD_AMOUNT; if (auto const destination = ctx.tx[~sfDestination]; @@ -183,41 +187,54 @@ LoanBrokerCoverWithdraw::doApply() if (!broker) return tecINTERNAL; // LCOV_EXCL_LINE - auto const brokerPseudoID = broker->at(sfAccount); + auto const brokerPseudoID = *broker->at(sfAccount); // Decrease the LoanBroker's CoverAvailable by Amount broker->at(sfCoverAvailable) -= amount; view().update(broker); - if (dstAcct != account_ && !amount.native() && !amount.holds()) + // Move the funds from the broker's pseudo-account to the dstAcct + + if (dstAcct != account_ && !amount.native()) { - STAmount const maxSourceAmount( - Issue{amount.get().currency, brokerPseudoID}, - amount.mantissa(), - amount.exponent(), - amount < beast::zero); + bool const mptDirect = amount.holds(); + STAmount const maxSourceAmount = + Payment::getMaxSourceAmount(brokerPseudoID, amount); SLE::pointer sleDst = view().peek(keylet::account(dstAcct)); + if (!sleDst) + return tecINTERNAL; - auto ret = Payment::makeRipplePayment(Payment::RipplePaymentParams{ - .ctx = ctx_, - .maxSourceAmount = maxSourceAmount, - .srcAccountID = account_, - .dstAccountID = dstAcct, - .sleDst = sleDst, - .dstAmount = amount, - .deliverMin = std::nullopt, - .j = j_}); - - // Always claim a fee - if (!isTesSuccess(ret) && !isTecClaim(ret)) + if (!mptDirect) { - JLOG(j_.info()) - << "LoanBrokerCoverWithdraw: changing result from " - << transToken(ret) - << " to tecPATH_DRY for IOU payment with Destination"; - ret = tecPATH_DRY; + // If sending the Cover to a different account, then this is + // effectively a payment. Use the Payment transaction code to call + // the payment engine, though only a subset of the functionality is + // supported in this transaction. e.g. No paths, no partial + // payments. + + auto const ret = + Payment::makeRipplePayment(Payment::RipplePaymentParams{ + .ctx = ctx_, + .maxSourceAmount = maxSourceAmount, + .srcAccountID = brokerPseudoID, + .dstAccountID = dstAcct, + .sleDst = sleDst, + .dstAmount = amount, + .paths = STPathSet{}, + .deliverMin = std::nullopt, + .j = j_}); + + // Always claim a fee + if (!isTesSuccess(ret) && !isTecClaim(ret)) + { + JLOG(j_.info()) + << "LoanBrokerCoverWithdraw: changing result from " + << transToken(ret) + << " to tecPATH_DRY for IOU payment with Destination"; + return tecPATH_DRY; + } + return ret; } - return ret; } // Transfer assets from pseudo-account to depositor. diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index bd20ba36ed..a92fdc6c04 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -48,8 +48,8 @@ Payment::makeTxConsequences(PreflightContext const& ctx) } STAmount -getMaxSourceAmount( - AccountID const& account, +Payment::getMaxSourceAmount( + AccountID const& senderAccount, STAmount const& dstAmount, std::optional const& sendMax) { @@ -59,7 +59,7 @@ getMaxSourceAmount( return dstAmount; else return STAmount( - Issue{dstAmount.get().currency, account}, + Issue{dstAmount.get().currency, senderAccount}, dstAmount.mantissa(), dstAmount.exponent(), dstAmount < beast::zero); @@ -454,6 +454,7 @@ Payment::doApply() .dstAccountID = dstAccountID, .sleDst = sleDst, .dstAmount = dstAmount, + .paths = ctx_.tx.getFieldPathSet(sfPaths), .deliverMin = deliverMin, .partialPaymentAllowed = partialPaymentAllowed, .defaultPathsAllowed = defaultPathsAllowed, @@ -462,79 +463,19 @@ Payment::doApply() } else if (mptDirect) { - JLOG(j_.trace()) << " dstAmount=" << dstAmount.getFullText(); - auto const& mptIssue = dstAmount.get(); - - if (auto const ter = requireAuth(view(), mptIssue, account_); - ter != tesSUCCESS) - return ter; - - if (auto const ter = requireAuth(view(), mptIssue, dstAccountID); - ter != tesSUCCESS) - return ter; - - if (auto const ter = - canTransfer(view(), mptIssue, account_, dstAccountID); - ter != tesSUCCESS) - return ter; - - if (auto err = verifyDepositPreauth( - ctx_.tx, - ctx_.view(), - account_, - dstAccountID, - sleDst, - ctx_.journal); - !isTesSuccess(err)) - return err; - - auto const& issuer = mptIssue.getIssuer(); - - // Transfer rate - Rate rate{QUALITY_ONE}; - // Payment between the holders - if (account_ != issuer && dstAccountID != issuer) - { - // If globally/individually locked then - // - can't send between holders - // - holder can send back to issuer - // - issuer can send to holder - if (isAnyFrozen(view(), {account_, dstAccountID}, mptIssue)) - return tecLOCKED; - - // Get the rate for a payment between the holders. - rate = transferRate(view(), mptIssue.getMptID()); - } - - // Amount to deliver. - STAmount amountDeliver = dstAmount; - // Factor in the transfer rate. - // No rounding. It'll change once MPT integrated into DEX. - STAmount requiredMaxSourceAmount = multiply(dstAmount, rate); - - // Send more than the account wants to pay or less than - // the account wants to deliver (if no SendMax). - // Adjust the amount to deliver. - if (partialPaymentAllowed && requiredMaxSourceAmount > maxSourceAmount) - { - requiredMaxSourceAmount = maxSourceAmount; - // No rounding. It'll change once MPT integrated into DEX. - amountDeliver = divide(maxSourceAmount, rate); - } - - if (requiredMaxSourceAmount > maxSourceAmount || - (deliverMin && amountDeliver < *deliverMin)) - return tecPATH_PARTIAL; - - PaymentSandbox pv(&view()); - auto res = accountSend( - pv, account_, dstAccountID, amountDeliver, ctx_.journal); - if (res == tesSUCCESS) - pv.apply(ctx_.rawView()); - else if (res == tecINSUFFICIENT_FUNDS || res == tecPATH_DRY) - res = tecPATH_PARTIAL; - - return res; + return makeMPTDirectPayment(RipplePaymentParams{ + .ctx = ctx_, + .maxSourceAmount = maxSourceAmount, + .srcAccountID = account_, + .dstAccountID = dstAccountID, + .sleDst = sleDst, + .dstAmount = dstAmount, + .paths = ctx_.tx.getFieldPathSet(sfPaths), + .deliverMin = deliverMin, + .partialPaymentAllowed = partialPaymentAllowed, + .defaultPathsAllowed = defaultPathsAllowed, + .limitQuality = limitQuality, + .j = j_}); } XRPL_ASSERT(dstAmount.native(), "ripple::Payment::doApply : amount is XRP"); @@ -676,7 +617,7 @@ Payment::makeRipplePayment(Payment::RipplePaymentParams const& p) p.dstAmount, p.dstAccountID, p.srcAccountID, - p.ctx.tx.getFieldPathSet(sfPaths), + p.paths, p.ctx.tx[~sfDomainID], p.ctx.app.logs(), &rcInput); @@ -707,4 +648,83 @@ Payment::makeRipplePayment(Payment::RipplePaymentParams const& p) return terResult; } +TER +Payment::makeMPTDirectPayment(Payment::RipplePaymentParams const& p) +{ + JLOG(p.j.trace()) << " p.dstAmount=" << p.dstAmount.getFullText(); + auto const& mptIssue = p.dstAmount.get(); + + if (auto const ter = requireAuth(p.ctx.view(), mptIssue, p.srcAccountID); + ter != tesSUCCESS) + return ter; + + if (auto const ter = requireAuth(p.ctx.view(), mptIssue, p.dstAccountID); + ter != tesSUCCESS) + return ter; + + if (auto const ter = + canTransfer(p.ctx.view(), mptIssue, p.srcAccountID, p.dstAccountID); + ter != tesSUCCESS) + return ter; + + if (auto err = verifyDepositPreauth( + p.ctx.tx, + p.ctx.view(), + p.srcAccountID, + p.dstAccountID, + p.sleDst, + p.ctx.journal); + !isTesSuccess(err)) + return err; + + auto const& issuer = mptIssue.getIssuer(); + + // Transfer rate + Rate rate{QUALITY_ONE}; + // Payment between the holders + if (p.srcAccountID != issuer && p.dstAccountID != issuer) + { + // If globally/individually locked then + // - can't send between holders + // - holder can send back to issuer + // - issuer can send to holder + if (isAnyFrozen( + p.ctx.view(), {p.srcAccountID, p.dstAccountID}, mptIssue)) + return tecLOCKED; + + // Get the rate for a payment between the holders. + rate = transferRate(p.ctx.view(), mptIssue.getMptID()); + } + + // Amount to deliver. + STAmount amountDeliver = p.dstAmount; + // Factor in the transfer rate. + // No rounding. It'll change once MPT integrated into DEX. + STAmount requiredMaxSourceAmount = multiply(p.dstAmount, rate); + + // Send more than the account wants to pay or less than + // the account wants to deliver (if no SendMax). + // Adjust the amount to deliver. + if (p.partialPaymentAllowed && requiredMaxSourceAmount > p.maxSourceAmount) + { + requiredMaxSourceAmount = p.maxSourceAmount; + // No rounding. It'll change once MPT integrated into DEX. + amountDeliver = divide(p.maxSourceAmount, rate); + } + + if (requiredMaxSourceAmount > p.maxSourceAmount || + (p.deliverMin && amountDeliver < *p.deliverMin)) + return tecPATH_PARTIAL; + + PaymentSandbox pv(&p.ctx.view()); + auto res = accountSend( + pv, p.srcAccountID, p.dstAccountID, amountDeliver, p.ctx.journal); + if (res == tesSUCCESS) + pv.apply(p.ctx.rawView()); + else if (res == tecINSUFFICIENT_FUNDS || res == tecPATH_DRY) + res = tecPATH_PARTIAL; + + return res; +} + } // namespace ripple diff --git a/src/xrpld/app/tx/detail/Payment.h b/src/xrpld/app/tx/detail/Payment.h index f201f3f9be..2a6751f88b 100644 --- a/src/xrpld/app/tx/detail/Payment.h +++ b/src/xrpld/app/tx/detail/Payment.h @@ -69,6 +69,9 @@ public: AccountID const& dstAccountID; SLE::pointer sleDst; STAmount const& dstAmount; + // Paths need to be explicitly included because other transactions don't + // have them defined + STPathSet const& paths; std::optional const& deliverMin; bool partialPaymentAllowed = false; bool defaultPathsAllowed = true; @@ -76,8 +79,17 @@ public: beast::Journal j; }; + static STAmount + getMaxSourceAmount( + AccountID const& senderAccount, + STAmount const& dstAmount, + std::optional const& sendMax = {}); + static TER makeRipplePayment(RipplePaymentParams const& p); + + static TER + makeMPTDirectPayment(RipplePaymentParams const& p); }; } // namespace ripple From e6e6360c1a1b2a09d8727de9e4ef98cdabb45e92 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 16 Jul 2025 18:31:10 -0400 Subject: [PATCH 022/291] Support MPT payments via Payment tx for LBCoverWithdraw.Destination --- .../app/tx/detail/LoanBrokerCoverWithdraw.cpp | 62 ++++++++++--------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp index 9b155b32c2..5ec059d685 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp @@ -204,37 +204,41 @@ LoanBrokerCoverWithdraw::doApply() if (!sleDst) return tecINTERNAL; - if (!mptDirect) + Payment::RipplePaymentParams paymentParams{ + .ctx = ctx_, + .maxSourceAmount = maxSourceAmount, + .srcAccountID = brokerPseudoID, + .dstAccountID = dstAcct, + .sleDst = sleDst, + .dstAmount = amount, + .paths = STPathSet{}, + .deliverMin = std::nullopt, + .j = j_}; + + // If sending the Cover to a different account, then this is + // effectively a payment. Use the Payment transaction code to call + // the payment engine, though only a subset of the functionality is + // supported in this transaction. e.g. No paths, no partial + // payments. + TER ret; + if (mptDirect) { - // If sending the Cover to a different account, then this is - // effectively a payment. Use the Payment transaction code to call - // the payment engine, though only a subset of the functionality is - // supported in this transaction. e.g. No paths, no partial - // payments. - - auto const ret = - Payment::makeRipplePayment(Payment::RipplePaymentParams{ - .ctx = ctx_, - .maxSourceAmount = maxSourceAmount, - .srcAccountID = brokerPseudoID, - .dstAccountID = dstAcct, - .sleDst = sleDst, - .dstAmount = amount, - .paths = STPathSet{}, - .deliverMin = std::nullopt, - .j = j_}); - - // Always claim a fee - if (!isTesSuccess(ret) && !isTecClaim(ret)) - { - JLOG(j_.info()) - << "LoanBrokerCoverWithdraw: changing result from " - << transToken(ret) - << " to tecPATH_DRY for IOU payment with Destination"; - return tecPATH_DRY; - } - return ret; + ret = Payment::makeMPTDirectPayment(paymentParams); } + else + { + ret = Payment::makeRipplePayment(paymentParams); + } + // Always claim a fee + if (!isTesSuccess(ret) && !isTecClaim(ret)) + { + JLOG(j_.info()) + << "LoanBrokerCoverWithdraw: changing result from " + << transToken(ret) + << " to tecPATH_DRY for IOU payment with Destination"; + return tecPATH_DRY; + } + return ret; } // Transfer assets from pseudo-account to depositor. From e4480569f72c4795a9852ecb7c9d9328b675b71c Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 16 Jul 2025 18:58:25 -0400 Subject: [PATCH 023/291] TEMP: Change VaultWithdraw back so tests pass --- src/xrpld/app/tx/detail/VaultWithdraw.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.cpp b/src/xrpld/app/tx/detail/VaultWithdraw.cpp index decc82407b..f5a4f302ec 100644 --- a/src/xrpld/app/tx/detail/VaultWithdraw.cpp +++ b/src/xrpld/app/tx/detail/VaultWithdraw.cpp @@ -138,7 +138,7 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx) } // The destination account must have consented to receive the asset by // creating a RippleState or MPToken - authType = AuthType::StrongAuth; + // authType = AuthType::StrongAuth; } // Destination MPToken must exist (if asset is an MPT) From c46d8941928595d230d8a8f92980621203e2f6fe Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 17 Jul 2025 20:01:20 -0400 Subject: [PATCH 024/291] Implement LoanBrokerCoverClawback and many test cases - Not all tests are passing yet --- .../xrpl/protocol/detail/transactions.macro | 22 +- src/test/app/LoanBroker_test.cpp | 223 ++++++++++-- src/test/jtx/TestHelpers.h | 29 ++ src/test/jtx/impl/TestHelpers.cpp | 10 + src/test/jtx/impl/mpt.cpp | 2 +- src/test/jtx/mpt.h | 2 +- .../app/tx/detail/LoanBrokerCoverClawback.cpp | 343 ++++++++++++++++++ .../app/tx/detail/LoanBrokerCoverClawback.h | 53 +++ 8 files changed, 648 insertions(+), 36 deletions(-) create mode 100644 src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp create mode 100644 src/xrpld/app/tx/detail/LoanBrokerCoverClawback.h diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 73d9a56ab0..968d4c0ede 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -880,11 +880,23 @@ TRANSACTION(ttLOAN_BROKER_COVER_WITHDRAW, 77, LoanBrokerCoverWithdraw, {sfDestinationTag, soeOPTIONAL}, })) +/** This transaction claws back First Loss Capital from a Loan Broker to + the issuer of the capital */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttLOAN_BROKER_COVER_CLAWBACK, 78, LoanBrokerCoverClawback, + Delegation::delegatable, + noPriv, ({ + {sfLoanBrokerID, soeOPTIONAL}, + {sfAmount, soeOPTIONAL, soeMPTSupported}, +})) + /** This transaction creates a Loan */ #if TRANSACTION_INCLUDE # include #endif -TRANSACTION(ttLOAN_SET, 78, LoanSet, +TRANSACTION(ttLOAN_SET, 80, LoanSet, Delegation::delegatable, noPriv, ({ {sfLoanBrokerID, soeREQUIRED}, @@ -911,7 +923,7 @@ TRANSACTION(ttLOAN_SET, 78, LoanSet, #if TRANSACTION_INCLUDE # include #endif -TRANSACTION(ttLOAN_DELETE, 79, LoanDelete, +TRANSACTION(ttLOAN_DELETE, 81, LoanDelete, Delegation::delegatable, noPriv, ({ {sfLoanID, soeREQUIRED}, @@ -921,7 +933,7 @@ TRANSACTION(ttLOAN_DELETE, 79, LoanDelete, #if TRANSACTION_INCLUDE # include #endif -TRANSACTION(ttLOAN_MANAGE, 80, LoanManage, +TRANSACTION(ttLOAN_MANAGE, 82, LoanManage, Delegation::delegatable, noPriv, ({ {sfLoanID, soeREQUIRED}, @@ -931,7 +943,7 @@ TRANSACTION(ttLOAN_MANAGE, 80, LoanManage, #if TRANSACTION_INCLUDE # include #endif -TRANSACTION(ttLOAN_DRAW, 81, LoanDraw, +TRANSACTION(ttLOAN_DRAW, 83, LoanDraw, Delegation::delegatable, noPriv, ({ {sfLoanID, soeREQUIRED}, @@ -942,7 +954,7 @@ TRANSACTION(ttLOAN_DRAW, 81, LoanDraw, #if TRANSACTION_INCLUDE # include #endif -TRANSACTION(ttLOAN_PAY, 82, LoanPay, +TRANSACTION(ttLOAN_PAY, 84, LoanPay, Delegation::delegatable, noPriv, ({ {sfLoanID, soeREQUIRED}, diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index eb133628ef..778b37b3a2 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -85,7 +85,17 @@ class LoanBroker_test : public beast::unit_test::suite // 2. LoanBrokerCoverWithdraw env(coverWithdraw(alice, brokerKeylet.key, asset(1000)), ter(temDISABLED)); - // 3. LoanBrokerDelete + // 3. LoanBrokerCoverClawback + env(coverClawback(alice), ter(temDISABLED)); + env(coverClawback(alice), + loanBrokerID(brokerKeylet.key), + ter(temDISABLED)); + env(coverClawback(alice), amount(asset(0)), ter(temDISABLED)); + env(coverClawback(alice), + loanBrokerID(brokerKeylet.key), + amount(asset(1000)), + ter(temDISABLED)); + // 4. LoanBrokerDelete env(del(alice, brokerKeylet.key), ter(temDISABLED)); }; failAll(all - featureMPTokensV1); @@ -98,8 +108,12 @@ class LoanBroker_test : public beast::unit_test::suite { jtx::PrettyAsset asset; uint256 vaultID; - VaultInfo(jtx::PrettyAsset const& asset_, uint256 const& vaultID_) - : asset(asset_), vaultID(vaultID_) + jtx::Account pseudoAccount; + VaultInfo( + jtx::PrettyAsset const& asset_, + uint256 const& vaultID_, + AccountID const& pseudo) + : asset(asset_), vaultID(vaultID_), pseudoAccount("vault", pseudo) { } }; @@ -108,16 +122,17 @@ class LoanBroker_test : public beast::unit_test::suite lifecycle( char const* label, jtx::Env& env, + jtx::Account const& issuer, jtx::Account const& alice, jtx::Account const& evan, jtx::Account const& bystander, VaultInfo const& vault, + VaultInfo const& badVault, std::function modifyJTx, std::function checkBroker, std::function changeBroker, std::function checkChangedBroker) { - auto const keylet = keylet::loanbroker(alice.id(), env.seq(alice)); { auto const& asset = vault.asset.raw(); testcase << "Lifecycle: " @@ -131,6 +146,31 @@ class LoanBroker_test : public beast::unit_test::suite using namespace jtx; using namespace loanBroker; + // Bogus assets to use in test cases + static PrettyAsset const badMptAsset = [&]() { + MPTTester badMptt{env, evan, mptInitNoFund}; + badMptt.create( + {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); + return badMptt["BAD"]; + }(); + static PrettyAsset const badIouAsset = evan["BAD"]; + static Account const nonExistent{"NonExistent"}; + static PrettyAsset const ghostIouAsset = nonExistent["GST"]; + PrettyAsset const vaultPseudoIouAsset = vault.pseudoAccount["PSD"]; + + auto const badKeylet = keylet::loanbroker(alice.id(), env.seq(alice)); + env(set(alice, badVault.vaultID)); + auto const badBrokerPseudo = [&]() { + if (auto const le = env.le(badKeylet); BEAST_EXPECT(le)) + { + return Account{"Bad Broker pseudo-account", le->at(sfAccount)}; + } + // Just to make the build work + return vault.pseudoAccount; + }(); + PrettyAsset const badBrokerPseudoIouAsset = badBrokerPseudo["WAT"]; + + auto const keylet = keylet::loanbroker(alice.id(), env.seq(alice)); { // Start with default values auto jtx = env.jt(set(alice, vault.vaultID)); @@ -166,6 +206,7 @@ class LoanBroker_test : public beast::unit_test::suite // Load the pseudo-account Account const pseudoAccount{ "Broker pseudo-account", broker->at(sfAccount)}; + auto const pseudoKeylet = keylet::account(pseudoAccount); if (auto const pseudo = env.le(pseudoKeylet); BEAST_EXPECT(pseudo)) { @@ -200,13 +241,16 @@ class LoanBroker_test : public beast::unit_test::suite } auto verifyCoverAmount = - [&env, &vault, &broker, &pseudoAccount, this](auto n) { + [&env, &vault, &pseudoAccount, &broker, &keylet, this](auto n) { using namespace jtx; - auto const amount = vault.asset(n); - BEAST_EXPECT( - broker->at(sfCoverAvailable) == amount.number()); - env.require(balance(pseudoAccount, amount)); + if (BEAST_EXPECT(broker = env.le(keylet))) + { + auto const amount = vault.asset(n); + BEAST_EXPECT( + broker->at(sfCoverAvailable) == amount.number()); + env.require(balance(pseudoAccount, amount)); + } }; // Test Cover funding before allowing alterations @@ -223,12 +267,80 @@ class LoanBroker_test : public beast::unit_test::suite verifyCoverAmount(0); + // Test cover clawback failure cases BEFORE depositing any cover + // Need one of brokerID or amount + env(coverClawback(alice), ter(temINVALID)); + env(coverClawback(alice), + loanBrokerID(uint256(0)), + ter(temINVALID)); + env(coverClawback(alice), amount(XRP(1000)), ter(temBAD_AMOUNT)); + env(coverClawback(alice), + amount(vault.asset(-10)), + ter(temBAD_AMOUNT)); + // Clawbacks with an MPT need to specify the broker ID + env(coverClawback(alice), amount(badMptAsset(1)), ter(temINVALID)); + env(coverClawback(evan), + loanBrokerID(vault.vaultID), + ter(tecNO_ENTRY)); + // Only the issuer can clawback + env(coverClawback(alice), + loanBrokerID(keylet.key), + ter(tecNO_PERMISSION)); + if (vault.asset.raw().native()) + { + // Can not clawback XRP under any circumstances + env(coverClawback(issuer), + loanBrokerID(keylet.key), + ter(tecNO_PERMISSION)); + } + else + { + if (vault.asset.raw().holds()) + { + // Clawbacks without a loanBrokerID need to specify an IOU + // with the broker's pseudo-account as the issuer + env(coverClawback(alice), + amount(ghostIouAsset(1)), + ter(tecNO_ENTRY)); + env(coverClawback(alice), + amount(badIouAsset(1)), + ter(tecOBJECT_NOT_FOUND)); + env(coverClawback(alice), + amount(vaultPseudoIouAsset(1)), + ter(tecNO_ENTRY)); + // If we specify a pseudo-account as the IOU amount, it + // needs to match the loan broker + env(coverClawback(alice), + loanBrokerID(keylet.key), + amount(badBrokerPseudoIouAsset(10)), + ter(tecWRONG_ASSET)); + PrettyAsset const brokerWrongCurrencyAsset = + pseudoAccount["WAT"]; + env(coverClawback(alice), + loanBrokerID(keylet.key), + amount(brokerWrongCurrencyAsset(10)), + ter(tecWRONG_ASSET)); + } + else + { + // Clawbacks with an MPT need to specify the broker ID, even + // if the asset is valid + BEAST_EXPECT(vault.asset.raw().holds()); + env(coverClawback(alice), + amount(vault.asset(10)), + ter(temINVALID)); + } + // Since no cover has been deposited, there's nothing to claw + // back + env(coverClawback(issuer), + loanBrokerID(keylet.key), + amount(vault.asset(10)), + ter(tecINSUFFICIENT_FUNDS)); + } + // Fund the cover deposit env(coverDeposit(alice, keylet.key, vault.asset(10))); - if (BEAST_EXPECT(broker = env.le(keylet))) - { - verifyCoverAmount(10); - } + verifyCoverAmount(10); // Test withdrawal failure cases env(coverWithdraw(alice, uint256(0), vault.asset(10)), @@ -258,29 +370,61 @@ class LoanBroker_test : public beast::unit_test::suite // Withdraw some of the cover amount env(coverWithdraw(alice, keylet.key, vault.asset(7))); - if (BEAST_EXPECT(broker = env.le(keylet))) - { - verifyCoverAmount(3); - } + verifyCoverAmount(3); // Add some more cover env(coverDeposit(alice, keylet.key, vault.asset(5))); - if (BEAST_EXPECT(broker = env.le(keylet))) - { - verifyCoverAmount(8); - } + verifyCoverAmount(8); // Withdraw some more. Send it to Evan. Very generous, considering // how much trouble he's been. env(coverWithdraw(alice, keylet.key, vault.asset(2)), destination(evan)); - if (BEAST_EXPECT(broker = env.le(keylet))) - { - verifyCoverAmount(6); - } + verifyCoverAmount(6); env.close(); + if (!vault.asset.raw().native()) + { + // Issuer claws back some of the cover + env(coverClawback(issuer), + loanBrokerID(keylet.key), + amount(vault.asset(2))); + verifyCoverAmount(4); + + // Deposit some back + env(coverDeposit(alice, keylet.key, vault.asset(5))); + verifyCoverAmount(9); + + // Issuer claws it all back in various different ways + for (auto const& jt : { + env.jt( + coverClawback(issuer), loanBrokerID(keylet.key)), + env.jt( + coverClawback(issuer), + loanBrokerID(keylet.key), + amount(vault.asset(0))), + env.jt( + coverClawback(issuer), + loanBrokerID(keylet.key), + amount(vault.asset(6))), + // amount will be truncated to what's available + env.jt( + coverClawback(issuer), + loanBrokerID(keylet.key), + amount(vault.asset(100))), + }) + { + // Issuer claws it all back + env(jt); + verifyCoverAmount(0); + + // Deposit some back + env(coverDeposit(alice, keylet.key, vault.asset(6))); + verifyCoverAmount(6); + } + } + // no-op env(set(alice, vault.vaultID), loanBrokerID(keylet.key)); @@ -335,6 +479,7 @@ class LoanBroker_test : public beast::unit_test::suite //} env(del(alice, keylet.key)); + env(del(alice, badKeylet.key)); env.close(); { broker = env.le(keylet); @@ -404,14 +549,27 @@ class LoanBroker_test : public beast::unit_test::suite auto [tx, keylet] = vault.create({.owner = alice, .asset = asset}); env(tx); env.close(); - BEAST_EXPECT(env.le(keylet)); - - vaults.emplace_back(asset, keylet.key); + if (auto const le = env.le(keylet); BEAST_EXPECT(env.le(keylet))) + { + vaults.emplace_back(asset, keylet.key, le->at(sfAccount)); + } env(vault.deposit( {.depositor = alice, .id = keylet.key, .amount = asset(50)})); env.close(); } + VaultInfo const badVault = [&]() -> VaultInfo { + auto [tx, keylet] = + vault.create({.owner = alice, .asset = iouAsset}); + env(tx); + env.close(); + if (auto const le = env.le(keylet); BEAST_EXPECT(env.le(keylet))) + { + return {iouAsset, keylet.key, le->at(sfAccount)}; + } + // This should never happen + return {iouAsset, keylet.key, evan.id()}; + }(); auto const aliceOriginalCount = env.ownerCount(alice); @@ -478,10 +636,12 @@ class LoanBroker_test : public beast::unit_test::suite lifecycle( "default fields", env, + issuer, alice, evan, bystander, vault, + badVault, // No modifications {}, [&](SLE::const_ref broker) { @@ -497,7 +657,7 @@ class LoanBroker_test : public beast::unit_test::suite BEAST_EXPECT(broker->at(sfCoverRateLiquidation) == 0); BEAST_EXPECT( - env.ownerCount(alice) == aliceOriginalCount + 2); + env.ownerCount(alice) == aliceOriginalCount + 4); }, [&](SLE::const_ref broker) { // Modifications @@ -563,10 +723,12 @@ class LoanBroker_test : public beast::unit_test::suite lifecycle( "non-default fields", env, + issuer, alice, evan, bystander, vault, + badVault, [&](jtx::JTx const& jv) { testData = "spam spam spam spam"; // Finally, create another Loan Broker with none of the @@ -610,6 +772,9 @@ public: { testDisabled(); testLifecycle(); + + // TODO: Write clawback failure tests with an issuer / MPT that doesn't + // have the right flags set. } }; diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index 4240a1e387..b7ce9287cb 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -177,6 +177,29 @@ public: } }; +struct stAmountField : public JTxField +{ + using SF = SF_AMOUNT; + using SV = STAmount; + using OV = Json::Value; + using base = JTxField; + +protected: + using base::value_; + +public: + explicit stAmountField(SF const& sfield, SV const& value) + : JTxField(sfield, value) + { + } + + OV + value() const override + { + return value_.getJson(JsonOptions::none); + } +}; + struct blobField : public JTxField { using SF = SF_VL; @@ -292,6 +315,8 @@ using simpleField = JTxFieldWrapper>; */ auto const data = JTxFieldWrapper(sfData); +auto const amount = JTxFieldWrapper(sfAmount); + // TODO We only need this long "requires" clause as polyfill, for C++20 // implementations which are missing header. Replace with // `std::ranges::range`, and accordingly use std::ranges::begin/end @@ -739,6 +764,10 @@ coverWithdraw( STAmount const& amount, std::uint32_t flags = 0); +// Must specify at least one of loanBrokerID or amount. +Json::Value +coverClawback(AccountID const& account, std::uint32_t flags = 0); + auto const loanBrokerID = JTxFieldWrapper(sfLoanBrokerID); auto const managementFeeRate = diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index add8cde25d..3b61604fc6 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -426,6 +426,16 @@ coverWithdraw( return jv; } +Json::Value +coverClawback(AccountID const& account, std::uint32_t flags) +{ + Json::Value jv; + jv[sfTransactionType] = jss::LoanBrokerCoverClawback; + jv[sfAccount] = to_string(account); + jv[sfFlags] = flags; + return jv; +} + } // namespace loanBroker /* Loan */ diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index d33432d316..4df538d0c2 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -413,7 +413,7 @@ MPTTester::getFlags(std::optional const& holder) const } MPT -MPTTester::operator[](std::string const& name) +MPTTester::operator[](std::string const& name) const { return MPT(name, issuanceID()); } diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index 64eaa452f5..3204bfa98b 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -214,7 +214,7 @@ public: getBalance(Account const& account) const; MPT - operator[](std::string const& name); + operator[](std::string const& name) const; private: using SLEP = std::shared_ptr; diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp new file mode 100644 index 0000000000..0f2914887d --- /dev/null +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp @@ -0,0 +1,343 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +// +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +bool +LoanBrokerCoverClawback::isEnabled(PreflightContext const& ctx) +{ + return lendingProtocolEnabled(ctx); +} + +NotTEC +LoanBrokerCoverClawback::preflight(PreflightContext const& ctx) +{ + auto const brokerID = ctx.tx[~sfLoanBrokerID]; + auto const amount = ctx.tx[~sfAmount]; + + if (!brokerID && !amount) + return temINVALID; + + if (brokerID && *brokerID == beast::zero) + return temINVALID; + + if (amount) + { + if (amount->native()) + return temBAD_AMOUNT; + + // Zero is OK, and indicates "take it all" (down to the minimum cover) + if (*amount < beast::zero) + return temBAD_AMOUNT; + + // This should be redundant + if (!isLegalNet(*amount)) + return temBAD_AMOUNT; // LCOV_EXCL_LINE + + if (!brokerID) + { + if (amount->holds()) + return temINVALID; + + auto const account = ctx.tx[sfAccount]; + auto const holder = amount->getIssuer(); + if (holder == account || holder == beast::zero) + return temINVALID; + } + } + + return tesSUCCESS; +} + +Expected +determineBrokerID(ReadView const& view, STTx const& tx) +{ + if (auto const brokerID = tx[~sfLoanBrokerID]) + return *brokerID; + + auto const dstAmount = tx[~sfAmount]; + if (!dstAmount || !dstAmount->holds()) + return Unexpected{tecINTERNAL}; + + auto const holder = dstAmount->getIssuer(); + auto const sle = view.read(keylet::account(holder)); + if (!sle) + return Unexpected{tecNO_ENTRY}; + + if (auto const brokerID = sle->at(~sfLoanBrokerID)) + return *brokerID; + + // Or tecWRONG_ASSET? + return Unexpected{tecOBJECT_NOT_FOUND}; +} + +Expected +determineAsset( + ReadView const& view, + AccountID const& account, + AccountID const& brokerPseudoAccountID, + STAmount const& amount) +{ + if (amount.holds()) + return amount.asset(); + + // An IOU has an issue, which could be either end of the trust line. + // This check only applies to IOUs + auto const holder = amount.getIssuer(); + + if (holder == account) + { + return amount.asset(); + } + else if (holder == brokerPseudoAccountID) + { + // We want the asset to match the vault asset, so use the account as the + // issuer + return Issue{amount.getCurrency(), account}; + } + else + return Unexpected(tecWRONG_ASSET); +} + +Expected +determineClawAmount( + SLE const& sleBroker, + Asset const& vaultAsset, + std::optional const& amount) +{ + auto const maxClawAmount = sleBroker[sfCoverAvailable] - + tenthBipsOfValue(sleBroker[sfDebtTotal], + TenthBips32(sleBroker[sfCoverRateMinimum])); + if (maxClawAmount <= beast::zero) + return Unexpected(tecINSUFFICIENT_FUNDS); + + // Use the vaultAsset here, because it will be the right type in all + // circumstances. The amount may be an IOU indicating the pseudo-account's + // asset, which is correct, but not what is needed here. + if (!amount || *amount == beast::zero) + return STAmount{vaultAsset, maxClawAmount}; + Number const magnitude{*amount}; + if (magnitude > maxClawAmount) + return STAmount{vaultAsset, maxClawAmount}; + return STAmount{vaultAsset, magnitude}; +} + +template +static TER +preclaimHelper( + PreclaimContext const& ctx, + SLE const& sleIssuer, + STAmount const& clawAmount); + +template <> +TER +preclaimHelper( + PreclaimContext const& ctx, + SLE const& sleIssuer, + STAmount const& clawAmount) +{ + // If AllowTrustLineClawback is not set or NoFreeze is set, return no + // permission + if (!(sleIssuer.isFlag(lsfAllowTrustLineClawback)) || + (sleIssuer.isFlag(lsfNoFreeze))) + return tecNO_PERMISSION; + + return tesSUCCESS; +} + +template <> +TER +preclaimHelper( + PreclaimContext const& ctx, + SLE const& sleIssuer, + STAmount const& clawAmount) +{ + auto const issuanceKey = + keylet::mptIssuance(clawAmount.get().getMptID()); + auto const sleIssuance = ctx.view.read(issuanceKey); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + if (!sleIssuance->isFlag(lsfMPTCanClawback) || + !sleIssuance->isFlag(lsfMPTCanLock)) + return tecNO_PERMISSION; + + // With all the checking already done, this should be impossible + if (sleIssuance->at(sfIssuer) != sleIssuer[sfAccount]) + return tecINTERNAL; // LCOV_EXCL_LINE + + return tesSUCCESS; +} + +TER +LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx) +{ + auto const& tx = ctx.tx; + + auto const account = tx[sfAccount]; + auto const findBrokerID = determineBrokerID(ctx.view, tx); + if (!findBrokerID) + return findBrokerID.error(); + auto const brokerID = *findBrokerID; + auto const amount = tx[~sfAmount]; + + auto const sleBroker = ctx.view.read(keylet::loanbroker(brokerID)); + if (!sleBroker) + { + JLOG(ctx.j.warn()) << "LoanBroker does not exist."; + return tecNO_ENTRY; + } + + auto const brokerPseudoAccountID = sleBroker->at(sfAccount); + + auto const vault = ctx.view.read(keylet::vault(sleBroker->at(sfVaultID))); + if (!vault) + return tecINTERNAL; + + auto const vaultAsset = vault->at(sfAsset); + + if (vaultAsset.native()) + { + JLOG(ctx.j.warn()) << "Cannot clawback native asset."; + return tecNO_PERMISSION; + } + + // Only the issuer of the vault asset can claw it back from the broker's + // cover funds. + if (vaultAsset.getIssuer() != account) + { + JLOG(ctx.j.warn()) << "Account is not the issuer of the vault asset."; + return tecNO_PERMISSION; + } + + if (amount) + { + auto const findAsset = + determineAsset(ctx.view, account, brokerPseudoAccountID, *amount); + if (!findAsset) + return findAsset.error(); + auto const txAsset = *findAsset; + if (txAsset != vaultAsset) + { + JLOG(ctx.j.warn()) << "Account is the correct issuer, but trying " + "to clawback the wrong asset from LoanBroker"; + return tecWRONG_ASSET; + } + } + + auto const findClawAmount = + determineClawAmount(*sleBroker, vaultAsset, amount); + if (!findClawAmount) + { + JLOG(ctx.j.warn()) << "LoanBroker cover is already at minimum."; + return findClawAmount.error(); + } + STAmount const clawAmount = *findClawAmount; + + // Explicitly check the balance of the trust line / MPT to make sure the + // balance is actually there. It should always match `stCoverAvailable`, so + // if there isn't, this is an internal error. + if (accountHolds( + ctx.view, + brokerPseudoAccountID, + vaultAsset, + fhIGNORE_FREEZE, + ahIGNORE_AUTH, + ctx.j) < clawAmount) + return tecINTERNAL; // tecINSUFFICIENT_FUNDS; LCOV_EXCL_LINE + + // Check if the vault asset issuer has the correct flags + auto const sleIssuer = + ctx.view.read(keylet::account(vaultAsset.getIssuer())); + return std::visit( + [&](T const&) { + return preclaimHelper(ctx, *sleIssuer, clawAmount); + }, + vaultAsset.value()); +} + +TER +LoanBrokerCoverClawback::doApply() +{ + auto const& tx = ctx_.tx; + auto const account = tx[sfAccount]; + auto const findBrokerID = determineBrokerID(view(), tx); + if (!findBrokerID) + return tecINTERNAL; // LCOV_EXCL_LINE + auto const brokerID = *findBrokerID; + auto const amount = tx[~sfAmount]; + + auto sleBroker = view().peek(keylet::loanbroker(brokerID)); + if (!sleBroker) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const brokerPseudoID = *sleBroker->at(sfAccount); + + auto const vault = view().read(keylet::vault(sleBroker->at(sfVaultID))); + if (!vault) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const vaultAsset = vault->at(sfAsset); + + auto const findClawAmount = + determineClawAmount(*sleBroker, vaultAsset, amount); + if (!findClawAmount) + return tecINTERNAL; // LCOV_EXCL_LINE + STAmount const clawAmount = *findClawAmount; + // Just for paranoia's sake + if (clawAmount.native()) + return tecINTERNAL; // LCOV_EXCL_LINE + + // Decrease the LoanBroker's CoverAvailable by Amount + sleBroker->at(sfCoverAvailable) -= clawAmount; + view().update(sleBroker); + + // Transfer assets from pseudo-account to depositor. + return accountSend( + view(), brokerPseudoID, account, clawAmount, j_, WaiveTransferFee::Yes); +} + +//------------------------------------------------------------------------------ + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.h b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.h new file mode 100644 index 0000000000..68133b5364 --- /dev/null +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.h @@ -0,0 +1,53 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_LOANBROKERCOVERCLAWBACK_H_INCLUDED +#define RIPPLE_TX_LOANBROKERCOVERCLAWBACK_H_INCLUDED + +#include + +namespace ripple { + +class LoanBrokerCoverClawback : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit LoanBrokerCoverClawback(ApplyContext& ctx) : Transactor(ctx) + { + } + + static bool + isEnabled(PreflightContext const& ctx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +//------------------------------------------------------------------------------ + +} // namespace ripple + +#endif From 03d253ae20702ad939cc7b29a87957fe847abdc3 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 18 Jul 2025 18:30:03 -0400 Subject: [PATCH 025/291] Fix LoanBrokerCoverClawback tests? --- src/test/app/LoanBroker_test.cpp | 67 ++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 778b37b3a2..478538e097 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -152,6 +152,7 @@ class LoanBroker_test : public beast::unit_test::suite badMptt.create( {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); return badMptt["BAD"]; + env.close(); }(); static PrettyAsset const badIouAsset = evan["BAD"]; static Account const nonExistent{"NonExistent"}; @@ -160,6 +161,7 @@ class LoanBroker_test : public beast::unit_test::suite auto const badKeylet = keylet::loanbroker(alice.id(), env.seq(alice)); env(set(alice, badVault.vaultID)); + env.close(); auto const badBrokerPseudo = [&]() { if (auto const le = env.le(badKeylet); BEAST_EXPECT(le)) { @@ -305,18 +307,19 @@ class LoanBroker_test : public beast::unit_test::suite env(coverClawback(alice), amount(badIouAsset(1)), ter(tecOBJECT_NOT_FOUND)); + // Pseudo-account is not for a broker env(coverClawback(alice), amount(vaultPseudoIouAsset(1)), - ter(tecNO_ENTRY)); + ter(tecOBJECT_NOT_FOUND)); // If we specify a pseudo-account as the IOU amount, it // needs to match the loan broker - env(coverClawback(alice), + env(coverClawback(issuer), loanBrokerID(keylet.key), amount(badBrokerPseudoIouAsset(10)), ter(tecWRONG_ASSET)); PrettyAsset const brokerWrongCurrencyAsset = pseudoAccount["WAT"]; - env(coverClawback(alice), + env(coverClawback(issuer), loanBrokerID(keylet.key), amount(brokerWrongCurrencyAsset(10)), ter(tecWRONG_ASSET)); @@ -337,9 +340,11 @@ class LoanBroker_test : public beast::unit_test::suite amount(vault.asset(10)), ter(tecINSUFFICIENT_FUNDS)); } + env.close(); // Fund the cover deposit env(coverDeposit(alice, keylet.key, vault.asset(10))); + env.close(); verifyCoverAmount(10); // Test withdrawal failure cases @@ -370,19 +375,20 @@ class LoanBroker_test : public beast::unit_test::suite // Withdraw some of the cover amount env(coverWithdraw(alice, keylet.key, vault.asset(7))); + env.close(); verifyCoverAmount(3); // Add some more cover env(coverDeposit(alice, keylet.key, vault.asset(5))); + env.close(); verifyCoverAmount(8); // Withdraw some more. Send it to Evan. Very generous, considering // how much trouble he's been. env(coverWithdraw(alice, keylet.key, vault.asset(2)), destination(evan)); - verifyCoverAmount(6); - env.close(); + verifyCoverAmount(6); if (!vault.asset.raw().native()) { @@ -390,43 +396,62 @@ class LoanBroker_test : public beast::unit_test::suite env(coverClawback(issuer), loanBrokerID(keylet.key), amount(vault.asset(2))); + env.close(); verifyCoverAmount(4); // Deposit some back env(coverDeposit(alice, keylet.key, vault.asset(5))); + env.close(); verifyCoverAmount(9); // Issuer claws it all back in various different ways - for (auto const& jt : { - env.jt( - coverClawback(issuer), loanBrokerID(keylet.key)), - env.jt( + for (auto const& tx : { + // defer autofills until submission time + env.json( coverClawback(issuer), loanBrokerID(keylet.key), - amount(vault.asset(0))), - env.jt( + fee(none), + seq(none), + sig(none)), + env.json( coverClawback(issuer), loanBrokerID(keylet.key), - amount(vault.asset(6))), + amount(vault.asset(0)), + fee(none), + seq(none), + sig(none)), + env.json( + coverClawback(issuer), + loanBrokerID(keylet.key), + amount(vault.asset(6)), + fee(none), + seq(none), + sig(none)), // amount will be truncated to what's available - env.jt( + env.json( coverClawback(issuer), loanBrokerID(keylet.key), - amount(vault.asset(100))), + amount(vault.asset(100)), + fee(none), + seq(none), + sig(none)), }) { // Issuer claws it all back - env(jt); + env(tx); + env.close(); verifyCoverAmount(0); // Deposit some back env(coverDeposit(alice, keylet.key, vault.asset(6))); + env.close(); verifyCoverAmount(6); } } // no-op env(set(alice, vault.vaultID), loanBrokerID(keylet.key)); + env.close(); // Make modifications to the broker if (changeBroker) @@ -445,6 +470,7 @@ class LoanBroker_test : public beast::unit_test::suite loanBrokerID(broker->key()), debtMaximum(Number(0)), data("")); + env.close(); // Check the updated fields if (BEAST_EXPECT(broker = env.le(keylet))) @@ -459,6 +485,10 @@ class LoanBroker_test : public beast::unit_test::suite // evan tries to delete the broker env(del(evan, keylet.key), ter(tecNO_PERMISSION)); + // Get the "bad" broker out of the way + env(del(alice, badKeylet.key)); + env.close(); + // Note alice's balance of the asset and the broker account's cover // funds auto const aliceBalance = env.balance(alice, vault.asset); @@ -479,7 +509,6 @@ class LoanBroker_test : public beast::unit_test::suite //} env(del(alice, keylet.key)); - env(del(alice, badKeylet.key)); env.close(); { broker = env.le(keylet); @@ -521,11 +550,15 @@ class LoanBroker_test : public beast::unit_test::suite env.fund(XRP(100'000), issuer, noripple(alice, evan, bystander)); env.close(); + env(fset(issuer, asfAllowTrustLineClawback)); + env.close(); + // Create assets PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; PrettyAsset const iouAsset = issuer["IOU"]; env(trust(alice, iouAsset(1'000'000))); env(trust(evan, iouAsset(1'000'000))); + env.close(); env(pay(issuer, evan, iouAsset(100'000))); env(pay(issuer, alice, iouAsset(100'000))); env.close(); @@ -533,9 +566,11 @@ class LoanBroker_test : public beast::unit_test::suite MPTTester mptt{env, issuer, mptInitNoFund}; mptt.create( {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); + env.close(); PrettyAsset const mptAsset = mptt["MPT"]; mptt.authorize({.account = alice}); mptt.authorize({.account = evan}); + env.close(); env(pay(issuer, alice, mptAsset(100'000))); env(pay(issuer, evan, mptAsset(100'000))); env.close(); From e1edd43205cad5d8b92b473aeb49a398ec83c695 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 18 Jul 2025 19:19:08 -0400 Subject: [PATCH 026/291] fixup! Fix LoanBrokerCoverClawback tests? --- src/test/app/LoanBroker_test.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 478538e097..6df1d0a54b 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -21,8 +21,11 @@ #include #include #include +#include #include #include +#include +#include #include #include From f7aef49509ed3794cdc6632ad8285928c101293e Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 18 Jul 2025 20:29:32 -0400 Subject: [PATCH 027/291] Add pseudo account type to injected data in RPC account_info result - Low hanging fruit --- include/xrpl/protocol/jss.h | 1 + src/test/app/LoanBroker_test.cpp | 48 ++++++++++++++++++++++++++ src/xrpld/rpc/handlers/AccountInfo.cpp | 19 ++++++++++ 3 files changed, 68 insertions(+) diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 2adf06d075..76fc01a315 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -500,6 +500,7 @@ JSS(propose_seq); // out: LedgerPropose JSS(proposers); // out: NetworkOPs, LedgerConsensus JSS(protocol); // out: NetworkOPs, PeerImp JSS(proxied); // out: RPC ping +JSS(pseudo_account); // out: AccountInfo JSS(pubkey_node); // out: NetworkOPs JSS(pubkey_publisher); // out: ValidatorList JSS(pubkey_validator); // out: NetworkOPs, ValidatorList diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 6df1d0a54b..5ee1b712ca 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -245,6 +245,30 @@ class LoanBroker_test : public beast::unit_test::suite BEAST_EXPECT(pseudo->at(sfLoanBrokerID) == keylet.key); } + { + // Get the AccountInfo RPC result for the broker pseudo-account + std::string const pseudoStr = to_string(pseudoAccount.id()); + auto const accountInfo = env.rpc("account_info", pseudoStr); + if (BEAST_EXPECT(accountInfo.isObject())) + { + auto const& accountData = + accountInfo[jss::result][jss::account_data]; + if (BEAST_EXPECT(accountData.isObject())) + { + BEAST_EXPECT(accountData[jss::Account] == pseudoStr); + BEAST_EXPECT( + accountData[sfLoanBrokerID] == + to_string(keylet.key)); + } + auto const& pseudoInfo = + accountInfo[jss::result][jss::pseudo_account]; + if (BEAST_EXPECT(pseudoInfo.isObject())) + { + BEAST_EXPECT(pseudoInfo[jss::type] == "LoanBroker"); + } + } + } + auto verifyCoverAmount = [&env, &vault, &pseudoAccount, &broker, &keylet, this](auto n) { using namespace jtx; @@ -614,6 +638,30 @@ class LoanBroker_test : public beast::unit_test::suite // Create and update Loan Brokers for (auto const& vault : vaults) { + { + // Get the AccountInfo RPC result for the vault pseudo-account + std::string const pseudoStr = + to_string(vault.pseudoAccount.id()); + auto const accountInfo = env.rpc("account_info", pseudoStr); + if (BEAST_EXPECT(accountInfo.isObject())) + { + auto const& accountData = + accountInfo[jss::result][jss::account_data]; + if (BEAST_EXPECT(accountData.isObject())) + { + BEAST_EXPECT(accountData[jss::Account] == pseudoStr); + BEAST_EXPECT( + accountData[sfVaultID] == to_string(vault.vaultID)); + } + auto const& pseudoInfo = + accountInfo[jss::result][jss::pseudo_account]; + if (BEAST_EXPECT(pseudoInfo.isObject())) + { + BEAST_EXPECT(pseudoInfo[jss::type] == "Vault"); + } + } + } + using namespace loanBroker; auto badKeylet = keylet::vault(alice.id(), env.seq(alice)); diff --git a/src/xrpld/rpc/handlers/AccountInfo.cpp b/src/xrpld/rpc/handlers/AccountInfo.cpp index 3432021690..923f3417c8 100644 --- a/src/xrpld/rpc/handlers/AccountInfo.cpp +++ b/src/xrpld/rpc/handlers/AccountInfo.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -150,6 +151,24 @@ doAccountInfo(RPC::JsonContext& context) result[jss::account_flags] = std::move(acctFlags); + auto const pseudoFields = getPseudoAccountFields(); + for (auto const& pseudoField : pseudoFields) + { + if (sleAccepted->isFieldPresent(*pseudoField)) + { + std::string name = pseudoField->fieldName; + if (name.ends_with("ID")) + { + // Remove the ID suffix from the field name. + name = name.substr(0, name.size() - 2); + } + // ValidPseudoAccounts invariant guarantees that only one field + // can be set + result[jss::pseudo_account][jss::type] = name; + break; + } + } + // The document[https://xrpl.org/account_info.html#account_info] states // that signer_lists is a bool, however assigning any string value // works. Do not allow this. This check is for api Version 2 onwards From ed00018adc7ceeb0401d53ae385ca6754ee65ad4 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 18 Jul 2025 20:42:00 -0400 Subject: [PATCH 028/291] fixup! Add pseudo account type to injected data in RPC account_info result --- src/test/app/LoanBroker_test.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 5ee1b712ca..cf7f26267f 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include From 96f2a65f641333d5d17676cfad03e95b7e6bce81 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 21 Jul 2025 20:37:16 -0400 Subject: [PATCH 029/291] Fix formatting --- include/xrpl/protocol/SField.h | 4 +- src/test/app/AMM_test.cpp | 170 ++++++++---------- src/test/app/Vault_test.cpp | 15 +- src/test/jtx/impl/AMM.cpp | 9 +- src/xrpld/app/main/Application.cpp | 125 ++++++------- src/xrpld/app/misc/PermissionedDEXHelpers.cpp | 7 +- src/xrpld/overlay/Slot.h | 16 +- src/xrpld/overlay/detail/OverlayImpl.cpp | 13 +- 8 files changed, 161 insertions(+), 198 deletions(-) diff --git a/include/xrpl/protocol/SField.h b/include/xrpl/protocol/SField.h index a72332b888..c8e5d6dca3 100644 --- a/include/xrpl/protocol/SField.h +++ b/include/xrpl/protocol/SField.h @@ -149,8 +149,8 @@ public: sMD_ChangeNew = 0x02, // new value when it changes sMD_DeleteFinal = 0x04, // final value when it is deleted sMD_Create = 0x08, // value when it's created - sMD_Always = 0x10, // value when node containing it is affected at all - sMD_BaseTen = 0x20, // value is treated as base 10, overriding behavior + sMD_Always = 0x10, // value when node containing it is affected at all + sMD_BaseTen = 0x20, // value is treated as base 10, overriding behavior sMD_PseudoAccount = 0x40, // if this field is set in an ACCOUNT_ROOT // _only_, then it is a pseudo-account sMD_Default = diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index 382c24dec7..39960ffb71 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -1333,55 +1333,49 @@ private: // Equal deposit, tokens rounded to 0 testAMM([&](AMM& amm, Env& env) { - amm.deposit( - DepositArg{ - .tokens = IOUAmount{1, -12}, - .err = ter(tecAMM_INVALID_TOKENS)}); + amm.deposit(DepositArg{ + .tokens = IOUAmount{1, -12}, + .err = ter(tecAMM_INVALID_TOKENS)}); }); // Equal deposit limit, tokens rounded to 0 testAMM( [&](AMM& amm, Env& env) { - amm.deposit( - DepositArg{ - .asset1In = STAmount{USD, 1, -15}, - .asset2In = XRPAmount{1}, - .err = ter(tecAMM_INVALID_TOKENS)}); + amm.deposit(DepositArg{ + .asset1In = STAmount{USD, 1, -15}, + .asset2In = XRPAmount{1}, + .err = ter(tecAMM_INVALID_TOKENS)}); }, {.pool = {{USD(1'000'000), XRP(1'000'000)}}, .features = {features - fixAMMv1_3}}); testAMM([&](AMM& amm, Env& env) { - amm.deposit( - DepositArg{ - .asset1In = STAmount{USD, 1, -15}, - .asset2In = XRPAmount{1}, - .err = ter(tecAMM_INVALID_TOKENS)}); + amm.deposit(DepositArg{ + .asset1In = STAmount{USD, 1, -15}, + .asset2In = XRPAmount{1}, + .err = ter(tecAMM_INVALID_TOKENS)}); }); // Single deposit by asset, tokens rounded to 0 testAMM([&](AMM& amm, Env& env) { - amm.deposit( - DepositArg{ - .asset1In = STAmount{USD, 1, -15}, - .err = ter(tecAMM_INVALID_TOKENS)}); + amm.deposit(DepositArg{ + .asset1In = STAmount{USD, 1, -15}, + .err = ter(tecAMM_INVALID_TOKENS)}); }); // Single deposit by tokens, tokens rounded to 0 testAMM([&](AMM& amm, Env& env) { - amm.deposit( - DepositArg{ - .tokens = IOUAmount{1, -10}, - .asset1In = STAmount{USD, 1, -15}, - .err = ter(tecAMM_INVALID_TOKENS)}); + amm.deposit(DepositArg{ + .tokens = IOUAmount{1, -10}, + .asset1In = STAmount{USD, 1, -15}, + .err = ter(tecAMM_INVALID_TOKENS)}); }); // Single deposit with eprice, tokens rounded to 0 testAMM([&](AMM& amm, Env& env) { - amm.deposit( - DepositArg{ - .asset1In = STAmount{USD, 1, -15}, - .maxEP = STAmount{USD, 1, -1}, - .err = ter(tecAMM_INVALID_TOKENS)}); + amm.deposit(DepositArg{ + .asset1In = STAmount{USD, 1, -15}, + .maxEP = STAmount{USD, 1, -1}, + .err = ter(tecAMM_INVALID_TOKENS)}); }); } @@ -2253,20 +2247,17 @@ private: XRPAmount{1}, std::nullopt, ter(tecAMM_INVALID_TOKENS)); - ammAlice.withdraw( - WithdrawArg{ - .tokens = IOUAmount{1, -10}, - .err = ter(tecAMM_INVALID_TOKENS)}); - ammAlice.withdraw( - WithdrawArg{ - .asset1Out = STAmount{USD, 1, -15}, - .asset2Out = XRPAmount{1}, - .err = ter(tecAMM_INVALID_TOKENS)}); - ammAlice.withdraw( - WithdrawArg{ - .tokens = IOUAmount{1, -10}, - .asset1Out = STAmount{USD, 1, -15}, - .err = ter(tecAMM_INVALID_TOKENS)}); + ammAlice.withdraw(WithdrawArg{ + .tokens = IOUAmount{1, -10}, + .err = ter(tecAMM_INVALID_TOKENS)}); + ammAlice.withdraw(WithdrawArg{ + .asset1Out = STAmount{USD, 1, -15}, + .asset2Out = XRPAmount{1}, + .err = ter(tecAMM_INVALID_TOKENS)}); + ammAlice.withdraw(WithdrawArg{ + .tokens = IOUAmount{1, -10}, + .asset1Out = STAmount{USD, 1, -15}, + .err = ter(tecAMM_INVALID_TOKENS)}); }); } @@ -7565,9 +7556,8 @@ private: [&](AMM& amm, Env& env) { auto const err = env.enabled(fixAMMv1_3) ? ter(tesSUCCESS) : ter(tecUNFUNDED_AMM); - amm.deposit( - DepositArg{ - .account = alice, .asset1In = amount, .err = err}); + amm.deposit(DepositArg{ + .account = alice, .asset1In = amount, .err = err}); }, tfee); test( @@ -7662,11 +7652,10 @@ private: STAmount const depositGBP{ GBP, UINT64_C(10'1234567890123456), -16}; - ammAlice.deposit( - DepositArg{ - .account = bob, - .asset1In = depositEuro, - .asset2In = depositGBP}); + ammAlice.deposit(DepositArg{ + .account = bob, + .asset1In = depositEuro, + .asset2In = depositGBP}); invariant(ammAlice, env, "dep2", false); }, {{GBP(30'000), EUR(30'000)}}, @@ -7691,11 +7680,10 @@ private: STAmount const depositEuro{EUR, 1, exponent}; STAmount const depositGBP{GBP, 1, exponent}; - ammAlice.deposit( - DepositArg{ - .account = bob, - .asset1In = depositEuro, - .asset2In = depositGBP}); + ammAlice.deposit(DepositArg{ + .account = bob, + .asset1In = depositEuro, + .asset2In = depositGBP}); invariant( ammAlice, env, @@ -7720,10 +7708,9 @@ private: Fund::Acct); env.close(); - ammAlice.deposit( - DepositArg{ - .account = bob, - .tokens = IOUAmount{10'1234567890123456, -16}}); + ammAlice.deposit(DepositArg{ + .account = bob, + .tokens = IOUAmount{10'1234567890123456, -16}}); invariant(ammAlice, env, "dep4", false); }, {{GBP(7'000), EUR(30'000)}}, @@ -7753,11 +7740,10 @@ private: Fund::Acct); env.close(); - ammAlice.deposit( - DepositArg{ - .account = bob, - .tokens = tokens, - .asset1In = STAmount{EUR, 1, 6}}); + ammAlice.deposit(DepositArg{ + .account = bob, + .tokens = tokens, + .asset1In = STAmount{EUR, 1, 6}}); invariant(ammAlice, env, "dep5", false); }, {{GBP(7'000), EUR(30'000)}}, @@ -7822,12 +7808,11 @@ private: // tfTwoAsset withdraw mode testAMM( [&](AMM& ammAlice, Env& env) { - ammAlice.withdraw( - WithdrawArg{ - .account = alice, - .asset1Out = STAmount{GBP, 3'500}, - .asset2Out = STAmount{EUR, 15'000}, - .flags = tfTwoAsset}); + ammAlice.withdraw(WithdrawArg{ + .account = alice, + .asset1Out = STAmount{GBP, 3'500}, + .asset2Out = STAmount{EUR, 15'000}, + .flags = tfTwoAsset}); invariant(ammAlice, env, "with3", false); }, {{GBP(7'000), EUR(30'000)}}, @@ -7842,11 +7827,10 @@ private: // errors testAMM( [&](AMM& ammAlice, Env& env) { - ammAlice.withdraw( - WithdrawArg{ - .account = alice, - .asset1Out = STAmount{GBP, 1'234}, - .flags = tfSingleAsset}); + ammAlice.withdraw(WithdrawArg{ + .account = alice, + .asset1Out = STAmount{GBP, 1'234}, + .flags = tfSingleAsset}); invariant(ammAlice, env, "with4", false); }, {{GBP(7'000), EUR(30'000)}}, @@ -7866,15 +7850,13 @@ private: Fund::Acct); env.close(); - ammAlice.deposit( - DepositArg{ - .account = bob, .asset1In = STAmount{GBP, 3'456}}); + ammAlice.deposit(DepositArg{ + .account = bob, .asset1In = STAmount{GBP, 3'456}}); - ammAlice.withdraw( - WithdrawArg{ - .account = bob, - .asset1Out = STAmount{GBP, 1'000}, - .flags = tfOneAssetWithdrawAll}); + ammAlice.withdraw(WithdrawArg{ + .account = bob, + .asset1Out = STAmount{GBP, 1'000}, + .flags = tfOneAssetWithdrawAll}); invariant(ammAlice, env, "with5", false); }, {{GBP(7'000), EUR(30'000)}}, @@ -7885,12 +7867,11 @@ private: // tfOneAssetLPToken mode testAMM( [&](AMM& ammAlice, Env& env) { - ammAlice.withdraw( - WithdrawArg{ - .account = alice, - .tokens = 1'000, - .asset1Out = STAmount{GBP, 100}, - .flags = tfOneAssetLPToken}); + ammAlice.withdraw(WithdrawArg{ + .account = alice, + .tokens = 1'000, + .asset1Out = STAmount{GBP, 100}, + .flags = tfOneAssetLPToken}); invariant(ammAlice, env, "with6", false); }, {{GBP(7'000), EUR(30'000)}}, @@ -7901,12 +7882,11 @@ private: // tfLimitLPToken mode testAMM( [&](AMM& ammAlice, Env& env) { - ammAlice.withdraw( - WithdrawArg{ - .account = alice, - .asset1Out = STAmount{GBP, 100}, - .maxEP = IOUAmount{2}, - .flags = tfLimitLPToken}); + ammAlice.withdraw(WithdrawArg{ + .account = alice, + .asset1Out = STAmount{GBP, 100}, + .maxEP = IOUAmount{2}, + .flags = tfLimitLPToken}); invariant(ammAlice, env, "with7", true); }, {{GBP(7'000), EUR(30'000)}}, diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 22814b3086..31fe2961ff 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -943,14 +943,13 @@ class Vault_test : public beast::unit_test::suite { using namespace test::jtx; - auto testCase = [this]( - std::function test) { + auto testCase = [this](std::function test) { Env env{*this, testable_amendments() | featureSingleAssetVault}; Account issuer{"issuer"}; Account owner{"owner"}; diff --git a/src/test/jtx/impl/AMM.cpp b/src/test/jtx/impl/AMM.cpp index 90d0f0eacc..ca96401bc4 100644 --- a/src/test/jtx/impl/AMM.cpp +++ b/src/test/jtx/impl/AMM.cpp @@ -78,11 +78,10 @@ AMM::AMM( , msig_(ms) , fee_(fee) , ammAccount_(create(tfee, flags, seq, ter)) - , lptIssue_( - ripple::ammLPTIssue( - asset1_.issue().currency, - asset2_.issue().currency, - ammAccount_)) + , lptIssue_(ripple::ammLPTIssue( + asset1_.issue().currency, + asset2_.issue().currency, + ammAccount_)) , initialLPTokens_(initialTokens()) { } diff --git a/src/xrpld/app/main/Application.cpp b/src/xrpld/app/main/Application.cpp index 8e893a655e..98a40b8f1c 100644 --- a/src/xrpld/app/main/Application.cpp +++ b/src/xrpld/app/main/Application.cpp @@ -279,14 +279,13 @@ public: , m_journal(logs_->journal("Application")) // PerfLog must be started before any other threads are launched. - , perfLog_( - perf::make_PerfLog( - perf::setup_PerfLog( - config_->section("perf"), - config_->CONFIG_DIR), - *this, - logs_->journal("PerfLog"), - [this] { signalStop("PerfLog"); })) + , perfLog_(perf::make_PerfLog( + perf::setup_PerfLog( + config_->section("perf"), + config_->CONFIG_DIR), + *this, + logs_->journal("PerfLog"), + [this] { signalStop("PerfLog"); })) , m_txMaster(*this) @@ -294,34 +293,33 @@ public: config_->section(SECTION_INSIGHT), logs_->journal("Collector"))) - , m_jobQueue( - std::make_unique( - [](std::unique_ptr const& config) { - if (config->standalone() && !config->FORCE_MULTI_THREAD) - return 1; + , m_jobQueue(std::make_unique( + [](std::unique_ptr const& config) { + if (config->standalone() && !config->FORCE_MULTI_THREAD) + return 1; - if (config->WORKERS) - return config->WORKERS; + if (config->WORKERS) + return config->WORKERS; - auto count = - static_cast(std::thread::hardware_concurrency()); + auto count = + static_cast(std::thread::hardware_concurrency()); - // Be more aggressive about the number of threads to use - // for the job queue if the server is configured as - // "large" or "huge" if there are enough cores. - if (config->NODE_SIZE >= 4 && count >= 16) - count = 6 + std::min(count, 8); - else if (config->NODE_SIZE >= 3 && count >= 8) - count = 4 + std::min(count, 6); - else - count = 2 + std::min(count, 4); + // Be more aggressive about the number of threads to use + // for the job queue if the server is configured as + // "large" or "huge" if there are enough cores. + if (config->NODE_SIZE >= 4 && count >= 16) + count = 6 + std::min(count, 8); + else if (config->NODE_SIZE >= 3 && count >= 8) + count = 4 + std::min(count, 6); + else + count = 2 + std::min(count, 4); - return count; - }(config_), - m_collectorManager->group("jobq"), - logs_->journal("JobQueue"), - *logs_, - *perfLog_)) + return count; + }(config_), + m_collectorManager->group("jobq"), + logs_->journal("JobQueue"), + *logs_, + *perfLog_)) , m_nodeStoreScheduler(*m_jobQueue) @@ -346,10 +344,9 @@ public: , validatorKeys_(*config_, m_journal) - , m_resourceManager( - Resource::make_Manager( - m_collectorManager->collector(), - logs_->journal("Resource"))) + , m_resourceManager(Resource::make_Manager( + m_collectorManager->collector(), + logs_->journal("Resource"))) , m_nodeStore(m_shaMapStore->makeNodeStore( config_->PREFETCH_WORKERS > 0 ? config_->PREFETCH_WORKERS : 4)) @@ -358,18 +355,16 @@ public: , m_orderBookDB(*this) - , m_pathRequests( - std::make_unique( - *this, - logs_->journal("PathRequest"), - m_collectorManager->collector())) + , m_pathRequests(std::make_unique( + *this, + logs_->journal("PathRequest"), + m_collectorManager->collector())) - , m_ledgerMaster( - std::make_unique( - *this, - stopwatch(), - m_collectorManager->collector(), - logs_->journal("LedgerMaster"))) + , m_ledgerMaster(std::make_unique( + *this, + stopwatch(), + m_collectorManager->collector(), + logs_->journal("LedgerMaster"))) , ledgerCleaner_( make_LedgerCleaner(*this, logs_->journal("LedgerCleaner"))) @@ -389,11 +384,10 @@ public: gotTXSet(set, fromAcquire); })) - , m_ledgerReplayer( - std::make_unique( - *this, - *m_inboundLedgers, - make_PeerSetBuilder(*this))) + , m_ledgerReplayer(std::make_unique( + *this, + *m_inboundLedgers, + make_PeerSetBuilder(*this))) , m_acceptedLedgerCache( "AcceptedLedger", @@ -417,9 +411,8 @@ public: , cluster_(std::make_unique(logs_->journal("Overlay"))) - , peerReservations_( - std::make_unique( - logs_->journal("PeerReservationTable"))) + , peerReservations_(std::make_unique( + logs_->journal("PeerReservationTable"))) , validatorManifests_( std::make_unique(logs_->journal("ManifestCache"))) @@ -427,14 +420,13 @@ public: , publisherManifests_( std::make_unique(logs_->journal("ManifestCache"))) - , validators_( - std::make_unique( - *validatorManifests_, - *publisherManifests_, - *timeKeeper_, - config_->legacy("database_path"), - logs_->journal("ValidatorList"), - config_->VALIDATION_QUORUM)) + , validators_(std::make_unique( + *validatorManifests_, + *publisherManifests_, + *timeKeeper_, + config_->legacy("database_path"), + logs_->journal("ValidatorList"), + config_->VALIDATION_QUORUM)) , validatorSites_(std::make_unique(*this)) @@ -449,10 +441,9 @@ public: , mFeeTrack( std::make_unique(logs_->journal("LoadManager"))) - , hashRouter_( - std::make_unique( - setup_HashRouter(*config_), - stopwatch())) + , hashRouter_(std::make_unique( + setup_HashRouter(*config_), + stopwatch())) , mValidations( ValidationParms(), diff --git a/src/xrpld/app/misc/PermissionedDEXHelpers.cpp b/src/xrpld/app/misc/PermissionedDEXHelpers.cpp index 13793387c8..4251ac1519 100644 --- a/src/xrpld/app/misc/PermissionedDEXHelpers.cpp +++ b/src/xrpld/app/misc/PermissionedDEXHelpers.cpp @@ -41,11 +41,8 @@ accountInDomain( bool const inDomain = std::any_of( credentials.begin(), credentials.end(), [&](auto const& credential) { - auto const sleCred = view.read( - keylet::credential( - account, - credential[sfIssuer], - credential[sfCredentialType])); + auto const sleCred = view.read(keylet::credential( + account, credential[sfIssuer], credential[sfCredentialType])); if (!sleCred || !sleCred->isFlag(lsfAccepted)) return false; diff --git a/src/xrpld/overlay/Slot.h b/src/xrpld/overlay/Slot.h index b2d03164aa..0956eb06f7 100644 --- a/src/xrpld/overlay/Slot.h +++ b/src/xrpld/overlay/Slot.h @@ -791,15 +791,13 @@ Slots::updateSlotAndSquelch( { JLOG(journal_.trace()) << "updateSlotAndSquelch: new slot " << Slice(validator); - auto it = slots_ - .emplace( - std::make_pair( - validator, - Slot( - handler_, - logs_.journal("Slot"), - maxSelectedPeers_))) - .first; + auto it = + slots_ + .emplace(std::make_pair( + validator, + Slot( + handler_, logs_.journal("Slot"), maxSelectedPeers_))) + .first; it->second.update(validator, id, type, callback); } else diff --git a/src/xrpld/overlay/detail/OverlayImpl.cpp b/src/xrpld/overlay/detail/OverlayImpl.cpp index 015a31eedb..3cc5b2a024 100644 --- a/src/xrpld/overlay/detail/OverlayImpl.cpp +++ b/src/xrpld/overlay/detail/OverlayImpl.cpp @@ -133,13 +133,12 @@ OverlayImpl::OverlayImpl( , journal_(app_.journal("Overlay")) , serverHandler_(serverHandler) , m_resourceManager(resourceManager) - , m_peerFinder( - PeerFinder::make_Manager( - io_service, - stopwatch(), - app_.journal("PeerFinder"), - config, - collector)) + , m_peerFinder(PeerFinder::make_Manager( + io_service, + stopwatch(), + app_.journal("PeerFinder"), + config, + collector)) , m_resolver(resolver) , next_id_(1) , timer_count_(0) From 6c5945f9e8ec4181a53b27af5da09764597a59d8 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 11 Jul 2025 18:42:39 -0400 Subject: [PATCH 030/291] Make preflight1 and preflight2 private static Transactor functions - They should never be called by derived classes. --- src/xrpld/app/tx/detail/Escrow.cpp | 8 --- src/xrpld/app/tx/detail/Transactor.cpp | 90 +++++++++++++++----------- src/xrpld/app/tx/detail/Transactor.h | 35 ++++++---- 3 files changed, 75 insertions(+), 58 deletions(-) diff --git a/src/xrpld/app/tx/detail/Escrow.cpp b/src/xrpld/app/tx/detail/Escrow.cpp index 85711e2a7c..a698ab2a18 100644 --- a/src/xrpld/app/tx/detail/Escrow.cpp +++ b/src/xrpld/app/tx/detail/Escrow.cpp @@ -650,14 +650,6 @@ EscrowFinish::doPreflight(PreflightContext const& ctx) if (static_cast(cb) != static_cast(fb)) return temMALFORMED; - // Verify the transaction signature. If it doesn't work - // then don't do any more work. - { - auto const ret = detail::preflight2(ctx); - if (!isTesSuccess(ret)) - return ret; - } - if (cb && fb) { auto& router = ctx.app.getHashRouter(); diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 9e8d1c1e3b..41e8de1c1d 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -112,9 +112,53 @@ preflightCheckSigningKey(STObject const& sigObject, beast::Journal j) return tesSUCCESS; } +std::optional +preflightCheckSimulateKeys( + ApplyFlags flags, + STObject const& sigObject, + beast::Journal j) +{ + if (ctx.flags & tapDRY_RUN) // simulation + { + if (!ctx.tx.getSignature().empty()) + { + // NOTE: This code should never be hit because it's checked in the + // `simulate` RPC + return temINVALID; // LCOV_EXCL_LINE + } + + if (!ctx.tx.isFieldPresent(sfSigners)) + { + // no signers, no signature - a valid simulation + return tesSUCCESS; + } + + for (auto const& signer : ctx.tx.getFieldArray(sfSigners)) + { + if (signer.isFieldPresent(sfTxnSignature) && + !signer[sfTxnSignature].empty()) + { + // NOTE: This code should never be hit because it's + // checked in the `simulate` RPC + return temINVALID; // LCOV_EXCL_LINE + } + } + + if (!ctx.tx.getSigningPubKey().empty()) + { + // trying to single-sign _and_ multi-sign a transaction + return temINVALID; + } + return tesSUCCESS; + } + return {}; +} + +} // namespace detail + /** Performs early sanity checks on the account and fee fields */ NotTEC -preflight1(PreflightContext const& ctx, std::uint32_t flagMask) +Transactor::preflight1(PreflightContext const& ctx, std::uint32_t flagMask) { // This is inappropriate in preflight0, because only Change transactions // skip this function, and those do not allow an sfTicketSequence field. @@ -151,7 +195,7 @@ preflight1(PreflightContext const& ctx, std::uint32_t flagMask) return temBAD_FEE; } - if (auto const ret = preflightCheckSigningKey(ctx.tx, ctx.j)) + if (auto const ret = detail::preflightCheckSigningKey(ctx.tx, ctx.j)) return ret; // An AccountTxnID field constrains transaction ordering more than the @@ -177,41 +221,13 @@ preflight1(PreflightContext const& ctx, std::uint32_t flagMask) /** Checks whether the signature appears valid */ NotTEC -preflight2(PreflightContext const& ctx) +Transactor::preflight2(PreflightContext const& ctx) { - if (ctx.flags & tapDRY_RUN) // simulation - { - if (!ctx.tx.getSignature().empty()) - { - // NOTE: This code should never be hit because it's checked in the - // `simulate` RPC - return temINVALID; // LCOV_EXCL_LINE - } - - if (!ctx.tx.isFieldPresent(sfSigners)) - { - // no signers, no signature - a valid simulation - return tesSUCCESS; - } - - for (auto const& signer : ctx.tx.getFieldArray(sfSigners)) - { - if (signer.isFieldPresent(sfTxnSignature) && - !signer[sfTxnSignature].empty()) - { - // NOTE: This code should never be hit because it's - // checked in the `simulate` RPC - return temINVALID; // LCOV_EXCL_LINE - } - } - - if (!ctx.tx.getSigningPubKey().empty()) - { - // trying to single-sign _and_ multi-sign a transaction - return temINVALID; - } - return tesSUCCESS; - } + if (auto const ret = + detail::preflightCheckSimulateKeys(ctx.flags, ctx.tx, ctx.j)) + // Skips following checks if the transaction is being simulated, + // regardless of success or failure + return *ret; auto const sigValid = checkValidity( ctx.app.getHashRouter(), ctx.tx, ctx.rules, ctx.app.config()); @@ -223,8 +239,6 @@ preflight2(PreflightContext const& ctx) return tesSUCCESS; } -} // namespace detail - //------------------------------------------------------------------------------ Transactor::Transactor(ApplyContext& ctx) diff --git a/src/xrpld/app/tx/detail/Transactor.h b/src/xrpld/app/tx/detail/Transactor.h index d5e30fe638..9dcac97269 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -316,6 +316,18 @@ private: beast::Journal j); void trapTransaction(uint256) const; + + // Helper functions for preflight checks. Do not use directly. + /** Performs early sanity checks on the account and fee fields. + + (And passes flagMask to preflight0) + */ + static NotTEC + preflight1(PreflightContext const& ctx, std::uint32_t flagMask); + + /** Checks whether the signature appears valid */ + static NotTEC + preflight2(PreflightContext const& ctx); }; inline bool @@ -337,16 +349,15 @@ namespace detail { NotTEC preflightCheckSigningKey(STObject const& sigObject, beast::Journal j); -/** Performs early sanity checks on the account and fee fields. - - (And passes flagMask to preflight0) -*/ -NotTEC -preflight1(PreflightContext const& ctx, std::uint32_t flagMask); - -/** Checks whether the signature appears valid */ -NotTEC -preflight2(PreflightContext const& ctx); +/** Checks the special signing key state needed for simulation + * + * Normally called from preflight2 with ctx.tx. + */ +std::optional +preflightCheckSimulateKeys( + ApplyFlags flags, + STObject const& sigObject, + beast::Journal j); } // namespace detail // Defined in Change.cpp @@ -361,13 +372,13 @@ Transactor::preflight(PreflightContext const& ctx) if (!T::isEnabled(ctx)) return temDISABLED; - if (auto const ret = ripple::detail::preflight1(ctx, T::getFlagsMask(ctx))) + if (auto const ret = preflight1(ctx, T::getFlagsMask(ctx))) return ret; if (auto const ret = T::doPreflight(ctx)) return ret; - return ripple::detail::preflight2(ctx); + return preflight2(ctx); } template From 021bee8dd458571e4c77c1792568b4b83ec972bf Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 22 Jul 2025 12:10:58 -0400 Subject: [PATCH 031/291] fixup! Make preflight1 and preflight2 private static Transactor functions --- src/xrpld/app/tx/detail/Transactor.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 41e8de1c1d..5988199393 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -118,22 +118,23 @@ preflightCheckSimulateKeys( STObject const& sigObject, beast::Journal j) { - if (ctx.flags & tapDRY_RUN) // simulation + if (flags & tapDRY_RUN) // simulation { - if (!ctx.tx.getSignature().empty()) + std::optional const signature = sigObject[~sfTxnSignature]; + if (signature && !signature->empty()) { // NOTE: This code should never be hit because it's checked in the // `simulate` RPC return temINVALID; // LCOV_EXCL_LINE } - if (!ctx.tx.isFieldPresent(sfSigners)) + if (!sigObject.isFieldPresent(sfSigners)) { // no signers, no signature - a valid simulation return tesSUCCESS; } - for (auto const& signer : ctx.tx.getFieldArray(sfSigners)) + for (auto const& signer : sigObject.getFieldArray(sfSigners)) { if (signer.isFieldPresent(sfTxnSignature) && !signer[sfTxnSignature].empty()) @@ -144,7 +145,8 @@ preflightCheckSimulateKeys( } } - if (!ctx.tx.getSigningPubKey().empty()) + Slice const signingPubKey = sigObject[sfSigningPubKey]; + if (!signingPubKey.empty()) { // trying to single-sign _and_ multi-sign a transaction return temINVALID; From c76e485d01abee55b4ea9293e3462365b946a152 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 11 Jul 2025 19:17:11 -0400 Subject: [PATCH 032/291] Rename Transactor preflight functions - Rename Transactor::preflight to invokePreflight. - Rename doPreflight back to preflight. - Update instructions. - With preflight1 & 2 now uncallable, in-flight code in other branches should be easier to convert. --- src/test/app/AMM_test.cpp | 6 ++-- src/xrpld/app/tx/detail/AMMBid.cpp | 2 +- src/xrpld/app/tx/detail/AMMBid.h | 2 +- src/xrpld/app/tx/detail/AMMClawback.cpp | 2 +- src/xrpld/app/tx/detail/AMMClawback.h | 2 +- src/xrpld/app/tx/detail/AMMCreate.cpp | 2 +- src/xrpld/app/tx/detail/AMMCreate.h | 2 +- src/xrpld/app/tx/detail/AMMDelete.cpp | 2 +- src/xrpld/app/tx/detail/AMMDelete.h | 2 +- src/xrpld/app/tx/detail/AMMDeposit.cpp | 2 +- src/xrpld/app/tx/detail/AMMDeposit.h | 2 +- src/xrpld/app/tx/detail/AMMVote.cpp | 2 +- src/xrpld/app/tx/detail/AMMVote.h | 2 +- src/xrpld/app/tx/detail/AMMWithdraw.cpp | 2 +- src/xrpld/app/tx/detail/AMMWithdraw.h | 2 +- src/xrpld/app/tx/detail/CancelCheck.cpp | 2 +- src/xrpld/app/tx/detail/CancelCheck.h | 2 +- src/xrpld/app/tx/detail/CancelOffer.cpp | 2 +- src/xrpld/app/tx/detail/CancelOffer.h | 2 +- src/xrpld/app/tx/detail/CashCheck.cpp | 2 +- src/xrpld/app/tx/detail/CashCheck.h | 2 +- src/xrpld/app/tx/detail/Change.cpp | 2 +- src/xrpld/app/tx/detail/Clawback.cpp | 2 +- src/xrpld/app/tx/detail/Clawback.h | 2 +- src/xrpld/app/tx/detail/CreateCheck.cpp | 2 +- src/xrpld/app/tx/detail/CreateCheck.h | 2 +- src/xrpld/app/tx/detail/CreateOffer.cpp | 2 +- src/xrpld/app/tx/detail/CreateOffer.h | 2 +- src/xrpld/app/tx/detail/CreateTicket.cpp | 2 +- src/xrpld/app/tx/detail/CreateTicket.h | 2 +- src/xrpld/app/tx/detail/Credentials.cpp | 6 ++-- src/xrpld/app/tx/detail/Credentials.h | 6 ++-- src/xrpld/app/tx/detail/DID.cpp | 4 +-- src/xrpld/app/tx/detail/DID.h | 4 +-- src/xrpld/app/tx/detail/DelegateSet.cpp | 2 +- src/xrpld/app/tx/detail/DelegateSet.h | 2 +- src/xrpld/app/tx/detail/DeleteAccount.cpp | 2 +- src/xrpld/app/tx/detail/DeleteAccount.h | 2 +- src/xrpld/app/tx/detail/DeleteOracle.cpp | 2 +- src/xrpld/app/tx/detail/DeleteOracle.h | 2 +- src/xrpld/app/tx/detail/DepositPreauth.cpp | 2 +- src/xrpld/app/tx/detail/DepositPreauth.h | 2 +- src/xrpld/app/tx/detail/Escrow.cpp | 6 ++-- src/xrpld/app/tx/detail/Escrow.h | 6 ++-- src/xrpld/app/tx/detail/LedgerStateFix.cpp | 2 +- src/xrpld/app/tx/detail/LedgerStateFix.h | 2 +- src/xrpld/app/tx/detail/MPTokenAuthorize.cpp | 2 +- src/xrpld/app/tx/detail/MPTokenAuthorize.h | 2 +- .../app/tx/detail/MPTokenIssuanceCreate.cpp | 2 +- .../app/tx/detail/MPTokenIssuanceCreate.h | 2 +- .../app/tx/detail/MPTokenIssuanceDestroy.cpp | 2 +- .../app/tx/detail/MPTokenIssuanceDestroy.h | 2 +- .../app/tx/detail/MPTokenIssuanceSet.cpp | 2 +- src/xrpld/app/tx/detail/MPTokenIssuanceSet.h | 2 +- .../app/tx/detail/NFTokenAcceptOffer.cpp | 2 +- src/xrpld/app/tx/detail/NFTokenAcceptOffer.h | 2 +- src/xrpld/app/tx/detail/NFTokenBurn.cpp | 2 +- src/xrpld/app/tx/detail/NFTokenBurn.h | 2 +- .../app/tx/detail/NFTokenCancelOffer.cpp | 2 +- src/xrpld/app/tx/detail/NFTokenCancelOffer.h | 2 +- .../app/tx/detail/NFTokenCreateOffer.cpp | 2 +- src/xrpld/app/tx/detail/NFTokenCreateOffer.h | 2 +- src/xrpld/app/tx/detail/NFTokenMint.cpp | 2 +- src/xrpld/app/tx/detail/NFTokenMint.h | 2 +- src/xrpld/app/tx/detail/NFTokenModify.cpp | 2 +- src/xrpld/app/tx/detail/NFTokenModify.h | 2 +- src/xrpld/app/tx/detail/PayChan.cpp | 6 ++-- src/xrpld/app/tx/detail/PayChan.h | 6 ++-- src/xrpld/app/tx/detail/Payment.cpp | 2 +- src/xrpld/app/tx/detail/Payment.h | 2 +- .../tx/detail/PermissionedDomainDelete.cpp | 2 +- .../app/tx/detail/PermissionedDomainDelete.h | 2 +- .../app/tx/detail/PermissionedDomainSet.cpp | 2 +- .../app/tx/detail/PermissionedDomainSet.h | 2 +- src/xrpld/app/tx/detail/SetAccount.cpp | 2 +- src/xrpld/app/tx/detail/SetAccount.h | 2 +- src/xrpld/app/tx/detail/SetOracle.cpp | 2 +- src/xrpld/app/tx/detail/SetOracle.h | 2 +- src/xrpld/app/tx/detail/SetRegularKey.cpp | 2 +- src/xrpld/app/tx/detail/SetRegularKey.h | 2 +- src/xrpld/app/tx/detail/SetSignerList.cpp | 2 +- src/xrpld/app/tx/detail/SetSignerList.h | 2 +- src/xrpld/app/tx/detail/SetTrust.cpp | 2 +- src/xrpld/app/tx/detail/SetTrust.h | 2 +- src/xrpld/app/tx/detail/Transactor.h | 30 +++++++++++++------ src/xrpld/app/tx/detail/VaultClawback.cpp | 2 +- src/xrpld/app/tx/detail/VaultClawback.h | 2 +- src/xrpld/app/tx/detail/VaultCreate.cpp | 2 +- src/xrpld/app/tx/detail/VaultCreate.h | 2 +- src/xrpld/app/tx/detail/VaultDelete.cpp | 2 +- src/xrpld/app/tx/detail/VaultDelete.h | 2 +- src/xrpld/app/tx/detail/VaultDeposit.cpp | 2 +- src/xrpld/app/tx/detail/VaultDeposit.h | 2 +- src/xrpld/app/tx/detail/VaultSet.cpp | 2 +- src/xrpld/app/tx/detail/VaultSet.h | 2 +- src/xrpld/app/tx/detail/VaultWithdraw.cpp | 2 +- src/xrpld/app/tx/detail/VaultWithdraw.h | 2 +- src/xrpld/app/tx/detail/XChainBridge.cpp | 22 +++++++------- src/xrpld/app/tx/detail/XChainBridge.h | 16 +++++----- src/xrpld/app/tx/detail/applySteps.cpp | 2 +- 100 files changed, 153 insertions(+), 141 deletions(-) diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index b71bb65bf7..3bb2956964 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -3571,7 +3571,7 @@ private: env.current()->rules(), tapNONE, env.journal); - auto pf = Transactor::preflight(pfctx); + auto pf = Transactor::invokePreflight(pfctx); BEAST_EXPECT(pf == temDISABLED); env.app().config().features.insert(featureAMM); } @@ -3586,7 +3586,7 @@ private: env.current()->rules(), tapNONE, env.journal); - auto pf = Transactor::preflight(pfctx); + auto pf = Transactor::invokePreflight(pfctx); BEAST_EXPECT(pf != tesSUCCESS); } @@ -3601,7 +3601,7 @@ private: env.current()->rules(), tapNONE, env.journal); - auto pf = Transactor::preflight(pfctx); + auto pf = Transactor::invokePreflight(pfctx); BEAST_EXPECT(pf == temBAD_AMM_TOKENS); } } diff --git a/src/xrpld/app/tx/detail/AMMBid.cpp b/src/xrpld/app/tx/detail/AMMBid.cpp index d14b668790..2fc420974d 100644 --- a/src/xrpld/app/tx/detail/AMMBid.cpp +++ b/src/xrpld/app/tx/detail/AMMBid.cpp @@ -37,7 +37,7 @@ AMMBid::isEnabled(PreflightContext const& ctx) } NotTEC -AMMBid::doPreflight(PreflightContext const& ctx) +AMMBid::preflight(PreflightContext const& ctx) { if (auto const res = invalidAMMAssetPair( ctx.tx[sfAsset].get(), ctx.tx[sfAsset2].get())) diff --git a/src/xrpld/app/tx/detail/AMMBid.h b/src/xrpld/app/tx/detail/AMMBid.h index 9260ae78f2..9e69aca471 100644 --- a/src/xrpld/app/tx/detail/AMMBid.h +++ b/src/xrpld/app/tx/detail/AMMBid.h @@ -75,7 +75,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/AMMClawback.cpp b/src/xrpld/app/tx/detail/AMMClawback.cpp index bda676b478..d56321ce69 100644 --- a/src/xrpld/app/tx/detail/AMMClawback.cpp +++ b/src/xrpld/app/tx/detail/AMMClawback.cpp @@ -46,7 +46,7 @@ AMMClawback::getFlagsMask(PreflightContext const& ctx) } NotTEC -AMMClawback::doPreflight(PreflightContext const& ctx) +AMMClawback::preflight(PreflightContext const& ctx) { AccountID const issuer = ctx.tx[sfAccount]; AccountID const holder = ctx.tx[sfHolder]; diff --git a/src/xrpld/app/tx/detail/AMMClawback.h b/src/xrpld/app/tx/detail/AMMClawback.h index 828d07343b..0e354ebdc7 100644 --- a/src/xrpld/app/tx/detail/AMMClawback.h +++ b/src/xrpld/app/tx/detail/AMMClawback.h @@ -40,7 +40,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/AMMCreate.cpp b/src/xrpld/app/tx/detail/AMMCreate.cpp index 14bc8f7cda..c2e1503888 100644 --- a/src/xrpld/app/tx/detail/AMMCreate.cpp +++ b/src/xrpld/app/tx/detail/AMMCreate.cpp @@ -38,7 +38,7 @@ AMMCreate::isEnabled(PreflightContext const& ctx) } NotTEC -AMMCreate::doPreflight(PreflightContext const& ctx) +AMMCreate::preflight(PreflightContext const& ctx) { auto const amount = ctx.tx[sfAmount]; auto const amount2 = ctx.tx[sfAmount2]; diff --git a/src/xrpld/app/tx/detail/AMMCreate.h b/src/xrpld/app/tx/detail/AMMCreate.h index f6313dd8af..aab4a4fb27 100644 --- a/src/xrpld/app/tx/detail/AMMCreate.h +++ b/src/xrpld/app/tx/detail/AMMCreate.h @@ -67,7 +67,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); diff --git a/src/xrpld/app/tx/detail/AMMDelete.cpp b/src/xrpld/app/tx/detail/AMMDelete.cpp index 172a33073e..31e18573fc 100644 --- a/src/xrpld/app/tx/detail/AMMDelete.cpp +++ b/src/xrpld/app/tx/detail/AMMDelete.cpp @@ -34,7 +34,7 @@ AMMDelete::isEnabled(PreflightContext const& ctx) } NotTEC -AMMDelete::doPreflight(PreflightContext const& ctx) +AMMDelete::preflight(PreflightContext const& ctx) { return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/AMMDelete.h b/src/xrpld/app/tx/detail/AMMDelete.h index 87abf92c27..1c44571021 100644 --- a/src/xrpld/app/tx/detail/AMMDelete.h +++ b/src/xrpld/app/tx/detail/AMMDelete.h @@ -43,7 +43,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/AMMDeposit.cpp b/src/xrpld/app/tx/detail/AMMDeposit.cpp index 2aad1e05b9..9df16ef12a 100644 --- a/src/xrpld/app/tx/detail/AMMDeposit.cpp +++ b/src/xrpld/app/tx/detail/AMMDeposit.cpp @@ -43,7 +43,7 @@ AMMDeposit::getFlagsMask(PreflightContext const& ctx) } NotTEC -AMMDeposit::doPreflight(PreflightContext const& ctx) +AMMDeposit::preflight(PreflightContext const& ctx) { auto const flags = ctx.tx.getFlags(); diff --git a/src/xrpld/app/tx/detail/AMMDeposit.h b/src/xrpld/app/tx/detail/AMMDeposit.h index 982c2d8abd..09e1f040ff 100644 --- a/src/xrpld/app/tx/detail/AMMDeposit.h +++ b/src/xrpld/app/tx/detail/AMMDeposit.h @@ -75,7 +75,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/AMMVote.cpp b/src/xrpld/app/tx/detail/AMMVote.cpp index ca3435449d..1c5cd543eb 100644 --- a/src/xrpld/app/tx/detail/AMMVote.cpp +++ b/src/xrpld/app/tx/detail/AMMVote.cpp @@ -34,7 +34,7 @@ AMMVote::isEnabled(PreflightContext const& ctx) } NotTEC -AMMVote::doPreflight(PreflightContext const& ctx) +AMMVote::preflight(PreflightContext const& ctx) { if (auto const res = invalidAMMAssetPair( ctx.tx[sfAsset].get(), ctx.tx[sfAsset2].get())) diff --git a/src/xrpld/app/tx/detail/AMMVote.h b/src/xrpld/app/tx/detail/AMMVote.h index bc94517961..af7691a3b4 100644 --- a/src/xrpld/app/tx/detail/AMMVote.h +++ b/src/xrpld/app/tx/detail/AMMVote.h @@ -60,7 +60,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/AMMWithdraw.cpp b/src/xrpld/app/tx/detail/AMMWithdraw.cpp index 4ecfe1eb61..1150e09cdd 100644 --- a/src/xrpld/app/tx/detail/AMMWithdraw.cpp +++ b/src/xrpld/app/tx/detail/AMMWithdraw.cpp @@ -41,7 +41,7 @@ AMMWithdraw::getFlagsMask(PreflightContext const& ctx) } NotTEC -AMMWithdraw::doPreflight(PreflightContext const& ctx) +AMMWithdraw::preflight(PreflightContext const& ctx) { auto const flags = ctx.tx.getFlags(); diff --git a/src/xrpld/app/tx/detail/AMMWithdraw.h b/src/xrpld/app/tx/detail/AMMWithdraw.h index 7afda43e1e..e70f74bad2 100644 --- a/src/xrpld/app/tx/detail/AMMWithdraw.h +++ b/src/xrpld/app/tx/detail/AMMWithdraw.h @@ -82,7 +82,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/CancelCheck.cpp b/src/xrpld/app/tx/detail/CancelCheck.cpp index 15b647446b..82ec1bbd1e 100644 --- a/src/xrpld/app/tx/detail/CancelCheck.cpp +++ b/src/xrpld/app/tx/detail/CancelCheck.cpp @@ -36,7 +36,7 @@ CancelCheck::isEnabled(PreflightContext const& ctx) } NotTEC -CancelCheck::doPreflight(PreflightContext const& ctx) +CancelCheck::preflight(PreflightContext const& ctx) { return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/CancelCheck.h b/src/xrpld/app/tx/detail/CancelCheck.h index 3b40d24008..44cf3e0721 100644 --- a/src/xrpld/app/tx/detail/CancelCheck.h +++ b/src/xrpld/app/tx/detail/CancelCheck.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/CancelOffer.cpp b/src/xrpld/app/tx/detail/CancelOffer.cpp index 6141f63e48..99ca792b3b 100644 --- a/src/xrpld/app/tx/detail/CancelOffer.cpp +++ b/src/xrpld/app/tx/detail/CancelOffer.cpp @@ -26,7 +26,7 @@ namespace ripple { NotTEC -CancelOffer::doPreflight(PreflightContext const& ctx) +CancelOffer::preflight(PreflightContext const& ctx) { if (!ctx.tx[sfOfferSequence]) { diff --git a/src/xrpld/app/tx/detail/CancelOffer.h b/src/xrpld/app/tx/detail/CancelOffer.h index 8632115026..0813dd0aef 100644 --- a/src/xrpld/app/tx/detail/CancelOffer.h +++ b/src/xrpld/app/tx/detail/CancelOffer.h @@ -36,7 +36,7 @@ public: } static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/CashCheck.cpp b/src/xrpld/app/tx/detail/CashCheck.cpp index 02babcfbe5..7768db31c9 100644 --- a/src/xrpld/app/tx/detail/CashCheck.cpp +++ b/src/xrpld/app/tx/detail/CashCheck.cpp @@ -39,7 +39,7 @@ CashCheck::isEnabled(PreflightContext const& ctx) } NotTEC -CashCheck::doPreflight(PreflightContext const& ctx) +CashCheck::preflight(PreflightContext const& ctx) { // Exactly one of Amount or DeliverMin must be present. auto const optAmount = ctx.tx[~sfAmount]; diff --git a/src/xrpld/app/tx/detail/CashCheck.h b/src/xrpld/app/tx/detail/CashCheck.h index 5dbd21a8e3..cc3d564efe 100644 --- a/src/xrpld/app/tx/detail/CashCheck.h +++ b/src/xrpld/app/tx/detail/CashCheck.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/Change.cpp b/src/xrpld/app/tx/detail/Change.cpp index f1e95a376f..676886d063 100644 --- a/src/xrpld/app/tx/detail/Change.cpp +++ b/src/xrpld/app/tx/detail/Change.cpp @@ -35,7 +35,7 @@ namespace ripple { template <> NotTEC -Transactor::preflight(PreflightContext const& ctx) +Transactor::invokePreflight(PreflightContext const& ctx) { // 0 means "Allow any flags" if (auto const ret = preflight0(ctx, 0)) diff --git a/src/xrpld/app/tx/detail/Clawback.cpp b/src/xrpld/app/tx/detail/Clawback.cpp index 0375b6b718..012565ad95 100644 --- a/src/xrpld/app/tx/detail/Clawback.cpp +++ b/src/xrpld/app/tx/detail/Clawback.cpp @@ -88,7 +88,7 @@ Clawback::getFlagsMask(PreflightContext const& ctx) } NotTEC -Clawback::doPreflight(PreflightContext const& ctx) +Clawback::preflight(PreflightContext const& ctx) { if (auto const ret = std::visit( [&](T const&) { return preflightHelper(ctx); }, diff --git a/src/xrpld/app/tx/detail/Clawback.h b/src/xrpld/app/tx/detail/Clawback.h index 6353c79a55..8300d98fde 100644 --- a/src/xrpld/app/tx/detail/Clawback.h +++ b/src/xrpld/app/tx/detail/Clawback.h @@ -40,7 +40,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/CreateCheck.cpp b/src/xrpld/app/tx/detail/CreateCheck.cpp index 7b19629230..a8ebce4204 100644 --- a/src/xrpld/app/tx/detail/CreateCheck.cpp +++ b/src/xrpld/app/tx/detail/CreateCheck.cpp @@ -35,7 +35,7 @@ CreateCheck::isEnabled(PreflightContext const& ctx) } NotTEC -CreateCheck::doPreflight(PreflightContext const& ctx) +CreateCheck::preflight(PreflightContext const& ctx) { if (ctx.tx[sfAccount] == ctx.tx[sfDestination]) { diff --git a/src/xrpld/app/tx/detail/CreateCheck.h b/src/xrpld/app/tx/detail/CreateCheck.h index cb794332ca..1d9a7ce168 100644 --- a/src/xrpld/app/tx/detail/CreateCheck.h +++ b/src/xrpld/app/tx/detail/CreateCheck.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/CreateOffer.cpp b/src/xrpld/app/tx/detail/CreateOffer.cpp index 80414df4e0..3b5c851016 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.cpp +++ b/src/xrpld/app/tx/detail/CreateOffer.cpp @@ -63,7 +63,7 @@ CreateOffer::getFlagsMask(PreflightContext const& ctx) } NotTEC -CreateOffer::doPreflight(PreflightContext const& ctx) +CreateOffer::preflight(PreflightContext const& ctx) { auto& tx = ctx.tx; auto& j = ctx.j; diff --git a/src/xrpld/app/tx/detail/CreateOffer.h b/src/xrpld/app/tx/detail/CreateOffer.h index c2eade0f01..7ef9189ad7 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.h +++ b/src/xrpld/app/tx/detail/CreateOffer.h @@ -52,7 +52,7 @@ public: /** Enforce constraints beyond those of the Transactor base class. */ static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); /** Enforce constraints beyond those of the Transactor base class. */ static TER diff --git a/src/xrpld/app/tx/detail/CreateTicket.cpp b/src/xrpld/app/tx/detail/CreateTicket.cpp index 254cf68555..8d88dd7bf8 100644 --- a/src/xrpld/app/tx/detail/CreateTicket.cpp +++ b/src/xrpld/app/tx/detail/CreateTicket.cpp @@ -40,7 +40,7 @@ CreateTicket::isEnabled(PreflightContext const& ctx) } NotTEC -CreateTicket::doPreflight(PreflightContext const& ctx) +CreateTicket::preflight(PreflightContext const& ctx) { if (std::uint32_t const count = ctx.tx[sfTicketCount]; count < minValidCount || count > maxValidCount) diff --git a/src/xrpld/app/tx/detail/CreateTicket.h b/src/xrpld/app/tx/detail/CreateTicket.h index 746daf7c56..04ad6711c7 100644 --- a/src/xrpld/app/tx/detail/CreateTicket.h +++ b/src/xrpld/app/tx/detail/CreateTicket.h @@ -74,7 +74,7 @@ public: /** Enforce constraints beyond those of the Transactor base class. */ static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); /** Enforce constraints beyond those of the Transactor base class. */ static TER diff --git a/src/xrpld/app/tx/detail/Credentials.cpp b/src/xrpld/app/tx/detail/Credentials.cpp index 83873a9612..abd5539100 100644 --- a/src/xrpld/app/tx/detail/Credentials.cpp +++ b/src/xrpld/app/tx/detail/Credentials.cpp @@ -62,7 +62,7 @@ CredentialCreate::getFlagsMask(PreflightContext const& ctx) } NotTEC -CredentialCreate::doPreflight(PreflightContext const& ctx) +CredentialCreate::preflight(PreflightContext const& ctx) { auto const& tx = ctx.tx; auto& j = ctx.j; @@ -213,7 +213,7 @@ CredentialDelete::getFlagsMask(PreflightContext const& ctx) } NotTEC -CredentialDelete::doPreflight(PreflightContext const& ctx) +CredentialDelete::preflight(PreflightContext const& ctx) { auto const subject = ctx.tx[~sfSubject]; auto const issuer = ctx.tx[~sfIssuer]; @@ -297,7 +297,7 @@ CredentialAccept::getFlagsMask(PreflightContext const& ctx) } NotTEC -CredentialAccept::doPreflight(PreflightContext const& ctx) +CredentialAccept::preflight(PreflightContext const& ctx) { if (!ctx.tx[sfIssuer]) { diff --git a/src/xrpld/app/tx/detail/Credentials.h b/src/xrpld/app/tx/detail/Credentials.h index ee19c5c0a3..31b1ee8cf7 100644 --- a/src/xrpld/app/tx/detail/Credentials.h +++ b/src/xrpld/app/tx/detail/Credentials.h @@ -40,7 +40,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -67,7 +67,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -94,7 +94,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/DID.cpp b/src/xrpld/app/tx/detail/DID.cpp index d74c9733ae..8c1e23f4d5 100644 --- a/src/xrpld/app/tx/detail/DID.cpp +++ b/src/xrpld/app/tx/detail/DID.cpp @@ -49,7 +49,7 @@ DIDSet::isEnabled(PreflightContext const& ctx) } NotTEC -DIDSet::doPreflight(PreflightContext const& ctx) +DIDSet::preflight(PreflightContext const& ctx) { if (!ctx.tx.isFieldPresent(sfURI) && !ctx.tx.isFieldPresent(sfDIDDocument) && !ctx.tx.isFieldPresent(sfData)) @@ -175,7 +175,7 @@ DIDDelete::isEnabled(PreflightContext const& ctx) } NotTEC -DIDDelete::doPreflight(PreflightContext const& ctx) +DIDDelete::preflight(PreflightContext const& ctx) { return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/DID.h b/src/xrpld/app/tx/detail/DID.h index 1995176d17..c9fbb199b9 100644 --- a/src/xrpld/app/tx/detail/DID.h +++ b/src/xrpld/app/tx/detail/DID.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); TER doApply() override; @@ -58,7 +58,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER deleteSLE(ApplyContext& ctx, Keylet sleKeylet, AccountID const owner); diff --git a/src/xrpld/app/tx/detail/DelegateSet.cpp b/src/xrpld/app/tx/detail/DelegateSet.cpp index ebd52c113e..5e6b17f187 100644 --- a/src/xrpld/app/tx/detail/DelegateSet.cpp +++ b/src/xrpld/app/tx/detail/DelegateSet.cpp @@ -35,7 +35,7 @@ DelegateSet::isEnabled(PreflightContext const& ctx) } NotTEC -DelegateSet::doPreflight(PreflightContext const& ctx) +DelegateSet::preflight(PreflightContext const& ctx) { auto const& permissions = ctx.tx.getFieldArray(sfPermissions); if (permissions.size() > permissionMaxSize) diff --git a/src/xrpld/app/tx/detail/DelegateSet.h b/src/xrpld/app/tx/detail/DelegateSet.h index 87166ab5fb..c95b0f8fd4 100644 --- a/src/xrpld/app/tx/detail/DelegateSet.h +++ b/src/xrpld/app/tx/detail/DelegateSet.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/DeleteAccount.cpp b/src/xrpld/app/tx/detail/DeleteAccount.cpp index 0aa3031130..4f49384b87 100644 --- a/src/xrpld/app/tx/detail/DeleteAccount.cpp +++ b/src/xrpld/app/tx/detail/DeleteAccount.cpp @@ -49,7 +49,7 @@ DeleteAccount::isEnabled(PreflightContext const& ctx) } NotTEC -DeleteAccount::doPreflight(PreflightContext const& ctx) +DeleteAccount::preflight(PreflightContext const& ctx) { if (ctx.tx[sfAccount] == ctx.tx[sfDestination]) // An account cannot be deleted and give itself the resulting XRP. diff --git a/src/xrpld/app/tx/detail/DeleteAccount.h b/src/xrpld/app/tx/detail/DeleteAccount.h index 42946bfc27..a67267ad7b 100644 --- a/src/xrpld/app/tx/detail/DeleteAccount.h +++ b/src/xrpld/app/tx/detail/DeleteAccount.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); diff --git a/src/xrpld/app/tx/detail/DeleteOracle.cpp b/src/xrpld/app/tx/detail/DeleteOracle.cpp index 2e49877d9b..ac078e5c9d 100644 --- a/src/xrpld/app/tx/detail/DeleteOracle.cpp +++ b/src/xrpld/app/tx/detail/DeleteOracle.cpp @@ -33,7 +33,7 @@ DeleteOracle::isEnabled(PreflightContext const& ctx) } NotTEC -DeleteOracle::doPreflight(PreflightContext const& ctx) +DeleteOracle::preflight(PreflightContext const& ctx) { return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/DeleteOracle.h b/src/xrpld/app/tx/detail/DeleteOracle.h index 87183268ff..f55b5d0f50 100644 --- a/src/xrpld/app/tx/detail/DeleteOracle.h +++ b/src/xrpld/app/tx/detail/DeleteOracle.h @@ -46,7 +46,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/DepositPreauth.cpp b/src/xrpld/app/tx/detail/DepositPreauth.cpp index a1bdafa852..3d1cbf7381 100644 --- a/src/xrpld/app/tx/detail/DepositPreauth.cpp +++ b/src/xrpld/app/tx/detail/DepositPreauth.cpp @@ -46,7 +46,7 @@ DepositPreauth::isEnabled(PreflightContext const& ctx) } NotTEC -DepositPreauth::doPreflight(PreflightContext const& ctx) +DepositPreauth::preflight(PreflightContext const& ctx) { bool const authArrPresent = ctx.tx.isFieldPresent(sfAuthorizeCredentials); bool const unauthArrPresent = diff --git a/src/xrpld/app/tx/detail/DepositPreauth.h b/src/xrpld/app/tx/detail/DepositPreauth.h index 3d969b8df4..384b098aef 100644 --- a/src/xrpld/app/tx/detail/DepositPreauth.h +++ b/src/xrpld/app/tx/detail/DepositPreauth.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/Escrow.cpp b/src/xrpld/app/tx/detail/Escrow.cpp index a698ab2a18..1d41e0bb4e 100644 --- a/src/xrpld/app/tx/detail/Escrow.cpp +++ b/src/xrpld/app/tx/detail/Escrow.cpp @@ -126,7 +126,7 @@ EscrowCreate::getFlagsMask(PreflightContext const& ctx) } NotTEC -EscrowCreate::doPreflight(PreflightContext const& ctx) +EscrowCreate::preflight(PreflightContext const& ctx) { STAmount const amount{ctx.tx[sfAmount]}; if (!isXRP(amount)) @@ -640,7 +640,7 @@ EscrowFinish::getFlagsMask(PreflightContext const& ctx) } NotTEC -EscrowFinish::doPreflight(PreflightContext const& ctx) +EscrowFinish::preflight(PreflightContext const& ctx) { auto const cb = ctx.tx[~sfCondition]; auto const fb = ctx.tx[~sfFulfillment]; @@ -1202,7 +1202,7 @@ EscrowCancel::getFlagsMask(PreflightContext const& ctx) } NotTEC -EscrowCancel::doPreflight(PreflightContext const& ctx) +EscrowCancel::preflight(PreflightContext const& ctx) { return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/Escrow.h b/src/xrpld/app/tx/detail/Escrow.h index 86ae208b67..9696515963 100644 --- a/src/xrpld/app/tx/detail/Escrow.h +++ b/src/xrpld/app/tx/detail/Escrow.h @@ -40,7 +40,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -67,7 +67,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); @@ -94,7 +94,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/LedgerStateFix.cpp b/src/xrpld/app/tx/detail/LedgerStateFix.cpp index 680cc30d95..eeb67bbc91 100644 --- a/src/xrpld/app/tx/detail/LedgerStateFix.cpp +++ b/src/xrpld/app/tx/detail/LedgerStateFix.cpp @@ -34,7 +34,7 @@ LedgerStateFix::isEnabled(PreflightContext const& ctx) } NotTEC -LedgerStateFix::doPreflight(PreflightContext const& ctx) +LedgerStateFix::preflight(PreflightContext const& ctx) { switch (ctx.tx[sfLedgerFixType]) { diff --git a/src/xrpld/app/tx/detail/LedgerStateFix.h b/src/xrpld/app/tx/detail/LedgerStateFix.h index e67dfd26c9..b93dcd6a00 100644 --- a/src/xrpld/app/tx/detail/LedgerStateFix.h +++ b/src/xrpld/app/tx/detail/LedgerStateFix.h @@ -41,7 +41,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp index f4a93ffb3e..f9dbb0995f 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp @@ -39,7 +39,7 @@ MPTokenAuthorize::getFlagsMask(PreflightContext const& ctx) } NotTEC -MPTokenAuthorize::doPreflight(PreflightContext const& ctx) +MPTokenAuthorize::preflight(PreflightContext const& ctx) { if (ctx.tx[sfAccount] == ctx.tx[~sfHolder]) return temMALFORMED; diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.h b/src/xrpld/app/tx/detail/MPTokenAuthorize.h index 63ca2c8b01..5c386db530 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.h +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.h @@ -49,7 +49,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp index 8f751015d8..f34b97a6b4 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp @@ -38,7 +38,7 @@ MPTokenIssuanceCreate::getFlagsMask(PreflightContext const& ctx) } NotTEC -MPTokenIssuanceCreate::doPreflight(PreflightContext const& ctx) +MPTokenIssuanceCreate::preflight(PreflightContext const& ctx) { if (auto const fee = ctx.tx[~sfTransferFee]) { diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h index 3df61d52ce..ce8f30fda9 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h @@ -56,7 +56,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); TER doApply() override; diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp index 7c0d5baf31..d52dcca8bf 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp @@ -38,7 +38,7 @@ MPTokenIssuanceDestroy::getFlagsMask(PreflightContext const& ctx) } NotTEC -MPTokenIssuanceDestroy::doPreflight(PreflightContext const& ctx) +MPTokenIssuanceDestroy::preflight(PreflightContext const& ctx) { return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h index 8718caf5b5..dd2131713b 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h @@ -40,7 +40,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp index a36df80d21..ca09ab9fe1 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp @@ -38,7 +38,7 @@ MPTokenIssuanceSet::getFlagsMask(PreflightContext const& ctx) } NotTEC -MPTokenIssuanceSet::doPreflight(PreflightContext const& ctx) +MPTokenIssuanceSet::preflight(PreflightContext const& ctx) { auto const txFlags = ctx.tx.getFlags(); diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h index ac2b2fa6da..55d6110fcd 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h @@ -40,7 +40,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER checkPermission(ReadView const& view, STTx const& tx); diff --git a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp index df56b0c763..6cb8e89c40 100644 --- a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp +++ b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp @@ -40,7 +40,7 @@ NFTokenAcceptOffer::getFlagsMask(PreflightContext const& ctx) } NotTEC -NFTokenAcceptOffer::doPreflight(PreflightContext const& ctx) +NFTokenAcceptOffer::preflight(PreflightContext const& ctx) { auto const bo = ctx.tx[~sfNFTokenBuyOffer]; auto const so = ctx.tx[~sfNFTokenSellOffer]; diff --git a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.h b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.h index 637944af92..b049aad46e 100644 --- a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.h +++ b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.h @@ -58,7 +58,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/NFTokenBurn.cpp b/src/xrpld/app/tx/detail/NFTokenBurn.cpp index 4948ef7f2a..80aec56af6 100644 --- a/src/xrpld/app/tx/detail/NFTokenBurn.cpp +++ b/src/xrpld/app/tx/detail/NFTokenBurn.cpp @@ -33,7 +33,7 @@ NFTokenBurn::isEnabled(PreflightContext const& ctx) } NotTEC -NFTokenBurn::doPreflight(PreflightContext const& ctx) +NFTokenBurn::preflight(PreflightContext const& ctx) { return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/NFTokenBurn.h b/src/xrpld/app/tx/detail/NFTokenBurn.h index ff930c1be7..f1ae0752db 100644 --- a/src/xrpld/app/tx/detail/NFTokenBurn.h +++ b/src/xrpld/app/tx/detail/NFTokenBurn.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/NFTokenCancelOffer.cpp b/src/xrpld/app/tx/detail/NFTokenCancelOffer.cpp index 5c6c406d2d..66edddcc82 100644 --- a/src/xrpld/app/tx/detail/NFTokenCancelOffer.cpp +++ b/src/xrpld/app/tx/detail/NFTokenCancelOffer.cpp @@ -41,7 +41,7 @@ NFTokenCancelOffer::getFlagsMask(PreflightContext const& ctx) } NotTEC -NFTokenCancelOffer::doPreflight(PreflightContext const& ctx) +NFTokenCancelOffer::preflight(PreflightContext const& ctx) { if (auto const& ids = ctx.tx[sfNFTokenOffers]; ids.empty() || (ids.size() > maxTokenOfferCancelCount)) diff --git a/src/xrpld/app/tx/detail/NFTokenCancelOffer.h b/src/xrpld/app/tx/detail/NFTokenCancelOffer.h index e112dce6f8..bdd628e58d 100644 --- a/src/xrpld/app/tx/detail/NFTokenCancelOffer.h +++ b/src/xrpld/app/tx/detail/NFTokenCancelOffer.h @@ -40,7 +40,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp b/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp index 46baaf7f49..76ebc9e24d 100644 --- a/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp +++ b/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp @@ -39,7 +39,7 @@ NFTokenCreateOffer::getFlagsMask(PreflightContext const& ctx) } NotTEC -NFTokenCreateOffer::doPreflight(PreflightContext const& ctx) +NFTokenCreateOffer::preflight(PreflightContext const& ctx) { auto const txFlags = ctx.tx.getFlags(); diff --git a/src/xrpld/app/tx/detail/NFTokenCreateOffer.h b/src/xrpld/app/tx/detail/NFTokenCreateOffer.h index fb1b6a575f..58946bad55 100644 --- a/src/xrpld/app/tx/detail/NFTokenCreateOffer.h +++ b/src/xrpld/app/tx/detail/NFTokenCreateOffer.h @@ -40,7 +40,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/NFTokenMint.cpp b/src/xrpld/app/tx/detail/NFTokenMint.cpp index 3dcad38ee1..db49e122e2 100644 --- a/src/xrpld/app/tx/detail/NFTokenMint.cpp +++ b/src/xrpld/app/tx/detail/NFTokenMint.cpp @@ -84,7 +84,7 @@ NFTokenMint::getFlagsMask(PreflightContext const& ctx) } NotTEC -NFTokenMint::doPreflight(PreflightContext const& ctx) +NFTokenMint::preflight(PreflightContext const& ctx) { if (auto const f = ctx.tx[~sfTransferFee]) { diff --git a/src/xrpld/app/tx/detail/NFTokenMint.h b/src/xrpld/app/tx/detail/NFTokenMint.h index 4af7448532..696ad856ab 100644 --- a/src/xrpld/app/tx/detail/NFTokenMint.h +++ b/src/xrpld/app/tx/detail/NFTokenMint.h @@ -43,7 +43,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/NFTokenModify.cpp b/src/xrpld/app/tx/detail/NFTokenModify.cpp index 54f26387b1..98cbd5760a 100644 --- a/src/xrpld/app/tx/detail/NFTokenModify.cpp +++ b/src/xrpld/app/tx/detail/NFTokenModify.cpp @@ -33,7 +33,7 @@ NFTokenModify::isEnabled(PreflightContext const& ctx) } NotTEC -NFTokenModify::doPreflight(PreflightContext const& ctx) +NFTokenModify::preflight(PreflightContext const& ctx) { if (auto owner = ctx.tx[~sfOwner]; owner == ctx.tx[sfAccount]) return temMALFORMED; diff --git a/src/xrpld/app/tx/detail/NFTokenModify.h b/src/xrpld/app/tx/detail/NFTokenModify.h index a0936c6af1..8a80e422c5 100644 --- a/src/xrpld/app/tx/detail/NFTokenModify.h +++ b/src/xrpld/app/tx/detail/NFTokenModify.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/PayChan.cpp b/src/xrpld/app/tx/detail/PayChan.cpp index 40c3a22871..d90a661321 100644 --- a/src/xrpld/app/tx/detail/PayChan.cpp +++ b/src/xrpld/app/tx/detail/PayChan.cpp @@ -179,7 +179,7 @@ PayChanCreate::getFlagsMask(PreflightContext const& ctx) } NotTEC -PayChanCreate::doPreflight(PreflightContext const& ctx) +PayChanCreate::preflight(PreflightContext const& ctx) { if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero)) return temBAD_AMOUNT; @@ -335,7 +335,7 @@ PayChanFund::getFlagsMask(PreflightContext const& ctx) } NotTEC -PayChanFund::doPreflight(PreflightContext const& ctx) +PayChanFund::preflight(PreflightContext const& ctx) { if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero)) return temBAD_AMOUNT; @@ -433,7 +433,7 @@ PayChanClaim::getFlagsMask(PreflightContext const& ctx) } NotTEC -PayChanClaim::doPreflight(PreflightContext const& ctx) +PayChanClaim::preflight(PreflightContext const& ctx) { auto const bal = ctx.tx[~sfBalance]; if (bal && (!isXRP(*bal) || *bal <= beast::zero)) diff --git a/src/xrpld/app/tx/detail/PayChan.h b/src/xrpld/app/tx/detail/PayChan.h index 1faa34499b..9390aa52b9 100644 --- a/src/xrpld/app/tx/detail/PayChan.h +++ b/src/xrpld/app/tx/detail/PayChan.h @@ -40,7 +40,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -69,7 +69,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); TER doApply() override; @@ -95,7 +95,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index 34ba3b94f6..da5e185770 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -90,7 +90,7 @@ Payment::getFlagsMask(PreflightContext const& ctx) } NotTEC -Payment::doPreflight(PreflightContext const& ctx) +Payment::preflight(PreflightContext const& ctx) { auto& tx = ctx.tx; auto& j = ctx.j; diff --git a/src/xrpld/app/tx/detail/Payment.h b/src/xrpld/app/tx/detail/Payment.h index 577b7827ae..3f16dd97bc 100644 --- a/src/xrpld/app/tx/detail/Payment.h +++ b/src/xrpld/app/tx/detail/Payment.h @@ -49,7 +49,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER checkPermission(ReadView const& view, STTx const& tx); diff --git a/src/xrpld/app/tx/detail/PermissionedDomainDelete.cpp b/src/xrpld/app/tx/detail/PermissionedDomainDelete.cpp index d89831dfe0..6087bae8ad 100644 --- a/src/xrpld/app/tx/detail/PermissionedDomainDelete.cpp +++ b/src/xrpld/app/tx/detail/PermissionedDomainDelete.cpp @@ -31,7 +31,7 @@ PermissionedDomainDelete::isEnabled(PreflightContext const& ctx) } NotTEC -PermissionedDomainDelete::doPreflight(PreflightContext const& ctx) +PermissionedDomainDelete::preflight(PreflightContext const& ctx) { auto const domain = ctx.tx.getFieldH256(sfDomainID); if (domain == beast::zero) diff --git a/src/xrpld/app/tx/detail/PermissionedDomainDelete.h b/src/xrpld/app/tx/detail/PermissionedDomainDelete.h index 574a99bffa..403d0786e3 100644 --- a/src/xrpld/app/tx/detail/PermissionedDomainDelete.h +++ b/src/xrpld/app/tx/detail/PermissionedDomainDelete.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/PermissionedDomainSet.cpp b/src/xrpld/app/tx/detail/PermissionedDomainSet.cpp index 98aedb7304..8e27fdddd6 100644 --- a/src/xrpld/app/tx/detail/PermissionedDomainSet.cpp +++ b/src/xrpld/app/tx/detail/PermissionedDomainSet.cpp @@ -36,7 +36,7 @@ PermissionedDomainSet::isEnabled(PreflightContext const& ctx) } NotTEC -PermissionedDomainSet::doPreflight(PreflightContext const& ctx) +PermissionedDomainSet::preflight(PreflightContext const& ctx) { if (auto err = credentials::checkArray( ctx.tx.getFieldArray(sfAcceptedCredentials), diff --git a/src/xrpld/app/tx/detail/PermissionedDomainSet.h b/src/xrpld/app/tx/detail/PermissionedDomainSet.h index 94761bae26..01e4bb86d0 100644 --- a/src/xrpld/app/tx/detail/PermissionedDomainSet.h +++ b/src/xrpld/app/tx/detail/PermissionedDomainSet.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/SetAccount.cpp b/src/xrpld/app/tx/detail/SetAccount.cpp index 9d39670130..1641027a8e 100644 --- a/src/xrpld/app/tx/detail/SetAccount.cpp +++ b/src/xrpld/app/tx/detail/SetAccount.cpp @@ -64,7 +64,7 @@ SetAccount::getFlagsMask(PreflightContext const& ctx) } NotTEC -SetAccount::doPreflight(PreflightContext const& ctx) +SetAccount::preflight(PreflightContext const& ctx) { auto& tx = ctx.tx; auto& j = ctx.j; diff --git a/src/xrpld/app/tx/detail/SetAccount.h b/src/xrpld/app/tx/detail/SetAccount.h index a16d2e0cdf..bcc0a61b1b 100644 --- a/src/xrpld/app/tx/detail/SetAccount.h +++ b/src/xrpld/app/tx/detail/SetAccount.h @@ -42,7 +42,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER checkPermission(ReadView const& view, STTx const& tx); diff --git a/src/xrpld/app/tx/detail/SetOracle.cpp b/src/xrpld/app/tx/detail/SetOracle.cpp index 4f6b9b700e..411f4b6e49 100644 --- a/src/xrpld/app/tx/detail/SetOracle.cpp +++ b/src/xrpld/app/tx/detail/SetOracle.cpp @@ -43,7 +43,7 @@ SetOracle::isEnabled(PreflightContext const& ctx) } NotTEC -SetOracle::doPreflight(PreflightContext const& ctx) +SetOracle::preflight(PreflightContext const& ctx) { auto const& dataSeries = ctx.tx.getFieldArray(sfPriceDataSeries); if (dataSeries.empty()) diff --git a/src/xrpld/app/tx/detail/SetOracle.h b/src/xrpld/app/tx/detail/SetOracle.h index a35ef74c4c..2da10105b2 100644 --- a/src/xrpld/app/tx/detail/SetOracle.h +++ b/src/xrpld/app/tx/detail/SetOracle.h @@ -46,7 +46,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/SetRegularKey.cpp b/src/xrpld/app/tx/detail/SetRegularKey.cpp index f4d04ec1c1..4e063e7d1f 100644 --- a/src/xrpld/app/tx/detail/SetRegularKey.cpp +++ b/src/xrpld/app/tx/detail/SetRegularKey.cpp @@ -49,7 +49,7 @@ SetRegularKey::calculateBaseFee(ReadView const& view, STTx const& tx) } NotTEC -SetRegularKey::doPreflight(PreflightContext const& ctx) +SetRegularKey::preflight(PreflightContext const& ctx) { if (ctx.rules.enabled(fixMasterKeyAsRegularKey) && ctx.tx.isFieldPresent(sfRegularKey) && diff --git a/src/xrpld/app/tx/detail/SetRegularKey.h b/src/xrpld/app/tx/detail/SetRegularKey.h index 00b9e47169..055748aa25 100644 --- a/src/xrpld/app/tx/detail/SetRegularKey.h +++ b/src/xrpld/app/tx/detail/SetRegularKey.h @@ -34,7 +34,7 @@ public: } static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); diff --git a/src/xrpld/app/tx/detail/SetSignerList.cpp b/src/xrpld/app/tx/detail/SetSignerList.cpp index 191b947560..a2b310bbd6 100644 --- a/src/xrpld/app/tx/detail/SetSignerList.cpp +++ b/src/xrpld/app/tx/detail/SetSignerList.cpp @@ -85,7 +85,7 @@ SetSignerList::getFlagsMask(PreflightContext const& ctx) } NotTEC -SetSignerList::doPreflight(PreflightContext const& ctx) +SetSignerList::preflight(PreflightContext const& ctx) { auto const result = determineOperation(ctx.tx, ctx.flags, ctx.j); diff --git a/src/xrpld/app/tx/detail/SetSignerList.h b/src/xrpld/app/tx/detail/SetSignerList.h index 575db5a127..be2df8152e 100644 --- a/src/xrpld/app/tx/detail/SetSignerList.h +++ b/src/xrpld/app/tx/detail/SetSignerList.h @@ -55,7 +55,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); TER doApply() override; diff --git a/src/xrpld/app/tx/detail/SetTrust.cpp b/src/xrpld/app/tx/detail/SetTrust.cpp index 72a6cc0efa..210cff33c8 100644 --- a/src/xrpld/app/tx/detail/SetTrust.cpp +++ b/src/xrpld/app/tx/detail/SetTrust.cpp @@ -74,7 +74,7 @@ SetTrust::getFlagsMask(PreflightContext const& ctx) } NotTEC -SetTrust::doPreflight(PreflightContext const& ctx) +SetTrust::preflight(PreflightContext const& ctx) { auto& tx = ctx.tx; auto& j = ctx.j; diff --git a/src/xrpld/app/tx/detail/SetTrust.h b/src/xrpld/app/tx/detail/SetTrust.h index d7ca54d1dc..443080bf74 100644 --- a/src/xrpld/app/tx/detail/SetTrust.h +++ b/src/xrpld/app/tx/detail/SetTrust.h @@ -39,7 +39,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER checkPermission(ReadView const& view, STTx const& tx); diff --git a/src/xrpld/app/tx/detail/Transactor.h b/src/xrpld/app/tx/detail/Transactor.h index 9dcac97269..5c2ba136d7 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -200,8 +200,8 @@ public: static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); - /* Do NOT define a preflight function in a derived class. - Instead, define + /* Do NOT define an invokePreflight function in a derived class. + Instead, define: // Optional if the transaction is gated on an amendment static bool @@ -211,12 +211,18 @@ public: static std::uint32_t getFlagsMask(PreflightContext const& ctx); + // Required, even if it just returns tesSUCCESS. static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); + + * Do not try to call preflight1 or preflight2 directly. + * Do not check whether relevant amendments are enabled in preflight. + Instead, define isEnabled. + * Do not check flags in preflight. Instead, define getFlagsMask. */ template static NotTEC - preflight(PreflightContext const& ctx); + invokePreflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx) @@ -317,15 +323,21 @@ private: void trapTransaction(uint256) const; - // Helper functions for preflight checks. Do not use directly. /** Performs early sanity checks on the account and fee fields. (And passes flagMask to preflight0) + + Do not try to call preflight1 from preflight() in derived classes. See + the description of invokePreflight for details. */ static NotTEC preflight1(PreflightContext const& ctx, std::uint32_t flagMask); - /** Checks whether the signature appears valid */ + /** Checks whether the signature appears valid + + Do not try to call preflight2 from preflight() in derived classes. See + the description of invokePreflight for details. + */ static NotTEC preflight2(PreflightContext const& ctx); }; @@ -363,11 +375,11 @@ preflightCheckSimulateKeys( // Defined in Change.cpp template <> NotTEC -Transactor::preflight(PreflightContext const& ctx); +Transactor::invokePreflight(PreflightContext const& ctx); template NotTEC -Transactor::preflight(PreflightContext const& ctx) +Transactor::invokePreflight(PreflightContext const& ctx) { if (!T::isEnabled(ctx)) return temDISABLED; @@ -375,7 +387,7 @@ Transactor::preflight(PreflightContext const& ctx) if (auto const ret = preflight1(ctx, T::getFlagsMask(ctx))) return ret; - if (auto const ret = T::doPreflight(ctx)) + if (auto const ret = T::preflight(ctx)) return ret; return preflight2(ctx); diff --git a/src/xrpld/app/tx/detail/VaultClawback.cpp b/src/xrpld/app/tx/detail/VaultClawback.cpp index 39d068e58d..7b4d15c592 100644 --- a/src/xrpld/app/tx/detail/VaultClawback.cpp +++ b/src/xrpld/app/tx/detail/VaultClawback.cpp @@ -37,7 +37,7 @@ VaultClawback::isEnabled(PreflightContext const& ctx) } NotTEC -VaultClawback::doPreflight(PreflightContext const& ctx) +VaultClawback::preflight(PreflightContext const& ctx) { if (ctx.tx[sfVaultID] == beast::zero) { diff --git a/src/xrpld/app/tx/detail/VaultClawback.h b/src/xrpld/app/tx/detail/VaultClawback.h index 58ee97db46..58a7e738c3 100644 --- a/src/xrpld/app/tx/detail/VaultClawback.h +++ b/src/xrpld/app/tx/detail/VaultClawback.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/VaultCreate.cpp b/src/xrpld/app/tx/detail/VaultCreate.cpp index 835201cb5f..569c462e7a 100644 --- a/src/xrpld/app/tx/detail/VaultCreate.cpp +++ b/src/xrpld/app/tx/detail/VaultCreate.cpp @@ -51,7 +51,7 @@ VaultCreate::getFlagsMask(PreflightContext const& ctx) } NotTEC -VaultCreate::doPreflight(PreflightContext const& ctx) +VaultCreate::preflight(PreflightContext const& ctx) { if (!validDataLength(ctx.tx[~sfData], maxDataPayloadLength)) return temMALFORMED; diff --git a/src/xrpld/app/tx/detail/VaultCreate.h b/src/xrpld/app/tx/detail/VaultCreate.h index effb7cd475..54074c4bce 100644 --- a/src/xrpld/app/tx/detail/VaultCreate.h +++ b/src/xrpld/app/tx/detail/VaultCreate.h @@ -40,7 +40,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); diff --git a/src/xrpld/app/tx/detail/VaultDelete.cpp b/src/xrpld/app/tx/detail/VaultDelete.cpp index 71f1f86bae..02b63c145d 100644 --- a/src/xrpld/app/tx/detail/VaultDelete.cpp +++ b/src/xrpld/app/tx/detail/VaultDelete.cpp @@ -34,7 +34,7 @@ VaultDelete::isEnabled(PreflightContext const& ctx) } NotTEC -VaultDelete::doPreflight(PreflightContext const& ctx) +VaultDelete::preflight(PreflightContext const& ctx) { if (ctx.tx[sfVaultID] == beast::zero) { diff --git a/src/xrpld/app/tx/detail/VaultDelete.h b/src/xrpld/app/tx/detail/VaultDelete.h index aa32db48ab..f8b838e02e 100644 --- a/src/xrpld/app/tx/detail/VaultDelete.h +++ b/src/xrpld/app/tx/detail/VaultDelete.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/VaultDeposit.cpp b/src/xrpld/app/tx/detail/VaultDeposit.cpp index 847ba06954..162a314ff2 100644 --- a/src/xrpld/app/tx/detail/VaultDeposit.cpp +++ b/src/xrpld/app/tx/detail/VaultDeposit.cpp @@ -39,7 +39,7 @@ VaultDeposit::isEnabled(PreflightContext const& ctx) } NotTEC -VaultDeposit::doPreflight(PreflightContext const& ctx) +VaultDeposit::preflight(PreflightContext const& ctx) { if (ctx.tx[sfVaultID] == beast::zero) { diff --git a/src/xrpld/app/tx/detail/VaultDeposit.h b/src/xrpld/app/tx/detail/VaultDeposit.h index 79cd8f4a02..e804520595 100644 --- a/src/xrpld/app/tx/detail/VaultDeposit.h +++ b/src/xrpld/app/tx/detail/VaultDeposit.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/VaultSet.cpp b/src/xrpld/app/tx/detail/VaultSet.cpp index feec2eb160..30a99fe153 100644 --- a/src/xrpld/app/tx/detail/VaultSet.cpp +++ b/src/xrpld/app/tx/detail/VaultSet.cpp @@ -41,7 +41,7 @@ VaultSet::isEnabled(PreflightContext const& ctx) } NotTEC -VaultSet::doPreflight(PreflightContext const& ctx) +VaultSet::preflight(PreflightContext const& ctx) { if (ctx.tx[sfVaultID] == beast::zero) { diff --git a/src/xrpld/app/tx/detail/VaultSet.h b/src/xrpld/app/tx/detail/VaultSet.h index 2e8004bec1..bce0077802 100644 --- a/src/xrpld/app/tx/detail/VaultSet.h +++ b/src/xrpld/app/tx/detail/VaultSet.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.cpp b/src/xrpld/app/tx/detail/VaultWithdraw.cpp index 32ae9fce1c..f628c0aa32 100644 --- a/src/xrpld/app/tx/detail/VaultWithdraw.cpp +++ b/src/xrpld/app/tx/detail/VaultWithdraw.cpp @@ -37,7 +37,7 @@ VaultWithdraw::isEnabled(PreflightContext const& ctx) } NotTEC -VaultWithdraw::doPreflight(PreflightContext const& ctx) +VaultWithdraw::preflight(PreflightContext const& ctx) { if (ctx.tx[sfVaultID] == beast::zero) { diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.h b/src/xrpld/app/tx/detail/VaultWithdraw.h index 9ac9e420ea..f01a8e4c9d 100644 --- a/src/xrpld/app/tx/detail/VaultWithdraw.h +++ b/src/xrpld/app/tx/detail/VaultWithdraw.h @@ -37,7 +37,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/XChainBridge.cpp b/src/xrpld/app/tx/detail/XChainBridge.cpp index 93619e620c..fb35f9b2ad 100644 --- a/src/xrpld/app/tx/detail/XChainBridge.cpp +++ b/src/xrpld/app/tx/detail/XChainBridge.cpp @@ -1210,7 +1210,7 @@ toClaim(STTx const& tx) template NotTEC -attestationDoPreflight(PreflightContext const& ctx) +attestationpreflight(PreflightContext const& ctx) { if (!publicKeyType(ctx.tx[sfPublicKey])) return temMALFORMED; @@ -1374,7 +1374,7 @@ XChainCreateBridge::isEnabled(PreflightContext const& ctx) } NotTEC -XChainCreateBridge::doPreflight(PreflightContext const& ctx) +XChainCreateBridge::preflight(PreflightContext const& ctx) { auto const account = ctx.tx[sfAccount]; auto const reward = ctx.tx[sfSignatureReward]; @@ -1558,7 +1558,7 @@ BridgeModify::getFlagsMask(PreflightContext const& ctx) } NotTEC -BridgeModify::doPreflight(PreflightContext const& ctx) +BridgeModify::preflight(PreflightContext const& ctx) { auto const account = ctx.tx[sfAccount]; auto const reward = ctx.tx[~sfSignatureReward]; @@ -1665,7 +1665,7 @@ XChainClaim::isEnabled(PreflightContext const& ctx) } NotTEC -XChainClaim::doPreflight(PreflightContext const& ctx) +XChainClaim::preflight(PreflightContext const& ctx) { STXChainBridge const bridgeSpec = ctx.tx[sfXChainBridge]; auto const amount = ctx.tx[sfAmount]; @@ -1900,7 +1900,7 @@ XChainCommit::isEnabled(PreflightContext const& ctx) } NotTEC -XChainCommit::doPreflight(PreflightContext const& ctx) +XChainCommit::preflight(PreflightContext const& ctx) { auto const amount = ctx.tx[sfAmount]; auto const bridgeSpec = ctx.tx[sfXChainBridge]; @@ -2011,7 +2011,7 @@ XChainCreateClaimID::isEnabled(PreflightContext const& ctx) } NotTEC -XChainCreateClaimID::doPreflight(PreflightContext const& ctx) +XChainCreateClaimID::preflight(PreflightContext const& ctx) { auto const reward = ctx.tx[sfSignatureReward]; @@ -2123,9 +2123,9 @@ XChainAddClaimAttestation::isEnabled(PreflightContext const& ctx) } NotTEC -XChainAddClaimAttestation::doPreflight(PreflightContext const& ctx) +XChainAddClaimAttestation::preflight(PreflightContext const& ctx) { - return attestationDoPreflight(ctx); + return attestationpreflight(ctx); } TER @@ -2149,9 +2149,9 @@ XChainAddAccountCreateAttestation::isEnabled(PreflightContext const& ctx) } NotTEC -XChainAddAccountCreateAttestation::doPreflight(PreflightContext const& ctx) +XChainAddAccountCreateAttestation::preflight(PreflightContext const& ctx) { - return attestationDoPreflight(ctx); + return attestationpreflight(ctx); } TER @@ -2175,7 +2175,7 @@ XChainCreateAccountCommit::isEnabled(PreflightContext const& ctx) } NotTEC -XChainCreateAccountCommit::doPreflight(PreflightContext const& ctx) +XChainCreateAccountCommit::preflight(PreflightContext const& ctx) { auto const amount = ctx.tx[sfAmount]; diff --git a/src/xrpld/app/tx/detail/XChainBridge.h b/src/xrpld/app/tx/detail/XChainBridge.h index b116418023..e210169bcf 100644 --- a/src/xrpld/app/tx/detail/XChainBridge.h +++ b/src/xrpld/app/tx/detail/XChainBridge.h @@ -43,7 +43,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -68,7 +68,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -104,7 +104,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -134,7 +134,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -166,7 +166,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -198,7 +198,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -222,7 +222,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); @@ -269,7 +269,7 @@ public: isEnabled(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index fbb117c372..c2e4e13f08 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -119,7 +119,7 @@ invoke_preflight(PreflightContext const& ctx) try { return with_txn_type(ctx.tx.getTxnType(), [&]() { - auto const tec = Transactor::preflight(ctx); + auto const tec = Transactor::invokePreflight(ctx); return std::make_pair( tec, isTesSuccess(tec) ? consequences_helper(ctx) From 24447969bee4017b21ddf10a6b5713bd0c67e0de Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 22 Jul 2025 12:43:54 -0400 Subject: [PATCH 033/291] fixup! Rename Transactor preflight functions --- src/xrpld/app/tx/detail/Batch.cpp | 2 +- src/xrpld/app/tx/detail/Batch.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/xrpld/app/tx/detail/Batch.cpp b/src/xrpld/app/tx/detail/Batch.cpp index 00595f55a1..5f05d9919e 100644 --- a/src/xrpld/app/tx/detail/Batch.cpp +++ b/src/xrpld/app/tx/detail/Batch.cpp @@ -210,7 +210,7 @@ Batch::getFlagsMask(PreflightContext const& ctx) * code otherwise. */ NotTEC -Batch::doPreflight(PreflightContext const& ctx) +Batch::preflight(PreflightContext const& ctx) { auto const parentBatchId = ctx.tx.getTransactionID(); auto const outerAccount = ctx.tx.getAccountID(sfAccount); diff --git a/src/xrpld/app/tx/detail/Batch.h b/src/xrpld/app/tx/detail/Batch.h index 858a3d9c3d..c8a264f328 100644 --- a/src/xrpld/app/tx/detail/Batch.h +++ b/src/xrpld/app/tx/detail/Batch.h @@ -47,7 +47,7 @@ public: getFlagsMask(PreflightContext const& ctx); static NotTEC - doPreflight(PreflightContext const& ctx); + preflight(PreflightContext const& ctx); static NotTEC checkSign(PreclaimContext const& ctx); From 4550d2bfbadccfa08df7563f141f895ec793dedd Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 22 Jul 2025 14:06:17 -0400 Subject: [PATCH 034/291] Fix build error --- src/test/app/LoanBroker_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index cf7f26267f..97e456903d 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -155,8 +155,8 @@ class LoanBroker_test : public beast::unit_test::suite MPTTester badMptt{env, evan, mptInitNoFund}; badMptt.create( {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); - return badMptt["BAD"]; env.close(); + return badMptt["BAD"]; }(); static PrettyAsset const badIouAsset = evan["BAD"]; static Account const nonExistent{"NonExistent"}; From 1e7462606c14a7b0c1141ca310cc40126e13068f Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 23 Jul 2025 17:24:53 -0400 Subject: [PATCH 035/291] Fix divide by 0 error on 0-interest loans; allow broker to self-lend * Unit tests for self lending --- src/test/app/Loan_test.cpp | 211 +++++++++++++++---- src/xrpld/app/misc/detail/LendingHelpers.cpp | 7 + src/xrpld/app/tx/detail/LoanSet.cpp | 6 - 3 files changed, 175 insertions(+), 49 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 497329e66d..aa30aeb0be 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -331,6 +331,53 @@ class Loan_test : public beast::unit_test::suite }; }; + BrokerInfo + createVaultAndBroker( + jtx::Env& env, + jtx::PrettyAsset const& asset, + jtx::Account const& lender) + { + using namespace jtx; + + Vault vault{env}; + + auto const deposit = asset(vaultDeposit); + auto const debtMaximumValue = asset(debtMaximumParameter).value(); + auto const coverDepositValue = asset(coverDepositParameter).value(); + + auto [tx, vaultKeylet] = + vault.create({.owner = lender, .asset = asset}); + env(tx); + env.close(); + BEAST_EXPECT(env.le(vaultKeylet)); + + env(vault.deposit( + {.depositor = lender, .id = vaultKeylet.key, .amount = deposit})); + env.close(); + if (auto const vault = env.le(keylet::vault(vaultKeylet.key)); + BEAST_EXPECT(vault)) + { + BEAST_EXPECT(vault->at(sfAssetsAvailable) == deposit.value()); + } + + auto const keylet = keylet::loanbroker(lender.id(), env.seq(lender)); + auto const testData = "spam spam spam spam"; + + using namespace loanBroker; + env(set(lender, vaultKeylet.key), + data(testData), + managementFeeRate(TenthBips16(100)), + debtMaximum(debtMaximumValue), + coverRateMinimum(TenthBips32(coverRateMinParameter)), + coverRateLiquidation(TenthBips32(percentageToTenthBips(25)))); + + env(coverDeposit(lender, keylet.key, coverDepositValue)); + + env.close(); + + return {asset, keylet.key}; + } + void lifecycle( std::string const& caseLabel, @@ -816,11 +863,6 @@ class Loan_test : public beast::unit_test::suite sig(sfCounterpartySignature, borrower), loanSetFee, ter(tecNO_PERMISSION)); - // can not lend money to yourself - env(set(lender, broker.brokerID, principalRequest, startDate), - sig(sfCounterpartySignature, lender), - loanSetFee, - ter(tecNO_PERMISSION)); // not a LoanBroker object, no counterparty env(set(lender, badKeylet.key, principalRequest, startDate), sig(sfCounterpartySignature, evan), @@ -1646,7 +1688,6 @@ class Loan_test : public beast::unit_test::suite Account const evan{"evan"}; // Do not fund alice Account const alice{"alice"}; - Vault vault{env}; // Fund the accounts and trust lines with the same amount so that tests // can use the same values regardless of the asset. @@ -1684,43 +1725,7 @@ class Loan_test : public beast::unit_test::suite std::vector brokers; for (auto const& asset : assets) { - auto const deposit = asset(vaultDeposit); - auto const debtMaximumValue = asset(debtMaximumParameter).value(); - auto const coverDepositValue = asset(coverDepositParameter).value(); - - auto [tx, vaultKeylet] = - vault.create({.owner = lender, .asset = asset}); - env(tx); - env.close(); - BEAST_EXPECT(env.le(vaultKeylet)); - - env(vault.deposit( - {.depositor = lender, - .id = vaultKeylet.key, - .amount = deposit})); - env.close(); - if (auto const vault = env.le(keylet::vault(vaultKeylet.key)); - BEAST_EXPECT(vault)) - { - BEAST_EXPECT(vault->at(sfAssetsAvailable) == deposit.value()); - } - - auto const keylet = - keylet::loanbroker(lender.id(), env.seq(lender)); - auto const testData = "spam spam spam spam"; - - using namespace loanBroker; - env(set(lender, vaultKeylet.key), - fee(increment), - data(testData), - managementFeeRate(TenthBips16(100)), - debtMaximum(debtMaximumValue), - coverRateMinimum(TenthBips32(coverRateMinParameter)), - coverRateLiquidation(TenthBips32(percentageToTenthBips(25)))); - - env(coverDeposit(lender, keylet.key, coverDepositValue)); - - brokers.emplace_back(asset, keylet.key); + brokers.emplace_back(createVaultAndBroker(env, asset, lender)); } // Create and update Loans @@ -1764,11 +1769,131 @@ class Loan_test : public beast::unit_test::suite } } + void + testSelfLoan() + { + testcase << "Self Loan"; + + using namespace jtx; + using namespace std::chrono_literals; + // Create 3 loan brokers: one for XRP, one for an IOU, and one for an + // MPT. That'll require three corresponding SAVs. + Env env(*this, all); + + Account const issuer{"issuer"}; + // For simplicity, lender will be the sole actor for the vault & + // brokers. + Account const lender{"lender"}; + + // Fund the accounts and trust lines with the same amount so that tests + // can use the same values regardless of the asset. + env.fund(XRP(100'000), issuer, noripple(lender)); + env.close(); + + // Use an XRP asset for simplicity + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + + // Create vaults and loan brokers + BrokerInfo broker{createVaultAndBroker(env, xrpAsset, lender)}; + + using namespace loan; + + auto const loanSetFee = fee(env.current()->fees().base * 2); + Number const principalRequest{1, 3}; + auto const startDate = env.now() + 60s; + + // The LoanSet json can be created without a counterparty signature, but + // it is malformed. + auto createJson = env.json( + set(lender, broker.brokerID, principalRequest, startDate), + fee(loanSetFee)); + + env(createJson, ter(temMALFORMED)); + + // Adding an empty counterparty signature object is also malformed, but + // fails at the RPC level. + createJson = env.json( + createJson, json(sfCounterpartySignature, Json::objectValue)); + + env(createJson, ter(telENV_RPC_FAILED)); + + // Copy the transaction signature into the counterparty signature. + Json::Value counterpartyJson{Json::objectValue}; + counterpartyJson[sfTxnSignature] = createJson[sfTxnSignature]; + counterpartyJson[sfSigningPubKey] = createJson[sfSigningPubKey]; + if (!BEAST_EXPECT(!createJson.isMember(jss::Signers))) + counterpartyJson[sfSigners] = createJson[sfSigners]; + + // The duplicated signature works + createJson = env.json( + createJson, json(sfCounterpartySignature, counterpartyJson)); + env(createJson); + + env.close(); + + // Loan is successfully created + { + auto const res = env.rpc("account_objects", lender.human()); + auto const objects = res[jss::result][jss::account_objects]; + + std::map types; + BEAST_EXPECT(objects.size() == 4); + for (auto const& object : objects) + { + ++types[object[sfLedgerEntryType].asString()]; + } + BEAST_EXPECT(types.size() == 4); + for (std::string const& type : + {"MPToken", "Vault", "LoanBroker", "Loan"}) + { + BEAST_EXPECT(types[type] == 1); + } + } + { + Json::Value params(Json::objectValue); + params[jss::account] = lender.human(); + params[jss::type] = "Loan"; + auto const res = + env.rpc("json", "account_objects", to_string(params)); + auto const objects = res[jss::result][jss::account_objects]; + + BEAST_EXPECT(objects.size() == 1); + + auto const loan = objects[0u]; + BEAST_EXPECT(loan[sfAssetsAvailable] == "1000"); + BEAST_EXPECT(loan[sfBorrower] == lender.human()); + BEAST_EXPECT(loan[sfCloseInterestRate] == 0); + BEAST_EXPECT(loan[sfClosePaymentFee] == "0"); + BEAST_EXPECT(loan[sfFlags] == 0); + BEAST_EXPECT(loan[sfGracePeriod] == 60); + BEAST_EXPECT(loan[sfInterestRate] == 0); + BEAST_EXPECT(loan[sfLateInterestRate] == 0); + BEAST_EXPECT(loan[sfLatePaymentFee] == "0"); + BEAST_EXPECT(loan[sfLoanBrokerID] == to_string(broker.brokerID)); + BEAST_EXPECT(loan[sfLoanOriginationFee] == "0"); + BEAST_EXPECT(loan[sfLoanSequence] == 1); + BEAST_EXPECT(loan[sfLoanServiceFee] == "0"); + BEAST_EXPECT( + loan[sfNextPaymentDueDate] == loan[sfStartDate].asUInt() + 60); + BEAST_EXPECT(loan[sfOverpaymentFee] == 0); + BEAST_EXPECT(loan[sfOverpaymentInterestRate] == 0); + BEAST_EXPECT(loan[sfPaymentInterval] == 60); + BEAST_EXPECT(loan[sfPaymentRemaining] == 1); + BEAST_EXPECT(loan[sfPreviousPaymentDate] == 0); + BEAST_EXPECT(loan[sfPrincipalOutstanding] == "1000"); + BEAST_EXPECT(loan[sfPrincipalRequested] == "1000"); + BEAST_EXPECT( + loan[sfStartDate].asUInt() == + startDate.time_since_epoch().count()); + } + } + public: void run() override { testDisabled(); + testSelfLoan(); testLifecycle(); } }; diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index ecdcaa13ee..9ade4883a0 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -48,6 +48,13 @@ loanPeriodicPayment( Number periodicRate, std::uint32_t paymentsRemaining) { + if (principalOutstanding == 0 || paymentsRemaining == 0) + return 0; + + // Special case for interest free loans - equal payments of the principal. + if (periodicRate == beast::zero) + return principalOutstanding / paymentsRemaining; + // TODO: Need a better name Number const timeFactor = power(1 + periodicRate, paymentsRemaining); diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index b42cdb1cbc..1ef84c0346 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -177,12 +177,6 @@ LoanSet::preclaim(PreclaimContext const& ctx) "of the LoanBroker."; return tecNO_PERMISSION; } - if (account == counterparty) - { - JLOG(ctx.j.warn()) << "Account and Counterparty are the same. Can not " - "loan money to yourself."; - return tecNO_PERMISSION; - } auto const borrower = counterparty == brokerOwner ? account : counterparty; if (auto const borrowerSle = ctx.view.read(keylet::account(borrower)); From 1adb410e8e6d3bdef3c41eade2a1dcc6f8502549 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 24 Jul 2025 13:58:05 -0400 Subject: [PATCH 036/291] fixup! Fix divide by 0 error on 0-interest loans; allow broker to self-lend --- src/test/app/Loan_test.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index aa30aeb0be..c9a6a86e05 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include From 57c78c56bade3b57dffde064bfc6a610e4d94ae1 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 24 Jul 2025 14:41:25 -0400 Subject: [PATCH 037/291] fixup! fixup! Fix divide by 0 error on 0-interest loans; allow broker to self-lend --- src/test/app/Loan_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index c9a6a86e05..57a863a4ad 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -1844,7 +1844,7 @@ class Loan_test : public beast::unit_test::suite ++types[object[sfLedgerEntryType].asString()]; } BEAST_EXPECT(types.size() == 4); - for (std::string const& type : + for (std::string const type : {"MPToken", "Vault", "LoanBroker", "Loan"}) { BEAST_EXPECT(types[type] == 1); From a2be55fbc92b24adcde9f3bc375807b39e151759 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 28 Jul 2025 19:02:52 -0400 Subject: [PATCH 038/291] Check LoanBrokerCoverWithdraw Destination and DestinationTag fields - See also #5572 / e7a7bb8 --- src/test/app/LoanBroker_test.cpp | 22 ++++++++++++++++++- .../app/tx/detail/LoanBrokerCoverWithdraw.cpp | 14 ++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 97e456903d..9f10db22d6 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include @@ -399,6 +400,17 @@ class LoanBroker_test : public beast::unit_test::suite destination(bystander), ter(expected)); } + + // Can not withdraw to the zero address + env(coverWithdraw(alice, keylet.key, vault.asset(1)), + destination(AccountID{}), + ter(temMALFORMED)); + + // If a destination tag is specified, a destination must be + // specified, too + env(coverWithdraw(alice, keylet.key, vault.asset(1)), + dtag(123), + ter(temMALFORMED)); verifyCoverAmount(10); // Withdraw some of the cover amount @@ -413,9 +425,17 @@ class LoanBroker_test : public beast::unit_test::suite // Withdraw some more. Send it to Evan. Very generous, considering // how much trouble he's been. - env(coverWithdraw(alice, keylet.key, vault.asset(2)), + env(coverWithdraw(alice, keylet.key, vault.asset(1)), destination(evan)); env.close(); + verifyCoverAmount(7); + + // Withdraw some more. Send it to Evan. Very generous, considering + // how much trouble he's been. + env(coverWithdraw(alice, keylet.key, vault.asset(1)), + destination(evan), + dtag(3)); + env.close(); verifyCoverAmount(6); if (!vault.asset.raw().native()) diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp index 5ec059d685..1f827cc7b8 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp @@ -63,10 +63,20 @@ LoanBrokerCoverWithdraw::preflight(PreflightContext const& ctx) return temBAD_AMOUNT; if (auto const destination = ctx.tx[~sfDestination]; - destination && *destination == beast::zero) + destination.has_value()) + { + if (*destination == beast::zero) + { + JLOG(ctx.j.debug()) + << "LoanBrokerCoverWithdraw: zero/empty destination account."; + return temMALFORMED; + } + } + else if (ctx.tx.isFieldPresent(sfDestinationTag)) { JLOG(ctx.j.debug()) - << "LoanBrokerCoverWithdraw: zero/empty destination account."; + << "LoanBrokerCoverWithdraw: sfDestinationTag is set but " + "sfDestination is not"; return temMALFORMED; } From bba9119a757271d2d81ca22d05be8f7583e807d6 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 29 Jul 2025 14:58:36 -0400 Subject: [PATCH 039/291] Fix a couple of errors - Mark variable only used in asserts as [[maybe_unused]]. - Restore commented out line that I think I resolved wrong in an earlier merge to fix Vault tests. --- src/xrpld/app/tx/detail/VaultWithdraw.cpp | 2 +- src/xrpld/ledger/detail/View.cpp | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.cpp b/src/xrpld/app/tx/detail/VaultWithdraw.cpp index 6279200b7d..ae81037fc7 100644 --- a/src/xrpld/app/tx/detail/VaultWithdraw.cpp +++ b/src/xrpld/app/tx/detail/VaultWithdraw.cpp @@ -148,7 +148,7 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx) } // The destination account must have consented to receive the asset by // creating a RippleState or MPToken - // authType = AuthType::StrongAuth; + authType = AuthType::StrongAuth; } // Destination MPToken (for an MPT) or trust line (for an IOU) must exist diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index a7e7f2e7f8..8530f0292f 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -1133,6 +1133,7 @@ createPseudoAccount( uint256 const& pseudoOwnerKey, SField const& ownerField) { + [[maybe_unused]] auto const& fields = getPseudoAccountFields(); XRPL_ASSERT( std::count_if( From 421cbb9abd7f778b7e83768b0f859567324454ec Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 29 Jul 2025 19:20:09 -0400 Subject: [PATCH 040/291] Add the "testBatchBypassCounterparty" test from auditors - (With some minor modifications) --- src/test/app/Loan_test.cpp | 95 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 57a863a4ad..57fe79e433 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -1889,6 +1889,100 @@ class Loan_test : public beast::unit_test::suite } } + void + testBatchBypassCounterparty() + { + testcase << "Batch Bypass Counterparty"; + + using namespace jtx; + using namespace std::chrono_literals; + Env env(*this, all); + + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1'000'000), lender, borrower); + env.close(); + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + + BrokerInfo broker{createVaultAndBroker(env, xrpAsset, lender)}; + + using namespace loan; + + auto const loanSetFee = fee(env.current()->fees().base * 2); + Number const principalRequest{1, 3}; + auto const startDate = env.now() + 60s; + + auto forgedLoanSet = + set(borrower, broker.brokerID, principalRequest, startDate, 0); + + Json::Value randomData{Json::objectValue}; + randomData[jss::SigningPubKey] = Json::StaticString{"2600"}; + Json::Value sigObject{Json::objectValue}; + sigObject[jss::SigningPubKey] = strHex(lender.pk().slice()); + Serializer ss; + ss.add32(HashPrefix::txSign); + parse(randomData).addWithoutSigningFields(ss); + auto const sig = ripple::sign(borrower.pk(), borrower.sk(), ss.slice()); + sigObject[jss::TxnSignature] = strHex(Slice{sig.data(), sig.size()}); + + forgedLoanSet[Json::StaticString{"CounterpartySignature"}] = sigObject; + + // ? Fails because the lender hasn't signed the tx + env(env.json(forgedLoanSet, fee(loanSetFee)), ter(telENV_RPC_FAILED)); + + auto const seq = env.seq(borrower); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + // ! Should fail because the lender hasn't signed the tx + env(batch::outer(borrower, seq, batchFee, tfAllOrNothing), + batch::inner(forgedLoanSet, seq + 1), + batch::inner(pay(borrower, lender, XRP(1)), seq + 2), + ter(tesSUCCESS)); + env.close(); + + // ? Check that the loan was created + { + Json::Value params(Json::objectValue); + params[jss::account] = borrower.human(); + params[jss::type] = "Loan"; + auto const res = + env.rpc("json", "account_objects", to_string(params)); + auto const objects = res[jss::result][jss::account_objects]; + BEAST_EXPECT(objects.size() == 1); + + auto const loan = objects[0u]; + BEAST_EXPECT(loan[sfAssetsAvailable] == "1000"); + BEAST_EXPECT(loan[sfBorrower] == borrower.human()); + BEAST_EXPECT(loan[sfCloseInterestRate] == 0); + BEAST_EXPECT(loan[sfClosePaymentFee] == "0"); + BEAST_EXPECT(loan[sfFlags] == 0); + BEAST_EXPECT(loan[sfGracePeriod] == 60); + BEAST_EXPECT(loan[sfInterestRate] == 0); + BEAST_EXPECT(loan[sfLateInterestRate] == 0); + BEAST_EXPECT(loan[sfLatePaymentFee] == "0"); + BEAST_EXPECT(loan[sfLoanBrokerID] == to_string(broker.brokerID)); + BEAST_EXPECT(loan[sfLoanOriginationFee] == "0"); + BEAST_EXPECT(loan[sfLoanSequence] == 1); + BEAST_EXPECT(loan[sfLoanServiceFee] == "0"); + BEAST_EXPECT( + loan[sfNextPaymentDueDate] == loan[sfStartDate].asUInt() + 60); + BEAST_EXPECT(loan[sfOverpaymentFee] == 0); + BEAST_EXPECT(loan[sfOverpaymentInterestRate] == 0); + BEAST_EXPECT(loan[sfPaymentInterval] == 60); + BEAST_EXPECT(loan[sfPaymentRemaining] == 1); + BEAST_EXPECT(loan[sfPreviousPaymentDate] == 0); + BEAST_EXPECT(loan[sfPrincipalOutstanding] == "1000"); + BEAST_EXPECT(loan[sfPrincipalRequested] == "1000"); + BEAST_EXPECT( + loan[sfStartDate].asUInt() == + startDate.time_since_epoch().count()); + + // TODO: Draw the funds for a full exploit + // env(draw(borrower, loanKeylet.key, XRP(500))); + } + } + public: void run() override @@ -1896,6 +1990,7 @@ public: testDisabled(); testSelfLoan(); testLifecycle(); + testBatchBypassCounterparty(); } }; From 9d1a23a811150994e0114c8ea8b07765b2a25613 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 29 Jul 2025 18:34:14 -0400 Subject: [PATCH 041/291] Ensure Counterparty Signatures are properly handled by Batch tx - Addresses FIND-001 from audit - LoanSet::preflight will require Counterparty to be set (it's normally optional) for an inner batch transaction, because the checks are done before the LoanBroker object can be accessed. - Adjust LoanSet::calculateBaseFee to not charge extra if an inner transaction. - Adds a Loan-specific test to Batch_test. --- src/test/app/Batch_test.cpp | 246 ++++++++++++++++++++- src/test/app/Loan_test.cpp | 34 +-- src/test/jtx/batch.h | 3 +- src/xrpld/app/ledger/detail/OpenLedger.cpp | 5 +- src/xrpld/app/misc/NetworkOPs.cpp | 17 +- src/xrpld/app/tx/detail/Batch.cpp | 68 ++++-- src/xrpld/app/tx/detail/LoanSet.cpp | 21 +- src/xrpld/app/tx/detail/Transactor.cpp | 18 +- src/xrpld/app/tx/detail/apply.cpp | 10 - src/xrpld/overlay/detail/PeerImp.cpp | 6 +- 10 files changed, 338 insertions(+), 90 deletions(-) diff --git a/src/test/app/Batch_test.cpp b/src/test/app/Batch_test.cpp index 92f286ca6a..8f889f4b00 100644 --- a/src/test/app/Batch_test.cpp +++ b/src/test/app/Batch_test.cpp @@ -2313,9 +2313,12 @@ class Batch_test : public beast::unit_test::suite Serializer s; parsed.object->add(s); auto const jrr = env.rpc("submit", strHex(s.slice()))[jss::result]; - BEAST_EXPECT( - jrr[jss::status] == "success" && - jrr[jss::engine_result] == "temINVALID_FLAG"); + BEAST_EXPECTS( + jrr[jss::status] == "error" && + jrr[jss::error] == "invalidTransaction" && + jrr[jss::error_exception] == + "fails local checks: Empty SigningPubKey.", + to_string(jrr)); env.close(); } @@ -2553,6 +2556,242 @@ class Batch_test : public beast::unit_test::suite } } + void + testLoan(FeatureBitset features) + { + testcase("loan"); + + using namespace test::jtx; + + test::jtx::Env env{ + *this, + envconfig(), + features | featureSingleAssetVault | featureLendingProtocol | + featureMPTokensV1}; + + Account const issuer{"issuer"}; + // For simplicity, lender will be the sole actor for the vault & + // brokers. + Account const lender{"lender"}; + // Borrower only wants to borrow + Account const borrower{"borrower"}; + + // Fund the accounts and trust lines with the same amount so that tests + // can use the same values regardless of the asset. + env.fund(XRP(100'000), issuer, noripple(lender, borrower)); + env.close(); + + // Just use an XRP asset + PrettyAsset const asset{xrpIssue(), 1'000'000}; + + Vault vault{env}; + + auto const deposit = asset(50'000); + auto const debtMaximumValue = asset(25'000).value(); + auto const coverDepositValue = asset(1000).value(); + + auto [tx, vaultKeylet] = + vault.create({.owner = lender, .asset = asset}); + env(tx); + env.close(); + BEAST_EXPECT(env.le(vaultKeylet)); + + env(vault.deposit( + {.depositor = lender, .id = vaultKeylet.key, .amount = deposit})); + env.close(); + + auto const brokerKeylet = + keylet::loanbroker(lender.id(), env.seq(lender)); + + { + using namespace loanBroker; + env(set(lender, vaultKeylet.key), + managementFeeRate(TenthBips16(100)), + debtMaximum(debtMaximumValue), + coverRateMinimum(TenthBips32(percentageToTenthBips(10))), + coverRateLiquidation(TenthBips32(percentageToTenthBips(25)))); + + env(coverDeposit(lender, brokerKeylet.key, coverDepositValue)); + + env.close(); + } + + { + using namespace loan; + using namespace std::chrono_literals; + + auto const lenderSeq = env.seq(lender); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + + auto const loanKeylet = keylet::loan(brokerKeylet.key, 1); + { + auto const [txIDs, batchID] = submitBatch( + env, + temBAD_SIGNATURE, + batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing), + batch::inner( + env.json( + set(lender, + brokerKeylet.key, + asset(1000).value(), + env.now() + 3600s), + // Not allowed to include the counterparty signature + sig(sfCounterpartySignature, borrower), + sig(none), + fee(none), + seq(none)), + lenderSeq + 1), + batch::inner( + draw( + lender, + loanKeylet.key, + STAmount{asset, asset(500).value()}), + lenderSeq + 2)); + } + { + auto const [txIDs, batchID] = submitBatch( + env, + telENV_RPC_FAILED, + batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing), + batch::inner( + env.json( + set(lender, + brokerKeylet.key, + asset(1000).value(), + env.now() + 3600s), + // Must include a CounterpartySignature field. + // Transaction will not even parse at the RPC layer + sig(none), + fee(none), + seq(none)), + lenderSeq + 1), + batch::inner( + draw( + lender, + loanKeylet.key, + STAmount{asset, asset(500).value()}), + lenderSeq + 2)); + } + { + auto const [txIDs, batchID] = submitBatch( + env, + temINVALID_INNER_BATCH, + batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing), + batch::inner( + env.json( + set(lender, + brokerKeylet.key, + asset(1000).value(), + env.now() + 3600s), + json(sfCounterpartySignature, Json::objectValue), + // Counterparty must be set + sig(none), + fee(none), + seq(none)), + lenderSeq + 1), + batch::inner( + draw( + lender, + loanKeylet.key, + STAmount{asset, asset(500).value()}), + lenderSeq + 2)); + } + { + auto const [txIDs, batchID] = submitBatch( + env, + temBAD_SIGNER, + batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing), + batch::inner( + env.json( + set(lender, + brokerKeylet.key, + asset(1000).value(), + env.now() + 3600s), + // Counterparty must sign the outer transaction + counterparty(borrower.id()), + json(sfCounterpartySignature, Json::objectValue), + sig(none), + fee(none), + seq(none)), + lenderSeq + 1), + batch::inner( + draw( + lender, + loanKeylet.key, + STAmount{asset, asset(500).value()}), + lenderSeq + 2)); + } + { + // LoanSet normally charges at least 2x base fee, but since the + // signature check is done by the batch, it only charges the + // base fee. + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing), + batch::inner( + env.json( + set(lender, + brokerKeylet.key, + asset(1000).value(), + env.now() + 3600s), + counterparty(borrower.id()), + json(sfCounterpartySignature, Json::objectValue), + sig(none), + fee(none), + seq(none)), + lenderSeq + 1), + batch::inner( + draw( + // However, this inner transaction will fail, + // because the lender is not allowed to draw the + // transaction + lender, + loanKeylet.key, + STAmount{asset, asset(500).value()}), + lenderSeq + 2), + batch::sig(borrower)); + } + env.close(); + BEAST_EXPECT(env.le(brokerKeylet)); + BEAST_EXPECT(!env.le(loanKeylet)); + { + // LoanSet normally charges at least 2x base fee, but since the + // signature check is done by the batch, it only charges the + // base fee. + auto const lenderSeq = env.seq(lender); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing), + batch::inner( + env.json( + set(lender, + brokerKeylet.key, + asset(1000).value(), + env.now() + 3600s), + counterparty(borrower.id()), + json(sfCounterpartySignature, Json::objectValue), + sig(none), + fee(none), + seq(none)), + lenderSeq + 1), + batch::inner( + manage(lender, loanKeylet.key, tfLoanImpair), + lenderSeq + 2), + batch::sig(borrower)); + } + env.close(); + BEAST_EXPECT(env.le(brokerKeylet)); + if (auto const sleLoan = env.le(loanKeylet); BEAST_EXPECT(sleLoan)) + { + BEAST_EXPECT(sleLoan->isFlag(lsfLoanImpaired)); + } + } + } + void testObjectCreateSequence(FeatureBitset features) { @@ -4147,6 +4386,7 @@ class Batch_test : public beast::unit_test::suite testAccountActivation(features); testAccountSet(features); testAccountDelete(features); + testLoan(features); testObjectCreateSequence(features); testObjectCreateTicket(features); testObjectCreate3rdParty(features); diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 57fe79e433..b366d1df1a 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -1938,7 +1938,7 @@ class Loan_test : public beast::unit_test::suite env(batch::outer(borrower, seq, batchFee, tfAllOrNothing), batch::inner(forgedLoanSet, seq + 1), batch::inner(pay(borrower, lender, XRP(1)), seq + 2), - ter(tesSUCCESS)); + ter(temBAD_SIGNATURE)); env.close(); // ? Check that the loan was created @@ -1949,37 +1949,7 @@ class Loan_test : public beast::unit_test::suite auto const res = env.rpc("json", "account_objects", to_string(params)); auto const objects = res[jss::result][jss::account_objects]; - BEAST_EXPECT(objects.size() == 1); - - auto const loan = objects[0u]; - BEAST_EXPECT(loan[sfAssetsAvailable] == "1000"); - BEAST_EXPECT(loan[sfBorrower] == borrower.human()); - BEAST_EXPECT(loan[sfCloseInterestRate] == 0); - BEAST_EXPECT(loan[sfClosePaymentFee] == "0"); - BEAST_EXPECT(loan[sfFlags] == 0); - BEAST_EXPECT(loan[sfGracePeriod] == 60); - BEAST_EXPECT(loan[sfInterestRate] == 0); - BEAST_EXPECT(loan[sfLateInterestRate] == 0); - BEAST_EXPECT(loan[sfLatePaymentFee] == "0"); - BEAST_EXPECT(loan[sfLoanBrokerID] == to_string(broker.brokerID)); - BEAST_EXPECT(loan[sfLoanOriginationFee] == "0"); - BEAST_EXPECT(loan[sfLoanSequence] == 1); - BEAST_EXPECT(loan[sfLoanServiceFee] == "0"); - BEAST_EXPECT( - loan[sfNextPaymentDueDate] == loan[sfStartDate].asUInt() + 60); - BEAST_EXPECT(loan[sfOverpaymentFee] == 0); - BEAST_EXPECT(loan[sfOverpaymentInterestRate] == 0); - BEAST_EXPECT(loan[sfPaymentInterval] == 60); - BEAST_EXPECT(loan[sfPaymentRemaining] == 1); - BEAST_EXPECT(loan[sfPreviousPaymentDate] == 0); - BEAST_EXPECT(loan[sfPrincipalOutstanding] == "1000"); - BEAST_EXPECT(loan[sfPrincipalRequested] == "1000"); - BEAST_EXPECT( - loan[sfStartDate].asUInt() == - startDate.time_since_epoch().count()); - - // TODO: Draw the funds for a full exploit - // env(draw(borrower, loanKeylet.key, XRP(500))); + BEAST_EXPECT(objects.size() == 0); } } diff --git a/src/test/jtx/batch.h b/src/test/jtx/batch.h index ab235c293f..2baa52fd06 100644 --- a/src/test/jtx/batch.h +++ b/src/test/jtx/batch.h @@ -68,8 +68,7 @@ public: inner( Json::Value const& txn, std::uint32_t const& sequence, - std::optional const& ticket = std::nullopt, - std::optional const& fee = std::nullopt) + std::optional const& ticket = std::nullopt) : txn_(txn), seq_(sequence), ticket_(ticket) { txn_[jss::SigningPubKey] = ""; diff --git a/src/xrpld/app/ledger/detail/OpenLedger.cpp b/src/xrpld/app/ledger/detail/OpenLedger.cpp index 2c98caaa6d..f3c779209e 100644 --- a/src/xrpld/app/ledger/detail/OpenLedger.cpp +++ b/src/xrpld/app/ledger/detail/OpenLedger.cpp @@ -124,8 +124,11 @@ OpenLedger::accept( auto const txId = tx->getTransactionID(); // skip batch txns + // The flag should only be settable if Batch feature is enabled. If + // Batch is not enabled, the flag is always invalid, so don't relay it + // regardless. // LCOV_EXCL_START - if (tx->isFlag(tfInnerBatchTxn) && rules.enabled(featureBatch)) + if (tx->isFlag(tfInnerBatchTxn)) { XRPL_ASSERT( txpair.second && txpair.second->isFieldPresent(sfParentBatchID), diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index 3220ce99fc..992a72cb9e 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -1687,10 +1687,11 @@ NetworkOPsImp::apply(std::unique_lock& batchLock) app_.getHashRouter().shouldRelay(e.transaction->getID()); if (auto const sttx = *(e.transaction->getSTransaction()); toSkip && - // Skip relaying if it's an inner batch txn and batch - // feature is enabled - !(sttx.isFlag(tfInnerBatchTxn) && - newOL->rules().enabled(featureBatch))) + // Skip relaying if it's an inner batch txn. The flag should + // only be set if the Batch feature is enabled. If Batch is + // not enabled, the flag is always invalid, so don't relay + // it regardless. + !sttx.isFlag(tfInnerBatchTxn)) { protocol::TMTransaction tx; Serializer s; @@ -3051,9 +3052,11 @@ NetworkOPsImp::pubProposedTransaction( std::shared_ptr const& transaction, TER result) { - // never publish an inner txn inside a batch txn - if (transaction->isFlag(tfInnerBatchTxn) && - ledger->rules().enabled(featureBatch)) + // never publish an inner txn inside a batch txn. The flag should + // only be set if the Batch feature is enabled. If Batch is not + // enabled, the flag is always invalid, so don't publish it + // regardless. + if (transaction->isFlag(tfInnerBatchTxn)) return; MultiApiJson jvObj = diff --git a/src/xrpld/app/tx/detail/Batch.cpp b/src/xrpld/app/tx/detail/Batch.cpp index 5f05d9919e..0678998dc3 100644 --- a/src/xrpld/app/tx/detail/Batch.cpp +++ b/src/xrpld/app/tx/detail/Batch.cpp @@ -245,6 +245,39 @@ Batch::preflight(PreflightContext const& ctx) std::unordered_set uniqueHashes; std::unordered_map> accountSeqTicket; + auto checkSignatureFields = [&parentBatchId, &j = ctx.j]( + STObject const& sig, + uint256 const& hash, + char const* label = "") -> NotTEC { + if (sig.isFieldPresent(sfTxnSignature)) + { + JLOG(j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "inner txn " << label << "cannot include TxnSignature. " + << "txID: " << hash; + return temBAD_SIGNATURE; + } + + if (sig.isFieldPresent(sfSigners)) + { + JLOG(j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "inner txn " << label << " cannot include Signers. " + << "txID: " << hash; + return temBAD_SIGNER; + } + + if (!sig.getFieldVL(sfSigningPubKey).empty()) + { + JLOG(j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "inner txn " << label << " SigningPubKey must be empty. " + << "txID: " << hash; + return temBAD_REGKEY; + } + + return tesSUCCESS; + }; for (STObject rb : rawTxns) { STTx const stx = STTx{std::move(rb)}; @@ -274,28 +307,18 @@ Batch::preflight(PreflightContext const& ctx) return temINVALID_FLAG; } - if (stx.isFieldPresent(sfTxnSignature)) - { - JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " - << "inner txn cannot include TxnSignature. " - << "txID: " << hash; - return temBAD_SIGNATURE; - } + if (auto const ret = checkSignatureFields(stx, hash)) + return ret; - if (stx.isFieldPresent(sfSigners)) + if (stx.isFieldPresent(sfCounterpartySignature)) { - JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " - << "inner txn cannot include Signers. " - << "txID: " << hash; - return temBAD_SIGNER; - } - - if (!stx.getSigningPubKey().empty()) - { - JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " - << "inner txn SigningPubKey must be empty. " - << "txID: " << hash; - return temBAD_REGKEY; + auto const counterpartySignature = + stx.getFieldObject(sfCounterpartySignature); + if (auto const ret = checkSignatureFields( + counterpartySignature, hash, "counterparty signature ")) + { + return ret; + } } auto const innerAccount = stx.getAccountID(sfAccount); @@ -376,6 +399,11 @@ Batch::preflight(PreflightContext const& ctx) // inner account to the required signers set. if (innerAccount != outerAccount) requiredSigners.insert(innerAccount); + // Some transactions have a Counterparty, who must also sign the + // transaction if they are not the outer account + if (auto const counterparty = stx.at(~sfCounterparty); + counterparty && counterparty != outerAccount) + requiredSigners.insert(*counterparty); } // Validation Batch Signers diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 1ef84c0346..8aec6cc268 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -62,6 +62,17 @@ NotTEC LoanSet::preflight(PreflightContext const& ctx) { auto const& tx = ctx.tx; + + // Special case for Batch inner transactions + if (tx.isFlag(tfInnerBatchTxn) && ctx.rules.enabled(featureBatch) && + !tx.isFieldPresent(sfCounterparty)) + { + auto const parentBatchId = ctx.parentBatchId.value_or(uint256{0}); + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "no Counterparty for inner LoanSet transaction."; + return temBAD_SIGNER; + } + auto const counterPartySig = ctx.tx.getFieldObject(sfCounterpartySignature); if (auto const ret = @@ -139,10 +150,12 @@ LoanSet::calculateBaseFee(ReadView const& view, STTx const& tx) auto const counterSig = tx.getFieldObject(sfCounterpartySignature); // Each signer adds one more baseFee to the minimum required fee - // for the transaction. Note that unlike the base class, if there are no - // signers, 1 extra signature is still counted for the single signer. - std::size_t const signerCount = - tx.isFieldPresent(sfSigners) ? tx.getFieldArray(sfSigners).size() : 1; + // for the transaction. Note that unlike the base class, the single signer + // is counted if present. It will only be absent in a batch inner + // transaction. + std::size_t const signerCount = tx.isFieldPresent(sfSigners) + ? tx.getFieldArray(sfSigners).size() + : (counterSig.isFieldPresent(sfTxnSignature) ? 1 : 0); return normalCost + (signerCount * baseFee); } diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 2b2a9b9e83..fc7888cc87 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -231,12 +231,17 @@ Transactor::preflight2(PreflightContext const& ctx) // regardless of success or failure return *ret; - auto const sigValid = checkValidity( - ctx.app.getHashRouter(), ctx.tx, ctx.rules, ctx.app.config()); - if (sigValid.first == Validity::SigBad) + // Skip signature check on batch inner transactions + if (!ctx.tx.isFlag(tfInnerBatchTxn) || !ctx.rules.enabled(featureBatch)) { - JLOG(ctx.j.debug()) << "preflight2: bad signature. " << sigValid.second; - return temINVALID; // LCOV_EXCL_LINE + auto const sigValid = checkValidity( + ctx.app.getHashRouter(), ctx.tx, ctx.rules, ctx.app.config()); + if (sigValid.first == Validity::SigBad) + { + JLOG(ctx.j.debug()) + << "preflight2: bad signature. " << sigValid.second; + return temINVALID; // LCOV_EXCL_LINE + } } return tesSUCCESS; } @@ -662,8 +667,7 @@ Transactor::checkSign( auto const pkSigner = sigObject.getFieldVL(sfSigningPubKey); // Ignore signature check on batch inner transactions - if (sigObject.isFlag(tfInnerBatchTxn) && - ctx.view.rules().enabled(featureBatch)) + if (ctx.parentBatchId && ctx.view.rules().enabled(featureBatch)) { // Defensive Check: These values are also checked in Batch::preflight if (sigObject.isFieldPresent(sfTxnSignature) || !pkSigner.empty() || diff --git a/src/xrpld/app/tx/detail/apply.cpp b/src/xrpld/app/tx/detail/apply.cpp index e2e0adae45..99f063407b 100644 --- a/src/xrpld/app/tx/detail/apply.cpp +++ b/src/xrpld/app/tx/detail/apply.cpp @@ -59,16 +59,6 @@ checkValidity( return { Validity::SigBad, "Malformed: Invalid inner batch transaction."}; - - std::string reason; - if (!passesLocalChecks(tx, reason)) - { - router.setFlags(id, SF_LOCALBAD); - return {Validity::SigGoodOnly, reason}; - } - - router.setFlags(id, SF_SIGGOOD); - return {Validity::Valid, ""}; } if (any(flags & SF_SIGBAD)) diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index 23b4760488..d36ccf2d3c 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -1286,8 +1286,7 @@ PeerImp::handleTransaction( // Charge strongly for attempting to relay a txn with tfInnerBatchTxn // LCOV_EXCL_START - if (stx->isFlag(tfInnerBatchTxn) && - getCurrentTransactionRules()->enabled(featureBatch)) + if (stx->isFlag(tfInnerBatchTxn)) { JLOG(p_journal_.warn()) << "Ignoring Network relayed Tx containing " "tfInnerBatchTxn (handleTransaction)."; @@ -2851,8 +2850,7 @@ PeerImp::checkTransaction( { // charge strongly for relaying batch txns // LCOV_EXCL_START - if (stx->isFlag(tfInnerBatchTxn) && - getCurrentTransactionRules()->enabled(featureBatch)) + if (stx->isFlag(tfInnerBatchTxn)) { JLOG(p_journal_.warn()) << "Ignoring Network relayed Tx containing " "tfInnerBatchTxn (checkSignature)."; From ec8eaf0d737bbd3fa434257cc2fb65f83c52e80f Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 29 Jul 2025 20:31:31 -0400 Subject: [PATCH 042/291] fixup! Ensure Counterparty Signatures are properly handled by Batch tx --- src/test/app/Loan_test.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index b366d1df1a..8c130d5a4d 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +28,7 @@ #include #include #include +#include #include #include From 84acebeb7fbcff5540418625abc2f945fd75e5b7 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 30 Jul 2025 13:56:50 -0400 Subject: [PATCH 043/291] Enforce valid range of LoanSet.InterestRate - Addresses FIND-002 from audit. - Enforces a range of 0-100% in 1/10 bips. - Also add a couple of unit test checks. --- include/xrpl/protocol/Protocol.h | 11 +++++++++++ src/test/app/Loan_test.cpp | 13 +++++++++++++ src/xrpld/app/tx/detail/LoanSet.cpp | 2 ++ 3 files changed, 26 insertions(+) diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 94d8a9696b..acd9fe26bd 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -146,6 +146,14 @@ static_assert(maxCoverRate == TenthBips32(100'000u)); Valid values are between 0 and 100% inclusive. */ TenthBips32 constexpr maxOverpaymentFee = percentageToTenthBips(100); +static_assert(maxOverpaymentFee == TenthBips32(100'000u)); + +/** Annualized interest rate of the Loan in 1/10 bips. + * + * Valid values are between 0 and 100% inclusive. + */ +TenthBips32 constexpr maxInterestRate = percentageToTenthBips(100); +static_assert(maxInterestRate == TenthBips32(100'000u)); /** The maximum premium added to the interest rate for late payments on a loan * in 1/10 bips. @@ -153,6 +161,7 @@ TenthBips32 constexpr maxOverpaymentFee = percentageToTenthBips(100); * Valid values are between 0 and 100% inclusive. */ TenthBips32 constexpr maxLateInterestRate = percentageToTenthBips(100); +static_assert(maxLateInterestRate == TenthBips32(100'000u)); /** The maximum close interest rate charged for repaying a loan early in 1/10 * bips. @@ -160,6 +169,7 @@ TenthBips32 constexpr maxLateInterestRate = percentageToTenthBips(100); * Valid values are between 0 and 100% inclusive. */ TenthBips32 constexpr maxCloseInterestRate = percentageToTenthBips(100); +static_assert(maxCloseInterestRate == TenthBips32(100'000u)); /** The maximum overpayment interest rate charged on loan overpayments in 1/10 * bips. @@ -167,6 +177,7 @@ TenthBips32 constexpr maxCloseInterestRate = percentageToTenthBips(100); * Valid values are between 0 and 100% inclusive. */ TenthBips32 constexpr maxOverpaymentInterestRate = percentageToTenthBips(100); +static_assert(maxOverpaymentInterestRate == TenthBips32(100'000u)); /** The maximum length of a URI inside an NFT */ std::size_t constexpr maxTokenURILength = 256; diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 8c130d5a4d..94ec820158 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -748,6 +748,19 @@ class Loan_test : public beast::unit_test::suite loanSetFee, ter(temINVALID)); + // sfInterestRate: good value, bad account + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, borrower), + interestRate(maxInterestRate), + loanSetFee, + ter(tefBAD_AUTH)); + // sfInterestRate: too big + env(set(evan, broker.brokerID, principalRequest, startDate), + sig(sfCounterpartySignature, lender), + interestRate(maxInterestRate + 1), + loanSetFee, + ter(temINVALID)); + // sfLateInterestRate: good value, bad account env(set(evan, broker.brokerID, principalRequest, startDate), sig(sfCounterpartySignature, borrower), diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 8aec6cc268..2dbb6b799e 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -82,6 +82,8 @@ LoanSet::preflight(PreflightContext const& ctx) if (auto const data = tx[~sfData]; data && !data->empty() && !validDataLength(tx[~sfData], maxDataPayloadLength)) return temINVALID; + if (!validNumericRange(tx[~sfInterestRate], maxInterestRate)) + return temINVALID; if (!validNumericRange(tx[~sfOverpaymentFee], maxOverpaymentFee)) return temINVALID; if (!validNumericRange(tx[~sfLateInterestRate], maxLateInterestRate)) From a1e9091f1e2f225a7ac06b8df228a9c4ed4b2ee3 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 30 Jul 2025 17:25:58 -0400 Subject: [PATCH 044/291] Add the "testBatchBypassCounterparty" test from auditors - (With some minor modifications) --- src/test/app/Loan_test.cpp | 65 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 94ec820158..6fb603bfd6 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -338,14 +338,17 @@ class Loan_test : public beast::unit_test::suite createVaultAndBroker( jtx::Env& env, jtx::PrettyAsset const& asset, - jtx::Account const& lender) + jtx::Account const& lender, + std::optional debtMax = std::nullopt) { using namespace jtx; Vault vault{env}; auto const deposit = asset(vaultDeposit); - auto const debtMaximumValue = asset(debtMaximumParameter).value(); + auto const debtMaximumValue = debtMax + ? STAmount{asset.raw(), *debtMax} + : asset(debtMaximumParameter).value(); auto const coverDepositValue = asset(coverDepositParameter).value(); auto [tx, vaultKeylet] = @@ -1968,6 +1971,63 @@ class Loan_test : public beast::unit_test::suite } } + BrokerInfo + createVaultAndBrokerNoMaxDebt( + jtx::Env& env, + jtx::PrettyAsset const& asset, + jtx::Account const& lender) + { + return createVaultAndBroker(env, asset, lender, Number(0)); + } + + void + testWrongMaxDebtBehavior() + { + testcase << "Wrong Max Debt Behavior"; + + using namespace jtx; + using namespace std::chrono_literals; + Env env(*this, all); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + + env.fund(XRP(100'000), issuer, noripple(lender)); + env.close(); + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + + BrokerInfo broker{createVaultAndBrokerNoMaxDebt(env, xrpAsset, lender)}; + + if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); + BEAST_EXPECT(brokerSle)) + { + BEAST_EXPECT(brokerSle->at(sfDebtMaximum) == 0); + } + + using namespace loan; + + auto const loanSetFee = fee(env.current()->fees().base * 2); + Number const principalRequest{1, 3}; + auto const startDate = env.now() + 60s; + + auto createJson = env.json( + set(lender, broker.brokerID, principalRequest, startDate), + fee(loanSetFee)); + + Json::Value counterpartyJson{Json::objectValue}; + counterpartyJson[sfTxnSignature] = createJson[sfTxnSignature]; + counterpartyJson[sfSigningPubKey] = createJson[sfSigningPubKey]; + if (!BEAST_EXPECT(!createJson.isMember(jss::Signers))) + counterpartyJson[sfSigners] = createJson[sfSigners]; + + createJson = env.json( + createJson, json(sfCounterpartySignature, counterpartyJson)); + env(createJson); + + env.close(); + } + public: void run() override @@ -1976,6 +2036,7 @@ public: testSelfLoan(); testLifecycle(); testBatchBypassCounterparty(); + testWrongMaxDebtBehavior(); } }; From d58399d417fba42a4e505b123ce60aa00b06516c Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 30 Jul 2025 17:38:15 -0400 Subject: [PATCH 045/291] Correct handling of LoanBroker.DebtMaximum zero values - Addresses FIND-003 from audit. - Behavior changed to treat a DebtMaximum value of 0 to mean "no limit", as defined in spec. - No need to add unit tests, because the previous commit covers 0, and tests already exist for non-zero limits. --- src/xrpld/app/tx/detail/LoanSet.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 2dbb6b799e..3d9f1775b0 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -243,7 +243,8 @@ LoanSet::preclaim(PreclaimContext const& ctx) return tecINSUFFICIENT_FUNDS; } auto const newDebtTotal = brokerSle->at(sfDebtTotal) + principalRequested; - if (brokerSle->at(sfDebtMaximum) < newDebtTotal) + if (auto const debtMaximum = brokerSle->at(sfDebtMaximum); + debtMaximum != 0 && debtMaximum < newDebtTotal) { JLOG(ctx.j.warn()) << "Loan would exceed the maximum debt limit of the LoanBroker."; From f89c88dbb88d210d9a84c9a8f31c583f1a8e0291 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 5 Aug 2025 12:22:49 -0400 Subject: [PATCH 046/291] Add code coverage for STParsedJSON edge cases - Move several tests from `STObject_test` to new `STParsedJSON_test` unchanged. - Add 3 test cases to cover edge cases for UInt16 values. Co-authored-by: Denis Angell --- src/test/protocol/STObject_test.cpp | 215 ----------------- src/test/protocol/STParsedJSON_test.cpp | 302 ++++++++++++++++++++++++ 2 files changed, 302 insertions(+), 215 deletions(-) create mode 100644 src/test/protocol/STParsedJSON_test.cpp diff --git a/src/test/protocol/STObject_test.cpp b/src/test/protocol/STObject_test.cpp index e02cfeeea4..47a7ab0ad2 100644 --- a/src/test/protocol/STObject_test.cpp +++ b/src/test/protocol/STObject_test.cpp @@ -19,223 +19,11 @@ #include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - namespace ripple { class STObject_test : public beast::unit_test::suite { public: - bool - parseJSONString(std::string const& json, Json::Value& to) - { - Json::Reader reader; - return reader.parse(json, to) && to.isObject(); - } - - void - testParseJSONArrayWithInvalidChildrenObjects() - { - testcase("parse json array invalid children"); - try - { - /* - - STArray/STObject constructs don't really map perfectly to json - arrays/objects. - - STObject is an associative container, mapping fields to value, but - an STObject may also have a Field as its name, stored outside the - associative structure. The name is important, so to maintain - fidelity, it will take TWO json objects to represent them. - - */ - std::string faulty( - "{\"Template\":[{" - "\"ModifiedNode\":{\"Sequence\":1}, " - "\"DeletedNode\":{\"Sequence\":1}" - "}]}"); - - std::unique_ptr so; - Json::Value faultyJson; - bool parsedOK(parseJSONString(faulty, faultyJson)); - unexpected(!parsedOK, "failed to parse"); - STParsedJSONObject parsed("test", faultyJson); - BEAST_EXPECT(!parsed.object); - } - catch (std::runtime_error& e) - { - std::string what(e.what()); - unexpected(what.find("First level children of `Template`") != 0); - } - } - - void - testParseJSONArray() - { - testcase("parse json array"); - std::string const json( - "{\"Template\":[{\"ModifiedNode\":{\"Sequence\":1}}]}"); - - Json::Value jsonObject; - bool parsedOK(parseJSONString(json, jsonObject)); - if (parsedOK) - { - STParsedJSONObject parsed("test", jsonObject); - BEAST_EXPECT(parsed.object); - std::string const& serialized( - to_string(parsed.object->getJson(JsonOptions::none))); - BEAST_EXPECT(serialized == json); - } - else - { - fail("Couldn't parse json: " + json); - } - } - - void - testParseJSONEdgeCases() - { - testcase("parse json object"); - - { - std::string const goodJson(R"({"CloseResolution":19,"Method":250,)" - R"("TransactionResult":"tecFROZEN"})"); - - Json::Value jv; - if (BEAST_EXPECT(parseJSONString(goodJson, jv))) - { - STParsedJSONObject parsed("test", jv); - if (BEAST_EXPECT(parsed.object)) - { - std::string const& serialized( - to_string(parsed.object->getJson(JsonOptions::none))); - BEAST_EXPECT(serialized == goodJson); - } - } - } - - { - std::string const goodJson( - R"({"CloseResolution":19,"Method":"250",)" - R"("TransactionResult":"tecFROZEN"})"); - std::string const expectedJson( - R"({"CloseResolution":19,"Method":250,)" - R"("TransactionResult":"tecFROZEN"})"); - - Json::Value jv; - if (BEAST_EXPECT(parseJSONString(goodJson, jv))) - { - // Integer values are always parsed as int, - // unless they're too big. We want a small uint. - jv["CloseResolution"] = Json::UInt(19); - STParsedJSONObject parsed("test", jv); - if (BEAST_EXPECT(parsed.object)) - { - std::string const& serialized( - to_string(parsed.object->getJson(JsonOptions::none))); - BEAST_EXPECT(serialized == expectedJson); - } - } - } - - { - std::string const json(R"({"CloseResolution":19,"Method":250,)" - R"("TransactionResult":"terQUEUED"})"); - - Json::Value jv; - if (BEAST_EXPECT(parseJSONString(json, jv))) - { - STParsedJSONObject parsed("test", jv); - BEAST_EXPECT(!parsed.object); - BEAST_EXPECT(parsed.error); - BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); - BEAST_EXPECT( - parsed.error[jss::error_message] == - "Field 'test.TransactionResult' is out of range."); - } - } - - { - std::string const json(R"({"CloseResolution":19,"Method":"pony",)" - R"("TransactionResult":"tesSUCCESS"})"); - - Json::Value jv; - if (BEAST_EXPECT(parseJSONString(json, jv))) - { - STParsedJSONObject parsed("test", jv); - BEAST_EXPECT(!parsed.object); - BEAST_EXPECT(parsed.error); - BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); - BEAST_EXPECT( - parsed.error[jss::error_message] == - "Field 'test.Method' has bad type."); - } - } - - { - std::string const json( - R"({"CloseResolution":19,"Method":3294967296,)" - R"("TransactionResult":"tesSUCCESS"})"); - - Json::Value jv; - if (BEAST_EXPECT(parseJSONString(json, jv))) - { - STParsedJSONObject parsed("test", jv); - BEAST_EXPECT(!parsed.object); - BEAST_EXPECT(parsed.error); - BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); - BEAST_EXPECT( - parsed.error[jss::error_message] == - "Field 'test.Method' is out of range."); - } - } - - { - std::string const json(R"({"CloseResolution":-10,"Method":42,)" - R"("TransactionResult":"tesSUCCESS"})"); - - Json::Value jv; - if (BEAST_EXPECT(parseJSONString(json, jv))) - { - STParsedJSONObject parsed("test", jv); - BEAST_EXPECT(!parsed.object); - BEAST_EXPECT(parsed.error); - BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); - BEAST_EXPECT( - parsed.error[jss::error_message] == - "Field 'test.CloseResolution' is out of range."); - } - } - - { - std::string const json( - R"({"CloseResolution":19,"Method":3.141592653,)" - R"("TransactionResult":"tesSUCCESS"})"); - - Json::Value jv; - if (BEAST_EXPECT(parseJSONString(json, jv))) - { - STParsedJSONObject parsed("test", jv); - BEAST_EXPECT(!parsed.object); - BEAST_EXPECT(parsed.error); - BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); - BEAST_EXPECT( - parsed.error[jss::error_message] == - "Field 'test.Method' has bad type."); - } - } - } - void testSerialization() { @@ -730,9 +518,6 @@ public: testFields(); testSerialization(); - testParseJSONArray(); - testParseJSONArrayWithInvalidChildrenObjects(); - testParseJSONEdgeCases(); testMalformed(); } }; diff --git a/src/test/protocol/STParsedJSON_test.cpp b/src/test/protocol/STParsedJSON_test.cpp new file mode 100644 index 0000000000..05e95225b0 --- /dev/null +++ b/src/test/protocol/STParsedJSON_test.cpp @@ -0,0 +1,302 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2013 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include +#include + +namespace ripple { + +class STParsedJSON_test : public beast::unit_test::suite +{ +public: + bool + parseJSONString(std::string const& json, Json::Value& to) + { + Json::Reader reader; + return reader.parse(json, to) && to.isObject(); + } + + void + testParseJSONArrayWithInvalidChildrenObjects() + { + testcase("parse json array invalid children"); + try + { + /* + + STArray/STObject constructs don't really map perfectly to json + arrays/objects. + + STObject is an associative container, mapping fields to value, but + an STObject may also have a Field as its name, stored outside the + associative structure. The name is important, so to maintain + fidelity, it will take TWO json objects to represent them. + + */ + std::string faulty( + "{\"Template\":[{" + "\"ModifiedNode\":{\"Sequence\":1}, " + "\"DeletedNode\":{\"Sequence\":1}" + "}]}"); + + std::unique_ptr so; + Json::Value faultyJson; + bool parsedOK(parseJSONString(faulty, faultyJson)); + unexpected(!parsedOK, "failed to parse"); + STParsedJSONObject parsed("test", faultyJson); + BEAST_EXPECT(!parsed.object); + } + catch (std::runtime_error& e) + { + std::string what(e.what()); + unexpected(what.find("First level children of `Template`") != 0); + } + } + + void + testParseJSONArray() + { + testcase("parse json array"); + std::string const json( + "{\"Template\":[{\"ModifiedNode\":{\"Sequence\":1}}]}"); + + Json::Value jsonObject; + bool parsedOK(parseJSONString(json, jsonObject)); + if (parsedOK) + { + STParsedJSONObject parsed("test", jsonObject); + BEAST_EXPECT(parsed.object); + std::string const& serialized( + to_string(parsed.object->getJson(JsonOptions::none))); + BEAST_EXPECT(serialized == json); + } + else + { + fail("Couldn't parse json: " + json); + } + } + + void + testParseJSONEdgeCases() + { + testcase("parse json object"); + + { + std::string const goodJson( + R"({"CloseResolution":19,"Method":250,)" + R"("TransactionResult":"tecFROZEN"})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(goodJson, jv))) + { + STParsedJSONObject parsed("test", jv); + if (BEAST_EXPECT(parsed.object)) + { + std::string const& serialized( + to_string(parsed.object->getJson(JsonOptions::none))); + BEAST_EXPECT(serialized == goodJson); + } + } + } + + { + std::string const goodJson( + R"({"CloseResolution":19,"Method":"250",)" + R"("TransactionResult":"tecFROZEN"})"); + std::string const expectedJson( + R"({"CloseResolution":19,"Method":250,)" + R"("TransactionResult":"tecFROZEN"})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(goodJson, jv))) + { + // Integer values are always parsed as int, + // unless they're too big. We want a small uint. + jv["CloseResolution"] = Json::UInt(19); + STParsedJSONObject parsed("test", jv); + if (BEAST_EXPECT(parsed.object)) + { + std::string const& serialized( + to_string(parsed.object->getJson(JsonOptions::none))); + BEAST_EXPECT(serialized == expectedJson); + } + } + } + + { + std::string const json( + R"({"CloseResolution":19,"Method":250,)" + R"("TransactionResult":"terQUEUED"})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(json, jv))) + { + STParsedJSONObject parsed("test", jv); + BEAST_EXPECT(!parsed.object); + BEAST_EXPECT(parsed.error); + BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); + BEAST_EXPECT( + parsed.error[jss::error_message] == + "Field 'test.TransactionResult' is out of range."); + } + } + + { + std::string const json( + R"({"CloseResolution":19,"Method":"pony",)" + R"("TransactionResult":"tesSUCCESS"})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(json, jv))) + { + STParsedJSONObject parsed("test", jv); + BEAST_EXPECT(!parsed.object); + BEAST_EXPECT(parsed.error); + BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); + BEAST_EXPECT( + parsed.error[jss::error_message] == + "Field 'test.Method' has bad type."); + } + } + + { + std::string const json( + R"({"CloseResolution":19,"Method":3294967296,)" + R"("TransactionResult":"tesSUCCESS"})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(json, jv))) + { + STParsedJSONObject parsed("test", jv); + BEAST_EXPECT(!parsed.object); + BEAST_EXPECT(parsed.error); + BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); + BEAST_EXPECT( + parsed.error[jss::error_message] == + "Field 'test.Method' is out of range."); + } + } + + { + std::string const json( + R"({"CloseResolution":-10,"Method":42,)" + R"("TransactionResult":"tesSUCCESS"})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(json, jv))) + { + STParsedJSONObject parsed("test", jv); + BEAST_EXPECT(!parsed.object); + BEAST_EXPECT(parsed.error); + BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); + BEAST_EXPECT( + parsed.error[jss::error_message] == + "Field 'test.CloseResolution' is out of range."); + } + } + + { + std::string const json( + R"({"CloseResolution":19,"Method":3.141592653,)" + R"("TransactionResult":"tesSUCCESS"})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(json, jv))) + { + STParsedJSONObject parsed("test", jv); + BEAST_EXPECT(!parsed.object); + BEAST_EXPECT(parsed.error); + BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); + BEAST_EXPECT( + parsed.error[jss::error_message] == + "Field 'test.Method' has bad type."); + } + } + + { + std::string const json( + R"({"CloseResolution":19,"Method":250,)" + R"("TransferFee":"65536"})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(json, jv))) + { + STParsedJSONObject parsed("test", jv); + BEAST_EXPECT(!parsed.object); + BEAST_EXPECT(parsed.error); + BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); + BEAST_EXPECT( + parsed.error[jss::error_message] == + "Field 'test.TransferFee' has invalid data."); + } + } + + { + std::string const json( + R"({"CloseResolution":19,"Method":250,)" + R"("TransferFee":"Payment"})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(json, jv))) + { + STParsedJSONObject parsed("test", jv); + BEAST_EXPECT(!parsed.object); + BEAST_EXPECT(parsed.error); + BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); + BEAST_EXPECT( + parsed.error[jss::error_message] == + "Field 'test.TransferFee' has invalid data."); + } + } + + { + std::string const json( + R"({"CloseResolution":19,"Method":250,)" + R"("TransferFee":true})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(json, jv))) + { + STParsedJSONObject parsed("test", jv); + BEAST_EXPECT(!parsed.object); + BEAST_EXPECT(parsed.error); + BEAST_EXPECT(parsed.error[jss::error] == "invalidParams"); + BEAST_EXPECT( + parsed.error[jss::error_message] == + "Field 'test.TransferFee' has bad type."); + } + } + } + + void + run() override + { + // Instantiate a jtx::Env so debugLog writes are exercised. + test::jtx::Env env(*this); + testParseJSONArrayWithInvalidChildrenObjects(); + testParseJSONArray(); + testParseJSONEdgeCases(); + } +}; + +BEAST_DEFINE_TESTSUITE(STParsedJSON, protocol, ripple); + +} // namespace ripple From b07087f10bbdb714538c9d20561884f87cdda87b Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 5 Aug 2025 13:20:29 -0400 Subject: [PATCH 047/291] Fix formatting --- src/test/protocol/STParsedJSON_test.cpp | 35 ++++++++++--------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/src/test/protocol/STParsedJSON_test.cpp b/src/test/protocol/STParsedJSON_test.cpp index 05e95225b0..082a4624f2 100644 --- a/src/test/protocol/STParsedJSON_test.cpp +++ b/src/test/protocol/STParsedJSON_test.cpp @@ -100,9 +100,8 @@ public: testcase("parse json object"); { - std::string const goodJson( - R"({"CloseResolution":19,"Method":250,)" - R"("TransactionResult":"tecFROZEN"})"); + std::string const goodJson(R"({"CloseResolution":19,"Method":250,)" + R"("TransactionResult":"tecFROZEN"})"); Json::Value jv; if (BEAST_EXPECT(parseJSONString(goodJson, jv))) @@ -142,9 +141,8 @@ public: } { - std::string const json( - R"({"CloseResolution":19,"Method":250,)" - R"("TransactionResult":"terQUEUED"})"); + std::string const json(R"({"CloseResolution":19,"Method":250,)" + R"("TransactionResult":"terQUEUED"})"); Json::Value jv; if (BEAST_EXPECT(parseJSONString(json, jv))) @@ -160,9 +158,8 @@ public: } { - std::string const json( - R"({"CloseResolution":19,"Method":"pony",)" - R"("TransactionResult":"tesSUCCESS"})"); + std::string const json(R"({"CloseResolution":19,"Method":"pony",)" + R"("TransactionResult":"tesSUCCESS"})"); Json::Value jv; if (BEAST_EXPECT(parseJSONString(json, jv))) @@ -196,9 +193,8 @@ public: } { - std::string const json( - R"({"CloseResolution":-10,"Method":42,)" - R"("TransactionResult":"tesSUCCESS"})"); + std::string const json(R"({"CloseResolution":-10,"Method":42,)" + R"("TransactionResult":"tesSUCCESS"})"); Json::Value jv; if (BEAST_EXPECT(parseJSONString(json, jv))) @@ -232,9 +228,8 @@ public: } { - std::string const json( - R"({"CloseResolution":19,"Method":250,)" - R"("TransferFee":"65536"})"); + std::string const json(R"({"CloseResolution":19,"Method":250,)" + R"("TransferFee":"65536"})"); Json::Value jv; if (BEAST_EXPECT(parseJSONString(json, jv))) @@ -250,9 +245,8 @@ public: } { - std::string const json( - R"({"CloseResolution":19,"Method":250,)" - R"("TransferFee":"Payment"})"); + std::string const json(R"({"CloseResolution":19,"Method":250,)" + R"("TransferFee":"Payment"})"); Json::Value jv; if (BEAST_EXPECT(parseJSONString(json, jv))) @@ -268,9 +262,8 @@ public: } { - std::string const json( - R"({"CloseResolution":19,"Method":250,)" - R"("TransferFee":true})"); + std::string const json(R"({"CloseResolution":19,"Method":250,)" + R"("TransferFee":true})"); Json::Value jv; if (BEAST_EXPECT(parseJSONString(json, jv))) From 162875616d03729e1bc618688b7a7eb65e5e0e60 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 5 Aug 2025 21:05:26 -0400 Subject: [PATCH 048/291] Correct more issues related to 0-interest loans and some rounding issues - Addresses FIND-012 from audit. - If computePeriodicPaymentParts rounds the principal part to 0, add a small amount so that some principal is paid regardless of how extreme the loan parameters are. For XRP and MPTs, this just adds 1. For IOUs, compute an epsilon based on the scale of the original loan. (IOUs untested.) - Also move this function out of the detail namespace so direct unit tests can be written. (Pending.) - Adds the testLoanPayComputePeriodicPaymentValidRateInvariant from auditors with some minor modifications. - Fixes an assert that the periodic rate > 0, which won't be true if the loan interest rate is 0. --- src/test/app/Loan_test.cpp | 122 ++++++++++++++++++++++++++-- src/xrpld/app/misc/LendingHelpers.h | 117 +++++++++++++++----------- src/xrpld/app/tx/detail/LoanPay.cpp | 2 +- 3 files changed, 188 insertions(+), 53 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 6fb603bfd6..9146f23dd9 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -1824,7 +1824,10 @@ class Loan_test : public beast::unit_test::suite // The LoanSet json can be created without a counterparty signature, but // it is malformed. auto createJson = env.json( - set(lender, broker.brokerID, principalRequest, startDate), + set(lender, + broker.brokerID, + broker.asset(principalRequest).value(), + startDate), fee(loanSetFee)); env(createJson, ter(temMALFORMED)); @@ -1868,7 +1871,7 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT(types[type] == 1); } } - { + auto const loanID = [&]() { Json::Value params(Json::objectValue); params[jss::account] = lender.human(); params[jss::type] = "Loan"; @@ -1879,7 +1882,7 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT(objects.size() == 1); auto const loan = objects[0u]; - BEAST_EXPECT(loan[sfAssetsAvailable] == "1000"); + BEAST_EXPECT(loan[sfAssetsAvailable] == "1000000000"); BEAST_EXPECT(loan[sfBorrower] == lender.human()); BEAST_EXPECT(loan[sfCloseInterestRate] == 0); BEAST_EXPECT(loan[sfClosePaymentFee] == "0"); @@ -1899,12 +1902,23 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT(loan[sfPaymentInterval] == 60); BEAST_EXPECT(loan[sfPaymentRemaining] == 1); BEAST_EXPECT(loan[sfPreviousPaymentDate] == 0); - BEAST_EXPECT(loan[sfPrincipalOutstanding] == "1000"); - BEAST_EXPECT(loan[sfPrincipalRequested] == "1000"); + BEAST_EXPECT(loan[sfPrincipalOutstanding] == "1000000000"); + BEAST_EXPECT(loan[sfPrincipalRequested] == "1000000000"); BEAST_EXPECT( loan[sfStartDate].asUInt() == startDate.time_since_epoch().count()); - } + + return loan["index"].asString(); + }(); + auto const loanKeylet{keylet::loan(uint256{std::string_view(loanID)})}; + + env.close(startDate); + + // Draw the loan + env(draw(lender, loanKeylet.key, broker.asset(1000))); + env.close(); + // Make a payment + env(pay(lender, loanKeylet.key, broker.asset(1000))); } void @@ -2028,6 +2042,101 @@ class Loan_test : public beast::unit_test::suite env.close(); } + void + testLoanPayComputePeriodicPaymentValidRateInvariant() + { + testcase + << "LoanPay ripple::detail::computePeriodicPayment : valid rate"; + + using namespace jtx; + using namespace std::chrono_literals; + Env env(*this, all); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1'000'000), issuer, lender, borrower); + env.close(); + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + BrokerInfo broker{createVaultAndBroker(env, xrpAsset, lender)}; + + using namespace loan; + + auto const loanSetFee = fee(env.current()->fees().base * 2); + Number const principalRequest{640562, -5}; + auto const startDate = env.now() + 60s; + + Number const serviceFee{2462611968}; + std::uint32_t const numPayments{4294967295}; + + auto createJson = env.json( + set(borrower, broker.brokerID, principalRequest, startDate), + fee(loanSetFee), + loanServiceFee(serviceFee), + paymentTotal(numPayments), + json(sfCounterpartySignature, Json::objectValue)); + + createJson["CloseInterestRate"] = 55374; + createJson["ClosePaymentFee"] = "3825205248"; + createJson["GracePeriod"] = 0; + createJson["LatePaymentFee"] = "237"; + createJson["LoanOriginationFee"] = "0"; + createJson["OverpaymentFee"] = 35167; + createJson["OverpaymentInterestRate"] = 1360; + createJson["PaymentInterval"] = 727; + + Number const actualPrincipal{6}; + + auto const brokerStateBefore = + env.le(keylet::loanbroker(broker.brokerID)); + auto const loanSequence = brokerStateBefore->at(sfLoanSequence); + auto const keylet = keylet::loan(broker.brokerID, loanSequence); + + createJson = env.json(createJson, sig(sfCounterpartySignature, lender)); + env(createJson, ter(tesSUCCESS)); + env.close(startDate); + + if (auto const loan = env.le(keylet); BEAST_EXPECT(loan)) + { + // Verify the payment decreased the principal + BEAST_EXPECT(loan->at(sfPaymentRemaining) == numPayments); + BEAST_EXPECT(loan->at(sfPrincipalRequested) == actualPrincipal); + BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == actualPrincipal); + BEAST_EXPECT(loan->at(sfAssetsAvailable) == actualPrincipal); + } + + auto loanDrawTx = env.json( + draw(borrower, keylet.key, STAmount{broker.asset, Number{6}})); + env(loanDrawTx, ter(tesSUCCESS)); + env.close(); + + if (auto const loan = env.le(keylet); BEAST_EXPECT(loan)) + { + // Verify the payment decreased the principal + BEAST_EXPECT(loan->at(sfPaymentRemaining) == numPayments); + BEAST_EXPECT(loan->at(sfPrincipalRequested) == actualPrincipal); + BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == actualPrincipal); + BEAST_EXPECT(loan->at(sfAssetsAvailable) == actualPrincipal - 6); + } + + auto loanPayTx = env.json( + pay(borrower, keylet.key, STAmount{broker.asset, serviceFee + 6})); + env(loanPayTx, ter(tesSUCCESS)); + env.close(); + + if (auto const loan = env.le(keylet); BEAST_EXPECT(loan)) + { + // Verify the payment decreased the principal + BEAST_EXPECT(loan->at(sfPaymentRemaining) == numPayments - 1); + BEAST_EXPECT(loan->at(sfPrincipalRequested) == actualPrincipal); + BEAST_EXPECT( + loan->at(sfPrincipalOutstanding) == actualPrincipal - 1); + BEAST_EXPECT(loan->at(sfAssetsAvailable) == actualPrincipal - 6); + } + } + public: void run() override @@ -2037,6 +2146,7 @@ public: testLifecycle(); testBatchBypassCounterparty(); testWrongMaxDebtBehavior(); + testLoanPayComputePeriodicPaymentValidRateInvariant(); } }; diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 2b51f3d14d..022e9557e9 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -153,48 +153,6 @@ loanAccruedInterest( std::uint32_t prevPaymentDate, std::uint32_t paymentInterval); -struct PeriodicPayment -{ - Number interest; - Number principal; -}; - -template -PeriodicPayment -computePeriodicPaymentParts( - A const& asset, - Number const& originalPrincipal, - Number const& principalOutstanding, - Number const& periodicPaymentAmount, - Number const& periodicRate, - std::uint32_t paymentRemaining) -{ - if (paymentRemaining == 1) - { - // If there's only one payment left, we need to pay off the principal. - Number const interest = roundToAsset( - asset, - periodicPaymentAmount - principalOutstanding, - originalPrincipal); - return {interest, principalOutstanding}; - } - Number const interest = roundToAsset( - asset, principalOutstanding * periodicRate, originalPrincipal); - XRPL_ASSERT( - interest >= 0, - "ripple::detail::computePeriodicPayment : valid interest"); - - auto const roundedPayment = - roundToAsset(asset, periodicPaymentAmount, originalPrincipal); - Number const principal = - roundToAsset(asset, roundedPayment - interest, originalPrincipal); - XRPL_ASSERT( - principal > 0 && principal <= principalOutstanding, - "ripple::detail::computePeriodicPayment : valid principal"); - - return {interest, principal}; -} - inline Number minusManagementFee(Number value, TenthBips32 managementFeeRate) { @@ -283,6 +241,72 @@ loanLatePaymentInterest( originalPrincipal); } +struct PeriodicPaymentParts +{ + Number interest; + Number principal; +}; + +template +PeriodicPaymentParts +computePeriodicPaymentParts( + A const& asset, + Number const& originalPrincipal, + Number const& principalOutstanding, + Number const& periodicPaymentAmount, + Number const& periodicRate, + std::uint32_t paymentRemaining) +{ + if (paymentRemaining == 1) + { + // If there's only one payment left, we need to pay off the principal. + Number const interest = roundToAsset( + asset, + periodicPaymentAmount - principalOutstanding, + originalPrincipal); + return {interest, principalOutstanding}; + } + Number const interest = roundToAsset( + asset, principalOutstanding * periodicRate, originalPrincipal); + XRPL_ASSERT( + interest >= 0, + "ripple::detail::computePeriodicPayment : valid interest"); + + auto const roundedPayment = [&]() { + auto roundedPayment = + roundToAsset(asset, periodicPaymentAmount, originalPrincipal); + if (roundedPayment > interest) + return roundedPayment; + auto newPayment = roundedPayment; + if (asset.native() || !asset.template holds()) + { + // integral types, just add one + ++newPayment; + } + else + { + // Non-integral types: IOU. Add "dust" that will not be lost in + // rounding. + auto const epsilon = Number{1, originalPrincipal.exponent() - 14}; + newPayment += epsilon; + } + roundedPayment = roundToAsset(asset, newPayment, originalPrincipal); + XRPL_ASSERT_PARTS( + roundedPayment == newPayment, + "ripple::computePeriodicPaymentParts", + "epsilon preserved in rounding"); + return roundedPayment; + }(); + Number const principal = + roundToAsset(asset, roundedPayment - interest, originalPrincipal); + XRPL_ASSERT_PARTS( + principal > 0 && principal <= principalOutstanding, + "ripple::computePeriodicPaymentParts", + "valid principal"); + + return {interest, principal}; +} + struct LoanPaymentParts { Number principalPaid; @@ -335,7 +359,8 @@ loanComputePaymentParts( Number const periodicRate = detail::loanPeriodicRate(interestRate, paymentInterval); XRPL_ASSERT( - periodicRate > 0, "ripple::loanComputePaymentParts : valid rate"); + interestRate == 0 || periodicRate > 0, + "ripple::loanComputePaymentParts : valid rate"); // Don't round the payment amount. Only round the final computations using // it. @@ -345,7 +370,7 @@ loanComputePaymentParts( periodicPaymentAmount > 0, "ripple::computePeriodicPayment : valid payment"); - auto const periodic = detail::computePeriodicPaymentParts( + auto const periodic = computePeriodicPaymentParts( asset, originalPrincipalRequested, principalOutstandingField, @@ -514,12 +539,12 @@ loanComputePaymentParts( Number totalInterestPaid = 0; Number loanValueChange = 0; - std::optional future = periodic; + std::optional future = periodic; for (int i = 0; i < fullPeriodicPayments; ++i) { // Only do the work if we need to if (!future) - future = detail::computePeriodicPaymentParts( + future = computePeriodicPaymentParts( asset, originalPrincipalRequested, principalOutstandingField, diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 866a5d9914..6ed405aa6d 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -170,7 +170,7 @@ LoanPay::doApply() if (!vaultSle) return tefBAD_LEDGER; // LCOV_EXCL_LINE auto const vaultPseudoAccount = vaultSle->at(sfAccount); - auto const asset = vaultSle->at(sfAsset); + auto const asset = *vaultSle->at(sfAsset); //------------------------------------------------------ // Loan object state changes From 7dcc682e29dee086cbfe16a00eaa8a41487dae45 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 6 Aug 2025 19:49:03 -0400 Subject: [PATCH 049/291] Make LoanSet.CounterpartySignature optional in the Tx layout - Still required for the transaction to succeed (except inside a Batch, because the batch signers take care of that). - Started adding tests for Loan-related RPC and low-level math checks. Currently only implemented "sign" on a LoanSet to verify it can be done. --- .../xrpl/protocol/detail/transactions.macro | 2 +- include/xrpl/protocol/jss.h | 1 + src/test/app/Batch_test.cpp | 28 ------- src/test/app/Loan_test.cpp | 81 +++++++++++++++++-- src/xrpld/app/tx/detail/Batch.cpp | 2 + src/xrpld/app/tx/detail/LoanSet.cpp | 38 +++++++-- 6 files changed, 107 insertions(+), 45 deletions(-) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index fec6399238..651cdd509e 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -904,7 +904,7 @@ TRANSACTION(ttLOAN_SET, 80, LoanSet, {sfLoanBrokerID, soeREQUIRED}, {sfData, soeOPTIONAL}, {sfCounterparty, soeOPTIONAL}, - {sfCounterpartySignature, soeREQUIRED}, + {sfCounterpartySignature, soeOPTIONAL}, {sfLoanOriginationFee, soeOPTIONAL}, {sfLoanServiceFee, soeOPTIONAL}, {sfLatePaymentFee, soeOPTIONAL}, diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 76fc01a315..55e0eb22d4 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -59,6 +59,7 @@ JSS(BaseAsset); // in: Oracle JSS(BidMax); // in: AMM Bid JSS(BidMin); // in: AMM Bid JSS(ClearFlag); // field. +JSS(CounterpartySignature);// field. JSS(DeliverMax); // out: alias to Amount JSS(DeliverMin); // in: TransactionSign JSS(Destination); // in: TransactionSign; field. diff --git a/src/test/app/Batch_test.cpp b/src/test/app/Batch_test.cpp index 8f889f4b00..fc5e58635d 100644 --- a/src/test/app/Batch_test.cpp +++ b/src/test/app/Batch_test.cpp @@ -2648,30 +2648,6 @@ class Batch_test : public beast::unit_test::suite STAmount{asset, asset(500).value()}), lenderSeq + 2)); } - { - auto const [txIDs, batchID] = submitBatch( - env, - telENV_RPC_FAILED, - batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing), - batch::inner( - env.json( - set(lender, - brokerKeylet.key, - asset(1000).value(), - env.now() + 3600s), - // Must include a CounterpartySignature field. - // Transaction will not even parse at the RPC layer - sig(none), - fee(none), - seq(none)), - lenderSeq + 1), - batch::inner( - draw( - lender, - loanKeylet.key, - STAmount{asset, asset(500).value()}), - lenderSeq + 2)); - } { auto const [txIDs, batchID] = submitBatch( env, @@ -2683,7 +2659,6 @@ class Batch_test : public beast::unit_test::suite brokerKeylet.key, asset(1000).value(), env.now() + 3600s), - json(sfCounterpartySignature, Json::objectValue), // Counterparty must be set sig(none), fee(none), @@ -2709,7 +2684,6 @@ class Batch_test : public beast::unit_test::suite env.now() + 3600s), // Counterparty must sign the outer transaction counterparty(borrower.id()), - json(sfCounterpartySignature, Json::objectValue), sig(none), fee(none), seq(none)), @@ -2737,7 +2711,6 @@ class Batch_test : public beast::unit_test::suite asset(1000).value(), env.now() + 3600s), counterparty(borrower.id()), - json(sfCounterpartySignature, Json::objectValue), sig(none), fee(none), seq(none)), @@ -2773,7 +2746,6 @@ class Batch_test : public beast::unit_test::suite asset(1000).value(), env.now() + 3600s), counterparty(borrower.id()), - json(sfCounterpartySignature, Json::objectValue), sig(none), fee(none), seq(none)), diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 9146f23dd9..583e44c3d2 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -86,10 +86,11 @@ class Loan_test : public beast::unit_test::suite using namespace std::chrono_literals; using namespace loan; - // counter party signature is required on LoanSet + // counter party signature is optional on LoanSet. Confirm that by + // sending transaction without one. auto setTx = env.jt( set(alice, keylet.key, Number(10000), env.now() + 720h), - ter(temMALFORMED)); + ter(temDISABLED)); env(setTx); // All loan transactions are disabled. @@ -1822,23 +1823,35 @@ class Loan_test : public beast::unit_test::suite auto const startDate = env.now() + 60s; // The LoanSet json can be created without a counterparty signature, but - // it is malformed. + // it will not pass preflight auto createJson = env.json( set(lender, broker.brokerID, broker.asset(principalRequest).value(), startDate), fee(loanSetFee)); + env(createJson, ter(temBAD_SIGNER)); - env(createJson, ter(temMALFORMED)); - - // Adding an empty counterparty signature object is also malformed, but - // fails at the RPC level. + // Adding an empty counterparty signature object also fails, but + // at the RPC level. createJson = env.json( createJson, json(sfCounterpartySignature, Json::objectValue)); - env(createJson, ter(telENV_RPC_FAILED)); + if (auto const jt = env.jt(createJson); BEAST_EXPECT(jt.stx)) + { + Serializer s; + jt.stx->add(s); + auto const jr = env.rpc("submit", strHex(s.slice())); + + BEAST_EXPECT(jr.isMember(jss::result)); + auto const jResult = jr[jss::result]; + BEAST_EXPECT(jResult[jss::error] == "invalidTransaction"); + BEAST_EXPECT( + jResult[jss::error_exception] == + "fails local checks: Transaction has bad signature."); + } + // Copy the transaction signature into the counterparty signature. Json::Value counterpartyJson{Json::objectValue}; counterpartyJson[sfTxnSignature] = createJson[sfTxnSignature]; @@ -2137,6 +2150,55 @@ class Loan_test : public beast::unit_test::suite } } + void + testRPC() + { + // This will expand as more test cases are added. Some functionality is + // tested in other test functions. + testcase("RPC"); + + using namespace jtx; + + Env env(*this, all); + + Account const alice{"alice"}; + + { + auto const sk = alice.sk(); + auto const jr = env.rpc( + "sign", + encodeBase58Token(TokenType::FamilySeed, sk.data(), sk.size()), + R"({ "TransactionType" : "LoanSet", )" + R"("Account" : "rHP9W1SByc8on4vfFsBdt5sun2gZTnBhkx", )" + R"("Counterparty" : "rHP9W1SByc8on4vfFsBdt5sun2gZTnBhkx", )" + R"("LoanBrokerID" : )" + R"("EAFD2D37FE12815F00705F1B57173A16F94EE15E02D53AF5B683942B57ED53E9", )" + R"("PrincipalRequested" : "100000000", )" + R"("StartDate" : "807730340", "PaymentTotal" : 10000, )" + R"("PaymentInterval" : 1, "GracePeriod" : 300, )" + R"("Flags" : 65536, "Fee" : "24", "Sequence" : 1 })", + "offline"); + + BEAST_EXPECT( + jr.isMember(jss::result) && + jr[jss::result].isMember(jss::tx_json)); + auto const& tx = jr[jss::result][jss::tx_json]; + BEAST_EXPECT(!tx.isMember(jss::CounterpartySignature)); + BEAST_EXPECT( + tx.isMember(jss::TxnSignature) && + tx[jss::TxnSignature].asString().length() == 142); + } + } + + void + testBasicMath() + { + // Test the functions defined in LendingHelpers.h + testcase("Basic Math"); + + pass(); + } + public: void run() override @@ -2147,6 +2209,9 @@ public: testBatchBypassCounterparty(); testWrongMaxDebtBehavior(); testLoanPayComputePeriodicPaymentValidRateInvariant(); + + testRPC(); + testBasicMath(); } }; diff --git a/src/xrpld/app/tx/detail/Batch.cpp b/src/xrpld/app/tx/detail/Batch.cpp index 0678998dc3..4814b88361 100644 --- a/src/xrpld/app/tx/detail/Batch.cpp +++ b/src/xrpld/app/tx/detail/Batch.cpp @@ -310,6 +310,8 @@ Batch::preflight(PreflightContext const& ctx) if (auto const ret = checkSignatureFields(stx, hash)) return ret; + // Note that the CounterpartySignature is optional, and should not be + // included, but if it is, ensure it doesn't contain a signature. if (stx.isFieldPresent(sfCounterpartySignature)) { auto const counterpartySignature = diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 3d9f1775b0..40605c30fc 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -73,11 +73,25 @@ LoanSet::preflight(PreflightContext const& ctx) return temBAD_SIGNER; } - auto const counterPartySig = ctx.tx.getFieldObject(sfCounterpartySignature); + // These extra hoops are because STObjects cannot be Proxy'd from STObject. + auto const counterPartySig = [&tx]() -> std::optional { + if (tx.isFieldPresent(sfCounterpartySignature)) + return tx.getFieldObject(sfCounterpartySignature); + return std::nullopt; + }(); + if (!tx.isFlag(tfInnerBatchTxn) && !counterPartySig) + { + JLOG(ctx.j.warn()) + << "LoanSet transaction must have a CounterpartySignature."; + return temBAD_SIGNER; + } - if (auto const ret = - ripple::detail::preflightCheckSigningKey(counterPartySig, ctx.j)) - return ret; + if (counterPartySig) + { + if (auto const ret = ripple::detail::preflightCheckSigningKey( + *counterPartySig, ctx.j)) + return ret; + } if (auto const data = tx[~sfData]; data && !data->empty() && !validDataLength(tx[~sfData], maxDataPayloadLength)) @@ -109,9 +123,12 @@ LoanSet::preflight(PreflightContext const& ctx) return temINVALID; // Copied from preflight2 - if (auto const ret = ripple::detail::preflightCheckSimulateKeys( - ctx.flags, counterPartySig, ctx.j)) - return *ret; + if (counterPartySig) + { + if (auto const ret = ripple::detail::preflightCheckSimulateKeys( + ctx.flags, *counterPartySig, ctx.j)) + return *ret; + } return tesSUCCESS; } @@ -136,7 +153,10 @@ LoanSet::checkSign(PreclaimContext const& ctx) }(); if (!counterSigner) return temBAD_SIGNER; - // Counterparty signature is required + + // Counterparty signature is optional. Presence is checked in preflight. + if (!ctx.tx.isFieldPresent(sfCounterpartySignature)) + return tesSUCCESS; auto const counterSig = ctx.tx.getFieldObject(sfCounterpartySignature); return Transactor::checkSign(ctx, *counterSigner, counterSig); } @@ -150,6 +170,8 @@ LoanSet::calculateBaseFee(ReadView const& view, STTx const& tx) // CounterpartySignature, whether a single signature or a multisignature XRPAmount const baseFee = view.fees().base; + // Counterparty signature is optional, but getFieldObject will return an + // empty object if it's not present. auto const counterSig = tx.getFieldObject(sfCounterpartySignature); // Each signer adds one more baseFee to the minimum required fee // for the transaction. Note that unlike the base class, the single signer From 6d137e44dc2c2a734e93b06df2f3341db0a63aa6 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sat, 9 Aug 2025 00:55:00 -0400 Subject: [PATCH 050/291] Add Counterparty signing support - Add a new parameter "signature_target" to "sign". Supports single and multisign, but I haven't written tests for multisign yet. - Skip account validation if this field is set, like multisigning. - Unit tests demonstrating examples. --- include/xrpl/protocol/Protocol.h | 1 + include/xrpl/protocol/STObject.h | 2 + include/xrpl/protocol/STTx.h | 6 +- include/xrpl/protocol/jss.h | 2 + src/libxrpl/protocol/STObject.cpp | 6 + src/libxrpl/protocol/STTx.cpp | 15 +- src/test/app/Loan_test.cpp | 311 +++++++++++++++++++++-- src/xrpld/app/main/Main.cpp | 2 +- src/xrpld/net/detail/RPCCall.cpp | 16 +- src/xrpld/rpc/detail/TransactionSign.cpp | 71 +++++- 10 files changed, 398 insertions(+), 34 deletions(-) diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 8919a475c6..5def4445d9 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -23,6 +23,7 @@ #include #include #include +#include #include diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index ef88fd08ae..4a3d104b01 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -389,6 +389,8 @@ public: setFieldV256(SField const& field, STVector256 const& v); void setFieldArray(SField const& field, STArray const& v); + void + setFieldObject(SField const& field, STObject const& v); template void diff --git a/include/xrpl/protocol/STTx.h b/include/xrpl/protocol/STTx.h index 012b33412a..1e3695050d 100644 --- a/include/xrpl/protocol/STTx.h +++ b/include/xrpl/protocol/STTx.h @@ -125,7 +125,11 @@ public: getJson(JsonOptions options, bool binary) const; void - sign(PublicKey const& publicKey, SecretKey const& secretKey); + sign( + PublicKey const& publicKey, + SecretKey const& secretKey, + std::optional> signatureTarget = + {}); enum class RequireFullyCanonicalSig : bool { no, yes }; diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 55e0eb22d4..eeeb048e04 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -59,6 +59,7 @@ JSS(BaseAsset); // in: Oracle JSS(BidMax); // in: AMM Bid JSS(BidMin); // in: AMM Bid JSS(ClearFlag); // field. +JSS(Counterparty); // field. JSS(CounterpartySignature);// field. JSS(DeliverMax); // out: alias to Amount JSS(DeliverMin); // in: TransactionSign @@ -567,6 +568,7 @@ JSS(settle_delay); // out: AccountChannels JSS(severity); // in: LogLevel JSS(shares); // out: VaultInfo JSS(signature); // out: NetworkOPs, ChannelAuthorize +JSS(signature_target); // in: TransactionSign JSS(signature_verified); // out: ChannelVerify JSS(signing_key); // out: NetworkOPs JSS(signing_keys); // out: ValidatorList diff --git a/src/libxrpl/protocol/STObject.cpp b/src/libxrpl/protocol/STObject.cpp index f7e9af7417..045a55e7a3 100644 --- a/src/libxrpl/protocol/STObject.cpp +++ b/src/libxrpl/protocol/STObject.cpp @@ -831,6 +831,12 @@ STObject::setFieldArray(SField const& field, STArray const& v) setFieldUsingAssignment(field, v); } +void +STObject::setFieldObject(SField const& field, STObject const& v) +{ + setFieldUsingAssignment(field, v); +} + Json::Value STObject::getJson(JsonOptions options) const { diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index 8893562b97..efff940a99 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -234,13 +234,24 @@ STTx::getSeqValue() const } void -STTx::sign(PublicKey const& publicKey, SecretKey const& secretKey) +STTx::sign( + PublicKey const& publicKey, + SecretKey const& secretKey, + std::optional> signatureTarget) { auto const data = getSigningData(*this); auto const sig = ripple::sign(publicKey, secretKey, makeSlice(data)); - setFieldVL(sfTxnSignature, sig); + if (signatureTarget) + { + auto& target = peekFieldObject(*signatureTarget); + target.setFieldVL(sfTxnSignature, sig); + } + else + { + setFieldVL(sfTxnSignature, sig); + } tid_ = getHash(HashPrefix::transactionID); } diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 583e44c3d2..441107d61b 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -2162,31 +2163,301 @@ class Loan_test : public beast::unit_test::suite Env env(*this, all); Account const alice{"alice"}; + std::string const borrowerPass = "borrower"; + std::string const borrowerSeed = "ssBRAsLpH4778sLNYC4ik1JBJsBVf"; + Account borrower{borrowerPass, KeyType::ed25519}; + auto const lenderPass = "lender"; + std::string const lenderSeed = "shPTCZGwTEhJrYT8NbcNkeaa8pzPM"; + Account lender{lenderPass, KeyType::ed25519}; + + env.fund(XRP(1'000'000), alice, lender, borrower); + env.close(); + env(noop(lender)); + env(noop(lender)); + env(noop(lender)); + env(noop(lender)); + env(noop(lender)); + env.close(); { - auto const sk = alice.sk(); - auto const jr = env.rpc( - "sign", - encodeBase58Token(TokenType::FamilySeed, sk.data(), sk.size()), - R"({ "TransactionType" : "LoanSet", )" - R"("Account" : "rHP9W1SByc8on4vfFsBdt5sun2gZTnBhkx", )" - R"("Counterparty" : "rHP9W1SByc8on4vfFsBdt5sun2gZTnBhkx", )" - R"("LoanBrokerID" : )" - R"("EAFD2D37FE12815F00705F1B57173A16F94EE15E02D53AF5B683942B57ED53E9", )" - R"("PrincipalRequested" : "100000000", )" - R"("StartDate" : "807730340", "PaymentTotal" : 10000, )" - R"("PaymentInterval" : 1, "GracePeriod" : 300, )" - R"("Flags" : 65536, "Fee" : "24", "Sequence" : 1 })", - "offline"); + testcase("RPC AccountSet"); + Json::Value txJson{Json::objectValue}; + txJson[sfTransactionType] = "AccountSet"; + txJson[sfAccount] = borrower.human(); + + auto const signParams = [&]() { + Json::Value signParams{Json::objectValue}; + signParams[jss::passphrase] = borrowerPass; + signParams[jss::key_type] = "ed25519"; + signParams[jss::tx_json] = txJson; + return signParams; + }(); + auto const jSign = env.rpc("json", "sign", to_string(signParams)); + BEAST_EXPECT( + jSign.isMember(jss::result) && + jSign[jss::result].isMember(jss::tx_json)); + auto txSignResult = jSign[jss::result][jss::tx_json]; + auto txSignBlob = jSign[jss::result][jss::tx_blob].asString(); + txSignResult.removeMember(jss::hash); + + auto const jtx = env.jt(txJson, sig(borrower)); + BEAST_EXPECT(txSignResult == jtx.jv); + + auto const jSubmit = env.rpc("submit", txSignBlob); + BEAST_EXPECT( + jSubmit.isMember(jss::result) && + jSubmit[jss::result].isMember(jss::engine_result) && + jSubmit[jss::result][jss::engine_result].asString() == + "tesSUCCESS"); + + env(jtx.jv, sig(none), seq(none), fee(none), ter(tefPAST_SEQ)); + } + + { + testcase("RPC LoanSet - illegal signature_target"); + + Json::Value txJson{Json::objectValue}; + txJson[sfTransactionType] = "AccountSet"; + txJson[sfAccount] = borrower.human(); + + auto const borrowerSignParams = [&]() { + Json::Value params{Json::objectValue}; + params[jss::passphrase] = borrowerPass; + params[jss::key_type] = "ed25519"; + params[jss::signature_target] = "Destination"; + params[jss::tx_json] = txJson; + return params; + }(); + auto const jSignBorrower = + env.rpc("json", "sign", to_string(borrowerSignParams)); + BEAST_EXPECT( + jSignBorrower.isMember(jss::result) && + jSignBorrower[jss::result].isMember(jss::error) && + jSignBorrower[jss::result][jss::error] == "invalidParams" && + jSignBorrower[jss::result].isMember(jss::error_message) && + jSignBorrower[jss::result][jss::error_message] == + "Destination"); + } + { + testcase("RPC LoanSet - sign and submit borrower initiated"); + // 1. Borrower creates the transaction + Json::Value txJson{Json::objectValue}; + txJson[sfTransactionType] = "LoanSet"; + txJson[sfAccount] = borrower.human(); + txJson[sfCounterparty] = lender.human(); + txJson[sfLoanBrokerID] = + "FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FECF83F" + "5C"; + txJson[sfPrincipalRequested] = "100000000"; + txJson[sfStartDate] = 807730340; + txJson[sfPaymentTotal] = 10000; + txJson[sfPaymentInterval] = 3600; + txJson[sfGracePeriod] = 300; + txJson[sfFlags] = 65536; // tfLoanOverpayment + txJson[sfFee] = "24"; + + // 2. Borrower signs the transaction + auto const borrowerSignParams = [&]() { + Json::Value params{Json::objectValue}; + params[jss::passphrase] = borrowerPass; + params[jss::key_type] = "ed25519"; + params[jss::tx_json] = txJson; + return params; + }(); + auto const jSignBorrower = + env.rpc("json", "sign", to_string(borrowerSignParams)); + BEAST_EXPECT( + jSignBorrower.isMember(jss::result) && + jSignBorrower[jss::result].isMember(jss::tx_json)); + auto const txBorrowerSignResult = + jSignBorrower[jss::result][jss::tx_json]; + auto const txBorrowerSignBlob = + jSignBorrower[jss::result][jss::tx_blob].asString(); + + // 2a. Borrower attempts to submit the transaction. It doesn't work + { + auto const jSubmitBlob = env.rpc("submit", txBorrowerSignBlob); + BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); + auto const jSubmitBlobResult = jSubmitBlob[jss::result]; + BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json)); + // Transaction fails because the CounterpartySignature is + // missing + BEAST_EXPECT( + jSubmitBlobResult.isMember(jss::engine_result) && + jSubmitBlobResult[jss::engine_result].asString() == + "temBAD_SIGNER"); + } + + // 3. Borrower sends the signed transaction to the lender + // 4. Lender signs the transaction + auto const lenderSignParams = [&]() { + Json::Value params{Json::objectValue}; + params[jss::passphrase] = lenderPass; + params[jss::key_type] = "ed25519"; + params[jss::signature_target] = "CounterpartySignature"; + params[jss::tx_json] = txBorrowerSignResult; + return params; + }(); + auto const jSignLender = + env.rpc("json", "sign", to_string(lenderSignParams)); + BEAST_EXPECT( + jSignLender.isMember(jss::result) && + jSignLender[jss::result].isMember(jss::tx_json)); + auto const txLenderSignResult = + jSignLender[jss::result][jss::tx_json]; + auto const txLenderSignBlob = + jSignLender[jss::result][jss::tx_blob].asString(); + + // 5. Lender submits the signed transaction blob + auto const jSubmitBlob = env.rpc("submit", txLenderSignBlob); + BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); + auto const jSubmitBlobResult = jSubmitBlob[jss::result]; + BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json)); + auto const jSubmitBlobTx = jSubmitBlobResult[jss::tx_json]; + // To get far enough to return tecNO_ENTRY means that the signatures + // all validated. Of course the transaction won't succeed because no + // Vault or Broker were created. + BEAST_EXPECT( + jSubmitBlobResult.isMember(jss::engine_result) && + jSubmitBlobResult[jss::engine_result].asString() == + "tecNO_ENTRY"); BEAST_EXPECT( - jr.isMember(jss::result) && - jr[jss::result].isMember(jss::tx_json)); - auto const& tx = jr[jss::result][jss::tx_json]; - BEAST_EXPECT(!tx.isMember(jss::CounterpartySignature)); + !jSubmitBlob.isMember(jss::error) && + !jSubmitBlobResult.isMember(jss::error)); + + // 4-alt. Lender submits the transaction json originally received + // from the Borrower. It gets signed, but is now a duplicate, so + // fails. Borrower could done this instead of steps 4 and 5. + auto const jSubmitJson = + env.rpc("json", "submit", to_string(lenderSignParams)); + BEAST_EXPECT(jSubmitJson.isMember(jss::result)); + auto const jSubmitJsonResult = jSubmitJson[jss::result]; + BEAST_EXPECT(jSubmitJsonResult.isMember(jss::tx_json)); + auto const jSubmitJsonTx = jSubmitJsonResult[jss::tx_json]; + // Since the previous tx claimed a fee, this duplicate is not going + // anywhere BEAST_EXPECT( - tx.isMember(jss::TxnSignature) && - tx[jss::TxnSignature].asString().length() == 142); + jSubmitJsonResult.isMember(jss::engine_result) && + jSubmitJsonResult[jss::engine_result].asString() == + "tefPAST_SEQ"); + + BEAST_EXPECT( + !jSubmitJson.isMember(jss::error) && + !jSubmitJsonResult.isMember(jss::error)); + + BEAST_EXPECT(jSubmitBlobTx == jSubmitJsonTx); + } + + { + testcase("RPC LoanSet - sign and submit lender initiated"); + // 1. Lender creates the transaction + Json::Value txJson{Json::objectValue}; + txJson[sfTransactionType] = "LoanSet"; + txJson[sfAccount] = lender.human(); + txJson[sfCounterparty] = borrower.human(); + txJson[sfLoanBrokerID] = + "FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FECF83F" + "5C"; + txJson[sfPrincipalRequested] = "100000000"; + txJson[sfStartDate] = 807730340; + txJson[sfPaymentTotal] = 10000; + txJson[sfPaymentInterval] = 3600; + txJson[sfGracePeriod] = 300; + txJson[sfFlags] = 65536; // tfLoanOverpayment + txJson[sfFee] = "24"; + + // 2. Lender signs the transaction + auto const lenderSignParams = [&]() { + Json::Value params{Json::objectValue}; + params[jss::passphrase] = lenderPass; + params[jss::key_type] = "ed25519"; + params[jss::tx_json] = txJson; + return params; + }(); + auto const jSignLender = + env.rpc("json", "sign", to_string(lenderSignParams)); + BEAST_EXPECT( + jSignLender.isMember(jss::result) && + jSignLender[jss::result].isMember(jss::tx_json)); + auto const txLenderSignResult = + jSignLender[jss::result][jss::tx_json]; + auto const txLenderSignBlob = + jSignLender[jss::result][jss::tx_blob].asString(); + + // 2a. Lender attempts to submit the transaction. It doesn't work + { + auto const jSubmitBlob = env.rpc("submit", txLenderSignBlob); + BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); + auto const jSubmitBlobResult = jSubmitBlob[jss::result]; + BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json)); + // Transaction fails because the CounterpartySignature is + // missing + BEAST_EXPECT( + jSubmitBlobResult.isMember(jss::engine_result) && + jSubmitBlobResult[jss::engine_result].asString() == + "temBAD_SIGNER"); + } + + // 3. Lender sends the signed transaction to the Borrower + // 4. Borrower signs the transaction + auto const borrowerSignParams = [&]() { + Json::Value params{Json::objectValue}; + params[jss::passphrase] = borrowerPass; + params[jss::key_type] = "ed25519"; + params[jss::signature_target] = "CounterpartySignature"; + params[jss::tx_json] = txLenderSignResult; + return params; + }(); + auto const jSignBorrower = + env.rpc("json", "sign", to_string(borrowerSignParams)); + BEAST_EXPECT( + jSignBorrower.isMember(jss::result) && + jSignBorrower[jss::result].isMember(jss::tx_json)); + auto const txBorrowerSignResult = + jSignBorrower[jss::result][jss::tx_json]; + auto const txBorrowerSignBlob = + jSignBorrower[jss::result][jss::tx_blob].asString(); + + // 5. Borrower submits the signed transaction blob + auto const jSubmitBlob = env.rpc("submit", txBorrowerSignBlob); + BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); + auto const jSubmitBlobResult = jSubmitBlob[jss::result]; + BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json)); + auto const jSubmitBlobTx = jSubmitBlobResult[jss::tx_json]; + // To get far enough to return tecNO_ENTRY means that the signatures + // all validated. Of course the transaction won't succeed because no + // Vault or Broker were created. + BEAST_EXPECT( + jSubmitBlobResult.isMember(jss::engine_result) && + jSubmitBlobResult[jss::engine_result].asString() == + "tecNO_ENTRY"); + + BEAST_EXPECT( + !jSubmitBlob.isMember(jss::error) && + !jSubmitBlobResult.isMember(jss::error)); + + // 4-alt. Borrower submits the transaction json originally received + // from the Lender. It gets signed, but is now a duplicate, so + // fails. Lender could done this instead of steps 4 and 5. + auto const jSubmitJson = + env.rpc("json", "submit", to_string(borrowerSignParams)); + BEAST_EXPECT(jSubmitJson.isMember(jss::result)); + auto const jSubmitJsonResult = jSubmitJson[jss::result]; + BEAST_EXPECT(jSubmitJsonResult.isMember(jss::tx_json)); + auto const jSubmitJsonTx = jSubmitJsonResult[jss::tx_json]; + // Since the previous tx claimed a fee, this duplicate is not going + // anywhere + BEAST_EXPECT( + jSubmitJsonResult.isMember(jss::engine_result) && + jSubmitJsonResult[jss::engine_result].asString() == + "tefPAST_SEQ"); + + BEAST_EXPECT( + !jSubmitJson.isMember(jss::error) && + !jSubmitJsonResult.isMember(jss::error)); + + BEAST_EXPECT(jSubmitBlobTx == jSubmitJsonTx); } } diff --git a/src/xrpld/app/main/Main.cpp b/src/xrpld/app/main/Main.cpp index 19c8c9910d..95423397c4 100644 --- a/src/xrpld/app/main/Main.cpp +++ b/src/xrpld/app/main/Main.cpp @@ -168,7 +168,7 @@ printHelp(po::options_description const& desc) " server_state [counters]\n" " sign [offline]\n" " sign_for " - "[offline]\n" + "[offline] []\n" " stop\n" " simulate [|] []\n" " submit |[ ]\n" diff --git a/src/xrpld/net/detail/RPCCall.cpp b/src/xrpld/net/detail/RPCCall.cpp index 0cc3cb6618..d66eca7a80 100644 --- a/src/xrpld/net/detail/RPCCall.cpp +++ b/src/xrpld/net/detail/RPCCall.cpp @@ -966,7 +966,16 @@ private: Json::Value txJSON; Json::Reader reader; bool const bOffline = - 3 == jvParams.size() && jvParams[2u].asString() == "offline"; + jvParams.size() >= 3 && jvParams[2u].asString() == "offline"; + std::optional const field = + [&jvParams, bOffline]() -> std::optional { + if (jvParams.size() < 3) + return std::nullopt; + if (jvParams.size() < 4 && bOffline) + return std::nullopt; + Json::UInt index = bOffline ? 3u : 2u; + return jvParams[index].asString(); + }(); if (1 == jvParams.size()) { @@ -979,7 +988,7 @@ private: return jvRequest; } else if ( - (2 == jvParams.size() || bOffline) && + (jvParams.size() >= 2 || bOffline) && reader.parse(jvParams[1u].asString(), txJSON)) { // Signing or submitting tx_json. @@ -991,6 +1000,9 @@ private: if (bOffline) jvRequest[jss::offline] = true; + if (field) + jvRequest[jss::signature_target] = *field; + return jvRequest; } diff --git a/src/xrpld/rpc/detail/TransactionSign.cpp b/src/xrpld/rpc/detail/TransactionSign.cpp index 175fd84c9b..aa7c706a19 100644 --- a/src/xrpld/rpc/detail/TransactionSign.cpp +++ b/src/xrpld/rpc/detail/TransactionSign.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -54,6 +55,7 @@ private: AccountID const* const multiSigningAcctID_; std::optional multiSignPublicKey_; Buffer multiSignature_; + std::optional> signatureTarget_; public: explicit SigningForParams() : multiSigningAcctID_(nullptr) @@ -116,12 +118,25 @@ public: return multiSignature_; } + std::optional> const& + getSignatureTarget() const + { + return signatureTarget_; + } + void setPublicKey(PublicKey const& multiSignPublicKey) { multiSignPublicKey_ = multiSignPublicKey; } + void + setSignatureTarget( + std::optional> const& field) + { + signatureTarget_ = field; + } + void moveMultiSignature(Buffer&& multiSignature) { @@ -427,6 +442,29 @@ transactionPreProcessImpl( bool const verify = !(params.isMember(jss::offline) && params[jss::offline].asBool()); + auto const signatureTarget = + [¶ms]() -> std::optional> { + if (params.isMember(jss::signature_target)) + return SField::getField(params[jss::signature_target].asString()); + return std::nullopt; + }(); + + // Make sure the signature target field is valid, if specified, and save the + // template for use later + auto const signatureTemplate = signatureTarget + ? InnerObjectFormats::getInstance().findSOTemplateBySField( + *signatureTarget) + : nullptr; + if (signatureTarget) + { + if (!signatureTemplate) + { // Invalid target field + return RPC::make_error( + rpcINVALID_PARAMS, signatureTarget->get().getName()); + } + signingArgs.setSignatureTarget(signatureTarget); + } + if (!params.isMember(jss::tx_json)) return RPC::missing_field_error(jss::tx_json); @@ -541,9 +579,10 @@ transactionPreProcessImpl( JLOG(j.trace()) << "verify: " << toBase58(calcAccountID(pk)) << " : " << toBase58(srcAddressID); - // Don't do this test if multisigning since the account and secret - // probably don't belong together in that case. - if (!signingArgs.isMultiSigning()) + // Don't do this test if multisigning or if the signature is going into + // an alternate field since the account and secret probably don't belong + // together in that case. + if (!signingArgs.isMultiSigning() && !signatureTarget) { // Make sure the account and secret belong together. if (tx_json.isMember(sfDelegate.jsonName)) @@ -598,7 +637,17 @@ transactionPreProcessImpl( { // If we're generating a multi-signature the SigningPubKey must be // empty, otherwise it must be the master account's public key. - parsed.object->setFieldVL( + STObject* sigObject = &*parsed.object; + if (signatureTarget) + { + // If the target object doesn't exist, make one. + if (!parsed.object->isFieldPresent(*signatureTarget)) + parsed.object->setFieldObject( + *signatureTarget, + STObject{*signatureTemplate, *signatureTarget}); + sigObject = &parsed.object->peekFieldObject(*signatureTarget); + } + sigObject->setFieldVL( sfSigningPubKey, signingArgs.isMultiSigning() ? Slice(nullptr, 0) : pk.slice()); @@ -630,7 +679,7 @@ transactionPreProcessImpl( } else if (signingArgs.isSingleSigning()) { - stTx->sign(pk, sk); + stTx->sign(pk, sk, signatureTarget); } return transactionPreProcessResult{std::move(stTx)}; @@ -1195,11 +1244,17 @@ transactionSignFor( signer.setFieldVL( sfSigningPubKey, signForParams.getPublicKey().slice()); + STObject& sigTarget = [&]() -> STObject& { + auto const target = signForParams.getSignatureTarget(); + if (target) + return sttx->peekFieldObject(*target); + return *sttx; + }(); // If there is not yet a Signers array, make one. - if (!sttx->isFieldPresent(sfSigners)) - sttx->setFieldArray(sfSigners, {}); + if (!sigTarget.isFieldPresent(sfSigners)) + sigTarget.setFieldArray(sfSigners, {}); - auto& signers = sttx->peekFieldArray(sfSigners); + auto& signers = sigTarget.peekFieldArray(sfSigners); signers.emplace_back(std::move(signer)); // The array must be sorted and validated. From bc3c9e1534ddd2a582912415429400fc8a069a84 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sat, 9 Aug 2025 01:19:38 -0400 Subject: [PATCH 051/291] fixup! Add Counterparty signing support --- src/test/app/Loan_test.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 441107d61b..8825f3fd29 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -25,7 +25,9 @@ #include #include #include +#include #include +#include #include #include #include From e62efa2a90bea3298df593c2aa801bac6c3f4a0b Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 18 Aug 2025 20:32:53 -0400 Subject: [PATCH 052/291] Fix unit tests broken by the addition of signature_target - Specifically some of the tests for sign and submit --- src/test/rpc/RPCCall_test.cpp | 98 +++++++++++++++++++++++++------- src/xrpld/net/detail/RPCCall.cpp | 4 +- 2 files changed, 79 insertions(+), 23 deletions(-) diff --git a/src/test/rpc/RPCCall_test.cpp b/src/test/rpc/RPCCall_test.cpp index be0f32b5ce..c77059235d 100644 --- a/src/test/rpc/RPCCall_test.cpp +++ b/src/test/rpc/RPCCall_test.cpp @@ -4643,10 +4643,34 @@ static RPCCallTestData const rpcCallTestArray[] = { } ] })"}, - {"sign: too many arguments.", + {"sign: offline flag with signature_target.", __LINE__, {"sign", "my_secret", R"({"json_argument":true})", "offline", "extra"}, RPCCallTestData::no_exception, + R"({ + "method" : "sign", + "params" : [ + { + "api_version" : %API_VER%, + "offline" : true, + "secret" : "my_secret", + "signature_target" : "extra", + "tx_json" : + { + "json_argument" : true + } + } + ] + })"}, + {"sign: too many arguments.", + __LINE__, + {"sign", + "my_secret", + R"({"json_argument":true})", + "offline", + "CounterpartySignature", + "extra"}, + RPCCallTestData::no_exception, R"({ "method" : "sign", "params" : [ @@ -4675,20 +4699,24 @@ static RPCCallTestData const rpcCallTestArray[] = { } ] })"}, - {"sign: invalid final argument.", + {"sign: misspelled offline flag interpreted as signature_target.", __LINE__, {"sign", "my_secret", R"({"json_argument":true})", "offlin"}, RPCCallTestData::no_exception, R"({ - "method" : "sign", - "params" : [ - { - "error" : "invalidParams", - "error_code" : 31, - "error_message" : "Invalid parameters." - } - ] - })"}, + "method" : "sign", + "params" : [ + { + "api_version" : %API_VER%, + "secret" : "my_secret", + "signature_target" : "offlin", + "tx_json" : + { + "json_argument" : true + } + } + ] + })"}, // sign_for // -------------------------------------------------------------------- @@ -4880,10 +4908,34 @@ static RPCCallTestData const rpcCallTestArray[] = { } ] })"}, - {"submit: too many arguments.", + {"submit: offline flag with signature_target.", __LINE__, {"submit", "my_secret", R"({"json_argument":true})", "offline", "extra"}, RPCCallTestData::no_exception, + R"({ + "method" : "submit", + "params" : [ + { + "api_version" : %API_VER%, + "offline" : true, + "secret" : "my_secret", + "signature_target" : "extra", + "tx_json" : + { + "json_argument" : true + } + } + ] + })"}, + {"submit: too many arguments.", + __LINE__, + {"submit", + "my_secret", + R"({"json_argument":true})", + "offline", + "CounterpartySignature", + "extra"}, + RPCCallTestData::no_exception, R"({ "method" : "submit", "params" : [ @@ -4912,19 +4964,23 @@ static RPCCallTestData const rpcCallTestArray[] = { } ] })"}, - {"submit: last argument not \"offline\".", + {"submit: misspelled offline flag interpreted as signature_target.", __LINE__, {"submit", "my_secret", R"({"json_argument":true})", "offlne"}, RPCCallTestData::no_exception, R"({ - "method" : "submit", - "params" : [ - { - "error" : "invalidParams", - "error_code" : 31, - "error_message" : "Invalid parameters." - } - ] + "method" : "submit", + "params" : [ + { + "api_version" : %API_VER%, + "secret" : "my_secret", + "signature_target" : "offlne", + "tx_json" : + { + "json_argument" : true + } + } + ] })"}, // submit_multisigned diff --git a/src/xrpld/net/detail/RPCCall.cpp b/src/xrpld/net/detail/RPCCall.cpp index d66eca7a80..7bd0aeeab5 100644 --- a/src/xrpld/net/detail/RPCCall.cpp +++ b/src/xrpld/net/detail/RPCCall.cpp @@ -1283,11 +1283,11 @@ public: {"server_definitions", &RPCParser::parseServerDefinitions, 0, 1}, {"server_info", &RPCParser::parseServerInfo, 0, 1}, {"server_state", &RPCParser::parseServerInfo, 0, 1}, - {"sign", &RPCParser::parseSignSubmit, 2, 3}, + {"sign", &RPCParser::parseSignSubmit, 2, 4}, {"sign_for", &RPCParser::parseSignFor, 3, 4}, {"stop", &RPCParser::parseAsIs, 0, 0}, {"simulate", &RPCParser::parseSimulate, 1, 2}, - {"submit", &RPCParser::parseSignSubmit, 1, 3}, + {"submit", &RPCParser::parseSignSubmit, 1, 4}, {"submit_multisigned", &RPCParser::parseSubmitMultiSigned, 1, 1}, {"transaction_entry", &RPCParser::parseTransactionEntry, 2, 2}, {"tx", &RPCParser::parseTx, 1, 4}, From 083ab7b0543e0aeb57c6790b6aa719e9fb26237c Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 18 Aug 2025 16:31:07 -0400 Subject: [PATCH 053/291] Prevent Vault unrealized loss from exceeding "unavailable" assets --- src/xrpld/app/tx/detail/InvariantCheck.cpp | 2 +- src/xrpld/app/tx/detail/LoanManage.cpp | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index debf3ee9ed..51ba4df5f3 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -2422,7 +2422,7 @@ ValidLoan::finalize( ReadView const& view, beast::Journal const& j) { - // Loan Brokers will not exist on ledger if the Lending Protocol amendment + // Loans will not exist on ledger if the Lending Protocol amendment // is not enabled, so there's no need to check it. for (auto const& [before, after] : loans_) diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index a218059ade..a23c18c269 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -287,8 +287,17 @@ impairLoan( beast::Journal j) { // Update the Vault object(set "paper loss") - vaultSle->at(sfLossUnrealized) += - principalOutstanding + interestOutstanding; + auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized); + vaultLossUnrealizedProxy += principalOutstanding + interestOutstanding; + if (vaultLossUnrealizedProxy > + vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable)) + { + // Having a loss greater than the vault's unavailable assets + // will leave the vault in an invalid / inconsistent state. + JLOG(j.warn()) << "Vault unrealized loss is too large, and will " + "corrupt the vault."; + return tecLIMIT_EXCEEDED; + } view.update(vaultSle); // Update the Loan object From 9920037d130a5d70589b7b12a9e9d7233c4d0f66 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 19 Aug 2025 19:52:30 -0400 Subject: [PATCH 054/291] Review feedback from @dangell7 - Cleaned up some of the `LEDGER_ENTRY` macros by eliding unnecessary parameters. - Define the transaction privileges in one place (InvariantCheck.cpp). - Give `EscrowFinish` the `mayAuthorizeMPT` privilege. - Rename the test helper `expectLine` to `expectHolding` since it handles both trust lines and MPTs. - Restructure the ""pseudo-account has 2 pseudo-account fields set" invariant test to loop over all defined pseudo-account fields. - Fix `operator<<` for `PrettyAmount` to handle `MPTIssue`s. - Add enforcement of the `AccountRootsDeletedClean` invariant if SAV is enabled, and clarify the comment for the pseudo-account field check. - Delete the 100% redundant `ttMPTOKEN_ISSUANCE_SET` check in `ValidMPTIssuance`. --- include/xrpl/protocol/LedgerFormats.h | 2 +- .../xrpl/protocol/detail/transactions.macro | 21 +- include/xrpl/protocol/jss.h | 6 +- src/test/app/AMMExtended_test.cpp | 194 ++++++++--------- src/test/app/AMM_test.cpp | 197 +++++++++--------- src/test/app/Invariants_test.cpp | 42 ++-- src/test/app/LPTokenTransfer_test.cpp | 16 +- src/test/jtx/TestHelpers.h | 10 +- src/test/jtx/impl/TestHelpers.cpp | 14 +- src/test/jtx/impl/amount.cpp | 8 +- src/xrpld/app/tx/detail/InvariantCheck.cpp | 38 +--- src/xrpld/rpc/detail/RPCHelpers.cpp | 2 +- 12 files changed, 262 insertions(+), 288 deletions(-) diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index e3efe8fec2..4cfcd72cfb 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -56,7 +56,7 @@ enum LedgerEntryType : std::uint16_t #pragma push_macro("LEDGER_ENTRY") #undef LEDGER_ENTRY -#define LEDGER_ENTRY(tag, value, name, rpcName, fields) tag = value, +#define LEDGER_ENTRY(tag, value, ...) tag = value, #include diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index a7aa86291e..58df8b5104 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -36,24 +36,7 @@ * * The `privileges` parameter of the TRANSACTION macro is a bitfield * defining which operations the transaction can perform. - * - * The values are only used in InvariantCheck.cpp - * Valid values are - noPriv - The transaction can not do any of the enumerated operations - createAcct - The transaction can create a new ACCOUNT_ROOT object. - createPseudoAcct - The transaction can create a pseudo account, - which implies createAcct - mustDeleteAcct - The transaction must delete an ACCOUNT_ROOT object - mayDeleteAcct - The transaction may delete an ACCOUNT_ROOT object, - but does not have to - overrideFreeze - The transaction can override some freeze rules - changeNFTCounts - The transaction can mint or burn an NFT - createMPTIssuance - The transaction can create a new MPT issuance - destroyMPTIssuance - The transaction can destroy an MPT issuance - mustAuthorizeMPT - The transaction MUST create or delete an MPT - object (except by issuer) - mayAuthorizeMPT - The transaction MAY create or delete an MPT - object (except by issuer) + * The values are defined and used in InvariantCheck.cpp */ /** This transaction type executes a payment. */ @@ -92,7 +75,7 @@ TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, /** This transaction type completes an existing escrow. */ TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, Delegation::delegatable, - noPriv, ({ + mayAuthorizeMPT, ({ {sfOwner, soeREQUIRED}, {sfOfferSequence, soeREQUIRED}, {sfFulfillment, soeOPTIONAL}, diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 2adf06d075..efb6a591db 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -718,11 +718,11 @@ JSS(write_load); // out: GetCounts #pragma push_macro("LEDGER_ENTRY_DUPLICATE") #undef LEDGER_ENTRY_DUPLICATE -#define LEDGER_ENTRY(tag, value, name, rpcName, fields) \ - JSS(name); \ +#define LEDGER_ENTRY(tag, value, name, rpcName, ...) \ + JSS(name); \ JSS(rpcName); -#define LEDGER_ENTRY_DUPLICATE(tag, value, name, rpcName, fields) JSS(rpcName); +#define LEDGER_ENTRY_DUPLICATE(tag, value, name, rpcName, ...) JSS(rpcName); #include diff --git a/src/test/app/AMMExtended_test.cpp b/src/test/app/AMMExtended_test.cpp index 893e9e4f75..a202ba18d1 100644 --- a/src/test/app/AMMExtended_test.cpp +++ b/src/test/app/AMMExtended_test.cpp @@ -181,9 +181,9 @@ private: BEAST_EXPECT(expectLedgerEntryRoot( env, alice, XRP(20'000) - XRP(50) - txfee(env, 1))); - BEAST_EXPECT(expectLine(env, bob, USD1(100))); - BEAST_EXPECT(expectLine(env, bob, USD2(0))); - BEAST_EXPECT(expectLine(env, carol, USD2(50))); + BEAST_EXPECT(expectHolding(env, bob, USD1(100))); + BEAST_EXPECT(expectHolding(env, bob, USD2(0))); + BEAST_EXPECT(expectHolding(env, carol, USD2(50))); } } @@ -220,7 +220,7 @@ private: BEAST_EXPECT(expectLedgerEntryRoot( env, carol, XRP(30'000) - (txfee(env, 1)))); BEAST_EXPECT(expectOffers(env, carol, 0)); - BEAST_EXPECT(expectLine(env, carol, USD(30'000))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'000))); // Order that can be filled env(offer(carol, XRP(100), USD(100)), @@ -230,7 +230,7 @@ private: XRP(10'000), USD(10'100), ammAlice.tokens())); BEAST_EXPECT(expectLedgerEntryRoot( env, carol, XRP(30'000) + XRP(100) - txfee(env, 2))); - BEAST_EXPECT(expectLine(env, carol, USD(29'900))); + BEAST_EXPECT(expectHolding(env, carol, USD(29'900))); BEAST_EXPECT(expectOffers(env, carol, 0)); }, {{XRP(10'100), USD(10'000)}}, @@ -254,7 +254,7 @@ private: BEAST_EXPECT(expectLedgerEntryRoot( env, carol, XRP(30'000) + XRP(100) - txfee(env, 1))); // AMM - BEAST_EXPECT(expectLine(env, carol, USD(29'900))); + BEAST_EXPECT(expectHolding(env, carol, USD(29'900))); BEAST_EXPECT(expectOffers(env, carol, 0)); }, {{XRP(10'100), USD(10'000)}}, @@ -327,7 +327,7 @@ private: USD(49), IOUAmount{273'861'278752583, -8})); - BEAST_EXPECT(expectLine(env, bob, STAmount{USD, 101})); + BEAST_EXPECT(expectHolding(env, bob, STAmount{USD, 101})); BEAST_EXPECT(expectLedgerEntryRoot( env, bob, XRP(300'000) - xrpTransferred - txfee(env, 1))); BEAST_EXPECT(expectOffers(env, bob, 0)); @@ -390,7 +390,7 @@ private: BEAST_EXPECT( ammBob.expectBalances(USD(300), XRP(1'000), ammBob.tokens())); - BEAST_EXPECT(expectLine(env, alice, USD(0))); + BEAST_EXPECT(expectHolding(env, alice, USD(0))); auto jrr = ledgerEntryRoot(env, alice); BEAST_EXPECT( @@ -423,7 +423,7 @@ private: BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{9'900'990'100}, USD(10'100), ammAlice.tokens())); // initial 30,000 - 10,000AMM - 100pay - BEAST_EXPECT(expectLine(env, alice, USD(19'900))); + BEAST_EXPECT(expectHolding(env, alice, USD(19'900))); // initial 30,000 - 10,0000AMM + 99.009900pay - fee*3 BEAST_EXPECT(expectLedgerEntryRoot( env, @@ -453,7 +453,7 @@ private: env(pay(alice, bob, USD(100)), sendmax(XRP(100))); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'100), USD(10'000), ammAlice.tokens())); - BEAST_EXPECT(expectLine(env, bob, USD(100))); + BEAST_EXPECT(expectHolding(env, bob, USD(100))); }, {{XRP(10'000), USD(10'100)}}, 0, @@ -533,7 +533,7 @@ private: STAmount{USD1, UINT64_C(5'030'181086519115), -12}, ammCarol.tokens())); BEAST_EXPECT(expectOffers(env, dan, 1, {{Amounts{XRP(200), EUR(20)}}})); - BEAST_EXPECT(expectLine(env, bob, STAmount{EUR1, 30})); + BEAST_EXPECT(expectHolding(env, bob, STAmount{EUR1, 30})); } void @@ -642,7 +642,7 @@ private: BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), USD(9'999), ammAlice.tokens())); BEAST_EXPECT(expectOffers(env, carol, 0)); - BEAST_EXPECT(expectLine(env, carol, USD(30'101))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'101))); BEAST_EXPECT(expectLedgerEntryRoot( env, carol, XRP(30'000) - XRP(100) - txfee(env, 1))); }, @@ -682,7 +682,7 @@ private: env(offer(alice, USD(100), XRP(200)), json(jss::Flags, tfSell)); BEAST_EXPECT( ammBob.expectBalances(XRP(1'100), USD(2'000), ammBob.tokens())); - BEAST_EXPECT(expectLine(env, alice, USD(200))); + BEAST_EXPECT(expectHolding(env, alice, USD(200))); BEAST_EXPECT(expectLedgerEntryRoot(env, alice, XRP(250))); BEAST_EXPECT(expectOffers(env, alice, 0)); } @@ -733,7 +733,7 @@ private: STAmount(XTS, UINT64_C(101'010101010101), -12), XXX(99), ammAlice.tokens())); - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, bob, STAmount{XTS, UINT64_C(98'989898989899), -12})); } else @@ -742,10 +742,10 @@ private: STAmount(XTS, UINT64_C(101'0101010101011), -13), XXX(99), ammAlice.tokens())); - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, bob, STAmount{XTS, UINT64_C(98'9898989898989), -13})); } - BEAST_EXPECT(expectLine(env, bob, XXX(101))); + BEAST_EXPECT(expectHolding(env, bob, XXX(101))); } void @@ -783,8 +783,8 @@ private: XRP(10'100), USD(10'000), ammAlice.tokens())); BEAST_EXPECT(ammBob.expectBalances( XRP(10'000), EUR(10'100), ammBob.tokens())); - BEAST_EXPECT(expectLine(env, carol, USD(15'100))); - BEAST_EXPECT(expectLine(env, carol, EUR(14'900))); + BEAST_EXPECT(expectHolding(env, carol, USD(15'100))); + BEAST_EXPECT(expectHolding(env, carol, EUR(14'900))); BEAST_EXPECT(expectOffers(env, carol, 0)); } @@ -816,8 +816,8 @@ private: BEAST_EXPECT(ammAlice.expectBalances( XRP(10'100), USD(10'000), ammAlice.tokens())); - BEAST_EXPECT(expectLine(env, carol, USD(15'100))); - BEAST_EXPECT(expectLine(env, carol, EUR(14'900))); + BEAST_EXPECT(expectHolding(env, carol, USD(15'100))); + BEAST_EXPECT(expectHolding(env, carol, EUR(14'900))); BEAST_EXPECT(expectOffers(env, carol, 0)); BEAST_EXPECT(expectOffers(env, bob, 0)); } @@ -850,8 +850,8 @@ private: BEAST_EXPECT(ammBob.expectBalances( XRP(10'000), EUR(10'100), ammBob.tokens())); - BEAST_EXPECT(expectLine(env, carol, USD(15'100))); - BEAST_EXPECT(expectLine(env, carol, EUR(14'900))); + BEAST_EXPECT(expectHolding(env, carol, USD(15'100))); + BEAST_EXPECT(expectHolding(env, carol, EUR(14'900))); BEAST_EXPECT(expectOffers(env, carol, 0)); BEAST_EXPECT(expectOffers(env, alice, 0)); } @@ -894,7 +894,7 @@ private: XRP(20'220), STAmount{USD, UINT64_C(197'8239366963403), -13}, ammBob.tokens())); - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, alice, STAmount{USD, UINT64_C(1'002'17606330366), -11})); BEAST_EXPECT(expectOffers(env, alice, 0)); } @@ -912,7 +912,7 @@ private: XRP(21'500), STAmount{USD, UINT64_C(186'046511627907), -12}, ammBob.tokens())); - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, alice, STAmount{USD, UINT64_C(1'013'953488372093), -12})); BEAST_EXPECT(expectOffers(env, alice, 0)); } @@ -953,7 +953,7 @@ private: // AMM doesn't pay the transfer fee BEAST_EXPECT(ammAlice.expectBalances( XRP(10'100), USD(10'000), ammAlice.tokens())); - BEAST_EXPECT(expectLine(env, carol, USD(30'100))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'100))); BEAST_EXPECT(expectOffers(env, carol, 0)); }, {{XRP(10'000), USD(10'100)}}, @@ -974,7 +974,7 @@ private: BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), USD(10'100), ammAlice.tokens())); // Carol pays 25% transfer fee - BEAST_EXPECT(expectLine(env, carol, USD(29'875))); + BEAST_EXPECT(expectHolding(env, carol, USD(29'875))); BEAST_EXPECT(expectOffers(env, carol, 0)); }, {{XRP(10'100), USD(10'000)}}, @@ -1011,9 +1011,9 @@ private: // AMM doesn't pay the transfer fee BEAST_EXPECT(ammAlice.expectBalances( XRP(10'100), USD(10'000), ammAlice.tokens())); - BEAST_EXPECT(expectLine(env, carol, USD(15'100))); + BEAST_EXPECT(expectHolding(env, carol, USD(15'100))); // Carol pays 25% transfer fee. - BEAST_EXPECT(expectLine(env, carol, EUR(14'875))); + BEAST_EXPECT(expectHolding(env, carol, EUR(14'875))); BEAST_EXPECT(expectOffers(env, carol, 0)); BEAST_EXPECT(expectOffers(env, bob, 0)); } @@ -1051,9 +1051,9 @@ private: // AMM doesn't pay the transfer fee BEAST_EXPECT(ammAlice.expectBalances( XRP(10'050), USD(10'000), ammAlice.tokens())); - BEAST_EXPECT(expectLine(env, carol, USD(15'050))); + BEAST_EXPECT(expectHolding(env, carol, USD(15'050))); // Carol pays 25% transfer fee. - BEAST_EXPECT(expectLine(env, carol, EUR(14'937.5))); + BEAST_EXPECT(expectHolding(env, carol, EUR(14'937.5))); BEAST_EXPECT(expectOffers(env, carol, 0)); BEAST_EXPECT( expectOffers(env, bob, 1, {{Amounts{EUR(50), XRP(50)}}})); @@ -1077,7 +1077,7 @@ private: env(pay(gw, carol, EUR(1'000)), sendmax(EUR(10'000))); env.close(); // 1000 / 0.8 - BEAST_EXPECT(expectLine(env, carol, EUR(1'250))); + BEAST_EXPECT(expectHolding(env, carol, EUR(1'250))); // The scenario: // o USD/XRP AMM is created. // o EUR/XRP Offer is created. @@ -1096,9 +1096,9 @@ private: // AMM doesn't pay the transfer fee BEAST_EXPECT(ammAlice.expectBalances( XRP(10'100), USD(10'000), ammAlice.tokens())); - BEAST_EXPECT(expectLine(env, carol, USD(100))); + BEAST_EXPECT(expectHolding(env, carol, USD(100))); // Carol pays 25% transfer fee: 1250 - 100(offer) - 25(transfer fee) - BEAST_EXPECT(expectLine(env, carol, EUR(1'125))); + BEAST_EXPECT(expectHolding(env, carol, EUR(1'125))); BEAST_EXPECT(expectOffers(env, carol, 0)); BEAST_EXPECT(expectOffers(env, bob, 0)); } @@ -1120,7 +1120,7 @@ private: env(pay(gw, alice, USD(11'000))); env(pay(gw, carol, EUR(1'000)), sendmax(EUR(10'000))); env.close(); - BEAST_EXPECT(expectLine(env, carol, EUR(1'000))); + BEAST_EXPECT(expectHolding(env, carol, EUR(1'000))); // The scenario: // o USD/XRP AMM is created. // o EUR/XRP Offer is created. @@ -1139,9 +1139,9 @@ private: // AMM pay doesn't transfer fee BEAST_EXPECT(ammAlice.expectBalances( XRP(10'100), USD(10'000), ammAlice.tokens())); - BEAST_EXPECT(expectLine(env, carol, USD(100))); + BEAST_EXPECT(expectHolding(env, carol, USD(100))); // Carol pays 25% transfer fee: 1000 - 100(offer) - 25(transfer fee) - BEAST_EXPECT(expectLine(env, carol, EUR(875))); + BEAST_EXPECT(expectHolding(env, carol, EUR(875))); BEAST_EXPECT(expectOffers(env, carol, 0)); BEAST_EXPECT(expectOffers(env, bob, 0)); } @@ -1170,7 +1170,7 @@ private: BEAST_EXPECT(ammBob.expectBalances( XRP(10'100), USD_bob(10'000), ammBob.tokens())); BEAST_EXPECT(expectOffers(env, alice, 0)); - BEAST_EXPECT(expectLine(env, alice, USD_bob(100))); + BEAST_EXPECT(expectHolding(env, alice, USD_bob(100))); } void @@ -1206,19 +1206,19 @@ private: env.close(); env(pay(dan, bob, D_BUX(100))); env.close(); - BEAST_EXPECT(expectLine(env, bob, D_BUX(100))); + BEAST_EXPECT(expectHolding(env, bob, D_BUX(100))); env(pay(ann, cam, D_BUX(60)), path(bob, dan), sendmax(A_BUX(200))); env.close(); - BEAST_EXPECT(expectLine(env, ann, A_BUX(none))); - BEAST_EXPECT(expectLine(env, ann, D_BUX(none))); - BEAST_EXPECT(expectLine(env, bob, A_BUX(72))); - BEAST_EXPECT(expectLine(env, bob, D_BUX(40))); - BEAST_EXPECT(expectLine(env, cam, A_BUX(none))); - BEAST_EXPECT(expectLine(env, cam, D_BUX(60))); - BEAST_EXPECT(expectLine(env, dan, A_BUX(none))); - BEAST_EXPECT(expectLine(env, dan, D_BUX(none))); + BEAST_EXPECT(expectHolding(env, ann, A_BUX(none))); + BEAST_EXPECT(expectHolding(env, ann, D_BUX(none))); + BEAST_EXPECT(expectHolding(env, bob, A_BUX(72))); + BEAST_EXPECT(expectHolding(env, bob, D_BUX(40))); + BEAST_EXPECT(expectHolding(env, cam, A_BUX(none))); + BEAST_EXPECT(expectHolding(env, cam, D_BUX(60))); + BEAST_EXPECT(expectHolding(env, dan, A_BUX(none))); + BEAST_EXPECT(expectHolding(env, dan, D_BUX(none))); AMM ammBob(env, bob, A_BUX(30), D_BUX(30)); @@ -1234,12 +1234,12 @@ private: BEAST_EXPECT( ammBob.expectBalances(A_BUX(30), D_BUX(30), ammBob.tokens())); - BEAST_EXPECT(expectLine(env, ann, A_BUX(none))); - BEAST_EXPECT(expectLine(env, ann, D_BUX(0))); - BEAST_EXPECT(expectLine(env, cam, A_BUX(none))); - BEAST_EXPECT(expectLine(env, cam, D_BUX(60))); - BEAST_EXPECT(expectLine(env, dan, A_BUX(0))); - BEAST_EXPECT(expectLine(env, dan, D_BUX(none))); + BEAST_EXPECT(expectHolding(env, ann, A_BUX(none))); + BEAST_EXPECT(expectHolding(env, ann, D_BUX(0))); + BEAST_EXPECT(expectHolding(env, cam, A_BUX(none))); + BEAST_EXPECT(expectHolding(env, cam, D_BUX(60))); + BEAST_EXPECT(expectHolding(env, dan, A_BUX(0))); + BEAST_EXPECT(expectHolding(env, dan, D_BUX(none))); } } @@ -1363,7 +1363,7 @@ private: env(pay(gw, bob, USD(50))); env.close(); - BEAST_EXPECT(expectLine(env, bob, USD(50))); + BEAST_EXPECT(expectHolding(env, bob, USD(50))); // Bob's offer should cross Alice's AMM env(offer(bob, XRP(50), USD(50))); @@ -1372,7 +1372,7 @@ private: BEAST_EXPECT( ammAlice.expectBalances(USD(1'050), XRP(1'000), ammAlice.tokens())); BEAST_EXPECT(expectOffers(env, bob, 0)); - BEAST_EXPECT(expectLine(env, bob, USD(0))); + BEAST_EXPECT(expectHolding(env, bob, USD(0))); } void @@ -1403,7 +1403,7 @@ private: env(pay(gw, bob, USD(50))); env.close(); - BEAST_EXPECT(expectLine(env, bob, USD(50))); + BEAST_EXPECT(expectHolding(env, bob, USD(50))); // Alice should not be able to create AMM without authorization. { @@ -1440,7 +1440,7 @@ private: BEAST_EXPECT( ammAlice.expectBalances(USD(1'050), XRP(1'000), ammAlice.tokens())); BEAST_EXPECT(expectOffers(env, bob, 0)); - BEAST_EXPECT(expectLine(env, bob, USD(0))); + BEAST_EXPECT(expectHolding(env, bob, USD(0))); } void @@ -1535,7 +1535,7 @@ private: // AMM offer is 51.282052XRP/11AUD, 11AUD/1.1 = 10AUD to bob BEAST_EXPECT( ammCarol.expectBalances(XRP(51), AUD(40), ammCarol.tokens())); - BEAST_EXPECT(expectLine(env, bob, AUD(10))); + BEAST_EXPECT(expectHolding(env, bob, AUD(10))); auto const result = find_paths(env, alice, bob, Account(bob)["USD"](25)); @@ -1950,10 +1950,10 @@ private: env(pay(alice, carol, USD(50)), path(~USD), sendmax(BTC(50))); - BEAST_EXPECT(expectLine(env, alice, BTC(50))); - BEAST_EXPECT(expectLine(env, bob, BTC(0))); - BEAST_EXPECT(expectLine(env, bob, USD(0))); - BEAST_EXPECT(expectLine(env, carol, USD(200))); + BEAST_EXPECT(expectHolding(env, alice, BTC(50))); + BEAST_EXPECT(expectHolding(env, bob, BTC(0))); + BEAST_EXPECT(expectHolding(env, bob, USD(0))); + BEAST_EXPECT(expectHolding(env, carol, USD(200))); BEAST_EXPECT( ammBob.expectBalances(BTC(150), USD(100), ammBob.tokens())); } @@ -1974,10 +1974,10 @@ private: env(pay(alice, carol, USD(50)), path(~XRP, ~USD), sendmax(BTC(50))); - BEAST_EXPECT(expectLine(env, alice, BTC(50))); - BEAST_EXPECT(expectLine(env, bob, BTC(0))); - BEAST_EXPECT(expectLine(env, bob, USD(0))); - BEAST_EXPECT(expectLine(env, carol, USD(200))); + BEAST_EXPECT(expectHolding(env, alice, BTC(50))); + BEAST_EXPECT(expectHolding(env, bob, BTC(0))); + BEAST_EXPECT(expectHolding(env, bob, USD(0))); + BEAST_EXPECT(expectHolding(env, carol, USD(200))); BEAST_EXPECT(ammBobBTC_XRP.expectBalances( BTC(150), XRP(100), ammBobBTC_XRP.tokens())); BEAST_EXPECT(ammBobXRP_USD.expectBalances( @@ -2003,8 +2003,8 @@ private: env, alice, xrpMinusFee(env, 10'000 - 50))); BEAST_EXPECT(expectLedgerEntryRoot( env, bob, XRP(10'000) - XRP(100) - ammCrtFee(env))); - BEAST_EXPECT(expectLine(env, bob, USD(0))); - BEAST_EXPECT(expectLine(env, carol, USD(200))); + BEAST_EXPECT(expectHolding(env, bob, USD(0))); + BEAST_EXPECT(expectHolding(env, carol, USD(200))); BEAST_EXPECT( ammBob.expectBalances(XRP(150), USD(100), ammBob.tokens())); } @@ -2024,10 +2024,10 @@ private: env(pay(alice, carol, XRP(50)), path(~XRP), sendmax(USD(50))); - BEAST_EXPECT(expectLine(env, alice, USD(50))); + BEAST_EXPECT(expectHolding(env, alice, USD(50))); BEAST_EXPECT(expectLedgerEntryRoot( env, bob, XRP(10'000) - XRP(150) - ammCrtFee(env))); - BEAST_EXPECT(expectLine(env, bob, USD(0))); + BEAST_EXPECT(expectHolding(env, bob, USD(0))); BEAST_EXPECT(expectLedgerEntryRoot(env, carol, XRP(10'000 + 50))); BEAST_EXPECT( ammBob.expectBalances(USD(150), XRP(100), ammBob.tokens())); @@ -2209,7 +2209,7 @@ private: sendmax(USD(0.4)), txflags(tfNoRippleDirect | tfPartialPayment)); - BEAST_EXPECT(expectLine(env, carol, EUR(1))); + BEAST_EXPECT(expectHolding(env, carol, EUR(1))); BEAST_EXPECT(ammBob.expectBalances( USD(8.4), XRPAmount{20}, ammBob.tokens())); } @@ -2244,7 +2244,7 @@ private: // alice buys 107.1428USD with 120GBP and pays 25% tr fee on 120GBP // 1,000 - 120*1.25 = 850GBP - BEAST_EXPECT(expectLine(env, alice, GBP(850))); + BEAST_EXPECT(expectHolding(env, alice, GBP(850))); if (!features[fixAMMv1_1]) { // 120GBP is swapped in for 107.1428USD @@ -2262,7 +2262,7 @@ private: } // 25% of 85.7142USD is paid in tr fee // 85.7142*1.25 = 107.1428USD - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount(USD, UINT64_C(1'085'714285714286), -12))); } @@ -2294,10 +2294,10 @@ private: // alice buys 120EUR with 120GBP via the offer // and pays 25% tr fee on 120GBP // 1,000 - 120*1.25 = 850GBP - BEAST_EXPECT(expectLine(env, alice, GBP(850))); + BEAST_EXPECT(expectHolding(env, alice, GBP(850))); // consumed offer is 120GBP/120EUR // ed doesn't pay tr fee - BEAST_EXPECT(expectLine(env, ed, EUR(880), GBP(1'120))); + BEAST_EXPECT(expectHolding(env, ed, EUR(880), GBP(1'120))); BEAST_EXPECT( expectOffers(env, ed, 1, {Amounts{GBP(880), EUR(880)}})); // 25% on 96EUR is paid in tr fee 96*1.25 = 120EUR @@ -2307,7 +2307,7 @@ private: STAmount{USD, UINT64_C(912'4087591240876), -13}, amm.tokens())); // 25% on 70.0729USD is paid in tr fee 70.0729*1.25 = 87.5912USD - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount(USD, UINT64_C(1'070'07299270073), -11))); } { @@ -2333,7 +2333,7 @@ private: txflags(tfNoRippleDirect | tfPartialPayment)); env.close(); - BEAST_EXPECT(expectLine(env, alice, GBP(850))); + BEAST_EXPECT(expectHolding(env, alice, GBP(850))); if (!features[fixAMMv1_1]) { // alice buys 107.1428EUR with 120GBP and pays 25% tr fee on @@ -2367,7 +2367,7 @@ private: amm2.tokens())); } // 25% on 63.1578USD is paid in tr fee 63.1578*1.25 = 78.9473USD - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount(USD, UINT64_C(1'063'157894736842), -12))); } { @@ -2386,7 +2386,7 @@ private: BEAST_EXPECT( amm.expectBalances(USD(1'100), EUR(1'000), amm.tokens())); // alice pays 25% tr fee on 100USD 1100-100*1.25 = 975USD - BEAST_EXPECT(expectLine(env, alice, USD(975), EUR(1'200))); + BEAST_EXPECT(expectHolding(env, alice, USD(975), EUR(1'200))); BEAST_EXPECT(expectOffers(env, alice, 0)); } @@ -2416,7 +2416,7 @@ private: // alice buys 125USD with 142.8571GBP and pays 25% tr fee // on 142.8571GBP // 1,000 - 142.8571*1.25 = 821.4285GBP - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, alice, STAmount(GBP, UINT64_C(821'4285714285712), -13))); // 142.8571GBP is swapped in for 125USD BEAST_EXPECT(amm.expectBalances( @@ -2425,7 +2425,7 @@ private: amm.tokens())); // 25% on 100USD is paid in tr fee // 100*1.25 = 125USD - BEAST_EXPECT(expectLine(env, carol, USD(1'100))); + BEAST_EXPECT(expectHolding(env, carol, USD(1'100))); } { // Payment via AMM with limit quality, deliver less @@ -2456,7 +2456,7 @@ private: // alice buys 28.125USD with 24GBP and pays 25% tr fee // on 24GBP // 1,200 - 24*1.25 = 1,170GBP - BEAST_EXPECT(expectLine(env, alice, GBP(1'170))); + BEAST_EXPECT(expectHolding(env, alice, GBP(1'170))); // 24GBP is swapped in for 28.125USD BEAST_EXPECT(amm.expectBalances( GBP(1'024), USD(1'171.875), amm.tokens())); @@ -2466,7 +2466,7 @@ private: // alice buys 28.125USD with 24GBP and pays 25% tr fee // on 24GBP // 1,200 - 24*1.25 =~ 1,170GBP - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, alice, STAmount{GBP, UINT64_C(1'169'999999999999), -12})); @@ -2478,7 +2478,7 @@ private: } // 25% on 22.5USD is paid in tr fee // 22.5*1.25 = 28.125USD - BEAST_EXPECT(expectLine(env, carol, USD(1'222.5))); + BEAST_EXPECT(expectHolding(env, carol, USD(1'222.5))); } { // Payment via offer and AMM with limit quality, deliver less @@ -2513,13 +2513,13 @@ private: // alice buys 70.4210EUR with 70.4210GBP via the offer // and pays 25% tr fee on 70.4210GBP // 1,400 - 70.4210*1.25 = 1400 - 88.0262 = 1311.9736GBP - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, alice, STAmount{GBP, UINT64_C(1'311'973684210527), -12})); // ed doesn't pay tr fee, the balances reflect consumed offer // 70.4210GBP/70.4210EUR - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, ed, STAmount{EUR, UINT64_C(1'329'578947368421), -12}, @@ -2543,13 +2543,13 @@ private: // alice buys 70.4210EUR with 70.4210GBP via the offer // and pays 25% tr fee on 70.4210GBP // 1,400 - 70.4210*1.25 = 1400 - 88.0262 = 1311.9736GBP - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, alice, STAmount{GBP, UINT64_C(1'311'973684210525), -12})); // ed doesn't pay tr fee, the balances reflect consumed offer // 70.4210GBP/70.4210EUR - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, ed, STAmount{EUR, UINT64_C(1'329'57894736842), -11}, @@ -2569,7 +2569,7 @@ private: amm.tokens())); } // 25% on 59.7321USD is paid in tr fee 59.7321*1.25 = 74.6651USD - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount(USD, UINT64_C(1'459'732142857143), -12))); } { @@ -2605,7 +2605,7 @@ private: // alice buys 53.3322EUR with 56.3368GBP via the amm // and pays 25% tr fee on 56.3368GBP // 1,400 - 56.3368*1.25 = 1400 - 70.4210 = 1329.5789GBP - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, alice, STAmount{GBP, UINT64_C(1'329'578947368421), -12})); @@ -2622,7 +2622,7 @@ private: // alice buys 53.3322EUR with 56.3368GBP via the amm // and pays 25% tr fee on 56.3368GBP // 1,400 - 56.3368*1.25 = 1400 - 70.4210 = 1329.5789GBP - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, alice, STAmount{GBP, UINT64_C(1'329'57894736842), -11})); @@ -2636,7 +2636,7 @@ private: } // 25% on 42.6658EUR is paid in tr fee 42.6658*1.25 = 53.3322EUR // 42.6658EUR/59.7321USD - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, ed, STAmount{USD, UINT64_C(1'340'267857142857), -12}, @@ -2649,7 +2649,7 @@ private: STAmount{EUR, UINT64_C(957'3341836734693), -13}, STAmount{USD, UINT64_C(1'340'267857142857), -12}}})); // 25% on 47.7857USD is paid in tr fee 47.7857*1.25 = 59.7321USD - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount(USD, UINT64_C(1'447'785714285714), -12))); } { @@ -2683,7 +2683,7 @@ private: // alice buys 53.3322EUR with 107.5308GBP // 25% on 86.0246GBP is paid in tr fee // 1,400 - 86.0246*1.25 = 1400 - 107.5308 = 1229.4691GBP - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, alice, STAmount{GBP, UINT64_C(1'292'469135802469), -12})); @@ -2704,7 +2704,7 @@ private: // alice buys 53.3322EUR with 107.5308GBP // 25% on 86.0246GBP is paid in tr fee // 1,400 - 86.0246*1.25 = 1400 - 107.5308 = 1229.4691GBP - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, alice, STAmount{GBP, UINT64_C(1'292'469135802466), -12})); @@ -2721,7 +2721,7 @@ private: amm2.tokens())); } // 25% on 66.7432USD is paid in tr fee 66.7432*1.25 = 83.4291USD - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount(USD, UINT64_C(1'466'743295019157), -12))); } { @@ -2778,7 +2778,7 @@ private: amm2.tokens())); } // 25% on 81.1111USD is paid in tr fee 81.1111*1.25 = 101.3888USD - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{USD, UINT64_C(1'481'111111111111), -12})); } } @@ -2808,7 +2808,7 @@ private: BEAST_EXPECT( ammBob.expectBalances(XRP(1'050), USD(1'000), ammBob.tokens())); - BEAST_EXPECT(expectLine(env, carol, USD(2'050))); + BEAST_EXPECT(expectHolding(env, carol, USD(2'050))); BEAST_EXPECT(expectOffers(env, bob, 1, {{{XRP(100), USD(50)}}})); } } diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index 39960ffb71..cfe1ffab16 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -97,8 +97,8 @@ private: AMM ammAlice(env, alice, USD(20'000), BTC(0.5)); BEAST_EXPECT(ammAlice.expectBalances( USD(20'000), BTC(0.5), IOUAmount{100, 0})); - BEAST_EXPECT(expectLine(env, alice, USD(0))); - BEAST_EXPECT(expectLine(env, alice, BTC(0))); + BEAST_EXPECT(expectHolding(env, alice, USD(0))); + BEAST_EXPECT(expectHolding(env, alice, BTC(0))); } // Require authorization is set, account is authorized @@ -1394,7 +1394,7 @@ private: BEAST_EXPECT(ammAlice.expectBalances( XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0})); // 30,000 less deposited 1,000 - BEAST_EXPECT(expectLine(env, carol, USD(29'000))); + BEAST_EXPECT(expectHolding(env, carol, USD(29'000))); // 30,000 less deposited 1,000 and 10 drops tx fee BEAST_EXPECT(expectLedgerEntryRoot( env, carol, XRPAmount{29'000'000'000 - baseFee})); @@ -1449,7 +1449,8 @@ private: IOUAmount{1, 7} + newLPTokens)); // 30,000 less deposited depositUSD - BEAST_EXPECT(expectLine(env, carol, USD(30'000) - depositUSD)); + BEAST_EXPECT( + expectHolding(env, carol, USD(30'000) - depositUSD)); // 30,000 less deposited depositXRP and 10 drops tx fee BEAST_EXPECT(expectLedgerEntryRoot( env, carol, XRP(30'000) - depositXRP - txfee(env, 1))); @@ -1553,15 +1554,15 @@ private: AMM ammAlice(env, alice, USD(20'000), BTC(0.5)); BEAST_EXPECT(ammAlice.expectBalances( USD(20'000), BTC(0.5), IOUAmount{100, 0})); - BEAST_EXPECT(expectLine(env, alice, USD(0))); - BEAST_EXPECT(expectLine(env, alice, BTC(0))); + BEAST_EXPECT(expectHolding(env, alice, USD(0))); + BEAST_EXPECT(expectHolding(env, alice, BTC(0))); fund(env, gw, {carol}, {USD(2'000), BTC(0.05)}, Fund::Acct); // no transfer fee on deposit ammAlice.deposit(carol, 10); BEAST_EXPECT(ammAlice.expectBalances( USD(22'000), BTC(0.55), IOUAmount{110, 0})); - BEAST_EXPECT(expectLine(env, carol, USD(0))); - BEAST_EXPECT(expectLine(env, carol, BTC(0))); + BEAST_EXPECT(expectHolding(env, carol, USD(0))); + BEAST_EXPECT(expectHolding(env, carol, BTC(0))); } // Tiny deposits @@ -2281,7 +2282,7 @@ private: BEAST_EXPECT( ammAlice.expectLPTokens(carol, IOUAmount{1'000'000, 0})); // 30,000 less deposited 1,000 - BEAST_EXPECT(expectLine(env, carol, USD(29'000))); + BEAST_EXPECT(expectHolding(env, carol, USD(29'000))); // 30,000 less deposited 1,000 and 10 drops tx fee BEAST_EXPECT(expectLedgerEntryRoot( env, carol, XRPAmount{29'000'000'000 - baseFee})); @@ -2290,7 +2291,7 @@ private: ammAlice.withdraw(carol, 1'000'000); BEAST_EXPECT( ammAlice.expectLPTokens(carol, IOUAmount(beast::Zero()))); - BEAST_EXPECT(expectLine(env, carol, USD(30'000))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'000))); BEAST_EXPECT(expectLedgerEntryRoot( env, carol, XRPAmount{30'000'000'000 - 2 * baseFee})); }); @@ -2525,22 +2526,22 @@ private: AMM ammAlice(env, alice, USD(20'000), BTC(0.5)); BEAST_EXPECT(ammAlice.expectBalances( USD(20'000), BTC(0.5), IOUAmount{100, 0})); - BEAST_EXPECT(expectLine(env, alice, USD(0))); - BEAST_EXPECT(expectLine(env, alice, BTC(0))); + BEAST_EXPECT(expectHolding(env, alice, USD(0))); + BEAST_EXPECT(expectHolding(env, alice, BTC(0))); fund(env, gw, {carol}, {USD(2'000), BTC(0.05)}, Fund::Acct); // no transfer fee on deposit ammAlice.deposit(carol, 10); BEAST_EXPECT(ammAlice.expectBalances( USD(22'000), BTC(0.55), IOUAmount{110, 0})); - BEAST_EXPECT(expectLine(env, carol, USD(0))); - BEAST_EXPECT(expectLine(env, carol, BTC(0))); + BEAST_EXPECT(expectHolding(env, carol, USD(0))); + BEAST_EXPECT(expectHolding(env, carol, BTC(0))); // no transfer fee on withdraw ammAlice.withdraw(carol, 10); BEAST_EXPECT(ammAlice.expectBalances( USD(20'000), BTC(0.5), IOUAmount{100, 0})); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0, 0})); - BEAST_EXPECT(expectLine(env, carol, USD(2'000))); - BEAST_EXPECT(expectLine(env, carol, BTC(0.05))); + BEAST_EXPECT(expectHolding(env, carol, USD(2'000))); + BEAST_EXPECT(expectHolding(env, carol, BTC(0.05))); } // Tiny withdraw @@ -3527,7 +3528,7 @@ private: // Alice doesn't have anymore lp tokens env(amm.bid({.account = alice, .bidMin = 500})); BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{500})); - BEAST_EXPECT(expectLine(env, alice, STAmount{lpIssue, 0})); + BEAST_EXPECT(expectHolding(env, alice, STAmount{lpIssue, 0})); // But trades with the discounted fee since she still owns the slot. // Alice pays 10011 drops in fees env(pay(alice, bob, USD(10)), path(~USD), sendmax(XRP(11))); @@ -3790,7 +3791,7 @@ private: BEAST_EXPECT(ammAlice.expectBalances( XRP(10'100), USD(10'000), ammAlice.tokens())); // Initial balance 30,000 + 100 - BEAST_EXPECT(expectLine(env, carol, USD(30'100))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'100))); // Initial balance 30,000 - 100(sendmax) - 10(tx fee) BEAST_EXPECT(expectLedgerEntryRoot( env, bob, XRP(30'000) - XRP(100) - txfee(env, 1))); @@ -3810,7 +3811,7 @@ private: BEAST_EXPECT(ammAlice.expectBalances( XRP(10'100), USD(10'000), ammAlice.tokens())); // Initial balance 30,000 + 100 - BEAST_EXPECT(expectLine(env, carol, USD(30'100))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'100))); // Initial balance 30,000 - 100(sendmax) - 10(tx fee) BEAST_EXPECT(expectLedgerEntryRoot( env, bob, XRP(30'000) - XRP(100) - txfee(env, 1))); @@ -3831,7 +3832,7 @@ private: BEAST_EXPECT(ammAlice.expectBalances( XRP(10'100), USD(10'000), ammAlice.tokens())); // Initial balance 30,000 + 100 - BEAST_EXPECT(expectLine(env, carol, USD(30'100))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'100))); // Initial balance 30,000 - 100(sendmax) - 10(tx fee) BEAST_EXPECT(expectLedgerEntryRoot( env, bob, XRP(30'000) - XRP(100) - txfee(env, 1))); @@ -3857,7 +3858,7 @@ private: BEAST_EXPECT(ammAlice.expectBalances( XRP(10'010), USD(10'000), ammAlice.tokens())); // Initial balance 30,000 + 10(limited by limitQuality) - BEAST_EXPECT(expectLine(env, carol, USD(30'010))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'010))); // Initial balance 30,000 - 10(limited by limitQuality) - 10(tx // fee) BEAST_EXPECT(expectLedgerEntryRoot( @@ -3897,7 +3898,7 @@ private: BEAST_EXPECT(ammAlice.expectBalances( XRP(10'010), USD(10'000), ammAlice.tokens())); // 10USD - 10% transfer fee - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{USD, UINT64_C(30'009'09090909091), -11})); @@ -3984,7 +3985,7 @@ private: BEAST_EXPECT(expectOffers(env, alice, 1, {{expectedAmounts}})); } // Initial 30,000 + 100 - BEAST_EXPECT(expectLine(env, carol, STAmount{USD, 30'100})); + BEAST_EXPECT(expectHolding(env, carol, STAmount{USD, 30'100})); // Initial 1,000 - 30082730(AMM pool) - 70798251(offer) - 10(tx fee) BEAST_EXPECT(expectLedgerEntryRoot( env, @@ -4027,7 +4028,7 @@ private: STAmount(EUR, UINT64_C(49'98750312422), -11), STAmount(USD, UINT64_C(49'98750312422), -11)}}})); // Initial 30,000 + 99.99999999999 - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{USD, UINT64_C(30'099'99999999999), -11})); @@ -4061,7 +4062,7 @@ private: BEAST_EXPECT(ammAlice.expectBalances( XRP(10'100), USD(10'000), ammAlice.tokens())); // Initial 30,000 + 200 - BEAST_EXPECT(expectLine(env, carol, USD(30'200))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'200))); } else { @@ -4069,7 +4070,7 @@ private: XRP(10'100), STAmount(USD, UINT64_C(10'000'00000000001), -11), ammAlice.tokens())); - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount(USD, UINT64_C(30'199'99999999999), -11))); @@ -4104,7 +4105,7 @@ private: env.close(); BEAST_EXPECT(ammAlice.expectBalances( XRP(1'050), USD(1'000), ammAlice.tokens())); - BEAST_EXPECT(expectLine(env, carol, USD(2'200))); + BEAST_EXPECT(expectHolding(env, carol, USD(2'200))); BEAST_EXPECT(expectOffers(env, bob, 0)); } @@ -4118,7 +4119,7 @@ private: BEAST_EXPECT(ammAlice.expectBalances( XRP(10'100), USD(10'000), ammAlice.tokens())); // Initial 1,000 + 100 - BEAST_EXPECT(expectLine(env, bob, USD(1'100))); + BEAST_EXPECT(expectHolding(env, bob, USD(1'100))); // Initial 30,000 - 100(offer) - 10(tx fee) BEAST_EXPECT(expectLedgerEntryRoot( env, bob, XRP(30'000) - XRP(100) - txfee(env, 1))); @@ -4145,9 +4146,9 @@ private: BEAST_EXPECT(ammAlice.expectBalances( GBP(1'100), EUR(1'000), ammAlice.tokens())); // Initial 30,000 - 100(offer) - 25% transfer fee - BEAST_EXPECT(expectLine(env, carol, GBP(29'875))); + BEAST_EXPECT(expectHolding(env, carol, GBP(29'875))); // Initial 30,000 + 100(offer) - BEAST_EXPECT(expectLine(env, carol, EUR(30'100))); + BEAST_EXPECT(expectHolding(env, carol, EUR(30'100))); BEAST_EXPECT(expectOffers(env, bob, 0)); }, {{GBP(1'000), EUR(1'100)}}, @@ -4285,12 +4286,12 @@ private: // = 58.825 = ~29941.17 // carol bought ~72.93EUR at the cost of ~70.68GBP // the offer is partially consumed - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{GBP, UINT64_C(29'941'16770347333), -11})); // Initial 30,000 + ~49.3(offers = 39.3(AMM) + 10(LOB)) - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{EUR, UINT64_C(30'049'31517120716), -11})); @@ -4324,20 +4325,20 @@ private: // = 88.35 = ~29911.64 // carol bought ~72.93EUR at the cost of ~70.68GBP // the offer is partially consumed - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{GBP, UINT64_C(29'911'64396400896), -11})); // Initial 30,000 + ~72.93(offers = 62.93(AMM) + 10(LOB)) - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{EUR, UINT64_C(30'072'93416277865), -11})); } // Initial 2000 + 10 = 2010 - BEAST_EXPECT(expectLine(env, bob, GBP(2'010))); + BEAST_EXPECT(expectHolding(env, bob, GBP(2'010))); // Initial 2000 - 10 * 1.25 = 1987.5 - BEAST_EXPECT(expectLine(env, ed, EUR(1'987.5))); + BEAST_EXPECT(expectHolding(env, ed, EUR(1'987.5))); }, {{GBP(1'000), EUR(1'100)}}, 0, @@ -4363,8 +4364,8 @@ private: env.close(); BEAST_EXPECT(ammAlice.expectBalances( GBP(1'100), EUR(1'000), ammAlice.tokens())); - BEAST_EXPECT(expectLine(env, bob, GBP(75))); - BEAST_EXPECT(expectLine(env, carol, EUR(30'080))); + BEAST_EXPECT(expectHolding(env, bob, GBP(75))); + BEAST_EXPECT(expectHolding(env, carol, EUR(30'080))); }, {{GBP(1'000), EUR(1'100)}}, 0, @@ -4401,12 +4402,12 @@ private: sendmax(CAN(195.3125)), txflags(tfPartialPayment)); env.close(); - BEAST_EXPECT(expectLine(env, bob, CAN(0))); - BEAST_EXPECT(expectLine(env, dan, CAN(356.25), GBP(43.75))); + BEAST_EXPECT(expectHolding(env, bob, CAN(0))); + BEAST_EXPECT(expectHolding(env, dan, CAN(356.25), GBP(43.75))); BEAST_EXPECT(ammAlice.expectBalances( GBP(10'125), EUR(10'000), ammAlice.tokens())); - BEAST_EXPECT(expectLine(env, ed, EUR(300), USD(100))); - BEAST_EXPECT(expectLine(env, carol, USD(80))); + BEAST_EXPECT(expectHolding(env, ed, EUR(300), USD(100))); + BEAST_EXPECT(expectHolding(env, carol, USD(80))); }, {{GBP(10'000), EUR(10'125)}}, 0, @@ -4523,7 +4524,7 @@ private: BEAST_EXPECT(btc_usd.expectBalances( BTC(10'100), USD(10'000), btc_usd.tokens())); - BEAST_EXPECT(expectLine(env, carol, USD(300))); + BEAST_EXPECT(expectHolding(env, carol, USD(300))); } // Dependent AMM @@ -4594,7 +4595,7 @@ private: STAmount{EUR, UINT64_C(10'917'2945958102), -10}, eth_eur.tokens())); } - BEAST_EXPECT(expectLine(env, carol, USD(300))); + BEAST_EXPECT(expectHolding(env, carol, USD(300))); } // AMM offers limit @@ -4620,7 +4621,7 @@ private: XRP(10'030), STAmount{USD, UINT64_C(9'970'089730807577), -12}, ammAlice.tokens())); - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{USD, UINT64_C(30'029'91026919241), -11})); @@ -4631,7 +4632,7 @@ private: XRP(10'030), STAmount{USD, UINT64_C(9'970'089730807827), -12}, ammAlice.tokens())); - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{USD, UINT64_C(30'029'91026919217), -11})); @@ -4663,14 +4664,14 @@ private: if (!features[fixAMMv1_1]) { // Carol gets ~100USD - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{USD, UINT64_C(30'099'99999999999), -11})); } else { - BEAST_EXPECT(expectLine(env, carol, USD(30'100))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'100))); } BEAST_EXPECT(expectOffers( env, @@ -4717,7 +4718,7 @@ private: 1, {{{XRPAmount{50'074'628}, STAmount{USD, UINT64_C(50'07512950697), -11}}}})); - BEAST_EXPECT(expectLine(env, carol, USD(30'100))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'100))); } } @@ -4809,11 +4810,11 @@ private: env(offer(carol, STAmount{token2, 100}, STAmount{token1, 100})); env.close(); BEAST_EXPECT( - expectLine(env, alice, STAmount{token1, 10'000'100}) && - expectLine(env, alice, STAmount{token2, 9'999'900})); + expectHolding(env, alice, STAmount{token1, 10'000'100}) && + expectHolding(env, alice, STAmount{token2, 9'999'900})); BEAST_EXPECT( - expectLine(env, carol, STAmount{token2, 1'000'100}) && - expectLine(env, carol, STAmount{token1, 999'900})); + expectHolding(env, carol, STAmount{token2, 1'000'100}) && + expectHolding(env, carol, STAmount{token1, 999'900})); BEAST_EXPECT( expectOffers(env, alice, 0) && expectOffers(env, carol, 0)); }); @@ -5034,7 +5035,7 @@ private: BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{1'000})); ammAlice.withdrawAll(carol, USD(3'000)); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0})); - BEAST_EXPECT(expectLine(env, carol, USD(30'000))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'000))); // Set fee to 1% ammAlice.vote(alice, 1'000); BEAST_EXPECT(ammAlice.expectTradingFee(1'000)); @@ -5043,12 +5044,12 @@ private: ammAlice.deposit(carol, USD(3'000)); BEAST_EXPECT(ammAlice.expectLPTokens( carol, IOUAmount{994'981155689671, -12})); - BEAST_EXPECT(expectLine(env, carol, USD(27'000))); + BEAST_EXPECT(expectHolding(env, carol, USD(27'000))); // Set fee to 0 ammAlice.vote(alice, 0); ammAlice.withdrawAll(carol, USD(0)); // Carol gets back less than the original deposit - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{USD, UINT64_C(29'994'96220068281), -11})); @@ -5109,13 +5110,13 @@ private: ammAlice.deposit(carol, USD(3'000)); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{1'000})); - BEAST_EXPECT(expectLine(env, carol, USD(27'000))); + BEAST_EXPECT(expectHolding(env, carol, USD(27'000))); // Set fee to 1% ammAlice.vote(alice, 1'000); BEAST_EXPECT(ammAlice.expectTradingFee(1'000)); // Single withdrawal. Carol gets ~5USD less than deposited. ammAlice.withdrawAll(carol, USD(0)); - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{USD, UINT64_C(29'994'97487437186), -11})); @@ -5189,9 +5190,9 @@ private: {USD(1'000), EUR(1'000)}, Fund::Acct); // Alice contributed 1010EUR and 1000USD to the pool - BEAST_EXPECT(expectLine(env, alice, EUR(28'990))); - BEAST_EXPECT(expectLine(env, alice, USD(29'000))); - BEAST_EXPECT(expectLine(env, carol, USD(30'000))); + BEAST_EXPECT(expectHolding(env, alice, EUR(28'990))); + BEAST_EXPECT(expectHolding(env, alice, USD(29'000))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'000))); // Carol pays to Alice with no fee env(pay(carol, alice, EUR(10)), path(~EUR), @@ -5199,9 +5200,9 @@ private: txflags(tfNoRippleDirect)); env.close(); // Alice has 10EUR more and Carol has 10USD less - BEAST_EXPECT(expectLine(env, alice, EUR(29'000))); - BEAST_EXPECT(expectLine(env, alice, USD(29'000))); - BEAST_EXPECT(expectLine(env, carol, USD(29'990))); + BEAST_EXPECT(expectHolding(env, alice, EUR(29'000))); + BEAST_EXPECT(expectHolding(env, alice, USD(29'000))); + BEAST_EXPECT(expectHolding(env, carol, USD(29'990))); // Set fee to 1% ammAlice.vote(alice, 1'000); @@ -5213,10 +5214,10 @@ private: txflags(tfNoRippleDirect)); env.close(); // Bob sends 10.1~EUR to pay 10USD - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, bob, STAmount{EUR, UINT64_C(989'8989898989899), -13})); // Carol got 10USD - BEAST_EXPECT(expectLine(env, carol, USD(30'000))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'000))); BEAST_EXPECT(ammAlice.expectBalances( USD(1'000), STAmount{EUR, UINT64_C(1'010'10101010101), -11}, @@ -5233,8 +5234,8 @@ private: // No fee env(offer(carol, EUR(10), USD(10))); env.close(); - BEAST_EXPECT(expectLine(env, carol, USD(29'990))); - BEAST_EXPECT(expectLine(env, carol, EUR(30'010))); + BEAST_EXPECT(expectHolding(env, carol, USD(29'990))); + BEAST_EXPECT(expectHolding(env, carol, EUR(30'010))); // Change pool composition back env(offer(carol, USD(10), EUR(10))); env.close(); @@ -5245,11 +5246,11 @@ private: env.close(); // Alice gets fewer ~4.97EUR for ~5.02USD, the difference goes // to the pool - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{USD, UINT64_C(29'995'02512562814), -11})); - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{EUR, UINT64_C(30'004'97487437186), -11})); @@ -5299,16 +5300,16 @@ private: path(~USD), sendmax(EUR(15)), txflags(tfNoRippleDirect)); - BEAST_EXPECT(expectLine(env, ed, USD(2'010))); + BEAST_EXPECT(expectHolding(env, ed, USD(2'010))); if (!features[fixAMMv1_1]) { - BEAST_EXPECT(expectLine(env, bob, EUR(1'990))); + BEAST_EXPECT(expectHolding(env, bob, EUR(1'990))); BEAST_EXPECT(ammAlice.expectBalances( USD(1'000), EUR(1'005), ammAlice.tokens())); } else { - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, bob, STAmount(EUR, UINT64_C(1989'999999999999), -12))); BEAST_EXPECT(ammAlice.expectBalances( USD(1'000), @@ -5336,10 +5337,10 @@ private: path(~USD), sendmax(EUR(15)), txflags(tfNoRippleDirect)); - BEAST_EXPECT(expectLine(env, ed, USD(2'010))); + BEAST_EXPECT(expectHolding(env, ed, USD(2'010))); if (!features[fixAMMv1_1]) { - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, bob, STAmount{EUR, UINT64_C(1'989'987453007618), -12})); @@ -5350,7 +5351,7 @@ private: } else { - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, bob, STAmount{EUR, UINT64_C(1'989'987453007628), -12})); @@ -5381,8 +5382,8 @@ private: path(~USD), sendmax(EUR(15)), txflags(tfNoRippleDirect)); - BEAST_EXPECT(expectLine(env, ed, USD(2'010))); - BEAST_EXPECT(expectLine(env, bob, EUR(1'990))); + BEAST_EXPECT(expectHolding(env, ed, USD(2'010))); + BEAST_EXPECT(expectHolding(env, bob, EUR(1'990))); BEAST_EXPECT(ammAlice.expectBalances( USD(1'005), EUR(1'000), ammAlice.tokens())); BEAST_EXPECT(expectOffers(env, carol, 0)); @@ -5408,8 +5409,8 @@ private: path(~USD), sendmax(EUR(15)), txflags(tfNoRippleDirect)); - BEAST_EXPECT(expectLine(env, ed, USD(2'010))); - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding(env, ed, USD(2'010))); + BEAST_EXPECT(expectHolding( env, bob, STAmount{EUR, UINT64_C(1'989'993923296712), -12})); BEAST_EXPECT(ammAlice.expectBalances( USD(1'004), @@ -5480,47 +5481,47 @@ private: else BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), USD(10'000), IOUAmount{10'000'000})); - BEAST_EXPECT(expectLine(env, ben, USD(1'500'000))); - BEAST_EXPECT(expectLine(env, simon, USD(1'500'000))); - BEAST_EXPECT(expectLine(env, chris, USD(1'500'000))); - BEAST_EXPECT(expectLine(env, dan, USD(1'500'000))); + BEAST_EXPECT(expectHolding(env, ben, USD(1'500'000))); + BEAST_EXPECT(expectHolding(env, simon, USD(1'500'000))); + BEAST_EXPECT(expectHolding(env, chris, USD(1'500'000))); + BEAST_EXPECT(expectHolding(env, dan, USD(1'500'000))); if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, carol, STAmount{USD, UINT64_C(30'000'00000000001), -11})); else if (features[fixAMMv1_1] && !features[fixAMMv1_3]) - BEAST_EXPECT(expectLine(env, carol, USD(30'000))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'000))); else - BEAST_EXPECT(expectLine(env, carol, USD(30'000))); - BEAST_EXPECT(expectLine(env, ed, USD(1'500'000))); - BEAST_EXPECT(expectLine(env, paul, USD(1'500'000))); + BEAST_EXPECT(expectHolding(env, carol, USD(30'000))); + BEAST_EXPECT(expectHolding(env, ed, USD(1'500'000))); + BEAST_EXPECT(expectHolding(env, paul, USD(1'500'000))); if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, nataly, STAmount{USD, UINT64_C(1'500'000'000000002), -9})); else if (features[fixAMMv1_1] && !features[fixAMMv1_3]) - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, nataly, STAmount{USD, UINT64_C(1'500'000'000000005), -9})); else - BEAST_EXPECT(expectLine(env, nataly, USD(1'500'000))); + BEAST_EXPECT(expectHolding(env, nataly, USD(1'500'000))); ammAlice.withdrawAll(alice); BEAST_EXPECT(!ammAlice.ammExists()); if (!features[fixAMMv1_1]) - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, alice, STAmount{USD, UINT64_C(30'000'0000000013), -10})); else if (features[fixAMMv1_3]) - BEAST_EXPECT(expectLine( + BEAST_EXPECT(expectHolding( env, alice, STAmount{USD, UINT64_C(30'000'0000000003), -10})); else - BEAST_EXPECT(expectLine(env, alice, USD(30'000))); + BEAST_EXPECT(expectHolding(env, alice, USD(30'000))); // alice XRP balance is 30,000initial - 50 ammcreate fee - // 10drops fee BEAST_EXPECT( @@ -5883,7 +5884,7 @@ private: BEAST_EXPECT(amm->expectBalances( USD(1'000), ETH(1'000), amm->tokens())); } - BEAST_EXPECT(expectLine(env, bob, USD(2'100))); + BEAST_EXPECT(expectHolding(env, bob, USD(2'100))); q[i] = Quality(Amounts{ ETH(2'000) - env.balance(carol, ETH), env.balance(bob, USD) - USD(2'000)}); @@ -6056,7 +6057,7 @@ private: -13}}}})); } } - BEAST_EXPECT(expectLine(env, bob, USD(2'100))); + BEAST_EXPECT(expectHolding(env, bob, USD(2'100))); q[i] = Quality(Amounts{ ETH(2'000) - env.balance(carol, ETH), env.balance(bob, USD) - USD(2'000)}); @@ -6203,7 +6204,7 @@ private: sendmax(ETH(600))); env.close(); - BEAST_EXPECT(expectLine(env, bob, USD(2'100))); + BEAST_EXPECT(expectHolding(env, bob, USD(2'100))); if (i == 2 && !features[fixAMMv1_1]) { diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index 5e8d8a7b50..c91149b2f7 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -1482,23 +1482,6 @@ class Invariants_test : public beast::unit_test::suite sle->at(~sfVaultID) = std::nullopt; }, }, - { - "pseudo-account has 2 pseudo-account fields set", - [this](SLE::pointer& sle) { - BEAST_EXPECT(sle->at(~sfVaultID) && !sle->at(~sfAMMID)); - sle->at(~sfAMMID) = ~sle->at(~sfVaultID); - }, - }, - /* - { - "pseudo-account has 2 pseudo-account fields set", - [this](SLE::pointer& sle) { - BEAST_EXPECT( - sle->at(~sfVaultID) && !sle->at(~sfLoanBrokerID)); - sle->at(~sfLoanBrokerID) = ~sle->at(~sfVaultID); - }, - }, - */ { "pseudo-account sequence changed", [](SLE::pointer& sle) { sle->at(sfSequence) = 12345; }, @@ -1532,6 +1515,31 @@ class Invariants_test : public beast::unit_test::suite {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, createPseudo); } + for (auto const pField : getPseudoAccountFields()) + { + // createPseudo creates a vault, so sfVaultID will be set, and + // setting it again will not cause an error + if (pField == &sfVaultID) + continue; + doInvariantCheck( + {{"pseudo-account has 2 pseudo-account fields set"}}, + [&](Account const& A1, Account const&, ApplyContext& ac) { + auto sle = ac.view().peek(keylet::account(pseudoAccountID)); + if (!sle) + return false; + + auto const vaultID = ~sle->at(~sfVaultID); + BEAST_EXPECT(vaultID && !sle->isFieldPresent(*pField)); + sle->setFieldH256(*pField, *vaultID); + + ac.view().update(sle); + return true; + }, + XRPAmount{}, + STTx{ttACCOUNT_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + createPseudo); + } // Take one of the regular accounts and set the sequence to 0, which // will make it look like a pseudo-account diff --git a/src/test/app/LPTokenTransfer_test.cpp b/src/test/app/LPTokenTransfer_test.cpp index e95e974547..eccd864e71 100644 --- a/src/test/app/LPTokenTransfer_test.cpp +++ b/src/test/app/LPTokenTransfer_test.cpp @@ -287,11 +287,11 @@ class LPTokenTransfer_test : public jtx::AMMTest // with fixFrozenLPTokenTransfer enabled, alice's offer can no // longer cross with carol's offer BEAST_EXPECT( - expectLine(env, alice, STAmount{token1, 10'000'000}) && - expectLine(env, alice, STAmount{token2, 10'000'000})); + expectHolding(env, alice, STAmount{token1, 10'000'000}) && + expectHolding(env, alice, STAmount{token2, 10'000'000})); BEAST_EXPECT( - expectLine(env, carol, STAmount{token2, 10'000'000}) && - expectLine(env, carol, STAmount{token1, 10'000'000})); + expectHolding(env, carol, STAmount{token2, 10'000'000}) && + expectHolding(env, carol, STAmount{token1, 10'000'000})); BEAST_EXPECT( expectOffers(env, alice, 1) && expectOffers(env, carol, 0)); } @@ -300,11 +300,11 @@ class LPTokenTransfer_test : public jtx::AMMTest // alice's offer still crosses with carol's offer despite carol's // token1 is frozen BEAST_EXPECT( - expectLine(env, alice, STAmount{token1, 10'000'100}) && - expectLine(env, alice, STAmount{token2, 9'999'900})); + expectHolding(env, alice, STAmount{token1, 10'000'100}) && + expectHolding(env, alice, STAmount{token2, 9'999'900})); BEAST_EXPECT( - expectLine(env, carol, STAmount{token2, 10'000'100}) && - expectLine(env, carol, STAmount{token1, 9'999'900})); + expectHolding(env, carol, STAmount{token2, 10'000'100}) && + expectHolding(env, carol, STAmount{token1, 9'999'900})); BEAST_EXPECT( expectOffers(env, alice, 0) && expectOffers(env, carol, 0)); } diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index aa93f3236f..3aea274e53 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -453,7 +453,7 @@ PrettyAmount xrpMinusFee(Env const& env, std::int64_t xrpAmount); bool -expectLine( +expectHolding( Env& env, AccountID const& account, STAmount const& value, @@ -461,18 +461,18 @@ expectLine( template bool -expectLine( +expectHolding( Env& env, AccountID const& account, STAmount const& value, Amts const&... amts) { - return expectLine(env, account, value, false) && - expectLine(env, account, amts...); + return expectHolding(env, account, value, false) && + expectHolding(env, account, amts...); } bool -expectLine(Env& env, AccountID const& account, None const& value); +expectHolding(Env& env, AccountID const& account, None const& value); bool expectOffers( diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index 192c48c26b..20f24f0d84 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -103,7 +103,7 @@ xrpMinusFee(Env const& env, std::int64_t xrpAmount) }; [[nodiscard]] bool -expectLine( +expectHolding( Env& env, AccountID const& account, STAmount const& value, @@ -137,13 +137,17 @@ expectLine( } [[nodiscard]] bool -expectLine(Env& env, AccountID const& account, None const&, Issue const& issue) +expectHolding( + Env& env, + AccountID const& account, + None const&, + Issue const& issue) { return !env.le(keylet::line(account, issue)); } [[nodiscard]] bool -expectLine( +expectHolding( Env& env, AccountID const& account, None const&, @@ -153,11 +157,11 @@ expectLine( } [[nodiscard]] bool -expectLine(Env& env, AccountID const& account, None const& value) +expectHolding(Env& env, AccountID const& account, None const& value) { return std::visit( [&](auto const& issue) { - return expectLine(env, account, value, issue); + return expectHolding(env, account, value, issue); }, value.asset.value()); } diff --git a/src/test/jtx/impl/amount.cpp b/src/test/jtx/impl/amount.cpp index a1dbd25652..a04a6c87d2 100644 --- a/src/test/jtx/impl/amount.cpp +++ b/src/test/jtx/impl/amount.cpp @@ -91,12 +91,18 @@ operator<<(std::ostream& os, PrettyAmount const& amount) os << to_places(d, 6) << " XRP"; } - else + else if (amount.value().holds()) { os << amount.value().getText() << "/" << to_string(amount.value().issue().currency) << "(" << amount.name() << ")"; } + else + { + auto const& mptIssue = amount.value().asset().get(); + os << amount.value().getText() << "/" << to_string(mptIssue) << "(" + << amount.name() << ")"; + } return os; } diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 9fbec47f7c..10c008ea1f 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -485,7 +485,8 @@ AccountRootsDeletedClean::finalize( // feature is enabled. Enabled, or not, though, a fatal-level message will // be logged [[maybe_unused]] bool const enforce = - view.rules().enabled(featureInvariantsV1_1); + view.rules().enabled(featureInvariantsV1_1) || + view.rules().enabled(featureSingleAssetVault); auto const objectExists = [&view, enforce, &j](auto const& keylet) { (void)enforce; @@ -538,7 +539,8 @@ AccountRootsDeletedClean::finalize( return false; } - // Keys directly stored in the AccountRoot object + // If the account is a pseudo account, then the linked object must + // also be deleted. e.g. AMM, Vault, etc. for (auto const& field : getPseudoAccountFields()) { if (accountSLE->isFieldPresent(*field)) @@ -569,7 +571,7 @@ LedgerEntryTypesMatch::visitEntry( #pragma push_macro("LEDGER_ENTRY") #undef LEDGER_ENTRY -#define LEDGER_ENTRY(tag, value, name, rpcName, fields) case tag: +#define LEDGER_ENTRY(tag, ...) case tag: switch (after->getType()) { @@ -1507,36 +1509,6 @@ ValidMPTIssuance::finalize( return true; } - - if (tx.getTxnType() == ttMPTOKEN_ISSUANCE_SET) - { - if (mptIssuancesDeleted_ > 0) - { - JLOG(j.fatal()) << "Invariant failed: MPT issuance set " - "succeeded while removing MPT issuances"; - } - else if (mptIssuancesCreated_ > 0) - { - JLOG(j.fatal()) << "Invariant failed: MPT issuance set " - "succeeded while creating MPT issuances"; - } - else if (mptokensDeleted_ > 0) - { - JLOG(j.fatal()) << "Invariant failed: MPT issuance set " - "succeeded while removing MPTokens"; - } - else if (mptokensCreated_ > 0) - { - JLOG(j.fatal()) << "Invariant failed: MPT issuance set " - "succeeded while creating MPTokens"; - } - - return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 && - mptokensCreated_ == 0 && mptokensDeleted_ == 0; - } - - if (tx.getTxnType() == ttESCROW_FINISH) - return true; } if (mptIssuancesCreated_ != 0) diff --git a/src/xrpld/rpc/detail/RPCHelpers.cpp b/src/xrpld/rpc/detail/RPCHelpers.cpp index b98f31340a..85f5820dae 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCHelpers.cpp @@ -938,7 +938,7 @@ chooseLedgerEntryType(Json::Value const& params) #pragma push_macro("LEDGER_ENTRY") #undef LEDGER_ENTRY -#define LEDGER_ENTRY(tag, value, name, rpcName, fields) \ +#define LEDGER_ENTRY(tag, value, name, rpcName, ...) \ {jss::name, jss::rpcName, tag}, #include From a88aceb846b6588889b5810622e81d64da924a70 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 22 Aug 2025 16:02:59 -0400 Subject: [PATCH 055/291] Reformat transactions.macro --- .../xrpl/protocol/detail/transactions.macro | 195 ++++++++++++------ 1 file changed, 130 insertions(+), 65 deletions(-) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 58df8b5104..5b1a6ac774 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -45,7 +45,8 @@ #endif TRANSACTION(ttPAYMENT, 0, Payment, Delegation::delegatable, - createAcct, ({ + createAcct, + ({ {sfDestination, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, {sfSendMax, soeOPTIONAL, soeMPTSupported}, @@ -63,7 +64,8 @@ TRANSACTION(ttPAYMENT, 0, Payment, #endif TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfDestination, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, {sfCondition, soeOPTIONAL}, @@ -75,7 +77,8 @@ TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, /** This transaction type completes an existing escrow. */ TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, Delegation::delegatable, - mayAuthorizeMPT, ({ + mayAuthorizeMPT, + ({ {sfOwner, soeREQUIRED}, {sfOfferSequence, soeREQUIRED}, {sfFulfillment, soeOPTIONAL}, @@ -90,7 +93,8 @@ TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, #endif TRANSACTION(ttACCOUNT_SET, 3, AccountSet, Delegation::notDelegatable, - noPriv, ({ + noPriv, + ({ {sfEmailHash, soeOPTIONAL}, {sfWalletLocator, soeOPTIONAL}, {sfWalletSize, soeOPTIONAL}, @@ -109,7 +113,8 @@ TRANSACTION(ttACCOUNT_SET, 3, AccountSet, #endif TRANSACTION(ttESCROW_CANCEL, 4, EscrowCancel, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfOwner, soeREQUIRED}, {sfOfferSequence, soeREQUIRED}, })) @@ -120,7 +125,8 @@ TRANSACTION(ttESCROW_CANCEL, 4, EscrowCancel, #endif TRANSACTION(ttREGULAR_KEY_SET, 5, SetRegularKey, Delegation::notDelegatable, - noPriv, ({ + noPriv, + ({ {sfRegularKey, soeOPTIONAL}, })) @@ -132,7 +138,8 @@ TRANSACTION(ttREGULAR_KEY_SET, 5, SetRegularKey, #endif TRANSACTION(ttOFFER_CREATE, 7, OfferCreate, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfTakerPays, soeREQUIRED}, {sfTakerGets, soeREQUIRED}, {sfExpiration, soeOPTIONAL}, @@ -146,7 +153,8 @@ TRANSACTION(ttOFFER_CREATE, 7, OfferCreate, #endif TRANSACTION(ttOFFER_CANCEL, 8, OfferCancel, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfOfferSequence, soeREQUIRED}, })) @@ -158,7 +166,8 @@ TRANSACTION(ttOFFER_CANCEL, 8, OfferCancel, #endif TRANSACTION(ttTICKET_CREATE, 10, TicketCreate, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfTicketCount, soeREQUIRED}, })) @@ -172,7 +181,8 @@ TRANSACTION(ttTICKET_CREATE, 10, TicketCreate, #endif TRANSACTION(ttSIGNER_LIST_SET, 12, SignerListSet, Delegation::notDelegatable, - noPriv, ({ + noPriv, + ({ {sfSignerQuorum, soeREQUIRED}, {sfSignerEntries, soeOPTIONAL}, })) @@ -183,7 +193,8 @@ TRANSACTION(ttSIGNER_LIST_SET, 12, SignerListSet, #endif TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfDestination, soeREQUIRED}, {sfAmount, soeREQUIRED}, {sfSettleDelay, soeREQUIRED}, @@ -195,7 +206,8 @@ TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, /** This transaction type funds an existing unidirectional XRP payment channel. */ TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfChannel, soeREQUIRED}, {sfAmount, soeREQUIRED}, {sfExpiration, soeOPTIONAL}, @@ -204,7 +216,8 @@ TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund, /** This transaction type submits a claim against an existing unidirectional payment channel. */ TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfChannel, soeREQUIRED}, {sfAmount, soeOPTIONAL}, {sfBalance, soeOPTIONAL}, @@ -219,7 +232,8 @@ TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, #endif TRANSACTION(ttCHECK_CREATE, 16, CheckCreate, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfDestination, soeREQUIRED}, {sfSendMax, soeREQUIRED}, {sfExpiration, soeOPTIONAL}, @@ -233,7 +247,8 @@ TRANSACTION(ttCHECK_CREATE, 16, CheckCreate, #endif TRANSACTION(ttCHECK_CASH, 17, CheckCash, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfCheckID, soeREQUIRED}, {sfAmount, soeOPTIONAL}, {sfDeliverMin, soeOPTIONAL}, @@ -245,7 +260,8 @@ TRANSACTION(ttCHECK_CASH, 17, CheckCash, #endif TRANSACTION(ttCHECK_CANCEL, 18, CheckCancel, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfCheckID, soeREQUIRED}, })) @@ -255,7 +271,8 @@ TRANSACTION(ttCHECK_CANCEL, 18, CheckCancel, #endif TRANSACTION(ttDEPOSIT_PREAUTH, 19, DepositPreauth, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfAuthorize, soeOPTIONAL}, {sfUnauthorize, soeOPTIONAL}, {sfAuthorizeCredentials, soeOPTIONAL}, @@ -268,7 +285,8 @@ TRANSACTION(ttDEPOSIT_PREAUTH, 19, DepositPreauth, #endif TRANSACTION(ttTRUST_SET, 20, TrustSet, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfLimitAmount, soeOPTIONAL}, {sfQualityIn, soeOPTIONAL}, {sfQualityOut, soeOPTIONAL}, @@ -280,7 +298,8 @@ TRANSACTION(ttTRUST_SET, 20, TrustSet, #endif TRANSACTION(ttACCOUNT_DELETE, 21, AccountDelete, Delegation::notDelegatable, - mustDeleteAcct, ({ + mustDeleteAcct, + ({ {sfDestination, soeREQUIRED}, {sfDestinationTag, soeOPTIONAL}, {sfCredentialIDs, soeOPTIONAL}, @@ -294,7 +313,8 @@ TRANSACTION(ttACCOUNT_DELETE, 21, AccountDelete, #endif TRANSACTION(ttNFTOKEN_MINT, 25, NFTokenMint, Delegation::delegatable, - changeNFTCounts, ({ + changeNFTCounts, + ({ {sfNFTokenTaxon, soeREQUIRED}, {sfTransferFee, soeOPTIONAL}, {sfIssuer, soeOPTIONAL}, @@ -310,7 +330,8 @@ TRANSACTION(ttNFTOKEN_MINT, 25, NFTokenMint, #endif TRANSACTION(ttNFTOKEN_BURN, 26, NFTokenBurn, Delegation::delegatable, - changeNFTCounts, ({ + changeNFTCounts, + ({ {sfNFTokenID, soeREQUIRED}, {sfOwner, soeOPTIONAL}, })) @@ -321,7 +342,8 @@ TRANSACTION(ttNFTOKEN_BURN, 26, NFTokenBurn, #endif TRANSACTION(ttNFTOKEN_CREATE_OFFER, 27, NFTokenCreateOffer, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfNFTokenID, soeREQUIRED}, {sfAmount, soeREQUIRED}, {sfDestination, soeOPTIONAL}, @@ -335,7 +357,8 @@ TRANSACTION(ttNFTOKEN_CREATE_OFFER, 27, NFTokenCreateOffer, #endif TRANSACTION(ttNFTOKEN_CANCEL_OFFER, 28, NFTokenCancelOffer, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfNFTokenOffers, soeREQUIRED}, })) @@ -345,7 +368,8 @@ TRANSACTION(ttNFTOKEN_CANCEL_OFFER, 28, NFTokenCancelOffer, #endif TRANSACTION(ttNFTOKEN_ACCEPT_OFFER, 29, NFTokenAcceptOffer, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfNFTokenBuyOffer, soeOPTIONAL}, {sfNFTokenSellOffer, soeOPTIONAL}, {sfNFTokenBrokerFee, soeOPTIONAL}, @@ -357,7 +381,8 @@ TRANSACTION(ttNFTOKEN_ACCEPT_OFFER, 29, NFTokenAcceptOffer, #endif TRANSACTION(ttCLAWBACK, 30, Clawback, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfAmount, soeREQUIRED, soeMPTSupported}, {sfHolder, soeOPTIONAL}, })) @@ -368,7 +393,8 @@ TRANSACTION(ttCLAWBACK, 30, Clawback, #endif TRANSACTION(ttAMM_CLAWBACK, 31, AMMClawback, Delegation::delegatable, - mayDeleteAcct | overrideFreeze, ({ + mayDeleteAcct | overrideFreeze, + ({ {sfHolder, soeREQUIRED}, {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, @@ -381,7 +407,8 @@ TRANSACTION(ttAMM_CLAWBACK, 31, AMMClawback, #endif TRANSACTION(ttAMM_CREATE, 35, AMMCreate, Delegation::delegatable, - createPseudoAcct, ({ + createPseudoAcct, + ({ {sfAmount, soeREQUIRED}, {sfAmount2, soeREQUIRED}, {sfTradingFee, soeREQUIRED}, @@ -393,7 +420,8 @@ TRANSACTION(ttAMM_CREATE, 35, AMMCreate, #endif TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, {sfAmount, soeOPTIONAL}, @@ -409,7 +437,8 @@ TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit, #endif TRANSACTION(ttAMM_WITHDRAW, 37, AMMWithdraw, Delegation::delegatable, - mayDeleteAcct, ({ + mayDeleteAcct, + ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, {sfAmount, soeOPTIONAL}, @@ -424,7 +453,8 @@ TRANSACTION(ttAMM_WITHDRAW, 37, AMMWithdraw, #endif TRANSACTION(ttAMM_VOTE, 38, AMMVote, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, {sfTradingFee, soeREQUIRED}, @@ -436,7 +466,8 @@ TRANSACTION(ttAMM_VOTE, 38, AMMVote, #endif TRANSACTION(ttAMM_BID, 39, AMMBid, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, {sfBidMin, soeOPTIONAL}, @@ -450,7 +481,8 @@ TRANSACTION(ttAMM_BID, 39, AMMBid, #endif TRANSACTION(ttAMM_DELETE, 40, AMMDelete, Delegation::delegatable, - mustDeleteAcct, ({ + mustDeleteAcct, + ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, })) @@ -461,7 +493,8 @@ TRANSACTION(ttAMM_DELETE, 40, AMMDelete, #endif TRANSACTION(ttXCHAIN_CREATE_CLAIM_ID, 41, XChainCreateClaimID, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfXChainBridge, soeREQUIRED}, {sfSignatureReward, soeREQUIRED}, {sfOtherChainSource, soeREQUIRED}, @@ -470,7 +503,8 @@ TRANSACTION(ttXCHAIN_CREATE_CLAIM_ID, 41, XChainCreateClaimID, /** This transactions initiates a crosschain transaction */ TRANSACTION(ttXCHAIN_COMMIT, 42, XChainCommit, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfXChainBridge, soeREQUIRED}, {sfXChainClaimID, soeREQUIRED}, {sfAmount, soeREQUIRED}, @@ -480,7 +514,8 @@ TRANSACTION(ttXCHAIN_COMMIT, 42, XChainCommit, /** This transaction completes a crosschain transaction */ TRANSACTION(ttXCHAIN_CLAIM, 43, XChainClaim, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfXChainBridge, soeREQUIRED}, {sfXChainClaimID, soeREQUIRED}, {sfDestination, soeREQUIRED}, @@ -491,7 +526,8 @@ TRANSACTION(ttXCHAIN_CLAIM, 43, XChainClaim, /** This transaction initiates a crosschain account create transaction */ TRANSACTION(ttXCHAIN_ACCOUNT_CREATE_COMMIT, 44, XChainAccountCreateCommit, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfXChainBridge, soeREQUIRED}, {sfDestination, soeREQUIRED}, {sfAmount, soeREQUIRED}, @@ -501,7 +537,8 @@ TRANSACTION(ttXCHAIN_ACCOUNT_CREATE_COMMIT, 44, XChainAccountCreateCommit, /** This transaction adds an attestation to a claim */ TRANSACTION(ttXCHAIN_ADD_CLAIM_ATTESTATION, 45, XChainAddClaimAttestation, Delegation::delegatable, - createAcct, ({ + createAcct, + ({ {sfXChainBridge, soeREQUIRED}, {sfAttestationSignerAccount, soeREQUIRED}, @@ -520,7 +557,8 @@ TRANSACTION(ttXCHAIN_ADD_CLAIM_ATTESTATION, 45, XChainAddClaimAttestation, TRANSACTION(ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION, 46, XChainAddAccountCreateAttestation, Delegation::delegatable, - createAcct, ({ + createAcct, + ({ {sfXChainBridge, soeREQUIRED}, {sfAttestationSignerAccount, soeREQUIRED}, @@ -539,7 +577,8 @@ TRANSACTION(ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION, 46, /** This transaction modifies a sidechain */ TRANSACTION(ttXCHAIN_MODIFY_BRIDGE, 47, XChainModifyBridge, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfXChainBridge, soeREQUIRED}, {sfSignatureReward, soeOPTIONAL}, {sfMinAccountCreateAmount, soeOPTIONAL}, @@ -548,7 +587,8 @@ TRANSACTION(ttXCHAIN_MODIFY_BRIDGE, 47, XChainModifyBridge, /** This transactions creates a sidechain */ TRANSACTION(ttXCHAIN_CREATE_BRIDGE, 48, XChainCreateBridge, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfXChainBridge, soeREQUIRED}, {sfSignatureReward, soeREQUIRED}, {sfMinAccountCreateAmount, soeOPTIONAL}, @@ -560,7 +600,8 @@ TRANSACTION(ttXCHAIN_CREATE_BRIDGE, 48, XChainCreateBridge, #endif TRANSACTION(ttDID_SET, 49, DIDSet, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfDIDDocument, soeOPTIONAL}, {sfURI, soeOPTIONAL}, {sfData, soeOPTIONAL}, @@ -569,7 +610,8 @@ TRANSACTION(ttDID_SET, 49, DIDSet, /** This transaction type deletes a DID */ TRANSACTION(ttDID_DELETE, 50, DIDDelete, Delegation::delegatable, - noPriv, ({})) + noPriv, + ({})) /** This transaction type creates an Oracle instance */ #if TRANSACTION_INCLUDE @@ -577,7 +619,8 @@ TRANSACTION(ttDID_DELETE, 50, DIDDelete, #endif TRANSACTION(ttORACLE_SET, 51, OracleSet, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfOracleDocumentID, soeREQUIRED}, {sfProvider, soeOPTIONAL}, {sfURI, soeOPTIONAL}, @@ -592,7 +635,8 @@ TRANSACTION(ttORACLE_SET, 51, OracleSet, #endif TRANSACTION(ttORACLE_DELETE, 52, OracleDelete, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfOracleDocumentID, soeREQUIRED}, })) @@ -602,7 +646,8 @@ TRANSACTION(ttORACLE_DELETE, 52, OracleDelete, #endif TRANSACTION(ttLEDGER_STATE_FIX, 53, LedgerStateFix, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfLedgerFixType, soeREQUIRED}, {sfOwner, soeOPTIONAL}, })) @@ -613,7 +658,8 @@ TRANSACTION(ttLEDGER_STATE_FIX, 53, LedgerStateFix, #endif TRANSACTION(ttMPTOKEN_ISSUANCE_CREATE, 54, MPTokenIssuanceCreate, Delegation::delegatable, - createMPTIssuance, ({ + createMPTIssuance, + ({ {sfAssetScale, soeOPTIONAL}, {sfTransferFee, soeOPTIONAL}, {sfMaximumAmount, soeOPTIONAL}, @@ -627,7 +673,8 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_CREATE, 54, MPTokenIssuanceCreate, #endif TRANSACTION(ttMPTOKEN_ISSUANCE_DESTROY, 55, MPTokenIssuanceDestroy, Delegation::delegatable, - destroyMPTIssuance, ({ + destroyMPTIssuance, + ({ {sfMPTokenIssuanceID, soeREQUIRED}, })) @@ -637,7 +684,8 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_DESTROY, 55, MPTokenIssuanceDestroy, #endif TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfMPTokenIssuanceID, soeREQUIRED}, {sfHolder, soeOPTIONAL}, {sfDomainID, soeOPTIONAL}, @@ -649,7 +697,8 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet, #endif TRANSACTION(ttMPTOKEN_AUTHORIZE, 57, MPTokenAuthorize, Delegation::delegatable, - mustAuthorizeMPT, ({ + mustAuthorizeMPT, + ({ {sfMPTokenIssuanceID, soeREQUIRED}, {sfHolder, soeOPTIONAL}, })) @@ -660,7 +709,8 @@ TRANSACTION(ttMPTOKEN_AUTHORIZE, 57, MPTokenAuthorize, #endif TRANSACTION(ttCREDENTIAL_CREATE, 58, CredentialCreate, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfSubject, soeREQUIRED}, {sfCredentialType, soeREQUIRED}, {sfExpiration, soeOPTIONAL}, @@ -670,7 +720,8 @@ TRANSACTION(ttCREDENTIAL_CREATE, 58, CredentialCreate, /** This transaction type accept an Credential object */ TRANSACTION(ttCREDENTIAL_ACCEPT, 59, CredentialAccept, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfIssuer, soeREQUIRED}, {sfCredentialType, soeREQUIRED}, })) @@ -678,7 +729,8 @@ TRANSACTION(ttCREDENTIAL_ACCEPT, 59, CredentialAccept, /** This transaction type delete an Credential object */ TRANSACTION(ttCREDENTIAL_DELETE, 60, CredentialDelete, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfSubject, soeOPTIONAL}, {sfIssuer, soeOPTIONAL}, {sfCredentialType, soeREQUIRED}, @@ -690,7 +742,8 @@ TRANSACTION(ttCREDENTIAL_DELETE, 60, CredentialDelete, #endif TRANSACTION(ttNFTOKEN_MODIFY, 61, NFTokenModify, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfNFTokenID, soeREQUIRED}, {sfOwner, soeOPTIONAL}, {sfURI, soeOPTIONAL}, @@ -702,7 +755,8 @@ TRANSACTION(ttNFTOKEN_MODIFY, 61, NFTokenModify, #endif TRANSACTION(ttPERMISSIONED_DOMAIN_SET, 62, PermissionedDomainSet, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfDomainID, soeOPTIONAL}, {sfAcceptedCredentials, soeREQUIRED}, })) @@ -713,7 +767,8 @@ TRANSACTION(ttPERMISSIONED_DOMAIN_SET, 62, PermissionedDomainSet, #endif TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 63, PermissionedDomainDelete, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfDomainID, soeREQUIRED}, })) @@ -723,7 +778,8 @@ TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 63, PermissionedDomainDelete, #endif TRANSACTION(ttDELEGATE_SET, 64, DelegateSet, Delegation::notDelegatable, - noPriv, ({ + noPriv, + ({ {sfAuthorize, soeREQUIRED}, {sfPermissions, soeREQUIRED}, })) @@ -734,7 +790,8 @@ TRANSACTION(ttDELEGATE_SET, 64, DelegateSet, #endif TRANSACTION(ttVAULT_CREATE, 65, VaultCreate, Delegation::delegatable, - createPseudoAcct | createMPTIssuance, ({ + createPseudoAcct | createMPTIssuance, + ({ {sfAsset, soeREQUIRED, soeMPTSupported}, {sfAssetsMaximum, soeOPTIONAL}, {sfMPTokenMetadata, soeOPTIONAL}, @@ -749,7 +806,8 @@ TRANSACTION(ttVAULT_CREATE, 65, VaultCreate, #endif TRANSACTION(ttVAULT_SET, 66, VaultSet, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfVaultID, soeREQUIRED}, {sfAssetsMaximum, soeOPTIONAL}, {sfDomainID, soeOPTIONAL}, @@ -762,7 +820,8 @@ TRANSACTION(ttVAULT_SET, 66, VaultSet, #endif TRANSACTION(ttVAULT_DELETE, 67, VaultDelete, Delegation::delegatable, - mustDeleteAcct | destroyMPTIssuance, ({ + mustDeleteAcct | destroyMPTIssuance, + ({ {sfVaultID, soeREQUIRED}, })) @@ -772,7 +831,8 @@ TRANSACTION(ttVAULT_DELETE, 67, VaultDelete, #endif TRANSACTION(ttVAULT_DEPOSIT, 68, VaultDeposit, Delegation::delegatable, - mayAuthorizeMPT, ({ + mayAuthorizeMPT, + ({ {sfVaultID, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, })) @@ -783,7 +843,8 @@ TRANSACTION(ttVAULT_DEPOSIT, 68, VaultDeposit, #endif TRANSACTION(ttVAULT_WITHDRAW, 69, VaultWithdraw, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfVaultID, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, {sfDestination, soeOPTIONAL}, @@ -796,7 +857,8 @@ TRANSACTION(ttVAULT_WITHDRAW, 69, VaultWithdraw, #endif TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback, Delegation::delegatable, - noPriv, ({ + noPriv, + ({ {sfVaultID, soeREQUIRED}, {sfHolder, soeREQUIRED}, {sfAmount, soeOPTIONAL, soeMPTSupported}, @@ -823,7 +885,8 @@ TRANSACTION(ttBATCH, 71, Batch, #endif TRANSACTION(ttAMENDMENT, 100, EnableAmendment, Delegation::notDelegatable, - noPriv, ({ + noPriv, + ({ {sfLedgerSequence, soeREQUIRED}, {sfAmendment, soeREQUIRED}, })) @@ -833,7 +896,8 @@ TRANSACTION(ttAMENDMENT, 100, EnableAmendment, */ TRANSACTION(ttFEE, 101, SetFee, Delegation::notDelegatable, - noPriv, ({ + noPriv, + ({ {sfLedgerSequence, soeOPTIONAL}, // Old version uses raw numbers {sfBaseFee, soeOPTIONAL}, @@ -852,7 +916,8 @@ TRANSACTION(ttFEE, 101, SetFee, */ TRANSACTION(ttUNL_MODIFY, 102, UNLModify, Delegation::notDelegatable, - noPriv, ({ + noPriv, + ({ {sfUNLModifyDisabling, soeREQUIRED}, {sfLedgerSequence, soeREQUIRED}, {sfUNLModifyValidator, soeREQUIRED}, From 1cb4a49ad3339c4ac5c65f6a80d8486ff9109f21 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 25 Aug 2025 19:48:31 -0400 Subject: [PATCH 056/291] Make a note about PR #5650 integration --- src/xrpld/app/tx/detail/Transactor.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/xrpld/app/tx/detail/Transactor.h b/src/xrpld/app/tx/detail/Transactor.h index 5c2ba136d7..cb58b5eca2 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -381,6 +381,9 @@ template NotTEC Transactor::invokePreflight(PreflightContext const& ctx) { + // TODO: If #5650 is merged, use its transaction -> amendment lookup here to + // do a first-pass check. Rewrite or remove any `isEnabled` overloads that + // check those default amendments. if (!T::isEnabled(ctx)) return temDISABLED; From 044697c43854e62ad6b44583655dc9e0d33ebcf6 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 27 Aug 2025 15:24:58 -0400 Subject: [PATCH 057/291] fix: Allow the borrower to delete a completed / inactive loan, too. --- src/xrpld/app/tx/detail/LoanDelete.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/xrpld/app/tx/detail/LoanDelete.cpp b/src/xrpld/app/tx/detail/LoanDelete.cpp index 4cf9e9a929..e2ee709563 100644 --- a/src/xrpld/app/tx/detail/LoanDelete.cpp +++ b/src/xrpld/app/tx/detail/LoanDelete.cpp @@ -86,10 +86,11 @@ LoanDelete::preclaim(PreclaimContext const& ctx) // should be impossible return tecINTERNAL; // LCOV_EXCL_LINE } - if (loanBrokerSle->at(sfOwner) != account) + if (loanBrokerSle->at(sfOwner) != account && + loanSle->at(sfBorrower) != account) { JLOG(ctx.j.warn()) - << "LoanBroker for Loan does not belong to the account."; + << "Account is not Loan Broker Owner or Loan Borrower."; return tecNO_PERMISSION; } From 08a5e8428a0c580133914a62efde6e520839f59f Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 29 Aug 2025 20:08:46 -0400 Subject: [PATCH 058/291] Require all SFields to have a name - From @bronek https://github.com/XRPLF/rippled/pull/5590#discussion_r2290589770 - Adds consistency, and ensures sfInvalid and sfGeneric are handled the same as all other SFields --- include/xrpl/protocol/SField.h | 2 +- src/libxrpl/protocol/SField.cpp | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/include/xrpl/protocol/SField.h b/include/xrpl/protocol/SField.h index c8e5d6dca3..7bf5394641 100644 --- a/include/xrpl/protocol/SField.h +++ b/include/xrpl/protocol/SField.h @@ -187,7 +187,7 @@ public: char const* fn, int meta = sMD_Default, IsSigning signing = IsSigning::yes); - explicit SField(private_access_tag_t, int fc); + explicit SField(private_access_tag_t, int fc, char const* fn); static SField const& getField(int fieldCode); diff --git a/src/libxrpl/protocol/SField.cpp b/src/libxrpl/protocol/SField.cpp index 478e6a4066..fcf89f5e18 100644 --- a/src/libxrpl/protocol/SField.cpp +++ b/src/libxrpl/protocol/SField.cpp @@ -71,8 +71,8 @@ TypedField::TypedField(private_access_tag_t pat, Args&&... args) ##__VA_ARGS__); // SFields which, for historical reasons, do not follow naming conventions. -SField const sfInvalid(access, -1); -SField const sfGeneric(access, 0); +SField const sfInvalid(access, -1, ""); +SField const sfGeneric(access, 0, "Generic"); // The following two fields aren't used anywhere, but they break tests/have // downstream effects. SField const sfHash(access, STI_UINT256, 257, "hash"); @@ -111,10 +111,11 @@ SField::SField( knownNameToField[fieldName] = this; } -SField::SField(private_access_tag_t, int fc) +SField::SField(private_access_tag_t, int fc, char const* fn) : fieldCode(fc) , fieldType(STI_UNKNOWN) , fieldValue(0) + , fieldName(fn) , fieldMeta(sMD_Never) , fieldNum(++num) , signingField(IsSigning::yes) @@ -122,8 +123,12 @@ SField::SField(private_access_tag_t, int fc) { XRPL_ASSERT( !knownCodeToField.contains(fieldCode), - "ripple::SField::SField(fc) : fieldCode is unique"); + "ripple::SField::SField(fc,fn) : fieldCode is unique"); + XRPL_ASSERT( + !knownNameToField.contains(fieldName), + "ripple::SField::SField(fc,fn) : fieldName is unique"); knownCodeToField[fieldCode] = this; + knownNameToField[fieldName] = this; } SField const& From faae2514b901da924d45eb210cc4649225accb38 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 3 Sep 2025 15:58:19 -0400 Subject: [PATCH 059/291] Rewrite Units.h and same safe_cast.h restrictions to use concepts --- include/xrpl/basics/safe_cast.h | 7 ++- include/xrpl/protocol/Units.h | 92 +++++++++++---------------------- 2 files changed, 34 insertions(+), 65 deletions(-) diff --git a/include/xrpl/basics/safe_cast.h b/include/xrpl/basics/safe_cast.h index f193f1793f..a750a3b6fc 100644 --- a/include/xrpl/basics/safe_cast.h +++ b/include/xrpl/basics/safe_cast.h @@ -28,9 +28,8 @@ namespace ripple { // the destination can hold all values of the source. This is particularly // handy when the source or destination is an enumeration type. -template -static constexpr bool is_safetocasttovalue_v = - (std::is_integral_v && std::is_integral_v) && +template +concept SafeToCast = (std::is_integral_v && std::is_integral_v) && (std::is_signed::value || std::is_unsigned::value) && (std::is_signed::value != std::is_signed::value ? sizeof(Dest) > sizeof(Src) @@ -78,7 +77,7 @@ inline constexpr std:: unsafe_cast(Src s) noexcept { static_assert( - !is_safetocasttovalue_v, + !SafeToCast, "Only unsafe if casting signed to unsigned or " "destination is too small"); return static_cast(s); diff --git a/include/xrpl/protocol/Units.h b/include/xrpl/protocol/Units.h index d6085891e6..9459f5cb82 100644 --- a/include/xrpl/protocol/Units.h +++ b/include/xrpl/protocol/Units.h @@ -50,25 +50,32 @@ struct unitlessTag; class BipsTag; class TenthBipsTag; -template -using enable_if_unit_t = typename std::enable_if_t< - std::is_class_v && std::is_object_v && - std::is_object_v>; +// These names don't have to be too descriptive, because we're in the "unit" +// namespace. -/** `is_usable_unit_v` is checked to ensure that only values with +template +concept Valid = std::is_class_v && std::is_object_v && + std::is_object_v; + +/** `Usable` is checked to ensure that only values with known valid type tags can be used (sometimes transparently) in non-unit contexts. At the time of implementation, this includes all known tags, but more may be added in the future, and they should not be added automatically unless determined to be appropriate. */ -template > -constexpr bool is_usable_unit_v = - std::is_same_v || - std::is_same_v || - std::is_same_v || - std::is_same_v || - std::is_same_v; +template +concept Usable = Valid && + (std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v); + +template +concept Compatible = Valid && std::is_arithmetic_v && + std::is_arithmetic_v && + std::is_convertible_v; template class ValueUnit : private boost::totally_ordered>, @@ -85,25 +92,6 @@ public: private: value_type value_; -protected: - template - static constexpr bool is_compatible_v = - std::is_arithmetic_v && std::is_arithmetic_v && - std::is_convertible_v; - - template > - static constexpr bool is_compatiblevalue_v = - is_compatible_v && - std::is_same_v; - - template - using enable_if_compatible_t = - typename std::enable_if_t>; - - template - using enable_if_compatiblevalue_t = - typename std::enable_if_t>; - public: ValueUnit() = default; constexpr ValueUnit(ValueUnit const& other) = default; @@ -135,12 +123,9 @@ public: /** Instances with the same unit, and a type that is "safe" to convert to this one can be converted implicitly */ - template < - class Other, - class = std::enable_if_t< - is_compatible_v && - is_safetocasttovalue_v>> + template Other> constexpr ValueUnit(ValueUnit const& value) + requires SafeToCast : ValueUnit(safe_cast(value.value())) { } @@ -255,7 +240,7 @@ public: return value_ == other.value_; } - template > + template Other> constexpr bool operator==(ValueUnit const& other) const { @@ -268,7 +253,7 @@ public: return value_ == other; } - template > + template Other> constexpr bool operator!=(ValueUnit const& other) const { @@ -310,12 +295,13 @@ public: return static_cast(value_) / reference.value(); } - // `is_usable_unit_v` is checked to ensure that only values with + // `Usable` is checked to ensure that only values with // known valid type tags can be converted to JSON. At the time // of implementation, that includes all known tags, but more may // be added in the future. - std::enable_if_t, Json::Value> + Json::Value jsonClipped() const + requires Usable { if constexpr (std::is_integral_v) { @@ -372,43 +358,27 @@ to_string(ValueUnit const& amount) return std::to_string(amount.value()); } -template > +template constexpr bool can_muldiv_source_v = std::is_convertible_v; -template > +template constexpr bool can_muldiv_dest_v = can_muldiv_source_v && // Dest is also a source std::is_convertible_v && sizeof(typename Dest::value_type) >= sizeof(std::uint64_t); -template < - class Source1, - class Source2, - class = enable_if_unit_t, - class = enable_if_unit_t> +template constexpr bool can_muldiv_sources_v = can_muldiv_source_v && can_muldiv_source_v && std::is_same_v; -template < - class Source1, - class Source2, - class Dest, - class = enable_if_unit_t, - class = enable_if_unit_t, - class = enable_if_unit_t> +template constexpr bool can_muldiv_v = can_muldiv_sources_v && can_muldiv_dest_v; // Source and Dest can be the same by default -template < - class Source1, - class Source2, - class Dest, - class = enable_if_unit_t, - class = enable_if_unit_t, - class = enable_if_unit_t> +template constexpr bool can_muldiv_commute_v = can_muldiv_v && !std::is_same_v; From a72c237373f88be17aaf47ff93080867ac48ecca Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 3 Sep 2025 18:04:05 -0400 Subject: [PATCH 060/291] Review feedback from @Bronek - Remove unnecessary #include - Explanatory comments - Make the MPT InvariantCheck related to EscrowFinish amendment safe - Convert SField maps to unordered_maps - Make jtx::fee::operator() clearer - Rename checkMyPrivilege to hasPrivilege --- include/xrpl/protocol/Protocol.h | 1 - include/xrpl/protocol/SField.h | 6 +- .../xrpl/protocol/detail/transactions.macro | 2 +- src/libxrpl/protocol/SField.cpp | 4 +- src/test/jtx/impl/fee.cpp | 3 +- src/xrpld/app/tx/detail/InvariantCheck.cpp | 72 +++++++++++++++---- src/xrpld/ledger/detail/View.cpp | 4 ++ 7 files changed, 71 insertions(+), 21 deletions(-) diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 898fd06fbd..bd39233cca 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -22,7 +22,6 @@ #include #include -#include #include diff --git a/include/xrpl/protocol/SField.h b/include/xrpl/protocol/SField.h index 7bf5394641..2f85cf3b7c 100644 --- a/include/xrpl/protocol/SField.h +++ b/include/xrpl/protocol/SField.h @@ -300,7 +300,7 @@ public: static int compare(SField const& f1, SField const& f2); - static std::map const& + static std::unordered_map const& getKnownCodeToField() { return knownCodeToField; @@ -308,8 +308,8 @@ public: private: static int num; - static std::map knownCodeToField; - static std::map knownNameToField; + static std::unordered_map knownCodeToField; + static std::unordered_map knownNameToField; }; /** A field with a type known at compile time. */ diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 5b1a6ac774..e75ce67ab1 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -77,7 +77,7 @@ TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, /** This transaction type completes an existing escrow. */ TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, Delegation::delegatable, - mayAuthorizeMPT, + noPriv, ({ {sfOwner, soeREQUIRED}, {sfOfferSequence, soeREQUIRED}, diff --git a/src/libxrpl/protocol/SField.cpp b/src/libxrpl/protocol/SField.cpp index fcf89f5e18..57e5849c00 100644 --- a/src/libxrpl/protocol/SField.cpp +++ b/src/libxrpl/protocol/SField.cpp @@ -28,8 +28,8 @@ namespace ripple { // Storage for static const members. SField::IsSigning const SField::notSigning; int SField::num = 0; -std::map SField::knownCodeToField; -std::map SField::knownNameToField; +std::unordered_map SField::knownCodeToField; +std::unordered_map SField::knownNameToField; // Give only this translation unit permission to construct SFields struct SField::private_access_tag_t diff --git a/src/test/jtx/impl/fee.cpp b/src/test/jtx/impl/fee.cpp index b887849946..a58105a85f 100644 --- a/src/test/jtx/impl/fee.cpp +++ b/src/test/jtx/impl/fee.cpp @@ -31,9 +31,10 @@ fee::operator()(Env& env, JTx& jt) const if (!manual_) return; jt.fill_fee = false; + assert(!increment_ || !amount_); if (increment_) jt[sfFee] = STAmount(env.current()->fees().increment).getJson(); - if (amount_) + else if (amount_) jt[sfFee] = amount_->getJson(JsonOptions::none); } diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 10c008ea1f..83d5576836 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -37,6 +37,25 @@ namespace ripple { +/* +assert(enforce) + +There are several asserts (or XRPL_ASSERTs) in this file that check a variable +named `enforce` when an invariant fails. At first glace, those asserts may look +incorrect, but they are not. + +Those asserts take advantage of two facts: +1. `asserts` are not (normally) executed in release builds. +2. Invariants should *never* fail, except in tests that specifically modify + the open ledger to break them. + +To make it sort of a second-layer of invariant enforcement aimed at +_developers_. It's designed to fire if a developer writes code that violates an +invariant, and runs it in unit tests or a develop build that _does not have the +relevant amendments enabled_. It's intentionally a pain in the neck so that bad +code gets caught and fixed as early as possible. +*/ + enum Privilege { noPriv = 0x0000, // The transaction can not do any of the enumerated operations @@ -75,7 +94,7 @@ operator|(Privilege lhs, Privilege rhs) } bool -checkMyPrivilege(STTx const& tx, Privilege priv) +hasPrivilege(STTx const& tx, Privilege priv) { switch (tx.getTxnType()) { @@ -432,7 +451,7 @@ AccountRootsNotDeleted::finalize( // transaction when the total AMM LP Tokens balance goes to 0. // A successful AccountDelete or AMMDelete MUST delete exactly // one account root. - if (checkMyPrivilege(tx, mustDeleteAcct) && result == tesSUCCESS) + if (hasPrivilege(tx, mustDeleteAcct) && result == tesSUCCESS) { if (accountsDeleted_ == 1) return true; @@ -449,7 +468,7 @@ AccountRootsNotDeleted::finalize( // A successful AMMWithdraw/AMMClawback MAY delete one account root // when the total AMM LP Tokens balance goes to 0. Not every AMM withdraw // deletes the AMM account, accountsDeleted_ is set if it is deleted. - if (checkMyPrivilege(tx, mayDeleteAcct) && result == tesSUCCESS && + if (hasPrivilege(tx, mayDeleteAcct) && result == tesSUCCESS && accountsDeleted_ == 1) return true; @@ -505,6 +524,8 @@ AccountRootsDeletedClean::finalize( JLOG(j.fatal()) << "Invariant failed: account deletion left behind a " << typeName << " object"; + // The comment above starting with "assert(enforce)" explains this + // assert. XRPL_ASSERT( enforce, "ripple::AccountRootsDeletedClean::finalize::objectExists : " @@ -750,6 +771,8 @@ TransfersNotFrozen::finalize( // just in case so rippled doesn't crash in release. if (!issuerSle) { + // The comment above starting with "assert(enforce)" explains this + // assert. XRPL_ASSERT( enforce, "ripple::TransfersNotFrozen::finalize : enforce " @@ -938,7 +961,7 @@ TransfersNotFrozen::validateFrozenState( } // AMMClawbacks are allowed to override some freeze rules - if ((!isAMMLine || globalFreeze) && checkMyPrivilege(tx, overrideFreeze)) + if ((!isAMMLine || globalFreeze) && hasPrivilege(tx, overrideFreeze)) { JLOG(j.debug()) << "Invariant check allowing funds to be moved " << (change.balanceChangeSign > 0 ? "to" : "from") @@ -949,6 +972,7 @@ TransfersNotFrozen::validateFrozenState( JLOG(j.fatal()) << "Invariant failed: Attempting to move frozen funds for " << tx.getTransactionID(); + // The comment above starting with "assert(enforce)" explains this assert. XRPL_ASSERT( enforce, "ripple::TransfersNotFrozen::validateFrozenState : enforce " @@ -998,13 +1022,12 @@ ValidNewAccountRoot::finalize( } // From this point on we know exactly one account was created. - if (checkMyPrivilege(tx, createAcct | createPseudoAcct) && - result == tesSUCCESS) + if (hasPrivilege(tx, createAcct | createPseudoAcct) && result == tesSUCCESS) { bool const pseudoAccount = (pseudoAccount_ && view.rules().enabled(featureSingleAssetVault)); - if (pseudoAccount && !checkMyPrivilege(tx, createPseudoAcct)) + if (pseudoAccount && !hasPrivilege(tx, createPseudoAcct)) { JLOG(j.fatal()) << "Invariant failed: pseudo-account created by a " "wrong transaction type"; @@ -1238,7 +1261,7 @@ NFTokenCountTracking::finalize( ReadView const& view, beast::Journal const& j) { - if (!checkMyPrivilege(tx, changeNFTCounts)) + if (!hasPrivilege(tx, changeNFTCounts)) { if (beforeMintedTotal != afterMintedTotal) { @@ -1423,12 +1446,12 @@ ValidMPTIssuance::finalize( STTx const& tx, TER const result, XRPAmount const _fee, - ReadView const& _view, + ReadView const& view, beast::Journal const& j) { if (result == tesSUCCESS) { - if (checkMyPrivilege(tx, createMPTIssuance)) + if (hasPrivilege(tx, createMPTIssuance)) { if (mptIssuancesCreated_ == 0) { @@ -1449,7 +1472,7 @@ ValidMPTIssuance::finalize( return mptIssuancesCreated_ == 1 && mptIssuancesDeleted_ == 0; } - if (checkMyPrivilege(tx, destroyMPTIssuance)) + if (hasPrivilege(tx, destroyMPTIssuance)) { if (mptIssuancesDeleted_ == 0) { @@ -1470,7 +1493,17 @@ ValidMPTIssuance::finalize( return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 1; } - if (checkMyPrivilege(tx, mustAuthorizeMPT | mayAuthorizeMPT)) + // ttESCROW_FINISH may authorize an MPT, but it can't have the + // mayAuthorizeMPT privilege, because that may cause + // non-amendment-gated side effects. + bool const enforceEscrowFinish = (tx.getTxnType() == ttESCROW_FINISH) && + (view.rules().enabled(featureSingleAssetVault) + /* + TODO: Uncomment when LendingProtocol is defined + || view.rules().enabled(featureLendingProtocol)*/ + ); + if (hasPrivilege(tx, mustAuthorizeMPT | mayAuthorizeMPT) || + enforceEscrowFinish) { bool const submittedByIssuer = tx.isFieldPresent(sfHolder); @@ -1496,7 +1529,7 @@ ValidMPTIssuance::finalize( return false; } else if ( - !submittedByIssuer && !checkMyPrivilege(tx, mayAuthorizeMPT) && + !submittedByIssuer && hasPrivilege(tx, mustAuthorizeMPT) && (mptokensCreated_ + mptokensDeleted_ != 1)) { // if the holder submitted this tx, then a mptoken must be @@ -1509,6 +1542,17 @@ ValidMPTIssuance::finalize( return true; } + if (tx.getTxnType() == ttESCROW_FINISH) + { + // ttESCROW_FINISH may authorize an MPT, but it can't have the + // mayAuthorizeMPT privilege, because that may cause + // non-amendment-gated side effects. + XRPL_ASSERT_PARTS( + !enforceEscrowFinish, + "ripple::ValidMPTIssuance::finalize", + "not escrow finish tx"); + return true; + } } if (mptIssuancesCreated_ != 0) @@ -1711,6 +1755,8 @@ ValidPseudoAccounts::finalize( beast::Journal const& j) { bool const enforce = view.rules().enabled(featureSingleAssetVault); + + // The comment above starting with "assert(enforce)" explains this assert. XRPL_ASSERT( errors_.empty() || enforce, "ripple::ValidPseudoAccounts::finalize : no bad " diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index 9e7cb34e19..3a46a4bd32 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -1092,9 +1092,13 @@ getPseudoAccountFields() static std::vector const pseudoFields = []() { auto const ar = LedgerFormats::getInstance().findByType(ltACCOUNT_ROOT); if (!ar) + { + // LCOV_EXCL_START LogicError( "ripple::isPseudoAccount : unable to find account root ledger " "format"); + // LCOV_EXCL_STOP + } auto const& soTemplate = ar->getSOTemplate(); std::vector pseudoFields; From 58551da6984217db908e60cf80cd8085de30d07e Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 3 Sep 2025 19:18:24 -0400 Subject: [PATCH 061/291] Review feedback from @dangell7 and @Bronek - Added more `STParsedJSON` test cases. - Add a `safe_cast` conversion in `parseUint32`. --- src/libxrpl/protocol/STParsedJSON.cpp | 4 ++- src/test/protocol/STParsedJSON_test.cpp | 44 +++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/libxrpl/protocol/STParsedJSON.cpp b/src/libxrpl/protocol/STParsedJSON.cpp index db199a73ed..02dfde3966 100644 --- a/src/libxrpl/protocol/STParsedJSON.cpp +++ b/src/libxrpl/protocol/STParsedJSON.cpp @@ -353,7 +353,9 @@ parseUint32( else { ret = detail::make_stvar( - field, beast::lexicalCastThrow(value.asString())); + field, + safe_cast( + beast::lexicalCastThrow(value.asString()))); } } if (!ret) diff --git a/src/test/protocol/STParsedJSON_test.cpp b/src/test/protocol/STParsedJSON_test.cpp index 082a4624f2..bd62196a99 100644 --- a/src/test/protocol/STParsedJSON_test.cpp +++ b/src/test/protocol/STParsedJSON_test.cpp @@ -140,6 +140,30 @@ public: } } + { + std::string const goodJson( + R"({"CloseResolution":"19","Method":"250",)" + R"("TransactionResult":"tecFROZEN"})"); + std::string const expectedJson( + R"({"CloseResolution":19,"Method":250,)" + R"("TransactionResult":"tecFROZEN"})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(goodJson, jv))) + { + // Integer values are always parsed as int, + // unless they're too big. We want a small uint. + jv["CloseResolution"] = Json::UInt(19); + STParsedJSONObject parsed("test", jv); + if (BEAST_EXPECT(parsed.object)) + { + std::string const& serialized( + to_string(parsed.object->getJson(JsonOptions::none))); + BEAST_EXPECT(serialized == expectedJson); + } + } + } + { std::string const json(R"({"CloseResolution":19,"Method":250,)" R"("TransactionResult":"terQUEUED"})"); @@ -227,6 +251,26 @@ public: } } + { + std::string const goodJson(R"({"CloseResolution":19,"Method":250,)" + R"("TransferFee":"65535"})"); + std::string const expectedJson( + R"({"CloseResolution":19,"Method":250,)" + R"("TransferFee":65535})"); + + Json::Value jv; + if (BEAST_EXPECT(parseJSONString(goodJson, jv))) + { + STParsedJSONObject parsed("test", jv); + if (BEAST_EXPECT(parsed.object)) + { + std::string const& serialized( + to_string(parsed.object->getJson(JsonOptions::none))); + BEAST_EXPECT(serialized == expectedJson); + } + } + } + { std::string const json(R"({"CloseResolution":19,"Method":250,)" R"("TransferFee":"65536"})"); From bb491431bdc3021c78cd5aa88b07339ea4ec8fa6 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 3 Sep 2025 20:51:40 -0400 Subject: [PATCH 062/291] Review feedback from @dangell7 and @mvadari - Rewrite isTesSuccess to use TERSubset::operator bool - Add Transactor::preflightSigValidated for expensive operations that should be done after signature validation. These things would have been done after preflight2 before this refactor. - Split Batch and EscrowFinish preflight to use preflightSigValidated, too. --- include/xrpl/protocol/TER.h | 3 ++- src/xrpld/app/tx/detail/Batch.cpp | 18 +++++++++++++++++- src/xrpld/app/tx/detail/Batch.h | 3 +++ src/xrpld/app/tx/detail/Escrow.cpp | 9 +++++++++ src/xrpld/app/tx/detail/Escrow.h | 3 +++ src/xrpld/app/tx/detail/Transactor.cpp | 6 ++++++ src/xrpld/app/tx/detail/Transactor.h | 13 ++++++++++++- 7 files changed, 52 insertions(+), 3 deletions(-) diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index 9ace6b80f8..0a3a3b999e 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -673,7 +673,8 @@ isTerRetry(TER x) noexcept inline bool isTesSuccess(TER x) noexcept { - return (x == tesSUCCESS); + // Makes use of TERSubset::operator bool() + return !(x); } inline bool diff --git a/src/xrpld/app/tx/detail/Batch.cpp b/src/xrpld/app/tx/detail/Batch.cpp index 5f05d9919e..5b0f5b4d8f 100644 --- a/src/xrpld/app/tx/detail/Batch.cpp +++ b/src/xrpld/app/tx/detail/Batch.cpp @@ -241,7 +241,6 @@ Batch::preflight(PreflightContext const& ctx) } // Validation Inner Batch Txns - std::unordered_set requiredSigners; std::unordered_set uniqueHashes; std::unordered_map> accountSeqTicket; @@ -371,6 +370,23 @@ Batch::preflight(PreflightContext const& ctx) } } } + } + + return tesSUCCESS; +} + +NotTEC +Batch::preflightSigValidated(PreflightContext const& ctx) +{ + auto const parentBatchId = ctx.tx.getTransactionID(); + auto const outerAccount = ctx.tx.getAccountID(sfAccount); + auto const& rawTxns = ctx.tx.getFieldArray(sfRawTransactions); + + // Build the signers list + std::unordered_set requiredSigners; + for (STObject const& rb : rawTxns) + { + auto const innerAccount = rb.getAccountID(sfAccount); // If the inner account is the same as the outer account, do not add the // inner account to the required signers set. diff --git a/src/xrpld/app/tx/detail/Batch.h b/src/xrpld/app/tx/detail/Batch.h index c8a264f328..427cc6bf34 100644 --- a/src/xrpld/app/tx/detail/Batch.h +++ b/src/xrpld/app/tx/detail/Batch.h @@ -49,6 +49,9 @@ public: static NotTEC preflight(PreflightContext const& ctx); + static NotTEC + preflightSigValidated(PreflightContext const& ctx); + static NotTEC checkSign(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/Escrow.cpp b/src/xrpld/app/tx/detail/Escrow.cpp index fa906eb22a..0cf07c8c07 100644 --- a/src/xrpld/app/tx/detail/Escrow.cpp +++ b/src/xrpld/app/tx/detail/Escrow.cpp @@ -650,6 +650,15 @@ EscrowFinish::preflight(PreflightContext const& ctx) if (static_cast(cb) != static_cast(fb)) return temMALFORMED; + return tesSUCCESS; +} + +NotTEC +EscrowFinish::preflightSigValidated(PreflightContext const& ctx) +{ + auto const cb = ctx.tx[~sfCondition]; + auto const fb = ctx.tx[~sfFulfillment]; + if (cb && fb) { auto& router = ctx.app.getHashRouter(); diff --git a/src/xrpld/app/tx/detail/Escrow.h b/src/xrpld/app/tx/detail/Escrow.h index 9696515963..fd08b4764a 100644 --- a/src/xrpld/app/tx/detail/Escrow.h +++ b/src/xrpld/app/tx/detail/Escrow.h @@ -69,6 +69,9 @@ public: static NotTEC preflight(PreflightContext const& ctx); + static NotTEC + preflightSigValidated(PreflightContext const& ctx); + static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 5988199393..3d2bb8197e 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -267,6 +267,12 @@ Transactor::getFlagsMask(PreflightContext const& ctx) return tfUniversalMask; } +NotTEC +Transactor::preflightSigValidated(PreflightContext const& ctx) +{ + return tesSUCCESS; +} + TER Transactor::checkPermission(ReadView const& view, STTx const& tx) { diff --git a/src/xrpld/app/tx/detail/Transactor.h b/src/xrpld/app/tx/detail/Transactor.h index cb58b5eca2..29a8df7574 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -215,6 +215,10 @@ public: static NotTEC preflight(PreflightContext const& ctx); + // Optional, rarely needed, if the transaction does any expensive + // checks after the signature is verified. + static NotTEC preflightSigValidated(PreflightContext const& ctx); + * Do not try to call preflight1 or preflight2 directly. * Do not check whether relevant amendments are enabled in preflight. Instead, define isEnabled. @@ -284,6 +288,10 @@ protected: static std::uint32_t getFlagsMask(PreflightContext const& ctx); + // Base class always returns tesSUCCESS + static NotTEC + preflightSigValidated(PreflightContext const& ctx); + static bool validDataLength(std::optional const& slice, std::size_t maxLength); @@ -393,7 +401,10 @@ Transactor::invokePreflight(PreflightContext const& ctx) if (auto const ret = T::preflight(ctx)) return ret; - return preflight2(ctx); + if (auto const ret = preflight2(ctx)) + return ret; + + return T::preflightSigValidated(ctx); } template From 450d38293df59df9236a1c96ac9156772cc47a35 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 4 Sep 2025 13:14:19 -0400 Subject: [PATCH 063/291] Remove unused variable --- src/xrpld/app/tx/detail/Batch.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/xrpld/app/tx/detail/Batch.cpp b/src/xrpld/app/tx/detail/Batch.cpp index 5b0f5b4d8f..a7a766d068 100644 --- a/src/xrpld/app/tx/detail/Batch.cpp +++ b/src/xrpld/app/tx/detail/Batch.cpp @@ -213,7 +213,6 @@ NotTEC Batch::preflight(PreflightContext const& ctx) { auto const parentBatchId = ctx.tx.getTransactionID(); - auto const outerAccount = ctx.tx.getAccountID(sfAccount); auto const flags = ctx.tx.getFlags(); if (std::popcount( From 0d500343ed48c0ec73d9d7b1e3419306a3cab0b7 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 4 Sep 2025 14:08:54 -0400 Subject: [PATCH 064/291] New invariant privilege "mayDeleteMPT", used by VauleDeposit & Clawback --- include/xrpl/protocol/detail/transactions.macro | 4 ++-- src/xrpld/app/tx/detail/InvariantCheck.cpp | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index f9f74d2492..09e1c183b2 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -844,7 +844,7 @@ TRANSACTION(ttVAULT_DEPOSIT, 68, VaultDeposit, #endif TRANSACTION(ttVAULT_WITHDRAW, 69, VaultWithdraw, Delegation::delegatable, - noPriv, + mayDeleteMPT, ({ {sfVaultID, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, @@ -858,7 +858,7 @@ TRANSACTION(ttVAULT_WITHDRAW, 69, VaultWithdraw, #endif TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback, Delegation::delegatable, - noPriv, + mayDeleteMPT, ({ {sfVaultID, soeREQUIRED}, {sfHolder, soeREQUIRED}, diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 201419e149..1a2b69c579 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -76,6 +76,8 @@ enum Privilege { // object (except by issuer) mayAuthorizeMPT = 0x0200, // The transaction MAY create or delete an MPT // object (except by issuer) + mayDeleteMPT = + 0x0400, // The transaction MAY delete an MPT object. May not create. }; constexpr Privilege operator|(Privilege lhs, Privilege rhs) @@ -1554,10 +1556,9 @@ ValidMPTIssuance::finalize( return true; } - if ((tx.getTxnType() == ttVAULT_CLAWBACK || - tx.getTxnType() == ttVAULT_WITHDRAW) && - mptokensDeleted_ == 1 && mptokensCreated_ == 0 && - mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0) + if (hasPrivilege(tx, mayDeleteMPT) && mptokensDeleted_ == 1 && + mptokensCreated_ == 0 && mptIssuancesCreated_ == 0 && + mptIssuancesDeleted_ == 0) return true; } From 7a78449f6d319e21d5ce09a41d9611cc12a54b2f Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 4 Sep 2025 15:53:04 -0400 Subject: [PATCH 065/291] Fix minor review feedback from @Bronek - Clean up assert, log, and comments --- src/xrpld/app/tx/detail/Transactor.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 14faffd048..6da22de9fb 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -685,15 +685,12 @@ Transactor::checkSign( } // Check Single Sign - // This ternary is only needed to handle `simulate` XRPL_ASSERT( - !pkSigner.empty(), - "ripple::Transactor::checkSingleSign : non-empty signer or simulation"); + !pkSigner.empty(), "ripple::Transactor::checkSign : non-empty signer"); if (!publicKeyType(makeSlice(pkSigner))) { - JLOG(ctx.j.trace()) - << "checkSingleSign: signing public key type is unknown"; + JLOG(ctx.j.trace()) << "checkSign: signing public key type is unknown"; return tefBAD_AUTH; // FIXME: should be better error! } @@ -823,7 +820,6 @@ Transactor::checkSingleSign( return tesSUCCESS; } -// TODO generalize NotTEC Transactor::checkMultiSign( PreclaimContext const& ctx, From adb260fe174be1ccecdaea78de83539ab866f422 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 4 Sep 2025 20:21:30 -0400 Subject: [PATCH 066/291] Review feedback from @dangell7 and @gregtatcam - Mostly adding comments. - Fixed some function parameter names that weren't updated after a copy/paste. - LoanBrokerCoverWithdraw does not need to check for freeze when sending to the issuer. - Reorder the fund transfer cases in LoanBrokerCoverWithdraw to make it clearer that some transfers are direct, and some use the payment engine. - Only look up the vault ID once in LoanBrokerSet --- include/xrpl/protocol/Indexes.h | 8 ++--- .../app/tx/detail/LoanBrokerCoverClawback.cpp | 7 +++++ .../app/tx/detail/LoanBrokerCoverWithdraw.cpp | 31 +++++++++++-------- src/xrpld/app/tx/detail/LoanBrokerSet.cpp | 6 ++-- 4 files changed, 32 insertions(+), 20 deletions(-) diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index 154b824d1f..ed10e4a2e5 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -348,18 +348,18 @@ Keylet loanbroker(AccountID const& owner, std::uint32_t seq) noexcept; inline Keylet -loanbroker(uint256 const& vaultKey) +loanbroker(uint256 const& key) { - return {ltLOAN_BROKER, vaultKey}; + return {ltLOAN_BROKER, key}; } Keylet loan(uint256 const& loanBrokerID, std::uint32_t loanSeq) noexcept; inline Keylet -loan(uint256 const& vaultKey) +loan(uint256 const& key) { - return {ltLOAN, vaultKey}; + return {ltLOAN, key}; } Keylet diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp index 0f2914887d..3f9e102de1 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp @@ -64,6 +64,7 @@ LoanBrokerCoverClawback::preflight(PreflightContext const& ctx) if (amount) { + // XRP has no counterparty, and thus nobody can claw it back if (amount->native()) return temBAD_AMOUNT; @@ -81,6 +82,9 @@ LoanBrokerCoverClawback::preflight(PreflightContext const& ctx) return temINVALID; auto const account = ctx.tx[sfAccount]; + // Since we don't have a LoanBrokerID, holder _should_ be the loan + // broker's pseudo-account, but we don't know yet whether it is, so + // use a generic placeholder name. auto const holder = amount->getIssuer(); if (holder == account || holder == beast::zero) return temINVALID; @@ -100,6 +104,9 @@ determineBrokerID(ReadView const& view, STTx const& tx) if (!dstAmount || !dstAmount->holds()) return Unexpected{tecINTERNAL}; + // Since we don't have a LoanBrokerID, holder _should_ be the loan broker's + // pseudo-account, but we don't know yet whether it is, so use a generic + // placeholder name. auto const holder = dstAmount->getIssuer(); auto const sle = view.read(keylet::account(holder)); if (!sle) diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp index 1f827cc7b8..58bb390ce1 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp @@ -145,11 +145,12 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) // The broker's pseudo-account is the source of funds. auto const pseudoAccountID = sleBroker->at(sfAccount); - // Cannot send a frozen Asset - if (auto const ret = checkFrozen(ctx.view, pseudoAccountID, vaultAsset)) - return ret; + // Check for freezes, unless sending directly to the issuer if (dstAcct != vaultAsset.getIssuer()) { + // Cannot send a frozen Asset + if (auto const ret = checkFrozen(ctx.view, pseudoAccountID, vaultAsset)) + return ret; // Destination account cannot receive if asset is deep frozen if (auto const ret = checkDeepFrozen(ctx.view, dstAcct, vaultAsset)) return ret; @@ -205,8 +206,21 @@ LoanBrokerCoverWithdraw::doApply() // Move the funds from the broker's pseudo-account to the dstAcct - if (dstAcct != account_ && !amount.native()) + if (dstAcct == account_ || amount.native()) { + // Transfer assets directly from pseudo-account to depositor. + // Because this is either a self-transfer or an XRP payment, there is no + // need to use the payment engine. + return accountSend( + view(), brokerPseudoID, dstAcct, amount, j_, WaiveTransferFee::Yes); + } + + { + // If sending the Cover to a different account, then this is + // effectively a payment. Use the Payment transaction code to call + // the payment engine, though only a subset of the functionality is + // supported in this transaction. e.g. No paths, no partial + // payments. bool const mptDirect = amount.holds(); STAmount const maxSourceAmount = Payment::getMaxSourceAmount(brokerPseudoID, amount); @@ -225,11 +239,6 @@ LoanBrokerCoverWithdraw::doApply() .deliverMin = std::nullopt, .j = j_}; - // If sending the Cover to a different account, then this is - // effectively a payment. Use the Payment transaction code to call - // the payment engine, though only a subset of the functionality is - // supported in this transaction. e.g. No paths, no partial - // payments. TER ret; if (mptDirect) { @@ -250,10 +259,6 @@ LoanBrokerCoverWithdraw::doApply() } return ret; } - - // Transfer assets from pseudo-account to depositor. - return accountSend( - view(), brokerPseudoID, dstAcct, amount, j_, WaiveTransferFee::Yes); } //------------------------------------------------------------------------------ diff --git a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp index 70f329af16..a4569ef5cf 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp @@ -87,6 +87,8 @@ LoanBrokerSet::preclaim(PreclaimContext const& ctx) auto const& tx = ctx.tx; auto const account = tx[sfAccount]; + auto const vaultID = tx[sfVaultID]; + if (auto const brokerID = tx[~sfLoanBrokerID]) { auto const sleBroker = ctx.view.read(keylet::loanbroker(*brokerID)); @@ -95,7 +97,7 @@ LoanBrokerSet::preclaim(PreclaimContext const& ctx) JLOG(ctx.j.warn()) << "LoanBroker does not exist."; return tecNO_ENTRY; } - if (tx[sfVaultID] != sleBroker->at(sfVaultID)) + if (vaultID != sleBroker->at(sfVaultID)) { JLOG(ctx.j.warn()) << "Can not change VaultID on an existing LoanBroker."; @@ -109,7 +111,6 @@ LoanBrokerSet::preclaim(PreclaimContext const& ctx) } else { - auto const vaultID = tx[sfVaultID]; auto const sleVault = ctx.view.read(keylet::vault(vaultID)); if (!sleVault) { @@ -150,7 +151,6 @@ LoanBrokerSet::doApply() else { // Create a new LoanBroker pointing back to the given Vault - auto const vaultID = tx[sfVaultID]; auto const sleVault = view.read(keylet::vault(vaultID)); auto const vaultPseudoID = sleVault->at(sfAccount); auto const sequence = tx.getSeqValue(); From f0c96ccfe6f13e98c7558cec3e72cd4d5b82b10d Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 5 Sep 2025 17:41:06 -0400 Subject: [PATCH 067/291] Review feedback from @yinyiqian1 - Rewrite to_short_string to call strHex directly instead of building the whole hex string first. - Change PrettyAsset::scale_ back to a uint32 since the Number conversion elides any potential multiplication overflow. - Clean ups. --- include/xrpl/basics/base_uint.h | 7 ++++++- src/test/jtx/TestHelpers.h | 2 +- src/test/jtx/amount.h | 2 +- src/test/unit_test/FileDirGuard.h | 1 - 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/include/xrpl/basics/base_uint.h b/include/xrpl/basics/base_uint.h index 1eacb16385..ca83b8e220 100644 --- a/include/xrpl/basics/base_uint.h +++ b/include/xrpl/basics/base_uint.h @@ -636,7 +636,12 @@ template inline std::string to_short_string(base_uint const& a) { - return to_string(a).substr(0, 8) + "..."; + // LCOV_EXCL_START + if constexpr (a.bytes <= 4) + return to_string(a); + else + // LCOV_EXCL_STOP + return strHex(a.cbegin(), a.cbegin() + 4) + "..."; } template diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index 3aea274e53..7d14f23c92 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -361,7 +361,7 @@ ownerCount(test::jtx::Env const& env, test::jtx::Account const& account); [[nodiscard]] inline bool -checkVL(Slice const& result, std::string expected) +checkVL(Slice const& result, std::string const& expected) { Serializer s; s.addRaw(result); diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h index d0b21d31ce..fbe78bf3ec 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -181,7 +181,7 @@ struct PrettyAsset { private: Asset asset_; - std::uint64_t scale_; + std::uint32_t scale_; public: template diff --git a/src/test/unit_test/FileDirGuard.h b/src/test/unit_test/FileDirGuard.h index ea7efc49a7..d3cabc2092 100644 --- a/src/test/unit_test/FileDirGuard.h +++ b/src/test/unit_test/FileDirGuard.h @@ -29,7 +29,6 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. #include namespace ripple { - namespace detail { /** From 6db14ccb132072ba119966646aeaa88bf9a279c8 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 5 Sep 2025 20:00:10 -0400 Subject: [PATCH 068/291] More review feedback from @gregtatcam - Also fix a few build errors that I missed earlier. - Updated freeze check rules for LoanSet. - Fixed the debt total calculation and check in LoanSet. - Removed _some_ unnecessary includes. --- .../xrpl/protocol/detail/ledger_entries.macro | 2 +- src/test/app/Loan_test.cpp | 68 +++++++++++++------ src/xrpld/app/tx/detail/Batch.cpp | 2 +- .../app/tx/detail/LoanBrokerCoverClawback.cpp | 2 + src/xrpld/app/tx/detail/LoanBrokerSet.cpp | 15 +--- src/xrpld/app/tx/detail/LoanSet.cpp | 61 +++++++++++------ 6 files changed, 91 insertions(+), 59 deletions(-) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 7a5894df24..3d0a4e1b8b 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -558,7 +558,7 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({ {sfPreviousPaymentDate, soeREQUIRED}, {sfNextPaymentDueDate, soeREQUIRED}, {sfPaymentRemaining, soeREQUIRED}, - {sfAssetsAvailable, soeREQUIRED}, + {sfAssetsAvailable, soeDEFAULT}, {sfPrincipalOutstanding, soeREQUIRED}, // Save the original request amount for rounding / scaling of // other computations, particularly for IOUs diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 8825f3fd29..defad8b383 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -925,6 +925,17 @@ class Loan_test : public beast::unit_test::suite if (!BEAST_EXPECT(brokerSle)) return; + auto const vaultPseudo = [&]() { + auto const vaultSle = + env.le(keylet::vault(brokerSle->at(sfVaultID))); + if (!BEAST_EXPECT(vaultSle)) + // This will be wrong, but the test has failed anyway. + return lender; + auto const vaultPseudo = + Account("Vault pseudo-account", vaultSle->at(sfAccount)); + return vaultPseudo; + }(); + auto const [freeze, deepfreeze, unfreeze, expectedResult] = [&]() -> std::tuple< std::function, @@ -976,8 +987,8 @@ class Loan_test : public beast::unit_test::suite } }(); - // Try freezing both the lender and the pseudo-account - for (auto const& account : {lender, pseudoAcct}) + // Try freezing the accounts that can't be frozen + for (auto const& account : {vaultPseudo, evan, pseudoAcct}) { if (freeze) { @@ -1020,29 +1031,42 @@ class Loan_test : public beast::unit_test::suite // automatically.) env(trust(evan, issuer[iouCurrency](100'000))); - // Freeze evan - deepfreeze(evan); + for (auto const& account : + {// these accounts can't be frozen, which deep freeze + // implies + vaultPseudo, + evan, + pseudoAcct, + // these accounts can't be deep frozen + lender}) + { + // Freeze evan + deepfreeze(evan); - // Try to create a loan with a deep frozen line - env(set(evan, broker.brokerID, debtMaximumRequest, startDate), - sig(sfCounterpartySignature, lender), - loanSetFee, - ter(expectedResult)); + // Try to create a loan with a deep frozen line + env(set(evan, + broker.brokerID, + debtMaximumRequest, + startDate), + sig(sfCounterpartySignature, lender), + loanSetFee, + ter(expectedResult)); - // Unfreeze evan - BEAST_EXPECT(unfreeze); - unfreeze(evan); + // Unfreeze evan + BEAST_EXPECT(unfreeze); + unfreeze(evan); - // Ensure the line is unfrozen with a request that is fine - // except too it requests more principal than the broker can - // carry - env(set(evan, - broker.brokerID, - debtMaximumRequest + 1, - startDate), - sig(sfCounterpartySignature, lender), - loanSetFee, - ter(tecLIMIT_EXCEEDED)); + // Ensure the line is unfrozen with a request that is fine + // except too it requests more principal than the broker can + // carry + env(set(evan, + broker.brokerID, + debtMaximumRequest + 1, + startDate), + sig(sfCounterpartySignature, lender), + loanSetFee, + ter(tecLIMIT_EXCEEDED)); + } } } diff --git a/src/xrpld/app/tx/detail/Batch.cpp b/src/xrpld/app/tx/detail/Batch.cpp index 6549551c1e..c2dc014a95 100644 --- a/src/xrpld/app/tx/detail/Batch.cpp +++ b/src/xrpld/app/tx/detail/Batch.cpp @@ -418,7 +418,7 @@ Batch::preflightSigValidated(PreflightContext const& ctx) requiredSigners.insert(innerAccount); // Some transactions have a Counterparty, who must also sign the // transaction if they are not the outer account - if (auto const counterparty = stx.at(~sfCounterparty); + if (auto const counterparty = rb.at(~sfCounterparty); counterparty && counterparty != outerAccount) requiredSigners.insert(*counterparty); } diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp index 3f9e102de1..7dcc37dd04 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp @@ -133,6 +133,8 @@ determineAsset( // This check only applies to IOUs auto const holder = amount.getIssuer(); + // holder can be the submitting account (the issuer of the asset) if a + // LoanBrokerID was provided in the transaction. if (holder == account) { return amount.asset(); diff --git a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp index a4569ef5cf..b3e6527c17 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp @@ -20,28 +20,14 @@ #include // #include -#include -#include #include #include #include #include -#include -#include -#include -#include -#include #include -#include #include #include -#include -#include -#include -#include -#include -#include namespace ripple { @@ -151,6 +137,7 @@ LoanBrokerSet::doApply() else { // Create a new LoanBroker pointing back to the given Vault + auto const vaultID = tx[sfVaultID]; auto const sleVault = view.read(keylet::vault(vaultID)); auto const vaultPseudoID = sleVault->at(sfAccount); auto const sequence = tx.getSeqValue(); diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 40605c30fc..8d2024b965 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -20,26 +20,15 @@ #include // #include -#include -#include #include #include -#include #include -#include #include -#include #include -#include -#include -#include -#include #include #include -#include #include -#include #include #include #include @@ -231,28 +220,41 @@ LoanSet::preclaim(PreclaimContext const& ctx) // Should be impossible return tefBAD_LEDGER; // LCOV_EXCL_LINE Asset const asset = vault->at(sfAsset); + auto const vaultPseudo = vault->at(sfAccount); if (auto const ter = canAddHolding(ctx.view, asset)) return ter; - if (auto const ret = checkFrozen(ctx.view, brokerOwner, asset)) + // vaultPseudo is going to send funds, so it can't be frozen. + if (auto const ret = checkFrozen(ctx.view, vaultPseudo, asset)) { - JLOG(ctx.j.warn()) << "Broker owner account is frozen."; + JLOG(ctx.j.warn()) << "Vault pseudo-account is frozen."; return ret; } + // borrower is eventually going to have to pay back the loan, so it can't be + // frozen now. It is also going to receive funds, so it can't be deep + // frozen, but being frozen is a prerequisite for being deep frozen, so + // checking the one is sufficient. + if (auto const ret = checkFrozen(ctx.view, borrower, asset)) + { + JLOG(ctx.j.warn()) << "Borrower account is frozen."; + return ret; + } + // TODO: Remove when LoanDraw is combined with LoanSet + // brokerPseudo is eventually going to send funds to the borrower, so it + // can't be frozen now. It is also going to receive funds, so it can't be + // deep frozen, but being frozen is a prerequisite for being deep frozen, so + // checking the one is sufficient. if (auto const ret = checkFrozen(ctx.view, brokerPseudo, asset)) { JLOG(ctx.j.warn()) << "Broker pseudo-account account is frozen."; return ret; } - if (auto const ret = checkDeepFrozen(ctx.view, borrower, asset)) + // brokerOwner is going to receive funds if there's an origination fee, so + // it can't be deep frozen + if (auto const ret = checkDeepFrozen(ctx.view, brokerOwner, asset)) { - JLOG(ctx.j.warn()) << "Borrower account is deep frozen."; - return ret; - } - if (auto const ret = checkDeepFrozen(ctx.view, brokerPseudo, asset)) - { - JLOG(ctx.j.warn()) << "Broker pseudo-account account is deep frozen."; + JLOG(ctx.j.warn()) << "Broker owner account is frozen."; return ret; } @@ -264,7 +266,24 @@ LoanSet::preclaim(PreclaimContext const& ctx) << "Insufficient assets available in the Vault to fund the loan."; return tecINSUFFICIENT_FUNDS; } - auto const newDebtTotal = brokerSle->at(sfDebtTotal) + principalRequested; + + TenthBips32 const interestRate{tx[~sfInterestRate].value_or(0)}; + auto const paymentInterval = + tx[~sfPaymentInterval].value_or(defaultPaymentInterval); + auto const paymentTotal = tx[~sfPaymentTotal].value_or(defaultPaymentTotal); + TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; + + auto const totalInterest = loanInterestOutstandingMinusFee( + asset, + principalRequested, + principalRequested, + interestRate, + paymentInterval, + paymentTotal, + managementFeeRate); + + auto const newDebtTotal = + brokerSle->at(sfDebtTotal) + principalRequested + totalInterest; if (auto const debtMaximum = brokerSle->at(sfDebtMaximum); debtMaximum != 0 && debtMaximum < newDebtTotal) { From 5335c04616694c3d3ac233cced7aeec3d96e5532 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 5 Sep 2025 20:28:12 -0400 Subject: [PATCH 069/291] Fix constexpr expression --- include/xrpl/basics/base_uint.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/xrpl/basics/base_uint.h b/include/xrpl/basics/base_uint.h index ca83b8e220..5cd1c5aa26 100644 --- a/include/xrpl/basics/base_uint.h +++ b/include/xrpl/basics/base_uint.h @@ -637,7 +637,7 @@ inline std::string to_short_string(base_uint const& a) { // LCOV_EXCL_START - if constexpr (a.bytes <= 4) + if constexpr (base_uint::bytes <= 4) return to_string(a); else // LCOV_EXCL_STOP From 24f58a2e947047778a8cbdb0a5caef726195927d Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 5 Sep 2025 23:46:48 -0400 Subject: [PATCH 070/291] Fix test loop --- src/test/app/Loan_test.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index defad8b383..f389b66e9a 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -1041,7 +1041,7 @@ class Loan_test : public beast::unit_test::suite lender}) { // Freeze evan - deepfreeze(evan); + deepfreeze(account); // Try to create a loan with a deep frozen line env(set(evan, @@ -1054,7 +1054,7 @@ class Loan_test : public beast::unit_test::suite // Unfreeze evan BEAST_EXPECT(unfreeze); - unfreeze(evan); + unfreeze(account); // Ensure the line is unfrozen with a request that is fine // except too it requests more principal than the broker can From fa2c595a591f7c9d8268e878de7734f697d7f24f Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 8 Sep 2025 17:57:56 -0400 Subject: [PATCH 071/291] Add more value validation in LoanSet - Don't allow negative numbers. - Don't send the origination fee if it's defined, but 0. --- src/xrpld/app/tx/detail/LoanSet.cpp | 27 ++++++++++++++++--------- src/xrpld/app/tx/detail/Transactor.h | 30 ++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 8d2024b965..e928b6f851 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -85,6 +85,16 @@ LoanSet::preflight(PreflightContext const& ctx) if (auto const data = tx[~sfData]; data && !data->empty() && !validDataLength(tx[~sfData], maxDataPayloadLength)) return temINVALID; + for (auto const& field : + {sfLoanOriginationFee, + sfLoanServiceFee, + sfLatePaymentFee, + sfClosePaymentFee, + sfPrincipalRequested}) + { + if (!validNumericMinimum(tx[~field])) + return temINVALID; + } if (!validNumericRange(tx[~sfInterestRate], maxInterestRate)) return temINVALID; if (!validNumericRange(tx[~sfOverpaymentFee], maxOverpaymentFee)) @@ -98,17 +108,16 @@ LoanSet::preflight(PreflightContext const& ctx) return temINVALID; if (auto const paymentTotal = tx[~sfPaymentTotal]; - paymentTotal && *paymentTotal == 0) + paymentTotal && *paymentTotal <= 0) return temINVALID; - if (auto const paymentInterval = - tx[~sfPaymentInterval].value_or(LoanSet::defaultPaymentInterval); - paymentInterval < LoanSet::minPaymentInterval) + if (auto const paymentInterval = tx[~sfPaymentInterval]; + !validNumericMinimum(paymentInterval, LoanSet::minPaymentInterval)) return temINVALID; - else if (auto const gracePeriod = - tx[~sfGracePeriod].value_or(LoanSet::defaultGracePeriod); - gracePeriod > paymentInterval) + else if (!validNumericRange( + tx[~sfGracePeriod], + paymentInterval.value_or(LoanSet::defaultPaymentInterval))) return temINVALID; // Copied from preflight2 @@ -367,7 +376,7 @@ LoanSet::doApply() return ter; // 2. Transfer originationFee, if any, from vault pseudo-account to // LoanBroker owner. - if (originationFee) + if (originationFee && *originationFee) { // Create the holding if it doesn't already exist (necessary for MPTs). // The owner may have deleted their MPT / line at some point. @@ -377,7 +386,7 @@ LoanSet::doApply() brokerOwnerSle->at(sfBalance).value().xrp(), vaultAsset, j_); - ter != tesSUCCESS && ter != tecDUPLICATE) + !isTesSuccess(ter) && ter != tecDUPLICATE) // ignore tecDUPLICATE. That means the holding already exists, and // is fine here return ter; diff --git a/src/xrpld/app/tx/detail/Transactor.h b/src/xrpld/app/tx/detail/Transactor.h index 969a456164..fc01da5f63 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -312,6 +312,18 @@ protected: unit::ValueUnit max, unit::ValueUnit min = {}); + /// Minimum will usually be zero. + template + static bool + validNumericMinimum(std::optional value, T min = {}); + + /// Minimum will usually be zero. + template + static bool + validNumericMinimum( + std::optional value, + unit::ValueUnit min = {}); + private: std::pair reset(XRPAmount fee); @@ -429,6 +441,24 @@ Transactor::validNumericRange( return validNumericRange(value, max.value(), min.value()); } +template +bool +Transactor::validNumericMinimum(std::optional value, T min) +{ + if (!value) + return true; + return value >= min; +} + +template +bool +Transactor::validNumericMinimum( + std::optional value, + unit::ValueUnit min) +{ + return validNumericMinimum(value, min.value()); +} + } // namespace ripple #endif From 3f1a0f3a3dc3e0fbfda596665f62734c89254822 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 9 Sep 2025 15:25:51 -0400 Subject: [PATCH 072/291] Fix some build errors from yesterday --- src/xrpld/app/tx/detail/LoanSet.cpp | 14 +++++++------- src/xrpld/app/tx/detail/Transactor.h | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index e928b6f851..5a5054b1b0 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -86,13 +86,13 @@ LoanSet::preflight(PreflightContext const& ctx) !validDataLength(tx[~sfData], maxDataPayloadLength)) return temINVALID; for (auto const& field : - {sfLoanOriginationFee, - sfLoanServiceFee, - sfLatePaymentFee, - sfClosePaymentFee, - sfPrincipalRequested}) + {&sfLoanOriginationFee, + &sfLoanServiceFee, + &sfLatePaymentFee, + &sfClosePaymentFee, + &sfPrincipalRequested}) { - if (!validNumericMinimum(tx[~field])) + if (!validNumericMinimum(tx[~*field])) return temINVALID; } if (!validNumericRange(tx[~sfInterestRate], maxInterestRate)) @@ -376,7 +376,7 @@ LoanSet::doApply() return ter; // 2. Transfer originationFee, if any, from vault pseudo-account to // LoanBroker owner. - if (originationFee && *originationFee) + if (originationFee && (*originationFee != Number{})) { // Create the holding if it doesn't already exist (necessary for MPTs). // The owner may have deleted their MPT / line at some point. diff --git a/src/xrpld/app/tx/detail/Transactor.h b/src/xrpld/app/tx/detail/Transactor.h index fc01da5f63..8c13962998 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -310,19 +310,19 @@ protected: validNumericRange( std::optional value, unit::ValueUnit max, - unit::ValueUnit min = {}); + unit::ValueUnit min = unit::ValueUnit{}); /// Minimum will usually be zero. template static bool - validNumericMinimum(std::optional value, T min = {}); + validNumericMinimum(std::optional value, T min = T{}); /// Minimum will usually be zero. template static bool validNumericMinimum( std::optional value, - unit::ValueUnit min = {}); + unit::ValueUnit min = unit::ValueUnit{}); private: std::pair From e5941d5b23660da4a1c04cfff9e4e7c45f2334e4 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 9 Sep 2025 16:29:35 -0400 Subject: [PATCH 073/291] Simplify flag exclusion check in LoanManage --- src/xrpld/app/tx/detail/LoanManage.cpp | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index a23c18c269..82cb00917c 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -64,17 +64,8 @@ LoanManage::preflight(PreflightContext const& ctx) return temINVALID; // Flags are mutually exclusive - int numFlags = 0; - for (auto const flag : { - tfLoanDefault, - tfLoanImpair, - tfLoanUnimpair, - }) - { - if (ctx.tx.isFlag(flag)) - ++numFlags; - } - if (numFlags > 1) + auto const flags = ctx.tx[sfFlags] & tfUniversalMask; + if ((flags & (flags - 1)) != 0) { JLOG(ctx.j.warn()) << "LoanManage: Only one of tfLoanDefault, tfLoanImpair, or " From 1897ac5ee75086941928c7ae22c619b43e51bfeb Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 11 Jul 2025 17:06:26 -0400 Subject: [PATCH 074/291] Make a few tweaks to the changes in 43fe49e7 - This was the first merge after I got back from sabbatical. I made these same changes in "ximinez/lending-XLS-66" commit 9d052dc, but after seeing some test failures, I think they belong here. --- src/test/jtx/impl/Env.cpp | 4 +++- src/xrpld/app/tx/detail/Escrow.cpp | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index 9ba80aec42..ae99e1b5d6 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -218,7 +218,9 @@ Env::balance(Account const& account, MPTIssue const& mptIssue) const if (!sle) return {STAmount(mptIssue, 0), account.name()}; - STAmount const amount{mptIssue, sle->getFieldU64(sfOutstandingAmount)}; + // Make it negative + STAmount const amount{ + mptIssue, sle->getFieldU64(sfOutstandingAmount), 0, true}; return {amount, lookup(issuer).name()}; } else diff --git a/src/xrpld/app/tx/detail/Escrow.cpp b/src/xrpld/app/tx/detail/Escrow.cpp index dd0ffac778..827a7c5fbd 100644 --- a/src/xrpld/app/tx/detail/Escrow.cpp +++ b/src/xrpld/app/tx/detail/Escrow.cpp @@ -81,8 +81,8 @@ constexpr HashRouterFlags SF_CF_VALID = HashRouterFlags::PRIVATE6; TxConsequences EscrowCreate::makeTxConsequences(PreflightContext const& ctx) { - return TxConsequences{ - ctx.tx, isXRP(ctx.tx[sfAmount]) ? ctx.tx[sfAmount].xrp() : beast::zero}; + auto const amount = ctx.tx[sfAmount]; + return TxConsequences{ctx.tx, isXRP(amount) ? amount.xrp() : beast::zero}; } template From 7aaad4a8ef78bdcb04403738f6b8af07dce1bc7a Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 10 Sep 2025 17:00:05 -0400 Subject: [PATCH 075/291] Fix Vault tests broken by negative MPT issuer balance --- src/test/app/Vault_test.cpp | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 9e14d9e094..60ac5f44bd 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -3503,7 +3503,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(100, 0))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(1000, 0))); + STAmount(d.share, Number(-1000, 0))); { testcase("Scale redeem exact"); @@ -3528,7 +3528,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(90, 0))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(900, 0))); + STAmount(d.share, Number(-900, 0))); } { @@ -3563,7 +3563,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(900 - 25, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(900 - 25, 0))); + STAmount(d.share, -Number(900 - 25, 0))); } { @@ -3590,7 +3590,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(875 - 21, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(875 - 21, 0))); + STAmount(d.share, -Number(875 - 21, 0))); } { @@ -3651,7 +3651,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(100, 0))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(1000, 0))); + STAmount(d.share, Number(-1000, 0))); { testcase("Scale withdraw exact"); @@ -3679,7 +3679,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(90, 0))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(900, 0))); + STAmount(d.share, Number(-900, 0))); } { @@ -3726,7 +3726,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(900 - 25, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(900 - 25, 0))); + STAmount(d.share, -Number(900 - 25, 0))); } { @@ -3755,7 +3755,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(875 - 38, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(875 - 38, 0))); + STAmount(d.share, -Number(875 - 38, 0))); } { @@ -3784,7 +3784,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(837 - 37, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(837 - 37, 0))); + STAmount(d.share, -Number(837 - 37, 0))); } { @@ -3807,7 +3807,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(800 - 1, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(800 - 1, 0))); + STAmount(d.share, -Number(800 - 1, 0))); } { @@ -3870,7 +3870,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(100, 0))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(1000, 0))); + STAmount(d.share, -Number(1000, 0))); { testcase("Scale clawback exact"); // assetsToSharesWithdraw: @@ -3898,7 +3898,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(90, 0))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(900, 0))); + STAmount(d.share, -Number(900, 0))); } { @@ -3938,7 +3938,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(900 - 25, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(900 - 25, 0))); + STAmount(d.share, -Number(900 - 25, 0))); } { @@ -3968,7 +3968,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(875 - 38, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(875 - 38, 0))); + STAmount(d.share, -Number(875 - 38, 0))); } { @@ -3998,7 +3998,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(837 - 37, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(837 - 37, 0))); + STAmount(d.share, -Number(837 - 37, 0))); } { @@ -4022,7 +4022,7 @@ class Vault_test : public beast::unit_test::suite STAmount(d.asset, Number(800 - 1, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == - STAmount(d.share, Number(800 - 1, 0))); + STAmount(d.share, -Number(800 - 1, 0))); } { From 5ff6bc496afcd7f567f0a32e8531d6fa893673e0 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 15 Jul 2025 14:20:44 -0400 Subject: [PATCH 076/291] fixup! Make a few tweaks to the changes in 43fe49e7 --- src/test/app/EscrowToken_test.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/app/EscrowToken_test.cpp b/src/test/app/EscrowToken_test.cpp index e81064c825..223b86a5d1 100644 --- a/src/test/app/EscrowToken_test.cpp +++ b/src/test/app/EscrowToken_test.cpp @@ -3426,7 +3426,7 @@ struct EscrowToken_test : public beast::unit_test::suite auto const preAliceMPT = env.balance(alice, MPT); auto const preOutstanding = env.balance(gw, MPT); auto const preEscrowed = issuerMPTEscrowed(env, MPT); - BEAST_EXPECT(preOutstanding == MPT(10'000)); + BEAST_EXPECT(preOutstanding == MPT(-10'000)); BEAST_EXPECT(preEscrowed == 0); env(escrow::create(alice, gw, MPT(1'000)), @@ -3449,7 +3449,7 @@ struct EscrowToken_test : public beast::unit_test::suite BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000)); BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); - BEAST_EXPECT(env.balance(gw, MPT) == preOutstanding - MPT(1'000)); + BEAST_EXPECT(env.balance(gw, MPT) == preOutstanding + MPT(1'000)); BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == preEscrowed); } } @@ -3759,7 +3759,7 @@ struct EscrowToken_test : public beast::unit_test::suite BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 10); BEAST_EXPECT(env.balance(bob, MPT) == MPT(0)); BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); - BEAST_EXPECT(env.balance(gw, MPT) == MPT(10)); + BEAST_EXPECT(env.balance(gw, MPT) == MPT(-10)); mptGw.authorize({.account = bob, .flags = tfMPTUnauthorize}); mptGw.destroy( {.id = mptGw.issuanceID(), From 1b295c7c001d4148d9e62db3b96dd660f5a487ca Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 10 Sep 2025 20:33:01 -0400 Subject: [PATCH 077/291] Use Permission to check if a Transactor is enabled - Adds a Permission::getTxFeature lookup function to find the controlling amendment for a Transactor, if any. - Returns temDISABLED from preflight if there is an amendment and it's not enabled. - Still need to go through and remove all the now-redundant isEnabled functions. --- include/xrpl/protocol/Permissions.h | 3 +++ src/libxrpl/protocol/Permissions.cpp | 21 +++++++++++++++------ src/xrpld/app/tx/detail/Transactor.h | 9 +++++++++ 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/include/xrpl/protocol/Permissions.h b/include/xrpl/protocol/Permissions.h index cf49ff7382..924a5af0da 100644 --- a/include/xrpl/protocol/Permissions.h +++ b/include/xrpl/protocol/Permissions.h @@ -83,6 +83,9 @@ public: std::optional getGranularTxType(GranularPermissionType const& gpType) const; + std::optional> const + getTxFeature(TxType txType) const; + bool isDelegatable(std::uint32_t const& permissionValue, Rules const& rules) const; diff --git a/src/libxrpl/protocol/Permissions.cpp b/src/libxrpl/protocol/Permissions.cpp index 781799f128..af4b7e42c4 100644 --- a/src/libxrpl/protocol/Permissions.cpp +++ b/src/libxrpl/protocol/Permissions.cpp @@ -131,6 +131,19 @@ Permission::getGranularTxType(GranularPermissionType const& gpType) const return std::nullopt; } +std::optional> const +Permission::getTxFeature(TxType txType) const +{ + auto const txFeaturesIt = txFeatureMap_.find(txType); + XRPL_ASSERT( + txFeaturesIt != txFeatureMap_.end(), + "ripple::Permissions::isDelegatable : tx exists in txFeatureMap_"); + + if (txFeaturesIt->second == uint256{}) + return std::nullopt; + return txFeaturesIt->second; +} + bool Permission::isDelegatable( std::uint32_t const& permissionValue, @@ -150,16 +163,12 @@ Permission::isDelegatable( if (it == delegatableTx_.end()) return false; - auto const txFeaturesIt = txFeatureMap_.find(txType); - XRPL_ASSERT( - txFeaturesIt != txFeatureMap_.end(), - "ripple::Permissions::isDelegatable : tx exists in txFeatureMap_"); + auto const feature = getTxFeature(txType); // fixDelegateV1_1: Delegation is only allowed if the required amendment // for the transaction is enabled. For transactions that do not require // an amendment, delegation is always allowed. - if (txFeaturesIt->second != uint256{} && - !rules.enabled(txFeaturesIt->second)) + if (feature && !rules.enabled(*feature)) return false; } diff --git a/src/xrpld/app/tx/detail/Transactor.h b/src/xrpld/app/tx/detail/Transactor.h index 29a8df7574..9b5638d9e1 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -392,6 +392,15 @@ Transactor::invokePreflight(PreflightContext const& ctx) // TODO: If #5650 is merged, use its transaction -> amendment lookup here to // do a first-pass check. Rewrite or remove any `isEnabled` overloads that // check those default amendments. + + // Using this lookup does NOT require checking the fixDelegateV1_1. The data + // exists regardless of whether it is enabled. + auto const feature = + Permission::getInstance().getTxFeature(ctx.tx.getTxnType()); + + if (feature && !ctx.rules.enabled(*feature)) + return temDISABLED; + if (!T::isEnabled(ctx)) return temDISABLED; From 49eb1cc54edee2b78cabfa0d23148ac9d5f5d3d8 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 15 Sep 2025 20:30:02 -0400 Subject: [PATCH 078/291] Remove LoanDraw transaction (#5792) --- .../xrpl/protocol/detail/ledger_entries.macro | 4 + .../xrpl/protocol/detail/transactions.macro | 3 +- src/test/app/Batch_test.cpp | 36 +- src/test/app/EscrowToken_test.cpp | 10 +- src/test/app/Loan_test.cpp | 770 +++++++++++------- src/test/jtx/TestHelpers.h | 3 +- src/test/jtx/impl/TestHelpers.cpp | 4 +- src/xrpld/app/tx/detail/LoanDelete.cpp | 4 +- src/xrpld/app/tx/detail/LoanDraw.cpp | 4 +- src/xrpld/app/tx/detail/LoanDraw.h | 2 + src/xrpld/app/tx/detail/LoanManage.cpp | 11 + src/xrpld/app/tx/detail/LoanPay.cpp | 7 - src/xrpld/app/tx/detail/LoanSet.cpp | 38 +- 13 files changed, 536 insertions(+), 360 deletions(-) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 3d0a4e1b8b..4effb35381 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -558,7 +558,11 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({ {sfPreviousPaymentDate, soeREQUIRED}, {sfNextPaymentDueDate, soeREQUIRED}, {sfPaymentRemaining, soeREQUIRED}, +//#if LOANDRAW +// TODO: Remove this when you remove the rest of the LOANDRAW blocks. +// Directives don't work with macro expansion. {sfAssetsAvailable, soeDEFAULT}, +//#endif {sfPrincipalOutstanding, soeREQUIRED}, // Save the original request amount for rounding / scaling of // other computations, particularly for IOUs diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 6a40508adb..c78133038b 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -1031,7 +1031,6 @@ TRANSACTION(ttLOAN_SET, 80, LoanSet, {sfCloseInterestRate, soeOPTIONAL}, {sfOverpaymentInterestRate, soeOPTIONAL}, {sfPrincipalRequested, soeREQUIRED}, - {sfStartDate, soeREQUIRED}, {sfPaymentTotal, soeOPTIONAL}, {sfPaymentInterval, soeOPTIONAL}, {sfGracePeriod, soeOPTIONAL}, @@ -1059,6 +1058,7 @@ TRANSACTION(ttLOAN_MANAGE, 82, LoanManage, {sfLoanID, soeREQUIRED}, })) +#if LOANDRAW /** The Borrower uses this transaction to draws funds from the Loan. */ #if TRANSACTION_INCLUDE # include @@ -1070,6 +1070,7 @@ TRANSACTION(ttLOAN_DRAW, 83, LoanDraw, {sfLoanID, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, })) +#endif /** The Borrower uses this transaction to make a Payment on the Loan. */ #if TRANSACTION_INCLUDE diff --git a/src/test/app/Batch_test.cpp b/src/test/app/Batch_test.cpp index fc5e58635d..94d044dc1e 100644 --- a/src/test/app/Batch_test.cpp +++ b/src/test/app/Batch_test.cpp @@ -2631,10 +2631,7 @@ class Batch_test : public beast::unit_test::suite batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing), batch::inner( env.json( - set(lender, - brokerKeylet.key, - asset(1000).value(), - env.now() + 3600s), + set(lender, brokerKeylet.key, asset(1000).value()), // Not allowed to include the counterparty signature sig(sfCounterpartySignature, borrower), sig(none), @@ -2642,8 +2639,7 @@ class Batch_test : public beast::unit_test::suite seq(none)), lenderSeq + 1), batch::inner( - draw( - lender, + pay(lender, loanKeylet.key, STAmount{asset, asset(500).value()}), lenderSeq + 2)); @@ -2655,18 +2651,14 @@ class Batch_test : public beast::unit_test::suite batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing), batch::inner( env.json( - set(lender, - brokerKeylet.key, - asset(1000).value(), - env.now() + 3600s), + set(lender, brokerKeylet.key, asset(1000).value()), // Counterparty must be set sig(none), fee(none), seq(none)), lenderSeq + 1), batch::inner( - draw( - lender, + pay(lender, loanKeylet.key, STAmount{asset, asset(500).value()}), lenderSeq + 2)); @@ -2678,10 +2670,7 @@ class Batch_test : public beast::unit_test::suite batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing), batch::inner( env.json( - set(lender, - brokerKeylet.key, - asset(1000).value(), - env.now() + 3600s), + set(lender, brokerKeylet.key, asset(1000).value()), // Counterparty must sign the outer transaction counterparty(borrower.id()), sig(none), @@ -2689,8 +2678,7 @@ class Batch_test : public beast::unit_test::suite seq(none)), lenderSeq + 1), batch::inner( - draw( - lender, + pay(lender, loanKeylet.key, STAmount{asset, asset(500).value()}), lenderSeq + 2)); @@ -2706,17 +2694,14 @@ class Batch_test : public beast::unit_test::suite batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing), batch::inner( env.json( - set(lender, - brokerKeylet.key, - asset(1000).value(), - env.now() + 3600s), + set(lender, brokerKeylet.key, asset(1000).value()), counterparty(borrower.id()), sig(none), fee(none), seq(none)), lenderSeq + 1), batch::inner( - draw( + pay( // However, this inner transaction will fail, // because the lender is not allowed to draw the // transaction @@ -2741,10 +2726,7 @@ class Batch_test : public beast::unit_test::suite batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing), batch::inner( env.json( - set(lender, - brokerKeylet.key, - asset(1000).value(), - env.now() + 3600s), + set(lender, brokerKeylet.key, asset(1000).value()), counterparty(borrower.id()), sig(none), fee(none), diff --git a/src/test/app/EscrowToken_test.cpp b/src/test/app/EscrowToken_test.cpp index f6831e378e..e930c2f9ac 100644 --- a/src/test/app/EscrowToken_test.cpp +++ b/src/test/app/EscrowToken_test.cpp @@ -3503,7 +3503,7 @@ struct EscrowToken_test : public beast::unit_test::suite BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 125); BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 125); - BEAST_EXPECT(env.balance(gw, MPT) == MPT(20'000)); + BEAST_EXPECT(env.balance(gw, MPT) == MPT(-20'000)); // bob can finish escrow env(escrow::finish(bob, alice, seq1), @@ -3522,7 +3522,7 @@ struct EscrowToken_test : public beast::unit_test::suite : MPT(20'000); BEAST_EXPECT(mptEscrowed(env, alice, MPT) == escrowedWithFix); BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == escrowedWithFix); - BEAST_EXPECT(env.balance(gw, MPT) == outstandingWithFix); + BEAST_EXPECT(env.balance(gw, MPT) == -outstandingWithFix); } // test locked rate: cancel @@ -3567,7 +3567,7 @@ struct EscrowToken_test : public beast::unit_test::suite BEAST_EXPECT(env.balance(alice, MPT) == preAlice); BEAST_EXPECT(env.balance(bob, MPT) == preBob); - BEAST_EXPECT(env.balance(gw, MPT) == MPT(20'000)); + BEAST_EXPECT(env.balance(gw, MPT) == MPT(-20'000)); BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); } @@ -3608,7 +3608,7 @@ struct EscrowToken_test : public beast::unit_test::suite BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 125); BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 125); - BEAST_EXPECT(env.balance(gw, MPT) == MPT(20'000)); + BEAST_EXPECT(env.balance(gw, MPT) == MPT(-20'000)); // bob can finish escrow env(escrow::finish(gw, alice, seq1), @@ -3620,7 +3620,7 @@ struct EscrowToken_test : public beast::unit_test::suite BEAST_EXPECT(env.balance(alice, MPT) == preAlice - delta); BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); - BEAST_EXPECT(env.balance(gw, MPT) == MPT(19'875)); + BEAST_EXPECT(env.balance(gw, MPT) == MPT(-19'875)); } } diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index f389b66e9a..c2492bc75b 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -34,6 +34,7 @@ #include #include +#include #include #include @@ -63,8 +64,10 @@ class Loan_test : public beast::unit_test::suite static constexpr auto const coverDepositParameter = 1000; static constexpr auto const coverRateMinParameter = percentageToTenthBips(10); + static constexpr auto const coverRateLiquidationParameter = + percentageToTenthBips(25); static constexpr auto const maxCoveredLoanValue = 1000 * 100 / 10; - static constexpr auto const vaultDeposit = 50'000; + static constexpr auto const vaultDeposit = 1'000'000; static constexpr auto const debtMaximumParameter = 25'000; std::string const iouCurrency{"IOU"}; @@ -91,9 +94,8 @@ class Loan_test : public beast::unit_test::suite // counter party signature is optional on LoanSet. Confirm that by // sending transaction without one. - auto setTx = env.jt( - set(alice, keylet.key, Number(10000), env.now() + 720h), - ter(temDISABLED)); + auto setTx = + env.jt(set(alice, keylet.key, Number(10000)), ter(temDISABLED)); env(setTx); // All loan transactions are disabled. @@ -109,8 +111,10 @@ class Loan_test : public beast::unit_test::suite env(del(alice, loanKeylet.key), ter(temDISABLED)); // 3. LoanManage env(manage(alice, loanKeylet.key, tfLoanImpair), ter(temDISABLED)); +#if LOANDRAW && 0 // 4. LoanDraw env(draw(alice, loanKeylet.key, XRP(500)), ter(temDISABLED)); +#endif // 5. LoanPay env(pay(alice, loanKeylet.key, XRP(500)), ter(temDISABLED)); }; @@ -136,11 +140,11 @@ class Loan_test : public beast::unit_test::suite NetClock::time_point startDate = {}; std::uint32_t nextPaymentDate = 0; std::uint32_t paymentRemaining = 0; - Number assetsAvailable = 0; - Number const principalRequested; + Number const principalRequested = 0; Number principalOutstanding = 0; std::uint32_t flags = 0; std::uint32_t paymentInterval = 0; + TenthBips32 const interestRate{}; }; struct VerifyLoanStatus @@ -168,7 +172,6 @@ class Loan_test : public beast::unit_test::suite void checkBroker( - Number const& assetsAvailable, Number const& principalRequested, Number const& principalOutstanding, TenthBips32 interestRate, @@ -200,7 +203,7 @@ class Loan_test : public beast::unit_test::suite Number(1, -8)))); env.test.BEAST_EXPECT( env.balance(pseudoAccount, broker.asset).number() == - brokerSle->at(sfCoverAvailable) + assetsAvailable); + brokerSle->at(sfCoverAvailable)); env.test.BEAST_EXPECT( brokerSle->at(sfOwnerCount) == ownerCount); @@ -239,7 +242,6 @@ class Loan_test : public beast::unit_test::suite std::uint32_t ownerCount) const { checkBroker( - state.assetsAvailable, state.principalRequested, state.principalOutstanding, interestRate, @@ -253,7 +255,6 @@ class Loan_test : public beast::unit_test::suite std::uint32_t previousPaymentDate, std::uint32_t nextPaymentDate, std::uint32_t paymentRemaining, - Number const& assetsAvailable, Number const& principalRequested, Number const& principalOutstanding, std::uint32_t flags) const @@ -267,8 +268,9 @@ class Loan_test : public beast::unit_test::suite loan->at(sfNextPaymentDueDate) == nextPaymentDate); env.test.BEAST_EXPECT( loan->at(sfPaymentRemaining) == paymentRemaining); - env.test.BEAST_EXPECT( - loan->at(sfAssetsAvailable) == assetsAvailable); +#if LOANDRAW + env.test.BEAST_EXPECT(loan->at(sfAssetsAvailable) == 0); +#endif env.test.BEAST_EXPECT( loan->at(sfPrincipalRequested) == principalRequested); env.test.BEAST_EXPECT( @@ -281,7 +283,6 @@ class Loan_test : public beast::unit_test::suite auto const interestRate = TenthBips32{loan->at(sfInterestRate)}; auto const paymentInterval = loan->at(sfPaymentInterval); checkBroker( - assetsAvailable, principalRequested, principalOutstanding, interestRate, @@ -331,7 +332,6 @@ class Loan_test : public beast::unit_test::suite state.previousPaymentDate, state.nextPaymentDate, state.paymentRemaining, - state.assetsAvailable, state.principalRequested, state.principalOutstanding, state.flags); @@ -379,7 +379,7 @@ class Loan_test : public beast::unit_test::suite managementFeeRate(TenthBips16(100)), debtMaximum(debtMaximumValue), coverRateMinimum(TenthBips32(coverRateMinParameter)), - coverRateLiquidation(TenthBips32(percentageToTenthBips(25)))); + coverRateLiquidation(TenthBips32(coverRateLiquidationParameter))); env(coverDeposit(lender, keylet.key, coverDepositValue)); @@ -388,6 +388,85 @@ class Loan_test : public beast::unit_test::suite return {asset, keylet.key}; } + LoanState + getCurrentState( + jtx::Env const& env, + BrokerInfo const& broker, + Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus) + { + using namespace std::chrono_literals; + using d = NetClock::duration; + using tp = NetClock::time_point; + // Lookup the current loan state + if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan)) + { + LoanState state{ + .previousPaymentDate = loan->at(sfPreviousPaymentDate), + .startDate = tp{d{loan->at(sfStartDate)}}, + .nextPaymentDate = loan->at(sfNextPaymentDueDate), + .paymentRemaining = loan->at(sfPaymentRemaining), + .principalRequested = loan->at(sfPrincipalRequested), + .principalOutstanding = loan->at(sfPrincipalOutstanding), + .flags = loan->at(sfFlags), + .paymentInterval = loan->at(sfPaymentInterval), + .interestRate = TenthBips32{loan->at(sfInterestRate)}, + }; + BEAST_EXPECT(state.previousPaymentDate == 0); + BEAST_EXPECT( + tp{d{state.nextPaymentDate}} == state.startDate + 600s); + BEAST_EXPECT(state.paymentRemaining == 12); + BEAST_EXPECT( + state.principalOutstanding == broker.asset(1000).value()); + BEAST_EXPECT( + state.principalOutstanding == state.principalRequested); + BEAST_EXPECT(state.paymentInterval == 600); + + verifyLoanStatus(state); + + return state; + } + + return LoanState{}; + } + + bool + canImpairLoan( + jtx::Env const& env, + BrokerInfo const& broker, + LoanState const& state) + { + if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); + BEAST_EXPECT(brokerSle)) + { + if (auto const vaultSle = + env.le(keylet::vault(brokerSle->at(sfVaultID))); + BEAST_EXPECT(vaultSle)) + { + // log << vaultSle->getJson() << std::endl; + auto const assetsUnavailable = vaultSle->at(sfAssetsTotal) - + vaultSle->at(sfAssetsAvailable); + auto const interestOutstanding = + loanInterestOutstandingMinusFee( + broker.asset, + state.principalRequested, + state.principalOutstanding, + state.interestRate, + state.paymentInterval, + state.paymentRemaining, + TenthBips32{brokerSle->at(sfManagementFeeRate)}); + auto const unrealizedLoss = vaultSle->at(sfLossUnrealized) + + state.principalOutstanding + interestOutstanding; + + if (unrealizedLoss > assetsUnavailable) + { + return false; + } + } + } + return true; + } + void lifecycle( std::string const& caseLabel, @@ -429,7 +508,7 @@ class Loan_test : public beast::unit_test::suite // No loans yet verifyLoanStatus.checkBroker( - 0, broker.asset(loanAmount).value(), 0, TenthBips32{0}, 1, 0, 0); + broker.asset(loanAmount).value(), 0, TenthBips32{0}, 1, 0, 0); if (!BEAST_EXPECT(loanSequence != 0)) return; @@ -444,7 +523,6 @@ class Loan_test : public beast::unit_test::suite auto const loanSetFee = fee(env.current()->fees().base * 2); Number const principalRequest = broker.asset(loanAmount).value(); - auto const startDate = env.now() + 3600s; auto const originationFee = broker.asset(1).value(); auto const serviceFee = broker.asset(2).value(); auto const lateFee = broker.asset(3).value(); @@ -482,9 +560,11 @@ class Loan_test : public beast::unit_test::suite auto const interval = 600; auto const grace = 60; + auto const borrowerStartbalance = env.balance(borrower, broker.asset); + // Use the defined values auto createJtx = env.jt( - set(borrower, broker.brokerID, principalRequest, startDate, flags), + set(borrower, broker.brokerID, principalRequest, flags), sig(sfCounterpartySignature, lender), loanOriginationFee(originationFee), loanServiceFee(serviceFee), @@ -504,12 +584,29 @@ class Loan_test : public beast::unit_test::suite env.close(); + auto const startDate = + env.current()->info().parentCloseTime.time_since_epoch().count(); + if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); BEAST_EXPECT(brokerSle)) { BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 1); } + { + // Need to account for fees if the loan is in XRP + PrettyAmount adjustment = broker.asset(0); + if (broker.asset.raw().native()) + { + adjustment = 2 * env.current()->fees().base; + } + + BEAST_EXPECT( + env.balance(borrower, broker.asset).value() == + borrowerStartbalance.value() + principalRequest - + originationFee - adjustment.value()); + } + auto const loanFlags = createJtx.stx->isFlag(tfLoanOverpayment) ? lsfLoanOverpayment : LedgerSpecificFlags(0); @@ -534,27 +631,26 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT(loan->at(sfCloseInterestRate) == closeInterest); BEAST_EXPECT( loan->at(sfOverpaymentInterestRate) == overpaymentInterest); - BEAST_EXPECT( - loan->at(sfStartDate) == startDate.time_since_epoch().count()); + BEAST_EXPECT(loan->at(sfStartDate) == startDate); BEAST_EXPECT(loan->at(sfPaymentInterval) == interval); BEAST_EXPECT(loan->at(sfGracePeriod) == grace); BEAST_EXPECT(loan->at(sfPreviousPaymentDate) == 0); BEAST_EXPECT( - loan->at(sfNextPaymentDueDate) == - startDate.time_since_epoch().count() + interval); + loan->at(sfNextPaymentDueDate) == startDate + interval); BEAST_EXPECT(loan->at(sfPaymentRemaining) == total); - BEAST_EXPECT( - loan->at(sfAssetsAvailable) == - principalRequest - originationFee); +#if LOANDRAW + BEAST_EXPECT(loan->at(sfAssetsAvailable) == beast::zero); +#endif BEAST_EXPECT(loan->at(sfPrincipalRequested) == principalRequest); BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == principalRequest); } + auto state = getCurrentState(env, broker, keylet, verifyLoanStatus); + verifyLoanStatus( 0, - startDate.time_since_epoch().count() + interval, + startDate + interval, total, - principalRequest - originationFee, principalRequest, principalRequest, loanFlags | 0); @@ -586,13 +682,16 @@ class Loan_test : public beast::unit_test::suite // due env(manage(lender, keylet.key, tfLoanDefault), ter(tecTOO_SOON)); - // Impair the loan - env(manage(lender, keylet.key, tfLoanImpair)); + // Check the vault + bool const canImpair = canImpairLoan(env, broker, state); + // Impair the loan, if possible + env(manage(lender, keylet.key, tfLoanImpair), + canImpair ? ter(tesSUCCESS) : ter(tecLIMIT_EXCEEDED)); // Unimpair the loan - env(manage(lender, keylet.key, tfLoanUnimpair)); + env(manage(lender, keylet.key, tfLoanUnimpair), + canImpair ? ter(tesSUCCESS) : ter(tecNO_PERMISSION)); - auto const nextDueDate = - startDate.time_since_epoch().count() + interval; + auto const nextDueDate = startDate + interval; env.close(); @@ -600,7 +699,6 @@ class Loan_test : public beast::unit_test::suite 0, nextDueDate, total, - principalRequest - originationFee, principalRequest, principalRequest, loanFlags | 0); @@ -613,15 +711,11 @@ class Loan_test : public beast::unit_test::suite env.close(); // Verify the loan is at EOL - auto const assetsAvailable = [&, &keylet = keylet]() { - if (auto loan = env.le(keylet); BEAST_EXPECT(loan)) - { - BEAST_EXPECT(loan->at(sfPaymentRemaining) == 0); - BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == 0); - return loan->at(sfAssetsAvailable); - } - return Number(0); - }(); + if (auto loan = env.le(keylet); BEAST_EXPECT(loan)) + { + BEAST_EXPECT(loan->at(sfPaymentRemaining) == 0); + BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == 0); + } auto const borrowerStartingBalance = env.balance(borrower, broker.asset); @@ -637,16 +731,30 @@ class Loan_test : public beast::unit_test::suite env(del(lender, broker.brokerID), ter(tecNO_ENTRY)); // Delete the loan - env(del(lender, keylet.key)); + // Either the borrower or the lender can delete the loan. Alternate + // between who does it across tests. + static unsigned deleteCounter = 0; + auto const deleter = ++deleteCounter % 2 ? lender : borrower; + env(del(deleter, keylet.key)); env.close(); + PrettyAmount adjustment = broker.asset(0); + if (deleter == borrower) + { + // Need to account for fees if the loan is in XRP + if (broker.asset.raw().native()) + { + adjustment = env.current()->fees().base; + } + } + // No loans left verifyLoanStatus.checkBroker( - 0, broker.asset(1000).value(), 0, interest, 1, 0, 0); + broker.asset(1000).value(), 0, interest, 1, 0, 0); BEAST_EXPECT( env.balance(borrower, broker.asset).value() == - borrowerStartingBalance.value() + assetsAvailable); + borrowerStartingBalance.value() - adjustment); BEAST_EXPECT(env.ownerCount(borrower) == borrowerOwnerCount); if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); @@ -704,7 +812,6 @@ class Loan_test : public beast::unit_test::suite Number const debtMaximumRequest = broker.asset(debtMaximumParameter).value(); - auto const startDate = env.now() + 3600s; auto const loanSetFee = fee(env.current()->fees().base * 2); auto const pseudoAcct = [&]() { @@ -718,24 +825,20 @@ class Loan_test : public beast::unit_test::suite auto badKeylet = keylet::vault(lender.id(), env.seq(lender)); // Try some failure cases // flags are checked first - env(set(evan, - broker.brokerID, - principalRequest, - startDate, - tfLoanSetMask), + env(set(evan, broker.brokerID, principalRequest, tfLoanSetMask), sig(sfCounterpartySignature, lender), loanSetFee, ter(temINVALID_FLAG)); // field length validation // sfData: good length, bad account - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, borrower), data(std::string(maxDataPayloadLength, 'X')), loanSetFee, ter(tefBAD_AUTH)); // sfData: too long - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, lender), data(std::string(maxDataPayloadLength + 1, 'Y')), loanSetFee, @@ -743,105 +846,105 @@ class Loan_test : public beast::unit_test::suite // field range validation // sfOverpaymentFee: good value, bad account - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, borrower), overpaymentFee(maxOverpaymentFee), loanSetFee, ter(tefBAD_AUTH)); // sfOverpaymentFee: too big - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, lender), overpaymentFee(maxOverpaymentFee + 1), loanSetFee, ter(temINVALID)); // sfInterestRate: good value, bad account - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, borrower), interestRate(maxInterestRate), loanSetFee, ter(tefBAD_AUTH)); // sfInterestRate: too big - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, lender), interestRate(maxInterestRate + 1), loanSetFee, ter(temINVALID)); // sfLateInterestRate: good value, bad account - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, borrower), lateInterestRate(maxLateInterestRate), loanSetFee, ter(tefBAD_AUTH)); // sfLateInterestRate: too big - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, lender), lateInterestRate(maxLateInterestRate + 1), loanSetFee, ter(temINVALID)); // sfCloseInterestRate: good value, bad account - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, borrower), closeInterestRate(maxCloseInterestRate), loanSetFee, ter(tefBAD_AUTH)); // sfCloseInterestRate: too big - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, lender), closeInterestRate(maxCloseInterestRate + 1), loanSetFee, ter(temINVALID)); // sfOverpaymentInterestRate: good value, bad account - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, borrower), overpaymentInterestRate(maxOverpaymentInterestRate), loanSetFee, ter(tefBAD_AUTH)); // sfOverpaymentInterestRate: too big - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, lender), overpaymentInterestRate(maxOverpaymentInterestRate + 1), loanSetFee, ter(temINVALID)); // sfPaymentTotal: good value, bad account - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, borrower), paymentTotal(LoanSet::minPaymentTotal), loanSetFee, ter(tefBAD_AUTH)); // sfPaymentTotal: too small (there is no max) - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, lender), paymentTotal(LoanSet::minPaymentTotal - 1), loanSetFee, ter(temINVALID)); // sfPaymentInterval: good value, bad account - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, borrower), paymentInterval(LoanSet::minPaymentInterval), loanSetFee, ter(tefBAD_AUTH)); // sfPaymentInterval: too small (there is no max) - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, lender), paymentInterval(LoanSet::minPaymentInterval - 1), loanSetFee, ter(temINVALID)); // sfGracePeriod: good value, bad account - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, borrower), paymentInterval(LoanSet::minPaymentInterval * 2), gracePeriod(LoanSet::minPaymentInterval * 2), loanSetFee, ter(tefBAD_AUTH)); // sfGracePeriod: larger than paymentInterval - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, lender), paymentInterval(LoanSet::minPaymentInterval * 2), gracePeriod(LoanSet::minPaymentInterval * 3), @@ -849,18 +952,18 @@ class Loan_test : public beast::unit_test::suite ter(temINVALID)); // insufficient fee - single sign - env(set(borrower, broker.brokerID, principalRequest, startDate), + env(set(borrower, broker.brokerID, principalRequest), sig(sfCounterpartySignature, lender), ter(telINSUF_FEE_P)); // insufficient fee - multisign - env(set(borrower, broker.brokerID, principalRequest, startDate), + env(set(borrower, broker.brokerID, principalRequest), counterparty(lender), msig(evan, lender), msig(sfCounterpartySignature, evan, borrower), fee(env.current()->fees().base * 5 - 1), ter(telINSUF_FEE_P)); // multisign sufficient fee, but no signers set up - env(set(borrower, broker.brokerID, principalRequest, startDate), + env(set(borrower, broker.brokerID, principalRequest), counterparty(lender), msig(evan, lender), msig(sfCounterpartySignature, evan, borrower), @@ -868,51 +971,43 @@ class Loan_test : public beast::unit_test::suite ter(tefNOT_MULTI_SIGNING)); // not the broker owner, no counterparty, not signed by broker // owner - env(set(borrower, broker.brokerID, principalRequest, startDate), + env(set(borrower, broker.brokerID, principalRequest), sig(sfCounterpartySignature, evan), loanSetFee, ter(tefBAD_AUTH)); - // bad start date - in the past - env(set(evan, - broker.brokerID, - principalRequest, - env.closed()->info().closeTime - 1s), - sig(sfCounterpartySignature, lender), - loanSetFee, - ter(tecEXPIRED)); // not the broker owner, counterparty is borrower - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), counterparty(borrower), sig(sfCounterpartySignature, borrower), loanSetFee, ter(tecNO_PERMISSION)); // not a LoanBroker object, no counterparty - env(set(lender, badKeylet.key, principalRequest, startDate), + env(set(lender, badKeylet.key, principalRequest), sig(sfCounterpartySignature, evan), loanSetFee, ter(temBAD_SIGNER)); // not a LoanBroker object, counterparty is valid - env(set(lender, badKeylet.key, principalRequest, startDate), + env(set(lender, badKeylet.key, principalRequest), counterparty(borrower), sig(sfCounterpartySignature, borrower), loanSetFee, ter(tecNO_ENTRY)); // borrower doesn't exist - env(set(lender, broker.brokerID, principalRequest, startDate), + env(set(lender, broker.brokerID, principalRequest), counterparty(alice), sig(sfCounterpartySignature, alice), loanSetFee, ter(terNO_ACCOUNT)); // Request more funds than the vault has available - env(set(evan, broker.brokerID, totalVaultRequest + 1, startDate), + env(set(evan, broker.brokerID, totalVaultRequest + 1), sig(sfCounterpartySignature, lender), loanSetFee, ter(tecINSUFFICIENT_FUNDS)); // Request more funds than the broker's first-loss capital can // cover. - env(set(evan, broker.brokerID, maxCoveredLoanRequest + 1, startDate), + env(set(evan, broker.brokerID, maxCoveredLoanRequest + 1), sig(sfCounterpartySignature, lender), loanSetFee, ter(tecINSUFFICIENT_FUNDS)); @@ -988,18 +1083,15 @@ class Loan_test : public beast::unit_test::suite }(); // Try freezing the accounts that can't be frozen - for (auto const& account : {vaultPseudo, evan, pseudoAcct}) + if (freeze) { - if (freeze) + for (auto const& account : {vaultPseudo, evan}) { // Freeze the account freeze(account); // Try to create a loan with a frozen line - env(set(evan, - broker.brokerID, - debtMaximumRequest, - startDate), + env(set(evan, broker.brokerID, debtMaximumRequest), sig(sfCounterpartySignature, lender), loanSetFee, ter(expectedResult)); @@ -1007,18 +1099,15 @@ class Loan_test : public beast::unit_test::suite // Unfreeze the account BEAST_EXPECT(unfreeze); unfreeze(account); - } - // Ensure the line is unfrozen with a request that is fine - // except too it requests more principal than the broker can - // carry - env(set(evan, - broker.brokerID, - debtMaximumRequest + 1, - startDate), - sig(sfCounterpartySignature, lender), - loanSetFee, - ter(tecLIMIT_EXCEEDED)); + // Ensure the line is unfrozen with a request that is fine + // except too it requests more principal than the broker can + // carry + env(set(evan, broker.brokerID, debtMaximumRequest + 1), + sig(sfCounterpartySignature, lender), + loanSetFee, + ter(tecLIMIT_EXCEEDED)); + } } // Deep freeze the borrower, which prevents them from receiving @@ -1027,7 +1116,7 @@ class Loan_test : public beast::unit_test::suite { // Make sure evan has a trust line that so the issuer can // freeze it. (Don't need to do this for the borrower, - // because LoanDraw will create a line to the borrower + // because LoanSet will create a line to the borrower // automatically.) env(trust(evan, issuer[iouCurrency](100'000))); @@ -1036,7 +1125,6 @@ class Loan_test : public beast::unit_test::suite // implies vaultPseudo, evan, - pseudoAcct, // these accounts can't be deep frozen lender}) { @@ -1044,10 +1132,7 @@ class Loan_test : public beast::unit_test::suite deepfreeze(account); // Try to create a loan with a deep frozen line - env(set(evan, - broker.brokerID, - debtMaximumRequest, - startDate), + env(set(evan, broker.brokerID, debtMaximumRequest), sig(sfCounterpartySignature, lender), loanSetFee, ter(expectedResult)); @@ -1059,10 +1144,7 @@ class Loan_test : public beast::unit_test::suite // Ensure the line is unfrozen with a request that is fine // except too it requests more principal than the broker can // carry - env(set(evan, - broker.brokerID, - debtMaximumRequest + 1, - startDate), + env(set(evan, broker.brokerID, debtMaximumRequest + 1), sig(sfCounterpartySignature, lender), loanSetFee, ter(tecLIMIT_EXCEEDED)); @@ -1073,54 +1155,66 @@ class Loan_test : public beast::unit_test::suite // Finally! Create a loan std::string testData; - auto currentState = [&](Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus) { - // Lookup the current loan state - if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan)) - { - LoanState state{ - .previousPaymentDate = loan->at(sfPreviousPaymentDate), - .startDate = tp{d{loan->at(sfStartDate)}}, - .nextPaymentDate = loan->at(sfNextPaymentDueDate), - .paymentRemaining = loan->at(sfPaymentRemaining), - .assetsAvailable = loan->at(sfAssetsAvailable), - .principalRequested = loan->at(sfPrincipalRequested), - .principalOutstanding = loan->at(sfPrincipalOutstanding), - .flags = loan->at(sfFlags), - .paymentInterval = loan->at(sfPaymentInterval), - }; - BEAST_EXPECT(state.previousPaymentDate == 0); - BEAST_EXPECT( - tp{d{state.nextPaymentDate}} == state.startDate + 600s); - BEAST_EXPECT(state.paymentRemaining == 12); - BEAST_EXPECT( - state.assetsAvailable == broker.asset(999).value()); - BEAST_EXPECT( - state.principalOutstanding == broker.asset(1000).value()); - BEAST_EXPECT( - state.principalOutstanding == state.principalRequested); - BEAST_EXPECT(state.paymentInterval == 600); - - verifyLoanStatus(state); - - return state; - } - - return LoanState{ - .previousPaymentDate = 0, - .startDate = tp{d{0}}, - .nextPaymentDate = 0, - .paymentRemaining = 0, - .assetsAvailable = 0, - .principalRequested = 0, - .principalOutstanding = 0, - .flags = 0, - .paymentInterval = 0, + auto coverAvailable = + [&env, this](uint256 const& brokerID, Number const& expected) { + if (auto const brokerSle = env.le(keylet::loanbroker(brokerID)); + BEAST_EXPECT(brokerSle)) + { + auto const available = brokerSle->at(sfCoverAvailable); + BEAST_EXPECT(available == expected); + return available; + } + return Number{}; }; + auto getDefaultInfo = [&env, this]( + LoanState const& state, + BrokerInfo const& broker) { + if (auto const brokerSle = + env.le(keylet::loanbroker(broker.brokerID)); + BEAST_EXPECT(brokerSle)) + { + BEAST_EXPECT( + state.principalRequested == state.principalOutstanding); + auto const interestOutstanding = + loanInterestOutstandingMinusFee( + broker.asset, + state.principalRequested, + state.principalOutstanding, + state.interestRate, + state.paymentInterval, + state.paymentRemaining, + TenthBips32{brokerSle->at(sfManagementFeeRate)}); + auto const defaultAmount = roundToAsset( + broker.asset, + std::min( + tenthBipsOfValue( + tenthBipsOfValue( + brokerSle->at(sfDebtTotal), + coverRateMinParameter), + coverRateLiquidationParameter), + state.principalOutstanding + interestOutstanding), + state.principalRequested); + return std::make_pair(defaultAmount, brokerSle->at(sfOwner)); + } + return std::make_pair(Number{}, AccountID{}); + }; + auto replenishCover = [&env, &coverAvailable]( + BrokerInfo const& broker, + AccountID const& brokerAcct, + Number const& startingCoverAvailable, + Number const& amountToBeCovered) { + coverAvailable( + broker.brokerID, startingCoverAvailable - amountToBeCovered); + env(loanBroker::coverDeposit( + brokerAcct, + broker.brokerID, + STAmount{broker.asset, amountToBeCovered})); + coverAvailable(broker.brokerID, startingCoverAvailable); + env.close(); }; - auto defaultBeforeStartDate = [&](std::uint32_t baseFlag, - bool impair = true) { + auto defaultImmediately = [&](std::uint32_t baseFlag, + bool impair = true) { return [&, impair, baseFlag]( Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { @@ -1129,22 +1223,34 @@ class Loan_test : public beast::unit_test::suite // Default the loan // Initialize values with the current state - auto state = currentState(loanKeylet, verifyLoanStatus); + auto state = + getCurrentState(env, broker, loanKeylet, verifyLoanStatus); BEAST_EXPECT(state.flags == baseFlag); + auto const& broker = verifyLoanStatus.broker; + auto const startingCoverAvailable = coverAvailable( + broker.brokerID, + broker.asset(coverDepositParameter).number()); + if (impair) { - // Impair the loan - env(manage(lender, loanKeylet.key, tfLoanImpair)); - - state.flags |= tfLoanImpair; - state.nextPaymentDate = - env.now().time_since_epoch().count(); - verifyLoanStatus(state); - - // Once the loan is impaired, it can't be impaired again + // Check the vault + bool const canImpair = canImpairLoan(env, broker, state); + // Impair the loan, if possible env(manage(lender, loanKeylet.key, tfLoanImpair), - ter(tecNO_PERMISSION)); + canImpair ? ter(tesSUCCESS) : ter(tecLIMIT_EXCEEDED)); + + if (canImpair) + { + state.flags |= tfLoanImpair; + state.nextPaymentDate = + env.now().time_since_epoch().count(); + + // Once the loan is impaired, it can't be impaired again + env(manage(lender, loanKeylet.key, tfLoanImpair), + ter(tecNO_PERMISSION)); + } + verifyLoanStatus(state); } auto const nextDueDate = tp{d{state.nextPaymentDate}}; @@ -1158,25 +1264,40 @@ class Loan_test : public beast::unit_test::suite // defaulted env.close(nextDueDate + 60s); +#if LOANDRAW && 0 if (impair) { // Impaired loans can't be drawn against env(draw(borrower, loanKeylet.key, broker.asset(100)), ter(tecNO_PERMISSION)); } +#endif + + auto const [amountToBeCovered, brokerAcct] = + getDefaultInfo(state, broker); // Default the loan env(manage(lender, loanKeylet.key, tfLoanDefault)); + env.close(); + + // The LoanBroker just lost some of it's first-loss capital. + // Replenish it. + replenishCover( + broker, + brokerAcct, + startingCoverAvailable, + amountToBeCovered); state.flags |= tfLoanDefault; state.paymentRemaining = 0; - state.assetsAvailable = 0; state.principalOutstanding = 0; verifyLoanStatus(state); +#if LOANDRAW && 0 // Defaulted loans can't be drawn against, either env(draw(borrower, loanKeylet.key, broker.asset(100)), ter(tecNO_PERMISSION)); +#endif // Once a loan is defaulted, it can't be managed env(manage(lender, loanKeylet.key, tfLoanUnimpair), @@ -1192,8 +1313,10 @@ class Loan_test : public beast::unit_test::suite VerifyLoanStatus const& verifyLoanStatus) { // toEndOfLife // - auto state = currentState(loanKeylet, verifyLoanStatus); + auto state = + getCurrentState(env, broker, loanKeylet, verifyLoanStatus); BEAST_EXPECT(state.flags == baseFlag); +#if LOANDRAW && 0 auto const borrowerStartingBalance = env.balance(borrower, broker.asset); @@ -1218,15 +1341,20 @@ class Loan_test : public beast::unit_test::suite // PrettyAsset scaling. STAmount const drawAmount{broker.asset, state.assetsAvailable}; env(draw(borrower, loanKeylet.key, drawAmount)); +#else + STAmount const drawAmount = + STAmount(broker.asset, state.principalRequested - 1); +#endif env.close(state.startDate + 20s); auto const loanAge = (env.now() - state.startDate).count(); BEAST_EXPECT(loanAge == 30); - state.assetsAvailable -= drawAmount; verifyLoanStatus(state); +#if LOANDRAW && 0 BEAST_EXPECT( env.balance(borrower, broker.asset) == borrowerStartingBalance + drawAmount - adjustment); +#endif // Send some bogus pay transactions env(pay(borrower, @@ -1271,6 +1399,8 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT( periodicRate == Number(2283105022831050, -21, Number::unchecked{})); + STAmount const principalOutstanding{ + broker.asset, state.principalOutstanding}; STAmount const accruedInterest{ broker.asset, state.principalOutstanding * periodicRate * loanAge / @@ -1282,8 +1412,7 @@ class Loan_test : public beast::unit_test::suite broker.asset, state.principalOutstanding * Number(36, -3)}; BEAST_EXPECT(prepaymentPenalty == broker.asset(36)); STAmount const closePaymentFee = broker.asset(4); - auto const payoffAmount = - STAmount{broker.asset, state.principalOutstanding} + + auto const payoffAmount = principalOutstanding + accruedInterest + prepaymentPenalty + closePaymentFee; BEAST_EXPECT( payoffAmount == @@ -1300,7 +1429,7 @@ class Loan_test : public beast::unit_test::suite env.close(); // Need to account for fees if the loan is in XRP - adjustment = broker.asset(0); + PrettyAmount adjustment = broker.asset(0); if (broker.asset.raw().native()) { adjustment = env.current()->fees().base; @@ -1310,9 +1439,22 @@ class Loan_test : public beast::unit_test::suite state.principalOutstanding = 0; verifyLoanStatus(state); - BEAST_EXPECT( - env.balance(borrower, broker.asset) == - borrowerBalanceBeforePayment - payoffAmount - adjustment); + STAmount const balanceChangeAmount{ + broker.asset, + roundToAsset( + broker.asset, + payoffAmount, + borrowerBalanceBeforePayment.number())}; + { + auto const difference = roundToReference( + env.balance(borrower, broker.asset) - + (borrowerBalanceBeforePayment - + balanceChangeAmount - adjustment), + STAmount{ + broker.asset, + borrowerBalanceBeforePayment.value() * 10}); + BEAST_EXPECT(difference == beast::zero); + } // Can't impair or default a paid off loan env(manage(lender, loanKeylet.key, tfLoanImpair), @@ -1322,13 +1464,13 @@ class Loan_test : public beast::unit_test::suite }; }; - // There are a lot of fields that can be set on a loan, but most of - // them only affect the "math" when a payment is made. The only one - // that really affects behavior is the `tfLoanOverpayment` flag. + // There are a lot of fields that can be set on a loan, but most + // of them only affect the "math" when a payment is made. The + // only one that really affects behavior is the + // `tfLoanOverpayment` flag. lifecycle( caseLabel, - "Loan overpayment allowed - Impair and Default before start " - "date", + "Loan overpayment allowed - Impair and Default", env, loanAmount, interestExponent, @@ -1338,12 +1480,11 @@ class Loan_test : public beast::unit_test::suite broker, pseudoAcct, tfLoanOverpayment, - defaultBeforeStartDate(lsfLoanOverpayment)); + defaultImmediately(lsfLoanOverpayment)); lifecycle( caseLabel, - "Loan overpayment prohibited - Impair and Default before start " - "date", + "Loan overpayment prohibited - Impair and Default", env, loanAmount, interestExponent, @@ -1353,13 +1494,11 @@ class Loan_test : public beast::unit_test::suite broker, pseudoAcct, 0, - defaultBeforeStartDate(0)); + defaultImmediately(0)); lifecycle( caseLabel, - "Loan overpayment allowed - Default without Impair before " - "start " - "date", + "Loan overpayment allowed - Default without Impair", env, loanAmount, interestExponent, @@ -1369,13 +1508,11 @@ class Loan_test : public beast::unit_test::suite broker, pseudoAcct, tfLoanOverpayment, - defaultBeforeStartDate(lsfLoanOverpayment, false)); + defaultImmediately(lsfLoanOverpayment, false)); lifecycle( caseLabel, - "Loan overpayment prohibited - Default without Impair before " - "start " - "date", + "Loan overpayment prohibited - Default without Impair", env, loanAmount, interestExponent, @@ -1385,7 +1522,7 @@ class Loan_test : public beast::unit_test::suite broker, pseudoAcct, 0, - defaultBeforeStartDate(0, false)); + defaultImmediately(0, false)); lifecycle( caseLabel, @@ -1404,8 +1541,16 @@ class Loan_test : public beast::unit_test::suite // toEndOfLife // // Initialize values with the current state - auto state = currentState(loanKeylet, verifyLoanStatus); + auto state = + getCurrentState(env, broker, loanKeylet, verifyLoanStatus); BEAST_EXPECT(state.flags == lsfLoanOverpayment); + + auto const& broker = verifyLoanStatus.broker; + auto const startingCoverAvailable = coverAvailable( + broker.brokerID, + broker.asset(coverDepositParameter).number()); + +#if LOANDRAW && 0 auto const borrowerStartingBalance = env.balance(borrower, broker.asset); @@ -1454,25 +1599,42 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT( env.balance(borrower, broker.asset) == borrowerStartingBalance + drawAmount - adjustment); +#endif // move past the due date + grace period (60s) env.close(tp{d{state.nextPaymentDate}} + 60s + 20s); +#if LOANDRAW && 0 // Try to draw env(draw(borrower, loanKeylet.key, broker.asset(100)), ter(tecNO_PERMISSION)); +#endif + + auto const [amountToBeCovered, brokerAcct] = + getDefaultInfo(state, broker); // default the loan env(manage(lender, loanKeylet.key, tfLoanDefault)); + env.close(); + + // The LoanBroker just lost some of it's first-loss capital. + // Replenish it. + replenishCover( + broker, + brokerAcct, + startingCoverAvailable, + amountToBeCovered); + state.paymentRemaining = 0; - state.assetsAvailable = 0; state.principalOutstanding = 0; state.flags |= tfLoanDefault; verifyLoanStatus(state); +#if LOANDRAW && 0 // Same error, different check env(draw(borrower, loanKeylet.key, broker.asset(100)), ter(tecNO_PERMISSION)); +#endif // Can't make a payment on it either env(pay(borrower, loanKeylet.key, broker.asset(300)), @@ -1526,13 +1688,14 @@ class Loan_test : public beast::unit_test::suite // toEndOfLife // // Draw and make multiple payments - auto state = currentState(loanKeylet, verifyLoanStatus); + auto state = + getCurrentState(env, broker, loanKeylet, verifyLoanStatus); BEAST_EXPECT(state.flags == 0); - // Advance to the start date of the loan - env.close(state.startDate + 5s); + env.close(); verifyLoanStatus(state); +#if LOANDRAW && 0 auto const borrowerStartingBalance = env.balance(borrower, broker.asset); @@ -1548,15 +1711,17 @@ class Loan_test : public beast::unit_test::suite // PrettyAsset scaling. STAmount const drawAmount{broker.asset, state.assetsAvailable}; env(draw(borrower, loanKeylet.key, drawAmount)); +#endif env.close(state.startDate + 20s); auto const loanAge = (env.now() - state.startDate).count(); BEAST_EXPECT(loanAge == 30); - state.assetsAvailable -= drawAmount; +#if LOANDRAW && 0 verifyLoanStatus(state); BEAST_EXPECT( env.balance(borrower, broker.asset) == borrowerStartingBalance + drawAmount - adjustment); +#endif // Periodic payment amount will consist of // 1. principal outstanding (1000) @@ -1580,8 +1745,8 @@ class Loan_test : public beast::unit_test::suite STAmount const principalRequestedAmount{ broker.asset, state.principalRequested}; - // Compute the payment based on the number of payments - // remaining + // Compute the payment based on the number of + // payments remaining auto const rateFactor = power(1 + periodicRate, state.paymentRemaining); Number const rawPeriodicPayment = @@ -1590,8 +1755,8 @@ class Loan_test : public beast::unit_test::suite STAmount const periodicPayment = roundToReference( STAmount{broker.asset, rawPeriodicPayment}, principalRequestedAmount); - // Only check the first payment since the rounding may - // drift as payments are made + // Only check the first payment since the rounding + // may drift as payments are made BEAST_EXPECT( state.paymentRemaining < 12 || STAmount(broker.asset, rawPeriodicPayment) == @@ -1600,8 +1765,8 @@ class Loan_test : public beast::unit_test::suite STAmount const totalDue = roundToReference( periodicPayment + broker.asset(2), principalRequestedAmount); - // Only check the first payment since the rounding may - // drift as payments are made + // Only check the first payment since the rounding + // may drift as payments are made BEAST_EXPECT( state.paymentRemaining < 12 || totalDue == @@ -1613,8 +1778,8 @@ class Loan_test : public beast::unit_test::suite // taken STAmount const transactionAmount = STAmount{broker.asset, totalDue} + broker.asset(10); - // Only check the first payment since the rounding may - // drift as payments are made + // Only check the first payment since the rounding + // may drift as payments are made BEAST_EXPECT( state.paymentRemaining < 12 || transactionAmount == @@ -1676,7 +1841,7 @@ class Loan_test : public beast::unit_test::suite env.close(); // Need to account for fees if the loan is in XRP - adjustment = broker.asset(0); + PrettyAmount adjustment = broker.asset(0); if (broker.asset.raw().native()) { adjustment = env.current()->fees().base; @@ -1721,8 +1886,8 @@ class Loan_test : public beast::unit_test::suite testcase("Lifecycle"); using namespace jtx; - // Create 3 loan brokers: one for XRP, one for an IOU, and one for an - // MPT. That'll require three corresponding SAVs. + // Create 3 loan brokers: one for XRP, one for an IOU, and one for + // an MPT. That'll require three corresponding SAVs. Env env(*this, all); Account const issuer{"issuer"}; @@ -1736,21 +1901,21 @@ class Loan_test : public beast::unit_test::suite // Do not fund alice Account const alice{"alice"}; - // Fund the accounts and trust lines with the same amount so that tests - // can use the same values regardless of the asset. - env.fund(XRP(100'000), issuer, noripple(lender, borrower, evan)); + // Fund the accounts and trust lines with the same amount so that + // tests can use the same values regardless of the asset. + env.fund(XRP(100'000'000), issuer, noripple(lender, borrower, evan)); env.close(); // Create assets PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; PrettyAsset const iouAsset = issuer[iouCurrency]; - env(trust(lender, iouAsset(1'000'000))); - env(trust(borrower, iouAsset(1'000'000))); - env(trust(evan, iouAsset(1'000'000))); - env(pay(issuer, evan, iouAsset(100'000))); - env(pay(issuer, lender, iouAsset(100'000))); + env(trust(lender, iouAsset(10'000'000))); + env(trust(borrower, iouAsset(10'000'000))); + env(trust(evan, iouAsset(10'000'000))); + env(pay(issuer, evan, iouAsset(1'000'000))); + env(pay(issuer, lender, iouAsset(10'000'000))); // Fund the borrower with enough to cover interest and fees - env(pay(issuer, borrower, iouAsset(1'000))); + env(pay(issuer, borrower, iouAsset(10'000))); env.close(); MPTTester mptt{env, issuer, mptInitNoFund}; @@ -1760,10 +1925,10 @@ class Loan_test : public beast::unit_test::suite mptt.authorize({.account = lender}); mptt.authorize({.account = borrower}); mptt.authorize({.account = evan}); - env(pay(issuer, lender, mptAsset(100'000))); - env(pay(issuer, evan, mptAsset(100'000))); + env(pay(issuer, lender, mptAsset(10'000'000))); + env(pay(issuer, evan, mptAsset(1'000'000))); // Fund the borrower with enough to cover interest and fees - env(pay(issuer, borrower, mptAsset(1'000))); + env(pay(issuer, borrower, mptAsset(10'000))); env.close(); std::array const assets{xrpAsset, mptAsset, iouAsset}; @@ -1823,8 +1988,8 @@ class Loan_test : public beast::unit_test::suite using namespace jtx; using namespace std::chrono_literals; - // Create 3 loan brokers: one for XRP, one for an IOU, and one for an - // MPT. That'll require three corresponding SAVs. + // Create 3 loan brokers: one for XRP, one for an IOU, and one for + // an MPT. That'll require three corresponding SAVs. Env env(*this, all); Account const issuer{"issuer"}; @@ -1832,9 +1997,9 @@ class Loan_test : public beast::unit_test::suite // brokers. Account const lender{"lender"}; - // Fund the accounts and trust lines with the same amount so that tests - // can use the same values regardless of the asset. - env.fund(XRP(100'000), issuer, noripple(lender)); + // Fund the accounts and trust lines with the same amount so that + // tests can use the same values regardless of the asset. + env.fund(XRP(100'000'000), issuer, noripple(lender)); env.close(); // Use an XRP asset for simplicity @@ -1847,15 +2012,13 @@ class Loan_test : public beast::unit_test::suite auto const loanSetFee = fee(env.current()->fees().base * 2); Number const principalRequest{1, 3}; - auto const startDate = env.now() + 60s; - // The LoanSet json can be created without a counterparty signature, but - // it will not pass preflight + // The LoanSet json can be created without a counterparty signature, + // but it will not pass preflight auto createJson = env.json( set(lender, broker.brokerID, - broker.asset(principalRequest).value(), - startDate), + broker.asset(principalRequest).value()), fee(loanSetFee)); env(createJson, ter(temBAD_SIGNER)); @@ -1893,6 +2056,8 @@ class Loan_test : public beast::unit_test::suite env.close(); + auto const startDate = env.current()->info().parentCloseTime; + // Loan is successfully created { auto const res = env.rpc("account_objects", lender.human()); @@ -1922,7 +2087,6 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT(objects.size() == 1); auto const loan = objects[0u]; - BEAST_EXPECT(loan[sfAssetsAvailable] == "1000000000"); BEAST_EXPECT(loan[sfBorrower] == lender.human()); BEAST_EXPECT(loan[sfCloseInterestRate] == 0); BEAST_EXPECT(loan[sfClosePaymentFee] == "0"); @@ -1954,9 +2118,11 @@ class Loan_test : public beast::unit_test::suite env.close(startDate); +#if LOANDRAW && 0 // Draw the loan env(draw(lender, loanKeylet.key, broker.asset(1000))); env.close(); +#endif // Make a payment env(pay(lender, loanKeylet.key, broker.asset(1000))); } @@ -1973,7 +2139,7 @@ class Loan_test : public beast::unit_test::suite Account const lender{"lender"}; Account const borrower{"borrower"}; - env.fund(XRP(1'000'000), lender, borrower); + env.fund(XRP(vaultDeposit * 100), lender, borrower); env.close(); PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; @@ -1984,10 +2150,9 @@ class Loan_test : public beast::unit_test::suite auto const loanSetFee = fee(env.current()->fees().base * 2); Number const principalRequest{1, 3}; - auto const startDate = env.now() + 60s; auto forgedLoanSet = - set(borrower, broker.brokerID, principalRequest, startDate, 0); + set(borrower, broker.brokerID, principalRequest, 0); Json::Value randomData{Json::objectValue}; randomData[jss::SigningPubKey] = Json::StaticString{"2600"}; @@ -2046,7 +2211,7 @@ class Loan_test : public beast::unit_test::suite Account const issuer{"issuer"}; Account const lender{"lender"}; - env.fund(XRP(100'000), issuer, noripple(lender)); + env.fund(XRP(vaultDeposit * 100), issuer, noripple(lender)); env.close(); PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; @@ -2063,11 +2228,9 @@ class Loan_test : public beast::unit_test::suite auto const loanSetFee = fee(env.current()->fees().base * 2); Number const principalRequest{1, 3}; - auto const startDate = env.now() + 60s; auto createJson = env.json( - set(lender, broker.brokerID, principalRequest, startDate), - fee(loanSetFee)); + set(lender, broker.brokerID, principalRequest), fee(loanSetFee)); Json::Value counterpartyJson{Json::objectValue}; counterpartyJson[sfTxnSignature] = createJson[sfTxnSignature]; @@ -2085,8 +2248,8 @@ class Loan_test : public beast::unit_test::suite void testLoanPayComputePeriodicPaymentValidRateInvariant() { - testcase - << "LoanPay ripple::detail::computePeriodicPayment : valid rate"; + testcase << "LoanPay ripple::detail::computePeriodicPayment : " + "valid rate"; using namespace jtx; using namespace std::chrono_literals; @@ -2096,7 +2259,7 @@ class Loan_test : public beast::unit_test::suite Account const lender{"lender"}; Account const borrower{"borrower"}; - env.fund(XRP(1'000'000), issuer, lender, borrower); + env.fund(XRP(vaultDeposit * 100), issuer, lender, borrower); env.close(); PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; @@ -2106,13 +2269,12 @@ class Loan_test : public beast::unit_test::suite auto const loanSetFee = fee(env.current()->fees().base * 2); Number const principalRequest{640562, -5}; - auto const startDate = env.now() + 60s; Number const serviceFee{2462611968}; std::uint32_t const numPayments{4294967295}; auto createJson = env.json( - set(borrower, broker.brokerID, principalRequest, startDate), + set(borrower, broker.brokerID, principalRequest), fee(loanSetFee), loanServiceFee(serviceFee), paymentTotal(numPayments), @@ -2136,7 +2298,7 @@ class Loan_test : public beast::unit_test::suite createJson = env.json(createJson, sig(sfCounterpartySignature, lender)); env(createJson, ter(tesSUCCESS)); - env.close(startDate); + env.close(); if (auto const loan = env.le(keylet); BEAST_EXPECT(loan)) { @@ -2144,11 +2306,13 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT(loan->at(sfPaymentRemaining) == numPayments); BEAST_EXPECT(loan->at(sfPrincipalRequested) == actualPrincipal); BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == actualPrincipal); - BEAST_EXPECT(loan->at(sfAssetsAvailable) == actualPrincipal); } - auto loanDrawTx = env.json( - draw(borrower, keylet.key, STAmount{broker.asset, Number{6}})); +#if LOANDRAW && 0 + auto loanDrawTx = + env.json(draw(borrower, keylet.key, STAmount{broker.asset, Number { + 6 + }})); env(loanDrawTx, ter(tesSUCCESS)); env.close(); @@ -2158,8 +2322,8 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT(loan->at(sfPaymentRemaining) == numPayments); BEAST_EXPECT(loan->at(sfPrincipalRequested) == actualPrincipal); BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == actualPrincipal); - BEAST_EXPECT(loan->at(sfAssetsAvailable) == actualPrincipal - 6); } +#endif auto loanPayTx = env.json( pay(borrower, keylet.key, STAmount{broker.asset, serviceFee + 6})); @@ -2173,21 +2337,28 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT(loan->at(sfPrincipalRequested) == actualPrincipal); BEAST_EXPECT( loan->at(sfPrincipalOutstanding) == actualPrincipal - 1); - BEAST_EXPECT(loan->at(sfAssetsAvailable) == actualPrincipal - 6); } } void testRPC() { - // This will expand as more test cases are added. Some functionality is - // tested in other test functions. + // This will expand as more test cases are added. Some functionality + // is tested in other test functions. testcase("RPC"); using namespace jtx; Env env(*this, all); + auto lowerFee = [&]() { + // Run the local fee back down. + while (env.app().getFeeTrack().lowerLocalFee()) + ; + }; + + auto const baseFee = env.current()->fees().base; + Account const alice{"alice"}; std::string const borrowerPass = "borrower"; std::string const borrowerSeed = "ssBRAsLpH4778sLNYC4ik1JBJsBVf"; @@ -2229,6 +2400,7 @@ class Loan_test : public beast::unit_test::suite auto const jtx = env.jt(txJson, sig(borrower)); BEAST_EXPECT(txSignResult == jtx.jv); + lowerFee(); auto const jSubmit = env.rpc("submit", txSignBlob); BEAST_EXPECT( jSubmit.isMember(jss::result) && @@ -2236,6 +2408,7 @@ class Loan_test : public beast::unit_test::suite jSubmit[jss::result][jss::engine_result].asString() == "tesSUCCESS"); + lowerFee(); env(jtx.jv, sig(none), seq(none), fee(none), ter(tefPAST_SEQ)); } @@ -2275,12 +2448,11 @@ class Loan_test : public beast::unit_test::suite "FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FECF83F" "5C"; txJson[sfPrincipalRequested] = "100000000"; - txJson[sfStartDate] = 807730340; txJson[sfPaymentTotal] = 10000; txJson[sfPaymentInterval] = 3600; txJson[sfGracePeriod] = 300; txJson[sfFlags] = 65536; // tfLoanOverpayment - txJson[sfFee] = "24"; + txJson[sfFee] = to_string(24 * baseFee / 10); // 2. Borrower signs the transaction auto const borrowerSignParams = [&]() { @@ -2292,16 +2464,19 @@ class Loan_test : public beast::unit_test::suite }(); auto const jSignBorrower = env.rpc("json", "sign", to_string(borrowerSignParams)); - BEAST_EXPECT( + BEAST_EXPECTS( jSignBorrower.isMember(jss::result) && - jSignBorrower[jss::result].isMember(jss::tx_json)); + jSignBorrower[jss::result].isMember(jss::tx_json), + to_string(jSignBorrower)); auto const txBorrowerSignResult = jSignBorrower[jss::result][jss::tx_json]; auto const txBorrowerSignBlob = jSignBorrower[jss::result][jss::tx_blob].asString(); - // 2a. Borrower attempts to submit the transaction. It doesn't work + // 2a. Borrower attempts to submit the transaction. It doesn't + // work { + lowerFee(); auto const jSubmitBlob = env.rpc("submit", txBorrowerSignBlob); BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); auto const jSubmitBlobResult = jSubmitBlob[jss::result]; @@ -2335,38 +2510,43 @@ class Loan_test : public beast::unit_test::suite jSignLender[jss::result][jss::tx_blob].asString(); // 5. Lender submits the signed transaction blob + lowerFee(); auto const jSubmitBlob = env.rpc("submit", txLenderSignBlob); BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); auto const jSubmitBlobResult = jSubmitBlob[jss::result]; BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json)); auto const jSubmitBlobTx = jSubmitBlobResult[jss::tx_json]; - // To get far enough to return tecNO_ENTRY means that the signatures - // all validated. Of course the transaction won't succeed because no - // Vault or Broker were created. - BEAST_EXPECT( + // To get far enough to return tecNO_ENTRY means that the + // signatures all validated. Of course the transaction won't + // succeed because no Vault or Broker were created. + BEAST_EXPECTS( jSubmitBlobResult.isMember(jss::engine_result) && - jSubmitBlobResult[jss::engine_result].asString() == - "tecNO_ENTRY"); + jSubmitBlobResult[jss::engine_result].asString() == + "tecNO_ENTRY", + to_string(jSubmitBlobResult)); BEAST_EXPECT( !jSubmitBlob.isMember(jss::error) && !jSubmitBlobResult.isMember(jss::error)); - // 4-alt. Lender submits the transaction json originally received - // from the Borrower. It gets signed, but is now a duplicate, so - // fails. Borrower could done this instead of steps 4 and 5. + // 4-alt. Lender submits the transaction json originally + // received from the Borrower. It gets signed, but is now a + // duplicate, so fails. Borrower could done this instead of + // steps 4 and 5. + lowerFee(); auto const jSubmitJson = env.rpc("json", "submit", to_string(lenderSignParams)); BEAST_EXPECT(jSubmitJson.isMember(jss::result)); auto const jSubmitJsonResult = jSubmitJson[jss::result]; BEAST_EXPECT(jSubmitJsonResult.isMember(jss::tx_json)); auto const jSubmitJsonTx = jSubmitJsonResult[jss::tx_json]; - // Since the previous tx claimed a fee, this duplicate is not going - // anywhere - BEAST_EXPECT( + // Since the previous tx claimed a fee, this duplicate is not + // going anywhere + BEAST_EXPECTS( jSubmitJsonResult.isMember(jss::engine_result) && - jSubmitJsonResult[jss::engine_result].asString() == - "tefPAST_SEQ"); + jSubmitJsonResult[jss::engine_result].asString() == + "tefPAST_SEQ", + to_string(jSubmitJsonResult)); BEAST_EXPECT( !jSubmitJson.isMember(jss::error) && @@ -2386,12 +2566,11 @@ class Loan_test : public beast::unit_test::suite "FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FECF83F" "5C"; txJson[sfPrincipalRequested] = "100000000"; - txJson[sfStartDate] = 807730340; txJson[sfPaymentTotal] = 10000; txJson[sfPaymentInterval] = 3600; txJson[sfGracePeriod] = 300; txJson[sfFlags] = 65536; // tfLoanOverpayment - txJson[sfFee] = "24"; + txJson[sfFee] = to_string(24 * baseFee / 10); // 2. Lender signs the transaction auto const lenderSignParams = [&]() { @@ -2411,8 +2590,10 @@ class Loan_test : public beast::unit_test::suite auto const txLenderSignBlob = jSignLender[jss::result][jss::tx_blob].asString(); - // 2a. Lender attempts to submit the transaction. It doesn't work + // 2a. Lender attempts to submit the transaction. It doesn't + // work { + lowerFee(); auto const jSubmitBlob = env.rpc("submit", txLenderSignBlob); BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); auto const jSubmitBlobResult = jSubmitBlob[jss::result]; @@ -2446,38 +2627,43 @@ class Loan_test : public beast::unit_test::suite jSignBorrower[jss::result][jss::tx_blob].asString(); // 5. Borrower submits the signed transaction blob + lowerFee(); auto const jSubmitBlob = env.rpc("submit", txBorrowerSignBlob); BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); auto const jSubmitBlobResult = jSubmitBlob[jss::result]; BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json)); auto const jSubmitBlobTx = jSubmitBlobResult[jss::tx_json]; - // To get far enough to return tecNO_ENTRY means that the signatures - // all validated. Of course the transaction won't succeed because no - // Vault or Broker were created. - BEAST_EXPECT( + // To get far enough to return tecNO_ENTRY means that the + // signatures all validated. Of course the transaction won't + // succeed because no Vault or Broker were created. + BEAST_EXPECTS( jSubmitBlobResult.isMember(jss::engine_result) && - jSubmitBlobResult[jss::engine_result].asString() == - "tecNO_ENTRY"); + jSubmitBlobResult[jss::engine_result].asString() == + "tecNO_ENTRY", + to_string(jSubmitBlobResult)); BEAST_EXPECT( !jSubmitBlob.isMember(jss::error) && !jSubmitBlobResult.isMember(jss::error)); - // 4-alt. Borrower submits the transaction json originally received - // from the Lender. It gets signed, but is now a duplicate, so - // fails. Lender could done this instead of steps 4 and 5. + // 4-alt. Borrower submits the transaction json originally + // received from the Lender. It gets signed, but is now a + // duplicate, so fails. Lender could done this instead of steps + // 4 and 5. + lowerFee(); auto const jSubmitJson = env.rpc("json", "submit", to_string(borrowerSignParams)); BEAST_EXPECT(jSubmitJson.isMember(jss::result)); auto const jSubmitJsonResult = jSubmitJson[jss::result]; BEAST_EXPECT(jSubmitJsonResult.isMember(jss::tx_json)); auto const jSubmitJsonTx = jSubmitJsonResult[jss::tx_json]; - // Since the previous tx claimed a fee, this duplicate is not going - // anywhere - BEAST_EXPECT( + // Since the previous tx claimed a fee, this duplicate is not + // going anywhere + BEAST_EXPECTS( jSubmitJsonResult.isMember(jss::engine_result) && - jSubmitJsonResult[jss::engine_result].asString() == - "tefPAST_SEQ"); + jSubmitJsonResult[jss::engine_result].asString() == + "tefPAST_SEQ", + to_string(jSubmitJsonResult)); BEAST_EXPECT( !jSubmitJson.isMember(jss::error) && diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index e563e7af1b..efe7e0c83b 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -793,7 +793,6 @@ Json::Value set(AccountID const& account, uint256 const& loanBrokerID, Number principalRequested, - NetClock::time_point const& startDate, std::uint32_t flags = 0); auto const counterparty = JTxFieldWrapper(sfCounterparty); @@ -835,12 +834,14 @@ manage(AccountID const& account, uint256 const& loanID, std::uint32_t flags); Json::Value del(AccountID const& account, uint256 const& loanID, std::uint32_t flags = 0); +#if loandraw Json::Value draw( AccountID const& account, uint256 const& loanID, STAmount const& amount, std::uint32_t flags = 0); +#endif Json::Value pay(AccountID const& account, diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index 3f3b6f527e..05959dadfb 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -450,7 +450,6 @@ Json::Value set(AccountID const& account, uint256 const& loanBrokerID, Number principalRequested, - NetClock::time_point const& startDate, std::uint32_t flags) { Json::Value jv; @@ -459,7 +458,6 @@ set(AccountID const& account, jv[sfLoanBrokerID] = to_string(loanBrokerID); jv[sfPrincipalRequested] = to_string(principalRequested); jv[sfFlags] = flags; - jv[sfStartDate] = startDate.time_since_epoch().count(); return jv; } @@ -485,6 +483,7 @@ del(AccountID const& account, uint256 const& loanID, std::uint32_t flags) return jv; } +#if LOANDRAW Json::Value draw( AccountID const& account, @@ -500,6 +499,7 @@ draw( jv[sfFlags] = flags; return jv; } +#endif Json::Value pay(AccountID const& account, diff --git a/src/xrpld/app/tx/detail/LoanDelete.cpp b/src/xrpld/app/tx/detail/LoanDelete.cpp index e2ee709563..4e075d8e41 100644 --- a/src/xrpld/app/tx/detail/LoanDelete.cpp +++ b/src/xrpld/app/tx/detail/LoanDelete.cpp @@ -121,9 +121,10 @@ LoanDelete::doApply() auto const vaultSle = view.peek(keylet ::vault(brokerSle->at(sfVaultID))); if (!vaultSle) return tefBAD_LEDGER; // LCOV_EXCL_LINE - auto const vaultAsset = vaultSle->at(sfAsset); +#if LOANDRAW // transfer any remaining funds to the borrower + auto const vaultAsset = vaultSle->at(sfAsset); auto assetsAvailableProxy = loanSle->at(sfAssetsAvailable); if (assetsAvailableProxy != 0) { @@ -136,6 +137,7 @@ LoanDelete::doApply() WaiveTransferFee::Yes)) return ter; } +#endif // Remove LoanID from Directory of the LoanBroker pseudo-account. if (!view.dirRemove( diff --git a/src/xrpld/app/tx/detail/LoanDraw.cpp b/src/xrpld/app/tx/detail/LoanDraw.cpp index 3b99ccf900..79ec742d5d 100644 --- a/src/xrpld/app/tx/detail/LoanDraw.cpp +++ b/src/xrpld/app/tx/detail/LoanDraw.cpp @@ -1,3 +1,4 @@ +#if LOANDRAW //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled @@ -121,7 +122,7 @@ LoanDraw::preclaim(PreclaimContext const& ctx) if (amount.asset() != asset) { - JLOG(ctx.j.warn()) << "Loan amount does not match the Vault asset."; + JLOG(ctx.j.warn()) << "Draw amount does not match the Vault asset."; return tecWRONG_ASSET; } @@ -189,3 +190,4 @@ LoanDraw::doApply() //------------------------------------------------------------------------------ } // namespace ripple +#endif diff --git a/src/xrpld/app/tx/detail/LoanDraw.h b/src/xrpld/app/tx/detail/LoanDraw.h index a4d0a29b79..471744f32f 100644 --- a/src/xrpld/app/tx/detail/LoanDraw.h +++ b/src/xrpld/app/tx/detail/LoanDraw.h @@ -1,3 +1,4 @@ +#if LOANDRAW //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled @@ -51,3 +52,4 @@ public: } // namespace ripple #endif +#endif diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index 82cb00917c..345457e6f5 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -168,10 +168,15 @@ defaultLoan( auto brokerDebtTotalProxy = brokerSle->at(sfDebtTotal); auto const totalDefaultAmount = principalOutstanding + interestOutstanding; +#if LOANDRAW // The default Amount equals the outstanding principal and interest, // excluding any funds unclaimed by the Borrower. auto loanAssetsAvailableProxy = loanSle->at(sfAssetsAvailable); auto const defaultAmount = totalDefaultAmount - loanAssetsAvailableProxy; +#else + // TODO: get rid of this and just use totalDefaultAmount + auto const defaultAmount = totalDefaultAmount; +#endif // Apply the First-Loss Capital to the Default Amount TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)}; TenthBips32 const coverRateLiquidation{ @@ -185,7 +190,11 @@ defaultLoan( coverRateLiquidation), defaultAmount), originalPrincipalRequested); +#if LOANDRAW auto const returnToVault = defaultCovered + loanAssetsAvailableProxy; +#else + auto const returnToVault = defaultCovered; +#endif auto const vaultDefaultAmount = defaultAmount - defaultCovered; // Update the Vault object: @@ -250,7 +259,9 @@ defaultLoan( // Update the Loan object: loanSle->setFlag(lsfLoanDefault); loanSle->at(sfPaymentRemaining) = 0; +#if LOANDRAW loanAssetsAvailableProxy = 0; +#endif loanSle->at(sfPrincipalOutstanding) = 0; view.update(loanSle); diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 6ed405aa6d..15b7fcb867 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -82,7 +82,6 @@ LoanPay::preclaim(PreclaimContext const& ctx) TenthBips32 const interestRate{loanSle->at(sfInterestRate)}; auto const paymentRemaining = loanSle->at(sfPaymentRemaining); TenthBips32 const lateInterestRate{loanSle->at(sfLateInterestRate)}; - auto const startDate = loanSle->at(sfStartDate); if (loanSle->at(sfBorrower) != account) { @@ -90,12 +89,6 @@ LoanPay::preclaim(PreclaimContext const& ctx) return tecNO_PERMISSION; } - if (!hasExpired(ctx.view, startDate)) - { - JLOG(ctx.j.warn()) << "Loan has not started yet."; - return tecTOO_SOON; - } - if (paymentRemaining == 0 || principalOutstanding == 0) { JLOG(ctx.j.warn()) << "Loan is already paid off."; diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 5a5054b1b0..3d44b895d4 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -187,12 +187,6 @@ LoanSet::preclaim(PreclaimContext const& ctx) { auto const& tx = ctx.tx; - if (auto const startDate(tx[sfStartDate]); hasExpired(ctx.view, startDate)) - { - JLOG(ctx.j.warn()) << "Start date is in the past."; - return tecEXPIRED; - } - auto const account = tx[sfAccount]; auto const brokerID = tx[sfLoanBrokerID]; @@ -223,7 +217,6 @@ LoanSet::preclaim(PreclaimContext const& ctx) return terNO_ACCOUNT; } - auto const brokerPseudo = brokerSle->at(sfAccount); auto const vault = ctx.view.read(keylet::vault(brokerSle->at(sfVaultID))); if (!vault) // Should be impossible @@ -249,16 +242,6 @@ LoanSet::preclaim(PreclaimContext const& ctx) JLOG(ctx.j.warn()) << "Borrower account is frozen."; return ret; } - // TODO: Remove when LoanDraw is combined with LoanSet - // brokerPseudo is eventually going to send funds to the borrower, so it - // can't be frozen now. It is also going to receive funds, so it can't be - // deep frozen, but being frozen is a prerequisite for being deep frozen, so - // checking the one is sufficient. - if (auto const ret = checkFrozen(ctx.view, brokerPseudo, asset)) - { - JLOG(ctx.j.warn()) << "Broker pseudo-account account is frozen."; - return ret; - } // brokerOwner is going to receive funds if there's an origination fee, so // it can't be deep frozen if (auto const ret = checkDeepFrozen(ctx.view, brokerOwner, asset)) @@ -360,16 +343,26 @@ LoanSet::doApply() if (mPriorBalance < view.fees().accountReserve(ownerCount)) return tecINSUFFICIENT_RESERVE; - // Create a holding for the borrower if one does not already exist. - // Account for the origination fee using two payments // // 1. Transfer loanAssetsAvailable (principalRequested - originationFee) - // from vault pseudo-account to LoanBroker pseudo-account. + // from vault pseudo-account to the borrower. + // Create a holding for the borrower if one does not already exist. + if (auto const ter = addEmptyHolding( + view, + borrower, + borrowerSle->at(sfBalance).value().xrp(), + vaultAsset, + j_); + ter && ter != tecDUPLICATE) + // ignore tecDUPLICATE. That means the holding already exists, and + // is fine here + return ter; + if (auto const ter = accountSend( view, vaultPseudo, - brokerPseudo, + borrower, STAmount{vaultAsset, loanAssetsAvailable}, j_, WaiveTransferFee::Yes)) @@ -414,7 +407,7 @@ LoanSet::doApply() paymentInterval, paymentTotal, managementFeeRate); - auto const startDate = tx[sfStartDate]; + auto const startDate = view.info().closeTime.time_since_epoch().count(); auto loanSequence = brokerSle->at(sfLoanSequence); // Create the loan @@ -453,7 +446,6 @@ LoanSet::doApply() loan->at(sfPreviousPaymentDate) = 0; loan->at(sfNextPaymentDueDate) = startDate + paymentInterval; loan->at(sfPaymentRemaining) = paymentTotal; - loan->at(sfAssetsAvailable) = loanAssetsAvailable; view.insert(loan); // Update the balances in the vault From 5ac905be01bbfe65fc80ca3a85b9043a8814941f Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 16 Sep 2025 14:51:18 -0400 Subject: [PATCH 079/291] Fix issuer MPT balance sign on tests introduced by 5571 --- src/test/app/EscrowToken_test.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/app/EscrowToken_test.cpp b/src/test/app/EscrowToken_test.cpp index f6831e378e..e930c2f9ac 100644 --- a/src/test/app/EscrowToken_test.cpp +++ b/src/test/app/EscrowToken_test.cpp @@ -3503,7 +3503,7 @@ struct EscrowToken_test : public beast::unit_test::suite BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 125); BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 125); - BEAST_EXPECT(env.balance(gw, MPT) == MPT(20'000)); + BEAST_EXPECT(env.balance(gw, MPT) == MPT(-20'000)); // bob can finish escrow env(escrow::finish(bob, alice, seq1), @@ -3522,7 +3522,7 @@ struct EscrowToken_test : public beast::unit_test::suite : MPT(20'000); BEAST_EXPECT(mptEscrowed(env, alice, MPT) == escrowedWithFix); BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == escrowedWithFix); - BEAST_EXPECT(env.balance(gw, MPT) == outstandingWithFix); + BEAST_EXPECT(env.balance(gw, MPT) == -outstandingWithFix); } // test locked rate: cancel @@ -3567,7 +3567,7 @@ struct EscrowToken_test : public beast::unit_test::suite BEAST_EXPECT(env.balance(alice, MPT) == preAlice); BEAST_EXPECT(env.balance(bob, MPT) == preBob); - BEAST_EXPECT(env.balance(gw, MPT) == MPT(20'000)); + BEAST_EXPECT(env.balance(gw, MPT) == MPT(-20'000)); BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); } @@ -3608,7 +3608,7 @@ struct EscrowToken_test : public beast::unit_test::suite BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 125); BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 125); - BEAST_EXPECT(env.balance(gw, MPT) == MPT(20'000)); + BEAST_EXPECT(env.balance(gw, MPT) == MPT(-20'000)); // bob can finish escrow env(escrow::finish(gw, alice, seq1), @@ -3620,7 +3620,7 @@ struct EscrowToken_test : public beast::unit_test::suite BEAST_EXPECT(env.balance(alice, MPT) == preAlice - delta); BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); - BEAST_EXPECT(env.balance(gw, MPT) == MPT(19'875)); + BEAST_EXPECT(env.balance(gw, MPT) == MPT(-19'875)); } } From 49e1f97476c805ff0937a6ed98b1c1553c87753b Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 16 Sep 2025 15:20:23 -0400 Subject: [PATCH 080/291] Fix formatting --- src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp index 0b15246315..d74e3dccb6 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp @@ -64,7 +64,6 @@ static constexpr std::array mptMutabilityFlags = { NotTEC MPTokenIssuanceSet::preflight(PreflightContext const& ctx) { - auto const mutableFlags = ctx.tx[~sfMutableFlags]; auto const metadata = ctx.tx[~sfMPTokenMetadata]; auto const transferFee = ctx.tx[~sfTransferFee]; From f85ce9c742581e66d3db2cb79888bac1cedd0783 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 16 Sep 2025 17:15:57 -0400 Subject: [PATCH 081/291] Remove or simplify Transactor::isEnabled overrides - Remove the ones where the only condition was the required amendment specified in transactor.macro. - Simplify the one that had other conditions, removing the required amendment from the conditions. --- src/xrpld/app/tx/detail/AMMClawback.cpp | 6 ------ src/xrpld/app/tx/detail/AMMClawback.h | 3 --- src/xrpld/app/tx/detail/Batch.cpp | 6 ------ src/xrpld/app/tx/detail/Batch.h | 3 --- src/xrpld/app/tx/detail/CancelCheck.cpp | 6 ------ src/xrpld/app/tx/detail/CancelCheck.h | 3 --- src/xrpld/app/tx/detail/CashCheck.cpp | 6 ------ src/xrpld/app/tx/detail/CashCheck.h | 3 --- src/xrpld/app/tx/detail/Clawback.cpp | 6 ------ src/xrpld/app/tx/detail/Clawback.h | 3 --- src/xrpld/app/tx/detail/CreateCheck.cpp | 6 ------ src/xrpld/app/tx/detail/CreateCheck.h | 3 --- src/xrpld/app/tx/detail/CreateTicket.cpp | 6 ------ src/xrpld/app/tx/detail/CreateTicket.h | 3 --- src/xrpld/app/tx/detail/Credentials.cpp | 17 ----------------- src/xrpld/app/tx/detail/Credentials.h | 9 --------- src/xrpld/app/tx/detail/DID.cpp | 12 ------------ src/xrpld/app/tx/detail/DID.h | 6 ------ src/xrpld/app/tx/detail/DelegateSet.cpp | 6 ------ src/xrpld/app/tx/detail/DelegateSet.h | 3 --- src/xrpld/app/tx/detail/DeleteOracle.cpp | 6 ------ src/xrpld/app/tx/detail/DeleteOracle.h | 3 --- src/xrpld/app/tx/detail/DepositPreauth.cpp | 3 --- src/xrpld/app/tx/detail/LedgerStateFix.cpp | 6 ------ src/xrpld/app/tx/detail/LedgerStateFix.h | 3 --- src/xrpld/app/tx/detail/MPTokenAuthorize.cpp | 6 ------ src/xrpld/app/tx/detail/MPTokenAuthorize.h | 3 --- .../app/tx/detail/MPTokenIssuanceCreate.cpp | 3 --- .../app/tx/detail/MPTokenIssuanceDestroy.cpp | 6 ------ .../app/tx/detail/MPTokenIssuanceDestroy.h | 3 --- src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp | 3 --- src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp | 6 ------ src/xrpld/app/tx/detail/NFTokenAcceptOffer.h | 3 --- src/xrpld/app/tx/detail/NFTokenBurn.cpp | 6 ------ src/xrpld/app/tx/detail/NFTokenBurn.h | 3 --- src/xrpld/app/tx/detail/NFTokenCancelOffer.cpp | 6 ------ src/xrpld/app/tx/detail/NFTokenCancelOffer.h | 3 --- src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp | 6 ------ src/xrpld/app/tx/detail/NFTokenCreateOffer.h | 3 --- src/xrpld/app/tx/detail/NFTokenMint.cpp | 3 --- src/xrpld/app/tx/detail/NFTokenModify.cpp | 3 +-- .../app/tx/detail/PermissionedDomainDelete.cpp | 6 ------ .../app/tx/detail/PermissionedDomainDelete.h | 3 --- .../app/tx/detail/PermissionedDomainSet.cpp | 3 +-- src/xrpld/app/tx/detail/SetOracle.cpp | 6 ------ src/xrpld/app/tx/detail/SetOracle.h | 3 --- src/xrpld/app/tx/detail/Transactor.h | 7 ++----- 47 files changed, 4 insertions(+), 227 deletions(-) diff --git a/src/xrpld/app/tx/detail/AMMClawback.cpp b/src/xrpld/app/tx/detail/AMMClawback.cpp index d56321ce69..2aaeca7fec 100644 --- a/src/xrpld/app/tx/detail/AMMClawback.cpp +++ b/src/xrpld/app/tx/detail/AMMClawback.cpp @@ -33,12 +33,6 @@ namespace ripple { -bool -AMMClawback::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureAMMClawback); -} - std::uint32_t AMMClawback::getFlagsMask(PreflightContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/AMMClawback.h b/src/xrpld/app/tx/detail/AMMClawback.h index 0e354ebdc7..1984937971 100644 --- a/src/xrpld/app/tx/detail/AMMClawback.h +++ b/src/xrpld/app/tx/detail/AMMClawback.h @@ -33,9 +33,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static std::uint32_t getFlagsMask(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/Batch.cpp b/src/xrpld/app/tx/detail/Batch.cpp index a7a766d068..f81ae5d0f1 100644 --- a/src/xrpld/app/tx/detail/Batch.cpp +++ b/src/xrpld/app/tx/detail/Batch.cpp @@ -164,12 +164,6 @@ Batch::calculateBaseFee(ReadView const& view, STTx const& tx) return signerFees + txnFees + batchBase; } -bool -Batch::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureBatch); -} - std::uint32_t Batch::getFlagsMask(PreflightContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/Batch.h b/src/xrpld/app/tx/detail/Batch.h index 427cc6bf34..07863a5f33 100644 --- a/src/xrpld/app/tx/detail/Batch.h +++ b/src/xrpld/app/tx/detail/Batch.h @@ -40,9 +40,6 @@ public: static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); - static bool - isEnabled(PreflightContext const& ctx); - static std::uint32_t getFlagsMask(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/CancelCheck.cpp b/src/xrpld/app/tx/detail/CancelCheck.cpp index 82ec1bbd1e..598e8bf720 100644 --- a/src/xrpld/app/tx/detail/CancelCheck.cpp +++ b/src/xrpld/app/tx/detail/CancelCheck.cpp @@ -29,12 +29,6 @@ namespace ripple { -bool -CancelCheck::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureChecks); -} - NotTEC CancelCheck::preflight(PreflightContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/CancelCheck.h b/src/xrpld/app/tx/detail/CancelCheck.h index 44cf3e0721..d9b70f919c 100644 --- a/src/xrpld/app/tx/detail/CancelCheck.h +++ b/src/xrpld/app/tx/detail/CancelCheck.h @@ -33,9 +33,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/CashCheck.cpp b/src/xrpld/app/tx/detail/CashCheck.cpp index 7768db31c9..f8ab6189a3 100644 --- a/src/xrpld/app/tx/detail/CashCheck.cpp +++ b/src/xrpld/app/tx/detail/CashCheck.cpp @@ -32,12 +32,6 @@ namespace ripple { -bool -CashCheck::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureChecks); -} - NotTEC CashCheck::preflight(PreflightContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/CashCheck.h b/src/xrpld/app/tx/detail/CashCheck.h index cc3d564efe..da7ef22e3c 100644 --- a/src/xrpld/app/tx/detail/CashCheck.h +++ b/src/xrpld/app/tx/detail/CashCheck.h @@ -33,9 +33,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/Clawback.cpp b/src/xrpld/app/tx/detail/Clawback.cpp index 012565ad95..f417c71bb8 100644 --- a/src/xrpld/app/tx/detail/Clawback.cpp +++ b/src/xrpld/app/tx/detail/Clawback.cpp @@ -75,12 +75,6 @@ preflightHelper(PreflightContext const& ctx) return tesSUCCESS; } -bool -Clawback::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureClawback); -} - std::uint32_t Clawback::getFlagsMask(PreflightContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/Clawback.h b/src/xrpld/app/tx/detail/Clawback.h index 8300d98fde..b02233c2ed 100644 --- a/src/xrpld/app/tx/detail/Clawback.h +++ b/src/xrpld/app/tx/detail/Clawback.h @@ -33,9 +33,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static std::uint32_t getFlagsMask(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/CreateCheck.cpp b/src/xrpld/app/tx/detail/CreateCheck.cpp index a8ebce4204..8b2b119577 100644 --- a/src/xrpld/app/tx/detail/CreateCheck.cpp +++ b/src/xrpld/app/tx/detail/CreateCheck.cpp @@ -28,12 +28,6 @@ namespace ripple { -bool -CreateCheck::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureChecks); -} - NotTEC CreateCheck::preflight(PreflightContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/CreateCheck.h b/src/xrpld/app/tx/detail/CreateCheck.h index 1d9a7ce168..0e414ce012 100644 --- a/src/xrpld/app/tx/detail/CreateCheck.h +++ b/src/xrpld/app/tx/detail/CreateCheck.h @@ -33,9 +33,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/CreateTicket.cpp b/src/xrpld/app/tx/detail/CreateTicket.cpp index 8d88dd7bf8..d48da2d780 100644 --- a/src/xrpld/app/tx/detail/CreateTicket.cpp +++ b/src/xrpld/app/tx/detail/CreateTicket.cpp @@ -33,12 +33,6 @@ CreateTicket::makeTxConsequences(PreflightContext const& ctx) return TxConsequences{ctx.tx, ctx.tx[sfTicketCount]}; } -bool -CreateTicket::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureTicketBatch); -} - NotTEC CreateTicket::preflight(PreflightContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/CreateTicket.h b/src/xrpld/app/tx/detail/CreateTicket.h index 04ad6711c7..86cf91bb5a 100644 --- a/src/xrpld/app/tx/detail/CreateTicket.h +++ b/src/xrpld/app/tx/detail/CreateTicket.h @@ -69,9 +69,6 @@ public: static TxConsequences makeTxConsequences(PreflightContext const& ctx); - static bool - isEnabled(PreflightContext const& ctx); - /** Enforce constraints beyond those of the Transactor base class. */ static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/Credentials.cpp b/src/xrpld/app/tx/detail/Credentials.cpp index abd5539100..8661b8ab16 100644 --- a/src/xrpld/app/tx/detail/Credentials.cpp +++ b/src/xrpld/app/tx/detail/Credentials.cpp @@ -48,12 +48,6 @@ using namespace credentials; // ------- CREATE -------------------------- -bool -CredentialCreate::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureCredentials); -} - std::uint32_t CredentialCreate::getFlagsMask(PreflightContext const& ctx) { @@ -199,11 +193,6 @@ CredentialCreate::doApply() } // ------- DELETE -------------------------- -bool -CredentialDelete::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureCredentials); -} std::uint32_t CredentialDelete::getFlagsMask(PreflightContext const& ctx) @@ -283,12 +272,6 @@ CredentialDelete::doApply() // ------- APPLY -------------------------- -bool -CredentialAccept::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureCredentials); -} - std::uint32_t CredentialAccept::getFlagsMask(PreflightContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/Credentials.h b/src/xrpld/app/tx/detail/Credentials.h index 31b1ee8cf7..a5885a2226 100644 --- a/src/xrpld/app/tx/detail/Credentials.h +++ b/src/xrpld/app/tx/detail/Credentials.h @@ -33,9 +33,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static std::uint32_t getFlagsMask(PreflightContext const& ctx); @@ -60,9 +57,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static std::uint32_t getFlagsMask(PreflightContext const& ctx); @@ -87,9 +81,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static std::uint32_t getFlagsMask(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/DID.cpp b/src/xrpld/app/tx/detail/DID.cpp index 8c1e23f4d5..bcbd65d919 100644 --- a/src/xrpld/app/tx/detail/DID.cpp +++ b/src/xrpld/app/tx/detail/DID.cpp @@ -42,12 +42,6 @@ namespace ripple { //------------------------------------------------------------------------------ -bool -DIDSet::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureDID); -} - NotTEC DIDSet::preflight(PreflightContext const& ctx) { @@ -168,12 +162,6 @@ DIDSet::doApply() return addSLE(ctx_, sleDID, account_); } -bool -DIDDelete::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureDID); -} - NotTEC DIDDelete::preflight(PreflightContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/DID.h b/src/xrpld/app/tx/detail/DID.h index c9fbb199b9..54ae6630f2 100644 --- a/src/xrpld/app/tx/detail/DID.h +++ b/src/xrpld/app/tx/detail/DID.h @@ -33,9 +33,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static NotTEC preflight(PreflightContext const& ctx); @@ -54,9 +51,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/DelegateSet.cpp b/src/xrpld/app/tx/detail/DelegateSet.cpp index 7119c9ac27..50fdfedbc7 100644 --- a/src/xrpld/app/tx/detail/DelegateSet.cpp +++ b/src/xrpld/app/tx/detail/DelegateSet.cpp @@ -27,12 +27,6 @@ namespace ripple { -bool -DelegateSet::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featurePermissionDelegation); -} - NotTEC DelegateSet::preflight(PreflightContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/DelegateSet.h b/src/xrpld/app/tx/detail/DelegateSet.h index c95b0f8fd4..c72b1e3c58 100644 --- a/src/xrpld/app/tx/detail/DelegateSet.h +++ b/src/xrpld/app/tx/detail/DelegateSet.h @@ -33,9 +33,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/DeleteOracle.cpp b/src/xrpld/app/tx/detail/DeleteOracle.cpp index ac078e5c9d..5831bddb93 100644 --- a/src/xrpld/app/tx/detail/DeleteOracle.cpp +++ b/src/xrpld/app/tx/detail/DeleteOracle.cpp @@ -26,12 +26,6 @@ namespace ripple { -bool -DeleteOracle::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featurePriceOracle); -} - NotTEC DeleteOracle::preflight(PreflightContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/DeleteOracle.h b/src/xrpld/app/tx/detail/DeleteOracle.h index f55b5d0f50..bbbfc6f525 100644 --- a/src/xrpld/app/tx/detail/DeleteOracle.h +++ b/src/xrpld/app/tx/detail/DeleteOracle.h @@ -42,9 +42,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/DepositPreauth.cpp b/src/xrpld/app/tx/detail/DepositPreauth.cpp index 3d1cbf7381..4e7df27210 100644 --- a/src/xrpld/app/tx/detail/DepositPreauth.cpp +++ b/src/xrpld/app/tx/detail/DepositPreauth.cpp @@ -33,9 +33,6 @@ namespace ripple { bool DepositPreauth::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureDepositPreauth)) - return false; - bool const authArrPresent = ctx.tx.isFieldPresent(sfAuthorizeCredentials); bool const unauthArrPresent = ctx.tx.isFieldPresent(sfUnauthorizeCredentials); diff --git a/src/xrpld/app/tx/detail/LedgerStateFix.cpp b/src/xrpld/app/tx/detail/LedgerStateFix.cpp index eeb67bbc91..d20eeca011 100644 --- a/src/xrpld/app/tx/detail/LedgerStateFix.cpp +++ b/src/xrpld/app/tx/detail/LedgerStateFix.cpp @@ -27,12 +27,6 @@ namespace ripple { -bool -LedgerStateFix::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(fixNFTokenPageLinks); -} - NotTEC LedgerStateFix::preflight(PreflightContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/LedgerStateFix.h b/src/xrpld/app/tx/detail/LedgerStateFix.h index b93dcd6a00..bff51a723c 100644 --- a/src/xrpld/app/tx/detail/LedgerStateFix.h +++ b/src/xrpld/app/tx/detail/LedgerStateFix.h @@ -37,9 +37,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp index 521b81944e..351462c445 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp @@ -26,12 +26,6 @@ namespace ripple { -bool -MPTokenAuthorize::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureMPTokensV1); -} - std::uint32_t MPTokenAuthorize::getFlagsMask(PreflightContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.h b/src/xrpld/app/tx/detail/MPTokenAuthorize.h index 60419e1827..43a962e24e 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.h +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.h @@ -42,9 +42,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static std::uint32_t getFlagsMask(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp index 8ca1294dcc..18e2d219b9 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp @@ -28,9 +28,6 @@ namespace ripple { bool MPTokenIssuanceCreate::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureMPTokensV1)) - return false; - if (ctx.tx.isFieldPresent(sfDomainID) && !(ctx.rules.enabled(featurePermissionedDomains) && ctx.rules.enabled(featureSingleAssetVault))) diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp index d52dcca8bf..2f6c8e24db 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp @@ -25,12 +25,6 @@ namespace ripple { -bool -MPTokenIssuanceDestroy::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureMPTokensV1); -} - std::uint32_t MPTokenIssuanceDestroy::getFlagsMask(PreflightContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h index dd2131713b..2cebdb7352 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.h @@ -33,9 +33,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static std::uint32_t getFlagsMask(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp index d74e3dccb6..e54767ffdd 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp @@ -29,9 +29,6 @@ namespace ripple { bool MPTokenIssuanceSet::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureMPTokensV1)) - return false; - return !ctx.tx.isFieldPresent(sfDomainID) || (ctx.rules.enabled(featurePermissionedDomains) && ctx.rules.enabled(featureSingleAssetVault)); diff --git a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp index 6cb8e89c40..4d88288a12 100644 --- a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp +++ b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp @@ -27,12 +27,6 @@ namespace ripple { -bool -NFTokenAcceptOffer::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureNonFungibleTokensV1); -} - std::uint32_t NFTokenAcceptOffer::getFlagsMask(PreflightContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.h b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.h index b049aad46e..995581d1ff 100644 --- a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.h +++ b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.h @@ -51,9 +51,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static std::uint32_t getFlagsMask(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/NFTokenBurn.cpp b/src/xrpld/app/tx/detail/NFTokenBurn.cpp index 80aec56af6..cb1b564402 100644 --- a/src/xrpld/app/tx/detail/NFTokenBurn.cpp +++ b/src/xrpld/app/tx/detail/NFTokenBurn.cpp @@ -26,12 +26,6 @@ namespace ripple { -bool -NFTokenBurn::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureNonFungibleTokensV1); -} - NotTEC NFTokenBurn::preflight(PreflightContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/NFTokenBurn.h b/src/xrpld/app/tx/detail/NFTokenBurn.h index f1ae0752db..3f5296c217 100644 --- a/src/xrpld/app/tx/detail/NFTokenBurn.h +++ b/src/xrpld/app/tx/detail/NFTokenBurn.h @@ -33,9 +33,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/NFTokenCancelOffer.cpp b/src/xrpld/app/tx/detail/NFTokenCancelOffer.cpp index 66edddcc82..92a4d2f4dc 100644 --- a/src/xrpld/app/tx/detail/NFTokenCancelOffer.cpp +++ b/src/xrpld/app/tx/detail/NFTokenCancelOffer.cpp @@ -28,12 +28,6 @@ namespace ripple { -bool -NFTokenCancelOffer::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureNonFungibleTokensV1); -} - std::uint32_t NFTokenCancelOffer::getFlagsMask(PreflightContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/NFTokenCancelOffer.h b/src/xrpld/app/tx/detail/NFTokenCancelOffer.h index bdd628e58d..b35be0e757 100644 --- a/src/xrpld/app/tx/detail/NFTokenCancelOffer.h +++ b/src/xrpld/app/tx/detail/NFTokenCancelOffer.h @@ -33,9 +33,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static std::uint32_t getFlagsMask(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp b/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp index 76ebc9e24d..9e6b0fadcb 100644 --- a/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp +++ b/src/xrpld/app/tx/detail/NFTokenCreateOffer.cpp @@ -26,12 +26,6 @@ namespace ripple { -bool -NFTokenCreateOffer::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureNonFungibleTokensV1); -} - std::uint32_t NFTokenCreateOffer::getFlagsMask(PreflightContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/NFTokenCreateOffer.h b/src/xrpld/app/tx/detail/NFTokenCreateOffer.h index 58946bad55..0a1c631298 100644 --- a/src/xrpld/app/tx/detail/NFTokenCreateOffer.h +++ b/src/xrpld/app/tx/detail/NFTokenCreateOffer.h @@ -33,9 +33,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static std::uint32_t getFlagsMask(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/NFTokenMint.cpp b/src/xrpld/app/tx/detail/NFTokenMint.cpp index db49e122e2..98f7a75627 100644 --- a/src/xrpld/app/tx/detail/NFTokenMint.cpp +++ b/src/xrpld/app/tx/detail/NFTokenMint.cpp @@ -49,9 +49,6 @@ hasOfferFields(PreflightContext const& ctx) bool NFTokenMint::isEnabled(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureNonFungibleTokensV1)) - return false; - return ctx.rules.enabled(featureNFTokenMintOffer) || !hasOfferFields(ctx); } diff --git a/src/xrpld/app/tx/detail/NFTokenModify.cpp b/src/xrpld/app/tx/detail/NFTokenModify.cpp index 98cbd5760a..2a1a348187 100644 --- a/src/xrpld/app/tx/detail/NFTokenModify.cpp +++ b/src/xrpld/app/tx/detail/NFTokenModify.cpp @@ -28,8 +28,7 @@ namespace ripple { bool NFTokenModify::isEnabled(PreflightContext const& ctx) { - return ctx.rules.enabled(featureNonFungibleTokensV1_1) && - ctx.rules.enabled(featureDynamicNFT); + return ctx.rules.enabled(featureNonFungibleTokensV1_1); } NotTEC diff --git a/src/xrpld/app/tx/detail/PermissionedDomainDelete.cpp b/src/xrpld/app/tx/detail/PermissionedDomainDelete.cpp index 6087bae8ad..5d5828c9c9 100644 --- a/src/xrpld/app/tx/detail/PermissionedDomainDelete.cpp +++ b/src/xrpld/app/tx/detail/PermissionedDomainDelete.cpp @@ -24,12 +24,6 @@ namespace ripple { -bool -PermissionedDomainDelete::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featurePermissionedDomains); -} - NotTEC PermissionedDomainDelete::preflight(PreflightContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/PermissionedDomainDelete.h b/src/xrpld/app/tx/detail/PermissionedDomainDelete.h index 403d0786e3..8b5fa68e59 100644 --- a/src/xrpld/app/tx/detail/PermissionedDomainDelete.h +++ b/src/xrpld/app/tx/detail/PermissionedDomainDelete.h @@ -33,9 +33,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/PermissionedDomainSet.cpp b/src/xrpld/app/tx/detail/PermissionedDomainSet.cpp index 8e27fdddd6..755772c244 100644 --- a/src/xrpld/app/tx/detail/PermissionedDomainSet.cpp +++ b/src/xrpld/app/tx/detail/PermissionedDomainSet.cpp @@ -31,8 +31,7 @@ namespace ripple { bool PermissionedDomainSet::isEnabled(PreflightContext const& ctx) { - return ctx.rules.enabled(featurePermissionedDomains) && - ctx.rules.enabled(featureCredentials); + return ctx.rules.enabled(featureCredentials); } NotTEC diff --git a/src/xrpld/app/tx/detail/SetOracle.cpp b/src/xrpld/app/tx/detail/SetOracle.cpp index 98c4530fba..2bdbb7cf6a 100644 --- a/src/xrpld/app/tx/detail/SetOracle.cpp +++ b/src/xrpld/app/tx/detail/SetOracle.cpp @@ -36,12 +36,6 @@ tokenPairKey(STObject const& pair) pair.getFieldCurrency(sfQuoteAsset).currency()); } -bool -SetOracle::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featurePriceOracle); -} - NotTEC SetOracle::preflight(PreflightContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/SetOracle.h b/src/xrpld/app/tx/detail/SetOracle.h index 2da10105b2..656b656019 100644 --- a/src/xrpld/app/tx/detail/SetOracle.h +++ b/src/xrpld/app/tx/detail/SetOracle.h @@ -42,9 +42,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/Transactor.h b/src/xrpld/app/tx/detail/Transactor.h index 9b5638d9e1..939b23cf93 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -203,7 +203,8 @@ public: /* Do NOT define an invokePreflight function in a derived class. Instead, define: - // Optional if the transaction is gated on an amendment + // Optional if the transaction is gated on an amendment that + // isn't specified in transactions.macro static bool isEnabled(PreflightContext const& ctx); @@ -389,10 +390,6 @@ template NotTEC Transactor::invokePreflight(PreflightContext const& ctx) { - // TODO: If #5650 is merged, use its transaction -> amendment lookup here to - // do a first-pass check. Rewrite or remove any `isEnabled` overloads that - // check those default amendments. - // Using this lookup does NOT require checking the fixDelegateV1_1. The data // exists regardless of whether it is enabled. auto const feature = From 5c072579d71a4ebffaccf9e66cf61577d272ad73 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 16 Sep 2025 18:50:24 -0400 Subject: [PATCH 082/291] Review feedback from @yinyiqian1 and @Bronek - Rewrite all of the templates in Units.h to use concepts. - Restrict to_short_string to reasonably sized values. - Rephrase some comments, and fix some typos. --- include/xrpl/basics/base_uint.h | 10 +- include/xrpl/protocol/TxFlags.h | 4 +- include/xrpl/protocol/Units.h | 115 +++++++-------------- include/xrpl/protocol/detail/sfields.macro | 2 +- src/test/jtx/amount.h | 1 - src/xrpld/app/tx/detail/InvariantCheck.cpp | 12 +-- 6 files changed, 51 insertions(+), 93 deletions(-) diff --git a/include/xrpl/basics/base_uint.h b/include/xrpl/basics/base_uint.h index 5cd1c5aa26..b1a4622cc4 100644 --- a/include/xrpl/basics/base_uint.h +++ b/include/xrpl/basics/base_uint.h @@ -636,12 +636,10 @@ template inline std::string to_short_string(base_uint const& a) { - // LCOV_EXCL_START - if constexpr (base_uint::bytes <= 4) - return to_string(a); - else - // LCOV_EXCL_STOP - return strHex(a.cbegin(), a.cbegin() + 4) + "..."; + static_assert( + base_uint::bytes > 4, + "For 4 bytes or less, use a native type"); + return strHex(a.cbegin(), a.cbegin() + 4) + "..."; } template diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index 4eb311bcb5..ebbccd591c 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -143,8 +143,8 @@ constexpr std::uint32_t const tfTransferable = 0x00000008; constexpr std::uint32_t const tfMutable = 0x00000010; // MPTokenIssuanceCreate flags: -// NOTE - there is intentionally no flag here for lsfMPTLocked, which -// this transaction cannot mutate. +// Note: tf/lsfMPTLocked is intentionally omitted, since this transaction +// is not allowed to modify it. constexpr std::uint32_t const tfMPTCanLock = lsfMPTCanLock; constexpr std::uint32_t const tfMPTRequireAuth = lsfMPTRequireAuth; constexpr std::uint32_t const tfMPTCanEscrow = lsfMPTCanEscrow; diff --git a/include/xrpl/protocol/Units.h b/include/xrpl/protocol/Units.h index 9459f5cb82..6b2ae67059 100644 --- a/include/xrpl/protocol/Units.h +++ b/include/xrpl/protocol/Units.h @@ -77,6 +77,16 @@ concept Compatible = Valid && std::is_arithmetic_v && std::is_arithmetic_v && std::is_convertible_v; +template +concept Integral = std::is_integral_v; + +template +concept IntegralValue = Integral; + +template +concept CastableValue = IntegralValue && IntegralValue && + std::is_same_v; + template class ValueUnit : private boost::totally_ordered>, private boost::additive>, @@ -218,8 +228,8 @@ public: return *this; } - template - std::enable_if_t, ValueUnit&> + template + ValueUnit& operator%=(value_type const& rhs) { value_ %= rhs; @@ -358,49 +368,27 @@ to_string(ValueUnit const& amount) return std::to_string(amount.value()); } -template -constexpr bool can_muldiv_source_v = +template +concept muldivSource = Valid && std::is_convertible_v; -template -constexpr bool can_muldiv_dest_v = - can_muldiv_source_v && // Dest is also a source +template +concept muldivDest = muldivSource && // Dest is also a source std::is_convertible_v && sizeof(typename Dest::value_type) >= sizeof(std::uint64_t); -template -constexpr bool can_muldiv_sources_v = - can_muldiv_source_v && can_muldiv_source_v && +template +concept muldivSources = muldivSource && muldivSource && std::is_same_v; -template -constexpr bool can_muldiv_v = - can_muldiv_sources_v && can_muldiv_dest_v; +template +concept muldivable = muldivSources && muldivDest; // Source and Dest can be the same by default -template -constexpr bool can_muldiv_commute_v = can_muldiv_v && +template +concept muldivCommutable = muldivable && !std::is_same_v; -template -using enable_muldiv_source_t = - typename std::enable_if_t>; - -template -using enable_muldiv_dest_t = typename std::enable_if_t>; - -template -using enable_muldiv_sources_t = - typename std::enable_if_t>; - -template -using enable_muldiv_t = - typename std::enable_if_t>; - -template -using enable_muldiv_commute_t = - typename std::enable_if_t>; - template ValueUnit scalar(T value) @@ -408,11 +396,7 @@ scalar(T value) return ValueUnit{value}; } -template < - class Source1, - class Source2, - class Dest, - class = enable_muldiv_t> +template Dest> std::optional mulDivU(Source1 value, Dest mul, Source2 div) { @@ -426,7 +410,7 @@ mulDivU(Source1 value, Dest mul, Source2 div) XRPL_ASSERT( mul.value() >= 0, "ripple::unit::mulDivU : minimum mul input"); XRPL_ASSERT( - div.value() >= 0, "ripple::unit::mulDivU : minimum div input"); + div.value() > 0, "ripple::unit::mulDivU : minimum div input"); return std::nullopt; } @@ -477,11 +461,7 @@ using TenthBips = unit::ValueUnit; using TenthBips16 = TenthBips; using TenthBips32 = TenthBips; -template < - class Source1, - class Source2, - class Dest, - class = unit::enable_muldiv_t> +template Dest> std::optional mulDiv(Source1 value, Dest mul, Source2 div) { @@ -491,8 +471,7 @@ mulDiv(Source1 value, Dest mul, Source2 div) template < class Source1, class Source2, - class Dest, - class = unit::enable_muldiv_commute_t> + unit::muldivCommutable Dest> std::optional mulDiv(Dest value, Source1 mul, Source2 div) { @@ -500,7 +479,7 @@ mulDiv(Dest value, Source1 mul, Source2 div) return unit::mulDivU(mul, value, div); } -template > +template std::optional mulDiv(std::uint64_t value, Dest mul, std::uint64_t div) { @@ -509,7 +488,7 @@ mulDiv(std::uint64_t value, Dest mul, std::uint64_t div) return unit::mulDivU(unit::scalar(value), mul, unit::scalar(div)); } -template > +template std::optional mulDiv(Dest value, std::uint64_t mul, std::uint64_t div) { @@ -517,10 +496,7 @@ mulDiv(Dest value, std::uint64_t mul, std::uint64_t div) return mulDiv(mul, value, div); } -template < - class Source1, - class Source2, - class = unit::enable_muldiv_sources_t> +template Source2> std::optional mulDiv(Source1 value, std::uint64_t mul, Source2 div) { @@ -534,10 +510,7 @@ mulDiv(Source1 value, std::uint64_t mul, Source2 div) return unitresult->value(); } -template < - class Source1, - class Source2, - class = unit::enable_muldiv_sources_t> +template Source2> std::optional mulDiv(std::uint64_t value, Source1 mul, Source2 div) { @@ -545,44 +518,32 @@ mulDiv(std::uint64_t value, Source1 mul, Source2 div) return mulDiv(mul, value, div); } -template -constexpr std::enable_if_t< - std::is_same_v && - std::is_integral_v && - std::is_integral_v, - Dest> +template Src> +constexpr Dest safe_cast(Src s) noexcept { // Dest may not have an explicit value constructor return Dest{safe_cast(s.value())}; } -template -constexpr std::enable_if_t< - std::is_integral_v && std::is_integral_v, - Dest> +template +constexpr Dest safe_cast(Src s) noexcept { // Dest may not have an explicit value constructor return Dest{safe_cast(s)}; } -template -constexpr std::enable_if_t< - std::is_same_v && - std::is_integral_v && - std::is_integral_v, - Dest> +template Src> +constexpr Dest unsafe_cast(Src s) noexcept { // Dest may not have an explicit value constructor return Dest{unsafe_cast(s.value())}; } -template -constexpr std::enable_if_t< - std::is_integral_v && std::is_integral_v, - Dest> +template +constexpr Dest unsafe_cast(Src s) noexcept { // Dest may not have an explicit value constructor diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 12371b6839..10fe015dac 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -175,7 +175,7 @@ TYPED_SFIELD(sfEmitParentTxnID, UINT256, 11) TYPED_SFIELD(sfEmitNonce, UINT256, 12) TYPED_SFIELD(sfEmitHookHash, UINT256, 13) TYPED_SFIELD(sfAMMID, UINT256, 14, - SField::sMD_PseudoAccount |SField::sMD_Default) + SField::sMD_PseudoAccount | SField::sMD_Default) // 256-bit (uncommon) TYPED_SFIELD(sfBookDirectory, UINT256, 16) diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h index fbe78bf3ec..a793f3a287 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -34,7 +34,6 @@ #include namespace ripple { - namespace detail { struct epsilon_multiple diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 7af93a5e3e..40062104bb 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -41,7 +41,7 @@ namespace ripple { assert(enforce) There are several asserts (or XRPL_ASSERTs) in this file that check a variable -named `enforce` when an invariant fails. At first glace, those asserts may look +named `enforce` when an invariant fails. At first glance, those asserts may look incorrect, but they are not. Those asserts take advantage of two facts: @@ -49,11 +49,11 @@ Those asserts take advantage of two facts: 2. Invariants should *never* fail, except in tests that specifically modify the open ledger to break them. -To make it sort of a second-layer of invariant enforcement aimed at -_developers_. It's designed to fire if a developer writes code that violates an -invariant, and runs it in unit tests or a develop build that _does not have the -relevant amendments enabled_. It's intentionally a pain in the neck so that bad -code gets caught and fixed as early as possible. +This makes `assert(enforce)` sort of a second-layer of invariant enforcement +aimed at _developers_. It's designed to fire if a developer writes code that +violates an invariant, and runs it in unit tests or a develop build that _does +not have the relevant amendments enabled_. It's intentionally a pain in the neck +so that bad code gets caught and fixed as early as possible. */ enum Privilege { From 1fbd3fcfa430cf8f98015293343f3fb57220e804 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 16 Sep 2025 19:31:01 -0400 Subject: [PATCH 083/291] Handle Flags not present in LoanManage - Addresses RIPD-3418 --- src/test/app/Loan_test.cpp | 6 ++++++ src/xrpld/app/tx/detail/LoanManage.cpp | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index c2492bc75b..b1863ea4b6 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -658,6 +658,12 @@ class Loan_test : public beast::unit_test::suite // Manage the loan // no-op env(manage(lender, keylet.key, 0)); + { + // no flags + auto jt = manage(lender, keylet.key, 0); + jt.removeMember(sfFlags.getName()); + env(jt); + } // Only the lender can manage env(manage(evan, keylet.key, 0), ter(tecNO_PERMISSION)); // unknown flags diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index 345457e6f5..94ffd2dfd6 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -64,7 +64,7 @@ LoanManage::preflight(PreflightContext const& ctx) return temINVALID; // Flags are mutually exclusive - auto const flags = ctx.tx[sfFlags] & tfUniversalMask; + auto const flags = ctx.tx[~sfFlags].value_or(0) & tfUniversalMask; if ((flags & (flags - 1)) != 0) { JLOG(ctx.j.warn()) From d1c708ca01d45c16597ed0a7543948b6604b5684 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 17 Sep 2025 13:12:02 -0400 Subject: [PATCH 084/291] Remove A BUNCH of unnecessary includes from the Loan/Lending files --- src/test/app/LoanBroker_test.cpp | 24 +---------------- src/test/app/Loan_test.cpp | 27 +------------------ src/xrpld/app/misc/LendingHelpers.h | 11 -------- src/xrpld/app/misc/detail/LendingHelpers.cpp | 1 - .../app/tx/detail/LoanBrokerCoverClawback.cpp | 21 --------------- .../app/tx/detail/LoanBrokerCoverDeposit.cpp | 19 ------------- .../app/tx/detail/LoanBrokerCoverWithdraw.cpp | 19 ------------- src/xrpld/app/tx/detail/LoanBrokerDelete.cpp | 19 ------------- src/xrpld/app/tx/detail/LoanBrokerSet.cpp | 8 ------ src/xrpld/app/tx/detail/LoanDelete.cpp | 21 --------------- src/xrpld/app/tx/detail/LoanDraw.cpp | 21 --------------- src/xrpld/app/tx/detail/LoanManage.cpp | 20 -------------- src/xrpld/app/tx/detail/LoanPay.cpp | 21 --------------- src/xrpld/app/tx/detail/LoanSet.cpp | 10 ------- 14 files changed, 2 insertions(+), 240 deletions(-) diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 9f10db22d6..7fee30943c 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -17,31 +17,9 @@ */ //============================================================================== -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include namespace ripple { namespace test { diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index b1863ea4b6..6f45a284a9 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -17,38 +17,13 @@ */ //============================================================================== -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include #include #include #include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include namespace ripple { namespace test { diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 022e9557e9..77e5abd12c 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -20,21 +20,10 @@ #ifndef RIPPLE_APP_MISC_LENDINGHELPERS_H_INCLUDED #define RIPPLE_APP_MISC_LENDINGHELPERS_H_INCLUDED -#include #include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include - namespace ripple { struct PreflightContext; diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index 9ade4883a0..25b6a47fd6 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -19,7 +19,6 @@ #include // -#include #include namespace ripple { diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp index 7dcc37dd04..3b078ff665 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp @@ -20,27 +20,6 @@ #include // #include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include namespace ripple { diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp index 5b9e8bba0e..43a5d56b1e 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp @@ -20,25 +20,6 @@ #include // #include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include namespace ripple { diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp index 58bb390ce1..8e2e02ff39 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp @@ -21,25 +21,6 @@ // #include #include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include namespace ripple { diff --git a/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp b/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp index d5eeb9c152..3cb6495a49 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp @@ -20,25 +20,6 @@ #include // #include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include namespace ripple { diff --git a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp index b3e6527c17..7abb26cdce 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp @@ -20,14 +20,6 @@ #include // #include -#include -#include - -#include -#include -#include -#include -#include namespace ripple { diff --git a/src/xrpld/app/tx/detail/LoanDelete.cpp b/src/xrpld/app/tx/detail/LoanDelete.cpp index 4e075d8e41..e9e98fe730 100644 --- a/src/xrpld/app/tx/detail/LoanDelete.cpp +++ b/src/xrpld/app/tx/detail/LoanDelete.cpp @@ -20,27 +20,6 @@ #include // #include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include namespace ripple { diff --git a/src/xrpld/app/tx/detail/LoanDraw.cpp b/src/xrpld/app/tx/detail/LoanDraw.cpp index 79ec742d5d..6665c30942 100644 --- a/src/xrpld/app/tx/detail/LoanDraw.cpp +++ b/src/xrpld/app/tx/detail/LoanDraw.cpp @@ -21,27 +21,6 @@ #include // #include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include namespace ripple { diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index 94ffd2dfd6..fb68358d73 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -20,28 +20,8 @@ #include // #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include namespace ripple { diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 15b7fcb867..e871ca11f6 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -20,27 +20,6 @@ #include // #include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include namespace ripple { diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 3d44b895d4..1317595202 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -20,18 +20,8 @@ #include // #include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include namespace ripple { From 2c0c4567f4fbc4fa6d9f16fb29ddc3fc1c4abe4f Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 17 Sep 2025 15:41:03 -0400 Subject: [PATCH 085/291] Rewrite LoanManage flag check to skip if field not set or 0 --- src/xrpld/app/tx/detail/LoanManage.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index fb68358d73..546ab27eb6 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -44,13 +44,16 @@ LoanManage::preflight(PreflightContext const& ctx) return temINVALID; // Flags are mutually exclusive - auto const flags = ctx.tx[~sfFlags].value_or(0) & tfUniversalMask; - if ((flags & (flags - 1)) != 0) + if (auto const flagField = ctx.tx[~sfFlags]; flagField && *flagField) { - JLOG(ctx.j.warn()) - << "LoanManage: Only one of tfLoanDefault, tfLoanImpair, or " - "tfLoanUnimpair can be set."; - return temINVALID_FLAG; + auto const flags = *flagField & tfUniversalMask; + if ((flags & (flags - 1)) != 0) + { + JLOG(ctx.j.warn()) + << "LoanManage: Only one of tfLoanDefault, tfLoanImpair, or " + "tfLoanUnimpair can be set."; + return temINVALID_FLAG; + } } return tesSUCCESS; From b9a2eb3399b3adf717af954a2d7d7ee98ba89ca0 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 17 Sep 2025 16:54:50 -0400 Subject: [PATCH 086/291] Review feedback from @gregtatcam - Remove a redundant struct LoanPaymentParts declaration. - Add pointers to the XLS-66 spec for all of the LendingHeler formulas. - Changed if/else if in LoanManage. --- src/xrpld/app/misc/LendingHelpers.h | 32 +++++++++++++++----- src/xrpld/app/misc/detail/LendingHelpers.cpp | 22 ++++++++++++++ src/xrpld/app/tx/detail/LoanManage.cpp | 4 +-- 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 77e5abd12c..5e7c58883d 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -36,14 +36,6 @@ namespace detail { // These functions should rarely be used directly. More often, the ultimate // result needs to be roundToAsset'd. -struct LoanPaymentParts -{ - Number principalPaid; - Number interestPaid; - Number valueChange; - Number feePaid; -}; - Number loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval); @@ -70,6 +62,10 @@ loanTotalValueOutstanding( { return roundToAsset( asset, + /* + * This formula is from the XLS-66 spec, section 3.2.4.2 (Total Loan + * Value Calculation), specifically "totalValueOutstanding = ..." + */ periodicPayment * paymentsRemaining, originalPrincipal, Number::upward); @@ -85,6 +81,10 @@ loanTotalValueOutstanding( std::uint32_t paymentInterval, std::uint32_t paymentsRemaining) { + /* + * This function is derived from the XLS-66 spec, section 3.2.4.2 (Total + * Loan Value Calculation) + */ return loanTotalValueOutstanding( asset, originalPrincipal, @@ -101,6 +101,10 @@ loanTotalInterestOutstanding( Number principalOutstanding, Number totalValueOutstanding) { + /* + * This formula is from the XLS-66 spec, section 3.2.4.2 (Total Loan + * Value Calculation), specifically "totalInterestOutstanding = ..." + */ return totalValueOutstanding - principalOutstanding; } @@ -114,6 +118,10 @@ loanTotalInterestOutstanding( std::uint32_t paymentInterval, std::uint32_t paymentsRemaining) { + /* + * This formula is derived from the XLS-66 spec, section 3.2.4.2 (Total Loan + * Value Calculation) + */ return loanTotalInterestOutstanding( principalOutstanding, loanTotalValueOutstanding( @@ -246,6 +254,10 @@ computePeriodicPaymentParts( Number const& periodicRate, std::uint32_t paymentRemaining) { + /* + * This function is derived from the XLS-66 spec, section 3.2.4.1.1 (Regular + * Payment) + */ if (paymentRemaining == 1) { // If there's only one payment left, we need to pay off the principal. @@ -313,6 +325,10 @@ loanComputePaymentParts( STAmount const& amount, beast::Journal j) { + /* + * This function is an implementation of the XLS-66 spec, section 3.2.4.3 + * (Transaction Pseudo-code) + */ Number const originalPrincipalRequested = loan->at(sfPrincipalRequested); auto principalOutstandingField = loan->at(sfPrincipalOutstanding); bool const allowOverpayment = loan->isFlag(lsfLoanOverpayment); diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index 25b6a47fd6..2cf31d9c68 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -37,6 +37,11 @@ loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval) { // Need floating point math for this one, since we're dividing by some // large numbers + /* + * This formula is from the XLS-66 spec, section 3.2.4.1.1 (Regular + * Payment), specifically "periodicRate = ...", though it is duplicated in + * other places. + */ return tenthBipsOfValue(Number(paymentInterval), interestRate) / (365 * 24 * 60 * 60); } @@ -54,6 +59,11 @@ loanPeriodicPayment( if (periodicRate == beast::zero) return principalOutstanding / paymentsRemaining; + /* + * This formula is from the XLS-66 spec, section 3.2.4.1.1 (Regular + * Payment), though the awkwardly-named "timeFactor" is computed only once + * and used twice. + */ // TODO: Need a better name Number const timeFactor = power(1 + periodicRate, paymentsRemaining); @@ -69,6 +79,10 @@ loanPeriodicPayment( { if (principalOutstanding == 0 || paymentsRemaining == 0) return 0; + /* + * This function is derived from the XLS-66 spec, section 3.2.4.1.1 (Regular + * payment), though it is duplicated in other places. + */ Number const periodicRate = loanPeriodicRate(interestRate, paymentInterval); return loanPeriodicPayment( @@ -83,6 +97,10 @@ loanLatePaymentInterest( std::uint32_t startDate, std::uint32_t prevPaymentDate) { + /* + * This formula is from the XLS-66 spec, section 3.2.4.1.2 (Late payment), + * specifically "latePaymentInterest = ..." + */ auto const lastPaymentDate = std::max(prevPaymentDate, startDate); auto const secondsSinceLastPayment = @@ -103,6 +121,10 @@ loanAccruedInterest( std::uint32_t prevPaymentDate, std::uint32_t paymentInterval) { + /* + * This formula is from the XLS-66 spec, section 3.2.4.1.4 (Early Full + * Repayment), specifically "accruedInterest = ...". + */ auto const lastPaymentDate = std::max(prevPaymentDate, startDate); auto const secondsSinceLastPayment = diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index 546ab27eb6..498466f10e 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -399,7 +399,7 @@ LoanManage::doApply() j_)) return ter; } - if (tx.isFlag(tfLoanImpair)) + else if (tx.isFlag(tfLoanImpair)) { if (auto const ter = impairLoan( view, @@ -413,7 +413,7 @@ LoanManage::doApply() j_)) return ter; } - if (tx.isFlag(tfLoanUnimpair)) + else if (tx.isFlag(tfLoanUnimpair)) { if (auto const ter = unimpairLoan( view, From b5ddc812eaf9846805c316c0df2de1467611cb31 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 17 Sep 2025 19:37:59 -0400 Subject: [PATCH 087/291] Review feedback from @gregtatcam: - In progress - Add explanatory comments, and start refactoring loanComputePaymentParts into functions for readability. --- include/xrpl/protocol/STAmount.h | 19 ++++- src/libxrpl/basics/Number.cpp | 2 - src/libxrpl/protocol/STAmount.cpp | 15 ++-- src/xrpld/app/misc/LendingHelpers.h | 124 +++++++++++++++++++--------- src/xrpld/app/tx/detail/LoanPay.cpp | 18 +++- 5 files changed, 130 insertions(+), 48 deletions(-) diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index 8c455af85e..d3f67da4af 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -695,6 +695,17 @@ divRoundStrict( std::uint64_t getRate(STAmount const& offerOut, STAmount const& offerIn); +/** Round an arbitrary precision Amount to the precision of a reference Amount. + * + * This is used to ensure that calculations involving IOU amounts do not collect + * dust beyond the precision of the reference value. + * + * @param value The value to be rounded + * @param referenceValue A reference value to establish the precision limit of + * `value`. Should be larger than `value`. + * @param rounding Optional Number rounding mode + * + */ STAmount roundToReference( STAmount const value, @@ -702,12 +713,16 @@ roundToReference( Number::rounding_mode rounding = Number::getround()); /** Round an arbitrary precision Number to the precision of a given Asset. + * + * This is used to ensure that calculations do not collect dust beyond the + * precision of the reference value for IOUs, or fractional amounts for the + * integral types XRP and MPT. * * @param asset The relevant asset * @param value The value to be rounded * @param referenceValue Only relevant to IOU assets. A reference value to * establish the precision limit of `value`. Should be larger than - * `value`. + * `value`. * @param rounding Optional Number rounding mode */ template @@ -722,6 +737,8 @@ roundToAsset( STAmount const ret{asset, value}; if (ret.asset().native() || !ret.asset().holds()) return ret; + // Not that the ctor will round integral types (XRP, MPT) via canonicalize, + // so no extra work is needed for those. return roundToReference(ret, STAmount{asset, referenceValue}); } diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 26bdf68918..f43288b57b 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -20,8 +20,6 @@ #include #include -#include - #include #include #include diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 78d9321083..15524d605a 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -1515,20 +1515,25 @@ roundToReference( STAmount referenceValue, Number::rounding_mode rounding) { + // Nothing to do for intgral types. if (value.asset().native() || !value.asset().holds()) return value; - NumberRoundModeGuard mg(rounding); - if (referenceValue.negative() != value.negative()) - referenceValue.negate(); - - if (value.exponent() > referenceValue.exponent() && + // If the value is greater than or equal to the referenceValue (ignoring + // sign), then rounding will do nothing, so just return the value. + if (value.exponent() > referenceValue.exponent() || (value.exponent() == referenceValue.exponent() && value.mantissa() >= referenceValue.mantissa())) return value; + + if (referenceValue.negative() != value.negative()) + referenceValue.negate(); + + NumberRoundModeGuard mg(rounding); // With an IOU, the total will be truncated to the precision of the // larger value: referenceValue STAmount const total = referenceValue + value; + // Remove the reference value, and we're left with the rounded value. STAmount const result = total - referenceValue; return result; } diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 5e7c58883d..0400f16026 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -316,6 +316,72 @@ struct LoanPaymentParts Number feePaid; }; +struct LatePaymentParams +{ +}; + +template +Expected +handleLatePayment( + A const& asset, + ApplyView& view, + STObject::ValueProxy& principalOutstandingField, + STObject::ValueProxy& paymentRemainingField, + STObject::ValueProxy& prevPaymentDateField, + STObject::ValueProxy& nextDueDateField, + PeriodicPaymentParts const& periodic, + std::uint32_t const startDate, + std::uint32_t const paymentInterval, + TenthBips32 const lateInterestRate, + Number const& originalPrincipalRequested, + Number const& periodicPaymentAmount, + Number const& latePaymentFee, + STAmount const& amount, + beast::Journal j) +{ + if (!hasExpired(view, nextDueDateField)) + return Unexpected(tesSUCCESS); + + // the payment is late + auto const latePaymentInterest = loanLatePaymentInterest( + asset, + principalOutstandingField, + lateInterestRate, + view.parentCloseTime(), + startDate, + prevPaymentDateField, + originalPrincipalRequested); + XRPL_ASSERT( + latePaymentInterest >= 0, + "ripple::loanComputePaymentParts : valid late interest"); + auto const latePaymentAmount = + periodicPaymentAmount + latePaymentInterest + latePaymentFee; + + if (amount < latePaymentAmount) + { + JLOG(j.warn()) << "Late loan payment amount is insufficient. Due: " + << latePaymentAmount << ", paid: " << amount; + return Unexpected(tecINSUFFICIENT_PAYMENT); + } + + paymentRemainingField -= 1; + // A single payment always pays the same amount of principal. Only the + // interest and fees are extra for a late payment + principalOutstandingField -= periodic.principal; + + // Make sure this does an assignment + prevPaymentDateField = nextDueDateField; + nextDueDateField += paymentInterval; + + // A late payment increases the value of the loan by the difference + // between periodic and late payment interest + return LoanPaymentParts{ + periodic.principal, + latePaymentInterest + periodic.interest, + latePaymentInterest, + latePaymentFee}; +} + template Expected loanComputePaymentParts( @@ -408,47 +474,25 @@ loanComputePaymentParts( // ------------------------------------------------------------- // late payment handling - if (hasExpired(view, nextDueDateField)) - { - // the payment is late - auto const latePaymentInterest = loanLatePaymentInterest( + if (auto const latePaymentParts = handleLatePayment( asset, + view, principalOutstandingField, - lateInterestRate, - view.parentCloseTime(), - startDate, + paymentRemainingField, prevPaymentDateField, - originalPrincipalRequested); - XRPL_ASSERT( - latePaymentInterest >= 0, - "ripple::loanComputePaymentParts : valid late interest"); - auto const latePaymentAmount = - periodicPaymentAmount + latePaymentInterest + latePaymentFee; - - if (amount < latePaymentAmount) - { - JLOG(j.warn()) << "Late loan payment amount is insufficient. Due: " - << latePaymentAmount << ", paid: " << amount; - return Unexpected(tecINSUFFICIENT_PAYMENT); - } - - paymentRemainingField -= 1; - // A single payment always pays the same amount of principal. Only the - // interest and fees are extra for a late payment - principalOutstandingField -= periodic.principal; - - // Make sure this does an assignment - prevPaymentDateField = nextDueDateField; - nextDueDateField += paymentInterval; - - // A late payment increases the value of the loan by the difference - // between periodic and late payment interest - return LoanPaymentParts{ - periodic.principal, - latePaymentInterest + periodic.interest, - latePaymentInterest, - latePaymentFee}; - } + nextDueDateField, + periodic, + startDate, + paymentInterval, + lateInterestRate, + originalPrincipalRequested, + periodicPaymentAmount, + latePaymentFee, + amount, + j)) + return *latePaymentParts; + else if (latePaymentParts.error()) + return latePaymentParts; // ------------------------------------------------------------- // full payment handling @@ -505,7 +549,7 @@ loanComputePaymentParts( } // ------------------------------------------------------------- - // normal payment handling + // regular periodic payment handling // if the payment is not late nor if it's a full payment, then it must be a // periodic one, with possible overpayments @@ -582,6 +626,8 @@ loanComputePaymentParts( paymentRemainingField) + totalInterestPaid; + // ------------------------------------------------------------- + // overpayment handling Number overpaymentInterestPortion = 0; if (allowOverpayment) { diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index e871ca11f6..511c4f9f39 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -187,6 +187,13 @@ LoanPay::doApply() auto const totalPaidToBroker = paymentParts->feePaid + managementFee; + XRPL_ASSERT_PARTS( + (totalPaidToVault + totalPaidToBroker) == + (paymentParts->principalPaid + paymentParts->interestPaid + + paymentParts->feePaid), + "ripple::LoanPay::doApply", + "payments add up"); + // If there is not enough first-loss capital auto coverAvailableField = brokerSle->at(sfCoverAvailable); auto debtTotalField = brokerSle->at(sfDebtTotal); @@ -201,6 +208,8 @@ LoanPay::doApply() // Add the fee to to First Loss Cover Pool coverAvailableField += totalPaidToBroker; } + auto const brokerPayee = + sufficientCover ? brokerOwner : brokerPseudoAccount; // Decrease LoanBroker Debt by the amount paid, add the Loan value change, // and subtract the change in the management fee @@ -241,6 +250,13 @@ LoanPay::doApply() "ripple::LoanPay::doApply", "payment agreement"); + auto const accountBalanceBefore = + accountHolds(view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); + auto const vaultBalanceBefore = accountHolds( + view, vaultPseudoAccount, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); + auto const brokerBalanceBefore = accountHolds( + view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); + if (auto const ter = accountSend( view, account_, @@ -252,7 +268,7 @@ LoanPay::doApply() if (auto const ter = accountSend( view, account_, - sufficientCover ? brokerOwner : brokerPseudoAccount, + brokerPayee, paidToBroker, j_, WaiveTransferFee::Yes)) From 55baf35d9cb469e4755b8a379bde57d504c671b1 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 18 Sep 2025 13:30:50 -0400 Subject: [PATCH 088/291] Fix build errors due to private type access --- src/xrpld/app/misc/LendingHelpers.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 68cd695235..90644a7308 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -319,15 +319,15 @@ struct LatePaymentParams { }; -template +template Expected handleLatePayment( A const& asset, ApplyView& view, - STObject::ValueProxy& principalOutstandingField, - STObject::ValueProxy& paymentRemainingField, - STObject::ValueProxy& prevPaymentDateField, - STObject::ValueProxy& nextDueDateField, + NumberProxy& principalOutstandingField, + Int32Proxy& paymentRemainingField, + Int32Proxy& prevPaymentDateField, + Int32Proxy& nextDueDateField, PeriodicPaymentParts const& periodic, std::uint32_t const startDate, std::uint32_t const paymentInterval, From 9e84c373063987a3659783906a4f580ac685afdf Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 18 Sep 2025 14:54:49 -0400 Subject: [PATCH 089/291] Refactor full payment into a function, and update some formatting --- src/xrpld/app/misc/LendingHelpers.h | 167 ++++++++++++++++++---------- 1 file changed, 109 insertions(+), 58 deletions(-) diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 90644a7308..980d42689f 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -319,6 +319,13 @@ struct LatePaymentParams { }; +/* Handle possible late payments. + * + * If this function processed a late payment, the return value will be + * a LoanPaymentParts object. If the loan is not late, the return will be an + * Unexpected(tesSUCCESS). Otherwise, it'll be an Unexpected with the error code + * the caller is expected to return. + */ template Expected handleLatePayment( @@ -381,6 +388,83 @@ handleLatePayment( latePaymentFee}; } +/* Handle possible full payments. + * + * If this function processed a full payment, the return value will be + * a LoanPaymentParts object. If the payment should not be considered as a full + * payment, the return will be an Unexpected(tesSUCCESS). Otherwise, it'll be an + * Unexpected with the error code the caller is expected to return. + */ +template +Expected +handleFullPayment( + A const& asset, + ApplyView& view, + NumberProxy& principalOutstandingField, + Int32Proxy& paymentRemainingField, + Int32Proxy& prevPaymentDateField, + std::uint32_t const startDate, + std::uint32_t const paymentInterval, + TenthBips32 const closeInterestRate, + Number const& originalPrincipalRequested, + Number const& totalInterestOutstanding, + Number const& periodicRate, + Number const& closePaymentFee, + STAmount const& amount, + beast::Journal j) +{ + if (paymentRemainingField <= 1) + // If this is the last payment, it has to be a regular payment + return Unexpected(tesSUCCESS); + + // If there is more than one payment remaining, see if enough was + // paid for a full payment + auto const accruedInterest = roundToAsset( + asset, + detail::loanAccruedInterest( + principalOutstandingField, + periodicRate, + view.parentCloseTime(), + startDate, + prevPaymentDateField, + paymentInterval), + originalPrincipalRequested); + XRPL_ASSERT( + accruedInterest >= 0, + "ripple::loanComputePaymentParts : valid accrued interest"); + auto const closePrepaymentInterest = roundToAsset( + asset, + tenthBipsOfValue(principalOutstandingField.value(), closeInterestRate), + originalPrincipalRequested); + XRPL_ASSERT( + closePrepaymentInterest >= 0, + "ripple::loanComputePaymentParts : valid prepayment " + "interest"); + auto const totalInterest = accruedInterest + closePrepaymentInterest; + auto const closeFullPayment = + principalOutstandingField + totalInterest + closePaymentFee; + + if (amount < closeFullPayment) + // If the payment is less than the full payment amount, it's not + // sufficient to be a full payment. + return Unexpected(tesSUCCESS); + + // Make a full payment + + // A full payment decreases the value of the loan by the + // difference between the interest paid and the expected + // outstanding interest return + auto const valueChange = totalInterest - totalInterestOutstanding; + + LoanPaymentParts const result{ + principalOutstandingField, totalInterest, valueChange, closePaymentFee}; + + paymentRemainingField = 0; + principalOutstandingField = 0; + + return result; +} + template Expected loanComputePaymentParts( @@ -391,8 +475,8 @@ loanComputePaymentParts( beast::Journal j) { /* - * This function is an implementation of the XLS-66 spec, section 3.2.4.3 - * (Transaction Pseudo-code) + * This function is an implementation of the XLS-66 spec, + * section 3.2.4.3 (Transaction Pseudo-code) */ Number const originalPrincipalRequested = loan->at(sfPrincipalRequested); auto principalOutstandingField = loan->at(sfPrincipalOutstanding); @@ -432,8 +516,8 @@ loanComputePaymentParts( interestRate == 0 || periodicRate > 0, "ripple::loanComputePaymentParts : valid rate"); - // Don't round the payment amount. Only round the final computations using - // it. + // Don't round the payment amount. Only round the final computations + // using it. Number const periodicPaymentAmount = detail::loanPeriodicPayment( principalOutstandingField, periodicRate, paymentRemainingField); XRPL_ASSERT( @@ -495,63 +579,30 @@ loanComputePaymentParts( // ------------------------------------------------------------- // full payment handling - if (paymentRemainingField > 1) - { - // If there is more than one payment remaining, see if enough was paid - // for a full payment - auto const accruedInterest = roundToAsset( + if (auto const fullPaymentParts = handleFullPayment( asset, - detail::loanAccruedInterest( - principalOutstandingField, - periodicRate, - view.parentCloseTime(), - startDate, - prevPaymentDateField, - paymentInterval), - originalPrincipalRequested); - XRPL_ASSERT( - accruedInterest >= 0, - "ripple::loanComputePaymentParts : valid accrued interest"); - auto const closePrepaymentInterest = roundToAsset( - asset, - tenthBipsOfValue( - principalOutstandingField.value(), closeInterestRate), - originalPrincipalRequested); - XRPL_ASSERT( - closePrepaymentInterest >= 0, - "ripple::loanComputePaymentParts : valid prepayment " - "interest"); - auto const totalInterest = accruedInterest + closePrepaymentInterest; - auto const closeFullPayment = - principalOutstandingField + totalInterest + closePaymentFee; - - // if the payment is equal or higher than full payment amount, make a - // full payment - if (amount >= closeFullPayment) - { - // A full payment decreases the value of the loan by the - // difference between the interest paid and the expected - // outstanding interest return - auto const valueChange = totalInterest - totalInterestOutstanding; - - LoanPaymentParts const result{ - principalOutstandingField, - totalInterest, - valueChange, - closePaymentFee}; - - paymentRemainingField = 0; - principalOutstandingField = 0; - - return result; - } - } + view, + principalOutstandingField, + paymentRemainingField, + prevPaymentDateField, + startDate, + paymentInterval, + closeInterestRate, + originalPrincipalRequested, + totalInterestOutstanding, + periodicRate, + closePaymentFee, + amount, + j)) + return *fullPaymentParts; + else if (fullPaymentParts.error()) + return fullPaymentParts; // ------------------------------------------------------------- // regular periodic payment handling - // if the payment is not late nor if it's a full payment, then it must be a - // periodic one, with possible overpayments + // if the payment is not late nor if it's a full payment, then it must + // be a periodic one, with possible overpayments auto const totalDue = roundToAsset( asset, @@ -649,8 +700,8 @@ loanComputePaymentParts( overpayment - interestPortion - feePortion, originalPrincipalRequested); - // Don't process an overpayment if the whole amount (or more!) gets - // eaten by fees + // Don't process an overpayment if the whole amount (or more!) + // gets eaten by fees if (remainder > 0) { overpaymentInterestPortion = interestPortion; From 267b537a1341c37d4739f769a8ab2a5936cde310 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sat, 20 Sep 2025 17:09:48 -0400 Subject: [PATCH 090/291] Review feedback from @Bronek - Fix a moved assert message - Rename `Transactor::isEnabled` to `checkExtraFeatures`, and finish cleaning up a few classes I missed on the first pass (Vault* & XChainBridge*). --- src/libxrpl/protocol/Permissions.cpp | 2 +- src/xrpld/app/tx/detail/AMMBid.cpp | 2 +- src/xrpld/app/tx/detail/AMMBid.h | 2 +- src/xrpld/app/tx/detail/AMMCreate.cpp | 2 +- src/xrpld/app/tx/detail/AMMCreate.h | 2 +- src/xrpld/app/tx/detail/AMMDelete.cpp | 2 +- src/xrpld/app/tx/detail/AMMDelete.h | 2 +- src/xrpld/app/tx/detail/AMMDeposit.cpp | 2 +- src/xrpld/app/tx/detail/AMMDeposit.h | 2 +- src/xrpld/app/tx/detail/AMMVote.cpp | 2 +- src/xrpld/app/tx/detail/AMMVote.h | 2 +- src/xrpld/app/tx/detail/AMMWithdraw.cpp | 2 +- src/xrpld/app/tx/detail/AMMWithdraw.h | 2 +- src/xrpld/app/tx/detail/CreateOffer.cpp | 2 +- src/xrpld/app/tx/detail/CreateOffer.h | 2 +- src/xrpld/app/tx/detail/DeleteAccount.cpp | 2 +- src/xrpld/app/tx/detail/DeleteAccount.h | 2 +- src/xrpld/app/tx/detail/DepositPreauth.cpp | 2 +- src/xrpld/app/tx/detail/DepositPreauth.h | 2 +- src/xrpld/app/tx/detail/Escrow.cpp | 2 +- src/xrpld/app/tx/detail/Escrow.h | 2 +- .../app/tx/detail/MPTokenIssuanceCreate.cpp | 2 +- .../app/tx/detail/MPTokenIssuanceCreate.h | 2 +- .../app/tx/detail/MPTokenIssuanceSet.cpp | 2 +- src/xrpld/app/tx/detail/MPTokenIssuanceSet.h | 2 +- src/xrpld/app/tx/detail/NFTokenMint.cpp | 2 +- src/xrpld/app/tx/detail/NFTokenMint.h | 2 +- src/xrpld/app/tx/detail/NFTokenModify.cpp | 2 +- src/xrpld/app/tx/detail/NFTokenModify.h | 2 +- src/xrpld/app/tx/detail/PayChan.cpp | 2 +- src/xrpld/app/tx/detail/PayChan.h | 2 +- src/xrpld/app/tx/detail/Payment.cpp | 2 +- src/xrpld/app/tx/detail/Payment.h | 2 +- .../app/tx/detail/PermissionedDomainSet.cpp | 2 +- .../app/tx/detail/PermissionedDomainSet.h | 2 +- src/xrpld/app/tx/detail/Transactor.h | 10 ++-- src/xrpld/app/tx/detail/VaultClawback.cpp | 6 --- src/xrpld/app/tx/detail/VaultClawback.h | 3 -- src/xrpld/app/tx/detail/VaultCreate.cpp | 5 +- src/xrpld/app/tx/detail/VaultCreate.h | 2 +- src/xrpld/app/tx/detail/VaultDelete.cpp | 6 --- src/xrpld/app/tx/detail/VaultDelete.h | 3 -- src/xrpld/app/tx/detail/VaultDeposit.cpp | 6 --- src/xrpld/app/tx/detail/VaultDeposit.h | 3 -- src/xrpld/app/tx/detail/VaultSet.cpp | 5 +- src/xrpld/app/tx/detail/VaultSet.h | 2 +- src/xrpld/app/tx/detail/VaultWithdraw.cpp | 6 --- src/xrpld/app/tx/detail/VaultWithdraw.h | 3 -- src/xrpld/app/tx/detail/XChainBridge.cpp | 48 ------------------- src/xrpld/app/tx/detail/XChainBridge.h | 24 ---------- 50 files changed, 45 insertions(+), 157 deletions(-) diff --git a/src/libxrpl/protocol/Permissions.cpp b/src/libxrpl/protocol/Permissions.cpp index af4b7e42c4..eb0c0489d1 100644 --- a/src/libxrpl/protocol/Permissions.cpp +++ b/src/libxrpl/protocol/Permissions.cpp @@ -137,7 +137,7 @@ Permission::getTxFeature(TxType txType) const auto const txFeaturesIt = txFeatureMap_.find(txType); XRPL_ASSERT( txFeaturesIt != txFeatureMap_.end(), - "ripple::Permissions::isDelegatable : tx exists in txFeatureMap_"); + "ripple::Permissions::getTxFeature : tx exists in txFeatureMap_"); if (txFeaturesIt->second == uint256{}) return std::nullopt; diff --git a/src/xrpld/app/tx/detail/AMMBid.cpp b/src/xrpld/app/tx/detail/AMMBid.cpp index 169292ab45..769668b07b 100644 --- a/src/xrpld/app/tx/detail/AMMBid.cpp +++ b/src/xrpld/app/tx/detail/AMMBid.cpp @@ -31,7 +31,7 @@ namespace ripple { bool -AMMBid::isEnabled(PreflightContext const& ctx) +AMMBid::checkExtraFeatures(PreflightContext const& ctx) { return ammEnabled(ctx.rules); } diff --git a/src/xrpld/app/tx/detail/AMMBid.h b/src/xrpld/app/tx/detail/AMMBid.h index 9e69aca471..4a527b6a93 100644 --- a/src/xrpld/app/tx/detail/AMMBid.h +++ b/src/xrpld/app/tx/detail/AMMBid.h @@ -72,7 +72,7 @@ public: } static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/AMMCreate.cpp b/src/xrpld/app/tx/detail/AMMCreate.cpp index 3c6a57cd19..63e20b42fb 100644 --- a/src/xrpld/app/tx/detail/AMMCreate.cpp +++ b/src/xrpld/app/tx/detail/AMMCreate.cpp @@ -32,7 +32,7 @@ namespace ripple { bool -AMMCreate::isEnabled(PreflightContext const& ctx) +AMMCreate::checkExtraFeatures(PreflightContext const& ctx) { return ammEnabled(ctx.rules); } diff --git a/src/xrpld/app/tx/detail/AMMCreate.h b/src/xrpld/app/tx/detail/AMMCreate.h index aab4a4fb27..98231e5554 100644 --- a/src/xrpld/app/tx/detail/AMMCreate.h +++ b/src/xrpld/app/tx/detail/AMMCreate.h @@ -64,7 +64,7 @@ public: } static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/AMMDelete.cpp b/src/xrpld/app/tx/detail/AMMDelete.cpp index f1338d5275..663a4c4b0a 100644 --- a/src/xrpld/app/tx/detail/AMMDelete.cpp +++ b/src/xrpld/app/tx/detail/AMMDelete.cpp @@ -28,7 +28,7 @@ namespace ripple { bool -AMMDelete::isEnabled(PreflightContext const& ctx) +AMMDelete::checkExtraFeatures(PreflightContext const& ctx) { return ammEnabled(ctx.rules); } diff --git a/src/xrpld/app/tx/detail/AMMDelete.h b/src/xrpld/app/tx/detail/AMMDelete.h index 1c44571021..36dace2e18 100644 --- a/src/xrpld/app/tx/detail/AMMDelete.h +++ b/src/xrpld/app/tx/detail/AMMDelete.h @@ -40,7 +40,7 @@ public: } static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/AMMDeposit.cpp b/src/xrpld/app/tx/detail/AMMDeposit.cpp index 23c0f1a5a2..8a3e50ed63 100644 --- a/src/xrpld/app/tx/detail/AMMDeposit.cpp +++ b/src/xrpld/app/tx/detail/AMMDeposit.cpp @@ -30,7 +30,7 @@ namespace ripple { bool -AMMDeposit::isEnabled(PreflightContext const& ctx) +AMMDeposit::checkExtraFeatures(PreflightContext const& ctx) { return ammEnabled(ctx.rules); } diff --git a/src/xrpld/app/tx/detail/AMMDeposit.h b/src/xrpld/app/tx/detail/AMMDeposit.h index 09e1f040ff..c1a37be452 100644 --- a/src/xrpld/app/tx/detail/AMMDeposit.h +++ b/src/xrpld/app/tx/detail/AMMDeposit.h @@ -69,7 +69,7 @@ public: } static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); static std::uint32_t getFlagsMask(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/AMMVote.cpp b/src/xrpld/app/tx/detail/AMMVote.cpp index 4fc7a5b3d5..0ffbb38b37 100644 --- a/src/xrpld/app/tx/detail/AMMVote.cpp +++ b/src/xrpld/app/tx/detail/AMMVote.cpp @@ -28,7 +28,7 @@ namespace ripple { bool -AMMVote::isEnabled(PreflightContext const& ctx) +AMMVote::checkExtraFeatures(PreflightContext const& ctx) { return ammEnabled(ctx.rules); } diff --git a/src/xrpld/app/tx/detail/AMMVote.h b/src/xrpld/app/tx/detail/AMMVote.h index af7691a3b4..dc99480111 100644 --- a/src/xrpld/app/tx/detail/AMMVote.h +++ b/src/xrpld/app/tx/detail/AMMVote.h @@ -57,7 +57,7 @@ public: } static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/AMMWithdraw.cpp b/src/xrpld/app/tx/detail/AMMWithdraw.cpp index d6590178ba..f5af9dfb9c 100644 --- a/src/xrpld/app/tx/detail/AMMWithdraw.cpp +++ b/src/xrpld/app/tx/detail/AMMWithdraw.cpp @@ -29,7 +29,7 @@ namespace ripple { bool -AMMWithdraw::isEnabled(PreflightContext const& ctx) +AMMWithdraw::checkExtraFeatures(PreflightContext const& ctx) { return ammEnabled(ctx.rules); } diff --git a/src/xrpld/app/tx/detail/AMMWithdraw.h b/src/xrpld/app/tx/detail/AMMWithdraw.h index 5cb8f4de31..31a7904626 100644 --- a/src/xrpld/app/tx/detail/AMMWithdraw.h +++ b/src/xrpld/app/tx/detail/AMMWithdraw.h @@ -77,7 +77,7 @@ public: } static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); static std::uint32_t getFlagsMask(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/CreateOffer.cpp b/src/xrpld/app/tx/detail/CreateOffer.cpp index cf8c22bdbc..5bd2752222 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.cpp +++ b/src/xrpld/app/tx/detail/CreateOffer.cpp @@ -44,7 +44,7 @@ CreateOffer::makeTxConsequences(PreflightContext const& ctx) } bool -CreateOffer::isEnabled(PreflightContext const& ctx) +CreateOffer::checkExtraFeatures(PreflightContext const& ctx) { if (ctx.tx.isFieldPresent(sfDomainID) && !ctx.rules.enabled(featurePermissionedDEX)) diff --git a/src/xrpld/app/tx/detail/CreateOffer.h b/src/xrpld/app/tx/detail/CreateOffer.h index 3a20cd9ea1..c38e244b34 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.h +++ b/src/xrpld/app/tx/detail/CreateOffer.h @@ -44,7 +44,7 @@ public: makeTxConsequences(PreflightContext const& ctx); static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); static std::uint32_t getFlagsMask(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/DeleteAccount.cpp b/src/xrpld/app/tx/detail/DeleteAccount.cpp index a5a8bd592f..364a53b50b 100644 --- a/src/xrpld/app/tx/detail/DeleteAccount.cpp +++ b/src/xrpld/app/tx/detail/DeleteAccount.cpp @@ -39,7 +39,7 @@ namespace ripple { bool -DeleteAccount::isEnabled(PreflightContext const& ctx) +DeleteAccount::checkExtraFeatures(PreflightContext const& ctx) { if (!ctx.rules.enabled(featureDeletableAccounts)) return false; diff --git a/src/xrpld/app/tx/detail/DeleteAccount.h b/src/xrpld/app/tx/detail/DeleteAccount.h index a67267ad7b..ee9db97d50 100644 --- a/src/xrpld/app/tx/detail/DeleteAccount.h +++ b/src/xrpld/app/tx/detail/DeleteAccount.h @@ -34,7 +34,7 @@ public: } static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/DepositPreauth.cpp b/src/xrpld/app/tx/detail/DepositPreauth.cpp index e666ac996e..bbbec3cbe7 100644 --- a/src/xrpld/app/tx/detail/DepositPreauth.cpp +++ b/src/xrpld/app/tx/detail/DepositPreauth.cpp @@ -31,7 +31,7 @@ namespace ripple { bool -DepositPreauth::isEnabled(PreflightContext const& ctx) +DepositPreauth::checkExtraFeatures(PreflightContext const& ctx) { bool const authArrPresent = ctx.tx.isFieldPresent(sfAuthorizeCredentials); bool const unauthArrPresent = diff --git a/src/xrpld/app/tx/detail/DepositPreauth.h b/src/xrpld/app/tx/detail/DepositPreauth.h index 384b098aef..ead17742cd 100644 --- a/src/xrpld/app/tx/detail/DepositPreauth.h +++ b/src/xrpld/app/tx/detail/DepositPreauth.h @@ -34,7 +34,7 @@ public: } static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/Escrow.cpp b/src/xrpld/app/tx/detail/Escrow.cpp index e6b7ad05a6..969fd4dd4c 100644 --- a/src/xrpld/app/tx/detail/Escrow.cpp +++ b/src/xrpld/app/tx/detail/Escrow.cpp @@ -631,7 +631,7 @@ checkCondition(Slice f, Slice c) } bool -EscrowFinish::isEnabled(PreflightContext const& ctx) +EscrowFinish::checkExtraFeatures(PreflightContext const& ctx) { return !ctx.tx.isFieldPresent(sfCredentialIDs) || ctx.rules.enabled(featureCredentials); diff --git a/src/xrpld/app/tx/detail/Escrow.h b/src/xrpld/app/tx/detail/Escrow.h index fd08b4764a..8956be2939 100644 --- a/src/xrpld/app/tx/detail/Escrow.h +++ b/src/xrpld/app/tx/detail/Escrow.h @@ -61,7 +61,7 @@ public: } static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); static std::uint32_t getFlagsMask(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp index b4e457b606..eec4187573 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp @@ -26,7 +26,7 @@ namespace ripple { bool -MPTokenIssuanceCreate::isEnabled(PreflightContext const& ctx) +MPTokenIssuanceCreate::checkExtraFeatures(PreflightContext const& ctx) { if (ctx.tx.isFieldPresent(sfDomainID) && !(ctx.rules.enabled(featurePermissionedDomains) && diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h index fc79134a78..842ed88641 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.h @@ -51,7 +51,7 @@ public: } static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); static std::uint32_t getFlagsMask(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp index 1c25490dc1..c406a8ec5f 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp @@ -27,7 +27,7 @@ namespace ripple { bool -MPTokenIssuanceSet::isEnabled(PreflightContext const& ctx) +MPTokenIssuanceSet::checkExtraFeatures(PreflightContext const& ctx) { return !ctx.tx.isFieldPresent(sfDomainID) || (ctx.rules.enabled(featurePermissionedDomains) && diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h index 55d6110fcd..f63812097e 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h @@ -34,7 +34,7 @@ public: } static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); static std::uint32_t getFlagsMask(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/NFTokenMint.cpp b/src/xrpld/app/tx/detail/NFTokenMint.cpp index 34d42830a8..8149d3b59d 100644 --- a/src/xrpld/app/tx/detail/NFTokenMint.cpp +++ b/src/xrpld/app/tx/detail/NFTokenMint.cpp @@ -47,7 +47,7 @@ hasOfferFields(PreflightContext const& ctx) } bool -NFTokenMint::isEnabled(PreflightContext const& ctx) +NFTokenMint::checkExtraFeatures(PreflightContext const& ctx) { return ctx.rules.enabled(featureNFTokenMintOffer) || !hasOfferFields(ctx); } diff --git a/src/xrpld/app/tx/detail/NFTokenMint.h b/src/xrpld/app/tx/detail/NFTokenMint.h index 696ad856ab..1606514559 100644 --- a/src/xrpld/app/tx/detail/NFTokenMint.h +++ b/src/xrpld/app/tx/detail/NFTokenMint.h @@ -37,7 +37,7 @@ public: } static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); static std::uint32_t getFlagsMask(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/NFTokenModify.cpp b/src/xrpld/app/tx/detail/NFTokenModify.cpp index 2a1a348187..6ae095411b 100644 --- a/src/xrpld/app/tx/detail/NFTokenModify.cpp +++ b/src/xrpld/app/tx/detail/NFTokenModify.cpp @@ -26,7 +26,7 @@ namespace ripple { bool -NFTokenModify::isEnabled(PreflightContext const& ctx) +NFTokenModify::checkExtraFeatures(PreflightContext const& ctx) { return ctx.rules.enabled(featureNonFungibleTokensV1_1); } diff --git a/src/xrpld/app/tx/detail/NFTokenModify.h b/src/xrpld/app/tx/detail/NFTokenModify.h index 8a80e422c5..04784381fb 100644 --- a/src/xrpld/app/tx/detail/NFTokenModify.h +++ b/src/xrpld/app/tx/detail/NFTokenModify.h @@ -34,7 +34,7 @@ public: } static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/PayChan.cpp b/src/xrpld/app/tx/detail/PayChan.cpp index 3255df878a..32c0abeb93 100644 --- a/src/xrpld/app/tx/detail/PayChan.cpp +++ b/src/xrpld/app/tx/detail/PayChan.cpp @@ -423,7 +423,7 @@ PayChanFund::doApply() //------------------------------------------------------------------------------ bool -PayChanClaim::isEnabled(PreflightContext const& ctx) +PayChanClaim::checkExtraFeatures(PreflightContext const& ctx) { return !ctx.tx.isFieldPresent(sfCredentialIDs) || ctx.rules.enabled(featureCredentials); diff --git a/src/xrpld/app/tx/detail/PayChan.h b/src/xrpld/app/tx/detail/PayChan.h index 9390aa52b9..b25a4529be 100644 --- a/src/xrpld/app/tx/detail/PayChan.h +++ b/src/xrpld/app/tx/detail/PayChan.h @@ -89,7 +89,7 @@ public: } static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); static std::uint32_t getFlagsMask(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index 6c77b7b7f8..8bc0e891d0 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -66,7 +66,7 @@ getMaxSourceAmount( } bool -Payment::isEnabled(PreflightContext const& ctx) +Payment::checkExtraFeatures(PreflightContext const& ctx) { if (ctx.tx.isFieldPresent(sfCredentialIDs) && !ctx.rules.enabled(featureCredentials)) diff --git a/src/xrpld/app/tx/detail/Payment.h b/src/xrpld/app/tx/detail/Payment.h index 3f16dd97bc..04bba390e2 100644 --- a/src/xrpld/app/tx/detail/Payment.h +++ b/src/xrpld/app/tx/detail/Payment.h @@ -43,7 +43,7 @@ public: makeTxConsequences(PreflightContext const& ctx); static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); static std::uint32_t getFlagsMask(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/PermissionedDomainSet.cpp b/src/xrpld/app/tx/detail/PermissionedDomainSet.cpp index 2f7f4f49a8..d9fa481bb6 100644 --- a/src/xrpld/app/tx/detail/PermissionedDomainSet.cpp +++ b/src/xrpld/app/tx/detail/PermissionedDomainSet.cpp @@ -29,7 +29,7 @@ namespace ripple { bool -PermissionedDomainSet::isEnabled(PreflightContext const& ctx) +PermissionedDomainSet::checkExtraFeatures(PreflightContext const& ctx) { return ctx.rules.enabled(featureCredentials); } diff --git a/src/xrpld/app/tx/detail/PermissionedDomainSet.h b/src/xrpld/app/tx/detail/PermissionedDomainSet.h index 01e4bb86d0..ed27896a3b 100644 --- a/src/xrpld/app/tx/detail/PermissionedDomainSet.h +++ b/src/xrpld/app/tx/detail/PermissionedDomainSet.h @@ -34,7 +34,7 @@ public: } static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/Transactor.h b/src/xrpld/app/tx/detail/Transactor.h index 939b23cf93..88b0664ea2 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -206,7 +206,7 @@ public: // Optional if the transaction is gated on an amendment that // isn't specified in transactions.macro static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); // Optional if the transaction uses any flags other than tfUniversal static std::uint32_t @@ -222,7 +222,7 @@ public: * Do not try to call preflight1 or preflight2 directly. * Do not check whether relevant amendments are enabled in preflight. - Instead, define isEnabled. + Instead, define checkExtraFeatures. * Do not check flags in preflight. Instead, define getFlagsMask. */ template @@ -283,7 +283,7 @@ protected: // Base class always returns true static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); // Base class always returns tfUniversalMask static std::uint32_t @@ -352,7 +352,7 @@ private: }; inline bool -Transactor::isEnabled(PreflightContext const& ctx) +Transactor::checkExtraFeatures(PreflightContext const& ctx) { return true; } @@ -398,7 +398,7 @@ Transactor::invokePreflight(PreflightContext const& ctx) if (feature && !ctx.rules.enabled(*feature)) return temDISABLED; - if (!T::isEnabled(ctx)) + if (!T::checkExtraFeatures(ctx)) return temDISABLED; if (auto const ret = preflight1(ctx, T::getFlagsMask(ctx))) diff --git a/src/xrpld/app/tx/detail/VaultClawback.cpp b/src/xrpld/app/tx/detail/VaultClawback.cpp index e2ed24a096..45a56a6292 100644 --- a/src/xrpld/app/tx/detail/VaultClawback.cpp +++ b/src/xrpld/app/tx/detail/VaultClawback.cpp @@ -32,12 +32,6 @@ namespace ripple { -bool -VaultClawback::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureSingleAssetVault); -} - NotTEC VaultClawback::preflight(PreflightContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/VaultClawback.h b/src/xrpld/app/tx/detail/VaultClawback.h index 58a7e738c3..65f0164686 100644 --- a/src/xrpld/app/tx/detail/VaultClawback.h +++ b/src/xrpld/app/tx/detail/VaultClawback.h @@ -33,9 +33,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/VaultCreate.cpp b/src/xrpld/app/tx/detail/VaultCreate.cpp index 29bf91907f..9447976a32 100644 --- a/src/xrpld/app/tx/detail/VaultCreate.cpp +++ b/src/xrpld/app/tx/detail/VaultCreate.cpp @@ -36,10 +36,9 @@ namespace ripple { bool -VaultCreate::isEnabled(PreflightContext const& ctx) +VaultCreate::checkExtraFeatures(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureSingleAssetVault) || - !ctx.rules.enabled(featureMPTokensV1)) + if (!ctx.rules.enabled(featureMPTokensV1)) return false; return !ctx.tx.isFieldPresent(sfDomainID) || diff --git a/src/xrpld/app/tx/detail/VaultCreate.h b/src/xrpld/app/tx/detail/VaultCreate.h index 54074c4bce..3f952d540a 100644 --- a/src/xrpld/app/tx/detail/VaultCreate.h +++ b/src/xrpld/app/tx/detail/VaultCreate.h @@ -34,7 +34,7 @@ public: } static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); static std::uint32_t getFlagsMask(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/VaultDelete.cpp b/src/xrpld/app/tx/detail/VaultDelete.cpp index 31ca73e389..ab7db78956 100644 --- a/src/xrpld/app/tx/detail/VaultDelete.cpp +++ b/src/xrpld/app/tx/detail/VaultDelete.cpp @@ -28,12 +28,6 @@ namespace ripple { -bool -VaultDelete::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureSingleAssetVault); -} - NotTEC VaultDelete::preflight(PreflightContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/VaultDelete.h b/src/xrpld/app/tx/detail/VaultDelete.h index f8b838e02e..2b77e84469 100644 --- a/src/xrpld/app/tx/detail/VaultDelete.h +++ b/src/xrpld/app/tx/detail/VaultDelete.h @@ -33,9 +33,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/VaultDeposit.cpp b/src/xrpld/app/tx/detail/VaultDeposit.cpp index d583f2e6e6..75cf81b0b0 100644 --- a/src/xrpld/app/tx/detail/VaultDeposit.cpp +++ b/src/xrpld/app/tx/detail/VaultDeposit.cpp @@ -33,12 +33,6 @@ namespace ripple { -bool -VaultDeposit::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureSingleAssetVault); -} - NotTEC VaultDeposit::preflight(PreflightContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/VaultDeposit.h b/src/xrpld/app/tx/detail/VaultDeposit.h index e804520595..50515ce3d8 100644 --- a/src/xrpld/app/tx/detail/VaultDeposit.h +++ b/src/xrpld/app/tx/detail/VaultDeposit.h @@ -33,9 +33,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/VaultSet.cpp b/src/xrpld/app/tx/detail/VaultSet.cpp index e1a0c58b56..6057e40cfa 100644 --- a/src/xrpld/app/tx/detail/VaultSet.cpp +++ b/src/xrpld/app/tx/detail/VaultSet.cpp @@ -31,11 +31,8 @@ namespace ripple { bool -VaultSet::isEnabled(PreflightContext const& ctx) +VaultSet::checkExtraFeatures(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureSingleAssetVault)) - return false; - return !ctx.tx.isFieldPresent(sfDomainID) || ctx.rules.enabled(featurePermissionedDomains); } diff --git a/src/xrpld/app/tx/detail/VaultSet.h b/src/xrpld/app/tx/detail/VaultSet.h index bce0077802..b3eecbbab5 100644 --- a/src/xrpld/app/tx/detail/VaultSet.h +++ b/src/xrpld/app/tx/detail/VaultSet.h @@ -34,7 +34,7 @@ public: } static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.cpp b/src/xrpld/app/tx/detail/VaultWithdraw.cpp index e4b1f3bdb2..509b795058 100644 --- a/src/xrpld/app/tx/detail/VaultWithdraw.cpp +++ b/src/xrpld/app/tx/detail/VaultWithdraw.cpp @@ -30,12 +30,6 @@ namespace ripple { -bool -VaultWithdraw::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureSingleAssetVault); -} - NotTEC VaultWithdraw::preflight(PreflightContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.h b/src/xrpld/app/tx/detail/VaultWithdraw.h index f01a8e4c9d..0b713d403b 100644 --- a/src/xrpld/app/tx/detail/VaultWithdraw.h +++ b/src/xrpld/app/tx/detail/VaultWithdraw.h @@ -33,9 +33,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/XChainBridge.cpp b/src/xrpld/app/tx/detail/XChainBridge.cpp index 4f8c2ab624..2587845df5 100644 --- a/src/xrpld/app/tx/detail/XChainBridge.cpp +++ b/src/xrpld/app/tx/detail/XChainBridge.cpp @@ -1367,12 +1367,6 @@ attestationDoApply(ApplyContext& ctx) } // namespace //------------------------------------------------------------------------------ -bool -XChainCreateBridge::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureXChainBridge); -} - NotTEC XChainCreateBridge::preflight(PreflightContext const& ctx) { @@ -1545,12 +1539,6 @@ XChainCreateBridge::doApply() //------------------------------------------------------------------------------ -bool -BridgeModify::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureXChainBridge); -} - std::uint32_t BridgeModify::getFlagsMask(PreflightContext const& ctx) { @@ -1658,12 +1646,6 @@ BridgeModify::doApply() //------------------------------------------------------------------------------ -bool -XChainClaim::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureXChainBridge); -} - NotTEC XChainClaim::preflight(PreflightContext const& ctx) { @@ -1893,12 +1875,6 @@ XChainCommit::makeTxConsequences(PreflightContext const& ctx) return TxConsequences{ctx.tx, maxSpend}; } -bool -XChainCommit::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureXChainBridge); -} - NotTEC XChainCommit::preflight(PreflightContext const& ctx) { @@ -2004,12 +1980,6 @@ XChainCommit::doApply() //------------------------------------------------------------------------------ -bool -XChainCreateClaimID::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureXChainBridge); -} - NotTEC XChainCreateClaimID::preflight(PreflightContext const& ctx) { @@ -2116,12 +2086,6 @@ XChainCreateClaimID::doApply() //------------------------------------------------------------------------------ -bool -XChainAddClaimAttestation::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureXChainBridge); -} - NotTEC XChainAddClaimAttestation::preflight(PreflightContext const& ctx) { @@ -2142,12 +2106,6 @@ XChainAddClaimAttestation::doApply() //------------------------------------------------------------------------------ -bool -XChainAddAccountCreateAttestation::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureXChainBridge); -} - NotTEC XChainAddAccountCreateAttestation::preflight(PreflightContext const& ctx) { @@ -2168,12 +2126,6 @@ XChainAddAccountCreateAttestation::doApply() //------------------------------------------------------------------------------ -bool -XChainCreateAccountCommit::isEnabled(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureXChainBridge); -} - NotTEC XChainCreateAccountCommit::preflight(PreflightContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/XChainBridge.h b/src/xrpld/app/tx/detail/XChainBridge.h index e210169bcf..0e9c0358d2 100644 --- a/src/xrpld/app/tx/detail/XChainBridge.h +++ b/src/xrpld/app/tx/detail/XChainBridge.h @@ -39,9 +39,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static NotTEC preflight(PreflightContext const& ctx); @@ -61,9 +58,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static std::uint32_t getFlagsMask(PreflightContext const& ctx); @@ -100,9 +94,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static NotTEC preflight(PreflightContext const& ctx); @@ -130,9 +121,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static NotTEC preflight(PreflightContext const& ctx); @@ -162,9 +150,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static NotTEC preflight(PreflightContext const& ctx); @@ -194,9 +179,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static NotTEC preflight(PreflightContext const& ctx); @@ -218,9 +200,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static NotTEC preflight(PreflightContext const& ctx); @@ -265,9 +244,6 @@ public: { } - static bool - isEnabled(PreflightContext const& ctx); - static NotTEC preflight(PreflightContext const& ctx); From 67578b8f50f6006f5cfb205e688a1a7ca1b4f802 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 22 Sep 2025 11:17:13 -0400 Subject: [PATCH 091/291] Review feedback from @mvadari - Clean up some of the flag and extra feature checks for readability --- src/xrpld/app/tx/detail/CreateOffer.cpp | 10 ++++++++-- src/xrpld/app/tx/detail/DeleteAccount.cpp | 7 +++++-- src/xrpld/app/tx/detail/DepositPreauth.cpp | 8 +++++--- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/xrpld/app/tx/detail/CreateOffer.cpp b/src/xrpld/app/tx/detail/CreateOffer.cpp index 5bd2752222..86750eb51d 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.cpp +++ b/src/xrpld/app/tx/detail/CreateOffer.cpp @@ -56,9 +56,12 @@ CreateOffer::checkExtraFeatures(PreflightContext const& ctx) std::uint32_t CreateOffer::getFlagsMask(PreflightContext const& ctx) { - if (ctx.rules.enabled(featurePermissionedDEX) && - ctx.tx.isFieldPresent(sfDomainID)) + // The tfOfferCreateMask is built assuming that PermissionedDEX is + // enabled + if (ctx.rules.enabled(featurePermissionedDEX)) return tfOfferCreateMask; + // If PermissionedDEX is not enabled, add tfHybrid to the mask, + // indicating it is not allowed. return tfOfferCreateMask | tfHybrid; } @@ -70,6 +73,9 @@ CreateOffer::preflight(PreflightContext const& ctx) std::uint32_t const uTxFlags = tx.getFlags(); + if (tx.isFlag(tfHybrid) && !tx.isFieldPresent(sfDomainID)) + return temINVALID_FLAG; + bool const bImmediateOrCancel(uTxFlags & tfImmediateOrCancel); bool const bFillOrKill(uTxFlags & tfFillOrKill); diff --git a/src/xrpld/app/tx/detail/DeleteAccount.cpp b/src/xrpld/app/tx/detail/DeleteAccount.cpp index 364a53b50b..565d938c83 100644 --- a/src/xrpld/app/tx/detail/DeleteAccount.cpp +++ b/src/xrpld/app/tx/detail/DeleteAccount.cpp @@ -44,8 +44,11 @@ DeleteAccount::checkExtraFeatures(PreflightContext const& ctx) if (!ctx.rules.enabled(featureDeletableAccounts)) return false; - return !ctx.tx.isFieldPresent(sfCredentialIDs) || - ctx.rules.enabled(featureCredentials); + if (ctx.tx.isFieldPresent(sfCredentialIDs) && + !ctx.rules.enabled(featureCredentials)) + return false; + + return true; } NotTEC diff --git a/src/xrpld/app/tx/detail/DepositPreauth.cpp b/src/xrpld/app/tx/detail/DepositPreauth.cpp index bbbec3cbe7..236b59a173 100644 --- a/src/xrpld/app/tx/detail/DepositPreauth.cpp +++ b/src/xrpld/app/tx/detail/DepositPreauth.cpp @@ -36,10 +36,12 @@ DepositPreauth::checkExtraFeatures(PreflightContext const& ctx) bool const authArrPresent = ctx.tx.isFieldPresent(sfAuthorizeCredentials); bool const unauthArrPresent = ctx.tx.isFieldPresent(sfUnauthorizeCredentials); - int const authCredPresent = - static_cast(authArrPresent) + static_cast(unauthArrPresent); + bool const authCredPresent = authArrPresent || unauthArrPresent; - return !authCredPresent || ctx.rules.enabled(featureCredentials); + if (authCredPresent && !ctx.rules.enabled(featureCredentials)) + return false; + + return true; } NotTEC From 9bfa88cd8618906f96d292478e782137e48c7fa4 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 24 Sep 2025 13:30:57 -0400 Subject: [PATCH 092/291] Fix build error - New code referenced `isEnabled`, which was renamed in an updated prereq PR --- src/xrpld/app/misc/LendingHelpers.h | 2 +- src/xrpld/app/misc/detail/LendingHelpers.cpp | 6 +++--- src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp | 4 ++-- src/xrpld/app/tx/detail/LoanBrokerCoverClawback.h | 2 +- src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp | 4 ++-- src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.h | 2 +- src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp | 4 ++-- src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h | 2 +- src/xrpld/app/tx/detail/LoanBrokerDelete.cpp | 4 ++-- src/xrpld/app/tx/detail/LoanBrokerDelete.h | 2 +- src/xrpld/app/tx/detail/LoanBrokerSet.cpp | 4 ++-- src/xrpld/app/tx/detail/LoanBrokerSet.h | 2 +- src/xrpld/app/tx/detail/LoanDelete.cpp | 4 ++-- src/xrpld/app/tx/detail/LoanDelete.h | 2 +- src/xrpld/app/tx/detail/LoanDraw.cpp | 4 ++-- src/xrpld/app/tx/detail/LoanDraw.h | 2 +- src/xrpld/app/tx/detail/LoanManage.cpp | 4 ++-- src/xrpld/app/tx/detail/LoanManage.h | 2 +- src/xrpld/app/tx/detail/LoanPay.cpp | 4 ++-- src/xrpld/app/tx/detail/LoanPay.h | 2 +- src/xrpld/app/tx/detail/LoanSet.cpp | 4 ++-- src/xrpld/app/tx/detail/LoanSet.h | 2 +- 22 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 980d42689f..b5fc10a35e 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -29,7 +29,7 @@ struct PreflightContext; // Lending protocol has dependencies, so capture them here. bool -lendingProtocolEnabled(PreflightContext const& ctx); +checkLendingProtocolDependencies(PreflightContext const& ctx); namespace detail { // These functions should rarely be used directly. More often, the ultimate diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index 2cf31d9c68..66d88728e5 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -24,10 +24,10 @@ namespace ripple { bool -lendingProtocolEnabled(PreflightContext const& ctx) +checkLendingProtocolDependencies(PreflightContext const& ctx) { - return ctx.rules.enabled(featureLendingProtocol) && - VaultCreate::isEnabled(ctx); + return ctx.rules.enabled(featureSingleAssetVault) && + VaultCreate::checkExtraFeatures(ctx); } namespace detail { diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp index 3b078ff665..e2358d8ace 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp @@ -24,9 +24,9 @@ namespace ripple { bool -LoanBrokerCoverClawback::isEnabled(PreflightContext const& ctx) +LoanBrokerCoverClawback::checkExtraFeatures(PreflightContext const& ctx) { - return lendingProtocolEnabled(ctx); + return checkLendingProtocolDependencies(ctx); } NotTEC diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.h b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.h index 68133b5364..500e8c243d 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.h +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.h @@ -34,7 +34,7 @@ public: } static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp index 43a5d56b1e..5aa3aba2cd 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp @@ -24,9 +24,9 @@ namespace ripple { bool -LoanBrokerCoverDeposit::isEnabled(PreflightContext const& ctx) +LoanBrokerCoverDeposit::checkExtraFeatures(PreflightContext const& ctx) { - return lendingProtocolEnabled(ctx); + return checkLendingProtocolDependencies(ctx); } NotTEC diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.h b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.h index 548b873dec..d3b88dc089 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.h +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.h @@ -34,7 +34,7 @@ public: } static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp index 8e2e02ff39..cb47c1953f 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp @@ -25,9 +25,9 @@ namespace ripple { bool -LoanBrokerCoverWithdraw::isEnabled(PreflightContext const& ctx) +LoanBrokerCoverWithdraw::checkExtraFeatures(PreflightContext const& ctx) { - return lendingProtocolEnabled(ctx); + return checkLendingProtocolDependencies(ctx); } NotTEC diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h index ce7eb79abe..27f6e7fe5b 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h @@ -34,7 +34,7 @@ public: } static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp b/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp index 3cb6495a49..2a35d83c31 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp @@ -24,9 +24,9 @@ namespace ripple { bool -LoanBrokerDelete::isEnabled(PreflightContext const& ctx) +LoanBrokerDelete::checkExtraFeatures(PreflightContext const& ctx) { - return lendingProtocolEnabled(ctx); + return checkLendingProtocolDependencies(ctx); } NotTEC diff --git a/src/xrpld/app/tx/detail/LoanBrokerDelete.h b/src/xrpld/app/tx/detail/LoanBrokerDelete.h index 41e1ff6853..21248a0331 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerDelete.h +++ b/src/xrpld/app/tx/detail/LoanBrokerDelete.h @@ -34,7 +34,7 @@ public: } static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp index 7abb26cdce..72f60053f2 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp @@ -24,9 +24,9 @@ namespace ripple { bool -LoanBrokerSet::isEnabled(PreflightContext const& ctx) +LoanBrokerSet::checkExtraFeatures(PreflightContext const& ctx) { - return lendingProtocolEnabled(ctx); + return checkLendingProtocolDependencies(ctx); } NotTEC diff --git a/src/xrpld/app/tx/detail/LoanBrokerSet.h b/src/xrpld/app/tx/detail/LoanBrokerSet.h index 7e4e02211e..c7a12875a2 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerSet.h +++ b/src/xrpld/app/tx/detail/LoanBrokerSet.h @@ -34,7 +34,7 @@ public: } static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/LoanDelete.cpp b/src/xrpld/app/tx/detail/LoanDelete.cpp index e9e98fe730..5d3fee69f6 100644 --- a/src/xrpld/app/tx/detail/LoanDelete.cpp +++ b/src/xrpld/app/tx/detail/LoanDelete.cpp @@ -24,9 +24,9 @@ namespace ripple { bool -LoanDelete::isEnabled(PreflightContext const& ctx) +LoanDelete::checkExtraFeatures(PreflightContext const& ctx) { - return lendingProtocolEnabled(ctx); + return checkLendingProtocolDependencies(ctx); } NotTEC diff --git a/src/xrpld/app/tx/detail/LoanDelete.h b/src/xrpld/app/tx/detail/LoanDelete.h index 74825b5bcd..4013fb082a 100644 --- a/src/xrpld/app/tx/detail/LoanDelete.h +++ b/src/xrpld/app/tx/detail/LoanDelete.h @@ -34,7 +34,7 @@ public: } static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/LoanDraw.cpp b/src/xrpld/app/tx/detail/LoanDraw.cpp index 6665c30942..2b11f59f9a 100644 --- a/src/xrpld/app/tx/detail/LoanDraw.cpp +++ b/src/xrpld/app/tx/detail/LoanDraw.cpp @@ -25,9 +25,9 @@ namespace ripple { bool -LoanDraw::isEnabled(PreflightContext const& ctx) +LoanDraw::checkExtraFeatures(PreflightContext const& ctx) { - return lendingProtocolEnabled(ctx); + return checkLendingProtocolDependencies(ctx); } NotTEC diff --git a/src/xrpld/app/tx/detail/LoanDraw.h b/src/xrpld/app/tx/detail/LoanDraw.h index 471744f32f..e76436635b 100644 --- a/src/xrpld/app/tx/detail/LoanDraw.h +++ b/src/xrpld/app/tx/detail/LoanDraw.h @@ -35,7 +35,7 @@ public: } static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index 498466f10e..2e2815db6a 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -26,9 +26,9 @@ namespace ripple { bool -LoanManage::isEnabled(PreflightContext const& ctx) +LoanManage::checkExtraFeatures(PreflightContext const& ctx) { - return lendingProtocolEnabled(ctx); + return checkLendingProtocolDependencies(ctx); } std::uint32_t diff --git a/src/xrpld/app/tx/detail/LoanManage.h b/src/xrpld/app/tx/detail/LoanManage.h index 7f61008c57..ade02f4c20 100644 --- a/src/xrpld/app/tx/detail/LoanManage.h +++ b/src/xrpld/app/tx/detail/LoanManage.h @@ -34,7 +34,7 @@ public: } static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); static std::uint32_t getFlagsMask(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 511c4f9f39..5b31780880 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -24,9 +24,9 @@ namespace ripple { bool -LoanPay::isEnabled(PreflightContext const& ctx) +LoanPay::checkExtraFeatures(PreflightContext const& ctx) { - return lendingProtocolEnabled(ctx); + return checkLendingProtocolDependencies(ctx); } NotTEC diff --git a/src/xrpld/app/tx/detail/LoanPay.h b/src/xrpld/app/tx/detail/LoanPay.h index 3f4b208aa2..555990dbd0 100644 --- a/src/xrpld/app/tx/detail/LoanPay.h +++ b/src/xrpld/app/tx/detail/LoanPay.h @@ -34,7 +34,7 @@ public: } static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 1317595202..065e73434a 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -26,9 +26,9 @@ namespace ripple { bool -LoanSet::isEnabled(PreflightContext const& ctx) +LoanSet::checkExtraFeatures(PreflightContext const& ctx) { - return lendingProtocolEnabled(ctx); + return checkLendingProtocolDependencies(ctx); } std::uint32_t diff --git a/src/xrpld/app/tx/detail/LoanSet.h b/src/xrpld/app/tx/detail/LoanSet.h index 342f822168..cb98e361c0 100644 --- a/src/xrpld/app/tx/detail/LoanSet.h +++ b/src/xrpld/app/tx/detail/LoanSet.h @@ -34,7 +34,7 @@ public: } static bool - isEnabled(PreflightContext const& ctx); + checkExtraFeatures(PreflightContext const& ctx); static std::uint32_t getFlagsMask(PreflightContext const& ctx); From 6e2965f01acf479f4739f62c0f88f3e9da0d8a63 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 25 Sep 2025 15:25:37 -0400 Subject: [PATCH 093/291] Add test cases for Vault prereq amendments being disabled --- src/test/app/Vault_test.cpp | 57 ++++++++++++++++++++----- src/xrpld/app/tx/detail/VaultCreate.cpp | 7 ++- src/xrpld/app/tx/detail/VaultSet.cpp | 7 ++- 3 files changed, 56 insertions(+), 15 deletions(-) diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 159bfd0796..a293ded39c 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -604,12 +604,13 @@ class Vault_test : public beast::unit_test::suite test(env, issuer, owner, asset, vault); }; - testCase( - [&](Env& env, - Account const& issuer, - Account const& owner, - Asset const& asset, - Vault& vault) { + auto testDisabled = [&](TER resultAfterCreate = temDISABLED) { + return [&, resultAfterCreate]( + Env& env, + Account const& issuer, + Account const& owner, + Asset const& asset, + Vault& vault) { testcase("disabled single asset vault"); auto [tx, keylet] = @@ -618,7 +619,7 @@ class Vault_test : public beast::unit_test::suite { auto tx = vault.set({.owner = owner, .id = keylet.key}); - env(tx, ter{temDISABLED}); + env(tx, data("test"), ter{resultAfterCreate}); } { @@ -626,7 +627,7 @@ class Vault_test : public beast::unit_test::suite {.depositor = owner, .id = keylet.key, .amount = asset(10)}); - env(tx, ter{temDISABLED}); + env(tx, ter{resultAfterCreate}); } { @@ -634,7 +635,7 @@ class Vault_test : public beast::unit_test::suite {.depositor = owner, .id = keylet.key, .amount = asset(10)}); - env(tx, ter{temDISABLED}); + env(tx, ter{resultAfterCreate}); } { @@ -643,15 +644,49 @@ class Vault_test : public beast::unit_test::suite .id = keylet.key, .holder = owner, .amount = asset(10)}); - env(tx, ter{temDISABLED}); + env(tx, ter{resultAfterCreate}); } { auto tx = vault.del({.owner = owner, .id = keylet.key}); + env(tx, ter{resultAfterCreate}); + } + }; + }; + + testCase( + testDisabled(), + {.features = testable_amendments() - featureSingleAssetVault}); + + testCase( + testDisabled(tecNO_ENTRY), + {.features = testable_amendments() - featureMPTokensV1}); + + testCase( + [&](Env& env, + Account const& issuer, + Account const& owner, + Asset const& asset, + Vault& vault) { + testcase("disabled permissioned domains"); + + auto [tx, keylet] = + vault.create({.owner = owner, .asset = asset}); + env(tx); + + tx[sfFlags] = tx[sfFlags].asUInt() | tfVaultPrivate; + tx[sfDomainID] = to_string(base_uint<256>(42ul)); + env(tx, ter{temDISABLED}); + + { + auto tx = vault.set({.owner = owner, .id = keylet.key}); + env(tx, data("Test")); + + tx[sfDomainID] = to_string(base_uint<256>(13ul)); env(tx, ter{temDISABLED}); } }, - {.features = testable_amendments() - featureSingleAssetVault}); + {.features = testable_amendments() - featurePermissionedDomains}); testCase([&](Env& env, Account const& issuer, diff --git a/src/xrpld/app/tx/detail/VaultCreate.cpp b/src/xrpld/app/tx/detail/VaultCreate.cpp index 9447976a32..0b5db4317e 100644 --- a/src/xrpld/app/tx/detail/VaultCreate.cpp +++ b/src/xrpld/app/tx/detail/VaultCreate.cpp @@ -41,8 +41,11 @@ VaultCreate::checkExtraFeatures(PreflightContext const& ctx) if (!ctx.rules.enabled(featureMPTokensV1)) return false; - return !ctx.tx.isFieldPresent(sfDomainID) || - ctx.rules.enabled(featurePermissionedDomains); + if (ctx.tx.isFieldPresent(sfDomainID) && + !ctx.rules.enabled(featurePermissionedDomains)) + return false; + + return true; } std::uint32_t diff --git a/src/xrpld/app/tx/detail/VaultSet.cpp b/src/xrpld/app/tx/detail/VaultSet.cpp index 6057e40cfa..e9f7551154 100644 --- a/src/xrpld/app/tx/detail/VaultSet.cpp +++ b/src/xrpld/app/tx/detail/VaultSet.cpp @@ -33,8 +33,11 @@ namespace ripple { bool VaultSet::checkExtraFeatures(PreflightContext const& ctx) { - return !ctx.tx.isFieldPresent(sfDomainID) || - ctx.rules.enabled(featurePermissionedDomains); + if (ctx.tx.isFieldPresent(sfDomainID) && + !ctx.rules.enabled(featurePermissionedDomains)) + return false; + + return true; } NotTEC From 4c8f7968c479b439561f2987c65334cac4817c48 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 25 Sep 2025 14:01:20 -0400 Subject: [PATCH 094/291] Tweak payment rounding --- src/test/app/Loan_test.cpp | 73 +++++++++++++++++++---------- src/test/jtx/amount.h | 8 ++-- src/xrpld/app/misc/LendingHelpers.h | 12 +++-- 3 files changed, 62 insertions(+), 31 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 6f45a284a9..c8eaba08da 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -1733,9 +1733,13 @@ class Loan_test : public beast::unit_test::suite Number const rawPeriodicPayment = state.principalOutstanding * periodicRate * rateFactor / (rateFactor - 1); - STAmount const periodicPayment = roundToReference( - STAmount{broker.asset, rawPeriodicPayment}, - principalRequestedAmount); + STAmount const periodicPayment{ + broker.asset, + roundToAsset( + broker.asset, + rawPeriodicPayment, + principalRequestedAmount, + Number::upward)}; // Only check the first payment since the rounding // may drift as payments are made BEAST_EXPECT( @@ -1745,15 +1749,19 @@ class Loan_test : public beast::unit_test::suite // Include the service fee STAmount const totalDue = roundToReference( periodicPayment + broker.asset(2), - principalRequestedAmount); + principalRequestedAmount, + Number::upward); // Only check the first payment since the rounding // may drift as payments are made BEAST_EXPECT( state.paymentRemaining < 12 || totalDue == roundToReference( - broker.asset(Number(8533457001162141, -14)), - principalRequestedAmount)); + broker.asset( + Number(8533457001162141, -14), + Number::upward), + principalRequestedAmount, + Number::upward)); // Try to pay a little extra to show that it's _not_ // taken @@ -1765,8 +1773,11 @@ class Loan_test : public beast::unit_test::suite state.paymentRemaining < 12 || transactionAmount == roundToReference( - broker.asset(Number(9533457001162141, -14)), - principalRequestedAmount)); + broker.asset( + Number(9533457001162141, -14), + Number::upward), + principalRequestedAmount, + Number::upward)); auto const totalDueAmount = STAmount{broker.asset, totalDue}; @@ -1775,34 +1786,48 @@ class Loan_test : public beast::unit_test::suite Number const rawInterest = state.paymentRemaining == 1 ? rawPeriodicPayment - state.principalOutstanding : state.principalOutstanding * periodicRate; - STAmount const interest = roundToReference( - STAmount{broker.asset, rawInterest}, - principalRequestedAmount); + STAmount const interest{ + broker.asset, + roundToAsset( + broker.asset, + rawInterest, + principalRequestedAmount, + Number::upward)}; BEAST_EXPECT( state.paymentRemaining < 12 || - roundToReference( - STAmount{broker.asset, rawInterest}, - principalRequestedAmount) == + interest == roundToReference( - broker.asset(Number(2283105022831050, -18)), - principalRequestedAmount)); + broker.asset( + Number(2283105022831050, -18), + Number::upward), + principalRequestedAmount, + Number::upward)); BEAST_EXPECT(interest >= Number(0)); auto const rawPrincipal = rawPeriodicPayment - rawInterest; BEAST_EXPECT( state.paymentRemaining < 12 || - roundToReference( - STAmount{broker.asset, rawPrincipal}, - principalRequestedAmount) == + roundToAsset( + broker.asset, + rawPrincipal, + principalRequestedAmount, + Number::upward) == roundToReference( - broker.asset(Number(8333228690659858, -14)), - principalRequestedAmount)); + broker.asset( + Number(8333228690659858, -14), + Number::upward), + principalRequestedAmount, + Number::upward)); BEAST_EXPECT( state.paymentRemaining > 1 || rawPrincipal == state.principalOutstanding); - auto const principal = roundToReference( - STAmount{broker.asset, periodicPayment - interest}, - principalRequestedAmount); + STAmount const principal{ + broker.asset, + roundToAsset( + broker.asset, + periodicPayment - interest, + principalRequestedAmount, + Number::downward)}; BEAST_EXPECT( principal > Number(0) && principal <= state.principalOutstanding); diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h index a793f3a287..f14a65f6b8 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -213,14 +213,16 @@ public: template PrettyAmount - operator()(T v) const + operator()(T v, Number::rounding_mode rounding = Number::getround()) const { - return operator()(Number(v)); + return operator()(Number(v), rounding); } PrettyAmount - operator()(Number v) const + operator()(Number v, Number::rounding_mode rounding = Number::getround()) + const { + NumberRoundModeGuard mg(rounding); STAmount amount{asset_, v * scale_}; return {amount, ""}; } diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index b5fc10a35e..fceb44dfbd 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -263,18 +263,22 @@ computePeriodicPaymentParts( Number const interest = roundToAsset( asset, periodicPaymentAmount - principalOutstanding, - originalPrincipal); + originalPrincipal, + Number::upward); return {interest, principalOutstanding}; } Number const interest = roundToAsset( - asset, principalOutstanding * periodicRate, originalPrincipal); + asset, + principalOutstanding * periodicRate, + originalPrincipal, + Number::upward); XRPL_ASSERT( interest >= 0, "ripple::detail::computePeriodicPayment : valid interest"); auto const roundedPayment = [&]() { - auto roundedPayment = - roundToAsset(asset, periodicPaymentAmount, originalPrincipal); + auto roundedPayment = roundToAsset( + asset, periodicPaymentAmount, originalPrincipal, Number::upward); if (roundedPayment > interest) return roundedPayment; auto newPayment = roundedPayment; From 66216a3d41c310fd7f7d08e5fc8e3507198830bc Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 26 Sep 2025 15:32:57 -0400 Subject: [PATCH 095/291] Review feedback from @mvadari - Add a test for preflightCheckSigningKey --- src/test/rpc/AccountSet_test.cpp | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/test/rpc/AccountSet_test.cpp b/src/test/rpc/AccountSet_test.cpp index 3615a715cd..1b6d608ded 100644 --- a/src/test/rpc/AccountSet_test.cpp +++ b/src/test/rpc/AccountSet_test.cpp @@ -19,6 +19,8 @@ #include +#include + #include #include #include @@ -578,6 +580,32 @@ public: env.close(); } + void + testBadSigningKey() + { + using namespace test::jtx; + testcase("Bad singing key"); + Env env(*this); + Account const alice("alice"); + + env.fund(XRP(10000), alice); + env.close(); + + auto jtx = env.jt(noop("alice"), ter(temBAD_SIGNATURE)); + if (!BEAST_EXPECT(jtx.stx)) + return; + auto stx = std::make_shared(*jtx.stx); + stx->at(sfSigningPubKey) = makeSlice(std::string("badkey")); + + env.app().openLedger().modify([&](OpenView& view, beast::Journal j) { + auto const result = + ripple::apply(env.app(), view, *stx, tapNONE, j); + BEAST_EXPECT(result.ter == temBAD_SIGNATURE); + BEAST_EXPECT(!result.applied); + return result.applied; + }); + } + void run() override { @@ -594,6 +622,7 @@ public: testRequireAuthWithDir(); testTransferRate(); testTicket(); + testBadSigningKey(); } }; From 0bd6b1ae49850dc19ee96ca8842ebb081c2b7ca2 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 26 Sep 2025 16:05:15 -0400 Subject: [PATCH 096/291] Review feedback from @mvadari - Clarify the purpose of the assert in calculateOwnerReserveFee --- src/xrpld/app/tx/detail/Transactor.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index cbd3138ed4..1c98233964 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -311,11 +311,20 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx) XRPAmount Transactor::calculateOwnerReserveFee(ReadView const& view, STTx const& tx) { - // One reserve increment is typically much greater than one base fee. + // Assumption: One reserve increment is typically much greater than one base + // fee. + // This check is in an assert so that it will come to the attention of + // developers if that assumption is not correct. If the owner reserve is not + // significantly larger than the base fee (or even worse, smaller), we will + // need to rethink charging an owner reserve as a transaction fee. + // TODO: This function is static, and I don't want to add more parameters. + // When it is finally refactored to be in a context that has access to the + // Application, include "app().overlay().networkID() > 2 ||" in the + // condition. XRPL_ASSERT( view.fees().increment > view.fees().base * 100, - "ripple::Transactor::calculateOwnerReserveFee : Owner reserve is much " - "greater than base fee"); + "ripple::Transactor::calculateOwnerReserveFee : Owner reserve is " + "reasonable"); return view.fees().increment; } From 7612fd0ba6daae63f4e406d429fef44fc26acb82 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 26 Sep 2025 19:23:13 -0400 Subject: [PATCH 097/291] Review feedback from @gregtatcam - Amendment checking correctness. - Sync some changes between LoanBrokerCoverWithdraw and CoverDeposit. - Check for nulls. - Add documentation. --- src/xrpld/app/tx/detail/InvariantCheck.cpp | 13 ++++---- .../app/tx/detail/LoanBrokerCoverDeposit.cpp | 6 +++- .../app/tx/detail/LoanBrokerCoverWithdraw.cpp | 2 ++ src/xrpld/overlay/detail/PeerImp.cpp | 32 +++++++++++++++++++ 4 files changed, 46 insertions(+), 7 deletions(-) diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 9b08887b76..20a647821f 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -1522,15 +1522,14 @@ ValidMPTIssuance::finalize( return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 1; } + bool const lendingProtocolEnabled = + view.rules().enabled(featureLendingProtocol); // ttESCROW_FINISH may authorize an MPT, but it can't have the // mayAuthorizeMPT privilege, because that may cause // non-amendment-gated side effects. bool const enforceEscrowFinish = (tx.getTxnType() == ttESCROW_FINISH) && - (view.rules().enabled(featureSingleAssetVault) - /* - TODO: Uncomment when LendingProtocol is defined - || view.rules().enabled(featureLendingProtocol)*/ - ); + (view.rules().enabled(featureSingleAssetVault) || + lendingProtocolEnabled); if (hasPrivilege(tx, mustAuthorizeMPT | mayAuthorizeMPT) || enforceEscrowFinish) { @@ -1548,7 +1547,9 @@ ValidMPTIssuance::finalize( "succeeded but deleted issuances"; return false; } - else if (mptokensCreated_ + mptokensDeleted_ > 1) + else if ( + lendingProtocolEnabled && + mptokensCreated_ + mptokensDeleted_ > 1) { JLOG(j.fatal()) << "Invariant failed: MPT authorize succeeded " "but created/deleted bad number mptokens"; diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp index 5aa3aba2cd..c75dc95f98 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp @@ -35,7 +35,11 @@ LoanBrokerCoverDeposit::preflight(PreflightContext const& ctx) if (ctx.tx[sfLoanBrokerID] == beast::zero) return temINVALID; - if (ctx.tx[sfAmount] <= beast::zero) + auto const dstAmount = ctx.tx[sfAmount]; + if (dstAmount <= beast::zero) + return temBAD_AMOUNT; + + if (!isLegalNet(dstAmount)) return temBAD_AMOUNT; return tesSUCCESS; diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp index cb47c1953f..0505218dcb 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp @@ -91,6 +91,8 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) return tecNO_PERMISSION; } auto const vault = ctx.view.read(keylet::vault(sleBroker->at(sfVaultID))); + if (!vault) + return tefBAD_LEDGER; // LCOV_EXCL_LINE auto const vaultAsset = vault->at(sfAsset); if (amount.asset() != vaultAsset) diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index c09d0be6a9..127ab4655e 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -1387,6 +1387,22 @@ PeerImp::handleTransaction( // Charge strongly for attempting to relay a txn with tfInnerBatchTxn // LCOV_EXCL_START + /* + There is no need to check whether the featureBatch amendment is + enabled. + + * If the `tfInnerBatchTxn` flag is set, and the amendment is + enabled, then it's an invalid transaction because inner batch + transactions should not be relayed. + * If the `tfInnerBatchTxn` flag is set, and the amendment is *not* + enabled, then the transaction is malformed because it's using an + "unknown" flag. There's no need to waste the resources to send it + to the transaction engine. + + We don't normally check transaction validity at this level, but + since we _need_ to check it when the amendment is enabled, we may as + well drop it if the flag is set regardless. + */ if (stx->isFlag(tfInnerBatchTxn)) { JLOG(p_journal_.warn()) << "Ignoring Network relayed Tx containing " @@ -2951,6 +2967,22 @@ PeerImp::checkTransaction( { // charge strongly for relaying batch txns // LCOV_EXCL_START + /* + There is no need to check whether the featureBatch amendment is + enabled. + + * If the `tfInnerBatchTxn` flag is set, and the amendment is + enabled, then it's an invalid transaction because inner batch + transactions should not be relayed. + * If the `tfInnerBatchTxn` flag is set, and the amendment is *not* + enabled, then the transaction is malformed because it's using an + "unknown" flag. There's no need to waste the resources to send it + to the transaction engine. + + We don't normally check transaction validity at this level, but + since we _need_ to check it when the amendment is enabled, we may as + well drop it if the flag is set regardless. + */ if (stx->isFlag(tfInnerBatchTxn)) { JLOG(p_journal_.warn()) << "Ignoring Network relayed Tx containing " From a3365545df77a763b45a3998ebfa554217b84cff Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sun, 28 Sep 2025 19:10:12 -0400 Subject: [PATCH 098/291] Review feedback from @Bronek - Exclude impossible logging from code coverage, too. --- src/xrpld/app/tx/detail/Transactor.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 30a7ce70cc..9178130681 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -237,11 +237,11 @@ Transactor::preflight2(PreflightContext const& ctx) auto const sigValid = checkValidity( ctx.app.getHashRouter(), ctx.tx, ctx.rules, ctx.app.config()); if (sigValid.first == Validity::SigBad) - { + { // LCOV_EXCL_START JLOG(ctx.j.debug()) << "preflight2: bad signature. " << sigValid.second; - return temINVALID; // LCOV_EXCL_LINE - } + return temINVALID; + } // LCOV_EXCL_STOP } return tesSUCCESS; } From 7a3f7aebcf1e18a0d1cd454f166513a419fbb8bc Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 29 Sep 2025 21:42:03 -0400 Subject: [PATCH 099/291] Review feedback from @gregtatcam * Write more comments explaining what's going on. * Rename some variables. * Do a final safety check for valid values in `LoanPay`. --- src/test/app/Loan_test.cpp | 8 +- src/xrpld/app/misc/LendingHelpers.h | 153 ++++++++++++++++------------ src/xrpld/app/tx/detail/LoanPay.cpp | 18 +++- 3 files changed, 107 insertions(+), 72 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index c8eaba08da..d0345b517b 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -1721,9 +1721,6 @@ class Loan_test : public beast::unit_test::suite while (state.paymentRemaining > 0) { - testcase << "Payments remaining: " - << state.paymentRemaining; - STAmount const principalRequestedAmount{ broker.asset, state.principalRequested}; // Compute the payment based on the number of @@ -1740,6 +1737,11 @@ class Loan_test : public beast::unit_test::suite rawPeriodicPayment, principalRequestedAmount, Number::upward)}; + + testcase + << "Payments remaining: " << state.paymentRemaining + << ", computed payment amount: " << periodicPayment; + // Only check the first payment since the rounding // may drift as payments are made BEAST_EXPECT( diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index fceb44dfbd..bc8e5c3b9a 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -267,6 +267,15 @@ computePeriodicPaymentParts( Number::upward); return {interest, principalOutstanding}; } + /* + * From the spec, once the periodicPayment is computed: + * + * The principal and interest portions can be derived as follows: + * interest = principalOutstanding * periodicRate + * principal = periodicPayment - interest + * + * Because those values deal with funds, they need to be rounded. + */ Number const interest = roundToAsset( asset, principalOutstanding * periodicRate, @@ -276,6 +285,9 @@ computePeriodicPaymentParts( interest >= 0, "ripple::detail::computePeriodicPayment : valid interest"); + // To compute the principal using the above formulas, use the rounded + // payment amount, ensuring that some principal is paid regardless of any + // other results. auto const roundedPayment = [&]() { auto roundedPayment = roundToAsset( asset, periodicPaymentAmount, originalPrincipal, Number::upward); @@ -311,11 +323,22 @@ computePeriodicPaymentParts( return {interest, principal}; } +// This structure is explained in the XLS-66 spec, section 3.2.4.4 (Failure +// Conditions) struct LoanPaymentParts { + /// principal_paid is the amount of principal that the payment covered. Number principalPaid; + /// interest_paid is the amount of interest that the payment covered. Number interestPaid; + /** + * value_change is the amount by which the total value of the Loan changed. + * If value_change < 0, Loan value decreased. + * If value_change > 0, Loan value increased. + * This is 0 for regular payments. + */ Number valueChange; + /// fee_paid is the amount of fee that the payment covered. Number feePaid; }; @@ -335,10 +358,10 @@ Expected handleLatePayment( A const& asset, ApplyView& view, - NumberProxy& principalOutstandingField, - Int32Proxy& paymentRemainingField, - Int32Proxy& prevPaymentDateField, - Int32Proxy& nextDueDateField, + NumberProxy& principalOutstandingProxy, + Int32Proxy& paymentRemainingProxy, + Int32Proxy& prevPaymentDateProxy, + Int32Proxy& nextDueDateProxy, PeriodicPaymentParts const& periodic, std::uint32_t const startDate, std::uint32_t const paymentInterval, @@ -349,21 +372,21 @@ handleLatePayment( STAmount const& amount, beast::Journal j) { - if (!hasExpired(view, nextDueDateField)) + if (!hasExpired(view, nextDueDateProxy)) return Unexpected(tesSUCCESS); // the payment is late auto const latePaymentInterest = loanLatePaymentInterest( asset, - principalOutstandingField, + principalOutstandingProxy, lateInterestRate, view.parentCloseTime(), startDate, - prevPaymentDateField, + prevPaymentDateProxy, originalPrincipalRequested); XRPL_ASSERT( latePaymentInterest >= 0, - "ripple::loanComputePaymentParts : valid late interest"); + "ripple::handleLatePayment : valid late interest"); auto const latePaymentAmount = periodicPaymentAmount + latePaymentInterest + latePaymentFee; @@ -374,14 +397,14 @@ handleLatePayment( return Unexpected(tecINSUFFICIENT_PAYMENT); } - paymentRemainingField -= 1; + paymentRemainingProxy -= 1; // A single payment always pays the same amount of principal. Only the // interest and fees are extra for a late payment - principalOutstandingField -= periodic.principal; + principalOutstandingProxy -= periodic.principal; // Make sure this does an assignment - prevPaymentDateField = nextDueDateField; - nextDueDateField += paymentInterval; + prevPaymentDateProxy = nextDueDateProxy; + nextDueDateProxy += paymentInterval; // A late payment increases the value of the loan by the difference // between periodic and late payment interest @@ -404,9 +427,9 @@ Expected handleFullPayment( A const& asset, ApplyView& view, - NumberProxy& principalOutstandingField, - Int32Proxy& paymentRemainingField, - Int32Proxy& prevPaymentDateField, + NumberProxy& principalOutstandingProxy, + Int32Proxy& paymentRemainingProxy, + Int32Proxy& prevPaymentDateProxy, std::uint32_t const startDate, std::uint32_t const paymentInterval, TenthBips32 const closeInterestRate, @@ -417,7 +440,7 @@ handleFullPayment( STAmount const& amount, beast::Journal j) { - if (paymentRemainingField <= 1) + if (paymentRemainingProxy <= 1) // If this is the last payment, it has to be a regular payment return Unexpected(tesSUCCESS); @@ -426,27 +449,27 @@ handleFullPayment( auto const accruedInterest = roundToAsset( asset, detail::loanAccruedInterest( - principalOutstandingField, + principalOutstandingProxy, periodicRate, view.parentCloseTime(), startDate, - prevPaymentDateField, + prevPaymentDateProxy, paymentInterval), originalPrincipalRequested); XRPL_ASSERT( accruedInterest >= 0, - "ripple::loanComputePaymentParts : valid accrued interest"); + "ripple::handleFullPayment : valid accrued interest"); auto const closePrepaymentInterest = roundToAsset( asset, - tenthBipsOfValue(principalOutstandingField.value(), closeInterestRate), + tenthBipsOfValue(principalOutstandingProxy.value(), closeInterestRate), originalPrincipalRequested); XRPL_ASSERT( closePrepaymentInterest >= 0, - "ripple::loanComputePaymentParts : valid prepayment " + "ripple::handleFullPayment : valid prepayment " "interest"); auto const totalInterest = accruedInterest + closePrepaymentInterest; auto const closeFullPayment = - principalOutstandingField + totalInterest + closePaymentFee; + principalOutstandingProxy + totalInterest + closePaymentFee; if (amount < closeFullPayment) // If the payment is less than the full payment amount, it's not @@ -461,17 +484,17 @@ handleFullPayment( auto const valueChange = totalInterest - totalInterestOutstanding; LoanPaymentParts const result{ - principalOutstandingField, totalInterest, valueChange, closePaymentFee}; + principalOutstandingProxy, totalInterest, valueChange, closePaymentFee}; - paymentRemainingField = 0; - principalOutstandingField = 0; + paymentRemainingProxy = 0; + principalOutstandingProxy = 0; return result; } template Expected -loanComputePaymentParts( +loanMakePayment( A const& asset, ApplyView& view, SLE::ref loan, @@ -483,7 +506,7 @@ loanComputePaymentParts( * section 3.2.4.3 (Transaction Pseudo-code) */ Number const originalPrincipalRequested = loan->at(sfPrincipalRequested); - auto principalOutstandingField = loan->at(sfPrincipalOutstanding); + auto principalOutstandingProxy = loan->at(sfPrincipalOutstanding); bool const allowOverpayment = loan->isFlag(lsfLoanOverpayment); TenthBips32 const interestRate{loan->at(sfInterestRate)}; @@ -499,13 +522,13 @@ loanComputePaymentParts( TenthBips32 const overpaymentFee{loan->at(sfOverpaymentFee)}; std::uint32_t const paymentInterval = loan->at(sfPaymentInterval); - auto paymentRemainingField = loan->at(sfPaymentRemaining); + auto paymentRemainingProxy = loan->at(sfPaymentRemaining); - auto prevPaymentDateField = loan->at(sfPreviousPaymentDate); + auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDate); std::uint32_t const startDate = loan->at(sfStartDate); - auto nextDueDateField = loan->at(sfNextPaymentDueDate); + auto nextDueDateProxy = loan->at(sfNextPaymentDueDate); - if (paymentRemainingField == 0 || principalOutstandingField == 0) + if (paymentRemainingProxy == 0 || principalOutstandingProxy == 0) { // Loan complete JLOG(j.warn()) << "Loan is already paid off."; @@ -518,12 +541,12 @@ loanComputePaymentParts( detail::loanPeriodicRate(interestRate, paymentInterval); XRPL_ASSERT( interestRate == 0 || periodicRate > 0, - "ripple::loanComputePaymentParts : valid rate"); + "ripple::loanMakePayment : valid rate"); // Don't round the payment amount. Only round the final computations // using it. Number const periodicPaymentAmount = detail::loanPeriodicPayment( - principalOutstandingField, periodicRate, paymentRemainingField); + principalOutstandingProxy, periodicRate, paymentRemainingProxy); XRPL_ASSERT( periodicPaymentAmount > 0, "ripple::computePeriodicPayment : valid payment"); @@ -531,30 +554,30 @@ loanComputePaymentParts( auto const periodic = computePeriodicPaymentParts( asset, originalPrincipalRequested, - principalOutstandingField, + principalOutstandingProxy, periodicPaymentAmount, periodicRate, - paymentRemainingField); + paymentRemainingProxy); Number const totalValueOutstanding = detail::loanTotalValueOutstanding( asset, originalPrincipalRequested, periodicPaymentAmount, - paymentRemainingField); + paymentRemainingProxy); XRPL_ASSERT( totalValueOutstanding > 0, - "ripple::loanComputePaymentParts : valid total value"); + "ripple::loanMakePayment : valid total value"); Number const totalInterestOutstanding = detail::loanTotalInterestOutstanding( - principalOutstandingField, totalValueOutstanding); + principalOutstandingProxy, totalValueOutstanding); XRPL_ASSERT_PARTS( totalInterestOutstanding >= 0, - "ripple::loanComputePaymentParts", + "ripple::loanMakePayment", "valid total interest"); XRPL_ASSERT_PARTS( totalValueOutstanding - totalInterestOutstanding == - principalOutstandingField, - "ripple::loanComputePaymentParts", + principalOutstandingProxy, + "ripple::loanMakePayment", "valid principal computation"); view.update(loan); @@ -564,10 +587,10 @@ loanComputePaymentParts( if (auto const latePaymentParts = handleLatePayment( asset, view, - principalOutstandingField, - paymentRemainingField, - prevPaymentDateField, - nextDueDateField, + principalOutstandingProxy, + paymentRemainingProxy, + prevPaymentDateProxy, + nextDueDateProxy, periodic, startDate, paymentInterval, @@ -586,9 +609,9 @@ loanComputePaymentParts( if (auto const fullPaymentParts = handleFullPayment( asset, view, - principalOutstandingField, - paymentRemainingField, - prevPaymentDateField, + principalOutstandingProxy, + paymentRemainingProxy, + prevPaymentDateProxy, startDate, paymentInterval, closeInterestRate, @@ -617,7 +640,7 @@ loanComputePaymentParts( std::optional mg(Number::downward); std::int64_t const fullPeriodicPayments = [&]() { std::int64_t const full{amount / totalDue}; - return full < paymentRemainingField ? full : paymentRemainingField; + return full < paymentRemainingProxy ? full : paymentRemainingProxy; }(); mg.reset(); // Temporary asserts @@ -635,8 +658,8 @@ loanComputePaymentParts( return Unexpected(tecINSUFFICIENT_PAYMENT); } - nextDueDateField += paymentInterval * fullPeriodicPayments; - prevPaymentDateField = nextDueDateField - paymentInterval; + nextDueDateProxy += paymentInterval * fullPeriodicPayments; + prevPaymentDateProxy = nextDueDateProxy - paymentInterval; Number totalPrincipalPaid = 0; Number totalInterestPaid = 0; @@ -650,21 +673,21 @@ loanComputePaymentParts( future = computePeriodicPaymentParts( asset, originalPrincipalRequested, - principalOutstandingField, + principalOutstandingProxy, periodicPaymentAmount, periodicRate, - paymentRemainingField); + paymentRemainingProxy); XRPL_ASSERT( future->interest <= periodic.interest, - "ripple::loanComputePaymentParts : decreasing interest"); + "ripple::loanMakePayment : decreasing interest"); XRPL_ASSERT( future->principal >= periodic.principal, - "ripple::loanComputePaymentParts : increasing principal"); + "ripple::loanMakePayment : increasing principal"); totalPrincipalPaid += future->principal; totalInterestPaid += future->interest; - paymentRemainingField -= 1; - principalOutstandingField -= future->principal; + paymentRemainingProxy -= 1; + principalOutstandingProxy -= future->principal; future.reset(); } @@ -674,10 +697,10 @@ loanComputePaymentParts( Number const newInterest = detail::loanTotalInterestOutstanding( asset, originalPrincipalRequested, - principalOutstandingField, + principalOutstandingProxy, interestRate, paymentInterval, - paymentRemainingField) + + paymentRemainingProxy) + totalInterestPaid; // ------------------------------------------------------------- @@ -686,7 +709,7 @@ loanComputePaymentParts( if (allowOverpayment) { Number const overpayment = std::min( - principalOutstandingField.value(), + principalOutstandingProxy.value(), amount - (totalPrincipalPaid + totalInterestPaid + totalFeePaid)); if (roundToAsset(asset, overpayment, originalPrincipalRequested) > 0) @@ -713,7 +736,7 @@ loanComputePaymentParts( totalInterestPaid += interestPortion; totalFeePaid += feePortion; - principalOutstandingField -= remainder; + principalOutstandingProxy -= remainder; } } } @@ -726,19 +749,19 @@ loanComputePaymentParts( XRPL_ASSERT( roundToAsset(asset, totalPrincipalPaid, originalPrincipalRequested) == totalPrincipalPaid, - "ripple::loanComputePaymentParts : totalPrincipalPaid rounded"); + "ripple::loanMakePayment : totalPrincipalPaid rounded"); XRPL_ASSERT( roundToAsset(asset, totalInterestPaid, originalPrincipalRequested) == totalInterestPaid, - "ripple::loanComputePaymentParts : totalInterestPaid rounded"); + "ripple::loanMakePayment : totalInterestPaid rounded"); XRPL_ASSERT( roundToAsset(asset, loanValueChange, originalPrincipalRequested) == loanValueChange, - "ripple::loanComputePaymentParts : loanValueChange rounded"); + "ripple::loanMakePayment : loanValueChange rounded"); XRPL_ASSERT( roundToAsset(asset, totalFeePaid, originalPrincipalRequested) == totalFeePaid, - "ripple::loanComputePaymentParts : totalFeePaid rounded"); + "ripple::loanMakePayment : totalFeePaid rounded"); return LoanPaymentParts{ totalPrincipalPaid, totalInterestPaid, loanValueChange, totalFeePaid}; } diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 5b31780880..92dbbd2044 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -148,14 +148,16 @@ LoanPay::doApply() // Loan object state changes Number const originalPrincipalRequested = loanSle->at(sfPrincipalRequested); - view.update(loanSle); - Expected paymentParts = - loanComputePaymentParts(asset, view, loanSle, amount, j_); + loanMakePayment(asset, view, loanSle, amount, j_); if (!paymentParts) return paymentParts.error(); + // If the payment computation completed without error, the loanSle object + // has been modified. + view.update(loanSle); + // If the loan was impaired, it isn't anymore. loanSle->clearFlag(lsfLoanImpaired); @@ -171,6 +173,14 @@ LoanPay::doApply() paymentParts->feePaid >= 0, "ripple::LoanPay::doApply", "valid fee paid"); + if (paymentParts->principalPaid <= 0 || paymentParts->interestPaid < 0 || + paymentParts->feePaid < 0) + { + // LCOV_EXCL_START + JLOG(j_.fatal()) << "Loan payment computation returned invalid values."; + return tecINTERNAL; + // LCOV_EXCL_STOP + } //------------------------------------------------------ // LoanBroker object state changes @@ -205,7 +215,7 @@ LoanPay::doApply() originalPrincipalRequested); if (!sufficientCover) { - // Add the fee to to First Loss Cover Pool + // Add the fee to First Loss Cover Pool coverAvailableField += totalPaidToBroker; } auto const brokerPayee = From e1b234cc515449196371633df18fdaed6166a0f8 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 30 Sep 2025 14:39:08 -0400 Subject: [PATCH 100/291] fix: Transaction sig checking functions do not get a full context - Fixes a bug introduced by PR #5594, commit 550f90a75e, which introduced the concept of a "sigObject", in which the signature is checked without necessarily being the transaction. - Fortunately, no code uses the "sigObject" as anything other than the transactor yet, so the bug is harmless for now. - This fix removes the "PreclaimContext" from the parameter list, and adds only the individual parts needed by the function, none of which are the transaction. --- src/xrpld/app/tx/detail/Transactor.cpp | 78 +++++++++++++------------- src/xrpld/app/tx/detail/Transactor.h | 19 ++++--- 2 files changed, 52 insertions(+), 45 deletions(-) diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 112017ebaf..f6b8b3c9d2 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -660,14 +660,15 @@ Transactor::apply() NotTEC Transactor::checkSign( - PreclaimContext const& ctx, + ReadView const& view, + ApplyFlags flags, AccountID const& idAccount, - STObject const& sigObject) + STObject const& sigObject, + beast::Journal const j) { auto const pkSigner = sigObject.getFieldVL(sfSigningPubKey); // Ignore signature check on batch inner transactions - if (sigObject.isFlag(tfInnerBatchTxn) && - ctx.view.rules().enabled(featureBatch)) + if (sigObject.isFlag(tfInnerBatchTxn) && view.rules().enabled(featureBatch)) { // Defensive Check: These values are also checked in Batch::preflight if (sigObject.isFieldPresent(sfTxnSignature) || !pkSigner.empty() || @@ -678,7 +679,7 @@ Transactor::checkSign( return tesSUCCESS; } - if ((ctx.flags & tapDRY_RUN) && pkSigner.empty() && + if ((flags & tapDRY_RUN) && pkSigner.empty() && !sigObject.isFieldPresent(sfSigners)) { // simulate: skip signature validation when neither SigningPubKey nor @@ -688,9 +689,9 @@ Transactor::checkSign( // If the pk is empty and not simulate or simulate and signers, // then we must be multi-signing. - if (ctx.tx.isFieldPresent(sfSigners)) + if (sigObject.isFieldPresent(sfSigners)) { - return checkMultiSign(ctx, idAccount, sigObject); + return checkMultiSign(view, flags, idAccount, sigObject, j); } // Check Single Sign @@ -699,7 +700,7 @@ Transactor::checkSign( if (!publicKeyType(makeSlice(pkSigner))) { - JLOG(ctx.j.trace()) << "checkSign: signing public key type is unknown"; + JLOG(j.trace()) << "checkSign: signing public key type is unknown"; return tefBAD_AUTH; // FIXME: should be better error! } @@ -707,11 +708,11 @@ Transactor::checkSign( auto const idSigner = pkSigner.empty() ? idAccount : calcAccountID(PublicKey(makeSlice(pkSigner))); - auto const sleAccount = ctx.view.read(keylet::account(idAccount)); + auto const sleAccount = view.read(keylet::account(idAccount)); if (!sleAccount) return terNO_ACCOUNT; - return checkSingleSign(ctx, idSigner, idAccount, sleAccount); + return checkSingleSign(view, idSigner, idAccount, sleAccount, j); } NotTEC @@ -720,7 +721,7 @@ Transactor::checkSign(PreclaimContext const& ctx) auto const idAccount = ctx.tx.isFieldPresent(sfDelegate) ? ctx.tx.getAccountID(sfDelegate) : ctx.tx.getAccountID(sfAccount); - return checkSign(ctx, idAccount, ctx.tx); + return checkSign(ctx.view, ctx.flags, idAccount, ctx.tx, ctx.j); } NotTEC @@ -735,7 +736,8 @@ Transactor::checkBatchSign(PreclaimContext const& ctx) Blob const& pkSigner = signer.getFieldVL(sfSigningPubKey); if (pkSigner.empty()) { - if (ret = checkMultiSign(ctx, idAccount, signer); + if (ret = checkMultiSign( + ctx.view, ctx.flags, idAccount, signer, ctx.j); !isTesSuccess(ret)) return ret; } @@ -759,7 +761,8 @@ Transactor::checkBatchSign(PreclaimContext const& ctx) return tesSUCCESS; } - if (ret = checkSingleSign(ctx, idSigner, idAccount, sleAccount); + if (ret = checkSingleSign( + ctx.view, idSigner, idAccount, sleAccount, ctx.j); !isTesSuccess(ret)) return ret; } @@ -769,14 +772,15 @@ Transactor::checkBatchSign(PreclaimContext const& ctx) NotTEC Transactor::checkSingleSign( - PreclaimContext const& ctx, + ReadView const& view, AccountID const& idSigner, AccountID const& idAccount, - std::shared_ptr sleAccount) + std::shared_ptr sleAccount, + beast::Journal const j) { bool const isMasterDisabled = sleAccount->isFlag(lsfDisableMaster); - if (ctx.view.rules().enabled(fixMasterKeyAsRegularKey)) + if (view.rules().enabled(fixMasterKeyAsRegularKey)) { // Signed with regular key. if ((*sleAccount)[~sfRegularKey] == idSigner) @@ -813,16 +817,14 @@ Transactor::checkSingleSign( else if (sleAccount->isFieldPresent(sfRegularKey)) { // Signing key does not match master or regular key. - JLOG(ctx.j.trace()) - << "checkSingleSign: Not authorized to use account."; + JLOG(j.trace()) << "checkSingleSign: Not authorized to use account."; return tefBAD_AUTH; } else { // No regular key on account and signing key does not match master key. // FIXME: Why differentiate this case from tefBAD_AUTH? - JLOG(ctx.j.trace()) - << "checkSingleSign: Not authorized to use account."; + JLOG(j.trace()) << "checkSingleSign: Not authorized to use account."; return tefBAD_AUTH_MASTER; } @@ -831,17 +833,19 @@ Transactor::checkSingleSign( NotTEC Transactor::checkMultiSign( - PreclaimContext const& ctx, + ReadView const& view, + ApplyFlags flags, AccountID const& id, - STObject const& sigObject) + STObject const& sigObject, + beast::Journal const j) { // Get id's SignerList and Quorum. std::shared_ptr sleAccountSigners = - ctx.view.read(keylet::signers(id)); + view.read(keylet::signers(id)); // If the signer list doesn't exist the account is not multi-signing. if (!sleAccountSigners) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Invalid: Not a multi-signing account."; return tefNOT_MULTI_SIGNING; } @@ -856,7 +860,7 @@ Transactor::checkMultiSign( "ripple::Transactor::checkMultiSign : signer list ID is 0"); auto accountSigners = - SignerEntries::deserialize(*sleAccountSigners, ctx.j, "ledger"); + SignerEntries::deserialize(*sleAccountSigners, j, "ledger"); if (!accountSigners) return accountSigners.error(); @@ -880,7 +884,7 @@ Transactor::checkMultiSign( { if (++iter == accountSigners->end()) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Invalid SigningAccount.Account."; return tefBAD_SIGNATURE; } @@ -888,7 +892,7 @@ Transactor::checkMultiSign( if (iter->account != txSignerAcctID) { // The SigningAccount is not in the SignerEntries. - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Invalid SigningAccount.Account."; return tefBAD_SIGNATURE; } @@ -902,13 +906,13 @@ Transactor::checkMultiSign( // STTx::checkMultiSign if (!spk.empty() && !publicKeyType(makeSlice(spk))) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "checkMultiSign: signing public key type is unknown"; return tefBAD_SIGNATURE; } XRPL_ASSERT( - (ctx.flags & tapDRY_RUN) || !spk.empty(), + (flags & tapDRY_RUN) || !spk.empty(), "ripple::Transactor::checkMultiSign : non-empty signer or " "simulation"); AccountID const signingAcctIDFromPubKey = spk.empty() @@ -940,8 +944,7 @@ Transactor::checkMultiSign( // In any of these cases we need to know whether the account is in // the ledger. Determine that now. - auto const sleTxSignerRoot = - ctx.view.read(keylet::account(txSignerAcctID)); + auto const sleTxSignerRoot = view.read(keylet::account(txSignerAcctID)); if (signingAcctIDFromPubKey == txSignerAcctID) { @@ -954,7 +957,7 @@ Transactor::checkMultiSign( if (signerAccountFlags & lsfDisableMaster) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Signer:Account lsfDisableMaster."; return tefMASTER_DISABLED; } @@ -966,21 +969,21 @@ Transactor::checkMultiSign( // Public key must hash to the account's regular key. if (!sleTxSignerRoot) { - JLOG(ctx.j.trace()) << "applyTransaction: Non-phantom signer " - "lacks account root."; + JLOG(j.trace()) << "applyTransaction: Non-phantom signer " + "lacks account root."; return tefBAD_SIGNATURE; } if (!sleTxSignerRoot->isFieldPresent(sfRegularKey)) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Account lacks RegularKey."; return tefBAD_SIGNATURE; } if (signingAcctIDFromPubKey != sleTxSignerRoot->getAccountID(sfRegularKey)) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Account doesn't match RegularKey."; return tefBAD_SIGNATURE; } @@ -992,8 +995,7 @@ Transactor::checkMultiSign( // Cannot perform transaction if quorum is not met. if (weightSum < sleAccountSigners->getFieldU32(sfSignerQuorum)) { - JLOG(ctx.j.trace()) - << "applyTransaction: Signers failed to meet quorum."; + JLOG(j.trace()) << "applyTransaction: Signers failed to meet quorum."; return tefBAD_QUORUM; } diff --git a/src/xrpld/app/tx/detail/Transactor.h b/src/xrpld/app/tx/detail/Transactor.h index 429dcec6fc..17ef62e607 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -283,9 +283,11 @@ protected: static NotTEC checkSign( - PreclaimContext const& ctx, - AccountID const& id, - STObject const& sigObject); + ReadView const& view, + ApplyFlags flags, + AccountID const& idAccount, + STObject const& sigObject, + beast::Journal const j); // Base class always returns true static bool @@ -323,15 +325,18 @@ private: payFee(); static NotTEC checkSingleSign( - PreclaimContext const& ctx, + ReadView const& view, AccountID const& idSigner, AccountID const& idAccount, - std::shared_ptr sleAccount); + std::shared_ptr sleAccount, + beast::Journal const j); static NotTEC checkMultiSign( - PreclaimContext const& ctx, + ReadView const& view, + ApplyFlags flags, AccountID const& id, - STObject const& sigObject); + STObject const& sigObject, + beast::Journal const j); void trapTransaction(uint256) const; From b127b24daee7d918c342a4e5da5d1f46b1ed7046 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 30 Sep 2025 22:25:53 -0400 Subject: [PATCH 101/291] Addressed review feedback from @gregtatcam: - All current items are done - Mostly comments - Restructured PaymentParts (formerly PeriodicPaymentParts) to bring along fees, and removed the computed / combined PeriodicPayment from places that should be using PaymentParts instead. --- src/test/app/Invariants_test.cpp | 21 ++++++ src/test/app/Loan_test.cpp | 47 +++++++++----- src/test/jtx/TestHelpers.h | 8 +-- src/test/jtx/impl/TestHelpers.cpp | 12 ++-- src/test/jtx/impl/sig.cpp | 6 +- src/test/jtx/multisign.h | 6 ++ src/test/jtx/sig.h | 22 +++++-- src/xrpld/app/misc/LendingHelpers.h | 82 ++++++++++++++---------- src/xrpld/app/tx/detail/InvariantCheck.h | 2 +- src/xrpld/app/tx/detail/LoanManage.cpp | 18 +----- src/xrpld/app/tx/detail/LoanManage.h | 36 +++++++++++ src/xrpld/app/tx/detail/LoanPay.cpp | 35 +++++++++- 12 files changed, 206 insertions(+), 89 deletions(-) diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index 2b3cc84faf..f279c8dec2 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -162,6 +162,9 @@ class Invariants_test : public beast::unit_test::suite auto sle = ac.view().peek(keylet::account(A1.id())); if (!sle) return false; + // Clear the balance so the "account deletion left behind a + // non-zero balance" check doesn't trip earlier than the desired + // check. sle->at(sfBalance) = beast::zero; ac.view().erase(sle); return true; @@ -190,6 +193,9 @@ class Invariants_test : public beast::unit_test::suite auto sleA2 = ac.view().peek(keylet::account(A2.id())); if (!sleA1 || !sleA2) return false; + // Clear the balance so the "account deletion left behind a + // non-zero balance" check doesn't trip earlier than the desired + // check. sleA1->at(sfBalance) = beast::zero; sleA2->at(sfBalance) = beast::zero; ac.view().erase(sleA1); @@ -232,6 +238,9 @@ class Invariants_test : public beast::unit_test::suite auto const sleA1 = ac.view().peek(keylet::account(a1)); if (!sleA1) return false; + // Clear the balance so the "account deletion left behind a + // non-zero balance" check doesn't trip earlier than the desired + // check. sleA1->at(sfBalance) = beast::zero; BEAST_EXPECT(sleA1->at(sfOwnerCount) == 0); adjustOwnerCount(ac.view(), sleA1, 1, ac.journal); @@ -270,6 +279,9 @@ class Invariants_test : public beast::unit_test::suite auto const key = std::invoke(keyletfunc, a1); auto const newSLE = std::make_shared(key); ac.view().insert(newSLE); + // Clear the balance so the "account deletion left behind a + // non-zero balance" check doesn't trip earlier than the + // desired check. sleA1->at(sfBalance) = beast::zero; ac.view().erase(sleA1); @@ -287,6 +299,9 @@ class Invariants_test : public beast::unit_test::suite auto sle = ac.view().peek(keylet::account(A1.id())); if (!sle) return false; + // Clear the balance so the "account deletion left behind a + // non-zero balance" check doesn't trip earlier than the desired + // check. sle->at(sfBalance) = beast::zero; sle->at(sfOwnerCount) = 0; ac.view().erase(sle); @@ -319,6 +334,9 @@ class Invariants_test : public beast::unit_test::suite BEAST_EXPECT(sle->at(~sfAMMID)); BEAST_EXPECT(sle->at(~sfAMMID) == ammKey); + // Clear the balance so the "account deletion left behind a + // non-zero balance" check doesn't trip earlier than the desired + // check. sle->at(sfBalance) = beast::zero; sle->at(sfOwnerCount) = 0; ac.view().erase(sle); @@ -383,6 +401,9 @@ class Invariants_test : public beast::unit_test::suite !ac.view().exists(ownerDirKeylet) || ac.view().emptyDirDelete(ownerDirKeylet)); + // Clear the balance so the "account deletion left behind a + // non-zero balance" check doesn't trip earlier than the desired + // check. sle->at(sfBalance) = beast::zero; sle->at(sfOwnerCount) = 0; ac.view().erase(sle); diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index d0345b517b..7627289dc0 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -55,7 +55,7 @@ class Loan_test : public beast::unit_test::suite // Single Asset Vault depends on MPTokensV1, but don't test every combo // of that. using namespace jtx; - auto failAll = [this](FeatureBitset features, bool goodVault = false) { + auto failAll = [this](FeatureBitset features) { Env env(*this, features); Account const alice{"alice"}; @@ -96,7 +96,7 @@ class Loan_test : public beast::unit_test::suite failAll(all - featureMPTokensV1); failAll(all - featureSingleAssetVault - featureLendingProtocol); failAll(all - featureSingleAssetVault); - failAll(all - featureLendingProtocol, true); + failAll(all - featureLendingProtocol); } struct BrokerInfo @@ -122,6 +122,9 @@ class Loan_test : public beast::unit_test::suite TenthBips32 const interestRate{}; }; + /** Helper class to compare the expected state of a loan and loan broker + * against the data in the ledger. + */ struct VerifyLoanStatus { public: @@ -145,6 +148,8 @@ class Loan_test : public beast::unit_test::suite { } + /** Checks the expected broker state against the ledger + */ void checkBroker( Number const& principalRequested, @@ -194,6 +199,9 @@ class Loan_test : public beast::unit_test::suite if (ownerCount == 0) { // Allow some slop for rounding IOUs + + // TODO: This needs to be an exact match once all the + // other rounding issues are worked out. auto const total = vaultSle->at(sfAssetsTotal); auto const available = vaultSle->at(sfAssetsAvailable); env.test.BEAST_EXPECT( @@ -210,21 +218,7 @@ class Loan_test : public beast::unit_test::suite } } - void - checkBroker( - LoanState const& state, - TenthBips32 interestRate, - std::uint32_t ownerCount) const - { - checkBroker( - state.principalRequested, - state.principalOutstanding, - interestRate, - state.paymentInterval, - state.paymentRemaining, - ownerCount); - } - + /** Checks both the loan and broker expect states against the ledger */ void operator()( std::uint32_t previousPaymentDate, @@ -300,6 +294,7 @@ class Loan_test : public beast::unit_test::suite } } + /** Checks both the loan and broker expect states against the ledger */ void operator()(LoanState const& state) const { @@ -442,6 +437,16 @@ class Loan_test : public beast::unit_test::suite return true; } + /** Runs through the complete lifecycle of a loan + * + * 1. Create a loan. + * 2. Test a bunch of transaction failure conditions. + * 3. Use the `toEndOfLife` callback to take the loan to 0. How that is done + * depends on the callback. e.g. Default, Early payoff, make all the + * normal payments, etc. + * 4. Delete the loan. The loan will alternate between being deleted by the + * lender and the borrower. + */ void lifecycle( std::string const& caseLabel, @@ -745,6 +750,14 @@ class Loan_test : public beast::unit_test::suite } } + /** Wrapper to run a series of lifecycle tests for a given asset and loan + * amount + * + * Will be used in the future to vary the loan parameters. For now, it is + * only called once. + * + * Tests a bunch of LoanSet failure conditions before lifecycle. + */ template void testCaseWrapper( diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index efe7e0c83b..f9220eb3fa 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -746,21 +746,19 @@ set(AccountID const& account, uint256 const& vaultId, std::uint32_t flags = 0); // Use "del" because "delete" is a reserved word in C++. Json::Value -del(AccountID const& account, - uint256 const& loanBrokerID, - std::uint32_t flags = 0); +del(AccountID const& account, uint256 const& brokerID, std::uint32_t flags = 0); Json::Value coverDeposit( AccountID const& account, - uint256 const& loanBrokerID, + uint256 const& brokerID, STAmount const& amount, std::uint32_t flags = 0); Json::Value coverWithdraw( AccountID const& account, - uint256 const& loanBrokerID, + uint256 const& brokerID, STAmount const& amount, std::uint32_t flags = 0); diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index 05959dadfb..656e195303 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -388,12 +388,12 @@ set(AccountID const& account, uint256 const& vaultId, uint32_t flags) } Json::Value -del(AccountID const& account, uint256 const& loanBrokerID, uint32_t flags) +del(AccountID const& account, uint256 const& brokerID, uint32_t flags) { Json::Value jv; jv[sfTransactionType] = jss::LoanBrokerDelete; jv[sfAccount] = to_string(account); - jv[sfLoanBrokerID] = to_string(loanBrokerID); + jv[sfLoanBrokerID] = to_string(brokerID); jv[sfFlags] = flags; return jv; } @@ -401,14 +401,14 @@ del(AccountID const& account, uint256 const& loanBrokerID, uint32_t flags) Json::Value coverDeposit( AccountID const& account, - uint256 const& loanBrokerID, + uint256 const& brokerID, STAmount const& amount, uint32_t flags) { Json::Value jv; jv[sfTransactionType] = jss::LoanBrokerCoverDeposit; jv[sfAccount] = to_string(account); - jv[sfLoanBrokerID] = to_string(loanBrokerID); + jv[sfLoanBrokerID] = to_string(brokerID); jv[sfAmount] = amount.getJson(JsonOptions::none); jv[sfFlags] = flags; return jv; @@ -417,14 +417,14 @@ coverDeposit( Json::Value coverWithdraw( AccountID const& account, - uint256 const& loanBrokerID, + uint256 const& brokerID, STAmount const& amount, uint32_t flags) { Json::Value jv; jv[sfTransactionType] = jss::LoanBrokerCoverWithdraw; jv[sfAccount] = to_string(account); - jv[sfLoanBrokerID] = to_string(loanBrokerID); + jv[sfLoanBrokerID] = to_string(brokerID); jv[sfAmount] = amount.getJson(JsonOptions::none); jv[sfFlags] = flags; return jv; diff --git a/src/test/jtx/impl/sig.cpp b/src/test/jtx/impl/sig.cpp index 2763df5271..6ea1c153cb 100644 --- a/src/test/jtx/impl/sig.cpp +++ b/src/test/jtx/impl/sig.cpp @@ -29,19 +29,19 @@ sig::operator()(Env&, JTx& jt) const { if (!manual_) return; - if (!subField) + if (!subField_) jt.fill_sig = false; if (account_) { // VFALCO Inefficient pre-C++14 auto const account = *account_; - auto callback = [subField = subField, account](Env&, JTx& jtx) { + auto callback = [subField = subField_, account](Env&, JTx& jtx) { // Where to put the signature. Supports sfCounterPartySignature. auto& sigObject = subField ? jtx[*subField] : jtx.jv; jtx::sign(jtx.jv, account, sigObject); }; - if (!subField) + if (!subField_) jt.mainSigners.emplace_back(callback); else jt.postSigners.emplace_back(callback); diff --git a/src/test/jtx/multisign.h b/src/test/jtx/multisign.h index aedc660d92..7a035a9ff0 100644 --- a/src/test/jtx/multisign.h +++ b/src/test/jtx/multisign.h @@ -67,7 +67,13 @@ class msig { public: std::vector signers; + /** Alternative transaction object field in which to place the signer list. + * + * subField is only supported if an account_ is provided as well. + */ SField const* const subField = nullptr; + /// Used solely as a convenience placeholder for ctors that do _not_ specify + /// a subfield. static constexpr SField* const topLevel = nullptr; msig(SField const* subField_, std::vector signers_) diff --git a/src/test/jtx/sig.h b/src/test/jtx/sig.h index c101d9e491..b96a306a37 100644 --- a/src/test/jtx/sig.h +++ b/src/test/jtx/sig.h @@ -35,9 +35,19 @@ class sig { private: bool manual_ = true; - /// subField only supported with explicit account - SField const* const subField = nullptr; + /** Alternative transaction object field in which to place the signature. + * + * subField is only supported if an account_ is provided as well. + */ + SField const* const subField_ = nullptr; + /** Account that will generate the signature. + * + * If not provided, no signature will be added by this helper. See also + * Env::autofill_sig. + */ std::optional account_; + /// Used solely as a convenience placeholder for ctors that do _not_ specify + /// a subfield. static constexpr SField* const topLevel = nullptr; public: @@ -49,13 +59,13 @@ public: { } - explicit sig(SField const* subField_, Account const& account) - : subField(subField_), account_(account) + explicit sig(SField const* subField, Account const& account) + : subField_(subField), account_(account) { } - explicit sig(SField const& subField_, Account const& account) - : sig(&subField_, account) + explicit sig(SField const& subField, Account const& account) + : sig(&subField, account) { } diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index bc8e5c3b9a..cea123d75d 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -237,19 +237,21 @@ loanLatePaymentInterest( originalPrincipal); } -struct PeriodicPaymentParts +struct PaymentParts { Number interest; Number principal; + Number fee; }; template -PeriodicPaymentParts +PaymentParts computePeriodicPaymentParts( A const& asset, Number const& originalPrincipal, Number const& principalOutstanding, Number const& periodicPaymentAmount, + Number const& serviceFee, Number const& periodicRate, std::uint32_t paymentRemaining) { @@ -257,6 +259,8 @@ computePeriodicPaymentParts( * This function is derived from the XLS-66 spec, section 3.2.4.1.1 (Regular * Payment) */ + Number const roundedFee = + roundToAsset(asset, serviceFee, originalPrincipal); if (paymentRemaining == 1) { // If there's only one payment left, we need to pay off the principal. @@ -265,7 +269,10 @@ computePeriodicPaymentParts( periodicPaymentAmount - principalOutstanding, originalPrincipal, Number::upward); - return {interest, principalOutstanding}; + return { + .interest = interest, + .principal = principalOutstanding, + .fee = roundedFee}; } /* * From the spec, once the periodicPayment is computed: @@ -320,7 +327,7 @@ computePeriodicPaymentParts( "ripple::computePeriodicPaymentParts", "valid principal"); - return {interest, principal}; + return {.interest = interest, .principal = principal, .fee = roundedFee}; } // This structure is explained in the XLS-66 spec, section 3.2.4.4 (Failure @@ -342,16 +349,18 @@ struct LoanPaymentParts Number feePaid; }; -struct LatePaymentParams -{ -}; - /* Handle possible late payments. * * If this function processed a late payment, the return value will be * a LoanPaymentParts object. If the loan is not late, the return will be an * Unexpected(tesSUCCESS). Otherwise, it'll be an Unexpected with the error code * the caller is expected to return. + * + * + * This function is an implementation of the XLS-66 spec, based on + * * section 3.2.4.3 (Transaction Pseudo-code), specifically the bit + * labeled "the payment is late" + * * section 3.2.4.1.2 (Late Payment) */ template Expected @@ -362,12 +371,11 @@ handleLatePayment( Int32Proxy& paymentRemainingProxy, Int32Proxy& prevPaymentDateProxy, Int32Proxy& nextDueDateProxy, - PeriodicPaymentParts const& periodic, + PaymentParts const& periodic, std::uint32_t const startDate, std::uint32_t const paymentInterval, TenthBips32 const lateInterestRate, Number const& originalPrincipalRequested, - Number const& periodicPaymentAmount, Number const& latePaymentFee, STAmount const& amount, beast::Journal j) @@ -376,6 +384,8 @@ handleLatePayment( return Unexpected(tesSUCCESS); // the payment is late + // Late payment interest is only the part of the interest that comes from + // being late, as computed by 3.2.4.1.2. auto const latePaymentInterest = loanLatePaymentInterest( asset, principalOutstandingProxy, @@ -387,20 +397,23 @@ handleLatePayment( XRPL_ASSERT( latePaymentInterest >= 0, "ripple::handleLatePayment : valid late interest"); - auto const latePaymentAmount = - periodicPaymentAmount + latePaymentInterest + latePaymentFee; + PaymentParts const late{ + .interest = latePaymentInterest + periodic.interest, + .principal = periodic.principal, + .fee = latePaymentFee + periodic.fee}; + auto const totalDue = late.principal + late.interest + late.fee; - if (amount < latePaymentAmount) + if (amount < totalDue) { JLOG(j.warn()) << "Late loan payment amount is insufficient. Due: " - << latePaymentAmount << ", paid: " << amount; + << totalDue << ", paid: " << amount; return Unexpected(tecINSUFFICIENT_PAYMENT); } paymentRemainingProxy -= 1; // A single payment always pays the same amount of principal. Only the // interest and fees are extra for a late payment - principalOutstandingProxy -= periodic.principal; + principalOutstandingProxy -= late.principal; // Make sure this does an assignment prevPaymentDateProxy = nextDueDateProxy; @@ -409,10 +422,10 @@ handleLatePayment( // A late payment increases the value of the loan by the difference // between periodic and late payment interest return LoanPaymentParts{ - periodic.principal, - latePaymentInterest + periodic.interest, - latePaymentInterest, - latePaymentFee}; + .principalPaid = late.principal, + .interestPaid = late.interest, + .valueChange = latePaymentInterest, + .feePaid = late.fee}; } /* Handle possible full payments. @@ -459,21 +472,21 @@ handleFullPayment( XRPL_ASSERT( accruedInterest >= 0, "ripple::handleFullPayment : valid accrued interest"); - auto const closePrepaymentInterest = roundToAsset( + auto const prepaymentPenalty = roundToAsset( asset, tenthBipsOfValue(principalOutstandingProxy.value(), closeInterestRate), originalPrincipalRequested); XRPL_ASSERT( - closePrepaymentInterest >= 0, + prepaymentPenalty >= 0, "ripple::handleFullPayment : valid prepayment " "interest"); - auto const totalInterest = accruedInterest + closePrepaymentInterest; + auto const totalInterest = accruedInterest + prepaymentPenalty; auto const closeFullPayment = principalOutstandingProxy + totalInterest + closePaymentFee; if (amount < closeFullPayment) // If the payment is less than the full payment amount, it's not - // sufficient to be a full payment. + // sufficient to be a full payment, but that's not an error. return Unexpected(tesSUCCESS); // Make a full payment @@ -484,7 +497,10 @@ handleFullPayment( auto const valueChange = totalInterest - totalInterestOutstanding; LoanPaymentParts const result{ - principalOutstandingProxy, totalInterest, valueChange, closePaymentFee}; + .principalPaid = principalOutstandingProxy, + .interestPaid = totalInterest, + .valueChange = valueChange, + .feePaid = closePaymentFee}; paymentRemainingProxy = 0; principalOutstandingProxy = 0; @@ -556,6 +572,7 @@ loanMakePayment( originalPrincipalRequested, principalOutstandingProxy, periodicPaymentAmount, + serviceFee, periodicRate, paymentRemainingProxy); @@ -596,7 +613,6 @@ loanMakePayment( paymentInterval, lateInterestRate, originalPrincipalRequested, - periodicPaymentAmount, latePaymentFee, amount, j)) @@ -631,11 +647,7 @@ loanMakePayment( // if the payment is not late nor if it's a full payment, then it must // be a periodic one, with possible overpayments - auto const totalDue = roundToAsset( - asset, - periodicPaymentAmount + serviceFee, - originalPrincipalRequested, - Number::upward); + auto const totalDue = periodic.interest + periodic.principal + periodic.fee; std::optional mg(Number::downward); std::int64_t const fullPeriodicPayments = [&]() { @@ -654,7 +666,7 @@ loanMakePayment( if (fullPeriodicPayments < 1) { JLOG(j.warn()) << "Periodic loan payment amount is insufficient. Due: " - << periodicPaymentAmount << ", paid: " << amount; + << totalDue << ", paid: " << amount; return Unexpected(tecINSUFFICIENT_PAYMENT); } @@ -665,7 +677,7 @@ loanMakePayment( Number totalInterestPaid = 0; Number loanValueChange = 0; - std::optional future = periodic; + std::optional future = periodic; for (int i = 0; i < fullPeriodicPayments; ++i) { // Only do the work if we need to @@ -675,6 +687,7 @@ loanMakePayment( originalPrincipalRequested, principalOutstandingProxy, periodicPaymentAmount, + serviceFee, periodicRate, paymentRemainingProxy); XRPL_ASSERT( @@ -763,7 +776,10 @@ loanMakePayment( totalFeePaid, "ripple::loanMakePayment : totalFeePaid rounded"); return LoanPaymentParts{ - totalPrincipalPaid, totalInterestPaid, loanValueChange, totalFeePaid}; + .principalPaid = totalPrincipalPaid, + .interestPaid = totalInterestPaid, + .valueChange = loanValueChange, + .feePaid = totalFeePaid}; } } // namespace ripple diff --git a/src/xrpld/app/tx/detail/InvariantCheck.h b/src/xrpld/app/tx/detail/InvariantCheck.h index c3725cbc51..b2527c53e0 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.h +++ b/src/xrpld/app/tx/detail/InvariantCheck.h @@ -176,7 +176,7 @@ public: class AccountRootsDeletedClean { // Pair is . Before is used for most of the checks, so that - // if, for example, and object ID field is cleared, but the object is not + // if, for example, an object ID field is cleared, but the object is not // deleted, it can still be found. After is used specifically for any checks // that are expected as part of the deletion, such as zeroing out the // balance. diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index 2e2815db6a..38efff1570 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -133,14 +133,13 @@ LoanManage::preclaim(PreclaimContext const& ctx) } TER -defaultLoan( +LoanManage::defaultLoan( ApplyView& view, SLE::ref loanSle, SLE::ref brokerSle, SLE::ref vaultSle, Number const& principalOutstanding, Number const& interestOutstanding, - std::uint32_t paymentInterval, Asset const& vaultAsset, beast::Journal j) { @@ -260,15 +259,12 @@ defaultLoan( } TER -impairLoan( +LoanManage::impairLoan( ApplyView& view, SLE::ref loanSle, - SLE::ref brokerSle, SLE::ref vaultSle, Number const& principalOutstanding, Number const& interestOutstanding, - std::uint32_t paymentInterval, - Asset const& vaultAsset, beast::Journal j) { // Update the Vault object(set "paper loss") @@ -300,15 +296,13 @@ impairLoan( } TER -unimpairLoan( +LoanManage::unimpairLoan( ApplyView& view, SLE::ref loanSle, - SLE::ref brokerSle, SLE::ref vaultSle, Number const& principalOutstanding, Number const& interestOutstanding, std::uint32_t paymentInterval, - Asset const& vaultAsset, beast::Journal j) { // Update the Vault object(clear "paper loss") @@ -394,7 +388,6 @@ LoanManage::doApply() vaultSle, principalOutstanding, interestOutstanding, - paymentInterval, vaultAsset, j_)) return ter; @@ -404,12 +397,9 @@ LoanManage::doApply() if (auto const ter = impairLoan( view, loanSle, - brokerSle, vaultSle, principalOutstanding, interestOutstanding, - paymentInterval, - vaultAsset, j_)) return ter; } @@ -418,12 +408,10 @@ LoanManage::doApply() if (auto const ter = unimpairLoan( view, loanSle, - brokerSle, vaultSle, principalOutstanding, interestOutstanding, paymentInterval, - vaultAsset, j_)) return ter; } diff --git a/src/xrpld/app/tx/detail/LoanManage.h b/src/xrpld/app/tx/detail/LoanManage.h index ade02f4c20..92c5854115 100644 --- a/src/xrpld/app/tx/detail/LoanManage.h +++ b/src/xrpld/app/tx/detail/LoanManage.h @@ -45,6 +45,42 @@ public: static TER preclaim(PreclaimContext const& ctx); + /** Helper function that might be needed by other transactors + */ + static TER + defaultLoan( + ApplyView& view, + SLE::ref loanSle, + SLE::ref brokerSle, + SLE::ref vaultSle, + Number const& principalOutstanding, + Number const& interestOutstanding, + Asset const& vaultAsset, + beast::Journal j); + + /** Helper function that might be needed by other transactors + */ + static TER + impairLoan( + ApplyView& view, + SLE::ref loanSle, + SLE::ref vaultSle, + Number const& principalOutstanding, + Number const& interestOutstanding, + beast::Journal j); + + /** Helper function that might be needed by other transactors + */ + static TER + unimpairLoan( + ApplyView& view, + SLE::ref loanSle, + SLE::ref vaultSle, + Number const& principalOutstanding, + Number const& interestOutstanding, + std::uint32_t paymentInterval, + beast::Journal j); + TER doApply() override; }; diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 92dbbd2044..fe58c66711 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -20,6 +20,7 @@ #include // #include +#include namespace ripple { @@ -148,6 +149,37 @@ LoanPay::doApply() // Loan object state changes Number const originalPrincipalRequested = loanSle->at(sfPrincipalRequested); + // Unimpair the loan if it was impaired. Do this before the payment is + // attempted, so the original values can be used. If the payment fails, this + // change will be discarded. + if (loanSle->isFlag(lsfLoanImpaired)) + { + TenthBips32 const interestRate{loanSle->at(sfInterestRate)}; + auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding); + + TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; + auto const paymentInterval = loanSle->at(sfPaymentInterval); + auto const paymentsRemaining = loanSle->at(sfPaymentRemaining); + + auto const interestOutstanding = loanInterestOutstandingMinusFee( + asset, + originalPrincipalRequested, + principalOutstanding.value(), + interestRate, + paymentInterval, + paymentsRemaining, + managementFeeRate); + + LoanManage::unimpairLoan( + view, + loanSle, + vaultSle, + principalOutstanding, + interestOutstanding, + paymentInterval, + j_); + } + Expected paymentParts = loanMakePayment(asset, view, loanSle, amount, j_); @@ -158,9 +190,6 @@ LoanPay::doApply() // has been modified. view.update(loanSle); - // If the loan was impaired, it isn't anymore. - loanSle->clearFlag(lsfLoanImpaired); - XRPL_ASSERT_PARTS( paymentParts->principalPaid > 0, "ripple::LoanPay::doApply", From 1888784e8e77205ca25487feea9b1e439a08037a Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 1 Oct 2025 14:14:13 -0400 Subject: [PATCH 102/291] Review feedback from @Bronek: Add an assert --- src/xrpld/rpc/handlers/AccountInfo.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/xrpld/rpc/handlers/AccountInfo.cpp b/src/xrpld/rpc/handlers/AccountInfo.cpp index d8ab90cb9b..9a6a23ad55 100644 --- a/src/xrpld/rpc/handlers/AccountInfo.cpp +++ b/src/xrpld/rpc/handlers/AccountInfo.cpp @@ -160,6 +160,10 @@ doAccountInfo(RPC::JsonContext& context) { // Remove the ID suffix from the field name. name = name.substr(0, name.size() - 2); + XRPL_ASSERT_PARTS( + !name.empty(), + "ripple::doAccountInfo", + "name is not empty") } // ValidPseudoAccounts invariant guarantees that only one field // can be set From aac25e8c7964216bdf10e6dd6b92acfb71e688dd Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 1 Oct 2025 15:56:06 -0400 Subject: [PATCH 103/291] fixup! Review feedback from @Bronek: Add an assert --- src/xrpld/rpc/handlers/AccountInfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrpld/rpc/handlers/AccountInfo.cpp b/src/xrpld/rpc/handlers/AccountInfo.cpp index 9a6a23ad55..62d54499b2 100644 --- a/src/xrpld/rpc/handlers/AccountInfo.cpp +++ b/src/xrpld/rpc/handlers/AccountInfo.cpp @@ -163,7 +163,7 @@ doAccountInfo(RPC::JsonContext& context) XRPL_ASSERT_PARTS( !name.empty(), "ripple::doAccountInfo", - "name is not empty") + "name is not empty"); } // ValidPseudoAccounts invariant guarantees that only one field // can be set From d576e9d214b82043b29bc7a7301a36a6b19f184b Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 2 Oct 2025 22:07:33 -0400 Subject: [PATCH 104/291] Include exception messages in multi signature check result --- src/libxrpl/protocol/STTx.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index efff940a99..98895df1a6 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -524,6 +524,7 @@ multiSignHelper( // Verify the signature. bool validSig = false; + std::optional errorWhat; try { auto spk = signer.getFieldVL(sfSigningPubKey); @@ -537,15 +538,16 @@ multiSignHelper( fullyCanonical); } } - catch (std::exception const&) + catch (std::exception const& e) { // We assume any problem lies with the signature. validSig = false; + errorWhat = e.what(); } if (!validSig) return Unexpected( std::string("Invalid signature on account ") + - toBase58(accountID) + "."); + toBase58(accountID) + errorWhat.value_or("") + "."); } // All signatures verified. return {}; From 23045fcbef4db6edd06421f85ebfe2b3912bac8f Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 3 Oct 2025 14:56:39 -0400 Subject: [PATCH 105/291] Add support for extra transaction signature validation - Restructures `STTx` signature checking code to be able to handle a `sigObject`, which may be the full transaction, or may be an object field containing a separate signature. Either way, the `sigObject` can be a single- or multi-sign signature. - This is distinct from 550f90a75e (#5594), which changed the check in Transactor, which validates whether a given account is allowed to sign for the given transaction. This cryptographically checks the signature validity. --- include/xrpl/protocol/STTx.h | 44 +++++++++-- src/libxrpl/protocol/STTx.cpp | 134 +++++++++++++++++++++++----------- 2 files changed, 129 insertions(+), 49 deletions(-) diff --git a/include/xrpl/protocol/STTx.h b/include/xrpl/protocol/STTx.h index f0d2157283..1e3695050d 100644 --- a/include/xrpl/protocol/STTx.h +++ b/include/xrpl/protocol/STTx.h @@ -88,7 +88,13 @@ public: // Outer transaction functions / signature functions. Blob - getSignature() const; + getSignature(STObject const& sigObject) const; + + Blob + getSignature() const + { + return getSignature(*this); + } uint256 getSigningHash() const; @@ -119,13 +125,34 @@ public: getJson(JsonOptions options, bool binary) const; void - sign(PublicKey const& publicKey, SecretKey const& secretKey); + sign( + PublicKey const& publicKey, + SecretKey const& secretKey, + std::optional> signatureTarget = + {}); - /** Check the signature. - @return `true` if valid signature. If invalid, the error message string. - */ enum class RequireFullyCanonicalSig : bool { no, yes }; + /** Check the signature. + @param requireCanonicalSig If `true`, check that the signature is fully + canonical. If `false`, only check that the signature is valid. + @param rules The current ledger rules. + @param pSig Pointer to object that contains the signature fields, if not + using "this". Will most often be null + @return `true` if valid signature. If invalid, the error message string. + */ + Expected + checkSign( + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules, + STObject const* pSig) const; + + /** Check the signature. + @param requireCanonicalSig If `true`, check that the signature is fully + canonical. If `false`, only check that the signature is valid. + @param rules The current ledger rules. + @return `true` if valid signature. If invalid, the error message string. + */ Expected checkSign(RequireFullyCanonicalSig requireCanonicalSig, Rules const& rules) const; @@ -155,12 +182,15 @@ public: private: Expected - checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const; + checkSingleSign( + RequireFullyCanonicalSig requireCanonicalSig, + STObject const* pSig) const; Expected checkMultiSign( RequireFullyCanonicalSig requireCanonicalSig, - Rules const& rules) const; + Rules const& rules, + STObject const* pSig) const; Expected checkBatchSingleSign( diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index 8be8f906a5..20ca76e789 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -200,11 +200,11 @@ STTx::getSigningHash() const } Blob -STTx::getSignature() const +STTx::getSignature(STObject const& sigObject) const { try { - return getFieldVL(sfTxnSignature); + return sigObject.getFieldVL(sfTxnSignature); } catch (std::exception const&) { @@ -234,35 +234,69 @@ STTx::getSeqValue() const } void -STTx::sign(PublicKey const& publicKey, SecretKey const& secretKey) +STTx::sign( + PublicKey const& publicKey, + SecretKey const& secretKey, + std::optional> signatureTarget) { auto const data = getSigningData(*this); auto const sig = ripple::sign(publicKey, secretKey, makeSlice(data)); - setFieldVL(sfTxnSignature, sig); + if (signatureTarget) + { + auto& target = peekFieldObject(*signatureTarget); + target.setFieldVL(sfTxnSignature, sig); + } + else + { + setFieldVL(sfTxnSignature, sig); + } tid_ = getHash(HashPrefix::transactionID); } +Expected +STTx::checkSign( + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules, + STObject const* pSig) const +{ + try + { + // Determine whether we're single- or multi-signing by looking + // at the SigningPubKey. If it's empty we must be + // multi-signing. Otherwise we're single-signing. + STObject const& sigObject{pSig ? *pSig : *this}; + + Blob const& signingPubKey = sigObject.getFieldVL(sfSigningPubKey); + return signingPubKey.empty() + ? checkMultiSign(requireCanonicalSig, rules, pSig) + : checkSingleSign(requireCanonicalSig, pSig); + } + catch (std::exception const&) + { + } + return Unexpected("Internal signature check failure."); +} + Expected STTx::checkSign( RequireFullyCanonicalSig requireCanonicalSig, Rules const& rules) const { - try + if (auto const ret = checkSign(requireCanonicalSig, rules, nullptr); !ret) + return ret; + + /* Placeholder for field that will be added by Lending Protocol + if (isFieldPresent(sfCounterpartySignature)) { - // Determine whether we're single- or multi-signing by looking - // at the SigningPubKey. If it's empty we must be - // multi-signing. Otherwise we're single-signing. - Blob const& signingPubKey = getFieldVL(sfSigningPubKey); - return signingPubKey.empty() - ? checkMultiSign(requireCanonicalSig, rules) - : checkSingleSign(requireCanonicalSig); + auto const counterSig = getFieldObject(sfCounterpartySignature); + if (auto const ret = checkSign(requireCanonicalSig, rules, &counterSig); + !ret) + return Unexpected("Counterparty: " + ret.error()); } - catch (std::exception const&) - { - } - return Unexpected("Internal signature check failure."); + */ + return {}; } Expected @@ -382,23 +416,23 @@ STTx::getMetaSQL( static Expected singleSignHelper( - STObject const& signer, + STObject const& sigObject, Slice const& data, bool const fullyCanonical) { // We don't allow both a non-empty sfSigningPubKey and an sfSigners. // That would allow the transaction to be signed two ways. So if both // fields are present the signature is invalid. - if (signer.isFieldPresent(sfSigners)) + if (sigObject.isFieldPresent(sfSigners)) return Unexpected("Cannot both single- and multi-sign."); bool validSig = false; try { - auto const spk = signer.getFieldVL(sfSigningPubKey); + auto const spk = sigObject.getFieldVL(sfSigningPubKey); if (publicKeyType(makeSlice(spk))) { - Blob const signature = signer.getFieldVL(sfTxnSignature); + Blob const signature = sigObject.getFieldVL(sfTxnSignature); validSig = verify( PublicKey(makeSlice(spk)), data, @@ -418,12 +452,15 @@ singleSignHelper( } Expected -STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const +STTx::checkSingleSign( + RequireFullyCanonicalSig requireCanonicalSig, + STObject const* pSig) const { + STObject const& sigObject{pSig ? *pSig : *this}; auto const data = getSigningData(*this); bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || (requireCanonicalSig == STTx::RequireFullyCanonicalSig::yes); - return singleSignHelper(*this, makeSlice(data), fullyCanonical); + return singleSignHelper(sigObject, makeSlice(data), fullyCanonical); } Expected @@ -440,31 +477,29 @@ STTx::checkBatchSingleSign( Expected multiSignHelper( - STObject const& signerObj, + STObject const& sigObject, + std::optional txnAccountID, bool const fullyCanonical, std::function makeMsg, Rules const& rules) { // Make sure the MultiSigners are present. Otherwise they are not // attempting multi-signing and we just have a bad SigningPubKey. - if (!signerObj.isFieldPresent(sfSigners)) + if (!sigObject.isFieldPresent(sfSigners)) return Unexpected("Empty SigningPubKey."); // We don't allow both an sfSigners and an sfTxnSignature. Both fields // being present would indicate that the transaction is signed both ways. - if (signerObj.isFieldPresent(sfTxnSignature)) + if (sigObject.isFieldPresent(sfTxnSignature)) return Unexpected("Cannot both single- and multi-sign."); - STArray const& signers{signerObj.getFieldArray(sfSigners)}; + STArray const& signers{sigObject.getFieldArray(sfSigners)}; // There are well known bounds that the number of signers must be within. if (signers.size() < STTx::minMultiSigners || signers.size() > STTx::maxMultiSigners(&rules)) return Unexpected("Invalid Signers array size."); - // We also use the sfAccount field inside the loop. Get it once. - auto const txnAccountID = signerObj.getAccountID(sfAccount); - // Signers must be in sorted order by AccountID. AccountID lastAccountID(beast::zero); @@ -472,8 +507,10 @@ multiSignHelper( { auto const accountID = signer.getAccountID(sfAccount); - // The account owner may not multisign for themselves. - if (accountID == txnAccountID) + // The account owner may not usually multisign for themselves. + // If they can, txnAccountID will be unseated, which is not equal to any + // value. + if (txnAccountID == accountID) return Unexpected("Invalid multisigner."); // No duplicate signers allowed. @@ -489,6 +526,7 @@ multiSignHelper( // Verify the signature. bool validSig = false; + std::optional errorWhat; try { auto spk = signer.getFieldVL(sfSigningPubKey); @@ -502,15 +540,16 @@ multiSignHelper( fullyCanonical); } } - catch (std::exception const&) + catch (std::exception const& e) { // We assume any problem lies with the signature. validSig = false; + errorWhat = e.what(); } if (!validSig) return Unexpected( std::string("Invalid signature on account ") + - toBase58(accountID) + "."); + toBase58(accountID) + errorWhat.value_or("") + "."); } // All signatures verified. return {}; @@ -532,8 +571,9 @@ STTx::checkBatchMultiSign( serializeBatch(dataStart, getFlags(), getBatchTransactionIDs()); return multiSignHelper( batchSigner, + std::nullopt, fullyCanonical, - [&dataStart](AccountID const& accountID) mutable -> Serializer { + [&dataStart](AccountID const& accountID) -> Serializer { Serializer s = dataStart; finishMultiSigningData(accountID, s); return s; @@ -544,19 +584,28 @@ STTx::checkBatchMultiSign( Expected STTx::checkMultiSign( RequireFullyCanonicalSig requireCanonicalSig, - Rules const& rules) const + Rules const& rules, + STObject const* pSig) const { + STObject const& sigObject{pSig ? *pSig : *this}; + bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || (requireCanonicalSig == RequireFullyCanonicalSig::yes); + // Used inside the loop in multiSignHelper to enforce that + // the account owner may not multisign for themselves. + auto const txnAccountID = + pSig ? std::nullopt : std::optional(getAccountID(sfAccount)); + // We can ease the computational load inside the loop a bit by // pre-constructing part of the data that we hash. Fill a Serializer // with the stuff that stays constant from signature to signature. Serializer dataStart = startMultiSigningData(*this); return multiSignHelper( - *this, + sigObject, + txnAccountID, fullyCanonical, - [&dataStart](AccountID const& accountID) mutable -> Serializer { + [&dataStart](AccountID const& accountID) -> Serializer { Serializer s = dataStart; finishMultiSigningData(accountID, s); return s; @@ -588,11 +637,12 @@ STTx::getBatchTransactionIDs() const XRPL_ASSERT( getFieldArray(sfRawTransactions).size() != 0, "STTx::getBatchTransactionIDs : empty raw transactions"); - if (batch_txn_ids_.size() != 0) - return batch_txn_ids_; - - for (STObject const& rb : getFieldArray(sfRawTransactions)) - batch_txn_ids_.push_back(rb.getHash(HashPrefix::transactionID)); + // Don't early return so that the size assert is always hit. + if (batch_txn_ids_.size() == 0) + { + for (STObject const& rb : getFieldArray(sfRawTransactions)) + batch_txn_ids_.push_back(rb.getHash(HashPrefix::transactionID)); + } XRPL_ASSERT( batch_txn_ids_.size() == getFieldArray(sfRawTransactions).size(), From 7120cce99600b40c14253a21db2ef441ebf6f80a Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 6 Oct 2025 15:10:54 -0400 Subject: [PATCH 106/291] Add jtx, STObject, and RPC support for sig object fields --- include/xrpl/protocol/STObject.h | 5 ++ include/xrpl/protocol/jss.h | 1 + src/libxrpl/protocol/STObject.cpp | 16 ++++ src/test/jtx/JTx.h | 6 +- src/test/jtx/TestHelpers.h | 2 +- src/test/jtx/amount.h | 8 +- src/test/jtx/impl/Env.cpp | 19 ++++- src/test/jtx/impl/mpt.cpp | 2 +- src/test/jtx/impl/multisign.cpp | 17 +++- src/test/jtx/impl/sig.cpp | 14 +++- src/test/jtx/impl/utility.cpp | 12 ++- src/test/jtx/mpt.h | 2 +- src/test/jtx/multisign.h | 53 ++++++++++++- src/test/jtx/sig.h | 25 +++++- src/test/jtx/utility.h | 6 ++ src/test/rpc/RPCCall_test.cpp | 98 +++++++++++++++++++----- src/xrpld/app/main/Main.cpp | 2 +- src/xrpld/app/tx/detail/Batch.cpp | 74 +++++++++++++----- src/xrpld/rpc/detail/RPCCall.cpp | 20 ++++- src/xrpld/rpc/detail/TransactionSign.cpp | 71 +++++++++++++++-- 20 files changed, 377 insertions(+), 76 deletions(-) diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index 1c22b08aba..84706c5833 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -244,6 +244,9 @@ public: getFieldPathSet(SField const& field) const; STVector256 const& getFieldV256(SField const& field) const; + // If not found, returns an object constructed with the given field + STObject + getFieldObject(SField const& field) const; STArray const& getFieldArray(SField const& field) const; STCurrency const& @@ -390,6 +393,8 @@ public: setFieldV256(SField const& field, STVector256 const& v); void setFieldArray(SField const& field, STArray const& v); + void + setFieldObject(SField const& field, STObject const& v); template void diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 8609aedaef..bb5af1fc4d 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -569,6 +569,7 @@ JSS(settle_delay); // out: AccountChannels JSS(severity); // in: LogLevel JSS(shares); // out: VaultInfo JSS(signature); // out: NetworkOPs, ChannelAuthorize +JSS(signature_target); // in: TransactionSign JSS(signature_verified); // out: ChannelVerify JSS(signing_key); // out: NetworkOPs JSS(signing_keys); // out: ValidatorList diff --git a/src/libxrpl/protocol/STObject.cpp b/src/libxrpl/protocol/STObject.cpp index 77e5fd1ad9..8fb21a638d 100644 --- a/src/libxrpl/protocol/STObject.cpp +++ b/src/libxrpl/protocol/STObject.cpp @@ -688,6 +688,16 @@ STObject::getFieldV256(SField const& field) const return getFieldByConstRef(field, empty); } +STObject +STObject::getFieldObject(SField const& field) const +{ + STObject const empty{field}; + auto ret = getFieldByConstRef(field, empty); + if (ret != empty) + ret.applyTemplateFromSField(field); + return ret; +} + STArray const& STObject::getFieldArray(SField const& field) const { @@ -833,6 +843,12 @@ STObject::setFieldArray(SField const& field, STArray const& v) setFieldUsingAssignment(field, v); } +void +STObject::setFieldObject(SField const& field, STObject const& v) +{ + setFieldUsingAssignment(field, v); +} + Json::Value STObject::getJson(JsonOptions options) const { diff --git a/src/test/jtx/JTx.h b/src/test/jtx/JTx.h index 198839dd28..36127f1843 100644 --- a/src/test/jtx/JTx.h +++ b/src/test/jtx/JTx.h @@ -54,7 +54,11 @@ struct JTx bool fill_sig = true; bool fill_netid = true; std::shared_ptr stx; - std::function signer; + // Functions that sign the transaction from the Account + std::vector> mainSigners; + // Functions that sign something else after the mainSigners, such as + // sfCounterpartySignature + std::vector> postSigners; JTx() = default; JTx(JTx const&) = default; diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index 7d14f23c92..5582ec7261 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -615,7 +615,7 @@ create( } // namespace check -static constexpr FeeLevel64 baseFeeLevel{256}; +static constexpr FeeLevel64 baseFeeLevel{TxQ::baseLevel}; static constexpr FeeLevel64 minEscalationFeeLevel = baseFeeLevel * 500; template diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h index a793f3a287..f14a65f6b8 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -213,14 +213,16 @@ public: template PrettyAmount - operator()(T v) const + operator()(T v, Number::rounding_mode rounding = Number::getround()) const { - return operator()(Number(v)); + return operator()(Number(v), rounding); } PrettyAmount - operator()(Number v) const + operator()(Number v, Number::rounding_mode rounding = Number::getround()) + const { + NumberRoundModeGuard mg(rounding); STAmount amount{asset_, v * scale_}; return {amount, ""}; } diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index ae99e1b5d6..23c1780244 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -34,6 +34,7 @@ #include #include +#include #include #include #include @@ -531,8 +532,22 @@ void Env::autofill_sig(JTx& jt) { auto& jv = jt.jv; - if (jt.signer) - return jt.signer(*this, jt); + + scope_success success([&]() { + // Call all the post-signers after the main signers or autofill are done + for (auto const& signer : jt.postSigners) + signer(*this, jt); + }); + + // Call all the main signers + if (!jt.mainSigners.empty()) + { + for (auto const& signer : jt.mainSigners) + signer(*this, jt); + return; + } + + // If the sig is still needed, get it here. if (!jt.fill_sig) return; auto const account = jv.isMember(sfDelegate.jsonName) diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index f2f51492e3..aaa5e433f2 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -507,7 +507,7 @@ MPTTester::getFlags(std::optional const& holder) const } MPT -MPTTester::operator[](std::string const& name) +MPTTester::operator[](std::string const& name) const { return MPT(name, issuanceID()); } diff --git a/src/test/jtx/impl/multisign.cpp b/src/test/jtx/impl/multisign.cpp index 6ed6df6804..fc6163a2f3 100644 --- a/src/test/jtx/impl/multisign.cpp +++ b/src/test/jtx/impl/multisign.cpp @@ -69,8 +69,15 @@ void msig::operator()(Env& env, JTx& jt) const { auto const mySigners = signers; - jt.signer = [mySigners, &env](Env&, JTx& jtx) { - jtx[sfSigningPubKey.getJsonName()] = ""; + auto callback = [subField = subField, mySigners, &env](Env&, JTx& jtx) { + // Where to put the signature. Supports sfCounterPartySignature. + auto& sigObject = subField ? jtx[*subField] : jtx.jv; + + // The signing pub key is only required at the top level. + if (!subField) + sigObject[sfSigningPubKey] = ""; + else if (sigObject.isNull()) + sigObject = Json::Value(Json::objectValue); std::optional st; try { @@ -81,7 +88,7 @@ msig::operator()(Env& env, JTx& jt) const env.test.log << pretty(jtx.jv) << std::endl; Rethrow(); } - auto& js = jtx[sfSigners.getJsonName()]; + auto& js = sigObject[sfSigners]; for (std::size_t i = 0; i < mySigners.size(); ++i) { auto const& e = mySigners[i]; @@ -96,6 +103,10 @@ msig::operator()(Env& env, JTx& jt) const strHex(Slice{sig.data(), sig.size()}); } }; + if (!subField) + jt.mainSigners.emplace_back(callback); + else + jt.postSigners.emplace_back(callback); } } // namespace jtx diff --git a/src/test/jtx/impl/sig.cpp b/src/test/jtx/impl/sig.cpp index fa1977fe08..6ea1c153cb 100644 --- a/src/test/jtx/impl/sig.cpp +++ b/src/test/jtx/impl/sig.cpp @@ -29,12 +29,22 @@ sig::operator()(Env&, JTx& jt) const { if (!manual_) return; - jt.fill_sig = false; + if (!subField_) + jt.fill_sig = false; if (account_) { // VFALCO Inefficient pre-C++14 auto const account = *account_; - jt.signer = [account](Env&, JTx& jtx) { jtx::sign(jtx.jv, account); }; + auto callback = [subField = subField_, account](Env&, JTx& jtx) { + // Where to put the signature. Supports sfCounterPartySignature. + auto& sigObject = subField ? jtx[*subField] : jtx.jv; + + jtx::sign(jtx.jv, account, sigObject); + }; + if (!subField_) + jt.mainSigners.emplace_back(callback); + else + jt.postSigners.emplace_back(callback); } } diff --git a/src/test/jtx/impl/utility.cpp b/src/test/jtx/impl/utility.cpp index 27b45a32cb..fbdbaee4ef 100644 --- a/src/test/jtx/impl/utility.cpp +++ b/src/test/jtx/impl/utility.cpp @@ -44,14 +44,20 @@ parse(Json::Value const& jv) } void -sign(Json::Value& jv, Account const& account) +sign(Json::Value& jv, Account const& account, Json::Value& sigObject) { - jv[jss::SigningPubKey] = strHex(account.pk().slice()); + sigObject[jss::SigningPubKey] = strHex(account.pk().slice()); Serializer ss; ss.add32(HashPrefix::txSign); parse(jv).addWithoutSigningFields(ss); auto const sig = ripple::sign(account.pk(), account.sk(), ss.slice()); - jv[jss::TxnSignature] = strHex(Slice{sig.data(), sig.size()}); + sigObject[jss::TxnSignature] = strHex(Slice{sig.data(), sig.size()}); +} + +void +sign(Json::Value& jv, Account const& account) +{ + sign(jv, account, jv); } void diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index 2eacac68ec..422afa8fab 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -235,7 +235,7 @@ public: getBalance(Account const& account) const; MPT - operator[](std::string const& name); + operator[](std::string const& name) const; private: using SLEP = std::shared_ptr; diff --git a/src/test/jtx/multisign.h b/src/test/jtx/multisign.h index 1fed895c6d..7a035a9ff0 100644 --- a/src/test/jtx/multisign.h +++ b/src/test/jtx/multisign.h @@ -67,18 +67,63 @@ class msig { public: std::vector signers; + /** Alternative transaction object field in which to place the signer list. + * + * subField is only supported if an account_ is provided as well. + */ + SField const* const subField = nullptr; + /// Used solely as a convenience placeholder for ctors that do _not_ specify + /// a subfield. + static constexpr SField* const topLevel = nullptr; - msig(std::vector signers_) : signers(std::move(signers_)) + msig(SField const* subField_, std::vector signers_) + : signers(std::move(signers_)), subField(subField_) { sortSigners(signers); } + msig(SField const& subField_, std::vector signers_) + : msig{&subField_, signers_} + { + } + + msig(std::vector signers_) : msig(topLevel, signers_) + { + } + template requires std::convertible_to - explicit msig(AccountType&& a0, Accounts&&... aN) - : signers{std::forward(a0), std::forward(aN)...} + explicit msig(SField const* subField_, AccountType&& a0, Accounts&&... aN) + : msig{ + subField_, + std::vector{ + std::forward(a0), + std::forward(aN)...}} + { + } + + template + requires std::convertible_to + explicit msig(SField const& subField_, AccountType&& a0, Accounts&&... aN) + : msig{ + &subField_, + std::vector{ + std::forward(a0), + std::forward(aN)...}} + { + } + + template + requires( + std::convertible_to && + !std::is_same_v) + explicit msig(AccountType&& a0, Accounts&&... aN) + : msig{ + topLevel, + std::vector{ + std::forward(a0), + std::forward(aN)...}} { - sortSigners(signers); } void diff --git a/src/test/jtx/sig.h b/src/test/jtx/sig.h index aa65a4f697..b96a306a37 100644 --- a/src/test/jtx/sig.h +++ b/src/test/jtx/sig.h @@ -35,7 +35,20 @@ class sig { private: bool manual_ = true; + /** Alternative transaction object field in which to place the signature. + * + * subField is only supported if an account_ is provided as well. + */ + SField const* const subField_ = nullptr; + /** Account that will generate the signature. + * + * If not provided, no signature will be added by this helper. See also + * Env::autofill_sig. + */ std::optional account_; + /// Used solely as a convenience placeholder for ctors that do _not_ specify + /// a subfield. + static constexpr SField* const topLevel = nullptr; public: explicit sig(autofill_t) : manual_(false) @@ -46,7 +59,17 @@ public: { } - explicit sig(Account const& account) : account_(account) + explicit sig(SField const* subField, Account const& account) + : subField_(subField), account_(account) + { + } + + explicit sig(SField const& subField, Account const& account) + : sig(&subField, account) + { + } + + explicit sig(Account const& account) : sig(topLevel, account) { } diff --git a/src/test/jtx/utility.h b/src/test/jtx/utility.h index 2c21fbd3ff..9e89c3bb93 100644 --- a/src/test/jtx/utility.h +++ b/src/test/jtx/utility.h @@ -51,6 +51,12 @@ struct parse_error : std::logic_error STObject parse(Json::Value const& jv); +/** Sign automatically into a specific Json field of the jv object. + @note This only works on accounts with multi-signing off. +*/ +void +sign(Json::Value& jv, Account const& account, Json::Value& sigObject); + /** Sign automatically. @note This only works on accounts with multi-signing off. */ diff --git a/src/test/rpc/RPCCall_test.cpp b/src/test/rpc/RPCCall_test.cpp index d22896388d..26a6a536ef 100644 --- a/src/test/rpc/RPCCall_test.cpp +++ b/src/test/rpc/RPCCall_test.cpp @@ -4643,10 +4643,34 @@ static RPCCallTestData const rpcCallTestArray[] = { } ] })"}, - {"sign: too many arguments.", + {"sign: offline flag with signature_target.", __LINE__, {"sign", "my_secret", R"({"json_argument":true})", "offline", "extra"}, RPCCallTestData::no_exception, + R"({ + "method" : "sign", + "params" : [ + { + "api_version" : %API_VER%, + "offline" : true, + "secret" : "my_secret", + "signature_target" : "extra", + "tx_json" : + { + "json_argument" : true + } + } + ] + })"}, + {"sign: too many arguments.", + __LINE__, + {"sign", + "my_secret", + R"({"json_argument":true})", + "offline", + "CounterpartySignature", + "extra"}, + RPCCallTestData::no_exception, R"({ "method" : "sign", "params" : [ @@ -4675,20 +4699,24 @@ static RPCCallTestData const rpcCallTestArray[] = { } ] })"}, - {"sign: invalid final argument.", + {"sign: misspelled offline flag interpreted as signature_target.", __LINE__, {"sign", "my_secret", R"({"json_argument":true})", "offlin"}, RPCCallTestData::no_exception, R"({ - "method" : "sign", - "params" : [ - { - "error" : "invalidParams", - "error_code" : 31, - "error_message" : "Invalid parameters." - } - ] - })"}, + "method" : "sign", + "params" : [ + { + "api_version" : %API_VER%, + "secret" : "my_secret", + "signature_target" : "offlin", + "tx_json" : + { + "json_argument" : true + } + } + ] + })"}, // sign_for // -------------------------------------------------------------------- @@ -4880,10 +4908,34 @@ static RPCCallTestData const rpcCallTestArray[] = { } ] })"}, - {"submit: too many arguments.", + {"submit: offline flag with signature_target.", __LINE__, {"submit", "my_secret", R"({"json_argument":true})", "offline", "extra"}, RPCCallTestData::no_exception, + R"({ + "method" : "submit", + "params" : [ + { + "api_version" : %API_VER%, + "offline" : true, + "secret" : "my_secret", + "signature_target" : "extra", + "tx_json" : + { + "json_argument" : true + } + } + ] + })"}, + {"submit: too many arguments.", + __LINE__, + {"submit", + "my_secret", + R"({"json_argument":true})", + "offline", + "CounterpartySignature", + "extra"}, + RPCCallTestData::no_exception, R"({ "method" : "submit", "params" : [ @@ -4912,19 +4964,23 @@ static RPCCallTestData const rpcCallTestArray[] = { } ] })"}, - {"submit: last argument not \"offline\".", + {"submit: misspelled offline flag interpreted as signature_target.", __LINE__, {"submit", "my_secret", R"({"json_argument":true})", "offlne"}, RPCCallTestData::no_exception, R"({ - "method" : "submit", - "params" : [ - { - "error" : "invalidParams", - "error_code" : 31, - "error_message" : "Invalid parameters." - } - ] + "method" : "submit", + "params" : [ + { + "api_version" : %API_VER%, + "secret" : "my_secret", + "signature_target" : "offlne", + "tx_json" : + { + "json_argument" : true + } + } + ] })"}, // submit_multisigned diff --git a/src/xrpld/app/main/Main.cpp b/src/xrpld/app/main/Main.cpp index 2353d7acd1..3dff9d4d5f 100644 --- a/src/xrpld/app/main/Main.cpp +++ b/src/xrpld/app/main/Main.cpp @@ -173,7 +173,7 @@ printHelp(po::options_description const& desc) " server_state [counters]\n" " sign [offline]\n" " sign_for " - "[offline]\n" + "[offline] []\n" " stop\n" " simulate [|] []\n" " submit |[ ]\n" diff --git a/src/xrpld/app/tx/detail/Batch.cpp b/src/xrpld/app/tx/detail/Batch.cpp index cba89348d0..4ffdd2d57c 100644 --- a/src/xrpld/app/tx/detail/Batch.cpp +++ b/src/xrpld/app/tx/detail/Batch.cpp @@ -237,6 +237,39 @@ Batch::preflight(PreflightContext const& ctx) std::unordered_set uniqueHashes; std::unordered_map> accountSeqTicket; + auto checkSignatureFields = [&parentBatchId, &j = ctx.j]( + STObject const& sig, + uint256 const& hash, + char const* label = "") -> NotTEC { + if (sig.isFieldPresent(sfTxnSignature)) + { + JLOG(j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "inner txn " << label << "cannot include TxnSignature. " + << "txID: " << hash; + return temBAD_SIGNATURE; + } + + if (sig.isFieldPresent(sfSigners)) + { + JLOG(j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "inner txn " << label << " cannot include Signers. " + << "txID: " << hash; + return temBAD_SIGNER; + } + + if (!sig.getFieldVL(sfSigningPubKey).empty()) + { + JLOG(j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "inner txn " << label << " SigningPubKey must be empty. " + << "txID: " << hash; + return temBAD_REGKEY; + } + + return tesSUCCESS; + }; for (STObject rb : rawTxns) { STTx const stx = STTx{std::move(rb)}; @@ -266,29 +299,23 @@ Batch::preflight(PreflightContext const& ctx) return temINVALID_FLAG; } - if (stx.isFieldPresent(sfTxnSignature)) - { - JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " - << "inner txn cannot include TxnSignature. " - << "txID: " << hash; - return temBAD_SIGNATURE; - } + if (auto const ret = checkSignatureFields(stx, hash)) + return ret; - if (stx.isFieldPresent(sfSigners)) + /* Placeholder for field that will be added by Lending Protocol + // Note that the CounterpartySignature is optional, and should not be + // included, but if it is, ensure it doesn't contain a signature. + if (stx.isFieldPresent(sfCounterpartySignature)) { - JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " - << "inner txn cannot include Signers. " - << "txID: " << hash; - return temBAD_SIGNER; - } - - if (!stx.getSigningPubKey().empty()) - { - JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " - << "inner txn SigningPubKey must be empty. " - << "txID: " << hash; - return temBAD_REGKEY; + auto const counterpartySignature = + stx.getFieldObject(sfCounterpartySignature); + if (auto const ret = checkSignatureFields( + counterpartySignature, hash, "counterparty signature ")) + { + return ret; + } } + */ auto const innerAccount = stx.getAccountID(sfAccount); if (auto const preflightResult = ripple::preflight( @@ -385,6 +412,13 @@ Batch::preflightSigValidated(PreflightContext const& ctx) // inner account to the required signers set. if (innerAccount != outerAccount) requiredSigners.insert(innerAccount); + /* Placeholder for field that will be added by Lending Protocol + // Some transactions have a Counterparty, who must also sign the + // transaction if they are not the outer account + if (auto const counterparty = rb.at(~sfCounterparty); + counterparty && counterparty != outerAccount) + requiredSigners.insert(*counterparty); + */ } // Validation Batch Signers diff --git a/src/xrpld/rpc/detail/RPCCall.cpp b/src/xrpld/rpc/detail/RPCCall.cpp index 57432d920f..822eb440b3 100644 --- a/src/xrpld/rpc/detail/RPCCall.cpp +++ b/src/xrpld/rpc/detail/RPCCall.cpp @@ -965,7 +965,16 @@ private: Json::Value txJSON; Json::Reader reader; bool const bOffline = - 3 == jvParams.size() && jvParams[2u].asString() == "offline"; + jvParams.size() >= 3 && jvParams[2u].asString() == "offline"; + std::optional const field = + [&jvParams, bOffline]() -> std::optional { + if (jvParams.size() < 3) + return std::nullopt; + if (jvParams.size() < 4 && bOffline) + return std::nullopt; + Json::UInt index = bOffline ? 3u : 2u; + return jvParams[index].asString(); + }(); if (1 == jvParams.size()) { @@ -978,7 +987,7 @@ private: return jvRequest; } else if ( - (2 == jvParams.size() || bOffline) && + (jvParams.size() >= 2 || bOffline) && reader.parse(jvParams[1u].asString(), txJSON)) { // Signing or submitting tx_json. @@ -990,6 +999,9 @@ private: if (bOffline) jvRequest[jss::offline] = true; + if (field) + jvRequest[jss::signature_target] = *field; + return jvRequest; } @@ -1270,11 +1282,11 @@ public: {"server_definitions", &RPCParser::parseServerDefinitions, 0, 1}, {"server_info", &RPCParser::parseServerInfo, 0, 1}, {"server_state", &RPCParser::parseServerInfo, 0, 1}, - {"sign", &RPCParser::parseSignSubmit, 2, 3}, + {"sign", &RPCParser::parseSignSubmit, 2, 4}, {"sign_for", &RPCParser::parseSignFor, 3, 4}, {"stop", &RPCParser::parseAsIs, 0, 0}, {"simulate", &RPCParser::parseSimulate, 1, 2}, - {"submit", &RPCParser::parseSignSubmit, 1, 3}, + {"submit", &RPCParser::parseSignSubmit, 1, 4}, {"submit_multisigned", &RPCParser::parseSubmitMultiSigned, 1, 1}, {"transaction_entry", &RPCParser::parseTransactionEntry, 2, 2}, {"tx", &RPCParser::parseTx, 1, 4}, diff --git a/src/xrpld/rpc/detail/TransactionSign.cpp b/src/xrpld/rpc/detail/TransactionSign.cpp index 175fd84c9b..aa7c706a19 100644 --- a/src/xrpld/rpc/detail/TransactionSign.cpp +++ b/src/xrpld/rpc/detail/TransactionSign.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -54,6 +55,7 @@ private: AccountID const* const multiSigningAcctID_; std::optional multiSignPublicKey_; Buffer multiSignature_; + std::optional> signatureTarget_; public: explicit SigningForParams() : multiSigningAcctID_(nullptr) @@ -116,12 +118,25 @@ public: return multiSignature_; } + std::optional> const& + getSignatureTarget() const + { + return signatureTarget_; + } + void setPublicKey(PublicKey const& multiSignPublicKey) { multiSignPublicKey_ = multiSignPublicKey; } + void + setSignatureTarget( + std::optional> const& field) + { + signatureTarget_ = field; + } + void moveMultiSignature(Buffer&& multiSignature) { @@ -427,6 +442,29 @@ transactionPreProcessImpl( bool const verify = !(params.isMember(jss::offline) && params[jss::offline].asBool()); + auto const signatureTarget = + [¶ms]() -> std::optional> { + if (params.isMember(jss::signature_target)) + return SField::getField(params[jss::signature_target].asString()); + return std::nullopt; + }(); + + // Make sure the signature target field is valid, if specified, and save the + // template for use later + auto const signatureTemplate = signatureTarget + ? InnerObjectFormats::getInstance().findSOTemplateBySField( + *signatureTarget) + : nullptr; + if (signatureTarget) + { + if (!signatureTemplate) + { // Invalid target field + return RPC::make_error( + rpcINVALID_PARAMS, signatureTarget->get().getName()); + } + signingArgs.setSignatureTarget(signatureTarget); + } + if (!params.isMember(jss::tx_json)) return RPC::missing_field_error(jss::tx_json); @@ -541,9 +579,10 @@ transactionPreProcessImpl( JLOG(j.trace()) << "verify: " << toBase58(calcAccountID(pk)) << " : " << toBase58(srcAddressID); - // Don't do this test if multisigning since the account and secret - // probably don't belong together in that case. - if (!signingArgs.isMultiSigning()) + // Don't do this test if multisigning or if the signature is going into + // an alternate field since the account and secret probably don't belong + // together in that case. + if (!signingArgs.isMultiSigning() && !signatureTarget) { // Make sure the account and secret belong together. if (tx_json.isMember(sfDelegate.jsonName)) @@ -598,7 +637,17 @@ transactionPreProcessImpl( { // If we're generating a multi-signature the SigningPubKey must be // empty, otherwise it must be the master account's public key. - parsed.object->setFieldVL( + STObject* sigObject = &*parsed.object; + if (signatureTarget) + { + // If the target object doesn't exist, make one. + if (!parsed.object->isFieldPresent(*signatureTarget)) + parsed.object->setFieldObject( + *signatureTarget, + STObject{*signatureTemplate, *signatureTarget}); + sigObject = &parsed.object->peekFieldObject(*signatureTarget); + } + sigObject->setFieldVL( sfSigningPubKey, signingArgs.isMultiSigning() ? Slice(nullptr, 0) : pk.slice()); @@ -630,7 +679,7 @@ transactionPreProcessImpl( } else if (signingArgs.isSingleSigning()) { - stTx->sign(pk, sk); + stTx->sign(pk, sk, signatureTarget); } return transactionPreProcessResult{std::move(stTx)}; @@ -1195,11 +1244,17 @@ transactionSignFor( signer.setFieldVL( sfSigningPubKey, signForParams.getPublicKey().slice()); + STObject& sigTarget = [&]() -> STObject& { + auto const target = signForParams.getSignatureTarget(); + if (target) + return sttx->peekFieldObject(*target); + return *sttx; + }(); // If there is not yet a Signers array, make one. - if (!sttx->isFieldPresent(sfSigners)) - sttx->setFieldArray(sfSigners, {}); + if (!sigTarget.isFieldPresent(sfSigners)) + sigTarget.setFieldArray(sfSigners, {}); - auto& signers = sttx->peekFieldArray(sfSigners); + auto& signers = sigTarget.peekFieldArray(sfSigners); signers.emplace_back(std::move(signer)); // The array must be sorted and validated. From f35d0dc8290c7112686ad96579f622254c5059cc Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Thu, 9 Oct 2025 00:22:38 -0400 Subject: [PATCH 107/291] Add convenience functions to MPT test-framework. (#5870) --- src/test/app/MPToken_test.cpp | 6 +- src/test/jtx/amount.h | 7 + src/test/jtx/impl/mpt.cpp | 268 +++++++++++++++++++++++++++------- src/test/jtx/mpt.h | 84 +++++++++-- 4 files changed, 293 insertions(+), 72 deletions(-) diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index e9740e67de..4b8eb871a2 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -1171,7 +1171,7 @@ class MPToken_test : public beast::unit_test::suite env(credentials::accept(bob, credIssuer1, credType)); env.close(); - MPTTester mptAlice(env, alice, {}); + MPTTester mptAlice(env, alice); env.close(); mptAlice.create({ @@ -1213,7 +1213,7 @@ class MPToken_test : public beast::unit_test::suite env(credentials::accept(bob, credIssuer1, credType)); env.close(); - MPTTester mptAlice(env, alice, {}); + MPTTester mptAlice(env, alice); env.close(); mptAlice.create({ @@ -1293,7 +1293,7 @@ class MPToken_test : public beast::unit_test::suite env(credentials::accept(carol, credIssuer2, credType)); env.close(); - MPTTester mptAlice(env, alice, {}); + MPTTester mptAlice(env, alice); env.close(); mptAlice.create({ diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h index f14a65f6b8..f413d87c5a 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -232,6 +232,13 @@ public: { return {asset_}; } + + template + bool + holds() const + { + return asset_.holds(); + } }; //------------------------------------------------------------------------------ diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index aaa5e433f2..5d77865226 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -81,17 +81,67 @@ MPTTester::MPTTester(Env& env, Account const& issuer, MPTInit const& arg) env_.require(owners(it.second, 0)); } } + if (arg.create) + create(*arg.create); } -void -MPTTester::create(MPTCreate const& arg) +MPTTester::MPTTester( + Env& env, + Account const& issuer, + MPTID const& id, + std::vector const& holders, + bool close) + : env_(env) + , issuer_(issuer) + , holders_(makeHolders(holders)) + , id_(id) + , close_(close) { - if (id_) - Throw("MPT can't be reused"); - id_ = makeMptID(env_.seq(issuer_), issuer_); +} + +static MPTCreate +makeMPTCreate(MPTInitDef const& arg) +{ + if (arg.pay) + return { + .maxAmt = arg.maxAmt, + .transferFee = arg.transferFee, + .pay = {{arg.holders, *arg.pay}}, + .flags = arg.flags, + .authHolder = arg.authHolder}; + return { + .maxAmt = arg.maxAmt, + .transferFee = arg.transferFee, + .authorize = arg.holders, + .flags = arg.flags, + .authHolder = arg.authHolder}; +} + +MPTTester::MPTTester(MPTInitDef const& arg) + : MPTTester{ + arg.env, + arg.issuer, + MPTInit{ + .fund = arg.fund, + .close = arg.close, + .create = makeMPTCreate(arg)}} +{ +} + +MPTTester::operator MPT() const +{ + if (!id_) + Throw("MPT has not been created"); + return MPT("", *id_); +} + +Json::Value +MPTTester::createjv(MPTCreate const& arg) +{ + if (!arg.issuer) + Throw("MPTTester::createjv: issuer is not set"); Json::Value jv; - jv[sfAccount] = issuer_.human(); - jv[sfTransactionType] = jss::MPTokenIssuanceCreate; + jv[sfAccount] = arg.issuer->human(); if (arg.assetScale) jv[sfAssetScale] = *arg.assetScale; if (arg.transferFee) @@ -104,6 +154,25 @@ MPTTester::create(MPTCreate const& arg) jv[sfDomainID] = to_string(*arg.domainID); if (arg.mutableFlags) jv[sfMutableFlags] = *arg.mutableFlags; + jv[sfTransactionType] = jss::MPTokenIssuanceCreate; + + return jv; +} + +void +MPTTester::create(MPTCreate const& arg) +{ + if (id_) + Throw("MPT can't be reused"); + id_ = makeMptID(env_.seq(issuer_), issuer_); + Json::Value jv = createjv( + {.issuer = issuer_, + .maxAmt = arg.maxAmt, + .assetScale = arg.assetScale, + .transferFee = arg.transferFee, + .metadata = arg.metadata, + .mutableFlags = arg.mutableFlags, + .domainID = arg.domainID}); if (submit(arg, jv) != tesSUCCESS) { // Verify issuance doesn't exist @@ -114,26 +183,62 @@ MPTTester::create(MPTCreate const& arg) id_.reset(); } else + { env_.require(mptflags(*this, arg.flags.value_or(0))); + auto authAndPay = [&](auto const& accts, auto const&& getAcct) { + for (auto const& it : accts) + { + authorize({.account = getAcct(it)}); + if ((arg.flags.value_or(0) & tfMPTRequireAuth) && + arg.authHolder) + authorize({.account = issuer_, .holder = getAcct(it)}); + if (arg.pay && arg.pay->first.empty()) + pay(issuer_, getAcct(it), arg.pay->second); + } + if (arg.pay) + { + for (auto const& p : arg.pay->first) + pay(issuer_, p, arg.pay->second); + } + }; + if (arg.authorize) + { + if (arg.authorize->empty()) + authAndPay(holders_, [](auto const& it) { return it.second; }); + else + authAndPay(*arg.authorize, [](auto const& it) { return it; }); + } + else if (arg.pay) + { + if (arg.pay->first.empty()) + authAndPay(holders_, [](auto const& it) { return it.second; }); + else + authAndPay(arg.pay->first, [](auto const& it) { return it; }); + } + } +} + +Json::Value +MPTTester::destroyjv(MPTDestroy const& arg) +{ + Json::Value jv; + if (!arg.issuer || !arg.id) + Throw("MPTTester::destroyjv: issuer/id is not set"); + jv[sfAccount] = arg.issuer->human(); + jv[sfMPTokenIssuanceID] = to_string(*arg.id); + jv[sfTransactionType] = jss::MPTokenIssuanceDestroy; + + return jv; } void MPTTester::destroy(MPTDestroy const& arg) { - Json::Value jv; - if (arg.issuer) - jv[sfAccount] = arg.issuer->human(); - else - jv[sfAccount] = issuer_.human(); - if (arg.id) - jv[sfMPTokenIssuanceID] = to_string(*arg.id); - else - { - if (!id_) - Throw("MPT has not been created"); - jv[sfMPTokenIssuanceID] = to_string(*id_); - } - jv[sfTransactionType] = jss::MPTokenIssuanceDestroy; + if (!arg.id && !id_) + Throw("MPT has not been created"); + Json::Value jv = destroyjv( + {.issuer = arg.issuer ? arg.issuer : issuer_, + .id = arg.id ? arg.id : id_}); submit(arg, jv); } @@ -146,25 +251,32 @@ MPTTester::holder(std::string const& holder_) const return it->second; } +Json::Value +MPTTester::authorizejv(MPTAuthorize const& arg) +{ + Json::Value jv; + if (!arg.account || !arg.id) + Throw( + "MPTTester::authorizejv: issuer/id is not set"); + jv[sfAccount] = arg.account->human(); + jv[sfMPTokenIssuanceID] = to_string(*arg.id); + if (arg.holder) + jv[sfHolder] = arg.holder->human(); + jv[sfTransactionType] = jss::MPTokenAuthorize; + + return jv; +} + void MPTTester::authorize(MPTAuthorize const& arg) { - Json::Value jv; - if (arg.account) - jv[sfAccount] = arg.account->human(); - else - jv[sfAccount] = issuer_.human(); - jv[sfTransactionType] = jss::MPTokenAuthorize; - if (arg.id) - jv[sfMPTokenIssuanceID] = to_string(*arg.id); - else - { - if (!id_) - Throw("MPT has not been created"); - jv[sfMPTokenIssuanceID] = to_string(*id_); - } - if (arg.holder) - jv[sfHolder] = arg.holder->human(); + if (!arg.id && !id_) + Throw("MPT has not been created"); + Json::Value jv = authorizejv({ + .account = arg.account ? arg.account : issuer_, + .holder = arg.holder, + .id = arg.id ? arg.id : id_, + }); if (auto const result = submit(arg, jv); result == tesSUCCESS) { // Issuer authorizes @@ -220,24 +332,34 @@ MPTTester::authorize(MPTAuthorize const& arg) } void -MPTTester::set(MPTSet const& arg) +MPTTester::authorizeHolders(Holders const& holders) +{ + for (auto const& holder : holders) + { + authorize({.account = holder}); + } +} + +Json::Value +MPTTester::setjv(MPTSet const& arg) { Json::Value jv; - if (arg.account) - jv[sfAccount] = arg.account->human(); - else - jv[sfAccount] = issuer_.human(); - jv[sfTransactionType] = jss::MPTokenIssuanceSet; - if (arg.id) - jv[sfMPTokenIssuanceID] = to_string(*arg.id); - else - { - if (!id_) - Throw("MPT has not been created"); - jv[sfMPTokenIssuanceID] = to_string(*id_); - } + if (!arg.account || !arg.id) + Throw("MPTTester::setjv: issuer/id is not set"); + jv[sfAccount] = arg.account->human(); + jv[sfMPTokenIssuanceID] = to_string(*arg.id); if (arg.holder) - jv[sfHolder] = arg.holder->human(); + { + std::visit( + [&jv](T const& holder) { + if constexpr (std::is_same_v) + jv[sfHolder] = holder.human(); + else if constexpr (std::is_same_v) + jv[sfHolder] = toBase58(holder); + }, + *arg.holder); + } + if (arg.delegate) jv[sfDelegate] = arg.delegate->human(); if (arg.domainID) @@ -248,7 +370,27 @@ MPTTester::set(MPTSet const& arg) jv[sfTransferFee] = *arg.transferFee; if (arg.metadata) jv[sfMPTokenMetadata] = strHex(*arg.metadata); - if (submit(arg, jv) == tesSUCCESS && (arg.flags || arg.mutableFlags)) + jv[sfTransactionType] = jss::MPTokenIssuanceSet; + + return jv; +} + +void +MPTTester::set(MPTSet const& arg) +{ + if (!arg.id && !id_) + Throw("MPT has not been created"); + Json::Value jv = setjv( + {.account = arg.account ? arg.account : issuer_, + .holder = arg.holder, + .id = arg.id ? arg.id : id_, + .mutableFlags = arg.mutableFlags, + .transferFee = arg.transferFee, + .metadata = arg.metadata, + .delegate = arg.delegate, + .domainID = arg.domainID}); + if (submit(arg, jv) == tesSUCCESS && + (arg.flags.value_or(0) || arg.mutableFlags)) { auto require = [&](std::optional const& holder, bool unchanged) { @@ -300,8 +442,9 @@ MPTTester::set(MPTSet const& arg) }; if (arg.account) require(std::nullopt, arg.holder.has_value()); - if (arg.holder) - require(*arg.holder, false); + if (auto const account = + (arg.holder ? std::get_if(&(*arg.holder)) : nullptr)) + require(*account, false); } } @@ -474,6 +617,13 @@ MPTTester::mpt(std::int64_t amount) const return ripple::test::jtx::MPT(issuer_.name(), *id_)(amount); } +MPTTester::operator Asset() const +{ + if (!id_) + Throw("MPT has not been created"); + return Asset(*id_); +} + std::int64_t MPTTester::getBalance(Account const& account) const { @@ -507,11 +657,17 @@ MPTTester::getFlags(std::optional const& holder) const } MPT -MPTTester::operator[](std::string const& name) const +MPTTester::operator[](std::string const& name) { return MPT(name, issuanceID()); } +PrettyAmount +MPTTester::operator()(std::uint64_t amount) const +{ + return MPT("", issuanceID())(amount); +} + } // namespace jtx } // namespace test } // namespace ripple diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index 422afa8fab..652887f3ce 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -33,6 +33,8 @@ namespace jtx { class MPTTester; +auto const MPTDEXFlags = tfMPTCanTrade | tfMPTCanTransfer; + // Check flags settings on MPT create class mptflags { @@ -86,31 +88,59 @@ public: operator()(Env& env) const; }; -struct MPTInit -{ - std::vector holders = {}; - PrettyAmount const xrp = XRP(10'000); - PrettyAmount const xrpHolders = XRP(10'000); - bool fund = true; - bool close = true; -}; -static MPTInit const mptInitNoFund{.fund = false}; +using Holders = std::vector; struct MPTCreate { + static inline std::vector AllHolders = {}; + std::optional issuer = std::nullopt; std::optional maxAmt = std::nullopt; std::optional assetScale = std::nullopt; std::optional transferFee = std::nullopt; std::optional metadata = std::nullopt; std::optional ownerCount = std::nullopt; std::optional holderCount = std::nullopt; - bool fund = true; + // authorize if seated. + // if empty vector then authorize all holders + std::optional> authorize = std::nullopt; + // pay if seated. if authorize is not seated then authorize. + // if empty vector then pay to either authorize or all holders. + std::optional, std::uint64_t>> pay = + std::nullopt; std::optional flags = {0}; std::optional mutableFlags = std::nullopt; + bool authHolder = false; std::optional domainID = std::nullopt; std::optional err = std::nullopt; }; +struct MPTInit +{ + Holders holders = {}; + PrettyAmount const xrp = XRP(10'000); + PrettyAmount const xrpHolders = XRP(10'000); + bool fund = true; + bool close = true; + // create MPTIssuanceID if seated and follow rules for MPTCreate args + std::optional create = std::nullopt; +}; +static MPTInit const mptInitNoFund{.fund = false}; + +struct MPTInitDef +{ + Env& env; + Account issuer; + Holders holders = {}; + std::uint16_t transferFee = 0; + std::optional pay = std::nullopt; + std::uint32_t flags = MPTDEXFlags; + bool authHolder = false; + bool fund = false; + bool close = true; + std::optional maxAmt = std::nullopt; + std::optional err = std::nullopt; +}; + struct MPTDestroy { std::optional issuer = std::nullopt; @@ -135,7 +165,7 @@ struct MPTAuthorize struct MPTSet { std::optional account = std::nullopt; - std::optional holder = std::nullopt; + std::optional> holder = std::nullopt; std::optional id = std::nullopt; std::optional ownerCount = std::nullopt; std::optional holderCount = std::nullopt; @@ -151,26 +181,49 @@ struct MPTSet class MPTTester { Env& env_; - Account const& issuer_; + Account const issuer_; std::unordered_map const holders_; std::optional id_; bool close_; public: MPTTester(Env& env, Account const& issuer, MPTInit const& constr = {}); + MPTTester(MPTInitDef const& constr); + MPTTester( + Env& env, + Account const& issuer, + MPTID const& id, + std::vector const& holders = {}, + bool close = true); + operator MPT() const; void create(MPTCreate const& arg = MPTCreate{}); + static Json::Value + createjv(MPTCreate const& arg = MPTCreate{}); + void destroy(MPTDestroy const& arg = MPTDestroy{}); + static Json::Value + destroyjv(MPTDestroy const& arg = MPTDestroy{}); + void authorize(MPTAuthorize const& arg = MPTAuthorize{}); + static Json::Value + authorizejv(MPTAuthorize const& arg = MPTAuthorize{}); + + void + authorizeHolders(Holders const& holders); + void set(MPTSet const& set = {}); + static Json::Value + setjv(MPTSet const& set = {}); + [[nodiscard]] bool checkDomainID(std::optional expected) const; @@ -235,7 +288,12 @@ public: getBalance(Account const& account) const; MPT - operator[](std::string const& name) const; + operator[](std::string const& name); + + PrettyAmount + operator()(std::uint64_t amount) const; + + operator Asset() const; private: using SLEP = std::shared_ptr; From 5117ef831eb93957888b37d5fb93411bbe07ff1b Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 9 Oct 2025 14:14:49 -0400 Subject: [PATCH 108/291] Merge branch 'develop' into ximinez/lending-XLS-66 --- .github/scripts/strategy-matrix/generate.py | 8 +- .github/scripts/strategy-matrix/linux.json | 92 +- .github/scripts/strategy-matrix/macos.json | 3 +- .github/scripts/strategy-matrix/windows.json | 3 +- .github/workflows/reusable-build-test.yml | 2 +- .github/workflows/upload-conan-deps.yml | 5 +- include/xrpl/basics/IntrusivePointer.ipp | 2 + include/xrpl/beast/net/IPAddress.h | 4 + include/xrpl/ledger/ApplyView.h | 2 + include/xrpl/protocol/detail/b58_utils.h | 2 + .../xrpl/protocol/detail/transactions.macro | 12 +- include/xrpl/resource/detail/Logic.h | 2 + src/libxrpl/basics/Log.cpp | 8 + src/libxrpl/basics/contract.cpp | 2 + src/libxrpl/json/Object.cpp | 4 +- src/libxrpl/json/json_value.cpp | 24 + src/libxrpl/ledger/ApplyStateTable.cpp | 2 + src/libxrpl/ledger/ApplyView.cpp | 2 + src/libxrpl/ledger/BookDirs.cpp | 4 + src/libxrpl/ledger/View.cpp | 12 +- src/libxrpl/protocol/BuildInfo.cpp | 2 +- src/libxrpl/protocol/STBase.cpp | 2 + src/libxrpl/protocol/TxMeta.cpp | 2 + src/test/app/Invariants_test.cpp | 1706 ++++++++++++++++- src/test/app/Vault_test.cpp | 63 + src/test/rpc/Simulate_test.cpp | 92 +- src/xrpld/app/ledger/Ledger.cpp | 4 + src/xrpld/app/ledger/detail/InboundLedger.cpp | 4 + src/xrpld/app/ledger/detail/LedgerMaster.cpp | 2 + src/xrpld/app/main/Application.cpp | 8 + src/xrpld/app/misc/NetworkOPs.cpp | 10 + src/xrpld/app/misc/detail/ValidatorList.cpp | 14 +- src/xrpld/app/paths/Pathfinder.cpp | 2 + src/xrpld/app/paths/detail/BookStep.cpp | 6 + src/xrpld/app/paths/detail/DirectStep.cpp | 2 + src/xrpld/app/paths/detail/FlowDebugInfo.h | 2 + src/xrpld/app/paths/detail/PaySteps.cpp | 6 + src/xrpld/app/paths/detail/StrandFlow.h | 8 + src/xrpld/app/rdb/RelationalDatabase.h | 4 +- src/xrpld/app/rdb/backend/detail/Node.cpp | 6 + src/xrpld/app/tx/detail/Change.cpp | 2 + src/xrpld/app/tx/detail/DeleteAccount.cpp | 2 + src/xrpld/app/tx/detail/InvariantCheck.cpp | 948 +++++++++ src/xrpld/app/tx/detail/InvariantCheck.h | 73 +- src/xrpld/app/tx/detail/Offer.h | 2 + src/xrpld/app/tx/detail/OfferStream.cpp | 2 + src/xrpld/app/tx/detail/SetSignerList.cpp | 2 + src/xrpld/app/tx/detail/Transactor.cpp | 2 + src/xrpld/app/tx/detail/VaultDeposit.cpp | 5 +- src/xrpld/app/tx/detail/VaultSet.cpp | 3 + src/xrpld/app/tx/detail/XChainBridge.cpp | 2 + src/xrpld/app/tx/detail/applySteps.cpp | 8 + src/xrpld/nodestore/backend/NuDBFactory.cpp | 2 + .../nodestore/backend/RocksDBFactory.cpp | 2 + src/xrpld/overlay/Compression.h | 4 + src/xrpld/overlay/detail/PeerImp.cpp | 6 + src/xrpld/peerfinder/detail/Counts.h | 2 + src/xrpld/peerfinder/detail/Logic.h | 2 + src/xrpld/perflog/detail/PerfLogImp.cpp | 18 + src/xrpld/rpc/detail/Handler.cpp | 2 + src/xrpld/rpc/detail/Status.cpp | 2 + src/xrpld/rpc/handlers/AccountChannels.cpp | 2 + src/xrpld/rpc/handlers/AccountLines.cpp | 2 + src/xrpld/rpc/handlers/AccountOffers.cpp | 2 + src/xrpld/rpc/handlers/AccountTx.cpp | 4 + src/xrpld/rpc/handlers/Fee1.cpp | 3 + src/xrpld/shamap/detail/SHAMap.cpp | 4 + src/xrpld/shamap/detail/SHAMapDelta.cpp | 6 + 68 files changed, 3149 insertions(+), 105 deletions(-) diff --git a/.github/scripts/strategy-matrix/generate.py b/.github/scripts/strategy-matrix/generate.py index fd05895b0e..025d553b5e 100755 --- a/.github/scripts/strategy-matrix/generate.py +++ b/.github/scripts/strategy-matrix/generate.py @@ -74,14 +74,14 @@ def generate_strategy_matrix(all: bool, config: Config) -> list: continue # RHEL: - # - 9.4 using GCC 12: Debug and Unity on linux/amd64. - # - 9.6 using Clang: Release and no Unity on linux/amd64. + # - 9 using GCC 12: Debug and Unity on linux/amd64. + # - 10 using Clang: Release and no Unity on linux/amd64. if os['distro_name'] == 'rhel': skip = True - if os['distro_version'] == '9.4': + if os['distro_version'] == '9': if f'{os['compiler_name']}-{os['compiler_version']}' == 'gcc-12' and build_type == 'Debug' and '-Dunity=ON' in cmake_args and architecture['platform'] == 'linux/amd64': skip = False - elif os['distro_version'] == '9.6': + elif os['distro_version'] == '10': if f'{os['compiler_name']}-{os['compiler_version']}' == 'clang-any' and build_type == 'Release' and '-Dunity=OFF' in cmake_args and architecture['platform'] == 'linux/amd64': skip = False if skip: diff --git a/.github/scripts/strategy-matrix/linux.json b/.github/scripts/strategy-matrix/linux.json index 44eaebd074..08313daf0a 100644 --- a/.github/scripts/strategy-matrix/linux.json +++ b/.github/scripts/strategy-matrix/linux.json @@ -14,139 +14,155 @@ "distro_name": "debian", "distro_version": "bookworm", "compiler_name": "gcc", - "compiler_version": "12" + "compiler_version": "12", + "image_sha": "6f723eb" }, { "distro_name": "debian", "distro_version": "bookworm", "compiler_name": "gcc", - "compiler_version": "13" + "compiler_version": "13", + "image_sha": "6f723eb" }, { "distro_name": "debian", "distro_version": "bookworm", "compiler_name": "gcc", - "compiler_version": "14" + "compiler_version": "14", + "image_sha": "6f723eb" }, { "distro_name": "debian", "distro_version": "bookworm", "compiler_name": "gcc", - "compiler_version": "15" + "compiler_version": "15", + "image_sha": "6f723eb" }, { "distro_name": "debian", "distro_version": "bookworm", "compiler_name": "clang", - "compiler_version": "16" + "compiler_version": "16", + "image_sha": "6f723eb" }, { "distro_name": "debian", "distro_version": "bookworm", "compiler_name": "clang", - "compiler_version": "17" + "compiler_version": "17", + "image_sha": "6f723eb" }, { "distro_name": "debian", "distro_version": "bookworm", "compiler_name": "clang", - "compiler_version": "18" + "compiler_version": "18", + "image_sha": "6f723eb" }, { "distro_name": "debian", "distro_version": "bookworm", "compiler_name": "clang", - "compiler_version": "19" + "compiler_version": "19", + "image_sha": "6f723eb" }, { "distro_name": "debian", "distro_version": "bookworm", "compiler_name": "clang", - "compiler_version": "20" + "compiler_version": "20", + "image_sha": "6f723eb" }, { "distro_name": "rhel", - "distro_version": "9.4", + "distro_version": "9", "compiler_name": "gcc", - "compiler_version": "12" + "compiler_version": "12", + "image_sha": "0ab1e4c" }, { "distro_name": "rhel", - "distro_version": "9.4", + "distro_version": "9", "compiler_name": "gcc", - "compiler_version": "13" + "compiler_version": "13", + "image_sha": "0ab1e4c" }, { "distro_name": "rhel", - "distro_version": "9.4", + "distro_version": "9", "compiler_name": "gcc", - "compiler_version": "14" + "compiler_version": "14", + "image_sha": "0ab1e4c" }, { "distro_name": "rhel", - "distro_version": "9.6", - "compiler_name": "gcc", - "compiler_version": "13" - }, - { - "distro_name": "rhel", - "distro_version": "9.6", - "compiler_name": "gcc", - "compiler_version": "14" - }, - { - "distro_name": "rhel", - "distro_version": "9.4", + "distro_version": "9", "compiler_name": "clang", - "compiler_version": "any" + "compiler_version": "any", + "image_sha": "0ab1e4c" }, { "distro_name": "rhel", - "distro_version": "9.6", + "distro_version": "10", + "compiler_name": "gcc", + "compiler_version": "14", + "image_sha": "0ab1e4c" + }, + { + "distro_name": "rhel", + "distro_version": "10", "compiler_name": "clang", - "compiler_version": "any" + "compiler_version": "any", + "image_sha": "0ab1e4c" }, { "distro_name": "ubuntu", "distro_version": "jammy", "compiler_name": "gcc", - "compiler_version": "12" + "compiler_version": "12", + "image_sha": "6f723eb" }, { "distro_name": "ubuntu", "distro_version": "noble", "compiler_name": "gcc", - "compiler_version": "13" + "compiler_version": "13", + "image_sha": "6f723eb" }, { "distro_name": "ubuntu", "distro_version": "noble", "compiler_name": "gcc", - "compiler_version": "14" + "compiler_version": "14", + "image_sha": "6f723eb" }, { "distro_name": "ubuntu", "distro_version": "noble", "compiler_name": "clang", - "compiler_version": "16" + "compiler_version": "16", + "image_sha": "6f723eb" }, { "distro_name": "ubuntu", "distro_version": "noble", "compiler_name": "clang", - "compiler_version": "17" + "compiler_version": "17", + "image_sha": "6f723eb" }, { "distro_name": "ubuntu", "distro_version": "noble", "compiler_name": "clang", - "compiler_version": "18" + "compiler_version": "18", + "image_sha": "6f723eb" }, { "distro_name": "ubuntu", "distro_version": "noble", "compiler_name": "clang", - "compiler_version": "19" + "compiler_version": "19", + "image_sha": "6f723eb" } ], "build_type": ["Debug", "Release"], diff --git a/.github/scripts/strategy-matrix/macos.json b/.github/scripts/strategy-matrix/macos.json index de37639ddd..14b6089620 100644 --- a/.github/scripts/strategy-matrix/macos.json +++ b/.github/scripts/strategy-matrix/macos.json @@ -10,7 +10,8 @@ "distro_name": "macos", "distro_version": "", "compiler_name": "", - "compiler_version": "" + "compiler_version": "", + "image_sha": "" } ], "build_type": ["Debug", "Release"], diff --git a/.github/scripts/strategy-matrix/windows.json b/.github/scripts/strategy-matrix/windows.json index 08b41e3f89..8637b31012 100644 --- a/.github/scripts/strategy-matrix/windows.json +++ b/.github/scripts/strategy-matrix/windows.json @@ -10,7 +10,8 @@ "distro_name": "windows", "distro_version": "", "compiler_name": "", - "compiler_version": "" + "compiler_version": "", + "image_sha": "" } ], "build_type": ["Debug", "Release"], diff --git a/.github/workflows/reusable-build-test.yml b/.github/workflows/reusable-build-test.yml index c274cf2b21..5bc9cf2557 100644 --- a/.github/workflows/reusable-build-test.yml +++ b/.github/workflows/reusable-build-test.yml @@ -52,7 +52,7 @@ jobs: cmake_args: ${{ matrix.cmake_args }} cmake_target: ${{ matrix.cmake_target }} runs_on: ${{ toJSON(matrix.architecture.runner) }} - image: ${{ contains(matrix.architecture.platform, 'linux') && format('ghcr.io/xrplf/ci/{0}-{1}:{2}-{3}-sha-5dd7158', matrix.os.distro_name, matrix.os.distro_version, matrix.os.compiler_name, matrix.os.compiler_version) || '' }} + image: ${{ contains(matrix.architecture.platform, 'linux') && format('ghcr.io/xrplf/ci/{0}-{1}:{2}-{3}-sha-{4}', matrix.os.distro_name, matrix.os.distro_version, matrix.os.compiler_name, matrix.os.compiler_version, matrix.os.image_sha) || '' }} config_name: ${{ matrix.config_name }} secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/upload-conan-deps.yml b/.github/workflows/upload-conan-deps.yml index cbae8a4c86..680602d978 100644 --- a/.github/workflows/upload-conan-deps.yml +++ b/.github/workflows/upload-conan-deps.yml @@ -40,11 +40,13 @@ concurrency: cancel-in-progress: true jobs: + # Generate the strategy matrix to be used by the following job. generate-matrix: uses: ./.github/workflows/reusable-strategy-matrix.yml with: strategy_matrix: ${{ github.event_name == 'pull_request' && 'minimal' || 'all' }} + # Build and upload the dependencies for each configuration. run-upload-conan-deps: needs: - generate-matrix @@ -53,8 +55,7 @@ jobs: matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }} max-parallel: 10 runs-on: ${{ matrix.architecture.runner }} - container: ${{ contains(matrix.architecture.platform, 'linux') && format('ghcr.io/xrplf/ci/{0}-{1}:{2}-{3}-sha-5dd7158', matrix.os.distro_name, matrix.os.distro_version, matrix.os.compiler_name, matrix.os.compiler_version) || null }} - + container: ${{ contains(matrix.architecture.platform, 'linux') && format('ghcr.io/xrplf/ci/{0}-{1}:{2}-{3}-sha-{4}', matrix.os.distro_name, matrix.os.distro_version, matrix.os.compiler_name, matrix.os.compiler_version, matrix.os.image_sha) || null }} steps: - name: Cleanup workspace if: ${{ runner.os == 'macOS' }} diff --git a/include/xrpl/basics/IntrusivePointer.ipp b/include/xrpl/basics/IntrusivePointer.ipp index 1ac3f2bab4..4d037bc329 100644 --- a/include/xrpl/basics/IntrusivePointer.ipp +++ b/include/xrpl/basics/IntrusivePointer.ipp @@ -654,12 +654,14 @@ SharedWeakUnion::convertToWeak() break; case destroy: // We just added a weak ref. How could we destroy? + // LCOV_EXCL_START UNREACHABLE( "ripple::SharedWeakUnion::convertToWeak : destroying freshly " "added ref"); delete p; unsafeSetRawPtr(nullptr); return true; // Should never happen + // LCOV_EXCL_STOP case partialDestroy: // This is a weird case. We just converted the last strong // pointer to a weak pointer. diff --git a/include/xrpl/beast/net/IPAddress.h b/include/xrpl/beast/net/IPAddress.h index fb5dac90ec..f3c7387bb8 100644 --- a/include/xrpl/beast/net/IPAddress.h +++ b/include/xrpl/beast/net/IPAddress.h @@ -94,7 +94,11 @@ hash_append(Hasher& h, beast::IP::Address const& addr) noexcept else if (addr.is_v6()) hash_append(h, addr.to_v6().to_bytes()); else + { + // LCOV_EXCL_START UNREACHABLE("beast::hash_append : invalid address type"); + // LCOV_EXCL_STOP + } } } // namespace beast diff --git a/include/xrpl/ledger/ApplyView.h b/include/xrpl/ledger/ApplyView.h index a1b2a2348a..c00d040e09 100644 --- a/include/xrpl/ledger/ApplyView.h +++ b/include/xrpl/ledger/ApplyView.h @@ -284,12 +284,14 @@ public: { if (key.type != ltOFFER) { + // LCOV_EXCL_START UNREACHABLE( "ripple::ApplyView::dirAppend : only Offers are appended to " "book directories"); // Only Offers are appended to book directories. Call dirInsert() // instead return std::nullopt; + // LCOV_EXCL_STOP } return dirAdd(true, directory, key.key, describe); } diff --git a/include/xrpl/protocol/detail/b58_utils.h b/include/xrpl/protocol/detail/b58_utils.h index ecd301524f..3908822661 100644 --- a/include/xrpl/protocol/detail/b58_utils.h +++ b/include/xrpl/protocol/detail/b58_utils.h @@ -129,10 +129,12 @@ inplace_bigint_div_rem(std::span numerator, std::uint64_t divisor) { // should never happen, but if it does then it seems natural to define // the a null set of numbers to be zero, so the remainder is also zero. + // LCOV_EXCL_START UNREACHABLE( "ripple::b58_fast::detail::inplace_bigint_div_rem : empty " "numerator"); return 0; + // LCOV_EXCL_STOP } auto to_u128 = [](std::uint64_t high, diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index ce194d0e2c..1d14ae0379 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -851,7 +851,7 @@ TRANSACTION(ttDELEGATE_SET, 64, DelegateSet, TRANSACTION(ttVAULT_CREATE, 65, VaultCreate, Delegation::delegatable, featureSingleAssetVault, - createPseudoAcct | createMPTIssuance, + createPseudoAcct | createMPTIssuance | mustModifyVault, ({ {sfAsset, soeREQUIRED, soeMPTSupported}, {sfAssetsMaximum, soeOPTIONAL}, @@ -869,7 +869,7 @@ TRANSACTION(ttVAULT_CREATE, 65, VaultCreate, TRANSACTION(ttVAULT_SET, 66, VaultSet, Delegation::delegatable, featureSingleAssetVault, - noPriv, + mustModifyVault, ({ {sfVaultID, soeREQUIRED}, {sfAssetsMaximum, soeOPTIONAL}, @@ -884,7 +884,7 @@ TRANSACTION(ttVAULT_SET, 66, VaultSet, TRANSACTION(ttVAULT_DELETE, 67, VaultDelete, Delegation::delegatable, featureSingleAssetVault, - mustDeleteAcct | destroyMPTIssuance, + mustDeleteAcct | destroyMPTIssuance | mustModifyVault, ({ {sfVaultID, soeREQUIRED}, })) @@ -896,7 +896,7 @@ TRANSACTION(ttVAULT_DELETE, 67, VaultDelete, TRANSACTION(ttVAULT_DEPOSIT, 68, VaultDeposit, Delegation::delegatable, featureSingleAssetVault, - mayAuthorizeMPT, + mayAuthorizeMPT | mustModifyVault, ({ {sfVaultID, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, @@ -909,7 +909,7 @@ TRANSACTION(ttVAULT_DEPOSIT, 68, VaultDeposit, TRANSACTION(ttVAULT_WITHDRAW, 69, VaultWithdraw, Delegation::delegatable, featureSingleAssetVault, - mayDeleteMPT, + mayDeleteMPT | mustModifyVault, ({ {sfVaultID, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, @@ -924,7 +924,7 @@ TRANSACTION(ttVAULT_WITHDRAW, 69, VaultWithdraw, TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback, Delegation::delegatable, featureSingleAssetVault, - mayDeleteMPT, + mayDeleteMPT | mustModifyVault, ({ {sfVaultID, soeREQUIRED}, {sfHolder, soeREQUIRED}, diff --git a/include/xrpl/resource/detail/Logic.h b/include/xrpl/resource/detail/Logic.h index b07ee00e73..0fc5a9035a 100644 --- a/include/xrpl/resource/detail/Logic.h +++ b/include/xrpl/resource/detail/Logic.h @@ -436,10 +436,12 @@ public: admin_.erase(admin_.iterator_to(entry)); break; default: + // LCOV_EXCL_START UNREACHABLE( "ripple::Resource::Logic::release : invalid entry " "kind"); break; + // LCOV_EXCL_STOP } inactive_.push_back(entry); entry.whenExpires = m_clock.now() + secondsUntilExpiration; diff --git a/src/libxrpl/basics/Log.cpp b/src/libxrpl/basics/Log.cpp index 14873a3fd7..95419dda20 100644 --- a/src/libxrpl/basics/Log.cpp +++ b/src/libxrpl/basics/Log.cpp @@ -239,9 +239,11 @@ Logs::fromSeverity(beast::severities::Severity level) case kError: return lsERROR; + // LCOV_EXCL_START default: UNREACHABLE("ripple::Logs::fromSeverity : invalid severity"); [[fallthrough]]; + // LCOV_EXCL_STOP case kFatal: break; } @@ -265,9 +267,11 @@ Logs::toSeverity(LogSeverity level) return kWarning; case lsERROR: return kError; + // LCOV_EXCL_START default: UNREACHABLE("ripple::Logs::toSeverity : invalid severity"); [[fallthrough]]; + // LCOV_EXCL_STOP case lsFATAL: break; } @@ -292,9 +296,11 @@ Logs::toString(LogSeverity s) return "Error"; case lsFATAL: return "Fatal"; + // LCOV_EXCL_START default: UNREACHABLE("ripple::Logs::toString : invalid severity"); return "Unknown"; + // LCOV_EXCL_STOP } } @@ -356,9 +362,11 @@ Logs::format( case kError: output += "ERR "; break; + // LCOV_EXCL_START default: UNREACHABLE("ripple::Logs::format : invalid severity"); [[fallthrough]]; + // LCOV_EXCL_STOP case kFatal: output += "FTL "; break; diff --git a/src/libxrpl/basics/contract.cpp b/src/libxrpl/basics/contract.cpp index b5a7b3f368..ea75929be0 100644 --- a/src/libxrpl/basics/contract.cpp +++ b/src/libxrpl/basics/contract.cpp @@ -36,6 +36,7 @@ LogThrow(std::string const& title) [[noreturn]] void LogicError(std::string const& s) noexcept { + // LCOV_EXCL_START JLOG(debugLog().fatal()) << s; std::cerr << "Logic error: " << s << std::endl; // Use a non-standard contract naming here (without namespace) because @@ -45,6 +46,7 @@ LogicError(std::string const& s) noexcept // For the above reasons, we want this contract to stand out. UNREACHABLE("LogicError", {{"message", s}}); std::abort(); + // LCOV_EXCL_STOP } } // namespace ripple diff --git a/src/libxrpl/json/Object.cpp b/src/libxrpl/json/Object.cpp index 62f686e228..55e573c0db 100644 --- a/src/libxrpl/json/Object.cpp +++ b/src/libxrpl/json/Object.cpp @@ -174,7 +174,7 @@ Array::append(Json::Value const& v) return; } } - UNREACHABLE("Json::Array::append : invalid type"); + UNREACHABLE("Json::Array::append : invalid type"); // LCOV_EXCL_LINE } void @@ -209,7 +209,7 @@ Object::set(std::string const& k, Json::Value const& v) return; } } - UNREACHABLE("Json::Object::set : invalid type"); + UNREACHABLE("Json::Object::set : invalid type"); // LCOV_EXCL_LINE } //------------------------------------------------------------------------------ diff --git a/src/libxrpl/json/json_value.cpp b/src/libxrpl/json/json_value.cpp index 1df8f6cf31..7e4d8b6d81 100644 --- a/src/libxrpl/json/json_value.cpp +++ b/src/libxrpl/json/json_value.cpp @@ -213,8 +213,10 @@ Value::Value(ValueType type) : type_(type), allocated_(0) value_.bool_ = false; break; + // LCOV_EXCL_START default: UNREACHABLE("Json::Value::Value(ValueType) : invalid type"); + // LCOV_EXCL_STOP } } @@ -290,8 +292,10 @@ Value::Value(Value const& other) : type_(other.type_) value_.map_ = new ObjectValues(*other.value_.map_); break; + // LCOV_EXCL_START default: UNREACHABLE("Json::Value::Value(Value const&) : invalid type"); + // LCOV_EXCL_STOP } } @@ -318,8 +322,10 @@ Value::~Value() delete value_.map_; break; + // LCOV_EXCL_START default: UNREACHABLE("Json::Value::~Value : invalid type"); + // LCOV_EXCL_STOP } } @@ -419,8 +425,10 @@ operator<(Value const& x, Value const& y) return *x.value_.map_ < *y.value_.map_; } + // LCOV_EXCL_START default: UNREACHABLE("Json::operator<(Value, Value) : invalid type"); + // LCOV_EXCL_STOP } return 0; // unreachable @@ -465,8 +473,10 @@ operator==(Value const& x, Value const& y) return x.value_.map_->size() == y.value_.map_->size() && *x.value_.map_ == *y.value_.map_; + // LCOV_EXCL_START default: UNREACHABLE("Json::operator==(Value, Value) : invalid type"); + // LCOV_EXCL_STOP } return 0; // unreachable @@ -506,8 +516,10 @@ Value::asString() const case objectValue: JSON_ASSERT_MESSAGE(false, "Type is not convertible to string"); + // LCOV_EXCL_START default: UNREACHABLE("Json::Value::asString : invalid type"); + // LCOV_EXCL_STOP } return ""; // unreachable @@ -548,8 +560,10 @@ Value::asInt() const case objectValue: JSON_ASSERT_MESSAGE(false, "Type is not convertible to int"); + // LCOV_EXCL_START default: UNREACHABLE("Json::Value::asInt : invalid type"); + // LCOV_EXCL_STOP } return 0; // unreachable; @@ -590,8 +604,10 @@ Value::asUInt() const case objectValue: JSON_ASSERT_MESSAGE(false, "Type is not convertible to uint"); + // LCOV_EXCL_START default: UNREACHABLE("Json::Value::asUInt : invalid type"); + // LCOV_EXCL_STOP } return 0; // unreachable; @@ -622,8 +638,10 @@ Value::asDouble() const case objectValue: JSON_ASSERT_MESSAGE(false, "Type is not convertible to double"); + // LCOV_EXCL_START default: UNREACHABLE("Json::Value::asDouble : invalid type"); + // LCOV_EXCL_STOP } return 0; // unreachable; @@ -654,8 +672,10 @@ Value::asBool() const case objectValue: return value_.map_->size() != 0; + // LCOV_EXCL_START default: UNREACHABLE("Json::Value::asBool : invalid type"); + // LCOV_EXCL_STOP } return false; // unreachable; @@ -710,8 +730,10 @@ Value::isConvertibleTo(ValueType other) const return other == objectValue || (other == nullValue && value_.map_->size() == 0); + // LCOV_EXCL_START default: UNREACHABLE("Json::Value::isConvertible : invalid type"); + // LCOV_EXCL_STOP } return false; // unreachable; @@ -744,8 +766,10 @@ Value::size() const case objectValue: return Int(value_.map_->size()); + // LCOV_EXCL_START default: UNREACHABLE("Json::Value::size : invalid type"); + // LCOV_EXCL_STOP } return 0; // unreachable; diff --git a/src/libxrpl/ledger/ApplyStateTable.cpp b/src/libxrpl/ledger/ApplyStateTable.cpp index 7b041939d4..aaad056c58 100644 --- a/src/libxrpl/ledger/ApplyStateTable.cpp +++ b/src/libxrpl/ledger/ApplyStateTable.cpp @@ -259,9 +259,11 @@ ApplyStateTable::apply( } else { + // LCOV_EXCL_START UNREACHABLE( "ripple::detail::ApplyStateTable::apply : unsupported " "operation type"); + // LCOV_EXCL_STOP } } diff --git a/src/libxrpl/ledger/ApplyView.cpp b/src/libxrpl/ledger/ApplyView.cpp index 3c15156cf7..f02583d0d1 100644 --- a/src/libxrpl/ledger/ApplyView.cpp +++ b/src/libxrpl/ledger/ApplyView.cpp @@ -187,8 +187,10 @@ ApplyView::emptyDirDelete(Keylet const& directory) if (directory.type != ltDIR_NODE || node->getFieldH256(sfRootIndex) != directory.key) { + // LCOV_EXCL_START UNREACHABLE("ripple::ApplyView::emptyDirDelete : invalid node type"); return false; + // LCOV_EXCL_STOP } // The directory still contains entries and so it cannot be removed diff --git a/src/libxrpl/ledger/BookDirs.cpp b/src/libxrpl/ledger/BookDirs.cpp index f777d23aca..61ec160b1b 100644 --- a/src/libxrpl/ledger/BookDirs.cpp +++ b/src/libxrpl/ledger/BookDirs.cpp @@ -36,7 +36,9 @@ BookDirs::BookDirs(ReadView const& view, Book const& book) { if (!cdirFirst(*view_, key_, sle_, entry_, index_)) { + // LCOV_EXCL_START UNREACHABLE("ripple::BookDirs::BookDirs : directory is empty"); + // LCOV_EXCL_STOP } } } @@ -110,9 +112,11 @@ BookDirs::const_iterator::operator++() } else if (!cdirFirst(*view_, cur_key_, sle_, entry_, index_)) { + // LCOV_EXCL_START UNREACHABLE( "ripple::BookDirs::const_iterator::operator++ : directory is " "empty"); + // LCOV_EXCL_STOP } } diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index f9a4aeb1c4..25da88ecee 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -324,10 +324,12 @@ isVaultPseudoAccountFrozen( auto const issuer = mptIssuance->getAccountID(sfIssuer); auto const mptIssuer = view.read(keylet::account(issuer)); if (mptIssuer == nullptr) - { // LCOV_EXCL_START + { + // LCOV_EXCL_START UNREACHABLE("ripple::isVaultPseudoAccountFrozen : null MPToken issuer"); return false; - } // LCOV_EXCL_STOP + // LCOV_EXCL_STOP + } if (!mptIssuer->isFieldPresent(sfVaultID)) return false; // not a Vault pseudo-account, common case @@ -338,7 +340,8 @@ isVaultPseudoAccountFrozen( { // LCOV_EXCL_START UNREACHABLE("ripple::isVaultPseudoAccountFrozen : null vault"); return false; - } // LCOV_EXCL_STOP + // LCOV_EXCL_STOP + } return isAnyFrozen(view, {issuer, account}, vault->at(sfAsset), depth + 1); } @@ -2682,7 +2685,8 @@ enforceMPTokenAuthorization( UNREACHABLE( "ripple::enforceMPTokenAuthorization : condition list is incomplete"); return tefINTERNAL; -} // LCOV_EXCL_STOP + // LCOV_EXCL_STOP +} TER canTransfer( diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp index b54c74c80d..0cee2995db 100644 --- a/src/libxrpl/protocol/BuildInfo.cpp +++ b/src/libxrpl/protocol/BuildInfo.cpp @@ -36,7 +36,7 @@ namespace BuildInfo { // and follow the format described at http://semver.org/ //------------------------------------------------------------------------------ // clang-format off -char const* const versionString = "2.6.1" +char const* const versionString = "3.0.0-b1" // clang-format on #if defined(DEBUG) || defined(SANITIZER) diff --git a/src/libxrpl/protocol/STBase.cpp b/src/libxrpl/protocol/STBase.cpp index 417b7e2302..ecbe6833d0 100644 --- a/src/libxrpl/protocol/STBase.cpp +++ b/src/libxrpl/protocol/STBase.cpp @@ -112,7 +112,9 @@ void STBase::add(Serializer& s) const { // Should never be called + // LCOV_EXCL_START UNREACHABLE("ripple::STBase::add : not implemented"); + // LCOV_EXCL_STOP } bool diff --git a/src/libxrpl/protocol/TxMeta.cpp b/src/libxrpl/protocol/TxMeta.cpp index 2343a6a794..833a0677b9 100644 --- a/src/libxrpl/protocol/TxMeta.cpp +++ b/src/libxrpl/protocol/TxMeta.cpp @@ -238,9 +238,11 @@ TxMeta::getAffectedNode(uint256 const& node) if (n.getFieldH256(sfLedgerIndex) == node) return n; } + // LCOV_EXCL_START UNREACHABLE("ripple::TxMeta::getAffectedNode(uint256) : node not found"); Throw("Affected node not found"); return *(mNodes.begin()); // Silence compiler warning. + // LCOV_EXCL_STOP } STObject diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index f279c8dec2..d2b716ef04 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -24,9 +24,18 @@ #include #include +#include #include +#include +#include #include +#include +#include #include +#include +#include +#include +#include #include @@ -66,8 +75,9 @@ class Invariants_test : public beast::unit_test::suite * checker. * @preclose See "Preclose" above. Note that @preclose runs *before* * @precheck, but is the last parameter for historical reasons - * + * @setTxAccount optionally set to add sfAccount to tx (either A1 or A2) */ + enum class TxAccount : int { None = 0, A1, A2 }; void doInvariantCheck( std::vector const& expect_logs, @@ -76,7 +86,8 @@ class Invariants_test : public beast::unit_test::suite STTx tx = STTx{ttACCOUNT_SET, [](STObject&) {}}, std::initializer_list ters = {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, - Preclose const& preclose = {}) + Preclose const& preclose = {}, + TxAccount setTxAccount = TxAccount::None) { using namespace test::jtx; FeatureBitset amendments = testable_amendments() | @@ -93,6 +104,9 @@ class Invariants_test : public beast::unit_test::suite OpenView ov{*env.current()}; test::StreamSink sink{beast::severities::kWarning}; beast::Journal jlog{sink}; + if (setTxAccount != TxAccount::None) + tx.setAccountID( + sfAccount, setTxAccount == TxAccount::A1 ? A1.id() : A2.id()); ApplyContext ac{ env.app(), ov, @@ -117,12 +131,13 @@ class Invariants_test : public beast::unit_test::suite BEAST_EXPECT( messages.starts_with("Invariant failed:") || messages.starts_with("Transaction caused an exception")); + // std::cerr << messages << '\n'; for (auto const& m : expect_logs) { if (messages.find(m) == std::string::npos) { // uncomment if you want to log the invariant failure - // message log << " --> " << m << std::endl; + // std::cerr << " --> " << m << std::endl; fail(); } } @@ -2147,6 +2162,1690 @@ class Invariants_test : public beast::unit_test::suite } } + void + testVault() + { + using namespace test::jtx; + + struct AccountAmount + { + AccountID account; + int amount; + }; + struct Adjustements + { + std::optional assetsTotal = {}; + std::optional assetsAvailable = {}; + std::optional lossUnrealized = {}; + std::optional assetsMaximum = {}; + std::optional sharesTotal = {}; + std::optional vaultAssets = {}; + std::optional accountAssets = {}; + std::optional accountShares = {}; + }; + auto constexpr adjust = [&](ApplyView& ac, + ripple::Keylet keylet, + Adjustements args) { + auto sleVault = ac.peek(keylet); + if (!sleVault) + return false; + + auto const mptIssuanceID = (*sleVault)[sfShareMPTID]; + auto sleShares = ac.peek(keylet::mptIssuance(mptIssuanceID)); + if (!sleShares) + return false; + + // These two fields are adjusted in absolute terms + if (args.lossUnrealized) + (*sleVault)[sfLossUnrealized] = *args.lossUnrealized; + if (args.assetsMaximum) + (*sleVault)[sfAssetsMaximum] = *args.assetsMaximum; + + // Remaining fields are adjusted in terms of difference + if (args.assetsTotal) + (*sleVault)[sfAssetsTotal] = + *(*sleVault)[sfAssetsTotal] + *args.assetsTotal; + if (args.assetsAvailable) + (*sleVault)[sfAssetsAvailable] = + *(*sleVault)[sfAssetsAvailable] + *args.assetsAvailable; + ac.update(sleVault); + + if (args.sharesTotal) + (*sleShares)[sfOutstandingAmount] = + *(*sleShares)[sfOutstandingAmount] + *args.sharesTotal; + ac.update(sleShares); + + auto const assets = *(*sleVault)[sfAsset]; + auto const pseudoId = *(*sleVault)[sfAccount]; + if (args.vaultAssets) + { + if (assets.native()) + { + auto slePseudoAccount = ac.peek(keylet::account(pseudoId)); + if (!slePseudoAccount) + return false; + (*slePseudoAccount)[sfBalance] = + *(*slePseudoAccount)[sfBalance] + *args.vaultAssets; + ac.update(slePseudoAccount); + } + else if (assets.holds()) + { + auto const mptId = assets.get().getMptID(); + auto sleMPToken = ac.peek(keylet::mptoken(mptId, pseudoId)); + if (!sleMPToken) + return false; + (*sleMPToken)[sfMPTAmount] = + *(*sleMPToken)[sfMPTAmount] + *args.vaultAssets; + ac.update(sleMPToken); + } + else + return false; // Not supporting testing with IOU + } + + if (args.accountAssets) + { + auto const& pair = *args.accountAssets; + if (assets.native()) + { + auto sleAccount = ac.peek(keylet::account(pair.account)); + if (!sleAccount) + return false; + (*sleAccount)[sfBalance] = + *(*sleAccount)[sfBalance] + pair.amount; + ac.update(sleAccount); + } + else if (assets.holds()) + { + auto const mptID = assets.get().getMptID(); + auto sleMPToken = + ac.peek(keylet::mptoken(mptID, pair.account)); + if (!sleMPToken) + return false; + (*sleMPToken)[sfMPTAmount] = + *(*sleMPToken)[sfMPTAmount] + pair.amount; + ac.update(sleMPToken); + } + else + return false; // Not supporting testing with IOU + } + + if (args.accountShares) + { + auto const& pair = *args.accountShares; + auto sleMPToken = + ac.peek(keylet::mptoken(mptIssuanceID, pair.account)); + if (!sleMPToken) + return false; + (*sleMPToken)[sfMPTAmount] = + *(*sleMPToken)[sfMPTAmount] + pair.amount; + ac.update(sleMPToken); + } + return true; + }; + + constexpr auto args = + [](AccountID id, int adjustement, auto fn) -> Adjustements { + Adjustements sample = { + .assetsTotal = adjustement, + .assetsAvailable = adjustement, + .lossUnrealized = 0, + .sharesTotal = adjustement, + .vaultAssets = adjustement, + .accountAssets = // + AccountAmount{id, -adjustement}, + .accountShares = // + AccountAmount{id, adjustement}}; + fn(sample); + return sample; + }; + + Account A3{"A3"}; + Account A4{"A4"}; + auto const precloseXrp = + [&](Account const& A1, Account const& A2, Env& env) -> bool { + env.fund(XRP(1000), A3, A4); + Vault vault{env}; + auto [tx, keylet] = + vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + env(vault.deposit( + {.depositor = A1, .id = keylet.key, .amount = XRP(10)})); + env(vault.deposit( + {.depositor = A2, .id = keylet.key, .amount = XRP(10)})); + env(vault.deposit( + {.depositor = A3, .id = keylet.key, .amount = XRP(10)})); + return true; + }; + + testcase << "Vault general checks"; + doInvariantCheck( + {"vault deletion succeeded without deleting a vault"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + ac.view().update(sleVault); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_DELETE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, _] = vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + {"vault updated by a wrong transaction type"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + ac.view().erase(sleVault); + return true; + }, + XRPAmount{}, + STTx{ttPAYMENT, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, _] = vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + {"vault updated by a wrong transaction type"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + ac.view().update(sleVault); + return true; + }, + XRPAmount{}, + STTx{ttPAYMENT, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, _] = vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + {"vault updated by a wrong transaction type"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const sequence = ac.view().seq(); + auto const vaultKeylet = keylet::vault(A1.id(), sequence); + auto sleVault = std::make_shared(vaultKeylet); + auto const vaultPage = ac.view().dirInsert( + keylet::ownerDir(A1.id()), + sleVault->key(), + describeOwnerDir(A1.id())); + sleVault->setFieldU64(sfOwnerNode, *vaultPage); + ac.view().insert(sleVault); + return true; + }, + XRPAmount{}, + STTx{ttPAYMENT, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); + + doInvariantCheck( + {"vault deleted by a wrong transaction type"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + ac.view().erase(sleVault); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_SET, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, _] = vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + {"vault operation updated more than single vault"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + ac.view().erase(sleVault); + } + { + auto const keylet = keylet::vault(A2.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + ac.view().erase(sleVault); + } + return true; + }, + XRPAmount{}, + STTx{ttVAULT_DELETE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + { + auto [tx, _] = + vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + } + { + auto [tx, _] = + vault.create({.owner = A2, .asset = xrpIssue()}); + env(tx); + } + return true; + }); + + doInvariantCheck( + {"vault operation updated more than single vault"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const sequence = ac.view().seq(); + auto const insertVault = [&](Account const A) { + auto const vaultKeylet = keylet::vault(A.id(), sequence); + auto sleVault = std::make_shared(vaultKeylet); + auto const vaultPage = ac.view().dirInsert( + keylet::ownerDir(A.id()), + sleVault->key(), + describeOwnerDir(A.id())); + sleVault->setFieldU64(sfOwnerNode, *vaultPage); + ac.view().insert(sleVault); + }; + insertVault(A1); + insertVault(A2); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_CREATE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); + + doInvariantCheck( + {"deleted vault must also delete shares"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + ac.view().erase(sleVault); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_DELETE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, _] = vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + {"deleted vault must have no shares outstanding", + "deleted vault must have no assets outstanding", + "deleted vault must have no assets available"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + auto sleShares = ac.view().peek( + keylet::mptIssuance((*sleVault)[sfShareMPTID])); + if (!sleShares) + return false; + ac.view().erase(sleVault); + ac.view().erase(sleShares); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_DELETE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, keylet] = + vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + env(vault.deposit( + {.depositor = A1, .id = keylet.key, .amount = XRP(10)})); + return true; + }); + + doInvariantCheck( + {"vault operation succeeded without modifying a vault"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + auto sleShares = ac.view().peek( + keylet::mptIssuance((*sleVault)[sfShareMPTID])); + if (!sleShares) + return false; + // Note, such an "orphaned" update of MPT issuance attached to a + // vault is invalid; ttVAULT_SET must also update Vault object. + sleShares->setFieldH256(sfDomainID, uint256(13)); + ac.view().update(sleShares); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"vault operation succeeded without modifying a vault"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + return true; + }, + XRPAmount{}, + STTx{ttVAULT_CREATE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, _] = vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + {"vault operation succeeded without modifying a vault"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + return true; + }, + XRPAmount{}, + STTx{ttVAULT_DEPOSIT, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, _] = vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + {"vault operation succeeded without modifying a vault"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + return true; + }, + XRPAmount{}, + STTx{ttVAULT_WITHDRAW, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, _] = vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + {"vault operation succeeded without modifying a vault"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + return true; + }, + XRPAmount{}, + STTx{ttVAULT_CLAWBACK, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, _] = vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + {"vault operation succeeded without modifying a vault"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + return true; + }, + XRPAmount{}, + STTx{ttVAULT_DELETE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, _] = vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + {"updated vault must have shares"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + (*sleVault)[sfAssetsMaximum] = 200; + ac.view().update(sleVault); + + auto sleShares = ac.view().peek( + keylet::mptIssuance((*sleVault)[sfShareMPTID])); + if (!sleShares) + return false; + ac.view().erase(sleShares); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_SET, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, _] = vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + {"vault operation succeeded without updating shares", + "assets available must not be greater than assets outstanding"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + (*sleVault)[sfAssetsTotal] = 9; + ac.view().update(sleVault); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_WITHDRAW, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, keylet] = + vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + env(vault.deposit( + {.depositor = A1, .id = keylet.key, .amount = XRP(10)})); + return true; + }); + + doInvariantCheck( + {"set must not change assets outstanding", + "set must not change assets available", + "set must not change shares outstanding", + "set must not change vault balance", + "assets available must be positive", + "assets available must not be greater than assets outstanding", + "assets outstanding must be positive"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + auto slePseudoAccount = + ac.view().peek(keylet::account(*(*sleVault)[sfAccount])); + if (!slePseudoAccount) + return false; + (*slePseudoAccount)[sfBalance] = + *(*slePseudoAccount)[sfBalance] - 10; + ac.view().update(slePseudoAccount); + + // Move 10 drops to A4 to enforce total XRP balance + auto sleA4 = ac.view().peek(keylet::account(A4.id())); + if (!sleA4) + return false; + (*sleA4)[sfBalance] = *(*sleA4)[sfBalance] + 10; + ac.view().update(sleA4); + + return adjust( + ac.view(), + keylet, + args(A2.id(), 0, [&](Adjustements& sample) { + sample.assetsAvailable = (DROPS_PER_XRP * -100).value(); + sample.assetsTotal = (DROPS_PER_XRP * -200).value(); + sample.sharesTotal = -1; + })); + }, + XRPAmount{}, + STTx{ttVAULT_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"violation of vault immutable data"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + sleVault->setFieldIssue( + sfAsset, STIssue{sfAsset, MPTIssue(MPTID(42))}); + ac.view().update(sleVault); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp); + + doInvariantCheck( + {"violation of vault immutable data"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + sleVault->setAccountID(sfAccount, A2.id()); + ac.view().update(sleVault); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp); + + doInvariantCheck( + {"violation of vault immutable data"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + (*sleVault)[sfShareMPTID] = MPTID(42); + ac.view().update(sleVault); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp); + + doInvariantCheck( + {"vault transaction must not change loss unrealized", + "set must not change assets outstanding"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), 0, [&](Adjustements& sample) { + sample.lossUnrealized = 13; + sample.assetsTotal = 20; + })); + }, + XRPAmount{}, + STTx{ttVAULT_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"loss unrealized must not exceed the difference " + "between assets outstanding and available", + "vault transaction must not change loss unrealized"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), 100, [&](Adjustements& sample) { + sample.lossUnrealized = 13; + })); + }, + XRPAmount{}, + STTx{ + ttVAULT_DEPOSIT, + [](STObject& tx) { + tx.setFieldAmount(sfAmount, XRPAmount(200)); + }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"set assets outstanding must not exceed assets maximum"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), 0, [&](Adjustements& sample) { + sample.assetsMaximum = 1; + })); + }, + XRPAmount{}, + STTx{ttVAULT_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"assets maximum must be positive"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), 0, [&](Adjustements& sample) { + sample.assetsMaximum = -1; + })); + }, + XRPAmount{}, + STTx{ttVAULT_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"set must not change shares outstanding", + "updated zero sized vault must have no assets outstanding", + "updated zero sized vault must have no assets available"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + ac.view().update(sleVault); + auto sleShares = ac.view().peek( + keylet::mptIssuance((*sleVault)[sfShareMPTID])); + if (!sleShares) + return false; + (*sleShares)[sfOutstandingAmount] = 0; + ac.view().update(sleShares); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"updated shares must not exceed maximum"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + auto sleShares = ac.view().peek( + keylet::mptIssuance((*sleVault)[sfShareMPTID])); + if (!sleShares) + return false; + (*sleShares)[sfMaximumAmount] = 10; + ac.view().update(sleShares); + + return adjust( + ac.view(), keylet, args(A2.id(), 10, [](Adjustements&) {})); + }, + XRPAmount{}, + STTx{ttVAULT_DEPOSIT, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"updated shares must not exceed maximum"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + adjust( + ac.view(), keylet, args(A2.id(), 10, [](Adjustements&) {})); + + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + auto sleShares = ac.view().peek( + keylet::mptIssuance((*sleVault)[sfShareMPTID])); + if (!sleShares) + return false; + (*sleShares)[sfOutstandingAmount] = maxMPTokenAmount + 1; + ac.view().update(sleShares); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_DEPOSIT, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + testcase << "Vault create"; + doInvariantCheck( + { + "created vault must be empty", + "updated zero sized vault must have no assets outstanding", + "create operation must not have updated a vault", + }, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + (*sleVault)[sfAssetsTotal] = 9; + ac.view().update(sleVault); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_CREATE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, keylet] = + vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + { + "created vault must be empty", + "updated zero sized vault must have no assets available", + "assets available must not be greater than assets outstanding", + "create operation must not have updated a vault", + }, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + (*sleVault)[sfAssetsAvailable] = 9; + ac.view().update(sleVault); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_CREATE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, keylet] = + vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + { + "created vault must be empty", + "loss unrealized must not exceed the difference between assets " + "outstanding and available", + "vault transaction must not change loss unrealized", + "create operation must not have updated a vault", + }, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + (*sleVault)[sfLossUnrealized] = 1; + ac.view().update(sleVault); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_CREATE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, keylet] = + vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + { + "created vault must be empty", + "create operation must not have updated a vault", + }, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + auto sleShares = ac.view().peek( + keylet::mptIssuance((*sleVault)[sfShareMPTID])); + if (!sleShares) + return false; + ac.view().update(sleVault); + (*sleShares)[sfOutstandingAmount] = 9; + ac.view().update(sleShares); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_CREATE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, keylet] = + vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + { + "assets maximum must be positive", + "create operation must not have updated a vault", + }, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + (*sleVault)[sfAssetsMaximum] = Number(-1); + ac.view().update(sleVault); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_CREATE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, keylet] = + vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + {"create operation must not have updated a vault", + "shares issuer and vault pseudo-account must be the same", + "shares issuer must be a pseudo-account", + "shares issuer pseudo-account must point back to the vault"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + auto sleVault = ac.view().peek(keylet); + if (!sleVault) + return false; + auto sleShares = ac.view().peek( + keylet::mptIssuance((*sleVault)[sfShareMPTID])); + if (!sleShares) + return false; + ac.view().update(sleVault); + (*sleShares)[sfIssuer] = A1.id(); + ac.view().update(sleShares); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_CREATE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + [&](Account const& A1, Account const& A2, Env& env) { + Vault vault{env}; + auto [tx, keylet] = + vault.create({.owner = A1, .asset = xrpIssue()}); + env(tx); + return true; + }); + + doInvariantCheck( + {"vault created by a wrong transaction type", + "account root created illegally"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + // The code below will create a valid vault with (almost) all + // the invariants holding. Except one: it is created by the + // wrong transaction type. + auto const sequence = ac.view().seq(); + auto const vaultKeylet = keylet::vault(A1.id(), sequence); + auto sleVault = std::make_shared(vaultKeylet); + auto const vaultPage = ac.view().dirInsert( + keylet::ownerDir(A1.id()), + sleVault->key(), + describeOwnerDir(A1.id())); + sleVault->setFieldU64(sfOwnerNode, *vaultPage); + + auto pseudoId = + pseudoAccountAddress(ac.view(), vaultKeylet.key); + // Create pseudo-account. + auto sleAccount = + std::make_shared(keylet::account(pseudoId)); + sleAccount->setAccountID(sfAccount, pseudoId); + sleAccount->setFieldAmount(sfBalance, STAmount{}); + std::uint32_t const seqno = // + ac.view().rules().enabled(featureSingleAssetVault) // + ? 0 // + : sequence; + sleAccount->setFieldU32(sfSequence, seqno); + sleAccount->setFieldU32( + sfFlags, + lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth); + sleAccount->setFieldH256(sfVaultID, vaultKeylet.key); + ac.view().insert(sleAccount); + + auto const sharesMptId = makeMptID(sequence, pseudoId); + auto const sharesKeylet = keylet::mptIssuance(sharesMptId); + auto sleShares = std::make_shared(sharesKeylet); + auto const sharesPage = ac.view().dirInsert( + keylet::ownerDir(pseudoId), + sharesKeylet, + describeOwnerDir(pseudoId)); + sleShares->setFieldU64(sfOwnerNode, *sharesPage); + + sleShares->at(sfFlags) = 0; + sleShares->at(sfIssuer) = pseudoId; + sleShares->at(sfOutstandingAmount) = 0; + sleShares->at(sfSequence) = sequence; + + sleVault->at(sfAccount) = pseudoId; + sleVault->at(sfFlags) = 0; + sleVault->at(sfSequence) = sequence; + sleVault->at(sfOwner) = A1.id(); + sleVault->at(sfAssetsTotal) = Number(0); + sleVault->at(sfAssetsAvailable) = Number(0); + sleVault->at(sfLossUnrealized) = Number(0); + sleVault->at(sfShareMPTID) = sharesMptId; + sleVault->at(sfWithdrawalPolicy) = + vaultStrategyFirstComeFirstServe; + + ac.view().insert(sleVault); + ac.view().insert(sleShares); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_SET, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}); + + doInvariantCheck( + {"shares issuer and vault pseudo-account must be the same", + "shares issuer pseudo-account must point back to the vault"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const sequence = ac.view().seq(); + auto const vaultKeylet = keylet::vault(A1.id(), sequence); + auto sleVault = std::make_shared(vaultKeylet); + auto const vaultPage = ac.view().dirInsert( + keylet::ownerDir(A1.id()), + sleVault->key(), + describeOwnerDir(A1.id())); + sleVault->setFieldU64(sfOwnerNode, *vaultPage); + + auto pseudoId = + pseudoAccountAddress(ac.view(), vaultKeylet.key); + // Create pseudo-account. + auto sleAccount = + std::make_shared(keylet::account(pseudoId)); + sleAccount->setAccountID(sfAccount, pseudoId); + sleAccount->setFieldAmount(sfBalance, STAmount{}); + std::uint32_t const seqno = // + ac.view().rules().enabled(featureSingleAssetVault) // + ? 0 // + : sequence; + sleAccount->setFieldU32(sfSequence, seqno); + sleAccount->setFieldU32( + sfFlags, + lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth); + // sleAccount->setFieldH256(sfVaultID, vaultKeylet.key); + // Setting wrong vault key + sleAccount->setFieldH256(sfVaultID, uint256(42)); + ac.view().insert(sleAccount); + + auto const sharesMptId = makeMptID(sequence, pseudoId); + auto const sharesKeylet = keylet::mptIssuance(sharesMptId); + auto sleShares = std::make_shared(sharesKeylet); + auto const sharesPage = ac.view().dirInsert( + keylet::ownerDir(pseudoId), + sharesKeylet, + describeOwnerDir(pseudoId)); + sleShares->setFieldU64(sfOwnerNode, *sharesPage); + + sleShares->at(sfFlags) = 0; + sleShares->at(sfIssuer) = pseudoId; + sleShares->at(sfOutstandingAmount) = 0; + sleShares->at(sfSequence) = sequence; + + // sleVault->at(sfAccount) = pseudoId; + // Setting wrong pseudo acocunt ID + sleVault->at(sfAccount) = A2.id(); + sleVault->at(sfFlags) = 0; + sleVault->at(sfSequence) = sequence; + sleVault->at(sfOwner) = A1.id(); + sleVault->at(sfAssetsTotal) = Number(0); + sleVault->at(sfAssetsAvailable) = Number(0); + sleVault->at(sfLossUnrealized) = Number(0); + sleVault->at(sfShareMPTID) = sharesMptId; + sleVault->at(sfWithdrawalPolicy) = + vaultStrategyFirstComeFirstServe; + + ac.view().insert(sleVault); + ac.view().insert(sleShares); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_CREATE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}); + + doInvariantCheck( + {"shares issuer and vault pseudo-account must be the same", + "shares issuer must exist"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const sequence = ac.view().seq(); + auto const vaultKeylet = keylet::vault(A1.id(), sequence); + auto sleVault = std::make_shared(vaultKeylet); + auto const vaultPage = ac.view().dirInsert( + keylet::ownerDir(A1.id()), + sleVault->key(), + describeOwnerDir(A1.id())); + sleVault->setFieldU64(sfOwnerNode, *vaultPage); + + auto const sharesMptId = makeMptID(sequence, A2.id()); + auto const sharesKeylet = keylet::mptIssuance(sharesMptId); + auto sleShares = std::make_shared(sharesKeylet); + auto const sharesPage = ac.view().dirInsert( + keylet::ownerDir(A2.id()), + sharesKeylet, + describeOwnerDir(A2.id())); + sleShares->setFieldU64(sfOwnerNode, *sharesPage); + + sleShares->at(sfFlags) = 0; + // Setting wrong pseudo acocunt ID + sleShares->at(sfIssuer) = AccountID(uint160(42)); + sleShares->at(sfOutstandingAmount) = 0; + sleShares->at(sfSequence) = sequence; + + sleVault->at(sfAccount) = A2.id(); + sleVault->at(sfFlags) = 0; + sleVault->at(sfSequence) = sequence; + sleVault->at(sfOwner) = A1.id(); + sleVault->at(sfAssetsTotal) = Number(0); + sleVault->at(sfAssetsAvailable) = Number(0); + sleVault->at(sfLossUnrealized) = Number(0); + sleVault->at(sfShareMPTID) = sharesMptId; + sleVault->at(sfWithdrawalPolicy) = + vaultStrategyFirstComeFirstServe; + + ac.view().insert(sleVault); + ac.view().insert(sleShares); + return true; + }, + XRPAmount{}, + STTx{ttVAULT_CREATE, [](STObject&) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}); + + testcase << "Vault deposit"; + doInvariantCheck( + {"deposit must change vault balance"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), 0, [&](Adjustements& sample) {})); + }, + XRPAmount{}, + STTx{ttVAULT_DEPOSIT, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp); + + doInvariantCheck( + {"deposit assets outstanding must not exceed assets maximum"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), 200, [&](Adjustements& sample) { + sample.assetsMaximum = 1; + })); + }, + XRPAmount{}, + STTx{ + ttVAULT_DEPOSIT, + [](STObject& tx) { + tx.setFieldAmount(sfAmount, XRPAmount(200)); + }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + // This really convoluted unit tests makes the zero balance on the + // depositor, by sending them the same amount as the transaction fee. + // The operation makes no sense, but the defensive check in + // ValidVault::finalize is otherwise impossible to trigger. + doInvariantCheck( + {"deposit must increase vault balance", + "deposit must change depositor balance"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + + // Move 10 drops to A4 to enforce total XRP balance + auto sleA4 = ac.view().peek(keylet::account(A4.id())); + if (!sleA4) + return false; + (*sleA4)[sfBalance] = *(*sleA4)[sfBalance] + 10; + ac.view().update(sleA4); + + return adjust( + ac.view(), + keylet, + args(A3.id(), -10, [&](Adjustements& sample) { + sample.accountAssets->amount = -100; + })); + }, + XRPAmount{100}, + STTx{ + ttVAULT_DEPOSIT, + [&](STObject& tx) { + tx[sfFee] = XRPAmount(100); + tx[sfAccount] = A3.id(); + }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp); + + doInvariantCheck( + {"deposit must increase vault balance", + "deposit must decrease depositor balance", + "deposit must change vault and depositor balance by equal amount", + "deposit and assets outstanding must add up", + "deposit and assets available must add up"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + + // Move 10 drops from A2 to A3 to enforce total XRP balance + auto sleA3 = ac.view().peek(keylet::account(A3.id())); + if (!sleA3) + return false; + (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] + 10; + ac.view().update(sleA3); + + return adjust( + ac.view(), + keylet, + args(A2.id(), 10, [&](Adjustements& sample) { + sample.vaultAssets = -20; + sample.accountAssets->amount = 10; + })); + }, + XRPAmount{}, + STTx{ + ttVAULT_DEPOSIT, + [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"deposit must change depositor balance"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + + // Move 10 drops from A3 to vault to enforce total XRP balance + auto sleA3 = ac.view().peek(keylet::account(A3.id())); + if (!sleA3) + return false; + (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] - 10; + ac.view().update(sleA3); + + return adjust( + ac.view(), + keylet, + args(A2.id(), 10, [&](Adjustements& sample) { + sample.accountAssets->amount = 0; + })); + }, + XRPAmount{}, + STTx{ + ttVAULT_DEPOSIT, + [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"deposit must change depositor shares"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), 10, [&](Adjustements& sample) { + sample.accountShares->amount = 0; + })); + }, + XRPAmount{}, + STTx{ + ttVAULT_DEPOSIT, + [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"deposit must change vault shares"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), 10, [&](Adjustements& sample) { + sample.sharesTotal = 0; + })); + }, + XRPAmount{}, + STTx{ + ttVAULT_DEPOSIT, + [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"deposit must increase depositor shares", + "deposit must change depositor and vault shares by equal amount", + "deposit must not change vault balance by more than deposited " + "amount"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), 10, [&](Adjustements& sample) { + sample.accountShares->amount = -5; + sample.sharesTotal = -10; + })); + }, + XRPAmount{}, + STTx{ + ttVAULT_DEPOSIT, + [](STObject& tx) { tx[sfAmount] = XRPAmount(5); }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"deposit and assets outstanding must add up", + "deposit and assets available must add up"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), 10, [&](Adjustements& sample) { + sample.assetsTotal = 7; + sample.assetsAvailable = 7; + })); + }, + XRPAmount{}, + STTx{ + ttVAULT_DEPOSIT, + [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + testcase << "Vault withdrawal"; + doInvariantCheck( + {"withdrawal must change vault balance"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), 0, [&](Adjustements& sample) {})); + }, + XRPAmount{}, + STTx{ttVAULT_WITHDRAW, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp); + + // Almost identical to the really convoluted test for deposit, where the + // depositor spends only the transaction fee. In case of withdrawal, + // this test is almost the same as normal withdrawal where the + // sfDestination would have been A4, but has been omitted. + doInvariantCheck( + {"withdrawal must change one destination balance"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + + // Move 10 drops to A4 to enforce total XRP balance + auto sleA4 = ac.view().peek(keylet::account(A4.id())); + if (!sleA4) + return false; + (*sleA4)[sfBalance] = *(*sleA4)[sfBalance] + 10; + ac.view().update(sleA4); + + return adjust( + ac.view(), + keylet, + args(A3.id(), -10, [&](Adjustements& sample) { + sample.accountAssets->amount = -100; + })); + }, + XRPAmount{100}, + STTx{ + ttVAULT_WITHDRAW, + [&](STObject& tx) { + tx[sfFee] = XRPAmount(100); + tx[sfAccount] = A3.id(); + // This commented out line causes the invariant violation. + // tx[sfDestination] = A4.id(); + }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp); + + doInvariantCheck( + {"withdrawal must change vault and destination balance by " + "equal amount", + "withdrawal must decrease vault balance", + "withdrawal must increase destination balance", + "withdrawal and assets outstanding must add up", + "withdrawal and assets available must add up"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + + // Move 10 drops from A2 to A3 to enforce total XRP balance + auto sleA3 = ac.view().peek(keylet::account(A3.id())); + if (!sleA3) + return false; + (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] + 10; + ac.view().update(sleA3); + + return adjust( + ac.view(), + keylet, + args(A2.id(), -10, [&](Adjustements& sample) { + sample.vaultAssets = 10; + sample.accountAssets->amount = -20; + })); + }, + XRPAmount{}, + STTx{ttVAULT_WITHDRAW, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"withdrawal must change one destination balance"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + if (!adjust( + ac.view(), + keylet, + args(A2.id(), -10, [&](Adjustements& sample) { + *sample.vaultAssets -= 5; + }))) + return false; + auto sleA3 = ac.view().peek(keylet::account(A3.id())); + if (!sleA3) + return false; + (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] + 5; + ac.view().update(sleA3); + return true; + }, + XRPAmount{}, + STTx{ + ttVAULT_WITHDRAW, + [&](STObject& tx) { tx.setAccountID(sfDestination, A3.id()); }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"withdrawal must change depositor shares"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), -10, [&](Adjustements& sample) { + sample.accountShares->amount = 0; + })); + }, + XRPAmount{}, + STTx{ttVAULT_WITHDRAW, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"withdrawal must change vault shares"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), -10, [&](Adjustements& sample) { + sample.sharesTotal = 0; + })); + }, + XRPAmount{}, + STTx{ttVAULT_WITHDRAW, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"withdrawal must decrease depositor shares", + "withdrawal must change depositor and vault shares by equal " + "amount"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), -10, [&](Adjustements& sample) { + sample.accountShares->amount = 5; + sample.sharesTotal = 10; + })); + }, + XRPAmount{}, + STTx{ttVAULT_WITHDRAW, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + doInvariantCheck( + {"withdrawal and assets outstanding must add up", + "withdrawal and assets available must add up"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), -10, [&](Adjustements& sample) { + sample.assetsTotal = -15; + sample.assetsAvailable = -15; + })); + }, + XRPAmount{}, + STTx{ttVAULT_WITHDRAW, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + auto const precloseMpt = + [&](Account const& A1, Account const& A2, Env& env) -> bool { + env.fund(XRP(1000), A3, A4); + + // Create MPT asset + { + Json::Value jv; + jv[sfAccount] = A3.human(); + jv[sfTransactionType] = jss::MPTokenIssuanceCreate; + jv[sfFlags] = tfMPTCanTransfer; + env(jv); + env.close(); + } + + auto const mptID = makeMptID(env.seq(A3) - 1, A3); + Asset asset = MPTIssue(mptID); + // Authorize A1 A2 A4 + { + Json::Value jv; + jv[sfAccount] = A1.human(); + jv[sfTransactionType] = jss::MPTokenAuthorize; + jv[sfMPTokenIssuanceID] = to_string(mptID); + env(jv); + jv[sfAccount] = A2.human(); + env(jv); + jv[sfAccount] = A4.human(); + env(jv); + + env.close(); + } + // Send tokens to A1 A2 A4 + { + env(pay(A3, A1, asset(1000))); + env(pay(A3, A2, asset(1000))); + env(pay(A3, A4, asset(1000))); + env.close(); + } + + Vault vault{env}; + auto [tx, keylet] = vault.create({.owner = A1, .asset = asset}); + env(tx); + env(vault.deposit( + {.depositor = A1, .id = keylet.key, .amount = asset(10)})); + env(vault.deposit( + {.depositor = A2, .id = keylet.key, .amount = asset(10)})); + env(vault.deposit( + {.depositor = A4, .id = keylet.key, .amount = asset(10)})); + return true; + }; + + doInvariantCheck( + {"withdrawal must decrease depositor shares", + "withdrawal must change depositor and vault shares by equal " + "amount"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq() - 2); + return adjust( + ac.view(), + keylet, + args(A2.id(), -10, [&](Adjustements& sample) { + sample.accountShares->amount = 5; + })); + }, + XRPAmount{}, + STTx{ + ttVAULT_WITHDRAW, + [&](STObject& tx) { tx[sfAccount] = A3.id(); }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseMpt, + TxAccount::A2); + + testcase << "Vault clawback"; + doInvariantCheck( + {"clawback must change vault balance"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq() - 2); + return adjust( + ac.view(), + keylet, + args(A2.id(), -1, [&](Adjustements& sample) { + sample.vaultAssets = 0; + })); + }, + XRPAmount{}, + STTx{ + ttVAULT_CLAWBACK, + [&](STObject& tx) { tx[sfAccount] = A3.id(); }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseMpt); + + // Not the same as below check: attempt to clawback XRP + doInvariantCheck( + {"clawback may only be performed by the asset issuer"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq()); + return adjust( + ac.view(), + keylet, + args(A2.id(), 0, [&](Adjustements& sample) {})); + }, + XRPAmount{}, + STTx{ttVAULT_CLAWBACK, [](STObject&) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp); + + // Not the same as above check: attempt to clawback MPT by bad account + doInvariantCheck( + {"clawback may only be performed by the asset issuer"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq() - 2); + return adjust( + ac.view(), + keylet, + args(A2.id(), 0, [&](Adjustements& sample) {})); + }, + XRPAmount{}, + STTx{ + ttVAULT_CLAWBACK, + [&](STObject& tx) { tx[sfAccount] = A4.id(); }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseMpt); + + doInvariantCheck( + {"clawback must decrease vault balance", + "clawback must decrease holder shares", + "clawback must change vault shares"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq() - 2); + return adjust( + ac.view(), + keylet, + args(A4.id(), 10, [&](Adjustements& sample) { + sample.sharesTotal = 0; + })); + }, + XRPAmount{}, + STTx{ + ttVAULT_CLAWBACK, + [&](STObject& tx) { + tx[sfAccount] = A3.id(); + tx[sfHolder] = A4.id(); + }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseMpt); + + doInvariantCheck( + {"clawback must change holder shares"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq() - 2); + return adjust( + ac.view(), + keylet, + args(A4.id(), -10, [&](Adjustements& sample) { + sample.accountShares->amount = 0; + })); + }, + XRPAmount{}, + STTx{ + ttVAULT_CLAWBACK, + [&](STObject& tx) { + tx[sfAccount] = A3.id(); + tx[sfHolder] = A4.id(); + }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseMpt); + + doInvariantCheck( + {"clawback must change holder and vault shares by equal amount", + "clawback and assets outstanding must add up", + "clawback and assets available must add up"}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const keylet = keylet::vault(A1.id(), ac.view().seq() - 2); + return adjust( + ac.view(), + keylet, + args(A4.id(), -10, [&](Adjustements& sample) { + sample.accountShares->amount = -8; + sample.assetsTotal = -7; + sample.assetsAvailable = -7; + })); + }, + XRPAmount{}, + STTx{ + ttVAULT_CLAWBACK, + [&](STObject& tx) { + tx[sfAccount] = A3.id(); + tx[sfHolder] = A4.id(); + }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseMpt); + } + public: void run() override @@ -2169,6 +3868,7 @@ public: testNoModifiedUnmodifiableFields(); testValidPseudoAccounts(); testValidLoanBroker(); + testVault(); } }; diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index a293ded39c..b5f94f38d1 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -177,6 +177,14 @@ class Vault_test : public beast::unit_test::suite env.close(); } + { + testcase(prefix + " set maximum is idempotent, set it again"); + auto tx = vault.set({.owner = owner, .id = keylet.key}); + tx[sfAssetsMaximum] = asset(150).number(); + env(tx); + env.close(); + } + { testcase(prefix + " set data"); auto tx = vault.set({.owner = owner, .id = keylet.key}); @@ -218,6 +226,7 @@ class Vault_test : public beast::unit_test::suite .id = keylet.key, .amount = asset(1000)}); env(tx, ter(tecINSUFFICIENT_FUNDS)); + env.close(); } { @@ -385,6 +394,27 @@ class Vault_test : public beast::unit_test::suite env.balance(depositor, shares) == share(50 * scale)); } + if (!asset.raw().native()) + { + testcase(prefix + " issuer deposits"); + auto tx = vault.deposit( + {.depositor = issuer, + .id = keylet.key, + .amount = asset(10)}); + env(tx); + env.close(); + BEAST_EXPECT(env.balance(issuer, shares) == share(10 * scale)); + + testcase(prefix + " issuer withdraws"); + tx = vault.withdraw( + {.depositor = issuer, + .id = keylet.key, + .amount = share(10 * scale)}); + env(tx); + env.close(); + BEAST_EXPECT(env.balance(issuer, shares) == share(0 * scale)); + } + { testcase(prefix + " withdraw remaining assets"); auto tx = vault.withdraw( @@ -454,6 +484,8 @@ class Vault_test : public beast::unit_test::suite .amount = asset(10)}); tx[sfDestination] = erin.human(); env(tx); + env.close(); + // Erin returns assets to issuer env(pay(erin, issuer, asset(10))); env.close(); @@ -479,12 +511,14 @@ class Vault_test : public beast::unit_test::suite testcase(prefix + " fail to delete because wrong owner"); auto tx = vault.del({.owner = issuer, .id = keylet.key}); env(tx, ter(tecNO_PERMISSION)); + env.close(); } { testcase(prefix + " delete empty vault"); auto tx = vault.del({.owner = owner, .id = keylet.key}); env(tx); + env.close(); BEAST_EXPECT(!env.le(keylet)); } }; @@ -1363,6 +1397,26 @@ class Vault_test : public beast::unit_test::suite { using namespace test::jtx; { + { + testcase("IOU fail because MPT is disabled"); + Env env{ + *this, + (testable_amendments() - featureMPTokensV1) | + featureSingleAssetVault}; + Account issuer{"issuer"}; + Account owner{"owner"}; + env.fund(XRP(1000), issuer, owner); + env.close(); + + Vault vault{env}; + Asset asset = issuer["IOU"].asset(); + auto [tx, keylet] = + vault.create({.owner = owner, .asset = asset}); + + env(tx, ter(temDISABLED)); + env.close(); + } + { testcase("IOU fail create frozen"); Env env{*this, testable_amendments() | featureSingleAssetVault}; @@ -2913,6 +2967,12 @@ class Vault_test : public beast::unit_test::suite tx[sfDomainID] = to_string(domainId); env(tx); env.close(); + + // Should be idempotent + tx = vault.set({.owner = owner, .id = keylet.key}); + tx[sfDomainID] = to_string(domainId); + env(tx); + env.close(); } } @@ -3068,6 +3128,7 @@ class Vault_test : public beast::unit_test::suite .id = keylet.key, .amount = asset(50)}); env(tx); + env.close(); tx = vault.clawback( {.issuer = issuer, @@ -3082,6 +3143,7 @@ class Vault_test : public beast::unit_test::suite .holder = owner, .amount = asset(0)}); env(tx); + env.close(); tx = vault.del({ .owner = owner, @@ -3128,6 +3190,7 @@ class Vault_test : public beast::unit_test::suite auto tx = vault.deposit( {.depositor = owner, .id = keylet.key, .amount = asset(50)}); env(tx); + env.close(); } { diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index 0a36a8a841..5e7ab5be4d 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -136,11 +136,12 @@ class Simulate_test : public beast::unit_test::suite jtx::Env& env, Json::Value const& tx, std::function const& validate, Json::Value const& expectedMetadataKey, - bool testSerialized = true) + Json::Value const& expectedMetadataValue) { env.close(); @@ -149,8 +150,13 @@ class Simulate_test : public beast::unit_test::suite validate( env.rpc("json", "simulate", to_string(params)), tx, - expectedMetadataKey); - validate(env.rpc("simulate", to_string(tx)), tx, expectedMetadataKey); + expectedMetadataKey, + expectedMetadataValue); + validate( + env.rpc("simulate", to_string(tx)), + tx, + expectedMetadataKey, + expectedMetadataValue); BEAST_EXPECTS( env.current()->txCount() == 0, @@ -1218,73 +1224,93 @@ class Simulate_test : public beast::unit_test::suite testcase("Successful transaction with additional metadata"); using namespace jtx; + using namespace std::chrono_literals; Env env{*this, envconfig([&](std::unique_ptr cfg) { cfg->NETWORK_ID = 1025; return cfg; })}; Account const alice("alice"); + Account const bob("bob"); - env.fund(XRP(10000), alice); + env.fund(XRP(10000), alice, bob); env.close(); + // deliver_amount is unavailable in the metadata before 2014-02-01 + // so proceed to 2014-02-01 + env.close(NetClock::time_point{446000000s}); { - auto validateOutput = [&](Json::Value const& resp, - Json::Value const& tx, - Json::Value const& expectedMetadataKey) { - auto result = resp[jss::result]; + auto validateOutput = + [&](Json::Value const& resp, + Json::Value const& tx, + Json::Value const& expectedMetadataKey, + Json::Value const& expectedMetadataValue) { + auto result = resp[jss::result]; - BEAST_EXPECT(result[jss::engine_result] == "tesSUCCESS"); - BEAST_EXPECT(result[jss::engine_result_code] == 0); - BEAST_EXPECT( - result[jss::engine_result_message] == - "The simulated transaction would have been applied."); - - if (BEAST_EXPECT( - result.isMember(jss::meta) || - result.isMember(jss::meta_blob))) - { - Json::Value const metadata = getJsonMetadata(result); - - BEAST_EXPECT(metadata[sfTransactionIndex.jsonName] == 0); + BEAST_EXPECT(result[jss::engine_result] == "tesSUCCESS"); + BEAST_EXPECT(result[jss::engine_result_code] == 0); BEAST_EXPECT( - metadata[sfTransactionResult.jsonName] == "tesSUCCESS"); - BEAST_EXPECT( - metadata.isMember(expectedMetadataKey.asString())); - } - }; + result[jss::engine_result_message] == + "The simulated transaction would have been applied."); + + if (BEAST_EXPECT( + result.isMember(jss::meta) || + result.isMember(jss::meta_blob))) + { + Json::Value const metadata = getJsonMetadata(result); + + BEAST_EXPECT( + metadata[sfTransactionIndex.jsonName] == 0); + BEAST_EXPECT( + metadata[sfTransactionResult.jsonName] == + "tesSUCCESS"); + BEAST_EXPECT( + metadata.isMember(expectedMetadataKey.asString())); + BEAST_EXPECT( + metadata[expectedMetadataKey.asString()] == + expectedMetadataValue); + } + }; { Json::Value tx; - tx[jss::Account] = env.master.human(); + tx[jss::Account] = alice.human(); tx[jss::TransactionType] = jss::Payment; - tx[sfDestination] = alice.human(); + tx[sfDestination] = bob.human(); tx[sfAmount] = "100"; // test delivered amount testTxJsonMetadataField( - env, tx, validateOutput, jss::delivered_amount); + env, tx, validateOutput, jss::delivered_amount, "100"); } { Json::Value tx; - tx[jss::Account] = env.master.human(); + tx[jss::Account] = alice.human(); tx[jss::TransactionType] = jss::NFTokenMint; tx[sfNFTokenTaxon] = 1; + Json::Value nftokenId = + to_string(token::getNextID(env, alice, 1)); // test nft synthetic testTxJsonMetadataField( - env, tx, validateOutput, jss::nftoken_id); + env, tx, validateOutput, jss::nftoken_id, nftokenId); } { Json::Value tx; - tx[jss::Account] = env.master.human(); + tx[jss::Account] = alice.human(); tx[jss::TransactionType] = jss::MPTokenIssuanceCreate; + Json::Value mptIssuanceId = + to_string(makeMptID(env.seq(alice), alice)); // test mpt issuance id testTxJsonMetadataField( - env, tx, validateOutput, jss::mpt_issuance_id); + env, + tx, + validateOutput, + jss::mpt_issuance_id, + mptIssuanceId); } } } diff --git a/src/xrpld/app/ledger/Ledger.cpp b/src/xrpld/app/ledger/Ledger.cpp index 6de4f2cbde..64a6b16cbc 100644 --- a/src/xrpld/app/ledger/Ledger.cpp +++ b/src/xrpld/app/ledger/Ledger.cpp @@ -433,8 +433,10 @@ Ledger::read(Keylet const& k) const { if (k.key == beast::zero) { + // LCOV_EXCL_START UNREACHABLE("ripple::Ledger::read : zero key"); return nullptr; + // LCOV_EXCL_STOP } auto const& item = stateMap_.peekItem(k.key); if (!item) @@ -860,6 +862,7 @@ Ledger::assertSensible(beast::Journal ledgerJ) const return true; } + // LCOV_EXCL_START Json::Value j = getJson({*this, {}}); j[jss::accountTreeHash] = to_string(info_.accountHash); @@ -870,6 +873,7 @@ Ledger::assertSensible(beast::Journal ledgerJ) const UNREACHABLE("ripple::Ledger::assertSensible : ledger is not sensible"); return false; + // LCOV_EXCL_STOP } // update the skip list with the information from our previous ledger diff --git a/src/xrpld/app/ledger/detail/InboundLedger.cpp b/src/xrpld/app/ledger/detail/InboundLedger.cpp index eafa939506..47d546c3af 100644 --- a/src/xrpld/app/ledger/detail/InboundLedger.cpp +++ b/src/xrpld/app/ledger/detail/InboundLedger.cpp @@ -963,8 +963,10 @@ InboundLedger::takeAsRootNode(Slice const& data, SHAMapAddNode& san) if (!mHaveHeader) { + // LCOV_EXCL_START UNREACHABLE("ripple::InboundLedger::takeAsRootNode : no ledger header"); return false; + // LCOV_EXCL_STOP } AccountStateSF filter( @@ -988,8 +990,10 @@ InboundLedger::takeTxRootNode(Slice const& data, SHAMapAddNode& san) if (!mHaveHeader) { + // LCOV_EXCL_START UNREACHABLE("ripple::InboundLedger::takeTxRootNode : no ledger header"); return false; + // LCOV_EXCL_STOP } TransactionStateSF filter( diff --git a/src/xrpld/app/ledger/detail/LedgerMaster.cpp b/src/xrpld/app/ledger/detail/LedgerMaster.cpp index 78f0375b16..f39e8af0b6 100644 --- a/src/xrpld/app/ledger/detail/LedgerMaster.cpp +++ b/src/xrpld/app/ledger/detail/LedgerMaster.cpp @@ -1273,11 +1273,13 @@ LedgerMaster::findNewLedgersToPublish( } else if (hash->isZero()) { + // LCOV_EXCL_START JLOG(m_journal.fatal()) << "Ledger: " << valSeq << " does not have hash for " << seq; UNREACHABLE( "ripple::LedgerMaster::findNewLedgersToPublish : ledger " "not found"); + // LCOV_EXCL_STOP } else { diff --git a/src/xrpld/app/main/Application.cpp b/src/xrpld/app/main/Application.cpp index 05b8f5e5fa..616afc957d 100644 --- a/src/xrpld/app/main/Application.cpp +++ b/src/xrpld/app/main/Application.cpp @@ -1994,11 +1994,13 @@ ApplicationImp::loadOldLedger( if (!loadLedger) { + // LCOV_EXCL_START JLOG(m_journal.fatal()) << "Replay ledger missing/damaged"; UNREACHABLE( "ripple::ApplicationImp::loadOldLedger : replay ledger " "missing/damaged"); return false; + // LCOV_EXCL_STOP } } } @@ -2025,28 +2027,34 @@ ApplicationImp::loadOldLedger( if (loadLedger->info().accountHash.isZero()) { + // LCOV_EXCL_START JLOG(m_journal.fatal()) << "Ledger is empty."; UNREACHABLE( "ripple::ApplicationImp::loadOldLedger : ledger is empty"); return false; + // LCOV_EXCL_STOP } if (!loadLedger->walkLedger(journal("Ledger"), true)) { + // LCOV_EXCL_START JLOG(m_journal.fatal()) << "Ledger is missing nodes."; UNREACHABLE( "ripple::ApplicationImp::loadOldLedger : ledger is missing " "nodes"); return false; + // LCOV_EXCL_STOP } if (!loadLedger->assertSensible(journal("Ledger"))) { + // LCOV_EXCL_START JLOG(m_journal.fatal()) << "Ledger is not sensible."; UNREACHABLE( "ripple::ApplicationImp::loadOldLedger : ledger is not " "sensible"); return false; + // LCOV_EXCL_STOP } m_ledgerMaster->setLedgerRangePresent( diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index 3334052d4a..f7875b1bf3 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -1799,11 +1799,13 @@ NetworkOPsImp::getOwnerInfo( case ltACCOUNT_ROOT: case ltDIR_NODE: + // LCOV_EXCL_START default: UNREACHABLE( "ripple::NetworkOPsImp::getOwnerInfo : invalid " "type"); break; + // LCOV_EXCL_STOP } } @@ -3834,12 +3836,14 @@ NetworkOPsImp::addAccountHistoryJob(SubAccountHistoryInfoWeak subInfo) accountId, minLedger, maxLedger, marker, 0, true}; return db->newestAccountTxPage(options); } + // LCOV_EXCL_START default: { UNREACHABLE( "ripple::NetworkOPsImp::addAccountHistoryJob::" "getMoreTxns : invalid database type"); return {}; } + // LCOV_EXCL_STOP } }; @@ -4033,10 +4037,12 @@ NetworkOPsImp::subAccountHistoryStart( } else { + // LCOV_EXCL_START UNREACHABLE( "ripple::NetworkOPsImp::subAccountHistoryStart : failed to " "access genesis account"); return; + // LCOV_EXCL_STOP } } subInfo.index_->historyLastLedgerSeq_ = ledger->seq(); @@ -4143,7 +4149,11 @@ NetworkOPsImp::subBook(InfoSub::ref isrListener, Book const& book) if (auto listeners = app_.getOrderBookDB().makeBookListeners(book)) listeners->addSubscriber(isrListener); else + { + // LCOV_EXCL_START UNREACHABLE("ripple::NetworkOPsImp::subBook : null book listeners"); + // LCOV_EXCL_STOP + } return true; } diff --git a/src/xrpld/app/misc/detail/ValidatorList.cpp b/src/xrpld/app/misc/detail/ValidatorList.cpp index 2b45cec3be..92095b7211 100644 --- a/src/xrpld/app/misc/detail/ValidatorList.cpp +++ b/src/xrpld/app/misc/detail/ValidatorList.cpp @@ -1167,15 +1167,17 @@ ValidatorList::applyList( } if (!publicKeyType(*pubKeyOpt)) - { // LCOV_EXCL_START - // This is an impossible situation because we will never load an - // invalid public key type (see checks in `ValidatorList::load`) however - // we can only arrive here if the key used by the manifest matched one of - // the loaded keys + { + // This is an impossible situation because we will never load an + // invalid public key type (see checks in `ValidatorList::load`) however + // we can only arrive here if the key used by the manifest matched one + // of the loaded keys + // LCOV_EXCL_START UNREACHABLE( "ripple::ValidatorList::applyList : invalid public key type"); return PublisherListStats{result}; - } // LCOV_EXCL_STOP + // LCOV_EXCL_STOP + } PublicKey pubKey = *pubKeyOpt; if (result > ListDisposition::pending) diff --git a/src/xrpld/app/paths/Pathfinder.cpp b/src/xrpld/app/paths/Pathfinder.cpp index 41a3697888..4bc9304853 100644 --- a/src/xrpld/app/paths/Pathfinder.cpp +++ b/src/xrpld/app/paths/Pathfinder.cpp @@ -648,8 +648,10 @@ Pathfinder::getBestPaths( if (path.empty()) { + // LCOV_EXCL_START UNREACHABLE("ripple::Pathfinder::getBestPaths : path not found"); continue; + // LCOV_EXCL_STOP } bool startsWithIssuer = false; diff --git a/src/xrpld/app/paths/detail/BookStep.cpp b/src/xrpld/app/paths/detail/BookStep.cpp index 97cf87c046..54d0d8d0c9 100644 --- a/src/xrpld/app/paths/detail/BookStep.cpp +++ b/src/xrpld/app/paths/detail/BookStep.cpp @@ -1113,11 +1113,13 @@ BookStep::revImp( { case -1: { // something went very wrong + // LCOV_EXCL_START JLOG(j_.error()) << "BookStep remainingOut < 0 " << to_string(remainingOut); UNREACHABLE("ripple::BookStep::revImp : remaining less than zero"); cache_.emplace(beast::zero, beast::zero); return {beast::zero, beast::zero}; + // LCOV_EXCL_STOP } case 0: { // due to normalization, remainingOut can be zero without @@ -1283,12 +1285,14 @@ BookStep::fwdImp( switch (remainingIn.signum()) { case -1: { + // LCOV_EXCL_START // something went very wrong JLOG(j_.error()) << "BookStep remainingIn < 0 " << to_string(remainingIn); UNREACHABLE("ripple::BookStep::fwdImp : remaining less than zero"); cache_.emplace(beast::zero, beast::zero); return {beast::zero, beast::zero}; + // LCOV_EXCL_STOP } case 0: { // due to normalization, remainingIn can be zero without @@ -1421,8 +1425,10 @@ bookStepEqual(Step const& step, ripple::Book const& book) bool const outXRP = isXRP(book.out.currency); if (inXRP && outXRP) { + // LCOV_EXCL_START UNREACHABLE("ripple::test::bookStepEqual : no XRP to XRP book step"); return false; // no such thing as xrp/xrp book step + // LCOV_EXCL_STOP } if (inXRP && !outXRP) return equalHelper< diff --git a/src/xrpld/app/paths/detail/DirectStep.cpp b/src/xrpld/app/paths/detail/DirectStep.cpp index 03d207e008..a0808985b5 100644 --- a/src/xrpld/app/paths/detail/DirectStep.cpp +++ b/src/xrpld/app/paths/detail/DirectStep.cpp @@ -931,10 +931,12 @@ DirectStepI::check(StrandContext const& ctx) const { if (!ctx.prevStep) { + // LCOV_EXCL_START UNREACHABLE( "ripple::DirectStepI::check : prev seen book without a " "prev step"); return temBAD_PATH_LOOP; + // LCOV_EXCL_STOP } // This is OK if the previous step is a book step that outputs this diff --git a/src/xrpld/app/paths/detail/FlowDebugInfo.h b/src/xrpld/app/paths/detail/FlowDebugInfo.h index eec1d7c5a6..290a201e68 100644 --- a/src/xrpld/app/paths/detail/FlowDebugInfo.h +++ b/src/xrpld/app/paths/detail/FlowDebugInfo.h @@ -126,10 +126,12 @@ struct FlowDebugInfo auto i = timePoints.find(tag); if (i == timePoints.end()) { + // LCOV_EXCL_START UNREACHABLE( "ripple::path::detail::FlowDebugInfo::duration : timepoint not " "found"); return std::chrono::duration(0); + // LCOV_EXCL_STOP } auto const& t = i->second; return std::chrono::duration_cast>( diff --git a/src/xrpld/app/paths/detail/PaySteps.cpp b/src/xrpld/app/paths/detail/PaySteps.cpp index 6eb38eee83..f5621bcdf7 100644 --- a/src/xrpld/app/paths/detail/PaySteps.cpp +++ b/src/xrpld/app/paths/detail/PaySteps.cpp @@ -95,11 +95,13 @@ toStep( if (e1->isOffer() && e2->isAccount()) { + // LCOV_EXCL_START // should already be taken care of JLOG(j.error()) << "Found offer/account payment step. Aborting payment strand."; UNREACHABLE("ripple::toStep : offer/account payment payment strand"); return {temBAD_PATH, std::unique_ptr{}}; + // LCOV_EXCL_STOP } XRPL_ASSERT( @@ -392,8 +394,10 @@ toStrand( next->getCurrency() != curIssue.currency) { // Should never happen + // LCOV_EXCL_START UNREACHABLE("ripple::toStrand : offer currency mismatch"); return {temBAD_PATH, Strand{}}; + // LCOV_EXCL_STOP } auto s = toStep( @@ -457,9 +461,11 @@ toStrand( if (!checkStrand()) { + // LCOV_EXCL_START JLOG(j.warn()) << "Flow check strand failed"; UNREACHABLE("ripple::toStrand : invalid strand"); return {temBAD_PATH, Strand{}}; + // LCOV_EXCL_STOP } return {tesSUCCESS, std::move(result)}; diff --git a/src/xrpld/app/paths/detail/StrandFlow.h b/src/xrpld/app/paths/detail/StrandFlow.h index 47037c5f5e..7ccad9bf4f 100644 --- a/src/xrpld/app/paths/detail/StrandFlow.h +++ b/src/xrpld/app/paths/detail/StrandFlow.h @@ -167,6 +167,7 @@ flow( // Something is very wrong // throwing out the sandbox can only increase liquidity // yet the limiting is still limiting + // LCOV_EXCL_START JLOG(j.fatal()) << "Re-executed limiting step failed. r.first: " << to_string(get(r.first)) @@ -175,6 +176,7 @@ flow( "ripple::flow : first step re-executing the " "limiting step failed"); return Result{strand, std::move(ofrsToRm)}; + // LCOV_EXCL_STOP } } else if (!strand[i]->equalOut(r.second, stepOut)) @@ -202,6 +204,7 @@ flow( // Something is very wrong // throwing out the sandbox can only increase liquidity // yet the limiting is still limiting + // LCOV_EXCL_START #ifndef NDEBUG JLOG(j.fatal()) << "Re-executed limiting step failed. r.second: " @@ -213,6 +216,7 @@ flow( "ripple::flow : limiting step re-executing the " "limiting step failed"); return Result{strand, std::move(ofrsToRm)}; + // LCOV_EXCL_STOP } } @@ -238,6 +242,7 @@ flow( // The limits should already have been found, so executing a // strand forward from the limiting step should not find a // new limit + // LCOV_EXCL_START #ifndef NDEBUG JLOG(j.fatal()) << "Re-executed forward pass failed. r.first: " @@ -249,6 +254,7 @@ flow( "ripple::flow : non-limiting step re-executing the " "forward pass failed"); return Result{strand, std::move(ofrsToRm)}; + // LCOV_EXCL_STOP } stepIn = r.second; } @@ -499,8 +505,10 @@ public: { if (i >= cur_.size()) { + // LCOV_EXCL_START UNREACHABLE("ripple::ActiveStrands::get : input out of range"); return nullptr; + // LCOV_EXCL_STOP } return cur_[i]; } diff --git a/src/xrpld/app/rdb/RelationalDatabase.h b/src/xrpld/app/rdb/RelationalDatabase.h index 25b16f04a1..1a5b2ba830 100644 --- a/src/xrpld/app/rdb/RelationalDatabase.h +++ b/src/xrpld/app/rdb/RelationalDatabase.h @@ -235,12 +235,14 @@ rangeCheckedCast(C c) std::numeric_limits::is_signed && c < std::numeric_limits::lowest())) { - /* This should never happen */ + // This should never happen + // LCOV_EXCL_START UNREACHABLE("ripple::rangeCheckedCast : domain error"); JLOG(debugLog().error()) << "rangeCheckedCast domain error:" << " value = " << c << " min = " << std::numeric_limits::lowest() << " max: " << std::numeric_limits::max(); + // LCOV_EXCL_STOP } return static_cast(c); diff --git a/src/xrpld/app/rdb/backend/detail/Node.cpp b/src/xrpld/app/rdb/backend/detail/Node.cpp index 6a0544091b..88f11a272b 100644 --- a/src/xrpld/app/rdb/backend/detail/Node.cpp +++ b/src/xrpld/app/rdb/backend/detail/Node.cpp @@ -58,9 +58,11 @@ to_string(TableType type) return "Transactions"; case TableType::AccountTransactions: return "AccountTransactions"; + // LCOV_EXCL_START default: UNREACHABLE("ripple::detail::to_string : invalid TableType"); return "Unknown"; + // LCOV_EXCL_STOP } } @@ -202,18 +204,22 @@ saveValidatedLedger( if (!ledger->info().accountHash.isNonZero()) { + // LCOV_EXCL_START JLOG(j.fatal()) << "AH is zero: " << getJson({*ledger, {}}); UNREACHABLE("ripple::detail::saveValidatedLedger : zero account hash"); + // LCOV_EXCL_STOP } if (ledger->info().accountHash != ledger->stateMap().getHash().as_uint256()) { + // LCOV_EXCL_START JLOG(j.fatal()) << "sAL: " << ledger->info().accountHash << " != " << ledger->stateMap().getHash(); JLOG(j.fatal()) << "saveAcceptedLedger: seq=" << seq << ", current=" << current; UNREACHABLE( "ripple::detail::saveValidatedLedger : mismatched account hash"); + // LCOV_EXCL_STOP } XRPL_ASSERT( diff --git a/src/xrpld/app/tx/detail/Change.cpp b/src/xrpld/app/tx/detail/Change.cpp index aff76ba71a..f78eaf0f61 100644 --- a/src/xrpld/app/tx/detail/Change.cpp +++ b/src/xrpld/app/tx/detail/Change.cpp @@ -155,9 +155,11 @@ Change::doApply() return applyFee(); case ttUNL_MODIFY: return applyUNLModify(); + // LCOV_EXCL_START default: UNREACHABLE("ripple::Change::doApply : invalid transaction type"); return tefFAILURE; + // LCOV_EXCL_STOP } } diff --git a/src/xrpld/app/tx/detail/DeleteAccount.cpp b/src/xrpld/app/tx/detail/DeleteAccount.cpp index 565d938c83..d52e84d755 100644 --- a/src/xrpld/app/tx/detail/DeleteAccount.cpp +++ b/src/xrpld/app/tx/detail/DeleteAccount.cpp @@ -399,12 +399,14 @@ DeleteAccount::doApply() return {result, SkipEntry::No}; } + // LCOV_EXCL_START UNREACHABLE( "ripple::DeleteAccount::doApply : undeletable item not found " "in preclaim"); JLOG(j_.error()) << "DeleteAccount undeletable item not " "found in preclaim."; return {tecHAS_OBLIGATIONS, SkipEntry::No}; + // LCOV_EXCL_STOP }, ctx_.journal); if (ter != tesSUCCESS) diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 20a647821f..3495cc3a02 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -24,17 +24,26 @@ #include #include +#include #include #include #include #include +#include +#include +#include +#include #include #include #include +#include #include #include #include +#include +#include + namespace ripple { /* @@ -78,6 +87,8 @@ enum Privilege { // object (except by issuer) mayDeleteMPT = 0x0400, // The transaction MAY delete an MPT object. May not create. + mustModifyVault = + 0x0800, // The transaction must modify, delete or create, a vault }; constexpr Privilege operator|(Privilege lhs, Privilege rhs) @@ -2482,4 +2493,941 @@ ValidLoan::finalize( return true; } +ValidVault::Vault +ValidVault::Vault::make(SLE const& from) +{ + XRPL_ASSERT( + from.getType() == ltVAULT, + "ValidVault::Vault::make : from Vault object"); + + ValidVault::Vault self; + self.key = from.key(); + self.asset = from.at(sfAsset); + self.pseudoId = from.getAccountID(sfAccount); + self.shareMPTID = from.getFieldH192(sfShareMPTID); + self.assetsTotal = from.at(sfAssetsTotal); + self.assetsAvailable = from.at(sfAssetsAvailable); + self.assetsMaximum = from.at(sfAssetsMaximum); + self.lossUnrealized = from.at(sfLossUnrealized); + return self; +} + +ValidVault::Shares +ValidVault::Shares::make(SLE const& from) +{ + XRPL_ASSERT( + from.getType() == ltMPTOKEN_ISSUANCE, + "ValidVault::Shares::make : from MPTokenIssuance object"); + + ValidVault::Shares self; + self.share = MPTIssue( + makeMptID(from.getFieldU32(sfSequence), from.getAccountID(sfIssuer))); + self.sharesTotal = from.at(sfOutstandingAmount); + self.sharesMaximum = from[~sfMaximumAmount].value_or(maxMPTokenAmount); + return self; +} + +void +ValidVault::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + // If `before` is empty, this means an object is being created, in which + // case `isDelete` must be false. Otherwise `before` and `after` are set and + // `isDelete` indicates whether an object is being deleted or modified. + XRPL_ASSERT( + after != nullptr && (before != nullptr || !isDelete), + "ripple::ValidVault::visitEntry : some object is available"); + + // `Number balance` will capture the difference (delta) between "before" + // state (zero if created) and "after" state (zero if destroyed), so the + // invariants can validate that the change in account balances matches the + // change in vault balances, stored to deltas_ at the end of this function. + Number balance{}; + + // By default do not add anything to deltas + std::int8_t sign = 0; + if (before) + { + switch (before->getType()) + { + case ltVAULT: + beforeVault_.push_back(Vault::make(*before)); + break; + case ltMPTOKEN_ISSUANCE: + // At this moment we have no way of telling if this object holds + // vault shares or something else. Save it for finalize. + beforeMPTs_.push_back(Shares::make(*before)); + balance = static_cast( + before->getFieldU64(sfOutstandingAmount)); + sign = 1; + break; + case ltMPTOKEN: + balance = + static_cast(before->getFieldU64(sfMPTAmount)); + sign = -1; + break; + case ltACCOUNT_ROOT: + case ltRIPPLE_STATE: + balance = before->getFieldAmount(sfBalance); + sign = -1; + break; + default:; + } + } + + if (!isDelete && after) + { + switch (after->getType()) + { + case ltVAULT: + afterVault_.push_back(Vault::make(*after)); + break; + case ltMPTOKEN_ISSUANCE: + // At this moment we have no way of telling if this object holds + // vault shares or something else. Save it for finalize. + afterMPTs_.push_back(Shares::make(*after)); + balance -= Number(static_cast( + after->getFieldU64(sfOutstandingAmount))); + sign = 1; + break; + case ltMPTOKEN: + balance -= Number( + static_cast(after->getFieldU64(sfMPTAmount))); + sign = -1; + break; + case ltACCOUNT_ROOT: + case ltRIPPLE_STATE: + balance -= Number(after->getFieldAmount(sfBalance)); + sign = -1; + break; + default:; + } + } + + uint256 const key = (before ? before->key() : after->key()); + if (sign && balance != zero) + deltas_[key] = balance * sign; +} + +bool +ValidVault::finalize( + STTx const& tx, + TER const ret, + XRPAmount const fee, + ReadView const& view, + beast::Journal const& j) +{ + bool const enforce = view.rules().enabled(featureSingleAssetVault); + + if (!isTesSuccess(ret)) + return true; // Do not perform checks + + if (afterVault_.empty() && beforeVault_.empty()) + { + if (hasPrivilege(tx, mustModifyVault)) + { + JLOG(j.fatal()) << // + "Invariant failed: vault operation succeeded without modifying " + "a vault"; + XRPL_ASSERT( + enforce, "ripple::ValidVault::finalize : vault noop invariant"); + return !enforce; + } + + return true; // Not a vault operation + } + else if (!hasPrivilege(tx, mustModifyVault)) // TODO: mayModifyVault + { + JLOG(j.fatal()) << // + "Invariant failed: vault updated by a wrong transaction type"; + XRPL_ASSERT( + enforce, + "ripple::ValidVault::finalize : illegal vault transaction " + "invariant"); + return !enforce; // Also not a vault operation + } + + if (beforeVault_.size() > 1 || afterVault_.size() > 1) + { + JLOG(j.fatal()) << // + "Invariant failed: vault operation updated more than single vault"; + XRPL_ASSERT( + enforce, "ripple::ValidVault::finalize : single vault invariant"); + return !enforce; // That's all we can do here + } + + auto const txnType = tx.getTxnType(); + + // We do special handling for ttVAULT_DELETE first, because it's the only + // vault-modifying transaction without an "after" state of the vault + if (afterVault_.empty()) + { + if (txnType != ttVAULT_DELETE) + { + JLOG(j.fatal()) << // + "Invariant failed: vault deleted by a wrong transaction type"; + XRPL_ASSERT( + enforce, + "ripple::ValidVault::finalize : illegal vault deletion " + "invariant"); + return !enforce; // That's all we can do here + } + + // Note, if afterVault_ is empty then we know that beforeVault_ is not + // empty, as enforced at the top of this function + auto const& beforeVault = beforeVault_[0]; + + // At this moment we only know a vault is being deleted and there + // might be some MPTokenIssuance objects which are deleted in the + // same transaction. Find the one matching this vault. + auto const deletedShares = [&]() -> std::optional { + for (auto const& e : beforeMPTs_) + { + if (e.share.getMptID() == beforeVault.shareMPTID) + return std::move(e); + } + return std::nullopt; + }(); + + if (!deletedShares) + { + JLOG(j.fatal()) << "Invariant failed: deleted vault must also " + "delete shares"; + XRPL_ASSERT( + enforce, + "ripple::ValidVault::finalize : shares deletion invariant"); + return !enforce; // That's all we can do here + } + + bool result = true; + if (deletedShares->sharesTotal != 0) + { + JLOG(j.fatal()) << "Invariant failed: deleted vault must have no " + "shares outstanding"; + result = false; + } + if (beforeVault.assetsTotal != zero) + { + JLOG(j.fatal()) << "Invariant failed: deleted vault must have no " + "assets outstanding"; + result = false; + } + if (beforeVault.assetsAvailable != zero) + { + JLOG(j.fatal()) << "Invariant failed: deleted vault must have no " + "assets available"; + result = false; + } + + return result; + } + else if (txnType == ttVAULT_DELETE) + { + JLOG(j.fatal()) << "Invariant failed: vault deletion succeeded without " + "deleting a vault"; + XRPL_ASSERT( + enforce, "ripple::ValidVault::finalize : vault deletion invariant"); + return !enforce; // That's all we can do here + } + + // Note, `afterVault_.empty()` is handled above + auto const& afterVault = afterVault_[0]; + XRPL_ASSERT( + beforeVault_.empty() || beforeVault_[0].key == afterVault.key, + "ripple::ValidVault::finalize : single vault operation"); + + auto const updatedShares = [&]() -> std::optional { + // At this moment we only know that a vault is being updated and there + // might be some MPTokenIssuance objects which are also updated in the + // same transaction. Find the one matching the shares to this vault. + // Note, we expect updatedMPTs collection to be extremely small. For + // such collections linear search is faster than lookup. + for (auto const& e : afterMPTs_) + { + if (e.share.getMptID() == afterVault.shareMPTID) + return e; + } + + auto const sleShares = + view.read(keylet::mptIssuance(afterVault.shareMPTID)); + + return sleShares ? std::optional(Shares::make(*sleShares)) + : std::nullopt; + }(); + + bool result = true; + + // Universal transaction checks + if (!beforeVault_.empty()) + { + auto const& beforeVault = beforeVault_[0]; + if (afterVault.asset != beforeVault.asset || + afterVault.pseudoId != beforeVault.pseudoId || + afterVault.shareMPTID != beforeVault.shareMPTID) + { + JLOG(j.fatal()) + << "Invariant failed: violation of vault immutable data"; + result = false; + } + } + + if (!updatedShares) + { + JLOG(j.fatal()) << "Invariant failed: updated vault must have shares"; + XRPL_ASSERT( + enforce, + "ripple::ValidVault::finalize : vault has shares invariant"); + return !enforce; // That's all we can do here + } + + if (updatedShares->sharesTotal == 0) + { + if (afterVault.assetsTotal != zero) + { + JLOG(j.fatal()) << "Invariant failed: updated zero sized " + "vault must have no assets outstanding"; + result = false; + } + if (afterVault.assetsAvailable != zero) + { + JLOG(j.fatal()) << "Invariant failed: updated zero sized " + "vault must have no assets available"; + result = false; + } + } + else if (updatedShares->sharesTotal > updatedShares->sharesMaximum) + { + JLOG(j.fatal()) // + << "Invariant failed: updated shares must not exceed maximum " + << updatedShares->sharesMaximum; + result = false; + } + + if (afterVault.assetsAvailable < zero) + { + JLOG(j.fatal()) + << "Invariant failed: assets available must be positive"; + result = false; + } + + if (afterVault.assetsAvailable > afterVault.assetsTotal) + { + JLOG(j.fatal()) << "Invariant failed: assets available must " + "not be greater than assets outstanding"; + result = false; + } + else if ( + afterVault.lossUnrealized > + afterVault.assetsTotal - afterVault.assetsAvailable) + { + JLOG(j.fatal()) // + << "Invariant failed: loss unrealized must not exceed " + "the difference between assets outstanding and available"; + result = false; + } + + if (afterVault.assetsTotal < zero) + { + JLOG(j.fatal()) + << "Invariant failed: assets outstanding must be positive"; + result = false; + } + + if (afterVault.assetsMaximum < zero) + { + JLOG(j.fatal()) << "Invariant failed: assets maximum must be positive"; + result = false; + } + + // Thanks to this check we can simply do `assert(!beforeVault_.empty()` when + // enforcing invariants on transaction types other than ttVAULT_CREATE + if (beforeVault_.empty() && txnType != ttVAULT_CREATE) + { + JLOG(j.fatal()) << // + "Invariant failed: vault created by a wrong transaction type"; + XRPL_ASSERT( + enforce, "ripple::ValidVault::finalize : vault creation invariant"); + return !enforce; // That's all we can do here + } + + if (!beforeVault_.empty() && + afterVault.lossUnrealized != beforeVault_[0].lossUnrealized) + { + JLOG(j.fatal()) << // + "Invariant failed: vault transaction must not change loss " + "unrealized"; + result = false; + } + + auto const beforeShares = [&]() -> std::optional { + if (beforeVault_.empty()) + return std::nullopt; + auto const& beforeVault = beforeVault_[0]; + + for (auto const& e : beforeMPTs_) + { + if (e.share.getMptID() == beforeVault.shareMPTID) + return std::move(e); + } + return std::nullopt; + }(); + + if (!beforeShares && + (tx.getTxnType() == ttVAULT_DEPOSIT || // + tx.getTxnType() == ttVAULT_WITHDRAW || // + tx.getTxnType() == ttVAULT_CLAWBACK)) + { + JLOG(j.fatal()) << "Invariant failed: vault operation succeeded " + "without updating shares"; + XRPL_ASSERT( + enforce, "ripple::ValidVault::finalize : shares noop invariant"); + return !enforce; // That's all we can do here + } + + auto const& vaultAsset = afterVault.asset; + auto const deltaAssets = [&](AccountID const& id) -> std::optional { + auto const get = // + [&](auto const& it, std::int8_t sign = 1) -> std::optional { + if (it == deltas_.end()) + return std::nullopt; + + return it->second * sign; + }; + + return std::visit( + [&](TIss const& issue) { + if constexpr (std::is_same_v) + { + if (isXRP(issue)) + return get(deltas_.find(keylet::account(id).key)); + return get( + deltas_.find(keylet::line(id, issue).key), + id > issue.getIssuer() ? -1 : 1); + } + else if constexpr (std::is_same_v) + { + return get(deltas_.find( + keylet::mptoken(issue.getMptID(), id).key)); + } + }, + vaultAsset.value()); + }; + auto const deltaShares = [&](AccountID const& id) -> std::optional { + auto const it = [&]() { + if (id == afterVault.pseudoId) + return deltas_.find( + keylet::mptIssuance(afterVault.shareMPTID).key); + return deltas_.find(keylet::mptoken(afterVault.shareMPTID, id).key); + }(); + + return it != deltas_.end() ? std::optional(it->second) + : std::nullopt; + }; + + // Technically this does not need to be a lambda, but it's more + // convenient thanks to early "return false"; the not-so-nice + // alternatives are several layers of nested if/else or more complex + // (i.e. brittle) if statements. + result &= [&]() { + switch (txnType) + { + case ttVAULT_CREATE: { + bool result = true; + + if (!beforeVault_.empty()) + { + JLOG(j.fatal()) // + << "Invariant failed: create operation must not have " + "updated a vault"; + result = false; + } + + if (afterVault.assetsAvailable != zero || + afterVault.assetsTotal != zero || + afterVault.lossUnrealized != zero || + updatedShares->sharesTotal != 0) + { + JLOG(j.fatal()) // + << "Invariant failed: created vault must be empty"; + result = false; + } + + if (afterVault.pseudoId != updatedShares->share.getIssuer()) + { + JLOG(j.fatal()) // + << "Invariant failed: shares issuer and vault " + "pseudo-account must be the same"; + result = false; + } + + auto const sleSharesIssuer = view.read( + keylet::account(updatedShares->share.getIssuer())); + if (!sleSharesIssuer) + { + JLOG(j.fatal()) // + << "Invariant failed: shares issuer must exist"; + return false; + } + + if (!isPseudoAccount(sleSharesIssuer)) + { + JLOG(j.fatal()) // + << "Invariant failed: shares issuer must be a " + "pseudo-account"; + result = false; + } + + if (auto const vaultId = (*sleSharesIssuer)[~sfVaultID]; + !vaultId || *vaultId != afterVault.key) + { + JLOG(j.fatal()) // + << "Invariant failed: shares issuer pseudo-account " + "must point back to the vault"; + result = false; + } + + return result; + } + case ttVAULT_SET: { + bool result = true; + + XRPL_ASSERT( + !beforeVault_.empty(), + "ripple::ValidVault::finalize : set updated a vault"); + auto const& beforeVault = beforeVault_[0]; + + auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId); + if (vaultDeltaAssets) + { + JLOG(j.fatal()) << // + "Invariant failed: set must not change vault balance"; + result = false; + } + + if (beforeVault.assetsTotal != afterVault.assetsTotal) + { + JLOG(j.fatal()) << // + "Invariant failed: set must not change assets " + "outstanding"; + result = false; + } + + if (afterVault.assetsMaximum > zero && + afterVault.assetsTotal > afterVault.assetsMaximum) + { + JLOG(j.fatal()) << // + "Invariant failed: set assets outstanding must not " + "exceed assets maximum"; + result = false; + } + + if (beforeVault.assetsAvailable != afterVault.assetsAvailable) + { + JLOG(j.fatal()) << // + "Invariant failed: set must not change assets " + "available"; + result = false; + } + + if (beforeShares && updatedShares && + beforeShares->sharesTotal != updatedShares->sharesTotal) + { + JLOG(j.fatal()) << // + "Invariant failed: set must not change shares " + "outstanding"; + result = false; + } + + return result; + } + case ttVAULT_DEPOSIT: { + bool result = true; + + XRPL_ASSERT( + !beforeVault_.empty(), + "ripple::ValidVault::finalize : deposit updated a vault"); + auto const& beforeVault = beforeVault_[0]; + + auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId); + + if (!vaultDeltaAssets) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must change vault balance"; + return false; // That's all we can do + } + + if (*vaultDeltaAssets > tx[sfAmount]) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must not change vault " + "balance by more than deposited amount"; + result = false; + } + + if (*vaultDeltaAssets <= zero) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must increase vault balance"; + result = false; + } + + // Any payments (including deposits) made by the issuer + // do not change their balance, but create funds instead. + bool const issuerDeposit = [&]() -> bool { + if (vaultAsset.native()) + return false; + return tx[sfAccount] == vaultAsset.getIssuer(); + }(); + + if (!issuerDeposit) + { + auto const accountDeltaAssets = + [&]() -> std::optional { + if (auto ret = deltaAssets(tx[sfAccount]); ret) + { + // Compensate for transaction fee deduced from + // sfAccount + if (vaultAsset.native()) + *ret += fee.drops(); + if (*ret != zero) + return ret; + } + return std::nullopt; + }(); + + if (!accountDeltaAssets) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must change depositor " + "balance"; + return false; + } + + if (*accountDeltaAssets >= zero) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must decrease depositor " + "balance"; + result = false; + } + + if (*accountDeltaAssets * -1 != *vaultDeltaAssets) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must change vault and " + "depositor balance by equal amount"; + result = false; + } + } + + if (afterVault.assetsMaximum > zero && + afterVault.assetsTotal > afterVault.assetsMaximum) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit assets outstanding must not " + "exceed assets maximum"; + result = false; + } + + auto const accountDeltaShares = deltaShares(tx[sfAccount]); + if (!accountDeltaShares) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must change depositor " + "shares"; + return false; // That's all we can do + } + + if (*accountDeltaShares <= zero) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must increase depositor " + "shares"; + result = false; + } + + auto const vaultDeltaShares = deltaShares(afterVault.pseudoId); + if (!vaultDeltaShares) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must change vault shares"; + return false; // That's all we can do + } + + if (*vaultDeltaShares * -1 != *accountDeltaShares) + { + JLOG(j.fatal()) << // + "Invariant failed: deposit must change depositor and " + "vault shares by equal amount"; + result = false; + } + + if (beforeVault.assetsTotal + *vaultDeltaAssets != + afterVault.assetsTotal) + { + JLOG(j.fatal()) << "Invariant failed: deposit and assets " + "outstanding must add up"; + result = false; + } + if (beforeVault.assetsAvailable + *vaultDeltaAssets != + afterVault.assetsAvailable) + { + JLOG(j.fatal()) << "Invariant failed: deposit and assets " + "available must add up"; + result = false; + } + + return result; + } + case ttVAULT_WITHDRAW: { + bool result = true; + + XRPL_ASSERT( + !beforeVault_.empty(), + "ripple::ValidVault::finalize : withdrawal updated a " + "vault"); + auto const& beforeVault = beforeVault_[0]; + + auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId); + + if (!vaultDeltaAssets) + { + JLOG(j.fatal()) << "Invariant failed: withdrawal must " + "change vault balance"; + return false; // That's all we can do + } + + if (*vaultDeltaAssets >= zero) + { + JLOG(j.fatal()) << "Invariant failed: withdrawal must " + "decrease vault balance"; + result = false; + } + + // Any payments (including withdrawal) going to the issuer + // do not change their balance, but destroy funds instead. + bool const issuerWithdrawal = [&]() -> bool { + if (vaultAsset.native()) + return false; + auto const destination = + tx[~sfDestination].value_or(tx[sfAccount]); + return destination == vaultAsset.getIssuer(); + }(); + + if (!issuerWithdrawal) + { + auto const accountDeltaAssets = + [&]() -> std::optional { + if (auto ret = deltaAssets(tx[sfAccount]); ret) + { + // Compensate for transaction fee deduced from + // sfAccount + if (vaultAsset.native()) + *ret += fee.drops(); + if (*ret != zero) + return ret; + } + return std::nullopt; + }(); + + auto const otherAccountDelta = + [&]() -> std::optional { + if (auto const destination = tx[~sfDestination]; + destination && *destination != tx[sfAccount]) + return deltaAssets(*destination); + return std::nullopt; + }(); + + if (accountDeltaAssets.has_value() == + otherAccountDelta.has_value()) + { + JLOG(j.fatal()) << // + "Invariant failed: withdrawal must change one " + "destination balance"; + return false; + } + + auto const destinationDelta = // + accountDeltaAssets ? *accountDeltaAssets + : *otherAccountDelta; + + if (destinationDelta <= zero) + { + JLOG(j.fatal()) << // + "Invariant failed: withdrawal must increase " + "destination balance"; + result = false; + } + + if (*vaultDeltaAssets * -1 != destinationDelta) + { + JLOG(j.fatal()) << // + "Invariant failed: withdrawal must change vault " + "and destination balance by equal amount"; + result = false; + } + } + + auto const accountDeltaShares = deltaShares(tx[sfAccount]); + if (!accountDeltaShares) + { + JLOG(j.fatal()) << // + "Invariant failed: withdrawal must change depositor " + "shares"; + return false; + } + + if (*accountDeltaShares >= zero) + { + JLOG(j.fatal()) << // + "Invariant failed: withdrawal must decrease depositor " + "shares"; + result = false; + } + + auto const vaultDeltaShares = deltaShares(afterVault.pseudoId); + if (!vaultDeltaShares) + { + JLOG(j.fatal()) << // + "Invariant failed: withdrawal must change vault shares"; + return false; // That's all we can do + } + + if (*vaultDeltaShares * -1 != *accountDeltaShares) + { + JLOG(j.fatal()) << // + "Invariant failed: withdrawal must change depositor " + "and vault shares by equal amount"; + result = false; + } + + // Note, vaultBalance is negative (see check above) + if (beforeVault.assetsTotal + *vaultDeltaAssets != + afterVault.assetsTotal) + { + JLOG(j.fatal()) << "Invariant failed: withdrawal and " + "assets outstanding must add up"; + result = false; + } + + if (beforeVault.assetsAvailable + *vaultDeltaAssets != + afterVault.assetsAvailable) + { + JLOG(j.fatal()) << "Invariant failed: withdrawal and " + "assets available must add up"; + result = false; + } + + return result; + } + case ttVAULT_CLAWBACK: { + bool result = true; + + XRPL_ASSERT( + !beforeVault_.empty(), + "ripple::ValidVault::finalize : clawback updated a vault"); + auto const& beforeVault = beforeVault_[0]; + + if (vaultAsset.native() || + vaultAsset.getIssuer() != tx[sfAccount]) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback may only be performed by " + "the asset issuer"; + return false; // That's all we can do + } + + auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId); + + if (!vaultDeltaAssets) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback must change vault balance"; + return false; // That's all we can do + } + + if (*vaultDeltaAssets >= zero) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback must decrease vault " + "balance"; + result = false; + } + + auto const accountDeltaShares = deltaShares(tx[sfHolder]); + if (!accountDeltaShares) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback must change holder shares"; + return false; // That's all we can do + } + + if (*accountDeltaShares >= zero) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback must decrease holder " + "shares"; + result = false; + } + + auto const vaultDeltaShares = deltaShares(afterVault.pseudoId); + if (!vaultDeltaShares) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback must change vault shares"; + return false; // That's all we can do + } + + if (*vaultDeltaShares * -1 != *accountDeltaShares) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback must change holder and " + "vault shares by equal amount"; + result = false; + } + + if (beforeVault.assetsTotal + *vaultDeltaAssets != + afterVault.assetsTotal) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback and assets outstanding " + "must add up"; + result = false; + } + + if (beforeVault.assetsAvailable + *vaultDeltaAssets != + afterVault.assetsAvailable) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback and assets available must " + "add up"; + result = false; + } + + return result; + } + + default: + // LCOV_EXCL_START + UNREACHABLE( + "ripple::ValidVault::finalize : unknown transaction type"); + return false; + // LCOV_EXCL_STOP + } + }(); + + if (!result) + { + // The comment at the top of this file starting with "assert(enforce)" + // explains this assert. + XRPL_ASSERT(enforce, "ripple::ValidVault::finalize : vault invariants"); + return !enforce; + } + + return true; +} + } // namespace ripple diff --git a/src/xrpld/app/tx/detail/InvariantCheck.h b/src/xrpld/app/tx/detail/InvariantCheck.h index b2527c53e0..420bb646dd 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.h +++ b/src/xrpld/app/tx/detail/InvariantCheck.h @@ -20,8 +20,10 @@ #ifndef RIPPLE_APP_TX_INVARIANTCHECK_H_INCLUDED #define RIPPLE_APP_TX_INVARIANTCHECK_H_INCLUDED +#include #include #include +#include #include #include #include @@ -831,6 +833,74 @@ public: beast::Journal const&); }; +/* + * @brief Invariants: Vault object and MPTokenIssuance for vault shares + * + * - vault deleted and vault created is empty + * - vault created must be linked to pseudo-account for shares and assets + * - vault must have MPTokenIssuance for shares + * - vault without shares outstanding must have no shares + * - loss unrealized does not exceed the difference between assets total and + * assets available + * - assets available do not exceed assets total + * - vault deposit increases assets and share issuance, and adds to: + * total assets, assets available, shares outstanding + * - vault withdrawal and clawback reduce assets and share issuance, and + * subtracts from: total assets, assets available, shares outstanding + * - vault set must not alter the vault assets or shares balance + * - no vault transaction can change loss unrealized (it's updated by loan + * transactions) + * + */ +class ValidVault +{ + Number static constexpr zero{}; + + struct Vault final + { + uint256 key = beast::zero; + Asset asset = {}; + AccountID pseudoId = {}; + uint192 shareMPTID = beast::zero; + Number assetsTotal = 0; + Number assetsAvailable = 0; + Number assetsMaximum = 0; + Number lossUnrealized = 0; + + Vault static make(SLE const&); + }; + + struct Shares final + { + MPTIssue share = {}; + std::uint64_t sharesTotal = 0; + std::uint64_t sharesMaximum = 0; + + Shares static make(SLE const&); + }; + + std::vector afterVault_ = {}; + std::vector afterMPTs_ = {}; + std::vector beforeVault_ = {}; + std::vector beforeMPTs_ = {}; + std::unordered_map deltas_ = {}; + +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< @@ -856,7 +926,8 @@ using InvariantChecks = std::tuple< NoModifiedUnmodifiableFields, ValidPseudoAccounts, ValidLoanBroker, - ValidLoan>; + ValidLoan, + ValidVault>; /** * @brief get a tuple of all invariant checks diff --git a/src/xrpld/app/tx/detail/Offer.h b/src/xrpld/app/tx/detail/Offer.h index c214bea23f..58bd65ce46 100644 --- a/src/xrpld/app/tx/detail/Offer.h +++ b/src/xrpld/app/tx/detail/Offer.h @@ -225,11 +225,13 @@ template void TOffer::setFieldAmounts() { + // LCOV_EXCL_START #ifdef _MSC_VER UNREACHABLE("ripple::TOffer::setFieldAmounts : must be specialized"); #else static_assert(sizeof(TOut) == -1, "Must be specialized"); #endif + // LCOV_EXCL_STOP } template diff --git a/src/xrpld/app/tx/detail/OfferStream.cpp b/src/xrpld/app/tx/detail/OfferStream.cpp index 8e1215f5c8..9d43e419d7 100644 --- a/src/xrpld/app/tx/detail/OfferStream.cpp +++ b/src/xrpld/app/tx/detail/OfferStream.cpp @@ -369,10 +369,12 @@ TOfferStreamBase::step() std::is_same_v)) return shouldRmSmallIncreasedQOffer(); } + // LCOV_EXCL_START UNREACHABLE( "rippls::TOfferStreamBase::step::rmSmallIncreasedQOffer : XRP " "vs XRP offer"); return false; + // LCOV_EXCL_STOP }(); if (rmSmallIncreasedQOffer) diff --git a/src/xrpld/app/tx/detail/SetSignerList.cpp b/src/xrpld/app/tx/detail/SetSignerList.cpp index b5d9d4d5b8..ec2f902009 100644 --- a/src/xrpld/app/tx/detail/SetSignerList.cpp +++ b/src/xrpld/app/tx/detail/SetSignerList.cpp @@ -134,8 +134,10 @@ SetSignerList::doApply() default: break; } + // LCOV_EXCL_START UNREACHABLE("ripple::SetSignerList::doApply : invalid operation"); return temMALFORMED; + // LCOV_EXCL_STOP } void diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index b61680273a..5eb7eced72 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -1188,11 +1188,13 @@ Transactor::operator()() if (!s2.isEquivalent(ctx_.tx)) { + // LCOV_EXCL_START JLOG(j_.fatal()) << "Transaction serdes mismatch"; JLOG(j_.info()) << to_string(ctx_.tx.getJson(JsonOptions::none)); JLOG(j_.fatal()) << s2.getJson(JsonOptions::none); UNREACHABLE( "ripple::Transactor::operator() : transaction serdes mismatch"); + // LCOV_EXCL_STOP } } #endif diff --git a/src/xrpld/app/tx/detail/VaultDeposit.cpp b/src/xrpld/app/tx/detail/VaultDeposit.cpp index 75cf81b0b0..80382934ad 100644 --- a/src/xrpld/app/tx/detail/VaultDeposit.cpp +++ b/src/xrpld/app/tx/detail/VaultDeposit.cpp @@ -156,7 +156,10 @@ VaultDeposit::preclaim(PreclaimContext const& ctx) !isTesSuccess(ter)) return ter; - if (accountHolds( + // Asset issuer does not have any balance, they can just create funds by + // depositing in the vault. + if ((vaultAsset.native() || vaultAsset.getIssuer() != account) && + accountHolds( ctx.view, account, vaultAsset, diff --git a/src/xrpld/app/tx/detail/VaultSet.cpp b/src/xrpld/app/tx/detail/VaultSet.cpp index e9f7551154..eff47621fa 100644 --- a/src/xrpld/app/tx/detail/VaultSet.cpp +++ b/src/xrpld/app/tx/detail/VaultSet.cpp @@ -186,6 +186,9 @@ VaultSet::doApply() view().update(sleIssuance); } + // Note, we must update Vault object even if only DomainID is being updated + // in Issuance object. Otherwise it's really difficult for Vault invariants + // to verify the operation. view().update(vault); return tesSUCCESS; diff --git a/src/xrpld/app/tx/detail/XChainBridge.cpp b/src/xrpld/app/tx/detail/XChainBridge.cpp index 2587845df5..5f5c081e2f 100644 --- a/src/xrpld/app/tx/detail/XChainBridge.cpp +++ b/src/xrpld/app/tx/detail/XChainBridge.cpp @@ -223,10 +223,12 @@ claimHelper( auto i = signersList.find(a.keyAccount); if (i == signersList.end()) { + // LCOV_EXCL_START UNREACHABLE( "ripple::claimHelper : invalid inputs"); // should have already // been checked continue; + // LCOV_EXCL_STOP } weight += i->second; rewardAccounts.push_back(a.rewardAccount); diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index c2e4e13f08..1cad93eedc 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -129,10 +129,12 @@ invoke_preflight(PreflightContext const& ctx) catch (UnknownTxnType const& e) { // Should never happen + // LCOV_EXCL_START JLOG(ctx.j.fatal()) << "Unknown transaction type in preflight: " << e.txnType; UNREACHABLE("ripple::invoke_preflight : unknown transaction type"); return {temUNKNOWN, TxConsequences{temUNKNOWN}}; + // LCOV_EXCL_STOP } } @@ -183,10 +185,12 @@ invoke_preclaim(PreclaimContext const& ctx) catch (UnknownTxnType const& e) { // Should never happen + // LCOV_EXCL_START JLOG(ctx.j.fatal()) << "Unknown transaction type in preclaim: " << e.txnType; UNREACHABLE("ripple::invoke_preclaim : unknown transaction type"); return temUNKNOWN; + // LCOV_EXCL_STOP } } @@ -217,9 +221,11 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx) } catch (UnknownTxnType const& e) { + // LCOV_EXCL_START UNREACHABLE( "ripple::invoke_calculateBaseFee : unknown transaction type"); return XRPAmount{0}; + // LCOV_EXCL_STOP } } @@ -277,10 +283,12 @@ invoke_apply(ApplyContext& ctx) catch (UnknownTxnType const& e) { // Should never happen + // LCOV_EXCL_START JLOG(ctx.journal.fatal()) << "Unknown transaction type in apply: " << e.txnType; UNREACHABLE("ripple::invoke_apply : unknown transaction type"); return {temUNKNOWN, false}; + // LCOV_EXCL_STOP } } diff --git a/src/xrpld/nodestore/backend/NuDBFactory.cpp b/src/xrpld/nodestore/backend/NuDBFactory.cpp index 2f4e9d502e..727dec6f3e 100644 --- a/src/xrpld/nodestore/backend/NuDBFactory.cpp +++ b/src/xrpld/nodestore/backend/NuDBFactory.cpp @@ -121,11 +121,13 @@ public: using namespace boost::filesystem; if (db_.is_open()) { + // LCOV_EXCL_START UNREACHABLE( "ripple::NodeStore::NuDBBackend::open : database is already " "open"); JLOG(j_.error()) << "database is already open"; return; + // LCOV_EXCL_STOP } auto const folder = path(name_); auto const dp = (folder / "nudb.dat").string(); diff --git a/src/xrpld/nodestore/backend/RocksDBFactory.cpp b/src/xrpld/nodestore/backend/RocksDBFactory.cpp index 0e421cd6bd..57c136a10a 100644 --- a/src/xrpld/nodestore/backend/RocksDBFactory.cpp +++ b/src/xrpld/nodestore/backend/RocksDBFactory.cpp @@ -232,11 +232,13 @@ public: { if (m_db) { + // LCOV_EXCL_START UNREACHABLE( "ripple::NodeStore::RocksDBBackend::open : database is already " "open"); JLOG(m_journal.error()) << "database is already open"; return; + // LCOV_EXCL_STOP } rocksdb::DB* db = nullptr; m_options.create_if_missing = createIfMissing; diff --git a/src/xrpld/overlay/Compression.h b/src/xrpld/overlay/Compression.h index 3a278a3403..35af4bf925 100644 --- a/src/xrpld/overlay/Compression.h +++ b/src/xrpld/overlay/Compression.h @@ -60,12 +60,14 @@ decompress( in, inSize, decompressed, decompressedSize); else { + // LCOV_EXCL_START JLOG(debugLog().warn()) << "decompress: invalid compression algorithm " << static_cast(algorithm); UNREACHABLE( "ripple::compression::decompress : invalid compression " "algorithm"); + // LCOV_EXCL_STOP } } catch (...) @@ -98,11 +100,13 @@ compress( in, inSize, std::forward(bf)); else { + // LCOV_EXCL_START JLOG(debugLog().warn()) << "compress: invalid compression algorithm" << static_cast(algorithm); UNREACHABLE( "ripple::compression::compress : invalid compression " "algorithm"); + // LCOV_EXCL_STOP } } catch (...) diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index 127ab4655e..fe07983f89 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -2249,10 +2249,12 @@ PeerImp::onValidatorListMessage( case ListDisposition::invalid: case ListDisposition::unsupported_version: break; + // LCOV_EXCL_START default: UNREACHABLE( "ripple::PeerImp::onValidatorListMessage : invalid best list " "disposition"); + // LCOV_EXCL_STOP } // Charge based on the worst result @@ -2293,10 +2295,12 @@ PeerImp::onValidatorListMessage( // If it happens frequently, that's probably bad. fee_.update(Resource::feeInvalidData, "version"); break; + // LCOV_EXCL_START default: UNREACHABLE( "ripple::PeerImp::onValidatorListMessage : invalid worst list " "disposition"); + // LCOV_EXCL_STOP } // Log based on all the results. @@ -2353,10 +2357,12 @@ PeerImp::onValidatorListMessage( << "Ignored " << count << "invalid " << messageType << "(s) from peer " << remote_address_; break; + // LCOV_EXCL_START default: UNREACHABLE( "ripple::PeerImp::onValidatorListMessage : invalid list " "disposition"); + // LCOV_EXCL_STOP } } } diff --git a/src/xrpld/peerfinder/detail/Counts.h b/src/xrpld/peerfinder/detail/Counts.h index 821431c5bb..a35473ddb5 100644 --- a/src/xrpld/peerfinder/detail/Counts.h +++ b/src/xrpld/peerfinder/detail/Counts.h @@ -295,10 +295,12 @@ private: m_closingCount += n; break; + // LCOV_EXCL_START default: UNREACHABLE( "ripple::PeerFinder::Counts::adjust : invalid input state"); break; + // LCOV_EXCL_STOP }; } diff --git a/src/xrpld/peerfinder/detail/Logic.h b/src/xrpld/peerfinder/detail/Logic.h index 4b92a1d143..74bec8431e 100644 --- a/src/xrpld/peerfinder/detail/Logic.h +++ b/src/xrpld/peerfinder/detail/Logic.h @@ -976,11 +976,13 @@ public: << slot->remote_endpoint(); break; + // LCOV_EXCL_START default: UNREACHABLE( "ripple::PeerFinder::Logic::on_closed : invalid slot " "state"); break; + // LCOV_EXCL_STOP } } diff --git a/src/xrpld/perflog/detail/PerfLogImp.cpp b/src/xrpld/perflog/detail/PerfLogImp.cpp index a34faf885c..583ec095e1 100644 --- a/src/xrpld/perflog/detail/PerfLogImp.cpp +++ b/src/xrpld/perflog/detail/PerfLogImp.cpp @@ -53,9 +53,11 @@ PerfLogImp::Counters::Counters( if (!inserted) { // Ensure that no other function populates this entry. + // LCOV_EXCL_START UNREACHABLE( "ripple::perf::PerfLogImp::Counters::Counters : failed to " "insert label"); + // LCOV_EXCL_STOP } } } @@ -68,9 +70,11 @@ PerfLogImp::Counters::Counters( if (!inserted) { // Ensure that no other function populates this entry. + // LCOV_EXCL_START UNREACHABLE( "ripple::perf::PerfLogImp::Counters::Counters : failed to " "insert job type"); + // LCOV_EXCL_STOP } } } @@ -329,8 +333,10 @@ PerfLogImp::rpcStart(std::string const& method, std::uint64_t const requestId) auto counter = counters_.rpc_.find(method); if (counter == counters_.rpc_.end()) { + // LCOV_EXCL_START UNREACHABLE("ripple::perf::PerfLogImp::rpcStart : valid method input"); return; + // LCOV_EXCL_STOP } { @@ -351,8 +357,10 @@ PerfLogImp::rpcEnd( auto counter = counters_.rpc_.find(method); if (counter == counters_.rpc_.end()) { + // LCOV_EXCL_START UNREACHABLE("ripple::perf::PerfLogImp::rpcEnd : valid method input"); return; + // LCOV_EXCL_STOP } steady_time_point startTime; { @@ -365,8 +373,10 @@ PerfLogImp::rpcEnd( } else { + // LCOV_EXCL_START UNREACHABLE( "ripple::perf::PerfLogImp::rpcEnd : valid requestId input"); + // LCOV_EXCL_STOP } } std::lock_guard lock(counter->second.mutex); @@ -384,9 +394,11 @@ PerfLogImp::jobQueue(JobType const type) auto counter = counters_.jq_.find(type); if (counter == counters_.jq_.end()) { + // LCOV_EXCL_START UNREACHABLE( "ripple::perf::PerfLogImp::jobQueue : valid job type input"); return; + // LCOV_EXCL_STOP } std::lock_guard lock(counter->second.mutex); ++counter->second.value.queued; @@ -402,10 +414,13 @@ PerfLogImp::jobStart( auto counter = counters_.jq_.find(type); if (counter == counters_.jq_.end()) { + // LCOV_EXCL_START UNREACHABLE( "ripple::perf::PerfLogImp::jobStart : valid job type input"); return; + // LCOV_EXCL_STOP } + { std::lock_guard lock(counter->second.mutex); ++counter->second.value.started; @@ -422,10 +437,13 @@ PerfLogImp::jobFinish(JobType const type, microseconds dur, int instance) auto counter = counters_.jq_.find(type); if (counter == counters_.jq_.end()) { + // LCOV_EXCL_START UNREACHABLE( "ripple::perf::PerfLogImp::jobFinish : valid job type input"); return; + // LCOV_EXCL_STOP } + { std::lock_guard lock(counter->second.mutex); ++counter->second.value.finished; diff --git a/src/xrpld/rpc/detail/Handler.cpp b/src/xrpld/rpc/detail/Handler.cpp index 3b32524ee2..d15c5aaed0 100644 --- a/src/xrpld/rpc/detail/Handler.cpp +++ b/src/xrpld/rpc/detail/Handler.cpp @@ -39,8 +39,10 @@ byRef(Function const& f) result = f(context); if (result.type() != Json::objectValue) { + // LCOV_EXCL_START UNREACHABLE("ripple::RPC::byRef : result is object"); result = RPC::makeObjectValue(result); + // LCOV_EXCL_STOP } return Status(); diff --git a/src/xrpld/rpc/detail/Status.cpp b/src/xrpld/rpc/detail/Status.cpp index 738219b6b5..ce15003968 100644 --- a/src/xrpld/rpc/detail/Status.cpp +++ b/src/xrpld/rpc/detail/Status.cpp @@ -51,8 +51,10 @@ Status::codeString() const return sStr.str(); } + // LCOV_EXCL_START UNREACHABLE("ripple::RPC::codeString : invalid type"); return ""; + // LCOV_EXCL_STOP } void diff --git a/src/xrpld/rpc/handlers/AccountChannels.cpp b/src/xrpld/rpc/handlers/AccountChannels.cpp index 1b0046ab64..17e46f052f 100644 --- a/src/xrpld/rpc/handlers/AccountChannels.cpp +++ b/src/xrpld/rpc/handlers/AccountChannels.cpp @@ -169,8 +169,10 @@ doAccountChannels(RPC::JsonContext& context) std::shared_ptr const& sleCur) { if (!sleCur) { + // LCOV_EXCL_START UNREACHABLE("ripple::doAccountChannels : null SLE"); return false; + // LCOV_EXCL_STOP } if (++count == limit) diff --git a/src/xrpld/rpc/handlers/AccountLines.cpp b/src/xrpld/rpc/handlers/AccountLines.cpp index 893ca9a190..146a9527a9 100644 --- a/src/xrpld/rpc/handlers/AccountLines.cpp +++ b/src/xrpld/rpc/handlers/AccountLines.cpp @@ -193,8 +193,10 @@ doAccountLines(RPC::JsonContext& context) std::shared_ptr const& sleCur) { if (!sleCur) { + // LCOV_EXCL_START UNREACHABLE("ripple::doAccountLines : null SLE"); return false; + // LCOV_EXCL_STOP } if (++count == limit) diff --git a/src/xrpld/rpc/handlers/AccountOffers.cpp b/src/xrpld/rpc/handlers/AccountOffers.cpp index e65b39b35b..1f2b76efe4 100644 --- a/src/xrpld/rpc/handlers/AccountOffers.cpp +++ b/src/xrpld/rpc/handlers/AccountOffers.cpp @@ -145,8 +145,10 @@ doAccountOffers(RPC::JsonContext& context) std::shared_ptr const& sle) { if (!sle) { + // LCOV_EXCL_START UNREACHABLE("ripple::doAccountOffers : null SLE"); return false; + // LCOV_EXCL_STOP } if (++count == limit) diff --git a/src/xrpld/rpc/handlers/AccountTx.cpp b/src/xrpld/rpc/handlers/AccountTx.cpp index 6b1dccdba9..e053c2adc0 100644 --- a/src/xrpld/rpc/handlers/AccountTx.cpp +++ b/src/xrpld/rpc/handlers/AccountTx.cpp @@ -353,9 +353,13 @@ populateJsonResponse( jvObj[jss::meta], sttx, *txnMeta); } else + { + // LCOV_EXCL_START UNREACHABLE( "ripple::populateJsonResponse : missing " "transaction medatata"); + // LCOV_EXCL_STOP + } } } } diff --git a/src/xrpld/rpc/handlers/Fee1.cpp b/src/xrpld/rpc/handlers/Fee1.cpp index 6d15a4d95f..ecb4ad4b29 100644 --- a/src/xrpld/rpc/handlers/Fee1.cpp +++ b/src/xrpld/rpc/handlers/Fee1.cpp @@ -32,9 +32,12 @@ doFee(RPC::JsonContext& context) auto result = context.app.getTxQ().doRPC(context.app); if (result.type() == Json::objectValue) return result; + + // LCOV_EXCL_START UNREACHABLE("ripple::doFee : invalid result type"); RPC::inject_error(rpcINTERNAL, context.params); return context.params; + // LCOV_EXCL_STOP } } // namespace ripple diff --git a/src/xrpld/shamap/detail/SHAMap.cpp b/src/xrpld/shamap/detail/SHAMap.cpp index d2415a2ff2..026149be56 100644 --- a/src/xrpld/shamap/detail/SHAMap.cpp +++ b/src/xrpld/shamap/detail/SHAMap.cpp @@ -545,8 +545,10 @@ SHAMap::onlyBelow(SHAMapTreeNode* node) const if (!nextNode) { + // LCOV_EXCL_START UNREACHABLE("ripple::SHAMap::onlyBelow : no next node"); return no_item; + // LCOV_EXCL_STOP } node = nextNode; @@ -922,8 +924,10 @@ SHAMap::updateGiveItem( if (!node || (node->peekItem()->key() != tag)) { + // LCOV_EXCL_START UNREACHABLE("ripple::SHAMap::updateGiveItem : invalid node"); return false; + // LCOV_EXCL_STOP } if (node->getType() != type) diff --git a/src/xrpld/shamap/detail/SHAMapDelta.cpp b/src/xrpld/shamap/detail/SHAMapDelta.cpp index 2adce62efc..ebdaffad14 100644 --- a/src/xrpld/shamap/detail/SHAMapDelta.cpp +++ b/src/xrpld/shamap/detail/SHAMapDelta.cpp @@ -149,8 +149,10 @@ SHAMap::compare(SHAMap const& otherMap, Delta& differences, int maxCount) const if (!ourNode || !otherNode) { + // LCOV_EXCL_START UNREACHABLE("ripple::SHAMap::compare : missing a node"); Throw(type_, uint256()); + // LCOV_EXCL_STOP } if (ourNode->isLeaf() && otherNode->isLeaf()) @@ -230,7 +232,11 @@ SHAMap::compare(SHAMap const& otherMap, Delta& differences, int maxCount) const } } else + { + // LCOV_EXCL_START UNREACHABLE("ripple::SHAMap::compare : invalid node"); + // LCOV_EXCL_STOP + } } return true; From c179135e1543b348b1df22b13a940274a8ab5644 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 9 Oct 2025 14:52:54 -0400 Subject: [PATCH 109/291] Fix the build and test errors introduced by the previous merge: ** Vault Invariants for Loan txs need to be fleshed out. ** - Give the appropriate Loan transactions vault modification privileges. - Give the appropriate Loan transactions the ability to authorize (create in this case) MPTokens. - Check that LoanManage does not leave Vault in an inconsistent state (AssetsAvailable > AssetsTotal). For IOU vaults, if the difference is dust, "round up". --- .../xrpl/protocol/detail/transactions.macro | 11 +++--- src/xrpld/app/tx/detail/InvariantCheck.cpp | 15 ++++++-- src/xrpld/app/tx/detail/LoanManage.cpp | 36 ++++++++++++++++++- src/xrpld/app/tx/detail/LoanPay.cpp | 4 +++ src/xrpld/app/tx/detail/LoanSet.cpp | 4 +++ 5 files changed, 63 insertions(+), 7 deletions(-) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 1d14ae0379..3b15646642 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -993,7 +993,7 @@ TRANSACTION(ttLOAN_BROKER_COVER_DEPOSIT, 76, LoanBrokerCoverDeposit, TRANSACTION(ttLOAN_BROKER_COVER_WITHDRAW, 77, LoanBrokerCoverWithdraw, Delegation::delegatable, featureLendingProtocol, - noPriv, ({ + mayAuthorizeMPT, ({ {sfLoanBrokerID, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, {sfDestination, soeOPTIONAL}, @@ -1020,7 +1020,7 @@ TRANSACTION(ttLOAN_BROKER_COVER_CLAWBACK, 78, LoanBrokerCoverClawback, TRANSACTION(ttLOAN_SET, 80, LoanSet, Delegation::delegatable, featureLendingProtocol, - noPriv, ({ + mayAuthorizeMPT | mustModifyVault, ({ {sfLoanBrokerID, soeREQUIRED}, {sfData, soeOPTIONAL}, {sfCounterparty, soeOPTIONAL}, @@ -1058,7 +1058,10 @@ TRANSACTION(ttLOAN_DELETE, 81, LoanDelete, TRANSACTION(ttLOAN_MANAGE, 82, LoanManage, Delegation::delegatable, featureLendingProtocol, - noPriv, ({ + // All of the LoanManage options will modify the vault, but the + // transaction can succeed without options, essentially making it + // a noop. + mayModifyVault, ({ {sfLoanID, soeREQUIRED}, })) @@ -1083,7 +1086,7 @@ TRANSACTION(ttLOAN_DRAW, 83, LoanDraw, TRANSACTION(ttLOAN_PAY, 84, LoanPay, Delegation::delegatable, featureLendingProtocol, - noPriv, ({ + mayAuthorizeMPT | mustModifyVault, ({ {sfLoanID, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, })) diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 3495cc3a02..67a09f1104 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -89,6 +89,8 @@ enum Privilege { 0x0400, // The transaction MAY delete an MPT object. May not create. mustModifyVault = 0x0800, // The transaction must modify, delete or create, a vault + mayModifyVault = + 0x1000, // The transaction MAY modify, delete or create, a vault }; constexpr Privilege operator|(Privilege lhs, Privilege rhs) @@ -2638,7 +2640,8 @@ ValidVault::finalize( return true; // Not a vault operation } - else if (!hasPrivilege(tx, mustModifyVault)) // TODO: mayModifyVault + else if (!(hasPrivilege(tx, mustModifyVault) || + hasPrivilege(tx, mayModifyVault))) { JLOG(j.fatal()) << // "Invariant failed: vault updated by a wrong transaction type"; @@ -2853,7 +2856,8 @@ ValidVault::finalize( } if (!beforeVault_.empty() && - afterVault.lossUnrealized != beforeVault_[0].lossUnrealized) + afterVault.lossUnrealized != beforeVault_[0].lossUnrealized && + tx.getTxnType() != ttLOAN_MANAGE) { JLOG(j.fatal()) << // "Invariant failed: vault transaction must not change loss " @@ -3410,6 +3414,13 @@ ValidVault::finalize( return result; } + case ttLOAN_SET: + case ttLOAN_MANAGE: + case ttLOAN_PAY: { + // TBD + return true; + } + default: // LCOV_EXCL_START UNREACHABLE( diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index 38efff1570..c35a742c1a 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -184,6 +184,7 @@ LoanManage::defaultLoan( { // Decrease the Total Value of the Vault: auto vaultAssetsTotalProxy = vaultSle->at(sfAssetsTotal); + auto vaultAssetsAvailableProxy = vaultSle->at(sfAssetsAvailable); if (vaultAssetsTotalProxy < vaultDefaultAmount) { // LCOV_EXCL_START @@ -195,7 +196,40 @@ LoanManage::defaultLoan( vaultAssetsTotalProxy -= vaultDefaultAmount; // Increase the Asset Available of the Vault by liquidated First-Loss // Capital and any unclaimed funds amount: - vaultSle->at(sfAssetsAvailable) += returnToVault; + vaultAssetsAvailableProxy += returnToVault; + if (*vaultAssetsAvailableProxy > *vaultAssetsTotalProxy && + !vaultAsset.native() && vaultAsset.holds()) + { + auto const difference = + vaultAssetsAvailableProxy - vaultAssetsTotalProxy; + JLOG(j.debug()) + << "Vault assets available: " << *vaultAssetsAvailableProxy + << "(" << vaultAssetsAvailableProxy->value().exponent() + << "), Total: " << *vaultAssetsTotalProxy << "(" + << vaultAssetsTotalProxy->value().exponent() + << "), Difference: " << difference << "(" + << difference.exponent() << ")"; + if (vaultAssetsAvailableProxy->value().exponent() - + difference.exponent() > + 13) + { + // If the difference is dust, bring the total up to match + // the available + JLOG(j.debug()) + << "Difference between vault assets available and total is " + "dust. Set both to the larger value."; + vaultAssetsTotalProxy = vaultAssetsAvailableProxy; + } + } + if (*vaultAssetsAvailableProxy > *vaultAssetsTotalProxy) + { + JLOG(j.warn()) << "Vault assets available must not be greater " + "than assets outstanding. Available: " + << *vaultAssetsAvailableProxy + << ", Total: " << *vaultAssetsTotalProxy; + return tecLIMIT_EXCEEDED; + } + // The loss has been realized if (loanSle->isFlag(lsfLoanImpaired)) { diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index fe58c66711..747664fbaf 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -275,6 +275,10 @@ LoanPay::doApply() vaultSle->at(sfAssetsAvailable) += totalPaidToVault; vaultSle->at(sfAssetsTotal) += vaultValueChange; + XRPL_ASSERT_PARTS( + *vaultSle->at(sfAssetsAvailable) <= *vaultSle->at(sfAssetsTotal), + "ripple::LoanPay::doApply", + "assets available must not be greater than assets outstanding"); // Move funds STAmount const paidToVault(asset, totalPaidToVault); diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 8c14a62fbe..239eddb526 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -447,6 +447,10 @@ LoanSet::doApply() // Update the balances in the vault vaultSle->at(sfAssetsAvailable) -= principalRequested; vaultSle->at(sfAssetsTotal) += loanInterestToVault; + XRPL_ASSERT_PARTS( + *vaultSle->at(sfAssetsAvailable) <= *vaultSle->at(sfAssetsTotal), + "ripple::LoanSet::doApply", + "assets available must not be greater than assets outstanding"); view.update(vaultSle); // Update the balances in the loan broker From 66dbc05a9fad9f336b995301cff35b51b7952efe Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 10 Oct 2025 12:54:39 -0400 Subject: [PATCH 110/291] Update ValidVault to allow LoanPay to change LossUnrealized - Making a payment on an impaired loan will unimpair the loan, which changes Vault.LossUnrealized. - Add a step in unit tests to impair a loan before making a payment, to verify, and prevent future similar regressions. - Resolves regression RIPD-3650 --- src/test/app/Loan_test.cpp | 6 ++++++ src/xrpld/app/tx/detail/InvariantCheck.cpp | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 7627289dc0..c7c601de3d 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -1856,6 +1856,12 @@ class Loan_test : public beast::unit_test::suite auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset); + if (canImpairLoan(env, broker, state)) + // Making a payment will unimpair the loan + env(manage(lender, loanKeylet.key, tfLoanImpair)); + + env.close(); + // Make the payment env(pay(borrower, loanKeylet.key, transactionAmount)); diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 67a09f1104..fa43579bd5 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -2857,7 +2857,7 @@ ValidVault::finalize( if (!beforeVault_.empty() && afterVault.lossUnrealized != beforeVault_[0].lossUnrealized && - tx.getTxnType() != ttLOAN_MANAGE) + txnType != ttLOAN_MANAGE && txnType != ttLOAN_PAY) { JLOG(j.fatal()) << // "Invariant failed: vault transaction must not change loss " From 9c28578262be2b8816e56200890afffb5eb8458d Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 17 Oct 2025 16:26:23 -0400 Subject: [PATCH 111/291] Review feedback from @a1q123456 - Convert STTx::getSignature to be static. - Make the STTx::checkSign that takes a signature object private, and change it and all the other relevant functions to take the signature object as a `const&`. - Change `getBatchTransactionIDs` to return a `const&` to reduce the number of vector copies. Rename `batchTxnIds_` to CamelCase. Update the text of the internal comment. --- include/xrpl/protocol/STTx.h | 40 ++++++++++++++++----------------- src/libxrpl/protocol/STTx.cpp | 42 +++++++++++++++++------------------ 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/include/xrpl/protocol/STTx.h b/include/xrpl/protocol/STTx.h index 1e3695050d..ac0223ee77 100644 --- a/include/xrpl/protocol/STTx.h +++ b/include/xrpl/protocol/STTx.h @@ -87,8 +87,8 @@ public: getFullText() const override; // Outer transaction functions / signature functions. - Blob - getSignature(STObject const& sigObject) const; + static Blob + getSignature(STObject const& sigObject); Blob getSignature() const @@ -133,20 +133,6 @@ public: enum class RequireFullyCanonicalSig : bool { no, yes }; - /** Check the signature. - @param requireCanonicalSig If `true`, check that the signature is fully - canonical. If `false`, only check that the signature is valid. - @param rules The current ledger rules. - @param pSig Pointer to object that contains the signature fields, if not - using "this". Will most often be null - @return `true` if valid signature. If invalid, the error message string. - */ - Expected - checkSign( - RequireFullyCanonicalSig requireCanonicalSig, - Rules const& rules, - STObject const* pSig) const; - /** Check the signature. @param requireCanonicalSig If `true`, check that the signature is fully canonical. If `false`, only check that the signature is valid. @@ -177,20 +163,34 @@ public: char status, std::string const& escapedMetaData) const; - std::vector + std::vector const& getBatchTransactionIDs() const; private: + /** Check the signature. + @param requireCanonicalSig If `true`, check that the signature is fully + canonical. If `false`, only check that the signature is valid. + @param rules The current ledger rules. + @param sigObject Reference to object that contains the signature fields. + Will be *this more often than not. + @return `true` if valid signature. If invalid, the error message string. + */ + Expected + checkSign( + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules, + STObject const& sigObject) const; + Expected checkSingleSign( RequireFullyCanonicalSig requireCanonicalSig, - STObject const* pSig) const; + STObject const& sigObject) const; Expected checkMultiSign( RequireFullyCanonicalSig requireCanonicalSig, Rules const& rules, - STObject const* pSig) const; + STObject const& sigObject) const; Expected checkBatchSingleSign( @@ -209,7 +209,7 @@ private: move(std::size_t n, void* buf) override; friend class detail::STVar; - mutable std::vector batch_txn_ids_; + mutable std::vector batchTxnIds_; }; bool diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index 20ca76e789..7326f48424 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -200,7 +200,7 @@ STTx::getSigningHash() const } Blob -STTx::getSignature(STObject const& sigObject) const +STTx::getSignature(STObject const& sigObject) { try { @@ -259,19 +259,18 @@ Expected STTx::checkSign( RequireFullyCanonicalSig requireCanonicalSig, Rules const& rules, - STObject const* pSig) const + STObject const& sigObject) const { try { // Determine whether we're single- or multi-signing by looking // at the SigningPubKey. If it's empty we must be // multi-signing. Otherwise we're single-signing. - STObject const& sigObject{pSig ? *pSig : *this}; Blob const& signingPubKey = sigObject.getFieldVL(sfSigningPubKey); return signingPubKey.empty() - ? checkMultiSign(requireCanonicalSig, rules, pSig) - : checkSingleSign(requireCanonicalSig, pSig); + ? checkMultiSign(requireCanonicalSig, rules, sigObject) + : checkSingleSign(requireCanonicalSig, sigObject); } catch (std::exception const&) { @@ -284,14 +283,14 @@ STTx::checkSign( RequireFullyCanonicalSig requireCanonicalSig, Rules const& rules) const { - if (auto const ret = checkSign(requireCanonicalSig, rules, nullptr); !ret) + if (auto const ret = checkSign(requireCanonicalSig, rules, *this); !ret) return ret; /* Placeholder for field that will be added by Lending Protocol if (isFieldPresent(sfCounterpartySignature)) { auto const counterSig = getFieldObject(sfCounterpartySignature); - if (auto const ret = checkSign(requireCanonicalSig, rules, &counterSig); + if (auto const ret = checkSign(requireCanonicalSig, rules, counterSig); !ret) return Unexpected("Counterparty: " + ret.error()); } @@ -454,9 +453,8 @@ singleSignHelper( Expected STTx::checkSingleSign( RequireFullyCanonicalSig requireCanonicalSig, - STObject const* pSig) const + STObject const& sigObject) const { - STObject const& sigObject{pSig ? *pSig : *this}; auto const data = getSigningData(*this); bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || (requireCanonicalSig == STTx::RequireFullyCanonicalSig::yes); @@ -585,17 +583,16 @@ Expected STTx::checkMultiSign( RequireFullyCanonicalSig requireCanonicalSig, Rules const& rules, - STObject const* pSig) const + STObject const& sigObject) const { - STObject const& sigObject{pSig ? *pSig : *this}; - bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || (requireCanonicalSig == RequireFullyCanonicalSig::yes); // Used inside the loop in multiSignHelper to enforce that // the account owner may not multisign for themselves. - auto const txnAccountID = - pSig ? std::nullopt : std::optional(getAccountID(sfAccount)); + auto const txnAccountID = &sigObject != this + ? std::nullopt + : std::optional(getAccountID(sfAccount)); // We can ease the computational load inside the loop a bit by // pre-constructing part of the data that we hash. Fill a Serializer @@ -618,7 +615,7 @@ STTx::checkMultiSign( * * This function returns a vector of transaction IDs by extracting them from * the field array `sfRawTransactions` within the STTx. If the batch - * transaction IDs have already been computed and cached in `batch_txn_ids_`, + * transaction IDs have already been computed and cached in `batchTxnIds_`, * it returns the cached vector. Otherwise, it computes the transaction IDs, * caches them, and then returns the vector. * @@ -628,7 +625,7 @@ STTx::checkMultiSign( * empty and that the size of the computed batch transaction IDs matches the * size of the `sfRawTransactions` field array. */ -std::vector +std::vector const& STTx::getBatchTransactionIDs() const { XRPL_ASSERT( @@ -637,17 +634,20 @@ STTx::getBatchTransactionIDs() const XRPL_ASSERT( getFieldArray(sfRawTransactions).size() != 0, "STTx::getBatchTransactionIDs : empty raw transactions"); - // Don't early return so that the size assert is always hit. - if (batch_txn_ids_.size() == 0) + + // The list of inner ids is built once, then reused on subsequent calls. + // After the list is built, it must always have the same size as the array + // `sfRawTransactions`. The assert below verifies that. + if (batchTxnIds_.size() == 0) { for (STObject const& rb : getFieldArray(sfRawTransactions)) - batch_txn_ids_.push_back(rb.getHash(HashPrefix::transactionID)); + batchTxnIds_.push_back(rb.getHash(HashPrefix::transactionID)); } XRPL_ASSERT( - batch_txn_ids_.size() == getFieldArray(sfRawTransactions).size(), + batchTxnIds_.size() == getFieldArray(sfRawTransactions).size(), "STTx::getBatchTransactionIDs : batch transaction IDs size mismatch"); - return batch_txn_ids_; + return batchTxnIds_; } //------------------------------------------------------------------------------ From d353f4a2e66418c350e2aa018eb86b5a5d78ff2d Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 1 Oct 2025 17:18:16 -0400 Subject: [PATCH 112/291] Remove conditional LoanDraw code --- .../xrpl/protocol/detail/ledger_entries.macro | 5 - .../xrpl/protocol/detail/transactions.macro | 14 -- src/test/app/Loan_test.cpp | 167 +---------------- src/test/jtx/TestHelpers.h | 9 - src/test/jtx/impl/TestHelpers.cpp | 18 -- src/xrpld/app/tx/detail/LoanDelete.cpp | 17 -- src/xrpld/app/tx/detail/LoanDraw.cpp | 172 ------------------ src/xrpld/app/tx/detail/LoanDraw.h | 55 ------ src/xrpld/app/tx/detail/LoanManage.cpp | 26 +-- 9 files changed, 6 insertions(+), 477 deletions(-) delete mode 100644 src/xrpld/app/tx/detail/LoanDraw.cpp delete mode 100644 src/xrpld/app/tx/detail/LoanDraw.h diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 611a3331c8..bfecd0c92f 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -563,11 +563,6 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({ {sfPreviousPaymentDate, soeREQUIRED}, {sfNextPaymentDueDate, soeREQUIRED}, {sfPaymentRemaining, soeREQUIRED}, -//#if LOANDRAW -// TODO: Remove this when you remove the rest of the LOANDRAW blocks. -// Directives don't work with macro expansion. - {sfAssetsAvailable, soeDEFAULT}, -//#endif {sfPrincipalOutstanding, soeREQUIRED}, // Save the original request amount for rounding / scaling of // other computations, particularly for IOUs diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 3b15646642..1ded330157 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -1065,20 +1065,6 @@ TRANSACTION(ttLOAN_MANAGE, 82, LoanManage, {sfLoanID, soeREQUIRED}, })) -#if LOANDRAW -/** The Borrower uses this transaction to draws funds from the Loan. */ -#if TRANSACTION_INCLUDE -# include -#endif -TRANSACTION(ttLOAN_DRAW, 83, LoanDraw, - Delegation::delegatable, - featureLendingProtocol, - noPriv, ({ - {sfLoanID, soeREQUIRED}, - {sfAmount, soeREQUIRED, soeMPTSupported}, -})) -#endif - /** The Borrower uses this transaction to make a Payment on the Loan. */ #if TRANSACTION_INCLUDE # include diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index c7c601de3d..84e45e4ad5 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -86,11 +86,7 @@ class Loan_test : public beast::unit_test::suite env(del(alice, loanKeylet.key), ter(temDISABLED)); // 3. LoanManage env(manage(alice, loanKeylet.key, tfLoanImpair), ter(temDISABLED)); -#if LOANDRAW && 0 - // 4. LoanDraw - env(draw(alice, loanKeylet.key, XRP(500)), ter(temDISABLED)); -#endif - // 5. LoanPay + // 4. LoanPay env(pay(alice, loanKeylet.key, XRP(500)), ter(temDISABLED)); }; failAll(all - featureMPTokensV1); @@ -237,9 +233,6 @@ class Loan_test : public beast::unit_test::suite loan->at(sfNextPaymentDueDate) == nextPaymentDate); env.test.BEAST_EXPECT( loan->at(sfPaymentRemaining) == paymentRemaining); -#if LOANDRAW - env.test.BEAST_EXPECT(loan->at(sfAssetsAvailable) == 0); -#endif env.test.BEAST_EXPECT( loan->at(sfPrincipalRequested) == principalRequested); env.test.BEAST_EXPECT( @@ -618,9 +611,6 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT( loan->at(sfNextPaymentDueDate) == startDate + interval); BEAST_EXPECT(loan->at(sfPaymentRemaining) == total); -#if LOANDRAW - BEAST_EXPECT(loan->at(sfAssetsAvailable) == beast::zero); -#endif BEAST_EXPECT(loan->at(sfPrincipalRequested) == principalRequest); BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == principalRequest); } @@ -1258,15 +1248,6 @@ class Loan_test : public beast::unit_test::suite // defaulted env.close(nextDueDate + 60s); -#if LOANDRAW && 0 - if (impair) - { - // Impaired loans can't be drawn against - env(draw(borrower, loanKeylet.key, broker.asset(100)), - ter(tecNO_PERMISSION)); - } -#endif - auto const [amountToBeCovered, brokerAcct] = getDefaultInfo(state, broker); @@ -1287,12 +1268,6 @@ class Loan_test : public beast::unit_test::suite state.principalOutstanding = 0; verifyLoanStatus(state); -#if LOANDRAW && 0 - // Defaulted loans can't be drawn against, either - env(draw(borrower, loanKeylet.key, broker.asset(100)), - ter(tecNO_PERMISSION)); -#endif - // Once a loan is defaulted, it can't be managed env(manage(lender, loanKeylet.key, tfLoanUnimpair), ter(tecNO_PERMISSION)); @@ -1310,45 +1285,13 @@ class Loan_test : public beast::unit_test::suite auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus); BEAST_EXPECT(state.flags == baseFlag); -#if LOANDRAW && 0 - auto const borrowerStartingBalance = - env.balance(borrower, broker.asset); - - // Try to make a payment before the loan starts - env(pay(borrower, loanKeylet.key, broker.asset(500)), - ter(tecTOO_SOON)); - - // Advance to the start date of the loan - env.close(state.startDate + 5s); - - verifyLoanStatus(state); - - // Need to account for fees if the loan is in XRP - PrettyAmount adjustment = broker.asset(0); - if (broker.asset.raw().native()) - { - adjustment = 2 * env.current()->fees().base; - } - - // Draw the entire available balance - // Need to create the STAmount directly to avoid - // PrettyAsset scaling. - STAmount const drawAmount{broker.asset, state.assetsAvailable}; - env(draw(borrower, loanKeylet.key, drawAmount)); -#else STAmount const drawAmount = STAmount(broker.asset, state.principalRequested - 1); -#endif env.close(state.startDate + 20s); auto const loanAge = (env.now() - state.startDate).count(); BEAST_EXPECT(loanAge == 30); verifyLoanStatus(state); -#if LOANDRAW && 0 - BEAST_EXPECT( - env.balance(borrower, broker.asset) == - borrowerStartingBalance + drawAmount - adjustment); -#endif // Send some bogus pay transactions env(pay(borrower, @@ -1544,64 +1487,8 @@ class Loan_test : public beast::unit_test::suite broker.brokerID, broker.asset(coverDepositParameter).number()); -#if LOANDRAW && 0 - auto const borrowerStartingBalance = - env.balance(borrower, broker.asset); - - // Draw the balance - env(draw( - borrower, - keylet::loan(uint256(0)).key, - broker.asset(10)), - ter(temINVALID)); - env(draw(borrower, loanKeylet.key, broker.asset(-100)), - ter(temBAD_AMOUNT)); - env(draw(borrower, broker.brokerID, broker.asset(100)), - ter(tecNO_ENTRY)); - env(draw(evan, loanKeylet.key, broker.asset(500)), - ter(tecNO_PERMISSION)); - env(draw(borrower, loanKeylet.key, broker.asset(500)), - ter(tecTOO_SOON)); - - // Advance to the start date of the loan - env.close(state.startDate + 5s); - env(draw(borrower, loanKeylet.key, broker.asset(10000)), - ter(tecINSUFFICIENT_FUNDS)); - { - auto const otherAsset = - broker.asset.raw() == assets[0].raw() ? assets[1] - : assets[0]; - env(draw(borrower, loanKeylet.key, otherAsset(100)), - ter(tecWRONG_ASSET)); - } - - verifyLoanStatus(state); - - // Need to account for fees if the loan is in XRP - PrettyAmount adjustment = broker.asset(0); - if (broker.asset.raw().native()) - { - adjustment = 5 * env.current()->fees().base; - } - - // Draw about half the balance - auto const drawAmount = broker.asset(500); - env(draw(borrower, loanKeylet.key, drawAmount)); - - state.assetsAvailable -= drawAmount.number(); - verifyLoanStatus(state); - BEAST_EXPECT( - env.balance(borrower, broker.asset) == - borrowerStartingBalance + drawAmount - adjustment); -#endif - // move past the due date + grace period (60s) env.close(tp{d{state.nextPaymentDate}} + 60s + 20s); -#if LOANDRAW && 0 - // Try to draw - env(draw(borrower, loanKeylet.key, broker.asset(100)), - ter(tecNO_PERMISSION)); -#endif auto const [amountToBeCovered, brokerAcct] = getDefaultInfo(state, broker); @@ -1624,12 +1511,6 @@ class Loan_test : public beast::unit_test::suite verifyLoanStatus(state); -#if LOANDRAW && 0 - // Same error, different check - env(draw(borrower, loanKeylet.key, broker.asset(100)), - ter(tecNO_PERMISSION)); -#endif - // Can't make a payment on it either env(pay(borrower, loanKeylet.key, broker.asset(300)), ter(tecKILLED)); @@ -1689,34 +1570,10 @@ class Loan_test : public beast::unit_test::suite verifyLoanStatus(state); -#if LOANDRAW && 0 - auto const borrowerStartingBalance = - env.balance(borrower, broker.asset); - - // Need to account for fees if the loan is in XRP - PrettyAmount adjustment = broker.asset(0); - if (broker.asset.raw().native()) - { - adjustment = env.current()->fees().base; - } - - // Draw the entire available balance - // Need to create the STAmount directly to avoid - // PrettyAsset scaling. - STAmount const drawAmount{broker.asset, state.assetsAvailable}; - env(draw(borrower, loanKeylet.key, drawAmount)); -#endif env.close(state.startDate + 20s); auto const loanAge = (env.now() - state.startDate).count(); BEAST_EXPECT(loanAge == 30); -#if LOANDRAW && 0 - verifyLoanStatus(state); - BEAST_EXPECT( - env.balance(borrower, broker.asset) == - borrowerStartingBalance + drawAmount - adjustment); -#endif - // Periodic payment amount will consist of // 1. principal outstanding (1000) // 2. interest interest rate (at 12%) @@ -2145,11 +2002,6 @@ class Loan_test : public beast::unit_test::suite env.close(startDate); -#if LOANDRAW && 0 - // Draw the loan - env(draw(lender, loanKeylet.key, broker.asset(1000))); - env.close(); -#endif // Make a payment env(pay(lender, loanKeylet.key, broker.asset(1000))); } @@ -2335,23 +2187,6 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == actualPrincipal); } -#if LOANDRAW && 0 - auto loanDrawTx = - env.json(draw(borrower, keylet.key, STAmount{broker.asset, Number { - 6 - }})); - env(loanDrawTx, ter(tesSUCCESS)); - env.close(); - - if (auto const loan = env.le(keylet); BEAST_EXPECT(loan)) - { - // Verify the payment decreased the principal - BEAST_EXPECT(loan->at(sfPaymentRemaining) == numPayments); - BEAST_EXPECT(loan->at(sfPrincipalRequested) == actualPrincipal); - BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == actualPrincipal); - } -#endif - auto loanPayTx = env.json( pay(borrower, keylet.key, STAmount{broker.asset, serviceFee + 6})); env(loanPayTx, ter(tesSUCCESS)); diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index 13ddb9c16f..d87c955e30 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -835,15 +835,6 @@ manage(AccountID const& account, uint256 const& loanID, std::uint32_t flags); Json::Value del(AccountID const& account, uint256 const& loanID, std::uint32_t flags = 0); -#if loandraw -Json::Value -draw( - AccountID const& account, - uint256 const& loanID, - STAmount const& amount, - std::uint32_t flags = 0); -#endif - Json::Value pay(AccountID const& account, uint256 const& loanID, diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index 0741dc13df..618ecba71c 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -487,24 +487,6 @@ del(AccountID const& account, uint256 const& loanID, std::uint32_t flags) return jv; } -#if LOANDRAW -Json::Value -draw( - AccountID const& account, - uint256 const& loanID, - STAmount const& amount, - std::uint32_t flags) -{ - Json::Value jv; - jv[sfTransactionType] = jss::LoanDraw; - jv[sfAccount] = to_string(account); - jv[sfLoanID] = to_string(loanID); - jv[sfAmount] = amount.getJson(); - jv[sfFlags] = flags; - return jv; -} -#endif - Json::Value pay(AccountID const& account, uint256 const& loanID, diff --git a/src/xrpld/app/tx/detail/LoanDelete.cpp b/src/xrpld/app/tx/detail/LoanDelete.cpp index 5d3fee69f6..218880a41c 100644 --- a/src/xrpld/app/tx/detail/LoanDelete.cpp +++ b/src/xrpld/app/tx/detail/LoanDelete.cpp @@ -101,23 +101,6 @@ LoanDelete::doApply() if (!vaultSle) return tefBAD_LEDGER; // LCOV_EXCL_LINE -#if LOANDRAW - // transfer any remaining funds to the borrower - auto const vaultAsset = vaultSle->at(sfAsset); - auto assetsAvailableProxy = loanSle->at(sfAssetsAvailable); - if (assetsAvailableProxy != 0) - { - if (auto const ter = accountSend( - view, - brokerPseudoAccount, - borrower, - STAmount{vaultAsset, assetsAvailableProxy}, - j_, - WaiveTransferFee::Yes)) - return ter; - } -#endif - // Remove LoanID from Directory of the LoanBroker pseudo-account. if (!view.dirRemove( keylet::ownerDir(brokerPseudoAccount), diff --git a/src/xrpld/app/tx/detail/LoanDraw.cpp b/src/xrpld/app/tx/detail/LoanDraw.cpp deleted file mode 100644 index 2b11f59f9a..0000000000 --- a/src/xrpld/app/tx/detail/LoanDraw.cpp +++ /dev/null @@ -1,172 +0,0 @@ -#if LOANDRAW -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2025 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#include -// -#include - -namespace ripple { - -bool -LoanDraw::checkExtraFeatures(PreflightContext const& ctx) -{ - return checkLendingProtocolDependencies(ctx); -} - -NotTEC -LoanDraw::preflight(PreflightContext const& ctx) -{ - if (ctx.tx[sfLoanID] == beast::zero) - return temINVALID; - - if (ctx.tx[sfAmount] <= beast::zero) - return temBAD_AMOUNT; - - return tesSUCCESS; -} - -TER -LoanDraw::preclaim(PreclaimContext const& ctx) -{ - auto const& tx = ctx.tx; - - auto const account = tx[sfAccount]; - auto const loanID = tx[sfLoanID]; - auto const amount = tx[sfAmount]; - - auto const loanSle = ctx.view.read(keylet::loan(loanID)); - if (!loanSle) - { - JLOG(ctx.j.warn()) << "Loan does not exist."; - return tecNO_ENTRY; - } - - if (loanSle->at(sfBorrower) != account) - { - JLOG(ctx.j.warn()) << "Loan does not belong to the account."; - return tecNO_PERMISSION; - } - - if (loanSle->isFlag(lsfLoanImpaired) || loanSle->isFlag(lsfLoanDefault)) - { - JLOG(ctx.j.warn()) << "Loan is impaired or in default."; - return tecNO_PERMISSION; - } - - if (!hasExpired(ctx.view, loanSle->at(sfStartDate))) - { - JLOG(ctx.j.warn()) << "Loan has not started yet."; - return tecTOO_SOON; - } - - auto const loanBrokerID = loanSle->at(sfLoanBrokerID); - auto const loanBrokerSle = ctx.view.read(keylet::loanbroker(loanBrokerID)); - if (!loanBrokerSle) - { - // This should be impossible - // LCOV_EXCL_START - JLOG(ctx.j.fatal()) << "LoanBroker does not exist."; - return tefBAD_LEDGER; - // LCOV_EXCL_STOP - } - auto const brokerPseudoAccount = loanBrokerSle->at(sfAccount); - auto const vaultID = loanBrokerSle->at(sfVaultID); - auto const vaultSle = ctx.view.read(keylet::vault(vaultID)); - if (!vaultSle) - { - // This should be impossible - // LCOV_EXCL_START - JLOG(ctx.j.fatal()) << "Vault does not exist."; - return tefBAD_LEDGER; - // LCOV_EXCL_STOP - } - auto const asset = vaultSle->at(sfAsset); - - if (amount.asset() != asset) - { - JLOG(ctx.j.warn()) << "Draw amount does not match the Vault asset."; - return tecWRONG_ASSET; - } - - if (loanSle->at(sfAssetsAvailable) < amount) - { - JLOG(ctx.j.warn()) << "Loan does not have enough assets available."; - return tecINSUFFICIENT_FUNDS; - } - - if (auto const ret = checkFrozen(ctx.view, brokerPseudoAccount, asset)) - { - JLOG(ctx.j.warn()) << "Loan Broker pseudo-account is frozen."; - return ret; - } - if (auto const ret = checkDeepFrozen(ctx.view, account, asset)) - { - JLOG(ctx.j.warn()) - << "Borrower account cannot receive funds (deep frozen)."; - return ret; - } - - if (hasExpired(ctx.view, loanSle->at(sfNextPaymentDueDate))) - { - JLOG(ctx.j.warn()) << "Loan payment is overdue."; - return tecNO_PERMISSION; - } - - return tesSUCCESS; -} - -TER -LoanDraw::doApply() -{ - auto const& tx = ctx_.tx; - auto& view = ctx_.view(); - - auto const amount = tx[sfAmount]; - - auto const loanID = tx[sfLoanID]; - auto const loanSle = view.peek(keylet::loan(loanID)); - if (!loanSle) - return tefBAD_LEDGER; // LCOV_EXCL_LINE - - auto const brokerID = loanSle->at(sfLoanBrokerID); - auto const brokerSle = view.peek(keylet::loanbroker(brokerID)); - if (!brokerSle) - return tefBAD_LEDGER; // LCOV_EXCL_LINE - auto const brokerPseudoAccount = brokerSle->at(sfAccount); - - if (auto const ter = accountSend( - view, - brokerPseudoAccount, - account_, - amount, - j_, - WaiveTransferFee::Yes)) - return ter; - - loanSle->at(sfAssetsAvailable) -= amount; - view.update(loanSle); - - return tesSUCCESS; -} - -//------------------------------------------------------------------------------ - -} // namespace ripple -#endif diff --git a/src/xrpld/app/tx/detail/LoanDraw.h b/src/xrpld/app/tx/detail/LoanDraw.h deleted file mode 100644 index e76436635b..0000000000 --- a/src/xrpld/app/tx/detail/LoanDraw.h +++ /dev/null @@ -1,55 +0,0 @@ -#if LOANDRAW -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2025 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#ifndef RIPPLE_TX_LOANDRAW_H_INCLUDED -#define RIPPLE_TX_LOANDRAW_H_INCLUDED - -#include - -namespace ripple { - -class LoanDraw : public Transactor -{ -public: - static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; - - explicit LoanDraw(ApplyContext& ctx) : Transactor(ctx) - { - } - - static bool - checkExtraFeatures(PreflightContext const& ctx); - - static NotTEC - preflight(PreflightContext const& ctx); - - static TER - preclaim(PreclaimContext const& ctx); - - TER - doApply() override; -}; - -//------------------------------------------------------------------------------ - -} // namespace ripple - -#endif -#endif diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index c35a742c1a..99622d26ed 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -150,15 +150,6 @@ LoanManage::defaultLoan( auto brokerDebtTotalProxy = brokerSle->at(sfDebtTotal); auto const totalDefaultAmount = principalOutstanding + interestOutstanding; -#if LOANDRAW - // The default Amount equals the outstanding principal and interest, - // excluding any funds unclaimed by the Borrower. - auto loanAssetsAvailableProxy = loanSle->at(sfAssetsAvailable); - auto const defaultAmount = totalDefaultAmount - loanAssetsAvailableProxy; -#else - // TODO: get rid of this and just use totalDefaultAmount - auto const defaultAmount = totalDefaultAmount; -#endif // Apply the First-Loss Capital to the Default Amount TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)}; TenthBips32 const coverRateLiquidation{ @@ -170,14 +161,10 @@ LoanManage::defaultLoan( tenthBipsOfValue( brokerDebtTotalProxy.value(), coverRateMinimum), coverRateLiquidation), - defaultAmount), + totalDefaultAmount), originalPrincipalRequested); -#if LOANDRAW - auto const returnToVault = defaultCovered + loanAssetsAvailableProxy; -#else - auto const returnToVault = defaultCovered; -#endif - auto const vaultDefaultAmount = defaultAmount - defaultCovered; + + auto const vaultDefaultAmount = totalDefaultAmount - defaultCovered; // Update the Vault object: @@ -196,7 +183,7 @@ LoanManage::defaultLoan( vaultAssetsTotalProxy -= vaultDefaultAmount; // Increase the Asset Available of the Vault by liquidated First-Loss // Capital and any unclaimed funds amount: - vaultAssetsAvailableProxy += returnToVault; + vaultAssetsAvailableProxy += defaultCovered; if (*vaultAssetsAvailableProxy > *vaultAssetsTotalProxy && !vaultAsset.native() && vaultAsset.holds()) { @@ -275,9 +262,6 @@ LoanManage::defaultLoan( // Update the Loan object: loanSle->setFlag(lsfLoanDefault); loanSle->at(sfPaymentRemaining) = 0; -#if LOANDRAW - loanAssetsAvailableProxy = 0; -#endif loanSle->at(sfPrincipalOutstanding) = 0; view.update(loanSle); @@ -287,7 +271,7 @@ LoanManage::defaultLoan( view, brokerSle->at(sfAccount), vaultSle->at(sfAccount), - STAmount{vaultAsset, returnToVault}, + STAmount{vaultAsset, defaultCovered}, j, WaiveTransferFee::Yes); } From 63edf035a6c1b32a53e9c973ce5d6e6d539dec5e Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 1 Oct 2025 20:09:48 -0400 Subject: [PATCH 113/291] Start converting Loans to use fixed payments and track value - Not expected to build --- include/xrpl/protocol/STAmount.h | 36 +-- .../xrpl/protocol/detail/ledger_entries.macro | 28 +- include/xrpl/protocol/detail/sfields.macro | 7 +- src/libxrpl/protocol/STAmount.cpp | 24 +- src/test/app/Loan_test.cpp | 21 +- src/test/protocol/STParsedJSON_test.cpp | 40 +-- src/xrpld/app/misc/LendingHelpers.h | 292 ++++++++++-------- src/xrpld/app/misc/detail/LendingHelpers.cpp | 12 +- src/xrpld/app/tx/detail/InvariantCheck.cpp | 2 +- .../app/tx/detail/LoanBrokerCoverWithdraw.cpp | 2 +- src/xrpld/app/tx/detail/LoanManage.cpp | 8 +- src/xrpld/app/tx/detail/LoanPay.cpp | 16 +- src/xrpld/app/tx/detail/LoanSet.cpp | 170 +++++----- 13 files changed, 353 insertions(+), 305 deletions(-) diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index d3f67da4af..560059e49a 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -66,15 +66,15 @@ public: static int const cMaxOffset = 80; // Maximum native value supported by the code - static std::uint64_t const cMinValue = 1000000000000000ull; - static std::uint64_t const cMaxValue = 9999999999999999ull; - static std::uint64_t const cMaxNative = 9000000000000000000ull; + static std::uint64_t const cMinValue = 1'000'000'000'000'000ull; + static std::uint64_t const cMaxValue = 9'999'999'999'999'999ull; + static std::uint64_t const cMaxNative = 9'000'000'000'000'000'000ull; // Max native value on network. - static std::uint64_t const cMaxNativeN = 100000000000000000ull; - static std::uint64_t const cIssuedCurrency = 0x8000000000000000ull; - static std::uint64_t const cPositive = 0x4000000000000000ull; - static std::uint64_t const cMPToken = 0x2000000000000000ull; + static std::uint64_t const cMaxNativeN = 100'000'000'000'000'000ull; + static std::uint64_t const cIssuedCurrency = 0x8'000'000'000'000'000ull; + static std::uint64_t const cPositive = 0x4'000'000'000'000'000ull; + static std::uint64_t const cMPToken = 0x2'000'000'000'000'000ull; static std::uint64_t const cValueMask = ~(cPositive | cMPToken); static std::uint64_t const uRateOne; @@ -695,21 +695,22 @@ divRoundStrict( std::uint64_t getRate(STAmount const& offerOut, STAmount const& offerIn); -/** Round an arbitrary precision Amount to the precision of a reference Amount. +/** Round an arbitrary precision Amount to the precision of an STAmount that has + * a given exponent. * * This is used to ensure that calculations involving IOU amounts do not collect * dust beyond the precision of the reference value. * * @param value The value to be rounded - * @param referenceValue A reference value to establish the precision limit of - * `value`. Should be larger than `value`. + * @param scale An exponent value to establish the precision limit of + * `value`. Should be larger than `value.exponent()`. * @param rounding Optional Number rounding mode * */ STAmount -roundToReference( - STAmount const value, - STAmount referenceValue, +roundToScale( + STAmount value, + std::int32_t scale, Number::rounding_mode rounding = Number::getround()); /** Round an arbitrary precision Number to the precision of a given Asset. @@ -720,9 +721,8 @@ roundToReference( * * @param asset The relevant asset * @param value The value to be rounded - * @param referenceValue Only relevant to IOU assets. A reference value to - * establish the precision limit of `value`. Should be larger than - * `value`. + * @param scale Only relevant to IOU assets. An exponent value to establish the + * precision limit of `value`. Should be larger than `value.exponent()`. * @param rounding Optional Number rounding mode */ template @@ -730,7 +730,7 @@ Number roundToAsset( A const& asset, Number const& value, - Number const& referenceValue, + std::int32_t scale, Number::rounding_mode rounding = Number::getround()) { NumberRoundModeGuard mg(rounding); @@ -739,7 +739,7 @@ roundToAsset( return ret; // Not that the ctor will round integral types (XRP, MPT) via canonicalize, // so no extra work is needed for those. - return roundToReference(ret, STAmount{asset, referenceValue}); + return roundToScale(ret, scale); } //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index bfecd0c92f..99e16e6b70 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -548,25 +548,29 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({ {sfLoanBrokerID, soeREQUIRED}, {sfLoanSequence, soeREQUIRED}, {sfBorrower, soeREQUIRED}, - {sfLoanOriginationFee, soeREQUIRED}, - {sfLoanServiceFee, soeREQUIRED}, - {sfLatePaymentFee, soeREQUIRED}, - {sfClosePaymentFee, soeREQUIRED}, - {sfOverpaymentFee, soeREQUIRED}, - {sfInterestRate, soeREQUIRED}, - {sfLateInterestRate, soeREQUIRED}, - {sfCloseInterestRate, soeREQUIRED}, - {sfOverpaymentInterestRate, soeREQUIRED}, + {sfLoanOriginationFee, soeDEFAULT}, + {sfLoanServiceFee, soeDEFAULT}, + {sfLatePaymentFee, soeDEFAULT}, + {sfClosePaymentFee, soeDEFAULT}, + {sfOverpaymentFee, soeDEFAULT}, + {sfInterestRate, soeDEFAULT}, + {sfLateInterestRate, soeDEFAULT}, + {sfCloseInterestRate, soeDEFAULT}, + {sfOverpaymentInterestRate, soeDEFAULT}, {sfStartDate, soeREQUIRED}, {sfPaymentInterval, soeREQUIRED}, {sfGracePeriod, soeREQUIRED}, + {sfPeriodicPayment, soeREQUIRED}, {sfPreviousPaymentDate, soeREQUIRED}, {sfNextPaymentDueDate, soeREQUIRED}, {sfPaymentRemaining, soeREQUIRED}, {sfPrincipalOutstanding, soeREQUIRED}, - // Save the original request amount for rounding / scaling of - // other computations, particularly for IOUs - {sfPrincipalRequested, soeREQUIRED}, + {sfTotalValueOutstanding, soeDEFAULT}, + // Based on the original principal borrowed, used for + // rounding calculated values so they are all on a + // consistent scale - that is, they all have the same + // number of decimal places after the decimal point. + {sfLoanScale, soeDEFAULT}, })) #undef EXPAND diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index af1429461f..3c3dda69e0 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -239,12 +239,11 @@ TYPED_SFIELD(sfLatePaymentFee, NUMBER, 11) TYPED_SFIELD(sfClosePaymentFee, NUMBER, 12) TYPED_SFIELD(sfPrincipalOutstanding, NUMBER, 13) TYPED_SFIELD(sfPrincipalRequested, NUMBER, 14) +TYPED_SFIELD(sfTotalValueOutstanding, NUMBER, 15) +TYPED_SFIELD(sfPeriodicPayment, NUMBER, 16) // int32 -// NOTE: Do not use `sfDummyInt32`. It's so far the only use of INT32 -// in this file and has been defined here for test only. -// TODO: Replace `sfDummyInt32` with actually useful field. -TYPED_SFIELD(sfDummyInt32, INT32, 1) // for tests only +TYPED_SFIELD(sfLoanScale, INT32, 1) // currency amount (common) TYPED_SFIELD(sfAmount, AMOUNT, 1) diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 15524d605a..74f4576831 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -1510,32 +1510,28 @@ canonicalizeRoundStrict( } STAmount -roundToReference( - STAmount const value, - STAmount referenceValue, - Number::rounding_mode rounding) +roundToScale(STAmount value, std::int32_t scale, Number::rounding_mode rounding) { // Nothing to do for intgral types. if (value.asset().native() || !value.asset().holds()) return value; - // If the value is greater than or equal to the referenceValue (ignoring - // sign), then rounding will do nothing, so just return the value. - if (value.exponent() > referenceValue.exponent() || - (value.exponent() == referenceValue.exponent() && - value.mantissa() >= referenceValue.mantissa())) + // If the value's exponent is greater than or equal to the scale, then + // rounding will do nothing, and might even lose precision, so just return + // the value. + if (value.exponent() >= scale) return value; - if (referenceValue.negative() != value.negative()) - referenceValue.negate(); + STAmount referenceValue{ + value.asset(), STAmount::cMinValue, scale, value.negative()}; NumberRoundModeGuard mg(rounding); // With an IOU, the total will be truncated to the precision of the // larger value: referenceValue - STAmount const total = referenceValue + value; + value += referenceValue; // Remove the reference value, and we're left with the rounded value. - STAmount const result = total - referenceValue; - return result; + value -= referenceValue; + return value; } namespace { diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 84e45e4ad5..8535ed95f5 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -111,7 +111,7 @@ class Loan_test : public beast::unit_test::suite NetClock::time_point startDate = {}; std::uint32_t nextPaymentDate = 0; std::uint32_t paymentRemaining = 0; - Number const principalRequested = 0; + std::int32_t const loanScale = 0; Number principalOutstanding = 0; std::uint32_t flags = 0; std::uint32_t paymentInterval = 0; @@ -221,7 +221,7 @@ class Loan_test : public beast::unit_test::suite std::uint32_t nextPaymentDate, std::uint32_t paymentRemaining, Number const& principalRequested, - Number const& principalOutstanding, + Number const& loanScale, std::uint32_t flags) const { using namespace jtx; @@ -233,13 +233,12 @@ class Loan_test : public beast::unit_test::suite loan->at(sfNextPaymentDueDate) == nextPaymentDate); env.test.BEAST_EXPECT( loan->at(sfPaymentRemaining) == paymentRemaining); - env.test.BEAST_EXPECT( - loan->at(sfPrincipalRequested) == principalRequested); + env.test.BEAST_EXPECT(loan->at(sfLoanScale) == loanScale); env.test.BEAST_EXPECT( loan->at(sfPrincipalOutstanding) == principalOutstanding); env.test.BEAST_EXPECT( - loan->at(sfPrincipalRequested) == - broker.asset(loanAmount).value()); + loan->at(sfLoanScale) == + broker.asset(loanAmount).value().exponent()); env.test.BEAST_EXPECT(loan->at(sfFlags) == flags); auto const interestRate = TenthBips32{loan->at(sfInterestRate)}; @@ -369,7 +368,7 @@ class Loan_test : public beast::unit_test::suite .startDate = tp{d{loan->at(sfStartDate)}}, .nextPaymentDate = loan->at(sfNextPaymentDueDate), .paymentRemaining = loan->at(sfPaymentRemaining), - .principalRequested = loan->at(sfPrincipalRequested), + .loanScale = loan->at(sfLoanScale), .principalOutstanding = loan->at(sfPrincipalOutstanding), .flags = loan->at(sfFlags), .paymentInterval = loan->at(sfPaymentInterval), @@ -611,7 +610,7 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT( loan->at(sfNextPaymentDueDate) == startDate + interval); BEAST_EXPECT(loan->at(sfPaymentRemaining) == total); - BEAST_EXPECT(loan->at(sfPrincipalRequested) == principalRequest); + BEAST_EXPECT(loan->at(sfLoanScale) == principalRequest.exponent()); BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == principalRequest); } @@ -1991,7 +1990,7 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT(loan[sfPaymentRemaining] == 1); BEAST_EXPECT(loan[sfPreviousPaymentDate] == 0); BEAST_EXPECT(loan[sfPrincipalOutstanding] == "1000000000"); - BEAST_EXPECT(loan[sfPrincipalRequested] == "1000000000"); + BEAST_EXPECT(loan[sfLoanScale] == 0); BEAST_EXPECT( loan[sfStartDate].asUInt() == startDate.time_since_epoch().count()); @@ -2183,7 +2182,7 @@ class Loan_test : public beast::unit_test::suite { // Verify the payment decreased the principal BEAST_EXPECT(loan->at(sfPaymentRemaining) == numPayments); - BEAST_EXPECT(loan->at(sfPrincipalRequested) == actualPrincipal); + BEAST_EXPECT(loan->at(sfLoanScale) == actualPrincipal.exponent()); BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == actualPrincipal); } @@ -2196,7 +2195,7 @@ class Loan_test : public beast::unit_test::suite { // Verify the payment decreased the principal BEAST_EXPECT(loan->at(sfPaymentRemaining) == numPayments - 1); - BEAST_EXPECT(loan->at(sfPrincipalRequested) == actualPrincipal); + BEAST_EXPECT(loan->at(sfLoanScale) == actualPrincipal.exponent()); BEAST_EXPECT( loan->at(sfPrincipalOutstanding) == actualPrincipal - 1); } diff --git a/src/test/protocol/STParsedJSON_test.cpp b/src/test/protocol/STParsedJSON_test.cpp index 1e1e1fb9f4..12408c272e 100644 --- a/src/test/protocol/STParsedJSON_test.cpp +++ b/src/test/protocol/STParsedJSON_test.cpp @@ -743,63 +743,63 @@ class STParsedJSON_test : public beast::unit_test::suite { Json::Value j; int const minInt32 = -2147483648; - j[sfDummyInt32] = minInt32; + j[sfLoanScale] = minInt32; STParsedJSONObject obj("Test", j); BEAST_EXPECT(obj.object.has_value()); - if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) - BEAST_EXPECT(obj.object->getFieldI32(sfDummyInt32) == minInt32); + if (BEAST_EXPECT(obj.object->isFieldPresent(sfLoanScale))) + BEAST_EXPECT(obj.object->getFieldI32(sfLoanScale) == minInt32); } // max value { Json::Value j; int const maxInt32 = 2147483647; - j[sfDummyInt32] = maxInt32; + j[sfLoanScale] = maxInt32; STParsedJSONObject obj("Test", j); BEAST_EXPECT(obj.object.has_value()); - if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) - BEAST_EXPECT(obj.object->getFieldI32(sfDummyInt32) == maxInt32); + if (BEAST_EXPECT(obj.object->isFieldPresent(sfLoanScale))) + BEAST_EXPECT(obj.object->getFieldI32(sfLoanScale) == maxInt32); } // max uint value { Json::Value j; unsigned int const maxUInt32 = 2147483647u; - j[sfDummyInt32] = maxUInt32; + j[sfLoanScale] = maxUInt32; STParsedJSONObject obj("Test", j); BEAST_EXPECT(obj.object.has_value()); - if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) + if (BEAST_EXPECT(obj.object->isFieldPresent(sfLoanScale))) BEAST_EXPECT( - obj.object->getFieldI32(sfDummyInt32) == + obj.object->getFieldI32(sfLoanScale) == static_cast(maxUInt32)); } // Test with string value { Json::Value j; - j[sfDummyInt32] = "2147483647"; + j[sfLoanScale] = "2147483647"; STParsedJSONObject obj("Test", j); BEAST_EXPECT(obj.object.has_value()); - if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) + if (BEAST_EXPECT(obj.object->isFieldPresent(sfLoanScale))) BEAST_EXPECT( - obj.object->getFieldI32(sfDummyInt32) == 2147483647u); + obj.object->getFieldI32(sfLoanScale) == 2147483647u); } // Test with string negative value { Json::Value j; int value = -2147483648; - j[sfDummyInt32] = std::to_string(value); + j[sfLoanScale] = std::to_string(value); STParsedJSONObject obj("Test", j); BEAST_EXPECT(obj.object.has_value()); - if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) - BEAST_EXPECT(obj.object->getFieldI32(sfDummyInt32) == value); + if (BEAST_EXPECT(obj.object->isFieldPresent(sfLoanScale))) + BEAST_EXPECT(obj.object->getFieldI32(sfLoanScale) == value); } // Test out of range value for int32 (negative) { Json::Value j; - j[sfDummyInt32] = "-2147483649"; + j[sfLoanScale] = "-2147483649"; STParsedJSONObject obj("Test", j); BEAST_EXPECT(!obj.object.has_value()); } @@ -807,7 +807,7 @@ class STParsedJSON_test : public beast::unit_test::suite // Test out of range value for int32 (positive) { Json::Value j; - j[sfDummyInt32] = 2147483648u; + j[sfLoanScale] = 2147483648u; STParsedJSONObject obj("Test", j); BEAST_EXPECT(!obj.object.has_value()); } @@ -815,7 +815,7 @@ class STParsedJSON_test : public beast::unit_test::suite // Test string value out of range { Json::Value j; - j[sfDummyInt32] = "2147483648"; + j[sfLoanScale] = "2147483648"; STParsedJSONObject obj("Test", j); BEAST_EXPECT(!obj.object.has_value()); } @@ -823,7 +823,7 @@ class STParsedJSON_test : public beast::unit_test::suite // Test bad_type (arrayValue) { Json::Value j; - j[sfDummyInt32] = Json::Value(Json::arrayValue); + j[sfLoanScale] = Json::Value(Json::arrayValue); STParsedJSONObject obj("Test", j); BEAST_EXPECT(!obj.object.has_value()); } @@ -831,7 +831,7 @@ class STParsedJSON_test : public beast::unit_test::suite // Test bad_type (objectValue) { Json::Value j; - j[sfDummyInt32] = Json::Value(Json::objectValue); + j[sfLoanScale] = Json::Value(Json::objectValue); STParsedJSONObject obj("Test", j); BEAST_EXPECT(!obj.object.has_value()); } diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index cea123d75d..5f28ed9120 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -40,22 +40,100 @@ loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval); Number loanPeriodicPayment( - Number principalOutstanding, - Number periodicRate, + Number const& principalOutstanding, + Number const& periodicRate, std::uint32_t paymentsRemaining); Number loanPeriodicPayment( - Number principalOutstanding, + Number const& principalOutstanding, TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t paymentsRemaining); +Number +loanLatePaymentInterest( + Number const& principalOutstanding, + TenthBips32 lateInterestRate, + NetClock::time_point parentCloseTime, + std::uint32_t startDate, + std::uint32_t prevPaymentDate); + +Number +loanAccruedInterest( + Number const& principalOutstanding, + Number const& periodicRate, + NetClock::time_point parentCloseTime, + std::uint32_t startDate, + std::uint32_t prevPaymentDate, + std::uint32_t paymentInterval); + +inline Number +minusManagementFee(Number const& value, TenthBips32 managementFeeRate) +{ + return tenthBipsOfValue(value, tenthBipsPerUnity - managementFeeRate); +} + +} // namespace detail + +template +Number +valueMinusManagementFee( + A const& asset, + Number const& value, + TenthBips32 managementFeeRate, + std::int32_t scale) +{ + return roundToAsset( + asset, detail::minusManagementFee(value, managementFeeRate), scale); +} + +Number +loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval) +{ + return detail::loanPeriodicRate(interestRate, paymentInterval); +} + +template +Number +loanPeriodicPayment( + A const& asset, + Number const& principalOutstanding, + Number const& periodicRate, + std::uint32_t paymentsRemaining, + std::int32_t scale) +{ + return roundToAsset( + asset, + detail::loanPeriodicPayment( + principalOutstanding, periodicRate, paymentsRemaining), + scale, + Number::upward); +} + +template +Number +loanPeriodicPayment( + A const& asset, + Number const& principalOutstanding, + TenthBips32 interestRate, + std::uint32_t paymentInterval, + std::uint32_t paymentsRemaining, + std::int32_t scale) +{ + loanPeriodicPayment( + asset, + principalOutstanding, + loanPeriodicRate(interestRate, paymentInterval), + paymentsRemaining, + scale); +} + template Number loanTotalValueOutstanding( A asset, - Number const& originalPrincipal, + std::int32_t scale, Number const& periodicPayment, std::uint32_t paymentsRemaining) { @@ -66,7 +144,7 @@ loanTotalValueOutstanding( * Value Calculation), specifically "totalValueOutstanding = ..." */ periodicPayment * paymentsRemaining, - originalPrincipal, + scale, Number::upward); } @@ -74,7 +152,7 @@ template Number loanTotalValueOutstanding( A asset, - Number const& originalPrincipal, + std::int32_t scale, Number const& principalOutstanding, TenthBips32 interestRate, std::uint32_t paymentInterval, @@ -86,19 +164,21 @@ loanTotalValueOutstanding( */ return loanTotalValueOutstanding( asset, - originalPrincipal, + scale, loanPeriodicPayment( + asset, principalOutstanding, interestRate, paymentInterval, - paymentsRemaining), + paymentsRemaining, + scale), paymentsRemaining); } inline Number loanTotalInterestOutstanding( - Number principalOutstanding, - Number totalValueOutstanding) + Number const& principalOutstanding, + Number const& totalValueOutstanding) { /* * This formula is from the XLS-66 spec, section 3.2.4.2 (Total Loan @@ -111,7 +191,7 @@ template Number loanTotalInterestOutstanding( A asset, - Number const& originalPrincipal, + std::int32_t scale, Number const& principalOutstanding, TenthBips32 interestRate, std::uint32_t paymentInterval, @@ -125,94 +205,47 @@ loanTotalInterestOutstanding( principalOutstanding, loanTotalValueOutstanding( asset, - originalPrincipal, + scale, principalOutstanding, interestRate, paymentInterval, paymentsRemaining)); } -Number -loanLatePaymentInterest( - Number principalOutstanding, - TenthBips32 lateInterestRate, - NetClock::time_point parentCloseTime, - std::uint32_t startDate, - std::uint32_t prevPaymentDate); - -Number -loanAccruedInterest( - Number principalOutstanding, - Number periodicRate, - NetClock::time_point parentCloseTime, - std::uint32_t startDate, - std::uint32_t prevPaymentDate, - std::uint32_t paymentInterval); - -inline Number -minusManagementFee(Number value, TenthBips32 managementFeeRate) -{ - return tenthBipsOfValue(value, tenthBipsPerUnity - managementFeeRate); -} - -} // namespace detail - template Number -valueMinusManagementFee( +loanInterestOutstandingMinusFee( A const& asset, - Number const& value, + Number const& totalInterestOutstanding, TenthBips32 managementFeeRate, - Number const& originalPrincipal) + std::int32_t scale) { - return roundToAsset( - asset, - detail::minusManagementFee(value, managementFeeRate), - originalPrincipal); + return valueMinusManagementFee( + asset, totalInterestOutstanding, managementFeeRate, scale); } template Number loanInterestOutstandingMinusFee( A const& asset, - Number const& originalPrincipal, + std::int32_t scale, Number const& principalOutstanding, TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t paymentsRemaining, TenthBips32 managementFeeRate) { - return valueMinusManagementFee( + return loanInterestOutstandingMinusFee( asset, - detail::loanTotalInterestOutstanding( + loanTotalInterestOutstanding( asset, - originalPrincipal, + scale, principalOutstanding, interestRate, paymentInterval, paymentsRemaining), managementFeeRate, - originalPrincipal); -} - -template -Number -loanPeriodicPayment( - A const& asset, - Number const& principalOutstanding, - TenthBips32 interestRate, - std::uint32_t paymentInterval, - std::uint32_t paymentsRemaining, - Number const& originalPrincipal) -{ - return roundToAsset( - asset, - detail::loanPeriodicPayment( - principalOutstanding, - interestRate, - paymentInterval, - paymentsRemaining), - originalPrincipal); + scale); } template @@ -224,7 +257,7 @@ loanLatePaymentInterest( NetClock::time_point parentCloseTime, std::uint32_t startDate, std::uint32_t prevPaymentDate, - Number const& originalPrincipal) + Number const& scale) { return roundToAsset( asset, @@ -234,7 +267,15 @@ loanLatePaymentInterest( parentCloseTime, startDate, prevPaymentDate), - originalPrincipal); + scale); +} + +template +bool +rounded(A const& asset, Number const& value, std::int32_t scale) +{ + return roundToAsset(asset, value, scale, Number::downward) == value && + roundToAsset(asset, value, scale, Number::upward) == value; } struct PaymentParts @@ -246,9 +287,10 @@ struct PaymentParts template PaymentParts -computePeriodicPaymentParts( +computePaymentParts( A const& asset, - Number const& originalPrincipal, + std::int32_t scale, + Number const& totalValueOutstanding, Number const& principalOutstanding, Number const& periodicPaymentAmount, Number const& serviceFee, @@ -259,16 +301,17 @@ computePeriodicPaymentParts( * This function is derived from the XLS-66 spec, section 3.2.4.1.1 (Regular * Payment) */ - Number const roundedFee = - roundToAsset(asset, serviceFee, originalPrincipal); - if (paymentRemaining == 1) + XRPL_ASSERT_PARTS( + rounded(asset, totalValueOutstanding, scale) && + rounded(asset, principalOutstanding, scale) && + rounded(asset, periodicPaymentAmount, scale), + "ripple::computePaymentParts", + "Asset values are rounded"); + Number const roundedFee = roundToAsset(asset, serviceFee, scale); + if (paymentRemaining == 1 || periodicPaymentAmount > totalValueOutstanding) { // If there's only one payment left, we need to pay off the principal. - Number const interest = roundToAsset( - asset, - periodicPaymentAmount - principalOutstanding, - originalPrincipal, - Number::upward); + Number const interest = totalValueOutstanding - principalOutstanding; return { .interest = interest, .principal = principalOutstanding, @@ -284,10 +327,7 @@ computePeriodicPaymentParts( * Because those values deal with funds, they need to be rounded. */ Number const interest = roundToAsset( - asset, - principalOutstanding * periodicRate, - originalPrincipal, - Number::upward); + asset, principalOutstanding * periodicRate, scale, Number::upward); XRPL_ASSERT( interest >= 0, "ripple::detail::computePeriodicPayment : valid interest"); @@ -296,8 +336,8 @@ computePeriodicPaymentParts( // payment amount, ensuring that some principal is paid regardless of any // other results. auto const roundedPayment = [&]() { - auto roundedPayment = roundToAsset( - asset, periodicPaymentAmount, originalPrincipal, Number::upward); + auto roundedPayment = + roundToAsset(asset, periodicPaymentAmount, scale, Number::upward); if (roundedPayment > interest) return roundedPayment; auto newPayment = roundedPayment; @@ -310,21 +350,21 @@ computePeriodicPaymentParts( { // Non-integral types: IOU. Add "dust" that will not be lost in // rounding. - auto const epsilon = Number{1, originalPrincipal.exponent() - 14}; + auto const epsilon = Number{1, scale - 14}; newPayment += epsilon; } - roundedPayment = roundToAsset(asset, newPayment, originalPrincipal); + roundedPayment = roundToAsset(asset, newPayment, scale); XRPL_ASSERT_PARTS( roundedPayment == newPayment, - "ripple::computePeriodicPaymentParts", + "ripple::computePaymentParts", "epsilon preserved in rounding"); return roundedPayment; }(); Number const principal = - roundToAsset(asset, roundedPayment - interest, originalPrincipal); + roundToAsset(asset, roundedPayment - interest, scale); XRPL_ASSERT_PARTS( principal > 0 && principal <= principalOutstanding, - "ripple::computePeriodicPaymentParts", + "ripple::computePaymentParts", "valid principal"); return {.interest = interest, .principal = principal, .fee = roundedFee}; @@ -375,7 +415,7 @@ handleLatePayment( std::uint32_t const startDate, std::uint32_t const paymentInterval, TenthBips32 const lateInterestRate, - Number const& originalPrincipalRequested, + std::int32_t loanScale, Number const& latePaymentFee, STAmount const& amount, beast::Journal j) @@ -393,7 +433,7 @@ handleLatePayment( view.parentCloseTime(), startDate, prevPaymentDateProxy, - originalPrincipalRequested); + loanScale); XRPL_ASSERT( latePaymentInterest >= 0, "ripple::handleLatePayment : valid late interest"); @@ -446,7 +486,7 @@ handleFullPayment( std::uint32_t const startDate, std::uint32_t const paymentInterval, TenthBips32 const closeInterestRate, - Number const& originalPrincipalRequested, + std::int32_t loanScale, Number const& totalInterestOutstanding, Number const& periodicRate, Number const& closePaymentFee, @@ -468,14 +508,14 @@ handleFullPayment( startDate, prevPaymentDateProxy, paymentInterval), - originalPrincipalRequested); + loanScale); XRPL_ASSERT( accruedInterest >= 0, "ripple::handleFullPayment : valid accrued interest"); auto const prepaymentPenalty = roundToAsset( asset, tenthBipsOfValue(principalOutstandingProxy.value(), closeInterestRate), - originalPrincipalRequested); + loanScale); XRPL_ASSERT( prepaymentPenalty >= 0, "ripple::handleFullPayment : valid prepayment " @@ -521,7 +561,8 @@ loanMakePayment( * This function is an implementation of the XLS-66 spec, * section 3.2.4.3 (Transaction Pseudo-code) */ - Number const originalPrincipalRequested = loan->at(sfPrincipalRequested); + std::int32_t const loanScale = loan->at(sfLoanScale); + auto totalValueOutstandingProxy = loan->at(sfTotalValueOutstanding); auto principalOutstandingProxy = loan->at(sfPrincipalOutstanding); bool const allowOverpayment = loan->isFlag(lsfLoanOverpayment); @@ -533,8 +574,8 @@ loanMakePayment( Number const serviceFee = loan->at(sfLoanServiceFee); Number const latePaymentFee = loan->at(sfLatePaymentFee); - Number const closePaymentFee = roundToAsset( - asset, loan->at(sfClosePaymentFee), originalPrincipalRequested); + Number const closePaymentFee = + roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale); TenthBips32 const overpaymentFee{loan->at(sfOverpaymentFee)}; std::uint32_t const paymentInterval = loan->at(sfPaymentInterval); @@ -567,26 +608,23 @@ loanMakePayment( periodicPaymentAmount > 0, "ripple::computePeriodicPayment : valid payment"); - auto const periodic = computePeriodicPaymentParts( + auto const periodic = computePaymentParts( asset, - originalPrincipalRequested, + loanScale, + totalValueOutstandingProxy, principalOutstandingProxy, periodicPaymentAmount, serviceFee, periodicRate, paymentRemainingProxy); - Number const totalValueOutstanding = detail::loanTotalValueOutstanding( - asset, - originalPrincipalRequested, - periodicPaymentAmount, - paymentRemainingProxy); + Number const totalValueOutstanding = loanTotalValueOutstanding( + asset, loanScale, periodicPaymentAmount, paymentRemainingProxy); XRPL_ASSERT( totalValueOutstanding > 0, "ripple::loanMakePayment : valid total value"); - Number const totalInterestOutstanding = - detail::loanTotalInterestOutstanding( - principalOutstandingProxy, totalValueOutstanding); + Number const totalInterestOutstanding = loanTotalInterestOutstanding( + principalOutstandingProxy, totalValueOutstanding); XRPL_ASSERT_PARTS( totalInterestOutstanding >= 0, "ripple::loanMakePayment", @@ -612,7 +650,7 @@ loanMakePayment( startDate, paymentInterval, lateInterestRate, - originalPrincipalRequested, + loanScale, latePaymentFee, amount, j)) @@ -631,7 +669,7 @@ loanMakePayment( startDate, paymentInterval, closeInterestRate, - originalPrincipalRequested, + loanScale, totalInterestOutstanding, periodicRate, closePaymentFee, @@ -682,9 +720,10 @@ loanMakePayment( { // Only do the work if we need to if (!future) - future = computePeriodicPaymentParts( + future = computePaymentParts( asset, - originalPrincipalRequested, + loanScale, + totalValueOutstandingProxy, principalOutstandingProxy, periodicPaymentAmount, serviceFee, @@ -707,9 +746,9 @@ loanMakePayment( Number totalFeePaid = serviceFee * fullPeriodicPayments; - Number const newInterest = detail::loanTotalInterestOutstanding( + Number const newInterest = loanTotalInterestOutstanding( asset, - originalPrincipalRequested, + loanScale, principalOutstandingProxy, interestRate, paymentInterval, @@ -725,20 +764,18 @@ loanMakePayment( principalOutstandingProxy.value(), amount - (totalPrincipalPaid + totalInterestPaid + totalFeePaid)); - if (roundToAsset(asset, overpayment, originalPrincipalRequested) > 0) + if (roundToAsset(asset, overpayment, loanScale) > 0) { Number const interestPortion = roundToAsset( asset, tenthBipsOfValue(overpayment, overpaymentInterestRate), - originalPrincipalRequested); + loanScale); Number const feePortion = roundToAsset( asset, tenthBipsOfValue(overpayment, overpaymentFee), - originalPrincipalRequested); + loanScale); Number const remainder = roundToAsset( - asset, - overpayment - interestPortion - feePortion, - originalPrincipalRequested); + asset, overpayment - interestPortion - feePortion, loanScale); // Don't process an overpayment if the whole amount (or more!) // gets eaten by fees @@ -760,20 +797,17 @@ loanMakePayment( // Check the final results are rounded, to double-check that the // intermediate steps were rounded. XRPL_ASSERT( - roundToAsset(asset, totalPrincipalPaid, originalPrincipalRequested) == + roundToAsset(asset, totalPrincipalPaid, loanScale) == totalPrincipalPaid, "ripple::loanMakePayment : totalPrincipalPaid rounded"); XRPL_ASSERT( - roundToAsset(asset, totalInterestPaid, originalPrincipalRequested) == - totalInterestPaid, + roundToAsset(asset, totalInterestPaid, loanScale) == totalInterestPaid, "ripple::loanMakePayment : totalInterestPaid rounded"); XRPL_ASSERT( - roundToAsset(asset, loanValueChange, originalPrincipalRequested) == - loanValueChange, + roundToAsset(asset, loanValueChange, loanScale) == loanValueChange, "ripple::loanMakePayment : loanValueChange rounded"); XRPL_ASSERT( - roundToAsset(asset, totalFeePaid, originalPrincipalRequested) == - totalFeePaid, + roundToAsset(asset, totalFeePaid, loanScale) == totalFeePaid, "ripple::loanMakePayment : totalFeePaid rounded"); return LoanPaymentParts{ .principalPaid = totalPrincipalPaid, diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index 66d88728e5..c56e05acff 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -48,8 +48,8 @@ loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval) Number loanPeriodicPayment( - Number principalOutstanding, - Number periodicRate, + Number const& principalOutstanding, + Number const& periodicRate, std::uint32_t paymentsRemaining) { if (principalOutstanding == 0 || paymentsRemaining == 0) @@ -72,7 +72,7 @@ loanPeriodicPayment( Number loanPeriodicPayment( - Number principalOutstanding, + Number const& principalOutstanding, TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t paymentsRemaining) @@ -91,7 +91,7 @@ loanPeriodicPayment( Number loanLatePaymentInterest( - Number principalOutstanding, + Number const& principalOutstanding, TenthBips32 lateInterestRate, NetClock::time_point parentCloseTime, std::uint32_t startDate, @@ -114,8 +114,8 @@ loanLatePaymentInterest( Number loanAccruedInterest( - Number principalOutstanding, - Number periodicRate, + Number const& principalOutstanding, + Number const& periodicRate, NetClock::time_point parentCloseTime, std::uint32_t startDate, std::uint32_t prevPaymentDate, diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index fa43579bd5..2cb03181dd 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -2203,7 +2203,7 @@ NoModifiedUnmodifiableFields::finalize( fieldChanged(before, after, sfStartDate) || fieldChanged(before, after, sfPaymentInterval) || fieldChanged(before, after, sfGracePeriod) || - fieldChanged(before, after, sfPrincipalRequested); + fieldChanged(before, after, sfLoanScale); break; default: /* diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp index 0505218dcb..894e69cac8 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp @@ -146,7 +146,7 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) vaultAsset, tenthBipsOfValue( currentDebtTotal, TenthBips32(sleBroker->at(sfCoverRateMinimum))), - currentDebtTotal); + currentDebtTotal.exponent()); if (coverAvail < amount) return tecINSUFFICIENT_FUNDS; if ((coverAvail - amount) < minimumCover) diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index 99622d26ed..ac5da7f7dc 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -145,7 +145,7 @@ LoanManage::defaultLoan( { // Calculate the amount of the Default that First-Loss Capital covers: - Number const originalPrincipalRequested = loanSle->at(sfPrincipalRequested); + std::int32_t const loanScale = loanSle->at(sfLoanScale); TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; auto brokerDebtTotalProxy = brokerSle->at(sfDebtTotal); auto const totalDefaultAmount = principalOutstanding + interestOutstanding; @@ -162,7 +162,7 @@ LoanManage::defaultLoan( brokerDebtTotalProxy.value(), coverRateMinimum), coverRateLiquidation), totalDefaultAmount), - originalPrincipalRequested); + loanScale); auto const vaultDefaultAmount = totalDefaultAmount - defaultCovered; @@ -380,7 +380,7 @@ LoanManage::doApply() auto const vaultAsset = vaultSle->at(sfAsset); TenthBips32 const interestRate{loanSle->at(sfInterestRate)}; - Number const originalPrincipalRequested = loanSle->at(sfPrincipalRequested); + std::int32_t const loanScale = loanSle->at(sfLoanScale); auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding); TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; @@ -388,7 +388,7 @@ LoanManage::doApply() auto const paymentsRemaining = loanSle->at(sfPaymentRemaining); auto const interestOutstanding = loanInterestOutstandingMinusFee( vaultAsset, - originalPrincipalRequested, + loanScale, principalOutstanding.value(), interestRate, paymentInterval, diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 747664fbaf..71e73eeb07 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -147,7 +147,7 @@ LoanPay::doApply() //------------------------------------------------------ // Loan object state changes - Number const originalPrincipalRequested = loanSle->at(sfPrincipalRequested); + std::int32_t const loanScale = loanSle->at(sfLoanScale); // Unimpair the loan if it was impaired. Do this before the payment is // attempted, so the original values can be used. If the payment fails, this @@ -163,7 +163,7 @@ LoanPay::doApply() auto const interestOutstanding = loanInterestOutstandingMinusFee( asset, - originalPrincipalRequested, + loanScale, principalOutstanding.value(), interestRate, paymentInterval, @@ -219,7 +219,7 @@ LoanPay::doApply() auto const managementFee = roundToAsset( asset, tenthBipsOfValue(paymentParts->interestPaid, managementFeeRate), - originalPrincipalRequested); + loanScale); auto const totalPaidToVault = paymentParts->principalPaid + paymentParts->interestPaid - managementFee; @@ -241,7 +241,7 @@ LoanPay::doApply() bool const sufficientCover = coverAvailableField >= roundToAsset(asset, tenthBipsOfValue(debtTotalField.value(), coverRateMinimum), - originalPrincipalRequested); + loanScale); if (!sufficientCover) { // Add the fee to First Loss Cover Pool @@ -253,15 +253,11 @@ LoanPay::doApply() // Decrease LoanBroker Debt by the amount paid, add the Loan value change, // and subtract the change in the management fee auto const vaultValueChange = valueMinusManagementFee( - asset, - paymentParts->valueChange, - managementFeeRate, - originalPrincipalRequested); + asset, paymentParts->valueChange, managementFeeRate, loanScale); // debtDecrease may be negative, increasing the debt auto const debtDecrease = totalPaidToVault - vaultValueChange; XRPL_ASSERT_PARTS( - roundToAsset(asset, debtDecrease, originalPrincipalRequested) == - debtDecrease, + roundToAsset(asset, debtDecrease, loanScale) == debtDecrease, "ripple::LoanPay::doApply", "debtDecrease rounding good"); if (debtDecrease >= debtTotalField) diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 239eddb526..71619c4157 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -246,48 +246,6 @@ LoanSet::preclaim(PreclaimContext const& ctx) return ret; } - auto const principalRequested = tx[sfPrincipalRequested]; - if (auto const assetsAvailable = vault->at(sfAssetsAvailable); - assetsAvailable < principalRequested) - { - JLOG(ctx.j.warn()) - << "Insufficient assets available in the Vault to fund the loan."; - return tecINSUFFICIENT_FUNDS; - } - - TenthBips32 const interestRate{tx[~sfInterestRate].value_or(0)}; - auto const paymentInterval = - tx[~sfPaymentInterval].value_or(defaultPaymentInterval); - auto const paymentTotal = tx[~sfPaymentTotal].value_or(defaultPaymentTotal); - TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; - - auto const totalInterest = loanInterestOutstandingMinusFee( - asset, - principalRequested, - principalRequested, - interestRate, - paymentInterval, - paymentTotal, - managementFeeRate); - - auto const newDebtTotal = - brokerSle->at(sfDebtTotal) + principalRequested + totalInterest; - if (auto const debtMaximum = brokerSle->at(sfDebtMaximum); - debtMaximum != 0 && debtMaximum < newDebtTotal) - { - JLOG(ctx.j.warn()) - << "Loan would exceed the maximum debt limit of the LoanBroker."; - return tecLIMIT_EXCEEDED; - } - TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)}; - if (brokerSle->at(sfCoverAvailable) < - tenthBipsOfValue(newDebtTotal, coverRateMinimum)) - { - JLOG(ctx.j.warn()) - << "Insufficient first-loss capital to cover the loan."; - return tecINSUFFICIENT_FUNDS; - } - return tesSUCCESS; } @@ -327,17 +285,89 @@ LoanSet::doApply() { return tefBAD_LEDGER; // LCOV_EXCL_LINE } - auto const principalRequested = roundToAsset( - vaultAsset, tx[sfPrincipalRequested], tx[sfPrincipalRequested]); + auto const principalRequested = [&](Number const& requested) { + return roundToAsset(vaultAsset, requested, requested.exponent()); + }(tx[sfPrincipalRequested]); + auto const loanScale = principalRequested.exponent(); + + if (auto const assetsAvailable = vaultSle->at(sfAssetsAvailable); + assetsAvailable < principalRequested) + { + JLOG(j_.warn()) + << "Insufficient assets available in the Vault to fund the loan."; + return tecINSUFFICIENT_FUNDS; + } + TenthBips32 const interestRate{tx[~sfInterestRate].value_or(0)}; - auto const originationFee = tx[~sfLoanOriginationFee]; - auto const loanAssetsAvailable = - principalRequested - originationFee.value_or(Number{}); + + auto const originationFee = roundToAsset( + vaultAsset, tx[~sfLoanOriginationFee].value_or(Number{}), loanScale); + auto const loanAssetsToBorrower = principalRequested - originationFee; + + auto const paymentInterval = + tx[~sfPaymentInterval].value_or(defaultPaymentInterval); + auto const paymentTotal = tx[~sfPaymentTotal].value_or(defaultPaymentTotal); + + auto const periodicRate = loanPeriodicRate(interestRate, paymentInterval); + auto const periodicPayment = loanPeriodicPayment( + vaultAsset, principalRequested, periodicRate, paymentTotal, loanScale); + + auto const totalValueOutstanding = loanTotalValueOutstanding( + vaultAsset, loanScale, periodicPayment, paymentTotal); + + { + // Check that some principal is paid each period. Since the first + // payment pays the least principal, if it's good, they'll all be good. + auto const paymentParts = computePaymentParts( + vaultAsset, + loanScale, + totalValueOutstanding, + principalRequested, + periodicPayment, + tx[~sfLoanServiceFee].value_or(Number{}), + periodicRate, + paymentTotal); + + if (paymentParts.principal <= 0) + { + JLOG(j_.warn()) << "Loan is unable to pay principal."; + return tecLIMIT_EXCEEDED; + } + } + + TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; + + auto const totalInterestOwedToVault = [&]() { + auto const totalInterestOutstanding = loanTotalInterestOutstanding( + principalRequested, totalValueOutstanding); + + return loanInterestOutstandingMinusFee( + vaultAsset, totalInterestOutstanding, managementFeeRate, loanScale); + }(); + + auto const newDebtTotal = brokerSle->at(sfDebtTotal) + principalRequested + + totalInterestOwedToVault; + if (auto const debtMaximum = brokerSle->at(sfDebtMaximum); + debtMaximum != 0 && debtMaximum < newDebtTotal) + { + JLOG(j_.warn()) + << "Loan would exceed the maximum debt limit of the LoanBroker."; + return tecLIMIT_EXCEEDED; + } + TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)}; + if (brokerSle->at(sfCoverAvailable) < + tenthBipsOfValue(newDebtTotal, coverRateMinimum)) + { + JLOG(j_.warn()) << "Insufficient first-loss capital to cover the loan."; + return tecINSUFFICIENT_FUNDS; + } adjustOwnerCount(view, borrowerSle, 1, j_); - auto ownerCount = borrowerSle->at(sfOwnerCount); - if (mPriorBalance < view.fees().accountReserve(ownerCount)) - return tecINSUFFICIENT_RESERVE; + { + auto ownerCount = borrowerSle->at(sfOwnerCount); + if (mPriorBalance < view.fees().accountReserve(ownerCount)) + return tecINSUFFICIENT_RESERVE; + } // Account for the origination fee using two payments // @@ -359,13 +389,13 @@ LoanSet::doApply() view, vaultPseudo, borrower, - STAmount{vaultAsset, loanAssetsAvailable}, + STAmount{vaultAsset, loanAssetsToBorrower}, j_, WaiveTransferFee::Yes)) return ter; // 2. Transfer originationFee, if any, from vault pseudo-account to // LoanBroker owner. - if (originationFee && (*originationFee != Number{})) + if (originationFee != Number{}) { // Create the holding if it doesn't already exist (necessary for MPTs). // The owner may have deleted their MPT / line at some point. @@ -383,31 +413,20 @@ LoanSet::doApply() view, vaultPseudo, brokerOwner, - STAmount{vaultAsset, *originationFee}, + STAmount{vaultAsset, originationFee}, j_, WaiveTransferFee::Yes)) return ter; } - auto const paymentInterval = - tx[~sfPaymentInterval].value_or(defaultPaymentInterval); - auto const paymentTotal = tx[~sfPaymentTotal].value_or(defaultPaymentTotal); - TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; // The portion of the loan interest that will go to the vault (total // interest minus the management fee) - auto const loanInterestToVault = loanInterestOutstandingMinusFee( - vaultAsset, - principalRequested, - principalRequested, - interestRate, - paymentInterval, - paymentTotal, - managementFeeRate); auto const startDate = view.info().closeTime.time_since_epoch().count(); - auto loanSequence = brokerSle->at(sfLoanSequence); + auto loanSequenceProxy = brokerSle->at(sfLoanSequence); // Create the loan - auto loan = std::make_shared(keylet::loan(brokerID, *loanSequence)); + auto loan = + std::make_shared(keylet::loan(brokerID, *loanSequenceProxy)); // Prevent copy/paste errors auto setLoanField = @@ -417,12 +436,11 @@ LoanSet::doApply() loan->at(field) = tx[field].value_or(defValue); }; - // Set required tx fields and pre-computed fields - loan->at(sfPrincipalRequested) = principalRequested; - loan->at(sfPrincipalOutstanding) = principalRequested; + // Set required and fixed tx fields + loan->at(sfLoanScale) = principalRequested.exponent(); loan->at(sfStartDate) = startDate; loan->at(sfPaymentInterval) = paymentInterval; - loan->at(sfLoanSequence) = loanSequence; + loan->at(sfLoanSequence) = *loanSequenceProxy; loan->at(sfLoanBrokerID) = brokerID; loan->at(sfBorrower) = borrower; // Set all other transaction fields directly from the transaction @@ -438,7 +456,9 @@ LoanSet::doApply() setLoanField(~sfCloseInterestRate); setLoanField(~sfOverpaymentInterestRate); setLoanField(~sfGracePeriod, defaultGracePeriod); - // Set dynamic fields to their initial values + // Set dynamic / computed fields to their initial values + loan->at(sfPrincipalOutstanding) = principalRequested; + loan->at(sfTotalValueOutstanding) = totalValueOutstanding; loan->at(sfPreviousPaymentDate) = 0; loan->at(sfNextPaymentDueDate) = startDate + paymentInterval; loan->at(sfPaymentRemaining) = paymentTotal; @@ -446,7 +466,7 @@ LoanSet::doApply() // Update the balances in the vault vaultSle->at(sfAssetsAvailable) -= principalRequested; - vaultSle->at(sfAssetsTotal) += loanInterestToVault; + vaultSle->at(sfAssetsTotal) += totalInterestOwedToVault; XRPL_ASSERT_PARTS( *vaultSle->at(sfAssetsAvailable) <= *vaultSle->at(sfAssetsTotal), "ripple::LoanSet::doApply", @@ -454,11 +474,11 @@ LoanSet::doApply() view.update(vaultSle); // Update the balances in the loan broker - brokerSle->at(sfDebtTotal) += principalRequested + loanInterestToVault; + brokerSle->at(sfDebtTotal) += principalRequested + totalInterestOwedToVault; // The broker's owner count is solely for the number of outstanding loans, // and is distinct from the broker's pseudo-account's owner count adjustOwnerCount(view, brokerSle, 1, j_); - loanSequence += 1; + loanSequenceProxy += 1; view.update(brokerSle); // Put the loan into the pseudo-account's directory From 25092939432cf91086ba524edde3dc46a00f4fe3 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 2 Oct 2025 16:49:59 -0400 Subject: [PATCH 114/291] Disable blocks of code using #if LOANCOMPLETE to enable build - Will add those back later as the appropriate helpers are updated. - Tests are not expected to pass. --- src/test/app/Loan_test.cpp | 22 ++++++++++++++-------- src/xrpld/app/misc/LendingHelpers.h | 12 ++++++------ src/xrpld/app/tx/detail/LoanPay.cpp | 6 ++++++ 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 8535ed95f5..de9c257525 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -155,6 +155,7 @@ class Loan_test : public beast::unit_test::suite std::uint32_t paymentsRemaining, std::uint32_t ownerCount) const { +#if LOANCOMPLETE using namespace jtx; if (auto brokerSle = env.le(keylet::loanbroker(broker.brokerID)); env.test.BEAST_EXPECT(brokerSle)) @@ -212,6 +213,7 @@ class Loan_test : public beast::unit_test::suite } } } +#endif } /** Checks both the loan and broker expect states against the ledger */ @@ -220,8 +222,8 @@ class Loan_test : public beast::unit_test::suite std::uint32_t previousPaymentDate, std::uint32_t nextPaymentDate, std::uint32_t paymentRemaining, - Number const& principalRequested, Number const& loanScale, + Number const& principalOutstanding, std::uint32_t flags) const { using namespace jtx; @@ -244,7 +246,7 @@ class Loan_test : public beast::unit_test::suite auto const interestRate = TenthBips32{loan->at(sfInterestRate)}; auto const paymentInterval = loan->at(sfPaymentInterval); checkBroker( - principalRequested, + loanScale, principalOutstanding, interestRate, paymentInterval, @@ -259,6 +261,7 @@ class Loan_test : public beast::unit_test::suite env.le(keylet::vault(brokerSle->at(sfVaultID))); env.test.BEAST_EXPECT(vaultSle)) { +#if LOANCOMPLETE if ((flags & lsfLoanImpaired) && !(flags & lsfLoanDefault)) { @@ -269,7 +272,7 @@ class Loan_test : public beast::unit_test::suite principalOutstanding + loanInterestOutstandingMinusFee( broker.asset, - principalRequested, + loanScale, principalOutstanding, interestRate, paymentInterval, @@ -281,6 +284,7 @@ class Loan_test : public beast::unit_test::suite env.test.BEAST_EXPECT( vaultSle->at(sfLossUnrealized) == 0); } +#endif } } } @@ -294,7 +298,7 @@ class Loan_test : public beast::unit_test::suite state.previousPaymentDate, state.nextPaymentDate, state.paymentRemaining, - state.principalRequested, + state.loanScale, state.principalOutstanding, state.flags); }; @@ -381,7 +385,7 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT( state.principalOutstanding == broker.asset(1000).value()); BEAST_EXPECT( - state.principalOutstanding == state.principalRequested); + state.principalOutstanding.exponent() == state.loanScale); BEAST_EXPECT(state.paymentInterval == 600); verifyLoanStatus(state); @@ -411,7 +415,7 @@ class Loan_test : public beast::unit_test::suite auto const interestOutstanding = loanInterestOutstandingMinusFee( broker.asset, - state.principalRequested, + state.loanScale, state.principalOutstanding, state.interestRate, state.paymentInterval, @@ -757,6 +761,7 @@ class Loan_test : public beast::unit_test::suite Number const& loanAmount, int interestExponent) { +#if LOANCOMPLETE using namespace jtx; auto const& asset = broker.asset.raw(); @@ -1157,11 +1162,11 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT(brokerSle)) { BEAST_EXPECT( - state.principalRequested == state.principalOutstanding); + state.loanScale == state.principalOutstanding.exponent()); auto const interestOutstanding = loanInterestOutstandingMinusFee( broker.asset, - state.principalRequested, + state.loanScale, state.principalOutstanding, state.interestRate, state.paymentInterval, @@ -1761,6 +1766,7 @@ class Loan_test : public beast::unit_test::suite env(manage(lender, loanKeylet.key, tfLoanDefault), ter(tecNO_PERMISSION)); }); +#endif } void diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 5f28ed9120..b0114c4921 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -88,7 +88,7 @@ valueMinusManagementFee( asset, detail::minusManagementFee(value, managementFeeRate), scale); } -Number +inline Number loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval) { return detail::loanPeriodicRate(interestRate, paymentInterval); @@ -121,7 +121,7 @@ loanPeriodicPayment( std::uint32_t paymentsRemaining, std::int32_t scale) { - loanPeriodicPayment( + return loanPeriodicPayment( asset, principalOutstanding, loanPeriodicRate(interestRate, paymentInterval), @@ -272,7 +272,7 @@ loanLatePaymentInterest( template bool -rounded(A const& asset, Number const& value, std::int32_t scale) +isRounded(A const& asset, Number const& value, std::int32_t scale) { return roundToAsset(asset, value, scale, Number::downward) == value && roundToAsset(asset, value, scale, Number::upward) == value; @@ -302,9 +302,9 @@ computePaymentParts( * Payment) */ XRPL_ASSERT_PARTS( - rounded(asset, totalValueOutstanding, scale) && - rounded(asset, principalOutstanding, scale) && - rounded(asset, periodicPaymentAmount, scale), + isRounded(asset, totalValueOutstanding, scale) && + isRounded(asset, principalOutstanding, scale) && + isRounded(asset, periodicPaymentAmount, scale), "ripple::computePaymentParts", "Asset values are rounded"); Number const roundedFee = roundToAsset(asset, serviceFee, scale); diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 71e73eeb07..829e97ae4c 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -45,6 +45,8 @@ LoanPay::preflight(PreflightContext const& ctx) TER LoanPay::preclaim(PreclaimContext const& ctx) { + return temDISABLED; +#if LOANCOMPLETE auto const& tx = ctx.tx; auto const account = tx[sfAccount]; @@ -117,11 +119,14 @@ LoanPay::preclaim(PreclaimContext const& ctx) } return tesSUCCESS; +#endif } TER LoanPay::doApply() { + return temDISABLED; +#if LOANCOMPLETE auto const& tx = ctx_.tx; auto& view = ctx_.view(); @@ -314,6 +319,7 @@ LoanPay::doApply() return ter; return tesSUCCESS; +#endif } //------------------------------------------------------------------------------ From e1939d0d20ac144c51729fbe85e5db8824e23c87 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 2 Oct 2025 21:01:19 -0400 Subject: [PATCH 115/291] Convert LoanSet to precomputed all values - Tests are not expected to pass --- .../xrpl/protocol/detail/ledger_entries.macro | 7 +- include/xrpl/protocol/detail/sfields.macro | 2 + src/test/app/Loan_test.cpp | 2 + src/xrpld/app/misc/LendingHelpers.h | 289 ++++++++++++------ src/xrpld/app/tx/detail/LoanManage.cpp | 3 + src/xrpld/app/tx/detail/LoanSet.cpp | 103 ++++--- 6 files changed, 265 insertions(+), 141 deletions(-) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 99e16e6b70..56497aa858 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -561,11 +561,12 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({ {sfPaymentInterval, soeREQUIRED}, {sfGracePeriod, soeREQUIRED}, {sfPeriodicPayment, soeREQUIRED}, - {sfPreviousPaymentDate, soeREQUIRED}, + {sfPreviousPaymentDate, soeDEFAULT}, {sfNextPaymentDueDate, soeREQUIRED}, - {sfPaymentRemaining, soeREQUIRED}, - {sfPrincipalOutstanding, soeREQUIRED}, + {sfPaymentRemaining, soeDEFAULT}, + {sfPrincipalOutstanding, soeDEFAULT}, {sfTotalValueOutstanding, soeDEFAULT}, + {sfInterestOwed, soeDEFAULT}, // Based on the original principal borrowed, used for // rounding calculated values so they are all on a // consistent scale - that is, they all have the same diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 3c3dda69e0..2fca180b7b 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -241,6 +241,8 @@ TYPED_SFIELD(sfPrincipalOutstanding, NUMBER, 13) TYPED_SFIELD(sfPrincipalRequested, NUMBER, 14) TYPED_SFIELD(sfTotalValueOutstanding, NUMBER, 15) TYPED_SFIELD(sfPeriodicPayment, NUMBER, 16) +TYPED_SFIELD(sfReferencePrincipal, NUMBER, 17) +TYPED_SFIELD(sfInterestOwed, NUMBER, 18) // int32 TYPED_SFIELD(sfLoanScale, INT32, 1) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index de9c257525..495c6a405d 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -402,6 +402,7 @@ class Loan_test : public beast::unit_test::suite BrokerInfo const& broker, LoanState const& state) { +#if LOANCOMPLETE if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); BEAST_EXPECT(brokerSle)) { @@ -430,6 +431,7 @@ class Loan_test : public beast::unit_test::suite } } } +#endif return true; } diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index b0114c4921..c1a18106e4 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -31,6 +31,16 @@ struct PreflightContext; bool checkLendingProtocolDependencies(PreflightContext const& ctx); +struct PaymentParts +{ + Number rawInterest; + Number rawPrincipal; + Number roundedInterest; + Number roundedPrincipal; + Number roundedPayment; + bool final = false; +}; + namespace detail { // These functions should rarely be used directly. More often, the ultimate // result needs to be roundToAsset'd. @@ -74,11 +84,104 @@ minusManagementFee(Number const& value, TenthBips32 managementFeeRate) return tenthBipsOfValue(value, tenthBipsPerUnity - managementFeeRate); } +template +PaymentParts +computePaymentParts( + A const& asset, + std::int32_t scale, + Number const& totalValueOutstanding, + Number const& principalOutstanding, + Number const& referencePrincipal, + Number const& periodicPayment, + Number const& periodicRate, + std::uint32_t paymentRemaining) +{ + /* + * This function is derived from the XLS-66 spec, section 3.2.4.1.1 (Regular + * Payment) + */ + XRPL_ASSERT_PARTS( + isRounded(asset, totalValueOutstanding, scale) && + isRounded(asset, principalOutstanding, scale), + "ripple::detail::computePaymentParts", + "Outstanding values are rounded"); + auto const roundedPeriodicPayment = + roundToAsset(asset, periodicPayment, scale, Number::upward); + if (paymentRemaining == 1 || totalValueOutstanding <= periodicPayment) + { + // If there's only one payment left, we need to pay off the principal. + // + // The totalValueOutstanding should never be less than the + // periodicPayment until the last scheduled payment, but if it ever is, + // make it the last payment. + Number rawInterest = totalValueOutstanding - referencePrincipal; + Number roundedInterest = totalValueOutstanding - principalOutstanding; + + // This is only expected to be true on the last payment + XRPL_ASSERT_PARTS( + rawInterest + referencePrincipal == + roundedInterest + principalOutstanding, + "ripple::detail::computePaymentParts", + "last payment is complete"); + + Number const interest = totalValueOutstanding - principalOutstanding; + return { + .rawInterest = rawInterest, + .rawPrincipal = referencePrincipal, + .roundedInterest = roundedInterest, + .roundedPrincipal = principalOutstanding, + .roundedPayment = roundedPeriodicPayment, + .final = true}; + } + /* + * From the spec, once the periodicPayment is computed: + * + * The principal and interest portions can be derived as follows: + * interest = principalOutstanding * periodicRate + * principal = periodicPayment - interest + * + * Because those values deal with funds, they need to be rounded. + */ + Number const rawInterest = referencePrincipal * periodicRate; + Number const rawPrincipal = periodicPayment - rawInterest; + XRPL_ASSERT_PARTS( + rawInterest >= 0, + "ripple::detail::computePaymentParts", + "valid raw interest"); + XRPL_ASSERT_PARTS( + rawPrincipal > 0 && rawPrincipal <= referencePrincipal, + "ripple::detail::computePaymentParts", + "valid raw principal"); + + Number const roundedInterest = + roundToAsset(asset, rawInterest, scale, Number::upward); + Number const roundedPrincipal = roundedPeriodicPayment - roundedInterest; + XRPL_ASSERT_PARTS( + roundedInterest >= 0, + "ripple::detail::computePaymentParts", + "valid rounded interest"); + XRPL_ASSERT_PARTS( + roundedPrincipal >= 0 && roundedPrincipal <= principalOutstanding, + "ripple::detail::computePaymentParts", + "valid rounded principal"); + XRPL_ASSERT_PARTS( + isRounded(asset, roundedPrincipal, scale), + "ripple::detail::computePaymentParts", + "principal is rounded"); + + return { + .rawInterest = rawInterest, + .rawPrincipal = rawPrincipal, + .roundedInterest = roundedInterest, + .roundedPrincipal = roundedPrincipal, + .roundedPayment = roundedPeriodicPayment}; +} + } // namespace detail template Number -valueMinusManagementFee( +valueMinusFee( A const& asset, Number const& value, TenthBips32 managementFeeRate, @@ -88,6 +191,90 @@ valueMinusManagementFee( asset, detail::minusManagementFee(value, managementFeeRate), scale); } +struct LoanProperties +{ + Number periodicPayment; + Number totalValueOutstanding; + Number interestOwedToVault; + std::int32_t loanScale; + Number firstPaymentPrincipal; +}; + +template +LoanProperties +computeLoanProperties( + A const& asset, + Number const& principalOutstanding, + Number const& referencePrincipal, + TenthBips32 interestRate, + std::uint32_t paymentInterval, + std::uint32_t paymentsRemaining, + TenthBips32 managementFeeRate) +{ + auto const periodicRate = + detail::loanPeriodicRate(interestRate, paymentInterval); + auto const periodicPayment = detail::loanPeriodicPayment( + principalOutstanding, periodicRate, paymentsRemaining); + Number const totalValueOutstanding = [&]() { + NumberRoundModeGuard mg(Number::to_nearest); + // Use STAmount's internal rounding instead of roundToAsset, because + // we're going to use this result to determine the scale for all the + // other rounding. + return STAmount{ + asset, + /* + * This formula is from the XLS-66 spec, section 3.2.4.2 (Total + * Loan Value Calculation), specifically "totalValueOutstanding + * = ..." + */ + periodicPayment * paymentsRemaining}; + }(); + // Base the loan scale on the total value, since that's going to be the + // biggest number involved + auto const loanScale = totalValueOutstanding.exponent(); + + auto const firstPaymentPrincipal = [&]() { + // Compute the unrounded parts for the first payment. Ensure that the + // principal payment will actually change the principal. + auto const paymentParts = detail::computePaymentParts( + asset, + loanScale, + totalValueOutstanding, + principalOutstanding, + referencePrincipal, + periodicPayment, + periodicRate, + paymentsRemaining); + + // We only care about the unrounded principal part. It needs to be large + // enough that it will affect the reference principal. + auto const remaining = referencePrincipal - paymentParts.rawPrincipal; + if (remaining == referencePrincipal) + // No change, so the first payment effectively pays no principal. + // Whether that's a problem is left to the caller. + return Number{0}; + return paymentParts.rawPrincipal; + }(); + + auto const interestOwedToVault = valueMinusFee( + asset, + /* + * This formula is from the XLS-66 spec, section 3.2.4.2 (Total Loan + * Value Calculation), specifically "totalInterestOutstanding = ..." + */ + totalValueOutstanding - principalOutstanding, + managementFeeRate, + loanScale); + + return LoanProperties{ + .periodicPayment = periodicPayment, + .totalValueOutstanding = totalValueOutstanding, + .interestOwedToVault = interestOwedToVault, + .loanScale = loanScale, + .firstPaymentPrincipal = firstPaymentPrincipal}; +} + +#if LOANCOMPLETE inline Number loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval) { @@ -211,6 +398,7 @@ loanTotalInterestOutstanding( paymentInterval, paymentsRemaining)); } +#endif template Number @@ -220,10 +408,11 @@ loanInterestOutstandingMinusFee( TenthBips32 managementFeeRate, std::int32_t scale) { - return valueMinusManagementFee( + return valueMinusFee( asset, totalInterestOutstanding, managementFeeRate, scale); } +#if LOANCOMPLETE template Number loanInterestOutstandingMinusFee( @@ -247,6 +436,7 @@ loanInterestOutstandingMinusFee( managementFeeRate, scale); } +#endif template Number @@ -278,98 +468,6 @@ isRounded(A const& asset, Number const& value, std::int32_t scale) roundToAsset(asset, value, scale, Number::upward) == value; } -struct PaymentParts -{ - Number interest; - Number principal; - Number fee; -}; - -template -PaymentParts -computePaymentParts( - A const& asset, - std::int32_t scale, - Number const& totalValueOutstanding, - Number const& principalOutstanding, - Number const& periodicPaymentAmount, - Number const& serviceFee, - Number const& periodicRate, - std::uint32_t paymentRemaining) -{ - /* - * This function is derived from the XLS-66 spec, section 3.2.4.1.1 (Regular - * Payment) - */ - XRPL_ASSERT_PARTS( - isRounded(asset, totalValueOutstanding, scale) && - isRounded(asset, principalOutstanding, scale) && - isRounded(asset, periodicPaymentAmount, scale), - "ripple::computePaymentParts", - "Asset values are rounded"); - Number const roundedFee = roundToAsset(asset, serviceFee, scale); - if (paymentRemaining == 1 || periodicPaymentAmount > totalValueOutstanding) - { - // If there's only one payment left, we need to pay off the principal. - Number const interest = totalValueOutstanding - principalOutstanding; - return { - .interest = interest, - .principal = principalOutstanding, - .fee = roundedFee}; - } - /* - * From the spec, once the periodicPayment is computed: - * - * The principal and interest portions can be derived as follows: - * interest = principalOutstanding * periodicRate - * principal = periodicPayment - interest - * - * Because those values deal with funds, they need to be rounded. - */ - Number const interest = roundToAsset( - asset, principalOutstanding * periodicRate, scale, Number::upward); - XRPL_ASSERT( - interest >= 0, - "ripple::detail::computePeriodicPayment : valid interest"); - - // To compute the principal using the above formulas, use the rounded - // payment amount, ensuring that some principal is paid regardless of any - // other results. - auto const roundedPayment = [&]() { - auto roundedPayment = - roundToAsset(asset, periodicPaymentAmount, scale, Number::upward); - if (roundedPayment > interest) - return roundedPayment; - auto newPayment = roundedPayment; - if (asset.native() || !asset.template holds()) - { - // integral types, just add one - ++newPayment; - } - else - { - // Non-integral types: IOU. Add "dust" that will not be lost in - // rounding. - auto const epsilon = Number{1, scale - 14}; - newPayment += epsilon; - } - roundedPayment = roundToAsset(asset, newPayment, scale); - XRPL_ASSERT_PARTS( - roundedPayment == newPayment, - "ripple::computePaymentParts", - "epsilon preserved in rounding"); - return roundedPayment; - }(); - Number const principal = - roundToAsset(asset, roundedPayment - interest, scale); - XRPL_ASSERT_PARTS( - principal > 0 && principal <= principalOutstanding, - "ripple::computePaymentParts", - "valid principal"); - - return {.interest = interest, .principal = principal, .fee = roundedFee}; -} - // This structure is explained in the XLS-66 spec, section 3.2.4.4 (Failure // Conditions) struct LoanPaymentParts @@ -420,6 +518,8 @@ handleLatePayment( STAmount const& amount, beast::Journal j) { + return Unexpected(temDISABLED); +#if LOANCOMPLETE if (!hasExpired(view, nextDueDateProxy)) return Unexpected(tesSUCCESS); @@ -466,6 +566,7 @@ handleLatePayment( .interestPaid = late.interest, .valueChange = latePaymentInterest, .feePaid = late.fee}; +#endif } /* Handle possible full payments. diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index ac5da7f7dc..ff83134ac7 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -361,6 +361,8 @@ LoanManage::unimpairLoan( TER LoanManage::doApply() { + return temDISABLED; +#if LOANCOMPLETE auto const& tx = ctx_.tx; auto& view = ctx_.view(); @@ -435,6 +437,7 @@ LoanManage::doApply() } return tesSUCCESS; +#endif } //------------------------------------------------------------------------------ diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 71619c4157..447eec5967 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -285,10 +285,7 @@ LoanSet::doApply() { return tefBAD_LEDGER; // LCOV_EXCL_LINE } - auto const principalRequested = [&](Number const& requested) { - return roundToAsset(vaultAsset, requested, requested.exponent()); - }(tx[sfPrincipalRequested]); - auto const loanScale = principalRequested.exponent(); + auto const principalRequested = tx[sfPrincipalRequested]; if (auto const assetsAvailable = vaultSle->at(sfAssetsAvailable); assetsAvailable < principalRequested) @@ -300,53 +297,68 @@ LoanSet::doApply() TenthBips32 const interestRate{tx[~sfInterestRate].value_or(0)}; - auto const originationFee = roundToAsset( - vaultAsset, tx[~sfLoanOriginationFee].value_or(Number{}), loanScale); - auto const loanAssetsToBorrower = principalRequested - originationFee; - auto const paymentInterval = tx[~sfPaymentInterval].value_or(defaultPaymentInterval); auto const paymentTotal = tx[~sfPaymentTotal].value_or(defaultPaymentTotal); - auto const periodicRate = loanPeriodicRate(interestRate, paymentInterval); - auto const periodicPayment = loanPeriodicPayment( - vaultAsset, principalRequested, periodicRate, paymentTotal, loanScale); - - auto const totalValueOutstanding = loanTotalValueOutstanding( - vaultAsset, loanScale, periodicPayment, paymentTotal); + auto const properties = computeLoanProperties( + vaultAsset, + principalRequested, + principalRequested, + interestRate, + paymentInterval, + paymentTotal, + TenthBips32{brokerSle->at(sfManagementFeeRate)}); + if (properties.firstPaymentPrincipal <= 0) { - // Check that some principal is paid each period. Since the first - // payment pays the least principal, if it's good, they'll all be good. - auto const paymentParts = computePaymentParts( - vaultAsset, - loanScale, - totalValueOutstanding, - principalRequested, - periodicPayment, - tx[~sfLoanServiceFee].value_or(Number{}), - periodicRate, - paymentTotal); - - if (paymentParts.principal <= 0) - { - JLOG(j_.warn()) << "Loan is unable to pay principal."; - return tecLIMIT_EXCEEDED; - } + // Check that some reference principal is paid each period. Since the + // first payment pays the least principal, if it's good, they'll all be + // good. Note that the outstanding principal is rounded, and may not + // change right away. + JLOG(j_.warn()) << "Loan is unable to pay principal."; + return tecLIMIT_EXCEEDED; + } + // Check that the other computed values are valid + if (properties.interestOwedToVault < 0 || + properties.totalValueOutstanding <= 0 || + properties.periodicPayment <= 0) + { + // LCOV_EXCL_START + JLOG(j_.warn()) + << "Computed loan properties are invalid. Does not compute."; + return tecINTERNAL; + // LCOV_EXCL_STOP } - TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; + // Check that relevant values won't lose precision + { + static std::map> const + valueFields{ + {"Principal Requested", ~sfPrincipalRequested}, + {"Origination fee", ~sfLoanOriginationFee}, + {"Service fee", ~sfLoanServiceFee}, + {"Late Payment fee", ~sfLatePaymentFee}, + {"Close Payment fee", ~sfClosePaymentFee} + // Overpayment fee is really a rate. Don't include it. + }; + for (auto const& [name, field] : valueFields) + { + if (auto const value = tx[field]; + value && !isRounded(vaultAsset, *value, properties.loanScale)) + { + JLOG(j_.warn()) << name << " has too much precision."; + return tecPRECISION_LOSS; + } + } + } + auto const originationFee = tx[~sfLoanOriginationFee].value_or(Number{}); - auto const totalInterestOwedToVault = [&]() { - auto const totalInterestOutstanding = loanTotalInterestOutstanding( - principalRequested, totalValueOutstanding); + auto const loanAssetsToBorrower = principalRequested - originationFee; - return loanInterestOutstandingMinusFee( - vaultAsset, totalInterestOutstanding, managementFeeRate, loanScale); - }(); - - auto const newDebtTotal = brokerSle->at(sfDebtTotal) + principalRequested + - totalInterestOwedToVault; + auto const newDebtDelta = + principalRequested + properties.interestOwedToVault; + auto const newDebtTotal = brokerSle->at(sfDebtTotal) + newDebtDelta; if (auto const debtMaximum = brokerSle->at(sfDebtMaximum); debtMaximum != 0 && debtMaximum < newDebtTotal) { @@ -458,7 +470,10 @@ LoanSet::doApply() setLoanField(~sfGracePeriod, defaultGracePeriod); // Set dynamic / computed fields to their initial values loan->at(sfPrincipalOutstanding) = principalRequested; - loan->at(sfTotalValueOutstanding) = totalValueOutstanding; + loan->at(sfReferencePrincipal) = principalRequested; + loan->at(sfPeriodicPayment) = properties.periodicPayment; + loan->at(sfTotalValueOutstanding) = properties.totalValueOutstanding; + loan->at(sfInterestOwed) = properties.interestOwedToVault; loan->at(sfPreviousPaymentDate) = 0; loan->at(sfNextPaymentDueDate) = startDate + paymentInterval; loan->at(sfPaymentRemaining) = paymentTotal; @@ -466,7 +481,7 @@ LoanSet::doApply() // Update the balances in the vault vaultSle->at(sfAssetsAvailable) -= principalRequested; - vaultSle->at(sfAssetsTotal) += totalInterestOwedToVault; + vaultSle->at(sfAssetsTotal) += properties.interestOwedToVault; XRPL_ASSERT_PARTS( *vaultSle->at(sfAssetsAvailable) <= *vaultSle->at(sfAssetsTotal), "ripple::LoanSet::doApply", @@ -474,7 +489,7 @@ LoanSet::doApply() view.update(vaultSle); // Update the balances in the loan broker - brokerSle->at(sfDebtTotal) += principalRequested + totalInterestOwedToVault; + brokerSle->at(sfDebtTotal) += newDebtDelta; // The broker's owner count is solely for the number of outstanding loans, // and is distinct from the broker's pseudo-account's owner count adjustOwnerCount(view, brokerSle, 1, j_); From 3cc8e5564a51031c682e61b8fc7a4acd16fec3d8 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 2 Oct 2025 22:09:35 -0400 Subject: [PATCH 116/291] Start converting LoanManage to handle pre-computed fields - Tests are not expected to pass --- src/xrpld/app/tx/detail/LoanManage.cpp | 76 ++++++++------------------ src/xrpld/app/tx/detail/LoanManage.h | 7 --- src/xrpld/app/tx/detail/LoanSet.cpp | 5 +- 3 files changed, 27 insertions(+), 61 deletions(-) diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index ff83134ac7..5e68616d5b 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -125,7 +125,8 @@ LoanManage::preclaim(PreclaimContext const& ctx) if (loanBrokerSle->at(sfOwner) != account) { JLOG(ctx.j.warn()) - << "LoanBroker for Loan does not belong to the account."; + << "LoanBroker for Loan does not belong to the account. LoanModify " + "can only be submitted by the Loan Broker."; return tecNO_PERMISSION; } @@ -138,8 +139,6 @@ LoanManage::defaultLoan( SLE::ref loanSle, SLE::ref brokerSle, SLE::ref vaultSle, - Number const& principalOutstanding, - Number const& interestOutstanding, Asset const& vaultAsset, beast::Journal j) { @@ -148,7 +147,8 @@ LoanManage::defaultLoan( std::int32_t const loanScale = loanSle->at(sfLoanScale); TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; auto brokerDebtTotalProxy = brokerSle->at(sfDebtTotal); - auto const totalDefaultAmount = principalOutstanding + interestOutstanding; + + Number const totalDefaultAmount = loanSle->at(sfTotalValueOutstanding); // Apply the First-Loss Capital to the Default Amount TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)}; @@ -156,6 +156,11 @@ LoanManage::defaultLoan( brokerSle->at(sfCoverRateLiquidation)}; auto const defaultCovered = roundToAsset( vaultAsset, + /* + * This formula is from the XLS-66 spec, section 3.2.3.2 (State + * Changes), specifically "if the `tfLoanDefault` flag is set" / "Apply + * the First-Loss Capital to the Default Amount" + */ std::min( tenthBipsOfValue( tenthBipsOfValue( @@ -220,14 +225,16 @@ LoanManage::defaultLoan( // The loss has been realized if (loanSle->isFlag(lsfLoanImpaired)) { + Number const lossRealized = loanSle->at(sfPrincipalOutstanding) + + loanSle->at(sfInterestOwed); auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized); - if (vaultLossUnrealizedProxy < totalDefaultAmount) + if (vaultLossUnrealizedProxy < lossRealized) { JLOG(j.warn()) << "Vault unrealized loss is less than the default amount"; return tefBAD_LEDGER; } - vaultLossUnrealizedProxy -= totalDefaultAmount; + vaultLossUnrealizedProxy -= lossRealized; } view.update(vaultSle); } @@ -281,13 +288,13 @@ LoanManage::impairLoan( ApplyView& view, SLE::ref loanSle, SLE::ref vaultSle, - Number const& principalOutstanding, - Number const& interestOutstanding, beast::Journal j) { + Number const lossUnrealized = + loanSle->at(sfPrincipalOutstanding) + loanSle->at(sfInterestOwed); // Update the Vault object(set "paper loss") auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized); - vaultLossUnrealizedProxy += principalOutstanding + interestOutstanding; + vaultLossUnrealizedProxy += lossUnrealized; if (vaultLossUnrealizedProxy > vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable)) { @@ -318,14 +325,12 @@ LoanManage::unimpairLoan( ApplyView& view, SLE::ref loanSle, SLE::ref vaultSle, - Number const& principalOutstanding, - Number const& interestOutstanding, - std::uint32_t paymentInterval, beast::Journal j) { // Update the Vault object(clear "paper loss") auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized); - auto const lossReversed = principalOutstanding + interestOutstanding; + Number const lossReversed = + loanSle->at(sfPrincipalOutstanding) + loanSle->at(sfInterestOwed); if (vaultLossUnrealizedProxy < lossReversed) { // LCOV_EXCL_START @@ -339,6 +344,7 @@ LoanManage::unimpairLoan( // Update the Loan object loanSle->clearFlag(lsfLoanImpaired); + auto const paymentInterval = loanSle->at(sfPaymentInterval); auto const normalPaymentDueDate = std::max(loanSle->at(sfPreviousPaymentDate), loanSle->at(sfStartDate)) + paymentInterval; @@ -381,58 +387,22 @@ LoanManage::doApply() return tefBAD_LEDGER; // LCOV_EXCL_LINE auto const vaultAsset = vaultSle->at(sfAsset); - TenthBips32 const interestRate{loanSle->at(sfInterestRate)}; - std::int32_t const loanScale = loanSle->at(sfLoanScale); - auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding); - - TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; - auto const paymentInterval = loanSle->at(sfPaymentInterval); - auto const paymentsRemaining = loanSle->at(sfPaymentRemaining); - auto const interestOutstanding = loanInterestOutstandingMinusFee( - vaultAsset, - loanScale, - principalOutstanding.value(), - interestRate, - paymentInterval, - paymentsRemaining, - managementFeeRate); - // Valid flag combinations are checked in preflight. No flags is valid - // just a noop. if (tx.isFlag(tfLoanDefault)) { - if (auto const ter = defaultLoan( - view, - loanSle, - brokerSle, - vaultSle, - principalOutstanding, - interestOutstanding, - vaultAsset, - j_)) + if (auto const ter = + defaultLoan(view, loanSle, brokerSle, vaultSle, vaultAsset, j_)) return ter; } else if (tx.isFlag(tfLoanImpair)) { - if (auto const ter = impairLoan( - view, - loanSle, - vaultSle, - principalOutstanding, - interestOutstanding, - j_)) + if (auto const ter = impairLoan(view, loanSle, vaultSle, j_)) return ter; } else if (tx.isFlag(tfLoanUnimpair)) { - if (auto const ter = unimpairLoan( - view, - loanSle, - vaultSle, - principalOutstanding, - interestOutstanding, - paymentInterval, - j_)) + if (auto const ter = unimpairLoan(view, loanSle, vaultSle, j_)) return ter; } diff --git a/src/xrpld/app/tx/detail/LoanManage.h b/src/xrpld/app/tx/detail/LoanManage.h index 92c5854115..0a043ca48c 100644 --- a/src/xrpld/app/tx/detail/LoanManage.h +++ b/src/xrpld/app/tx/detail/LoanManage.h @@ -53,8 +53,6 @@ public: SLE::ref loanSle, SLE::ref brokerSle, SLE::ref vaultSle, - Number const& principalOutstanding, - Number const& interestOutstanding, Asset const& vaultAsset, beast::Journal j); @@ -65,8 +63,6 @@ public: ApplyView& view, SLE::ref loanSle, SLE::ref vaultSle, - Number const& principalOutstanding, - Number const& interestOutstanding, beast::Journal j); /** Helper function that might be needed by other transactors @@ -76,9 +72,6 @@ public: ApplyView& view, SLE::ref loanSle, SLE::ref vaultSle, - Number const& principalOutstanding, - Number const& interestOutstanding, - std::uint32_t paymentInterval, beast::Journal j); TER diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 447eec5967..7be4b9fe21 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -347,7 +347,10 @@ LoanSet::doApply() if (auto const value = tx[field]; value && !isRounded(vaultAsset, *value, properties.loanScale)) { - JLOG(j_.warn()) << name << " has too much precision."; + JLOG(j_.warn()) + << name << " has too much precision. Total loan value is " + << properties.totalValueOutstanding << " with a scale of " + << properties.loanScale; return tecPRECISION_LOSS; } } From fb8dafa6a85d3c3fb9313f7bc9b78a483d3d955d Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 3 Oct 2025 18:51:55 -0400 Subject: [PATCH 117/291] Refactor Number, add tests, update LoanManage, update LoanPay - Loan tests are not expected to pass. - Refactor Number to put rounding logic into reusable functions. - Add Number tests to explicitly test rounding - may be redundant, but easy to reason about. Verifies that the default rounding matches banker's rounding. - Enable LoanManage. Apparently any remaining issues were fixed by the previous commit's cleanups. - Partially enabled LoanPay. --- src/libxrpl/basics/Number.cpp | 140 +++++++++++++------------ src/test/basics/Number_test.cpp | 110 +++++++++++++++++++ src/xrpld/app/tx/detail/LoanManage.cpp | 3 - src/xrpld/app/tx/detail/LoanPay.cpp | 3 - 4 files changed, 185 insertions(+), 71 deletions(-) diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index f43288b57b..928b638d43 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -93,6 +93,18 @@ public: // tie, round towards even. int round() noexcept; + + // Modify the result to the correctly rounded value + void + doRoundUp(rep& mantissa, int& exponent, std::string location); + + // Modify the result to the correctly rounded value + void + doRoundDown(rep& mantissa, int& exponent); + + // Modify the result to the correctly rounded value + void + doRound(rep& drops); }; inline void @@ -170,6 +182,61 @@ Number::Guard::round() noexcept return 0; } +void +Number::Guard::doRoundUp(rep& mantissa, int& exponent, std::string location) +{ + auto r = round(); + if (r == 1 || (r == 0 && (mantissa & 1) == 1)) + { + ++mantissa; + if (mantissa > maxMantissa) + { + mantissa /= 10; + ++exponent; + } + } + if (exponent < minExponent) + { + mantissa = 0; + exponent = Number{}.exponent_; + } + if (exponent > maxExponent) + throw std::overflow_error(location); +} + +void +Number::Guard::doRoundDown(rep& mantissa, int& exponent) +{ + auto r = round(); + if (r == 1 || (r == 0 && (mantissa & 1) == 1)) + { + --mantissa; + if (mantissa < minMantissa) + { + mantissa *= 10; + --exponent; + } + } + if (exponent < minExponent) + { + mantissa = 0; + exponent = Number{}.exponent_; + } +} + +// Modify the result to the correctly rounded value +void +Number::Guard::doRound(rep& drops) +{ + auto r = round(); + if (r == 1 || (r == 0 && (drops & 1) == 1)) + { + ++drops; + } + if (is_negative()) + drops = -drops; +} + // Number constexpr Number one{1000000000000000, -15, Number::unchecked{}}; @@ -209,18 +276,7 @@ Number::normalize() return; } - auto r = g.round(); - if (r == 1 || (r == 0 && (mantissa_ & 1) == 1)) - { - ++mantissa_; - if (mantissa_ > maxMantissa) - { - mantissa_ /= 10; - ++exponent_; - } - } - if (exponent_ > maxExponent) - throw std::overflow_error("Number::normalize 2"); + g.doRoundUp(mantissa_, exponent_, "Number::normalize 2"); if (negative) mantissa_ = -mantissa_; @@ -292,18 +348,7 @@ Number::operator+=(Number const& y) xm /= 10; ++xe; } - auto r = g.round(); - if (r == 1 || (r == 0 && (xm & 1) == 1)) - { - ++xm; - if (xm > maxMantissa) - { - xm /= 10; - ++xe; - } - } - if (xe > maxExponent) - throw std::overflow_error("Number::addition overflow"); + g.doRoundUp(xm, xe, "Number::addition overflow"); } else { @@ -323,21 +368,7 @@ Number::operator+=(Number const& y) xm -= g.pop(); --xe; } - auto r = g.round(); - if (r == 1 || (r == 0 && (xm & 1) == 1)) - { - --xm; - if (xm < minMantissa) - { - xm *= 10; - --xe; - } - } - if (xe < minExponent) - { - xm = 0; - xe = Number{}.exponent_; - } + g.doRoundDown(xm, xe); } mantissa_ = xm * xn; exponent_ = xe; @@ -417,25 +448,10 @@ Number::operator*=(Number const& y) } xm = static_cast(zm); xe = ze; - auto r = g.round(); - if (r == 1 || (r == 0 && (xm & 1) == 1)) - { - ++xm; - if (xm > maxMantissa) - { - xm /= 10; - ++xe; - } - } - if (xe < minExponent) - { - xm = 0; - xe = Number{}.exponent_; - } - if (xe > maxExponent) - throw std::overflow_error( - "Number::multiplication overflow : exponent is " + - std::to_string(xe)); + g.doRoundUp( + xm, + xe, + "Number::multiplication overflow : exponent is " + std::to_string(xe)); mantissa_ = xm * zn; exponent_ = xe; XRPL_ASSERT( @@ -500,13 +516,7 @@ Number::operator rep() const throw std::overflow_error("Number::operator rep() overflow"); drops *= 10; } - auto r = g.round(); - if (r == 1 || (r == 0 && (drops & 1) == 1)) - { - ++drops; - } - if (g.is_negative()) - drops = -drops; + g.doRound(drops); } return drops; } diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index f24c0b35e1..acebf86cc5 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -744,6 +744,115 @@ public: BEAST_EXPECT(Number(-100, -30000).truncate() == Number(0, 0)); } + void + testRounding() + { + // Test that rounding works as expected. + testcase("Rounding"); + + using NumberRoundings = std::map; + + std::map const expected{ + // Positive numbers + {Number{13, -1}, + {{Number::to_nearest, 1}, + {Number::towards_zero, 1}, + {Number::downward, 1}, + {Number::upward, 2}}}, + {Number{23, -1}, + {{Number::to_nearest, 2}, + {Number::towards_zero, 2}, + {Number::downward, 2}, + {Number::upward, 3}}}, + {Number{15, -1}, + {{Number::to_nearest, 2}, + {Number::towards_zero, 1}, + {Number::downward, 1}, + {Number::upward, 2}}}, + {Number{25, -1}, + {{Number::to_nearest, 2}, + {Number::towards_zero, 2}, + {Number::downward, 2}, + {Number::upward, 3}}}, + {Number{152, -2}, + {{Number::to_nearest, 2}, + {Number::towards_zero, 1}, + {Number::downward, 1}, + {Number::upward, 2}}}, + {Number{252, -2}, + {{Number::to_nearest, 3}, + {Number::towards_zero, 2}, + {Number::downward, 2}, + {Number::upward, 3}}}, + {Number{17, -1}, + {{Number::to_nearest, 2}, + {Number::towards_zero, 1}, + {Number::downward, 1}, + {Number::upward, 2}}}, + {Number{27, -1}, + {{Number::to_nearest, 3}, + {Number::towards_zero, 2}, + {Number::downward, 2}, + {Number::upward, 3}}}, + + // Negative numbers + {Number{-13, -1}, + {{Number::to_nearest, -1}, + {Number::towards_zero, -1}, + {Number::downward, -2}, + {Number::upward, -1}}}, + {Number{-23, -1}, + {{Number::to_nearest, -2}, + {Number::towards_zero, -2}, + {Number::downward, -3}, + {Number::upward, -2}}}, + {Number{-15, -1}, + {{Number::to_nearest, -2}, + {Number::towards_zero, -1}, + {Number::downward, -2}, + {Number::upward, -1}}}, + {Number{-25, -1}, + {{Number::to_nearest, -2}, + {Number::towards_zero, -2}, + {Number::downward, -3}, + {Number::upward, -2}}}, + {Number{-152, -2}, + {{Number::to_nearest, -2}, + {Number::towards_zero, -1}, + {Number::downward, -2}, + {Number::upward, -1}}}, + {Number{-252, -2}, + {{Number::to_nearest, -3}, + {Number::towards_zero, -2}, + {Number::downward, -3}, + {Number::upward, -2}}}, + {Number{-17, -1}, + {{Number::to_nearest, -2}, + {Number::towards_zero, -1}, + {Number::downward, -2}, + {Number::upward, -1}}}, + {Number{-27, -1}, + {{Number::to_nearest, -3}, + {Number::towards_zero, -2}, + {Number::downward, -3}, + {Number::upward, -2}}}, + }; + + for (auto const& [num, roundings] : expected) + { + for (auto const& [mode, val] : roundings) + { + NumberRoundModeGuard g{mode}; + auto const res = static_cast(num); + BEAST_EXPECTS( + res == val, + to_string(num) + " with mode " + std::to_string(mode) + + " expected " + std::to_string(val) + " got " + + std::to_string(res)); + } + } + } + void run() override { @@ -765,6 +874,7 @@ public: test_inc_dec(); test_toSTAmount(); test_truncate(); + testRounding(); } }; diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index 5e68616d5b..5bc55870b4 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -367,8 +367,6 @@ LoanManage::unimpairLoan( TER LoanManage::doApply() { - return temDISABLED; -#if LOANCOMPLETE auto const& tx = ctx_.tx; auto& view = ctx_.view(); @@ -407,7 +405,6 @@ LoanManage::doApply() } return tesSUCCESS; -#endif } //------------------------------------------------------------------------------ diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 829e97ae4c..a74810b8e2 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -45,8 +45,6 @@ LoanPay::preflight(PreflightContext const& ctx) TER LoanPay::preclaim(PreclaimContext const& ctx) { - return temDISABLED; -#if LOANCOMPLETE auto const& tx = ctx.tx; auto const account = tx[sfAccount]; @@ -119,7 +117,6 @@ LoanPay::preclaim(PreclaimContext const& ctx) } return tesSUCCESS; -#endif } TER From 96d0258f51a894fe5cef17d46bc67a873f89811f Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 3 Oct 2025 22:38:17 -0400 Subject: [PATCH 118/291] Update LoanPay - Enable the rest of LoanPay. - Start updating the helper functions. - Tests are not expected to pass. --- src/xrpld/app/misc/LendingHelpers.h | 226 ++++++++++++------- src/xrpld/app/misc/detail/LendingHelpers.cpp | 14 +- src/xrpld/app/tx/detail/LoanPay.cpp | 166 ++++++++------ 3 files changed, 244 insertions(+), 162 deletions(-) diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index c1a18106e4..c1fe42d46e 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -37,6 +37,7 @@ struct PaymentParts Number rawPrincipal; Number roundedInterest; Number roundedPrincipal; + // We may not need roundedPayment Number roundedPayment; bool final = false; }; @@ -66,8 +67,7 @@ loanLatePaymentInterest( Number const& principalOutstanding, TenthBips32 lateInterestRate, NetClock::time_point parentCloseTime, - std::uint32_t startDate, - std::uint32_t prevPaymentDate); + std::uint32_t nextPaymentDueDate); Number loanAccruedInterest( @@ -139,8 +139,6 @@ computePaymentParts( * The principal and interest portions can be derived as follows: * interest = principalOutstanding * periodicRate * principal = periodicPayment - interest - * - * Because those values deal with funds, they need to be rounded. */ Number const rawInterest = referencePrincipal * periodicRate; Number const rawPrincipal = periodicPayment - rawInterest; @@ -153,11 +151,45 @@ computePaymentParts( "ripple::detail::computePaymentParts", "valid raw principal"); - Number const roundedInterest = - roundToAsset(asset, rawInterest, scale, Number::upward); - Number const roundedPrincipal = roundedPeriodicPayment - roundedInterest; + // if (count($A20), MIN(Z19, Z19 - FLOOR(AA19 - Y20, 1)), "") + // Z19 = outstanding principal + // AA19 = reference principal + // Y20 = raw principal + + Number const roundedPrincipal = [&]() { + Number const p = std::max( + Number{}, + std::min( + principalOutstanding, + principalOutstanding - + roundToAsset( + asset, + referencePrincipal - rawPrincipal, + scale, + Number::downward))); + // if the estimated principal payment would leave the principal higher + // than the "total "after payment" value of the loan, make the principal + // payment also take the principal down to that same "after" value. + // This should mean that all interest is paid, or that the loan has some + // tricky parameters. + if (principalOutstanding - p > + totalValueOutstanding - roundedPeriodicPayment) + return roundedPeriodicPayment; + // Use the amount that will get principal outstanding as close to + // reference principal as possible. + return p; + }(); + + // if(count($A20), if(AB19 < $B$5, AB19 - Z19, CEILING($B$10-W20, 1)), "") + // AB19 = total loan value + // $B$5 = periodic payment (unrounded) + // Z19 = outstanding principal + // $B$10 = periodic payment (rounded up) + // W20 = rounded principal + + Number const roundedInterest = roundedPeriodicPayment - roundedPrincipal; XRPL_ASSERT_PARTS( - roundedInterest >= 0, + roundedInterest >= 0 && isRounded(asset, roundedInterest, scale), "ripple::detail::computePaymentParts", "valid rounded interest"); XRPL_ASSERT_PARTS( @@ -213,6 +245,10 @@ computeLoanProperties( { auto const periodicRate = detail::loanPeriodicRate(interestRate, paymentInterval); + XRPL_ASSERT( + interestRate == 0 || periodicRate > 0, + "ripple::loanMakePayment : valid rate"); + auto const periodicPayment = detail::loanPeriodicPayment( principalOutstanding, periodicRate, paymentsRemaining); Number const totalValueOutstanding = [&]() { @@ -445,9 +481,8 @@ loanLatePaymentInterest( Number const& principalOutstanding, TenthBips32 lateInterestRate, NetClock::time_point parentCloseTime, - std::uint32_t startDate, - std::uint32_t prevPaymentDate, - Number const& scale) + std::uint32_t nextPaymentDueDate, + std::int32_t const& scale) { return roundToAsset( asset, @@ -455,8 +490,7 @@ loanLatePaymentInterest( principalOutstanding, lateInterestRate, parentCloseTime, - startDate, - prevPaymentDate), + nextPaymentDueDate), scale); } @@ -484,9 +518,34 @@ struct LoanPaymentParts */ Number valueChange; /// fee_paid is the amount of fee that the payment covered. - Number feePaid; + Number feeToPay; }; +template +void +doPayment( + PaymentParts const& payment, + NumberProxy& totalValueOutstandingProxy, + NumberProxy& principalOutstandingProxy, + NumberProxy& referencePrincipalProxy, + Int32Proxy& paymentRemainingProxy, + Int32Proxy& prevPaymentDateProxy, + Int32Proxy& nextDueDateProxy, + std::uint32_t paymentInterval) +{ + paymentRemainingProxy -= 1; + // A single payment always pays the same amount of principal. Only the + // interest and fees are extra for a late payment + referencePrincipalProxy -= payment.rawPrincipal; + principalOutstandingProxy -= payment.roundedPrincipal; + totalValueOutstandingProxy -= + payment.roundedPrincipal + payment.roundedInterest; + + // Make sure this does an assignment + prevPaymentDateProxy = nextDueDateProxy; + nextDueDateProxy += paymentInterval; +} + /* Handle possible late payments. * * If this function processed a late payment, the return value will be @@ -501,25 +560,20 @@ struct LoanPaymentParts * * section 3.2.4.1.2 (Late Payment) */ template -Expected +Expected, TER> handleLatePayment( A const& asset, ApplyView& view, NumberProxy& principalOutstandingProxy, - Int32Proxy& paymentRemainingProxy, - Int32Proxy& prevPaymentDateProxy, Int32Proxy& nextDueDateProxy, PaymentParts const& periodic, - std::uint32_t const startDate, - std::uint32_t const paymentInterval, TenthBips32 const lateInterestRate, std::int32_t loanScale, + Number const& paymentFee, Number const& latePaymentFee, STAmount const& amount, beast::Journal j) { - return Unexpected(temDISABLED); -#if LOANCOMPLETE if (!hasExpired(view, nextDueDateProxy)) return Unexpected(tesSUCCESS); @@ -531,17 +585,23 @@ handleLatePayment( principalOutstandingProxy, lateInterestRate, view.parentCloseTime(), - startDate, - prevPaymentDateProxy, + nextDueDateProxy, loanScale); XRPL_ASSERT( latePaymentInterest >= 0, "ripple::handleLatePayment : valid late interest"); PaymentParts const late{ - .interest = latePaymentInterest + periodic.interest, - .principal = periodic.principal, - .fee = latePaymentFee + periodic.fee}; - auto const totalDue = late.principal + late.interest + late.fee; + .rawInterest = periodic.rawInterest + latePaymentInterest, + .rawPrincipal = periodic.rawPrincipal, + .roundedInterest = periodic.roundedInterest + latePaymentInterest, + .roundedPrincipal = periodic.roundedPrincipal, + .roundedPayment = periodic.roundedPayment}; + auto const fee = paymentFee + latePaymentFee; + auto const totalDue = late.roundedPrincipal + late.roundedInterest + fee; + XRPL_ASSERT_PARTS( + isRounded(asset, totalDue, loanScale), + "ripple::handleLatePayment", + "total due is rounded"); if (amount < totalDue) { @@ -550,23 +610,15 @@ handleLatePayment( return Unexpected(tecINSUFFICIENT_PAYMENT); } - paymentRemainingProxy -= 1; - // A single payment always pays the same amount of principal. Only the - // interest and fees are extra for a late payment - principalOutstandingProxy -= late.principal; - - // Make sure this does an assignment - prevPaymentDateProxy = nextDueDateProxy; - nextDueDateProxy += paymentInterval; - // A late payment increases the value of the loan by the difference // between periodic and late payment interest - return LoanPaymentParts{ - .principalPaid = late.principal, - .interestPaid = late.interest, - .valueChange = latePaymentInterest, - .feePaid = late.fee}; -#endif + return std::make_pair( + late, + LoanPaymentParts{ + .principalPaid = late.roundedPrincipal, + .interestPaid = late.roundedInterest, + .valueChange = latePaymentInterest, + .feeToPay = fee}); } /* Handle possible full payments. @@ -641,7 +693,7 @@ handleFullPayment( .principalPaid = principalOutstandingProxy, .interestPaid = totalInterest, .valueChange = valueChange, - .feePaid = closePaymentFee}; + .feeToPay = closePaymentFee}; paymentRemainingProxy = 0; principalOutstandingProxy = 0; @@ -664,7 +716,9 @@ loanMakePayment( */ std::int32_t const loanScale = loan->at(sfLoanScale); auto totalValueOutstandingProxy = loan->at(sfTotalValueOutstanding); + auto interestOwedProxy = loan->at(sfInterestOwed); auto principalOutstandingProxy = loan->at(sfPrincipalOutstanding); + auto referencePrincipalProxy = loan->at(sfReferencePrincipal); bool const allowOverpayment = loan->isFlag(lsfLoanOverpayment); TenthBips32 const interestRate{loan->at(sfInterestRate)}; @@ -679,7 +733,7 @@ loanMakePayment( roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale); TenthBips32 const overpaymentFee{loan->at(sfOverpaymentFee)}; - std::uint32_t const paymentInterval = loan->at(sfPaymentInterval); + auto const periodicPayment = loan->at(sfPeriodicPayment); auto paymentRemainingProxy = loan->at(sfPaymentRemaining); auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDate); @@ -693,6 +747,7 @@ loanMakePayment( return Unexpected(tecKILLED); } + std::uint32_t const paymentInterval = loan->at(sfPaymentInterval); // Compute the normal periodic rate, payment, etc. // We'll need it in the remaining calculations Number const periodicRate = @@ -701,66 +756,61 @@ loanMakePayment( interestRate == 0 || periodicRate > 0, "ripple::loanMakePayment : valid rate"); - // Don't round the payment amount. Only round the final computations - // using it. - Number const periodicPaymentAmount = detail::loanPeriodicPayment( - principalOutstandingProxy, periodicRate, paymentRemainingProxy); XRPL_ASSERT( - periodicPaymentAmount > 0, - "ripple::computePeriodicPayment : valid payment"); + *totalValueOutstandingProxy > 0, + "ripple::loanMakePayment : valid total value"); + XRPL_ASSERT_PARTS( + *interestOwedProxy >= 0, + "ripple::loanMakePayment", + "valid interest owed"); - auto const periodic = computePaymentParts( + view.update(loan); + + auto const periodic = detail::computePaymentParts( asset, loanScale, totalValueOutstandingProxy, principalOutstandingProxy, - periodicPaymentAmount, - serviceFee, + referencePrincipalProxy, + periodicPayment, periodicRate, paymentRemainingProxy); - Number const totalValueOutstanding = loanTotalValueOutstanding( - asset, loanScale, periodicPaymentAmount, paymentRemainingProxy); - XRPL_ASSERT( - totalValueOutstanding > 0, - "ripple::loanMakePayment : valid total value"); - Number const totalInterestOutstanding = loanTotalInterestOutstanding( - principalOutstandingProxy, totalValueOutstanding); - XRPL_ASSERT_PARTS( - totalInterestOutstanding >= 0, - "ripple::loanMakePayment", - "valid total interest"); - XRPL_ASSERT_PARTS( - totalValueOutstanding - totalInterestOutstanding == - principalOutstandingProxy, - "ripple::loanMakePayment", - "valid principal computation"); - - view.update(loan); - // ------------------------------------------------------------- // late payment handling if (auto const latePaymentParts = handleLatePayment( asset, view, principalOutstandingProxy, - paymentRemainingProxy, - prevPaymentDateProxy, nextDueDateProxy, periodic, - startDate, - paymentInterval, lateInterestRate, loanScale, + serviceFee, latePaymentFee, amount, j)) - return *latePaymentParts; + { + doPayment( + latePaymentParts->first, + totalValueOutstandingProxy, + principalOutstandingProxy, + referencePrincipalProxy, + paymentRemainingProxy, + prevPaymentDateProxy, + nextDueDateProxy, + paymentInterval); + + return latePaymentParts->second; + } else if (latePaymentParts.error()) - return latePaymentParts; + return Unexpected(latePaymentParts.error()); // ------------------------------------------------------------- // full payment handling + auto const totalInterestOutstanding = + totalValueOutstandingProxy - principalOutstandingProxy; + if (auto const fullPaymentParts = handleFullPayment( asset, view, @@ -783,11 +833,16 @@ loanMakePayment( // ------------------------------------------------------------- // regular periodic payment handling + return Unexpected(temDISABLED); +#if LOANCOMPLETE // if the payment is not late nor if it's a full payment, then it must // be a periodic one, with possible overpayments auto const totalDue = periodic.interest + periodic.principal + periodic.fee; + // TODO: Don't attempt to figure out the number of payments beforehand. Just + // loop over making payments until the `amount` is used up or the loan is + // paid off. std::optional mg(Number::downward); std::int64_t const fullPeriodicPayments = [&]() { std::int64_t const full{amount / totalDue}; @@ -826,7 +881,7 @@ loanMakePayment( loanScale, totalValueOutstandingProxy, principalOutstandingProxy, - periodicPaymentAmount, + periodicPayment, serviceFee, periodicRate, paymentRemainingProxy); @@ -845,7 +900,7 @@ loanMakePayment( future.reset(); } - Number totalFeePaid = serviceFee * fullPeriodicPayments; + Number totalfeeToPay = serviceFee * fullPeriodicPayments; Number const newInterest = loanTotalInterestOutstanding( asset, @@ -863,7 +918,7 @@ loanMakePayment( { Number const overpayment = std::min( principalOutstandingProxy.value(), - amount - (totalPrincipalPaid + totalInterestPaid + totalFeePaid)); + amount - (totalPrincipalPaid + totalInterestPaid + totalfeeToPay)); if (roundToAsset(asset, overpayment, loanScale) > 0) { @@ -885,7 +940,7 @@ loanMakePayment( overpaymentInterestPortion = interestPortion; totalPrincipalPaid += remainder; totalInterestPaid += interestPortion; - totalFeePaid += feePortion; + totalfeeToPay += feePortion; principalOutstandingProxy -= remainder; } @@ -908,13 +963,14 @@ loanMakePayment( roundToAsset(asset, loanValueChange, loanScale) == loanValueChange, "ripple::loanMakePayment : loanValueChange rounded"); XRPL_ASSERT( - roundToAsset(asset, totalFeePaid, loanScale) == totalFeePaid, - "ripple::loanMakePayment : totalFeePaid rounded"); + roundToAsset(asset, totalfeeToPay, loanScale) == totalfeeToPay, + "ripple::loanMakePayment : totalfeeToPay rounded"); return LoanPaymentParts{ .principalPaid = totalPrincipalPaid, .interestPaid = totalInterestPaid, .valueChange = loanValueChange, - .feePaid = totalFeePaid}; + .feeToPay = totalfeeToPay}; +#endif } } // namespace ripple diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index c56e05acff..a64ffb92b4 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -94,20 +94,18 @@ loanLatePaymentInterest( Number const& principalOutstanding, TenthBips32 lateInterestRate, NetClock::time_point parentCloseTime, - std::uint32_t startDate, - std::uint32_t prevPaymentDate) + std::uint32_t nextPaymentDueDate) { /* * This formula is from the XLS-66 spec, section 3.2.4.1.2 (Late payment), * specifically "latePaymentInterest = ..." + * + * The spec is to be updated to base the duration on the next due date */ - auto const lastPaymentDate = std::max(prevPaymentDate, startDate); + auto const secondsOverdue = + parentCloseTime.time_since_epoch().count() - nextPaymentDueDate; - auto const secondsSinceLastPayment = - parentCloseTime.time_since_epoch().count() - lastPaymentDate; - - auto const rate = - loanPeriodicRate(lateInterestRate, secondsSinceLastPayment); + auto const rate = loanPeriodicRate(lateInterestRate, secondsOverdue); return principalOutstanding * rate; } diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index a74810b8e2..48eaab83a3 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -122,8 +122,6 @@ LoanPay::preclaim(PreclaimContext const& ctx) TER LoanPay::doApply() { - return temDISABLED; -#if LOANCOMPLETE auto const& tx = ctx_.tx; auto& view = ctx_.view(); @@ -149,37 +147,13 @@ LoanPay::doApply() //------------------------------------------------------ // Loan object state changes - std::int32_t const loanScale = loanSle->at(sfLoanScale); // Unimpair the loan if it was impaired. Do this before the payment is // attempted, so the original values can be used. If the payment fails, this // change will be discarded. if (loanSle->isFlag(lsfLoanImpaired)) { - TenthBips32 const interestRate{loanSle->at(sfInterestRate)}; - auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding); - - TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; - auto const paymentInterval = loanSle->at(sfPaymentInterval); - auto const paymentsRemaining = loanSle->at(sfPaymentRemaining); - - auto const interestOutstanding = loanInterestOutstandingMinusFee( - asset, - loanScale, - principalOutstanding.value(), - interestRate, - paymentInterval, - paymentsRemaining, - managementFeeRate); - - LoanManage::unimpairLoan( - view, - loanSle, - vaultSle, - principalOutstanding, - interestOutstanding, - paymentInterval, - j_); + LoanManage::unimpairLoan(view, loanSle, vaultSle, j_); } Expected paymentParts = @@ -193,7 +167,8 @@ LoanPay::doApply() view.update(loanSle); XRPL_ASSERT_PARTS( - paymentParts->principalPaid > 0, + // It is possible to pay 0 interest + paymentParts->principalPaid >= 0, "ripple::LoanPay::doApply", "valid principal paid"); XRPL_ASSERT_PARTS( @@ -201,78 +176,83 @@ LoanPay::doApply() "ripple::LoanPay::doApply", "valid interest paid"); XRPL_ASSERT_PARTS( - paymentParts->feePaid >= 0, + paymentParts->feeToPay >= 0, "ripple::LoanPay::doApply", "valid fee paid"); - if (paymentParts->principalPaid <= 0 || paymentParts->interestPaid < 0 || - paymentParts->feePaid < 0) + if (paymentParts->principalPaid < 0 || paymentParts->interestPaid < 0 || + paymentParts->feeToPay < 0) { // LCOV_EXCL_START JLOG(j_.fatal()) << "Loan payment computation returned invalid values."; - return tecINTERNAL; + return tecLIMIT_EXCEEDED; // LCOV_EXCL_STOP } + std::int32_t const loanScale = loanSle->at(sfLoanScale); + //------------------------------------------------------ // LoanBroker object state changes view.update(brokerSle); TenthBips32 managementFeeRate{brokerSle->at(sfManagementFeeRate)}; - auto const managementFee = roundToAsset( - asset, - tenthBipsOfValue(paymentParts->interestPaid, managementFeeRate), - loanScale); + auto interestOwedProxy = loanSle->at(sfInterestOwed); - auto const totalPaidToVault = paymentParts->principalPaid + - paymentParts->interestPaid - managementFee; + auto const [managementFee, interestPaidToVault] = [&]() { + auto const managementFee = roundToAsset( + asset, + tenthBipsOfValue(paymentParts->interestPaid, managementFeeRate), + loanScale); + auto const interest = paymentParts->interestPaid - managementFee; + auto const owed = *interestOwedProxy; + if (interest > owed) + return std::make_pair(interest - owed, owed); + return std::make_pair(managementFee, interest); + }(); + XRPL_ASSERT_PARTS( + managementFee >= 0 && interestPaidToVault >= 0 && + (managementFee + interestPaidToVault == + paymentParts->interestPaid) && + isRounded(asset, managementFee, loanScale) && + isRounded(asset, interestPaidToVault, loanScale), + "ripple::LoanPay::doApply", + "management fee computation is valid"); + auto const totalPaidToVault = + paymentParts->principalPaid + interestPaidToVault; - auto const totalPaidToBroker = paymentParts->feePaid + managementFee; + auto const totalPaidToBroker = paymentParts->feeToPay + managementFee; XRPL_ASSERT_PARTS( (totalPaidToVault + totalPaidToBroker) == (paymentParts->principalPaid + paymentParts->interestPaid + - paymentParts->feePaid), + paymentParts->feeToPay), "ripple::LoanPay::doApply", "payments add up"); - // If there is not enough first-loss capital - auto coverAvailableField = brokerSle->at(sfCoverAvailable); - auto debtTotalField = brokerSle->at(sfDebtTotal); - TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)}; + auto debtTotalProxy = brokerSle->at(sfDebtTotal); - bool const sufficientCover = coverAvailableField >= - roundToAsset(asset, - tenthBipsOfValue(debtTotalField.value(), coverRateMinimum), - loanScale); - if (!sufficientCover) - { - // Add the fee to First Loss Cover Pool - coverAvailableField += totalPaidToBroker; - } - auto const brokerPayee = - sufficientCover ? brokerOwner : brokerPseudoAccount; - - // Decrease LoanBroker Debt by the amount paid, add the Loan value change, - // and subtract the change in the management fee - auto const vaultValueChange = valueMinusManagementFee( - asset, paymentParts->valueChange, managementFeeRate, loanScale); - // debtDecrease may be negative, increasing the debt - auto const debtDecrease = totalPaidToVault - vaultValueChange; + // Decrease LoanBroker Debt by the amount paid, add the Loan value change + // (which might be negative). debtDecrease may be negative, increasing the + // debt + auto const debtDecrease = totalPaidToVault - paymentParts->valueChange; XRPL_ASSERT_PARTS( - roundToAsset(asset, debtDecrease, loanScale) == debtDecrease, + isRounded(asset, debtDecrease, loanScale), "ripple::LoanPay::doApply", "debtDecrease rounding good"); - if (debtDecrease >= debtTotalField) - debtTotalField = 0; + // Despite our best efforts, it's possible for rounding errors to accumulate + // in the loan broker's debt total. This is because the broker may have more + // that one loan with significantly different scales. + if (debtDecrease >= debtTotalProxy) + debtTotalProxy = 0; else - debtTotalField -= debtDecrease; + debtTotalProxy -= debtDecrease; //------------------------------------------------------ // Vault object state changes view.update(vaultSle); vaultSle->at(sfAssetsAvailable) += totalPaidToVault; - vaultSle->at(sfAssetsTotal) += vaultValueChange; + vaultSle->at(sfAssetsTotal) += paymentParts->valueChange; + interestOwedProxy -= interestPaidToVault; XRPL_ASSERT_PARTS( *vaultSle->at(sfAssetsAvailable) <= *vaultSle->at(sfAssetsTotal), "ripple::LoanPay::doApply", @@ -287,16 +267,37 @@ LoanPay::doApply() "amount is sufficient"); XRPL_ASSERT_PARTS( paidToVault + paidToBroker <= paymentParts->principalPaid + - paymentParts->interestPaid + paymentParts->feePaid, + paymentParts->interestPaid + paymentParts->feeToPay, "ripple::LoanPay::doApply", "payment agreement"); + // Determine where to send the broker's fee + auto coverAvailableProxy = brokerSle->at(sfCoverAvailable); + TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)}; + + bool const sufficientCover = coverAvailableProxy >= + roundToAsset(asset, + tenthBipsOfValue(debtTotalProxy.value(), coverRateMinimum), + loanScale); + if (!sufficientCover) + { + // If there is not enough first-loss capital, add the fee to First Loss + // Cover Pool. Note that this moves the entire fee - it does not attempt + // to split it. The broker can Withdraw it later if they want, or leave + // it for future needs. + coverAvailableProxy += totalPaidToBroker; + } + auto const brokerPayee = + sufficientCover ? brokerOwner : brokerPseudoAccount; + +#if !NDEBUG auto const accountBalanceBefore = accountHolds(view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); auto const vaultBalanceBefore = accountHolds( view, vaultPseudoAccount, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); auto const brokerBalanceBefore = accountHolds( view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); +#endif if (auto const ter = accountSend( view, @@ -315,8 +316,35 @@ LoanPay::doApply() WaiveTransferFee::Yes)) return ter; - return tesSUCCESS; +#if !NDEBUG + auto const accountBalanceAfter = + accountHolds(view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); + auto const vaultBalanceAfter = accountHolds( + view, vaultPseudoAccount, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); + auto const brokerBalanceAfter = accountHolds( + view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); + + auto const balanceScale = std::max( + {accountBalanceBefore.exponent(), + vaultBalanceBefore.exponent(), + brokerBalanceBefore.exponent(), + accountBalanceAfter.exponent(), + vaultBalanceAfter.exponent(), + brokerBalanceAfter.exponent()}); + XRPL_ASSERT_PARTS( + roundToAsset( + asset, + accountBalanceBefore + vaultBalanceBefore + brokerBalanceBefore, + balanceScale) == + roundToAsset( + asset, + accountBalanceAfter + vaultBalanceAfter + brokerBalanceAfter, + balanceScale), + "ripple::LoanPay::doApply", + "funds are conserved (with rounding)"); #endif + + return tesSUCCESS; } //------------------------------------------------------------------------------ From 97e2c10359032382cb62025da6093c8237096c53 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sat, 4 Oct 2025 18:07:28 -0500 Subject: [PATCH 119/291] Continue progress updating the LoanHelpers - May not build --- src/xrpld/app/misc/LendingHelpers.h | 632 +++++++++++++++------------- 1 file changed, 341 insertions(+), 291 deletions(-) diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index c1fe42d46e..e4ab1c05df 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -31,7 +31,9 @@ struct PreflightContext; bool checkLendingProtocolDependencies(PreflightContext const& ctx); -struct PaymentParts +// This structure is used internally to compute the breakdown of a +// single loan payment +struct PaymentComponents { Number rawInterest; Number rawPrincipal; @@ -42,6 +44,25 @@ struct PaymentParts bool final = false; }; +// This structure is explained in the XLS-66 spec, section 3.2.4.4 (Failure +// Conditions) +struct LoanPaymentParts +{ + /// principal_paid is the amount of principal that the payment covered. + Number principalPaid; + /// interest_paid is the amount of interest that the payment covered. + Number interestPaid; + /** + * value_change is the amount by which the total value of the Loan changed. + * If value_change < 0, Loan value decreased. + * If value_change > 0, Loan value increased. + * This is 0 for regular payments. + */ + Number valueChange; + /// fee_paid is the amount of fee that the payment covered. + Number feeToPay; +}; + namespace detail { // These functions should rarely be used directly. More often, the ultimate // result needs to be roundToAsset'd. @@ -79,14 +100,14 @@ loanAccruedInterest( std::uint32_t paymentInterval); inline Number -minusManagementFee(Number const& value, TenthBips32 managementFeeRate) +minusFee(Number const& value, TenthBips32 managementFeeRate) { return tenthBipsOfValue(value, tenthBipsPerUnity - managementFeeRate); } template -PaymentParts -computePaymentParts( +PaymentComponents +computePaymentComponents( A const& asset, std::int32_t scale, Number const& totalValueOutstanding, @@ -103,7 +124,7 @@ computePaymentParts( XRPL_ASSERT_PARTS( isRounded(asset, totalValueOutstanding, scale) && isRounded(asset, principalOutstanding, scale), - "ripple::detail::computePaymentParts", + "ripple::detail::computePaymentComponents", "Outstanding values are rounded"); auto const roundedPeriodicPayment = roundToAsset(asset, periodicPayment, scale, Number::upward); @@ -121,7 +142,7 @@ computePaymentParts( XRPL_ASSERT_PARTS( rawInterest + referencePrincipal == roundedInterest + principalOutstanding, - "ripple::detail::computePaymentParts", + "ripple::detail::computePaymentComponents", "last payment is complete"); Number const interest = totalValueOutstanding - principalOutstanding; @@ -130,7 +151,7 @@ computePaymentParts( .rawPrincipal = referencePrincipal, .roundedInterest = roundedInterest, .roundedPrincipal = principalOutstanding, - .roundedPayment = roundedPeriodicPayment, + .roundedPayment = roundedInterest + principalOutstanding, .final = true}; } /* @@ -144,11 +165,11 @@ computePaymentParts( Number const rawPrincipal = periodicPayment - rawInterest; XRPL_ASSERT_PARTS( rawInterest >= 0, - "ripple::detail::computePaymentParts", + "ripple::detail::computePaymentComponents", "valid raw interest"); XRPL_ASSERT_PARTS( rawPrincipal > 0 && rawPrincipal <= referencePrincipal, - "ripple::detail::computePaymentParts", + "ripple::detail::computePaymentComponents", "valid raw principal"); // if (count($A20), MIN(Z19, Z19 - FLOOR(AA19 - Y20, 1)), "") @@ -190,15 +211,15 @@ computePaymentParts( Number const roundedInterest = roundedPeriodicPayment - roundedPrincipal; XRPL_ASSERT_PARTS( roundedInterest >= 0 && isRounded(asset, roundedInterest, scale), - "ripple::detail::computePaymentParts", + "ripple::detail::computePaymentComponents", "valid rounded interest"); XRPL_ASSERT_PARTS( roundedPrincipal >= 0 && roundedPrincipal <= principalOutstanding, - "ripple::detail::computePaymentParts", + "ripple::detail::computePaymentComponents", "valid rounded principal"); XRPL_ASSERT_PARTS( isRounded(asset, roundedPrincipal, scale), - "ripple::detail::computePaymentParts", + "ripple::detail::computePaymentComponents", "principal is rounded"); return { @@ -209,6 +230,229 @@ computePaymentParts( .roundedPayment = roundedPeriodicPayment}; } +struct PaymentComponentsPlus : public PaymentComponents +{ + Number fee{0}; + Number valueChange{0}; + + PaymentComponentsPlus( + PaymentComponents const& p, + Number f, + Number v = Number{}) + : PaymentComponents(p), fee(f), valueChange(v) + { + } +}; + +template +LoanPaymentParts +doPayment( + PaymentComponentsPlus const& payment, + NumberProxy& totalValueOutstandingProxy, + NumberProxy& principalOutstandingProxy, + NumberProxy& referencePrincipalProxy, + Int32Proxy& paymentRemainingProxy, + Int32Proxy& prevPaymentDateProxy, + Int32Proxy& nextDueDateProxy, + std::uint32_t paymentInterval) +{ + if (payment.final) + { + paymentRemainingProxy = 0; + XRPL_ASSERT_PARTS( + referencePrincipalProxy == payment.rawPrincipal, + "ripple::detail::doPayment", + "Full reference principal payment"); + XRPL_ASSERT_PARTS( + principalOutstandingProxy == payment.roundedPrincipal, + "ripple::detail::doPayment", + "Full principal payment"); + XRPL_ASSSERT_PARTS( + totalValueOutstandingProxy == + payment.roundedPrincipal + payment.roundedInterest, + "ripple::detail::doPayment", + "Full value payment"); + + prevPaymentDateProxy = nextDueDateProxy; + // May as well... + nextDueDateProxy = 0; + } + else + { + paymentRemainingProxy -= 1; + + prevPaymentDateProxy = nextDueDateProxy; + nextDueDateProxy += paymentInterval; + } + // A single payment always pays the same amount of principal. Only the + // interest and fees are extra for a late payment + referencePrincipalProxy -= payment.rawPrincipal; + principalOutstandingProxy -= payment.roundedPrincipal; + totalValueOutstandingProxy -= + payment.roundedPrincipal + payment.roundedInterest; + + return LoanPaymentParts{ + .principalPaid = payment.roundedPrincipal, + .interestPaid = payment.roundedInterest, + .valueChange = payment.valueChange, + .feeToPay = payment.fee}; +} + +/* Handle possible late payments. + * + * If this function processed a late payment, the return value will be + * a LoanPaymentParts object. If the loan is not late, the return will be an + * Unexpected(tesSUCCESS). Otherwise, it'll be an Unexpected with the error code + * the caller is expected to return. + * + * + * This function is an implementation of the XLS-66 spec, based on + * * section 3.2.4.3 (Transaction Pseudo-code), specifically the bit + * labeled "the payment is late" + * * section 3.2.4.1.2 (Late Payment) + */ +template +Expected +handleLatePayment( + A const& asset, + ApplyView& view, + NumberProxy& principalOutstandingProxy, + Int32Proxy& nextDueDateProxy, + PaymentComponentsPlus const& periodic, + TenthBips32 const lateInterestRate, + std::int32_t loanScale, + Number const& latePaymentFee, + STAmount const& amount, + beast::Journal j) +{ + if (!hasExpired(view, nextDueDateProxy)) + return Unexpected(tesSUCCESS); + + // the payment is late + // Late payment interest is only the part of the interest that comes from + // being late, as computed by 3.2.4.1.2. + auto const latePaymentInterest = loanLatePaymentInterest( + asset, + principalOutstandingProxy, + lateInterestRate, + view.parentCloseTime(), + nextDueDateProxy, + loanScale); + XRPL_ASSERT( + latePaymentInterest >= 0, + "ripple::detail::handleLatePayment : valid late interest"); + PaymentComponentsPlus const late{ + PaymentComponents{ + .rawInterest = periodic.rawInterest + latePaymentInterest, + .rawPrincipal = periodic.rawPrincipal, + .roundedInterest = periodic.roundedInterest + latePaymentInterest, + .roundedPrincipal = periodic.roundedPrincipal, + .roundedPayment = periodic.roundedPayment}, + // A late payment pays both the normal fee, and the extra fee + periodic.fee + latePaymentFee, + // A late payment increases the value of the loan by the difference + // between periodic and late payment interest + latePaymentInterest}; + auto const totalDue = + late.roundedPrincipal + late.roundedInterest + late.fee; + XRPL_ASSERT_PARTS( + isRounded(asset, totalDue, loanScale), + "ripple::detail::handleLatePayment", + "total due is rounded"); + + if (amount < totalDue) + { + JLOG(j.warn()) << "Late loan payment amount is insufficient. Due: " + << totalDue << ", paid: " << amount; + return Unexpected(tecINSUFFICIENT_PAYMENT); + } + + return late; +} + +/* Handle possible full payments. + * + * If this function processed a full payment, the return value will be + * a PaymentComponentsPlus object. If the payment should not be considered as a + * full payment, the return will be an Unexpected(tesSUCCESS). Otherwise, it'll + * be an Unexpected with the error code the caller is expected to return. + */ +template +Expected +handleFullPayment( + A const& asset, + ApplyView& view, + NumberProxy& principalOutstandingProxy, + NumberProxy& referencePrincipalProxy, + Int32Proxy& paymentRemainingProxy, + Int32Proxy& prevPaymentDateProxy, + std::uint32_t const startDate, + std::uint32_t const paymentInterval, + TenthBips32 const closeInterestRate, + std::int32_t loanScale, + Number const& totalInterestOutstanding, + Number const& periodicRate, + Number const& closePaymentFee, + STAmount const& amount, + beast::Journal j) +{ + if (paymentRemainingProxy <= 1) + // If this is the last payment, it has to be a regular payment + return Unexpected(tesSUCCESS); + + // If there is more than one payment remaining, see if enough was + // paid for a full payment + auto const accruedInterest = roundToAsset( + asset, + detail::loanAccruedInterest( + principalOutstandingProxy, + periodicRate, + view.parentCloseTime(), + startDate, + prevPaymentDateProxy, + paymentInterval), + loanScale); + XRPL_ASSERT( + accruedInterest >= 0, + "ripple::detail::handleFullPayment : valid accrued interest"); + auto const prepaymentPenalty = roundToAsset( + asset, + tenthBipsOfValue(principalOutstandingProxy.value(), closeInterestRate), + loanScale); + XRPL_ASSERT( + prepaymentPenalty >= 0, + "ripple::detail::handleFullPayment : valid prepayment " + "interest"); + auto const totalInterest = accruedInterest + prepaymentPenalty; + auto const closeFullPayment = + principalOutstandingProxy + totalInterest + closePaymentFee; + + if (amount < closeFullPayment) + // If the payment is less than the full payment amount, it's not + // sufficient to be a full payment, but that's not an error. + return Unexpected(tesSUCCESS); + + // Make a full payment + + PaymentComponentsPlus const result{ + PaymentComponents{ + .rawInterest = principalOutstandingProxy + totalInterest - + referencePrincipalProxy, + .rawPrincipal = referencePrincipalProxy, + .roundedInterest = totalInterest, + .roundedPrincipal = principalOutstandingProxy, + .roundedPayment = closeFullPayment, + .final = true}, + // A full payment only pays the single close payment fee + closePaymentFee, + // A full payment decreases the value of the loan by the + // difference between the interest paid and the expected + // outstanding interest return + totalInterest - totalInterestOutstanding}; + + return result; +} + } // namespace detail template @@ -220,7 +464,7 @@ valueMinusFee( std::int32_t scale) { return roundToAsset( - asset, detail::minusManagementFee(value, managementFeeRate), scale); + asset, detail::minusFee(value, managementFeeRate), scale); } struct LoanProperties @@ -272,7 +516,7 @@ computeLoanProperties( auto const firstPaymentPrincipal = [&]() { // Compute the unrounded parts for the first payment. Ensure that the // principal payment will actually change the principal. - auto const paymentParts = detail::computePaymentParts( + auto const paymentComponents = detail::computePaymentComponents( asset, loanScale, totalValueOutstanding, @@ -284,12 +528,13 @@ computeLoanProperties( // We only care about the unrounded principal part. It needs to be large // enough that it will affect the reference principal. - auto const remaining = referencePrincipal - paymentParts.rawPrincipal; + auto const remaining = + referencePrincipal - paymentComponents.rawPrincipal; if (remaining == referencePrincipal) // No change, so the first payment effectively pays no principal. // Whether that's a problem is left to the caller. return Number{0}; - return paymentParts.rawPrincipal; + return paymentComponents.rawPrincipal; }(); auto const interestOwedToVault = valueMinusFee( @@ -498,207 +743,8 @@ template bool isRounded(A const& asset, Number const& value, std::int32_t scale) { - return roundToAsset(asset, value, scale, Number::downward) == value && - roundToAsset(asset, value, scale, Number::upward) == value; -} - -// This structure is explained in the XLS-66 spec, section 3.2.4.4 (Failure -// Conditions) -struct LoanPaymentParts -{ - /// principal_paid is the amount of principal that the payment covered. - Number principalPaid; - /// interest_paid is the amount of interest that the payment covered. - Number interestPaid; - /** - * value_change is the amount by which the total value of the Loan changed. - * If value_change < 0, Loan value decreased. - * If value_change > 0, Loan value increased. - * This is 0 for regular payments. - */ - Number valueChange; - /// fee_paid is the amount of fee that the payment covered. - Number feeToPay; -}; - -template -void -doPayment( - PaymentParts const& payment, - NumberProxy& totalValueOutstandingProxy, - NumberProxy& principalOutstandingProxy, - NumberProxy& referencePrincipalProxy, - Int32Proxy& paymentRemainingProxy, - Int32Proxy& prevPaymentDateProxy, - Int32Proxy& nextDueDateProxy, - std::uint32_t paymentInterval) -{ - paymentRemainingProxy -= 1; - // A single payment always pays the same amount of principal. Only the - // interest and fees are extra for a late payment - referencePrincipalProxy -= payment.rawPrincipal; - principalOutstandingProxy -= payment.roundedPrincipal; - totalValueOutstandingProxy -= - payment.roundedPrincipal + payment.roundedInterest; - - // Make sure this does an assignment - prevPaymentDateProxy = nextDueDateProxy; - nextDueDateProxy += paymentInterval; -} - -/* Handle possible late payments. - * - * If this function processed a late payment, the return value will be - * a LoanPaymentParts object. If the loan is not late, the return will be an - * Unexpected(tesSUCCESS). Otherwise, it'll be an Unexpected with the error code - * the caller is expected to return. - * - * - * This function is an implementation of the XLS-66 spec, based on - * * section 3.2.4.3 (Transaction Pseudo-code), specifically the bit - * labeled "the payment is late" - * * section 3.2.4.1.2 (Late Payment) - */ -template -Expected, TER> -handleLatePayment( - A const& asset, - ApplyView& view, - NumberProxy& principalOutstandingProxy, - Int32Proxy& nextDueDateProxy, - PaymentParts const& periodic, - TenthBips32 const lateInterestRate, - std::int32_t loanScale, - Number const& paymentFee, - Number const& latePaymentFee, - STAmount const& amount, - beast::Journal j) -{ - if (!hasExpired(view, nextDueDateProxy)) - return Unexpected(tesSUCCESS); - - // the payment is late - // Late payment interest is only the part of the interest that comes from - // being late, as computed by 3.2.4.1.2. - auto const latePaymentInterest = loanLatePaymentInterest( - asset, - principalOutstandingProxy, - lateInterestRate, - view.parentCloseTime(), - nextDueDateProxy, - loanScale); - XRPL_ASSERT( - latePaymentInterest >= 0, - "ripple::handleLatePayment : valid late interest"); - PaymentParts const late{ - .rawInterest = periodic.rawInterest + latePaymentInterest, - .rawPrincipal = periodic.rawPrincipal, - .roundedInterest = periodic.roundedInterest + latePaymentInterest, - .roundedPrincipal = periodic.roundedPrincipal, - .roundedPayment = periodic.roundedPayment}; - auto const fee = paymentFee + latePaymentFee; - auto const totalDue = late.roundedPrincipal + late.roundedInterest + fee; - XRPL_ASSERT_PARTS( - isRounded(asset, totalDue, loanScale), - "ripple::handleLatePayment", - "total due is rounded"); - - if (amount < totalDue) - { - JLOG(j.warn()) << "Late loan payment amount is insufficient. Due: " - << totalDue << ", paid: " << amount; - return Unexpected(tecINSUFFICIENT_PAYMENT); - } - - // A late payment increases the value of the loan by the difference - // between periodic and late payment interest - return std::make_pair( - late, - LoanPaymentParts{ - .principalPaid = late.roundedPrincipal, - .interestPaid = late.roundedInterest, - .valueChange = latePaymentInterest, - .feeToPay = fee}); -} - -/* Handle possible full payments. - * - * If this function processed a full payment, the return value will be - * a LoanPaymentParts object. If the payment should not be considered as a full - * payment, the return will be an Unexpected(tesSUCCESS). Otherwise, it'll be an - * Unexpected with the error code the caller is expected to return. - */ -template -Expected -handleFullPayment( - A const& asset, - ApplyView& view, - NumberProxy& principalOutstandingProxy, - Int32Proxy& paymentRemainingProxy, - Int32Proxy& prevPaymentDateProxy, - std::uint32_t const startDate, - std::uint32_t const paymentInterval, - TenthBips32 const closeInterestRate, - std::int32_t loanScale, - Number const& totalInterestOutstanding, - Number const& periodicRate, - Number const& closePaymentFee, - STAmount const& amount, - beast::Journal j) -{ - if (paymentRemainingProxy <= 1) - // If this is the last payment, it has to be a regular payment - return Unexpected(tesSUCCESS); - - // If there is more than one payment remaining, see if enough was - // paid for a full payment - auto const accruedInterest = roundToAsset( - asset, - detail::loanAccruedInterest( - principalOutstandingProxy, - periodicRate, - view.parentCloseTime(), - startDate, - prevPaymentDateProxy, - paymentInterval), - loanScale); - XRPL_ASSERT( - accruedInterest >= 0, - "ripple::handleFullPayment : valid accrued interest"); - auto const prepaymentPenalty = roundToAsset( - asset, - tenthBipsOfValue(principalOutstandingProxy.value(), closeInterestRate), - loanScale); - XRPL_ASSERT( - prepaymentPenalty >= 0, - "ripple::handleFullPayment : valid prepayment " - "interest"); - auto const totalInterest = accruedInterest + prepaymentPenalty; - auto const closeFullPayment = - principalOutstandingProxy + totalInterest + closePaymentFee; - - if (amount < closeFullPayment) - // If the payment is less than the full payment amount, it's not - // sufficient to be a full payment, but that's not an error. - return Unexpected(tesSUCCESS); - - // Make a full payment - - // A full payment decreases the value of the loan by the - // difference between the interest paid and the expected - // outstanding interest return - auto const valueChange = totalInterest - totalInterestOutstanding; - - LoanPaymentParts const result{ - .principalPaid = principalOutstandingProxy, - .interestPaid = totalInterest, - .valueChange = valueChange, - .feeToPay = closePaymentFee}; - - paymentRemainingProxy = 0; - principalOutstandingProxy = 0; - - return result; + return roundToAsset(asset, value, scale, Number::downward) == + roundToAsset(asset, value, scale, Number::upward); } template @@ -766,19 +812,21 @@ loanMakePayment( view.update(loan); - auto const periodic = detail::computePaymentParts( - asset, - loanScale, - totalValueOutstandingProxy, - principalOutstandingProxy, - referencePrincipalProxy, - periodicPayment, - periodicRate, - paymentRemainingProxy); + detail::PaymentComponentsPlus const periodic{ + detail::computePaymentComponents( + asset, + loanScale, + totalValueOutstandingProxy, + principalOutstandingProxy, + referencePrincipalProxy, + periodicPayment, + periodicRate, + paymentRemainingProxy), + serviceFee}; // ------------------------------------------------------------- // late payment handling - if (auto const latePaymentParts = handleLatePayment( + if (auto const latePaymentComponents = detail::handleLatePayment( asset, view, principalOutstandingProxy, @@ -786,13 +834,12 @@ loanMakePayment( periodic, lateInterestRate, loanScale, - serviceFee, latePaymentFee, amount, j)) { - doPayment( - latePaymentParts->first, + return doPayment( + *latePaymentComponents, totalValueOutstandingProxy, principalOutstandingProxy, referencePrincipalProxy, @@ -800,21 +847,23 @@ loanMakePayment( prevPaymentDateProxy, nextDueDateProxy, paymentInterval); - - return latePaymentParts->second; } - else if (latePaymentParts.error()) - return Unexpected(latePaymentParts.error()); + else if (latePaymentComponents.error()) + // error() will be the TER returned if a payment is not made. It will + // only evaluate to true if it's an error. Otherwise, tesSUCCESS means + // nothing was done, so continue. + return Unexpected(latePaymentComponents.error()); // ------------------------------------------------------------- // full payment handling auto const totalInterestOutstanding = totalValueOutstandingProxy - principalOutstandingProxy; - if (auto const fullPaymentParts = handleFullPayment( + if (auto const fullPaymentComponents = detail::handleFullPayment( asset, view, principalOutstandingProxy, + referencePrincipalProxy, paymentRemainingProxy, prevPaymentDateProxy, startDate, @@ -826,91 +875,92 @@ loanMakePayment( closePaymentFee, amount, j)) - return *fullPaymentParts; - else if (fullPaymentParts.error()) - return fullPaymentParts; + return doPayment( + *fullPaymentComponents, + totalValueOutstandingProxy, + principalOutstandingProxy, + referencePrincipalProxy, + paymentRemainingProxy, + prevPaymentDateProxy, + nextDueDateProxy, + paymentInterval); + else if (fullPaymentComponents.error()) + // error() will be the TER returned if a payment is not made. It will + // only evaluate to true if it's an error. Otherwise, tesSUCCESS means + // nothing was done, so continue. + return Unexpected(fullPaymentComponents.error()); // ------------------------------------------------------------- // regular periodic payment handling - return Unexpected(temDISABLED); -#if LOANCOMPLETE // if the payment is not late nor if it's a full payment, then it must // be a periodic one, with possible overpayments - auto const totalDue = periodic.interest + periodic.principal + periodic.fee; + // This will keep a running total of what is actually paid, if the payment + // is sufficient for a single payment + Number totalPaid = + periodic.roundedInterest + periodic.roundedPrincipal + periodic.fee; - // TODO: Don't attempt to figure out the number of payments beforehand. Just - // loop over making payments until the `amount` is used up or the loan is - // paid off. - std::optional mg(Number::downward); - std::int64_t const fullPeriodicPayments = [&]() { - std::int64_t const full{amount / totalDue}; - return full < paymentRemainingProxy ? full : paymentRemainingProxy; - }(); - mg.reset(); - // Temporary asserts - XRPL_ASSERT( - amount >= totalDue || fullPeriodicPayments == 0, - "temp full periodic rounding"); - XRPL_ASSERT( - amount < totalDue || fullPeriodicPayments >= 1, - "temp full periodic rounding"); - - if (fullPeriodicPayments < 1) + if (amount < totalPaid) { JLOG(j.warn()) << "Periodic loan payment amount is insufficient. Due: " - << totalDue << ", paid: " << amount; + << totalPaid << ", paid: " << amount; return Unexpected(tecINSUFFICIENT_PAYMENT); } - nextDueDateProxy += paymentInterval * fullPeriodicPayments; - prevPaymentDateProxy = nextDueDateProxy - paymentInterval; + LoanPaymentParts totalParts = detail::doPayment( + periodic, + totalValueOutstandingProxy, + principalOutstandingProxy, + referencePrincipalProxy, + paymentRemainingProxy, + prevPaymentDateProxy, + nextDueDateProxy, + paymentInterval); - Number totalPrincipalPaid = 0; - Number totalInterestPaid = 0; - Number loanValueChange = 0; - - std::optional future = periodic; - for (int i = 0; i < fullPeriodicPayments; ++i) + while (totalPaid < amount && paymentRemainingProxy > 0) { - // Only do the work if we need to - if (!future) - future = computePaymentParts( + // Try to make more payments + detail::PaymentComponentsPlus const nextPayment{ + detail::computePaymentComponents( asset, loanScale, totalValueOutstandingProxy, principalOutstandingProxy, + referencePrincipalProxy, periodicPayment, - serviceFee, periodicRate, - paymentRemainingProxy); + paymentRemainingProxy), + periodic.fee}; XRPL_ASSERT( - future->interest <= periodic.interest, + nextPayment.rawInterest <= periodic.rawInterest, "ripple::loanMakePayment : decreasing interest"); XRPL_ASSERT( - future->principal >= periodic.principal, + nextPayment.rawPrincipal >= periodic.rawPrincipal, "ripple::loanMakePayment : increasing principal"); - totalPrincipalPaid += future->principal; - totalInterestPaid += future->interest; - paymentRemainingProxy -= 1; - principalOutstandingProxy -= future->principal; + // the fee part doesn't change + auto const due = nextPayment.roundedInterest + + nextPayment.roundedPrincipal + periodic.fee; - future.reset(); + if (amount < totalPaid + due) + // We're done making payments. + break; + + totalPaid += due; + totalParts += detail::doPayment( + nextPayment, + totalValueOutstandingProxy, + principalOutstandingProxy, + referencePrincipalProxy, + paymentRemainingProxy, + prevPaymentDateProxy, + nextDueDateProxy, + paymentInterval); } - Number totalfeeToPay = serviceFee * fullPeriodicPayments; - - Number const newInterest = loanTotalInterestOutstanding( - asset, - loanScale, - principalOutstandingProxy, - interestRate, - paymentInterval, - paymentRemainingProxy) + - totalInterestPaid; - + return Unexpected(temDISABLED); +#if LOANCOMPLETE // ------------------------------------------------------------- // overpayment handling Number overpaymentInterestPortion = 0; From a8de91c57db926c3a8971df16068f89201e23685 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sun, 5 Oct 2025 15:34:15 -0500 Subject: [PATCH 120/291] Add LoanPaymentParts::operator+= - Builds, but tests are not expected to pass --- src/xrpld/app/misc/LendingHelpers.h | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index e4ab1c05df..db4a88b404 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -61,6 +61,16 @@ struct LoanPaymentParts Number valueChange; /// fee_paid is the amount of fee that the payment covered. Number feeToPay; + + LoanPaymentParts& + operator+=(LoanPaymentParts const& other) + { + principalPaid += other.principalPaid; + interestPaid += other.interestPaid; + valueChange += other.valueChange; + feeToPay += other.feeToPay; + return *this; + } }; namespace detail { @@ -267,7 +277,7 @@ doPayment( principalOutstandingProxy == payment.roundedPrincipal, "ripple::detail::doPayment", "Full principal payment"); - XRPL_ASSSERT_PARTS( + XRPL_ASSERT_PARTS( totalValueOutstandingProxy == payment.roundedPrincipal + payment.roundedInterest, "ripple::detail::doPayment", @@ -918,6 +928,8 @@ loanMakePayment( nextDueDateProxy, paymentInterval); + std::size_t numPayments = 1; + while (totalPaid < amount && paymentRemainingProxy > 0) { // Try to make more payments @@ -957,8 +969,22 @@ loanMakePayment( prevPaymentDateProxy, nextDueDateProxy, paymentInterval); + ++numPayments; } + XRPL_ASSERT_PARTS( + totalParts.principalPaid + totalParts.interestPaid == totalPaid, + "ripple::loanMakePayment", + "payment parts add up"); + XRPL_ASSERT_PARTS( + totalParts.valueChange == 0, + "ripple::loanMakePayment", + "no value change"); + XRPL_ASSERT_PARTS( + totalParts.feeToPay == periodic.fee * numPayments, + "ripple::loanMakePayment", + "fee parts add up"); + return Unexpected(temDISABLED); #if LOANCOMPLETE // ------------------------------------------------------------- From 0b8cd2d7cafaa3953bcdd08b67b33ab613e50d00 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sun, 5 Oct 2025 16:03:38 -0500 Subject: [PATCH 121/291] Cool idea: Make Loan.NextDueDate optional; clear it when complete - Check "simple" loanMakePayment failure conditions early --- .../xrpl/protocol/detail/ledger_entries.macro | 2 +- src/xrpld/app/misc/LendingHelpers.h | 67 ++++++++++++------- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 56497aa858..3c5fd13b69 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -562,7 +562,7 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({ {sfGracePeriod, soeREQUIRED}, {sfPeriodicPayment, soeREQUIRED}, {sfPreviousPaymentDate, soeDEFAULT}, - {sfNextPaymentDueDate, soeREQUIRED}, + {sfNextPaymentDueDate, soeOPTIONAL}, {sfPaymentRemaining, soeDEFAULT}, {sfPrincipalOutstanding, soeDEFAULT}, {sfTotalValueOutstanding, soeDEFAULT}, diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index db4a88b404..105feca443 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -254,7 +254,7 @@ struct PaymentComponentsPlus : public PaymentComponents } }; -template +template LoanPaymentParts doPayment( PaymentComponentsPlus const& payment, @@ -263,9 +263,13 @@ doPayment( NumberProxy& referencePrincipalProxy, Int32Proxy& paymentRemainingProxy, Int32Proxy& prevPaymentDateProxy, - Int32Proxy& nextDueDateProxy, + Int32OptionalProxy& nextDueDateProxy, std::uint32_t paymentInterval) { + XRPL_ASSERT_PARTS( + nextDueDateProxy, + "ripple::detail::doPayment", + "Next due date proxy set"); if (payment.final) { paymentRemainingProxy = 0; @@ -283,16 +287,19 @@ doPayment( "ripple::detail::doPayment", "Full value payment"); - prevPaymentDateProxy = nextDueDateProxy; - // May as well... - nextDueDateProxy = 0; + prevPaymentDateProxy = *nextDueDateProxy; + // Remove the field. This is the only condition where nextDueDate is + // allowed to be removed. + nextDueDateProxy = std::nullopt; } else { paymentRemainingProxy -= 1; - prevPaymentDateProxy = nextDueDateProxy; - nextDueDateProxy += paymentInterval; + prevPaymentDateProxy = *nextDueDateProxy; + // STObject::OptionalField does not define operator+=, and I don't want + // to add one right now. + nextDueDateProxy = *nextDueDateProxy + paymentInterval; } // A single payment always pays the same amount of principal. Only the // interest and fees are extra for a late payment @@ -321,21 +328,21 @@ doPayment( * labeled "the payment is late" * * section 3.2.4.1.2 (Late Payment) */ -template +template Expected handleLatePayment( A const& asset, - ApplyView& view, - NumberProxy& principalOutstandingProxy, - Int32Proxy& nextDueDateProxy, + ApplyView const& view, + Number const& principalOutstanding, + std::int32_t nextDueDate, PaymentComponentsPlus const& periodic, - TenthBips32 const lateInterestRate, + TenthBips32 lateInterestRate, std::int32_t loanScale, Number const& latePaymentFee, STAmount const& amount, beast::Journal j) { - if (!hasExpired(view, nextDueDateProxy)) + if (!hasExpired(view, nextDueDate)) return Unexpected(tesSUCCESS); // the payment is late @@ -343,10 +350,10 @@ handleLatePayment( // being late, as computed by 3.2.4.1.2. auto const latePaymentInterest = loanLatePaymentInterest( asset, - principalOutstandingProxy, + principalOutstanding, lateInterestRate, view.parentCloseTime(), - nextDueDateProxy, + nextDueDate, loanScale); XRPL_ASSERT( latePaymentInterest >= 0, @@ -770,10 +777,27 @@ loanMakePayment( * This function is an implementation of the XLS-66 spec, * section 3.2.4.3 (Transaction Pseudo-code) */ + auto principalOutstandingProxy = loan->at(sfPrincipalOutstanding); + auto paymentRemainingProxy = loan->at(sfPaymentRemaining); + + if (paymentRemainingProxy == 0 || principalOutstandingProxy == 0) + { + // Loan complete + JLOG(j.warn()) << "Loan is already paid off."; + return Unexpected(tecKILLED); + } + + // Next payment due date must be set unless the loan is complete + auto nextDueDateProxy = loan->at(~sfNextPaymentDueDate); + if (!nextDueDateProxy) + { + JLOG(j.warn()) << "Loan next payment due date is not set."; + return Unexpected(tecINTERNAL); + } + std::int32_t const loanScale = loan->at(sfLoanScale); auto totalValueOutstandingProxy = loan->at(sfTotalValueOutstanding); auto interestOwedProxy = loan->at(sfInterestOwed); - auto principalOutstandingProxy = loan->at(sfPrincipalOutstanding); auto referencePrincipalProxy = loan->at(sfReferencePrincipal); bool const allowOverpayment = loan->isFlag(lsfLoanOverpayment); @@ -790,18 +814,9 @@ loanMakePayment( TenthBips32 const overpaymentFee{loan->at(sfOverpaymentFee)}; auto const periodicPayment = loan->at(sfPeriodicPayment); - auto paymentRemainingProxy = loan->at(sfPaymentRemaining); auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDate); std::uint32_t const startDate = loan->at(sfStartDate); - auto nextDueDateProxy = loan->at(sfNextPaymentDueDate); - - if (paymentRemainingProxy == 0 || principalOutstandingProxy == 0) - { - // Loan complete - JLOG(j.warn()) << "Loan is already paid off."; - return Unexpected(tecKILLED); - } std::uint32_t const paymentInterval = loan->at(sfPaymentInterval); // Compute the normal periodic rate, payment, etc. @@ -840,7 +855,7 @@ loanMakePayment( asset, view, principalOutstandingProxy, - nextDueDateProxy, + *nextDueDateProxy, periodic, lateInterestRate, loanScale, From f8b63228b61e252f3bff5e719dc7618b23cece9d Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sun, 5 Oct 2025 21:31:24 -0500 Subject: [PATCH 122/291] Cool idea: Charge one extra base fee per 25 payments --- src/xrpld/app/misc/LendingHelpers.h | 128 ++++++++++++------- src/xrpld/app/misc/detail/LendingHelpers.cpp | 4 +- src/xrpld/app/tx/detail/LoanPay.cpp | 78 +++++++++++ src/xrpld/app/tx/detail/LoanPay.h | 3 + 4 files changed, 162 insertions(+), 51 deletions(-) diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 105feca443..9fef0e82d8 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -31,6 +31,9 @@ struct PreflightContext; bool checkLendingProtocolDependencies(PreflightContext const& ctx); +Number +loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval); + // This structure is used internally to compute the breakdown of a // single loan payment struct PaymentComponents @@ -73,13 +76,21 @@ struct LoanPaymentParts } }; +/// Ensure the periodic payment is always rounded consistently +template +Number +roundPeriodicPayment( + A const& asset, + Number const& periodicPayment, + std::int32_t scale) +{ + return roundToAsset(asset, periodicPayment, scale, Number::upward); +} + namespace detail { // These functions should rarely be used directly. More often, the ultimate // result needs to be roundToAsset'd. -Number -loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval); - Number loanPeriodicPayment( Number const& principalOutstanding, @@ -137,7 +148,7 @@ computePaymentComponents( "ripple::detail::computePaymentComponents", "Outstanding values are rounded"); auto const roundedPeriodicPayment = - roundToAsset(asset, periodicPayment, scale, Number::upward); + roundPeriodicPayment(asset, periodicPayment, scale); if (paymentRemaining == 1 || totalValueOutstanding <= periodicPayment) { // If there's only one payment left, we need to pay off the principal. @@ -394,15 +405,15 @@ handleLatePayment( * full payment, the return will be an Unexpected(tesSUCCESS). Otherwise, it'll * be an Unexpected with the error code the caller is expected to return. */ -template +template Expected handleFullPayment( A const& asset, ApplyView& view, - NumberProxy& principalOutstandingProxy, - NumberProxy& referencePrincipalProxy, - Int32Proxy& paymentRemainingProxy, - Int32Proxy& prevPaymentDateProxy, + Number const& principalOutstanding, + Number const& referencePrincipal, + std::uint32_t paymentRemaining, + std::uint32_t prevPaymentDate, std::uint32_t const startDate, std::uint32_t const paymentInterval, TenthBips32 const closeInterestRate, @@ -413,36 +424,24 @@ handleFullPayment( STAmount const& amount, beast::Journal j) { - if (paymentRemainingProxy <= 1) + if (paymentRemaining <= 1) // If this is the last payment, it has to be a regular payment return Unexpected(tesSUCCESS); - // If there is more than one payment remaining, see if enough was - // paid for a full payment - auto const accruedInterest = roundToAsset( + auto const totalInterest = calculateFullPaymentInterest( asset, - detail::loanAccruedInterest( - principalOutstandingProxy, - periodicRate, - view.parentCloseTime(), - startDate, - prevPaymentDateProxy, - paymentInterval), - loanScale); - XRPL_ASSERT( - accruedInterest >= 0, - "ripple::detail::handleFullPayment : valid accrued interest"); - auto const prepaymentPenalty = roundToAsset( - asset, - tenthBipsOfValue(principalOutstandingProxy.value(), closeInterestRate), - loanScale); - XRPL_ASSERT( - prepaymentPenalty >= 0, - "ripple::detail::handleFullPayment : valid prepayment " - "interest"); - auto const totalInterest = accruedInterest + prepaymentPenalty; + referencePrincipal, + periodicRate, + view.parentCloseTime(), + paymentInterval, + prevPaymentDate, + startDate, + closeInterestRate, + loanScale, + closePaymentFee); + auto const closeFullPayment = - principalOutstandingProxy + totalInterest + closePaymentFee; + principalOutstanding + totalInterest + closePaymentFee; if (amount < closeFullPayment) // If the payment is less than the full payment amount, it's not @@ -453,11 +452,11 @@ handleFullPayment( PaymentComponentsPlus const result{ PaymentComponents{ - .rawInterest = principalOutstandingProxy + totalInterest - - referencePrincipalProxy, - .rawPrincipal = referencePrincipalProxy, + .rawInterest = + principalOutstanding + totalInterest - referencePrincipal, + .rawPrincipal = referencePrincipal, .roundedInterest = totalInterest, - .roundedPrincipal = principalOutstandingProxy, + .roundedPrincipal = principalOutstanding, .roundedPayment = closeFullPayment, .final = true}, // A full payment only pays the single close payment fee @@ -504,8 +503,7 @@ computeLoanProperties( std::uint32_t paymentsRemaining, TenthBips32 managementFeeRate) { - auto const periodicRate = - detail::loanPeriodicRate(interestRate, paymentInterval); + auto const periodicRate = loanPeriodicRate(interestRate, paymentInterval); XRPL_ASSERT( interestRate == 0 || periodicRate > 0, "ripple::loanMakePayment : valid rate"); @@ -572,13 +570,47 @@ computeLoanProperties( .firstPaymentPrincipal = firstPaymentPrincipal}; } -#if LOANCOMPLETE -inline Number -loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval) +template +Number +calculateFullPaymentInterest( + A const& asset, + Number const& referencePrincipal, + Number const& periodicRate, + NetClock::time_point parentCloseTime, + std::uint32_t paymentInterval, + std::uint32_t prevPaymentDate, + std::uint32_t startDate, + TenthBips32 closeInterestRate, + std::int32_t loanScale, + Number const& closePaymentFee) { - return detail::loanPeriodicRate(interestRate, paymentInterval); + // If there is more than one payment remaining, see if enough was + // paid for a full payment + auto const accruedInterest = roundToAsset( + asset, + detail::loanAccruedInterest( + referencePrincipal, + periodicRate, + parentCloseTime, + startDate, + prevPaymentDate, + paymentInterval), + loanScale); + XRPL_ASSERT( + accruedInterest >= 0, + "ripple::detail::handleFullPayment : valid accrued interest"); + auto const prepaymentPenalty = roundToAsset( + asset, + tenthBipsOfValue(referencePrincipal, closeInterestRate), + loanScale); + XRPL_ASSERT( + prepaymentPenalty >= 0, + "ripple::detail::handleFullPayment : valid prepayment " + "interest"); + return accruedInterest + prepaymentPenalty; } +#if LOANCOMPLETE template Number loanPeriodicPayment( @@ -588,12 +620,11 @@ loanPeriodicPayment( std::uint32_t paymentsRemaining, std::int32_t scale) { - return roundToAsset( + return roundPeriodicPayment( asset, detail::loanPeriodicPayment( principalOutstanding, periodicRate, paymentsRemaining), - scale, - Number::upward); + scale); } template @@ -821,8 +852,7 @@ loanMakePayment( std::uint32_t const paymentInterval = loan->at(sfPaymentInterval); // Compute the normal periodic rate, payment, etc. // We'll need it in the remaining calculations - Number const periodicRate = - detail::loanPeriodicRate(interestRate, paymentInterval); + Number const periodicRate = loanPeriodicRate(interestRate, paymentInterval); XRPL_ASSERT( interestRate == 0 || periodicRate > 0, "ripple::loanMakePayment : valid rate"); diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index a64ffb92b4..eafb28cd34 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -30,8 +30,6 @@ checkLendingProtocolDependencies(PreflightContext const& ctx) VaultCreate::checkExtraFeatures(ctx); } -namespace detail { - Number loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval) { @@ -46,6 +44,8 @@ loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval) (365 * 24 * 60 * 60); } +namespace detail { + Number loanPeriodicPayment( Number const& principalOutstanding, diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 48eaab83a3..cccc09a03f 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -42,6 +42,84 @@ LoanPay::preflight(PreflightContext const& ctx) return tesSUCCESS; } +XRPAmount +LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx) +{ + auto const normalCost = Transactor::calculateBaseFee(view, tx); + auto const paymentsPerFeeIncrement = 20; + + // The fee is based on the number of potential payments, unless the loan is + // being fully paid off. + auto const amount = tx[sfAmount]; + auto const loanID = tx[sfLoanID]; + + auto const loanSle = view.read(keylet::loan(loanID)); + if (!loanSle) + // Let preclaim worry about the error for this + return normalCost; + + if (loanSle->at(sfPaymentRemaining) <= paymentsPerFeeIncrement) + { + // If there are fewer than paymentsPerFeeIncrement payments left, we can + // skip the computations. + return normalCost; + } + + if (hasExpired(view, loanSle->at(sfNextPaymentDueDate))) + // If the payment is late, it'll only make one payment + return normalCost; + + auto const brokerSle = + view.read(keylet::loanbroker(loanSle->at(sfLoanBrokerID))); + if (!brokerSle) + // Let preclaim worry about the error for this + return normalCost; + auto const vaultSle = view.read(keylet::vault(loanSle->at(sfVaultID))); + if (vaultSle) + // Let preclaim worry about the error for this + return normalCost; + + auto const asset = vaultSle->at(sfAsset); + auto const scale = loanSle->at(sfLoanScale); + + auto const regularPayment = + roundPeriodicPayment(asset, loanSle->at(sfPeriodicPayment), scale) + + loanSle->at(sfLoanServiceFee); + + if (amount < regularPayment * paymentsPerFeeIncrement) + // This is definitely paying fewer than paymentsPerFeeIncrement payments + return normalCost; + + // This computation isn't free, but it's relatively straightforward + if (auto const fullInterest = calculateFullPaymentInterest( + asset, + loanSle->at(sfReferencePrincipal), + loanPeriodicRate( + TenthBips32(loanSle->at(sfInterestRate)), + loanSle->at(sfPaymentInterval)), + view.parentCloseTime(), + loanSle->at(sfPaymentInterval), + loanSle->at(sfPreviousPaymentDate), + loanSle->at(sfStartDate), + TenthBips32(loanSle->at(sfCloseInterestRate)), + scale, + loanSle->at(sfClosePaymentFee)); + amount > loanSle->at(sfPrincipalOutstanding) + fullInterest + + loanSle->at(sfClosePaymentFee)) + return normalCost; + + NumberRoundModeGuard mg(Number::downward); + // Figure out how many payments will be made + auto const numPaymentEstimate = + static_cast(amount / regularPayment); + // Charge one base fee per paymentsPerFeeIncrement payments - use integer + // math (round down), then add one to ensure all this extra math is worth + // it. + auto const feeIncrements = numPaymentEstimate / paymentsPerFeeIncrement + 1; + + return feeIncrements * normalCost; +} + TER LoanPay::preclaim(PreclaimContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/LoanPay.h b/src/xrpld/app/tx/detail/LoanPay.h index 555990dbd0..11045f90da 100644 --- a/src/xrpld/app/tx/detail/LoanPay.h +++ b/src/xrpld/app/tx/detail/LoanPay.h @@ -39,6 +39,9 @@ public: static NotTEC preflight(PreflightContext const& ctx); + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + static TER preclaim(PreclaimContext const& ctx); From 1b31dbc4c9bc608875eafa786c538d0e14ca1bcc Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 6 Oct 2025 16:33:05 -0400 Subject: [PATCH 123/291] Fix build errors --- src/xrpld/app/misc/LendingHelpers.h | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 9fef0e82d8..24d87e9b17 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -166,7 +166,6 @@ computePaymentComponents( "ripple::detail::computePaymentComponents", "last payment is complete"); - Number const interest = totalValueOutstanding - principalOutstanding; return { .rawInterest = rawInterest, .rawPrincipal = referencePrincipal, @@ -830,7 +829,6 @@ loanMakePayment( auto totalValueOutstandingProxy = loan->at(sfTotalValueOutstanding); auto interestOwedProxy = loan->at(sfInterestOwed); auto referencePrincipalProxy = loan->at(sfReferencePrincipal); - bool const allowOverpayment = loan->isFlag(lsfLoanOverpayment); TenthBips32 const interestRate{loan->at(sfInterestRate)}; TenthBips32 const lateInterestRate{loan->at(sfLateInterestRate)}; @@ -1035,7 +1033,7 @@ loanMakePayment( // ------------------------------------------------------------- // overpayment handling Number overpaymentInterestPortion = 0; - if (allowOverpayment) + if (loan->isFlag(lsfLoanOverpayment)) { Number const overpayment = std::min( principalOutstandingProxy.value(), @@ -1068,8 +1066,8 @@ loanMakePayment( } } - loanValueChange = - (newInterest - totalInterestOutstanding) + overpaymentInterestPortion; + totalParts.valueChange += (newTotalInterest - totalInterestOutstanding) + + overpaymentInterestPortion; // Check the final results are rounded, to double-check that the // intermediate steps were rounded. From 8d982758cbe65a912d9e3cad2fcb11fcbacec0e8 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 6 Oct 2025 16:48:43 -0400 Subject: [PATCH 124/291] Ensure interest portion doesn't go negative - Ensure principal part is not bigger than whole payment. - Add some documentation. --- include/xrpl/protocol/detail/ledger_entries.macro | 14 ++++++++++++++ src/xrpld/app/misc/LendingHelpers.h | 15 +++++++-------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 3c5fd13b69..b48151f5cc 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -564,6 +564,20 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({ {sfPreviousPaymentDate, soeDEFAULT}, {sfNextPaymentDueDate, soeOPTIONAL}, {sfPaymentRemaining, soeDEFAULT}, + // The loan object tracks three values: + // - TotalValueOutstanding: The total amount owed by the borrower to + // the lender / vault. + // - PrincipalOutstanding: The portion of the TotalValueOutstanding + // that is from the prinicpal borrowed. + // - InterestOwed: The portion of the TotalValueOutstanding that + // represents interest owed to the vault. + // There are two additional values that can be computed from these: + // - InterestOutstanding: TotalValueOutstanding - PrincipalOutstanding + // This is the total amount of interest still pending on the loan, + // independent of management fees. + // - ManagementFeeOwed: InterestOutstanding - InterestOwed + // This is the amount of the total interest that will be sent to the + // broker as management fees. {sfPrincipalOutstanding, soeDEFAULT}, {sfTotalValueOutstanding, soeDEFAULT}, {sfInterestOwed, soeDEFAULT}, diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 24d87e9b17..d13bb78a19 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -151,7 +151,8 @@ computePaymentComponents( roundPeriodicPayment(asset, periodicPayment, scale); if (paymentRemaining == 1 || totalValueOutstanding <= periodicPayment) { - // If there's only one payment left, we need to pay off the principal. + // If there's only one payment left, we need to pay off each of the loan + // parts. // // The totalValueOutstanding should never be less than the // periodicPayment until the last scheduled payment, but if it ever is, @@ -217,8 +218,9 @@ computePaymentComponents( totalValueOutstanding - roundedPeriodicPayment) return roundedPeriodicPayment; // Use the amount that will get principal outstanding as close to - // reference principal as possible. - return p; + // reference principal as possible, but don't pay more than the rounded + // periodic payment, or we'll end up with negative interest. + return std::min(p, roundedPeriodicPayment); }(); // if(count($A20), if(AB19 < $B$5, AB19 - Z19, CEILING($B$10-W20, 1)), "") @@ -234,13 +236,10 @@ computePaymentComponents( "ripple::detail::computePaymentComponents", "valid rounded interest"); XRPL_ASSERT_PARTS( - roundedPrincipal >= 0 && roundedPrincipal <= principalOutstanding, + roundedPrincipal >= 0 && roundedPrincipal <= principalOutstanding && + isRounded(asset, roundedPrincipal, scale), "ripple::detail::computePaymentComponents", "valid rounded principal"); - XRPL_ASSERT_PARTS( - isRounded(asset, roundedPrincipal, scale), - "ripple::detail::computePaymentComponents", - "principal is rounded"); return { .rawInterest = rawInterest, From eeec90ee742d5c971c7aed436144c98a81f0d855 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 6 Oct 2025 18:31:44 -0400 Subject: [PATCH 125/291] Handle overpayment calculations --- src/xrpld/app/misc/LendingHelpers.h | 228 ++++++++++++++++++---------- src/xrpld/app/tx/detail/LoanPay.cpp | 10 +- 2 files changed, 153 insertions(+), 85 deletions(-) diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index d13bb78a19..e1ddf7e1b2 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -45,6 +45,7 @@ struct PaymentComponents // We may not need roundedPayment Number roundedPayment; bool final = false; + bool extra = false; }; // This structure is explained in the XLS-66 spec, section 3.2.4.4 (Failure @@ -279,39 +280,41 @@ doPayment( nextDueDateProxy, "ripple::detail::doPayment", "Next due date proxy set"); - if (payment.final) + if (!payment.extra) { - paymentRemainingProxy = 0; - XRPL_ASSERT_PARTS( - referencePrincipalProxy == payment.rawPrincipal, - "ripple::detail::doPayment", - "Full reference principal payment"); - XRPL_ASSERT_PARTS( - principalOutstandingProxy == payment.roundedPrincipal, - "ripple::detail::doPayment", - "Full principal payment"); - XRPL_ASSERT_PARTS( - totalValueOutstandingProxy == - payment.roundedPrincipal + payment.roundedInterest, - "ripple::detail::doPayment", - "Full value payment"); + if (payment.final) + { + paymentRemainingProxy = 0; + XRPL_ASSERT_PARTS( + referencePrincipalProxy == payment.rawPrincipal, + "ripple::detail::doPayment", + "Full reference principal payment"); + XRPL_ASSERT_PARTS( + principalOutstandingProxy == payment.roundedPrincipal, + "ripple::detail::doPayment", + "Full principal payment"); + XRPL_ASSERT_PARTS( + totalValueOutstandingProxy == + payment.roundedPrincipal + payment.roundedInterest, + "ripple::detail::doPayment", + "Full value payment"); - prevPaymentDateProxy = *nextDueDateProxy; - // Remove the field. This is the only condition where nextDueDate is - // allowed to be removed. - nextDueDateProxy = std::nullopt; - } - else - { - paymentRemainingProxy -= 1; + prevPaymentDateProxy = *nextDueDateProxy; + // Remove the field. This is the only condition where nextDueDate is + // allowed to be removed. + nextDueDateProxy = std::nullopt; + } + else + { + paymentRemainingProxy -= 1; - prevPaymentDateProxy = *nextDueDateProxy; - // STObject::OptionalField does not define operator+=, and I don't want - // to add one right now. - nextDueDateProxy = *nextDueDateProxy + paymentInterval; + prevPaymentDateProxy = *nextDueDateProxy; + // STObject::OptionalField does not define operator+=, and I don't + // want to add one right now. + nextDueDateProxy = *nextDueDateProxy + paymentInterval; + } } - // A single payment always pays the same amount of principal. Only the - // interest and fees are extra for a late payment + referencePrincipalProxy -= payment.rawPrincipal; principalOutstandingProxy -= payment.roundedPrincipal; totalValueOutstandingProxy -= @@ -800,6 +803,7 @@ loanMakePayment( ApplyView& view, SLE::ref loan, STAmount const& amount, + TenthBips16 managementFeeRate, beast::Journal j) { /* @@ -826,20 +830,16 @@ loanMakePayment( std::int32_t const loanScale = loan->at(sfLoanScale); auto totalValueOutstandingProxy = loan->at(sfTotalValueOutstanding); - auto interestOwedProxy = loan->at(sfInterestOwed); auto referencePrincipalProxy = loan->at(sfReferencePrincipal); TenthBips32 const interestRate{loan->at(sfInterestRate)}; TenthBips32 const lateInterestRate{loan->at(sfLateInterestRate)}; TenthBips32 const closeInterestRate{loan->at(sfCloseInterestRate)}; - TenthBips32 const overpaymentInterestRate{ - loan->at(sfOverpaymentInterestRate)}; Number const serviceFee = loan->at(sfLoanServiceFee); Number const latePaymentFee = loan->at(sfLatePaymentFee); Number const closePaymentFee = roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale); - TenthBips32 const overpaymentFee{loan->at(sfOverpaymentFee)}; auto const periodicPayment = loan->at(sfPeriodicPayment); @@ -857,10 +857,6 @@ loanMakePayment( XRPL_ASSERT( *totalValueOutstandingProxy > 0, "ripple::loanMakePayment : valid total value"); - XRPL_ASSERT_PARTS( - *interestOwedProxy >= 0, - "ripple::loanMakePayment", - "valid interest owed"); view.update(loan); @@ -1027,68 +1023,138 @@ loanMakePayment( "ripple::loanMakePayment", "fee parts add up"); - return Unexpected(temDISABLED); -#if LOANCOMPLETE // ------------------------------------------------------------- // overpayment handling - Number overpaymentInterestPortion = 0; if (loan->isFlag(lsfLoanOverpayment)) { - Number const overpayment = std::min( - principalOutstandingProxy.value(), - amount - (totalPrincipalPaid + totalInterestPaid + totalfeeToPay)); + TenthBips32 const overpaymentInterestRate{ + loan->at(sfOverpaymentInterestRate)}; + TenthBips32 const overpaymentFeeRate{loan->at(sfOverpaymentFee)}; - if (roundToAsset(asset, overpayment, loanScale) > 0) + Number const overpayment = amount - + (totalParts.principalPaid + totalParts.interestPaid + + totalParts.feeToPay); + XRPL_ASSERT( + overpayment >= 0 && isRounded(asset, overpayment, loanScale), + "ripple::loanMakePayment : valid overpayment amount"); + + Number const fee = roundToAsset( + asset, + tenthBipsOfValue(overpayment, overpaymentFeeRate), + loanScale); + + Number const payment = overpayment - fee; + + Number const interest = + tenthBipsOfValue(payment, overpaymentInterestRate); + Number const roundedInterest = roundToAsset(asset, interest, loanScale); + + detail::PaymentComponentsPlus overpaymentComponents{ + PaymentComponents{ + .rawInterest = interest, + .rawPrincipal = payment - interest, + .roundedInterest = roundedInterest, + .roundedPrincipal = payment - roundedInterest, + .roundedPayment = payment, + .extra = true}, + fee}; + + // Don't process an overpayment if the whole amount (or more!) + // gets eaten by fees and interest. + if (overpaymentComponents.rawPrincipal > 0 && + overpaymentComponents.roundedPrincipal > 0) { - Number const interestPortion = roundToAsset( - asset, - tenthBipsOfValue(overpayment, overpaymentInterestRate), - loanScale); - Number const feePortion = roundToAsset( - asset, - tenthBipsOfValue(overpayment, overpaymentFee), - loanScale); - Number const remainder = roundToAsset( - asset, overpayment - interestPortion - feePortion, loanScale); + Number const totalInterestOutstandingBefore = + totalValueOutstandingProxy - principalOutstandingProxy; - // Don't process an overpayment if the whole amount (or more!) - // gets eaten by fees - if (remainder > 0) + auto const oldLoanProperties = computeLoanProperties( + asset, + principalOutstandingProxy, + referencePrincipalProxy, + interestRate, + paymentInterval, + paymentRemainingProxy, + managementFeeRate); + + auto const accumulatedError = + oldLoanProperties.totalValueOutstanding - + totalValueOutstandingProxy; + + totalParts += detail::doPayment( + overpaymentComponents, + totalValueOutstandingProxy, + principalOutstandingProxy, + referencePrincipalProxy, + paymentRemainingProxy, + prevPaymentDateProxy, + nextDueDateProxy, + paymentInterval); + + auto const newLoanProperties = computeLoanProperties( + asset, + principalOutstandingProxy, + referencePrincipalProxy, + interestRate, + paymentInterval, + paymentRemainingProxy, + managementFeeRate); + + if (newLoanProperties.firstPaymentPrincipal <= 0 && + *principalOutstandingProxy > 0) { - overpaymentInterestPortion = interestPortion; - totalPrincipalPaid += remainder; - totalInterestPaid += interestPortion; - totalfeeToPay += feePortion; - - principalOutstandingProxy -= remainder; + // The overpayment has caused the loan to be in a state where + // no further principal can be paid. + JLOG(j.warn()) + << "Loan overpayment would cause loan to be stuck. " + "Rejecting overpayment."; + return Unexpected(tecLIMIT_EXCEEDED); } + // Check that the other computed values are valid + if (newLoanProperties.interestOwedToVault < 0 || + newLoanProperties.totalValueOutstanding <= 0 || + newLoanProperties.periodicPayment <= 0) + { + // LCOV_EXCL_START + JLOG(j.warn()) << "Computed loan properties are invalid. Does " + "not compute. TotalValueOutstanding: " + << newLoanProperties.totalValueOutstanding + << ", PeriodicPayment: " + << newLoanProperties.periodicPayment + << ", InterestOwedToVault: " + << newLoanProperties.interestOwedToVault; + return Unexpected(tecINTERNAL); + // LCOV_EXCL_STOP + } + + totalValueOutstandingProxy = + newLoanProperties.totalValueOutstanding; + loan->at(sfPeriodicPayment) = newLoanProperties.periodicPayment; + loan->at(sfInterestOwed) = newLoanProperties.interestOwedToVault; + + auto const totalInterestOutstandingAfter = + totalValueOutstandingProxy - principalOutstandingProxy; + + totalParts.valueChange += totalInterestOutstandingBefore - + totalInterestOutstandingAfter + + overpaymentComponents.roundedInterest + accumulatedError; } } - totalParts.valueChange += (newTotalInterest - totalInterestOutstanding) + - overpaymentInterestPortion; - // Check the final results are rounded, to double-check that the // intermediate steps were rounded. XRPL_ASSERT( - roundToAsset(asset, totalPrincipalPaid, loanScale) == - totalPrincipalPaid, - "ripple::loanMakePayment : totalPrincipalPaid rounded"); + isRounded(asset, totalParts.principalPaid, loanScale), + "ripple::loanMakePayment : total principal paid rounded"); XRPL_ASSERT( - roundToAsset(asset, totalInterestPaid, loanScale) == totalInterestPaid, - "ripple::loanMakePayment : totalInterestPaid rounded"); + isRounded(asset, totalParts.interestPaid, loanScale), + "ripple::loanMakePayment : total interest paid rounded"); XRPL_ASSERT( - roundToAsset(asset, loanValueChange, loanScale) == loanValueChange, - "ripple::loanMakePayment : loanValueChange rounded"); + isRounded(asset, totalParts.valueChange, loanScale), + "ripple::loanMakePayment : loan value change rounded"); XRPL_ASSERT( - roundToAsset(asset, totalfeeToPay, loanScale) == totalfeeToPay, - "ripple::loanMakePayment : totalfeeToPay rounded"); - return LoanPaymentParts{ - .principalPaid = totalPrincipalPaid, - .interestPaid = totalInterestPaid, - .valueChange = loanValueChange, - .feeToPay = totalfeeToPay}; -#endif + isRounded(asset, totalParts.feeToPay, loanScale), + "ripple::loanMakePayment : total fee to pay rounded"); + return totalParts; } } // namespace ripple diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index cccc09a03f..1f42e5616d 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -234,8 +234,10 @@ LoanPay::doApply() LoanManage::unimpairLoan(view, loanSle, vaultSle, j_); } + TenthBips16 managementFeeRate{brokerSle->at(sfManagementFeeRate)}; + Expected paymentParts = - loanMakePayment(asset, view, loanSle, amount, j_); + loanMakePayment(asset, view, loanSle, amount, managementFeeRate, j_); if (!paymentParts) return paymentParts.error(); @@ -245,11 +247,12 @@ LoanPay::doApply() view.update(loanSle); XRPL_ASSERT_PARTS( - // It is possible to pay 0 interest + // It is possible to pay 0 principal paymentParts->principalPaid >= 0, "ripple::LoanPay::doApply", "valid principal paid"); XRPL_ASSERT_PARTS( + // It is possible to pay 0 interest paymentParts->interestPaid >= 0, "ripple::LoanPay::doApply", "valid interest paid"); @@ -272,7 +275,6 @@ LoanPay::doApply() // LoanBroker object state changes view.update(brokerSle); - TenthBips32 managementFeeRate{brokerSle->at(sfManagementFeeRate)}; auto interestOwedProxy = loanSle->at(sfInterestOwed); auto const [managementFee, interestPaidToVault] = [&]() { @@ -283,7 +285,7 @@ LoanPay::doApply() auto const interest = paymentParts->interestPaid - managementFee; auto const owed = *interestOwedProxy; if (interest > owed) - return std::make_pair(interest - owed, owed); + return std::make_pair(paymentParts->interestPaid - owed, owed); return std::make_pair(managementFee, interest); }(); XRPL_ASSERT_PARTS( From fc2163708ac9188aaec5330a47d491e56f6747cc Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 6 Oct 2025 22:04:46 -0400 Subject: [PATCH 126/291] Update InvariantCheck, LoanManage, and LoanPay to make tests work - Primarily updating tests, and fixing stuff that didn't work. - Tests still not expected to pass. - Add Json::Value::isMember(StaticString) so SFields can be used. - Validate more fields in ValidLoan Invariant --- include/xrpl/json/json_value.h | 3 + .../xrpl/protocol/detail/ledger_entries.macro | 5 +- src/libxrpl/json/json_value.cpp | 6 + src/test/app/Loan_test.cpp | 314 +++++++----------- src/xrpld/app/tx/detail/InvariantCheck.cpp | 53 ++- src/xrpld/app/tx/detail/LoanManage.cpp | 18 +- src/xrpld/app/tx/detail/LoanPay.cpp | 34 +- 7 files changed, 210 insertions(+), 223 deletions(-) diff --git a/include/xrpl/json/json_value.h b/include/xrpl/json/json_value.h index 272d12d680..8ad9b48eb5 100644 --- a/include/xrpl/json/json_value.h +++ b/include/xrpl/json/json_value.h @@ -395,6 +395,9 @@ public: /// Return true if the object has a member named key. bool isMember(std::string const& key) const; + /// Return true if the object has a member named key. + bool + isMember(StaticString const& key) const; /// \brief Return a list of the member names. /// diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index b48151f5cc..f1b93c9a8c 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -573,12 +573,13 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({ // represents interest owed to the vault. // There are two additional values that can be computed from these: // - InterestOutstanding: TotalValueOutstanding - PrincipalOutstanding - // This is the total amount of interest still pending on the loan, + // The total amount of interest still pending on the loan, // independent of management fees. // - ManagementFeeOwed: InterestOutstanding - InterestOwed - // This is the amount of the total interest that will be sent to the + // The amount of the total interest that will be sent to the // broker as management fees. {sfPrincipalOutstanding, soeDEFAULT}, + {sfReferencePrincipal, soeDEFAULT}, {sfTotalValueOutstanding, soeDEFAULT}, {sfInterestOwed, soeDEFAULT}, // Based on the original principal borrowed, used for diff --git a/src/libxrpl/json/json_value.cpp b/src/libxrpl/json/json_value.cpp index 7e4d8b6d81..14f8facf98 100644 --- a/src/libxrpl/json/json_value.cpp +++ b/src/libxrpl/json/json_value.cpp @@ -1001,6 +1001,12 @@ Value::isMember(std::string const& key) const return isMember(key.c_str()); } +bool +Value::isMember(StaticString const& key) const +{ + return isMember(key.c_str()); +} + Value::Members Value::getMemberNames() const { diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 495c6a405d..b992cf278d 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -44,6 +44,7 @@ class Loan_test : public beast::unit_test::suite static constexpr auto const maxCoveredLoanValue = 1000 * 100 / 10; static constexpr auto const vaultDeposit = 1'000'000; static constexpr auto const debtMaximumParameter = 25'000; + static constexpr TenthBips16 const managementFeeRateParameter{100}; std::string const iouCurrency{"IOU"}; void @@ -112,7 +113,11 @@ class Loan_test : public beast::unit_test::suite std::uint32_t nextPaymentDate = 0; std::uint32_t paymentRemaining = 0; std::int32_t const loanScale = 0; + Number totalValue = 0; Number principalOutstanding = 0; + Number referencePrincipal = 0; + Number interestOwed = 0; + Number periodicPayment = 0; std::uint32_t flags = 0; std::uint32_t paymentInterval = 0; TenthBips32 const interestRate{}; @@ -148,36 +153,22 @@ class Loan_test : public beast::unit_test::suite */ void checkBroker( - Number const& principalRequested, Number const& principalOutstanding, + Number const& interestOwed, TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t paymentsRemaining, std::uint32_t ownerCount) const { -#if LOANCOMPLETE using namespace jtx; if (auto brokerSle = env.le(keylet::loanbroker(broker.brokerID)); env.test.BEAST_EXPECT(brokerSle)) { TenthBips16 const managementFeeRate{ brokerSle->at(sfManagementFeeRate)}; - auto const loanInterest = loanInterestOutstandingMinusFee( - broker.asset, - principalRequested, - principalOutstanding, - interestRate, - paymentInterval, - paymentsRemaining, - managementFeeRate); auto const brokerDebt = brokerSle->at(sfDebtTotal); - auto const expectedDebt = principalOutstanding + loanInterest; - env.test.BEAST_EXPECT( - // Allow some slop for rounding - brokerDebt == expectedDebt || - (expectedDebt != Number(0) && - ((brokerDebt - expectedDebt) / expectedDebt < - Number(1, -8)))); + auto const expectedDebt = principalOutstanding + interestOwed; + env.test.BEAST_EXPECT(brokerDebt == expectedDebt); env.test.BEAST_EXPECT( env.balance(pseudoAccount, broker.asset).number() == brokerSle->at(sfCoverAvailable)); @@ -213,7 +204,6 @@ class Loan_test : public beast::unit_test::suite } } } -#endif } /** Checks both the loan and broker expect states against the ledger */ @@ -223,7 +213,11 @@ class Loan_test : public beast::unit_test::suite std::uint32_t nextPaymentDate, std::uint32_t paymentRemaining, Number const& loanScale, + Number const& totalValue, Number const& principalOutstanding, + Number const& referencePrincipal, + Number const& interestOwed, + Number const& periodicPayment, std::uint32_t flags) const { using namespace jtx; @@ -236,18 +230,22 @@ class Loan_test : public beast::unit_test::suite env.test.BEAST_EXPECT( loan->at(sfPaymentRemaining) == paymentRemaining); env.test.BEAST_EXPECT(loan->at(sfLoanScale) == loanScale); + env.test.BEAST_EXPECT( + loan->at(sfTotalValueOutstanding) == totalValue); env.test.BEAST_EXPECT( loan->at(sfPrincipalOutstanding) == principalOutstanding); env.test.BEAST_EXPECT( - loan->at(sfLoanScale) == - broker.asset(loanAmount).value().exponent()); + loan->at(sfReferencePrincipal) == referencePrincipal); + env.test.BEAST_EXPECT(loan->at(sfInterestOwed) == interestOwed); + env.test.BEAST_EXPECT( + loan->at(sfPeriodicPayment) == periodicPayment); env.test.BEAST_EXPECT(loan->at(sfFlags) == flags); auto const interestRate = TenthBips32{loan->at(sfInterestRate)}; auto const paymentInterval = loan->at(sfPaymentInterval); checkBroker( - loanScale, principalOutstanding, + interestOwed, interestRate, paymentInterval, paymentRemaining, @@ -261,30 +259,18 @@ class Loan_test : public beast::unit_test::suite env.le(keylet::vault(brokerSle->at(sfVaultID))); env.test.BEAST_EXPECT(vaultSle)) { -#if LOANCOMPLETE if ((flags & lsfLoanImpaired) && !(flags & lsfLoanDefault)) { - TenthBips32 const managementFeeRate{ - brokerSle->at(sfManagementFeeRate)}; env.test.BEAST_EXPECT( vaultSle->at(sfLossUnrealized) == - principalOutstanding + - loanInterestOutstandingMinusFee( - broker.asset, - loanScale, - principalOutstanding, - interestRate, - paymentInterval, - paymentRemaining, - managementFeeRate)); + principalOutstanding + interestOwed); } else { env.test.BEAST_EXPECT( vaultSle->at(sfLossUnrealized) == 0); } -#endif } } } @@ -299,7 +285,11 @@ class Loan_test : public beast::unit_test::suite state.nextPaymentDate, state.paymentRemaining, state.loanScale, + state.totalValue, state.principalOutstanding, + state.referencePrincipal, + state.interestOwed, + state.periodicPayment, state.flags); }; }; @@ -342,7 +332,7 @@ class Loan_test : public beast::unit_test::suite using namespace loanBroker; env(set(lender, vaultKeylet.key), data(testData), - managementFeeRate(TenthBips16(100)), + managementFeeRate(managementFeeRateParameter), debtMaximum(debtMaximumValue), coverRateMinimum(TenthBips32(coverRateMinParameter)), coverRateLiquidation(TenthBips32(coverRateLiquidationParameter))); @@ -373,7 +363,11 @@ class Loan_test : public beast::unit_test::suite .nextPaymentDate = loan->at(sfNextPaymentDueDate), .paymentRemaining = loan->at(sfPaymentRemaining), .loanScale = loan->at(sfLoanScale), + .totalValue = loan->at(sfTotalValueOutstanding), .principalOutstanding = loan->at(sfPrincipalOutstanding), + .referencePrincipal = loan->at(sfReferencePrincipal), + .interestOwed = loan->at(sfInterestOwed), + .periodicPayment = loan->at(sfPeriodicPayment), .flags = loan->at(sfFlags), .paymentInterval = loan->at(sfPaymentInterval), .interestRate = TenthBips32{loan->at(sfInterestRate)}, @@ -402,7 +396,6 @@ class Loan_test : public beast::unit_test::suite BrokerInfo const& broker, LoanState const& state) { -#if LOANCOMPLETE if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); BEAST_EXPECT(brokerSle)) { @@ -413,17 +406,8 @@ class Loan_test : public beast::unit_test::suite // log << vaultSle->getJson() << std::endl; auto const assetsUnavailable = vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable); - auto const interestOutstanding = - loanInterestOutstandingMinusFee( - broker.asset, - state.loanScale, - state.principalOutstanding, - state.interestRate, - state.paymentInterval, - state.paymentRemaining, - TenthBips32{brokerSle->at(sfManagementFeeRate)}); auto const unrealizedLoss = vaultSle->at(sfLossUnrealized) + - state.principalOutstanding + interestOutstanding; + state.principalOutstanding + state.interestOwed; if (unrealizedLoss > assetsUnavailable) { @@ -431,7 +415,6 @@ class Loan_test : public beast::unit_test::suite } } } -#endif return true; } @@ -485,8 +468,7 @@ class Loan_test : public beast::unit_test::suite env, broker, loanAmount, pseudoAcct, keylet); // No loans yet - verifyLoanStatus.checkBroker( - broker.asset(loanAmount).value(), 0, TenthBips32{0}, 1, 0, 0); + verifyLoanStatus.checkBroker(0, 0, TenthBips32{0}, 1, 0, 0); if (!BEAST_EXPECT(loanSequence != 0)) return; @@ -622,12 +604,25 @@ class Loan_test : public beast::unit_test::suite auto state = getCurrentState(env, broker, keylet, verifyLoanStatus); + auto const loanProperties = computeLoanProperties( + broker.asset, + state.principalOutstanding, + state.referencePrincipal, + state.interestRate, + state.paymentInterval, + state.paymentRemaining, + managementFeeRateParameter); + verifyLoanStatus( 0, startDate + interval, total, + principalRequest.exponent(), + loanProperties.totalValueOutstanding, principalRequest, principalRequest, + loanProperties.interestOwedToVault, + loanProperties.periodicPayment, loanFlags | 0); // Manage the loan @@ -680,8 +675,12 @@ class Loan_test : public beast::unit_test::suite 0, nextDueDate, total, + principalRequest.exponent(), + loanProperties.totalValueOutstanding, principalRequest, principalRequest, + loanProperties.interestOwedToVault, + loanProperties.periodicPayment, loanFlags | 0); // Can't delete the loan yet. It has payments remaining. @@ -730,8 +729,7 @@ class Loan_test : public beast::unit_test::suite } // No loans left - verifyLoanStatus.checkBroker( - broker.asset(1000).value(), 0, interest, 1, 0, 0); + verifyLoanStatus.checkBroker(0, 0, interest, 1, 0, 0); BEAST_EXPECT( env.balance(borrower, broker.asset).value() == @@ -763,7 +761,6 @@ class Loan_test : public beast::unit_test::suite Number const& loanAmount, int interestExponent) { -#if LOANCOMPLETE using namespace jtx; auto const& asset = broker.asset.raw(); @@ -1165,15 +1162,6 @@ class Loan_test : public beast::unit_test::suite { BEAST_EXPECT( state.loanScale == state.principalOutstanding.exponent()); - auto const interestOutstanding = - loanInterestOutstandingMinusFee( - broker.asset, - state.loanScale, - state.principalOutstanding, - state.interestRate, - state.paymentInterval, - state.paymentRemaining, - TenthBips32{brokerSle->at(sfManagementFeeRate)}); auto const defaultAmount = roundToAsset( broker.asset, std::min( @@ -1182,8 +1170,8 @@ class Loan_test : public beast::unit_test::suite brokerSle->at(sfDebtTotal), coverRateMinParameter), coverRateLiquidationParameter), - state.principalOutstanding + interestOutstanding), - state.principalRequested); + state.principalOutstanding + state.interestOwed), + state.loanScale); return std::make_pair(defaultAmount, brokerSle->at(sfOwner)); } return std::make_pair(Number{}, AccountID{}); @@ -1271,7 +1259,10 @@ class Loan_test : public beast::unit_test::suite state.flags |= tfLoanDefault; state.paymentRemaining = 0; + state.totalValue = 0; state.principalOutstanding = 0; + state.referencePrincipal = 0; + state.interestOwed = 0; verifyLoanStatus(state); // Once a loan is defaulted, it can't be managed @@ -1279,6 +1270,10 @@ class Loan_test : public beast::unit_test::suite ter(tecNO_PERMISSION)); env(manage(lender, loanKeylet.key, tfLoanImpair), ter(tecNO_PERMISSION)); + // Can't make a payment on it either + env(pay(borrower, loanKeylet.key, broker.asset(300)), + ter(tecKILLED)); + }; }; @@ -1291,8 +1286,6 @@ class Loan_test : public beast::unit_test::suite auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus); BEAST_EXPECT(state.flags == baseFlag); - STAmount const drawAmount = - STAmount(broker.asset, state.principalRequested - 1); env.close(state.startDate + 20s); auto const loanAge = (env.now() - state.startDate).count(); BEAST_EXPECT(loanAge == 30); @@ -1360,7 +1353,7 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT( payoffAmount == broker.asset(Number(1040000114155251, -12))); - BEAST_EXPECT(payoffAmount > drawAmount); + BEAST_EXPECT(payoffAmount > state.principalOutstanding); // Try to pay a little extra to show that it's _not_ // taken auto const transactionAmount = payoffAmount + broker.asset(10); @@ -1384,18 +1377,12 @@ class Loan_test : public beast::unit_test::suite STAmount const balanceChangeAmount{ broker.asset, - roundToAsset( - broker.asset, - payoffAmount, - borrowerBalanceBeforePayment.number())}; + roundToAsset(broker.asset, payoffAmount, state.loanScale)}; { - auto const difference = roundToReference( + auto const difference = env.balance(borrower, broker.asset) - - (borrowerBalanceBeforePayment - - balanceChangeAmount - adjustment), - STAmount{ - broker.asset, - borrowerBalanceBeforePayment.value() * 10}); + (borrowerBalanceBeforePayment - balanceChangeAmount - + adjustment); BEAST_EXPECT(difference == beast::zero); } @@ -1467,63 +1454,6 @@ class Loan_test : public beast::unit_test::suite 0, defaultImmediately(0, false)); - lifecycle( - caseLabel, - "Loan overpayment allowed - Draw then default", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - [&](Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus) { - // toEndOfLife - // - // Initialize values with the current state - auto state = - getCurrentState(env, broker, loanKeylet, verifyLoanStatus); - BEAST_EXPECT(state.flags == lsfLoanOverpayment); - - auto const& broker = verifyLoanStatus.broker; - auto const startingCoverAvailable = coverAvailable( - broker.brokerID, - broker.asset(coverDepositParameter).number()); - - // move past the due date + grace period (60s) - env.close(tp{d{state.nextPaymentDate}} + 60s + 20s); - - auto const [amountToBeCovered, brokerAcct] = - getDefaultInfo(state, broker); - - // default the loan - env(manage(lender, loanKeylet.key, tfLoanDefault)); - env.close(); - - // The LoanBroker just lost some of it's first-loss capital. - // Replenish it. - replenishCover( - broker, - brokerAcct, - startingCoverAvailable, - amountToBeCovered); - - state.paymentRemaining = 0; - state.principalOutstanding = 0; - state.flags |= tfLoanDefault; - - verifyLoanStatus(state); - - // Can't make a payment on it either - env(pay(borrower, loanKeylet.key, broker.asset(300)), - ter(tecKILLED)); - - // Default - }); - lifecycle( caseLabel, "Loan overpayment prohibited - Pay off immediately", @@ -1594,52 +1524,36 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT( periodicRate == Number(2283105022831050, -21, Number::unchecked{})); + STAmount const roundedPeriodicPayment{ + broker.asset, + roundPeriodicPayment( + broker.asset, state.periodicPayment, state.loanScale)}; while (state.paymentRemaining > 0) { - STAmount const principalRequestedAmount{ - broker.asset, state.principalRequested}; - // Compute the payment based on the number of - // payments remaining - auto const rateFactor = - power(1 + periodicRate, state.paymentRemaining); - Number const rawPeriodicPayment = - state.principalOutstanding * periodicRate * rateFactor / - (rateFactor - 1); - STAmount const periodicPayment{ - broker.asset, - roundToAsset( - broker.asset, - rawPeriodicPayment, - principalRequestedAmount, - Number::upward)}; - - testcase - << "Payments remaining: " << state.paymentRemaining - << ", computed payment amount: " << periodicPayment; + testcase << "Payments remaining: " << state.paymentRemaining + << ", computed payment amount: " + << state.periodicPayment; // Only check the first payment since the rounding // may drift as payments are made BEAST_EXPECT( - state.paymentRemaining < 12 || - STAmount(broker.asset, rawPeriodicPayment) == - broker.asset(Number(8333457001162141, -14))); + roundedPeriodicPayment == + broker.asset(Number(8333457001162141, -14))); // Include the service fee - STAmount const totalDue = roundToReference( - periodicPayment + broker.asset(2), - principalRequestedAmount, + STAmount const totalDue = roundToScale( + roundedPeriodicPayment + broker.asset(2), + state.loanScale, Number::upward); // Only check the first payment since the rounding // may drift as payments are made BEAST_EXPECT( - state.paymentRemaining < 12 || totalDue == - roundToReference( - broker.asset( - Number(8533457001162141, -14), - Number::upward), - principalRequestedAmount, - Number::upward)); + roundToScale( + broker.asset( + Number(8533457001162141, -14), Number::upward), + state.loanScale, + Number::upward)); // Try to pay a little extra to show that it's _not_ // taken @@ -1648,53 +1562,52 @@ class Loan_test : public beast::unit_test::suite // Only check the first payment since the rounding // may drift as payments are made BEAST_EXPECT( - state.paymentRemaining < 12 || transactionAmount == - roundToReference( - broker.asset( - Number(9533457001162141, -14), - Number::upward), - principalRequestedAmount, - Number::upward)); + roundToScale( + broker.asset( + Number(9533457001162141, -14), Number::upward), + state.loanScale, + Number::upward)); auto const totalDueAmount = STAmount{broker.asset, totalDue}; // Compute the expected principal amount Number const rawInterest = state.paymentRemaining == 1 - ? rawPeriodicPayment - state.principalOutstanding - : state.principalOutstanding * periodicRate; + ? state.periodicPayment - state.referencePrincipal + : state.referencePrincipal * periodicRate; STAmount const interest{ broker.asset, roundToAsset( broker.asset, rawInterest, - principalRequestedAmount, + state.loanScale, Number::upward)}; BEAST_EXPECT( state.paymentRemaining < 12 || interest == - roundToReference( + roundToScale( broker.asset( Number(2283105022831050, -18), Number::upward), - principalRequestedAmount, + state.loanScale, Number::upward)); BEAST_EXPECT(interest >= Number(0)); - auto const rawPrincipal = rawPeriodicPayment - rawInterest; + auto const rawPrincipal = + state.periodicPayment - rawInterest; BEAST_EXPECT( state.paymentRemaining < 12 || roundToAsset( broker.asset, rawPrincipal, - principalRequestedAmount, + state.loanScale, Number::upward) == - roundToReference( + roundToScale( broker.asset( Number(8333228690659858, -14), Number::upward), - principalRequestedAmount, + state.loanScale, Number::upward)); BEAST_EXPECT( state.paymentRemaining > 1 || @@ -1703,8 +1616,8 @@ class Loan_test : public beast::unit_test::suite broker.asset, roundToAsset( broker.asset, - periodicPayment - interest, - principalRequestedAmount, + roundedPeriodicPayment - interest, + state.loanScale, Number::downward)}; BEAST_EXPECT( principal > Number(0) && @@ -1713,8 +1626,9 @@ class Loan_test : public beast::unit_test::suite state.paymentRemaining > 1 || principal == state.principalOutstanding); BEAST_EXPECT( - rawPrincipal + rawInterest == rawPeriodicPayment); - BEAST_EXPECT(principal + interest == periodicPayment); + rawPrincipal + rawInterest == state.periodicPayment); + BEAST_EXPECT( + principal + interest == roundedPeriodicPayment); auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset); @@ -1768,7 +1682,6 @@ class Loan_test : public beast::unit_test::suite env(manage(lender, loanKeylet.key, tfLoanDefault), ter(tecNO_PERMISSION)); }); -#endif } void @@ -1979,26 +1892,31 @@ class Loan_test : public beast::unit_test::suite auto const loan = objects[0u]; BEAST_EXPECT(loan[sfBorrower] == lender.human()); - BEAST_EXPECT(loan[sfCloseInterestRate] == 0); - BEAST_EXPECT(loan[sfClosePaymentFee] == "0"); + // soeDEFAULT fields are not returned if they're in the default + // state + BEAST_EXPECT(!loan.isMember(sfCloseInterestRate)); + BEAST_EXPECT(!loan.isMember(sfClosePaymentFee)); BEAST_EXPECT(loan[sfFlags] == 0); BEAST_EXPECT(loan[sfGracePeriod] == 60); - BEAST_EXPECT(loan[sfInterestRate] == 0); - BEAST_EXPECT(loan[sfLateInterestRate] == 0); - BEAST_EXPECT(loan[sfLatePaymentFee] == "0"); + BEAST_EXPECT(!loan.isMember(sfInterestRate)); + BEAST_EXPECT(!loan.isMember(sfLateInterestRate)); + BEAST_EXPECT(!loan.isMember(sfLatePaymentFee)); BEAST_EXPECT(loan[sfLoanBrokerID] == to_string(broker.brokerID)); - BEAST_EXPECT(loan[sfLoanOriginationFee] == "0"); + BEAST_EXPECT(!loan.isMember(sfLoanOriginationFee)); BEAST_EXPECT(loan[sfLoanSequence] == 1); - BEAST_EXPECT(loan[sfLoanServiceFee] == "0"); + BEAST_EXPECT(!loan.isMember(sfLoanServiceFee)); BEAST_EXPECT( loan[sfNextPaymentDueDate] == loan[sfStartDate].asUInt() + 60); - BEAST_EXPECT(loan[sfOverpaymentFee] == 0); - BEAST_EXPECT(loan[sfOverpaymentInterestRate] == 0); + BEAST_EXPECT(!loan.isMember(sfOverpaymentFee)); + BEAST_EXPECT(!loan.isMember(sfOverpaymentInterestRate)); BEAST_EXPECT(loan[sfPaymentInterval] == 60); + BEAST_EXPECT(loan[sfPeriodicPayment] == "1000000000"); BEAST_EXPECT(loan[sfPaymentRemaining] == 1); - BEAST_EXPECT(loan[sfPreviousPaymentDate] == 0); + BEAST_EXPECT(!loan.isMember(sfPreviousPaymentDate)); BEAST_EXPECT(loan[sfPrincipalOutstanding] == "1000000000"); - BEAST_EXPECT(loan[sfLoanScale] == 0); + BEAST_EXPECT(loan[sfReferencePrincipal] == "1000000000"); + BEAST_EXPECT(loan[sfTotalValueOutstanding] == "1000000000"); + BEAST_EXPECT(loan[sfLoanScale] == -6); BEAST_EXPECT( loan[sfStartDate].asUInt() == startDate.time_since_epoch().count()); diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 2cb03181dd..801590ca28 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -2479,17 +2479,54 @@ ValidLoan::finalize( << "Invariant failed: Loan Overpayment flag changed"; return false; } - if (after->at(sfAssetsAvailable) < 0) + // Must not be negative - STNumber + for (auto const field : + {&sfLoanServiceFee, + &sfLatePaymentFee, + &sfClosePaymentFee, + &sfPrincipalOutstanding, + &sfReferencePrincipal, + &sfTotalValueOutstanding, + &sfInterestOwed}) { - JLOG(j.fatal()) - << "Invariant failed: Loan assets available is negative"; - return false; + if (after->at(*field) < 0) + { + JLOG(j.fatal()) << "Invariant failed: " << field->getName() + << " is negative "; + return false; + } } - if (after->at(sfPrincipalOutstanding) < 0) + // Must not be negative - STUInt32 + for (auto const field : + {&sfOverpaymentFee, + &sfInterestRate, + &sfLateInterestRate, + &sfCloseInterestRate, + &sfOverpaymentInterestRate, + &sfStartDate, + &sfPaymentInterval, + &sfGracePeriod, + &sfPreviousPaymentDate, + &sfPaymentRemaining}) { - JLOG(j.fatal()) - << "Invariant failed: Loan principal outstanding is negative"; - return false; + if (after->at(*field) < 0) + { + JLOG(j.fatal()) << "Invariant failed: " << field->getName() + << " is negative "; + return false; + } + } + // Must be positive + for (auto const field : { + &sfPeriodicPayment, + }) + { + if (after->at(*field) <= 0) + { + JLOG(j.fatal()) << "Invariant failed: " << field->getName() + << " is zero or negative "; + return false; + } } } return true; diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index 5bc55870b4..87db5d3c00 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -145,10 +145,13 @@ LoanManage::defaultLoan( // Calculate the amount of the Default that First-Loss Capital covers: std::int32_t const loanScale = loanSle->at(sfLoanScale); - TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; auto brokerDebtTotalProxy = brokerSle->at(sfDebtTotal); - Number const totalDefaultAmount = loanSle->at(sfTotalValueOutstanding); + auto principalOutstandingProxy = loanSle->at(sfPrincipalOutstanding); + auto interestOwedProxy = loanSle->at(sfInterestOwed); + + Number const totalDefaultAmount = + principalOutstandingProxy + interestOwedProxy; // Apply the First-Loss Capital to the Default Amount TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)}; @@ -225,16 +228,14 @@ LoanManage::defaultLoan( // The loss has been realized if (loanSle->isFlag(lsfLoanImpaired)) { - Number const lossRealized = loanSle->at(sfPrincipalOutstanding) + - loanSle->at(sfInterestOwed); auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized); - if (vaultLossUnrealizedProxy < lossRealized) + if (vaultLossUnrealizedProxy < totalDefaultAmount) { JLOG(j.warn()) << "Vault unrealized loss is less than the default amount"; return tefBAD_LEDGER; } - vaultLossUnrealizedProxy -= lossRealized; + vaultLossUnrealizedProxy -= totalDefaultAmount; } view.update(vaultSle); } @@ -268,8 +269,11 @@ LoanManage::defaultLoan( // Update the Loan object: loanSle->setFlag(lsfLoanDefault); + loanSle->at(sfTotalValueOutstanding) = 0; loanSle->at(sfPaymentRemaining) = 0; - loanSle->at(sfPrincipalOutstanding) = 0; + loanSle->at(sfReferencePrincipal) = 0; + principalOutstandingProxy = 0; + interestOwedProxy = 0; view.update(loanSle); // Return funds from the LoanBroker pseudo-account to the diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 1f42e5616d..99bb05f9ec 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -373,10 +373,19 @@ LoanPay::doApply() #if !NDEBUG auto const accountBalanceBefore = accountHolds(view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); - auto const vaultBalanceBefore = accountHolds( - view, vaultPseudoAccount, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); - auto const brokerBalanceBefore = accountHolds( - view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); + auto const vaultBalanceBefore = account_ == vaultPseudoAccount + ? STAmount{asset, 0} + : accountHolds( + view, + vaultPseudoAccount, + asset, + fhIGNORE_FREEZE, + ahIGNORE_AUTH, + j_); + auto const brokerBalanceBefore = account_ == brokerPayee + ? STAmount{asset, 0} + : accountHolds( + view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); #endif if (auto const ter = accountSend( @@ -399,10 +408,19 @@ LoanPay::doApply() #if !NDEBUG auto const accountBalanceAfter = accountHolds(view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); - auto const vaultBalanceAfter = accountHolds( - view, vaultPseudoAccount, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); - auto const brokerBalanceAfter = accountHolds( - view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); + auto const vaultBalanceAfter = account_ == vaultPseudoAccount + ? STAmount{asset, 0} + : accountHolds( + view, + vaultPseudoAccount, + asset, + fhIGNORE_FREEZE, + ahIGNORE_AUTH, + j_); + auto const brokerBalanceAfter = account_ == brokerPayee + ? STAmount{asset, 0} + : accountHolds( + view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); auto const balanceScale = std::max( {accountBalanceBefore.exponent(), From 2dd239c59fc66b665667b95eae24c918a5abc7d3 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 7 Oct 2025 21:48:43 -0400 Subject: [PATCH 127/291] Update payment component computation and rounding, and more tests - Tests not expected to pass. - Check in LoanSet if a loan with interest actually has interest. tecPRECISION_LOSS if not. - Add checks in LoanPay for deep froze broker owner and pseudoaccount. - Fix management fee calculations in LoanPay and associated LoanBroker and Vault data updates. - Make state tracking next payment due date optional. - Add a test case showing multiple payments combined. - Update more tests to work with the new fields. --- src/test/app/Loan_test.cpp | 288 +++++++++++++++---- src/xrpld/app/misc/LendingHelpers.h | 121 ++++---- src/xrpld/app/misc/detail/LendingHelpers.cpp | 8 +- src/xrpld/app/tx/detail/LoanPay.cpp | 111 +++++-- src/xrpld/app/tx/detail/LoanSet.cpp | 11 +- 5 files changed, 406 insertions(+), 133 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index b992cf278d..3c1f022186 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -110,7 +110,7 @@ class Loan_test : public beast::unit_test::suite { std::uint32_t previousPaymentDate = 0; NetClock::time_point startDate = {}; - std::uint32_t nextPaymentDate = 0; + std::optional nextPaymentDate = 0; std::uint32_t paymentRemaining = 0; std::int32_t const loanScale = 0; Number totalValue = 0; @@ -210,7 +210,7 @@ class Loan_test : public beast::unit_test::suite void operator()( std::uint32_t previousPaymentDate, - std::uint32_t nextPaymentDate, + std::optional nextPaymentDate, std::uint32_t paymentRemaining, Number const& loanScale, Number const& totalValue, @@ -226,7 +226,7 @@ class Loan_test : public beast::unit_test::suite env.test.BEAST_EXPECT( loan->at(sfPreviousPaymentDate) == previousPaymentDate); env.test.BEAST_EXPECT( - loan->at(sfNextPaymentDueDate) == nextPaymentDate); + loan->at(~sfNextPaymentDueDate) == nextPaymentDate); env.test.BEAST_EXPECT( loan->at(sfPaymentRemaining) == paymentRemaining); env.test.BEAST_EXPECT(loan->at(sfLoanScale) == loanScale); @@ -360,7 +360,7 @@ class Loan_test : public beast::unit_test::suite LoanState state{ .previousPaymentDate = loan->at(sfPreviousPaymentDate), .startDate = tp{d{loan->at(sfStartDate)}}, - .nextPaymentDate = loan->at(sfNextPaymentDueDate), + .nextPaymentDate = loan->at(~sfNextPaymentDueDate), .paymentRemaining = loan->at(sfPaymentRemaining), .loanScale = loan->at(sfLoanScale), .totalValue = loan->at(sfTotalValueOutstanding), @@ -373,8 +373,9 @@ class Loan_test : public beast::unit_test::suite .interestRate = TenthBips32{loan->at(sfInterestRate)}, }; BEAST_EXPECT(state.previousPaymentDate == 0); - BEAST_EXPECT( - tp{d{state.nextPaymentDate}} == state.startDate + 600s); + if (BEAST_EXPECT(state.nextPaymentDate)) + BEAST_EXPECT( + tp{d{*state.nextPaymentDate}} == state.startDate + 600s); BEAST_EXPECT(state.paymentRemaining == 12); BEAST_EXPECT( state.principalOutstanding == broker.asset(1000).value()); @@ -1231,7 +1232,9 @@ class Loan_test : public beast::unit_test::suite verifyLoanStatus(state); } - auto const nextDueDate = tp{d{state.nextPaymentDate}}; + BEAST_EXPECT(state.nextPaymentDate); + auto const nextDueDate = + tp{d{state.nextPaymentDate.value_or(0)}}; // Can't default the loan yet. The grace period hasn't // expired @@ -1273,7 +1276,6 @@ class Loan_test : public beast::unit_test::suite // Can't make a payment on it either env(pay(borrower, loanKeylet.key, broker.asset(300)), ter(tecKILLED)); - }; }; @@ -1354,6 +1356,11 @@ class Loan_test : public beast::unit_test::suite payoffAmount == broker.asset(Number(1040000114155251, -12))); BEAST_EXPECT(payoffAmount > state.principalOutstanding); + // The terms of this loan actually make the early payoff more + // expensive than just making payments + BEAST_EXPECT( + payoffAmount > state.paymentRemaining * + (state.periodicPayment + broker.asset(2).value())); // Try to pay a little extra to show that it's _not_ // taken auto const transactionAmount = payoffAmount + broker.asset(10); @@ -1373,6 +1380,120 @@ class Loan_test : public beast::unit_test::suite state.paymentRemaining = 0; state.principalOutstanding = 0; + state.referencePrincipal = 0; + state.totalValue = 0; + state.interestOwed = 0; + if (BEAST_EXPECT(state.nextPaymentDate)) + state.previousPaymentDate = *state.nextPaymentDate; + state.nextPaymentDate.reset(); + verifyLoanStatus(state); + + STAmount const balanceChangeAmount{ + broker.asset, + roundToAsset(broker.asset, payoffAmount, state.loanScale)}; + { + auto const difference = + roundToScale( + env.balance(borrower, broker.asset), + state.loanScale) - + (borrowerBalanceBeforePayment - balanceChangeAmount - + adjustment); + BEAST_EXPECT(difference == beast::zero); + BEAST_EXPECT( + roundToScale(difference, state.loanScale) == + beast::zero); + } + + // Can't impair or default a paid off loan + env(manage(lender, loanKeylet.key, tfLoanImpair), + ter(tecNO_PERMISSION)); + env(manage(lender, loanKeylet.key, tfLoanDefault), + ter(tecNO_PERMISSION)); + }; + }; + + auto multiplePayoff = [&](std::uint32_t baseFlag) { + return [&, baseFlag]( + Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus) { + // toEndOfLife + // + auto state = + getCurrentState(env, broker, loanKeylet, verifyLoanStatus); + BEAST_EXPECT(state.flags == baseFlag); + env.close(state.startDate + 20s); + auto const loanAge = (env.now() - state.startDate).count(); + BEAST_EXPECT(loanAge == 30); + + verifyLoanStatus(state); + + // Send some bogus pay transactions + env(pay(borrower, + keylet::loan(uint256(0)).key, + broker.asset(10)), + ter(temINVALID)); + env(pay(borrower, loanKeylet.key, broker.asset(-100)), + ter(temBAD_AMOUNT)); + env(pay(borrower, broker.brokerID, broker.asset(100)), + ter(tecNO_ENTRY)); + env(pay(evan, loanKeylet.key, broker.asset(500)), + ter(tecNO_PERMISSION)); + + { + auto const otherAsset = + broker.asset.raw() == assets[0].raw() ? assets[1] + : assets[0]; + env(pay(borrower, loanKeylet.key, otherAsset(100)), + ter(tecWRONG_ASSET)); + } + + // Amount doesn't cover a single payment + env(pay(borrower, loanKeylet.key, STAmount{broker.asset, 1}), + ter(tecINSUFFICIENT_PAYMENT)); + + // Get the balance after these failed transactions take + // fees + auto const borrowerBalanceBeforePayment = + env.balance(borrower, broker.asset); + + // Make all the payments in one transaction + // service fee is 2 + auto const startingPayments = state.paymentRemaining; + auto const rawPayoff = startingPayments * + (state.periodicPayment + broker.asset(2).value()); + STAmount const payoffAmount{broker.asset, rawPayoff}; + BEAST_EXPECT( + payoffAmount == + broker.asset(Number(1024014840139457, -12))); + BEAST_EXPECT(payoffAmount > state.principalOutstanding); + + // Try to pay a little extra to show that it's _not_ + // taken + auto const transactionAmount = payoffAmount + broker.asset(10); + BEAST_EXPECT( + transactionAmount == + broker.asset(Number(1034014840139457, -12))); + env(pay(borrower, loanKeylet.key, transactionAmount)); + + env.close(); + + // Need to account for fees if the loan is in XRP + PrettyAmount adjustment = broker.asset(0); + if (broker.asset.raw().native()) + { + adjustment = env.current()->fees().base; + } + + state.paymentRemaining = 0; + state.principalOutstanding = 0; + state.referencePrincipal = 0; + state.totalValue = 0; + state.interestOwed = 0; + if (BEAST_EXPECT(state.nextPaymentDate)) + state.previousPaymentDate = *state.nextPaymentDate + + state.paymentInterval * + (startingPayments - 1); // 9280-2680=6600 + state.nextPaymentDate.reset(); verifyLoanStatus(state); STAmount const balanceChangeAmount{ @@ -1482,6 +1603,34 @@ class Loan_test : public beast::unit_test::suite tfLoanOverpayment, immediatePayoff(lsfLoanOverpayment)); + lifecycle( + caseLabel, + "Loan overpayment prohibited - Make multiple payments", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + 0, + multiplePayoff(0)); + + lifecycle( + caseLabel, + "Loan overpayment allowed - Make multiple payments", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + multiplePayoff(lsfLoanOverpayment)); + lifecycle( caseLabel, "Loan overpayment prohibited - Make payments", @@ -1529,20 +1678,24 @@ class Loan_test : public beast::unit_test::suite roundPeriodicPayment( broker.asset, state.periodicPayment, state.loanScale)}; + testcase << "\tPayment components: " + << "Payments remaining, rawInterest, rawPrincipal, " + "roundedInterest, " + "roundedPrincipal, roundedPayment, final, extra"; + while (state.paymentRemaining > 0) { - testcase << "Payments remaining: " << state.paymentRemaining - << ", computed payment amount: " - << state.periodicPayment; - + auto const serviceFee = broker.asset(2); // Only check the first payment since the rounding // may drift as payments are made BEAST_EXPECT( roundedPeriodicPayment == - broker.asset(Number(8333457001162141, -14))); + broker.asset( + Number(8333457001162141, -14), Number::upward)); + // 83334570.01162141 // Include the service fee STAmount const totalDue = roundToScale( - roundedPeriodicPayment + broker.asset(2), + roundedPeriodicPayment + serviceFee, state.loanScale, Number::upward); // Only check the first payment since the rounding @@ -1569,38 +1722,52 @@ class Loan_test : public beast::unit_test::suite state.loanScale, Number::upward)); - auto const totalDueAmount = - STAmount{broker.asset, totalDue}; - // Compute the expected principal amount - Number const rawInterest = state.paymentRemaining == 1 - ? state.periodicPayment - state.referencePrincipal - : state.referencePrincipal * periodicRate; - STAmount const interest{ - broker.asset, - roundToAsset( + auto const paymentComponents = + detail::computePaymentComponents( broker.asset, - rawInterest, state.loanScale, - Number::upward)}; + state.totalValue, + state.principalOutstanding, + state.referencePrincipal, + state.periodicPayment, + periodicRate, + state.paymentRemaining); + + testcase + << "\tPayment components: " << state.paymentRemaining + << ", " << paymentComponents.rawInterest << ", " + << paymentComponents.rawPrincipal << ", " + << paymentComponents.roundedInterest << ", " + << paymentComponents.roundedPrincipal << ", " + << paymentComponents.roundedPayment << ", " + << paymentComponents.final << ", " + << paymentComponents.extra; + + auto const totalDueAmount = STAmount{ + broker.asset, + paymentComponents.roundedPayment + serviceFee.number()}; + + BEAST_EXPECT( + paymentComponents.final || totalDue == totalDueAmount); + BEAST_EXPECT( state.paymentRemaining < 12 || - interest == + paymentComponents.roundedInterest == roundToScale( broker.asset( Number(2283105022831050, -18), Number::upward), state.loanScale, Number::upward)); - BEAST_EXPECT(interest >= Number(0)); + BEAST_EXPECT( + paymentComponents.roundedInterest >= Number(0)); - auto const rawPrincipal = - state.periodicPayment - rawInterest; BEAST_EXPECT( state.paymentRemaining < 12 || roundToAsset( broker.asset, - rawPrincipal, + paymentComponents.rawPrincipal, state.loanScale, Number::upward) == roundToScale( @@ -1610,25 +1777,27 @@ class Loan_test : public beast::unit_test::suite state.loanScale, Number::upward)); BEAST_EXPECT( - state.paymentRemaining > 1 || - rawPrincipal == state.principalOutstanding); - STAmount const principal{ - broker.asset, - roundToAsset( - broker.asset, - roundedPeriodicPayment - interest, - state.loanScale, - Number::downward)}; + !paymentComponents.final || + paymentComponents.rawPrincipal == + state.referencePrincipal); BEAST_EXPECT( - principal > Number(0) && - principal <= state.principalOutstanding); + paymentComponents.roundedPrincipal >= Number(0) && + paymentComponents.roundedPrincipal <= + state.principalOutstanding); BEAST_EXPECT( - state.paymentRemaining > 1 || - principal == state.principalOutstanding); + !paymentComponents.final || + paymentComponents.roundedPrincipal == + state.principalOutstanding); BEAST_EXPECT( - rawPrincipal + rawInterest == state.periodicPayment); + paymentComponents.final || + paymentComponents.rawPrincipal + + paymentComponents.rawInterest == + state.periodicPayment); BEAST_EXPECT( - principal + interest == roundedPeriodicPayment); + paymentComponents.final || + paymentComponents.roundedPrincipal + + paymentComponents.roundedInterest == + roundedPeriodicPayment); auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset); @@ -1665,9 +1834,29 @@ class Loan_test : public beast::unit_test::suite Number(1, -4)))); --state.paymentRemaining; - state.previousPaymentDate = state.nextPaymentDate; - state.nextPaymentDate += state.paymentInterval; - state.principalOutstanding -= principal; + if (BEAST_EXPECT(state.nextPaymentDate)) + { + state.previousPaymentDate = *state.nextPaymentDate; + if (paymentComponents.final) + { + state.nextPaymentDate.reset(); + state.paymentRemaining = 0; + } + else + { + *state.nextPaymentDate += state.paymentInterval; + } + } + state.principalOutstanding -= + paymentComponents.roundedPrincipal; + state.referencePrincipal -= paymentComponents.rawPrincipal; + state.totalValue -= paymentComponents.roundedPrincipal + + paymentComponents.roundedInterest; + state.interestOwed -= valueMinusFee( + broker.asset.raw(), + paymentComponents.roundedInterest, + managementFeeRateParameter, + state.loanScale); verifyLoanStatus(state); } @@ -1725,7 +1914,8 @@ class Loan_test : public beast::unit_test::suite MPTTester mptt{env, issuer, mptInitNoFund}; mptt.create( {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); - PrettyAsset const mptAsset = mptt.issuanceID(); + // Scale the MPT asset a little bit so we can get some interest + PrettyAsset const mptAsset{mptt.issuanceID(), 100}; mptt.authorize({.account = lender}); mptt.authorize({.account = borrower}); mptt.authorize({.account = evan}); diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index e1ddf7e1b2..69b77bf470 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -150,14 +150,13 @@ computePaymentComponents( "Outstanding values are rounded"); auto const roundedPeriodicPayment = roundPeriodicPayment(asset, periodicPayment, scale); - if (paymentRemaining == 1 || totalValueOutstanding <= periodicPayment) + if (paymentRemaining == 1 || + totalValueOutstanding <= roundedPeriodicPayment) { // If there's only one payment left, we need to pay off each of the loan // parts. - // - // The totalValueOutstanding should never be less than the - // periodicPayment until the last scheduled payment, but if it ever is, - // make it the last payment. + // rawInterest could be < 0 because we're computing it with the rounded + // value outstanding, but for the last payment, we also don't care. Number rawInterest = totalValueOutstanding - referencePrincipal; Number roundedInterest = totalValueOutstanding - principalOutstanding; @@ -169,7 +168,7 @@ computePaymentComponents( "last payment is complete"); return { - .rawInterest = rawInterest, + .rawInterest = std::max(Number{}, rawInterest), .rawPrincipal = referencePrincipal, .roundedInterest = roundedInterest, .roundedPrincipal = principalOutstanding, @@ -190,54 +189,54 @@ computePaymentComponents( "ripple::detail::computePaymentComponents", "valid raw interest"); XRPL_ASSERT_PARTS( - rawPrincipal > 0 && rawPrincipal <= referencePrincipal, + rawPrincipal >= 0 && rawPrincipal <= referencePrincipal, "ripple::detail::computePaymentComponents", "valid raw principal"); - // if (count($A20), MIN(Z19, Z19 - FLOOR(AA19 - Y20, 1)), "") - // Z19 = outstanding principal - // AA19 = reference principal - // Y20 = raw principal - Number const roundedPrincipal = [&]() { - Number const p = std::max( - Number{}, - std::min( - principalOutstanding, - principalOutstanding - - roundToAsset( - asset, - referencePrincipal - rawPrincipal, - scale, - Number::downward))); - // if the estimated principal payment would leave the principal higher - // than the "total "after payment" value of the loan, make the principal - // payment also take the principal down to that same "after" value. - // This should mean that all interest is paid, or that the loan has some - // tricky parameters. - if (principalOutstanding - p > - totalValueOutstanding - roundedPeriodicPayment) - return roundedPeriodicPayment; - // Use the amount that will get principal outstanding as close to - // reference principal as possible, but don't pay more than the rounded - // periodic payment, or we'll end up with negative interest. - return std::min(p, roundedPeriodicPayment); + // Round the raw principal after payment + auto const roundedPrincipalTarget = + roundToAsset(asset, referencePrincipal - rawPrincipal, scale); + // Determine the total value after payment + auto const totalValueTarget = + totalValueOutstanding - roundedPeriodicPayment; + // We want to get the principal down to the smaller of the two targets + auto const principalTarget = + std::min(roundedPrincipalTarget, totalValueTarget); + // What will get us to our target? + auto const p = principalOutstanding - principalTarget; + + XRPL_ASSERT_PARTS( + p >= 0 && p <= roundedPeriodicPayment, + "rippled::detail::computePaymentComponents", + "principal part not larger than total payment"); + // Make sure nothing goes negative + if (p > roundedPeriodicPayment || p > principalOutstanding) + return std::min(roundedPeriodicPayment, principalOutstanding); + else if (p < 0) + return Number{}; + + return p; }(); - // if(count($A20), if(AB19 < $B$5, AB19 - Z19, CEILING($B$10-W20, 1)), "") - // AB19 = total loan value - // $B$5 = periodic payment (unrounded) - // Z19 = outstanding principal - // $B$10 = periodic payment (rounded up) - // W20 = rounded principal + Number const roundedInterest = [&]() { + // Zero interest means ZERO interest + if (periodicRate == 0) + return Number{}; + auto i = roundedPeriodicPayment - roundedPrincipal; + // No negative interest! + if (i < 0) + return Number{}; + return i; + }(); - Number const roundedInterest = roundedPeriodicPayment - roundedPrincipal; XRPL_ASSERT_PARTS( roundedInterest >= 0 && isRounded(asset, roundedInterest, scale), "ripple::detail::computePaymentComponents", "valid rounded interest"); XRPL_ASSERT_PARTS( roundedPrincipal >= 0 && roundedPrincipal <= principalOutstanding && + roundedPrincipal <= roundedPeriodicPayment && isRounded(asset, roundedPrincipal, scale), "ripple::detail::computePaymentComponents", "valid rounded principal"); @@ -280,11 +279,12 @@ doPayment( nextDueDateProxy, "ripple::detail::doPayment", "Next due date proxy set"); + auto const totalValueDelta = payment.roundedPrincipal + + payment.roundedInterest - payment.valueChange; if (!payment.extra) { if (payment.final) { - paymentRemainingProxy = 0; XRPL_ASSERT_PARTS( referencePrincipalProxy == payment.rawPrincipal, "ripple::detail::doPayment", @@ -294,11 +294,12 @@ doPayment( "ripple::detail::doPayment", "Full principal payment"); XRPL_ASSERT_PARTS( - totalValueOutstandingProxy == - payment.roundedPrincipal + payment.roundedInterest, + totalValueOutstandingProxy == totalValueDelta, "ripple::detail::doPayment", "Full value payment"); + paymentRemainingProxy = 0; + prevPaymentDateProxy = *nextDueDateProxy; // Remove the field. This is the only condition where nextDueDate is // allowed to be removed. @@ -306,6 +307,19 @@ doPayment( } else { + XRPL_ASSERT_PARTS( + referencePrincipalProxy > payment.rawPrincipal, + "ripple::detail::doPayment", + "Full reference principal payment"); + XRPL_ASSERT_PARTS( + principalOutstandingProxy > payment.roundedPrincipal, + "ripple::detail::doPayment", + "Full principal payment"); + XRPL_ASSERT_PARTS( + totalValueOutstandingProxy > totalValueDelta, + "ripple::detail::doPayment", + "Full value payment"); + paymentRemainingProxy -= 1; prevPaymentDateProxy = *nextDueDateProxy; @@ -317,8 +331,7 @@ doPayment( referencePrincipalProxy -= payment.rawPrincipal; principalOutstandingProxy -= payment.roundedPrincipal; - totalValueOutstandingProxy -= - payment.roundedPrincipal + payment.roundedInterest; + totalValueOutstandingProxy -= totalValueDelta; return LoanPaymentParts{ .principalPaid = payment.roundedPrincipal, @@ -1008,10 +1021,15 @@ loanMakePayment( nextDueDateProxy, paymentInterval); ++numPayments; + + if (nextPayment.final) + break; } XRPL_ASSERT_PARTS( - totalParts.principalPaid + totalParts.interestPaid == totalPaid, + totalParts.principalPaid + totalParts.interestPaid + + totalParts.feeToPay == + totalPaid, "ripple::loanMakePayment", "payment parts add up"); XRPL_ASSERT_PARTS( @@ -1025,17 +1043,16 @@ loanMakePayment( // ------------------------------------------------------------- // overpayment handling - if (loan->isFlag(lsfLoanOverpayment)) + if (loan->isFlag(lsfLoanOverpayment) && paymentRemainingProxy > 0 && + nextDueDateProxy && totalPaid < amount) { TenthBips32 const overpaymentInterestRate{ loan->at(sfOverpaymentInterestRate)}; TenthBips32 const overpaymentFeeRate{loan->at(sfOverpaymentFee)}; - Number const overpayment = amount - - (totalParts.principalPaid + totalParts.interestPaid + - totalParts.feeToPay); + Number const overpayment = amount - totalPaid; XRPL_ASSERT( - overpayment >= 0 && isRounded(asset, overpayment, loanScale), + overpayment > 0 && isRounded(asset, overpayment, loanScale), "ripple::loanMakePayment : valid overpayment amount"); Number const fee = roundToAsset( diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index eafb28cd34..d32413437f 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -61,13 +61,11 @@ loanPeriodicPayment( /* * This formula is from the XLS-66 spec, section 3.2.4.1.1 (Regular - * Payment), though the awkwardly-named "timeFactor" is computed only once - * and used twice. + * Payment), though "raisedRate" is computed only once and used twice. */ - // TODO: Need a better name - Number const timeFactor = power(1 + periodicRate, paymentsRemaining); + Number const raisedRate = power(1 + periodicRate, paymentsRemaining); - return principalOutstanding * periodicRate * timeFactor / (timeFactor - 1); + return principalOutstanding * periodicRate * raisedRate / (raisedRate - 1); } Number diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 99bb05f9ec..e9c85af255 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -48,7 +48,7 @@ LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx) auto const normalCost = Transactor::calculateBaseFee(view, tx); auto const paymentsPerFeeIncrement = 20; - // The fee is based on the number of potential payments, unless the loan is + // The fee is based on the potential number of payments, unless the loan is // being fully paid off. auto const amount = tx[sfAmount]; auto const loanID = tx[sfLoanID]; @@ -136,17 +136,17 @@ LoanPay::preclaim(PreclaimContext const& ctx) return tecNO_ENTRY; } - auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding); - TenthBips32 const interestRate{loanSle->at(sfInterestRate)}; - auto const paymentRemaining = loanSle->at(sfPaymentRemaining); - TenthBips32 const lateInterestRate{loanSle->at(sfLateInterestRate)}; - if (loanSle->at(sfBorrower) != account) { JLOG(ctx.j.warn()) << "Loan does not belong to the account."; return tecNO_PERMISSION; } + auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding); + TenthBips32 const interestRate{loanSle->at(sfInterestRate)}; + auto const paymentRemaining = loanSle->at(sfPaymentRemaining); + TenthBips32 const lateInterestRate{loanSle->at(sfLateInterestRate)}; + if (paymentRemaining == 0 || principalOutstanding == 0) { JLOG(ctx.j.warn()) << "Loan is already paid off."; @@ -164,6 +164,7 @@ LoanPay::preclaim(PreclaimContext const& ctx) // LCOV_EXCL_STOP } auto const brokerPseudoAccount = loanBrokerSle->at(sfAccount); + auto const brokerOwner = loanBrokerSle->at(sfOwner); auto const vaultID = loanBrokerSle->at(sfVaultID); auto const vaultSle = ctx.view.read(keylet::vault(vaultID)); if (!vaultSle) @@ -175,6 +176,7 @@ LoanPay::preclaim(PreclaimContext const& ctx) // LCOV_EXCL_STOP } auto const asset = vaultSle->at(sfAsset); + auto const vaultPseudoAccount = vaultSle->at(sfAccount); if (amount.asset() != asset) { @@ -193,6 +195,18 @@ LoanPay::preclaim(PreclaimContext const& ctx) "funds (deep frozen)."; return ret; } + if (auto const ret = checkDeepFrozen(ctx.view, brokerOwner, asset)) + { + JLOG(ctx.j.warn()) + << "Loan Broker can not receive funds (deep frozen)."; + return ret; + } + if (auto const ret = checkDeepFrozen(ctx.view, vaultPseudoAccount, asset)) + { + JLOG(ctx.j.warn()) + << "Vault pseudo-account can not receive funds (deep frozen)."; + return ret; + } return tesSUCCESS; } @@ -235,12 +249,27 @@ LoanPay::doApply() } TenthBips16 managementFeeRate{brokerSle->at(sfManagementFeeRate)}; + auto const managementFeeOutstanding = [&]() { + auto const m = loanSle->at(sfTotalValueOutstanding) - + loanSle->at(sfPrincipalOutstanding) - loanSle->at(sfInterestOwed); + // It shouldn't be possible for this to result in a negative number, but + // with overpayments, who knows? + if (m < 0) + return Number{}; + return m; + }(); Expected paymentParts = loanMakePayment(asset, view, loanSle, amount, managementFeeRate, j_); if (!paymentParts) + { + XRPL_ASSERT_PARTS( + paymentParts.error(), + "ripple::LoanPay::doApply", + "payment error is an error"); return paymentParts.error(); + } // If the payment computation completed without error, the loanSle object // has been modified. @@ -256,6 +285,11 @@ LoanPay::doApply() paymentParts->interestPaid >= 0, "ripple::LoanPay::doApply", "valid interest paid"); + XRPL_ASSERT_PARTS( + // It should not be possible to pay 0 total + paymentParts->principalPaid + paymentParts->interestPaid > 0, + "ripple::LoanPay::doApply", + "valid principal paid"); XRPL_ASSERT_PARTS( paymentParts->feeToPay >= 0, "ripple::LoanPay::doApply", @@ -277,27 +311,45 @@ LoanPay::doApply() auto interestOwedProxy = loanSle->at(sfInterestOwed); - auto const [managementFee, interestPaidToVault] = [&]() { - auto const managementFee = roundToAsset( + auto const [managementFee, interestPaidForDebt, interestPaidExtra] = [&]() { + auto const interestOwed = + paymentParts->interestPaid - paymentParts->valueChange; + auto const interestPaidExtra = paymentParts->valueChange; + + auto const managementFeeOwed = std::min( + managementFeeOutstanding, + roundToAsset( + asset, + tenthBipsOfValue(interestOwed, managementFeeRate), + loanScale)); + auto const managementFeeExtra = roundToAsset( asset, - tenthBipsOfValue(paymentParts->interestPaid, managementFeeRate), + tenthBipsOfValue(interestPaidExtra, managementFeeRate), loanScale); - auto const interest = paymentParts->interestPaid - managementFee; + auto const interestForDebt = interestOwed - managementFeeOwed; + auto const interestExtra = interestPaidExtra - managementFeeExtra; auto const owed = *interestOwedProxy; - if (interest > owed) - return std::make_pair(paymentParts->interestPaid - owed, owed); - return std::make_pair(managementFee, interest); + if (interestForDebt > owed) + return std::make_tuple( + interestOwed - owed + managementFeeExtra, owed, interestExtra); + return std::make_tuple( + managementFeeOwed + managementFeeExtra, + interestForDebt, + interestExtra); }(); XRPL_ASSERT_PARTS( - managementFee >= 0 && interestPaidToVault >= 0 && - (managementFee + interestPaidToVault == + managementFee >= 0 && interestPaidForDebt >= 0 && + interestPaidExtra >= 0 && + (managementFee + interestPaidForDebt + interestPaidExtra == paymentParts->interestPaid) && isRounded(asset, managementFee, loanScale) && - isRounded(asset, interestPaidToVault, loanScale), + isRounded(asset, interestPaidForDebt, loanScale) && + isRounded(asset, interestPaidExtra, loanScale), "ripple::LoanPay::doApply", "management fee computation is valid"); - auto const totalPaidToVault = - paymentParts->principalPaid + interestPaidToVault; + auto const totalPaidToVaultForDebt = + paymentParts->principalPaid + interestPaidForDebt; + auto const totalPaidToVault = totalPaidToVaultForDebt + interestPaidExtra; auto const totalPaidToBroker = paymentParts->feeToPay + managementFee; @@ -311,33 +363,40 @@ LoanPay::doApply() auto debtTotalProxy = brokerSle->at(sfDebtTotal); // Decrease LoanBroker Debt by the amount paid, add the Loan value change - // (which might be negative). debtDecrease may be negative, increasing the - // debt - auto const debtDecrease = totalPaidToVault - paymentParts->valueChange; + // (which might be negative). totalPaidToVaultForDebt may be negative, + // increasing the debt XRPL_ASSERT_PARTS( - isRounded(asset, debtDecrease, loanScale), + isRounded(asset, totalPaidToVaultForDebt, loanScale), "ripple::LoanPay::doApply", - "debtDecrease rounding good"); + "totalPaidToVaultForDebt rounding good"); // Despite our best efforts, it's possible for rounding errors to accumulate // in the loan broker's debt total. This is because the broker may have more // that one loan with significantly different scales. - if (debtDecrease >= debtTotalProxy) + if (totalPaidToVaultForDebt >= debtTotalProxy) debtTotalProxy = 0; else - debtTotalProxy -= debtDecrease; + debtTotalProxy -= totalPaidToVaultForDebt; //------------------------------------------------------ // Vault object state changes view.update(vaultSle); + // auto const available = *vaultSle->at(sfAssetsAvailable); + // auto const total = *vaultSle->at(sfAssetsTotal); + // auto const unavailable = total - available; + vaultSle->at(sfAssetsAvailable) += totalPaidToVault; - vaultSle->at(sfAssetsTotal) += paymentParts->valueChange; + vaultSle->at(sfAssetsTotal) += interestPaidExtra; interestOwedProxy -= interestPaidToVault; XRPL_ASSERT_PARTS( *vaultSle->at(sfAssetsAvailable) <= *vaultSle->at(sfAssetsTotal), "ripple::LoanPay::doApply", "assets available must not be greater than assets outstanding"); + // auto const available = *vaultSle->at(sfAssetsAvailable); + // auto const total = *vaultSle->at(sfAssetsTotal); + // auto const unavailable = total - available; + // Move funds STAmount const paidToVault(asset, totalPaidToVault); STAmount const paidToBroker(asset, totalPaidToBroker); diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 7be4b9fe21..ee3cb507ab 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -317,7 +317,16 @@ LoanSet::doApply() // good. Note that the outstanding principal is rounded, and may not // change right away. JLOG(j_.warn()) << "Loan is unable to pay principal."; - return tecLIMIT_EXCEEDED; + return tecPRECISION_LOSS; + } + if (interestRate != 0 && + (properties.totalValueOutstanding - principalRequested) <= 0) + { + // Unless this is a zero-interst loan, there must be some interest due + // on the loan, even if it's (measurable) dust + JLOG(j_.warn()) << "Loan with " << interestRate + << "% interest has no interest due"; + return tecPRECISION_LOSS; } // Check that the other computed values are valid if (properties.interestOwedToVault < 0 || From 1efc532b211e4fa6b95ae9f66e80e56d14b8984e Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 9 Oct 2025 00:48:58 -0400 Subject: [PATCH 128/291] Implement Vito's new loan payment part rounding algorithm, and more - Implement AccountSendMulti - Document the derivations of loan components. - Add "loanPrincipalFromPeriodicPayment" helper. - Removed sfReferencePrincipal - LoanSet and LoanPay can create MPTokens as a side effect - LoanPay will send the fee to cover if the broker owner is deep frozen, and fail if both of them are deep frozen. - LoanPay will check auth for the receivers, or create holdings for the submitting account if needed. - LoanSet will fail if principal requested is not positive - Handle overpayment in a separate function - Add a test helper to check that balance changes went as expected - Fix more tests --- include/xrpl/ledger/View.h | 16 + .../xrpl/protocol/detail/ledger_entries.macro | 54 +- include/xrpl/protocol/detail/sfields.macro | 3 +- src/libxrpl/ledger/View.cpp | 378 ++++++++++++++ src/test/app/Loan_test.cpp | 317 +++++------- src/xrpld/app/misc/LendingHelpers.h | 466 ++++++++++++------ src/xrpld/app/misc/detail/LendingHelpers.cpp | 49 +- src/xrpld/app/tx/detail/InvariantCheck.cpp | 1 - src/xrpld/app/tx/detail/LoanManage.cpp | 2 +- src/xrpld/app/tx/detail/LoanPay.cpp | 134 ++--- src/xrpld/app/tx/detail/LoanSet.cpp | 129 +++-- 11 files changed, 1075 insertions(+), 474 deletions(-) diff --git a/include/xrpl/ledger/View.h b/include/xrpl/ledger/View.h index 5b47dde6d1..56c762a5a4 100644 --- a/include/xrpl/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -828,6 +828,22 @@ accountSend( beast::Journal j, WaiveTransferFee waiveFee = WaiveTransferFee::No); +using MultiplePaymentDestinations = std::vector>; +/** Like accountSend, except one account is sending multiple payments (with the + * same asset!) simultaneously + * + * Calls static accountSendMultiIOU if saAmount represents Issue. + * Calls static accountSendMultiMPT if saAmount represents MPTIssue. + */ +[[nodiscard]] TER +accountSendMulti( + ApplyView& view, + AccountID const& senderID, + Asset const& asset, + MultiplePaymentDestinations const& receivers, + beast::Journal j, + WaiveTransferFee waiveFee = WaiveTransferFee::No); + [[nodiscard]] TER issueIOU( ApplyView& view, diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index f1b93c9a8c..f4d1198e10 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -560,26 +560,54 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({ {sfStartDate, soeREQUIRED}, {sfPaymentInterval, soeREQUIRED}, {sfGracePeriod, soeREQUIRED}, - {sfPeriodicPayment, soeREQUIRED}, {sfPreviousPaymentDate, soeDEFAULT}, {sfNextPaymentDueDate, soeOPTIONAL}, - {sfPaymentRemaining, soeDEFAULT}, - // The loan object tracks three values: - // - TotalValueOutstanding: The total amount owed by the borrower to - // the lender / vault. - // - PrincipalOutstanding: The portion of the TotalValueOutstanding - // that is from the prinicpal borrowed. - // - InterestOwed: The portion of the TotalValueOutstanding that - // represents interest owed to the vault. - // There are two additional values that can be computed from these: - // - InterestOutstanding: TotalValueOutstanding - PrincipalOutstanding + // The loan object tracks these values: + // + // - PaymentRemaining: The number of payments left in the loan. When it + // reaches 0, the loan is paid off, and all other relevant values + // must also be 0. + // + // - PeriodicPayment: The fixed, unrounded amount to be paid each + // interval. Stored with as much precision as possible. + // Payment transactions must round this value *UP*. + // + // - TotalValueOutstanding: The rounded total amount owed by the + // borrower to the lender / vault. + // + // - PrincipalOutstanding: The rounded portion of the + // TotalValueOutstanding that is from the principal borrowed. + // + // - InterestOwed: The rounded portion of the TotalValueOutstanding that + // represents interest specifically owed to the vault. This may be less + // than the interest owed by the borrower, because it excludes the + // expected value of broker management fees. + // + // There are additional values that can be computed from these: + // + // - InterestOutstanding = TotalValueOutstanding - PrincipalOutstanding // The total amount of interest still pending on the loan, // independent of management fees. - // - ManagementFeeOwed: InterestOutstanding - InterestOwed + // + // - ManagementFeeOwed = InterestOutstanding - InterestOwed // The amount of the total interest that will be sent to the // broker as management fees. + // + // - TrueTotalLoanValue = PaymentRemaining * PeriodicPayment + // The unrounded true total value of the loan. + // + // - TrueTotalPrincialOutstanding can be computed using the algorithm + // in the ripple::detail::loanPrincipalFromPeriodicPayment function. + // + // - TrueTotalInterestOutstanding = TrueTotalLoanValue - + // TrueTotalPrincipalOutstanding + // The unrounded true total interest remaining. + // + // Note the the "True" values may differ significantly from the tracked + // rounded values. + {sfPaymentRemaining, soeDEFAULT}, + {sfPeriodicPayment, soeREQUIRED}, {sfPrincipalOutstanding, soeDEFAULT}, - {sfReferencePrincipal, soeDEFAULT}, {sfTotalValueOutstanding, soeDEFAULT}, {sfInterestOwed, soeDEFAULT}, // Based on the original principal borrowed, used for diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 2fca180b7b..1911c7e31d 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -241,8 +241,7 @@ TYPED_SFIELD(sfPrincipalOutstanding, NUMBER, 13) TYPED_SFIELD(sfPrincipalRequested, NUMBER, 14) TYPED_SFIELD(sfTotalValueOutstanding, NUMBER, 15) TYPED_SFIELD(sfPeriodicPayment, NUMBER, 16) -TYPED_SFIELD(sfReferencePrincipal, NUMBER, 17) -TYPED_SFIELD(sfInterestOwed, NUMBER, 18) +TYPED_SFIELD(sfInterestOwed, NUMBER, 17) // int32 TYPED_SFIELD(sfLoanScale, INT32, 1) diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index 0372e50c92..5964486bcd 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -1910,6 +1910,87 @@ rippleSendIOU( return terResult; } +// Send regardless of limits. +// --> receivers: Amount/currency/issuer to deliver to receivers. +// <-- saActual: Amount actually cost to sender. Sender pays fees. +static TER +rippleSendMultiIOU( + ApplyView& view, + AccountID const& senderID, + Asset const& asset, + MultiplePaymentDestinations const& receivers, + STAmount& actual, + beast::Journal j, + WaiveTransferFee waiveFee) +{ + auto const issuer = asset.getIssuer(); + + XRPL_ASSERT( + !isXRP(senderID), "ripple::rippleSendMultiIOU : sender is not XRP"); + + // These may diverge + STAmount takeFromSender{asset}; + actual = takeFromSender; + + // Failures return immediately. + for (auto const& r : receivers) + { + auto const& receiverID = r.first; + STAmount amount{asset, r.second}; + + /* If we aren't sending anything or if the sender is the same as the + * receiver then we don't need to do anything. + */ + if (!amount || (senderID == receiverID)) + continue; + + XRPL_ASSERT( + !isXRP(receiverID), + "ripple::rippleSendMultiIOU : receiver is not XRP"); + + if (senderID == issuer || receiverID == issuer || issuer == noAccount()) + { + // Direct send: redeeming IOUs and/or sending own IOUs. + if (auto const ter = rippleCreditIOU( + view, senderID, receiverID, amount, false, j)) + return ter; + actual += amount; + // Do not add amount to takeFromSender, because rippleCreditIOU took + // it. + + continue; + } + + // Sending 3rd party IOUs: transit. + + // Calculate the amount to transfer accounting + // for any transfer fees if the fee is not waived: + STAmount actualSend = (waiveFee == WaiveTransferFee::Yes) + ? amount + : multiply(amount, transferRate(view, issuer)); + actual += actualSend; + takeFromSender += actualSend; + + JLOG(j.debug()) << "rippleSendMultiIOU> " << to_string(senderID) + << " - > " << to_string(receiverID) + << " : deliver=" << amount.getFullText() + << " cost=" << actual.getFullText(); + + if (TER const terResult = + rippleCreditIOU(view, issuer, receiverID, amount, true, j)) + return terResult; + } + + if (senderID != issuer && takeFromSender) + { + if (TER const terResult = rippleCreditIOU( + view, senderID, issuer, takeFromSender, true, j)) + return terResult; + } + + return tesSUCCESS; +} + static TER accountSendIOU( ApplyView& view, @@ -2034,6 +2115,165 @@ accountSendIOU( return terResult; } +static TER +accountSendMultiIOU( + ApplyView& view, + AccountID const& senderID, + Asset const& asset, + MultiplePaymentDestinations const& receivers, + beast::Journal j, + WaiveTransferFee waiveFee) +{ + XRPL_ASSERT_PARTS( + receivers.size() > 1, + "ripple::accountSendMultiIOU", + "multiple recipients provided"); + + if (view.rules().enabled(fixAMMv1_1)) + { + if (asset.holds()) + { + return tecINTERNAL; + } + } + else + { + XRPL_ASSERT( + !asset.holds(), "ripple::accountSendMultiIOU : not MPT"); + } + + if (!asset.native()) + { + STAmount actual; + JLOG(j.trace()) << "accountSendMultiIOU: " << to_string(senderID) + << " sending " << receivers.size() << " IOUs"; + + return rippleSendMultiIOU( + view, senderID, asset, receivers, actual, j, waiveFee); + } + + /* XRP send which does not check reserve and can do pure adjustment. + * Note that sender or receiver may be null and this not a mistake; this + * setup could be used during pathfinding and it is carefully controlled to + * ensure that transfers are balanced. + */ + + SLE::pointer sender = senderID != beast::zero + ? view.peek(keylet::account(senderID)) + : SLE::pointer(); + + if (auto stream = j.trace()) + { + std::string sender_bal("-"); + + if (sender) + sender_bal = sender->getFieldAmount(sfBalance).getFullText(); + + stream << "accountSendMultiIOU> " << to_string(senderID) << " (" + << sender_bal << ") -> " << receivers.size() << " receivers."; + } + + // Failures return immediately. + STAmount takeFromSender{asset}; + for (auto const& r : receivers) + { + auto const& receiverID = r.first; + STAmount amount{asset, r.second}; + + takeFromSender += amount; + + if (view.rules().enabled(fixAMMv1_1)) + { + if (amount < beast::zero) + { + return tecINTERNAL; + } + } + else + { + XRPL_ASSERT( + amount >= beast::zero, + "ripple::accountSendMultiIOU : minimum amount"); + } + + /* If we aren't sending anything or if the sender is the same as the + * receiver then we don't need to do anything. + */ + if (!amount || (senderID == receiverID)) + continue; + + SLE::pointer receiver = receiverID != beast::zero + ? view.peek(keylet::account(receiverID)) + : SLE::pointer(); + + if (auto stream = j.trace()) + { + std::string receiver_bal("-"); + + if (receiver) + receiver_bal = + receiver->getFieldAmount(sfBalance).getFullText(); + + stream << "accountSendMultiIOU> " << to_string(senderID) << " -> " + << to_string(receiverID) << " (" << receiver_bal + << ") : " << amount.getFullText(); + } + + if (receiver) + { + // Increment XRP balance. + auto const rcvBal = receiver->getFieldAmount(sfBalance); + receiver->setFieldAmount(sfBalance, rcvBal + amount); + view.creditHook(xrpAccount(), receiverID, amount, -rcvBal); + + view.update(receiver); + } + + if (auto stream = j.trace()) + { + std::string receiver_bal("-"); + + if (receiver) + receiver_bal = + receiver->getFieldAmount(sfBalance).getFullText(); + + stream << "accountSendMultiIOU< " << to_string(senderID) << " -> " + << to_string(receiverID) << " (" << receiver_bal + << ") : " << amount.getFullText(); + } + } + + if (sender) + { + if (sender->getFieldAmount(sfBalance) < takeFromSender) + { + return TER{tecFAILED_PROCESSING}; + } + else + { + auto const sndBal = sender->getFieldAmount(sfBalance); + view.creditHook(senderID, xrpAccount(), takeFromSender, sndBal); + + // Decrement XRP balance. + sender->setFieldAmount(sfBalance, sndBal - takeFromSender); + view.update(sender); + } + } + + if (auto stream = j.trace()) + { + std::string sender_bal("-"); + std::string receiver_bal("-"); + + if (sender) + sender_bal = sender->getFieldAmount(sfBalance).getFullText(); + + stream << "accountSendMultiIOU< " << to_string(senderID) << " (" + << sender_bal << ") -> " << receivers.size() << " receivers."; + } + return tesSUCCESS; +} + static TER rippleCreditMPT( ApplyView& view, @@ -2162,6 +2402,102 @@ rippleSendMPT( return rippleCreditMPT(view, uSenderID, issuer, saActual, j); } +static TER +rippleSendMultiMPT( + ApplyView& view, + AccountID const& senderID, + Asset const& asset, + MultiplePaymentDestinations const& receivers, + STAmount& actual, + beast::Journal j, + WaiveTransferFee waiveFee) +{ + // Safe to get MPT since rippleSendMultiMPT is only called by + // accountSendMultiMPT + auto const issuer = asset.getIssuer(); + + auto const sle = + view.read(keylet::mptIssuance(asset.get().getMptID())); + if (!sle) + return tecOBJECT_NOT_FOUND; + + // These may diverge + STAmount takeFromSender{asset}; + actual = takeFromSender; + + for (auto const& r : receivers) + { + auto const& receiverID = r.first; + STAmount amount{asset, r.second}; + + XRPL_ASSERT( + senderID != receiverID, + "ripple::rippleSendMultiMPT : sender is not receiver"); + + XRPL_ASSERT( + amount >= beast::zero, + "ripple::rippleSendMultiMPT : minimum amount "); + + /* If we aren't sending anything or if the sender is the same as the + * receiver then we don't need to do anything. + */ + if (!amount || (senderID == receiverID)) + continue; + + if (senderID == issuer || receiverID == issuer) + { + // if sender is issuer, check that the new OutstandingAmount will + // not exceed MaximumAmount + if (senderID == issuer) + { + auto const sendAmount = amount.mpt().value(); + auto const maximumAmount = + sle->at(~sfMaximumAmount).value_or(maxMPTokenAmount); + if (sendAmount > maximumAmount - takeFromSender || + sle->getFieldU64(sfOutstandingAmount) > + maximumAmount - sendAmount - takeFromSender) + return tecPATH_DRY; + } + + // Direct send: redeeming MPTs and/or sending own MPTs. + if (auto const ter = + rippleCreditMPT(view, senderID, receiverID, amount, j)) + return ter; + actual += amount; + // Do not add amount to takeFromSender, because rippleCreditMPT took + // it + + continue; + } + + // Sending 3rd party MPTs: transit. + STAmount actualSend = (waiveFee == WaiveTransferFee::Yes) + ? amount + : multiply( + amount, + transferRate(view, amount.get().getMptID())); + actual += actualSend; + takeFromSender += actualSend; + + JLOG(j.debug()) << "rippleSendMultiMPT> " << to_string(senderID) + << " - > " << to_string(receiverID) + << " : deliver=" << amount.getFullText() + << " cost=" << actualSend.getFullText(); + + if (auto const terResult = + rippleCreditMPT(view, issuer, receiverID, amount, j)) + return terResult; + } + if (senderID != issuer && takeFromSender) + { + if (TER const terResult = + rippleCreditMPT(view, senderID, issuer, takeFromSender, j)) + return terResult; + } + + return tesSUCCESS; +} + static TER accountSendMPT( ApplyView& view, @@ -2187,6 +2523,23 @@ accountSendMPT( view, uSenderID, uReceiverID, saAmount, saActual, j, waiveFee); } +static TER +accountSendMultiMPT( + ApplyView& view, + AccountID const& senderID, + Asset const& asset, + MultiplePaymentDestinations const& receivers, + beast::Journal j, + WaiveTransferFee waiveFee) +{ + XRPL_ASSERT(asset.holds(), "ripple::accountSendMultiMPT : MPT"); + + STAmount actual; + + return rippleSendMultiMPT( + view, senderID, asset, receivers, actual, j, waiveFee); +} + TER accountSend( ApplyView& view, @@ -2208,6 +2561,31 @@ accountSend( saAmount.asset().value()); } +TER +accountSendMulti( + ApplyView& view, + AccountID const& senderID, + Asset const& asset, + MultiplePaymentDestinations const& receivers, + beast::Journal j, + WaiveTransferFee waiveFee) +{ + XRPL_ASSERT_PARTS( + receivers.size() > 1, + "ripple::accountSendMulti", + "multiple recipients provided"); + return std::visit( + [&](TIss const& issue) { + if constexpr (std::is_same_v) + return accountSendMultiIOU( + view, senderID, asset, receivers, j, waiveFee); + else + return accountSendMultiMPT( + view, senderID, asset, receivers, j, waiveFee); + }, + asset.value()); +} + static bool updateTrustLine( ApplyView& view, diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 3c1f022186..163a2e5d52 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -110,12 +110,11 @@ class Loan_test : public beast::unit_test::suite { std::uint32_t previousPaymentDate = 0; NetClock::time_point startDate = {}; - std::optional nextPaymentDate = 0; + std::uint32_t nextPaymentDate = 0; std::uint32_t paymentRemaining = 0; std::int32_t const loanScale = 0; Number totalValue = 0; Number principalOutstanding = 0; - Number referencePrincipal = 0; Number interestOwed = 0; Number periodicPayment = 0; std::uint32_t flags = 0; @@ -206,16 +205,40 @@ class Loan_test : public beast::unit_test::suite } } + void + checkPayment( + std::int32_t loanScale, + jtx::Account const& account, + jtx::PrettyAmount const& balanceBefore, + STAmount const& expectedPayment, + jtx::PrettyAmount const& adjustment) const + { + auto const borrowerScale = + std::max(loanScale, balanceBefore.number().exponent()); + + STAmount const balanceChangeAmount{ + broker.asset, + roundToAsset( + broker.asset, expectedPayment + adjustment, borrowerScale)}; + { + auto const difference = roundToScale( + env.balance(account, broker.asset) - + (balanceBefore - balanceChangeAmount), + borrowerScale); + env.test.BEAST_EXPECT( + roundToScale(difference, loanScale) >= beast::zero); + } + } + /** Checks both the loan and broker expect states against the ledger */ void operator()( std::uint32_t previousPaymentDate, - std::optional nextPaymentDate, + std::uint32_t nextPaymentDate, std::uint32_t paymentRemaining, Number const& loanScale, Number const& totalValue, Number const& principalOutstanding, - Number const& referencePrincipal, Number const& interestOwed, Number const& periodicPayment, std::uint32_t flags) const @@ -225,17 +248,18 @@ class Loan_test : public beast::unit_test::suite { env.test.BEAST_EXPECT( loan->at(sfPreviousPaymentDate) == previousPaymentDate); - env.test.BEAST_EXPECT( - loan->at(~sfNextPaymentDueDate) == nextPaymentDate); env.test.BEAST_EXPECT( loan->at(sfPaymentRemaining) == paymentRemaining); + if (paymentRemaining == 0) + env.test.BEAST_EXPECT(!loan->at(~sfNextPaymentDueDate)); + else + env.test.BEAST_EXPECT( + loan->at(sfNextPaymentDueDate) == nextPaymentDate); env.test.BEAST_EXPECT(loan->at(sfLoanScale) == loanScale); env.test.BEAST_EXPECT( loan->at(sfTotalValueOutstanding) == totalValue); env.test.BEAST_EXPECT( loan->at(sfPrincipalOutstanding) == principalOutstanding); - env.test.BEAST_EXPECT( - loan->at(sfReferencePrincipal) == referencePrincipal); env.test.BEAST_EXPECT(loan->at(sfInterestOwed) == interestOwed); env.test.BEAST_EXPECT( loan->at(sfPeriodicPayment) == periodicPayment); @@ -287,7 +311,6 @@ class Loan_test : public beast::unit_test::suite state.loanScale, state.totalValue, state.principalOutstanding, - state.referencePrincipal, state.interestOwed, state.periodicPayment, state.flags); @@ -360,12 +383,11 @@ class Loan_test : public beast::unit_test::suite LoanState state{ .previousPaymentDate = loan->at(sfPreviousPaymentDate), .startDate = tp{d{loan->at(sfStartDate)}}, - .nextPaymentDate = loan->at(~sfNextPaymentDueDate), + .nextPaymentDate = loan->at(sfNextPaymentDueDate), .paymentRemaining = loan->at(sfPaymentRemaining), .loanScale = loan->at(sfLoanScale), .totalValue = loan->at(sfTotalValueOutstanding), .principalOutstanding = loan->at(sfPrincipalOutstanding), - .referencePrincipal = loan->at(sfReferencePrincipal), .interestOwed = loan->at(sfInterestOwed), .periodicPayment = loan->at(sfPeriodicPayment), .flags = loan->at(sfFlags), @@ -373,9 +395,8 @@ class Loan_test : public beast::unit_test::suite .interestRate = TenthBips32{loan->at(sfInterestRate)}, }; BEAST_EXPECT(state.previousPaymentDate == 0); - if (BEAST_EXPECT(state.nextPaymentDate)) - BEAST_EXPECT( - tp{d{*state.nextPaymentDate}} == state.startDate + 600s); + BEAST_EXPECT( + tp{d{state.nextPaymentDate}} == state.startDate + 600s); BEAST_EXPECT(state.paymentRemaining == 12); BEAST_EXPECT( state.principalOutstanding == broker.asset(1000).value()); @@ -608,7 +629,6 @@ class Loan_test : public beast::unit_test::suite auto const loanProperties = computeLoanProperties( broker.asset, state.principalOutstanding, - state.referencePrincipal, state.interestRate, state.paymentInterval, state.paymentRemaining, @@ -621,7 +641,6 @@ class Loan_test : public beast::unit_test::suite principalRequest.exponent(), loanProperties.totalValueOutstanding, principalRequest, - principalRequest, loanProperties.interestOwedToVault, loanProperties.periodicPayment, loanFlags | 0); @@ -679,7 +698,6 @@ class Loan_test : public beast::unit_test::suite principalRequest.exponent(), loanProperties.totalValueOutstanding, principalRequest, - principalRequest, loanProperties.interestOwedToVault, loanProperties.periodicPayment, loanFlags | 0); @@ -1232,9 +1250,7 @@ class Loan_test : public beast::unit_test::suite verifyLoanStatus(state); } - BEAST_EXPECT(state.nextPaymentDate); - auto const nextDueDate = - tp{d{state.nextPaymentDate.value_or(0)}}; + auto const nextDueDate = tp{d{state.nextPaymentDate}}; // Can't default the loan yet. The grace period hasn't // expired @@ -1264,7 +1280,6 @@ class Loan_test : public beast::unit_test::suite state.paymentRemaining = 0; state.totalValue = 0; state.principalOutstanding = 0; - state.referencePrincipal = 0; state.interestOwed = 0; verifyLoanStatus(state); @@ -1279,8 +1294,9 @@ class Loan_test : public beast::unit_test::suite }; }; - auto immediatePayoff = [&](std::uint32_t baseFlag) { - return [&, baseFlag]( + auto singlePayment = [&](STAmount const& payoffAmount, + std::uint32_t baseFlag) { + return [&, payoffAmount, baseFlag]( Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { // toEndOfLife @@ -1323,6 +1339,58 @@ class Loan_test : public beast::unit_test::suite auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset); + BEAST_EXPECT(payoffAmount > state.principalOutstanding); + // Try to pay a little extra to show that it's _not_ + // taken + auto const transactionAmount = payoffAmount + broker.asset(10); + BEAST_EXPECT( + transactionAmount == + broker.asset(Number(1050000114155251, -12))); + env(pay(borrower, loanKeylet.key, transactionAmount)); + + env.close(); + + // Need to account for fees if the loan is in XRP + PrettyAmount adjustment = broker.asset(0); + if (broker.asset.raw().native()) + { + adjustment = env.current()->fees().base; + } + + state.paymentRemaining = 0; + state.principalOutstanding = 0; + state.totalValue = 0; + state.interestOwed = 0; + state.previousPaymentDate = state.nextPaymentDate; + verifyLoanStatus(state); + + verifyLoanStatus.checkPayment( + state.loanScale, + borrower, + borrowerBalanceBeforePayment, + payoffAmount, + adjustment); + + // Can't impair or default a paid off loan + env(manage(lender, loanKeylet.key, tfLoanImpair), + ter(tecNO_PERMISSION)); + env(manage(lender, loanKeylet.key, tfLoanDefault), + ter(tecNO_PERMISSION)); + }; + }; + + auto immediatePayoff = [&](std::uint32_t baseFlag) { + return [&, baseFlag]( + Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus) { + // toEndOfLife + // + auto const state = + getCurrentState(env, broker, loanKeylet, verifyLoanStatus); + env.close(state.startDate + 20s); + auto const loanAge = (env.now() - state.startDate).count(); + BEAST_EXPECT(loanAge == 30); + // Full payoff amount will consist of // 1. principal outstanding (1000) // 2. accrued interest (at 12%) @@ -1350,65 +1418,21 @@ class Loan_test : public beast::unit_test::suite broker.asset, state.principalOutstanding * Number(36, -3)}; BEAST_EXPECT(prepaymentPenalty == broker.asset(36)); STAmount const closePaymentFee = broker.asset(4); - auto const payoffAmount = principalOutstanding + - accruedInterest + prepaymentPenalty + closePaymentFee; + auto const payoffAmount = roundToScale( + principalOutstanding + accruedInterest + prepaymentPenalty + + closePaymentFee, + state.loanScale); BEAST_EXPECT( payoffAmount == broker.asset(Number(1040000114155251, -12))); - BEAST_EXPECT(payoffAmount > state.principalOutstanding); - // The terms of this loan actually make the early payoff more - // expensive than just making payments + + // The terms of this loan actually make the early payoff + // more expensive than just making payments BEAST_EXPECT( payoffAmount > state.paymentRemaining * (state.periodicPayment + broker.asset(2).value())); - // Try to pay a little extra to show that it's _not_ - // taken - auto const transactionAmount = payoffAmount + broker.asset(10); - BEAST_EXPECT( - transactionAmount == - broker.asset(Number(1050000114155251, -12))); - env(pay(borrower, loanKeylet.key, transactionAmount)); - env.close(); - - // Need to account for fees if the loan is in XRP - PrettyAmount adjustment = broker.asset(0); - if (broker.asset.raw().native()) - { - adjustment = env.current()->fees().base; - } - - state.paymentRemaining = 0; - state.principalOutstanding = 0; - state.referencePrincipal = 0; - state.totalValue = 0; - state.interestOwed = 0; - if (BEAST_EXPECT(state.nextPaymentDate)) - state.previousPaymentDate = *state.nextPaymentDate; - state.nextPaymentDate.reset(); - verifyLoanStatus(state); - - STAmount const balanceChangeAmount{ - broker.asset, - roundToAsset(broker.asset, payoffAmount, state.loanScale)}; - { - auto const difference = - roundToScale( - env.balance(borrower, broker.asset), - state.loanScale) - - (borrowerBalanceBeforePayment - balanceChangeAmount - - adjustment); - BEAST_EXPECT(difference == beast::zero); - BEAST_EXPECT( - roundToScale(difference, state.loanScale) == - beast::zero); - } - - // Can't impair or default a paid off loan - env(manage(lender, loanKeylet.key, tfLoanImpair), - ter(tecNO_PERMISSION)); - env(manage(lender, loanKeylet.key, tfLoanDefault), - ter(tecNO_PERMISSION)); + singlePayment(payoffAmount, baseFlag); }; }; @@ -1418,43 +1442,9 @@ class Loan_test : public beast::unit_test::suite VerifyLoanStatus const& verifyLoanStatus) { // toEndOfLife // - auto state = + + auto const state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus); - BEAST_EXPECT(state.flags == baseFlag); - env.close(state.startDate + 20s); - auto const loanAge = (env.now() - state.startDate).count(); - BEAST_EXPECT(loanAge == 30); - - verifyLoanStatus(state); - - // Send some bogus pay transactions - env(pay(borrower, - keylet::loan(uint256(0)).key, - broker.asset(10)), - ter(temINVALID)); - env(pay(borrower, loanKeylet.key, broker.asset(-100)), - ter(temBAD_AMOUNT)); - env(pay(borrower, broker.brokerID, broker.asset(100)), - ter(tecNO_ENTRY)); - env(pay(evan, loanKeylet.key, broker.asset(500)), - ter(tecNO_PERMISSION)); - - { - auto const otherAsset = - broker.asset.raw() == assets[0].raw() ? assets[1] - : assets[0]; - env(pay(borrower, loanKeylet.key, otherAsset(100)), - ter(tecWRONG_ASSET)); - } - - // Amount doesn't cover a single payment - env(pay(borrower, loanKeylet.key, STAmount{broker.asset, 1}), - ter(tecINSUFFICIENT_PAYMENT)); - - // Get the balance after these failed transactions take - // fees - auto const borrowerBalanceBeforePayment = - env.balance(borrower, broker.asset); // Make all the payments in one transaction // service fee is 2 @@ -1467,51 +1457,7 @@ class Loan_test : public beast::unit_test::suite broker.asset(Number(1024014840139457, -12))); BEAST_EXPECT(payoffAmount > state.principalOutstanding); - // Try to pay a little extra to show that it's _not_ - // taken - auto const transactionAmount = payoffAmount + broker.asset(10); - BEAST_EXPECT( - transactionAmount == - broker.asset(Number(1034014840139457, -12))); - env(pay(borrower, loanKeylet.key, transactionAmount)); - - env.close(); - - // Need to account for fees if the loan is in XRP - PrettyAmount adjustment = broker.asset(0); - if (broker.asset.raw().native()) - { - adjustment = env.current()->fees().base; - } - - state.paymentRemaining = 0; - state.principalOutstanding = 0; - state.referencePrincipal = 0; - state.totalValue = 0; - state.interestOwed = 0; - if (BEAST_EXPECT(state.nextPaymentDate)) - state.previousPaymentDate = *state.nextPaymentDate + - state.paymentInterval * - (startingPayments - 1); // 9280-2680=6600 - state.nextPaymentDate.reset(); - verifyLoanStatus(state); - - STAmount const balanceChangeAmount{ - broker.asset, - roundToAsset(broker.asset, payoffAmount, state.loanScale)}; - { - auto const difference = - env.balance(borrower, broker.asset) - - (borrowerBalanceBeforePayment - balanceChangeAmount - - adjustment); - BEAST_EXPECT(difference == beast::zero); - } - - // Can't impair or default a paid off loan - env(manage(lender, loanKeylet.key, tfLoanImpair), - ter(tecNO_PERMISSION)); - env(manage(lender, loanKeylet.key, tfLoanDefault), - ter(tecNO_PERMISSION)); + singlePayment(payoffAmount, baseFlag); }; }; @@ -1690,8 +1636,11 @@ class Loan_test : public beast::unit_test::suite // may drift as payments are made BEAST_EXPECT( roundedPeriodicPayment == - broker.asset( - Number(8333457001162141, -14), Number::upward)); + roundToScale( + broker.asset( + Number(8333457001162141, -14), Number::upward), + state.loanScale, + Number::upward)); // 83334570.01162141 // Include the service fee STAmount const totalDue = roundToScale( @@ -1729,7 +1678,6 @@ class Loan_test : public beast::unit_test::suite state.loanScale, state.totalValue, state.principalOutstanding, - state.referencePrincipal, state.periodicPayment, periodicRate, state.paymentRemaining); @@ -1776,10 +1724,6 @@ class Loan_test : public beast::unit_test::suite Number::upward), state.loanScale, Number::upward)); - BEAST_EXPECT( - !paymentComponents.final || - paymentComponents.rawPrincipal == - state.referencePrincipal); BEAST_EXPECT( paymentComponents.roundedPrincipal >= Number(0) && paymentComponents.roundedPrincipal <= @@ -1795,9 +1739,13 @@ class Loan_test : public beast::unit_test::suite state.periodicPayment); BEAST_EXPECT( paymentComponents.final || - paymentComponents.roundedPrincipal + - paymentComponents.roundedInterest == - roundedPeriodicPayment); + roundedPeriodicPayment >= + (paymentComponents.roundedPrincipal + + paymentComponents.roundedInterest) && + roundedPeriodicPayment - + (paymentComponents.roundedPrincipal + + paymentComponents.roundedInterest) < + 2); auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset); @@ -1821,35 +1769,25 @@ class Loan_test : public beast::unit_test::suite } // Check the result - auto const borrowerBalance = - env.balance(borrower, broker.asset); - auto const expectedBalance = borrowerBalanceBeforePayment - - totalDueAmount - adjustment; - BEAST_EXPECT( - borrowerBalance == expectedBalance || - (!broker.asset.raw().native() && - broker.asset.raw().holds() && - ((borrowerBalance - expectedBalance) / - expectedBalance < - Number(1, -4)))); + verifyLoanStatus.checkPayment( + state.loanScale, + borrower, + borrowerBalanceBeforePayment, + totalDueAmount, + adjustment); --state.paymentRemaining; - if (BEAST_EXPECT(state.nextPaymentDate)) + state.previousPaymentDate = state.nextPaymentDate; + if (paymentComponents.final) { - state.previousPaymentDate = *state.nextPaymentDate; - if (paymentComponents.final) - { - state.nextPaymentDate.reset(); - state.paymentRemaining = 0; - } - else - { - *state.nextPaymentDate += state.paymentInterval; - } + state.paymentRemaining = 0; + } + else + { + state.nextPaymentDate += state.paymentInterval; } state.principalOutstanding -= paymentComponents.roundedPrincipal; - state.referencePrincipal -= paymentComponents.rawPrincipal; state.totalValue -= paymentComponents.roundedPrincipal + paymentComponents.roundedInterest; state.interestOwed -= valueMinusFee( @@ -2104,7 +2042,6 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT(loan[sfPaymentRemaining] == 1); BEAST_EXPECT(!loan.isMember(sfPreviousPaymentDate)); BEAST_EXPECT(loan[sfPrincipalOutstanding] == "1000000000"); - BEAST_EXPECT(loan[sfReferencePrincipal] == "1000000000"); BEAST_EXPECT(loan[sfTotalValueOutstanding] == "1000000000"); BEAST_EXPECT(loan[sfLoanScale] == -6); BEAST_EXPECT( diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 69b77bf470..b9cd00de83 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -105,6 +105,12 @@ loanPeriodicPayment( std::uint32_t paymentInterval, std::uint32_t paymentsRemaining); +Number +loanPrincipalFromPeriodicPayment( + Number const& periodicPayment, + Number const& periodicRate, + std::uint32_t paymentsRemaining); + Number loanLatePaymentInterest( Number const& principalOutstanding, @@ -134,7 +140,6 @@ computePaymentComponents( std::int32_t scale, Number const& totalValueOutstanding, Number const& principalOutstanding, - Number const& referencePrincipal, Number const& periodicPayment, Number const& periodicRate, std::uint32_t paymentRemaining) @@ -154,27 +159,27 @@ computePaymentComponents( totalValueOutstanding <= roundedPeriodicPayment) { // If there's only one payment left, we need to pay off each of the loan - // parts. - // rawInterest could be < 0 because we're computing it with the rounded - // value outstanding, but for the last payment, we also don't care. - Number rawInterest = totalValueOutstanding - referencePrincipal; - Number roundedInterest = totalValueOutstanding - principalOutstanding; - - // This is only expected to be true on the last payment - XRPL_ASSERT_PARTS( - rawInterest + referencePrincipal == - roundedInterest + principalOutstanding, - "ripple::detail::computePaymentComponents", - "last payment is complete"); + // parts. It's probably impossible for the subtraction to result in a + // negative value, but don't leave anything to chance. + Number interest = + std::max(Number{}, totalValueOutstanding - principalOutstanding); + // Pay everything off return { - .rawInterest = std::max(Number{}, rawInterest), - .rawPrincipal = referencePrincipal, - .roundedInterest = roundedInterest, + .rawInterest = interest, + .rawPrincipal = principalOutstanding, + .roundedInterest = interest, .roundedPrincipal = principalOutstanding, - .roundedPayment = roundedInterest + principalOutstanding, + .roundedPayment = interest + principalOutstanding, .final = true}; } + + Number const rawValueOutstanding = periodicPayment * paymentRemaining; + Number const rawPrincipalOutstanding = loanPrincipalFromPeriodicPayment( + periodicPayment, periodicRate, paymentRemaining); + Number const rawInterestOutstanding = + rawValueOutstanding - rawInterestOutstanding; + /* * From the spec, once the periodicPayment is computed: * @@ -182,34 +187,63 @@ computePaymentComponents( * interest = principalOutstanding * periodicRate * principal = periodicPayment - interest */ - Number const rawInterest = referencePrincipal * periodicRate; + Number const rawInterest = rawPrincipalOutstanding * periodicRate; Number const rawPrincipal = periodicPayment - rawInterest; XRPL_ASSERT_PARTS( rawInterest >= 0, "ripple::detail::computePaymentComponents", "valid raw interest"); XRPL_ASSERT_PARTS( - rawPrincipal >= 0 && rawPrincipal <= referencePrincipal, + rawPrincipal >= 0 && rawPrincipal <= rawPrincipalOutstanding, "ripple::detail::computePaymentComponents", "valid raw principal"); + /* + Critical Calculation: Balancing Principal and Interest Outstanding + + This calculation maintains a delicate balance between keeping + principal outstanding and interest outstanding as close as possible to + reference values. However, we cannot perfectly match the reference + values due to rounding issues. + + Key considerations: + 1. Since the periodic payment is rounded up, we have excess funds + that can be used to pay down the loan faster than the reference + calculation. + + 2. We must ensure that loan repayment is not too fast, otherwise we + will end up with negative principal outstanding or negative + interest outstanding. + + 3. We cannot allow the borrower to repay interest ahead of schedule. + If the borrower makes an overpayment, the interest portion could + go negative, requiring complex recalculation to refund the borrower by + reflecting the overpayment in the principal portion of the loan. + */ + Number const roundedPrincipal = [&]() { - // Round the raw principal after payment - auto const roundedPrincipalTarget = - roundToAsset(asset, referencePrincipal - rawPrincipal, scale); - // Determine the total value after payment - auto const totalValueTarget = - totalValueOutstanding - roundedPeriodicPayment; - // We want to get the principal down to the smaller of the two targets - auto const principalTarget = - std::min(roundedPrincipalTarget, totalValueTarget); - // What will get us to our target? - auto const p = principalOutstanding - principalTarget; + auto const p = roundToAsset( + asset, + // Compute the delta that will get the tracked principalOutstanding + // amount as close to the raw principal amount after the payment as + // possible. + principalOutstanding - (rawPrincipalOutstanding - rawPrincipal), + scale, + Number::downward); XRPL_ASSERT_PARTS( - p >= 0 && p <= roundedPeriodicPayment, + p > 0, + "rippled::detail::computePaymentComponents", + "principal part positive"); + XRPL_ASSERT_PARTS( + p <= principalOutstanding, + "rippled::detail::computePaymentComponents", + "principal part not larger than outstanding principal"); + XRPL_ASSERT_PARTS( + p <= roundedPeriodicPayment, "rippled::detail::computePaymentComponents", "principal part not larger than total payment"); + // Make sure nothing goes negative if (p > roundedPeriodicPayment || p > principalOutstanding) return std::min(roundedPeriodicPayment, principalOutstanding); @@ -223,7 +257,28 @@ computePaymentComponents( // Zero interest means ZERO interest if (periodicRate == 0) return Number{}; - auto i = roundedPeriodicPayment - roundedPrincipal; + + // Compute the rounded interest outstanding + auto const interestOutstanding = + totalValueOutstanding - principalOutstanding; + // Compute the delta that will simply treat the rest of the rounded + // fixed payment amount as interest. + auto const iDiff = roundedPeriodicPayment - roundedPrincipal; + + // Compute the delta that will get the untracked interestOutstanding + // amount as close as possible to the raw interest amount after the + // payment as possible. + auto const iSync = interestOutstanding - + (roundToAsset(asset, rawInterestOutstanding, scale) - + roundToAsset(asset, rawInterest, scale)); + XRPL_ASSERT_PARTS( + isRounded(asset, iSync, scale), + "ripple::detail::computePaymentComponents", + "iSync is rounded"); + + // Use the smaller of the two to ensure we don't overpay interest. + auto const i = std::min({iSync, iDiff, interestOutstanding}); + // No negative interest! if (i < 0) return Number{}; @@ -240,6 +295,10 @@ computePaymentComponents( isRounded(asset, roundedPrincipal, scale), "ripple::detail::computePaymentComponents", "valid rounded principal"); + XRPL_ASSERT_PARTS( + roundedPrincipal + roundedInterest <= roundedPeriodicPayment, + "ripple::detail::computePaymentComponents", + "payment parts fit within payment limit"); return { .rawInterest = rawInterest, @@ -263,16 +322,15 @@ struct PaymentComponentsPlus : public PaymentComponents } }; -template +template LoanPaymentParts doPayment( PaymentComponentsPlus const& payment, NumberProxy& totalValueOutstandingProxy, NumberProxy& principalOutstandingProxy, - NumberProxy& referencePrincipalProxy, - Int32Proxy& paymentRemainingProxy, - Int32Proxy& prevPaymentDateProxy, - Int32OptionalProxy& nextDueDateProxy, + UInt32Proxy& paymentRemainingProxy, + UInt32Proxy& prevPaymentDateProxy, + UInt32OptionalProxy& nextDueDateProxy, std::uint32_t paymentInterval) { XRPL_ASSERT_PARTS( @@ -285,10 +343,6 @@ doPayment( { if (payment.final) { - XRPL_ASSERT_PARTS( - referencePrincipalProxy == payment.rawPrincipal, - "ripple::detail::doPayment", - "Full reference principal payment"); XRPL_ASSERT_PARTS( principalOutstandingProxy == payment.roundedPrincipal, "ripple::detail::doPayment", @@ -307,10 +361,6 @@ doPayment( } else { - XRPL_ASSERT_PARTS( - referencePrincipalProxy > payment.rawPrincipal, - "ripple::detail::doPayment", - "Full reference principal payment"); XRPL_ASSERT_PARTS( principalOutstandingProxy > payment.roundedPrincipal, "ripple::detail::doPayment", @@ -323,16 +373,23 @@ doPayment( paymentRemainingProxy -= 1; prevPaymentDateProxy = *nextDueDateProxy; - // STObject::OptionalField does not define operator+=, and I don't - // want to add one right now. + // STObject::OptionalField does not define operator+=, so do it the + // old-fashioned way. nextDueDateProxy = *nextDueDateProxy + paymentInterval; } } - referencePrincipalProxy -= payment.rawPrincipal; principalOutstandingProxy -= payment.roundedPrincipal; totalValueOutstandingProxy -= totalValueDelta; + XRPL_ASSERT_PARTS( + // Use an explicit cast because the template parameter can be + // ValueProxy or Number + static_cast(principalOutstandingProxy) <= + static_cast(totalValueOutstandingProxy), + "ripple::detail::doPayment", + "principal does not exceed total"); + return LoanPaymentParts{ .principalPaid = payment.roundedPrincipal, .interestPaid = payment.roundedInterest, @@ -340,6 +397,127 @@ doPayment( .feeToPay = payment.fee}; } +template < + AssetType A, + class NumberProxy, + class UInt32Proxy, + class UInt32OptionalProxy> +Expected +doOverpayment( + A const& asset, + ApplyView& view, + PaymentComponentsPlus const& overpaymentComponents, + NumberProxy& totalValueOutstandingProxy, + NumberProxy& principalOutstandingProxy, + NumberProxy& periodicPaymentProxy, + TenthBips32 const interestRate, + std::uint32_t const paymentInterval, + UInt32Proxy& paymentRemainingProxy, + UInt32Proxy& prevPaymentDateProxy, + UInt32OptionalProxy& nextDueDateProxy, + TenthBips16 managementFeeRate, + beast::Journal j) +{ + Number const totalInterestOutstandingBefore = + totalValueOutstandingProxy - principalOutstandingProxy; + + // Compute what the properties would be if the loan was new in its current + // state. They are not likely to match the original properties. We're + // interested in the error. + auto const oldLoanProperties = computeLoanProperties( + asset, + principalOutstandingProxy, + interestRate, + paymentInterval, + paymentRemainingProxy, + managementFeeRate); + + auto const accumulatedError = + oldLoanProperties.totalValueOutstanding - totalValueOutstandingProxy; + + { + // Use temp variables to do the payment, so they can be thrown away if + // they don't work + Number totalValueOutstanding = totalValueOutstandingProxy; + Number principalOutstanding = principalOutstandingProxy; + std::uint32_t paymentRemaining = paymentRemainingProxy; + std::uint32_t prevPaymentDate = prevPaymentDateProxy; + std::optional nextDueDate = nextDueDateProxy; + + auto const paymentParts = detail::doPayment( + overpaymentComponents, + totalValueOutstanding, + principalOutstanding, + paymentRemaining, + prevPaymentDate, + nextDueDate, + paymentInterval); + + auto newLoanProperties = computeLoanProperties( + asset, + principalOutstanding, + interestRate, + paymentInterval, + paymentRemaining, + managementFeeRate); + + newLoanProperties.totalValueOutstanding += accumulatedError; + + if (newLoanProperties.firstPaymentPrincipal <= 0 && + principalOutstanding > 0) + { + // The overpayment has caused the loan to be in a state + // where no further principal can be paid. + JLOG(j.warn()) + << "Loan overpayment would cause loan to be stuck. " + "Rejecting overpayment, but normal payments are unaffected."; + return Unexpected(tesSUCCESS); + } + // Check that the other computed values are valid + if (newLoanProperties.interestOwedToVault < 0 || + newLoanProperties.totalValueOutstanding <= 0 || + newLoanProperties.periodicPayment <= 0) + { + // LCOV_EXCL_START + JLOG(j.warn()) << "Computed loan properties are invalid. Does " + "not compute. TotalValueOutstanding: " + << newLoanProperties.totalValueOutstanding + << ", PeriodicPayment: " + << newLoanProperties.periodicPayment + << ", InterestOwedToVault: " + << newLoanProperties.interestOwedToVault; + return Unexpected(tesSUCCESS); + // LCOV_EXCL_STOP + } + + totalValueOutstandingProxy = + newLoanProperties.totalValueOutstanding + accumulatedError; + principalOutstandingProxy = principalOutstanding; + periodicPaymentProxy = newLoanProperties.periodicPayment; + + XRPL_ASSERT_PARTS( + paymentRemainingProxy == paymentRemaining, + "ripple::detail::doOverpayment", + "paymentRemaining is unchanged"); + paymentRemainingProxy = paymentRemaining; + XRPL_ASSERT_PARTS( + prevPaymentDateProxy == prevPaymentDate, + "ripple::detail::doOverpayment", + "prevPaymentDate is unchanged"); + prevPaymentDateProxy = prevPaymentDate; + XRPL_ASSERT_PARTS( + nextDueDateProxy == nextDueDate, + "ripple::detail::doOverpayment", + "nextDueDate is unchanged"); + nextDueDateProxy = nextDueDate; + + auto const totalInterestOutstandingAfter = + totalValueOutstanding - principalOutstanding; + + return paymentParts; + } +} + /* Handle possible late payments. * * If this function processed a late payment, the return value will be @@ -425,7 +603,7 @@ handleFullPayment( A const& asset, ApplyView& view, Number const& principalOutstanding, - Number const& referencePrincipal, + Number const& periodicPayment, std::uint32_t paymentRemaining, std::uint32_t prevPaymentDate, std::uint32_t const startDate, @@ -442,17 +620,20 @@ handleFullPayment( // If this is the last payment, it has to be a regular payment return Unexpected(tesSUCCESS); + Number const rawValueOutstanding = periodicPayment * paymentRemaining; + Number const rawPrincipalOutstanding = loanPrincipalFromPeriodicPayment( + periodicPayment, periodicRate, paymentRemaining); + auto const totalInterest = calculateFullPaymentInterest( asset, - referencePrincipal, + rawPrincipalOutstanding, periodicRate, view.parentCloseTime(), paymentInterval, prevPaymentDate, startDate, closeInterestRate, - loanScale, - closePaymentFee); + loanScale); auto const closeFullPayment = principalOutstanding + totalInterest + closePaymentFee; @@ -467,11 +648,11 @@ handleFullPayment( PaymentComponentsPlus const result{ PaymentComponents{ .rawInterest = - principalOutstanding + totalInterest - referencePrincipal, - .rawPrincipal = referencePrincipal, + principalOutstanding + totalInterest - rawPrincipalOutstanding, + .rawPrincipal = rawPrincipalOutstanding, .roundedInterest = totalInterest, .roundedPrincipal = principalOutstanding, - .roundedPayment = closeFullPayment, + .roundedPayment = principalOutstanding + totalInterest, .final = true}, // A full payment only pays the single close payment fee closePaymentFee, @@ -511,7 +692,6 @@ LoanProperties computeLoanProperties( A const& asset, Number const& principalOutstanding, - Number const& referencePrincipal, TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t paymentsRemaining, @@ -520,7 +700,7 @@ computeLoanProperties( auto const periodicRate = loanPeriodicRate(interestRate, paymentInterval); XRPL_ASSERT( interestRate == 0 || periodicRate > 0, - "ripple::loanMakePayment : valid rate"); + "ripple::computeLoanProperties : valid rate"); auto const periodicPayment = detail::loanPeriodicPayment( principalOutstanding, periodicRate, paymentsRemaining); @@ -539,7 +719,8 @@ computeLoanProperties( periodicPayment * paymentsRemaining}; }(); // Base the loan scale on the total value, since that's going to be the - // biggest number involved + // biggest number involved (barring unusual parameters for late, full, or + // over payments) auto const loanScale = totalValueOutstanding.exponent(); auto const firstPaymentPrincipal = [&]() { @@ -550,20 +731,13 @@ computeLoanProperties( loanScale, totalValueOutstanding, principalOutstanding, - referencePrincipal, periodicPayment, periodicRate, paymentsRemaining); - // We only care about the unrounded principal part. It needs to be large - // enough that it will affect the reference principal. - auto const remaining = - referencePrincipal - paymentComponents.rawPrincipal; - if (remaining == referencePrincipal) - // No change, so the first payment effectively pays no principal. - // Whether that's a problem is left to the caller. - return Number{0}; - return paymentComponents.rawPrincipal; + // The rounded principal part needs to be large enough to affect the + // principal. What to do if not is left to the caller + return paymentComponents.roundedPrincipal; }(); auto const interestOwedToVault = valueMinusFee( @@ -588,22 +762,21 @@ template Number calculateFullPaymentInterest( A const& asset, - Number const& referencePrincipal, + Number const& rawPrincipalOutstanding, Number const& periodicRate, NetClock::time_point parentCloseTime, std::uint32_t paymentInterval, std::uint32_t prevPaymentDate, std::uint32_t startDate, TenthBips32 closeInterestRate, - std::int32_t loanScale, - Number const& closePaymentFee) + std::int32_t loanScale) { // If there is more than one payment remaining, see if enough was // paid for a full payment auto const accruedInterest = roundToAsset( asset, detail::loanAccruedInterest( - referencePrincipal, + rawPrincipalOutstanding, periodicRate, parentCloseTime, startDate, @@ -613,9 +786,10 @@ calculateFullPaymentInterest( XRPL_ASSERT( accruedInterest >= 0, "ripple::detail::handleFullPayment : valid accrued interest"); + auto const prepaymentPenalty = roundToAsset( asset, - tenthBipsOfValue(referencePrincipal, closeInterestRate), + tenthBipsOfValue(rawPrincipalOutstanding, closeInterestRate), loanScale); XRPL_ASSERT( prepaymentPenalty >= 0, @@ -624,6 +798,36 @@ calculateFullPaymentInterest( return accruedInterest + prepaymentPenalty; } +template +Number +calculateFullPaymentInterest( + A const& asset, + Number const& periodicPayment, + Number const& periodicRate, + std::uint32_t paymentRemaining, + NetClock::time_point parentCloseTime, + std::uint32_t paymentInterval, + std::uint32_t prevPaymentDate, + std::uint32_t startDate, + TenthBips32 closeInterestRate, + std::int32_t loanScale) +{ + Number const rawPrincipalOutstanding = + detail::loanPrincipalFromPeriodicPayment( + periodicPayment, periodicRate, paymentRemaining); + + return calculateFullPaymentInterest( + asset, + rawPrincipalOutstanding, + periodicRate, + parentCloseTime, + paymentInterval, + prevPaymentDate, + startDate, + closeInterestRate, + loanScale); +} + #if LOANCOMPLETE template Number @@ -843,7 +1047,6 @@ loanMakePayment( std::int32_t const loanScale = loan->at(sfLoanScale); auto totalValueOutstandingProxy = loan->at(sfTotalValueOutstanding); - auto referencePrincipalProxy = loan->at(sfReferencePrincipal); TenthBips32 const interestRate{loan->at(sfInterestRate)}; TenthBips32 const lateInterestRate{loan->at(sfLateInterestRate)}; @@ -879,7 +1082,6 @@ loanMakePayment( loanScale, totalValueOutstandingProxy, principalOutstandingProxy, - referencePrincipalProxy, periodicPayment, periodicRate, paymentRemainingProxy), @@ -903,7 +1105,6 @@ loanMakePayment( *latePaymentComponents, totalValueOutstandingProxy, principalOutstandingProxy, - referencePrincipalProxy, paymentRemainingProxy, prevPaymentDateProxy, nextDueDateProxy, @@ -911,8 +1112,8 @@ loanMakePayment( } else if (latePaymentComponents.error()) // error() will be the TER returned if a payment is not made. It will - // only evaluate to true if it's an error. Otherwise, tesSUCCESS means - // nothing was done, so continue. + // only evaluate to true if it's unsuccessful. Otherwise, tesSUCCESS + // means nothing was done, so continue. return Unexpected(latePaymentComponents.error()); // ------------------------------------------------------------- @@ -924,7 +1125,7 @@ loanMakePayment( asset, view, principalOutstandingProxy, - referencePrincipalProxy, + periodicPayment, paymentRemainingProxy, prevPaymentDateProxy, startDate, @@ -940,15 +1141,14 @@ loanMakePayment( *fullPaymentComponents, totalValueOutstandingProxy, principalOutstandingProxy, - referencePrincipalProxy, paymentRemainingProxy, prevPaymentDateProxy, nextDueDateProxy, paymentInterval); else if (fullPaymentComponents.error()) // error() will be the TER returned if a payment is not made. It will - // only evaluate to true if it's an error. Otherwise, tesSUCCESS means - // nothing was done, so continue. + // only evaluate to true if it's unsuccessful. Otherwise, tesSUCCESS + // means nothing was done, so continue. return Unexpected(fullPaymentComponents.error()); // ------------------------------------------------------------- @@ -973,7 +1173,6 @@ loanMakePayment( periodic, totalValueOutstandingProxy, principalOutstandingProxy, - referencePrincipalProxy, paymentRemainingProxy, prevPaymentDateProxy, nextDueDateProxy, @@ -990,7 +1189,6 @@ loanMakePayment( loanScale, totalValueOutstandingProxy, principalOutstandingProxy, - referencePrincipalProxy, periodicPayment, periodicRate, paymentRemainingProxy), @@ -1015,7 +1213,6 @@ loanMakePayment( nextPayment, totalValueOutstandingProxy, principalOutstandingProxy, - referencePrincipalProxy, paymentRemainingProxy, prevPaymentDateProxy, nextDueDateProxy, @@ -1062,6 +1259,8 @@ loanMakePayment( Number const payment = overpayment - fee; + // TODO: Is the overpaymentInterestRate an APR or flat? + Number const interest = tenthBipsOfValue(payment, overpaymentInterestRate); Number const roundedInterest = roundToAsset(asset, interest, loanScale); @@ -1074,86 +1273,35 @@ loanMakePayment( .roundedPrincipal = payment - roundedInterest, .roundedPayment = payment, .extra = true}, - fee}; + fee, + roundedInterest}; // Don't process an overpayment if the whole amount (or more!) // gets eaten by fees and interest. if (overpaymentComponents.rawPrincipal > 0 && overpaymentComponents.roundedPrincipal > 0) { - Number const totalInterestOutstandingBefore = - totalValueOutstandingProxy - principalOutstandingProxy; - - auto const oldLoanProperties = computeLoanProperties( - asset, - principalOutstandingProxy, - referencePrincipalProxy, - interestRate, - paymentInterval, - paymentRemainingProxy, - managementFeeRate); - - auto const accumulatedError = - oldLoanProperties.totalValueOutstanding - - totalValueOutstandingProxy; - - totalParts += detail::doPayment( - overpaymentComponents, - totalValueOutstandingProxy, - principalOutstandingProxy, - referencePrincipalProxy, - paymentRemainingProxy, - prevPaymentDateProxy, - nextDueDateProxy, - paymentInterval); - - auto const newLoanProperties = computeLoanProperties( - asset, - principalOutstandingProxy, - referencePrincipalProxy, - interestRate, - paymentInterval, - paymentRemainingProxy, - managementFeeRate); - - if (newLoanProperties.firstPaymentPrincipal <= 0 && - *principalOutstandingProxy > 0) - { - // The overpayment has caused the loan to be in a state where - // no further principal can be paid. - JLOG(j.warn()) - << "Loan overpayment would cause loan to be stuck. " - "Rejecting overpayment."; - return Unexpected(tecLIMIT_EXCEEDED); - } - // Check that the other computed values are valid - if (newLoanProperties.interestOwedToVault < 0 || - newLoanProperties.totalValueOutstanding <= 0 || - newLoanProperties.periodicPayment <= 0) - { - // LCOV_EXCL_START - JLOG(j.warn()) << "Computed loan properties are invalid. Does " - "not compute. TotalValueOutstanding: " - << newLoanProperties.totalValueOutstanding - << ", PeriodicPayment: " - << newLoanProperties.periodicPayment - << ", InterestOwedToVault: " - << newLoanProperties.interestOwedToVault; - return Unexpected(tecINTERNAL); - // LCOV_EXCL_STOP - } - - totalValueOutstandingProxy = - newLoanProperties.totalValueOutstanding; - loan->at(sfPeriodicPayment) = newLoanProperties.periodicPayment; - loan->at(sfInterestOwed) = newLoanProperties.interestOwedToVault; - - auto const totalInterestOutstandingAfter = - totalValueOutstandingProxy - principalOutstandingProxy; - - totalParts.valueChange += totalInterestOutstandingBefore - - totalInterestOutstandingAfter + - overpaymentComponents.roundedInterest + accumulatedError; + auto periodicPaymentProxy = loan->at(sfPeriodicPayment); + if (auto const overResult = detail::doOverpayment( + asset, + view, + overpaymentComponents, + totalValueOutstandingProxy, + principalOutstandingProxy, + periodicPaymentProxy, + interestRate, + paymentInterval, + paymentRemainingProxy, + prevPaymentDateProxy, + nextDueDateProxy, + managementFeeRate, + j)) + totalParts += *overResult; + else if (overResult.error()) + // error() will be the TER returned if a payment is not made. It + // will only evaluate to true if it's unsuccessful. Otherwise, + // tesSUCCESS means nothing was done, so continue. + return Unexpected(overResult.error()); } } diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index d32413437f..447c940cc0 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -46,6 +46,31 @@ loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval) namespace detail { +Number +computeRaisedRate(Number const& periodicRate, std::uint32_t paymentsRemaining) +{ + /* + * This formula is from the XLS-66 spec, section 3.2.4.1.1 (Regular + * Payment), though "raisedRate" is computed only once and used twice. + */ + return power(1 + periodicRate, paymentsRemaining); +} + +Number +computePaymentFactor( + Number const& periodicRate, + std::uint32_t paymentsRemaining) +{ + /* + * This formula is from the XLS-66 spec, section 3.2.4.1.1 (Regular + * Payment), though "raisedRate" is computed only once and used twice. + */ + Number const raisedRate = + computeRaisedRate(periodicRate, paymentsRemaining); + + return (periodicRate * raisedRate) / (raisedRate - 1); +} + Number loanPeriodicPayment( Number const& principalOutstanding, @@ -61,11 +86,10 @@ loanPeriodicPayment( /* * This formula is from the XLS-66 spec, section 3.2.4.1.1 (Regular - * Payment), though "raisedRate" is computed only once and used twice. + * Payment). */ - Number const raisedRate = power(1 + periodicRate, paymentsRemaining); - - return principalOutstanding * periodicRate * raisedRate / (raisedRate - 1); + return principalOutstanding * + computePaymentFactor(periodicRate, paymentsRemaining); } Number @@ -87,6 +111,23 @@ loanPeriodicPayment( principalOutstanding, periodicRate, paymentsRemaining); } +Number +loanPrincipalFromPeriodicPayment( + Number const& periodicPayment, + Number const& periodicRate, + std::uint32_t paymentsRemaining) +{ + if (periodicRate == 0) + return periodicPayment * paymentsRemaining; + + /* + * This formula is the reverse of the one from the XLS-66 spec, + * section 3.2.4.1.1 (Regular Payment) used in loanPeriodicPayment + */ + return periodicPayment / + computePaymentFactor(periodicRate, paymentsRemaining); +} + Number loanLatePaymentInterest( Number const& principalOutstanding, diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 801590ca28..b9df2147ff 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -2485,7 +2485,6 @@ ValidLoan::finalize( &sfLatePaymentFee, &sfClosePaymentFee, &sfPrincipalOutstanding, - &sfReferencePrincipal, &sfTotalValueOutstanding, &sfInterestOwed}) { diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index 87db5d3c00..df866b1237 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -271,9 +271,9 @@ LoanManage::defaultLoan( loanSle->setFlag(lsfLoanDefault); loanSle->at(sfTotalValueOutstanding) = 0; loanSle->at(sfPaymentRemaining) = 0; - loanSle->at(sfReferencePrincipal) = 0; principalOutstandingProxy = 0; interestOwedProxy = 0; + loanSle->at(~sfNextPaymentDueDate) = std::nullopt; view.update(loanSle); // Return funds from the LoanBroker pseudo-account to the diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index e9c85af255..5142b219eb 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -90,20 +90,19 @@ LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx) // This is definitely paying fewer than paymentsPerFeeIncrement payments return normalCost; - // This computation isn't free, but it's relatively straightforward if (auto const fullInterest = calculateFullPaymentInterest( asset, - loanSle->at(sfReferencePrincipal), + loanSle->at(sfPeriodicPayment), loanPeriodicRate( TenthBips32(loanSle->at(sfInterestRate)), loanSle->at(sfPaymentInterval)), + loanSle->at(sfPaymentRemaining), view.parentCloseTime(), loanSle->at(sfPaymentInterval), loanSle->at(sfPreviousPaymentDate), loanSle->at(sfStartDate), TenthBips32(loanSle->at(sfCloseInterestRate)), - scale, - loanSle->at(sfClosePaymentFee)); + scale); amount > loanSle->at(sfPrincipalOutstanding) + fullInterest + loanSle->at(sfClosePaymentFee)) return normalCost; @@ -189,18 +188,6 @@ LoanPay::preclaim(PreclaimContext const& ctx) JLOG(ctx.j.warn()) << "Borrower account is frozen."; return ret; } - if (auto const ret = checkDeepFrozen(ctx.view, brokerPseudoAccount, asset)) - { - JLOG(ctx.j.warn()) << "Loan Broker pseudo-account can not receive " - "funds (deep frozen)."; - return ret; - } - if (auto const ret = checkDeepFrozen(ctx.view, brokerOwner, asset)) - { - JLOG(ctx.j.warn()) - << "Loan Broker can not receive funds (deep frozen)."; - return ret; - } if (auto const ret = checkDeepFrozen(ctx.view, vaultPseudoAccount, asset)) { JLOG(ctx.j.warn()) @@ -223,6 +210,7 @@ LoanPay::doApply() auto const loanSle = view.peek(keylet::loan(loanID)); if (!loanSle) return tefBAD_LEDGER; // LCOV_EXCL_LINE + std::int32_t const loanScale = loanSle->at(sfLoanScale); auto const brokerID = loanSle->at(sfLoanBrokerID); auto const brokerSle = view.peek(keylet::loanbroker(brokerID)); @@ -237,6 +225,41 @@ LoanPay::doApply() auto const vaultPseudoAccount = vaultSle->at(sfAccount); auto const asset = *vaultSle->at(sfAsset); + // Determine where to send the broker's fee + auto coverAvailableProxy = brokerSle->at(sfCoverAvailable); + TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)}; + auto debtTotalProxy = brokerSle->at(sfDebtTotal); + + // Send the broker fee to the owner if they have sufficient cover available, + // _and_ if the owner can receive funds. If not, so as not to block the + // payment, add it to the cover balance (send it to the broker pseudo + // account). + // + // Normally freeze status is checked in preflight, but we do it here to + // avoid duplicating the check. It'll claim a fee either way. + bool const sendBrokerFeeToOwner = coverAvailableProxy >= + roundToAsset(asset, + tenthBipsOfValue( + debtTotalProxy.value(), coverRateMinimum), + loanScale) && + !isDeepFrozen(view, brokerOwner, asset); + + auto const brokerPayee = + sendBrokerFeeToOwner ? brokerOwner : brokerPseudoAccount; + auto const brokerPayeeSle = view.peek(keylet::account(brokerPayee)); + if (!sendBrokerFeeToOwner) + { + // If we can't send the fee to the owner, and the pseudo-account is + // frozen, then we have to fail the payment. + if (auto const ret = checkDeepFrozen(view, brokerPayee, asset)) + { + JLOG(j_.warn()) + << "Both Loan Broker and Loan Broker pseudo-account " + "can not receive funds (deep frozen)."; + return ret; + } + } + //------------------------------------------------------ // Loan object state changes @@ -303,8 +326,6 @@ LoanPay::doApply() // LCOV_EXCL_STOP } - std::int32_t const loanScale = loanSle->at(sfLoanScale); - //------------------------------------------------------ // LoanBroker object state changes view.update(brokerSle); @@ -360,8 +381,6 @@ LoanPay::doApply() "ripple::LoanPay::doApply", "payments add up"); - auto debtTotalProxy = brokerSle->at(sfDebtTotal); - // Decrease LoanBroker Debt by the amount paid, add the Loan value change // (which might be negative). totalPaidToVaultForDebt may be negative, // increasing the debt @@ -387,7 +406,7 @@ LoanPay::doApply() vaultSle->at(sfAssetsAvailable) += totalPaidToVault; vaultSle->at(sfAssetsTotal) += interestPaidExtra; - interestOwedProxy -= interestPaidToVault; + interestOwedProxy -= interestPaidForDebt; XRPL_ASSERT_PARTS( *vaultSle->at(sfAssetsAvailable) <= *vaultSle->at(sfAssetsTotal), "ripple::LoanPay::doApply", @@ -398,27 +417,17 @@ LoanPay::doApply() // auto const unavailable = total - available; // Move funds - STAmount const paidToVault(asset, totalPaidToVault); - STAmount const paidToBroker(asset, totalPaidToBroker); XRPL_ASSERT_PARTS( - paidToVault + paidToBroker <= amount, + totalPaidToVault + totalPaidToBroker <= amount, "ripple::LoanPay::doApply", "amount is sufficient"); XRPL_ASSERT_PARTS( - paidToVault + paidToBroker <= paymentParts->principalPaid + + totalPaidToVault + totalPaidToBroker <= paymentParts->principalPaid + paymentParts->interestPaid + paymentParts->feeToPay, "ripple::LoanPay::doApply", "payment agreement"); - // Determine where to send the broker's fee - auto coverAvailableProxy = brokerSle->at(sfCoverAvailable); - TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)}; - - bool const sufficientCover = coverAvailableProxy >= - roundToAsset(asset, - tenthBipsOfValue(debtTotalProxy.value(), coverRateMinimum), - loanScale); - if (!sufficientCover) + if (!sendBrokerFeeToOwner) { // If there is not enough first-loss capital, add the fee to First Loss // Cover Pool. Note that this moves the entire fee - it does not attempt @@ -426,8 +435,6 @@ LoanPay::doApply() // it for future needs. coverAvailableProxy += totalPaidToBroker; } - auto const brokerPayee = - sufficientCover ? brokerOwner : brokerPseudoAccount; #if !NDEBUG auto const accountBalanceBefore = @@ -447,19 +454,40 @@ LoanPay::doApply() view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); #endif - if (auto const ter = accountSend( + if (totalPaidToVault != Number{}) + { + if (auto const ter = requireAuth( + view, asset, vaultPseudoAccount, AuthType::StrongAuth)) + return ter; + } + + if (totalPaidToBroker != Number{}) + { + if (brokerPayee == account_) + { + // The broker may have deleted their holding. Recreate it if needed + if (auto const ter = addEmptyHolding( + view, + brokerPayee, + brokerPayeeSle->at(sfBalance).value().xrp(), + asset, + j_); + ter && ter != tecDUPLICATE) + // ignore tecDUPLICATE. That means the holding already exists, + // and is fine here + return ter; + } + if (auto const ter = + requireAuth(view, asset, brokerPayee, AuthType::StrongAuth)) + return ter; + } + + if (auto const ter = accountSendMulti( view, account_, - vaultPseudoAccount, - paidToVault, - j_, - WaiveTransferFee::Yes)) - return ter; - if (auto const ter = accountSend( - view, - account_, - brokerPayee, - paidToBroker, + asset, + {{vaultPseudoAccount, totalPaidToVault}, + {brokerPayee, totalPaidToBroker}}, j_, WaiveTransferFee::Yes)) return ter; @@ -481,6 +509,7 @@ LoanPay::doApply() : accountHolds( view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); + /* auto const balanceScale = std::max( {accountBalanceBefore.exponent(), vaultBalanceBefore.exponent(), @@ -488,15 +517,10 @@ LoanPay::doApply() accountBalanceAfter.exponent(), vaultBalanceAfter.exponent(), brokerBalanceAfter.exponent()}); + */ XRPL_ASSERT_PARTS( - roundToAsset( - asset, - accountBalanceBefore + vaultBalanceBefore + brokerBalanceBefore, - balanceScale) == - roundToAsset( - asset, - accountBalanceAfter + vaultBalanceAfter + brokerBalanceAfter, - balanceScale), + accountBalanceBefore + vaultBalanceBefore + brokerBalanceBefore == + accountBalanceAfter + vaultBalanceAfter + brokerBalanceAfter, "ripple::LoanPay::doApply", "funds are conserved (with rounding)"); #endif diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index ee3cb507ab..5f8800caeb 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -79,12 +79,13 @@ LoanSet::preflight(PreflightContext const& ctx) {&sfLoanOriginationFee, &sfLoanServiceFee, &sfLatePaymentFee, - &sfClosePaymentFee, - &sfPrincipalRequested}) + &sfClosePaymentFee}) { if (!validNumericMinimum(tx[~*field])) return temINVALID; } + if (auto const p = tx[~sfPrincipalRequested]; p && p <= 0) + return temINVALID; if (!validNumericRange(tx[~sfInterestRate], maxInterestRate)) return temINVALID; if (!validNumericRange(tx[~sfOverpaymentFee], maxOverpaymentFee)) @@ -304,22 +305,14 @@ LoanSet::doApply() auto const properties = computeLoanProperties( vaultAsset, principalRequested, - principalRequested, interestRate, paymentInterval, paymentTotal, TenthBips32{brokerSle->at(sfManagementFeeRate)}); - if (properties.firstPaymentPrincipal <= 0) - { - // Check that some reference principal is paid each period. Since the - // first payment pays the least principal, if it's good, they'll all be - // good. Note that the outstanding principal is rounded, and may not - // change right away. - JLOG(j_.warn()) << "Loan is unable to pay principal."; - return tecPRECISION_LOSS; - } - if (interestRate != 0 && + // Guard 1: if there is no computed total interest over the life of the loan + // for a non-zero interest rate, we cannot properly amortize the loan + if (interestRate > TenthBips32{0} && (properties.totalValueOutstanding - principalRequested) <= 0) { // Unless this is a zero-interst loan, there must be some interest due @@ -328,6 +321,39 @@ LoanSet::doApply() << "% interest has no interest due"; return tecPRECISION_LOSS; } + + // Guard 2: if the rounded periodic payment is large enough that the loan + // can't be amortized in the specified number of payments, raise an error + if (auto const computedPayments = roundToAsset( + vaultAsset, + properties.totalValueOutstanding / + roundToAsset( + vaultAsset, + properties.periodicPayment, + properties.loanScale, + Number::upward), + properties.loanScale, + Number::upward); + computedPayments < paymentTotal) + { + JLOG(j_.warn()) << "Loan Periodic payment rounding will complete the " + "loan in less than the specified number of payments"; + return tecPRECISION_LOSS; + } + + // Guard 3: if the principal portion of the first periodic payment is too + // small to be accurately represented with the given rounding mode, raise an + // error + if (properties.firstPaymentPrincipal <= 0) + { + // Check that some reference principal is paid each period. Since + // the first payment pays the least principal, if it's good, they'll + // all be good. Note that the outstanding principal is rounded, and + // may not change right away. + JLOG(j_.warn()) << "Loan is unable to pay principal."; + return tecPRECISION_LOSS; + } + // Check that the other computed values are valid if (properties.interestOwedToVault < 0 || properties.totalValueOutstanding <= 0 || @@ -398,51 +424,57 @@ LoanSet::doApply() // 1. Transfer loanAssetsAvailable (principalRequested - originationFee) // from vault pseudo-account to the borrower. // Create a holding for the borrower if one does not already exist. - if (auto const ter = addEmptyHolding( - view, - borrower, - borrowerSle->at(sfBalance).value().xrp(), - vaultAsset, - j_); - ter && ter != tecDUPLICATE) - // ignore tecDUPLICATE. That means the holding already exists, and - // is fine here + + if (borrower == account_) + { + if (auto const ter = addEmptyHolding( + view, + borrower, + borrowerSle->at(sfBalance).value().xrp(), + vaultAsset, + j_); + ter && ter != tecDUPLICATE) + // ignore tecDUPLICATE. That means the holding already exists, and + // is fine here + return ter; + } + if (auto const ter = + requireAuth(view, vaultAsset, borrower, AuthType::StrongAuth)) return ter; - if (auto const ter = accountSend( - view, - vaultPseudo, - borrower, - STAmount{vaultAsset, loanAssetsToBorrower}, - j_, - WaiveTransferFee::Yes)) - return ter; // 2. Transfer originationFee, if any, from vault pseudo-account to // LoanBroker owner. if (originationFee != Number{}) { // Create the holding if it doesn't already exist (necessary for MPTs). // The owner may have deleted their MPT / line at some point. - if (auto const ter = addEmptyHolding( - view, - brokerOwner, - brokerOwnerSle->at(sfBalance).value().xrp(), - vaultAsset, - j_); - !isTesSuccess(ter) && ter != tecDUPLICATE) - // ignore tecDUPLICATE. That means the holding already exists, and - // is fine here - return ter; - if (auto const ter = accountSend( - view, - vaultPseudo, - brokerOwner, - STAmount{vaultAsset, originationFee}, - j_, - WaiveTransferFee::Yes)) + if (brokerOwner == account_) + { + if (auto const ter = addEmptyHolding( + view, + brokerOwner, + brokerOwnerSle->at(sfBalance).value().xrp(), + vaultAsset, + j_); + !isTesSuccess(ter) && ter != tecDUPLICATE) + // ignore tecDUPLICATE. That means the holding already exists, + // and is fine here + return ter; + } + if (auto const ter = requireAuth( + view, vaultAsset, brokerOwner, AuthType::StrongAuth)) return ter; } + if (auto const ter = accountSendMulti( + view, + vaultPseudo, + vaultAsset, + {{borrower, loanAssetsToBorrower}, {brokerOwner, originationFee}}, + j_, + WaiveTransferFee::Yes)) + return ter; + // The portion of the loan interest that will go to the vault (total // interest minus the management fee) auto const startDate = view.info().closeTime.time_since_epoch().count(); @@ -461,7 +493,7 @@ LoanSet::doApply() }; // Set required and fixed tx fields - loan->at(sfLoanScale) = principalRequested.exponent(); + loan->at(sfLoanScale) = properties.loanScale; loan->at(sfStartDate) = startDate; loan->at(sfPaymentInterval) = paymentInterval; loan->at(sfLoanSequence) = *loanSequenceProxy; @@ -482,7 +514,6 @@ LoanSet::doApply() setLoanField(~sfGracePeriod, defaultGracePeriod); // Set dynamic / computed fields to their initial values loan->at(sfPrincipalOutstanding) = principalRequested; - loan->at(sfReferencePrincipal) = principalRequested; loan->at(sfPeriodicPayment) = properties.periodicPayment; loan->at(sfTotalValueOutstanding) = properties.totalValueOutstanding; loan->at(sfInterestOwed) = properties.interestOwedToVault; From ec20974caead853751b5e3616970b1ac1473507b Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 9 Oct 2025 17:59:27 -0400 Subject: [PATCH 129/291] Simplify rules for creating holdings in `LoanSet` - Lender and borrower both signed the transaction. Duh. --- src/xrpld/app/tx/detail/LoanSet.cpp | 57 ++++++++++++++++------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 5f8800caeb..70dbfd668b 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -425,19 +425,21 @@ LoanSet::doApply() // from vault pseudo-account to the borrower. // Create a holding for the borrower if one does not already exist. - if (borrower == account_) - { - if (auto const ter = addEmptyHolding( - view, - borrower, - borrowerSle->at(sfBalance).value().xrp(), - vaultAsset, - j_); - ter && ter != tecDUPLICATE) - // ignore tecDUPLICATE. That means the holding already exists, and - // is fine here - return ter; - } + XRPL_ASSERT_PARTS( + borrower == account_ || borrower == counterparty, + "ripple::LoanSet::doApply", + "borrower signed transaction"); + if (auto const ter = addEmptyHolding( + view, + borrower, + borrowerSle->at(sfBalance).value().xrp(), + vaultAsset, + j_); + ter && ter != tecDUPLICATE) + // ignore tecDUPLICATE. That means the holding already exists, and + // is fine here + return ter; + if (auto const ter = requireAuth(view, vaultAsset, borrower, AuthType::StrongAuth)) return ter; @@ -448,19 +450,22 @@ LoanSet::doApply() { // Create the holding if it doesn't already exist (necessary for MPTs). // The owner may have deleted their MPT / line at some point. - if (brokerOwner == account_) - { - if (auto const ter = addEmptyHolding( - view, - brokerOwner, - brokerOwnerSle->at(sfBalance).value().xrp(), - vaultAsset, - j_); - !isTesSuccess(ter) && ter != tecDUPLICATE) - // ignore tecDUPLICATE. That means the holding already exists, - // and is fine here - return ter; - } + XRPL_ASSERT_PARTS( + brokerOwner == account_ || brokerOwner == counterparty, + "ripple::LoanSet::doApply", + "broker owner signed transaction"); + + if (auto const ter = addEmptyHolding( + view, + brokerOwner, + brokerOwnerSle->at(sfBalance).value().xrp(), + vaultAsset, + j_); + !isTesSuccess(ter) && ter != tecDUPLICATE) + // ignore tecDUPLICATE. That means the holding already exists, + // and is fine here + return ter; + if (auto const ter = requireAuth( view, vaultAsset, brokerOwner, AuthType::StrongAuth)) return ter; From bda6bb46d16b66df5b0df2004b469ad6f986acd8 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 10 Oct 2025 00:43:43 -0400 Subject: [PATCH 130/291] Fix the singlePayment test helper lambda --- src/test/app/Loan_test.cpp | 162 ++++++++++++++++++------------------- 1 file changed, 78 insertions(+), 84 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 163a2e5d52..4dce52995f 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -1294,89 +1294,79 @@ class Loan_test : public beast::unit_test::suite }; }; - auto singlePayment = [&](STAmount const& payoffAmount, + auto singlePayment = [&](Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus, + LoanState& state, + STAmount const& payoffAmount, + std::uint32_t numPayments, std::uint32_t baseFlag) { - return [&, payoffAmount, baseFlag]( - Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus) { - // toEndOfLife - // - auto state = - getCurrentState(env, broker, loanKeylet, verifyLoanStatus); - BEAST_EXPECT(state.flags == baseFlag); - env.close(state.startDate + 20s); - auto const loanAge = (env.now() - state.startDate).count(); - BEAST_EXPECT(loanAge == 30); + // toEndOfLife + // + verifyLoanStatus(state); - verifyLoanStatus(state); + // Send some bogus pay transactions + env(pay(borrower, keylet::loan(uint256(0)).key, broker.asset(10)), + ter(temINVALID)); + env(pay(borrower, loanKeylet.key, broker.asset(-100)), + ter(temBAD_AMOUNT)); + env(pay(borrower, broker.brokerID, broker.asset(100)), + ter(tecNO_ENTRY)); + env(pay(evan, loanKeylet.key, broker.asset(500)), + ter(tecNO_PERMISSION)); - // Send some bogus pay transactions - env(pay(borrower, - keylet::loan(uint256(0)).key, - broker.asset(10)), - ter(temINVALID)); - env(pay(borrower, loanKeylet.key, broker.asset(-100)), - ter(temBAD_AMOUNT)); - env(pay(borrower, broker.brokerID, broker.asset(100)), - ter(tecNO_ENTRY)); - env(pay(evan, loanKeylet.key, broker.asset(500)), - ter(tecNO_PERMISSION)); + { + auto const otherAsset = broker.asset.raw() == assets[0].raw() + ? assets[1] + : assets[0]; + env(pay(borrower, loanKeylet.key, otherAsset(100)), + ter(tecWRONG_ASSET)); + } - { - auto const otherAsset = - broker.asset.raw() == assets[0].raw() ? assets[1] - : assets[0]; - env(pay(borrower, loanKeylet.key, otherAsset(100)), - ter(tecWRONG_ASSET)); - } + // Amount doesn't cover a single payment + env(pay(borrower, loanKeylet.key, STAmount{broker.asset, 1}), + ter(tecINSUFFICIENT_PAYMENT)); - // Amount doesn't cover a single payment - env(pay(borrower, loanKeylet.key, STAmount{broker.asset, 1}), - ter(tecINSUFFICIENT_PAYMENT)); + // Get the balance after these failed transactions take + // fees + auto const borrowerBalanceBeforePayment = + env.balance(borrower, broker.asset); - // Get the balance after these failed transactions take - // fees - auto const borrowerBalanceBeforePayment = - env.balance(borrower, broker.asset); + BEAST_EXPECT(payoffAmount > state.principalOutstanding); + // Try to pay a little extra to show that it's _not_ + // taken + auto const transactionAmount = payoffAmount + broker.asset(10); - BEAST_EXPECT(payoffAmount > state.principalOutstanding); - // Try to pay a little extra to show that it's _not_ - // taken - auto const transactionAmount = payoffAmount + broker.asset(10); - BEAST_EXPECT( - transactionAmount == - broker.asset(Number(1050000114155251, -12))); - env(pay(borrower, loanKeylet.key, transactionAmount)); + env(pay(borrower, loanKeylet.key, transactionAmount)); - env.close(); + env.close(); - // Need to account for fees if the loan is in XRP - PrettyAmount adjustment = broker.asset(0); - if (broker.asset.raw().native()) - { - adjustment = env.current()->fees().base; - } + // Need to account for fees if the loan is in XRP + PrettyAmount adjustment = broker.asset(0); + if (broker.asset.raw().native()) + { + adjustment = env.current()->fees().base; + } - state.paymentRemaining = 0; - state.principalOutstanding = 0; - state.totalValue = 0; - state.interestOwed = 0; - state.previousPaymentDate = state.nextPaymentDate; - verifyLoanStatus(state); + state.paymentRemaining = 0; + state.principalOutstanding = 0; + state.totalValue = 0; + state.interestOwed = 0; + state.previousPaymentDate = state.nextPaymentDate + + state.paymentInterval * (numPayments - 1); + verifyLoanStatus(state); - verifyLoanStatus.checkPayment( - state.loanScale, - borrower, - borrowerBalanceBeforePayment, - payoffAmount, - adjustment); + verifyLoanStatus.checkPayment( + state.loanScale, + borrower, + borrowerBalanceBeforePayment, + payoffAmount, + adjustment); - // Can't impair or default a paid off loan - env(manage(lender, loanKeylet.key, tfLoanImpair), - ter(tecNO_PERMISSION)); - env(manage(lender, loanKeylet.key, tfLoanDefault), - ter(tecNO_PERMISSION)); - }; + // Can't impair or default a paid off loan + env(manage(lender, loanKeylet.key, tfLoanImpair), + ter(tecNO_PERMISSION)); + env(manage(lender, loanKeylet.key, tfLoanDefault), + ter(tecNO_PERMISSION)); }; auto immediatePayoff = [&](std::uint32_t baseFlag) { @@ -1385,7 +1375,7 @@ class Loan_test : public beast::unit_test::suite VerifyLoanStatus const& verifyLoanStatus) { // toEndOfLife // - auto const state = + auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus); env.close(state.startDate + 20s); auto const loanAge = (env.now() - state.startDate).count(); @@ -1432,7 +1422,13 @@ class Loan_test : public beast::unit_test::suite payoffAmount > state.paymentRemaining * (state.periodicPayment + broker.asset(2).value())); - singlePayment(payoffAmount, baseFlag); + singlePayment( + loanKeylet, + verifyLoanStatus, + state, + payoffAmount, + 1, + baseFlag); }; }; @@ -1443,8 +1439,9 @@ class Loan_test : public beast::unit_test::suite // toEndOfLife // - auto const state = + auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus); + env.close(); // Make all the payments in one transaction // service fee is 2 @@ -1457,7 +1454,13 @@ class Loan_test : public beast::unit_test::suite broker.asset(Number(1024014840139457, -12))); BEAST_EXPECT(payoffAmount > state.principalOutstanding); - singlePayment(payoffAmount, baseFlag); + singlePayment( + loanKeylet, + verifyLoanStatus, + state, + payoffAmount, + state.paymentRemaining, + baseFlag); }; }; @@ -1699,15 +1702,6 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT( paymentComponents.final || totalDue == totalDueAmount); - BEAST_EXPECT( - state.paymentRemaining < 12 || - paymentComponents.roundedInterest == - roundToScale( - broker.asset( - Number(2283105022831050, -18), - Number::upward), - state.loanScale, - Number::upward)); BEAST_EXPECT( paymentComponents.roundedInterest >= Number(0)); From d7196a8e56b2ceae4b0b1d7d6d0df1e5a587f1ba Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sun, 12 Oct 2025 19:18:14 -0400 Subject: [PATCH 131/291] Get remaining Loan unit tests working - Rename some of the helper functions / lambdas. - Update tracked interestOwed field better at final payoff. - Add checks in LoanSet that the fields can be represented in the asset type, and update test that fails those checks (testLoanPayComputePeriodicPaymentValidRateInvariant) - Also check that the computed periodic payment can be represented as the asset type, and doesn't round _UP_ to 0. - Update asserts to account for more scenarios, including initial loan computation. --- src/test/app/Loan_test.cpp | 61 ++++++------- src/xrpld/app/misc/LendingHelpers.h | 33 +++++-- src/xrpld/app/tx/detail/LoanSet.cpp | 136 +++++++++++++++++++--------- src/xrpld/app/tx/detail/LoanSet.h | 3 + 4 files changed, 146 insertions(+), 87 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 4dce52995f..c067dea02e 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -1369,7 +1369,7 @@ class Loan_test : public beast::unit_test::suite ter(tecNO_PERMISSION)); }; - auto immediatePayoff = [&](std::uint32_t baseFlag) { + auto fullPayment = [&](std::uint32_t baseFlag) { return [&, baseFlag]( Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { @@ -1432,7 +1432,7 @@ class Loan_test : public beast::unit_test::suite }; }; - auto multiplePayoff = [&](std::uint32_t baseFlag) { + auto combineAllPayments = [&](std::uint32_t baseFlag) { return [&, baseFlag]( Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { @@ -1536,7 +1536,7 @@ class Loan_test : public beast::unit_test::suite broker, pseudoAcct, 0, - immediatePayoff(0)); + fullPayment(0)); lifecycle( caseLabel, @@ -1550,11 +1550,11 @@ class Loan_test : public beast::unit_test::suite broker, pseudoAcct, tfLoanOverpayment, - immediatePayoff(lsfLoanOverpayment)); + fullPayment(lsfLoanOverpayment)); lifecycle( caseLabel, - "Loan overpayment prohibited - Make multiple payments", + "Loan overpayment prohibited - Combine all payments", env, loanAmount, interestExponent, @@ -1564,11 +1564,11 @@ class Loan_test : public beast::unit_test::suite broker, pseudoAcct, 0, - multiplePayoff(0)); + combineAllPayments(0)); lifecycle( caseLabel, - "Loan overpayment allowed - Make multiple payments", + "Loan overpayment allowed - Combine all payments", env, loanAmount, interestExponent, @@ -1578,7 +1578,7 @@ class Loan_test : public beast::unit_test::suite broker, pseudoAcct, tfLoanOverpayment, - multiplePayoff(lsfLoanOverpayment)); + combineAllPayments(lsfLoanOverpayment)); lifecycle( caseLabel, @@ -1775,20 +1775,21 @@ class Loan_test : public beast::unit_test::suite if (paymentComponents.final) { state.paymentRemaining = 0; + state.interestOwed = 0; } else { state.nextPaymentDate += state.paymentInterval; + state.interestOwed -= valueMinusFee( + broker.asset.raw(), + paymentComponents.roundedInterest, + managementFeeRateParameter, + state.loanScale); } state.principalOutstanding -= paymentComponents.roundedPrincipal; state.totalValue -= paymentComponents.roundedPrincipal + paymentComponents.roundedInterest; - state.interestOwed -= valueMinusFee( - broker.asset.raw(), - paymentComponents.roundedInterest, - managementFeeRateParameter, - state.loanScale); verifyLoanStatus(state); } @@ -2214,38 +2215,28 @@ class Loan_test : public beast::unit_test::suite createJson["OverpaymentInterestRate"] = 1360; createJson["PaymentInterval"] = 727; - Number const actualPrincipal{6}; - auto const brokerStateBefore = env.le(keylet::loanbroker(broker.brokerID)); auto const loanSequence = brokerStateBefore->at(sfLoanSequence); auto const keylet = keylet::loan(broker.brokerID, loanSequence); createJson = env.json(createJson, sig(sfCounterpartySignature, lender)); - env(createJson, ter(tesSUCCESS)); + // Fails in preclaim because principal requested can't be represented as + // XRP + env(createJson, ter(tecPRECISION_LOSS)); env.close(); - if (auto const loan = env.le(keylet); BEAST_EXPECT(loan)) - { - // Verify the payment decreased the principal - BEAST_EXPECT(loan->at(sfPaymentRemaining) == numPayments); - BEAST_EXPECT(loan->at(sfLoanScale) == actualPrincipal.exponent()); - BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == actualPrincipal); - } + BEAST_EXPECT(!env.le(keylet)); - auto loanPayTx = env.json( - pay(borrower, keylet.key, STAmount{broker.asset, serviceFee + 6})); - env(loanPayTx, ter(tesSUCCESS)); + Number const actualPrincipal{6}; + + createJson[sfPrincipalRequested] = actualPrincipal; + createJson.removeMember(sfSequence.jsonName); + createJson = env.json(createJson, sig(sfCounterpartySignature, lender)); + // Fails in doApply because the payment is too small to be represented + // as XRP. + env(createJson, ter(tecPRECISION_LOSS)); env.close(); - - if (auto const loan = env.le(keylet); BEAST_EXPECT(loan)) - { - // Verify the payment decreased the principal - BEAST_EXPECT(loan->at(sfPaymentRemaining) == numPayments - 1); - BEAST_EXPECT(loan->at(sfLoanScale) == actualPrincipal.exponent()); - BEAST_EXPECT( - loan->at(sfPrincipalOutstanding) == actualPrincipal - 1); - } } void diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index b9cd00de83..af056ffbd9 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -225,16 +225,19 @@ computePaymentComponents( auto const p = roundToAsset( asset, // Compute the delta that will get the tracked principalOutstanding - // amount as close to the raw principal amount after the payment as + // amount as close to the true principal amount after the payment as // possible. principalOutstanding - (rawPrincipalOutstanding - rawPrincipal), scale, Number::downward); + // The principal part can only be 0 during intial loan validation. If it + // is 0, the Loan will not be created, but we don't want an assert + // aborting the process before we get that far. XRPL_ASSERT_PARTS( - p > 0, + p >= 0, "rippled::detail::computePaymentComponents", - "principal part positive"); + "principal part not negative"); XRPL_ASSERT_PARTS( p <= principalOutstanding, "rippled::detail::computePaymentComponents", @@ -266,7 +269,7 @@ computePaymentComponents( auto const iDiff = roundedPeriodicPayment - roundedPrincipal; // Compute the delta that will get the untracked interestOutstanding - // amount as close as possible to the raw interest amount after the + // amount as close as possible to the true interest amount after the // payment as possible. auto const iSync = interestOutstanding - (roundToAsset(asset, rawInterestOutstanding, scale) - @@ -691,7 +694,7 @@ template LoanProperties computeLoanProperties( A const& asset, - Number const& principalOutstanding, + Number principalOutstanding, TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t paymentsRemaining, @@ -723,8 +726,14 @@ computeLoanProperties( // over payments) auto const loanScale = totalValueOutstanding.exponent(); + // Since we just figured out the loan scale, we haven't been able to + // validate that the principal fits in it, so to allow this function to + // succeed, round it here, and let the caller do the validation. + principalOutstanding = roundToAsset( + asset, principalOutstanding, loanScale, Number::to_nearest); + auto const firstPaymentPrincipal = [&]() { - // Compute the unrounded parts for the first payment. Ensure that the + // Compute the parts for the first payment. Ensure that the // principal payment will actually change the principal. auto const paymentComponents = detail::computePaymentComponents( asset, @@ -735,9 +744,9 @@ computeLoanProperties( periodicRate, paymentsRemaining); - // The rounded principal part needs to be large enough to affect the + // The unrounded principal part needs to be large enough to affect the // principal. What to do if not is left to the caller - return paymentComponents.roundedPrincipal; + return paymentComponents.rawPrincipal; }(); auto const interestOwedToVault = valueMinusFee( @@ -1086,6 +1095,10 @@ loanMakePayment( periodicRate, paymentRemainingProxy), serviceFee}; + XRPL_ASSERT_PARTS( + periodic.roundedPrincipal > 0, + "ripple::loanMakePayment", + "regular payment pays principal"); // ------------------------------------------------------------- // late payment handling @@ -1193,6 +1206,10 @@ loanMakePayment( periodicRate, paymentRemainingProxy), periodic.fee}; + XRPL_ASSERT_PARTS( + nextPayment.roundedPrincipal > 0, + "ripple::loanMakePayment", + "additional payment pays principal"); XRPL_ASSERT( nextPayment.rawInterest <= periodic.rawInterest, "ripple::loanMakePayment : decreasing interest"); diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 70dbfd668b..83f3f0a429 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -179,6 +179,21 @@ LoanSet::calculateBaseFee(ReadView const& view, STTx const& tx) return normalCost + (signerCount * baseFee); } +std::vector> const& +LoanSet::getValueFields() +{ + static std::vector> const valueFields{ + ~sfPrincipalRequested, + ~sfLoanOriginationFee, + ~sfLoanServiceFee, + ~sfLatePaymentFee, + ~sfClosePaymentFee + // Overpayment fee is really a rate. Don't check it here. + }; + + return valueFields; +} + TER LoanSet::preclaim(PreclaimContext const& ctx) { @@ -221,6 +236,24 @@ LoanSet::preclaim(PreclaimContext const& ctx) Asset const asset = vault->at(sfAsset); auto const vaultPseudo = vault->at(sfAccount); + // Check that relevant values can be represented as the vault asset type. + // This check is almost duplicated in doApply, but that check is done after + // the overall loan scale is known. This is mostly only relevant for + // integral (non-IOU) types + { + for (auto const& field : getValueFields()) + { + if (auto const value = tx[field]; + value && STAmount{asset, *value} != *value) + { + JLOG(ctx.j.warn()) << field.f->getName() << " (" << *value + << ") can not be represented as a(n) " + << to_string(asset) << "."; + return tecPRECISION_LOSS; + } + } + } + if (auto const ter = canAddHolding(ctx.view, asset)) return ter; @@ -310,43 +343,52 @@ LoanSet::doApply() paymentTotal, TenthBips32{brokerSle->at(sfManagementFeeRate)}); + // Check that relevant values won't lose precision. This is mostly only + // relevant for IOU assets. + { + for (auto const& field : getValueFields()) + { + if (auto const value = tx[field]; + value && !isRounded(vaultAsset, *value, properties.loanScale)) + { + JLOG(j_.warn()) + << field.f->getName() << " (" << *value + << ") has too much precision. Total loan value is " + << properties.totalValueOutstanding << " with a scale of " + << properties.loanScale; + return tecPRECISION_LOSS; + } + } + } + // Guard 1: if there is no computed total interest over the life of the loan // for a non-zero interest rate, we cannot properly amortize the loan if (interestRate > TenthBips32{0} && (properties.totalValueOutstanding - principalRequested) <= 0) { - // Unless this is a zero-interst loan, there must be some interest due + // Unless this is a zero-interest loan, there must be some interest due // on the loan, even if it's (measurable) dust JLOG(j_.warn()) << "Loan with " << interestRate << "% interest has no interest due"; return tecPRECISION_LOSS; } - - // Guard 2: if the rounded periodic payment is large enough that the loan - // can't be amortized in the specified number of payments, raise an error - if (auto const computedPayments = roundToAsset( - vaultAsset, - properties.totalValueOutstanding / - roundToAsset( - vaultAsset, - properties.periodicPayment, - properties.loanScale, - Number::upward), - properties.loanScale, - Number::upward); - computedPayments < paymentTotal) + // Guard 1a: If there is any interest computed over the life of the loan, + // for a zero interest rate, something went sideways. + if (interestRate == TenthBips32{0} && + (properties.totalValueOutstanding - principalRequested) > 0) { - JLOG(j_.warn()) << "Loan Periodic payment rounding will complete the " - "loan in less than the specified number of payments"; - return tecPRECISION_LOSS; + // LCOV_EXCL_START + JLOG(j_.warn()) << "Loan with 0% interest has interest due"; + return tecINTERNAL; + // LCOV_EXCL_STOP } - // Guard 3: if the principal portion of the first periodic payment is too + // Guard 2: if the principal portion of the first periodic payment is too // small to be accurately represented with the given rounding mode, raise an // error if (properties.firstPaymentPrincipal <= 0) { - // Check that some reference principal is paid each period. Since + // Check that some true (unrounded) principal is paid each period. Since // the first payment pays the least principal, if it's good, they'll // all be good. Note that the outstanding principal is rounded, and // may not change right away. @@ -354,6 +396,36 @@ LoanSet::doApply() return tecPRECISION_LOSS; } + // Guard 3: If the periodic payment is so small that it can't even be + // rounded to a representable value, then the loan can't be paid. Also, + // avoids dividing by 0. + auto const roundedPayment = roundPeriodicPayment( + vaultAsset, properties.periodicPayment, properties.loanScale); + if (roundedPayment == Number{}) + { + JLOG(j_.warn()) << "Loan Periodic payment (" + << properties.periodicPayment << ") rounds to 0. "; + return tecPRECISION_LOSS; + } + + // Guard 4: if the rounded periodic payment is large enough that the loan + // can't be amortized in the specified number of payments, raise an error + if (auto const computedPayments = roundToAsset( + vaultAsset, + properties.totalValueOutstanding / roundedPayment, + properties.loanScale, + Number::upward); + computedPayments < paymentTotal) + { + JLOG(j_.warn()) + << "Loan Periodic payment (" << properties.periodicPayment + << ") rounding (" << roundedPayment + << ") will complete the " + "loan in less than the specified number of payments (" + << computedPayments << " < " << paymentTotal << ")"; + return tecPRECISION_LOSS; + } + // Check that the other computed values are valid if (properties.interestOwedToVault < 0 || properties.totalValueOutstanding <= 0 || @@ -366,30 +438,6 @@ LoanSet::doApply() // LCOV_EXCL_STOP } - // Check that relevant values won't lose precision - { - static std::map> const - valueFields{ - {"Principal Requested", ~sfPrincipalRequested}, - {"Origination fee", ~sfLoanOriginationFee}, - {"Service fee", ~sfLoanServiceFee}, - {"Late Payment fee", ~sfLatePaymentFee}, - {"Close Payment fee", ~sfClosePaymentFee} - // Overpayment fee is really a rate. Don't include it. - }; - for (auto const& [name, field] : valueFields) - { - if (auto const value = tx[field]; - value && !isRounded(vaultAsset, *value, properties.loanScale)) - { - JLOG(j_.warn()) - << name << " has too much precision. Total loan value is " - << properties.totalValueOutstanding << " with a scale of " - << properties.loanScale; - return tecPRECISION_LOSS; - } - } - } auto const originationFee = tx[~sfLoanOriginationFee].value_or(Number{}); auto const loanAssetsToBorrower = principalRequested - originationFee; diff --git a/src/xrpld/app/tx/detail/LoanSet.h b/src/xrpld/app/tx/detail/LoanSet.h index cb98e361c0..68c7f4ca3c 100644 --- a/src/xrpld/app/tx/detail/LoanSet.h +++ b/src/xrpld/app/tx/detail/LoanSet.h @@ -48,6 +48,9 @@ public: static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); + static std::vector> const& + getValueFields(); + static TER preclaim(PreclaimContext const& ctx); From f9bd9f390affaf54578a3f7a1561c89d94704d8f Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 15 Oct 2025 20:51:11 -0400 Subject: [PATCH 132/291] Remove some unused variables, increase rounding tolerance --- src/test/app/Loan_test.cpp | 2 +- src/xrpld/app/misc/LendingHelpers.h | 5 +++-- src/xrpld/app/tx/detail/LoanPay.cpp | 2 -- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index c067dea02e..852b725aec 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -1739,7 +1739,7 @@ class Loan_test : public beast::unit_test::suite roundedPeriodicPayment - (paymentComponents.roundedPrincipal + paymentComponents.roundedInterest) < - 2); + 3); auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset); diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index af056ffbd9..50f385d1c0 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -178,7 +178,7 @@ computePaymentComponents( Number const rawPrincipalOutstanding = loanPrincipalFromPeriodicPayment( periodicPayment, periodicRate, paymentRemaining); Number const rawInterestOutstanding = - rawValueOutstanding - rawInterestOutstanding; + rawValueOutstanding - rawPrincipalOutstanding; /* * From the spec, once the periodicPayment is computed: @@ -514,8 +514,10 @@ doOverpayment( "nextDueDate is unchanged"); nextDueDateProxy = nextDueDate; + /* auto const totalInterestOutstandingAfter = totalValueOutstanding - principalOutstanding; + */ return paymentParts; } @@ -623,7 +625,6 @@ handleFullPayment( // If this is the last payment, it has to be a regular payment return Unexpected(tesSUCCESS); - Number const rawValueOutstanding = periodicPayment * paymentRemaining; Number const rawPrincipalOutstanding = loanPrincipalFromPeriodicPayment( periodicPayment, periodicRate, paymentRemaining); diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 5142b219eb..81d99ef6b4 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -162,8 +162,6 @@ LoanPay::preclaim(PreclaimContext const& ctx) return tefBAD_LEDGER; // LCOV_EXCL_STOP } - auto const brokerPseudoAccount = loanBrokerSle->at(sfAccount); - auto const brokerOwner = loanBrokerSle->at(sfOwner); auto const vaultID = loanBrokerSle->at(sfVaultID); auto const vaultSle = ctx.view.read(keylet::vault(vaultID)); if (!vaultSle) From baabf142e42c9a5a1f84c77b39ee05a8c0c89f4c Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 15 Oct 2025 23:28:00 -0400 Subject: [PATCH 133/291] Fix some build errors --- src/test/app/Loan_test.cpp | 14 +++++++------- src/xrpld/app/tx/detail/InvariantCheck.cpp | 22 +--------------------- src/xrpld/app/tx/detail/LoanPay.cpp | 2 +- 3 files changed, 9 insertions(+), 29 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 852b725aec..4248cc0f93 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -1733,13 +1733,13 @@ class Loan_test : public beast::unit_test::suite state.periodicPayment); BEAST_EXPECT( paymentComponents.final || - roundedPeriodicPayment >= - (paymentComponents.roundedPrincipal + - paymentComponents.roundedInterest) && - roundedPeriodicPayment - - (paymentComponents.roundedPrincipal + - paymentComponents.roundedInterest) < - 3); + ((roundedPeriodicPayment >= + (paymentComponents.roundedPrincipal + + paymentComponents.roundedInterest)) && + (roundedPeriodicPayment - + (paymentComponents.roundedPrincipal + + paymentComponents.roundedInterest) < + 3))); auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset); diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index b9df2147ff..6137c96b06 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -2495,27 +2495,7 @@ ValidLoan::finalize( return false; } } - // Must not be negative - STUInt32 - for (auto const field : - {&sfOverpaymentFee, - &sfInterestRate, - &sfLateInterestRate, - &sfCloseInterestRate, - &sfOverpaymentInterestRate, - &sfStartDate, - &sfPaymentInterval, - &sfGracePeriod, - &sfPreviousPaymentDate, - &sfPaymentRemaining}) - { - if (after->at(*field) < 0) - { - JLOG(j.fatal()) << "Invariant failed: " << field->getName() - << " is negative "; - return false; - } - } - // Must be positive + // Must be positive - STNumber for (auto const field : { &sfPeriodicPayment, }) diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 81d99ef6b4..a28e07f6d1 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -75,7 +75,7 @@ LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx) // Let preclaim worry about the error for this return normalCost; auto const vaultSle = view.read(keylet::vault(loanSle->at(sfVaultID))); - if (vaultSle) + if (!vaultSle) // Let preclaim worry about the error for this return normalCost; From 98d1cf07edf3020f95e7e5ef8237da776fc6b689 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 16 Oct 2025 16:04:16 -0400 Subject: [PATCH 134/291] Compare Numbers against beast::zero --- src/xrpld/app/tx/detail/LoanPay.cpp | 4 ++-- src/xrpld/app/tx/detail/LoanSet.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index a28e07f6d1..0985b194cd 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -452,14 +452,14 @@ LoanPay::doApply() view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); #endif - if (totalPaidToVault != Number{}) + if (totalPaidToVault != beast::zero) { if (auto const ter = requireAuth( view, asset, vaultPseudoAccount, AuthType::StrongAuth)) return ter; } - if (totalPaidToBroker != Number{}) + if (totalPaidToBroker != beast::zero) { if (brokerPayee == account_) { diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 83f3f0a429..5bd544bcf3 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -401,7 +401,7 @@ LoanSet::doApply() // avoids dividing by 0. auto const roundedPayment = roundPeriodicPayment( vaultAsset, properties.periodicPayment, properties.loanScale); - if (roundedPayment == Number{}) + if (roundedPayment == beast::zero) { JLOG(j_.warn()) << "Loan Periodic payment (" << properties.periodicPayment << ") rounds to 0. "; @@ -494,7 +494,7 @@ LoanSet::doApply() // 2. Transfer originationFee, if any, from vault pseudo-account to // LoanBroker owner. - if (originationFee != Number{}) + if (originationFee != beast::zero) { // Create the holding if it doesn't already exist (necessary for MPTs). // The owner may have deleted their MPT / line at some point. From 1bb306baf4491032cee0c517a3dc926ed3b6a759 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 20 Oct 2025 13:40:27 -0400 Subject: [PATCH 135/291] Fix formatting --- include/xrpl/protocol/detail/ledger_entries.macro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index f4d1198e10..2854e48534 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -599,7 +599,7 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({ // - TrueTotalPrincialOutstanding can be computed using the algorithm // in the ripple::detail::loanPrincipalFromPeriodicPayment function. // - // - TrueTotalInterestOutstanding = TrueTotalLoanValue - + // - TrueTotalInterestOutstanding = TrueTotalLoanValue - // TrueTotalPrincipalOutstanding // The unrounded true total interest remaining. // From dfc4933c3b4ec1be1e842200a4c214cb631d6005 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 16 Oct 2025 19:42:09 -0400 Subject: [PATCH 136/291] Convert Loans to track management fee and use updated rounding - Move management fee calculations out of transactors an into the appropriate functions in LendingHelpers.h. - Rewrite how overpayments are handled. May changed based on numerical analysis. - Update, fix, and clean up unit tests. Includes adding tolerances for some checks where an exact match is unlikely. - Add "integral()" function to Asset to simplify a common check. --- include/xrpl/protocol/Asset.h | 6 + .../xrpl/protocol/detail/ledger_entries.macro | 35 +- include/xrpl/protocol/detail/sfields.macro | 2 +- src/test/app/Loan_test.cpp | 195 ++- src/xrpld/app/misc/LendingHelpers.h | 1343 +++++++++++------ src/xrpld/app/misc/detail/LendingHelpers.cpp | 135 ++ src/xrpld/app/tx/detail/InvariantCheck.cpp | 2 +- src/xrpld/app/tx/detail/LoanManage.cpp | 29 +- src/xrpld/app/tx/detail/LoanPay.cpp | 118 +- src/xrpld/app/tx/detail/LoanSet.cpp | 19 +- 10 files changed, 1225 insertions(+), 659 deletions(-) diff --git a/include/xrpl/protocol/Asset.h b/include/xrpl/protocol/Asset.h index 4438106738..7a5dc67911 100644 --- a/include/xrpl/protocol/Asset.h +++ b/include/xrpl/protocol/Asset.h @@ -103,6 +103,12 @@ public: return holds() && get().native(); } + bool + integral() const + { + return !holds() || get().native(); + } + friend constexpr bool operator==(Asset const& lhs, Asset const& rhs); diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 2854e48534..0c86bf1f95 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -578,10 +578,10 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({ // - PrincipalOutstanding: The rounded portion of the // TotalValueOutstanding that is from the principal borrowed. // - // - InterestOwed: The rounded portion of the TotalValueOutstanding that - // represents interest specifically owed to the vault. This may be less - // than the interest owed by the borrower, because it excludes the - // expected value of broker management fees. + // - ManagementFeeOutstanding: The rounded portion of the + // TotalValueOutstanding that represents management fees + // specifically owed to the broker based on the initial + // loan parameters. // // There are additional values that can be computed from these: // @@ -589,9 +589,9 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({ // The total amount of interest still pending on the loan, // independent of management fees. // - // - ManagementFeeOwed = InterestOutstanding - InterestOwed - // The amount of the total interest that will be sent to the - // broker as management fees. + // - InterestOwedToVault = InterestOutstanding - ManagementFeeOutstanding + // The amount of the total interest that is owed to the vault, and + // will be sent to as part of a payment. // // - TrueTotalLoanValue = PaymentRemaining * PeriodicPayment // The unrounded true total value of the loan. @@ -603,18 +603,23 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({ // TrueTotalPrincipalOutstanding // The unrounded true total interest remaining. // + // - TrueTotalManagementFeeOutstanding = TrueTotalInterestOutstanding * + // LoanBroker.ManagementFeeRate + // The unrounded true total fee still owed to the broker. + // // Note the the "True" values may differ significantly from the tracked // rounded values. - {sfPaymentRemaining, soeDEFAULT}, - {sfPeriodicPayment, soeREQUIRED}, - {sfPrincipalOutstanding, soeDEFAULT}, - {sfTotalValueOutstanding, soeDEFAULT}, - {sfInterestOwed, soeDEFAULT}, - // Based on the original principal borrowed, used for + {sfPaymentRemaining, soeDEFAULT}, + {sfPeriodicPayment, soeREQUIRED}, + {sfPrincipalOutstanding, soeDEFAULT}, + {sfTotalValueOutstanding, soeDEFAULT}, + {sfManagementFeeOutstanding, soeDEFAULT}, + // Based on the computed total value at creation, used for // rounding calculated values so they are all on a // consistent scale - that is, they all have the same - // number of decimal places after the decimal point. - {sfLoanScale, soeDEFAULT}, + // number of digits after the decimal point (excluding + // trailing zeros). + {sfLoanScale, soeDEFAULT}, })) #undef EXPAND diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 1911c7e31d..086cda0e08 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -241,7 +241,7 @@ TYPED_SFIELD(sfPrincipalOutstanding, NUMBER, 13) TYPED_SFIELD(sfPrincipalRequested, NUMBER, 14) TYPED_SFIELD(sfTotalValueOutstanding, NUMBER, 15) TYPED_SFIELD(sfPeriodicPayment, NUMBER, 16) -TYPED_SFIELD(sfInterestOwed, NUMBER, 17) +TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17) // int32 TYPED_SFIELD(sfLoanScale, INT32, 1) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 4248cc0f93..093e41f835 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -115,10 +115,10 @@ class Loan_test : public beast::unit_test::suite std::int32_t const loanScale = 0; Number totalValue = 0; Number principalOutstanding = 0; - Number interestOwed = 0; + Number managementFeeOutstanding = 0; Number periodicPayment = 0; std::uint32_t flags = 0; - std::uint32_t paymentInterval = 0; + std::uint32_t const paymentInterval = 0; TenthBips32 const interestRate{}; }; @@ -239,7 +239,7 @@ class Loan_test : public beast::unit_test::suite Number const& loanScale, Number const& totalValue, Number const& principalOutstanding, - Number const& interestOwed, + Number const& managementFeeOutstanding, Number const& periodicPayment, std::uint32_t flags) const { @@ -260,16 +260,20 @@ class Loan_test : public beast::unit_test::suite loan->at(sfTotalValueOutstanding) == totalValue); env.test.BEAST_EXPECT( loan->at(sfPrincipalOutstanding) == principalOutstanding); - env.test.BEAST_EXPECT(loan->at(sfInterestOwed) == interestOwed); + env.test.BEAST_EXPECT( + loan->at(sfManagementFeeOutstanding) == + managementFeeOutstanding); env.test.BEAST_EXPECT( loan->at(sfPeriodicPayment) == periodicPayment); env.test.BEAST_EXPECT(loan->at(sfFlags) == flags); + auto const ls = calculateRoundedLoanState(loan); + auto const interestRate = TenthBips32{loan->at(sfInterestRate)}; auto const paymentInterval = loan->at(sfPaymentInterval); checkBroker( principalOutstanding, - interestOwed, + ls.interestDue, interestRate, paymentInterval, paymentRemaining, @@ -288,7 +292,7 @@ class Loan_test : public beast::unit_test::suite { env.test.BEAST_EXPECT( vaultSle->at(sfLossUnrealized) == - principalOutstanding + interestOwed); + totalValue - managementFeeOutstanding); } else { @@ -311,7 +315,7 @@ class Loan_test : public beast::unit_test::suite state.loanScale, state.totalValue, state.principalOutstanding, - state.interestOwed, + state.managementFeeOutstanding, state.periodicPayment, state.flags); }; @@ -388,7 +392,8 @@ class Loan_test : public beast::unit_test::suite .loanScale = loan->at(sfLoanScale), .totalValue = loan->at(sfTotalValueOutstanding), .principalOutstanding = loan->at(sfPrincipalOutstanding), - .interestOwed = loan->at(sfInterestOwed), + .managementFeeOutstanding = + loan->at(sfManagementFeeOutstanding), .periodicPayment = loan->at(sfPeriodicPayment), .flags = loan->at(sfFlags), .paymentInterval = loan->at(sfPaymentInterval), @@ -403,6 +408,19 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT( state.principalOutstanding.exponent() == state.loanScale); BEAST_EXPECT(state.paymentInterval == 600); + BEAST_EXPECT( + state.totalValue == + roundToAsset( + broker.asset, + state.periodicPayment * state.paymentRemaining, + state.loanScale)); + BEAST_EXPECT( + state.managementFeeOutstanding == + computeFee( + broker.asset, + state.totalValue - state.principalOutstanding, + managementFeeRateParameter, + state.loanScale)); verifyLoanStatus(state); @@ -429,7 +447,7 @@ class Loan_test : public beast::unit_test::suite auto const assetsUnavailable = vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable); auto const unrealizedLoss = vaultSle->at(sfLossUnrealized) + - state.principalOutstanding + state.interestOwed; + state.totalValue - state.managementFeeOutstanding; if (unrealizedLoss > assetsUnavailable) { @@ -627,7 +645,7 @@ class Loan_test : public beast::unit_test::suite auto state = getCurrentState(env, broker, keylet, verifyLoanStatus); auto const loanProperties = computeLoanProperties( - broker.asset, + broker.asset.raw(), state.principalOutstanding, state.interestRate, state.paymentInterval, @@ -641,7 +659,7 @@ class Loan_test : public beast::unit_test::suite principalRequest.exponent(), loanProperties.totalValueOutstanding, principalRequest, - loanProperties.interestOwedToVault, + loanProperties.managementFeeOwedToBroker, loanProperties.periodicPayment, loanFlags | 0); @@ -698,7 +716,7 @@ class Loan_test : public beast::unit_test::suite principalRequest.exponent(), loanProperties.totalValueOutstanding, principalRequest, - loanProperties.interestOwedToVault, + loanProperties.managementFeeOwedToBroker, loanProperties.periodicPayment, loanFlags | 0); @@ -1189,7 +1207,7 @@ class Loan_test : public beast::unit_test::suite brokerSle->at(sfDebtTotal), coverRateMinParameter), coverRateLiquidationParameter), - state.principalOutstanding + state.interestOwed), + state.totalValue - state.managementFeeOutstanding), state.loanScale); return std::make_pair(defaultAmount, brokerSle->at(sfOwner)); } @@ -1280,7 +1298,7 @@ class Loan_test : public beast::unit_test::suite state.paymentRemaining = 0; state.totalValue = 0; state.principalOutstanding = 0; - state.interestOwed = 0; + state.managementFeeOutstanding = 0; verifyLoanStatus(state); // Once a loan is defaulted, it can't be managed @@ -1350,7 +1368,7 @@ class Loan_test : public beast::unit_test::suite state.paymentRemaining = 0; state.principalOutstanding = 0; state.totalValue = 0; - state.interestOwed = 0; + state.managementFeeOutstanding = 0; state.previousPaymentDate = state.nextPaymentDate + state.paymentInterval * (numPayments - 1); verifyLoanStatus(state); @@ -1629,37 +1647,55 @@ class Loan_test : public beast::unit_test::suite testcase << "\tPayment components: " << "Payments remaining, rawInterest, rawPrincipal, " - "roundedInterest, " - "roundedPrincipal, roundedPayment, final, extra"; + "rawMFee, roundedInterest, roundedPrincipal, " + "roundedMFee, final, extra"; + + auto const serviceFee = broker.asset(2); + + BEAST_EXPECT( + roundedPeriodicPayment == + roundToScale( + broker.asset( + Number(8333457001162141, -14), Number::upward), + state.loanScale, + Number::upward)); + // 83334570.01162141 + // Include the service fee + STAmount const totalDue = roundToScale( + roundedPeriodicPayment + serviceFee, + state.loanScale, + Number::upward); + // Only check the first payment since the rounding + // may drift as payments are made + BEAST_EXPECT( + totalDue == + roundToScale( + broker.asset( + Number(8533457001162141, -14), Number::upward), + state.loanScale, + Number::upward)); + + { + auto const raw = calculateRawLoanState( + state.periodicPayment, + periodicRate, + state.paymentRemaining, + managementFeeRateParameter); + auto const rounded = calculateRoundedLoanState( + state.totalValue, + state.principalOutstanding, + state.managementFeeOutstanding); + testcase + << "\tLoan starting state: " << state.paymentRemaining + << ", " << raw.interestDue << ", " + << raw.principalOutstanding << ", " + << raw.managementFeeDue << ", " << rounded.interestDue + << ", " << rounded.principalOutstanding << ", " + << rounded.managementFeeDue; + } while (state.paymentRemaining > 0) { - auto const serviceFee = broker.asset(2); - // Only check the first payment since the rounding - // may drift as payments are made - BEAST_EXPECT( - roundedPeriodicPayment == - roundToScale( - broker.asset( - Number(8333457001162141, -14), Number::upward), - state.loanScale, - Number::upward)); - // 83334570.01162141 - // Include the service fee - STAmount const totalDue = roundToScale( - roundedPeriodicPayment + serviceFee, - state.loanScale, - Number::upward); - // Only check the first payment since the rounding - // may drift as payments are made - BEAST_EXPECT( - totalDue == - roundToScale( - broker.asset( - Number(8533457001162141, -14), Number::upward), - state.loanScale, - Number::upward)); - // Try to pay a little extra to show that it's _not_ // taken STAmount const transactionAmount = @@ -1675,32 +1711,48 @@ class Loan_test : public beast::unit_test::suite Number::upward)); // Compute the expected principal amount - auto const paymentComponents = - detail::computePaymentComponents( - broker.asset, - state.loanScale, - state.totalValue, - state.principalOutstanding, - state.periodicPayment, - periodicRate, - state.paymentRemaining); + auto const paymentComponents = computePaymentComponents( + broker.asset.raw(), + state.loanScale, + state.totalValue, + state.principalOutstanding, + state.managementFeeOutstanding, + state.periodicPayment, + periodicRate, + state.paymentRemaining, + managementFeeRateParameter); testcase << "\tPayment components: " << state.paymentRemaining << ", " << paymentComponents.rawInterest << ", " << paymentComponents.rawPrincipal << ", " + << paymentComponents.rawManagementFee << ", " << paymentComponents.roundedInterest << ", " << paymentComponents.roundedPrincipal << ", " - << paymentComponents.roundedPayment << ", " - << paymentComponents.final << ", " - << paymentComponents.extra; + << paymentComponents.roundedManagementFee << ", " + << (paymentComponents.final ? "true" : "false") << ", " + << (paymentComponents.extra ? "true" : "false"); auto const totalDueAmount = STAmount{ broker.asset, - paymentComponents.roundedPayment + serviceFee.number()}; + paymentComponents.roundedPrincipal + + paymentComponents.roundedInterest + + paymentComponents.roundedManagementFee + + serviceFee.number()}; + // Due to the rounding algorithms to keep the interest and + // principal in sync with "true" values, the computed amount + // may be a little less than the rounded fixed payment + // amount. For integral types, the difference should be < 3 + // (1 unit for each of the interest and management fee). For + // IOUs, the difference should be after the 8th digit. + Number const diff = totalDue - totalDueAmount; BEAST_EXPECT( - paymentComponents.final || totalDue == totalDueAmount); + paymentComponents.final || diff == beast::zero || + (diff > beast::zero && + ((broker.asset.raw().integral() && + (static_cast(diff) < 3)) || + (totalDue.exponent() - diff.exponent() > 8)))); BEAST_EXPECT( paymentComponents.roundedInterest >= Number(0)); @@ -1728,18 +1780,12 @@ class Loan_test : public beast::unit_test::suite state.principalOutstanding); BEAST_EXPECT( paymentComponents.final || - paymentComponents.rawPrincipal + - paymentComponents.rawInterest == - state.periodicPayment); - BEAST_EXPECT( - paymentComponents.final || - ((roundedPeriodicPayment >= - (paymentComponents.roundedPrincipal + - paymentComponents.roundedInterest)) && - (roundedPeriodicPayment - - (paymentComponents.roundedPrincipal + - paymentComponents.roundedInterest) < - 3))); + (state.periodicPayment.exponent() - + (paymentComponents.rawPrincipal + + paymentComponents.rawInterest + + paymentComponents.rawManagementFee - + state.periodicPayment) + .exponent()) > 14); auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset); @@ -1775,21 +1821,18 @@ class Loan_test : public beast::unit_test::suite if (paymentComponents.final) { state.paymentRemaining = 0; - state.interestOwed = 0; } else { state.nextPaymentDate += state.paymentInterval; - state.interestOwed -= valueMinusFee( - broker.asset.raw(), - paymentComponents.roundedInterest, - managementFeeRateParameter, - state.loanScale); } state.principalOutstanding -= paymentComponents.roundedPrincipal; + state.managementFeeOutstanding -= + paymentComponents.roundedManagementFee; state.totalValue -= paymentComponents.roundedPrincipal + - paymentComponents.roundedInterest; + paymentComponents.roundedInterest + + paymentComponents.roundedManagementFee; verifyLoanStatus(state); } diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 50f385d1c0..475cc39ca7 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -34,22 +34,38 @@ checkLendingProtocolDependencies(PreflightContext const& ctx); Number loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval); -// This structure is used internally to compute the breakdown of a -// single loan payment +/// Ensure the periodic payment is always rounded consistently +template +Number +roundPeriodicPayment( + A const& asset, + Number const& periodicPayment, + std::int32_t scale) +{ + return roundToAsset(asset, periodicPayment, scale, Number::upward); +} + +/// This structure is used internally to compute the breakdown of a +/// single loan payment struct PaymentComponents { Number rawInterest; Number rawPrincipal; + Number rawManagementFee; Number roundedInterest; Number roundedPrincipal; - // We may not need roundedPayment - Number roundedPayment; + // roundedManagementFee is explicitly on for the portion of the pre-computed + // periodic payment that goes toward the Broker's management fee, which is + // tracked by sfManagementFeeOutstanding + Number roundedManagementFee; + //// We may not need roundedPayment + // Number roundedPayment; bool final = false; bool extra = false; }; -// This structure is explained in the XLS-66 spec, section 3.2.4.4 (Failure -// Conditions) +/// This structure is explained in the XLS-66 spec, section 3.2.4.4 (Failure +/// Conditions) struct LoanPaymentParts { /// principal_paid is the amount of principal that the payment covered. @@ -63,8 +79,12 @@ struct LoanPaymentParts * This is 0 for regular payments. */ Number valueChange; - /// fee_paid is the amount of fee that the payment covered. - Number feeToPay; + /// managementFeePaid is amount of fee that is tracked by + /// sfManagementFeeOutstanding + Number managementFeePaid; + /// extraFeePaid is the amount of fee that the payment covered not tracked + /// by sfManagementFeeOutstanding. + Number extraFeePaid; LoanPaymentParts& operator+=(LoanPaymentParts const& other) @@ -72,22 +92,114 @@ struct LoanPaymentParts principalPaid += other.principalPaid; interestPaid += other.interestPaid; valueChange += other.valueChange; - feeToPay += other.feeToPay; + extraFeePaid += other.extraFeePaid; + managementFeePaid += other.managementFeePaid; return *this; } }; -/// Ensure the periodic payment is always rounded consistently +/** This structure describes the initial "computed" properties of a loan. + * + * It is used at loan creation and when the terms of a loan change, such as + * after an overpayment. + */ +struct LoanProperties +{ + Number periodicPayment; + Number totalValueOutstanding; + Number managementFeeOwedToBroker; + std::int32_t loanScale; + Number firstPaymentPrincipal; +}; + +/** This structure captures the current state of a loan and all the + relevant parts. + + Whether the values are raw (unrounded) or rounded will + depend on how it was computed. + + Many of the fields can be derived from each other, but they're all provided + here to reduce code duplication and possible mistakes. + e.g. + * interestOutstanding = valueOutstanding - principalOutstanding + * interestDue = interestOutstanding - managementFeeDue + */ +struct LoanState +{ + /// Total value still due to be paid by the borrower. + Number valueOutstanding; + /// Prinicipal still due to be paid by the borrower. + Number principalOutstanding; + /// Interest still due to be paid by the borrower. + Number interestOutstanding; + /// Interest still due to be paid TO the Vault. + // This is a portion of interestOutstanding + Number interestDue; + /// Management fee still due to be paid TO the broker. + // This is a portion of interestOutstanding + Number managementFeeDue; +}; + +LoanState +calculateRawLoanState( + Number const& periodicPayment, + Number const& periodicRate, + std::uint32_t const paymentRemaining, + TenthBips16 const managementFeeRate); + +LoanState +calculateRawLoanState( + Number const& periodicPayment, + TenthBips32 interestRate, + std::uint32_t paymentInterval, + std::uint32_t const paymentRemaining, + TenthBips16 const managementFeeRate); + +LoanState +calculateRoundedLoanState( + Number const& totalValueOutstanding, + Number const& principalOutstanding, + Number const& managementFeeOutstanding); + +LoanState +calculateRoundedLoanState(SLE::const_ref loan); + template Number -roundPeriodicPayment( +computeFee( A const& asset, - Number const& periodicPayment, + Number const& value, + TenthBips16 managementFeeRate, std::int32_t scale) { - return roundToAsset(asset, periodicPayment, scale, Number::upward); + return roundToAsset( + asset, + tenthBipsOfValue(value, managementFeeRate), + scale, + Number::downward); } +Number +calculateFullPaymentInterest( + Number const& rawPrincipalOutstanding, + Number const& periodicRate, + NetClock::time_point parentCloseTime, + std::uint32_t paymentInterval, + std::uint32_t prevPaymentDate, + std::uint32_t startDate, + TenthBips32 closeInterestRate); + +Number +calculateFullPaymentInterest( + Number const& periodicPayment, + Number const& periodicRate, + std::uint32_t paymentRemaining, + NetClock::time_point parentCloseTime, + std::uint32_t paymentInterval, + std::uint32_t prevPaymentDate, + std::uint32_t startDate, + TenthBips32 closeInterestRate); + namespace detail { // These functions should rarely be used directly. More often, the ultimate // result needs to be roundToAsset'd. @@ -127,200 +239,256 @@ loanAccruedInterest( std::uint32_t prevPaymentDate, std::uint32_t paymentInterval); +#if LOANCOMPLETE inline Number -minusFee(Number const& value, TenthBips32 managementFeeRate) +minusFee(Number const& value, TenthBips16 managementFeeRate) { return tenthBipsOfValue(value, tenthBipsPerUnity - managementFeeRate); } +#endif template -PaymentComponents -computePaymentComponents( +Number +computeRoundedPrincipalComponent( A const& asset, - std::int32_t scale, - Number const& totalValueOutstanding, Number const& principalOutstanding, - Number const& periodicPayment, - Number const& periodicRate, - std::uint32_t paymentRemaining) + Number const& rawPrincipalOutstanding, + Number const& rawPrincipal, + Number const& roundedPeriodicPayment, + std::int32_t scale) { - /* - * This function is derived from the XLS-66 spec, section 3.2.4.1.1 (Regular - * Payment) - */ - XRPL_ASSERT_PARTS( - isRounded(asset, totalValueOutstanding, scale) && - isRounded(asset, principalOutstanding, scale), - "ripple::detail::computePaymentComponents", - "Outstanding values are rounded"); - auto const roundedPeriodicPayment = - roundPeriodicPayment(asset, periodicPayment, scale); - if (paymentRemaining == 1 || - totalValueOutstanding <= roundedPeriodicPayment) - { - // If there's only one payment left, we need to pay off each of the loan - // parts. It's probably impossible for the subtraction to result in a - // negative value, but don't leave anything to chance. - Number interest = - std::max(Number{}, totalValueOutstanding - principalOutstanding); + // Adjust the principal payment by the rounding error between the true + // and rounded principal outstanding + auto const diff = roundToAsset( + asset, + principalOutstanding - rawPrincipalOutstanding, + scale, + Number::downward); - // Pay everything off - return { - .rawInterest = interest, - .rawPrincipal = principalOutstanding, - .roundedInterest = interest, - .roundedPrincipal = principalOutstanding, - .roundedPayment = interest + principalOutstanding, - .final = true}; + // If the rounded principal outstanding is greater than the true + // principal outstanding, we need to pay more principal to reduce + // the rounded principal outstanding + // + // If the rounded principal outstanding is less than the true + // principal outstanding, we need to pay less principal to allow the + // rounded principal outstanding to catch up + + auto const p = + roundToAsset(asset, rawPrincipal + diff, scale, Number::downward); + + // For particular loans, it's entirely possible for many of the first + // rounded payments to be all principal. + XRPL_ASSERT_PARTS( + p >= 0, + "rippled::detail::computeRoundedPrincipalComponent", + "principal part not negative"); + XRPL_ASSERT_PARTS( + p <= principalOutstanding, + "rippled::detail::computeRoundedPrincipalComponent", + "principal part not larger than outstanding principal"); + XRPL_ASSERT_PARTS( + !asset.integral() || abs(p - rawPrincipal) <= 1, + "rippled::detail::computeRoundedPrincipalComponent", + "principal part not larger than outstanding principal"); + XRPL_ASSERT_PARTS( + p <= roundedPeriodicPayment, + "rippled::detail::computeRoundedPrincipalComponent", + "principal part not larger than total payment"); + + // The asserts will be skipped in release builds, so check here to make + // sure nothing goes negative + if (p > roundedPeriodicPayment || p > principalOutstanding) + return std::min(roundedPeriodicPayment, principalOutstanding); + else if (p < 0) + return Number{}; + + return p; +} + +/** Returns the interest component of a payment WITHOUT accounting for + ** management fees + * + * In other words, it returns the combined value of the interest part that will + * go to the Vault and the management fee that will go to the Broker. + */ +template +Number +computeRoundedInterestComponent( + A const& asset, + Number const& interestOutstanding, + Number const& roundedPrincipal, + Number const& rawInterestOutstanding, + Number const& roundedPeriodicPayment, + std::int32_t scale) +{ + // Start by just using the non-principal part of the payment for interest + Number roundedInterest = roundedPeriodicPayment - roundedPrincipal; + XRPL_ASSERT( + isRounded(asset, roundedInterest, scale), + "ripple::detail::computeRoundedInterestComponent", + "initial interest computation is rounded"); + + { + // Adjust the interest payment by the rounding error between the true + // and rounded interest outstanding + // + // If the rounded interest outstanding is greater than the true interest + // outstanding, we need to pay more interest to reduce the rounded + // interest outstanding + // + // If the rounded interest outstanding is less than the true interest + // outstanding, we need to pay less interest to allow the rounded + // interest outstanding to catch up + auto const diff = roundToAsset( + asset, + interestOutstanding - rawInterestOutstanding, + scale, + Number::downward); + if (diff < beast::zero) + roundedInterest += diff; } - Number const rawValueOutstanding = periodicPayment * paymentRemaining; - Number const rawPrincipalOutstanding = loanPrincipalFromPeriodicPayment( - periodicPayment, periodicRate, paymentRemaining); - Number const rawInterestOutstanding = - rawValueOutstanding - rawPrincipalOutstanding; + // However, we cannot allow negative interest payments, therefore we need to + // cap the interest payment at 0. + // + // Ensure interest payment is non-negative and does not exceed the remaining + // payment after principal + return std::max(Number{}, roundedInterest); +} - /* - * From the spec, once the periodicPayment is computed: - * - * The principal and interest portions can be derived as follows: - * interest = principalOutstanding * periodicRate - * principal = periodicPayment - interest - */ - Number const rawInterest = rawPrincipalOutstanding * periodicRate; - Number const rawPrincipal = periodicPayment - rawInterest; - XRPL_ASSERT_PARTS( - rawInterest >= 0, - "ripple::detail::computePaymentComponents", - "valid raw interest"); - XRPL_ASSERT_PARTS( - rawPrincipal >= 0 && rawPrincipal <= rawPrincipalOutstanding, - "ripple::detail::computePaymentComponents", - "valid raw principal"); +// The Interest and Fee components need to be calculated together, because they +// can affect each other during computation in both directions. +template +std::pair +computeRoundedInterestAndFeeComponents( + A const& asset, + Number const& interestOutstanding, + Number const& managementFeeOutstanding, + Number const& roundedPrincipal, + Number const& rawInterestOutstanding, + Number const& rawManagementFeeOutstanding, + Number const& roundedPeriodicPayment, + Number const& periodicRate, + TenthBips16 managementFeeRate, + std::int32_t scale) +{ + // Zero interest means ZERO interest + if (periodicRate == 0) + return std::make_pair(Number{}, Number{}); - /* - Critical Calculation: Balancing Principal and Interest Outstanding + Number roundedInterest = computeRoundedInterestComponent( + asset, + interestOutstanding, + roundedPrincipal, + rawInterestOutstanding, + roundedPeriodicPayment, + scale); - This calculation maintains a delicate balance between keeping - principal outstanding and interest outstanding as close as possible to - reference values. However, we cannot perfectly match the reference - values due to rounding issues. + Number roundedFee = + computeFee(asset, roundedInterest, managementFeeRate, scale); - Key considerations: - 1. Since the periodic payment is rounded up, we have excess funds - that can be used to pay down the loan faster than the reference - calculation. - - 2. We must ensure that loan repayment is not too fast, otherwise we - will end up with negative principal outstanding or negative - interest outstanding. - - 3. We cannot allow the borrower to repay interest ahead of schedule. - If the borrower makes an overpayment, the interest portion could - go negative, requiring complex recalculation to refund the borrower by - reflecting the overpayment in the principal portion of the loan. - */ - - Number const roundedPrincipal = [&]() { - auto const p = roundToAsset( + { + // Adjust the interest fee by the rounding error between the true and + // rounded interest fee outstanding + auto const diff = roundToAsset( asset, - // Compute the delta that will get the tracked principalOutstanding - // amount as close to the true principal amount after the payment as - // possible. - principalOutstanding - (rawPrincipalOutstanding - rawPrincipal), + managementFeeOutstanding - rawManagementFeeOutstanding, scale, Number::downward); - // The principal part can only be 0 during intial loan validation. If it - // is 0, the Loan will not be created, but we don't want an assert - // aborting the process before we get that far. - XRPL_ASSERT_PARTS( - p >= 0, - "rippled::detail::computePaymentComponents", - "principal part not negative"); - XRPL_ASSERT_PARTS( - p <= principalOutstanding, - "rippled::detail::computePaymentComponents", - "principal part not larger than outstanding principal"); - XRPL_ASSERT_PARTS( - p <= roundedPeriodicPayment, - "rippled::detail::computePaymentComponents", - "principal part not larger than total payment"); + roundedFee += diff; - // Make sure nothing goes negative - if (p > roundedPeriodicPayment || p > principalOutstanding) - return std::min(roundedPeriodicPayment, principalOutstanding); - else if (p < 0) - return Number{}; + // But again, we cannot allow negative interest fees, therefore we need + // to cap the interest fee at 0 + roundedFee = std::max(Number{}, roundedFee); - return p; - }(); + // Finally, the rounded interest fee cannot exceed the outstanding + // interest fee + roundedFee = std::min(roundedFee, managementFeeOutstanding); + } - Number const roundedInterest = [&]() { - // Zero interest means ZERO interest - if (periodicRate == 0) - return Number{}; + // Remove the fee portion from the interest payment, as the fee is paid + // separately - // Compute the rounded interest outstanding - auto const interestOutstanding = - totalValueOutstanding - principalOutstanding; - // Compute the delta that will simply treat the rest of the rounded - // fixed payment amount as interest. - auto const iDiff = roundedPeriodicPayment - roundedPrincipal; + // Ensure that the interest payment does not become negative, this may + // happen with high interest fees + roundedInterest = std::max(Number{}, roundedInterest - roundedFee); - // Compute the delta that will get the untracked interestOutstanding - // amount as close as possible to the true interest amount after the - // payment as possible. - auto const iSync = interestOutstanding - - (roundToAsset(asset, rawInterestOutstanding, scale) - - roundToAsset(asset, rawInterest, scale)); - XRPL_ASSERT_PARTS( - isRounded(asset, iSync, scale), - "ripple::detail::computePaymentComponents", - "iSync is rounded"); + // Finally, ensure that the interest payment does not exceed the + // interest outstanding + roundedInterest = std::min(interestOutstanding, roundedInterest); - // Use the smaller of the two to ensure we don't overpay interest. - auto const i = std::min({iSync, iDiff, interestOutstanding}); - - // No negative interest! - if (i < 0) - return Number{}; - return i; - }(); + // Make sure the parts don't add up to too much + Number excess = roundedPeriodicPayment - roundedPrincipal - + roundedInterest - roundedFee; XRPL_ASSERT_PARTS( - roundedInterest >= 0 && isRounded(asset, roundedInterest, scale), - "ripple::detail::computePaymentComponents", - "valid rounded interest"); - XRPL_ASSERT_PARTS( - roundedPrincipal >= 0 && roundedPrincipal <= principalOutstanding && - roundedPrincipal <= roundedPeriodicPayment && - isRounded(asset, roundedPrincipal, scale), - "ripple::detail::computePaymentComponents", - "valid rounded principal"); - XRPL_ASSERT_PARTS( - roundedPrincipal + roundedInterest <= roundedPeriodicPayment, - "ripple::detail::computePaymentComponents", - "payment parts fit within payment limit"); + isRounded(asset, excess, scale), + "ripple::detail::computeRoundedInterestAndFeeComponents", + "excess is rounded"); - return { - .rawInterest = rawInterest, - .rawPrincipal = rawPrincipal, - .roundedInterest = roundedInterest, - .roundedPrincipal = roundedPrincipal, - .roundedPayment = roundedPeriodicPayment}; + if (excess < beast::zero) + { + // Take as much of the excess as we can out of the interest + auto part = std::min(roundedInterest, abs(excess)); + roundedInterest -= part; + excess += part; + + XRPL_ASSERT_PARTS( + excess <= beast::zero, + "ripple::detail::computeRoundedInterestAndFeeComponents", + "excess not positive (interest)"); + } + if (excess < beast::zero) + { + // If there's any left, take as much of the excess as we can out of the + // fee + auto part = std::min(roundedFee, abs(excess)); + roundedFee -= part; + excess += part; + } + + // The excess should never be negative, which indicates that the parts are + // trying to take more than the whole payment. The excess can be positive, + // which indicates that we're not going to take the whole payment amount, + // but if so, it must be small. + XRPL_ASSERT_PARTS( + excess == beast::zero || + (excess > beast::zero && + ((asset.integral() && excess < 3) || + (roundedPeriodicPayment.exponent() - excess.exponent() > 6))), + "ripple::detail::computeRoundedInterestAndFeeComponents", + "excess is zero (fee)"); + + XRPL_ASSERT_PARTS( + roundedFee >= beast::zero, + "ripple::detail::computeRoundedInterestAndFeeComponents", + "non-negative fee"); + XRPL_ASSERT_PARTS( + roundedInterest >= beast::zero, + "ripple::detail::computeRoundedInterestAndFeeComponents", + "non-negative interest"); + + return std::make_pair( + std::max(Number{}, roundedInterest), std::max(Number{}, roundedFee)); } struct PaymentComponentsPlus : public PaymentComponents { - Number fee{0}; + Number extraFee{0}; Number valueChange{0}; + Number totalDue; PaymentComponentsPlus( PaymentComponents const& p, Number f, Number v = Number{}) - : PaymentComponents(p), fee(f), valueChange(v) + : PaymentComponents(p) + , extraFee(f) + , valueChange(v) + , totalDue( + roundedPrincipal + roundedInterest + roundedManagementFee + + extraFee) { } }; @@ -331,6 +499,7 @@ doPayment( PaymentComponentsPlus const& payment, NumberProxy& totalValueOutstandingProxy, NumberProxy& principalOutstandingProxy, + NumberProxy& managementFeeOutstandingProxy, UInt32Proxy& paymentRemainingProxy, UInt32Proxy& prevPaymentDateProxy, UInt32OptionalProxy& nextDueDateProxy, @@ -341,7 +510,8 @@ doPayment( "ripple::detail::doPayment", "Next due date proxy set"); auto const totalValueDelta = payment.roundedPrincipal + - payment.roundedInterest - payment.valueChange; + payment.roundedInterest + payment.roundedManagementFee - + payment.valueChange; if (!payment.extra) { if (payment.final) @@ -367,11 +537,11 @@ doPayment( XRPL_ASSERT_PARTS( principalOutstandingProxy > payment.roundedPrincipal, "ripple::detail::doPayment", - "Full principal payment"); + "Partial principal payment"); XRPL_ASSERT_PARTS( totalValueOutstandingProxy > totalValueDelta, "ripple::detail::doPayment", - "Full value payment"); + "Partial value payment"); paymentRemainingProxy -= 1; @@ -380,10 +550,17 @@ doPayment( // old-fashioned way. nextDueDateProxy = *nextDueDateProxy + paymentInterval; } + // Management fees are expected to be relatively small, and could get to + // zero before the loan is paid off + XRPL_ASSERT_PARTS( + managementFeeOutstandingProxy >= payment.roundedManagementFee, + "ripple::detail::doPayment", + "Valid management fee"); } principalOutstandingProxy -= payment.roundedPrincipal; totalValueOutstandingProxy -= totalValueDelta; + managementFeeOutstandingProxy -= payment.roundedManagementFee; XRPL_ASSERT_PARTS( // Use an explicit cast because the template parameter can be @@ -392,138 +569,239 @@ doPayment( static_cast(totalValueOutstandingProxy), "ripple::detail::doPayment", "principal does not exceed total"); + XRPL_ASSERT_PARTS( + // Use an explicit cast because the template parameter can be + // ValueProxy or Number + static_cast(managementFeeOutstandingProxy) >= beast::zero, + "ripple::detail::doPayment", + "fee outstanding stays valid"); return LoanPaymentParts{ .principalPaid = payment.roundedPrincipal, .interestPaid = payment.roundedInterest, .valueChange = payment.valueChange, - .feeToPay = payment.fee}; + .managementFeePaid = payment.roundedManagementFee, + .extraFeePaid = payment.extraFee}; } -template < - AssetType A, - class NumberProxy, - class UInt32Proxy, - class UInt32OptionalProxy> +// This function mainly exists to guarantee isolation of the "sandbox" +// variables from the real / proxy variables that will affect actual +// ledger data in the caller. +template Expected -doOverpayment( +tryOverpayment( A const& asset, - ApplyView& view, PaymentComponentsPlus const& overpaymentComponents, - NumberProxy& totalValueOutstandingProxy, - NumberProxy& principalOutstandingProxy, - NumberProxy& periodicPaymentProxy, - TenthBips32 const interestRate, - std::uint32_t const paymentInterval, - UInt32Proxy& paymentRemainingProxy, - UInt32Proxy& prevPaymentDateProxy, - UInt32OptionalProxy& nextDueDateProxy, - TenthBips16 managementFeeRate, + Number& totalValueOutstanding, + Number& principalOutstanding, + Number& managementFeeOutstanding, + Number& periodicPayment, + TenthBips32 interestRate, + std::uint32_t paymentInterval, + std::uint32_t paymentRemaining, + std::uint32_t prevPaymentDate, + std::optional nextDueDate, + TenthBips16 const managementFeeRate, beast::Journal j) { - Number const totalInterestOutstandingBefore = - totalValueOutstandingProxy - principalOutstandingProxy; - // Compute what the properties would be if the loan was new in its current // state. They are not likely to match the original properties. We're // interested in the error. - auto const oldLoanProperties = computeLoanProperties( + auto const loanPropertiesBefore = computeLoanProperties( asset, - principalOutstandingProxy, + principalOutstanding, interestRate, paymentInterval, - paymentRemainingProxy, + paymentRemaining, managementFeeRate); - auto const accumulatedError = - oldLoanProperties.totalValueOutstanding - totalValueOutstandingProxy; + auto const accumulatedTotalValueError = + loanPropertiesBefore.totalValueOutstanding - totalValueOutstanding; + auto const accumulatedFeeError = + loanPropertiesBefore.managementFeeOwedToBroker - + managementFeeOutstanding; + auto const paymentParts = detail::doPayment( + overpaymentComponents, + totalValueOutstanding, + principalOutstanding, + managementFeeOutstanding, + paymentRemaining, + prevPaymentDate, + nextDueDate, + paymentInterval); + + auto newLoanProperties = computeLoanProperties( + asset, + principalOutstanding, + interestRate, + paymentInterval, + paymentRemaining, + managementFeeRate); + + newLoanProperties.totalValueOutstanding += accumulatedTotalValueError; + newLoanProperties.managementFeeOwedToBroker += accumulatedFeeError; + + if (newLoanProperties.firstPaymentPrincipal <= 0 && + principalOutstanding > 0) { - // Use temp variables to do the payment, so they can be thrown away if - // they don't work - Number totalValueOutstanding = totalValueOutstandingProxy; - Number principalOutstanding = principalOutstandingProxy; - std::uint32_t paymentRemaining = paymentRemainingProxy; - std::uint32_t prevPaymentDate = prevPaymentDateProxy; - std::optional nextDueDate = nextDueDateProxy; - - auto const paymentParts = detail::doPayment( - overpaymentComponents, - totalValueOutstanding, - principalOutstanding, - paymentRemaining, - prevPaymentDate, - nextDueDate, - paymentInterval); - - auto newLoanProperties = computeLoanProperties( - asset, - principalOutstanding, - interestRate, - paymentInterval, - paymentRemaining, - managementFeeRate); - - newLoanProperties.totalValueOutstanding += accumulatedError; - - if (newLoanProperties.firstPaymentPrincipal <= 0 && - principalOutstanding > 0) - { - // The overpayment has caused the loan to be in a state - // where no further principal can be paid. - JLOG(j.warn()) - << "Loan overpayment would cause loan to be stuck. " - "Rejecting overpayment, but normal payments are unaffected."; - return Unexpected(tesSUCCESS); - } - // Check that the other computed values are valid - if (newLoanProperties.interestOwedToVault < 0 || - newLoanProperties.totalValueOutstanding <= 0 || - newLoanProperties.periodicPayment <= 0) - { - // LCOV_EXCL_START - JLOG(j.warn()) << "Computed loan properties are invalid. Does " - "not compute. TotalValueOutstanding: " - << newLoanProperties.totalValueOutstanding - << ", PeriodicPayment: " - << newLoanProperties.periodicPayment - << ", InterestOwedToVault: " - << newLoanProperties.interestOwedToVault; - return Unexpected(tesSUCCESS); - // LCOV_EXCL_STOP - } - - totalValueOutstandingProxy = - newLoanProperties.totalValueOutstanding + accumulatedError; - principalOutstandingProxy = principalOutstanding; - periodicPaymentProxy = newLoanProperties.periodicPayment; - - XRPL_ASSERT_PARTS( - paymentRemainingProxy == paymentRemaining, - "ripple::detail::doOverpayment", - "paymentRemaining is unchanged"); - paymentRemainingProxy = paymentRemaining; - XRPL_ASSERT_PARTS( - prevPaymentDateProxy == prevPaymentDate, - "ripple::detail::doOverpayment", - "prevPaymentDate is unchanged"); - prevPaymentDateProxy = prevPaymentDate; - XRPL_ASSERT_PARTS( - nextDueDateProxy == nextDueDate, - "ripple::detail::doOverpayment", - "nextDueDate is unchanged"); - nextDueDateProxy = nextDueDate; - - /* - auto const totalInterestOutstandingAfter = - totalValueOutstanding - principalOutstanding; - */ - - return paymentParts; + // The overpayment has caused the loan to be in a state + // where no further principal can be paid. + JLOG(j.warn()) + << "Loan overpayment would cause loan to be stuck. " + "Rejecting overpayment, but normal payments are unaffected."; + return Unexpected(tesSUCCESS); } + + // Check that the other computed values are valid + if (newLoanProperties.periodicPayment <= 0 || + newLoanProperties.totalValueOutstanding <= 0 || + newLoanProperties.managementFeeOwedToBroker <= 0) + { + // LCOV_EXCL_START + JLOG(j.warn()) << "Overpayment not allowed: Computed loan " + "properties are invalid. Does " + "not compute. TotalValueOutstanding: " + << newLoanProperties.totalValueOutstanding + << ", PeriodicPayment : " + << newLoanProperties.periodicPayment + << ", ManagementFeeOwedToBroker: " + << newLoanProperties.managementFeeOwedToBroker; + return Unexpected(tesSUCCESS); + // LCOV_EXCL_STOP + } + + totalValueOutstanding = newLoanProperties.totalValueOutstanding; + periodicPayment = newLoanProperties.periodicPayment; + + return paymentParts; } -/* Handle possible late payments. +template +Expected +computeOverpayment( + A const& asset, + PaymentComponentsPlus const& overpaymentComponents, + NumberProxy& totalValueOutstandingProxy, + NumberProxy& principalOutstandingProxy, + NumberProxy& managementFeeOutstandingProxy, + NumberProxy& periodicPaymentProxy, + TenthBips32 const interestRate, + std::uint32_t const paymentInterval, + std::uint32_t const paymentRemaining, + std::uint32_t const prevPaymentDate, + std::optional const nextDueDate, + TenthBips16 const managementFeeRate, + beast::Journal j) +{ + // Use temp variables to do the payment, so they can be thrown away if + // they don't work + Number totalValueOutstanding = totalValueOutstandingProxy; + Number principalOutstanding = principalOutstandingProxy; + Number managementFeeOutstanding = managementFeeOutstandingProxy; + Number periodicPayment = periodicPaymentProxy; + + auto const ret = tryOverpayment( + asset, + overpaymentComponents, + totalValueOutstanding, + principalOutstanding, + managementFeeOutstanding, + periodicPayment, + interestRate, + paymentInterval, + paymentRemaining, + prevPaymentDate, + nextDueDate, + managementFeeRate, + j); + if (!ret) + return Unexpected(ret.error()); + + auto const& loanPaymentParts = *ret; + + if (principalOutstandingProxy <= principalOutstanding) + { + // LCOV_EXCL_START + JLOG(j.warn()) << "Overpayment not allowed: principal " + << "outstanding did not decrease. Before: " + << *principalOutstandingProxy + << ". After: " << principalOutstanding; + return Unexpected(tesSUCCESS); + // LCOV_EXCL_STOP + } + + XRPL_ASSERT_PARTS( + principalOutstandingProxy - principalOutstanding == + overpaymentComponents.roundedPrincipal, + "ripple::detail::computeOverpayment", + "principal change agrees"); + + XRPL_ASSERT_PARTS( + managementFeeOutstandingProxy - managementFeeOutstanding == + overpaymentComponents.roundedManagementFee && + overpaymentComponents.roundedManagementFee == beast::zero, + "ripple::detail::computeOverpayment", + "no fee change"); + + XRPL_ASSERT_PARTS( + totalValueOutstandingProxy - totalValueOutstanding - + overpaymentComponents.roundedPrincipal == + overpaymentComponents.valueChange, + "ripple::detail::computeOverpayment", + "value change agrees"); + + XRPL_ASSERT_PARTS( + loanPaymentParts.principalPaid == + overpaymentComponents.roundedPrincipal, + "ripple::detail::computeOverpayment", + "principal payment matches"); + XRPL_ASSERT_PARTS( + loanPaymentParts.interestPaid == overpaymentComponents.roundedInterest, + "ripple::detail::computeOverpayment", + "interest payment matches"); + XRPL_ASSERT_PARTS( + loanPaymentParts.valueChange == overpaymentComponents.valueChange, + "ripple::detail::computeOverpayment", + "value change matches"); + XRPL_ASSERT_PARTS( + loanPaymentParts.extraFeePaid == overpaymentComponents.extraFee, + "ripple::detail::computeOverpayment", + "extra fee payment matches"); + XRPL_ASSERT_PARTS( + loanPaymentParts.managementFeePaid == + overpaymentComponents.roundedManagementFee, + "ripple::detail::computeOverpayment", + "management fee payment matches"); + + // Update the loan object (via proxies) + totalValueOutstandingProxy = totalValueOutstanding; + principalOutstandingProxy = principalOutstanding; + managementFeeOutstandingProxy = managementFeeOutstanding; + periodicPaymentProxy = periodicPayment; + + return loanPaymentParts; +} + +std::pair +computeInterestAndFeeParts( + Number const& interest, + TenthBips16 managementFeeRate); + +template +std::pair +computeInterestAndFeeParts( + A const& asset, + Number const& interest, + TenthBips16 managementFeeRate, + std::int32_t loanScale) +{ + auto const fee = computeFee(asset, interest, managementFeeRate, loanScale); + + return std::make_pair(interest - fee, fee); +} + +/** Handle possible late payments. * * If this function processed a late payment, the return value will be * a LoanPaymentParts object. If the loan is not late, the return will be an @@ -538,7 +816,7 @@ doOverpayment( */ template Expected -handleLatePayment( +computeLatePayment( A const& asset, ApplyView const& view, Number const& principalOutstanding, @@ -548,47 +826,56 @@ handleLatePayment( std::int32_t loanScale, Number const& latePaymentFee, STAmount const& amount, + TenthBips16 managementFeeRate, beast::Journal j) { if (!hasExpired(view, nextDueDate)) return Unexpected(tesSUCCESS); // the payment is late - // Late payment interest is only the part of the interest that comes from - // being late, as computed by 3.2.4.1.2. + // Late payment interest is only the part of the interest that comes + // from being late, as computed by 3.2.4.1.2. auto const latePaymentInterest = loanLatePaymentInterest( - asset, principalOutstanding, lateInterestRate, view.parentCloseTime(), - nextDueDate, - loanScale); + nextDueDate); + + auto const [rawLateInterest, rawLateManagementFee] = + computeInterestAndFeeParts(latePaymentInterest, managementFeeRate); + auto const [roundedLateInterest, roundedLateManagementFee] = [&]() { + auto const interest = + roundToAsset(asset, latePaymentInterest, loanScale); + return computeInterestAndFeeParts( + asset, interest, managementFeeRate, loanScale); + }(); + XRPL_ASSERT( - latePaymentInterest >= 0, - "ripple::detail::handleLatePayment : valid late interest"); + roundedLateInterest >= 0, + "ripple::detail::computeLatePayment : valid late interest"); PaymentComponentsPlus const late{ PaymentComponents{ - .rawInterest = periodic.rawInterest + latePaymentInterest, + .rawInterest = periodic.rawInterest + rawLateInterest, .rawPrincipal = periodic.rawPrincipal, - .roundedInterest = periodic.roundedInterest + latePaymentInterest, + .rawManagementFee = periodic.rawManagementFee, + .roundedInterest = periodic.roundedInterest + roundedLateInterest, .roundedPrincipal = periodic.roundedPrincipal, - .roundedPayment = periodic.roundedPayment}, - // A late payment pays both the normal fee, and the extra fee - periodic.fee + latePaymentFee, + .roundedManagementFee = periodic.roundedManagementFee}, + // A late payment pays both the normal fee, and the extra fees + periodic.extraFee + latePaymentFee + roundedLateManagementFee, // A late payment increases the value of the loan by the difference // between periodic and late payment interest - latePaymentInterest}; - auto const totalDue = - late.roundedPrincipal + late.roundedInterest + late.fee; + roundedLateInterest}; + XRPL_ASSERT_PARTS( - isRounded(asset, totalDue, loanScale), - "ripple::detail::handleLatePayment", + isRounded(asset, late.totalDue, loanScale), + "ripple::detail::computeLatePayment", "total due is rounded"); - if (amount < totalDue) + if (amount < late.totalDue) { JLOG(j.warn()) << "Late loan payment amount is insufficient. Due: " - << totalDue << ", paid: " << amount; + << late.totalDue << ", paid: " << amount; return Unexpected(tecINSUFFICIENT_PAYMENT); } @@ -604,10 +891,11 @@ handleLatePayment( */ template Expected -handleFullPayment( +computeFullPayment( A const& asset, ApplyView& view, Number const& principalOutstanding, + Number const& managementFeeOutstanding, Number const& periodicPayment, std::uint32_t paymentRemaining, std::uint32_t prevPaymentDate, @@ -619,6 +907,7 @@ handleFullPayment( Number const& periodicRate, Number const& closePaymentFee, STAmount const& amount, + TenthBips16 managementFeeRate, beast::Journal j) { if (paymentRemaining <= 1) @@ -628,44 +917,64 @@ handleFullPayment( Number const rawPrincipalOutstanding = loanPrincipalFromPeriodicPayment( periodicPayment, periodicRate, paymentRemaining); - auto const totalInterest = calculateFullPaymentInterest( - asset, + // Full payment interest consists of accrued normal interest and the + // prepayment penalty, as computed by 3.2.4.1.4. + auto const fullPaymentInterest = calculateFullPaymentInterest( rawPrincipalOutstanding, periodicRate, view.parentCloseTime(), paymentInterval, prevPaymentDate, startDate, - closeInterestRate, - loanScale); + closeInterestRate); - auto const closeFullPayment = - principalOutstanding + totalInterest + closePaymentFee; + auto const [rawFullInterest, rawFullManagementFee] = + computeInterestAndFeeParts(fullPaymentInterest, managementFeeRate); + auto const + [roundedFullInterest, roundedFullManagementFee, roundedFullExtraFee] = + [&]() { + auto const interest = + roundToAsset(asset, fullPaymentInterest, loanScale); + auto const parts = computeInterestAndFeeParts( + asset, interest, managementFeeRate, loanScale); + // Apply as much of the fee to the outstanding fee, but no + // more + if (parts.second <= managementFeeOutstanding) + return std::make_tuple(parts.first, parts.second, Number{}); + return std::make_tuple( + parts.first, + managementFeeOutstanding, + parts.second - managementFeeOutstanding); + }(); - if (amount < closeFullPayment) + PaymentComponentsPlus const full{ + PaymentComponents{ + .rawInterest = rawFullInterest, + .rawPrincipal = rawPrincipalOutstanding, + .rawManagementFee = rawFullManagementFee, + .roundedInterest = roundedFullInterest, + .roundedPrincipal = principalOutstanding, + .roundedManagementFee = roundedFullManagementFee, + .final = true}, + // A full payment pays the single close payment fee, plus whatever part + // of the computed management fee is not outstanding in the Loan + closePaymentFee + roundedFullExtraFee, + // A full payment decreases the value of the loan by the + // difference between the interest paid and the expected + // outstanding interest return + roundedFullInterest - totalInterestOutstanding}; + + XRPL_ASSERT_PARTS( + isRounded(asset, full.totalDue, loanScale), + "ripple::detail::computeFullPayment", + "total due is rounded"); + + if (amount < full.totalDue) // If the payment is less than the full payment amount, it's not // sufficient to be a full payment, but that's not an error. return Unexpected(tesSUCCESS); - // Make a full payment - - PaymentComponentsPlus const result{ - PaymentComponents{ - .rawInterest = - principalOutstanding + totalInterest - rawPrincipalOutstanding, - .rawPrincipal = rawPrincipalOutstanding, - .roundedInterest = totalInterest, - .roundedPrincipal = principalOutstanding, - .roundedPayment = principalOutstanding + totalInterest, - .final = true}, - // A full payment only pays the single close payment fee - closePaymentFee, - // A full payment decreases the value of the loan by the - // difference between the interest paid and the expected - // outstanding interest return - totalInterest - totalInterestOutstanding}; - - return result; + return full; } } // namespace detail @@ -675,21 +984,162 @@ Number valueMinusFee( A const& asset, Number const& value, - TenthBips32 managementFeeRate, + TenthBips16 managementFeeRate, std::int32_t scale) { - return roundToAsset( - asset, detail::minusFee(value, managementFeeRate), scale); + return value - computeFee(asset, value, managementFeeRate, scale); } -struct LoanProperties +template +PaymentComponents +computePaymentComponents( + A const& asset, + std::int32_t scale, + Number const& totalValueOutstanding, + Number const& principalOutstanding, + Number const& managementFeeOutstanding, + Number const& periodicPayment, + Number const& periodicRate, + std::uint32_t paymentRemaining, + TenthBips16 managementFeeRate) { - Number periodicPayment; - Number totalValueOutstanding; - Number interestOwedToVault; - std::int32_t loanScale; - Number firstPaymentPrincipal; -}; + /* + * This function is derived from the XLS-66 spec, section 3.2.4.1.1 (Regular + * Payment) + */ + XRPL_ASSERT_PARTS( + isRounded(asset, totalValueOutstanding, scale) && + isRounded(asset, principalOutstanding, scale) && + isRounded(asset, managementFeeOutstanding, scale), + "ripple::detail::computePaymentComponents", + "Outstanding values are rounded"); + auto const roundedPeriodicPayment = + roundPeriodicPayment(asset, periodicPayment, scale); + + LoanState const raw = calculateRawLoanState( + periodicPayment, periodicRate, paymentRemaining, managementFeeRate); + + if (paymentRemaining == 1 || + totalValueOutstanding <= roundedPeriodicPayment) + { + // If there's only one payment left, we need to pay off each of the loan + // parts. It's probably impossible for the subtraction to result in a + // negative value, but don't leave anything to chance. + Number interest = std::max( + Number{}, + totalValueOutstanding - principalOutstanding - + managementFeeOutstanding); + + // Pay everything off + return PaymentComponents{ + .rawInterest = raw.interestOutstanding, + .rawPrincipal = raw.principalOutstanding, + .rawManagementFee = raw.managementFeeDue, + .roundedInterest = interest, + .roundedPrincipal = principalOutstanding, + .roundedManagementFee = managementFeeOutstanding, + //.roundedPayment = totalValueOutstanding, + .final = true}; + } + + /* + * From the spec, once the periodicPayment is computed: + * + * The principal and interest portions can be derived as follows: + * interest = principalOutstanding * periodicRate + * principal = periodicPayment - interest + */ + Number const rawInterest = raw.principalOutstanding * periodicRate; + Number const rawPrincipal = periodicPayment - rawInterest; + Number const rawFee = tenthBipsOfValue(rawInterest, managementFeeRate); + XRPL_ASSERT_PARTS( + rawInterest >= 0, + "ripple::detail::computePaymentComponents", + "valid raw interest"); + XRPL_ASSERT_PARTS( + rawPrincipal >= 0 && rawPrincipal <= raw.principalOutstanding, + "ripple::detail::computePaymentComponents", + "valid raw principal"); + XRPL_ASSERT_PARTS( + rawFee >= 0 && rawFee <= raw.managementFeeDue, + "ripple::detail::computePaymentComponents", + "valid raw fee"); + + /* + Critical Calculation: Balancing Principal and Interest Outstanding + + This calculation maintains a delicate balance between keeping + principal outstanding and interest outstanding as close as possible to + reference values. However, we cannot perfectly match the reference + values due to rounding issues. + + Key considerations: + 1. Since the periodic payment is rounded up, we have excess funds + that can be used to pay down the loan faster than the reference + calculation. + + 2. We must ensure that loan repayment is not too fast, otherwise we + will end up with negative principal outstanding or negative + interest outstanding. + + 3. We cannot allow the borrower to repay interest ahead of schedule. + If the borrower makes an overpayment, the interest portion could + go negative, requiring complex recalculation to refund the borrower by + reflecting the overpayment in the principal portion of the loan. + */ + + Number const roundedPrincipal = detail::computeRoundedPrincipalComponent( + asset, + principalOutstanding, + raw.principalOutstanding, + rawPrincipal, + roundedPeriodicPayment, + scale); + + auto const [roundedInterest, roundedFee] = + detail::computeRoundedInterestAndFeeComponents( + asset, + totalValueOutstanding - principalOutstanding, + managementFeeOutstanding, + roundedPrincipal, + raw.interestOutstanding, + raw.managementFeeDue, + roundedPeriodicPayment, + periodicRate, + managementFeeRate, + scale); + + XRPL_ASSERT_PARTS( + roundedInterest >= 0 && isRounded(asset, roundedInterest, scale), + "ripple::detail::computePaymentComponents", + "valid rounded interest"); + XRPL_ASSERT_PARTS( + roundedFee >= 0 && roundedFee <= managementFeeOutstanding && + isRounded(asset, roundedFee, scale), + "ripple::detail::computePaymentComponents", + "valid rounded fee"); + XRPL_ASSERT_PARTS( + roundedPrincipal >= 0 && roundedPrincipal <= principalOutstanding && + roundedPrincipal <= roundedPeriodicPayment && + isRounded(asset, roundedPrincipal, scale), + "ripple::detail::computePaymentComponents", + "valid rounded principal"); + XRPL_ASSERT_PARTS( + roundedPrincipal + roundedInterest + roundedFee <= + roundedPeriodicPayment, + "ripple::detail::computePaymentComponents", + "payment parts fit within payment limit"); + + return PaymentComponents{ + .rawInterest = rawInterest - rawFee, + .rawPrincipal = rawPrincipal, + .rawManagementFee = rawFee, + .roundedInterest = roundedInterest, + .roundedPrincipal = roundedPrincipal, + .roundedManagementFee = roundedFee, + //.roundedPayment = roundedPeriodicPayment + }; +} template LoanProperties @@ -699,7 +1149,7 @@ computeLoanProperties( TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t paymentsRemaining, - TenthBips32 managementFeeRate) + TenthBips16 managementFeeRate) { auto const periodicRate = loanPeriodicRate(interestRate, paymentInterval); XRPL_ASSERT( @@ -733,24 +1183,7 @@ computeLoanProperties( principalOutstanding = roundToAsset( asset, principalOutstanding, loanScale, Number::to_nearest); - auto const firstPaymentPrincipal = [&]() { - // Compute the parts for the first payment. Ensure that the - // principal payment will actually change the principal. - auto const paymentComponents = detail::computePaymentComponents( - asset, - loanScale, - totalValueOutstanding, - principalOutstanding, - periodicPayment, - periodicRate, - paymentsRemaining); - - // The unrounded principal part needs to be large enough to affect the - // principal. What to do if not is left to the caller - return paymentComponents.rawPrincipal; - }(); - - auto const interestOwedToVault = valueMinusFee( + auto const feeOwedToBroker = computeFee( asset, /* * This formula is from the XLS-66 spec, section 3.2.4.2 (Total Loan @@ -760,84 +1193,33 @@ computeLoanProperties( managementFeeRate, loanScale); + auto const firstPaymentPrincipal = [&]() { + // Compute the parts for the first payment. Ensure that the + // principal payment will actually change the principal. + auto const paymentComponents = computePaymentComponents( + asset, + loanScale, + totalValueOutstanding, + principalOutstanding, + feeOwedToBroker, + periodicPayment, + periodicRate, + paymentsRemaining, + managementFeeRate); + + // The unrounded principal part needs to be large enough to affect the + // principal. What to do if not is left to the caller + return paymentComponents.rawPrincipal; + }(); + return LoanProperties{ .periodicPayment = periodicPayment, .totalValueOutstanding = totalValueOutstanding, - .interestOwedToVault = interestOwedToVault, + .managementFeeOwedToBroker = feeOwedToBroker, .loanScale = loanScale, .firstPaymentPrincipal = firstPaymentPrincipal}; } -template -Number -calculateFullPaymentInterest( - A const& asset, - Number const& rawPrincipalOutstanding, - Number const& periodicRate, - NetClock::time_point parentCloseTime, - std::uint32_t paymentInterval, - std::uint32_t prevPaymentDate, - std::uint32_t startDate, - TenthBips32 closeInterestRate, - std::int32_t loanScale) -{ - // If there is more than one payment remaining, see if enough was - // paid for a full payment - auto const accruedInterest = roundToAsset( - asset, - detail::loanAccruedInterest( - rawPrincipalOutstanding, - periodicRate, - parentCloseTime, - startDate, - prevPaymentDate, - paymentInterval), - loanScale); - XRPL_ASSERT( - accruedInterest >= 0, - "ripple::detail::handleFullPayment : valid accrued interest"); - - auto const prepaymentPenalty = roundToAsset( - asset, - tenthBipsOfValue(rawPrincipalOutstanding, closeInterestRate), - loanScale); - XRPL_ASSERT( - prepaymentPenalty >= 0, - "ripple::detail::handleFullPayment : valid prepayment " - "interest"); - return accruedInterest + prepaymentPenalty; -} - -template -Number -calculateFullPaymentInterest( - A const& asset, - Number const& periodicPayment, - Number const& periodicRate, - std::uint32_t paymentRemaining, - NetClock::time_point parentCloseTime, - std::uint32_t paymentInterval, - std::uint32_t prevPaymentDate, - std::uint32_t startDate, - TenthBips32 closeInterestRate, - std::int32_t loanScale) -{ - Number const rawPrincipalOutstanding = - detail::loanPrincipalFromPeriodicPayment( - periodicPayment, periodicRate, paymentRemaining); - - return calculateFullPaymentInterest( - asset, - rawPrincipalOutstanding, - periodicRate, - parentCloseTime, - paymentInterval, - prevPaymentDate, - startDate, - closeInterestRate, - loanScale); -} - #if LOANCOMPLETE template Number @@ -955,21 +1337,18 @@ loanTotalInterestOutstanding( paymentInterval, paymentsRemaining)); } -#endif - template Number loanInterestOutstandingMinusFee( A const& asset, Number const& totalInterestOutstanding, - TenthBips32 managementFeeRate, + TenthBips16 managementFeeRate, std::int32_t scale) { return valueMinusFee( asset, totalInterestOutstanding, managementFeeRate, scale); } -#if LOANCOMPLETE template Number loanInterestOutstandingMinusFee( @@ -979,7 +1358,7 @@ loanInterestOutstandingMinusFee( TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t paymentsRemaining, - TenthBips32 managementFeeRate) + TenthBips16 managementFeeRate) { return loanInterestOutstandingMinusFee( asset, @@ -993,7 +1372,6 @@ loanInterestOutstandingMinusFee( managementFeeRate, scale); } -#endif template Number @@ -1014,6 +1392,7 @@ loanLatePaymentInterest( nextPaymentDueDate), scale); } +#endif template bool @@ -1029,8 +1408,8 @@ loanMakePayment( A const& asset, ApplyView& view, SLE::ref loan, + SLE::const_ref brokerSle, STAmount const& amount, - TenthBips16 managementFeeRate, beast::Journal j) { /* @@ -1047,6 +1426,9 @@ loanMakePayment( return Unexpected(tecKILLED); } + auto totalValueOutstandingProxy = loan->at(sfTotalValueOutstanding); + auto managementFeeOutstandingProxy = loan->at(sfManagementFeeOutstanding); + // Next payment due date must be set unless the loan is complete auto nextDueDateProxy = loan->at(~sfNextPaymentDueDate); if (!nextDueDateProxy) @@ -1056,7 +1438,6 @@ loanMakePayment( } std::int32_t const loanScale = loan->at(sfLoanScale); - auto totalValueOutstandingProxy = loan->at(sfTotalValueOutstanding); TenthBips32 const interestRate{loan->at(sfInterestRate)}; TenthBips32 const lateInterestRate{loan->at(sfLateInterestRate)}; @@ -1066,6 +1447,7 @@ loanMakePayment( Number const latePaymentFee = loan->at(sfLatePaymentFee); Number const closePaymentFee = roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale); + TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; auto const periodicPayment = loan->at(sfPeriodicPayment); @@ -1087,23 +1469,25 @@ loanMakePayment( view.update(loan); detail::PaymentComponentsPlus const periodic{ - detail::computePaymentComponents( + computePaymentComponents( asset, loanScale, totalValueOutstandingProxy, principalOutstandingProxy, + managementFeeOutstandingProxy, periodicPayment, periodicRate, - paymentRemainingProxy), + paymentRemainingProxy, + managementFeeRate), serviceFee}; XRPL_ASSERT_PARTS( - periodic.roundedPrincipal > 0, + periodic.roundedPrincipal >= 0, "ripple::loanMakePayment", - "regular payment pays principal"); + "regular payment valid principal"); // ------------------------------------------------------------- // late payment handling - if (auto const latePaymentComponents = detail::handleLatePayment( + if (auto const latePaymentComponents = detail::computeLatePayment( asset, view, principalOutstandingProxy, @@ -1113,12 +1497,14 @@ loanMakePayment( loanScale, latePaymentFee, amount, + managementFeeRate, j)) { return doPayment( *latePaymentComponents, totalValueOutstandingProxy, principalOutstandingProxy, + managementFeeOutstandingProxy, paymentRemainingProxy, prevPaymentDateProxy, nextDueDateProxy, @@ -1132,13 +1518,16 @@ loanMakePayment( // ------------------------------------------------------------- // full payment handling - auto const totalInterestOutstanding = - totalValueOutstandingProxy - principalOutstandingProxy; + LoanState const roundedLoanState = calculateRoundedLoanState( + totalValueOutstandingProxy, + principalOutstandingProxy, + managementFeeOutstandingProxy); - if (auto const fullPaymentComponents = detail::handleFullPayment( + if (auto const fullPaymentComponents = detail::computeFullPayment( asset, view, principalOutstandingProxy, + managementFeeOutstandingProxy, periodicPayment, paymentRemainingProxy, prevPaymentDateProxy, @@ -1146,15 +1535,17 @@ loanMakePayment( paymentInterval, closeInterestRate, loanScale, - totalInterestOutstanding, + roundedLoanState.interestDue, periodicRate, closePaymentFee, amount, + managementFeeRate, j)) return doPayment( *fullPaymentComponents, totalValueOutstandingProxy, principalOutstandingProxy, + managementFeeOutstandingProxy, paymentRemainingProxy, prevPaymentDateProxy, nextDueDateProxy, @@ -1173,8 +1564,7 @@ loanMakePayment( // This will keep a running total of what is actually paid, if the payment // is sufficient for a single payment - Number totalPaid = - periodic.roundedInterest + periodic.roundedPrincipal + periodic.fee; + Number totalPaid = periodic.totalDue; if (amount < totalPaid) { @@ -1187,6 +1577,7 @@ loanMakePayment( periodic, totalValueOutstandingProxy, principalOutstandingProxy, + managementFeeOutstandingProxy, paymentRemainingProxy, prevPaymentDateProxy, nextDueDateProxy, @@ -1198,15 +1589,17 @@ loanMakePayment( { // Try to make more payments detail::PaymentComponentsPlus const nextPayment{ - detail::computePaymentComponents( + computePaymentComponents( asset, loanScale, totalValueOutstandingProxy, principalOutstandingProxy, + managementFeeOutstandingProxy, periodicPayment, periodicRate, - paymentRemainingProxy), - periodic.fee}; + paymentRemainingProxy, + managementFeeRate), + serviceFee}; XRPL_ASSERT_PARTS( nextPayment.roundedPrincipal > 0, "ripple::loanMakePayment", @@ -1218,19 +1611,16 @@ loanMakePayment( nextPayment.rawPrincipal >= periodic.rawPrincipal, "ripple::loanMakePayment : increasing principal"); - // the fee part doesn't change - auto const due = nextPayment.roundedInterest + - nextPayment.roundedPrincipal + periodic.fee; - - if (amount < totalPaid + due) + if (amount < totalPaid + nextPayment.totalDue) // We're done making payments. break; - totalPaid += due; + totalPaid += nextPayment.totalDue; totalParts += detail::doPayment( nextPayment, totalValueOutstandingProxy, principalOutstandingProxy, + managementFeeOutstandingProxy, paymentRemainingProxy, prevPaymentDateProxy, nextDueDateProxy, @@ -1243,7 +1633,7 @@ loanMakePayment( XRPL_ASSERT_PARTS( totalParts.principalPaid + totalParts.interestPaid + - totalParts.feeToPay == + totalParts.extraFeePaid + totalParts.managementFeePaid == totalPaid, "ripple::loanMakePayment", "payment parts add up"); @@ -1252,7 +1642,7 @@ loanMakePayment( "ripple::loanMakePayment", "no value change"); XRPL_ASSERT_PARTS( - totalParts.feeToPay == periodic.fee * numPayments, + totalParts.extraFeePaid == periodic.extraFee * numPayments, "ripple::loanMakePayment", "fee parts add up"); @@ -1279,33 +1669,47 @@ loanMakePayment( // TODO: Is the overpaymentInterestRate an APR or flat? - Number const interest = - tenthBipsOfValue(payment, overpaymentInterestRate); - Number const roundedInterest = roundToAsset(asset, interest, loanScale); + auto const [rawOverpaymentInterest, rawOverpaymentManagementFee] = + [&]() { + Number const interest = + tenthBipsOfValue(payment, overpaymentInterestRate); + return detail::computeInterestAndFeeParts( + interest, managementFeeRate); + }(); + auto const + [roundedOverpaymentInterest, roundedOverpaymentManagementFee] = + [&]() { + Number const interest = + roundToAsset(asset, rawOverpaymentInterest, loanScale); + return detail::computeInterestAndFeeParts( + asset, interest, managementFeeRate, loanScale); + }(); detail::PaymentComponentsPlus overpaymentComponents{ PaymentComponents{ - .rawInterest = interest, - .rawPrincipal = payment - interest, - .roundedInterest = roundedInterest, - .roundedPrincipal = payment - roundedInterest, - .roundedPayment = payment, + .rawInterest = rawOverpaymentInterest, + .rawPrincipal = payment - rawOverpaymentInterest, + .rawManagementFee = 0, + .roundedInterest = roundedOverpaymentInterest, + .roundedPrincipal = payment - roundedOverpaymentInterest, + .roundedManagementFee = 0, .extra = true}, fee, - roundedInterest}; + roundedOverpaymentInterest}; // Don't process an overpayment if the whole amount (or more!) // gets eaten by fees and interest. if (overpaymentComponents.rawPrincipal > 0 && overpaymentComponents.roundedPrincipal > 0) { + // Can't just use periodicPayment here, because it might change auto periodicPaymentProxy = loan->at(sfPeriodicPayment); - if (auto const overResult = detail::doOverpayment( + if (auto const overResult = detail::computeOverpayment( asset, - view, overpaymentComponents, totalValueOutstandingProxy, principalOutstandingProxy, + managementFeeOutstandingProxy, periodicPaymentProxy, interestRate, paymentInterval, @@ -1335,8 +1739,11 @@ loanMakePayment( isRounded(asset, totalParts.valueChange, loanScale), "ripple::loanMakePayment : loan value change rounded"); XRPL_ASSERT( - isRounded(asset, totalParts.feeToPay, loanScale), - "ripple::loanMakePayment : total fee to pay rounded"); + isRounded(asset, totalParts.extraFeePaid, loanScale), + "ripple::loanMakePayment : extra fee paid rounded"); + XRPL_ASSERT( + isRounded(asset, totalParts.managementFeePaid, loanScale), + "ripple::loanMakePayment : management fee paid rounded"); return totalParts; } diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index 447c940cc0..755c251d3a 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -44,6 +44,130 @@ loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval) (365 * 24 * 60 * 60); } +Number +calculateFullPaymentInterest( + Number const& rawPrincipalOutstanding, + Number const& periodicRate, + NetClock::time_point parentCloseTime, + std::uint32_t paymentInterval, + std::uint32_t prevPaymentDate, + std::uint32_t startDate, + TenthBips32 closeInterestRate) +{ + // If there is more than one payment remaining, see if enough was + // paid for a full payment + auto const accruedInterest = detail::loanAccruedInterest( + rawPrincipalOutstanding, + periodicRate, + parentCloseTime, + startDate, + prevPaymentDate, + paymentInterval); + XRPL_ASSERT( + accruedInterest >= 0, + "ripple::detail::computeFullPaymentInterest : valid accrued interest"); + + auto const prepaymentPenalty = + tenthBipsOfValue(rawPrincipalOutstanding, closeInterestRate); + XRPL_ASSERT( + prepaymentPenalty >= 0, + "ripple::detail::computeFullPaymentInterest : valid prepayment " + "interest"); + + return accruedInterest + prepaymentPenalty; +} + +Number +calculateFullPaymentInterest( + Number const& periodicPayment, + Number const& periodicRate, + std::uint32_t paymentRemaining, + NetClock::time_point parentCloseTime, + std::uint32_t paymentInterval, + std::uint32_t prevPaymentDate, + std::uint32_t startDate, + TenthBips32 closeInterestRate) +{ + Number const rawPrincipalOutstanding = + detail::loanPrincipalFromPeriodicPayment( + periodicPayment, periodicRate, paymentRemaining); + + return calculateFullPaymentInterest( + rawPrincipalOutstanding, + periodicRate, + parentCloseTime, + paymentInterval, + prevPaymentDate, + startDate, + closeInterestRate); +} + +LoanState +calculateRawLoanState( + Number const& periodicPayment, + Number const& periodicRate, + std::uint32_t const paymentRemaining, + TenthBips16 const managementFeeRate) +{ + Number const rawValueOutstanding = periodicPayment * paymentRemaining; + Number const rawPrincipalOutstanding = + detail::loanPrincipalFromPeriodicPayment( + periodicPayment, periodicRate, paymentRemaining); + Number const rawInterestOutstanding = + rawValueOutstanding - rawPrincipalOutstanding; + Number const rawManagementFeeOutstanding = + tenthBipsOfValue(rawInterestOutstanding, managementFeeRate); + + return LoanState{ + .valueOutstanding = rawValueOutstanding, + .principalOutstanding = rawPrincipalOutstanding, + .interestOutstanding = rawInterestOutstanding, + .interestDue = rawInterestOutstanding - rawManagementFeeOutstanding, + .managementFeeDue = rawManagementFeeOutstanding}; +}; + +LoanState +calculateRawLoanState( + Number const& periodicPayment, + TenthBips32 interestRate, + std::uint32_t paymentInterval, + std::uint32_t const paymentRemaining, + TenthBips16 const managementFeeRate) +{ + return calculateRawLoanState( + periodicPayment, + loanPeriodicRate(interestRate, paymentInterval), + paymentRemaining, + managementFeeRate); +} + +LoanState +calculateRoundedLoanState( + Number const& totalValueOutstanding, + Number const& principalOutstanding, + Number const& managementFeeOutstanding) +{ + // This implementation is pretty trivial, but ensures the calculations are + // consistent everywhere, and reduces copy/paste errors. + Number const interestOutstanding = + totalValueOutstanding - principalOutstanding; + return { + .valueOutstanding = totalValueOutstanding, + .principalOutstanding = principalOutstanding, + .interestOutstanding = interestOutstanding, + .interestDue = interestOutstanding - managementFeeOutstanding, + .managementFeeDue = managementFeeOutstanding}; +} + +LoanState +calculateRoundedLoanState(SLE::const_ref loan) +{ + return calculateRoundedLoanState( + loan->at(sfTotalValueOutstanding), + loan->at(sfPrincipalOutstanding), + loan->at(sfManagementFeeOutstanding)); +} + namespace detail { Number @@ -128,6 +252,17 @@ loanPrincipalFromPeriodicPayment( computePaymentFactor(periodicRate, paymentsRemaining); } +std::pair +computeInterestAndFeeParts( + Number const& interest, + TenthBips16 managementFeeRate) +{ + auto const fee = tenthBipsOfValue(interest, managementFeeRate); + + // No error tracking needed here because this is extra + return std::make_pair(interest - fee, fee); +} + Number loanLatePaymentInterest( Number const& principalOutstanding, diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 6137c96b06..17a1fc8013 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -2486,7 +2486,7 @@ ValidLoan::finalize( &sfClosePaymentFee, &sfPrincipalOutstanding, &sfTotalValueOutstanding, - &sfInterestOwed}) + &sfManagementFeeOutstanding}) { if (after->at(*field) < 0) { diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index df866b1237..f0a69820a6 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -133,6 +133,13 @@ LoanManage::preclaim(PreclaimContext const& ctx) return tesSUCCESS; } +Number +owedToVault(SLE::ref loanSle) +{ + return loanSle->at(sfTotalValueOutstanding) - + loanSle->at(sfManagementFeeOutstanding); +} + TER LoanManage::defaultLoan( ApplyView& view, @@ -148,10 +155,13 @@ LoanManage::defaultLoan( auto brokerDebtTotalProxy = brokerSle->at(sfDebtTotal); auto principalOutstandingProxy = loanSle->at(sfPrincipalOutstanding); - auto interestOwedProxy = loanSle->at(sfInterestOwed); + auto managementFeeOutstandingProxy = + loanSle->at(sfManagementFeeOutstanding); - Number const totalDefaultAmount = - principalOutstandingProxy + interestOwedProxy; + auto totalValueOutstandingProxy = loanSle->at(sfTotalValueOutstanding); + auto paymentRemainingProxy = loanSle->at(sfPaymentRemaining); + + Number const totalDefaultAmount = owedToVault(loanSle); // Apply the First-Loss Capital to the Default Amount TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)}; @@ -269,10 +279,10 @@ LoanManage::defaultLoan( // Update the Loan object: loanSle->setFlag(lsfLoanDefault); - loanSle->at(sfTotalValueOutstanding) = 0; - loanSle->at(sfPaymentRemaining) = 0; + totalValueOutstandingProxy = 0; + paymentRemainingProxy = 0; principalOutstandingProxy = 0; - interestOwedProxy = 0; + managementFeeOutstandingProxy = 0; loanSle->at(~sfNextPaymentDueDate) = std::nullopt; view.update(loanSle); @@ -294,8 +304,8 @@ LoanManage::impairLoan( SLE::ref vaultSle, beast::Journal j) { - Number const lossUnrealized = - loanSle->at(sfPrincipalOutstanding) + loanSle->at(sfInterestOwed); + Number const lossUnrealized = owedToVault(loanSle); + // Update the Vault object(set "paper loss") auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized); vaultLossUnrealizedProxy += lossUnrealized; @@ -333,8 +343,7 @@ LoanManage::unimpairLoan( { // Update the Vault object(clear "paper loss") auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized); - Number const lossReversed = - loanSle->at(sfPrincipalOutstanding) + loanSle->at(sfInterestOwed); + Number const lossReversed = owedToVault(loanSle); if (vaultLossUnrealizedProxy < lossReversed) { // LCOV_EXCL_START diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 0985b194cd..c25c8fffb8 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -91,7 +91,6 @@ LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx) return normalCost; if (auto const fullInterest = calculateFullPaymentInterest( - asset, loanSle->at(sfPeriodicPayment), loanPeriodicRate( TenthBips32(loanSle->at(sfInterestRate)), @@ -101,8 +100,7 @@ LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx) loanSle->at(sfPaymentInterval), loanSle->at(sfPreviousPaymentDate), loanSle->at(sfStartDate), - TenthBips32(loanSle->at(sfCloseInterestRate)), - scale); + TenthBips32(loanSle->at(sfCloseInterestRate))); amount > loanSle->at(sfPrincipalOutstanding) + fullInterest + loanSle->at(sfClosePaymentFee)) return normalCost; @@ -269,19 +267,8 @@ LoanPay::doApply() LoanManage::unimpairLoan(view, loanSle, vaultSle, j_); } - TenthBips16 managementFeeRate{brokerSle->at(sfManagementFeeRate)}; - auto const managementFeeOutstanding = [&]() { - auto const m = loanSle->at(sfTotalValueOutstanding) - - loanSle->at(sfPrincipalOutstanding) - loanSle->at(sfInterestOwed); - // It shouldn't be possible for this to result in a negative number, but - // with overpayments, who knows? - if (m < 0) - return Number{}; - return m; - }(); - Expected paymentParts = - loanMakePayment(asset, view, loanSle, amount, managementFeeRate, j_); + loanMakePayment(asset, view, loanSle, brokerSle, amount, j_); if (!paymentParts) { @@ -312,11 +299,16 @@ LoanPay::doApply() "ripple::LoanPay::doApply", "valid principal paid"); XRPL_ASSERT_PARTS( - paymentParts->feeToPay >= 0, + paymentParts->extraFeePaid >= 0, "ripple::LoanPay::doApply", "valid fee paid"); + XRPL_ASSERT_PARTS( + paymentParts->managementFeePaid >= 0, + "ripple::LoanPay::doApply", + "valid management fee paid"); + if (paymentParts->principalPaid < 0 || paymentParts->interestPaid < 0 || - paymentParts->feeToPay < 0) + paymentParts->extraFeePaid < 0 || paymentParts->managementFeePaid < 0) { // LCOV_EXCL_START JLOG(j_.fatal()) << "Loan payment computation returned invalid values."; @@ -328,54 +320,29 @@ LoanPay::doApply() // LoanBroker object state changes view.update(brokerSle); - auto interestOwedProxy = loanSle->at(sfInterestOwed); + auto assetsAvailableProxy = vaultSle->at(sfAssetsAvailable); + // The vault may be at a different scale than the loan. Reduce rounding + // errors during the payment by rounding some of the values to that scale. + auto const vaultScale = assetsAvailableProxy->value().exponent(); - auto const [managementFee, interestPaidForDebt, interestPaidExtra] = [&]() { - auto const interestOwed = - paymentParts->interestPaid - paymentParts->valueChange; - auto const interestPaidExtra = paymentParts->valueChange; - - auto const managementFeeOwed = std::min( - managementFeeOutstanding, - roundToAsset( - asset, - tenthBipsOfValue(interestOwed, managementFeeRate), - loanScale)); - auto const managementFeeExtra = roundToAsset( - asset, - tenthBipsOfValue(interestPaidExtra, managementFeeRate), - loanScale); - auto const interestForDebt = interestOwed - managementFeeOwed; - auto const interestExtra = interestPaidExtra - managementFeeExtra; - auto const owed = *interestOwedProxy; - if (interestForDebt > owed) - return std::make_tuple( - interestOwed - owed + managementFeeExtra, owed, interestExtra); - return std::make_tuple( - managementFeeOwed + managementFeeExtra, - interestForDebt, - interestExtra); - }(); - XRPL_ASSERT_PARTS( - managementFee >= 0 && interestPaidForDebt >= 0 && - interestPaidExtra >= 0 && - (managementFee + interestPaidForDebt + interestPaidExtra == - paymentParts->interestPaid) && - isRounded(asset, managementFee, loanScale) && - isRounded(asset, interestPaidForDebt, loanScale) && - isRounded(asset, interestPaidExtra, loanScale), - "ripple::LoanPay::doApply", - "management fee computation is valid"); + auto const totalPaidToVaultRaw = + paymentParts->principalPaid + paymentParts->interestPaid; + auto const totalPaidToVaultRounded = + roundToAsset(asset, totalPaidToVaultRaw, vaultScale, Number::downward); auto const totalPaidToVaultForDebt = - paymentParts->principalPaid + interestPaidForDebt; - auto const totalPaidToVault = totalPaidToVaultForDebt + interestPaidExtra; + totalPaidToVaultRaw - paymentParts->valueChange; - auto const totalPaidToBroker = paymentParts->feeToPay + managementFee; + auto const totalPaidToBroker = + paymentParts->managementFeePaid + paymentParts->extraFeePaid; + auto const totalPaid = totalPaidToVaultRaw + totalPaidToBroker; + auto const totalParts = paymentParts->principalPaid + + paymentParts->interestPaid + paymentParts->managementFeePaid + + paymentParts->extraFeePaid; XRPL_ASSERT_PARTS( - (totalPaidToVault + totalPaidToBroker) == + (totalPaidToVaultRaw + totalPaidToBroker) == (paymentParts->principalPaid + paymentParts->interestPaid + - paymentParts->feeToPay), + paymentParts->managementFeePaid + paymentParts->extraFeePaid), "ripple::LoanPay::doApply", "payments add up"); @@ -398,32 +365,23 @@ LoanPay::doApply() // Vault object state changes view.update(vaultSle); - // auto const available = *vaultSle->at(sfAssetsAvailable); - // auto const total = *vaultSle->at(sfAssetsTotal); - // auto const unavailable = total - available; + { + auto assetsTotalProxy = vaultSle->at(sfAssetsTotal); - vaultSle->at(sfAssetsAvailable) += totalPaidToVault; - vaultSle->at(sfAssetsTotal) += interestPaidExtra; - interestOwedProxy -= interestPaidForDebt; - XRPL_ASSERT_PARTS( - *vaultSle->at(sfAssetsAvailable) <= *vaultSle->at(sfAssetsTotal), - "ripple::LoanPay::doApply", - "assets available must not be greater than assets outstanding"); + assetsAvailableProxy += totalPaidToVaultRounded; + assetsTotalProxy += paymentParts->valueChange; - // auto const available = *vaultSle->at(sfAssetsAvailable); - // auto const total = *vaultSle->at(sfAssetsTotal); - // auto const unavailable = total - available; + XRPL_ASSERT_PARTS( + *assetsAvailableProxy <= *assetsTotalProxy, + "ripple::LoanPay::doApply", + "assets available must not be greater than assets outstanding"); + } // Move funds XRPL_ASSERT_PARTS( - totalPaidToVault + totalPaidToBroker <= amount, + totalPaidToVaultRounded + totalPaidToBroker <= amount, "ripple::LoanPay::doApply", "amount is sufficient"); - XRPL_ASSERT_PARTS( - totalPaidToVault + totalPaidToBroker <= paymentParts->principalPaid + - paymentParts->interestPaid + paymentParts->feeToPay, - "ripple::LoanPay::doApply", - "payment agreement"); if (!sendBrokerFeeToOwner) { @@ -452,7 +410,7 @@ LoanPay::doApply() view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); #endif - if (totalPaidToVault != beast::zero) + if (totalPaidToVaultRounded != beast::zero) { if (auto const ter = requireAuth( view, asset, vaultPseudoAccount, AuthType::StrongAuth)) @@ -484,7 +442,7 @@ LoanPay::doApply() view, account_, asset, - {{vaultPseudoAccount, totalPaidToVault}, + {{vaultPseudoAccount, totalPaidToVaultRounded}, {brokerPayee, totalPaidToBroker}}, j_, WaiveTransferFee::Yes)) diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 5bd544bcf3..af18e12d27 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -341,7 +341,7 @@ LoanSet::doApply() interestRate, paymentInterval, paymentTotal, - TenthBips32{brokerSle->at(sfManagementFeeRate)}); + TenthBips16{brokerSle->at(sfManagementFeeRate)}); // Check that relevant values won't lose precision. This is mostly only // relevant for IOU assets. @@ -427,7 +427,7 @@ LoanSet::doApply() } // Check that the other computed values are valid - if (properties.interestOwedToVault < 0 || + if (properties.managementFeeOwedToBroker < 0 || properties.totalValueOutstanding <= 0 || properties.periodicPayment <= 0) { @@ -438,12 +438,16 @@ LoanSet::doApply() // LCOV_EXCL_STOP } + LoanState const state = calculateRoundedLoanState( + properties.totalValueOutstanding, + principalRequested, + properties.managementFeeOwedToBroker); + auto const originationFee = tx[~sfLoanOriginationFee].value_or(Number{}); auto const loanAssetsToBorrower = principalRequested - originationFee; - auto const newDebtDelta = - principalRequested + properties.interestOwedToVault; + auto const newDebtDelta = principalRequested + state.interestDue; auto const newDebtTotal = brokerSle->at(sfDebtTotal) + newDebtDelta; if (auto const debtMaximum = brokerSle->at(sfDebtMaximum); debtMaximum != 0 && debtMaximum < newDebtTotal) @@ -528,8 +532,7 @@ LoanSet::doApply() WaiveTransferFee::Yes)) return ter; - // The portion of the loan interest that will go to the vault (total - // interest minus the management fee) + // Get shortcuts to the loan property values auto const startDate = view.info().closeTime.time_since_epoch().count(); auto loanSequenceProxy = brokerSle->at(sfLoanSequence); @@ -569,7 +572,7 @@ LoanSet::doApply() loan->at(sfPrincipalOutstanding) = principalRequested; loan->at(sfPeriodicPayment) = properties.periodicPayment; loan->at(sfTotalValueOutstanding) = properties.totalValueOutstanding; - loan->at(sfInterestOwed) = properties.interestOwedToVault; + loan->at(sfManagementFeeOutstanding) = properties.managementFeeOwedToBroker; loan->at(sfPreviousPaymentDate) = 0; loan->at(sfNextPaymentDueDate) = startDate + paymentInterval; loan->at(sfPaymentRemaining) = paymentTotal; @@ -577,7 +580,7 @@ LoanSet::doApply() // Update the balances in the vault vaultSle->at(sfAssetsAvailable) -= principalRequested; - vaultSle->at(sfAssetsTotal) += properties.interestOwedToVault; + vaultSle->at(sfAssetsTotal) += state.interestDue; XRPL_ASSERT_PARTS( *vaultSle->at(sfAssetsAvailable) <= *vaultSle->at(sfAssetsTotal), "ripple::LoanSet::doApply", From 92431e11c8475de0afb167c53650a7107cbbaf7e Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 21 Oct 2025 18:05:56 -0400 Subject: [PATCH 137/291] Fix build error - unused variables --- src/xrpld/app/tx/detail/LoanPay.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index c25c8fffb8..8c36ddc9a6 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -335,10 +335,6 @@ LoanPay::doApply() auto const totalPaidToBroker = paymentParts->managementFeePaid + paymentParts->extraFeePaid; - auto const totalPaid = totalPaidToVaultRaw + totalPaidToBroker; - auto const totalParts = paymentParts->principalPaid + - paymentParts->interestPaid + paymentParts->managementFeePaid + - paymentParts->extraFeePaid; XRPL_ASSERT_PARTS( (totalPaidToVaultRaw + totalPaidToBroker) == (paymentParts->principalPaid + paymentParts->interestPaid + From 587cb2533c23096dda118ee01c17f0cb16ec25a3 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 21 Oct 2025 18:12:52 -0400 Subject: [PATCH 138/291] Fix build error - XRPL_ASSERT Antithesis expansion --- src/xrpld/app/misc/LendingHelpers.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 475cc39ca7..4c7d073fc3 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -323,7 +323,7 @@ computeRoundedInterestComponent( { // Start by just using the non-principal part of the payment for interest Number roundedInterest = roundedPeriodicPayment - roundedPrincipal; - XRPL_ASSERT( + XRPL_ASSERT_PARTS( isRounded(asset, roundedInterest, scale), "ripple::detail::computeRoundedInterestComponent", "initial interest computation is rounded"); From 8757022312a67014b1f9f99f1e1ef1191495fc10 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Tue, 21 Oct 2025 19:23:27 -0400 Subject: [PATCH 139/291] test: Add unit-test to verify correct behavior on broker deep freeze (#5904) * Add unit-test to verify correct behavior on broker deep freeze --- src/test/app/Loan_test.cpp | 90 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 093e41f835..a4aaf47272 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -2615,6 +2615,95 @@ class Loan_test : public beast::unit_test::suite } } + void + testServiceFeeOnBrokerDeepFreeze() + { + testcase << "Service Fee On Broker Deep Freeze"; + using namespace jtx; + using namespace loan; + Account const issuer("issuer"); + Account const borrower("borrower"); + Account const broker("broker"); + auto const IOU = issuer["IOU"]; + + for (bool const deepFreeze : {true, false}) + { + Env env(*this); + + auto getCoverBalance = [&](BrokerInfo const& brokerInfo, + auto const& accountField) { + if (auto const le = + env.le(keylet::loanbroker(brokerInfo.brokerID)); + BEAST_EXPECT(le)) + { + auto const account = le->at(accountField); + if (auto const sleLine = env.le(keylet::line(account, IOU)); + BEAST_EXPECT(sleLine)) + { + STAmount balance = sleLine->at(sfBalance); + if (account > issuer.id()) + balance.negate(); + return balance; + } + } + return STAmount{IOU}; + }; + + env.fund(XRP(20'000), issuer, broker, borrower); + env.close(); + + env(trust(broker, IOU(20'000'000))); + env(pay(issuer, broker, IOU(10'000'000))); + env(trust(borrower, IOU(20'000'000))); + env.close(); + + auto const brokerInfo = createVaultAndBroker(env, IOU, broker); + + BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == IOU(1'000)); + + auto const keylet = keylet::loan(brokerInfo.brokerID, 1); + + env(set(borrower, brokerInfo.brokerID, 10'000), + sig(sfCounterpartySignature, broker), + loanServiceFee(IOU(100).value()), + paymentInterval(100), + fee(XRP(100))); + env.close(); + if (auto const le = env.le(keylet::loan(keylet.key)); + BEAST_EXPECT(le)) + { + if (deepFreeze) + { + env(trust( + issuer, + broker["IOU"](0), + tfSetFreeze | tfSetDeepFreeze)); + env.close(); + } + + env(pay(borrower, keylet.key, IOU(10'100)), fee(XRP(100))); + env.close(); + + if (deepFreeze) + { + // The fee goes to the broker pseudo-account + BEAST_EXPECT( + getCoverBalance(brokerInfo, sfAccount) == IOU(1'100)); + BEAST_EXPECT( + getCoverBalance(brokerInfo, sfOwner) == IOU(8'999'000)); + } + else + { + // The fee goes to the broker account + BEAST_EXPECT( + getCoverBalance(brokerInfo, sfOwner) == IOU(8'999'100)); + BEAST_EXPECT( + getCoverBalance(brokerInfo, sfAccount) == IOU(1'000)); + } + } + }; + } + void testBasicMath() { @@ -2634,6 +2723,7 @@ public: testBatchBypassCounterparty(); testWrongMaxDebtBehavior(); testLoanPayComputePeriodicPaymentValidRateInvariant(); + testServiceFeeOnBrokerDeepFreeze(); testRPC(); testBasicMath(); From f6098fd80bb7c9ced3cdfd7b61d6d26599e1766b Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 21 Oct 2025 20:01:55 -0400 Subject: [PATCH 140/291] Expand use of Asset::integral() --- include/xrpl/protocol/STAmount.h | 11 ++++++++++- src/libxrpl/protocol/STAmount.cpp | 4 ++-- src/test/app/Loan_test.cpp | 6 ++---- src/test/app/Vault_test.cpp | 6 +++--- src/test/jtx/amount.h | 12 ++++++++++++ src/xrpld/app/tx/detail/LoanManage.cpp | 2 +- 6 files changed, 30 insertions(+), 11 deletions(-) diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index 560059e49a..eb74a56805 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -174,6 +174,9 @@ public: int exponent() const noexcept; + bool + integral() const noexcept; + bool native() const noexcept; @@ -454,6 +457,12 @@ STAmount::exponent() const noexcept return mOffset; } +inline bool +STAmount::integral() const noexcept +{ + return mAsset.integral(); +} + inline bool STAmount::native() const noexcept { @@ -735,7 +744,7 @@ roundToAsset( { NumberRoundModeGuard mg(rounding); STAmount const ret{asset, value}; - if (ret.asset().native() || !ret.asset().holds()) + if (ret.integral()) return ret; // Not that the ctor will round integral types (XRP, MPT) via canonicalize, // so no extra work is needed for those. diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 74f4576831..289abb0707 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -321,7 +321,7 @@ STAmount::xrp() const IOUAmount STAmount::iou() const { - if (native() || !holds()) + if (integral()) Throw("Cannot return non-IOU STAmount as IOUAmount"); auto mantissa = static_cast(mValue); @@ -1513,7 +1513,7 @@ STAmount roundToScale(STAmount value, std::int32_t scale, Number::rounding_mode rounding) { // Nothing to do for intgral types. - if (value.asset().native() || !value.asset().holds()) + if (value.integral()) return value; // If the value's exponent is greater than or equal to the scale, then diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index a4aaf47272..d5b1d6bb41 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -193,9 +193,7 @@ class Loan_test : public beast::unit_test::suite auto const available = vaultSle->at(sfAssetsAvailable); env.test.BEAST_EXPECT( total == available || - (!broker.asset.raw().native() && - broker.asset.raw().holds() && - available != 0 && + (!broker.asset.integral() && available != 0 && ((total - available) / available < Number(1, -6)))); env.test.BEAST_EXPECT( @@ -1750,7 +1748,7 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT( paymentComponents.final || diff == beast::zero || (diff > beast::zero && - ((broker.asset.raw().integral() && + ((broker.asset.integral() && (static_cast(diff) < 3)) || (totalDue.exponent() - diff.exponent() > 8)))); diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index b5f94f38d1..ce4f88bb38 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -84,14 +84,14 @@ class Vault_test : public beast::unit_test::suite this]() -> std::tuple { auto const vault = env.le(keylet); BEAST_EXPECT(vault != nullptr); - if (asset.raw().holds() && !asset.raw().native()) + if (!asset.integral()) BEAST_EXPECT(vault->at(sfScale) == 6); else BEAST_EXPECT(vault->at(sfScale) == 0); auto const shares = env.le(keylet::mptIssuance(vault->at(sfShareMPTID))); BEAST_EXPECT(shares != nullptr); - if (asset.raw().holds() && !asset.raw().native()) + if (!asset.integral()) BEAST_EXPECT(shares->at(sfAssetScale) == 6); else BEAST_EXPECT(shares->at(sfAssetScale) == 0); @@ -446,7 +446,7 @@ class Vault_test : public beast::unit_test::suite } } - if (!asset.raw().native() && asset.raw().holds()) + if (!asset.integral()) { testcase(prefix + " temporary authorization for 3rd party"); env(trust(erin, asset(1000))); diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h index f413d87c5a..f25f292531 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -233,6 +233,18 @@ public: return {asset_}; } + bool + integral() const + { + return asset_.integral(); + } + + bool + native() const + { + return asset_.native(); + } + template bool holds() const diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index f0a69820a6..00e742f4bb 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -203,7 +203,7 @@ LoanManage::defaultLoan( // Capital and any unclaimed funds amount: vaultAssetsAvailableProxy += defaultCovered; if (*vaultAssetsAvailableProxy > *vaultAssetsTotalProxy && - !vaultAsset.native() && vaultAsset.holds()) + !vaultAsset.integral()) { auto const difference = vaultAssetsAvailableProxy - vaultAssetsTotalProxy; From 5da586bffc272afb4dcac1a2d41b982503bc466f Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 21 Oct 2025 23:27:48 -0400 Subject: [PATCH 141/291] Add support for explicit overpayment flag in LoanPay - Uses the same name and value as for LoanSet: tfLoanOverpayment. - Untested. - Also create several placeholders for missing test cases. --- include/xrpl/protocol/TxFlags.h | 8 ++- src/test/app/Loan_test.cpp | 78 +++++++++++++++++++++++++++++ src/xrpld/app/misc/LendingHelpers.h | 5 +- src/xrpld/app/tx/detail/LoanPay.cpp | 23 ++++++++- src/xrpld/app/tx/detail/LoanPay.h | 3 ++ 5 files changed, 111 insertions(+), 6 deletions(-) diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index 3f2ac2c1ce..f74039ccfd 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -285,10 +285,14 @@ constexpr std::uint32_t tfIndependent = 0x00080000; constexpr std::uint32_t const tfBatchMask = ~(tfUniversal | tfAllOrNothing | tfOnlyOne | tfUntilFailure | tfIndependent) | tfInnerBatchTxn; -// LoanSet flags: -// True, indicates the loan supports overpayments +// LoanSet and LoanPay flags: +// LoanSet: True, indicates the loan supports overpayments +// LoanPay: True, indicates any excess in this payment can be used +// as an overpayment. False, no overpayments will be taken. constexpr std::uint32_t const tfLoanOverpayment = 0x00010000; +// Use two separate mask variables in case the set of flags diverges constexpr std::uint32_t const tfLoanSetMask = ~(tfUniversal | tfLoanOverpayment); +constexpr std::uint32_t const tfLoanPayMask = ~(tfUniversal | tfLoanOverpayment); // LoanManage flags: constexpr std::uint32_t const tfLoanDefault = 0x00010000; diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index d5b1d6bb41..4617050388 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -1330,6 +1330,19 @@ class Loan_test : public beast::unit_test::suite env(pay(evan, loanKeylet.key, broker.asset(500)), ter(tecNO_PERMISSION)); + // TODO: Write a general "isFlag" function? See STObject::isFlag. + // Maybe add a static overloaded member? + if (!(state.flags & lsfLoanOverpayment)) + { + // If the loan does not allow overpayments, send a payment that + // tries to make an overpayment + env(pay(borrower, + loanKeylet.key, + broker.asset(state.periodicPayment * 2), + tfLoanOverpayment), + ter(temINVALID_FLAG)); + } + { auto const otherAsset = broker.asset.raw() == assets[0].raw() ? assets[1] @@ -1845,6 +1858,71 @@ class Loan_test : public beast::unit_test::suite env(manage(lender, loanKeylet.key, tfLoanDefault), ter(tecNO_PERMISSION)); }); + +#if LOANCOMPLETE + // TODO + + lifecycle( + caseLabel, + "Loan overpayment allowed - Explicit overpayment", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + [&](Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus) { throw 0; }); + + lifecycle( + caseLabel, + "Loan overpayment prohibited - Late payment", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + [&](Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus) { throw 0; }); + + lifecycle( + caseLabel, + "Loan overpayment allowed - Late payment", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + [&](Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus) { throw 0; }); + + lifecycle( + caseLabel, + "Loan overpayment allowed - Late payment and overpayment", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + [&](Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus) { throw 0; }); + +#endif } void diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 4c7d073fc3..b843bfc344 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -1410,6 +1410,7 @@ loanMakePayment( SLE::ref loan, SLE::const_ref brokerSle, STAmount const& amount, + bool const overpaymentAllowed, beast::Journal j) { /* @@ -1648,8 +1649,8 @@ loanMakePayment( // ------------------------------------------------------------- // overpayment handling - if (loan->isFlag(lsfLoanOverpayment) && paymentRemainingProxy > 0 && - nextDueDateProxy && totalPaid < amount) + if (overpaymentAllowed && loan->isFlag(lsfLoanOverpayment) && + paymentRemainingProxy > 0 && nextDueDateProxy && totalPaid < amount) { TenthBips32 const overpaymentInterestRate{ loan->at(sfOverpaymentInterestRate)}; diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 8c36ddc9a6..54e79a2cb7 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -30,6 +30,12 @@ LoanPay::checkExtraFeatures(PreflightContext const& ctx) return checkLendingProtocolDependencies(ctx); } +std::uint32_t +LoanPay::getFlagsMask(PreflightContext const& ctx) +{ + return tfLoanPayMask; +} + NotTEC LoanPay::preflight(PreflightContext const& ctx) { @@ -139,6 +145,13 @@ LoanPay::preclaim(PreclaimContext const& ctx) return tecNO_PERMISSION; } + if (tx.isFlag(tfLoanOverpayment) && !loanSle->isFlag(lsfLoanOverpayment)) + { + JLOG(ctx.j.warn()) + << "Requested overpayment on a loan that doesn't allow it"; + return temINVALID_FLAG; + } + auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding); TenthBips32 const interestRate{loanSle->at(sfInterestRate)}; auto const paymentRemaining = loanSle->at(sfPaymentRemaining); @@ -267,8 +280,14 @@ LoanPay::doApply() LoanManage::unimpairLoan(view, loanSle, vaultSle, j_); } - Expected paymentParts = - loanMakePayment(asset, view, loanSle, brokerSle, amount, j_); + Expected paymentParts = loanMakePayment( + asset, + view, + loanSle, + brokerSle, + amount, + tx.isFlag(tfLoanOverpayment), + j_); if (!paymentParts) { diff --git a/src/xrpld/app/tx/detail/LoanPay.h b/src/xrpld/app/tx/detail/LoanPay.h index 11045f90da..f6255e5bc7 100644 --- a/src/xrpld/app/tx/detail/LoanPay.h +++ b/src/xrpld/app/tx/detail/LoanPay.h @@ -36,6 +36,9 @@ public: static bool checkExtraFeatures(PreflightContext const& ctx); + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); From 92144efbdf390afde7a072cdef07f9518a986bb2 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 22 Oct 2025 00:38:05 -0400 Subject: [PATCH 142/291] Check that the borrower has sufficient funds to make the LoanPay --- src/test/app/Loan_test.cpp | 31 ++++++++++++---- src/xrpld/app/tx/detail/LoanPay.cpp | 57 ++++++++++++++++++++++++----- 2 files changed, 71 insertions(+), 17 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 4617050388..9fd0f8541a 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -594,7 +594,7 @@ class Loan_test : public beast::unit_test::suite { // Need to account for fees if the loan is in XRP PrettyAmount adjustment = broker.asset(0); - if (broker.asset.raw().native()) + if (broker.asset.native()) { adjustment = 2 * env.current()->fees().base; } @@ -757,7 +757,7 @@ class Loan_test : public beast::unit_test::suite if (deleter == borrower) { // Need to account for fees if the loan is in XRP - if (broker.asset.raw().native()) + if (broker.asset.native()) { adjustment = env.current()->fees().base; } @@ -1061,12 +1061,12 @@ class Loan_test : public beast::unit_test::suite TER> { // Freeze / lock the asset std::function empty; - if (broker.asset.raw().native()) + if (broker.asset.native()) { // XRP can't be frozen return std::make_tuple(empty, empty, empty, tesSUCCESS); } - else if (broker.asset.raw().holds()) + else if (broker.asset.holds()) { auto freeze = [&](Account const& holder) { env(trust(issuer, holder[iouCurrency](0), tfSetFreeze)); @@ -1365,15 +1365,24 @@ class Loan_test : public beast::unit_test::suite // taken auto const transactionAmount = payoffAmount + broker.asset(10); + // Send a transaction that tries to pay more than the borrowers's + // balance + env(pay(borrower, + loanKeylet.key, + STAmount{ + broker.asset, + borrowerBalanceBeforePayment.number() * 2}), + ter(tecINSUFFICIENT_FUNDS)); + env(pay(borrower, loanKeylet.key, transactionAmount)); env.close(); // Need to account for fees if the loan is in XRP PrettyAmount adjustment = broker.asset(0); - if (broker.asset.raw().native()) + if (broker.asset.native()) { - adjustment = env.current()->fees().base; + adjustment = env.current()->fees().base * 2; } state.paymentRemaining = 0; @@ -1814,7 +1823,7 @@ class Loan_test : public beast::unit_test::suite // Need to account for fees if the loan is in XRP PrettyAmount adjustment = broker.asset(0); - if (broker.asset.raw().native()) + if (broker.asset.native()) { adjustment = env.current()->fees().base; } @@ -2730,7 +2739,6 @@ class Loan_test : public beast::unit_test::suite env(trust(broker, IOU(20'000'000))); env(pay(issuer, broker, IOU(10'000'000))); - env(trust(borrower, IOU(20'000'000))); env.close(); auto const brokerInfo = createVaultAndBroker(env, IOU, broker); @@ -2745,6 +2753,13 @@ class Loan_test : public beast::unit_test::suite paymentInterval(100), fee(XRP(100))); env.close(); + + env(trust(borrower, IOU(20'000'000))); + // The borrower increases their limit and acquires some IOU so they + // can pay interest + env(pay(issuer, borrower, IOU(500))); + env.close(); + if (auto const le = env.le(keylet::loan(keylet.key)); BEAST_EXPECT(le)) { diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 54e79a2cb7..64d7f8f518 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -22,6 +22,8 @@ #include #include +#include + namespace ripple { bool @@ -203,6 +205,27 @@ LoanPay::preclaim(PreclaimContext const& ctx) << "Vault pseudo-account can not receive funds (deep frozen)."; return ret; } + // Make sure the borrower has enough funds to make the payment! + // Do not support "partial payments" - if the transaction says to pay X, + // then the account must have X available, even if the loan payment takes + // less. + // Also assume that anybody taking loans is not using "community credit", + // which would let an IOU balance go negative up to the other side's limit. + // This may change in a later version. + if (auto const balance = accountHolds( + ctx.view, + account, + asset, + fhZERO_IF_FROZEN, + ahZERO_IF_UNAUTHORIZED, + ctx.j); + balance < amount) + { + JLOG(ctx.j.warn()) << "Payment amount too large. Amount: " + << to_string(amount.getJson()) + << ". Balance: " << to_string(balance.getJson()); + return tecINSUFFICIENT_FUNDS; + } return tesSUCCESS; } @@ -480,20 +503,36 @@ LoanPay::doApply() : accountHolds( view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); - /* - auto const balanceScale = std::max( - {accountBalanceBefore.exponent(), - vaultBalanceBefore.exponent(), - brokerBalanceBefore.exponent(), - accountBalanceAfter.exponent(), - vaultBalanceAfter.exponent(), - brokerBalanceAfter.exponent()}); - */ XRPL_ASSERT_PARTS( accountBalanceBefore + vaultBalanceBefore + brokerBalanceBefore == accountBalanceAfter + vaultBalanceAfter + brokerBalanceAfter, "ripple::LoanPay::doApply", "funds are conserved (with rounding)"); + XRPL_ASSERT_PARTS( + accountBalanceAfter >= beast::zero, + "ripple::LoanPay::doApply", + "positive account balance"); + XRPL_ASSERT_PARTS( + accountBalanceAfter < accountBalanceBefore, + "ripple::LoanPay::doApply", + "account balance decreased"); + XRPL_ASSERT_PARTS( + vaultBalanceAfter >= beast::zero && brokerBalanceAfter >= beast::zero, + "ripple::LoanPay::doApply", + "positive vault and broker balances"); + XRPL_ASSERT_PARTS( + vaultBalanceAfter >= vaultBalanceBefore, + "ripple::LoanPay::doApply", + "vault balance did not decrease"); + XRPL_ASSERT_PARTS( + brokerBalanceAfter >= brokerBalanceBefore, + "ripple::LoanPay::doApply", + "broker balance did not decrease"); + XRPL_ASSERT_PARTS( + vaultBalanceAfter > vaultBalanceBefore || + brokerBalanceAfter > brokerBalanceBefore, + "ripple::LoanPay::doApply", + "vault and/or broker balance increased"); #endif return tesSUCCESS; From dd938c4e7b3367e94a91ae7dc831974009c6c238 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 22 Oct 2025 10:56:36 -0400 Subject: [PATCH 143/291] Add missing header file --- src/xrpld/app/tx/detail/LoanPay.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 64d7f8f518..edbe192632 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -23,6 +23,7 @@ #include #include +#include namespace ripple { From 30e2650ff985a5bbc93e2f7374b2258e385dd990 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 22 Oct 2025 17:15:34 -0400 Subject: [PATCH 144/291] Use a loan scale of 0 for integral asset types - Loan scale is completely irrelevant to integral types (XRP, MPT), and the field is "soeDEFAULT", so when set to 0, it won't be stored on ledger, saving a little bit of space. --- src/test/app/Loan_test.cpp | 20 ++++++++++++++------ src/xrpld/app/misc/LendingHelpers.h | 9 ++++++++- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 9fd0f8541a..45a076b2ed 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -404,7 +404,10 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT( state.principalOutstanding == broker.asset(1000).value()); BEAST_EXPECT( - state.principalOutstanding.exponent() == state.loanScale); + state.loanScale == + (broker.asset.integral() + ? 0 + : state.principalOutstanding.exponent())); BEAST_EXPECT(state.paymentInterval == 600); BEAST_EXPECT( state.totalValue == @@ -636,7 +639,9 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT( loan->at(sfNextPaymentDueDate) == startDate + interval); BEAST_EXPECT(loan->at(sfPaymentRemaining) == total); - BEAST_EXPECT(loan->at(sfLoanScale) == principalRequest.exponent()); + BEAST_EXPECT( + loan->at(sfLoanScale) == + (broker.asset.integral() ? 0 : principalRequest.exponent())); BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == principalRequest); } @@ -654,7 +659,7 @@ class Loan_test : public beast::unit_test::suite 0, startDate + interval, total, - principalRequest.exponent(), + broker.asset.integral() ? 0 : principalRequest.exponent(), loanProperties.totalValueOutstanding, principalRequest, loanProperties.managementFeeOwedToBroker, @@ -711,7 +716,7 @@ class Loan_test : public beast::unit_test::suite 0, nextDueDate, total, - principalRequest.exponent(), + broker.asset.integral() ? 0 : principalRequest.exponent(), loanProperties.totalValueOutstanding, principalRequest, loanProperties.managementFeeOwedToBroker, @@ -1196,7 +1201,10 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT(brokerSle)) { BEAST_EXPECT( - state.loanScale == state.principalOutstanding.exponent()); + state.loanScale == + (broker.asset.integral() + ? 0 + : state.principalOutstanding.exponent())); auto const defaultAmount = roundToAsset( broker.asset, std::min( @@ -2166,7 +2174,7 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT(!loan.isMember(sfPreviousPaymentDate)); BEAST_EXPECT(loan[sfPrincipalOutstanding] == "1000000000"); BEAST_EXPECT(loan[sfTotalValueOutstanding] == "1000000000"); - BEAST_EXPECT(loan[sfLoanScale] == -6); + BEAST_EXPECT(!loan.isMember(sfLoanScale)); BEAST_EXPECT( loan[sfStartDate].asUInt() == startDate.time_since_epoch().count()); diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index b843bfc344..b63cdcb505 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -1158,7 +1158,7 @@ computeLoanProperties( auto const periodicPayment = detail::loanPeriodicPayment( principalOutstanding, periodicRate, paymentsRemaining); - Number const totalValueOutstanding = [&]() { + STAmount const totalValueOutstanding = [&]() { NumberRoundModeGuard mg(Number::to_nearest); // Use STAmount's internal rounding instead of roundToAsset, because // we're going to use this result to determine the scale for all the @@ -1176,6 +1176,13 @@ computeLoanProperties( // biggest number involved (barring unusual parameters for late, full, or // over payments) auto const loanScale = totalValueOutstanding.exponent(); + XRPL_ASSERT_PARTS( + totalValueOutstanding.integral() && loanScale == 0 || + !totalValueOutstanding.integral() && + loanScale == + static_cast(totalValueOutstanding).exponent(), + "ripple::computeLoanProperties", + "loanScale value fits expectations"); // Since we just figured out the loan scale, we haven't been able to // validate that the principal fits in it, so to allow this function to From b3593fac1c5f53e6e47d0950031c8d950af946e1 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 22 Oct 2025 20:15:18 -0400 Subject: [PATCH 145/291] test: Count crashed test suites --- src/test/unit_test/multi_runner.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/unit_test/multi_runner.cpp b/src/test/unit_test/multi_runner.cpp index 3755428ee3..7e31b28f5a 100644 --- a/src/test/unit_test/multi_runner.cpp +++ b/src/test/unit_test/multi_runner.cpp @@ -464,6 +464,8 @@ multi_runner_parent::~multi_runner_parent() continue_message_queue_ = false; message_queue_thread_.join(); + add_failures(running_suites_.count()); + print_results(os_); for (auto const& s : running_suites_) From 9814ec03097fe4d393a749ad49e7106590cbab1d Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 23 Oct 2025 00:54:58 -0400 Subject: [PATCH 146/291] Implement tfLoanFullPayment; use updated full payment calculations --- include/xrpl/protocol/TxFlags.h | 10 +- src/test/app/Loan_test.cpp | 53 +++-- src/xrpld/app/misc/LendingHelpers.h | 329 ++++++++++++++++------------ src/xrpld/app/tx/detail/LoanPay.cpp | 59 +++-- 4 files changed, 260 insertions(+), 191 deletions(-) diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index f74039ccfd..dbd7ac5b0e 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -290,9 +290,13 @@ constexpr std::uint32_t const tfBatchMask = // LoanPay: True, indicates any excess in this payment can be used // as an overpayment. False, no overpayments will be taken. constexpr std::uint32_t const tfLoanOverpayment = 0x00010000; -// Use two separate mask variables in case the set of flags diverges -constexpr std::uint32_t const tfLoanSetMask = ~(tfUniversal | tfLoanOverpayment); -constexpr std::uint32_t const tfLoanPayMask = ~(tfUniversal | tfLoanOverpayment); +// LoanPay exclusive flags: +// tfLoanFullPayment: True, indicates that the payment is +constexpr std::uint32_t const tfLoanFullPayment = 0x00020000; +constexpr std::uint32_t const tfLoanSetMask = ~(tfUniversal | + tfLoanOverpayment); +constexpr std::uint32_t const tfLoanPayMask = ~(tfUniversal | + tfLoanOverpayment | tfLoanFullPayment); // LoanManage flags: constexpr std::uint32_t const tfLoanDefault = 0x00010000; diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 45a076b2ed..c42367332c 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -1323,19 +1323,23 @@ class Loan_test : public beast::unit_test::suite LoanState& state, STAmount const& payoffAmount, std::uint32_t numPayments, - std::uint32_t baseFlag) { + std::uint32_t baseFlag, + std::uint32_t txFlags) { // toEndOfLife // verifyLoanStatus(state); // Send some bogus pay transactions - env(pay(borrower, keylet::loan(uint256(0)).key, broker.asset(10)), + env(pay(borrower, + keylet::loan(uint256(0)).key, + broker.asset(10), + txFlags), ter(temINVALID)); - env(pay(borrower, loanKeylet.key, broker.asset(-100)), + env(pay(borrower, loanKeylet.key, broker.asset(-100), txFlags), ter(temBAD_AMOUNT)); - env(pay(borrower, broker.brokerID, broker.asset(100)), + env(pay(borrower, broker.brokerID, broker.asset(100), txFlags), ter(tecNO_ENTRY)); - env(pay(evan, loanKeylet.key, broker.asset(500)), + env(pay(evan, loanKeylet.key, broker.asset(500), txFlags), ter(tecNO_PERMISSION)); // TODO: Write a general "isFlag" function? See STObject::isFlag. @@ -1347,7 +1351,7 @@ class Loan_test : public beast::unit_test::suite env(pay(borrower, loanKeylet.key, broker.asset(state.periodicPayment * 2), - tfLoanOverpayment), + tfLoanOverpayment | txFlags), ter(temINVALID_FLAG)); } @@ -1355,12 +1359,15 @@ class Loan_test : public beast::unit_test::suite auto const otherAsset = broker.asset.raw() == assets[0].raw() ? assets[1] : assets[0]; - env(pay(borrower, loanKeylet.key, otherAsset(100)), + env(pay(borrower, loanKeylet.key, otherAsset(100), txFlags), ter(tecWRONG_ASSET)); } // Amount doesn't cover a single payment - env(pay(borrower, loanKeylet.key, STAmount{broker.asset, 1}), + env(pay(borrower, + loanKeylet.key, + STAmount{broker.asset, 1}, + txFlags), ter(tecINSUFFICIENT_PAYMENT)); // Get the balance after these failed transactions take @@ -1379,10 +1386,11 @@ class Loan_test : public beast::unit_test::suite loanKeylet.key, STAmount{ broker.asset, - borrowerBalanceBeforePayment.number() * 2}), + borrowerBalanceBeforePayment.number() * 2}, + txFlags), ter(tecINSUFFICIENT_FUNDS)); - env(pay(borrower, loanKeylet.key, transactionAmount)); + env(pay(borrower, loanKeylet.key, transactionAmount, txFlags)); env.close(); @@ -1474,7 +1482,8 @@ class Loan_test : public beast::unit_test::suite state, payoffAmount, 1, - baseFlag); + baseFlag, + tfLoanFullPayment); }; }; @@ -1506,7 +1515,8 @@ class Loan_test : public beast::unit_test::suite state, payoffAmount, state.paymentRemaining, - baseFlag); + baseFlag, + 0); }; }; @@ -1676,7 +1686,7 @@ class Loan_test : public beast::unit_test::suite testcase << "\tPayment components: " << "Payments remaining, rawInterest, rawPrincipal, " "rawMFee, roundedInterest, roundedPrincipal, " - "roundedMFee, final, extra"; + "roundedMFee, special"; auto const serviceFee = broker.asset(2); @@ -1758,8 +1768,12 @@ class Loan_test : public beast::unit_test::suite << paymentComponents.roundedInterest << ", " << paymentComponents.roundedPrincipal << ", " << paymentComponents.roundedManagementFee << ", " - << (paymentComponents.final ? "true" : "false") << ", " - << (paymentComponents.extra ? "true" : "false"); + << (paymentComponents.specialCase == SpecialCase::final + ? "final" + : paymentComponents.specialCase == + SpecialCase::final + ? "extra" + : "none"); auto const totalDueAmount = STAmount{ broker.asset, @@ -1776,7 +1790,8 @@ class Loan_test : public beast::unit_test::suite // IOUs, the difference should be after the 8th digit. Number const diff = totalDue - totalDueAmount; BEAST_EXPECT( - paymentComponents.final || diff == beast::zero || + paymentComponents.specialCase == SpecialCase::final || + diff == beast::zero || (diff > beast::zero && ((broker.asset.integral() && (static_cast(diff) < 3)) || @@ -1803,11 +1818,11 @@ class Loan_test : public beast::unit_test::suite paymentComponents.roundedPrincipal <= state.principalOutstanding); BEAST_EXPECT( - !paymentComponents.final || + paymentComponents.specialCase != SpecialCase::final || paymentComponents.roundedPrincipal == state.principalOutstanding); BEAST_EXPECT( - paymentComponents.final || + paymentComponents.specialCase == SpecialCase::final || (state.periodicPayment.exponent() - (paymentComponents.rawPrincipal + paymentComponents.rawInterest + @@ -1846,7 +1861,7 @@ class Loan_test : public beast::unit_test::suite --state.paymentRemaining; state.previousPaymentDate = state.nextPaymentDate; - if (paymentComponents.final) + if (paymentComponents.specialCase == SpecialCase::final) { state.paymentRemaining = 0; } diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index b63cdcb505..2c6bc16052 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -45,6 +45,8 @@ roundPeriodicPayment( return roundToAsset(asset, periodicPayment, scale, Number::upward); } +enum class SpecialCase { none, final, extra }; + /// This structure is used internally to compute the breakdown of a /// single loan payment struct PaymentComponents @@ -58,10 +60,7 @@ struct PaymentComponents // periodic payment that goes toward the Broker's management fee, which is // tracked by sfManagementFeeOutstanding Number roundedManagementFee; - //// We may not need roundedPayment - // Number roundedPayment; - bool final = false; - bool extra = false; + SpecialCase specialCase = SpecialCase::none; }; /// This structure is explained in the XLS-66 spec, section 3.2.4.4 (Failure @@ -79,12 +78,8 @@ struct LoanPaymentParts * This is 0 for regular payments. */ Number valueChange; - /// managementFeePaid is amount of fee that is tracked by - /// sfManagementFeeOutstanding - Number managementFeePaid; - /// extraFeePaid is the amount of fee that the payment covered not tracked - /// by sfManagementFeeOutstanding. - Number extraFeePaid; + /// feePaid is amount of fee that is paid to the broker + Number feePaid; LoanPaymentParts& operator+=(LoanPaymentParts const& other) @@ -92,8 +87,7 @@ struct LoanPaymentParts principalPaid += other.principalPaid; interestPaid += other.interestPaid; valueChange += other.valueChange; - extraFeePaid += other.extraFeePaid; - managementFeePaid += other.managementFeePaid; + feePaid += other.feePaid; return *this; } }; @@ -509,40 +503,41 @@ doPayment( nextDueDateProxy, "ripple::detail::doPayment", "Next due date proxy set"); + auto const totalValueDelta = payment.roundedPrincipal + payment.roundedInterest + payment.roundedManagementFee - payment.valueChange; - if (!payment.extra) + if (payment.specialCase == SpecialCase::final) { - if (payment.final) + XRPL_ASSERT_PARTS( + principalOutstandingProxy == payment.roundedPrincipal, + "ripple::detail::doPayment", + "Full principal payment"); + XRPL_ASSERT_PARTS( + totalValueOutstandingProxy == totalValueDelta, + "ripple::detail::doPayment", + "Full value payment"); + XRPL_ASSERT_PARTS( + managementFeeOutstandingProxy == payment.roundedManagementFee, + "ripple::detail::doPayment", + "Full management fee payment"); + + paymentRemainingProxy = 0; + + prevPaymentDateProxy = *nextDueDateProxy; + // Remove the field. This is the only condition where nextDueDate is + // allowed to be removed. + nextDueDateProxy = std::nullopt; + + // Always zero out the the tracked values on a final payment + principalOutstandingProxy = 0; + totalValueOutstandingProxy = 0; + managementFeeOutstandingProxy = 0; + } + else + { + if (payment.specialCase != SpecialCase::extra) { - XRPL_ASSERT_PARTS( - principalOutstandingProxy == payment.roundedPrincipal, - "ripple::detail::doPayment", - "Full principal payment"); - XRPL_ASSERT_PARTS( - totalValueOutstandingProxy == totalValueDelta, - "ripple::detail::doPayment", - "Full value payment"); - - paymentRemainingProxy = 0; - - prevPaymentDateProxy = *nextDueDateProxy; - // Remove the field. This is the only condition where nextDueDate is - // allowed to be removed. - nextDueDateProxy = std::nullopt; - } - else - { - XRPL_ASSERT_PARTS( - principalOutstandingProxy > payment.roundedPrincipal, - "ripple::detail::doPayment", - "Partial principal payment"); - XRPL_ASSERT_PARTS( - totalValueOutstandingProxy > totalValueDelta, - "ripple::detail::doPayment", - "Partial value payment"); - paymentRemainingProxy -= 1; prevPaymentDateProxy = *nextDueDateProxy; @@ -550,17 +545,25 @@ doPayment( // old-fashioned way. nextDueDateProxy = *nextDueDateProxy + paymentInterval; } + XRPL_ASSERT_PARTS( + principalOutstandingProxy > payment.roundedPrincipal, + "ripple::detail::doPayment", + "Partial principal payment"); + XRPL_ASSERT_PARTS( + totalValueOutstandingProxy > totalValueDelta, + "ripple::detail::doPayment", + "Partial value payment"); // Management fees are expected to be relatively small, and could get to // zero before the loan is paid off XRPL_ASSERT_PARTS( managementFeeOutstandingProxy >= payment.roundedManagementFee, "ripple::detail::doPayment", "Valid management fee"); - } - principalOutstandingProxy -= payment.roundedPrincipal; - totalValueOutstandingProxy -= totalValueDelta; - managementFeeOutstandingProxy -= payment.roundedManagementFee; + principalOutstandingProxy -= payment.roundedPrincipal; + totalValueOutstandingProxy -= totalValueDelta; + managementFeeOutstandingProxy -= payment.roundedManagementFee; + } XRPL_ASSERT_PARTS( // Use an explicit cast because the template parameter can be @@ -580,8 +583,9 @@ doPayment( .principalPaid = payment.roundedPrincipal, .interestPaid = payment.roundedInterest, .valueChange = payment.valueChange, - .managementFeePaid = payment.roundedManagementFee, - .extraFeePaid = payment.extraFee}; + // Now that the adjustments have been made, the fee parts can be + // combined + .feePaid = payment.roundedManagementFee + payment.extraFee}; } // This function mainly exists to guarantee isolation of the "sandbox" @@ -765,14 +769,11 @@ computeOverpayment( "ripple::detail::computeOverpayment", "value change matches"); XRPL_ASSERT_PARTS( - loanPaymentParts.extraFeePaid == overpaymentComponents.extraFee, + loanPaymentParts.feePaid == + overpaymentComponents.extraFee + + overpaymentComponents.roundedManagementFee, "ripple::detail::computeOverpayment", - "extra fee payment matches"); - XRPL_ASSERT_PARTS( - loanPaymentParts.managementFeePaid == - overpaymentComponents.roundedManagementFee, - "ripple::detail::computeOverpayment", - "management fee payment matches"); + "fee payment matches"); // Update the loan object (via proxies) totalValueOutstandingProxy = totalValueOutstanding; @@ -885,9 +886,9 @@ computeLatePayment( /* Handle possible full payments. * * If this function processed a full payment, the return value will be - * a PaymentComponentsPlus object. If the payment should not be considered as a - * full payment, the return will be an Unexpected(tesSUCCESS). Otherwise, it'll - * be an Unexpected with the error code the caller is expected to return. + * a PaymentComponentsPlus object. Otherwise, it'll be an Unexpected with the + * error code the caller is expected to return. It should NEVER return + * tesSUCCESS */ template Expected @@ -912,7 +913,7 @@ computeFullPayment( { if (paymentRemaining <= 1) // If this is the last payment, it has to be a regular payment - return Unexpected(tesSUCCESS); + return Unexpected(tecKILLED); Number const rawPrincipalOutstanding = loanPrincipalFromPeriodicPayment( periodicPayment, periodicRate, paymentRemaining); @@ -930,22 +931,16 @@ computeFullPayment( auto const [rawFullInterest, rawFullManagementFee] = computeInterestAndFeeParts(fullPaymentInterest, managementFeeRate); - auto const - [roundedFullInterest, roundedFullManagementFee, roundedFullExtraFee] = - [&]() { - auto const interest = - roundToAsset(asset, fullPaymentInterest, loanScale); - auto const parts = computeInterestAndFeeParts( - asset, interest, managementFeeRate, loanScale); - // Apply as much of the fee to the outstanding fee, but no - // more - if (parts.second <= managementFeeOutstanding) - return std::make_tuple(parts.first, parts.second, Number{}); - return std::make_tuple( - parts.first, - managementFeeOutstanding, - parts.second - managementFeeOutstanding); - }(); + + auto const [roundedFullInterest, roundedFullManagementFee] = [&]() { + auto const interest = + roundToAsset(asset, fullPaymentInterest, loanScale); + auto const parts = computeInterestAndFeeParts( + asset, interest, managementFeeRate, loanScale); + // Apply as much of the fee to the outstanding fee, but no + // more + return std::make_tuple(parts.first, parts.second); + }(); PaymentComponentsPlus const full{ PaymentComponents{ @@ -954,11 +949,16 @@ computeFullPayment( .rawManagementFee = rawFullManagementFee, .roundedInterest = roundedFullInterest, .roundedPrincipal = principalOutstanding, - .roundedManagementFee = roundedFullManagementFee, - .final = true}, - // A full payment pays the single close payment fee, plus whatever part - // of the computed management fee is not outstanding in the Loan - closePaymentFee + roundedFullExtraFee, + // to make the accounting work later, the tracked part of the fee + // must be paid in full + .roundedManagementFee = managementFeeOutstanding, + .specialCase = SpecialCase::final}, + // A full payment pays the single close payment fee, plus the computed + // management fee part of the interest portion, but for tracking, the + // outstanding part is removed. That could make this value negative, but + // that's ok, because it's not used until it's recombined with + // roundedManagementFee. + closePaymentFee + roundedFullManagementFee - managementFeeOutstanding, // A full payment decreases the value of the loan by the // difference between the interest paid and the expected // outstanding interest return @@ -972,7 +972,7 @@ computeFullPayment( if (amount < full.totalDue) // If the payment is less than the full payment amount, it's not // sufficient to be a full payment, but that's not an error. - return Unexpected(tesSUCCESS); + return Unexpected(tecINSUFFICIENT_PAYMENT); return full; } @@ -1038,8 +1038,7 @@ computePaymentComponents( .roundedInterest = interest, .roundedPrincipal = principalOutstanding, .roundedManagementFee = managementFeeOutstanding, - //.roundedPayment = totalValueOutstanding, - .final = true}; + .specialCase = SpecialCase::final}; } /* @@ -1137,7 +1136,6 @@ computePaymentComponents( .roundedInterest = roundedInterest, .roundedPrincipal = roundedPrincipal, .roundedManagementFee = roundedFee, - //.roundedPayment = roundedPeriodicPayment }; } @@ -1409,6 +1407,112 @@ isRounded(A const& asset, Number const& value, std::int32_t scale) roundToAsset(asset, value, scale, Number::upward); } +template +Expected +loanMakeFullPayment( + A const& asset, + ApplyView& view, + SLE::ref loan, + SLE::const_ref brokerSle, + STAmount const& amount, + bool const overpaymentAllowed, + beast::Journal j) +{ + auto principalOutstandingProxy = loan->at(sfPrincipalOutstanding); + auto paymentRemainingProxy = loan->at(sfPaymentRemaining); + + if (paymentRemainingProxy == 0 || principalOutstandingProxy == 0) + { + // Loan complete + JLOG(j.warn()) << "Loan is already paid off."; + return Unexpected(tecKILLED); + } + + auto totalValueOutstandingProxy = loan->at(sfTotalValueOutstanding); + auto managementFeeOutstandingProxy = loan->at(sfManagementFeeOutstanding); + + // Next payment due date must be set unless the loan is complete + auto nextDueDateProxy = loan->at(~sfNextPaymentDueDate); + if (!nextDueDateProxy) + { + JLOG(j.warn()) << "Loan next payment due date is not set."; + return Unexpected(tecINTERNAL); + } + + std::int32_t const loanScale = loan->at(sfLoanScale); + + TenthBips32 const interestRate{loan->at(sfInterestRate)}; + TenthBips32 const closeInterestRate{loan->at(sfCloseInterestRate)}; + + Number const closePaymentFee = + roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale); + TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; + + auto const periodicPayment = loan->at(sfPeriodicPayment); + + auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDate); + std::uint32_t const startDate = loan->at(sfStartDate); + + std::uint32_t const paymentInterval = loan->at(sfPaymentInterval); + // Compute the normal periodic rate, payment, etc. + // We'll need it in the remaining calculations + Number const periodicRate = loanPeriodicRate(interestRate, paymentInterval); + XRPL_ASSERT( + interestRate == 0 || periodicRate > 0, + "ripple::loanMakeFullPayment : valid rate"); + + XRPL_ASSERT( + *totalValueOutstandingProxy > 0, + "ripple::loanMakeFullPayment : valid total value"); + + view.update(loan); + + // ------------------------------------------------------------- + // full payment handling + LoanState const roundedLoanState = calculateRoundedLoanState( + totalValueOutstandingProxy, + principalOutstandingProxy, + managementFeeOutstandingProxy); + + if (auto const fullPaymentComponents = detail::computeFullPayment( + asset, + view, + principalOutstandingProxy, + managementFeeOutstandingProxy, + periodicPayment, + paymentRemainingProxy, + prevPaymentDateProxy, + startDate, + paymentInterval, + closeInterestRate, + loanScale, + roundedLoanState.interestDue, + periodicRate, + closePaymentFee, + amount, + managementFeeRate, + j)) + return doPayment( + *fullPaymentComponents, + totalValueOutstandingProxy, + principalOutstandingProxy, + managementFeeOutstandingProxy, + paymentRemainingProxy, + prevPaymentDateProxy, + nextDueDateProxy, + paymentInterval); + else if (fullPaymentComponents.error()) + // error() will be the TER returned if a payment is not made. It + // will only evaluate to true if it's unsuccessful. Otherwise, + // tesSUCCESS means nothing was done, so continue. + return Unexpected(fullPaymentComponents.error()); + + // LCOV_EXCL_START + UNREACHABLE("ripple::loanMakeFullPayment : invalid result"); + return Unexpected(tecINTERNAL); + // LCOV_EXCL_STOP +} + template Expected loanMakePayment( @@ -1524,46 +1628,6 @@ loanMakePayment( // means nothing was done, so continue. return Unexpected(latePaymentComponents.error()); - // ------------------------------------------------------------- - // full payment handling - LoanState const roundedLoanState = calculateRoundedLoanState( - totalValueOutstandingProxy, - principalOutstandingProxy, - managementFeeOutstandingProxy); - - if (auto const fullPaymentComponents = detail::computeFullPayment( - asset, - view, - principalOutstandingProxy, - managementFeeOutstandingProxy, - periodicPayment, - paymentRemainingProxy, - prevPaymentDateProxy, - startDate, - paymentInterval, - closeInterestRate, - loanScale, - roundedLoanState.interestDue, - periodicRate, - closePaymentFee, - amount, - managementFeeRate, - j)) - return doPayment( - *fullPaymentComponents, - totalValueOutstandingProxy, - principalOutstandingProxy, - managementFeeOutstandingProxy, - paymentRemainingProxy, - prevPaymentDateProxy, - nextDueDateProxy, - paymentInterval); - else if (fullPaymentComponents.error()) - // error() will be the TER returned if a payment is not made. It will - // only evaluate to true if it's unsuccessful. Otherwise, tesSUCCESS - // means nothing was done, so continue. - return Unexpected(fullPaymentComponents.error()); - // ------------------------------------------------------------- // regular periodic payment handling @@ -1635,13 +1699,13 @@ loanMakePayment( paymentInterval); ++numPayments; - if (nextPayment.final) + if (nextPayment.specialCase == SpecialCase::final) break; } XRPL_ASSERT_PARTS( totalParts.principalPaid + totalParts.interestPaid + - totalParts.extraFeePaid + totalParts.managementFeePaid == + totalParts.feePaid == totalPaid, "ripple::loanMakePayment", "payment parts add up"); @@ -1649,10 +1713,6 @@ loanMakePayment( totalParts.valueChange == 0, "ripple::loanMakePayment", "no value change"); - XRPL_ASSERT_PARTS( - totalParts.extraFeePaid == periodic.extraFee * numPayments, - "ripple::loanMakePayment", - "fee parts add up"); // ------------------------------------------------------------- // overpayment handling @@ -1675,8 +1735,6 @@ loanMakePayment( Number const payment = overpayment - fee; - // TODO: Is the overpaymentInterestRate an APR or flat? - auto const [rawOverpaymentInterest, rawOverpaymentManagementFee] = [&]() { Number const interest = @@ -1701,7 +1759,7 @@ loanMakePayment( .roundedInterest = roundedOverpaymentInterest, .roundedPrincipal = payment - roundedOverpaymentInterest, .roundedManagementFee = 0, - .extra = true}, + .specialCase = SpecialCase::extra}, fee, roundedOverpaymentInterest}; @@ -1747,11 +1805,8 @@ loanMakePayment( isRounded(asset, totalParts.valueChange, loanScale), "ripple::loanMakePayment : loan value change rounded"); XRPL_ASSERT( - isRounded(asset, totalParts.extraFeePaid, loanScale), - "ripple::loanMakePayment : extra fee paid rounded"); - XRPL_ASSERT( - isRounded(asset, totalParts.managementFeePaid, loanScale), - "ripple::loanMakePayment : management fee paid rounded"); + isRounded(asset, totalParts.feePaid, loanScale), + "ripple::loanMakePayment : fee paid rounded"); return totalParts; } diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index edbe192632..f87346f299 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -55,6 +55,12 @@ XRPAmount LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx) { auto const normalCost = Transactor::calculateBaseFee(view, tx); + + if (tx.isFlag(tfLoanFullPayment)) + // The loan will be making one set of calculations for one (large) + // payment + return normalCost; + auto const paymentsPerFeeIncrement = 20; // The fee is based on the potential number of payments, unless the loan is @@ -99,21 +105,6 @@ LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx) // This is definitely paying fewer than paymentsPerFeeIncrement payments return normalCost; - if (auto const fullInterest = calculateFullPaymentInterest( - loanSle->at(sfPeriodicPayment), - loanPeriodicRate( - TenthBips32(loanSle->at(sfInterestRate)), - loanSle->at(sfPaymentInterval)), - loanSle->at(sfPaymentRemaining), - view.parentCloseTime(), - loanSle->at(sfPaymentInterval), - loanSle->at(sfPreviousPaymentDate), - loanSle->at(sfStartDate), - TenthBips32(loanSle->at(sfCloseInterestRate))); - amount > loanSle->at(sfPrincipalOutstanding) + fullInterest + - loanSle->at(sfClosePaymentFee)) - return normalCost; - NumberRoundModeGuard mg(Number::downward); // Figure out how many payments will be made auto const numPaymentEstimate = @@ -304,14 +295,23 @@ LoanPay::doApply() LoanManage::unimpairLoan(view, loanSle, vaultSle, j_); } - Expected paymentParts = loanMakePayment( - asset, - view, - loanSle, - brokerSle, - amount, - tx.isFlag(tfLoanOverpayment), - j_); + Expected const paymentParts = + tx.isFlag(tfLoanFullPayment) ? loanMakeFullPayment( + asset, + view, + loanSle, + brokerSle, + amount, + tx.isFlag(tfLoanOverpayment), + j_) + : loanMakePayment( + asset, + view, + loanSle, + brokerSle, + amount, + tx.isFlag(tfLoanOverpayment), + j_); if (!paymentParts) { @@ -342,16 +342,12 @@ LoanPay::doApply() "ripple::LoanPay::doApply", "valid principal paid"); XRPL_ASSERT_PARTS( - paymentParts->extraFeePaid >= 0, + paymentParts->feePaid >= 0, "ripple::LoanPay::doApply", "valid fee paid"); - XRPL_ASSERT_PARTS( - paymentParts->managementFeePaid >= 0, - "ripple::LoanPay::doApply", - "valid management fee paid"); if (paymentParts->principalPaid < 0 || paymentParts->interestPaid < 0 || - paymentParts->extraFeePaid < 0 || paymentParts->managementFeePaid < 0) + paymentParts->feePaid < 0) { // LCOV_EXCL_START JLOG(j_.fatal()) << "Loan payment computation returned invalid values."; @@ -375,13 +371,12 @@ LoanPay::doApply() auto const totalPaidToVaultForDebt = totalPaidToVaultRaw - paymentParts->valueChange; - auto const totalPaidToBroker = - paymentParts->managementFeePaid + paymentParts->extraFeePaid; + auto const totalPaidToBroker = paymentParts->feePaid; XRPL_ASSERT_PARTS( (totalPaidToVaultRaw + totalPaidToBroker) == (paymentParts->principalPaid + paymentParts->interestPaid + - paymentParts->managementFeePaid + paymentParts->extraFeePaid), + paymentParts->feePaid), "ripple::LoanPay::doApply", "payments add up"); From 6ad7d1c076a0b973e23361d23ee6cd3148f1eb4e Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 23 Oct 2025 01:02:03 -0400 Subject: [PATCH 147/291] Rewrite functionality calculations to use updated calculations --- src/xrpld/app/misc/LendingHelpers.h | 101 ++++++++++++++++------------ 1 file changed, 59 insertions(+), 42 deletions(-) diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 2c6bc16052..f15402cc83 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -595,6 +595,7 @@ template Expected tryOverpayment( A const& asset, + std::int32_t loanScale, PaymentComponentsPlus const& overpaymentComponents, Number& totalValueOutstanding, Number& principalOutstanding, @@ -602,50 +603,52 @@ tryOverpayment( Number& periodicPayment, TenthBips32 interestRate, std::uint32_t paymentInterval, + Number const& periodicRate, std::uint32_t paymentRemaining, std::uint32_t prevPaymentDate, std::optional nextDueDate, TenthBips16 const managementFeeRate, beast::Journal j) { - // Compute what the properties would be if the loan was new in its current - // state. They are not likely to match the original properties. We're - // interested in the error. - auto const loanPropertiesBefore = computeLoanProperties( - asset, - principalOutstanding, - interestRate, - paymentInterval, - paymentRemaining, - managementFeeRate); + auto const raw = calculateRawLoanState( + periodicPayment, periodicRate, paymentRemaining, managementFeeRate); + auto const rounded = calculateRoundedLoanState( + totalValueOutstanding, principalOutstanding, managementFeeOutstanding); - auto const accumulatedTotalValueError = - loanPropertiesBefore.totalValueOutstanding - totalValueOutstanding; - auto const accumulatedFeeError = - loanPropertiesBefore.managementFeeOwedToBroker - - managementFeeOutstanding; + auto const totalValueError = totalValueOutstanding - raw.valueOutstanding; + auto const principalError = principalOutstanding - raw.principalOutstanding; + auto const feeError = managementFeeOutstanding - raw.managementFeeDue; - auto const paymentParts = detail::doPayment( - overpaymentComponents, - totalValueOutstanding, - principalOutstanding, - managementFeeOutstanding, - paymentRemaining, - prevPaymentDate, - nextDueDate, - paymentInterval); + auto const newRawPrincipal = + raw.principalOutstanding - overpaymentComponents.roundedPrincipal; auto newLoanProperties = computeLoanProperties( asset, - principalOutstanding, + newRawPrincipal, interestRate, paymentInterval, paymentRemaining, managementFeeRate); - newLoanProperties.totalValueOutstanding += accumulatedTotalValueError; - newLoanProperties.managementFeeOwedToBroker += accumulatedFeeError; + auto const newRaw = calculateRawLoanState( + newLoanProperties.periodicPayment, + periodicRate, + paymentRemaining, + managementFeeRate); + totalValueOutstanding = roundToAsset( + asset, newRaw.valueOutstanding + totalValueError, loanScale); + principalOutstanding = roundToAsset( + asset, + newRaw.principalOutstanding + principalError, + loanScale, + Number::downward); + managementFeeOutstanding = + roundToAsset(asset, newRaw.managementFeeDue + feeError, loanScale); + + periodicPayment = newLoanProperties.periodicPayment; + + // check that the loan is still valid if (newLoanProperties.firstPaymentPrincipal <= 0 && principalOutstanding > 0) { @@ -660,7 +663,7 @@ tryOverpayment( // Check that the other computed values are valid if (newLoanProperties.periodicPayment <= 0 || newLoanProperties.totalValueOutstanding <= 0 || - newLoanProperties.managementFeeOwedToBroker <= 0) + newLoanProperties.managementFeeOwedToBroker < 0) { // LCOV_EXCL_START JLOG(j.warn()) << "Overpayment not allowed: Computed loan " @@ -675,16 +678,29 @@ tryOverpayment( // LCOV_EXCL_STOP } - totalValueOutstanding = newLoanProperties.totalValueOutstanding; - periodicPayment = newLoanProperties.periodicPayment; + auto const newRounded = calculateRoundedLoanState( + totalValueOutstanding, principalOutstanding, managementFeeOutstanding); + auto const valueChange = + newRounded.interestOutstanding - rounded.interestOutstanding; + XRPL_ASSERT_PARTS( + valueChange < beast::zero, + "ripple::detail::tryOverpayment", + "principal overpayment reduced value of loan"); - return paymentParts; + return LoanPaymentParts{ + .principalPaid = + rounded.principalOutstanding - newRounded.principalOutstanding, + .interestPaid = rounded.interestDue - newRounded.interestDue, + .valueChange = valueChange + overpaymentComponents.valueChange, + .feePaid = rounded.managementFeeDue - newRounded.managementFeeDue + + overpaymentComponents.extraFee}; } template Expected computeOverpayment( A const& asset, + std::int32_t loanScale, PaymentComponentsPlus const& overpaymentComponents, NumberProxy& totalValueOutstandingProxy, NumberProxy& principalOutstandingProxy, @@ -692,6 +708,7 @@ computeOverpayment( NumberProxy& periodicPaymentProxy, TenthBips32 const interestRate, std::uint32_t const paymentInterval, + Number const& periodicRate, std::uint32_t const paymentRemaining, std::uint32_t const prevPaymentDate, std::optional const nextDueDate, @@ -707,6 +724,7 @@ computeOverpayment( auto const ret = tryOverpayment( asset, + loanScale, overpaymentComponents, totalValueOutstanding, principalOutstanding, @@ -714,6 +732,7 @@ computeOverpayment( periodicPayment, interestRate, paymentInterval, + periodicRate, paymentRemaining, prevPaymentDate, nextDueDate, @@ -743,8 +762,7 @@ computeOverpayment( XRPL_ASSERT_PARTS( managementFeeOutstandingProxy - managementFeeOutstanding == - overpaymentComponents.roundedManagementFee && - overpaymentComponents.roundedManagementFee == beast::zero, + overpaymentComponents.roundedManagementFee, "ripple::detail::computeOverpayment", "no fee change"); @@ -760,14 +778,7 @@ computeOverpayment( overpaymentComponents.roundedPrincipal, "ripple::detail::computeOverpayment", "principal payment matches"); - XRPL_ASSERT_PARTS( - loanPaymentParts.interestPaid == overpaymentComponents.roundedInterest, - "ripple::detail::computeOverpayment", - "interest payment matches"); - XRPL_ASSERT_PARTS( - loanPaymentParts.valueChange == overpaymentComponents.valueChange, - "ripple::detail::computeOverpayment", - "value change matches"); + XRPL_ASSERT_PARTS( loanPaymentParts.feePaid == overpaymentComponents.extraFee + @@ -1768,10 +1779,15 @@ loanMakePayment( if (overpaymentComponents.rawPrincipal > 0 && overpaymentComponents.roundedPrincipal > 0) { - // Can't just use periodicPayment here, because it might change + XRPL_ASSERT_PARTS( + overpaymentComponents.valueChange >= beast::zero, + "ripple::loanMakePayment", + "overpayment penalty did not reduce value of loan"); + // Can't just use `periodicPayment` here, because it might change auto periodicPaymentProxy = loan->at(sfPeriodicPayment); if (auto const overResult = detail::computeOverpayment( asset, + loanScale, overpaymentComponents, totalValueOutstandingProxy, principalOutstandingProxy, @@ -1779,6 +1795,7 @@ loanMakePayment( periodicPaymentProxy, interestRate, paymentInterval, + periodicRate, paymentRemainingProxy, prevPaymentDateProxy, nextDueDateProxy, From f78c5f65bc3c31e49d80870299d842f772c1838d Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 23 Oct 2025 01:46:43 -0400 Subject: [PATCH 148/291] Allow issuers to send LoanPay - Implement a new helper accountCanSend, which is like accountHolds, but returns a meaningful value for issuers, and will include the available credit on the other side of a trust line. (The sfHighLimit or sfLowLimit as appropriate.) - Use this new helper when checking the available balance in LoanPay. --- include/xrpl/ledger/View.h | 43 +++++ src/libxrpl/ledger/View.cpp | 250 +++++++++++++++++++++------- src/xrpld/app/tx/detail/LoanPay.cpp | 21 +-- 3 files changed, 238 insertions(+), 76 deletions(-) diff --git a/include/xrpl/ledger/View.h b/include/xrpl/ledger/View.h index 56c762a5a4..4343c29b59 100644 --- a/include/xrpl/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -361,6 +361,49 @@ accountHolds( AuthHandling zeroIfUnauthorized, beast::Journal j); +// Returns the amount an account can spend total. +// +// These functions use accountHolds, but unlike accountHolds: +// * The account can go into debt. +// * If the account is the asset issuer the only limit is defined by the asset / +// issuance. +// +// <-- saAmount: amount of currency held by account. May be negative. +[[nodiscard]] STAmount +accountCanSend( + ReadView const& view, + AccountID const& account, + Currency const& currency, + AccountID const& issuer, + FreezeHandling zeroIfFrozen, + beast::Journal j); + +[[nodiscard]] STAmount +accountCanSend( + ReadView const& view, + AccountID const& account, + Issue const& issue, + FreezeHandling zeroIfFrozen, + beast::Journal j); + +[[nodiscard]] STAmount +accountCanSend( + ReadView const& view, + AccountID const& account, + MPTIssue const& mptIssue, + FreezeHandling zeroIfFrozen, + AuthHandling zeroIfUnauthorized, + beast::Journal j); + +[[nodiscard]] STAmount +accountCanSend( + ReadView const& view, + AccountID const& account, + Asset const& asset, + FreezeHandling zeroIfFrozen, + AuthHandling zeroIfUnauthorized, + beast::Journal j); + // Returns the amount an account can spend of the currency type saDefault, or // returns saDefault if this account is the issuer of the currency in // question. Should be used in favor of accountHolds when questioning how much diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index ea0d4b54fd..eeb704bc3b 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -383,6 +383,99 @@ isLPTokenFrozen( isFrozen(view, account, asset2.currency, asset2.account); } +static SLE::const_pointer +getLineIfUsable( + ReadView const& view, + AccountID const& account, + Currency const& currency, + AccountID const& issuer, + FreezeHandling zeroIfFrozen, + beast::Journal j) +{ + auto const sle = view.read(keylet::line(account, issuer, currency)); + + if (!sle) + { + return nullptr; + } + + if (zeroIfFrozen == fhZERO_IF_FROZEN) + { + if (isFrozen(view, account, currency, issuer) || + isDeepFrozen(view, account, currency, issuer)) + { + return nullptr; + } + + // when fixFrozenLPTokenTransfer is enabled, if currency is lptoken, + // we need to check if the associated assets have been frozen + if (view.rules().enabled(fixFrozenLPTokenTransfer)) + { + auto const sleIssuer = view.read(keylet::account(issuer)); + if (!sleIssuer) + { + return nullptr; // LCOV_EXCL_LINE + } + else if (sleIssuer->isFieldPresent(sfAMMID)) + { + auto const sleAmm = + view.read(keylet::amm((*sleIssuer)[sfAMMID])); + + if (!sleAmm || + isLPTokenFrozen( + view, + account, + (*sleAmm)[sfAsset].get(), + (*sleAmm)[sfAsset2].get())) + { + return nullptr; + } + } + } + } + + return sle; +} + +static STAmount +getTrustLineBalance( + ReadView const& view, + SLE::const_ref sle, + AccountID const& account, + Currency const& currency, + AccountID const& issuer, + bool includeOppositeLimit, + beast::Journal j) +{ + STAmount amount; + if (sle) + { + amount = sle->getFieldAmount(sfBalance); + bool const accountHigh = account > issuer; + auto const& oppositeField = accountHigh ? sfLowLimit : sfHighLimit; + if (accountHigh) + { + // Put balance in account terms. + amount.negate(); + } + if (includeOppositeLimit) + { + amount += sle->getFieldAmount(oppositeField); + } + amount.setIssuer(issuer); + } + else + { + amount.clear(Issue{currency, issuer}); + } + + JLOG(j.trace()) << "getTrustLineBalance:" + << " account=" << to_string(account) + << " amount=" << amount.getFullText(); + + return view.balanceHook(account, issuer, amount); +} + STAmount accountHolds( ReadView const& view, @@ -399,71 +492,10 @@ accountHolds( } // IOU: Return balance on trust line modulo freeze - auto const sle = view.read(keylet::line(account, issuer, currency)); - auto const allowBalance = [&]() { - if (!sle) - { - return false; - } + SLE::const_pointer const sle = + getLineIfUsable(view, account, currency, issuer, zeroIfFrozen, j); - if (zeroIfFrozen == fhZERO_IF_FROZEN) - { - if (isFrozen(view, account, currency, issuer) || - isDeepFrozen(view, account, currency, issuer)) - { - return false; - } - - // when fixFrozenLPTokenTransfer is enabled, if currency is lptoken, - // we need to check if the associated assets have been frozen - if (view.rules().enabled(fixFrozenLPTokenTransfer)) - { - auto const sleIssuer = view.read(keylet::account(issuer)); - if (!sleIssuer) - { - return false; // LCOV_EXCL_LINE - } - else if (sleIssuer->isFieldPresent(sfAMMID)) - { - auto const sleAmm = - view.read(keylet::amm((*sleIssuer)[sfAMMID])); - - if (!sleAmm || - isLPTokenFrozen( - view, - account, - (*sleAmm)[sfAsset].get(), - (*sleAmm)[sfAsset2].get())) - { - return false; - } - } - } - } - - return true; - }(); - - if (allowBalance) - { - amount = sle->getFieldAmount(sfBalance); - if (account > issuer) - { - // Put balance in account terms. - amount.negate(); - } - amount.setIssuer(issuer); - } - else - { - amount.clear(Issue{currency, issuer}); - } - - JLOG(j.trace()) << "accountHolds:" - << " account=" << to_string(account) - << " amount=" << amount.getFullText(); - - return view.balanceHook(account, issuer, amount); + return getTrustLineBalance(view, sle, account, currency, issuer, false, j); } STAmount @@ -550,6 +582,96 @@ accountHolds( asset.value()); } +STAmount +accountCanSend( + ReadView const& view, + AccountID const& account, + Currency const& currency, + AccountID const& issuer, + FreezeHandling zeroIfFrozen, + beast::Journal j) +{ + if (isXRP(currency)) + return accountHolds(view, account, currency, issuer, zeroIfFrozen, j); + + if (account == issuer) + // If the account is the issuer, then their limit is effectively + // infinite + return STAmount{ + Issue{currency, issuer}, STAmount::cMaxValue, STAmount::cMaxOffset}; + + // IOU: Return balance on trust line modulo freeze + SLE::const_pointer const sle = + getLineIfUsable(view, account, currency, issuer, zeroIfFrozen, j); + + return getTrustLineBalance(view, sle, account, currency, issuer, true, j); +} + +STAmount +accountCanSend( + ReadView const& view, + AccountID const& account, + Issue const& issue, + FreezeHandling zeroIfFrozen, + beast::Journal j) +{ + return accountCanSend( + view, account, issue.currency, issue.account, zeroIfFrozen, j); +} + +STAmount +accountCanSend( + ReadView const& view, + AccountID const& account, + MPTIssue const& mptIssue, + FreezeHandling zeroIfFrozen, + AuthHandling zeroIfUnauthorized, + beast::Journal j) +{ + if (account == mptIssue.getIssuer()) + { + // if the account is the issuer, and the issuance exists, their limit is + // the issuance limit minus the outstanding value + auto const issuance = + view.read(keylet::mptIssuance(mptIssue.getMptID())); + + if (!issuance) + { + return STAmount{mptIssue}; + } + return STAmount{ + mptIssue, + issuance->at(~sfMaximumAmount).value_or(maxMPTokenAmount) - + issuance->at(sfOutstandingAmount)}; + } + + return accountHolds( + view, account, mptIssue, zeroIfFrozen, zeroIfUnauthorized, j); +} + +[[nodiscard]] STAmount +accountCanSend( + ReadView const& view, + AccountID const& account, + Asset const& asset, + FreezeHandling zeroIfFrozen, + AuthHandling zeroIfUnauthorized, + beast::Journal j) +{ + return std::visit( + [&](auto const& value) { + if constexpr (std::is_same_v< + std::remove_cvref_t, + Issue>) + { + return accountCanSend(view, account, value, zeroIfFrozen, j); + } + return accountCanSend( + view, account, value, zeroIfFrozen, zeroIfUnauthorized, j); + }, + asset.value()); +} + STAmount accountFunds( ReadView const& view, diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index f87346f299..a5df496fcf 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -201,10 +201,7 @@ LoanPay::preclaim(PreclaimContext const& ctx) // Do not support "partial payments" - if the transaction says to pay X, // then the account must have X available, even if the loan payment takes // less. - // Also assume that anybody taking loans is not using "community credit", - // which would let an IOU balance go negative up to the other side's limit. - // This may change in a later version. - if (auto const balance = accountHolds( + if (auto const balance = accountCanSend( ctx.view, account, asset, @@ -427,11 +424,11 @@ LoanPay::doApply() } #if !NDEBUG - auto const accountBalanceBefore = - accountHolds(view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); + auto const accountBalanceBefore = accountCanSend( + view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); auto const vaultBalanceBefore = account_ == vaultPseudoAccount ? STAmount{asset, 0} - : accountHolds( + : accountCanSend( view, vaultPseudoAccount, asset, @@ -440,7 +437,7 @@ LoanPay::doApply() j_); auto const brokerBalanceBefore = account_ == brokerPayee ? STAmount{asset, 0} - : accountHolds( + : accountCanSend( view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); #endif @@ -483,11 +480,11 @@ LoanPay::doApply() return ter; #if !NDEBUG - auto const accountBalanceAfter = - accountHolds(view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); + auto const accountBalanceAfter = accountCanSend( + view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); auto const vaultBalanceAfter = account_ == vaultPseudoAccount ? STAmount{asset, 0} - : accountHolds( + : accountCanSend( view, vaultPseudoAccount, asset, @@ -496,7 +493,7 @@ LoanPay::doApply() j_); auto const brokerBalanceAfter = account_ == brokerPayee ? STAmount{asset, 0} - : accountHolds( + : accountCanSend( view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); XRPL_ASSERT_PARTS( From 044b9b82175cbe6dbe4fc051b3e664783d03b32d Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 23 Oct 2025 12:54:31 -0400 Subject: [PATCH 149/291] *facepalm* --- src/test/unit_test/multi_runner.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/unit_test/multi_runner.cpp b/src/test/unit_test/multi_runner.cpp index 7e31b28f5a..796d1241c9 100644 --- a/src/test/unit_test/multi_runner.cpp +++ b/src/test/unit_test/multi_runner.cpp @@ -464,7 +464,7 @@ multi_runner_parent::~multi_runner_parent() continue_message_queue_ = false; message_queue_thread_.join(); - add_failures(running_suites_.count()); + add_failures(running_suites_.size()); print_results(os_); From e306a7c75f04156e44897b04cd4940355788e88f Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 23 Oct 2025 14:11:31 -0400 Subject: [PATCH 150/291] Fix some build errors --- src/xrpld/app/misc/LendingHelpers.h | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index f15402cc83..ab86f841d8 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -865,14 +865,18 @@ computeLatePayment( XRPL_ASSERT( roundedLateInterest >= 0, "ripple::detail::computeLatePayment : valid late interest"); + XRPL_ASSERT_PARTS( + periodic.specialCase != SpecialCase::extra, + "ripple::detail::computeLatePayment", + "no extra parts to this payment"); + // Copy the periodic payment values, and add on the late interest. + // This preserves all the other fields without having to enumerate them. + PaymentComponents inner = periodic; + inner.rawInterest += rawLateInterest; + inner.roundedInterest += roundedLateInterest; + PaymentComponentsPlus const late{ - PaymentComponents{ - .rawInterest = periodic.rawInterest + rawLateInterest, - .rawPrincipal = periodic.rawPrincipal, - .rawManagementFee = periodic.rawManagementFee, - .roundedInterest = periodic.roundedInterest + roundedLateInterest, - .roundedPrincipal = periodic.roundedPrincipal, - .roundedManagementFee = periodic.roundedManagementFee}, + inner, // A late payment pays both the normal fee, and the extra fees periodic.extraFee + latePaymentFee + roundedLateManagementFee, // A late payment increases the value of the loan by the difference @@ -1575,7 +1579,6 @@ loanMakePayment( auto const periodicPayment = loan->at(sfPeriodicPayment); auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDate); - std::uint32_t const startDate = loan->at(sfStartDate); std::uint32_t const paymentInterval = loan->at(sfPaymentInterval); // Compute the normal periodic rate, payment, etc. From 9f5bc8f0dae4aae086d5a8597c5c05b5dfec3da1 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 23 Oct 2025 14:28:03 -0400 Subject: [PATCH 151/291] Fix more build errors --- src/xrpld/app/misc/LendingHelpers.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index ab86f841d8..bd12843257 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -1190,10 +1190,10 @@ computeLoanProperties( // over payments) auto const loanScale = totalValueOutstanding.exponent(); XRPL_ASSERT_PARTS( - totalValueOutstanding.integral() && loanScale == 0 || - !totalValueOutstanding.integral() && - loanScale == - static_cast(totalValueOutstanding).exponent(), + (totalValueOutstanding.integral() && loanScale == 0) || + (!totalValueOutstanding.integral() && + loanScale == + static_cast(totalValueOutstanding).exponent()), "ripple::computeLoanProperties", "loanScale value fits expectations"); From f60e2986270310969f01a2f44228cf40bdb080e2 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Thu, 23 Oct 2025 15:51:30 -0400 Subject: [PATCH 152/291] Extend LoanBroaker and Loan unit-tests. (#5863) - Add convenience functions to MPT test-framework. --- src/test/app/LoanBroker_test.cpp | 323 ++++++++++++++++++ src/test/app/Loan_test.cpp | 223 ++++++++++++ src/test/jtx/mpt.h | 2 +- src/xrpld/app/misc/LendingHelpers.h | 3 + .../app/tx/detail/LoanBrokerCoverClawback.cpp | 2 +- .../app/tx/detail/LoanBrokerCoverWithdraw.cpp | 2 +- src/xrpld/app/tx/detail/LoanBrokerDelete.cpp | 12 +- src/xrpld/app/tx/detail/LoanDelete.cpp | 4 +- src/xrpld/app/tx/detail/LoanManage.cpp | 2 + 9 files changed, 562 insertions(+), 11 deletions(-) diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 7fee30943c..259be872ed 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -851,12 +851,335 @@ class LoanBroker_test : public beast::unit_test::suite BEAST_EXPECT(env.ownerCount(alice) == aliceOriginalCount); } + enum LoanBrokerTest { + CoverClawback, + CoverDeposit, + CoverWithdraw, + Delete, + Set + }; + + void + testLoanBroker( + std::function getAsset, + LoanBrokerTest brokerTest) + { + using namespace jtx; + using namespace loanBroker; + Account const issuer{"issuer"}; + Account const alice{"alice"}; + Env env(*this); + Vault vault{env}; + + env.fund(XRP(100'000), issuer, alice); + env.close(); + + PrettyAsset const asset = [&]() { + if (getAsset) + return getAsset(env, issuer, alice); + env(trust(alice, issuer["IOU"](1'000'000))); + env.close(); + return PrettyAsset(issuer["IOU"]); + }(); + + env(pay(issuer, alice, asset(100'000))); + env.close(); + + auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = asset}); + env(tx); + env.close(); + auto const le = env.le(vaultKeylet); + VaultInfo vaultInfo = [&]() { + if (BEAST_EXPECT(le)) + return VaultInfo{asset, vaultKeylet.key, le->at(sfAccount)}; + return VaultInfo{asset, {}, {}}; + }(); + if (vaultInfo.vaultID == uint256{}) + return; + + env(vault.deposit( + {.depositor = alice, .id = vaultKeylet.key, .amount = asset(50)})); + env.close(); + + auto const brokerKeylet = + keylet::loanbroker(alice.id(), env.seq(alice)); + env(set(alice, vaultInfo.vaultID)); + env.close(); + + auto broker = env.le(brokerKeylet); + if (!BEAST_EXPECT(broker)) + return; + + if (brokerTest == CoverDeposit) + { + // preclaim: tecWRONG_ASET + env(coverDeposit(alice, brokerKeylet.key, issuer["BAD"](10)), + ter(tecWRONG_ASSET)); + + // preclaim: tecINSUFFICIENT_FUNDS + env(pay(alice, issuer, asset(100'000 - 50))); + env.close(); + env(coverDeposit(alice, brokerKeylet.key, vaultInfo.asset(10)), + ter(tecINSUFFICIENT_FUNDS)); + + // preclaim: tecFROZEN + env(fset(issuer, asfGlobalFreeze)); + env.close(); + env(coverDeposit(alice, brokerKeylet.key, vaultInfo.asset(10)), + ter(tecFROZEN)); + } + else + // Fund the cover deposit + env(coverDeposit(alice, brokerKeylet.key, vaultInfo.asset(10))); + env.close(); + + if (brokerTest == CoverWithdraw) + { + // preclaim: tecWRONG_ASSSET + env(coverWithdraw(alice, brokerKeylet.key, issuer["BAD"](10)), + ter(tecWRONG_ASSET)); + + // preclaim: tecNO_DST + Account const bogus{"bogus"}; + env(coverWithdraw(alice, brokerKeylet.key, asset(10)), + destination(bogus), + ter(tecNO_DST)); + + // preclaim: tecDST_TAG_NEEDED + Account const dest{"dest"}; + env.fund(XRP(1'000), dest); + env(fset(dest, asfRequireDest)); + env.close(); + env(coverWithdraw(alice, brokerKeylet.key, asset(10)), + destination(dest), + ter(tecDST_TAG_NEEDED)); + + // preclaim: tecNO_PERMISSION + env(fclear(dest, asfRequireDest)); + env(fset(dest, asfDepositAuth)); + env.close(); + env(coverWithdraw(alice, brokerKeylet.key, asset(10)), + destination(dest), + ter(tecNO_PERMISSION)); + + // preclaim: tecFROZEN + env(trust(dest, asset(1'000))); + env(fclear(dest, asfDepositAuth)); + env(fset(issuer, asfGlobalFreeze)); + env.close(); + env(coverWithdraw(alice, brokerKeylet.key, asset(10)), + destination(dest), + ter(tecFROZEN)); + + // preclaim:: tecFROZEN (deep frozen) + env(fclear(issuer, asfGlobalFreeze)); + env(trust( + issuer, asset(1'000), dest, tfSetFreeze | tfSetDeepFreeze)); + env(coverWithdraw(alice, brokerKeylet.key, asset(10)), + destination(dest), + ter(tecFROZEN)); + } + + if (brokerTest == CoverClawback) + { + if (asset.holds()) + { + // preclaim: AllowTrustLineClaback is not set + env(coverClawback(issuer), + loanBrokerID(brokerKeylet.key), + amount(vaultInfo.asset(2)), + ter(tecNO_PERMISSION)); + + // preclaim: NoFreeze is set + env(fset(issuer, asfAllowTrustLineClawback | asfNoFreeze)); + env.close(); + env(coverClawback(issuer), + loanBrokerID(brokerKeylet.key), + amount(vaultInfo.asset(2)), + ter(tecNO_PERMISSION)); + } + else + { + // preclaim: MPTCanClawback is not set or MPTCAnLock is not set + env(coverClawback(issuer), + loanBrokerID(brokerKeylet.key), + amount(vaultInfo.asset(2)), + ter(tecNO_PERMISSION)); + } + env.close(); + } + + if (brokerTest == Delete) + { + Account const borrower{"borrower"}; + env.fund(XRP(1'000), borrower); + env(loan::set(borrower, brokerKeylet.key, asset(50).value()), + sig(sfCounterpartySignature, alice), + fee(env.current()->fees().base * 2)); + + // preclaim: tecHAS_OBLIGATIONS + env(del(alice, brokerKeylet.key), ter(tecHAS_OBLIGATIONS)); + } + else + env(del(alice, brokerKeylet.key)); + + if (brokerTest == Set) + { + if (asset.holds()) + { + env(fclear(issuer, asfDefaultRipple)); + env.close(); + // preclaim: DefaultRipple is not set + env(set(alice, vaultInfo.vaultID), ter(terNO_RIPPLE)); + + env(fset(issuer, asfDefaultRipple)); + env.close(); + } + + auto const amt = env.balance(alice) - + env.current()->fees().accountReserve(env.ownerCount(alice)); + env(pay(alice, issuer, amt)); + + // preclaim:: tecINSUFFICIENT_RESERVE + env(set(alice, vaultInfo.vaultID), ter(tecINSUFFICIENT_RESERVE)); + } + } + + void + testInvalidLoanBrokerCoverClawback() + { + testcase("Invalid LoanBrokerCoverClawback"); + using namespace jtx; + using namespace loanBroker; + + // preflight + { + Account const alice{"alice"}; + Account const issuer{"issuer"}; + auto const USD = alice["USD"]; + Env env(*this); + env.fund(XRP(100'000), alice); + env.close(); + + auto jtx = env.jt(coverClawback(alice), amount(USD(100))); + + // holder == account + env(jtx, ter(temINVALID)); + + // holder == beast::zero + STAmount bad(Issue{USD.currency, beast::zero}, 100); + jtx.jv[sfAmount] = bad.getJson(); + jtx.stx = env.ust(jtx); + Serializer s; + jtx.stx->add(s); + auto const jrr = env.rpc("submit", strHex(s.slice()))[jss::result]; + // fails in doSubmit() on STTx construction + BEAST_EXPECT(jrr[jss::error] == "invalidTransaction"); + BEAST_EXPECT(jrr[jss::error_exception] == "invalid native account"); + } + + // preclaim + + // Issue: + // AllowTrustLineClawback is not set or NoFreeze is set + testLoanBroker({}, CoverClawback); + + // MPTIssue: + // MPTCanClawback is not set + testLoanBroker( + [&](Env& env, Account const& issuer, Account const& alice) -> MPT { + MPTTester mpt( + {.env = env, .issuer = issuer, .holders = {alice}}); + return mpt; + }, + CoverClawback); + // MPTCanLock is not set + testLoanBroker( + [&](Env& env, Account const& issuer, Account const& alice) -> MPT { + MPTTester mpt( + {.env = env, + .issuer = issuer, + .holders = {alice}, + .flags = MPTDEXFlags | tfMPTCanClawback}); + return mpt; + }, + CoverClawback); + } + + void + testInvalidLoanBrokerCoverDeposit() + { + testcase("Invalid LoanBrokerCoverDeposit"); + using namespace jtx; + + // preclaim: + // tecWRONG_ASSET, tecINSUFFICIENT_FUNDS, frozen asset + testLoanBroker({}, CoverDeposit); + } + + void + testInvalidLoanBrokerCoverWithdraw() + { + testcase("Invalid LoanBrokerCoverWithdraw"); + using namespace jtx; + + /* + preflight: illegal net + isLegalNet() check is probably redundant. STAmount parsing + should throw an exception on deserialize + + preclaim: tecWRONG_ASSET, tecNO_DST, tecDST_TAG_NEEDED, + tecNO_PERMISSION, checkFrozen failure, checkDeepFrozenFailure, + second+third tecINSUFFICIENT_FUNDS (can this happen)? + doApply: tecPATH_DRY (can it happen, funds already checked?) + */ + testLoanBroker({}, CoverWithdraw); + } + + void + testInvalidLoanBrokerDelete() + { + using namespace jtx; + testcase("Invalid LoanBrokerDelete"); + /* + preclaim: tecHAS_OBLIGATIONS + doApply: + accountSend failure, removeEmptyHolding failure, + all tecHAS_OBLIGATIONS (can any of these happen?) + */ + testLoanBroker({}, Delete); + } + + void + testInvalidLoanBrokerSet() + { + using namespace jtx; + testcase("Invalid LoanBrokerSet"); + + /*preclaim: canAddHolding failure (can it happen with MPT? + can't create Vault if CanTransfer is not enabled.) + doApply: + first+second dirLink failure, createPseudoAccount failure, + addEmptyHolding failure + can any of these happen? + */ + testLoanBroker({}, Set); + } + public: void run() override { testDisabled(); testLifecycle(); + testInvalidLoanBrokerCoverClawback(); + testInvalidLoanBrokerCoverDeposit(); + testInvalidLoanBrokerCoverWithdraw(); + testInvalidLoanBrokerDelete(); + testInvalidLoanBrokerSet(); // TODO: Write clawback failure tests with an issuer / MPT that doesn't // have the right flags set. diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index c42367332c..fefa7fc552 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -2827,6 +2827,224 @@ class Loan_test : public beast::unit_test::suite pass(); } + void + testInvalidLoanDelete() + { + testcase("Invalid LoanDelete"); + using namespace jtx; + using namespace loan; + + // preflight: temINVALID, LoanID == zero + { + Account const alice{"alice"}; + Env env(*this); + env.fund(XRP(1'000), alice); + env.close(); + env(del(alice, beast::zero), ter(temINVALID)); + } + } + + void + testInvalidLoanManage() + { + testcase("Invalid LoanManage"); + using namespace jtx; + using namespace loan; + + // preflight: temINVALID, LoanID == zero + { + Account const alice{"alice"}; + Env env(*this); + env.fund(XRP(1'000), alice); + env.close(); + env(manage(alice, beast::zero, tfLoanDefault), ter(temINVALID)); + } + } + + void + testInvalidLoanPay() + { + testcase("Invalid LoanPay"); + using namespace jtx; + using namespace loan; + Account const lender{"lender"}; + Account const issuer{"issuer"}; + Account const borrower{"borrower"}; + auto const IOU = issuer["IOU"]; + + // preclaim + Env env(*this); + env.fund(XRP(1'000), lender, issuer, borrower); + env(trust(lender, IOU(10'000'000))); + env(pay(issuer, lender, IOU(5'000'000))); + BrokerInfo brokerInfo{createVaultAndBroker(env, issuer["IOU"], lender)}; + + auto const loanSetFee = fee(env.current()->fees().base * 2); + STAmount const debtMaximumRequest = brokerInfo.asset(1'000).value(); + + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + sig(sfCounterpartySignature, lender), + loanSetFee); + + env.close(); + + std::uint32_t const loanSequence = 1; + auto const loanKeylet = keylet::loan(brokerInfo.brokerID, loanSequence); + + env(fset(issuer, asfGlobalFreeze)); + env.close(); + + // preclaim: tecFROZEN + env(pay(borrower, loanKeylet.key, debtMaximumRequest), ter(tecFROZEN)); + env.close(); + + env(fclear(issuer, asfGlobalFreeze)); + env.close(); + + auto const pseudoBroker = [&]() -> std::optional { + if (auto brokerSle = + env.le(keylet::loanbroker(brokerInfo.brokerID)); + BEAST_EXPECT(brokerSle)) + { + return Account{"pseudo", brokerSle->at(sfAccount)}; + } + else + { + return std::nullopt; + } + }(); + if (!pseudoBroker) + return; + + // Lender and pseudoaccount must both be frozen + env(trust( + issuer, + lender["IOU"](1'000), + lender, + tfSetFreeze | tfSetDeepFreeze)); + env(trust( + issuer, + (*pseudoBroker)["IOU"](1'000), + *pseudoBroker, + tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // preclaim: tecFROZEN due to deep frozen + env(pay(borrower, loanKeylet.key, debtMaximumRequest), ter(tecFROZEN)); + env.close(); + + // Only one needs to be unfrozen + env(trust( + issuer, lender["IOU"](1'000), tfClearFreeze | tfClearDeepFreeze)); + env.close(); + + env(pay(borrower, loanKeylet.key, debtMaximumRequest)); + env.close(); + + // preclaim: tecKILLED + // note that tecKILLED in loanMakePayment() + // doesn't happen because of the preclaim check. + env(pay(borrower, loanKeylet.key, debtMaximumRequest), ter(tecKILLED)); + } + + void + testInvalidLoanSet() + { + testcase("Invalid LoanSet"); + using namespace jtx; + using namespace loan; + Account const lender{"lender"}; + Account const issuer{"issuer"}; + Account const borrower{"borrower"}; + auto const IOU = issuer["IOU"]; + + auto testWrapper = [&](auto&& test) { + Env env(*this); + env.fund(XRP(1'000), lender, issuer, borrower); + env(trust(lender, IOU(10'000'000))); + env(pay(issuer, lender, IOU(5'000'000))); + BrokerInfo brokerInfo{ + createVaultAndBroker(env, issuer["IOU"], lender)}; + + auto const loanSetFee = fee(env.current()->fees().base * 2); + Number const debtMaximumRequest = brokerInfo.asset(1'000).value(); + test(env, brokerInfo, loanSetFee, debtMaximumRequest); + }; + + // preflight: + testWrapper([&](Env& env, + BrokerInfo const& brokerInfo, + jtx::fee const& loanSetFee, + Number const& debtMaximumRequest) { + // first temBAD_SIGNER: TODO + + // preflightCheckSigningKey() failure: + // can it happen? the signature is checked before transactor + // executes + + JTx tx = env.jt( + set(borrower, brokerInfo.brokerID, debtMaximumRequest), + sig(sfCounterpartySignature, lender), + loanSetFee); + STTx local = *(tx.stx); + auto counterpartySig = + local.getFieldObject(sfCounterpartySignature); + auto badPubKey = counterpartySig.getFieldVL(sfSigningPubKey); + badPubKey[20] ^= 0xAA; + counterpartySig.setFieldVL(sfSigningPubKey, badPubKey); + local.setFieldObject(sfCounterpartySignature, counterpartySig); + Json::Value jvResult; + jvResult[jss::tx_blob] = strHex(local.getSerializer().slice()); + auto res = env.rpc("json", "submit", to_string(jvResult))["result"]; + BEAST_EXPECT( + res[jss::error] == "invalidTransaction" && + res[jss::error_exception] == + "fails local checks: Counterparty: Invalid signature."); + }); + + // preclaim: + testWrapper([&](Env& env, + BrokerInfo const& brokerInfo, + jtx::fee const& loanSetFee, + Number const& debtMaximumRequest) { + // canAddHoldingFailure (IOU only, if MPT doesn't have + // MPTCanTransfer set, then can't create Vault/LoanBroker, + // and LoanSet will fail with different error + env(fclear(issuer, asfDefaultRipple)); + env.close(); + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + sig(sfCounterpartySignature, lender), + loanSetFee, + ter(terNO_RIPPLE)); + }); + + // doApply: + testWrapper([&](Env& env, + BrokerInfo const& brokerInfo, + jtx::fee const& loanSetFee, + Number const& debtMaximumRequest) { + auto const amt = env.balance(borrower) - + env.current()->fees().accountReserve(env.ownerCount(borrower)); + env(pay(borrower, issuer, amt)); + + // tecINSUFFICIENT_RESERVE + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + sig(sfCounterpartySignature, lender), + loanSetFee, + ter(tecINSUFFICIENT_RESERVE)); + + // addEmptyHolding failure + env(pay(issuer, borrower, amt)); + env(fset(issuer, asfGlobalFreeze)); + env.close(); + + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + sig(sfCounterpartySignature, lender), + loanSetFee, + ter(tecFROZEN)); + }); + } + public: void run() override @@ -2841,6 +3059,11 @@ public: testRPC(); testBasicMath(); + + testInvalidLoanDelete(); + testInvalidLoanManage(); + testInvalidLoanPay(); + testInvalidLoanSet(); } }; diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index f84241b309..e24fc9d327 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -296,7 +296,7 @@ public: operator Asset() const; private: - using SLEP = std::shared_ptr; + using SLEP = SLE::const_pointer; bool forObject( std::function const& cb, diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index bd12843257..874ec9887e 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -1549,8 +1549,11 @@ loanMakePayment( if (paymentRemainingProxy == 0 || principalOutstandingProxy == 0) { // Loan complete + // This is already checked in LoanPay::preclaim() + // LCOV_EXCL_START JLOG(j.warn()) << "Loan is already paid off."; return Unexpected(tecKILLED); + // LCOV_EXCL_STOP } auto totalValueOutstandingProxy = loan->at(sfTotalValueOutstanding); diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp index e2358d8ace..d278f733d6 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp @@ -81,7 +81,7 @@ determineBrokerID(ReadView const& view, STTx const& tx) auto const dstAmount = tx[~sfAmount]; if (!dstAmount || !dstAmount->holds()) - return Unexpected{tecINTERNAL}; + return Unexpected{tecINTERNAL}; // LCOV_EXCL_LINE // Since we don't have a LoanBrokerID, holder _should_ be the loan broker's // pseudo-account, but we don't know yet whether it is, so use a generic diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp index 894e69cac8..50793adb1e 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp @@ -209,7 +209,7 @@ LoanBrokerCoverWithdraw::doApply() Payment::getMaxSourceAmount(brokerPseudoID, amount); SLE::pointer sleDst = view().peek(keylet::account(dstAcct)); if (!sleDst) - return tecINTERNAL; + return tecINTERNAL; // LCOV_EXCL_LINE Payment::RipplePaymentParams paymentParams{ .ctx = ctx_, diff --git a/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp b/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp index 2a35d83c31..90703fa5b9 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp @@ -89,7 +89,7 @@ LoanBrokerDelete::doApply() broker->key(), false)) { - return tefBAD_LEDGER; + return tefBAD_LEDGER; // LCOV_EXCL_LINE } if (!view().dirRemove( keylet::ownerDir(vaultPseudoID), @@ -97,7 +97,7 @@ LoanBrokerDelete::doApply() broker->key(), false)) { - return tefBAD_LEDGER; + return tefBAD_LEDGER; // LCOV_EXCL_LINE } { @@ -118,26 +118,26 @@ LoanBrokerDelete::doApply() auto brokerPseudoSLE = view().peek(keylet::account(brokerPseudoID)); if (!brokerPseudoSLE) - return tefBAD_LEDGER; + return tefBAD_LEDGER; // LCOV_EXCL_LINE // Making the payment and removing the empty holding should have deleted any // obligations associated with the broker or broker pseudo-account. if (*brokerPseudoSLE->at(sfBalance)) { JLOG(j_.warn()) << "LoanBrokerDelete: Pseudo-account has a balance"; - return tecHAS_OBLIGATIONS; + return tecHAS_OBLIGATIONS; // LCOV_EXCL_LINE } if (brokerPseudoSLE->at(sfOwnerCount) != 0) { JLOG(j_.warn()) << "LoanBrokerDelete: Pseudo-account still owns objects"; - return tecHAS_OBLIGATIONS; + return tecHAS_OBLIGATIONS; // LCOV_EXCL_LINE } if (auto const directory = keylet::ownerDir(brokerPseudoID); view().read(directory)) { JLOG(j_.warn()) << "LoanBrokerDelete: Pseudo-account has a directory"; - return tecHAS_OBLIGATIONS; + return tecHAS_OBLIGATIONS; // LCOV_EXCL_LINE } view().erase(brokerPseudoSLE); diff --git a/src/xrpld/app/tx/detail/LoanDelete.cpp b/src/xrpld/app/tx/detail/LoanDelete.cpp index 218880a41c..ff91039da9 100644 --- a/src/xrpld/app/tx/detail/LoanDelete.cpp +++ b/src/xrpld/app/tx/detail/LoanDelete.cpp @@ -107,14 +107,14 @@ LoanDelete::doApply() loanSle->at(sfLoanBrokerNode), loanID, false)) - return tefBAD_LEDGER; + return tefBAD_LEDGER; // LCOV_EXCL_LINE // Remove LoanID from Directory of the Borrower. if (!view.dirRemove( keylet::ownerDir(borrower), loanSle->at(sfOwnerNode), loanID, false)) - return tefBAD_LEDGER; + return tefBAD_LEDGER; // LCOV_EXCL_LINE // Delete the Loan object view.erase(loanSle); diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index 00e742f4bb..7b3dcd205b 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -241,9 +241,11 @@ LoanManage::defaultLoan( auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized); if (vaultLossUnrealizedProxy < totalDefaultAmount) { + // LCOV_EXCL_START JLOG(j.warn()) << "Vault unrealized loss is less than the default amount"; return tefBAD_LEDGER; + // LCOV_EXCL_STOP } vaultLossUnrealizedProxy -= totalDefaultAmount; } From 6adb2eca7652cdb35e805d60cab912780ba02047 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 23 Oct 2025 17:58:23 -0400 Subject: [PATCH 153/291] Review feedbak from @tequdev, plus helpers - Fix LoanSet.calculateBaseFee with multisign. - Cleanups. - Add View helper functions for Loan and Vault transactions - preflightDestinationAndTag - checkDestinationAndTag - canSendToAccount - Used the helpers in appropriate Loan and Vault transactions. - They could also be used in older transactions, I'll save that for later. --- include/xrpl/ledger/View.h | 47 ++++++++++++- src/libxrpl/ledger/View.cpp | 67 +++++++++++++++++++ .../app/tx/detail/LoanBrokerCoverWithdraw.cpp | 50 +++----------- src/xrpld/app/tx/detail/LoanSet.cpp | 6 +- src/xrpld/app/tx/detail/VaultWithdraw.cpp | 33 +++------ 5 files changed, 136 insertions(+), 67 deletions(-) diff --git a/include/xrpl/ledger/View.h b/include/xrpl/ledger/View.h index 4343c29b59..c28afd5fdd 100644 --- a/include/xrpl/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -694,7 +694,7 @@ isPseudoAccount(std::shared_ptr sleAcct); getPseudoAccountFields(); [[nodiscard]] inline bool -isPseudoAccount(ReadView const& view, AccountID accountId) +isPseudoAccount(ReadView const& view, AccountID const& accountId) { return isPseudoAccount(view.read(keylet::account(accountId))); } @@ -702,6 +702,51 @@ isPseudoAccount(ReadView const& view, AccountID accountId) [[nodiscard]] TER canAddHolding(ReadView const& view, Asset const& asset); +/** Checks that the destination and destination tag are valid. + + - If destination is set, it must not be zero. + - If not set, there must not be a destination tag. +*/ +[[nodiscard]] NotTEC +preflightDestinationAndTag( + std::optional const& destination, + bool hasDestinationTag); + +/** Validates that the destination SLE and tag are valid + + - Checks that the SLE is not null. + - If the SLE requires a destination tag, checks that there is a tag. +*/ +[[nodiscard]] TER +checkDestinationAndTag(SLE::const_ref toSle, bool hasDestinationTag); + +/** Checks that from can sent to to (with an SLE) + + - Does the checkDestinationAndTag checks, plus: + - if the destination requires deposit auth, checks that the from account has + that auth. +*/ +[[nodiscard]] TER +canSendToAccount( + AccountID const& from, + ReadView const& view, + SLE::const_ref toSle, + bool hasDestinationTag); + +/** Checks that from can sent to to (with an AccountID) + + - Loads the SLE for "to" + - Checks whether the account is trying to send to itself. + Succeeds if the SLE exists, fails if not. + - Then does the checkDestinationAndTag check with an SLE. +*/ +[[nodiscard]] TER +canSendToAccount( + AccountID const& from, + ReadView const& view, + AccountID const& to, + bool hasDestinationTag); + /// Any transactors that call addEmptyHolding() in doApply must call /// canAddHolding() in preflight with the same View and Asset [[nodiscard]] TER diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index eeb704bc3b..c2d5682765 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -1340,6 +1340,73 @@ canAddHolding(ReadView const& view, Asset const& asset) asset.value()); } +[[nodiscard]] NotTEC +preflightDestinationAndTag( + std::optional const& destination, + bool hasDestinationTag) +{ + if (destination) + { + if (*destination == beast::zero) + { + return temMALFORMED; + } + } + else if (hasDestinationTag) + { + return temMALFORMED; + } + + return tesSUCCESS; +} + +[[nodiscard]] TER +checkDestinationAndTag(SLE::const_ref toSle, bool hasDestinationTag) +{ + if (toSle == nullptr) + return tecNO_DST; + + // The tag is basically account-specific information we don't + // understand, but we can require someone to fill it in. + if (toSle->isFlag(lsfRequireDestTag) && !hasDestinationTag) + return tecDST_TAG_NEEDED; // Cannot send without a tag + + return tesSUCCESS; +} + +[[nodiscard]] TER +canSendToAccount( + AccountID const& from, + ReadView const& view, + SLE::const_ref toSle, + bool hasDestinationTag) +{ + if (auto const ret = checkDestinationAndTag(toSle, hasDestinationTag)) + return ret; + + if (toSle->isFlag(lsfDepositAuth)) + { + if (!view.exists(keylet::depositPreauth(toSle->at(sfAccount), from))) + return tecNO_PERMISSION; + } + + return tesSUCCESS; +} + +[[nodiscard]] TER +canSendToAccount( + AccountID const& from, + ReadView const& view, + AccountID const& to, + bool hasDestinationTag) +{ + auto const toSle = view.read(keylet::account(to)); + if (from == to) + return toSle ? (TER)tesSUCCESS : (TER)tecINTERNAL; + + return canSendToAccount(from, view, toSle, hasDestinationTag); +} + [[nodiscard]] TER addEmptyHolding( ApplyView& view, diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp index 50793adb1e..b0f1b98154 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp @@ -43,23 +43,9 @@ LoanBrokerCoverWithdraw::preflight(PreflightContext const& ctx) if (!isLegalNet(dstAmount)) return temBAD_AMOUNT; - if (auto const destination = ctx.tx[~sfDestination]; - destination.has_value()) - { - if (*destination == beast::zero) - { - JLOG(ctx.j.debug()) - << "LoanBrokerCoverWithdraw: zero/empty destination account."; - return temMALFORMED; - } - } - else if (ctx.tx.isFieldPresent(sfDestinationTag)) - { - JLOG(ctx.j.debug()) - << "LoanBrokerCoverWithdraw: sfDestinationTag is set but " - "sfDestination is not"; - return temMALFORMED; - } + if (auto const ret = preflightDestinationAndTag( + ctx.tx[~sfDestination], ctx.tx.isFieldPresent(sfDestinationTag))) + return ret; return tesSUCCESS; } @@ -73,11 +59,7 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) auto const brokerID = tx[sfLoanBrokerID]; auto const amount = tx[sfAmount]; - auto const dstAcct = [&]() -> AccountID { - if (auto const dst = tx[~sfDestination]) - return *dst; - return account; - }(); + auto const dstAcct = tx[~sfDestination].value_or(account); auto const sleBroker = ctx.view.read(keylet::loanbroker(brokerID)); if (!sleBroker) @@ -103,19 +85,13 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) AuthType authType = AuthType::Legacy; if (account != dstAcct) { - auto const sleDst = ctx.view.read(keylet::account(dstAcct)); - if (sleDst == nullptr) - return tecNO_DST; + if (auto const ret = canSendToAccount( + account, + ctx.view, + dstAcct, + tx.isFieldPresent(sfDestinationTag))) + return ret; - if (sleDst->isFlag(lsfRequireDestTag) && - !tx.isFieldPresent(sfDestinationTag)) - return tecDST_TAG_NEEDED; // Cannot send without a tag - - if (sleDst->isFlag(lsfDepositAuth)) - { - if (!ctx.view.exists(keylet::depositPreauth(dstAcct, account))) - return tecNO_PERMISSION; - } // The destination account must have consented to receive the asset by // creating a RippleState or MPToken authType = AuthType::StrongAuth; @@ -171,11 +147,7 @@ LoanBrokerCoverWithdraw::doApply() auto const brokerID = tx[sfLoanBrokerID]; auto const amount = tx[sfAmount]; - auto const dstAcct = [&]() -> AccountID { - if (auto const dst = tx[~sfDestination]) - return *dst; - return account_; - }(); + auto const dstAcct = tx[~sfDestination].value_or(account_); auto broker = view().peek(keylet::loanbroker(brokerID)); if (!broker) diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index af18e12d27..61eff570af 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -172,8 +172,8 @@ LoanSet::calculateBaseFee(ReadView const& view, STTx const& tx) // for the transaction. Note that unlike the base class, the single signer // is counted if present. It will only be absent in a batch inner // transaction. - std::size_t const signerCount = tx.isFieldPresent(sfSigners) - ? tx.getFieldArray(sfSigners).size() + std::size_t const signerCount = counterSig.isFieldPresent(sfSigners) + ? counterSig.getFieldArray(sfSigners).size() : (counterSig.isFieldPresent(sfTxnSignature) ? 1 : 0); return normalCost + (signerCount * baseFee); @@ -513,7 +513,7 @@ LoanSet::doApply() brokerOwnerSle->at(sfBalance).value().xrp(), vaultAsset, j_); - !isTesSuccess(ter) && ter != tecDUPLICATE) + ter && ter != tecDUPLICATE) // ignore tecDUPLICATE. That means the holding already exists, // and is fine here return ter; diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.cpp b/src/xrpld/app/tx/detail/VaultWithdraw.cpp index bdf00f5c56..37c0f63e51 100644 --- a/src/xrpld/app/tx/detail/VaultWithdraw.cpp +++ b/src/xrpld/app/tx/detail/VaultWithdraw.cpp @@ -42,16 +42,9 @@ VaultWithdraw::preflight(PreflightContext const& ctx) if (ctx.tx[sfAmount] <= beast::zero) return temBAD_AMOUNT; - if (auto const destination = ctx.tx[~sfDestination]; - destination.has_value()) - { - if (*destination == beast::zero) - { - JLOG(ctx.j.debug()) - << "VaultWithdraw: zero/empty destination account."; - return temMALFORMED; - } - } + if (auto const ret = preflightDestinationAndTag( + ctx.tx[~sfDestination], ctx.tx.isFieldPresent(sfDestinationTag))) + return ret; return tesSUCCESS; } @@ -111,21 +104,13 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx) auto const account = ctx.tx[sfAccount]; auto const dstAcct = ctx.tx[~sfDestination].value_or(account); - auto const sleDst = ctx.view.read(keylet::account(dstAcct)); - if (sleDst == nullptr) - return account == dstAcct ? tecINTERNAL : tecNO_DST; - if (sleDst->isFlag(lsfRequireDestTag) && - !ctx.tx.isFieldPresent(sfDestinationTag)) - return tecDST_TAG_NEEDED; // Cannot send without a tag - - // Withdrawal to a 3rd party destination account is essentially a transfer, - // via shares in the vault. Enforce all the usual asset transfer checks. - if (account != dstAcct && sleDst->isFlag(lsfDepositAuth)) - { - if (!ctx.view.exists(keylet::depositPreauth(dstAcct, account))) - return tecNO_PERMISSION; - } + if (auto const ret = canSendToAccount( + account, + ctx.view, + dstAcct, + ctx.tx.isFieldPresent(sfDestinationTag))) + return ret; // If sending to Account (i.e. not a transfer), we will also create (only // if authorized) a trust line or MPToken as needed, in doApply(). From 66b547f6fc2df14ca9d145c2378ae72242ccbeee Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 23 Oct 2025 19:22:52 -0400 Subject: [PATCH 154/291] Review feedback from @Tapanito - Correct interest rounding calculation - Fix some comments - Don't explicitly break out of the multiple payment loop for the final payment. Assert that it will exit by the loop condition. --- src/xrpld/app/misc/LendingHelpers.h | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 874ec9887e..f96e437647 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -271,7 +271,7 @@ computeRoundedPrincipalComponent( roundToAsset(asset, rawPrincipal + diff, scale, Number::downward); // For particular loans, it's entirely possible for many of the first - // rounded payments to be all principal. + // rounded payments to be all interest. XRPL_ASSERT_PARTS( p >= 0, "rippled::detail::computeRoundedPrincipalComponent", @@ -338,8 +338,7 @@ computeRoundedInterestComponent( interestOutstanding - rawInterestOutstanding, scale, Number::downward); - if (diff < beast::zero) - roundedInterest += diff; + roundedInterest += diff; } // However, we cannot allow negative interest payments, therefore we need to @@ -974,9 +973,10 @@ computeFullPayment( // that's ok, because it's not used until it's recombined with // roundedManagementFee. closePaymentFee + roundedFullManagementFee - managementFeeOutstanding, - // A full payment decreases the value of the loan by the - // difference between the interest paid and the expected - // outstanding interest return + // A full payment changes the value of the loan by the difference + // between expected outstanding interest return and the actual interest + // paid. This value can be positive (increasing the value) or negative + // (decreasing the value). roundedFullInterest - totalInterestOutstanding}; XRPL_ASSERT_PARTS( @@ -1716,8 +1716,11 @@ loanMakePayment( paymentInterval); ++numPayments; - if (nextPayment.specialCase == SpecialCase::final) - break; + XRPL_ASSERT_PARTS( + (nextPayment.specialCase == SpecialCase::final) == + (paymentRemainingProxy == 0), + "ripple::loanMakePayment", + "final payment is the final payment"); } XRPL_ASSERT_PARTS( From 7c45f6a673d66f12ec4c190fcf52a170e1c8457a Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 24 Oct 2025 00:30:06 -0400 Subject: [PATCH 155/291] Fix broken Vault unit tests introduced in 6adb2ec - canSendToAccount will check if a destination tag is required _before_ checking if the sender is the destination. This is the original VaultWithdraw behavior, and I want to stay consistent. --- include/xrpl/ledger/View.h | 1 + src/libxrpl/ledger/View.cpp | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/include/xrpl/ledger/View.h b/include/xrpl/ledger/View.h index c28afd5fdd..47dc8743bc 100644 --- a/include/xrpl/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -730,6 +730,7 @@ checkDestinationAndTag(SLE::const_ref toSle, bool hasDestinationTag); canSendToAccount( AccountID const& from, ReadView const& view, + AccountID const& to, SLE::const_ref toSle, bool hasDestinationTag); diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index c2d5682765..98c1b25dfa 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -1378,15 +1378,19 @@ checkDestinationAndTag(SLE::const_ref toSle, bool hasDestinationTag) canSendToAccount( AccountID const& from, ReadView const& view, + AccountID const& to, SLE::const_ref toSle, bool hasDestinationTag) { if (auto const ret = checkDestinationAndTag(toSle, hasDestinationTag)) return ret; + if (from == to) + return tesSUCCESS; + if (toSle->isFlag(lsfDepositAuth)) { - if (!view.exists(keylet::depositPreauth(toSle->at(sfAccount), from))) + if (!view.exists(keylet::depositPreauth(to, from))) return tecNO_PERMISSION; } @@ -1401,10 +1405,8 @@ canSendToAccount( bool hasDestinationTag) { auto const toSle = view.read(keylet::account(to)); - if (from == to) - return toSle ? (TER)tesSUCCESS : (TER)tecINTERNAL; - return canSendToAccount(from, view, toSle, hasDestinationTag); + return canSendToAccount(from, view, to, toSle, hasDestinationTag); } [[nodiscard]] TER From 78ef800e30f603d78507e6df8a7ca0b273a894e5 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 24 Oct 2025 00:03:32 -0400 Subject: [PATCH 156/291] Fix more problems introduced by 6adb2ec - Renamed canSendToAccount to canWithdraw, because the semantics are a little different from a payment. Notably, if withdrawing to self, you can still include a destination tag. - Simplified the interface to canWithdraw to just pass in the STTx. - preflightDestinationAndTag is pretty pointless now, so removed it. --- include/xrpl/ledger/View.h | 66 ++++++++++++------- src/libxrpl/ledger/View.cpp | 35 ++++------ src/test/app/LoanBroker_test.cpp | 7 -- .../app/tx/detail/LoanBrokerCoverWithdraw.cpp | 16 ++--- src/xrpld/app/tx/detail/VaultWithdraw.cpp | 16 ++--- 5 files changed, 69 insertions(+), 71 deletions(-) diff --git a/include/xrpl/ledger/View.h b/include/xrpl/ledger/View.h index 47dc8743bc..8de5a8decb 100644 --- a/include/xrpl/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -702,16 +702,6 @@ isPseudoAccount(ReadView const& view, AccountID const& accountId) [[nodiscard]] TER canAddHolding(ReadView const& view, Asset const& asset); -/** Checks that the destination and destination tag are valid. - - - If destination is set, it must not be zero. - - If not set, there must not be a destination tag. -*/ -[[nodiscard]] NotTEC -preflightDestinationAndTag( - std::optional const& destination, - bool hasDestinationTag); - /** Validates that the destination SLE and tag are valid - Checks that the SLE is not null. @@ -720,34 +710,60 @@ preflightDestinationAndTag( [[nodiscard]] TER checkDestinationAndTag(SLE::const_ref toSle, bool hasDestinationTag); -/** Checks that from can sent to to (with an SLE) - - - Does the checkDestinationAndTag checks, plus: - - if the destination requires deposit auth, checks that the from account has - that auth. -*/ +/** Checks that can withdraw funds from an object to itself or a destination. + * + * The receiver may be either the submitting account (sfAccount) or a different + * destination account (sfDestination). + * + * - Checks that the receiver account exists. + * - If the receiver requires a destination tag, check that one exists, even + * if withdrawing to self. + * - If withdrawing to self, succeed. + * - If not, checks if the receiver requires deposit authorization, and if + * the sender has it. + */ [[nodiscard]] TER -canSendToAccount( +canWithdraw( AccountID const& from, ReadView const& view, AccountID const& to, SLE::const_ref toSle, bool hasDestinationTag); -/** Checks that from can sent to to (with an AccountID) - - - Loads the SLE for "to" - - Checks whether the account is trying to send to itself. - Succeeds if the SLE exists, fails if not. - - Then does the checkDestinationAndTag check with an SLE. -*/ +/** Checks that can withdraw funds from an object to itself or a destination. + * + * The receiver may be either the submitting account (sfAccount) or a different + * destination account (sfDestination). + * + * - Checks that the receiver account exists. + * - If the receiver requires a destination tag, check that one exists, even + * if withdrawing to self. + * - If withdrawing to self, succeed. + * - If not, checks if the receiver requires deposit authorization, and if + * the sender has it. + */ [[nodiscard]] TER -canSendToAccount( +canWithdraw( AccountID const& from, ReadView const& view, AccountID const& to, bool hasDestinationTag); +/** Checks that can withdraw funds from an object to itself or a destination. + * + * The receiver may be either the submitting account (sfAccount) or a different + * destination account (sfDestination). + * + * - Checks that the receiver account exists. + * - If the receiver requires a destination tag, check that one exists, even + * if withdrawing to self. + * - If withdrawing to self, succeed. + * - If not, checks if the receiver requires deposit authorization, and if + * the sender has it. + */ +[[nodiscard]] TER +canWithdraw(ReadView const& view, STTx const& tx); + /// Any transactors that call addEmptyHolding() in doApply must call /// canAddHolding() in preflight with the same View and Asset [[nodiscard]] TER diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index 98c1b25dfa..c52b313df8 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -1340,26 +1340,6 @@ canAddHolding(ReadView const& view, Asset const& asset) asset.value()); } -[[nodiscard]] NotTEC -preflightDestinationAndTag( - std::optional const& destination, - bool hasDestinationTag) -{ - if (destination) - { - if (*destination == beast::zero) - { - return temMALFORMED; - } - } - else if (hasDestinationTag) - { - return temMALFORMED; - } - - return tesSUCCESS; -} - [[nodiscard]] TER checkDestinationAndTag(SLE::const_ref toSle, bool hasDestinationTag) { @@ -1375,7 +1355,7 @@ checkDestinationAndTag(SLE::const_ref toSle, bool hasDestinationTag) } [[nodiscard]] TER -canSendToAccount( +canWithdraw( AccountID const& from, ReadView const& view, AccountID const& to, @@ -1398,7 +1378,7 @@ canSendToAccount( } [[nodiscard]] TER -canSendToAccount( +canWithdraw( AccountID const& from, ReadView const& view, AccountID const& to, @@ -1406,7 +1386,16 @@ canSendToAccount( { auto const toSle = view.read(keylet::account(to)); - return canSendToAccount(from, view, to, toSle, hasDestinationTag); + return canWithdraw(from, view, to, toSle, hasDestinationTag); +} + +[[nodiscard]] TER +canWithdraw(ReadView const& view, STTx const& tx) +{ + auto const from = tx[sfAccount]; + auto const to = tx[~sfDestination].value_or(from); + + return canWithdraw(from, view, to, tx.isFieldPresent(sfDestinationTag)); } [[nodiscard]] TER diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 259be872ed..57bff0bbfa 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -384,13 +384,6 @@ class LoanBroker_test : public beast::unit_test::suite destination(AccountID{}), ter(temMALFORMED)); - // If a destination tag is specified, a destination must be - // specified, too - env(coverWithdraw(alice, keylet.key, vault.asset(1)), - dtag(123), - ter(temMALFORMED)); - verifyCoverAmount(10); - // Withdraw some of the cover amount env(coverWithdraw(alice, keylet.key, vault.asset(7))); env.close(); diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp index b0f1b98154..e7143bcf50 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp @@ -43,9 +43,13 @@ LoanBrokerCoverWithdraw::preflight(PreflightContext const& ctx) if (!isLegalNet(dstAmount)) return temBAD_AMOUNT; - if (auto const ret = preflightDestinationAndTag( - ctx.tx[~sfDestination], ctx.tx.isFieldPresent(sfDestinationTag))) - return ret; + if (auto const destination = ctx.tx[~sfDestination]) + { + if (*destination == beast::zero) + { + return temMALFORMED; + } + } return tesSUCCESS; } @@ -85,11 +89,7 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) AuthType authType = AuthType::Legacy; if (account != dstAcct) { - if (auto const ret = canSendToAccount( - account, - ctx.view, - dstAcct, - tx.isFieldPresent(sfDestinationTag))) + if (auto const ret = canWithdraw(ctx.view, tx)) return ret; // The destination account must have consented to receive the asset by diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.cpp b/src/xrpld/app/tx/detail/VaultWithdraw.cpp index 37c0f63e51..d35fc56741 100644 --- a/src/xrpld/app/tx/detail/VaultWithdraw.cpp +++ b/src/xrpld/app/tx/detail/VaultWithdraw.cpp @@ -42,9 +42,13 @@ VaultWithdraw::preflight(PreflightContext const& ctx) if (ctx.tx[sfAmount] <= beast::zero) return temBAD_AMOUNT; - if (auto const ret = preflightDestinationAndTag( - ctx.tx[~sfDestination], ctx.tx.isFieldPresent(sfDestinationTag))) - return ret; + if (auto const destination = ctx.tx[~sfDestination]) + { + if (*destination == beast::zero) + { + return temMALFORMED; + } + } return tesSUCCESS; } @@ -105,11 +109,7 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx) auto const account = ctx.tx[sfAccount]; auto const dstAcct = ctx.tx[~sfDestination].value_or(account); - if (auto const ret = canSendToAccount( - account, - ctx.view, - dstAcct, - ctx.tx.isFieldPresent(sfDestinationTag))) + if (auto const ret = canWithdraw(ctx.view, ctx.tx)) return ret; // If sending to Account (i.e. not a transfer), we will also create (only From 88a770c71b30b30c965ad819c3c52153e07a6907 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Fri, 24 Oct 2025 12:32:26 -0400 Subject: [PATCH 157/291] Don't create empty holding for MPT or trust line issuer (#5877) - Adds a check to the MPToken creation invariant to ensure none are created for the issuer. - `addEmptyHolding()` will return success without doing anything for these scenarios. There is nothing to do, as with XRP. --------- Co-authored-by: Ed Hennis --- src/libxrpl/ledger/View.cpp | 6 ++-- src/test/app/Loan_test.cpp | 41 ++++++++++++++++++++++ src/xrpld/app/tx/detail/InvariantCheck.cpp | 28 +++++++++++++-- src/xrpld/app/tx/detail/InvariantCheck.h | 3 ++ 4 files changed, 74 insertions(+), 4 deletions(-) diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index c52b313df8..e4b295c415 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -1406,8 +1406,8 @@ addEmptyHolding( Issue const& issue, beast::Journal journal) { - // Every account can hold XRP. - if (issue.native()) + // Every account can hold XRP. An issuer can issue directly. + if (issue.native() || accountID == issue.getIssuer()) return tesSUCCESS; auto const& issuerId = issue.getIssuer(); @@ -1468,6 +1468,8 @@ addEmptyHolding( return tefINTERNAL; // LCOV_EXCL_LINE if (view.peek(keylet::mptoken(mptID, accountID))) return tecDUPLICATE; + if (accountID == mptIssue.getIssuer()) + return tesSUCCESS; return authorizeMPToken(view, priorBalance, mptID, accountID, journal); } diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index fefa7fc552..7ce7dd7a4d 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -2827,6 +2827,46 @@ class Loan_test : public beast::unit_test::suite pass(); } + void + testIssuerLoan() + { + testcase << "Issuer Loan"; + + using namespace jtx; + using namespace loan; + Account const issuer("issuer"); + Account const borrower = issuer; + Account const lender("lender"); + Env env(*this); + + env.fund(XRP(1'000), issuer, lender); + + std::int64_t constexpr issuerBalance = 10'000'000; + MPTTester asset( + {.env = env, + .issuer = issuer, + .holders = {lender}, + .pay = issuerBalance}); + + auto const broker = createVaultAndBroker(env, asset, lender, 200); + auto const loanSetFee = fee(env.current()->fees().base * 2); + // Create Loan + env(set(borrower, broker.brokerID, 200), + sig(sfCounterpartySignature, lender), + loanSetFee); + env.close(); + // Issuer should not create MPToken + BEAST_EXPECT(!env.le(keylet::mptoken(asset.issuanceID(), issuer))); + // Issuer "borrowed" 200, OutstandingAmount decreased by 200 + BEAST_EXPECT(env.balance(issuer, asset) == asset(-issuerBalance + 200)); + // Pay Loan + auto const loanKeylet = keylet::loan(broker.brokerID, 1); + env(pay(borrower, loanKeylet.key, asset(200))); + env.close(); + // Issuer "re-payed" 200, OutstandingAmount increased by 200 + BEAST_EXPECT(env.balance(issuer, asset) == asset(-issuerBalance)); + } + void testInvalidLoanDelete() { @@ -3049,6 +3089,7 @@ public: void run() override { + testIssuerLoan(); testDisabled(); testSelfLoan(); testLifecycle(); diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 17a1fc8013..2a44eac007 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -1479,7 +1479,12 @@ ValidMPTIssuance::visitEntry( if (isDelete) mptokensDeleted_++; else if (!before) + { mptokensCreated_++; + MPTIssue const mptIssue{after->at(sfMPTokenIssuanceID)}; + if (mptIssue.getIssuer() == after->at(sfAccount)) + mptCreatedByIssuer_ = true; + } } } @@ -1493,6 +1498,25 @@ ValidMPTIssuance::finalize( { if (result == tesSUCCESS) { + auto const& rules = view.rules(); + [[maybe_unused]] + bool enforceCreatedByIssuer = rules.enabled(featureSingleAssetVault) || + rules.enabled(featureLendingProtocol); + if (mptCreatedByIssuer_) + { + JLOG(j.fatal()) + << "Invariant failed: MPToken created for the MPT issuer"; + // The comment above starting with "assert(enforce)" explains this + // assert. + XRPL_ASSERT_PARTS( + enforceCreatedByIssuer, + "ripple::ValidMPTIssuance::finalize", + "no issuer MPToken"); + if (enforceCreatedByIssuer) + return false; + } + + auto const txnType = tx.getTxnType(); if (hasPrivilege(tx, createMPTIssuance)) { if (mptIssuancesCreated_ == 0) @@ -1540,7 +1564,7 @@ ValidMPTIssuance::finalize( // ttESCROW_FINISH may authorize an MPT, but it can't have the // mayAuthorizeMPT privilege, because that may cause // non-amendment-gated side effects. - bool const enforceEscrowFinish = (tx.getTxnType() == ttESCROW_FINISH) && + bool const enforceEscrowFinish = (txnType == ttESCROW_FINISH) && (view.rules().enabled(featureSingleAssetVault) || lendingProtocolEnabled); if (hasPrivilege(tx, mustAuthorizeMPT | mayAuthorizeMPT) || @@ -1591,7 +1615,7 @@ ValidMPTIssuance::finalize( return true; } - if (tx.getTxnType() == ttESCROW_FINISH) + if (txnType == ttESCROW_FINISH) { // ttESCROW_FINISH may authorize an MPT, but it can't have the // mayAuthorizeMPT privilege, because that may cause diff --git a/src/xrpld/app/tx/detail/InvariantCheck.h b/src/xrpld/app/tx/detail/InvariantCheck.h index 420bb646dd..98df0b3c53 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.h +++ b/src/xrpld/app/tx/detail/InvariantCheck.h @@ -576,6 +576,9 @@ class ValidMPTIssuance std::uint32_t mptokensCreated_ = 0; std::uint32_t mptokensDeleted_ = 0; + // non-MPT transactions may attempt to create + // MPToken by an issuer + bool mptCreatedByIssuer_ = false; public: void From 07fcbd5498766a9bcf362fa8d6f1875f60ff3946 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Fri, 24 Oct 2025 18:27:27 +0100 Subject: [PATCH 158/291] Add authorization check to `LoanSet` (#5875) - Add tests for unauthorized borrower and lender --- src/libxrpl/ledger/View.cpp | 20 ++ src/test/app/Loan_test.cpp | 463 ++++++++++++++++++++++++++++ src/xrpld/app/tx/detail/LoanSet.cpp | 1 + 3 files changed, 484 insertions(+) diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index e4b295c415..3d8187a660 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -1533,6 +1533,18 @@ authorizeMPToken( if (priorBalance < reserveCreate) return tecINSUFFICIENT_RESERVE; + // Defensive check before we attempt to create MPToken for the issuer + auto const mpt = view.read(keylet::mptIssuance(mptIssuanceID)); + if (!mpt || mpt->getAccountID(sfIssuer) == account) + { + // LCOV_EXCL_START + UNREACHABLE( + "ripple::authorizeMPToken : invalid issuance or issuers token"); + if (view.rules().enabled(featureLendingProtocol)) + return tecINTERNAL; + // LCOV_EXCL_STOP + } + auto const mptokenKey = keylet::mptoken(mptIssuanceID, account); auto mptoken = std::make_shared(mptokenKey); if (auto ter = dirLink(view, account, mptoken)) @@ -1608,6 +1620,14 @@ trustCreate( auto const& uLowAccountID = !bSrcHigh ? uSrcAccountID : uDstAccountID; auto const& uHighAccountID = bSrcHigh ? uSrcAccountID : uDstAccountID; + if (uLowAccountID == uHighAccountID) + { + // LCOV_EXCL_START + UNREACHABLE("ripple::trustCreate : trust line to self"); + if (view.rules().enabled(featureLendingProtocol)) + return tecINTERNAL; + // LCOV_EXCL_STOP + } auto const sleRippleState = std::make_shared(ltRIPPLE_STATE, uIndex); view.insert(sleRippleState); diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 7ce7dd7a4d..aa68398853 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -18,12 +18,14 @@ //============================================================================== #include +#include #include #include #include #include +#include namespace ripple { namespace test { @@ -1957,6 +1959,466 @@ class Loan_test : public beast::unit_test::suite #endif } + void + testLoanSet() + { + using namespace jtx; + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + struct CaseArgs + { + bool requireAuth = false; + bool authorizeBorrower = false; + int initialXRP = 1'000'000; + }; + + auto const testCase = + [&, this]( + std::function + mptTest, + std::function iouTest, + CaseArgs args = {}) { + Env env(*this, all); + env.fund(XRP(args.initialXRP), issuer, lender, borrower); + env.close(); + if (args.requireAuth) + { + env(fset(issuer, asfRequireAuth)); + env.close(); + } + + // We need two different asset types, MPT and IOU. Prepare MPT + // first + MPTTester mptt{env, issuer, mptInitNoFund}; + + auto const none = LedgerSpecificFlags(0); + mptt.create( + {.flags = tfMPTCanTransfer | tfMPTCanLock | + (args.requireAuth ? tfMPTRequireAuth : none)}); + env.close(); + PrettyAsset mptAsset = mptt.issuanceID(); + mptt.authorize({.account = lender}); + mptt.authorize({.account = borrower}); + env.close(); + if (args.requireAuth) + { + mptt.authorize({.account = issuer, .holder = lender}); + if (args.authorizeBorrower) + mptt.authorize({.account = issuer, .holder = borrower}); + env.close(); + } + + env(pay(issuer, lender, mptAsset(10'000'000))); + env.close(); + + // Prepare IOU + PrettyAsset const iouAsset = issuer[iouCurrency]; + env(trust(lender, iouAsset(10'000'000))); + env(trust(borrower, iouAsset(10'000'000))); + env.close(); + if (args.requireAuth) + { + env(trust(issuer, iouAsset(0), lender, tfSetfAuth)); + env(pay(issuer, lender, iouAsset(10'000'000))); + if (args.authorizeBorrower) + { + env(trust(issuer, iouAsset(0), borrower, tfSetfAuth)); + env(pay(issuer, borrower, iouAsset(10'000))); + } + } + else + { + env(pay(issuer, lender, iouAsset(10'000'000))); + env(pay(issuer, borrower, iouAsset(10'000))); + } + env.close(); + + // Create vaults and loan brokers + std::array const assets{mptAsset, iouAsset}; + std::vector brokers; + for (auto const& asset : assets) + { + brokers.emplace_back( + createVaultAndBroker(env, asset, lender)); + } + + if (mptTest) + (mptTest)(env, brokers[0], mptt); + if (iouTest) + (iouTest)(env, brokers[1]); + }; + + testCase( + [&, this](Env& env, BrokerInfo const& broker, auto&) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase("MPT issuer is borrower, issuer submits"); + env(set(issuer, broker.brokerID, principalRequest), + counterparty(lender), + sig(sfCounterpartySignature, lender), + fee(env.current()->fees().base * 5)); + + testcase("MPT issuer is borrower, lender submits"); + env(set(lender, broker.brokerID, principalRequest), + counterparty(issuer), + sig(sfCounterpartySignature, issuer), + fee(env.current()->fees().base * 5)); + }, + [&, this](Env& env, BrokerInfo const& broker) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase("IOU issuer is borrower, issuer submits"); + env(set(issuer, broker.brokerID, principalRequest), + counterparty(lender), + sig(sfCounterpartySignature, lender), + fee(env.current()->fees().base * 5)); + + testcase("IOU issuer is borrower, lender submits"); + env(set(lender, broker.brokerID, principalRequest), + counterparty(issuer), + sig(sfCounterpartySignature, issuer), + fee(env.current()->fees().base * 5)); + }, + CaseArgs{.requireAuth = true}); + + testCase( + [&, this](Env& env, BrokerInfo const& broker, auto&) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase("MPT unauthorized borrower, borrower submits"); + env(set(borrower, broker.brokerID, principalRequest), + counterparty(lender), + sig(sfCounterpartySignature, lender), + fee(env.current()->fees().base * 5), + ter{tecNO_AUTH}); + + testcase("MPT unauthorized borrower, lender submits"); + env(set(lender, broker.brokerID, principalRequest), + counterparty(borrower), + sig(sfCounterpartySignature, borrower), + fee(env.current()->fees().base * 5), + ter{tecNO_AUTH}); + }, + [&, this](Env& env, BrokerInfo const& broker) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase("IOU unauthorized borrower, borrower submits"); + env(set(borrower, broker.brokerID, principalRequest), + counterparty(lender), + sig(sfCounterpartySignature, lender), + fee(env.current()->fees().base * 5), + ter{tecNO_AUTH}); + + testcase("IOU unauthorized borrower, lender submits"); + env(set(lender, broker.brokerID, principalRequest), + counterparty(borrower), + sig(sfCounterpartySignature, borrower), + fee(env.current()->fees().base * 5), + ter{tecNO_AUTH}); + }, + CaseArgs{.requireAuth = true}); + + auto const [acctReserve, incReserve] = [this]() -> std::pair { + Env env{*this, testable_amendments()}; + return { + env.current()->fees().accountReserve(0).drops() / + DROPS_PER_XRP.drops(), + env.current()->fees().increment.drops() / + DROPS_PER_XRP.drops()}; + }(); + + testCase( + [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase( + "MPT authorized borrower, borrower submits, borrower has " + "no reserve"); + mptt.authorize( + {.account = borrower, .flags = tfMPTUnauthorize}); + env.close(); + + auto const mptoken = + keylet::mptoken(mptt.issuanceID(), borrower); + auto const sleMPT1 = env.le(mptoken); + BEAST_EXPECT(sleMPT1 == nullptr); + + // Burn some XRP + env(noop(borrower), fee(XRP(acctReserve * 2 + incReserve * 2))); + env.close(); + + // Cannot create loan, not enough reserve to create MPToken + env(set(borrower, broker.brokerID, principalRequest), + counterparty(lender), + sig(sfCounterpartySignature, lender), + fee(env.current()->fees().base * 5), + ter{tecINSUFFICIENT_RESERVE}); + env.close(); + + // Can create loan now, will implicitly create MPToken + env(pay(issuer, borrower, XRP(incReserve))); + env.close(); + env(set(borrower, broker.brokerID, principalRequest), + counterparty(lender), + sig(sfCounterpartySignature, lender), + fee(env.current()->fees().base * 5)); + env.close(); + + auto const sleMPT2 = env.le(mptoken); + BEAST_EXPECT(sleMPT2 != nullptr); + }, + {}, + CaseArgs{.initialXRP = acctReserve * 2 + incReserve * 8 + 1}); + + testCase( + {}, + [&, this](Env& env, BrokerInfo const& broker) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase( + "IOU authorized borrower, borrower submits, borrower has " + "no reserve"); + // Remove trust line from borrower to issuer + env.trust(broker.asset(0), borrower); + env.close(); + + env(pay(borrower, issuer, broker.asset(10'000))); + env.close(); + auto const trustline = + keylet::line(borrower, broker.asset.raw().get()); + auto const sleLine1 = env.le(trustline); + BEAST_EXPECT(sleLine1 == nullptr); + + // Burn some XRP + env(noop(borrower), fee(XRP(acctReserve * 2 + incReserve * 2))); + env.close(); + + // Cannot create loan, not enough reserve to create trust line + env(set(borrower, broker.brokerID, principalRequest), + counterparty(lender), + sig(sfCounterpartySignature, lender), + fee(env.current()->fees().base * 5), + ter{tecNO_LINE_INSUF_RESERVE}); + env.close(); + + // Can create loan now, will implicitly create trust line + env(pay(issuer, borrower, XRP(incReserve))); + env.close(); + env(set(borrower, broker.brokerID, principalRequest), + counterparty(lender), + sig(sfCounterpartySignature, lender), + fee(env.current()->fees().base * 5)); + env.close(); + + auto const sleLine2 = env.le(trustline); + BEAST_EXPECT(sleLine2 != nullptr); + }, + CaseArgs{.initialXRP = acctReserve * 2 + incReserve * 8 + 1}); + + testCase( + [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase( + "MPT authorized borrower, borrower submits, lender has " + "no reserve"); + auto const mptoken = keylet::mptoken(mptt.issuanceID(), lender); + auto const sleMPT1 = env.le(mptoken); + BEAST_EXPECT(sleMPT1 != nullptr); + + env(pay( + lender, issuer, broker.asset(sleMPT1->at(sfMPTAmount)))); + env.close(); + + mptt.authorize({.account = lender, .flags = tfMPTUnauthorize}); + env.close(); + + auto const sleMPT2 = env.le(mptoken); + BEAST_EXPECT(sleMPT2 == nullptr); + + // Burn some XRP + env(noop(lender), fee(XRP(incReserve))); + env.close(); + + // Cannot create loan, not enough reserve to create MPToken + env(set(borrower, broker.brokerID, principalRequest), + loanOriginationFee(broker.asset(1).value()), + counterparty(lender), + sig(sfCounterpartySignature, lender), + fee(env.current()->fees().base * 5), + ter{tecINSUFFICIENT_RESERVE}); + env.close(); + + // Can create loan now, will implicitly create MPToken + env(pay(issuer, lender, XRP(incReserve))); + env.close(); + env(set(borrower, broker.brokerID, principalRequest), + loanOriginationFee(broker.asset(1).value()), + counterparty(lender), + sig(sfCounterpartySignature, lender), + fee(env.current()->fees().base * 5)); + env.close(); + + auto const sleMPT3 = env.le(mptoken); + BEAST_EXPECT(sleMPT3 != nullptr); + }, + {}, + CaseArgs{.initialXRP = acctReserve * 2 + incReserve * 8 + 1}); + + testCase( + {}, + [&, this](Env& env, BrokerInfo const& broker) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase( + "IOU authorized borrower, borrower submits, lender has no " + "reserve"); + // Remove trust line from lender to issuer + env.trust(broker.asset(0), lender); + env.close(); + + auto const trustline = + keylet::line(lender, broker.asset.raw().get()); + auto const sleLine1 = env.le(trustline); + BEAST_EXPECT(sleLine1 != nullptr); + + env( + pay(lender, + issuer, + broker.asset(abs(sleLine1->at(sfBalance).value())))); + env.close(); + auto const sleLine2 = env.le(trustline); + BEAST_EXPECT(sleLine2 == nullptr); + + // Burn some XRP + env(noop(lender), fee(XRP(incReserve))); + env.close(); + + // Cannot create loan, not enough reserve to create trust line + env(set(borrower, broker.brokerID, principalRequest), + loanOriginationFee(broker.asset(1).value()), + counterparty(lender), + sig(sfCounterpartySignature, lender), + fee(env.current()->fees().base * 5), + ter{tecNO_LINE_INSUF_RESERVE}); + env.close(); + + // Can create loan now, will implicitly create trust line + env(pay(issuer, lender, XRP(incReserve))); + env.close(); + env(set(borrower, broker.brokerID, principalRequest), + loanOriginationFee(broker.asset(1).value()), + counterparty(lender), + sig(sfCounterpartySignature, lender), + fee(env.current()->fees().base * 5)); + env.close(); + + auto const sleLine3 = env.le(trustline); + BEAST_EXPECT(sleLine3 != nullptr); + }, + CaseArgs{.initialXRP = acctReserve * 2 + incReserve * 8 + 1}); + + testCase( + [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase("MPT authorized borrower, unauthorized lender"); + auto const mptoken = keylet::mptoken(mptt.issuanceID(), lender); + auto const sleMPT1 = env.le(mptoken); + BEAST_EXPECT(sleMPT1 != nullptr); + + env(pay( + lender, issuer, broker.asset(sleMPT1->at(sfMPTAmount)))); + env.close(); + + mptt.authorize({.account = lender, .flags = tfMPTUnauthorize}); + env.close(); + + auto const sleMPT2 = env.le(mptoken); + BEAST_EXPECT(sleMPT2 == nullptr); + + // Cannot create loan, lender not authorized to receive fee + env(set(borrower, broker.brokerID, principalRequest), + loanOriginationFee(broker.asset(1).value()), + counterparty(lender), + sig(sfCounterpartySignature, lender), + fee(env.current()->fees().base * 5), + ter{tecNO_AUTH}); + env.close(); + + // Can create loan without origination fee + env(set(borrower, broker.brokerID, principalRequest), + counterparty(lender), + sig(sfCounterpartySignature, lender), + fee(env.current()->fees().base * 5)); + env.close(); + + // No MPToken for lender - no authorization and no payment + auto const sleMPT3 = env.le(mptoken); + BEAST_EXPECT(sleMPT3 == nullptr); + }, + {}, + CaseArgs{.requireAuth = true, .authorizeBorrower = true}); + + testCase( + [&, this](Env& env, BrokerInfo const& broker, auto&) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase("MPT authorized borrower, borrower submits"); + env(set(borrower, broker.brokerID, principalRequest), + counterparty(lender), + sig(sfCounterpartySignature, lender), + fee(env.current()->fees().base * 5)); + }, + [&, this](Env& env, BrokerInfo const& broker) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase("IOU authorized borrower, borrower submits"); + env(set(borrower, broker.brokerID, principalRequest), + counterparty(lender), + sig(sfCounterpartySignature, lender), + fee(env.current()->fees().base * 5)); + }, + CaseArgs{.requireAuth = true, .authorizeBorrower = true}); + + testCase( + [&, this](Env& env, BrokerInfo const& broker, auto&) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase("MPT authorized borrower, lender submits"); + env(set(lender, broker.brokerID, principalRequest), + counterparty(borrower), + sig(sfCounterpartySignature, borrower), + fee(env.current()->fees().base * 5)); + }, + [&, this](Env& env, BrokerInfo const& broker) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase("IOU authorized borrower, lender submits"); + env(set(lender, broker.brokerID, principalRequest), + counterparty(borrower), + sig(sfCounterpartySignature, borrower), + fee(env.current()->fees().base * 5)); + }, + CaseArgs{.requireAuth = true, .authorizeBorrower = true}); + } + void testLifecycle() { @@ -3092,6 +3554,7 @@ public: testIssuerLoan(); testDisabled(); testSelfLoan(); + testLoanSet(); testLifecycle(); testBatchBypassCounterparty(); testWrongMaxDebtBehavior(); diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 61eff570af..4f69e3c5a8 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -234,6 +234,7 @@ LoanSet::preclaim(PreclaimContext const& ctx) // Should be impossible return tefBAD_LEDGER; // LCOV_EXCL_LINE Asset const asset = vault->at(sfAsset); + auto const vaultPseudo = vault->at(sfAccount); // Check that relevant values can be represented as the vault asset type. From 83e249eb8ac8040f76ce27c1b32d4a07c2b69bfc Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 24 Oct 2025 14:23:18 -0400 Subject: [PATCH 159/291] refactor: change what values are tracked as Payment Components - Part 1 - This is an intermediate commit. It leaves the old variables in place, so I can do verifications that the new computations are correct. They will be removed in the next commit. - PaymentComponents is an class used internally to break a payment value into principal, interest, and fees. --- src/test/app/Loan_test.cpp | 68 ++++--- src/xrpld/app/misc/LendingHelpers.h | 270 ++++++++++++++++++---------- src/xrpld/app/tx/detail/LoanPay.cpp | 4 + 3 files changed, 225 insertions(+), 117 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index aa68398853..4ec8798bc8 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -1349,13 +1349,22 @@ class Loan_test : public beast::unit_test::suite if (!(state.flags & lsfLoanOverpayment)) { // If the loan does not allow overpayments, send a payment that - // tries to make an overpayment + // tries to make an overpayment. Do not include `txFlags`, so we + // don't end up duplicating the next test transaction. env(pay(borrower, loanKeylet.key, broker.asset(state.periodicPayment * 2), - tfLoanOverpayment | txFlags), + tfLoanOverpayment), ter(temINVALID_FLAG)); } + // Try to send a payment marked as both full payment and + // overpayment. Do not include `txFlags`, so we don't duplicate the + // prior test transaction. + env(pay(borrower, + loanKeylet.key, + broker.asset(state.periodicPayment * 2), + tfLoanOverpayment | tfLoanFullPayment), + ter(temINVALID_FLAG)); { auto const otherAsset = broker.asset.raw() == assets[0].raw() @@ -1685,10 +1694,11 @@ class Loan_test : public beast::unit_test::suite roundPeriodicPayment( broker.asset, state.periodicPayment, state.loanScale)}; - testcase << "\tPayment components: " - << "Payments remaining, rawInterest, rawPrincipal, " - "rawMFee, roundedInterest, roundedPrincipal, " - "roundedMFee, special"; + testcase + << "\tPayment components: " + << "Payments remaining, rawInterest, rawPrincipal, " + "rawMFee, trackedValueDelta, trackedPrincipalDelta, " + "trackedMgmtFeeDelta, special"; auto const serviceFee = broker.asset(2); @@ -1767,21 +1777,20 @@ class Loan_test : public beast::unit_test::suite << ", " << paymentComponents.rawInterest << ", " << paymentComponents.rawPrincipal << ", " << paymentComponents.rawManagementFee << ", " - << paymentComponents.roundedInterest << ", " - << paymentComponents.roundedPrincipal << ", " - << paymentComponents.roundedManagementFee << ", " - << (paymentComponents.specialCase == SpecialCase::final + << paymentComponents.trackedValueDelta << ", " + << paymentComponents.trackedPrincipalDelta << ", " + << paymentComponents.trackedManagementFeeDelta << ", " + << (paymentComponents.specialCase == + PaymentSpecialCase::final ? "final" : paymentComponents.specialCase == - SpecialCase::final + PaymentSpecialCase::extra ? "extra" : "none"); auto const totalDueAmount = STAmount{ broker.asset, - paymentComponents.roundedPrincipal + - paymentComponents.roundedInterest + - paymentComponents.roundedManagementFee + + paymentComponents.trackedValueDelta + serviceFee.number()}; // Due to the rounding algorithms to keep the interest and @@ -1792,7 +1801,8 @@ class Loan_test : public beast::unit_test::suite // IOUs, the difference should be after the 8th digit. Number const diff = totalDue - totalDueAmount; BEAST_EXPECT( - paymentComponents.specialCase == SpecialCase::final || + paymentComponents.specialCase == + PaymentSpecialCase::final || diff == beast::zero || (diff > beast::zero && ((broker.asset.integral() && @@ -1800,7 +1810,9 @@ class Loan_test : public beast::unit_test::suite (totalDue.exponent() - diff.exponent() > 8)))); BEAST_EXPECT( - paymentComponents.roundedInterest >= Number(0)); + paymentComponents.trackedValueDelta >= + paymentComponents.trackedPrincipalDelta + + paymentComponents.trackedManagementFeeDelta); BEAST_EXPECT( state.paymentRemaining < 12 || @@ -1816,15 +1828,18 @@ class Loan_test : public beast::unit_test::suite state.loanScale, Number::upward)); BEAST_EXPECT( - paymentComponents.roundedPrincipal >= Number(0) && - paymentComponents.roundedPrincipal <= + paymentComponents.trackedPrincipalDelta >= + beast::zero && + paymentComponents.trackedPrincipalDelta <= state.principalOutstanding); BEAST_EXPECT( - paymentComponents.specialCase != SpecialCase::final || - paymentComponents.roundedPrincipal == + paymentComponents.specialCase != + PaymentSpecialCase::final || + paymentComponents.trackedPrincipalDelta == state.principalOutstanding); BEAST_EXPECT( - paymentComponents.specialCase == SpecialCase::final || + paymentComponents.specialCase == + PaymentSpecialCase::final || (state.periodicPayment.exponent() - (paymentComponents.rawPrincipal + paymentComponents.rawInterest + @@ -1863,7 +1878,8 @@ class Loan_test : public beast::unit_test::suite --state.paymentRemaining; state.previousPaymentDate = state.nextPaymentDate; - if (paymentComponents.specialCase == SpecialCase::final) + if (paymentComponents.specialCase == + PaymentSpecialCase::final) { state.paymentRemaining = 0; } @@ -1872,12 +1888,10 @@ class Loan_test : public beast::unit_test::suite state.nextPaymentDate += state.paymentInterval; } state.principalOutstanding -= - paymentComponents.roundedPrincipal; + paymentComponents.trackedPrincipalDelta; state.managementFeeOutstanding -= - paymentComponents.roundedManagementFee; - state.totalValue -= paymentComponents.roundedPrincipal + - paymentComponents.roundedInterest + - paymentComponents.roundedManagementFee; + paymentComponents.trackedManagementFeeDelta; + state.totalValue -= paymentComponents.trackedValueDelta; verifyLoanStatus(state); } diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index f96e437647..f0a41c36d5 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -45,22 +45,42 @@ roundPeriodicPayment( return roundToAsset(asset, periodicPayment, scale, Number::upward); } -enum class SpecialCase { none, final, extra }; +enum class PaymentSpecialCase { none, final, extra }; /// This structure is used internally to compute the breakdown of a /// single loan payment struct PaymentComponents { + // raw values are unrounded, and are based on pure math Number rawInterest; Number rawPrincipal; Number rawManagementFee; - Number roundedInterest; - Number roundedPrincipal; + // tracked values are rounded to the asset and loan scale, and correspond to + // fields in the Loan ledger object. + // trackedValueDelta modifies sfTotalValueOutstanding. + Number trackedValueDelta; + // trackedPrincipalDelta modifies sfPrincipalOutstanding. + Number trackedPrincipalDelta; + // trackedManagementFeeDelta modifies sfManagementFeeOutstanding. It will + // not include any "extra" fees that go directly to the broker, such as late + // fees. + Number trackedManagementFeeDelta; + + Number oldInterest; + Number oldPrincipal; // roundedManagementFee is explicitly on for the portion of the pre-computed // periodic payment that goes toward the Broker's management fee, which is // tracked by sfManagementFeeOutstanding - Number roundedManagementFee; - SpecialCase specialCase = SpecialCase::none; + Number oldManagementFee; + + PaymentSpecialCase specialCase = PaymentSpecialCase::none; + + Number + trackedInterestPart() const + { + return trackedValueDelta - + (trackedPrincipalDelta + trackedManagementFeeDelta); + } }; /// This structure is explained in the XLS-66 spec, section 3.2.4.4 (Failure @@ -84,6 +104,20 @@ struct LoanPaymentParts LoanPaymentParts& operator+=(LoanPaymentParts const& other) { + XRPL_ASSERT( + + other.principalPaid >= beast::zero, + "ripple::LoanPaymentParts::operator+= : other principal " + "non-negative"); + XRPL_ASSERT( + other.interestPaid >= beast::zero, + "ripple::LoanPaymentParts::operator+= : other interest paid " + "non-negative"); + XRPL_ASSERT( + other.feePaid >= beast::zero, + "ripple::LoanPaymentParts::operator+= : other fee paid " + "non-negative"); + principalPaid += other.principalPaid; interestPaid += other.interestPaid; valueChange += other.valueChange; @@ -468,8 +502,16 @@ computeRoundedInterestAndFeeComponents( struct PaymentComponentsPlus : public PaymentComponents { - Number extraFee{0}; - Number valueChange{0}; + // untrackedManagementFeeDelta includes any fees that go directly to the + // Broker, such as late fees. This value may be negative, though the final + // value returned in LoanPaymentParts.feePaid will never be negative. + Number untrackedManagementFee; + // untrackedInterest includes any fees that go directly to the Vault, such + // as late payment penalty interest. This value may be negative, though the + // final value returned in LoanPaymentParts.interestPaid will never be + // negative. + Number untrackedInterest; + Number oldValueChange; Number totalDue; PaymentComponentsPlus( @@ -477,12 +519,29 @@ struct PaymentComponentsPlus : public PaymentComponents Number f, Number v = Number{}) : PaymentComponents(p) - , extraFee(f) - , valueChange(v) + , untrackedManagementFee(f) + , untrackedInterest(v) + , oldValueChange(v) , totalDue( - roundedPrincipal + roundedInterest + roundedManagementFee + - extraFee) + trackedValueDelta + untrackedInterest + untrackedManagementFee) { + verify(); + } + + void + verify() const + { + assert( + totalDue == + oldPrincipal + oldInterest + oldManagementFee + + untrackedManagementFee); + assert(trackedInterestPart() == oldInterest - oldValueChange); + assert(trackedPrincipalDelta == oldPrincipal); + assert(trackedManagementFeeDelta == oldManagementFee); + assert( + trackedValueDelta == + oldPrincipal + oldInterest + oldManagementFee - oldValueChange); + assert(untrackedInterest == oldValueChange); } }; @@ -503,21 +562,20 @@ doPayment( "ripple::detail::doPayment", "Next due date proxy set"); - auto const totalValueDelta = payment.roundedPrincipal + - payment.roundedInterest + payment.roundedManagementFee - - payment.valueChange; - if (payment.specialCase == SpecialCase::final) + payment.verify(); + + if (payment.specialCase == PaymentSpecialCase::final) { XRPL_ASSERT_PARTS( - principalOutstandingProxy == payment.roundedPrincipal, + principalOutstandingProxy == payment.trackedPrincipalDelta, "ripple::detail::doPayment", "Full principal payment"); XRPL_ASSERT_PARTS( - totalValueOutstandingProxy == totalValueDelta, + totalValueOutstandingProxy == payment.trackedValueDelta, "ripple::detail::doPayment", "Full value payment"); XRPL_ASSERT_PARTS( - managementFeeOutstandingProxy == payment.roundedManagementFee, + managementFeeOutstandingProxy == payment.trackedManagementFeeDelta, "ripple::detail::doPayment", "Full management fee payment"); @@ -535,7 +593,7 @@ doPayment( } else { - if (payment.specialCase != SpecialCase::extra) + if (payment.specialCase != PaymentSpecialCase::extra) { paymentRemainingProxy -= 1; @@ -545,23 +603,23 @@ doPayment( nextDueDateProxy = *nextDueDateProxy + paymentInterval; } XRPL_ASSERT_PARTS( - principalOutstandingProxy > payment.roundedPrincipal, + principalOutstandingProxy > payment.trackedPrincipalDelta, "ripple::detail::doPayment", "Partial principal payment"); XRPL_ASSERT_PARTS( - totalValueOutstandingProxy > totalValueDelta, + totalValueOutstandingProxy > payment.trackedValueDelta, "ripple::detail::doPayment", "Partial value payment"); // Management fees are expected to be relatively small, and could get to // zero before the loan is paid off XRPL_ASSERT_PARTS( - managementFeeOutstandingProxy >= payment.roundedManagementFee, + managementFeeOutstandingProxy >= payment.trackedManagementFeeDelta, "ripple::detail::doPayment", "Valid management fee"); - principalOutstandingProxy -= payment.roundedPrincipal; - totalValueOutstandingProxy -= totalValueDelta; - managementFeeOutstandingProxy -= payment.roundedManagementFee; + principalOutstandingProxy -= payment.trackedPrincipalDelta; + totalValueOutstandingProxy -= payment.trackedValueDelta; + managementFeeOutstandingProxy -= payment.trackedManagementFeeDelta; } XRPL_ASSERT_PARTS( @@ -579,12 +637,16 @@ doPayment( "fee outstanding stays valid"); return LoanPaymentParts{ - .principalPaid = payment.roundedPrincipal, - .interestPaid = payment.roundedInterest, - .valueChange = payment.valueChange, - // Now that the adjustments have been made, the fee parts can be + .principalPaid = payment.trackedPrincipalDelta, + // Now that the Loan object has been updated, the tracked interest + // (computed here) and untracked interest can be combined. + .interestPaid = + payment.trackedInterestPart() + payment.untrackedInterest, + .valueChange = payment.untrackedInterest, + // Now that the Loan object has been updated, the fee parts can be // combined - .feePaid = payment.roundedManagementFee + payment.extraFee}; + .feePaid = + payment.trackedManagementFeeDelta + payment.untrackedManagementFee}; } // This function mainly exists to guarantee isolation of the "sandbox" @@ -618,8 +680,9 @@ tryOverpayment( auto const principalError = principalOutstanding - raw.principalOutstanding; auto const feeError = managementFeeOutstanding - raw.managementFeeDue; + overpaymentComponents.verify(); auto const newRawPrincipal = - raw.principalOutstanding - overpaymentComponents.roundedPrincipal; + raw.principalOutstanding - overpaymentComponents.trackedPrincipalDelta; auto newLoanProperties = computeLoanProperties( asset, @@ -690,14 +753,14 @@ tryOverpayment( .principalPaid = rounded.principalOutstanding - newRounded.principalOutstanding, .interestPaid = rounded.interestDue - newRounded.interestDue, - .valueChange = valueChange + overpaymentComponents.valueChange, + .valueChange = valueChange + overpaymentComponents.untrackedInterest, .feePaid = rounded.managementFeeDue - newRounded.managementFeeDue + - overpaymentComponents.extraFee}; + overpaymentComponents.untrackedManagementFee}; } template Expected -computeOverpayment( +doOverpayment( A const& asset, std::int32_t loanScale, PaymentComponentsPlus const& overpaymentComponents, @@ -753,36 +816,40 @@ computeOverpayment( // LCOV_EXCL_STOP } + overpaymentComponents.verify(); + + // We haven't updated the proxies yet, so they still have the original + // values. Use those to do some checks. XRPL_ASSERT_PARTS( - principalOutstandingProxy - principalOutstanding == - overpaymentComponents.roundedPrincipal, - "ripple::detail::computeOverpayment", + overpaymentComponents.trackedPrincipalDelta == + principalOutstandingProxy - principalOutstanding, + "ripple::detail::doOverpayment", "principal change agrees"); XRPL_ASSERT_PARTS( - managementFeeOutstandingProxy - managementFeeOutstanding == - overpaymentComponents.roundedManagementFee, - "ripple::detail::computeOverpayment", + overpaymentComponents.trackedManagementFeeDelta == + managementFeeOutstandingProxy - managementFeeOutstanding, + "ripple::detail::doOverpayment", "no fee change"); XRPL_ASSERT_PARTS( - totalValueOutstandingProxy - totalValueOutstanding - - overpaymentComponents.roundedPrincipal == - overpaymentComponents.valueChange, - "ripple::detail::computeOverpayment", + overpaymentComponents.untrackedInterest == + totalValueOutstandingProxy - totalValueOutstanding - + overpaymentComponents.trackedPrincipalDelta, + "ripple::detail::doOverpayment", "value change agrees"); XRPL_ASSERT_PARTS( - loanPaymentParts.principalPaid == - overpaymentComponents.roundedPrincipal, - "ripple::detail::computeOverpayment", + overpaymentComponents.trackedPrincipalDelta == + loanPaymentParts.principalPaid, + "ripple::detail::doOverpayment", "principal payment matches"); XRPL_ASSERT_PARTS( loanPaymentParts.feePaid == - overpaymentComponents.extraFee + - overpaymentComponents.roundedManagementFee, - "ripple::detail::computeOverpayment", + overpaymentComponents.untrackedManagementFee + + overpaymentComponents.trackedManagementFeeDelta, + "ripple::detail::doOverpayment", "fee payment matches"); // Update the loan object (via proxies) @@ -865,22 +932,27 @@ computeLatePayment( roundedLateInterest >= 0, "ripple::detail::computeLatePayment : valid late interest"); XRPL_ASSERT_PARTS( - periodic.specialCase != SpecialCase::extra, + periodic.specialCase != PaymentSpecialCase::extra, "ripple::detail::computeLatePayment", "no extra parts to this payment"); // Copy the periodic payment values, and add on the late interest. // This preserves all the other fields without having to enumerate them. - PaymentComponents inner = periodic; - inner.rawInterest += rawLateInterest; - inner.roundedInterest += roundedLateInterest; + periodic.verify(); + PaymentComponentsPlus const late = [&]() { + auto inner = periodic; + inner.rawInterest += rawLateInterest; + inner.oldInterest += roundedLateInterest; - PaymentComponentsPlus const late{ - inner, - // A late payment pays both the normal fee, and the extra fees - periodic.extraFee + latePaymentFee + roundedLateManagementFee, - // A late payment increases the value of the loan by the difference - // between periodic and late payment interest - roundedLateInterest}; + return PaymentComponentsPlus{ + inner, + // A late payment pays both the normal fee, and the extra fees + periodic.untrackedManagementFee + latePaymentFee + + roundedLateManagementFee, + // A late payment increases the value of the loan by the difference + // between periodic and late payment interest + periodic.untrackedInterest + roundedLateInterest}; + }(); + late.verify(); XRPL_ASSERT_PARTS( isRounded(asset, late.totalDue, loanScale), @@ -961,12 +1033,16 @@ computeFullPayment( .rawInterest = rawFullInterest, .rawPrincipal = rawPrincipalOutstanding, .rawManagementFee = rawFullManagementFee, - .roundedInterest = roundedFullInterest, - .roundedPrincipal = principalOutstanding, + .trackedValueDelta = principalOutstanding + + totalInterestOutstanding + managementFeeOutstanding, + .trackedPrincipalDelta = principalOutstanding, + .trackedManagementFeeDelta = managementFeeOutstanding, + .oldInterest = roundedFullInterest, + .oldPrincipal = principalOutstanding, // to make the accounting work later, the tracked part of the fee // must be paid in full - .roundedManagementFee = managementFeeOutstanding, - .specialCase = SpecialCase::final}, + .oldManagementFee = managementFeeOutstanding, + .specialCase = PaymentSpecialCase::final}, // A full payment pays the single close payment fee, plus the computed // management fee part of the interest portion, but for tracking, the // outstanding part is removed. That could make this value negative, but @@ -1050,10 +1126,14 @@ computePaymentComponents( .rawInterest = raw.interestOutstanding, .rawPrincipal = raw.principalOutstanding, .rawManagementFee = raw.managementFeeDue, - .roundedInterest = interest, - .roundedPrincipal = principalOutstanding, - .roundedManagementFee = managementFeeOutstanding, - .specialCase = SpecialCase::final}; + .trackedValueDelta = + interest + principalOutstanding + managementFeeOutstanding, + .trackedPrincipalDelta = principalOutstanding, + .trackedManagementFeeDelta = managementFeeOutstanding, + .oldInterest = interest, + .oldPrincipal = principalOutstanding, + .oldManagementFee = managementFeeOutstanding, + .specialCase = PaymentSpecialCase::final}; } /* @@ -1148,9 +1228,12 @@ computePaymentComponents( .rawInterest = rawInterest - rawFee, .rawPrincipal = rawPrincipal, .rawManagementFee = rawFee, - .roundedInterest = roundedInterest, - .roundedPrincipal = roundedPrincipal, - .roundedManagementFee = roundedFee, + .trackedValueDelta = roundedInterest + roundedPrincipal + roundedFee, + .trackedPrincipalDelta = roundedPrincipal, + .trackedManagementFeeDelta = roundedFee, + .oldInterest = roundedInterest, + .oldPrincipal = roundedPrincipal, + .oldManagementFee = roundedFee, }; } @@ -1610,7 +1693,7 @@ loanMakePayment( managementFeeRate), serviceFee}; XRPL_ASSERT_PARTS( - periodic.roundedPrincipal >= 0, + periodic.trackedPrincipalDelta >= 0, "ripple::loanMakePayment", "regular payment valid principal"); @@ -1690,9 +1773,9 @@ loanMakePayment( managementFeeRate), serviceFee}; XRPL_ASSERT_PARTS( - nextPayment.roundedPrincipal > 0, + nextPayment.trackedPrincipalDelta >= 0, "ripple::loanMakePayment", - "additional payment pays principal"); + "additional payment pays non-negative principal"); XRPL_ASSERT( nextPayment.rawInterest <= periodic.rawInterest, "ripple::loanMakePayment : decreasing interest"); @@ -1717,7 +1800,7 @@ loanMakePayment( ++numPayments; XRPL_ASSERT_PARTS( - (nextPayment.specialCase == SpecialCase::final) == + (nextPayment.specialCase == PaymentSpecialCase::final) == (paymentRemainingProxy == 0), "ripple::loanMakePayment", "final payment is the final payment"); @@ -1776,25 +1859,29 @@ loanMakePayment( .rawInterest = rawOverpaymentInterest, .rawPrincipal = payment - rawOverpaymentInterest, .rawManagementFee = 0, - .roundedInterest = roundedOverpaymentInterest, - .roundedPrincipal = payment - roundedOverpaymentInterest, - .roundedManagementFee = 0, - .specialCase = SpecialCase::extra}, + .trackedValueDelta = payment, + .trackedPrincipalDelta = payment - roundedOverpaymentInterest - + roundedOverpaymentManagementFee, + .trackedManagementFeeDelta = roundedOverpaymentManagementFee, + .oldInterest = roundedOverpaymentInterest, + .oldPrincipal = payment - roundedOverpaymentInterest, + .oldManagementFee = roundedOverpaymentManagementFee, + .specialCase = PaymentSpecialCase::extra}, fee, roundedOverpaymentInterest}; // Don't process an overpayment if the whole amount (or more!) // gets eaten by fees and interest. if (overpaymentComponents.rawPrincipal > 0 && - overpaymentComponents.roundedPrincipal > 0) + overpaymentComponents.trackedPrincipalDelta > 0) { XRPL_ASSERT_PARTS( - overpaymentComponents.valueChange >= beast::zero, + overpaymentComponents.untrackedInterest >= beast::zero, "ripple::loanMakePayment", "overpayment penalty did not reduce value of loan"); // Can't just use `periodicPayment` here, because it might change auto periodicPaymentProxy = loan->at(sfPeriodicPayment); - if (auto const overResult = detail::computeOverpayment( + if (auto const overResult = detail::doOverpayment( asset, loanScale, overpaymentComponents, @@ -1822,17 +1909,20 @@ loanMakePayment( // Check the final results are rounded, to double-check that the // intermediate steps were rounded. XRPL_ASSERT( - isRounded(asset, totalParts.principalPaid, loanScale), - "ripple::loanMakePayment : total principal paid rounded"); + isRounded(asset, totalParts.principalPaid, loanScale) && + totalParts.principalPaid >= beast::zero, + "ripple::loanMakePayment : total principal paid is valid"); XRPL_ASSERT( - isRounded(asset, totalParts.interestPaid, loanScale), - "ripple::loanMakePayment : total interest paid rounded"); + isRounded(asset, totalParts.interestPaid, loanScale) && + totalParts.interestPaid >= beast::zero, + "ripple::loanMakePayment : total interest paid is valid"); XRPL_ASSERT( isRounded(asset, totalParts.valueChange, loanScale), - "ripple::loanMakePayment : loan value change rounded"); + "ripple::loanMakePayment : loan value change is valid"); XRPL_ASSERT( - isRounded(asset, totalParts.feePaid, loanScale), - "ripple::loanMakePayment : fee paid rounded"); + isRounded(asset, totalParts.feePaid, loanScale) && + totalParts.feePaid >= beast::zero, + "ripple::loanMakePayment : fee paid is valid"); return totalParts; } diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index a5df496fcf..6bf9560afa 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -48,6 +48,10 @@ LoanPay::preflight(PreflightContext const& ctx) if (ctx.tx[sfAmount] <= beast::zero) return temBAD_AMOUNT; + // isFlag requires an exact match - all flags to be set - to return true. + if (ctx.tx.isFlag(tfLoanOverpayment | tfLoanFullPayment)) + return temINVALID_FLAG; + return tesSUCCESS; } From 45819477d3f671abbf9a71da05374fe51d09f4f9 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 24 Oct 2025 15:03:17 -0400 Subject: [PATCH 160/291] refactor: change what values are tracked as Payment Components - Part 2 - PaymentComponents is an class used internally to break a payment value into principal, interest, and fees. --- src/xrpld/app/misc/LendingHelpers.h | 48 +---------------------------- 1 file changed, 1 insertion(+), 47 deletions(-) diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index f0a41c36d5..df32720fbd 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -66,13 +66,6 @@ struct PaymentComponents // fees. Number trackedManagementFeeDelta; - Number oldInterest; - Number oldPrincipal; - // roundedManagementFee is explicitly on for the portion of the pre-computed - // periodic payment that goes toward the Broker's management fee, which is - // tracked by sfManagementFeeOutstanding - Number oldManagementFee; - PaymentSpecialCase specialCase = PaymentSpecialCase::none; Number @@ -511,7 +504,6 @@ struct PaymentComponentsPlus : public PaymentComponents // final value returned in LoanPaymentParts.interestPaid will never be // negative. Number untrackedInterest; - Number oldValueChange; Number totalDue; PaymentComponentsPlus( @@ -521,27 +513,9 @@ struct PaymentComponentsPlus : public PaymentComponents : PaymentComponents(p) , untrackedManagementFee(f) , untrackedInterest(v) - , oldValueChange(v) , totalDue( trackedValueDelta + untrackedInterest + untrackedManagementFee) { - verify(); - } - - void - verify() const - { - assert( - totalDue == - oldPrincipal + oldInterest + oldManagementFee + - untrackedManagementFee); - assert(trackedInterestPart() == oldInterest - oldValueChange); - assert(trackedPrincipalDelta == oldPrincipal); - assert(trackedManagementFeeDelta == oldManagementFee); - assert( - trackedValueDelta == - oldPrincipal + oldInterest + oldManagementFee - oldValueChange); - assert(untrackedInterest == oldValueChange); } }; @@ -562,8 +536,6 @@ doPayment( "ripple::detail::doPayment", "Next due date proxy set"); - payment.verify(); - if (payment.specialCase == PaymentSpecialCase::final) { XRPL_ASSERT_PARTS( @@ -680,7 +652,6 @@ tryOverpayment( auto const principalError = principalOutstanding - raw.principalOutstanding; auto const feeError = managementFeeOutstanding - raw.managementFeeDue; - overpaymentComponents.verify(); auto const newRawPrincipal = raw.principalOutstanding - overpaymentComponents.trackedPrincipalDelta; @@ -816,8 +787,6 @@ doOverpayment( // LCOV_EXCL_STOP } - overpaymentComponents.verify(); - // We haven't updated the proxies yet, so they still have the original // values. Use those to do some checks. XRPL_ASSERT_PARTS( @@ -937,11 +906,9 @@ computeLatePayment( "no extra parts to this payment"); // Copy the periodic payment values, and add on the late interest. // This preserves all the other fields without having to enumerate them. - periodic.verify(); PaymentComponentsPlus const late = [&]() { auto inner = periodic; inner.rawInterest += rawLateInterest; - inner.oldInterest += roundedLateInterest; return PaymentComponentsPlus{ inner, @@ -952,7 +919,6 @@ computeLatePayment( // between periodic and late payment interest periodic.untrackedInterest + roundedLateInterest}; }(); - late.verify(); XRPL_ASSERT_PARTS( isRounded(asset, late.totalDue, loanScale), @@ -1036,12 +1002,9 @@ computeFullPayment( .trackedValueDelta = principalOutstanding + totalInterestOutstanding + managementFeeOutstanding, .trackedPrincipalDelta = principalOutstanding, - .trackedManagementFeeDelta = managementFeeOutstanding, - .oldInterest = roundedFullInterest, - .oldPrincipal = principalOutstanding, // to make the accounting work later, the tracked part of the fee // must be paid in full - .oldManagementFee = managementFeeOutstanding, + .trackedManagementFeeDelta = managementFeeOutstanding, .specialCase = PaymentSpecialCase::final}, // A full payment pays the single close payment fee, plus the computed // management fee part of the interest portion, but for tracking, the @@ -1130,9 +1093,6 @@ computePaymentComponents( interest + principalOutstanding + managementFeeOutstanding, .trackedPrincipalDelta = principalOutstanding, .trackedManagementFeeDelta = managementFeeOutstanding, - .oldInterest = interest, - .oldPrincipal = principalOutstanding, - .oldManagementFee = managementFeeOutstanding, .specialCase = PaymentSpecialCase::final}; } @@ -1231,9 +1191,6 @@ computePaymentComponents( .trackedValueDelta = roundedInterest + roundedPrincipal + roundedFee, .trackedPrincipalDelta = roundedPrincipal, .trackedManagementFeeDelta = roundedFee, - .oldInterest = roundedInterest, - .oldPrincipal = roundedPrincipal, - .oldManagementFee = roundedFee, }; } @@ -1863,9 +1820,6 @@ loanMakePayment( .trackedPrincipalDelta = payment - roundedOverpaymentInterest - roundedOverpaymentManagementFee, .trackedManagementFeeDelta = roundedOverpaymentManagementFee, - .oldInterest = roundedOverpaymentInterest, - .oldPrincipal = payment - roundedOverpaymentInterest, - .oldManagementFee = roundedOverpaymentManagementFee, .specialCase = PaymentSpecialCase::extra}, fee, roundedOverpaymentInterest}; From 0950d41fce6fff4fc410ca6ffb8106b7fd3294fc Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 24 Oct 2025 15:37:58 -0400 Subject: [PATCH 161/291] refactor: De-templatize and move LendingHelper functions from .h to .cpp - Changes: 1. Removed the `AssetType` template parameter from all functions in favor of just using the `Asset` class. 2. Fully moved all `ripple::detail` functions from .h to .cpp. 3. Moved all definitions of non `detail` functions from .h to .cpp, except roundPeriodicPayment, just because it's small and I want it to be visible. Left declarations in .h 4. Moved `PaymentSpecialCase`, `PaymentComponents` and `computePaymentComponents` into `detail` and updated references. --- src/test/app/Loan_test.cpp | 36 +- src/xrpld/app/misc/LendingHelpers.h | 1724 +----------------- src/xrpld/app/misc/detail/LendingHelpers.cpp | 1709 +++++++++++++++-- 3 files changed, 1651 insertions(+), 1818 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 4ec8798bc8..d053530236 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -1761,16 +1761,17 @@ class Loan_test : public beast::unit_test::suite Number::upward)); // Compute the expected principal amount - auto const paymentComponents = computePaymentComponents( - broker.asset.raw(), - state.loanScale, - state.totalValue, - state.principalOutstanding, - state.managementFeeOutstanding, - state.periodicPayment, - periodicRate, - state.paymentRemaining, - managementFeeRateParameter); + auto const paymentComponents = + detail::computePaymentComponents( + broker.asset.raw(), + state.loanScale, + state.totalValue, + state.principalOutstanding, + state.managementFeeOutstanding, + state.periodicPayment, + periodicRate, + state.paymentRemaining, + managementFeeRateParameter); testcase << "\tPayment components: " << state.paymentRemaining @@ -1781,10 +1782,10 @@ class Loan_test : public beast::unit_test::suite << paymentComponents.trackedPrincipalDelta << ", " << paymentComponents.trackedManagementFeeDelta << ", " << (paymentComponents.specialCase == - PaymentSpecialCase::final + detail::PaymentSpecialCase::final ? "final" : paymentComponents.specialCase == - PaymentSpecialCase::extra + detail::PaymentSpecialCase::extra ? "extra" : "none"); @@ -1802,7 +1803,7 @@ class Loan_test : public beast::unit_test::suite Number const diff = totalDue - totalDueAmount; BEAST_EXPECT( paymentComponents.specialCase == - PaymentSpecialCase::final || + detail::PaymentSpecialCase::final || diff == beast::zero || (diff > beast::zero && ((broker.asset.integral() && @@ -1810,8 +1811,9 @@ class Loan_test : public beast::unit_test::suite (totalDue.exponent() - diff.exponent() > 8)))); BEAST_EXPECT( - paymentComponents.trackedValueDelta >= + paymentComponents.trackedValueDelta == paymentComponents.trackedPrincipalDelta + + paymentComponents.trackedInterestPart() + paymentComponents.trackedManagementFeeDelta); BEAST_EXPECT( @@ -1834,12 +1836,12 @@ class Loan_test : public beast::unit_test::suite state.principalOutstanding); BEAST_EXPECT( paymentComponents.specialCase != - PaymentSpecialCase::final || + detail::PaymentSpecialCase::final || paymentComponents.trackedPrincipalDelta == state.principalOutstanding); BEAST_EXPECT( paymentComponents.specialCase == - PaymentSpecialCase::final || + detail::PaymentSpecialCase::final || (state.periodicPayment.exponent() - (paymentComponents.rawPrincipal + paymentComponents.rawInterest + @@ -1879,7 +1881,7 @@ class Loan_test : public beast::unit_test::suite --state.paymentRemaining; state.previousPaymentDate = state.nextPaymentDate; if (paymentComponents.specialCase == - PaymentSpecialCase::final) + detail::PaymentSpecialCase::final) { state.paymentRemaining = 0; } diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index df32720fbd..2b687d4b17 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -35,47 +35,15 @@ Number loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval); /// Ensure the periodic payment is always rounded consistently -template -Number +inline Number roundPeriodicPayment( - A const& asset, + Asset const& asset, Number const& periodicPayment, std::int32_t scale) { return roundToAsset(asset, periodicPayment, scale, Number::upward); } -enum class PaymentSpecialCase { none, final, extra }; - -/// This structure is used internally to compute the breakdown of a -/// single loan payment -struct PaymentComponents -{ - // raw values are unrounded, and are based on pure math - Number rawInterest; - Number rawPrincipal; - Number rawManagementFee; - // tracked values are rounded to the asset and loan scale, and correspond to - // fields in the Loan ledger object. - // trackedValueDelta modifies sfTotalValueOutstanding. - Number trackedValueDelta; - // trackedPrincipalDelta modifies sfPrincipalOutstanding. - Number trackedPrincipalDelta; - // trackedManagementFeeDelta modifies sfManagementFeeOutstanding. It will - // not include any "extra" fees that go directly to the broker, such as late - // fees. - Number trackedManagementFeeDelta; - - PaymentSpecialCase specialCase = PaymentSpecialCase::none; - - Number - trackedInterestPart() const - { - return trackedValueDelta - - (trackedPrincipalDelta + trackedManagementFeeDelta); - } -}; - /// This structure is explained in the XLS-66 spec, section 3.2.4.4 (Failure /// Conditions) struct LoanPaymentParts @@ -95,28 +63,7 @@ struct LoanPaymentParts Number feePaid; LoanPaymentParts& - operator+=(LoanPaymentParts const& other) - { - XRPL_ASSERT( - - other.principalPaid >= beast::zero, - "ripple::LoanPaymentParts::operator+= : other principal " - "non-negative"); - XRPL_ASSERT( - other.interestPaid >= beast::zero, - "ripple::LoanPaymentParts::operator+= : other interest paid " - "non-negative"); - XRPL_ASSERT( - other.feePaid >= beast::zero, - "ripple::LoanPaymentParts::operator+= : other fee paid " - "non-negative"); - - principalPaid += other.principalPaid; - interestPaid += other.interestPaid; - valueChange += other.valueChange; - feePaid += other.feePaid; - return *this; - } + operator+=(LoanPaymentParts const& other); }; /** This structure describes the initial "computed" properties of a loan. @@ -185,20 +132,12 @@ calculateRoundedLoanState( LoanState calculateRoundedLoanState(SLE::const_ref loan); -template Number computeFee( - A const& asset, + Asset const& asset, Number const& value, TenthBips16 managementFeeRate, - std::int32_t scale) -{ - return roundToAsset( - asset, - tenthBipsOfValue(value, managementFeeRate), - scale, - Number::downward); -} + std::int32_t scale); Number calculateFullPaymentInterest( @@ -222,832 +161,39 @@ calculateFullPaymentInterest( TenthBips32 closeInterestRate); namespace detail { -// These functions should rarely be used directly. More often, the ultimate -// result needs to be roundToAsset'd. +// These classes and functions should only be accessed by LendingHelper +// functions and unit tests -Number -loanPeriodicPayment( - Number const& principalOutstanding, - Number const& periodicRate, - std::uint32_t paymentsRemaining); +enum class PaymentSpecialCase { none, final, extra }; -Number -loanPeriodicPayment( - Number const& principalOutstanding, - TenthBips32 interestRate, - std::uint32_t paymentInterval, - std::uint32_t paymentsRemaining); - -Number -loanPrincipalFromPeriodicPayment( - Number const& periodicPayment, - Number const& periodicRate, - std::uint32_t paymentsRemaining); - -Number -loanLatePaymentInterest( - Number const& principalOutstanding, - TenthBips32 lateInterestRate, - NetClock::time_point parentCloseTime, - std::uint32_t nextPaymentDueDate); - -Number -loanAccruedInterest( - Number const& principalOutstanding, - Number const& periodicRate, - NetClock::time_point parentCloseTime, - std::uint32_t startDate, - std::uint32_t prevPaymentDate, - std::uint32_t paymentInterval); - -#if LOANCOMPLETE -inline Number -minusFee(Number const& value, TenthBips16 managementFeeRate) +/// This structure is used internally to compute the breakdown of a +/// single loan payment +struct PaymentComponents { - return tenthBipsOfValue(value, tenthBipsPerUnity - managementFeeRate); -} -#endif + // raw values are unrounded, and are based on pure math + Number rawInterest; + Number rawPrincipal; + Number rawManagementFee; + // tracked values are rounded to the asset and loan scale, and correspond to + // fields in the Loan ledger object. + // trackedValueDelta modifies sfTotalValueOutstanding. + Number trackedValueDelta; + // trackedPrincipalDelta modifies sfPrincipalOutstanding. + Number trackedPrincipalDelta; + // trackedManagementFeeDelta modifies sfManagementFeeOutstanding. It will + // not include any "extra" fees that go directly to the broker, such as late + // fees. + Number trackedManagementFeeDelta; -template -Number -computeRoundedPrincipalComponent( - A const& asset, - Number const& principalOutstanding, - Number const& rawPrincipalOutstanding, - Number const& rawPrincipal, - Number const& roundedPeriodicPayment, - std::int32_t scale) -{ - // Adjust the principal payment by the rounding error between the true - // and rounded principal outstanding - auto const diff = roundToAsset( - asset, - principalOutstanding - rawPrincipalOutstanding, - scale, - Number::downward); + PaymentSpecialCase specialCase = PaymentSpecialCase::none; - // If the rounded principal outstanding is greater than the true - // principal outstanding, we need to pay more principal to reduce - // the rounded principal outstanding - // - // If the rounded principal outstanding is less than the true - // principal outstanding, we need to pay less principal to allow the - // rounded principal outstanding to catch up - - auto const p = - roundToAsset(asset, rawPrincipal + diff, scale, Number::downward); - - // For particular loans, it's entirely possible for many of the first - // rounded payments to be all interest. - XRPL_ASSERT_PARTS( - p >= 0, - "rippled::detail::computeRoundedPrincipalComponent", - "principal part not negative"); - XRPL_ASSERT_PARTS( - p <= principalOutstanding, - "rippled::detail::computeRoundedPrincipalComponent", - "principal part not larger than outstanding principal"); - XRPL_ASSERT_PARTS( - !asset.integral() || abs(p - rawPrincipal) <= 1, - "rippled::detail::computeRoundedPrincipalComponent", - "principal part not larger than outstanding principal"); - XRPL_ASSERT_PARTS( - p <= roundedPeriodicPayment, - "rippled::detail::computeRoundedPrincipalComponent", - "principal part not larger than total payment"); - - // The asserts will be skipped in release builds, so check here to make - // sure nothing goes negative - if (p > roundedPeriodicPayment || p > principalOutstanding) - return std::min(roundedPeriodicPayment, principalOutstanding); - else if (p < 0) - return Number{}; - - return p; -} - -/** Returns the interest component of a payment WITHOUT accounting for - ** management fees - * - * In other words, it returns the combined value of the interest part that will - * go to the Vault and the management fee that will go to the Broker. - */ -template -Number -computeRoundedInterestComponent( - A const& asset, - Number const& interestOutstanding, - Number const& roundedPrincipal, - Number const& rawInterestOutstanding, - Number const& roundedPeriodicPayment, - std::int32_t scale) -{ - // Start by just using the non-principal part of the payment for interest - Number roundedInterest = roundedPeriodicPayment - roundedPrincipal; - XRPL_ASSERT_PARTS( - isRounded(asset, roundedInterest, scale), - "ripple::detail::computeRoundedInterestComponent", - "initial interest computation is rounded"); - - { - // Adjust the interest payment by the rounding error between the true - // and rounded interest outstanding - // - // If the rounded interest outstanding is greater than the true interest - // outstanding, we need to pay more interest to reduce the rounded - // interest outstanding - // - // If the rounded interest outstanding is less than the true interest - // outstanding, we need to pay less interest to allow the rounded - // interest outstanding to catch up - auto const diff = roundToAsset( - asset, - interestOutstanding - rawInterestOutstanding, - scale, - Number::downward); - roundedInterest += diff; - } - - // However, we cannot allow negative interest payments, therefore we need to - // cap the interest payment at 0. - // - // Ensure interest payment is non-negative and does not exceed the remaining - // payment after principal - return std::max(Number{}, roundedInterest); -} - -// The Interest and Fee components need to be calculated together, because they -// can affect each other during computation in both directions. -template -std::pair -computeRoundedInterestAndFeeComponents( - A const& asset, - Number const& interestOutstanding, - Number const& managementFeeOutstanding, - Number const& roundedPrincipal, - Number const& rawInterestOutstanding, - Number const& rawManagementFeeOutstanding, - Number const& roundedPeriodicPayment, - Number const& periodicRate, - TenthBips16 managementFeeRate, - std::int32_t scale) -{ - // Zero interest means ZERO interest - if (periodicRate == 0) - return std::make_pair(Number{}, Number{}); - - Number roundedInterest = computeRoundedInterestComponent( - asset, - interestOutstanding, - roundedPrincipal, - rawInterestOutstanding, - roundedPeriodicPayment, - scale); - - Number roundedFee = - computeFee(asset, roundedInterest, managementFeeRate, scale); - - { - // Adjust the interest fee by the rounding error between the true and - // rounded interest fee outstanding - auto const diff = roundToAsset( - asset, - managementFeeOutstanding - rawManagementFeeOutstanding, - scale, - Number::downward); - - roundedFee += diff; - - // But again, we cannot allow negative interest fees, therefore we need - // to cap the interest fee at 0 - roundedFee = std::max(Number{}, roundedFee); - - // Finally, the rounded interest fee cannot exceed the outstanding - // interest fee - roundedFee = std::min(roundedFee, managementFeeOutstanding); - } - - // Remove the fee portion from the interest payment, as the fee is paid - // separately - - // Ensure that the interest payment does not become negative, this may - // happen with high interest fees - roundedInterest = std::max(Number{}, roundedInterest - roundedFee); - - // Finally, ensure that the interest payment does not exceed the - // interest outstanding - roundedInterest = std::min(interestOutstanding, roundedInterest); - - // Make sure the parts don't add up to too much - Number excess = roundedPeriodicPayment - roundedPrincipal - - roundedInterest - roundedFee; - - XRPL_ASSERT_PARTS( - isRounded(asset, excess, scale), - "ripple::detail::computeRoundedInterestAndFeeComponents", - "excess is rounded"); - - if (excess < beast::zero) - { - // Take as much of the excess as we can out of the interest - auto part = std::min(roundedInterest, abs(excess)); - roundedInterest -= part; - excess += part; - - XRPL_ASSERT_PARTS( - excess <= beast::zero, - "ripple::detail::computeRoundedInterestAndFeeComponents", - "excess not positive (interest)"); - } - if (excess < beast::zero) - { - // If there's any left, take as much of the excess as we can out of the - // fee - auto part = std::min(roundedFee, abs(excess)); - roundedFee -= part; - excess += part; - } - - // The excess should never be negative, which indicates that the parts are - // trying to take more than the whole payment. The excess can be positive, - // which indicates that we're not going to take the whole payment amount, - // but if so, it must be small. - XRPL_ASSERT_PARTS( - excess == beast::zero || - (excess > beast::zero && - ((asset.integral() && excess < 3) || - (roundedPeriodicPayment.exponent() - excess.exponent() > 6))), - "ripple::detail::computeRoundedInterestAndFeeComponents", - "excess is zero (fee)"); - - XRPL_ASSERT_PARTS( - roundedFee >= beast::zero, - "ripple::detail::computeRoundedInterestAndFeeComponents", - "non-negative fee"); - XRPL_ASSERT_PARTS( - roundedInterest >= beast::zero, - "ripple::detail::computeRoundedInterestAndFeeComponents", - "non-negative interest"); - - return std::make_pair( - std::max(Number{}, roundedInterest), std::max(Number{}, roundedFee)); -} - -struct PaymentComponentsPlus : public PaymentComponents -{ - // untrackedManagementFeeDelta includes any fees that go directly to the - // Broker, such as late fees. This value may be negative, though the final - // value returned in LoanPaymentParts.feePaid will never be negative. - Number untrackedManagementFee; - // untrackedInterest includes any fees that go directly to the Vault, such - // as late payment penalty interest. This value may be negative, though the - // final value returned in LoanPaymentParts.interestPaid will never be - // negative. - Number untrackedInterest; - Number totalDue; - - PaymentComponentsPlus( - PaymentComponents const& p, - Number f, - Number v = Number{}) - : PaymentComponents(p) - , untrackedManagementFee(f) - , untrackedInterest(v) - , totalDue( - trackedValueDelta + untrackedInterest + untrackedManagementFee) - { - } + Number + trackedInterestPart() const; }; -template -LoanPaymentParts -doPayment( - PaymentComponentsPlus const& payment, - NumberProxy& totalValueOutstandingProxy, - NumberProxy& principalOutstandingProxy, - NumberProxy& managementFeeOutstandingProxy, - UInt32Proxy& paymentRemainingProxy, - UInt32Proxy& prevPaymentDateProxy, - UInt32OptionalProxy& nextDueDateProxy, - std::uint32_t paymentInterval) -{ - XRPL_ASSERT_PARTS( - nextDueDateProxy, - "ripple::detail::doPayment", - "Next due date proxy set"); - - if (payment.specialCase == PaymentSpecialCase::final) - { - XRPL_ASSERT_PARTS( - principalOutstandingProxy == payment.trackedPrincipalDelta, - "ripple::detail::doPayment", - "Full principal payment"); - XRPL_ASSERT_PARTS( - totalValueOutstandingProxy == payment.trackedValueDelta, - "ripple::detail::doPayment", - "Full value payment"); - XRPL_ASSERT_PARTS( - managementFeeOutstandingProxy == payment.trackedManagementFeeDelta, - "ripple::detail::doPayment", - "Full management fee payment"); - - paymentRemainingProxy = 0; - - prevPaymentDateProxy = *nextDueDateProxy; - // Remove the field. This is the only condition where nextDueDate is - // allowed to be removed. - nextDueDateProxy = std::nullopt; - - // Always zero out the the tracked values on a final payment - principalOutstandingProxy = 0; - totalValueOutstandingProxy = 0; - managementFeeOutstandingProxy = 0; - } - else - { - if (payment.specialCase != PaymentSpecialCase::extra) - { - paymentRemainingProxy -= 1; - - prevPaymentDateProxy = *nextDueDateProxy; - // STObject::OptionalField does not define operator+=, so do it the - // old-fashioned way. - nextDueDateProxy = *nextDueDateProxy + paymentInterval; - } - XRPL_ASSERT_PARTS( - principalOutstandingProxy > payment.trackedPrincipalDelta, - "ripple::detail::doPayment", - "Partial principal payment"); - XRPL_ASSERT_PARTS( - totalValueOutstandingProxy > payment.trackedValueDelta, - "ripple::detail::doPayment", - "Partial value payment"); - // Management fees are expected to be relatively small, and could get to - // zero before the loan is paid off - XRPL_ASSERT_PARTS( - managementFeeOutstandingProxy >= payment.trackedManagementFeeDelta, - "ripple::detail::doPayment", - "Valid management fee"); - - principalOutstandingProxy -= payment.trackedPrincipalDelta; - totalValueOutstandingProxy -= payment.trackedValueDelta; - managementFeeOutstandingProxy -= payment.trackedManagementFeeDelta; - } - - XRPL_ASSERT_PARTS( - // Use an explicit cast because the template parameter can be - // ValueProxy or Number - static_cast(principalOutstandingProxy) <= - static_cast(totalValueOutstandingProxy), - "ripple::detail::doPayment", - "principal does not exceed total"); - XRPL_ASSERT_PARTS( - // Use an explicit cast because the template parameter can be - // ValueProxy or Number - static_cast(managementFeeOutstandingProxy) >= beast::zero, - "ripple::detail::doPayment", - "fee outstanding stays valid"); - - return LoanPaymentParts{ - .principalPaid = payment.trackedPrincipalDelta, - // Now that the Loan object has been updated, the tracked interest - // (computed here) and untracked interest can be combined. - .interestPaid = - payment.trackedInterestPart() + payment.untrackedInterest, - .valueChange = payment.untrackedInterest, - // Now that the Loan object has been updated, the fee parts can be - // combined - .feePaid = - payment.trackedManagementFeeDelta + payment.untrackedManagementFee}; -} - -// This function mainly exists to guarantee isolation of the "sandbox" -// variables from the real / proxy variables that will affect actual -// ledger data in the caller. -template -Expected -tryOverpayment( - A const& asset, - std::int32_t loanScale, - PaymentComponentsPlus const& overpaymentComponents, - Number& totalValueOutstanding, - Number& principalOutstanding, - Number& managementFeeOutstanding, - Number& periodicPayment, - TenthBips32 interestRate, - std::uint32_t paymentInterval, - Number const& periodicRate, - std::uint32_t paymentRemaining, - std::uint32_t prevPaymentDate, - std::optional nextDueDate, - TenthBips16 const managementFeeRate, - beast::Journal j) -{ - auto const raw = calculateRawLoanState( - periodicPayment, periodicRate, paymentRemaining, managementFeeRate); - auto const rounded = calculateRoundedLoanState( - totalValueOutstanding, principalOutstanding, managementFeeOutstanding); - - auto const totalValueError = totalValueOutstanding - raw.valueOutstanding; - auto const principalError = principalOutstanding - raw.principalOutstanding; - auto const feeError = managementFeeOutstanding - raw.managementFeeDue; - - auto const newRawPrincipal = - raw.principalOutstanding - overpaymentComponents.trackedPrincipalDelta; - - auto newLoanProperties = computeLoanProperties( - asset, - newRawPrincipal, - interestRate, - paymentInterval, - paymentRemaining, - managementFeeRate); - - auto const newRaw = calculateRawLoanState( - newLoanProperties.periodicPayment, - periodicRate, - paymentRemaining, - managementFeeRate); - - totalValueOutstanding = roundToAsset( - asset, newRaw.valueOutstanding + totalValueError, loanScale); - principalOutstanding = roundToAsset( - asset, - newRaw.principalOutstanding + principalError, - loanScale, - Number::downward); - managementFeeOutstanding = - roundToAsset(asset, newRaw.managementFeeDue + feeError, loanScale); - - periodicPayment = newLoanProperties.periodicPayment; - - // check that the loan is still valid - if (newLoanProperties.firstPaymentPrincipal <= 0 && - principalOutstanding > 0) - { - // The overpayment has caused the loan to be in a state - // where no further principal can be paid. - JLOG(j.warn()) - << "Loan overpayment would cause loan to be stuck. " - "Rejecting overpayment, but normal payments are unaffected."; - return Unexpected(tesSUCCESS); - } - - // Check that the other computed values are valid - if (newLoanProperties.periodicPayment <= 0 || - newLoanProperties.totalValueOutstanding <= 0 || - newLoanProperties.managementFeeOwedToBroker < 0) - { - // LCOV_EXCL_START - JLOG(j.warn()) << "Overpayment not allowed: Computed loan " - "properties are invalid. Does " - "not compute. TotalValueOutstanding: " - << newLoanProperties.totalValueOutstanding - << ", PeriodicPayment : " - << newLoanProperties.periodicPayment - << ", ManagementFeeOwedToBroker: " - << newLoanProperties.managementFeeOwedToBroker; - return Unexpected(tesSUCCESS); - // LCOV_EXCL_STOP - } - - auto const newRounded = calculateRoundedLoanState( - totalValueOutstanding, principalOutstanding, managementFeeOutstanding); - auto const valueChange = - newRounded.interestOutstanding - rounded.interestOutstanding; - XRPL_ASSERT_PARTS( - valueChange < beast::zero, - "ripple::detail::tryOverpayment", - "principal overpayment reduced value of loan"); - - return LoanPaymentParts{ - .principalPaid = - rounded.principalOutstanding - newRounded.principalOutstanding, - .interestPaid = rounded.interestDue - newRounded.interestDue, - .valueChange = valueChange + overpaymentComponents.untrackedInterest, - .feePaid = rounded.managementFeeDue - newRounded.managementFeeDue + - overpaymentComponents.untrackedManagementFee}; -} - -template -Expected -doOverpayment( - A const& asset, - std::int32_t loanScale, - PaymentComponentsPlus const& overpaymentComponents, - NumberProxy& totalValueOutstandingProxy, - NumberProxy& principalOutstandingProxy, - NumberProxy& managementFeeOutstandingProxy, - NumberProxy& periodicPaymentProxy, - TenthBips32 const interestRate, - std::uint32_t const paymentInterval, - Number const& periodicRate, - std::uint32_t const paymentRemaining, - std::uint32_t const prevPaymentDate, - std::optional const nextDueDate, - TenthBips16 const managementFeeRate, - beast::Journal j) -{ - // Use temp variables to do the payment, so they can be thrown away if - // they don't work - Number totalValueOutstanding = totalValueOutstandingProxy; - Number principalOutstanding = principalOutstandingProxy; - Number managementFeeOutstanding = managementFeeOutstandingProxy; - Number periodicPayment = periodicPaymentProxy; - - auto const ret = tryOverpayment( - asset, - loanScale, - overpaymentComponents, - totalValueOutstanding, - principalOutstanding, - managementFeeOutstanding, - periodicPayment, - interestRate, - paymentInterval, - periodicRate, - paymentRemaining, - prevPaymentDate, - nextDueDate, - managementFeeRate, - j); - if (!ret) - return Unexpected(ret.error()); - - auto const& loanPaymentParts = *ret; - - if (principalOutstandingProxy <= principalOutstanding) - { - // LCOV_EXCL_START - JLOG(j.warn()) << "Overpayment not allowed: principal " - << "outstanding did not decrease. Before: " - << *principalOutstandingProxy - << ". After: " << principalOutstanding; - return Unexpected(tesSUCCESS); - // LCOV_EXCL_STOP - } - - // We haven't updated the proxies yet, so they still have the original - // values. Use those to do some checks. - XRPL_ASSERT_PARTS( - overpaymentComponents.trackedPrincipalDelta == - principalOutstandingProxy - principalOutstanding, - "ripple::detail::doOverpayment", - "principal change agrees"); - - XRPL_ASSERT_PARTS( - overpaymentComponents.trackedManagementFeeDelta == - managementFeeOutstandingProxy - managementFeeOutstanding, - "ripple::detail::doOverpayment", - "no fee change"); - - XRPL_ASSERT_PARTS( - overpaymentComponents.untrackedInterest == - totalValueOutstandingProxy - totalValueOutstanding - - overpaymentComponents.trackedPrincipalDelta, - "ripple::detail::doOverpayment", - "value change agrees"); - - XRPL_ASSERT_PARTS( - overpaymentComponents.trackedPrincipalDelta == - loanPaymentParts.principalPaid, - "ripple::detail::doOverpayment", - "principal payment matches"); - - XRPL_ASSERT_PARTS( - loanPaymentParts.feePaid == - overpaymentComponents.untrackedManagementFee + - overpaymentComponents.trackedManagementFeeDelta, - "ripple::detail::doOverpayment", - "fee payment matches"); - - // Update the loan object (via proxies) - totalValueOutstandingProxy = totalValueOutstanding; - principalOutstandingProxy = principalOutstanding; - managementFeeOutstandingProxy = managementFeeOutstanding; - periodicPaymentProxy = periodicPayment; - - return loanPaymentParts; -} - -std::pair -computeInterestAndFeeParts( - Number const& interest, - TenthBips16 managementFeeRate); - -template -std::pair -computeInterestAndFeeParts( - A const& asset, - Number const& interest, - TenthBips16 managementFeeRate, - std::int32_t loanScale) -{ - auto const fee = computeFee(asset, interest, managementFeeRate, loanScale); - - return std::make_pair(interest - fee, fee); -} - -/** Handle possible late payments. - * - * If this function processed a late payment, the return value will be - * a LoanPaymentParts object. If the loan is not late, the return will be an - * Unexpected(tesSUCCESS). Otherwise, it'll be an Unexpected with the error code - * the caller is expected to return. - * - * - * This function is an implementation of the XLS-66 spec, based on - * * section 3.2.4.3 (Transaction Pseudo-code), specifically the bit - * labeled "the payment is late" - * * section 3.2.4.1.2 (Late Payment) - */ -template -Expected -computeLatePayment( - A const& asset, - ApplyView const& view, - Number const& principalOutstanding, - std::int32_t nextDueDate, - PaymentComponentsPlus const& periodic, - TenthBips32 lateInterestRate, - std::int32_t loanScale, - Number const& latePaymentFee, - STAmount const& amount, - TenthBips16 managementFeeRate, - beast::Journal j) -{ - if (!hasExpired(view, nextDueDate)) - return Unexpected(tesSUCCESS); - - // the payment is late - // Late payment interest is only the part of the interest that comes - // from being late, as computed by 3.2.4.1.2. - auto const latePaymentInterest = loanLatePaymentInterest( - principalOutstanding, - lateInterestRate, - view.parentCloseTime(), - nextDueDate); - - auto const [rawLateInterest, rawLateManagementFee] = - computeInterestAndFeeParts(latePaymentInterest, managementFeeRate); - auto const [roundedLateInterest, roundedLateManagementFee] = [&]() { - auto const interest = - roundToAsset(asset, latePaymentInterest, loanScale); - return computeInterestAndFeeParts( - asset, interest, managementFeeRate, loanScale); - }(); - - XRPL_ASSERT( - roundedLateInterest >= 0, - "ripple::detail::computeLatePayment : valid late interest"); - XRPL_ASSERT_PARTS( - periodic.specialCase != PaymentSpecialCase::extra, - "ripple::detail::computeLatePayment", - "no extra parts to this payment"); - // Copy the periodic payment values, and add on the late interest. - // This preserves all the other fields without having to enumerate them. - PaymentComponentsPlus const late = [&]() { - auto inner = periodic; - inner.rawInterest += rawLateInterest; - - return PaymentComponentsPlus{ - inner, - // A late payment pays both the normal fee, and the extra fees - periodic.untrackedManagementFee + latePaymentFee + - roundedLateManagementFee, - // A late payment increases the value of the loan by the difference - // between periodic and late payment interest - periodic.untrackedInterest + roundedLateInterest}; - }(); - - XRPL_ASSERT_PARTS( - isRounded(asset, late.totalDue, loanScale), - "ripple::detail::computeLatePayment", - "total due is rounded"); - - if (amount < late.totalDue) - { - JLOG(j.warn()) << "Late loan payment amount is insufficient. Due: " - << late.totalDue << ", paid: " << amount; - return Unexpected(tecINSUFFICIENT_PAYMENT); - } - - return late; -} - -/* Handle possible full payments. - * - * If this function processed a full payment, the return value will be - * a PaymentComponentsPlus object. Otherwise, it'll be an Unexpected with the - * error code the caller is expected to return. It should NEVER return - * tesSUCCESS - */ -template -Expected -computeFullPayment( - A const& asset, - ApplyView& view, - Number const& principalOutstanding, - Number const& managementFeeOutstanding, - Number const& periodicPayment, - std::uint32_t paymentRemaining, - std::uint32_t prevPaymentDate, - std::uint32_t const startDate, - std::uint32_t const paymentInterval, - TenthBips32 const closeInterestRate, - std::int32_t loanScale, - Number const& totalInterestOutstanding, - Number const& periodicRate, - Number const& closePaymentFee, - STAmount const& amount, - TenthBips16 managementFeeRate, - beast::Journal j) -{ - if (paymentRemaining <= 1) - // If this is the last payment, it has to be a regular payment - return Unexpected(tecKILLED); - - Number const rawPrincipalOutstanding = loanPrincipalFromPeriodicPayment( - periodicPayment, periodicRate, paymentRemaining); - - // Full payment interest consists of accrued normal interest and the - // prepayment penalty, as computed by 3.2.4.1.4. - auto const fullPaymentInterest = calculateFullPaymentInterest( - rawPrincipalOutstanding, - periodicRate, - view.parentCloseTime(), - paymentInterval, - prevPaymentDate, - startDate, - closeInterestRate); - - auto const [rawFullInterest, rawFullManagementFee] = - computeInterestAndFeeParts(fullPaymentInterest, managementFeeRate); - - auto const [roundedFullInterest, roundedFullManagementFee] = [&]() { - auto const interest = - roundToAsset(asset, fullPaymentInterest, loanScale); - auto const parts = computeInterestAndFeeParts( - asset, interest, managementFeeRate, loanScale); - // Apply as much of the fee to the outstanding fee, but no - // more - return std::make_tuple(parts.first, parts.second); - }(); - - PaymentComponentsPlus const full{ - PaymentComponents{ - .rawInterest = rawFullInterest, - .rawPrincipal = rawPrincipalOutstanding, - .rawManagementFee = rawFullManagementFee, - .trackedValueDelta = principalOutstanding + - totalInterestOutstanding + managementFeeOutstanding, - .trackedPrincipalDelta = principalOutstanding, - // to make the accounting work later, the tracked part of the fee - // must be paid in full - .trackedManagementFeeDelta = managementFeeOutstanding, - .specialCase = PaymentSpecialCase::final}, - // A full payment pays the single close payment fee, plus the computed - // management fee part of the interest portion, but for tracking, the - // outstanding part is removed. That could make this value negative, but - // that's ok, because it's not used until it's recombined with - // roundedManagementFee. - closePaymentFee + roundedFullManagementFee - managementFeeOutstanding, - // A full payment changes the value of the loan by the difference - // between expected outstanding interest return and the actual interest - // paid. This value can be positive (increasing the value) or negative - // (decreasing the value). - roundedFullInterest - totalInterestOutstanding}; - - XRPL_ASSERT_PARTS( - isRounded(asset, full.totalDue, loanScale), - "ripple::detail::computeFullPayment", - "total due is rounded"); - - if (amount < full.totalDue) - // If the payment is less than the full payment amount, it's not - // sufficient to be a full payment, but that's not an error. - return Unexpected(tecINSUFFICIENT_PAYMENT); - - return full; -} - -} // namespace detail - -template -Number -valueMinusFee( - A const& asset, - Number const& value, - TenthBips16 managementFeeRate, - std::int32_t scale) -{ - return value - computeFee(asset, value, managementFeeRate, scale); -} - -template PaymentComponents computePaymentComponents( - A const& asset, + Asset const& asset, std::int32_t scale, Number const& totalValueOutstanding, Number const& principalOutstanding, @@ -1055,830 +201,48 @@ computePaymentComponents( Number const& periodicPayment, Number const& periodicRate, std::uint32_t paymentRemaining, - TenthBips16 managementFeeRate) -{ - /* - * This function is derived from the XLS-66 spec, section 3.2.4.1.1 (Regular - * Payment) - */ - XRPL_ASSERT_PARTS( - isRounded(asset, totalValueOutstanding, scale) && - isRounded(asset, principalOutstanding, scale) && - isRounded(asset, managementFeeOutstanding, scale), - "ripple::detail::computePaymentComponents", - "Outstanding values are rounded"); - auto const roundedPeriodicPayment = - roundPeriodicPayment(asset, periodicPayment, scale); + TenthBips16 managementFeeRate); - LoanState const raw = calculateRawLoanState( - periodicPayment, periodicRate, paymentRemaining, managementFeeRate); +} // namespace detail - if (paymentRemaining == 1 || - totalValueOutstanding <= roundedPeriodicPayment) - { - // If there's only one payment left, we need to pay off each of the loan - // parts. It's probably impossible for the subtraction to result in a - // negative value, but don't leave anything to chance. - Number interest = std::max( - Number{}, - totalValueOutstanding - principalOutstanding - - managementFeeOutstanding); +Number +valueMinusFee( + Asset const& asset, + Number const& value, + TenthBips16 managementFeeRate, + std::int32_t scale); - // Pay everything off - return PaymentComponents{ - .rawInterest = raw.interestOutstanding, - .rawPrincipal = raw.principalOutstanding, - .rawManagementFee = raw.managementFeeDue, - .trackedValueDelta = - interest + principalOutstanding + managementFeeOutstanding, - .trackedPrincipalDelta = principalOutstanding, - .trackedManagementFeeDelta = managementFeeOutstanding, - .specialCase = PaymentSpecialCase::final}; - } - - /* - * From the spec, once the periodicPayment is computed: - * - * The principal and interest portions can be derived as follows: - * interest = principalOutstanding * periodicRate - * principal = periodicPayment - interest - */ - Number const rawInterest = raw.principalOutstanding * periodicRate; - Number const rawPrincipal = periodicPayment - rawInterest; - Number const rawFee = tenthBipsOfValue(rawInterest, managementFeeRate); - XRPL_ASSERT_PARTS( - rawInterest >= 0, - "ripple::detail::computePaymentComponents", - "valid raw interest"); - XRPL_ASSERT_PARTS( - rawPrincipal >= 0 && rawPrincipal <= raw.principalOutstanding, - "ripple::detail::computePaymentComponents", - "valid raw principal"); - XRPL_ASSERT_PARTS( - rawFee >= 0 && rawFee <= raw.managementFeeDue, - "ripple::detail::computePaymentComponents", - "valid raw fee"); - - /* - Critical Calculation: Balancing Principal and Interest Outstanding - - This calculation maintains a delicate balance between keeping - principal outstanding and interest outstanding as close as possible to - reference values. However, we cannot perfectly match the reference - values due to rounding issues. - - Key considerations: - 1. Since the periodic payment is rounded up, we have excess funds - that can be used to pay down the loan faster than the reference - calculation. - - 2. We must ensure that loan repayment is not too fast, otherwise we - will end up with negative principal outstanding or negative - interest outstanding. - - 3. We cannot allow the borrower to repay interest ahead of schedule. - If the borrower makes an overpayment, the interest portion could - go negative, requiring complex recalculation to refund the borrower by - reflecting the overpayment in the principal portion of the loan. - */ - - Number const roundedPrincipal = detail::computeRoundedPrincipalComponent( - asset, - principalOutstanding, - raw.principalOutstanding, - rawPrincipal, - roundedPeriodicPayment, - scale); - - auto const [roundedInterest, roundedFee] = - detail::computeRoundedInterestAndFeeComponents( - asset, - totalValueOutstanding - principalOutstanding, - managementFeeOutstanding, - roundedPrincipal, - raw.interestOutstanding, - raw.managementFeeDue, - roundedPeriodicPayment, - periodicRate, - managementFeeRate, - scale); - - XRPL_ASSERT_PARTS( - roundedInterest >= 0 && isRounded(asset, roundedInterest, scale), - "ripple::detail::computePaymentComponents", - "valid rounded interest"); - XRPL_ASSERT_PARTS( - roundedFee >= 0 && roundedFee <= managementFeeOutstanding && - isRounded(asset, roundedFee, scale), - "ripple::detail::computePaymentComponents", - "valid rounded fee"); - XRPL_ASSERT_PARTS( - roundedPrincipal >= 0 && roundedPrincipal <= principalOutstanding && - roundedPrincipal <= roundedPeriodicPayment && - isRounded(asset, roundedPrincipal, scale), - "ripple::detail::computePaymentComponents", - "valid rounded principal"); - XRPL_ASSERT_PARTS( - roundedPrincipal + roundedInterest + roundedFee <= - roundedPeriodicPayment, - "ripple::detail::computePaymentComponents", - "payment parts fit within payment limit"); - - return PaymentComponents{ - .rawInterest = rawInterest - rawFee, - .rawPrincipal = rawPrincipal, - .rawManagementFee = rawFee, - .trackedValueDelta = roundedInterest + roundedPrincipal + roundedFee, - .trackedPrincipalDelta = roundedPrincipal, - .trackedManagementFeeDelta = roundedFee, - }; -} - -template LoanProperties computeLoanProperties( - A const& asset, + Asset const& asset, Number principalOutstanding, TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t paymentsRemaining, - TenthBips16 managementFeeRate) -{ - auto const periodicRate = loanPeriodicRate(interestRate, paymentInterval); - XRPL_ASSERT( - interestRate == 0 || periodicRate > 0, - "ripple::computeLoanProperties : valid rate"); + TenthBips16 managementFeeRate); - auto const periodicPayment = detail::loanPeriodicPayment( - principalOutstanding, periodicRate, paymentsRemaining); - STAmount const totalValueOutstanding = [&]() { - NumberRoundModeGuard mg(Number::to_nearest); - // Use STAmount's internal rounding instead of roundToAsset, because - // we're going to use this result to determine the scale for all the - // other rounding. - return STAmount{ - asset, - /* - * This formula is from the XLS-66 spec, section 3.2.4.2 (Total - * Loan Value Calculation), specifically "totalValueOutstanding - * = ..." - */ - periodicPayment * paymentsRemaining}; - }(); - // Base the loan scale on the total value, since that's going to be the - // biggest number involved (barring unusual parameters for late, full, or - // over payments) - auto const loanScale = totalValueOutstanding.exponent(); - XRPL_ASSERT_PARTS( - (totalValueOutstanding.integral() && loanScale == 0) || - (!totalValueOutstanding.integral() && - loanScale == - static_cast(totalValueOutstanding).exponent()), - "ripple::computeLoanProperties", - "loanScale value fits expectations"); - - // Since we just figured out the loan scale, we haven't been able to - // validate that the principal fits in it, so to allow this function to - // succeed, round it here, and let the caller do the validation. - principalOutstanding = roundToAsset( - asset, principalOutstanding, loanScale, Number::to_nearest); - - auto const feeOwedToBroker = computeFee( - asset, - /* - * This formula is from the XLS-66 spec, section 3.2.4.2 (Total Loan - * Value Calculation), specifically "totalInterestOutstanding = ..." - */ - totalValueOutstanding - principalOutstanding, - managementFeeRate, - loanScale); - - auto const firstPaymentPrincipal = [&]() { - // Compute the parts for the first payment. Ensure that the - // principal payment will actually change the principal. - auto const paymentComponents = computePaymentComponents( - asset, - loanScale, - totalValueOutstanding, - principalOutstanding, - feeOwedToBroker, - periodicPayment, - periodicRate, - paymentsRemaining, - managementFeeRate); - - // The unrounded principal part needs to be large enough to affect the - // principal. What to do if not is left to the caller - return paymentComponents.rawPrincipal; - }(); - - return LoanProperties{ - .periodicPayment = periodicPayment, - .totalValueOutstanding = totalValueOutstanding, - .managementFeeOwedToBroker = feeOwedToBroker, - .loanScale = loanScale, - .firstPaymentPrincipal = firstPaymentPrincipal}; -} - -#if LOANCOMPLETE -template -Number -loanPeriodicPayment( - A const& asset, - Number const& principalOutstanding, - Number const& periodicRate, - std::uint32_t paymentsRemaining, - std::int32_t scale) -{ - return roundPeriodicPayment( - asset, - detail::loanPeriodicPayment( - principalOutstanding, periodicRate, paymentsRemaining), - scale); -} - -template -Number -loanPeriodicPayment( - A const& asset, - Number const& principalOutstanding, - TenthBips32 interestRate, - std::uint32_t paymentInterval, - std::uint32_t paymentsRemaining, - std::int32_t scale) -{ - return loanPeriodicPayment( - asset, - principalOutstanding, - loanPeriodicRate(interestRate, paymentInterval), - paymentsRemaining, - scale); -} - -template -Number -loanTotalValueOutstanding( - A asset, - std::int32_t scale, - Number const& periodicPayment, - std::uint32_t paymentsRemaining) -{ - return roundToAsset( - asset, - /* - * This formula is from the XLS-66 spec, section 3.2.4.2 (Total Loan - * Value Calculation), specifically "totalValueOutstanding = ..." - */ - periodicPayment * paymentsRemaining, - scale, - Number::upward); -} - -template -Number -loanTotalValueOutstanding( - A asset, - std::int32_t scale, - Number const& principalOutstanding, - TenthBips32 interestRate, - std::uint32_t paymentInterval, - std::uint32_t paymentsRemaining) -{ - /* - * This function is derived from the XLS-66 spec, section 3.2.4.2 (Total - * Loan Value Calculation) - */ - return loanTotalValueOutstanding( - asset, - scale, - loanPeriodicPayment( - asset, - principalOutstanding, - interestRate, - paymentInterval, - paymentsRemaining, - scale), - paymentsRemaining); -} - -inline Number -loanTotalInterestOutstanding( - Number const& principalOutstanding, - Number const& totalValueOutstanding) -{ - /* - * This formula is from the XLS-66 spec, section 3.2.4.2 (Total Loan - * Value Calculation), specifically "totalInterestOutstanding = ..." - */ - return totalValueOutstanding - principalOutstanding; -} - -template -Number -loanTotalInterestOutstanding( - A asset, - std::int32_t scale, - Number const& principalOutstanding, - TenthBips32 interestRate, - std::uint32_t paymentInterval, - std::uint32_t paymentsRemaining) -{ - /* - * This formula is derived from the XLS-66 spec, section 3.2.4.2 (Total Loan - * Value Calculation) - */ - return loanTotalInterestOutstanding( - principalOutstanding, - loanTotalValueOutstanding( - asset, - scale, - principalOutstanding, - interestRate, - paymentInterval, - paymentsRemaining)); -} -template -Number -loanInterestOutstandingMinusFee( - A const& asset, - Number const& totalInterestOutstanding, - TenthBips16 managementFeeRate, - std::int32_t scale) -{ - return valueMinusFee( - asset, totalInterestOutstanding, managementFeeRate, scale); -} - -template -Number -loanInterestOutstandingMinusFee( - A const& asset, - std::int32_t scale, - Number const& principalOutstanding, - TenthBips32 interestRate, - std::uint32_t paymentInterval, - std::uint32_t paymentsRemaining, - TenthBips16 managementFeeRate) -{ - return loanInterestOutstandingMinusFee( - asset, - loanTotalInterestOutstanding( - asset, - scale, - principalOutstanding, - interestRate, - paymentInterval, - paymentsRemaining), - managementFeeRate, - scale); -} - -template -Number -loanLatePaymentInterest( - A const& asset, - Number const& principalOutstanding, - TenthBips32 lateInterestRate, - NetClock::time_point parentCloseTime, - std::uint32_t nextPaymentDueDate, - std::int32_t const& scale) -{ - return roundToAsset( - asset, - detail::loanLatePaymentInterest( - principalOutstanding, - lateInterestRate, - parentCloseTime, - nextPaymentDueDate), - scale); -} -#endif - -template bool -isRounded(A const& asset, Number const& value, std::int32_t scale) -{ - return roundToAsset(asset, value, scale, Number::downward) == - roundToAsset(asset, value, scale, Number::upward); -} +isRounded(Asset const& asset, Number const& value, std::int32_t scale); -template Expected loanMakeFullPayment( - A const& asset, + Asset const& asset, ApplyView& view, SLE::ref loan, SLE::const_ref brokerSle, STAmount const& amount, bool const overpaymentAllowed, - beast::Journal j) -{ - auto principalOutstandingProxy = loan->at(sfPrincipalOutstanding); - auto paymentRemainingProxy = loan->at(sfPaymentRemaining); + beast::Journal j); - if (paymentRemainingProxy == 0 || principalOutstandingProxy == 0) - { - // Loan complete - JLOG(j.warn()) << "Loan is already paid off."; - return Unexpected(tecKILLED); - } - - auto totalValueOutstandingProxy = loan->at(sfTotalValueOutstanding); - auto managementFeeOutstandingProxy = loan->at(sfManagementFeeOutstanding); - - // Next payment due date must be set unless the loan is complete - auto nextDueDateProxy = loan->at(~sfNextPaymentDueDate); - if (!nextDueDateProxy) - { - JLOG(j.warn()) << "Loan next payment due date is not set."; - return Unexpected(tecINTERNAL); - } - - std::int32_t const loanScale = loan->at(sfLoanScale); - - TenthBips32 const interestRate{loan->at(sfInterestRate)}; - TenthBips32 const closeInterestRate{loan->at(sfCloseInterestRate)}; - - Number const closePaymentFee = - roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale); - TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; - - auto const periodicPayment = loan->at(sfPeriodicPayment); - - auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDate); - std::uint32_t const startDate = loan->at(sfStartDate); - - std::uint32_t const paymentInterval = loan->at(sfPaymentInterval); - // Compute the normal periodic rate, payment, etc. - // We'll need it in the remaining calculations - Number const periodicRate = loanPeriodicRate(interestRate, paymentInterval); - XRPL_ASSERT( - interestRate == 0 || periodicRate > 0, - "ripple::loanMakeFullPayment : valid rate"); - - XRPL_ASSERT( - *totalValueOutstandingProxy > 0, - "ripple::loanMakeFullPayment : valid total value"); - - view.update(loan); - - // ------------------------------------------------------------- - // full payment handling - LoanState const roundedLoanState = calculateRoundedLoanState( - totalValueOutstandingProxy, - principalOutstandingProxy, - managementFeeOutstandingProxy); - - if (auto const fullPaymentComponents = detail::computeFullPayment( - asset, - view, - principalOutstandingProxy, - managementFeeOutstandingProxy, - periodicPayment, - paymentRemainingProxy, - prevPaymentDateProxy, - startDate, - paymentInterval, - closeInterestRate, - loanScale, - roundedLoanState.interestDue, - periodicRate, - closePaymentFee, - amount, - managementFeeRate, - j)) - return doPayment( - *fullPaymentComponents, - totalValueOutstandingProxy, - principalOutstandingProxy, - managementFeeOutstandingProxy, - paymentRemainingProxy, - prevPaymentDateProxy, - nextDueDateProxy, - paymentInterval); - else if (fullPaymentComponents.error()) - // error() will be the TER returned if a payment is not made. It - // will only evaluate to true if it's unsuccessful. Otherwise, - // tesSUCCESS means nothing was done, so continue. - return Unexpected(fullPaymentComponents.error()); - - // LCOV_EXCL_START - UNREACHABLE("ripple::loanMakeFullPayment : invalid result"); - return Unexpected(tecINTERNAL); - // LCOV_EXCL_STOP -} - -template Expected loanMakePayment( - A const& asset, + Asset const& asset, ApplyView& view, SLE::ref loan, SLE::const_ref brokerSle, STAmount const& amount, bool const overpaymentAllowed, - beast::Journal j) -{ - /* - * This function is an implementation of the XLS-66 spec, - * section 3.2.4.3 (Transaction Pseudo-code) - */ - auto principalOutstandingProxy = loan->at(sfPrincipalOutstanding); - auto paymentRemainingProxy = loan->at(sfPaymentRemaining); - - if (paymentRemainingProxy == 0 || principalOutstandingProxy == 0) - { - // Loan complete - // This is already checked in LoanPay::preclaim() - // LCOV_EXCL_START - JLOG(j.warn()) << "Loan is already paid off."; - return Unexpected(tecKILLED); - // LCOV_EXCL_STOP - } - - auto totalValueOutstandingProxy = loan->at(sfTotalValueOutstanding); - auto managementFeeOutstandingProxy = loan->at(sfManagementFeeOutstanding); - - // Next payment due date must be set unless the loan is complete - auto nextDueDateProxy = loan->at(~sfNextPaymentDueDate); - if (!nextDueDateProxy) - { - JLOG(j.warn()) << "Loan next payment due date is not set."; - return Unexpected(tecINTERNAL); - } - - std::int32_t const loanScale = loan->at(sfLoanScale); - - TenthBips32 const interestRate{loan->at(sfInterestRate)}; - TenthBips32 const lateInterestRate{loan->at(sfLateInterestRate)}; - TenthBips32 const closeInterestRate{loan->at(sfCloseInterestRate)}; - - Number const serviceFee = loan->at(sfLoanServiceFee); - Number const latePaymentFee = loan->at(sfLatePaymentFee); - Number const closePaymentFee = - roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale); - TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; - - auto const periodicPayment = loan->at(sfPeriodicPayment); - - auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDate); - - std::uint32_t const paymentInterval = loan->at(sfPaymentInterval); - // Compute the normal periodic rate, payment, etc. - // We'll need it in the remaining calculations - Number const periodicRate = loanPeriodicRate(interestRate, paymentInterval); - XRPL_ASSERT( - interestRate == 0 || periodicRate > 0, - "ripple::loanMakePayment : valid rate"); - - XRPL_ASSERT( - *totalValueOutstandingProxy > 0, - "ripple::loanMakePayment : valid total value"); - - view.update(loan); - - detail::PaymentComponentsPlus const periodic{ - computePaymentComponents( - asset, - loanScale, - totalValueOutstandingProxy, - principalOutstandingProxy, - managementFeeOutstandingProxy, - periodicPayment, - periodicRate, - paymentRemainingProxy, - managementFeeRate), - serviceFee}; - XRPL_ASSERT_PARTS( - periodic.trackedPrincipalDelta >= 0, - "ripple::loanMakePayment", - "regular payment valid principal"); - - // ------------------------------------------------------------- - // late payment handling - if (auto const latePaymentComponents = detail::computeLatePayment( - asset, - view, - principalOutstandingProxy, - *nextDueDateProxy, - periodic, - lateInterestRate, - loanScale, - latePaymentFee, - amount, - managementFeeRate, - j)) - { - return doPayment( - *latePaymentComponents, - totalValueOutstandingProxy, - principalOutstandingProxy, - managementFeeOutstandingProxy, - paymentRemainingProxy, - prevPaymentDateProxy, - nextDueDateProxy, - paymentInterval); - } - else if (latePaymentComponents.error()) - // error() will be the TER returned if a payment is not made. It will - // only evaluate to true if it's unsuccessful. Otherwise, tesSUCCESS - // means nothing was done, so continue. - return Unexpected(latePaymentComponents.error()); - - // ------------------------------------------------------------- - // regular periodic payment handling - - // if the payment is not late nor if it's a full payment, then it must - // be a periodic one, with possible overpayments - - // This will keep a running total of what is actually paid, if the payment - // is sufficient for a single payment - Number totalPaid = periodic.totalDue; - - if (amount < totalPaid) - { - JLOG(j.warn()) << "Periodic loan payment amount is insufficient. Due: " - << totalPaid << ", paid: " << amount; - return Unexpected(tecINSUFFICIENT_PAYMENT); - } - - LoanPaymentParts totalParts = detail::doPayment( - periodic, - totalValueOutstandingProxy, - principalOutstandingProxy, - managementFeeOutstandingProxy, - paymentRemainingProxy, - prevPaymentDateProxy, - nextDueDateProxy, - paymentInterval); - - std::size_t numPayments = 1; - - while (totalPaid < amount && paymentRemainingProxy > 0) - { - // Try to make more payments - detail::PaymentComponentsPlus const nextPayment{ - computePaymentComponents( - asset, - loanScale, - totalValueOutstandingProxy, - principalOutstandingProxy, - managementFeeOutstandingProxy, - periodicPayment, - periodicRate, - paymentRemainingProxy, - managementFeeRate), - serviceFee}; - XRPL_ASSERT_PARTS( - nextPayment.trackedPrincipalDelta >= 0, - "ripple::loanMakePayment", - "additional payment pays non-negative principal"); - XRPL_ASSERT( - nextPayment.rawInterest <= periodic.rawInterest, - "ripple::loanMakePayment : decreasing interest"); - XRPL_ASSERT( - nextPayment.rawPrincipal >= periodic.rawPrincipal, - "ripple::loanMakePayment : increasing principal"); - - if (amount < totalPaid + nextPayment.totalDue) - // We're done making payments. - break; - - totalPaid += nextPayment.totalDue; - totalParts += detail::doPayment( - nextPayment, - totalValueOutstandingProxy, - principalOutstandingProxy, - managementFeeOutstandingProxy, - paymentRemainingProxy, - prevPaymentDateProxy, - nextDueDateProxy, - paymentInterval); - ++numPayments; - - XRPL_ASSERT_PARTS( - (nextPayment.specialCase == PaymentSpecialCase::final) == - (paymentRemainingProxy == 0), - "ripple::loanMakePayment", - "final payment is the final payment"); - } - - XRPL_ASSERT_PARTS( - totalParts.principalPaid + totalParts.interestPaid + - totalParts.feePaid == - totalPaid, - "ripple::loanMakePayment", - "payment parts add up"); - XRPL_ASSERT_PARTS( - totalParts.valueChange == 0, - "ripple::loanMakePayment", - "no value change"); - - // ------------------------------------------------------------- - // overpayment handling - if (overpaymentAllowed && loan->isFlag(lsfLoanOverpayment) && - paymentRemainingProxy > 0 && nextDueDateProxy && totalPaid < amount) - { - TenthBips32 const overpaymentInterestRate{ - loan->at(sfOverpaymentInterestRate)}; - TenthBips32 const overpaymentFeeRate{loan->at(sfOverpaymentFee)}; - - Number const overpayment = amount - totalPaid; - XRPL_ASSERT( - overpayment > 0 && isRounded(asset, overpayment, loanScale), - "ripple::loanMakePayment : valid overpayment amount"); - - Number const fee = roundToAsset( - asset, - tenthBipsOfValue(overpayment, overpaymentFeeRate), - loanScale); - - Number const payment = overpayment - fee; - - auto const [rawOverpaymentInterest, rawOverpaymentManagementFee] = - [&]() { - Number const interest = - tenthBipsOfValue(payment, overpaymentInterestRate); - return detail::computeInterestAndFeeParts( - interest, managementFeeRate); - }(); - auto const - [roundedOverpaymentInterest, roundedOverpaymentManagementFee] = - [&]() { - Number const interest = - roundToAsset(asset, rawOverpaymentInterest, loanScale); - return detail::computeInterestAndFeeParts( - asset, interest, managementFeeRate, loanScale); - }(); - - detail::PaymentComponentsPlus overpaymentComponents{ - PaymentComponents{ - .rawInterest = rawOverpaymentInterest, - .rawPrincipal = payment - rawOverpaymentInterest, - .rawManagementFee = 0, - .trackedValueDelta = payment, - .trackedPrincipalDelta = payment - roundedOverpaymentInterest - - roundedOverpaymentManagementFee, - .trackedManagementFeeDelta = roundedOverpaymentManagementFee, - .specialCase = PaymentSpecialCase::extra}, - fee, - roundedOverpaymentInterest}; - - // Don't process an overpayment if the whole amount (or more!) - // gets eaten by fees and interest. - if (overpaymentComponents.rawPrincipal > 0 && - overpaymentComponents.trackedPrincipalDelta > 0) - { - XRPL_ASSERT_PARTS( - overpaymentComponents.untrackedInterest >= beast::zero, - "ripple::loanMakePayment", - "overpayment penalty did not reduce value of loan"); - // Can't just use `periodicPayment` here, because it might change - auto periodicPaymentProxy = loan->at(sfPeriodicPayment); - if (auto const overResult = detail::doOverpayment( - asset, - loanScale, - overpaymentComponents, - totalValueOutstandingProxy, - principalOutstandingProxy, - managementFeeOutstandingProxy, - periodicPaymentProxy, - interestRate, - paymentInterval, - periodicRate, - paymentRemainingProxy, - prevPaymentDateProxy, - nextDueDateProxy, - managementFeeRate, - j)) - totalParts += *overResult; - else if (overResult.error()) - // error() will be the TER returned if a payment is not made. It - // will only evaluate to true if it's unsuccessful. Otherwise, - // tesSUCCESS means nothing was done, so continue. - return Unexpected(overResult.error()); - } - } - - // Check the final results are rounded, to double-check that the - // intermediate steps were rounded. - XRPL_ASSERT( - isRounded(asset, totalParts.principalPaid, loanScale) && - totalParts.principalPaid >= beast::zero, - "ripple::loanMakePayment : total principal paid is valid"); - XRPL_ASSERT( - isRounded(asset, totalParts.interestPaid, loanScale) && - totalParts.interestPaid >= beast::zero, - "ripple::loanMakePayment : total interest paid is valid"); - XRPL_ASSERT( - isRounded(asset, totalParts.valueChange, loanScale), - "ripple::loanMakePayment : loan value change is valid"); - XRPL_ASSERT( - isRounded(asset, totalParts.feePaid, loanScale) && - totalParts.feePaid >= beast::zero, - "ripple::loanMakePayment : fee paid is valid"); - return totalParts; -} + beast::Journal j); } // namespace ripple diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index 755c251d3a..7a74b47207 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -30,6 +30,30 @@ checkLendingProtocolDependencies(PreflightContext const& ctx) VaultCreate::checkExtraFeatures(ctx); } +LoanPaymentParts& +LoanPaymentParts::operator+=(LoanPaymentParts const& other) +{ + XRPL_ASSERT( + + other.principalPaid >= beast::zero, + "ripple::LoanPaymentParts::operator+= : other principal " + "non-negative"); + XRPL_ASSERT( + other.interestPaid >= beast::zero, + "ripple::LoanPaymentParts::operator+= : other interest paid " + "non-negative"); + XRPL_ASSERT( + other.feePaid >= beast::zero, + "ripple::LoanPaymentParts::operator+= : other fee paid " + "non-negative"); + + principalPaid += other.principalPaid; + interestPaid += other.interestPaid; + valueChange += other.valueChange; + feePaid += other.feePaid; + return *this; +} + Number loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval) { @@ -44,128 +68,11 @@ loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval) (365 * 24 * 60 * 60); } -Number -calculateFullPaymentInterest( - Number const& rawPrincipalOutstanding, - Number const& periodicRate, - NetClock::time_point parentCloseTime, - std::uint32_t paymentInterval, - std::uint32_t prevPaymentDate, - std::uint32_t startDate, - TenthBips32 closeInterestRate) +bool +isRounded(Asset const& asset, Number const& value, std::int32_t scale) { - // If there is more than one payment remaining, see if enough was - // paid for a full payment - auto const accruedInterest = detail::loanAccruedInterest( - rawPrincipalOutstanding, - periodicRate, - parentCloseTime, - startDate, - prevPaymentDate, - paymentInterval); - XRPL_ASSERT( - accruedInterest >= 0, - "ripple::detail::computeFullPaymentInterest : valid accrued interest"); - - auto const prepaymentPenalty = - tenthBipsOfValue(rawPrincipalOutstanding, closeInterestRate); - XRPL_ASSERT( - prepaymentPenalty >= 0, - "ripple::detail::computeFullPaymentInterest : valid prepayment " - "interest"); - - return accruedInterest + prepaymentPenalty; -} - -Number -calculateFullPaymentInterest( - Number const& periodicPayment, - Number const& periodicRate, - std::uint32_t paymentRemaining, - NetClock::time_point parentCloseTime, - std::uint32_t paymentInterval, - std::uint32_t prevPaymentDate, - std::uint32_t startDate, - TenthBips32 closeInterestRate) -{ - Number const rawPrincipalOutstanding = - detail::loanPrincipalFromPeriodicPayment( - periodicPayment, periodicRate, paymentRemaining); - - return calculateFullPaymentInterest( - rawPrincipalOutstanding, - periodicRate, - parentCloseTime, - paymentInterval, - prevPaymentDate, - startDate, - closeInterestRate); -} - -LoanState -calculateRawLoanState( - Number const& periodicPayment, - Number const& periodicRate, - std::uint32_t const paymentRemaining, - TenthBips16 const managementFeeRate) -{ - Number const rawValueOutstanding = periodicPayment * paymentRemaining; - Number const rawPrincipalOutstanding = - detail::loanPrincipalFromPeriodicPayment( - periodicPayment, periodicRate, paymentRemaining); - Number const rawInterestOutstanding = - rawValueOutstanding - rawPrincipalOutstanding; - Number const rawManagementFeeOutstanding = - tenthBipsOfValue(rawInterestOutstanding, managementFeeRate); - - return LoanState{ - .valueOutstanding = rawValueOutstanding, - .principalOutstanding = rawPrincipalOutstanding, - .interestOutstanding = rawInterestOutstanding, - .interestDue = rawInterestOutstanding - rawManagementFeeOutstanding, - .managementFeeDue = rawManagementFeeOutstanding}; -}; - -LoanState -calculateRawLoanState( - Number const& periodicPayment, - TenthBips32 interestRate, - std::uint32_t paymentInterval, - std::uint32_t const paymentRemaining, - TenthBips16 const managementFeeRate) -{ - return calculateRawLoanState( - periodicPayment, - loanPeriodicRate(interestRate, paymentInterval), - paymentRemaining, - managementFeeRate); -} - -LoanState -calculateRoundedLoanState( - Number const& totalValueOutstanding, - Number const& principalOutstanding, - Number const& managementFeeOutstanding) -{ - // This implementation is pretty trivial, but ensures the calculations are - // consistent everywhere, and reduces copy/paste errors. - Number const interestOutstanding = - totalValueOutstanding - principalOutstanding; - return { - .valueOutstanding = totalValueOutstanding, - .principalOutstanding = principalOutstanding, - .interestOutstanding = interestOutstanding, - .interestDue = interestOutstanding - managementFeeOutstanding, - .managementFeeDue = managementFeeOutstanding}; -} - -LoanState -calculateRoundedLoanState(SLE::const_ref loan) -{ - return calculateRoundedLoanState( - loan->at(sfTotalValueOutstanding), - loan->at(sfPrincipalOutstanding), - loan->at(sfManagementFeeOutstanding)); + return roundToAsset(asset, value, scale, Number::downward) == + roundToAsset(asset, value, scale, Number::upward); } namespace detail { @@ -306,6 +213,1566 @@ loanAccruedInterest( paymentInterval; } +Number +computeRoundedPrincipalComponent( + Asset const& asset, + Number const& principalOutstanding, + Number const& rawPrincipalOutstanding, + Number const& rawPrincipal, + Number const& roundedPeriodicPayment, + std::int32_t scale) +{ + // Adjust the principal payment by the rounding error between the true + // and rounded principal outstanding + auto const diff = roundToAsset( + asset, + principalOutstanding - rawPrincipalOutstanding, + scale, + Number::downward); + + // If the rounded principal outstanding is greater than the true + // principal outstanding, we need to pay more principal to reduce + // the rounded principal outstanding + // + // If the rounded principal outstanding is less than the true + // principal outstanding, we need to pay less principal to allow the + // rounded principal outstanding to catch up + + auto const p = + roundToAsset(asset, rawPrincipal + diff, scale, Number::downward); + + // For particular loans, it's entirely possible for many of the first + // rounded payments to be all interest. + XRPL_ASSERT_PARTS( + p >= 0, + "rippled::detail::computeRoundedPrincipalComponent", + "principal part not negative"); + XRPL_ASSERT_PARTS( + p <= principalOutstanding, + "rippled::detail::computeRoundedPrincipalComponent", + "principal part not larger than outstanding principal"); + XRPL_ASSERT_PARTS( + !asset.integral() || abs(p - rawPrincipal) <= 1, + "rippled::detail::computeRoundedPrincipalComponent", + "principal part not larger than outstanding principal"); + XRPL_ASSERT_PARTS( + p <= roundedPeriodicPayment, + "rippled::detail::computeRoundedPrincipalComponent", + "principal part not larger than total payment"); + + // The asserts will be skipped in release builds, so check here to make + // sure nothing goes negative + if (p > roundedPeriodicPayment || p > principalOutstanding) + return std::min(roundedPeriodicPayment, principalOutstanding); + else if (p < 0) + return Number{}; + + return p; +} + +/** Returns the interest component of a payment WITHOUT accounting for + ** management fees + * + * In other words, it returns the combined value of the interest part that will + * go to the Vault and the management fee that will go to the Broker. + */ + +Number +computeRoundedInterestComponent( + Asset const& asset, + Number const& interestOutstanding, + Number const& roundedPrincipal, + Number const& rawInterestOutstanding, + Number const& roundedPeriodicPayment, + std::int32_t scale) +{ + // Start by just using the non-principal part of the payment for interest + Number roundedInterest = roundedPeriodicPayment - roundedPrincipal; + XRPL_ASSERT_PARTS( + isRounded(asset, roundedInterest, scale), + "ripple::detail::computeRoundedInterestComponent", + "initial interest computation is rounded"); + + { + // Adjust the interest payment by the rounding error between the true + // and rounded interest outstanding + // + // If the rounded interest outstanding is greater than the true interest + // outstanding, we need to pay more interest to reduce the rounded + // interest outstanding + // + // If the rounded interest outstanding is less than the true interest + // outstanding, we need to pay less interest to allow the rounded + // interest outstanding to catch up + auto const diff = roundToAsset( + asset, + interestOutstanding - rawInterestOutstanding, + scale, + Number::downward); + roundedInterest += diff; + } + + // However, we cannot allow negative interest payments, therefore we need to + // cap the interest payment at 0. + // + // Ensure interest payment is non-negative and does not exceed the remaining + // payment after principal + return std::max(Number{}, roundedInterest); +} + +// The Interest and Fee components need to be calculated together, because they +// can affect each other during computation in both directions. + +std::pair +computeRoundedInterestAndFeeComponents( + Asset const& asset, + Number const& interestOutstanding, + Number const& managementFeeOutstanding, + Number const& roundedPrincipal, + Number const& rawInterestOutstanding, + Number const& rawManagementFeeOutstanding, + Number const& roundedPeriodicPayment, + Number const& periodicRate, + TenthBips16 managementFeeRate, + std::int32_t scale) +{ + // Zero interest means ZERO interest + if (periodicRate == 0) + return std::make_pair(Number{}, Number{}); + + Number roundedInterest = computeRoundedInterestComponent( + asset, + interestOutstanding, + roundedPrincipal, + rawInterestOutstanding, + roundedPeriodicPayment, + scale); + + Number roundedFee = + computeFee(asset, roundedInterest, managementFeeRate, scale); + + { + // Adjust the interest fee by the rounding error between the true and + // rounded interest fee outstanding + auto const diff = roundToAsset( + asset, + managementFeeOutstanding - rawManagementFeeOutstanding, + scale, + Number::downward); + + roundedFee += diff; + + // But again, we cannot allow negative interest fees, therefore we need + // to cap the interest fee at 0 + roundedFee = std::max(Number{}, roundedFee); + + // Finally, the rounded interest fee cannot exceed the outstanding + // interest fee + roundedFee = std::min(roundedFee, managementFeeOutstanding); + } + + // Remove the fee portion from the interest payment, as the fee is paid + // separately + + // Ensure that the interest payment does not become negative, this may + // happen with high interest fees + roundedInterest = std::max(Number{}, roundedInterest - roundedFee); + + // Finally, ensure that the interest payment does not exceed the + // interest outstanding + roundedInterest = std::min(interestOutstanding, roundedInterest); + + // Make sure the parts don't add up to too much + Number excess = roundedPeriodicPayment - roundedPrincipal - + roundedInterest - roundedFee; + + XRPL_ASSERT_PARTS( + isRounded(asset, excess, scale), + "ripple::detail::computeRoundedInterestAndFeeComponents", + "excess is rounded"); + + if (excess < beast::zero) + { + // Take as much of the excess as we can out of the interest + auto part = std::min(roundedInterest, abs(excess)); + roundedInterest -= part; + excess += part; + + XRPL_ASSERT_PARTS( + excess <= beast::zero, + "ripple::detail::computeRoundedInterestAndFeeComponents", + "excess not positive (interest)"); + } + if (excess < beast::zero) + { + // If there's any left, take as much of the excess as we can out of the + // fee + auto part = std::min(roundedFee, abs(excess)); + roundedFee -= part; + excess += part; + } + + // The excess should never be negative, which indicates that the parts are + // trying to take more than the whole payment. The excess can be positive, + // which indicates that we're not going to take the whole payment amount, + // but if so, it must be small. + XRPL_ASSERT_PARTS( + excess == beast::zero || + (excess > beast::zero && + ((asset.integral() && excess < 3) || + (roundedPeriodicPayment.exponent() - excess.exponent() > 6))), + "ripple::detail::computeRoundedInterestAndFeeComponents", + "excess is zero (fee)"); + + XRPL_ASSERT_PARTS( + roundedFee >= beast::zero, + "ripple::detail::computeRoundedInterestAndFeeComponents", + "non-negative fee"); + XRPL_ASSERT_PARTS( + roundedInterest >= beast::zero, + "ripple::detail::computeRoundedInterestAndFeeComponents", + "non-negative interest"); + + return std::make_pair( + std::max(Number{}, roundedInterest), std::max(Number{}, roundedFee)); +} + +struct PaymentComponentsPlus : public PaymentComponents +{ + // untrackedManagementFeeDelta includes any fees that go directly to the + // Broker, such as late fees. This value may be negative, though the final + // value returned in LoanPaymentParts.feePaid will never be negative. + Number untrackedManagementFee; + // untrackedInterest includes any fees that go directly to the Vault, such + // as late payment penalty interest. This value may be negative, though the + // final value returned in LoanPaymentParts.interestPaid will never be + // negative. + Number untrackedInterest; + Number totalDue; + + PaymentComponentsPlus( + PaymentComponents const& p, + Number f, + Number v = Number{}) + : PaymentComponents(p) + , untrackedManagementFee(f) + , untrackedInterest(v) + , totalDue( + trackedValueDelta + untrackedInterest + untrackedManagementFee) + { + } +}; + +template +LoanPaymentParts +doPayment( + PaymentComponentsPlus const& payment, + NumberProxy& totalValueOutstandingProxy, + NumberProxy& principalOutstandingProxy, + NumberProxy& managementFeeOutstandingProxy, + UInt32Proxy& paymentRemainingProxy, + UInt32Proxy& prevPaymentDateProxy, + UInt32OptionalProxy& nextDueDateProxy, + std::uint32_t paymentInterval) +{ + XRPL_ASSERT_PARTS( + nextDueDateProxy, + "ripple::detail::doPayment", + "Next due date proxy set"); + + if (payment.specialCase == PaymentSpecialCase::final) + { + XRPL_ASSERT_PARTS( + principalOutstandingProxy == payment.trackedPrincipalDelta, + "ripple::detail::doPayment", + "Full principal payment"); + XRPL_ASSERT_PARTS( + totalValueOutstandingProxy == payment.trackedValueDelta, + "ripple::detail::doPayment", + "Full value payment"); + XRPL_ASSERT_PARTS( + managementFeeOutstandingProxy == payment.trackedManagementFeeDelta, + "ripple::detail::doPayment", + "Full management fee payment"); + + paymentRemainingProxy = 0; + + prevPaymentDateProxy = *nextDueDateProxy; + // Remove the field. This is the only condition where nextDueDate is + // allowed to be removed. + nextDueDateProxy = std::nullopt; + + // Always zero out the the tracked values on a final payment + principalOutstandingProxy = 0; + totalValueOutstandingProxy = 0; + managementFeeOutstandingProxy = 0; + } + else + { + if (payment.specialCase != PaymentSpecialCase::extra) + { + paymentRemainingProxy -= 1; + + prevPaymentDateProxy = *nextDueDateProxy; + // STObject::OptionalField does not define operator+=, so do it the + // old-fashioned way. + nextDueDateProxy = *nextDueDateProxy + paymentInterval; + } + XRPL_ASSERT_PARTS( + principalOutstandingProxy > payment.trackedPrincipalDelta, + "ripple::detail::doPayment", + "Partial principal payment"); + XRPL_ASSERT_PARTS( + totalValueOutstandingProxy > payment.trackedValueDelta, + "ripple::detail::doPayment", + "Partial value payment"); + // Management fees are expected to be relatively small, and could get to + // zero before the loan is paid off + XRPL_ASSERT_PARTS( + managementFeeOutstandingProxy >= payment.trackedManagementFeeDelta, + "ripple::detail::doPayment", + "Valid management fee"); + + principalOutstandingProxy -= payment.trackedPrincipalDelta; + totalValueOutstandingProxy -= payment.trackedValueDelta; + managementFeeOutstandingProxy -= payment.trackedManagementFeeDelta; + } + + XRPL_ASSERT_PARTS( + // Use an explicit cast because the template parameter can be + // ValueProxy or Number + static_cast(principalOutstandingProxy) <= + static_cast(totalValueOutstandingProxy), + "ripple::detail::doPayment", + "principal does not exceed total"); + XRPL_ASSERT_PARTS( + // Use an explicit cast because the template parameter can be + // ValueProxy or Number + static_cast(managementFeeOutstandingProxy) >= beast::zero, + "ripple::detail::doPayment", + "fee outstanding stays valid"); + + return LoanPaymentParts{ + .principalPaid = payment.trackedPrincipalDelta, + // Now that the Loan object has been updated, the tracked interest + // (computed here) and untracked interest can be combined. + .interestPaid = + payment.trackedInterestPart() + payment.untrackedInterest, + .valueChange = payment.untrackedInterest, + // Now that the Loan object has been updated, the fee parts can be + // combined + .feePaid = + payment.trackedManagementFeeDelta + payment.untrackedManagementFee}; +} + +// This function mainly exists to guarantee isolation of the "sandbox" +// variables from the real / proxy variables that will affect actual +// ledger data in the caller. + +Expected +tryOverpayment( + Asset const& asset, + std::int32_t loanScale, + PaymentComponentsPlus const& overpaymentComponents, + Number& totalValueOutstanding, + Number& principalOutstanding, + Number& managementFeeOutstanding, + Number& periodicPayment, + TenthBips32 interestRate, + std::uint32_t paymentInterval, + Number const& periodicRate, + std::uint32_t paymentRemaining, + std::uint32_t prevPaymentDate, + std::optional nextDueDate, + TenthBips16 const managementFeeRate, + beast::Journal j) +{ + auto const raw = calculateRawLoanState( + periodicPayment, periodicRate, paymentRemaining, managementFeeRate); + auto const rounded = calculateRoundedLoanState( + totalValueOutstanding, principalOutstanding, managementFeeOutstanding); + + auto const totalValueError = totalValueOutstanding - raw.valueOutstanding; + auto const principalError = principalOutstanding - raw.principalOutstanding; + auto const feeError = managementFeeOutstanding - raw.managementFeeDue; + + auto const newRawPrincipal = + raw.principalOutstanding - overpaymentComponents.trackedPrincipalDelta; + + auto newLoanProperties = computeLoanProperties( + asset, + newRawPrincipal, + interestRate, + paymentInterval, + paymentRemaining, + managementFeeRate); + + auto const newRaw = calculateRawLoanState( + newLoanProperties.periodicPayment, + periodicRate, + paymentRemaining, + managementFeeRate); + + totalValueOutstanding = roundToAsset( + asset, newRaw.valueOutstanding + totalValueError, loanScale); + principalOutstanding = roundToAsset( + asset, + newRaw.principalOutstanding + principalError, + loanScale, + Number::downward); + managementFeeOutstanding = + roundToAsset(asset, newRaw.managementFeeDue + feeError, loanScale); + + periodicPayment = newLoanProperties.periodicPayment; + + // check that the loan is still valid + if (newLoanProperties.firstPaymentPrincipal <= 0 && + principalOutstanding > 0) + { + // The overpayment has caused the loan to be in a state + // where no further principal can be paid. + JLOG(j.warn()) + << "Loan overpayment would cause loan to be stuck. " + "Rejecting overpayment, but normal payments are unaffected."; + return Unexpected(tesSUCCESS); + } + + // Check that the other computed values are valid + if (newLoanProperties.periodicPayment <= 0 || + newLoanProperties.totalValueOutstanding <= 0 || + newLoanProperties.managementFeeOwedToBroker < 0) + { + // LCOV_EXCL_START + JLOG(j.warn()) << "Overpayment not allowed: Computed loan " + "properties are invalid. Does " + "not compute. TotalValueOutstanding: " + << newLoanProperties.totalValueOutstanding + << ", PeriodicPayment : " + << newLoanProperties.periodicPayment + << ", ManagementFeeOwedToBroker: " + << newLoanProperties.managementFeeOwedToBroker; + return Unexpected(tesSUCCESS); + // LCOV_EXCL_STOP + } + + auto const newRounded = calculateRoundedLoanState( + totalValueOutstanding, principalOutstanding, managementFeeOutstanding); + auto const valueChange = + newRounded.interestOutstanding - rounded.interestOutstanding; + XRPL_ASSERT_PARTS( + valueChange < beast::zero, + "ripple::detail::tryOverpayment", + "principal overpayment reduced value of loan"); + + return LoanPaymentParts{ + .principalPaid = + rounded.principalOutstanding - newRounded.principalOutstanding, + .interestPaid = rounded.interestDue - newRounded.interestDue, + .valueChange = valueChange + overpaymentComponents.untrackedInterest, + .feePaid = rounded.managementFeeDue - newRounded.managementFeeDue + + overpaymentComponents.untrackedManagementFee}; +} + +template +Expected +doOverpayment( + Asset const& asset, + std::int32_t loanScale, + PaymentComponentsPlus const& overpaymentComponents, + NumberProxy& totalValueOutstandingProxy, + NumberProxy& principalOutstandingProxy, + NumberProxy& managementFeeOutstandingProxy, + NumberProxy& periodicPaymentProxy, + TenthBips32 const interestRate, + std::uint32_t const paymentInterval, + Number const& periodicRate, + std::uint32_t const paymentRemaining, + std::uint32_t const prevPaymentDate, + std::optional const nextDueDate, + TenthBips16 const managementFeeRate, + beast::Journal j) +{ + // Use temp variables to do the payment, so they can be thrown away if + // they don't work + Number totalValueOutstanding = totalValueOutstandingProxy; + Number principalOutstanding = principalOutstandingProxy; + Number managementFeeOutstanding = managementFeeOutstandingProxy; + Number periodicPayment = periodicPaymentProxy; + + auto const ret = tryOverpayment( + asset, + loanScale, + overpaymentComponents, + totalValueOutstanding, + principalOutstanding, + managementFeeOutstanding, + periodicPayment, + interestRate, + paymentInterval, + periodicRate, + paymentRemaining, + prevPaymentDate, + nextDueDate, + managementFeeRate, + j); + if (!ret) + return Unexpected(ret.error()); + + auto const& loanPaymentParts = *ret; + + if (principalOutstandingProxy <= principalOutstanding) + { + // LCOV_EXCL_START + JLOG(j.warn()) << "Overpayment not allowed: principal " + << "outstanding did not decrease. Before: " + << *principalOutstandingProxy + << ". After: " << principalOutstanding; + return Unexpected(tesSUCCESS); + // LCOV_EXCL_STOP + } + + // We haven't updated the proxies yet, so they still have the original + // values. Use those to do some checks. + XRPL_ASSERT_PARTS( + overpaymentComponents.trackedPrincipalDelta == + principalOutstandingProxy - principalOutstanding, + "ripple::detail::doOverpayment", + "principal change agrees"); + + XRPL_ASSERT_PARTS( + overpaymentComponents.trackedManagementFeeDelta == + managementFeeOutstandingProxy - managementFeeOutstanding, + "ripple::detail::doOverpayment", + "no fee change"); + + XRPL_ASSERT_PARTS( + overpaymentComponents.untrackedInterest == + totalValueOutstandingProxy - totalValueOutstanding - + overpaymentComponents.trackedPrincipalDelta, + "ripple::detail::doOverpayment", + "value change agrees"); + + XRPL_ASSERT_PARTS( + overpaymentComponents.trackedPrincipalDelta == + loanPaymentParts.principalPaid, + "ripple::detail::doOverpayment", + "principal payment matches"); + + XRPL_ASSERT_PARTS( + loanPaymentParts.feePaid == + overpaymentComponents.untrackedManagementFee + + overpaymentComponents.trackedManagementFeeDelta, + "ripple::detail::doOverpayment", + "fee payment matches"); + + // Update the loan object (via proxies) + totalValueOutstandingProxy = totalValueOutstanding; + principalOutstandingProxy = principalOutstanding; + managementFeeOutstandingProxy = managementFeeOutstanding; + periodicPaymentProxy = periodicPayment; + + return loanPaymentParts; +} + +std::pair +computeInterestAndFeeParts( + Asset const& asset, + Number const& interest, + TenthBips16 managementFeeRate, + std::int32_t loanScale) +{ + auto const fee = computeFee(asset, interest, managementFeeRate, loanScale); + + return std::make_pair(interest - fee, fee); +} + +/** Handle possible late payments. + * + * If this function processed a late payment, the return value will be + * a LoanPaymentParts object. If the loan is not late, the return will be an + * Unexpected(tesSUCCESS). Otherwise, it'll be an Unexpected with the error code + * the caller is expected to return. + * + * + * This function is an implementation of the XLS-66 spec, based on + * * section 3.2.4.3 (Transaction Pseudo-code), specifically the bit + * labeled "the payment is late" + * * section 3.2.4.1.2 (Late Payment) + */ + +Expected +computeLatePayment( + Asset const& asset, + ApplyView const& view, + Number const& principalOutstanding, + std::int32_t nextDueDate, + PaymentComponentsPlus const& periodic, + TenthBips32 lateInterestRate, + std::int32_t loanScale, + Number const& latePaymentFee, + STAmount const& amount, + TenthBips16 managementFeeRate, + beast::Journal j) +{ + if (!hasExpired(view, nextDueDate)) + return Unexpected(tesSUCCESS); + + // the payment is late + // Late payment interest is only the part of the interest that comes + // from being late, as computed by 3.2.4.1.2. + auto const latePaymentInterest = loanLatePaymentInterest( + principalOutstanding, + lateInterestRate, + view.parentCloseTime(), + nextDueDate); + + auto const [rawLateInterest, rawLateManagementFee] = + computeInterestAndFeeParts(latePaymentInterest, managementFeeRate); + auto const [roundedLateInterest, roundedLateManagementFee] = [&]() { + auto const interest = + roundToAsset(asset, latePaymentInterest, loanScale); + return computeInterestAndFeeParts( + asset, interest, managementFeeRate, loanScale); + }(); + + XRPL_ASSERT( + roundedLateInterest >= 0, + "ripple::detail::computeLatePayment : valid late interest"); + XRPL_ASSERT_PARTS( + periodic.specialCase != PaymentSpecialCase::extra, + "ripple::detail::computeLatePayment", + "no extra parts to this payment"); + // Copy the periodic payment values, and add on the late interest. + // This preserves all the other fields without having to enumerate them. + PaymentComponentsPlus const late = [&]() { + auto inner = periodic; + inner.rawInterest += rawLateInterest; + + return PaymentComponentsPlus{ + inner, + // A late payment pays both the normal fee, and the extra fees + periodic.untrackedManagementFee + latePaymentFee + + roundedLateManagementFee, + // A late payment increases the value of the loan by the difference + // between periodic and late payment interest + periodic.untrackedInterest + roundedLateInterest}; + }(); + + XRPL_ASSERT_PARTS( + isRounded(asset, late.totalDue, loanScale), + "ripple::detail::computeLatePayment", + "total due is rounded"); + + if (amount < late.totalDue) + { + JLOG(j.warn()) << "Late loan payment amount is insufficient. Due: " + << late.totalDue << ", paid: " << amount; + return Unexpected(tecINSUFFICIENT_PAYMENT); + } + + return late; +} + +/* Handle possible full payments. + * + * If this function processed a full payment, the return value will be + * a PaymentComponentsPlus object. Otherwise, it'll be an Unexpected with the + * error code the caller is expected to return. It should NEVER return + * tesSUCCESS + */ + +Expected +computeFullPayment( + Asset const& asset, + ApplyView& view, + Number const& principalOutstanding, + Number const& managementFeeOutstanding, + Number const& periodicPayment, + std::uint32_t paymentRemaining, + std::uint32_t prevPaymentDate, + std::uint32_t const startDate, + std::uint32_t const paymentInterval, + TenthBips32 const closeInterestRate, + std::int32_t loanScale, + Number const& totalInterestOutstanding, + Number const& periodicRate, + Number const& closePaymentFee, + STAmount const& amount, + TenthBips16 managementFeeRate, + beast::Journal j) +{ + if (paymentRemaining <= 1) + // If this is the last payment, it has to be a regular payment + return Unexpected(tecKILLED); + + Number const rawPrincipalOutstanding = loanPrincipalFromPeriodicPayment( + periodicPayment, periodicRate, paymentRemaining); + + // Full payment interest consists of accrued normal interest and the + // prepayment penalty, as computed by 3.2.4.1.4. + auto const fullPaymentInterest = calculateFullPaymentInterest( + rawPrincipalOutstanding, + periodicRate, + view.parentCloseTime(), + paymentInterval, + prevPaymentDate, + startDate, + closeInterestRate); + + auto const [rawFullInterest, rawFullManagementFee] = + computeInterestAndFeeParts(fullPaymentInterest, managementFeeRate); + + auto const [roundedFullInterest, roundedFullManagementFee] = [&]() { + auto const interest = + roundToAsset(asset, fullPaymentInterest, loanScale); + auto const parts = computeInterestAndFeeParts( + asset, interest, managementFeeRate, loanScale); + // Apply as much of the fee to the outstanding fee, but no + // more + return std::make_tuple(parts.first, parts.second); + }(); + + PaymentComponentsPlus const full{ + PaymentComponents{ + .rawInterest = rawFullInterest, + .rawPrincipal = rawPrincipalOutstanding, + .rawManagementFee = rawFullManagementFee, + .trackedValueDelta = principalOutstanding + + totalInterestOutstanding + managementFeeOutstanding, + .trackedPrincipalDelta = principalOutstanding, + // to make the accounting work later, the tracked part of the fee + // must be paid in full + .trackedManagementFeeDelta = managementFeeOutstanding, + .specialCase = PaymentSpecialCase::final}, + // A full payment pays the single close payment fee, plus the computed + // management fee part of the interest portion, but for tracking, the + // outstanding part is removed. That could make this value negative, but + // that's ok, because it's not used until it's recombined with + // roundedManagementFee. + closePaymentFee + roundedFullManagementFee - managementFeeOutstanding, + // A full payment changes the value of the loan by the difference + // between expected outstanding interest return and the actual interest + // paid. This value can be positive (increasing the value) or negative + // (decreasing the value). + roundedFullInterest - totalInterestOutstanding}; + + XRPL_ASSERT_PARTS( + isRounded(asset, full.totalDue, loanScale), + "ripple::detail::computeFullPayment", + "total due is rounded"); + + if (amount < full.totalDue) + // If the payment is less than the full payment amount, it's not + // sufficient to be a full payment, but that's not an error. + return Unexpected(tecINSUFFICIENT_PAYMENT); + + return full; +} + +Number +PaymentComponents::trackedInterestPart() const +{ + return trackedValueDelta - + (trackedPrincipalDelta + trackedManagementFeeDelta); +} + +PaymentComponents +computePaymentComponents( + Asset const& asset, + std::int32_t scale, + Number const& totalValueOutstanding, + Number const& principalOutstanding, + Number const& managementFeeOutstanding, + Number const& periodicPayment, + Number const& periodicRate, + std::uint32_t paymentRemaining, + TenthBips16 managementFeeRate) +{ + /* + * This function is derived from the XLS-66 spec, section 3.2.4.1.1 (Regular + * Payment) + */ + XRPL_ASSERT_PARTS( + isRounded(asset, totalValueOutstanding, scale) && + isRounded(asset, principalOutstanding, scale) && + isRounded(asset, managementFeeOutstanding, scale), + "ripple::detail::computePaymentComponents", + "Outstanding values are rounded"); + auto const roundedPeriodicPayment = + roundPeriodicPayment(asset, periodicPayment, scale); + + LoanState const raw = calculateRawLoanState( + periodicPayment, periodicRate, paymentRemaining, managementFeeRate); + + if (paymentRemaining == 1 || + totalValueOutstanding <= roundedPeriodicPayment) + { + // If there's only one payment left, we need to pay off each of the loan + // parts. It's probably impossible for the subtraction to result in a + // negative value, but don't leave anything to chance. + Number interest = std::max( + Number{}, + totalValueOutstanding - principalOutstanding - + managementFeeOutstanding); + + // Pay everything off + return PaymentComponents{ + .rawInterest = raw.interestOutstanding, + .rawPrincipal = raw.principalOutstanding, + .rawManagementFee = raw.managementFeeDue, + .trackedValueDelta = + interest + principalOutstanding + managementFeeOutstanding, + .trackedPrincipalDelta = principalOutstanding, + .trackedManagementFeeDelta = managementFeeOutstanding, + .specialCase = PaymentSpecialCase::final}; + } + + /* + * From the spec, once the periodicPayment is computed: + * + * The principal and interest portions can be derived as follows: + * interest = principalOutstanding * periodicRate + * principal = periodicPayment - interest + */ + Number const rawInterest = raw.principalOutstanding * periodicRate; + Number const rawPrincipal = periodicPayment - rawInterest; + Number const rawFee = tenthBipsOfValue(rawInterest, managementFeeRate); + XRPL_ASSERT_PARTS( + rawInterest >= 0, + "ripple::detail::computePaymentComponents", + "valid raw interest"); + XRPL_ASSERT_PARTS( + rawPrincipal >= 0 && rawPrincipal <= raw.principalOutstanding, + "ripple::detail::computePaymentComponents", + "valid raw principal"); + XRPL_ASSERT_PARTS( + rawFee >= 0 && rawFee <= raw.managementFeeDue, + "ripple::detail::computePaymentComponents", + "valid raw fee"); + + /* + Critical Calculation: Balancing Principal and Interest Outstanding + + This calculation maintains a delicate balance between keeping + principal outstanding and interest outstanding as close as possible to + reference values. However, we cannot perfectly match the reference + values due to rounding issues. + + Key considerations: + 1. Since the periodic payment is rounded up, we have excess funds + that can be used to pay down the loan faster than the reference + calculation. + + 2. We must ensure that loan repayment is not too fast, otherwise we + will end up with negative principal outstanding or negative + interest outstanding. + + 3. We cannot allow the borrower to repay interest ahead of schedule. + If the borrower makes an overpayment, the interest portion could + go negative, requiring complex recalculation to refund the borrower by + reflecting the overpayment in the principal portion of the loan. + */ + + Number const roundedPrincipal = detail::computeRoundedPrincipalComponent( + asset, + principalOutstanding, + raw.principalOutstanding, + rawPrincipal, + roundedPeriodicPayment, + scale); + + auto const [roundedInterest, roundedFee] = + detail::computeRoundedInterestAndFeeComponents( + asset, + totalValueOutstanding - principalOutstanding, + managementFeeOutstanding, + roundedPrincipal, + raw.interestOutstanding, + raw.managementFeeDue, + roundedPeriodicPayment, + periodicRate, + managementFeeRate, + scale); + + XRPL_ASSERT_PARTS( + roundedInterest >= 0 && isRounded(asset, roundedInterest, scale), + "ripple::detail::computePaymentComponents", + "valid rounded interest"); + XRPL_ASSERT_PARTS( + roundedFee >= 0 && roundedFee <= managementFeeOutstanding && + isRounded(asset, roundedFee, scale), + "ripple::detail::computePaymentComponents", + "valid rounded fee"); + XRPL_ASSERT_PARTS( + roundedPrincipal >= 0 && roundedPrincipal <= principalOutstanding && + roundedPrincipal <= roundedPeriodicPayment && + isRounded(asset, roundedPrincipal, scale), + "ripple::detail::computePaymentComponents", + "valid rounded principal"); + XRPL_ASSERT_PARTS( + roundedPrincipal + roundedInterest + roundedFee <= + roundedPeriodicPayment, + "ripple::detail::computePaymentComponents", + "payment parts fit within payment limit"); + + return PaymentComponents{ + .rawInterest = rawInterest - rawFee, + .rawPrincipal = rawPrincipal, + .rawManagementFee = rawFee, + .trackedValueDelta = roundedInterest + roundedPrincipal + roundedFee, + .trackedPrincipalDelta = roundedPrincipal, + .trackedManagementFeeDelta = roundedFee, + }; +} + } // namespace detail +Number +calculateFullPaymentInterest( + Number const& rawPrincipalOutstanding, + Number const& periodicRate, + NetClock::time_point parentCloseTime, + std::uint32_t paymentInterval, + std::uint32_t prevPaymentDate, + std::uint32_t startDate, + TenthBips32 closeInterestRate) +{ + // If there is more than one payment remaining, see if enough was + // paid for a full payment + auto const accruedInterest = detail::loanAccruedInterest( + rawPrincipalOutstanding, + periodicRate, + parentCloseTime, + startDate, + prevPaymentDate, + paymentInterval); + XRPL_ASSERT( + accruedInterest >= 0, + "ripple::detail::computeFullPaymentInterest : valid accrued interest"); + + auto const prepaymentPenalty = + tenthBipsOfValue(rawPrincipalOutstanding, closeInterestRate); + XRPL_ASSERT( + prepaymentPenalty >= 0, + "ripple::detail::computeFullPaymentInterest : valid prepayment " + "interest"); + + return accruedInterest + prepaymentPenalty; +} + +Number +calculateFullPaymentInterest( + Number const& periodicPayment, + Number const& periodicRate, + std::uint32_t paymentRemaining, + NetClock::time_point parentCloseTime, + std::uint32_t paymentInterval, + std::uint32_t prevPaymentDate, + std::uint32_t startDate, + TenthBips32 closeInterestRate) +{ + Number const rawPrincipalOutstanding = + detail::loanPrincipalFromPeriodicPayment( + periodicPayment, periodicRate, paymentRemaining); + + return calculateFullPaymentInterest( + rawPrincipalOutstanding, + periodicRate, + parentCloseTime, + paymentInterval, + prevPaymentDate, + startDate, + closeInterestRate); +} + +LoanState +calculateRawLoanState( + Number const& periodicPayment, + Number const& periodicRate, + std::uint32_t const paymentRemaining, + TenthBips16 const managementFeeRate) +{ + Number const rawValueOutstanding = periodicPayment * paymentRemaining; + Number const rawPrincipalOutstanding = + detail::loanPrincipalFromPeriodicPayment( + periodicPayment, periodicRate, paymentRemaining); + Number const rawInterestOutstanding = + rawValueOutstanding - rawPrincipalOutstanding; + Number const rawManagementFeeOutstanding = + tenthBipsOfValue(rawInterestOutstanding, managementFeeRate); + + return LoanState{ + .valueOutstanding = rawValueOutstanding, + .principalOutstanding = rawPrincipalOutstanding, + .interestOutstanding = rawInterestOutstanding, + .interestDue = rawInterestOutstanding - rawManagementFeeOutstanding, + .managementFeeDue = rawManagementFeeOutstanding}; +}; + +LoanState +calculateRawLoanState( + Number const& periodicPayment, + TenthBips32 interestRate, + std::uint32_t paymentInterval, + std::uint32_t const paymentRemaining, + TenthBips16 const managementFeeRate) +{ + return calculateRawLoanState( + periodicPayment, + loanPeriodicRate(interestRate, paymentInterval), + paymentRemaining, + managementFeeRate); +} + +LoanState +calculateRoundedLoanState( + Number const& totalValueOutstanding, + Number const& principalOutstanding, + Number const& managementFeeOutstanding) +{ + // This implementation is pretty trivial, but ensures the calculations are + // consistent everywhere, and reduces copy/paste errors. + Number const interestOutstanding = + totalValueOutstanding - principalOutstanding; + return { + .valueOutstanding = totalValueOutstanding, + .principalOutstanding = principalOutstanding, + .interestOutstanding = interestOutstanding, + .interestDue = interestOutstanding - managementFeeOutstanding, + .managementFeeDue = managementFeeOutstanding}; +} + +LoanState +calculateRoundedLoanState(SLE::const_ref loan) +{ + return calculateRoundedLoanState( + loan->at(sfTotalValueOutstanding), + loan->at(sfPrincipalOutstanding), + loan->at(sfManagementFeeOutstanding)); +} + +Number +computeFee( + Asset const& asset, + Number const& value, + TenthBips16 managementFeeRate, + std::int32_t scale) +{ + return roundToAsset( + asset, + tenthBipsOfValue(value, managementFeeRate), + scale, + Number::downward); +} + +Number +valueMinusFee( + Asset const& asset, + Number const& value, + TenthBips16 managementFeeRate, + std::int32_t scale) +{ + return value - computeFee(asset, value, managementFeeRate, scale); +} + +LoanProperties +computeLoanProperties( + Asset const& asset, + Number principalOutstanding, + TenthBips32 interestRate, + std::uint32_t paymentInterval, + std::uint32_t paymentsRemaining, + TenthBips16 managementFeeRate) +{ + auto const periodicRate = loanPeriodicRate(interestRate, paymentInterval); + XRPL_ASSERT( + interestRate == 0 || periodicRate > 0, + "ripple::computeLoanProperties : valid rate"); + + auto const periodicPayment = detail::loanPeriodicPayment( + principalOutstanding, periodicRate, paymentsRemaining); + STAmount const totalValueOutstanding = [&]() { + NumberRoundModeGuard mg(Number::to_nearest); + // Use STAmount's internal rounding instead of roundToAsset, because + // we're going to use this result to determine the scale for all the + // other rounding. + return STAmount{ + asset, + /* + * This formula is from the XLS-66 spec, section 3.2.4.2 (Total + * Loan Value Calculation), specifically "totalValueOutstanding + * = ..." + */ + periodicPayment * paymentsRemaining}; + }(); + // Base the loan scale on the total value, since that's going to be the + // biggest number involved (barring unusual parameters for late, full, or + // over payments) + auto const loanScale = totalValueOutstanding.exponent(); + XRPL_ASSERT_PARTS( + (totalValueOutstanding.integral() && loanScale == 0) || + (!totalValueOutstanding.integral() && + loanScale == + static_cast(totalValueOutstanding).exponent()), + "ripple::computeLoanProperties", + "loanScale value fits expectations"); + + // Since we just figured out the loan scale, we haven't been able to + // validate that the principal fits in it, so to allow this function to + // succeed, round it here, and let the caller do the validation. + principalOutstanding = roundToAsset( + asset, principalOutstanding, loanScale, Number::to_nearest); + + auto const feeOwedToBroker = computeFee( + asset, + /* + * This formula is from the XLS-66 spec, section 3.2.4.2 (Total Loan + * Value Calculation), specifically "totalInterestOutstanding = ..." + */ + totalValueOutstanding - principalOutstanding, + managementFeeRate, + loanScale); + + auto const firstPaymentPrincipal = [&]() { + // Compute the parts for the first payment. Ensure that the + // principal payment will actually change the principal. + auto const paymentComponents = detail::computePaymentComponents( + asset, + loanScale, + totalValueOutstanding, + principalOutstanding, + feeOwedToBroker, + periodicPayment, + periodicRate, + paymentsRemaining, + managementFeeRate); + + // The unrounded principal part needs to be large enough to affect the + // principal. What to do if not is left to the caller + return paymentComponents.rawPrincipal; + }(); + + return LoanProperties{ + .periodicPayment = periodicPayment, + .totalValueOutstanding = totalValueOutstanding, + .managementFeeOwedToBroker = feeOwedToBroker, + .loanScale = loanScale, + .firstPaymentPrincipal = firstPaymentPrincipal}; +} + +Expected +loanMakeFullPayment( + Asset const& asset, + ApplyView& view, + SLE::ref loan, + SLE::const_ref brokerSle, + STAmount const& amount, + bool const overpaymentAllowed, + beast::Journal j) +{ + auto principalOutstandingProxy = loan->at(sfPrincipalOutstanding); + auto paymentRemainingProxy = loan->at(sfPaymentRemaining); + + if (paymentRemainingProxy == 0 || principalOutstandingProxy == 0) + { + // Loan complete + JLOG(j.warn()) << "Loan is already paid off."; + return Unexpected(tecKILLED); + } + + auto totalValueOutstandingProxy = loan->at(sfTotalValueOutstanding); + auto managementFeeOutstandingProxy = loan->at(sfManagementFeeOutstanding); + + // Next payment due date must be set unless the loan is complete + auto nextDueDateProxy = loan->at(~sfNextPaymentDueDate); + if (!nextDueDateProxy) + { + JLOG(j.warn()) << "Loan next payment due date is not set."; + return Unexpected(tecINTERNAL); + } + + std::int32_t const loanScale = loan->at(sfLoanScale); + + TenthBips32 const interestRate{loan->at(sfInterestRate)}; + TenthBips32 const closeInterestRate{loan->at(sfCloseInterestRate)}; + + Number const closePaymentFee = + roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale); + TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; + + auto const periodicPayment = loan->at(sfPeriodicPayment); + + auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDate); + std::uint32_t const startDate = loan->at(sfStartDate); + + std::uint32_t const paymentInterval = loan->at(sfPaymentInterval); + // Compute the normal periodic rate, payment, etc. + // We'll need it in the remaining calculations + Number const periodicRate = loanPeriodicRate(interestRate, paymentInterval); + XRPL_ASSERT( + interestRate == 0 || periodicRate > 0, + "ripple::loanMakeFullPayment : valid rate"); + + XRPL_ASSERT( + *totalValueOutstandingProxy > 0, + "ripple::loanMakeFullPayment : valid total value"); + + view.update(loan); + + // ------------------------------------------------------------- + // full payment handling + LoanState const roundedLoanState = calculateRoundedLoanState( + totalValueOutstandingProxy, + principalOutstandingProxy, + managementFeeOutstandingProxy); + + if (auto const fullPaymentComponents = detail::computeFullPayment( + asset, + view, + principalOutstandingProxy, + managementFeeOutstandingProxy, + periodicPayment, + paymentRemainingProxy, + prevPaymentDateProxy, + startDate, + paymentInterval, + closeInterestRate, + loanScale, + roundedLoanState.interestDue, + periodicRate, + closePaymentFee, + amount, + managementFeeRate, + j)) + return doPayment( + *fullPaymentComponents, + totalValueOutstandingProxy, + principalOutstandingProxy, + managementFeeOutstandingProxy, + paymentRemainingProxy, + prevPaymentDateProxy, + nextDueDateProxy, + paymentInterval); + else if (fullPaymentComponents.error()) + // error() will be the TER returned if a payment is not made. It + // will only evaluate to true if it's unsuccessful. Otherwise, + // tesSUCCESS means nothing was done, so continue. + return Unexpected(fullPaymentComponents.error()); + + // LCOV_EXCL_START + UNREACHABLE("ripple::loanMakeFullPayment : invalid result"); + return Unexpected(tecINTERNAL); + // LCOV_EXCL_STOP +} + +Expected +loanMakePayment( + Asset const& asset, + ApplyView& view, + SLE::ref loan, + SLE::const_ref brokerSle, + STAmount const& amount, + bool const overpaymentAllowed, + beast::Journal j) +{ + /* + * This function is an implementation of the XLS-66 spec, + * section 3.2.4.3 (Transaction Pseudo-code) + */ + auto principalOutstandingProxy = loan->at(sfPrincipalOutstanding); + auto paymentRemainingProxy = loan->at(sfPaymentRemaining); + + if (paymentRemainingProxy == 0 || principalOutstandingProxy == 0) + { + // Loan complete + // This is already checked in LoanPay::preclaim() + // LCOV_EXCL_START + JLOG(j.warn()) << "Loan is already paid off."; + return Unexpected(tecKILLED); + // LCOV_EXCL_STOP + } + + auto totalValueOutstandingProxy = loan->at(sfTotalValueOutstanding); + auto managementFeeOutstandingProxy = loan->at(sfManagementFeeOutstanding); + + // Next payment due date must be set unless the loan is complete + auto nextDueDateProxy = loan->at(~sfNextPaymentDueDate); + if (!nextDueDateProxy) + { + JLOG(j.warn()) << "Loan next payment due date is not set."; + return Unexpected(tecINTERNAL); + } + + std::int32_t const loanScale = loan->at(sfLoanScale); + + TenthBips32 const interestRate{loan->at(sfInterestRate)}; + TenthBips32 const lateInterestRate{loan->at(sfLateInterestRate)}; + TenthBips32 const closeInterestRate{loan->at(sfCloseInterestRate)}; + + Number const serviceFee = loan->at(sfLoanServiceFee); + Number const latePaymentFee = loan->at(sfLatePaymentFee); + Number const closePaymentFee = + roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale); + TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; + + auto const periodicPayment = loan->at(sfPeriodicPayment); + + auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDate); + + std::uint32_t const paymentInterval = loan->at(sfPaymentInterval); + // Compute the normal periodic rate, payment, etc. + // We'll need it in the remaining calculations + Number const periodicRate = loanPeriodicRate(interestRate, paymentInterval); + XRPL_ASSERT( + interestRate == 0 || periodicRate > 0, + "ripple::loanMakePayment : valid rate"); + + XRPL_ASSERT( + *totalValueOutstandingProxy > 0, + "ripple::loanMakePayment : valid total value"); + + view.update(loan); + + detail::PaymentComponentsPlus const periodic{ + detail::computePaymentComponents( + asset, + loanScale, + totalValueOutstandingProxy, + principalOutstandingProxy, + managementFeeOutstandingProxy, + periodicPayment, + periodicRate, + paymentRemainingProxy, + managementFeeRate), + serviceFee}; + XRPL_ASSERT_PARTS( + periodic.trackedPrincipalDelta >= 0, + "ripple::loanMakePayment", + "regular payment valid principal"); + + // ------------------------------------------------------------- + // late payment handling + if (auto const latePaymentComponents = detail::computeLatePayment( + asset, + view, + principalOutstandingProxy, + *nextDueDateProxy, + periodic, + lateInterestRate, + loanScale, + latePaymentFee, + amount, + managementFeeRate, + j)) + { + return doPayment( + *latePaymentComponents, + totalValueOutstandingProxy, + principalOutstandingProxy, + managementFeeOutstandingProxy, + paymentRemainingProxy, + prevPaymentDateProxy, + nextDueDateProxy, + paymentInterval); + } + else if (latePaymentComponents.error()) + // error() will be the TER returned if a payment is not made. It will + // only evaluate to true if it's unsuccessful. Otherwise, tesSUCCESS + // means nothing was done, so continue. + return Unexpected(latePaymentComponents.error()); + + // ------------------------------------------------------------- + // regular periodic payment handling + + // if the payment is not late nor if it's a full payment, then it must + // be a periodic one, with possible overpayments + + // This will keep a running total of what is actually paid, if the payment + // is sufficient for a single payment + Number totalPaid = periodic.totalDue; + + if (amount < totalPaid) + { + JLOG(j.warn()) << "Periodic loan payment amount is insufficient. Due: " + << totalPaid << ", paid: " << amount; + return Unexpected(tecINSUFFICIENT_PAYMENT); + } + + LoanPaymentParts totalParts = detail::doPayment( + periodic, + totalValueOutstandingProxy, + principalOutstandingProxy, + managementFeeOutstandingProxy, + paymentRemainingProxy, + prevPaymentDateProxy, + nextDueDateProxy, + paymentInterval); + + std::size_t numPayments = 1; + + while (totalPaid < amount && paymentRemainingProxy > 0) + { + // Try to make more payments + detail::PaymentComponentsPlus const nextPayment{ + detail::computePaymentComponents( + asset, + loanScale, + totalValueOutstandingProxy, + principalOutstandingProxy, + managementFeeOutstandingProxy, + periodicPayment, + periodicRate, + paymentRemainingProxy, + managementFeeRate), + serviceFee}; + XRPL_ASSERT_PARTS( + nextPayment.trackedPrincipalDelta >= 0, + "ripple::loanMakePayment", + "additional payment pays non-negative principal"); + XRPL_ASSERT( + nextPayment.rawInterest <= periodic.rawInterest, + "ripple::loanMakePayment : decreasing interest"); + XRPL_ASSERT( + nextPayment.rawPrincipal >= periodic.rawPrincipal, + "ripple::loanMakePayment : increasing principal"); + + if (amount < totalPaid + nextPayment.totalDue) + // We're done making payments. + break; + + totalPaid += nextPayment.totalDue; + totalParts += detail::doPayment( + nextPayment, + totalValueOutstandingProxy, + principalOutstandingProxy, + managementFeeOutstandingProxy, + paymentRemainingProxy, + prevPaymentDateProxy, + nextDueDateProxy, + paymentInterval); + ++numPayments; + + XRPL_ASSERT_PARTS( + (nextPayment.specialCase == detail::PaymentSpecialCase::final) == + (paymentRemainingProxy == 0), + "ripple::loanMakePayment", + "final payment is the final payment"); + } + + XRPL_ASSERT_PARTS( + totalParts.principalPaid + totalParts.interestPaid + + totalParts.feePaid == + totalPaid, + "ripple::loanMakePayment", + "payment parts add up"); + XRPL_ASSERT_PARTS( + totalParts.valueChange == 0, + "ripple::loanMakePayment", + "no value change"); + + // ------------------------------------------------------------- + // overpayment handling + if (overpaymentAllowed && loan->isFlag(lsfLoanOverpayment) && + paymentRemainingProxy > 0 && nextDueDateProxy && totalPaid < amount) + { + TenthBips32 const overpaymentInterestRate{ + loan->at(sfOverpaymentInterestRate)}; + TenthBips32 const overpaymentFeeRate{loan->at(sfOverpaymentFee)}; + + Number const overpayment = amount - totalPaid; + XRPL_ASSERT( + overpayment > 0 && isRounded(asset, overpayment, loanScale), + "ripple::loanMakePayment : valid overpayment amount"); + + Number const fee = roundToAsset( + asset, + tenthBipsOfValue(overpayment, overpaymentFeeRate), + loanScale); + + Number const payment = overpayment - fee; + + auto const [rawOverpaymentInterest, rawOverpaymentManagementFee] = + [&]() { + Number const interest = + tenthBipsOfValue(payment, overpaymentInterestRate); + return detail::computeInterestAndFeeParts( + interest, managementFeeRate); + }(); + auto const + [roundedOverpaymentInterest, roundedOverpaymentManagementFee] = + [&]() { + Number const interest = + roundToAsset(asset, rawOverpaymentInterest, loanScale); + return detail::computeInterestAndFeeParts( + asset, interest, managementFeeRate, loanScale); + }(); + + detail::PaymentComponentsPlus overpaymentComponents{ + detail::PaymentComponents{ + .rawInterest = rawOverpaymentInterest, + .rawPrincipal = payment - rawOverpaymentInterest, + .rawManagementFee = 0, + .trackedValueDelta = payment, + .trackedPrincipalDelta = payment - roundedOverpaymentInterest - + roundedOverpaymentManagementFee, + .trackedManagementFeeDelta = roundedOverpaymentManagementFee, + .specialCase = detail::PaymentSpecialCase::extra}, + fee, + roundedOverpaymentInterest}; + + // Don't process an overpayment if the whole amount (or more!) + // gets eaten by fees and interest. + if (overpaymentComponents.rawPrincipal > 0 && + overpaymentComponents.trackedPrincipalDelta > 0) + { + XRPL_ASSERT_PARTS( + overpaymentComponents.untrackedInterest >= beast::zero, + "ripple::loanMakePayment", + "overpayment penalty did not reduce value of loan"); + // Can't just use `periodicPayment` here, because it might change + auto periodicPaymentProxy = loan->at(sfPeriodicPayment); + if (auto const overResult = detail::doOverpayment( + asset, + loanScale, + overpaymentComponents, + totalValueOutstandingProxy, + principalOutstandingProxy, + managementFeeOutstandingProxy, + periodicPaymentProxy, + interestRate, + paymentInterval, + periodicRate, + paymentRemainingProxy, + prevPaymentDateProxy, + nextDueDateProxy, + managementFeeRate, + j)) + totalParts += *overResult; + else if (overResult.error()) + // error() will be the TER returned if a payment is not made. It + // will only evaluate to true if it's unsuccessful. Otherwise, + // tesSUCCESS means nothing was done, so continue. + return Unexpected(overResult.error()); + } + } + + // Check the final results are rounded, to double-check that the + // intermediate steps were rounded. + XRPL_ASSERT( + isRounded(asset, totalParts.principalPaid, loanScale) && + totalParts.principalPaid >= beast::zero, + "ripple::loanMakePayment : total principal paid is valid"); + XRPL_ASSERT( + isRounded(asset, totalParts.interestPaid, loanScale) && + totalParts.interestPaid >= beast::zero, + "ripple::loanMakePayment : total interest paid is valid"); + XRPL_ASSERT( + isRounded(asset, totalParts.valueChange, loanScale), + "ripple::loanMakePayment : loan value change is valid"); + XRPL_ASSERT( + isRounded(asset, totalParts.feePaid, loanScale) && + totalParts.feePaid >= beast::zero, + "ripple::loanMakePayment : fee paid is valid"); + return totalParts; +} } // namespace ripple From a3b82023d65c59e44c83a0da9f228750d3d8654b Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 24 Oct 2025 16:00:32 -0400 Subject: [PATCH 162/291] refactor: Pull computeOverpaymentComponents into a separate function --- src/xrpld/app/misc/detail/LendingHelpers.cpp | 91 ++++++++++++-------- 1 file changed, 53 insertions(+), 38 deletions(-) diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index 7a74b47207..b0f747d9fb 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -1125,6 +1125,51 @@ computePaymentComponents( }; } +PaymentComponentsPlus +computeOverpaymentComponents( + Asset const& asset, + int32_t const loanScale, + Number const& overpayment, + TenthBips32 const overpaymentInterestRate, + TenthBips32 const overpaymentFeeRate, + TenthBips16 const managementFeeRate) +{ + XRPL_ASSERT( + overpayment > 0 && isRounded(asset, overpayment, loanScale), + "ripple::loanMakePayment : valid overpayment amount"); + + Number const fee = roundToAsset( + asset, tenthBipsOfValue(overpayment, overpaymentFeeRate), loanScale); + + Number const payment = overpayment - fee; + + auto const [rawOverpaymentInterest, rawOverpaymentManagementFee] = [&]() { + Number const interest = + tenthBipsOfValue(payment, overpaymentInterestRate); + return detail::computeInterestAndFeeParts(interest, managementFeeRate); + }(); + auto const [roundedOverpaymentInterest, roundedOverpaymentManagementFee] = + [&]() { + Number const interest = + roundToAsset(asset, rawOverpaymentInterest, loanScale); + return detail::computeInterestAndFeeParts( + asset, interest, managementFeeRate, loanScale); + }(); + + return detail::PaymentComponentsPlus{ + detail::PaymentComponents{ + .rawInterest = rawOverpaymentInterest, + .rawPrincipal = payment - rawOverpaymentInterest, + .rawManagementFee = 0, + .trackedValueDelta = payment, + .trackedPrincipalDelta = payment - roundedOverpaymentInterest - + roundedOverpaymentManagementFee, + .trackedManagementFeeDelta = roundedOverpaymentManagementFee, + .specialCase = detail::PaymentSpecialCase::extra}, + fee, + roundedOverpaymentInterest}; +} + } // namespace detail Number @@ -1680,45 +1725,15 @@ loanMakePayment( TenthBips32 const overpaymentFeeRate{loan->at(sfOverpaymentFee)}; Number const overpayment = amount - totalPaid; - XRPL_ASSERT( - overpayment > 0 && isRounded(asset, overpayment, loanScale), - "ripple::loanMakePayment : valid overpayment amount"); - Number const fee = roundToAsset( - asset, - tenthBipsOfValue(overpayment, overpaymentFeeRate), - loanScale); - - Number const payment = overpayment - fee; - - auto const [rawOverpaymentInterest, rawOverpaymentManagementFee] = - [&]() { - Number const interest = - tenthBipsOfValue(payment, overpaymentInterestRate); - return detail::computeInterestAndFeeParts( - interest, managementFeeRate); - }(); - auto const - [roundedOverpaymentInterest, roundedOverpaymentManagementFee] = - [&]() { - Number const interest = - roundToAsset(asset, rawOverpaymentInterest, loanScale); - return detail::computeInterestAndFeeParts( - asset, interest, managementFeeRate, loanScale); - }(); - - detail::PaymentComponentsPlus overpaymentComponents{ - detail::PaymentComponents{ - .rawInterest = rawOverpaymentInterest, - .rawPrincipal = payment - rawOverpaymentInterest, - .rawManagementFee = 0, - .trackedValueDelta = payment, - .trackedPrincipalDelta = payment - roundedOverpaymentInterest - - roundedOverpaymentManagementFee, - .trackedManagementFeeDelta = roundedOverpaymentManagementFee, - .specialCase = detail::PaymentSpecialCase::extra}, - fee, - roundedOverpaymentInterest}; + detail::PaymentComponentsPlus const overpaymentComponents = + detail::computeOverpaymentComponents( + asset, + loanScale, + overpayment, + overpaymentInterestRate, + overpaymentFeeRate, + managementFeeRate); // Don't process an overpayment if the whole amount (or more!) // gets eaten by fees and interest. From fe4269cf8bcd673407a43f28b9c7c6079571ff5c Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 24 Oct 2025 18:33:57 -0400 Subject: [PATCH 163/291] Avoid negative payments: limit origination fee to the loan principal - Addresses FIND-006 from audit. - Removes the "minimum" check for sfLoanOriginationFee, and replaces it with a "valid range" check with the max value being sfPrincipalRequested. - Reuses the test from the report, with some modifications. - Also adds some more test cases for existing interest rate tests. --- src/test/app/Loan_test.cpp | 108 +++++++++++++++++++++++++++ src/xrpld/app/tx/detail/LoanSet.cpp | 10 +-- src/xrpld/app/tx/detail/Transactor.h | 2 +- 3 files changed, 114 insertions(+), 6 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index d053530236..752d50f104 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -893,12 +893,23 @@ class Loan_test : public beast::unit_test::suite interestRate(maxInterestRate), loanSetFee, ter(tefBAD_AUTH)); + env(set(evan, broker.brokerID, principalRequest), + sig(sfCounterpartySignature, borrower), + interestRate(TenthBips32(0)), + loanSetFee, + ter(tefBAD_AUTH)); // sfInterestRate: too big env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, lender), interestRate(maxInterestRate + 1), loanSetFee, ter(temINVALID)); + // sfInterestRate: too small + env(set(evan, broker.brokerID, principalRequest), + sig(sfCounterpartySignature, lender), + interestRate(TenthBips32(-1)), + loanSetFee, + ter(temINVALID)); // sfLateInterestRate: good value, bad account env(set(evan, broker.brokerID, principalRequest), @@ -906,12 +917,23 @@ class Loan_test : public beast::unit_test::suite lateInterestRate(maxLateInterestRate), loanSetFee, ter(tefBAD_AUTH)); + env(set(evan, broker.brokerID, principalRequest), + sig(sfCounterpartySignature, borrower), + lateInterestRate(TenthBips32(0)), + loanSetFee, + ter(tefBAD_AUTH)); // sfLateInterestRate: too big env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, lender), lateInterestRate(maxLateInterestRate + 1), loanSetFee, ter(temINVALID)); + // sfLateInterestRate: too small + env(set(evan, broker.brokerID, principalRequest), + sig(sfCounterpartySignature, lender), + lateInterestRate(TenthBips32(-1)), + loanSetFee, + ter(temINVALID)); // sfCloseInterestRate: good value, bad account env(set(evan, broker.brokerID, principalRequest), @@ -919,12 +941,22 @@ class Loan_test : public beast::unit_test::suite closeInterestRate(maxCloseInterestRate), loanSetFee, ter(tefBAD_AUTH)); + env(set(evan, broker.brokerID, principalRequest), + sig(sfCounterpartySignature, borrower), + closeInterestRate(TenthBips32(0)), + loanSetFee, + ter(tefBAD_AUTH)); // sfCloseInterestRate: too big env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, lender), closeInterestRate(maxCloseInterestRate + 1), loanSetFee, ter(temINVALID)); + env(set(evan, broker.brokerID, principalRequest), + sig(sfCounterpartySignature, lender), + closeInterestRate(TenthBips32(-1)), + loanSetFee, + ter(temINVALID)); // sfOverpaymentInterestRate: good value, bad account env(set(evan, broker.brokerID, principalRequest), @@ -932,12 +964,22 @@ class Loan_test : public beast::unit_test::suite overpaymentInterestRate(maxOverpaymentInterestRate), loanSetFee, ter(tefBAD_AUTH)); + env(set(evan, broker.brokerID, principalRequest), + sig(sfCounterpartySignature, borrower), + overpaymentInterestRate(TenthBips32(0)), + loanSetFee, + ter(tefBAD_AUTH)); // sfOverpaymentInterestRate: too big env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, lender), overpaymentInterestRate(maxOverpaymentInterestRate + 1), loanSetFee, ter(temINVALID)); + env(set(evan, broker.brokerID, principalRequest), + sig(sfCounterpartySignature, lender), + overpaymentInterestRate(TenthBips32(-1)), + loanSetFee, + ter(temINVALID)); // sfPaymentTotal: good value, bad account env(set(evan, broker.brokerID, principalRequest), @@ -3563,10 +3605,76 @@ class Loan_test : public beast::unit_test::suite }); } + void + testAccountSendMptMinAmountInvariant() + { + // (From FIND-006) + testcase << "LoanSet trigger ripple::accountSendMPT : minimum amount " + "and MPT"; + + using namespace jtx; + using namespace std::chrono_literals; + Env env(*this, all); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1'000'000), issuer, lender, borrower); + env.close(); + + MPTTester mptt{env, issuer, mptInitNoFund}; + mptt.create( + {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); + PrettyAsset const mptAsset = mptt.issuanceID(); + mptt.authorize({.account = lender}); + mptt.authorize({.account = borrower}); + env(pay(issuer, lender, mptAsset(2'000'000))); + env(pay(issuer, borrower, mptAsset(1'000))); + env.close(); + + BrokerInfo broker{createVaultAndBroker(env, mptAsset, lender)}; + + using namespace loan; + + auto const loanSetFee = fee(env.current()->fees().base * 2); + Number const principalRequest{1, 3}; + + auto createJson = env.json( + set(borrower, broker.brokerID, principalRequest), + fee(loanSetFee), + json(sfCounterpartySignature, Json::objectValue)); + + createJson["CloseInterestRate"] = 76671; + createJson["ClosePaymentFee"] = "2061925410"; + createJson["GracePeriod"] = 434; + createJson["InterestRate"] = 50302; + createJson["LateInterestRate"] = 30322; + createJson["LatePaymentFee"] = "294427911"; + createJson["LoanOriginationFee"] = "3250635102"; + createJson["LoanServiceFee"] = "9557386"; + createJson["OverpaymentFee"] = 51249; + createJson["OverpaymentInterestRate"] = 14304; + createJson["PaymentInterval"] = 434; + createJson["PaymentTotal"] = "2891743748"; + createJson["PrincipalRequested"] = "8516.98"; + + auto const brokerStateBefore = + env.le(keylet::loanbroker(broker.brokerID)); + auto const loanSequence = brokerStateBefore->at(sfLoanSequence); + auto const keylet = keylet::loan(broker.brokerID, loanSequence); + + createJson = env.json(createJson, sig(sfCounterpartySignature, lender)); + env(createJson, ter(temINVALID)); + env.close(); + } + public: void run() override { + testAccountSendMptMinAmountInvariant(); + testIssuerLoan(); testDisabled(); testSelfLoan(); diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 4f69e3c5a8..e207d2089a 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -76,15 +76,15 @@ LoanSet::preflight(PreflightContext const& ctx) !validDataLength(tx[~sfData], maxDataPayloadLength)) return temINVALID; for (auto const& field : - {&sfLoanOriginationFee, - &sfLoanServiceFee, - &sfLatePaymentFee, - &sfClosePaymentFee}) + {&sfLoanServiceFee, &sfLatePaymentFee, &sfClosePaymentFee}) { if (!validNumericMinimum(tx[~*field])) return temINVALID; } - if (auto const p = tx[~sfPrincipalRequested]; p && p <= 0) + // Principal Requested is required + if (auto const p = tx[sfPrincipalRequested]; p <= 0) + return temINVALID; + else if (!validNumericRange(tx[~sfLoanOriginationFee], p)) return temINVALID; if (!validNumericRange(tx[~sfInterestRate], maxInterestRate)) return temINVALID; diff --git a/src/xrpld/app/tx/detail/Transactor.h b/src/xrpld/app/tx/detail/Transactor.h index 044f604a11..0e67ba70c4 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -307,7 +307,7 @@ protected: template static bool - validNumericRange(std::optional value, T max, T min = {}); + validNumericRange(std::optional value, T max, T min = T{}); template static bool From 40ef91e7e08d10f0bdf4300cf93856159b3d9584 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sun, 26 Oct 2025 12:56:13 -0400 Subject: [PATCH 164/291] Limit how many payments can be made in a single LoanPay - Addresses FIND-005 from audit. - Tuning values defined in Protocol.h. Optimal values TBD. - loanPaymentsPerFeeIncrement: calculateBaseFee estimates the number of payments included in the Amount and charges "baseFee * number / loanPaymentsPerFeeIncrement". - loanMaximumPaymentsPerTransaction: If the number of payments (including overpayments if applicable) hits this limit, stop processing more payments, but DO NOT FAIL. - Fix the rounding in LoanSet for Guard 4 (sufficient computed payments) - Tweak several test parameters to account for the new limits. - Change payment component rounding for IOUs to "towards_zero". - Add some safety limits to loan calculations to prevent nonsensical values. --- include/xrpl/protocol/Protocol.h | 35 +++ src/test/app/Loan_test.cpp | 223 ++++++++++++++----- src/xrpld/app/misc/detail/LendingHelpers.cpp | 61 +++-- src/xrpld/app/tx/detail/LoanPay.cpp | 39 ++-- src/xrpld/app/tx/detail/LoanSet.cpp | 27 +-- 5 files changed, 278 insertions(+), 107 deletions(-) diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 503d5a5693..95f654e5f1 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -177,6 +177,41 @@ static_assert(maxCloseInterestRate == TenthBips32(100'000u)); TenthBips32 constexpr maxOverpaymentInterestRate = percentageToTenthBips(100); static_assert(maxOverpaymentInterestRate == TenthBips32(100'000u)); +/** LoanPay transaction cost will be one base fee per X combined payments + * + * The number of payments is estimated based on the Amount paid and the Loan's + * Fixed Payment size. Overpayments (indicated with the tfLoanOverpayment flag) + * count as one more payment. + * + * This number was chosen arbitrarily, but should not be changed once released + * without an amendment + */ +static constexpr int loanPaymentsPerFeeIncrement = 5; + +/** Maximum number of combined payments that a LoanPay transaction will process + * + * This limit is enforced during the loan payment process, and thus is not + * estimated. If the limit is hit, no further payments or overpayments will be + * processed, no matter how much of the transation Amount is left, but the + * transaction will succeed with the payments that have been processed up to + * that point. + * + * This limit is independent of loanPaymentsPerFeeIncrement, so a transaction + * could potentially be charged for many more payments than actually get + * processed. Users should take care not to submit a transaction paying more + * than loanMaximumPaymentsPerTransaction * Loan.PeriodicPayment. Because + * overpayments are charged as a payment, if submitting + * loanMaximumPaymentsPerTransaction * Loan.PeriodicPayment, users should not + * set the tfLoanOverpayment flag. + * + * Even though they're independent, loanMaximumPaymentsPerTransaction should be + * a multiple of loanPaymentsPerFeeIncrement. + * + * This number was chosen arbitrarily, but should not be changed once released + * without an amendment + */ +static constexpr int loanMaximumPaymentsPerTransaction = 100; + /** The maximum length of a URI inside an NFT */ std::size_t constexpr maxTokenURILength = 256; diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 752d50f104..b5f3923207 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -371,20 +371,20 @@ class Loan_test : public beast::unit_test::suite return {asset, keylet.key}; } + /// Get the state without checking anything LoanState getCurrentState( jtx::Env const& env, BrokerInfo const& broker, - Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus) + Keylet const& loanKeylet) { - using namespace std::chrono_literals; using d = NetClock::duration; using tp = NetClock::time_point; + // Lookup the current loan state if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan)) { - LoanState state{ + return LoanState{ .previousPaymentDate = loan->at(sfPreviousPaymentDate), .startDate = tp{d{loan->at(sfStartDate)}}, .nextPaymentDate = loan->at(sfNextPaymentDueDate), @@ -399,40 +399,52 @@ class Loan_test : public beast::unit_test::suite .paymentInterval = loan->at(sfPaymentInterval), .interestRate = TenthBips32{loan->at(sfInterestRate)}, }; - BEAST_EXPECT(state.previousPaymentDate == 0); - BEAST_EXPECT( - tp{d{state.nextPaymentDate}} == state.startDate + 600s); - BEAST_EXPECT(state.paymentRemaining == 12); - BEAST_EXPECT( - state.principalOutstanding == broker.asset(1000).value()); - BEAST_EXPECT( - state.loanScale == - (broker.asset.integral() - ? 0 - : state.principalOutstanding.exponent())); - BEAST_EXPECT(state.paymentInterval == 600); - BEAST_EXPECT( - state.totalValue == - roundToAsset( - broker.asset, - state.periodicPayment * state.paymentRemaining, - state.loanScale)); - BEAST_EXPECT( - state.managementFeeOutstanding == - computeFee( - broker.asset, - state.totalValue - state.principalOutstanding, - managementFeeRateParameter, - state.loanScale)); - - verifyLoanStatus(state); - - return state; } - return LoanState{}; } + /// Get the state and check the values against the parameters used in + /// `lifecycle` + LoanState + getCurrentState( + jtx::Env const& env, + BrokerInfo const& broker, + Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus) + { + using namespace std::chrono_literals; + using d = NetClock::duration; + using tp = NetClock::time_point; + + auto const state = getCurrentState(env, broker, loanKeylet); + BEAST_EXPECT(state.previousPaymentDate == 0); + BEAST_EXPECT(tp{d{state.nextPaymentDate}} == state.startDate + 600s); + BEAST_EXPECT(state.paymentRemaining == 12); + BEAST_EXPECT(state.principalOutstanding == broker.asset(1000).value()); + BEAST_EXPECT( + state.loanScale == + (broker.asset.integral() ? 0 + : state.principalOutstanding.exponent())); + BEAST_EXPECT(state.paymentInterval == 600); + BEAST_EXPECT( + state.totalValue == + roundToAsset( + broker.asset, + state.periodicPayment * state.paymentRemaining, + state.loanScale)); + BEAST_EXPECT( + state.managementFeeOutstanding == + computeFee( + broker.asset, + state.totalValue - state.principalOutstanding, + managementFeeRateParameter, + state.loanScale)); + + verifyLoanStatus(state); + + return state; + } + bool canImpairLoan( jtx::Env const& env, @@ -851,6 +863,8 @@ class Loan_test : public beast::unit_test::suite return Account("Broker pseudo-account", brokerPseudo); }(); + auto const baseFee = env.current()->fees().base; + auto badKeylet = keylet::vault(lender.id(), env.seq(lender)); // Try some failure cases // flags are checked first @@ -1379,11 +1393,13 @@ class Loan_test : public beast::unit_test::suite broker.asset(10), txFlags), ter(temINVALID)); - env(pay(borrower, loanKeylet.key, broker.asset(-100), txFlags), + // broker.asset(80) is less than a single payment, but all these + // checks fail before that matters + env(pay(borrower, loanKeylet.key, broker.asset(-80), txFlags), ter(temBAD_AMOUNT)); - env(pay(borrower, broker.brokerID, broker.asset(100), txFlags), + env(pay(borrower, broker.brokerID, broker.asset(80), txFlags), ter(tecNO_ENTRY)); - env(pay(evan, loanKeylet.key, broker.asset(500), txFlags), + env(pay(evan, loanKeylet.key, broker.asset(80), txFlags), ter(tecNO_PERMISSION)); // TODO: Write a general "isFlag" function? See STObject::isFlag. @@ -1395,8 +1411,13 @@ class Loan_test : public beast::unit_test::suite // don't end up duplicating the next test transaction. env(pay(borrower, loanKeylet.key, - broker.asset(state.periodicPayment * 2), + STAmount{ + broker.asset, + state.periodicPayment * Number{15, -1}}, tfLoanOverpayment), + fee(XRPAmount{ + baseFee * + (Number{15, -1} / loanPaymentsPerFeeIncrement + 1)}), ter(temINVALID_FLAG)); } // Try to send a payment marked as both full payment and @@ -1435,23 +1456,34 @@ class Loan_test : public beast::unit_test::suite // Send a transaction that tries to pay more than the borrowers's // balance + XRPAmount const badFee{ + baseFee * + (borrowerBalanceBeforePayment.number() * 2 / + state.periodicPayment / loanPaymentsPerFeeIncrement + + 1)}; env(pay(borrower, loanKeylet.key, STAmount{ broker.asset, borrowerBalanceBeforePayment.number() * 2}, txFlags), + fee(badFee), ter(tecINSUFFICIENT_FUNDS)); - env(pay(borrower, loanKeylet.key, transactionAmount, txFlags)); + XRPAmount const goodFee{ + baseFee * (numPayments / loanPaymentsPerFeeIncrement + 1)}; + env(pay(borrower, loanKeylet.key, transactionAmount, txFlags), + fee(goodFee)); env.close(); + // log << env.meta()->getJson() << std::endl; + // Need to account for fees if the loan is in XRP PrettyAmount adjustment = broker.asset(0); if (broker.asset.native()) { - adjustment = env.current()->fees().base * 2; + adjustment = badFee + goodFee; } state.paymentRemaining = 0; @@ -1781,27 +1813,28 @@ class Loan_test : public beast::unit_test::suite << "\tLoan starting state: " << state.paymentRemaining << ", " << raw.interestDue << ", " << raw.principalOutstanding << ", " - << raw.managementFeeDue << ", " << rounded.interestDue - << ", " << rounded.principalOutstanding << ", " + << raw.managementFeeDue << ", " + << rounded.valueOutstanding << ", " + << rounded.principalOutstanding << ", " << rounded.managementFeeDue; } + // Try to pay a little extra to show that it's _not_ + // taken + STAmount const transactionAmount = + STAmount{broker.asset, totalDue} + broker.asset(10); + // Only check the first payment since the rounding + // may drift as payments are made + BEAST_EXPECT( + transactionAmount == + roundToScale( + broker.asset( + Number(9533457001162141, -14), Number::upward), + state.loanScale, + Number::upward)); + while (state.paymentRemaining > 0) { - // Try to pay a little extra to show that it's _not_ - // taken - STAmount const transactionAmount = - STAmount{broker.asset, totalDue} + broker.asset(10); - // Only check the first payment since the rounding - // may drift as payments are made - BEAST_EXPECT( - transactionAmount == - roundToScale( - broker.asset( - Number(9533457001162141, -14), Number::upward), - state.loanScale, - Number::upward)); - // Compute the expected principal amount auto const paymentComponents = detail::computePaymentComponents( @@ -1954,6 +1987,78 @@ class Loan_test : public beast::unit_test::suite #if LOANCOMPLETE // TODO + auto time = [&](std::string label, std::function timed) { + if (!BEAST_EXPECT(timed)) + return; + + using clock_type = std::chrono::steady_clock; + using duration_type = std::chrono::milliseconds; + + auto const start = clock_type::now(); + timed(); + auto const duration = std::chrono::duration_cast( + clock_type::now() - start); + + log << label << " took " << duration.count() << "ms" << std::endl; + + return duration; + }; + + lifecycle( + caseLabel, + "timing", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + [&](Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus) { + // Estimate optimal values for loanPaymentsPerFeeIncrement and + // loanMaximumPaymentsPerTransaction. + using namespace loan; + + auto const state = + getCurrentState(env, broker, verifyLoanStatus.keylet); + auto const serviceFee = broker.asset(2).value(); + + STAmount const totalDue{ + broker.asset, + roundPeriodicPayment( + broker.asset, + state.periodicPayment + serviceFee, + state.loanScale)}; + + // Make a single payment + time("single payment", [&]() { + env(pay(borrower, loanKeylet.key, totalDue)); + }); + env.close(); + + // Make all but the final payment + auto const numPayments = (state.paymentRemaining - 2); + STAmount const bigPayment{broker.asset, totalDue * numPayments}; + XRPAmount const bigFee{ + baseFee * (numPayments / loanPaymentsPerFeeIncrement + 1)}; + time("ten payments", [&]() { + env(pay(borrower, loanKeylet.key, bigPayment), fee(bigFee)); + }); + env.close(); + + time("final payment", [&]() { + // Make the final payment + env( + pay(borrower, + loanKeylet.key, + totalDue + STAmount{broker.asset, 1})); + }); + env.close(); + }); + lifecycle( caseLabel, "Loan overpayment allowed - Explicit overpayment", @@ -3661,8 +3766,6 @@ class Loan_test : public beast::unit_test::suite auto const brokerStateBefore = env.le(keylet::loanbroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); createJson = env.json(createJson, sig(sfCounterpartySignature, lender)); env(createJson, ter(temINVALID)); @@ -3673,8 +3776,6 @@ public: void run() override { - testAccountSendMptMinAmountInvariant(); - testIssuerLoan(); testDisabled(); testSelfLoan(); @@ -3692,6 +3793,8 @@ public: testInvalidLoanManage(); testInvalidLoanPay(); testInvalidLoanSet(); + + testAccountSendMptMinAmountInvariant(); } }; diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index b0f747d9fb..edf3dd9646 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -228,7 +228,7 @@ computeRoundedPrincipalComponent( asset, principalOutstanding - rawPrincipalOutstanding, scale, - Number::downward); + asset.integral() ? Number::downward : Number::towards_zero); // If the rounded principal outstanding is greater than the true // principal outstanding, we need to pay more principal to reduce @@ -308,7 +308,7 @@ computeRoundedInterestComponent( asset, interestOutstanding - rawInterestOutstanding, scale, - Number::downward); + asset.integral() ? Number::downward : Number::towards_zero); roundedInterest += diff; } @@ -358,7 +358,7 @@ computeRoundedInterestAndFeeComponents( asset, managementFeeOutstanding - rawManagementFeeOutstanding, scale, - Number::downward); + asset.integral() ? Number::downward : Number::towards_zero); roundedFee += diff; @@ -383,18 +383,27 @@ computeRoundedInterestAndFeeComponents( roundedInterest = std::min(interestOutstanding, roundedInterest); // Make sure the parts don't add up to too much - Number excess = roundedPeriodicPayment - roundedPrincipal - - roundedInterest - roundedFee; + auto const initialTotal = roundedPrincipal + roundedInterest + roundedFee; + Number excess = roundedPeriodicPayment - initialTotal; XRPL_ASSERT_PARTS( isRounded(asset, excess, scale), "ripple::detail::computeRoundedInterestAndFeeComponents", "excess is rounded"); +#if LOANCOMPLETE + if (excess != beast::zero) + std::cout << "computeRoundedInterestAndFeeComponents excess is " + << excess << std::endl; +#endif + if (excess < beast::zero) { // Take as much of the excess as we can out of the interest - auto part = std::min(roundedInterest, abs(excess)); +#if LOANCOMPLETE + std::cout << "\tApplying excess to interest\n"; +#endif + auto part = std::min(roundedInterest, -excess); roundedInterest -= part; excess += part; @@ -407,7 +416,10 @@ computeRoundedInterestAndFeeComponents( { // If there's any left, take as much of the excess as we can out of the // fee - auto part = std::min(roundedFee, abs(excess)); +#if LOANCOMPLETE + std::cout << "\tApplying excess to fee\n"; +#endif + auto part = std::min(roundedFee, -excess); roundedFee -= part; excess += part; } @@ -422,7 +434,7 @@ computeRoundedInterestAndFeeComponents( ((asset.integral() && excess < 3) || (roundedPeriodicPayment.exponent() - excess.exponent() > 6))), "ripple::detail::computeRoundedInterestAndFeeComponents", - "excess is zero (fee)"); + "excess is extremely small (fee)"); XRPL_ASSERT_PARTS( roundedFee >= beast::zero, @@ -596,8 +608,9 @@ tryOverpayment( auto const principalError = principalOutstanding - raw.principalOutstanding; auto const feeError = managementFeeOutstanding - raw.managementFeeDue; - auto const newRawPrincipal = - raw.principalOutstanding - overpaymentComponents.trackedPrincipalDelta; + auto const newRawPrincipal = std::max( + raw.principalOutstanding - overpaymentComponents.trackedPrincipalDelta, + Number{0}); auto newLoanProperties = computeLoanProperties( asset, @@ -1136,7 +1149,8 @@ computeOverpaymentComponents( { XRPL_ASSERT( overpayment > 0 && isRounded(asset, overpayment, loanScale), - "ripple::loanMakePayment : valid overpayment amount"); + "ripple::detail::computeOverpaymentComponents : valid overpayment " + "amount"); Number const fee = roundToAsset( asset, tenthBipsOfValue(overpayment, overpaymentFeeRate), loanScale); @@ -1237,6 +1251,15 @@ calculateRawLoanState( std::uint32_t const paymentRemaining, TenthBips16 const managementFeeRate) { + if (paymentRemaining == 0) + { + return LoanState{ + .valueOutstanding = 0, + .principalOutstanding = 0, + .interestOutstanding = 0, + .interestDue = 0, + .managementFeeDue = 0}; + } Number const rawValueOutstanding = periodicPayment * paymentRemaining; Number const rawPrincipalOutstanding = detail::loanPrincipalFromPeriodicPayment( @@ -1556,11 +1579,9 @@ loanMakePayment( Number const serviceFee = loan->at(sfLoanServiceFee); Number const latePaymentFee = loan->at(sfLatePaymentFee); - Number const closePaymentFee = - roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale); TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; - auto const periodicPayment = loan->at(sfPeriodicPayment); + Number const periodicPayment = loan->at(sfPeriodicPayment); auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDate); @@ -1655,7 +1676,8 @@ loanMakePayment( std::size_t numPayments = 1; - while (totalPaid < amount && paymentRemainingProxy > 0) + while (totalPaid < amount && paymentRemainingProxy > 0 && + numPayments < loanMaximumPaymentsPerTransaction) { // Try to make more payments detail::PaymentComponentsPlus const nextPayment{ @@ -1718,13 +1740,18 @@ loanMakePayment( // ------------------------------------------------------------- // overpayment handling if (overpaymentAllowed && loan->isFlag(lsfLoanOverpayment) && - paymentRemainingProxy > 0 && nextDueDateProxy && totalPaid < amount) + paymentRemainingProxy > 0 && nextDueDateProxy && totalPaid < amount && + numPayments < loanMaximumPaymentsPerTransaction) { TenthBips32 const overpaymentInterestRate{ loan->at(sfOverpaymentInterestRate)}; TenthBips32 const overpaymentFeeRate{loan->at(sfOverpaymentFee)}; - Number const overpayment = amount - totalPaid; + // It shouldn't be possible for the overpayment to be greater than + // totalValueOutstanding, because that would have been processed as + // another normal payment. But cap it just in case. + Number const overpayment = + std::min(amount - totalPaid, *totalValueOutstandingProxy); detail::PaymentComponentsPlus const overpaymentComponents = detail::computeOverpaymentComponents( diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 6bf9560afa..ead8bd2784 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -23,6 +23,7 @@ #include #include +#include #include namespace ripple { @@ -65,8 +66,6 @@ LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx) // payment return normalCost; - auto const paymentsPerFeeIncrement = 20; - // The fee is based on the potential number of payments, unless the loan is // being fully paid off. auto const amount = tx[sfAmount]; @@ -77,10 +76,10 @@ LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx) // Let preclaim worry about the error for this return normalCost; - if (loanSle->at(sfPaymentRemaining) <= paymentsPerFeeIncrement) + if (loanSle->at(sfPaymentRemaining) <= loanPaymentsPerFeeIncrement) { - // If there are fewer than paymentsPerFeeIncrement payments left, we can - // skip the computations. + // If there are fewer than loanPaymentsPerFeeIncrement payments left to + // pay, we can skip the computations. return normalCost; } @@ -93,30 +92,36 @@ LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx) if (!brokerSle) // Let preclaim worry about the error for this return normalCost; - auto const vaultSle = view.read(keylet::vault(loanSle->at(sfVaultID))); + auto const vaultSle = view.read(keylet::vault(brokerSle->at(sfVaultID))); if (!vaultSle) // Let preclaim worry about the error for this return normalCost; auto const asset = vaultSle->at(sfAsset); + + if (asset != amount.asset()) + // Let preclaim worry about the error for this + return normalCost; + auto const scale = loanSle->at(sfLoanScale); auto const regularPayment = roundPeriodicPayment(asset, loanSle->at(sfPeriodicPayment), scale) + loanSle->at(sfLoanServiceFee); - if (amount < regularPayment * paymentsPerFeeIncrement) - // This is definitely paying fewer than paymentsPerFeeIncrement payments - return normalCost; - - NumberRoundModeGuard mg(Number::downward); - // Figure out how many payments will be made - auto const numPaymentEstimate = + // If making an overpayment, count it as a full payment because it will do + // about the same amount of work, if not more. + NumberRoundModeGuard mg( + tx.isFlag(tfLoanOverpayment) ? Number::upward : Number::downward); + // Estimate how many payments will be made + Number const numPaymentEstimate = static_cast(amount / regularPayment); - // Charge one base fee per paymentsPerFeeIncrement payments - use integer - // math (round down), then add one to ensure all this extra math is worth - // it. - auto const feeIncrements = numPaymentEstimate / paymentsPerFeeIncrement + 1; + // Charge one base fee per paymentsPerFeeIncrement payments, rounding up. + Number::setround(Number::upward); + auto const feeIncrements = std::max( + 1ll, + static_cast( + numPaymentEstimate / loanPaymentsPerFeeIncrement)); return feeIncrements * normalCost; } diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index e207d2089a..41e4b7d4ba 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -411,20 +411,21 @@ LoanSet::doApply() // Guard 4: if the rounded periodic payment is large enough that the loan // can't be amortized in the specified number of payments, raise an error - if (auto const computedPayments = roundToAsset( - vaultAsset, - properties.totalValueOutstanding / roundedPayment, - properties.loanScale, - Number::upward); - computedPayments < paymentTotal) { - JLOG(j_.warn()) - << "Loan Periodic payment (" << properties.periodicPayment - << ") rounding (" << roundedPayment - << ") will complete the " - "loan in less than the specified number of payments (" - << computedPayments << " < " << paymentTotal << ")"; - return tecPRECISION_LOSS; + NumberRoundModeGuard mg(Number::upward); + + if (std::int64_t const computedPayments{ + properties.totalValueOutstanding / roundedPayment}; + computedPayments < paymentTotal) + { + JLOG(j_.warn()) + << "Loan Periodic payment (" << properties.periodicPayment + << ") rounding (" << roundedPayment + << ") will complete the " + "loan in less than the specified number of payments (" + << computedPayments << " < " << paymentTotal << ")"; + return tecPRECISION_LOSS; + } } // Check that the other computed values are valid From 269fb44cd742d07b445e6955e97d9c5157b9155e Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sun, 26 Oct 2025 13:12:51 -0400 Subject: [PATCH 165/291] Add unit test from security audit (FIND-007) - The issue identified in this finding had previously been fixed, but the test is useful to verify. --- src/test/app/Loan_test.cpp | 105 +++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index b5f3923207..e69c0e22ce 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -3772,6 +3772,110 @@ class Loan_test : public beast::unit_test::suite env.close(); } + void + testLoanPayDebtDecreaseInvariant() + { + testcase + << "LoanPay ripple::LoanPay::doApply : debtDecrease rounding good"; + + using namespace jtx; + using namespace std::chrono_literals; + Env env(*this, all); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1'000'000), issuer, lender, borrower); + env.close(); + + PrettyAsset const iouAsset = issuer[iouCurrency]; + auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000))); + env(trustLenderTx); + auto trustBorrowerTx = + env.json(trust(borrower, iouAsset(1'000'000'000))); + env(trustBorrowerTx); + auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000)); + env(payLenderTx); + auto payIssuerTx = pay(issuer, borrower, iouAsset(1'000'000)); + env(payIssuerTx); + env.close(); + + BrokerInfo broker{createVaultAndBroker(env, iouAsset, lender)}; + + using namespace loan; + + auto const baseFee = env.current()->fees().base; + auto const loanSetFee = fee(baseFee * 2); + Number const principalRequest{1, 3}; + + auto createJson = env.json( + set(borrower, broker.brokerID, principalRequest), + fee(loanSetFee), + json(sfCounterpartySignature, Json::objectValue)); + + createJson["ClosePaymentFee"] = "0"; + createJson["GracePeriod"] = 60; + createJson["InterestRate"] = 24346; + createJson["LateInterestRate"] = 65535; + createJson["LatePaymentFee"] = "0"; + createJson["LoanOriginationFee"] = "218"; + createJson["LoanServiceFee"] = "0"; + createJson["PaymentInterval"] = 60; + createJson["PaymentTotal"] = 5678; + createJson["PrincipalRequested"] = "9924.81"; + + auto const brokerStateBefore = + env.le(keylet::loanbroker(broker.brokerID)); + auto const loanSequence = brokerStateBefore->at(sfLoanSequence); + auto const keylet = keylet::loan(broker.brokerID, loanSequence); + + createJson = env.json(createJson, sig(sfCounterpartySignature, lender)); + env(createJson, ter(tesSUCCESS)); + env.close(); + + auto const pseudoAcct = [&]() { + auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); + if (!BEAST_EXPECT(brokerSle)) + return lender; + auto const brokerPseudo = brokerSle->at(sfAccount); + return Account("Broker pseudo-account", brokerPseudo); + }(); + + VerifyLoanStatus verifyLoanStatus( + env, broker, Number{992481, -2}, pseudoAcct, keylet); + auto const originalState = getCurrentState(env, broker, keylet); + verifyLoanStatus(originalState); + + Number const payment{3'269'349'176'470'588, -12}; + XRPAmount const payFee{ + baseFee * + ((payment / originalState.periodicPayment) / + loanPaymentsPerFeeIncrement + + 1)}; + auto loanPayTx = env.json( + pay(borrower, keylet.key, STAmount{broker.asset, payment}), + fee(payFee)); + BEAST_EXPECT(to_string(payment) == "3269.349176470588"); + env(loanPayTx, ter(tesSUCCESS)); + env.close(); + + auto const newState = getCurrentState(env, broker, keylet); + BEAST_EXPECT(isRounded( + broker.asset, + newState.managementFeeOutstanding, + originalState.loanScale)); + BEAST_EXPECT( + newState.managementFeeOutstanding < + originalState.managementFeeOutstanding); + BEAST_EXPECT(isRounded( + broker.asset, newState.totalValue, originalState.loanScale)); + BEAST_EXPECT(isRounded( + broker.asset, + newState.principalOutstanding, + originalState.loanScale)); + } + public: void run() override @@ -3795,6 +3899,7 @@ public: testInvalidLoanSet(); testAccountSendMptMinAmountInvariant(); + testLoanPayDebtDecreaseInvariant(); } }; From b8b9e7a51ed7c6f34e8d510667bd86554553f6e8 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sun, 26 Oct 2025 15:05:51 -0400 Subject: [PATCH 166/291] Add repro unit tests, with modifications, from audit - FIND-005, FIND-009, and FIND-010. - Add the finding number to existing tests - FIND-001, FIND-003, FIND-012, FIND-007. - Tweak the interest rate failure log messages in LoanSet. --- src/test/app/Loan_test.cpp | 280 +++++++++++++++++++++++++++- src/xrpld/app/tx/detail/LoanSet.cpp | 15 +- 2 files changed, 284 insertions(+), 11 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index e69c0e22ce..8abc53ebca 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -2832,6 +2832,7 @@ class Loan_test : public beast::unit_test::suite void testBatchBypassCounterparty() { + // From FIND-001 testcase << "Batch Bypass Counterparty"; using namespace jtx; @@ -2880,7 +2881,7 @@ class Loan_test : public beast::unit_test::suite ter(temBAD_SIGNATURE)); env.close(); - // ? Check that the loan was created + // ? Check that the loan was NOT created { Json::Value params(Json::objectValue); params[jss::account] = borrower.human(); @@ -2904,6 +2905,7 @@ class Loan_test : public beast::unit_test::suite void testWrongMaxDebtBehavior() { + // From FIND-003 testcase << "Wrong Max Debt Behavior"; using namespace jtx; @@ -2950,6 +2952,7 @@ class Loan_test : public beast::unit_test::suite void testLoanPayComputePeriodicPaymentValidRateInvariant() { + // From FIND-012 testcase << "LoanPay ripple::detail::computePeriodicPayment : " "valid rate"; @@ -3775,6 +3778,7 @@ class Loan_test : public beast::unit_test::suite void testLoanPayDebtDecreaseInvariant() { + // From FIND-007 testcase << "LoanPay ripple::LoanPay::doApply : debtDecrease rounding good"; @@ -3876,6 +3880,271 @@ class Loan_test : public beast::unit_test::suite originalState.loanScale)); } + void + testLoanPayComputePeriodicPaymentValidTotalInterestInvariant() + { + // From FIND-010 + testcase << "ripple::loanComputePaymentParts : valid total interest"; + + using namespace jtx; + using namespace std::chrono_literals; + Env env(*this, all); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1'000'000), issuer, lender, borrower); + env.close(); + + PrettyAsset const iouAsset = issuer[iouCurrency]; + auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000))); + env(trustLenderTx); + auto trustBorrowerTx = + env.json(trust(borrower, iouAsset(1'000'000'000))); + env(trustBorrowerTx); + auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000)); + env(payLenderTx); + auto payIssuerTx = pay(issuer, borrower, iouAsset(1'000'000)); + env(payIssuerTx); + env.close(); + + BrokerInfo broker{createVaultAndBroker(env, iouAsset, lender)}; + + using namespace loan; + + auto const loanSetFee = fee(env.current()->fees().base * 2); + Number const principalRequest{1, 3}; + auto const startDate = env.now() + 60s; + + auto createJson = env.json( + set(borrower, broker.brokerID, principalRequest), + fee(loanSetFee), + json(sfCounterpartySignature, Json::objectValue)); + + createJson["CloseInterestRate"] = 47299; + createJson["ClosePaymentFee"] = "3985819770"; + createJson["GracePeriod"] = 0; + createJson["InterestRate"] = 92; + createJson["LatePaymentFee"] = "3866894865"; + createJson["LoanOriginationFee"] = "0"; + createJson["LoanServiceFee"] = "2348810240"; + createJson["OverpaymentFee"] = 58545; + createJson["PaymentInterval"] = 60; + createJson["PaymentTotal"] = 1; + createJson["PrincipalRequested"] = "0.000763058"; + + auto const brokerStateBefore = + env.le(keylet::loanbroker(broker.brokerID)); + auto const loanSequence = brokerStateBefore->at(sfLoanSequence); + auto const keylet = keylet::loan(broker.brokerID, loanSequence); + + createJson = env.json(createJson, sig(sfCounterpartySignature, lender)); + env(createJson, ter(tecPRECISION_LOSS)); + env.close(startDate); + + auto loanPayTx = env.json( + pay(borrower, keylet.key, STAmount{broker.asset, Number{}})); + loanPayTx["Amount"]["value"] = "0.000281284125490196"; + env(loanPayTx, ter(tecNO_ENTRY)); + env.close(); + } + + void + testDosLoanPay() + { + // From FIND-005 + testcase << "DoS LoanPay"; + + using namespace jtx; + using namespace std::chrono_literals; + Env env(*this, all); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1'000'000), issuer, lender, borrower); + env.close(); + + PrettyAsset const iouAsset = issuer[iouCurrency]; + env(trust(lender, iouAsset(100'000'000))); + env(trust(borrower, iouAsset(100'000'000))); + env(pay(issuer, lender, iouAsset(10'000'000))); + env(pay(issuer, borrower, iouAsset(1'000))); + env.close(); + + BrokerInfo broker{createVaultAndBroker(env, iouAsset, lender)}; + + using namespace loan; + + auto const loanSetFee = fee(env.current()->fees().base * 2); + Number const principalRequest{1, 3}; + auto const baseFee = env.current()->fees().base; + + auto createJson = env.json( + set(borrower, broker.brokerID, principalRequest), + fee(loanSetFee), + json(sfCounterpartySignature, Json::objectValue)); + + createJson["ClosePaymentFee"] = "0"; + createJson["GracePeriod"] = 60; + createJson["InterestRate"] = 20930; + createJson["LateInterestRate"] = 77049; + createJson["LatePaymentFee"] = "0"; + createJson["LoanServiceFee"] = "0"; + createJson["OverpaymentFee"] = 7; + createJson["OverpaymentInterestRate"] = 66653; + createJson["PaymentInterval"] = 60; + createJson["PaymentTotal"] = 3239184; + createJson["PrincipalRequested"] = "3959.37"; + + auto const brokerStateBefore = + env.le(keylet::loanbroker(broker.brokerID)); + auto const loanSequence = brokerStateBefore->at(sfLoanSequence); + auto const keylet = keylet::loan(broker.brokerID, loanSequence); + + createJson = env.json(createJson, sig(sfCounterpartySignature, lender)); + env(createJson, ter(tesSUCCESS)); + env.close(); + + auto const stateBefore = getCurrentState(env, broker, keylet); + BEAST_EXPECT(stateBefore.paymentRemaining == 3239184); + BEAST_EXPECT( + stateBefore.paymentRemaining > loanMaximumPaymentsPerTransaction); + + auto loanPayTx = env.json( + pay(borrower, keylet.key, STAmount{broker.asset, Number{}})); + Number const amount{395937, -2}; + loanPayTx["Amount"]["value"] = to_string(amount); + XRPAmount const payFee{ + baseFee * + std::int64_t( + amount / stateBefore.periodicPayment / + loanPaymentsPerFeeIncrement + + 1)}; + env(loanPayTx, ter(tesSUCCESS), fee(payFee)); + env.close(); + + auto const stateAfter = getCurrentState(env, broker, keylet); + BEAST_EXPECT( + stateAfter.paymentRemaining == + stateBefore.paymentRemaining - loanMaximumPaymentsPerTransaction); + } + + void + testLoanPayComputePeriodicPaymentValidTotalPrincipalPaidInvariant() + { + // From FIND-009 + testcase + << "ripple::loanComputePaymentParts : totalPrincipalPaid rounded"; + + using namespace jtx; + using namespace std::chrono_literals; + Env env(*this, all); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1'000'000), issuer, lender, borrower); + env.close(); + + PrettyAsset const iouAsset = issuer[iouCurrency]; + auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000))); + env(trustLenderTx); + auto trustBorrowerTx = + env.json(trust(borrower, iouAsset(1'000'000'000))); + env(trustBorrowerTx); + auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000)); + env(payLenderTx); + auto payIssuerTx = pay(issuer, borrower, iouAsset(1'000'000)); + env(payIssuerTx); + env.close(); + + BrokerInfo broker{createVaultAndBroker(env, iouAsset, lender)}; + + using namespace loan; + + auto const loanSetFee = fee(env.current()->fees().base * 2); + Number const principalRequest{1, 3}; + + auto createJson = env.json( + set(borrower, broker.brokerID, principalRequest), + fee(loanSetFee), + json(sfCounterpartySignature, Json::objectValue)); + + createJson["ClosePaymentFee"] = "0"; + createJson["GracePeriod"] = 0; + createJson["InterestRate"] = 24346; + createJson["LateInterestRate"] = 65535; + createJson["LatePaymentFee"] = "0"; + createJson["LoanOriginationFee"] = "218"; + createJson["LoanServiceFee"] = "0"; + createJson["PaymentInterval"] = 60; + createJson["PaymentTotal"] = 5678; + createJson["PrincipalRequested"] = "9924.81"; + + auto const brokerStateBefore = + env.le(keylet::loanbroker(broker.brokerID)); + auto const loanSequence = brokerStateBefore->at(sfLoanSequence); + auto const keylet = keylet::loan(broker.brokerID, loanSequence); + + createJson = env.json(createJson, sig(sfCounterpartySignature, lender)); + env(createJson, ter(tesSUCCESS)); + env.close(); + + auto const baseFee = env.current()->fees().base; + + auto const stateBefore = getCurrentState(env, broker, keylet); + + { + auto loanPayTx = env.json( + pay(borrower, keylet.key, STAmount{broker.asset, Number{}})); + Number const amount{3074'745'058'823'529, -12}; + BEAST_EXPECT(to_string(amount) == "3074.745058823529"); + XRPAmount const payFee{ + baseFee * + (amount / stateBefore.periodicPayment / + loanPaymentsPerFeeIncrement + + 1)}; + loanPayTx["Amount"]["value"] = to_string(amount); + env(loanPayTx, fee(payFee), ter(tesSUCCESS)); + env.close(); + } + + { + auto loanPayTx = env.json( + pay(borrower, keylet.key, STAmount{broker.asset, Number{}})); + Number const amount{6732'118'170'944'051, -12}; + BEAST_EXPECT(to_string(amount) == "6732.118170944051"); + XRPAmount const payFee{ + baseFee * + (amount / stateBefore.periodicPayment / + loanPaymentsPerFeeIncrement + + 1)}; + loanPayTx["Amount"]["value"] = to_string(amount); + env(loanPayTx, fee(payFee), ter(tesSUCCESS)); + env.close(); + } + + auto const stateAfter = getCurrentState(env, broker, keylet); + // Total interest outstanding is non-negative + BEAST_EXPECT(stateAfter.totalValue >= stateAfter.principalOutstanding); + // Principal paid is non-negative + BEAST_EXPECT( + stateBefore.principalOutstanding >= + stateAfter.principalOutstanding); + // Total value change is non-negative + BEAST_EXPECT(stateBefore.totalValue >= stateAfter.totalValue); + // Value delta is larger or same as principal delta (meaning + // non-negative interest paid) + BEAST_EXPECT( + (stateBefore.totalValue - stateAfter.totalValue) >= + (stateBefore.principalOutstanding - + stateAfter.principalOutstanding)); + } + public: void run() override @@ -3885,9 +4154,6 @@ public: testSelfLoan(); testLoanSet(); testLifecycle(); - testBatchBypassCounterparty(); - testWrongMaxDebtBehavior(); - testLoanPayComputePeriodicPaymentValidRateInvariant(); testServiceFeeOnBrokerDeepFreeze(); testRPC(); @@ -3898,8 +4164,14 @@ public: testInvalidLoanPay(); testInvalidLoanSet(); + testBatchBypassCounterparty(); + testLoanPayComputePeriodicPaymentValidRateInvariant(); testAccountSendMptMinAmountInvariant(); testLoanPayDebtDecreaseInvariant(); + testWrongMaxDebtBehavior(); + testLoanPayComputePeriodicPaymentValidTotalInterestInvariant(); + testDosLoanPay(); + testLoanPayComputePeriodicPaymentValidTotalPrincipalPaidInvariant(); } }; diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 41e4b7d4ba..681031777e 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -362,24 +362,25 @@ LoanSet::doApply() } } + auto const totalInterestOutstanding = + properties.totalValueOutstanding - principalRequested; // Guard 1: if there is no computed total interest over the life of the loan // for a non-zero interest rate, we cannot properly amortize the loan - if (interestRate > TenthBips32{0} && - (properties.totalValueOutstanding - principalRequested) <= 0) + if (interestRate > TenthBips32{0} && totalInterestOutstanding <= 0) { // Unless this is a zero-interest loan, there must be some interest due // on the loan, even if it's (measurable) dust - JLOG(j_.warn()) << "Loan with " << interestRate - << "% interest has no interest due"; + JLOG(j_.warn()) << "Loan for " << principalRequested << " with " + << interestRate << "% interest has no interest due"; return tecPRECISION_LOSS; } // Guard 1a: If there is any interest computed over the life of the loan, // for a zero interest rate, something went sideways. - if (interestRate == TenthBips32{0} && - (properties.totalValueOutstanding - principalRequested) > 0) + if (interestRate == TenthBips32{0} && totalInterestOutstanding > 0) { // LCOV_EXCL_START - JLOG(j_.warn()) << "Loan with 0% interest has interest due"; + JLOG(j_.warn()) << "Loan for " << principalRequested + << " with 0% interest has interest due"; return tecINTERNAL; // LCOV_EXCL_STOP } From 5ceefdfbd53984fd29b4e0abd5f818277264419b Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sun, 26 Oct 2025 15:05:51 -0400 Subject: [PATCH 167/291] Add repro unit test for FIND-008, with modifications, from audit --- src/test/app/Loan_test.cpp | 97 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 8abc53ebca..93d812b01c 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -4145,6 +4145,102 @@ class Loan_test : public beast::unit_test::suite stateAfter.principalOutstanding)); } + void + testLoanPayComputePeriodicPaymentValidTotalInterestPaidInvariant() + { + // From FIND-008 + testcase << "ripple::loanComputePaymentParts : loanValueChange rounded"; + + using namespace jtx; + using namespace std::chrono_literals; + Env env(*this, all); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1'000'000), issuer, lender, borrower); + env.close(); + + PrettyAsset const iouAsset = issuer[iouCurrency]; + auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000))); + env(trustLenderTx); + auto trustBorrowerTx = + env.json(trust(borrower, iouAsset(1'000'000'000))); + env(trustBorrowerTx); + auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000)); + env(payLenderTx); + auto payIssuerTx = pay(issuer, borrower, iouAsset(10'000'000)); + env(payIssuerTx); + env.close(); + + BrokerInfo broker{createVaultAndBroker(env, iouAsset, lender)}; + { + auto const coverDepositValue = + broker.asset(coverDepositParameter).value(); + env(loanBroker::coverDeposit( + lender, + broker.brokerID, + STAmount{broker.asset, coverDepositValue * 10})); + env.close(); + } + + using namespace loan; + + auto const loanSetFee = fee(env.current()->fees().base * 2); + Number const principalRequest{1, 3}; + + auto createJson = env.json( + set(borrower, broker.brokerID, principalRequest), + fee(loanSetFee), + json(sfCounterpartySignature, Json::objectValue)); + + createJson["ClosePaymentFee"] = "0"; + createJson["GracePeriod"] = 0; + createJson["InterestRate"] = 12833; + createJson["LateInterestRate"] = 77048; + createJson["LatePaymentFee"] = "0"; + createJson["LoanOriginationFee"] = "218"; + createJson["LoanServiceFee"] = "0"; + createJson["PaymentInterval"] = 752; + createJson["PaymentTotal"] = 5678; + createJson["PrincipalRequested"] = "9924.81"; + + auto const brokerStateBefore = + env.le(keylet::loanbroker(broker.brokerID)); + auto const loanSequence = brokerStateBefore->at(sfLoanSequence); + auto const keylet = keylet::loan(broker.brokerID, loanSequence); + + createJson = env.json(createJson, sig(sfCounterpartySignature, lender)); + env(createJson, ter(tesSUCCESS)); + env.close(); + + auto const baseFee = env.current()->fees().base; + + auto const stateBefore = getCurrentState(env, broker, keylet); + BEAST_EXPECT(stateBefore.paymentRemaining == 5678); + BEAST_EXPECT( + stateBefore.paymentRemaining > loanMaximumPaymentsPerTransaction); + + auto loanPayTx = env.json( + pay(borrower, keylet.key, STAmount{broker.asset, Number{}})); + Number const amount{9924'81, -2}; + BEAST_EXPECT(to_string(amount) == "9924.81"); + XRPAmount const payFee{ + baseFee * + (amount / stateBefore.periodicPayment / + loanPaymentsPerFeeIncrement + + 1)}; + loanPayTx["Amount"]["value"] = to_string(amount); + env(loanPayTx, fee(payFee), ter(tesSUCCESS)); + env.close(); + + auto const stateAfter = getCurrentState(env, broker, keylet); + BEAST_EXPECT( + stateAfter.paymentRemaining == + stateBefore.paymentRemaining - loanMaximumPaymentsPerTransaction); + } + public: void run() override @@ -4172,6 +4268,7 @@ public: testLoanPayComputePeriodicPaymentValidTotalInterestInvariant(); testDosLoanPay(); testLoanPayComputePeriodicPaymentValidTotalPrincipalPaidInvariant(); + testLoanPayComputePeriodicPaymentValidTotalInterestPaidInvariant(); } }; From da14c6f0185f106e4c04bf3220dd0109d5859268 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sun, 26 Oct 2025 18:53:12 -0400 Subject: [PATCH 168/291] Prevent numerical overflow of payment due dates - Addresses FIND-013 from audit. - Bases the limit on the current ledger time, and ensures that "payments * interval <= limit". This allows a loan to potentially run through "the end of time" successfully, but not go a second over. - Wrote several test cases, including a few that go right to "the end of time". --- src/test/app/Loan_test.cpp | 249 +++++++++++++++++++++++++++- src/xrpld/app/tx/detail/LoanSet.cpp | 36 +++- 2 files changed, 281 insertions(+), 4 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 93d812b01c..abb817868a 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -326,7 +326,8 @@ class Loan_test : public beast::unit_test::suite jtx::Env& env, jtx::PrettyAsset const& asset, jtx::Account const& lender, - std::optional debtMax = std::nullopt) + std::optional debtMax = std::nullopt, + std::optional coverRateMin = std::nullopt) { using namespace jtx; @@ -338,6 +339,10 @@ class Loan_test : public beast::unit_test::suite : asset(debtMaximumParameter).value(); auto const coverDepositValue = asset(coverDepositParameter).value(); + auto const coverRateMinValue = coverRateMin + ? TenthBips32(*coverRateMin) + : TenthBips32(coverRateMinParameter); + auto [tx, vaultKeylet] = vault.create({.owner = lender, .asset = asset}); env(tx); @@ -361,7 +366,7 @@ class Loan_test : public beast::unit_test::suite data(testData), managementFeeRate(managementFeeRateParameter), debtMaximum(debtMaximumValue), - coverRateMinimum(TenthBips32(coverRateMinParameter)), + coverRateMinimum(coverRateMinValue), coverRateLiquidation(TenthBips32(coverRateLiquidationParameter))); env(coverDeposit(lender, keylet.key, coverDepositValue)); @@ -2976,7 +2981,7 @@ class Loan_test : public beast::unit_test::suite Number const principalRequest{640562, -5}; Number const serviceFee{2462611968}; - std::uint32_t const numPayments{4294967295}; + std::uint32_t const numPayments{4294967295 / 800}; auto createJson = env.json( set(borrower, broker.brokerID, principalRequest), @@ -4241,6 +4246,243 @@ class Loan_test : public beast::unit_test::suite stateBefore.paymentRemaining - loanMaximumPaymentsPerTransaction); } + void + testLoanNextPaymentDueDateOverflow() + { + // For FIND-013 + testcase << "Prevent nextPaymentDueDate overflow"; + + using namespace jtx; + using namespace std::chrono_literals; + Env env(*this, all); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1'000'000), issuer, lender, borrower); + env.close(); + + PrettyAsset const iouAsset = issuer[iouCurrency]; + auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000))); + env(trustLenderTx); + auto trustBorrowerTx = + env.json(trust(borrower, iouAsset(1'000'000'000))); + env(trustBorrowerTx); + auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000)); + env(payLenderTx); + auto payIssuerTx = pay(issuer, borrower, iouAsset(10'000'000)); + env(payIssuerTx); + env.close(); + + BrokerInfo broker{ + createVaultAndBroker(env, iouAsset, lender, Number(0), 0)}; + + using namespace loan; + + auto const loanSetFee = fee(env.current()->fees().base * 2); + + using timeType = decltype(sfNextPaymentDueDate)::type::value_type; + static_assert(std::is_same_v); + timeType constexpr maxTime = std::numeric_limits::max(); + static_assert(maxTime == 4'294'967'295); + + auto const baseJson = [&]() { + auto createJson = env.json( + set(borrower, broker.brokerID, Number{55524'81, -2}), + fee(loanSetFee), + closePaymentFee(0), + gracePeriod(0), + interestRate(TenthBips32(12833)), + lateInterestRate(TenthBips32(77048)), + latePaymentFee(0), + loanOriginationFee(218), + json(sfCounterpartySignature, Json::objectValue)); + + createJson.removeMember(sfSequence.getJsonName()); + + return createJson; + }(); + + auto const brokerStateBefore = + env.le(keylet::loanbroker(broker.brokerID)); + auto const loanSequence = brokerStateBefore->at(sfLoanSequence); + auto const keylet = keylet::loan(broker.brokerID, loanSequence); + + auto const baseFee = env.current()->fees().base; + + auto parentCloseTime = [&]() { + return env.current()->parentCloseTime().time_since_epoch().count(); + }; + auto maxLoanTime = [&]() { + auto const startDate = parentCloseTime(); + + BEAST_EXPECT(startDate >= 50); + + return maxTime - startDate; + }; + + { + // straight-up overflow + auto const interval = maxLoanTime() + 1; + auto const total = 1; + auto createJson = env.json( + baseJson, paymentInterval(interval), paymentTotal(total)); + + env(createJson, + sig(sfCounterpartySignature, lender), + ter(tecKILLED)); + env.close(); + } + { + // straight-up overflow + // min interval is 60 + auto const interval = 60; + auto const total = maxLoanTime() + 1; + auto createJson = env.json( + baseJson, paymentInterval(interval), paymentTotal(total)); + + env(createJson, + sig(sfCounterpartySignature, lender), + ter(tecKILLED)); + env.close(); + } + { + // Overflow with multiplication of a few large intervals + auto const interval = 1'000'000'000; + auto const total = 10; + auto createJson = env.json( + baseJson, paymentInterval(interval), paymentTotal(total)); + + env(createJson, + sig(sfCounterpartySignature, lender), + ter(tecKILLED)); + env.close(); + } + { + // Overflow with multiplication of many small payments + // min interval is 60 + auto const interval = 60; + auto const total = 1'000'000'000; + auto createJson = env.json( + baseJson, paymentInterval(interval), paymentTotal(total)); + + env(createJson, + sig(sfCounterpartySignature, lender), + ter(tecKILLED)); + env.close(); + } + { + // Start date when the ledger is closed will be larger + auto const interval = maxLoanTime(); + auto const total = 1; + auto createJson = env.json( + baseJson, paymentInterval(interval), paymentTotal(total)); + + env(createJson, + sig(sfCounterpartySignature, lender), + ter(tesSUCCESS)); + env.close(); + + // The transaction is killed in the closed ledger + auto const meta = env.meta(); + if (BEAST_EXPECT(meta)) + { + BEAST_EXPECT(meta->at(sfTransactionResult) == tecKILLED); + } + + // If the transaction had succeeded, the loan would exist + auto const loanSle = env.le(keylet); + // but it doesn't + BEAST_EXPECT(!loanSle); + } + { + // Start date when the ledger is closed will be larger + auto const closeStartDate = (parentCloseTime() / 10 + 1) * 10; + auto const interval = maxTime - closeStartDate; + auto const total = 1; + auto createJson = env.json( + baseJson, paymentInterval(interval), paymentTotal(total)); + + env(createJson, + sig(sfCounterpartySignature, lender), + ter(tesSUCCESS)); + env.close(); + + // The transaction succeeds in the closed ledger + auto const meta = env.meta(); + if (BEAST_EXPECT(meta)) + { + BEAST_EXPECT(meta->at(sfTransactionResult) == tesSUCCESS); + } + + // This loan exists + auto const afterState = getCurrentState(env, broker, keylet); + BEAST_EXPECT(afterState.nextPaymentDate == maxTime); + BEAST_EXPECT(afterState.previousPaymentDate == 0); + BEAST_EXPECT(afterState.paymentRemaining == 1); + } + + { + // Ensure the borrower has funds to pay back the loan + env(pay(issuer, borrower, iouAsset(Number{1'055'524'81, -2}))); + + // Start date when the ledger is closed will be larger + auto const closeStartDate = (parentCloseTime() / 10 + 1) * 10; + auto const maxLoanTime = maxTime - closeStartDate; + auto const total = [&]() { + if (maxLoanTime % 5 == 0) + return 5; + if (maxLoanTime % 3 == 0) + return 3; + if (maxLoanTime % 2 == 0) + return 2; + return 0; + }(); + if (!BEAST_EXPECT(total != 0)) + return; + + auto const brokerState = + env.le(keylet::loanbroker(broker.brokerID)); + // Intentionally shadow the outer values + auto const loanSequence = brokerState->at(sfLoanSequence); + auto const keylet = keylet::loan(broker.brokerID, loanSequence); + + auto const interval = maxLoanTime / total; + auto createJson = env.json( + baseJson, paymentInterval(interval), paymentTotal(total)); + + env(createJson, + sig(sfCounterpartySignature, lender), + ter(tesSUCCESS)); + env.close(); + + // This loan exists + auto const beforeState = getCurrentState(env, broker, keylet); + BEAST_EXPECT( + beforeState.nextPaymentDate == closeStartDate + interval); + BEAST_EXPECT(beforeState.previousPaymentDate == 0); + BEAST_EXPECT(beforeState.paymentRemaining == total); + BEAST_EXPECT(beforeState.periodicPayment > 0); + + // pay all but the last payment + Number const payment = beforeState.periodicPayment * (total - 1); + XRPAmount const payFee{ + baseFee * ((total - 1) / loanPaymentsPerFeeIncrement + 1)}; + auto loanPayTx = env.json( + pay(borrower, keylet.key, STAmount{broker.asset, payment}), + fee(payFee)); + env(loanPayTx, ter(tesSUCCESS)); + env.close(); + + // The loan is on the last payment + auto const afterState = getCurrentState(env, broker, keylet); + BEAST_EXPECT(afterState.nextPaymentDate == maxTime); + BEAST_EXPECT(afterState.previousPaymentDate == maxTime - interval); + BEAST_EXPECT(afterState.paymentRemaining == 1); + } + } + public: void run() override @@ -4269,6 +4511,7 @@ public: testDosLoanPay(); testLoanPayComputePeriodicPaymentValidTotalPrincipalPaidInvariant(); testLoanPayComputePeriodicPaymentValidTotalInterestPaidInvariant(); + testLoanNextPaymentDueDateOverflow(); } }; diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 681031777e..df6d00d60d 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -194,11 +194,45 @@ LoanSet::getValueFields() return valueFields; } +std::uint32_t +getStartDate(ReadView const& view) +{ + return view.info().closeTime.time_since_epoch().count(); +} + TER LoanSet::preclaim(PreclaimContext const& ctx) { auto const& tx = ctx.tx; + { + // Check for numeric overflow of the schedule before we load any + // objects. NextPaymentDue date for the last payment is: + // startDate + (paymentInterval * paymentTotal). + // If that value is larger than "maxTime", the value + // overflows, and we kill the transaction. + using timeType = decltype(sfNextPaymentDueDate)::type::value_type; + static_assert(std::is_same_v); + timeType constexpr maxTime = std::numeric_limits::max(); + static_assert(maxTime == 4'294'967'295); + + auto const timeAvailable = maxTime - getStartDate(ctx.view); + + auto const interval = + ctx.tx.at(~sfPaymentInterval).value_or(defaultPaymentInterval); + auto const total = + ctx.tx.at(~sfPaymentTotal).value_or(defaultPaymentTotal); + + if (interval > timeAvailable) + return tecKILLED; + + if (total > timeAvailable) + return tecKILLED; + + if (timeAvailable / interval < total) + return tecKILLED; + } + auto const account = tx[sfAccount]; auto const brokerID = tx[sfLoanBrokerID]; @@ -536,7 +570,7 @@ LoanSet::doApply() return ter; // Get shortcuts to the loan property values - auto const startDate = view.info().closeTime.time_since_epoch().count(); + auto const startDate = getStartDate(view); auto loanSequenceProxy = brokerSle->at(sfLoanSequence); // Create the loan From f4404eafbdc5dc90df66b57028b73b350cb05cde Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sun, 26 Oct 2025 21:52:01 -0400 Subject: [PATCH 169/291] Fix build error - Use an explicit cast to std::int64_t in std::max call, because suffixes are inconsistent. --- src/xrpld/app/tx/detail/LoanPay.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index ead8bd2784..ae39b6ccdb 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -119,7 +119,7 @@ LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx) // Charge one base fee per paymentsPerFeeIncrement payments, rounding up. Number::setround(Number::upward); auto const feeIncrements = std::max( - 1ll, + std::int64_t(1), static_cast( numPaymentEstimate / loanPaymentsPerFeeIncrement)); From 310852ba2de2320c7a77327d893e71508b43de74 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 28 Oct 2025 14:49:15 -0400 Subject: [PATCH 170/291] refactor: Payment component calculation will target next true state - Compute the next "true" state, round the values off, then compute the deltas needed to get the current state to that state. Plus some data integrity checks. - Add `Number::zero`, which is longer to type, but more readable than `Number{}`. - Prepare to improve Loan unit tests: track managementFeeRate in BrokerInfo, define a LoanParameters object for creation options and start adding support for it, track and verify loan state while making payments. --- include/xrpl/basics/Number.h | 2 + src/libxrpl/basics/Number.cpp | 2 + src/test/app/Loan_test.cpp | 217 +++++++++++++--- src/xrpld/app/misc/LendingHelpers.h | 13 + src/xrpld/app/misc/detail/LendingHelpers.cpp | 255 +++++++++++++++++-- src/xrpld/app/tx/detail/LoanSet.cpp | 2 +- 6 files changed, 437 insertions(+), 54 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index 41c60d30a1..2911d06110 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -58,6 +58,8 @@ public: explicit Number(rep mantissa, int exponent); explicit constexpr Number(rep mantissa, int exponent, unchecked) noexcept; + static Number const zero; + constexpr rep mantissa() const noexcept; constexpr int diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 928b638d43..7c3ea2d3cc 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -43,6 +43,8 @@ namespace ripple { thread_local Number::rounding_mode Number::mode_ = Number::to_nearest; +Number const Number::zero{}; + Number::rounding_mode Number::getround() { diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index abb817868a..2042caabcd 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -102,12 +102,84 @@ class Loan_test : public beast::unit_test::suite { jtx::PrettyAsset asset; uint256 brokerID; - BrokerInfo(jtx::PrettyAsset const& asset_, uint256 const& brokerID_) - : asset(asset_), brokerID(brokerID_) + TenthBips16 managementFeeRate; + BrokerInfo( + jtx::PrettyAsset const& asset_, + uint256 const& brokerID_, + TenthBips16 mgmtRate) + : asset(asset_), brokerID(brokerID_), managementFeeRate(mgmtRate) { } }; + struct LoanParameters + { + BrokerInfo broker; + // The account submitting the transaction. May be borrower or broker. + jtx::Account account; + // The counterparty. Should be the other of borrower or broker. + jtx::Account counter; + // Whether the counterparty is specified in the `counterparty` field, or + // only signs. + bool counterpartyExplicit = true; + Number principalRequest; + STAmount setFee; + std::optional originationFee; + std::optional serviceFee; + std::optional lateFee; + std::optional closeFee; + std::optional overFee; + std::optional interest; + std::optional lateInterest; + std::optional closeInterest; + std::optional overpaymentInterest; + std::optional payTotal; + std::optional payInterval; + std::optional gracePd; + std::optional flags; + + template + jtx::JTx + operator()(jtx::Env& env, FN const&... fN) const + { + using namespace jtx; + using namespace jtx::loan; + + JTx jt{loan::set( + account, broker.brokerID, principalRequest, flags.value_or(0))}; + sig(sfCounterpartySignature, counter)(env, jt); + fee{setFee}(env, jt); + if (counterpartyExplicit) + counterparty(counter)(env, jt); + if (originationFee) + loanOriginationFee (*originationFee)(env, jt); + if (serviceFee) + loanServiceFee (*serviceFee)(env, jt); + if (lateFee) + latePaymentFee (*lateFee)(env, jt); + if (closeFee) + closePaymentFee (*closeFee)(env, jt); + if (overFee) + overpaymentFee (*overFee)(env, jt); + if (interest) + interestRate (*interest)(env, jt); + if (lateInterest) + lateInterestRate (*lateInterest)(env, jt); + if (closeInterest) + closeInterestRate (*closeInterest)(env, jt); + if (overpaymentInterest) + overpaymentInterestRate (*overpaymentInterest)(env, jt); + if (payTotal) + paymentTotal (*payTotal)(env, jt); + if (payInterval) + paymentInterval (*payInterval)(env, jt); + if (gracePd) + gracePeriod (*gracePd)(env, jt); + + return env.jt(jt, fN...); + } + }; + struct LoanState { std::uint32_t previousPaymentDate = 0; @@ -373,7 +445,7 @@ class Loan_test : public beast::unit_test::suite env.close(); - return {asset, keylet.key}; + return {asset, keylet.key, managementFeeRateParameter}; } /// Get the state without checking anything @@ -541,7 +613,7 @@ class Loan_test : public beast::unit_test::suite auto const borrowerOwnerCount = env.ownerCount(borrower); - auto const loanSetFee = fee(env.current()->fees().base * 2); + auto const loanSetFee = env.current()->fees().base * 2; Number const principalRequest = broker.asset(loanAmount).value(); auto const originationFee = broker.asset(1).value(); auto const serviceFee = broker.asset(2).value(); @@ -583,22 +655,50 @@ class Loan_test : public beast::unit_test::suite auto const borrowerStartbalance = env.balance(borrower, broker.asset); // Use the defined values - auto createJtx = env.jt( - set(borrower, broker.brokerID, principalRequest, flags), - sig(sfCounterpartySignature, lender), - loanOriginationFee(originationFee), - loanServiceFee(serviceFee), - latePaymentFee(lateFee), - closePaymentFee(closeFee), - overpaymentFee(overFee), - interestRate(interest), - lateInterestRate(lateInterest), - closeInterestRate(closeInterest), - overpaymentInterestRate(overpaymentInterest), - paymentTotal(total), - paymentInterval(interval), - gracePeriod(grace), - fee(loanSetFee)); + LoanParameters const loanParams{ + .broker = broker, + .account = borrower, + .counter = lender, + .counterpartyExplicit = false, + .principalRequest = principalRequest, + .setFee = loanSetFee, + .originationFee = originationFee, + .serviceFee = serviceFee, + .lateFee = lateFee, + .closeFee = closeFee, + .overFee = overFee, + .interest = interest, + .lateInterest = lateInterest, + .closeInterest = closeInterest, + .overpaymentInterest = overpaymentInterest, + .payTotal = total, + .payInterval = interval, + .gracePd = grace, + .flags = flags, + }; + auto createJtx = loanParams(env); +#if LOANCOMPLETE + { + auto createJtxOld = env.jt( + set(borrower, broker.brokerID, principalRequest, flags), + sig(sfCounterpartySignature, lender), + loanOriginationFee(originationFee), + loanServiceFee(serviceFee), + latePaymentFee(lateFee), + closePaymentFee(closeFee), + overpaymentFee(overFee), + interestRate(interest), + lateInterestRate(lateInterest), + closeInterestRate(closeInterest), + overpaymentInterestRate(overpaymentInterest), + paymentTotal(total), + paymentInterval(interval), + gracePeriod(grace), + fee(loanSetFee)); + BEAST_EXPECT( + createJtx.stx->getJson(0) == createJtxOld.stx->getJson(0)); + } +#endif // Successfully create a Loan env(createJtx); @@ -1777,7 +1877,7 @@ class Loan_test : public beast::unit_test::suite << "\tPayment components: " << "Payments remaining, rawInterest, rawPrincipal, " "rawMFee, trackedValueDelta, trackedPrincipalDelta, " - "trackedMgmtFeeDelta, special"; + "trackedInterestDelta, trackedMgmtFeeDelta, special"; auto const serviceFee = broker.asset(2); @@ -1821,6 +1921,7 @@ class Loan_test : public beast::unit_test::suite << raw.managementFeeDue << ", " << rounded.valueOutstanding << ", " << rounded.principalOutstanding << ", " + << rounded.interestOutstanding << ", " << rounded.managementFeeDue; } @@ -1838,6 +1939,19 @@ class Loan_test : public beast::unit_test::suite state.loanScale, Number::upward)); + auto const initialState = state; + detail::PaymentComponents totalPaid{ + .trackedValueDelta = 0, + .trackedPrincipalDelta = 0, + .trackedManagementFeeDelta = 0}; + Number totalInterestPaid = 0; + + ripple::LoanState currentTrueState = calculateRawLoanState( + state.periodicPayment, + periodicRate, + state.paymentRemaining, + managementFeeRateParameter); + while (state.paymentRemaining > 0) { // Compute the expected principal amount @@ -1853,13 +1967,27 @@ class Loan_test : public beast::unit_test::suite state.paymentRemaining, managementFeeRateParameter); + BEAST_EXPECT( + paymentComponents.trackedValueDelta <= + roundedPeriodicPayment); + + ripple::LoanState const nextTrueState = + calculateRawLoanState( + state.periodicPayment, + periodicRate, + state.paymentRemaining - 1, + managementFeeRateParameter); + detail::LoanDeltas const deltas = + currentTrueState - nextTrueState; + testcase << "\tPayment components: " << state.paymentRemaining - << ", " << paymentComponents.rawInterest << ", " - << paymentComponents.rawPrincipal << ", " - << paymentComponents.rawManagementFee << ", " + << ", " << deltas.interestDueDelta << ", " + << deltas.principalDelta << ", " + << deltas.managementFeeDueDelta << ", " << paymentComponents.trackedValueDelta << ", " << paymentComponents.trackedPrincipalDelta << ", " + << paymentComponents.trackedInterestPart() << ", " << paymentComponents.trackedManagementFeeDelta << ", " << (paymentComponents.specialCase == detail::PaymentSpecialCase::final @@ -1895,17 +2023,20 @@ class Loan_test : public beast::unit_test::suite paymentComponents.trackedPrincipalDelta + paymentComponents.trackedInterestPart() + paymentComponents.trackedManagementFeeDelta); + BEAST_EXPECT( + paymentComponents.trackedValueDelta <= + state.periodicPayment); BEAST_EXPECT( state.paymentRemaining < 12 || roundToAsset( broker.asset, - paymentComponents.rawPrincipal, + deltas.principalDelta, state.loanScale, Number::upward) == roundToScale( broker.asset( - Number(8333228690659858, -14), + Number(8333228695260180, -14), Number::upward), state.loanScale, Number::upward)); @@ -1923,10 +2054,8 @@ class Loan_test : public beast::unit_test::suite paymentComponents.specialCase == detail::PaymentSpecialCase::final || (state.periodicPayment.exponent() - - (paymentComponents.rawPrincipal + - paymentComponents.rawInterest + - paymentComponents.rawManagementFee - - state.periodicPayment) + (deltas.principalDelta + deltas.interestDueDelta + + deltas.managementFeeDueDelta - state.periodicPayment) .exponent()) > 14); auto const borrowerBalanceBeforePayment = @@ -1976,12 +2105,40 @@ class Loan_test : public beast::unit_test::suite state.totalValue -= paymentComponents.trackedValueDelta; verifyLoanStatus(state); + + totalPaid.trackedValueDelta += + paymentComponents.trackedValueDelta; + totalPaid.trackedPrincipalDelta += + paymentComponents.trackedPrincipalDelta; + totalPaid.trackedManagementFeeDelta += + paymentComponents.trackedManagementFeeDelta; + totalInterestPaid += + paymentComponents.trackedInterestPart(); + + currentTrueState = nextTrueState; } // Loan is paid off BEAST_EXPECT(state.paymentRemaining == 0); BEAST_EXPECT(state.principalOutstanding == 0); + // Make sure all the payments add up + BEAST_EXPECT( + totalPaid.trackedValueDelta == initialState.totalValue); + BEAST_EXPECT( + totalPaid.trackedPrincipalDelta == + initialState.principalOutstanding); + BEAST_EXPECT( + totalPaid.trackedManagementFeeDelta == + initialState.managementFeeOutstanding); + // This is almost a tautology given the previous checks, but + // check it anyway for completeness. + BEAST_EXPECT( + totalInterestPaid == + initialState.totalValue - + (initialState.principalOutstanding + + initialState.managementFeeOutstanding)); + // Can't impair or default a paid off loan env(manage(lender, loanKeylet.key, tfLoanImpair), ter(tecNO_PERMISSION)); diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 2b687d4b17..2e48ab86b0 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -170,10 +170,12 @@ enum class PaymentSpecialCase { none, final, extra }; /// single loan payment struct PaymentComponents { +#if LOANCOMPLETE // raw values are unrounded, and are based on pure math Number rawInterest; Number rawPrincipal; Number rawManagementFee; +#endif // tracked values are rounded to the asset and loan scale, and correspond to // fields in the Loan ledger object. // trackedValueDelta modifies sfTotalValueOutstanding. @@ -191,6 +193,14 @@ struct PaymentComponents trackedInterestPart() const; }; +struct LoanDeltas +{ + Number valueDelta; + Number principalDelta; + Number interestDueDelta; + Number managementFeeDueDelta; +}; + PaymentComponents computePaymentComponents( Asset const& asset, @@ -205,6 +215,9 @@ computePaymentComponents( } // namespace detail +detail::LoanDeltas +operator-(LoanState const& lhs, LoanState const& rhs); + Number valueMinusFee( Asset const& asset, diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index edf3dd9646..dc0eaf12d4 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -213,6 +213,7 @@ loanAccruedInterest( paymentInterval; } +#if LOANCOMPLETE Number computeRoundedPrincipalComponent( Asset const& asset, @@ -448,6 +449,7 @@ computeRoundedInterestAndFeeComponents( return std::make_pair( std::max(Number{}, roundedInterest), std::max(Number{}, roundedFee)); } +#endif struct PaymentComponentsPlus : public PaymentComponents { @@ -839,8 +841,10 @@ computeLatePayment( view.parentCloseTime(), nextDueDate); +#if LOANCOMPLETE auto const [rawLateInterest, rawLateManagementFee] = computeInterestAndFeeParts(latePaymentInterest, managementFeeRate); +#endif auto const [roundedLateInterest, roundedLateManagementFee] = [&]() { auto const interest = roundToAsset(asset, latePaymentInterest, loanScale); @@ -859,7 +863,9 @@ computeLatePayment( // This preserves all the other fields without having to enumerate them. PaymentComponentsPlus const late = [&]() { auto inner = periodic; +#if LOANCOMPLETE inner.rawInterest += rawLateInterest; +#endif return PaymentComponentsPlus{ inner, @@ -932,8 +938,10 @@ computeFullPayment( startDate, closeInterestRate); +#if LOANCOMPLETE auto const [rawFullInterest, rawFullManagementFee] = computeInterestAndFeeParts(fullPaymentInterest, managementFeeRate); +#endif auto const [roundedFullInterest, roundedFullManagementFee] = [&]() { auto const interest = @@ -947,9 +955,11 @@ computeFullPayment( PaymentComponentsPlus const full{ PaymentComponents{ +#if LOANCOMPLETE .rawInterest = rawFullInterest, .rawPrincipal = rawPrincipalOutstanding, .rawManagementFee = rawFullManagementFee, +#endif .trackedValueDelta = principalOutstanding + totalInterestOutstanding + managementFeeOutstanding, .trackedPrincipalDelta = principalOutstanding, @@ -1011,35 +1021,123 @@ computePaymentComponents( isRounded(asset, managementFeeOutstanding, scale), "ripple::detail::computePaymentComponents", "Outstanding values are rounded"); + XRPL_ASSERT_PARTS( + paymentRemaining > 0, + "ripple::detail::computePaymentComponents", + "some payments remaining"); auto const roundedPeriodicPayment = roundPeriodicPayment(asset, periodicPayment, scale); - LoanState const raw = calculateRawLoanState( - periodicPayment, periodicRate, paymentRemaining, managementFeeRate); + LoanState const trueTarget = calculateRawLoanState( + periodicPayment, periodicRate, paymentRemaining - 1, managementFeeRate); + LoanState const roundedTarget = LoanState{ + .valueOutstanding = + roundToAsset(asset, trueTarget.valueOutstanding, scale), + .principalOutstanding = + roundToAsset(asset, trueTarget.principalOutstanding, scale), + .interestOutstanding = + roundToAsset(asset, trueTarget.interestOutstanding, scale), + .interestDue = roundToAsset(asset, trueTarget.interestDue, scale), + .managementFeeDue = + roundToAsset(asset, trueTarget.managementFeeDue, scale)}; + LoanState const currentLedgerState = calculateRoundedLoanState( + totalValueOutstanding, principalOutstanding, managementFeeOutstanding); + + LoanDeltas deltas = currentLedgerState - roundedTarget; + + // It should be impossible for any of the deltas to be negative, but do + // defensive checks + if (deltas.principalDelta < beast::zero) + { + // LCOV_EXCL_START + UNREACHABLE( + "ripple::detail::computePaymentComponents : negative principal " + "delta"); + deltas.principalDelta = Number::zero; + // LCOV_EXCL_STOP + } + if (deltas.interestDueDelta < beast::zero) + { + // LCOV_EXCL_START + UNREACHABLE( + "ripple::detail::computePaymentComponents : negative interest " + "delta"); + deltas.interestDueDelta = Number::zero; + // LCOV_EXCL_STOP + } + if (deltas.managementFeeDueDelta < beast::zero) + { + // LCOV_EXCL_START + UNREACHABLE( + "ripple::detail::computePaymentComponents : negative management " + "fee delta"); + deltas.managementFeeDueDelta = Number::zero; + // LCOV_EXCL_STOP + } + + // Adjust the deltas if necessary for data integrity + XRPL_ASSERT_PARTS( + deltas.principalDelta <= currentLedgerState.principalOutstanding, + "ripple::detail::computePaymentComponents", + "principal delta not greater than outstanding"); + deltas.principalDelta = std::min( + deltas.principalDelta, currentLedgerState.principalOutstanding); + XRPL_ASSERT_PARTS( + deltas.interestDueDelta <= currentLedgerState.interestDue, + "ripple::detail::computePaymentComponents", + "interest due delta not greater than outstanding"); + deltas.interestDueDelta = std::min( + {deltas.interestDueDelta, + std::max(Number::zero, roundedPeriodicPayment - deltas.principalDelta), + currentLedgerState.interestDue}); + XRPL_ASSERT_PARTS( + deltas.managementFeeDueDelta <= currentLedgerState.managementFeeDue, + "ripple::detail::computePaymentComponents", + "management fee due delta not greater than outstanding"); + deltas.managementFeeDueDelta = std::min( + {deltas.managementFeeDueDelta, + roundedPeriodicPayment - + (deltas.principalDelta + deltas.interestDueDelta), + currentLedgerState.managementFeeDue}); + + // In case any adjustments were made (or if the original rounding didn't + // quite add up right), recompute the total value delta + deltas.valueDelta = deltas.principalDelta + deltas.interestDueDelta + + deltas.managementFeeDueDelta; if (paymentRemaining == 1 || totalValueOutstanding <= roundedPeriodicPayment) { // If there's only one payment left, we need to pay off each of the loan - // parts. It's probably impossible for the subtraction to result in a - // negative value, but don't leave anything to chance. - Number interest = std::max( - Number{}, - totalValueOutstanding - principalOutstanding - - managementFeeOutstanding); + // parts. + + XRPL_ASSERT( + deltas.valueDelta == totalValueOutstanding, + "ripple::detail::computePaymentComponents", + "last payment total value agrees"); + XRPL_ASSERT( + deltas.principalDelta == principalOutstanding, + "ripple::detail::computePaymentComponents", + "last payment principal agrees"); + XRPL_ASSERT( + deltas.managementFeeDueDelta == managementFeeOutstanding, + "ripple::detail::computePaymentComponents", + "last payment management fee agrees"); // Pay everything off return PaymentComponents{ +#if LOANCOMPLETE .rawInterest = raw.interestOutstanding, .rawPrincipal = raw.principalOutstanding, .rawManagementFee = raw.managementFeeDue, - .trackedValueDelta = - interest + principalOutstanding + managementFeeOutstanding, +#endif + .trackedValueDelta = totalValueOutstanding, .trackedPrincipalDelta = principalOutstanding, .trackedManagementFeeDelta = managementFeeOutstanding, .specialCase = PaymentSpecialCase::final}; } +#if LOANCOMPLETE /* * From the spec, once the periodicPayment is computed: * @@ -1127,14 +1225,91 @@ computePaymentComponents( roundedPeriodicPayment, "ripple::detail::computePaymentComponents", "payment parts fit within payment limit"); +#endif + + // Make sure the parts don't add up to too much + Number shortage = roundedPeriodicPayment - deltas.valueDelta; + + XRPL_ASSERT_PARTS( + isRounded(asset, shortage, scale), + "ripple::detail::computePaymentComponents", + "excess is rounded"); + + // The shortage must never be negative, which indicates that the parts are + // trying to take more than the whole payment. The excess can be positive, + // which indicates that we're not going to take the whole payment amount, + // but if so, it must be small. + auto takeFrom = [](Number& total, Number& component, Number& excess) { + if (excess > beast::zero) + { + // Take as much of the excess as we can out of the provided part and + // the total + auto part = std::min(component, excess); + total -= part; + component -= part; + excess -= part; + } + // If the excess goes negative, we took too much, which should be + // impossible + XRPL_ASSERT_PARTS( + excess >= beast::zero, + "ripple::detail::computePaymentComponents", + "excess non-negative"); + }; + if (shortage < beast::zero) + { + Number excess = -shortage; + + takeFrom(deltas.valueDelta, deltas.principalDelta, excess); + takeFrom(deltas.valueDelta, deltas.interestDueDelta, excess); + takeFrom(deltas.valueDelta, deltas.managementFeeDueDelta, excess); + + shortage = -excess; + } + + // The shortage should never be negative, which indicates that the parts are + // trying to take more than the whole payment. The shortage can be positive, + // which indicates that we're not going to take the whole payment amount, + // but if so, it must be small. + XRPL_ASSERT_PARTS( + shortage == beast::zero || + (shortage > beast::zero && + ((asset.integral() && shortage < 3) || + (scale - shortage.exponent() > 14))), + "ripple::detail::computePaymentComponents", + "excess is extremely small"); + + XRPL_ASSERT( + deltas.valueDelta == + deltas.principalDelta + deltas.interestDueDelta + + deltas.managementFeeDueDelta, + "ripple::detail::computePaymentComponents", + "total value adds up"); + + XRPL_ASSERT_PARTS( + deltas.principalDelta >= beast::zero, + "ripple::detail::computePaymentComponents", + "non-negative principal"); + XRPL_ASSERT_PARTS( + deltas.interestDueDelta >= beast::zero, + "ripple::detail::computePaymentComponents", + "non-negative interest"); + XRPL_ASSERT_PARTS( + deltas.managementFeeDueDelta >= beast::zero, + "ripple::detail::computePaymentComponents", + "non-negative fee"); return PaymentComponents{ +#if LOANCOMPLETE .rawInterest = rawInterest - rawFee, .rawPrincipal = rawPrincipal, .rawManagementFee = rawFee, - .trackedValueDelta = roundedInterest + roundedPrincipal + roundedFee, - .trackedPrincipalDelta = roundedPrincipal, - .trackedManagementFeeDelta = roundedFee, +#endif + // As a final safety check, don't return any negative values + .trackedValueDelta = std::max(deltas.valueDelta, Number::zero), + .trackedPrincipalDelta = std::max(deltas.principalDelta, Number::zero), + .trackedManagementFeeDelta = + std::max(deltas.managementFeeDueDelta, Number::zero), }; } @@ -1172,9 +1347,11 @@ computeOverpaymentComponents( return detail::PaymentComponentsPlus{ detail::PaymentComponents{ +#if LOANCOMPLETE .rawInterest = rawOverpaymentInterest, .rawPrincipal = payment - rawOverpaymentInterest, .rawManagementFee = 0, +#endif .trackedValueDelta = payment, .trackedPrincipalDelta = payment - roundedOverpaymentInterest - roundedOverpaymentManagementFee, @@ -1186,6 +1363,36 @@ computeOverpaymentComponents( } // namespace detail +detail::LoanDeltas +operator-(LoanState const& lhs, LoanState const& rhs) +{ + detail::LoanDeltas result{ + .valueDelta = lhs.valueOutstanding - rhs.valueOutstanding, + .principalDelta = lhs.principalOutstanding - rhs.principalOutstanding, + .interestDueDelta = lhs.interestDue - rhs.interestDue, + .managementFeeDueDelta = lhs.managementFeeDue - rhs.managementFeeDue, + }; + + XRPL_ASSERT_PARTS( + result.valueDelta >= 0, + "ripple::operator-(LoanState,LoanState)", + "valueDelta difference non-negative"); + XRPL_ASSERT_PARTS( + result.principalDelta >= 0, + "ripple::operator-(LoanState,LoanState)", + "principalDelta difference non-negative"); + XRPL_ASSERT_PARTS( + result.interestDueDelta >= 0, + "ripple::operator-(LoanState,LoanState)", + "interestDueDelta difference non-negative"); + XRPL_ASSERT_PARTS( + result.managementFeeDueDelta >= 0, + "ripple::operator-(LoanState,LoanState)", + "managementFeeDueDelta difference non-negative"); + + return result; +} + Number calculateFullPaymentInterest( Number const& rawPrincipalOutstanding, @@ -1404,20 +1611,21 @@ computeLoanProperties( auto const firstPaymentPrincipal = [&]() { // Compute the parts for the first payment. Ensure that the // principal payment will actually change the principal. - auto const paymentComponents = detail::computePaymentComponents( - asset, - loanScale, - totalValueOutstanding, - principalOutstanding, - feeOwedToBroker, + auto const startingState = calculateRawLoanState( periodicPayment, periodicRate, paymentsRemaining, managementFeeRate); + auto const firstPaymentState = calculateRawLoanState( + periodicPayment, + periodicRate, + paymentsRemaining - 1, + managementFeeRate); // The unrounded principal part needs to be large enough to affect the // principal. What to do if not is left to the caller - return paymentComponents.rawPrincipal; + return startingState.principalOutstanding - + firstPaymentState.principalOutstanding; }(); return LoanProperties{ @@ -1696,12 +1904,14 @@ loanMakePayment( nextPayment.trackedPrincipalDelta >= 0, "ripple::loanMakePayment", "additional payment pays non-negative principal"); +#if LOANCOMPLETE XRPL_ASSERT( nextPayment.rawInterest <= periodic.rawInterest, "ripple::loanMakePayment : decreasing interest"); XRPL_ASSERT( - nextPayment.rawPrincipal >= periodic.rawPrincipal, + nextPayment.rawPrinicpal >= periodic.rawPrincipal, "ripple::loanMakePayment : increasing principal"); +#endif if (amount < totalPaid + nextPayment.totalDue) // We're done making payments. @@ -1764,8 +1974,7 @@ loanMakePayment( // Don't process an overpayment if the whole amount (or more!) // gets eaten by fees and interest. - if (overpaymentComponents.rawPrincipal > 0 && - overpaymentComponents.trackedPrincipalDelta > 0) + if (overpaymentComponents.trackedPrincipalDelta > 0) { XRPL_ASSERT_PARTS( overpaymentComponents.untrackedInterest >= beast::zero, diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index df6d00d60d..346acba918 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -451,7 +451,7 @@ LoanSet::doApply() if (std::int64_t const computedPayments{ properties.totalValueOutstanding / roundedPayment}; - computedPayments < paymentTotal) + computedPayments != paymentTotal) { JLOG(j_.warn()) << "Loan Periodic payment (" << properties.periodicPayment From ec60dcf90d69a024cb8dcc29f5d5d4055a05f46b Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 28 Oct 2025 15:56:57 -0400 Subject: [PATCH 171/291] Ensure payment does not exceed Loan balance - Also disallow extra parameters on the ALWAYS_OR_UNREACHABLE macro, which should prevent me from mixing up XRPL_ASSERT and XRPL_ASSERT_PARTS. --- include/xrpl/beast/utility/instrumentation.h | 2 +- .../app/ledger/detail/LedgerReplayTask.cpp | 2 +- src/xrpld/app/misc/detail/LendingHelpers.cpp | 44 ++++++++++++------- 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/include/xrpl/beast/utility/instrumentation.h b/include/xrpl/beast/utility/instrumentation.h index 3594855eef..cb7e3ffa0e 100644 --- a/include/xrpl/beast/utility/instrumentation.h +++ b/include/xrpl/beast/utility/instrumentation.h @@ -32,7 +32,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // The duplication is because Visual Studio 2019 cannot compile that header // even with the option -Zc:__cplusplus added. #define ALWAYS(cond, message, ...) assert((message) && (cond)) -#define ALWAYS_OR_UNREACHABLE(cond, message, ...) assert((message) && (cond)) +#define ALWAYS_OR_UNREACHABLE(cond, message) assert((message) && (cond)) #define SOMETIMES(cond, message, ...) #define REACHABLE(message, ...) #define UNREACHABLE(message, ...) assert((message) && false) diff --git a/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp b/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp index a9d2657f9b..a8847e59a9 100644 --- a/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp +++ b/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp @@ -299,7 +299,7 @@ LedgerReplayTask::addDelta(std::shared_ptr const& delta) deltas_.empty() || deltas_.back()->ledgerSeq_ + 1 == delta->ledgerSeq_, "ripple::LedgerReplayTask::addDelta : no deltas or consecutive " - "sequence", ); + "sequence"); deltas_.push_back(delta); } } diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index dc0eaf12d4..2fd61aa7ff 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -1111,15 +1111,15 @@ computePaymentComponents( // If there's only one payment left, we need to pay off each of the loan // parts. - XRPL_ASSERT( + XRPL_ASSERT_PARTS( deltas.valueDelta == totalValueOutstanding, "ripple::detail::computePaymentComponents", "last payment total value agrees"); - XRPL_ASSERT( + XRPL_ASSERT_PARTS( deltas.principalDelta == principalOutstanding, "ripple::detail::computePaymentComponents", "last payment principal agrees"); - XRPL_ASSERT( + XRPL_ASSERT_PARTS( deltas.managementFeeDueDelta == managementFeeOutstanding, "ripple::detail::computePaymentComponents", "last payment management fee agrees"); @@ -1227,14 +1227,6 @@ computePaymentComponents( "payment parts fit within payment limit"); #endif - // Make sure the parts don't add up to too much - Number shortage = roundedPeriodicPayment - deltas.valueDelta; - - XRPL_ASSERT_PARTS( - isRounded(asset, shortage, scale), - "ripple::detail::computePaymentComponents", - "excess is rounded"); - // The shortage must never be negative, which indicates that the parts are // trying to take more than the whole payment. The excess can be positive, // which indicates that we're not going to take the whole payment amount, @@ -1256,13 +1248,35 @@ computePaymentComponents( "ripple::detail::computePaymentComponents", "excess non-negative"); }; + auto addressExcess = [&takeFrom](LoanDeltas& deltas, Number& excess) { + takeFrom(deltas.valueDelta, deltas.interestDueDelta, excess); + takeFrom(deltas.valueDelta, deltas.managementFeeDueDelta, excess); + takeFrom(deltas.valueDelta, deltas.principalDelta, excess); + }; + Number totalOverpayment = + deltas.valueDelta - currentLedgerState.valueOutstanding; + if (totalOverpayment > 0) + { + // LCOV_EXCL_START + UNREACHABLE( + "ripple::detail::computePaymentComponents : payment exceeded loan " + "state"); + addressExcess(deltas, totalOverpayment); + // LCOV_EXCL_STOP + } + // Make sure the parts don't add up to too much + Number shortage = roundedPeriodicPayment - deltas.valueDelta; + + XRPL_ASSERT_PARTS( + isRounded(asset, shortage, scale), + "ripple::detail::computePaymentComponents", + "shortage is rounded"); + if (shortage < beast::zero) { Number excess = -shortage; - takeFrom(deltas.valueDelta, deltas.principalDelta, excess); - takeFrom(deltas.valueDelta, deltas.interestDueDelta, excess); - takeFrom(deltas.valueDelta, deltas.managementFeeDueDelta, excess); + addressExcess(deltas, excess); shortage = -excess; } @@ -1279,7 +1293,7 @@ computePaymentComponents( "ripple::detail::computePaymentComponents", "excess is extremely small"); - XRPL_ASSERT( + XRPL_ASSERT_PARTS( deltas.valueDelta == deltas.principalDelta + deltas.interestDueDelta + deltas.managementFeeDueDelta, From 0febc60cbe7f772a628c52bab7864178afb00406 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 28 Oct 2025 18:31:31 -0400 Subject: [PATCH 172/291] Ignore the contents of SigningPubKey when multisigning with a target --- src/xrpld/rpc/detail/TransactionSign.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/xrpld/rpc/detail/TransactionSign.cpp b/src/xrpld/rpc/detail/TransactionSign.cpp index aa7c706a19..9f4cec763b 100644 --- a/src/xrpld/rpc/detail/TransactionSign.cpp +++ b/src/xrpld/rpc/detail/TransactionSign.cpp @@ -1097,7 +1097,11 @@ checkMultiSignFields(Json::Value const& jvRequest) if (!tx_json.isMember(sfSigningPubKey.getJsonName())) return RPC::missing_field_error("tx_json.SigningPubKey"); - if (!tx_json[sfSigningPubKey.getJsonName()].asString().empty()) + // Multi-signing into a signature_target object field is fine, + // because it means the signature is not for the transaction + // Account. + if (!jvRequest.isMember(jss::signature_target) && + !tx_json[sfSigningPubKey.getJsonName()].asString().empty()) return RPC::make_error( rpcINVALID_PARAMS, "When multi-signing 'tx_json.SigningPubKey' must be empty."); From d0c60cdf5990f0c7845bcec81e4464e2d7fdf449 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 28 Oct 2025 18:46:02 -0400 Subject: [PATCH 173/291] Fix broken unit test case --- src/test/app/Loan_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 2042caabcd..17a7ea2aae 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -2025,7 +2025,7 @@ class Loan_test : public beast::unit_test::suite paymentComponents.trackedManagementFeeDelta); BEAST_EXPECT( paymentComponents.trackedValueDelta <= - state.periodicPayment); + roundedPeriodicPayment); BEAST_EXPECT( state.paymentRemaining < 12 || From 64e0ee4be9c5fe5d70ed2b1d2d8e9667885b43e0 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 29 Oct 2025 12:25:03 -0400 Subject: [PATCH 174/291] Add POC unit test for null vault dereference --- src/test/app/LoanBroker_test.cpp | 69 ++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 57bff0bbfa..d2a8a3656d 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -19,6 +19,8 @@ #include +#include + #include namespace ripple { @@ -1162,10 +1164,77 @@ class LoanBroker_test : public beast::unit_test::suite testLoanBroker({}, Set); } + void + testLoanBrokerCoverDepositNullVault() + { + // This test is lifted directly from + // https://bugs.immunefi.com/dashboard/submission/57808 + using namespace jtx; + Env env(*this); + + Account const alice{"alice"}; + env.fund(XRP(10000), alice); + env.close(); + + // Create a Vault owned by alice with an XRP asset + PrettyAsset const asset{xrpIssue(), 1}; + Vault vault{env}; + auto const [createTx, vaultKeylet] = + vault.create({.owner = alice, .asset = asset}); + env(createTx); + env.close(); + + // Predict LoanBroker key using alice's current sequence BEFORE submit + auto const brokerKeylet = + keylet::loanbroker(alice.id(), env.seq(alice)); + + // Create LoanBroker pointing to the vault + env(loanBroker::set(alice, vaultKeylet.key)); + env.close(); + + // Build the CoverDeposit STTx directly + STTx tx{ttLOAN_BROKER_COVER_DEPOSIT, [](STObject&) {}}; + tx.setAccountID(sfAccount, alice.id()); + tx.setFieldH256(sfLoanBrokerID, brokerKeylet.key); + tx.setFieldAmount(sfAmount, asset(1)); + + // Create a writable view cloned from the current ledger and remove the + // vault SLE + OpenView ov{*env.current()}; + test::StreamSink sink{beast::severities::kWarning}; + beast::Journal jlog{sink}; + ApplyContext ac{ + env.app(), + ov, + tx, + tesSUCCESS, + env.current()->fees().base, + tapNONE, + jlog}; + + if (auto sleBroker = + ac.view().peek(keylet::loanbroker(brokerKeylet.key))) + { + auto const vaultID = (*sleBroker)[sfVaultID]; + if (auto sleVault = ac.view().peek(keylet::vault(vaultID))) + { + ac.view().erase(sleVault); + } + } + + // Invoke preclaim against the mutated (ApplyView) view; triggers + // nullptr deref + PreclaimContext pctx{ + env.app(), ac.view(), tesSUCCESS, tx, tapNONE, jlog}; + (void)LoanBrokerCoverDeposit::preclaim(pctx); + } + public: void run() override { + testLoanBrokerCoverDepositNullVault(); + testDisabled(); testLifecycle(); testInvalidLoanBrokerCoverClawback(); From 52490730b28caf28615164497352b244473f2b90 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 29 Oct 2025 12:51:12 -0400 Subject: [PATCH 175/291] Check for null Vault SLE in LoanBrokerCoverDeposit --- src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp index c75dc95f98..7263628185 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp @@ -66,6 +66,11 @@ LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx) return tecNO_PERMISSION; } auto const vault = ctx.view.read(keylet::vault(sleBroker->at(sfVaultID))); + if (!vault) + { + JLOG(ctx.j.fatal()) << "Vault is missing for Broker " << brokerID; + return tefBAD_LEDGER; + } auto const vaultAsset = vault->at(sfAsset); if (amount.asset() != vaultAsset) From e8dddcbb7f3e1ffa3fd215dde332217eafa5ecef Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Thu, 30 Oct 2025 23:01:05 -0400 Subject: [PATCH 176/291] Fix LoanBrokerDelete with empty or zero broker ID (RIPD-3858) (#5967) --- src/test/app/LoanBroker_test.cpp | 18 ++++++++++++++++++ src/xrpld/app/tx/detail/LoanBrokerDelete.cpp | 3 +++ 2 files changed, 21 insertions(+) diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index d2a8a3656d..6f9cffbf1d 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -1015,6 +1015,24 @@ class LoanBroker_test : public beast::unit_test::suite sig(sfCounterpartySignature, alice), fee(env.current()->fees().base * 2)); + // preflight: temINVALID (empty broker id) + { + auto jv = del(alice, brokerKeylet.key); + jv[sfLoanBrokerID] = ""; + env(jv, ter(temINVALID)); + } + // preflight: temINVALID (zero broker id) + { + // needs a flag to distinguish the parsed STTx from the prior + // test + auto jv = del(alice, uint256{}, tfFullyCanonicalSig); + BEAST_EXPECT( + jv[sfLoanBrokerID] == + "0000000000000000000000000000000000000000000000000000000000" + "000000"); + env(jv, ter(temINVALID)); + } + // preclaim: tecHAS_OBLIGATIONS env(del(alice, brokerKeylet.key), ter(tecHAS_OBLIGATIONS)); } diff --git a/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp b/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp index 90703fa5b9..07e05ec073 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp @@ -32,6 +32,9 @@ LoanBrokerDelete::checkExtraFeatures(PreflightContext const& ctx) NotTEC LoanBrokerDelete::preflight(PreflightContext const& ctx) { + if (ctx.tx[sfLoanBrokerID] == beast::zero) + return temINVALID; + return tesSUCCESS; } From b472cc34933c1fbc739e967de5ed0ebce7433783 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Fri, 24 Oct 2025 15:32:47 +0100 Subject: [PATCH 177/291] Unit tests for multisigned LoanSet counterparty --- src/test/app/Loan_test.cpp | 66 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 17a7ea2aae..109cf5bed5 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -2742,6 +2742,72 @@ class Loan_test : public beast::unit_test::suite fee(env.current()->fees().base * 5)); }, CaseArgs{.requireAuth = true, .authorizeBorrower = true}); + + jtx::Account const alice{"alice"}; + jtx::Account const bella{"bella"}; + auto const msigSetup = [&](Env& env, Account const& account) { + Json::Value tx1 = signers(account, 2, {{alice, 1}, {bella, 1}}); + env(tx1); + env.close(); + }; + + testCase( + [&, this](Env& env, BrokerInfo const& broker, auto&) { + using namespace loan; + msigSetup(env, lender); + Number const principalRequest = broker.asset(1'000).value(); + + testcase( + "MPT authorized borrower, borrower submits, lender " + "multisign"); + env(set(borrower, broker.brokerID, principalRequest), + counterparty(lender), + msig(sfCounterpartySignature, alice, bella), + fee(env.current()->fees().base * 5)); + }, + [&, this](Env& env, BrokerInfo const& broker) { + using namespace loan; + msigSetup(env, lender); + Number const principalRequest = broker.asset(1'000).value(); + + testcase( + "IOU authorized borrower, borrower submits, lender " + "multisign"); + env(set(borrower, broker.brokerID, principalRequest), + counterparty(lender), + msig(sfCounterpartySignature, alice, bella), + fee(env.current()->fees().base * 5)); + }, + CaseArgs{.requireAuth = true, .authorizeBorrower = true}); + + testCase( + [&, this](Env& env, BrokerInfo const& broker, auto&) { + using namespace loan; + msigSetup(env, borrower); + Number const principalRequest = broker.asset(1'000).value(); + + testcase( + "MPT authorized borrower, lender submits, borrower " + "multisign"); + env(set(lender, broker.brokerID, principalRequest), + counterparty(borrower), + msig(sfCounterpartySignature, alice, bella), + fee(env.current()->fees().base * 5)); + }, + [&, this](Env& env, BrokerInfo const& broker) { + using namespace loan; + msigSetup(env, borrower); + Number const principalRequest = broker.asset(1'000).value(); + + testcase( + "IOU authorized borrower, lender submits, borrower " + "multisign"); + env(set(lender, broker.brokerID, principalRequest), + counterparty(borrower), + msig(sfCounterpartySignature, alice, bella), + fee(env.current()->fees().base * 5)); + }, + CaseArgs{.requireAuth = true, .authorizeBorrower = true}); } void From 4920e65b14e8f07e0505f0400e957be2c0d4c63e Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 3 Nov 2025 12:31:20 -0500 Subject: [PATCH 178/291] fix: LoanBrokerSet is malformed if Cover fields don't align - sfCoverRateMinimum and sfCoverRateLiquidation must be both zero or both non-zero, because both are used in the default amount calculations, which is the only place they're really meaningful. --- src/test/app/LoanBroker_test.cpp | 33 ++++++++++++++++------- src/test/app/Loan_test.cpp | 2 +- src/xrpld/app/tx/detail/LoanBrokerSet.cpp | 11 ++++++++ 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 6f9cffbf1d..4be39f4954 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -658,6 +658,8 @@ class LoanBroker_test : public beast::unit_test::suite using namespace loanBroker; + TenthBips32 const tenthBipsZero{0}; + auto badKeylet = keylet::vault(alice.id(), env.seq(alice)); // Try some failure cases // not the vault owner @@ -683,22 +685,35 @@ class LoanBroker_test : public beast::unit_test::suite env(set(evan, vault.vaultID), managementFeeRate(maxManagementFeeRate + TenthBips16(10)), ter(temINVALID)); - // sfCoverRateMinimum: good value, bad account + // sfCoverRateMinimum and sfCoverRateLiquidation are linked + // Cover: good value, bad account env(set(evan, vault.vaultID), coverRateMinimum(maxCoverRate), - ter(tecNO_PERMISSION)); - // sfCoverRateMinimum: too big - env(set(evan, vault.vaultID), - coverRateMinimum(maxCoverRate + 1), - ter(temINVALID)); - // sfCoverRateLiquidation: good value, bad account - env(set(evan, vault.vaultID), coverRateLiquidation(maxCoverRate), ter(tecNO_PERMISSION)); - // sfCoverRateLiquidation: too big + // Cover: too big env(set(evan, vault.vaultID), + coverRateMinimum(maxCoverRate + 1), coverRateLiquidation(maxCoverRate + 1), ter(temINVALID)); + // Cover: zero min, non-zero liquidation - implicit and + // explicit zero values. + env(set(evan, vault.vaultID), + coverRateLiquidation(maxCoverRate), + ter(temINVALID)); + env(set(evan, vault.vaultID), + coverRateMinimum(tenthBipsZero), + coverRateLiquidation(maxCoverRate), + ter(temINVALID)); + // Cover: non-zero min, zero liquidation - implicit and + // explicit zero values. + env(set(evan, vault.vaultID), + coverRateMinimum(maxCoverRate), + ter(temINVALID)); + env(set(evan, vault.vaultID), + coverRateMinimum(maxCoverRate), + coverRateLiquidation(tenthBipsZero), + ter(temINVALID)); // sfDebtMaximum: good value, bad account env(set(evan, vault.vaultID), debtMaximum(Number(0)), diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 109cf5bed5..ebe8d8e608 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -4499,7 +4499,7 @@ class Loan_test : public beast::unit_test::suite env.close(); BrokerInfo broker{ - createVaultAndBroker(env, iouAsset, lender, Number(0), 0)}; + createVaultAndBroker(env, iouAsset, lender, Number(0), 1)}; using namespace loan; diff --git a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp index 72f60053f2..4b9ffff291 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp @@ -56,6 +56,17 @@ LoanBrokerSet::preflight(PreflightContext const& ctx) return temINVALID; } + { + auto const minimumZero = tx[~sfCoverRateMinimum].value_or(0) == 0; + auto const liquidationZero = + tx[~sfCoverRateLiquidation].value_or(0) == 0; + // Both must be zero or non-zero. + if (minimumZero != liquidationZero) + { + return temINVALID; + } + } + return tesSUCCESS; } From cdd6ad4ba4124038143589864ba1fc18af3e0726 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 3 Nov 2025 17:50:53 -0500 Subject: [PATCH 179/291] test: Fix test set up, since the order of preclaim checks changed - Need to have multisign set up to get to the fee check --- src/test/app/Loan_test.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index ebe8d8e608..a0fdf9eb4e 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -1146,12 +1146,30 @@ class Loan_test : public beast::unit_test::suite sig(sfCounterpartySignature, lender), ter(telINSUF_FEE_P)); // insufficient fee - multisign + env(signers(lender, 2, {{evan, 1}, {borrower, 1}})); + env(signers(borrower, 2, {{evan, 1}, {lender, 1}})); env(set(borrower, broker.brokerID, principalRequest), counterparty(lender), msig(evan, lender), msig(sfCounterpartySignature, evan, borrower), fee(env.current()->fees().base * 5 - 1), ter(telINSUF_FEE_P)); + // Bad multisign signatures for borrower (Account) + env(set(borrower, broker.brokerID, principalRequest), + counterparty(lender), + msig(alice, issuer), + msig(sfCounterpartySignature, evan, borrower), + fee(env.current()->fees().base * 5), + ter(tefBAD_SIGNATURE)); + // Bad multisign signatures for issuer (Counterparty) + env(set(borrower, broker.brokerID, principalRequest), + counterparty(lender), + msig(evan, lender), + msig(sfCounterpartySignature, alice, issuer), + fee(env.current()->fees().base * 5 - 1), + ter(tefBAD_SIGNATURE)); + env(signers(lender, none)); + env(signers(borrower, none)); // multisign sufficient fee, but no signers set up env(set(borrower, broker.brokerID, principalRequest), counterparty(lender), From 7925cc4052d15831b64746e6d2db700160351a9c Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 3 Nov 2025 20:39:01 -0500 Subject: [PATCH 180/291] fix: Check the borrower's balance in LoanSet for reserve - Previously, the submitting Account's balance was checked, which could have been the borrower or the lender. --- src/xrpld/app/tx/detail/LoanSet.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 346acba918..d8b0441b21 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -503,8 +503,11 @@ LoanSet::doApply() adjustOwnerCount(view, borrowerSle, 1, j_); { - auto ownerCount = borrowerSle->at(sfOwnerCount); - if (mPriorBalance < view.fees().accountReserve(ownerCount)) + auto const ownerCount = borrowerSle->at(sfOwnerCount); + auto const balance = account_ == borrower + ? mPriorBalance + : borrowerSle->at(sfBalance)->xrp(); + if (balance < view.fees().accountReserve(ownerCount)) return tecINSUFFICIENT_RESERVE; } From aed8e2b166a75fe7892fa61bd222fcf1af931053 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 4 Nov 2025 17:56:16 -0500 Subject: [PATCH 181/291] Fill in payment computation shortages (#5941) - Ensures a consistent fixed payment amount for the entire life of the loan, except the final payment, which is guaranteed to be the same or smaller. - Convert some Loan structs to compute values that had need manual updates to stay consistent. - Fail the transaction in `LoanPay` if it violates the Vault `assetsAvailable <= assetsTotal` invariant. - Use constexpr to check that min mantissa value for Number and STAmount is a power of 10, and compute the max in terms of the min. - Improve unit tests: - Use BrokerParameters and Loan Parameters instead of semi-global class values - In tests, check that the expected number of loan payments are made. - Add LoanBatch manual test to generate a set number of random loans, set them up, and pay them off. - Add LoanArbitrary manual test to run a single test with specific (hard-coded for now) parameters. - Add Number support to XRP_t. --- include/xrpl/basics/Number.h | 34 +- include/xrpl/protocol/Protocol.h | 2 + include/xrpl/protocol/STAmount.h | 18 +- src/libxrpl/basics/Number.cpp | 20 +- src/test/app/Loan_test.cpp | 1593 ++++++++++++++++-- src/test/jtx/Env.h | 4 +- src/test/jtx/amount.h | 15 + src/xrpld/app/misc/LendingHelpers.h | 35 +- src/xrpld/app/misc/detail/LendingHelpers.cpp | 226 +-- src/xrpld/app/tx/detail/LoanPay.cpp | 38 + src/xrpld/app/tx/detail/LoanSet.cpp | 155 +- src/xrpld/app/tx/detail/LoanSet.h | 10 + 12 files changed, 1768 insertions(+), 382 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index 2911d06110..60ea23a0a5 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -32,6 +32,15 @@ class Number; std::string to_string(Number const& amount); +template +constexpr bool +isPowerOfTen(T value) +{ + while (value >= 10 && value % 10 == 0) + value /= 10; + return value == 1; +} + class Number { using rep = std::int64_t; @@ -41,7 +50,9 @@ class Number public: // The range for the mantissa when normalized constexpr static std::int64_t minMantissa = 1'000'000'000'000'000LL; - constexpr static std::int64_t maxMantissa = 9'999'999'999'999'999LL; + static_assert(isPowerOfTen(minMantissa)); + constexpr static std::int64_t maxMantissa = minMantissa * 10 - 1; + static_assert(maxMantissa == 9'999'999'999'999'999LL); // The range for the exponent when normalized constexpr static int minExponent = -32768; @@ -58,8 +69,6 @@ public: explicit Number(rep mantissa, int exponent); explicit constexpr Number(rep mantissa, int exponent, unchecked) noexcept; - static Number const zero; - constexpr rep mantissa() const noexcept; constexpr int @@ -153,22 +162,7 @@ public: } Number - truncate() const noexcept - { - if (exponent_ >= 0 || mantissa_ == 0) - return *this; - - Number ret = *this; - while (ret.exponent_ < 0 && ret.mantissa_ != 0) - { - ret.exponent_ += 1; - ret.mantissa_ /= rep(10); - } - // We are guaranteed that normalize() will never throw an exception - // because exponent is either negative or zero at this point. - ret.normalize(); - return ret; - } + truncate() const noexcept; friend constexpr bool operator>(Number const& x, Number const& y) noexcept @@ -213,6 +207,8 @@ private: class Guard; }; +constexpr static Number numZero{}; + inline constexpr Number::Number(rep mantissa, int exponent, unchecked) noexcept : mantissa_{mantissa}, exponent_{exponent} { diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 5c443d387d..c8e0f77d7d 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -102,7 +102,9 @@ std::uint16_t constexpr maxTransferFee = 50000; * Example: 50% is 0.50 * bipsPerUnity = 5,000 bps. */ Bips32 constexpr bipsPerUnity(100 * 100); +static_assert(bipsPerUnity == Bips32{10'000}); TenthBips32 constexpr tenthBipsPerUnity(bipsPerUnity.value() * 10); +static_assert(tenthBipsPerUnity == TenthBips32(100'000)); constexpr Bips32 percentageToBips(std::uint32_t percentage) diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index 796a1fb930..fc1835fe72 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -66,16 +66,18 @@ public: static int const cMaxOffset = 80; // Maximum native value supported by the code - static std::uint64_t const cMinValue = 1'000'000'000'000'000ull; - static std::uint64_t const cMaxValue = 9'999'999'999'999'999ull; - static std::uint64_t const cMaxNative = 9'000'000'000'000'000'000ull; + constexpr static std::uint64_t cMinValue = 1'000'000'000'000'000ull; + static_assert(isPowerOfTen(cMinValue)); + constexpr static std::uint64_t cMaxValue = cMinValue * 10 - 1; + static_assert(cMaxValue == 9'999'999'999'999'999ull); + constexpr static std::uint64_t cMaxNative = 9'000'000'000'000'000'000ull; // Max native value on network. - static std::uint64_t const cMaxNativeN = 100'000'000'000'000'000ull; - static std::uint64_t const cIssuedCurrency = 0x8'000'000'000'000'000ull; - static std::uint64_t const cPositive = 0x4'000'000'000'000'000ull; - static std::uint64_t const cMPToken = 0x2'000'000'000'000'000ull; - static std::uint64_t const cValueMask = ~(cPositive | cMPToken); + constexpr static std::uint64_t cMaxNativeN = 100'000'000'000'000'000ull; + constexpr static std::uint64_t cIssuedCurrency = 0x8'000'000'000'000'000ull; + constexpr static std::uint64_t cPositive = 0x4'000'000'000'000'000ull; + constexpr static std::uint64_t cMPToken = 0x2'000'000'000'000'000ull; + constexpr static std::uint64_t cValueMask = ~(cPositive | cMPToken); static std::uint64_t const uRateOne; diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 7c3ea2d3cc..228def4720 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -43,8 +43,6 @@ namespace ripple { thread_local Number::rounding_mode Number::mode_ = Number::to_nearest; -Number const Number::zero{}; - Number::rounding_mode Number::getround() { @@ -523,6 +521,24 @@ Number::operator rep() const return drops; } +Number +Number::truncate() const noexcept +{ + if (exponent_ >= 0 || mantissa_ == 0) + return *this; + + Number ret = *this; + while (ret.exponent_ < 0 && ret.mantissa_ != 0) + { + ret.exponent_ += 1; + ret.mantissa_ /= rep(10); + } + // We are guaranteed that normalize() will never throw an exception + // because exponent is either negative or zero at this point. + ret.normalize(); + return ret; +} + std::string to_string(Number const& amount) { diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index a0fdf9eb4e..261a139c72 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -17,6 +17,8 @@ */ //============================================================================== +#include +// #include #include @@ -24,7 +26,7 @@ #include #include -#include +#include #include namespace ripple { @@ -32,21 +34,13 @@ namespace test { class Loan_test : public beast::unit_test::suite { +protected: // Ensure that all the features needed for Lending Protocol are included, // even if they are set to unsupported. FeatureBitset const all{ jtx::testable_amendments() | featureMPTokensV1 | featureSingleAssetVault | featureLendingProtocol}; - static constexpr auto const coverDepositParameter = 1000; - static constexpr auto const coverRateMinParameter = - percentageToTenthBips(10); - static constexpr auto const coverRateLiquidationParameter = - percentageToTenthBips(25); - static constexpr auto const maxCoveredLoanValue = 1000 * 100 / 10; - static constexpr auto const vaultDeposit = 1'000'000; - static constexpr auto const debtMaximumParameter = 25'000; - static constexpr TenthBips16 const managementFeeRateParameter{100}; std::string const iouCurrency{"IOU"}; void @@ -98,23 +92,52 @@ class Loan_test : public beast::unit_test::suite failAll(all - featureLendingProtocol); } + struct BrokerParameters + { + Number vaultDeposit = 1'000'000; + Number debtMax = 25'000; + TenthBips32 coverRateMin = percentageToTenthBips(10); + int coverDeposit = 1000; + TenthBips16 managementFeeRate{100}; + TenthBips32 coverRateLiquidation = percentageToTenthBips(25); + std::string data{}; + + Number + maxCoveredLoanValue(Number const& currentDebt) const + { + auto debtLimit = + coverDeposit * tenthBipsPerUnity.value() / coverRateMin.value(); + + return debtLimit - currentDebt; + } + + static BrokerParameters const& + defaults() + { + static BrokerParameters const result{}; + return result; + } + + // TODO: create an operator() which returns a transaction similar to + // LoanParameters + }; + struct BrokerInfo { jtx::PrettyAsset asset; uint256 brokerID; - TenthBips16 managementFeeRate; + BrokerParameters params; BrokerInfo( jtx::PrettyAsset const& asset_, uint256 const& brokerID_, - TenthBips16 mgmtRate) - : asset(asset_), brokerID(brokerID_), managementFeeRate(mgmtRate) + BrokerParameters const& p) + : asset(asset_), brokerID(brokerID_), params(p) { } }; struct LoanParameters { - BrokerInfo broker; // The account submitting the transaction. May be borrower or broker. jtx::Account account; // The counterparty. Should be the other of borrower or broker. @@ -123,42 +146,50 @@ class Loan_test : public beast::unit_test::suite // only signs. bool counterpartyExplicit = true; Number principalRequest; - STAmount setFee; - std::optional originationFee; - std::optional serviceFee; - std::optional lateFee; - std::optional closeFee; - std::optional overFee; - std::optional interest; - std::optional lateInterest; - std::optional closeInterest; - std::optional overpaymentInterest; - std::optional payTotal; - std::optional payInterval; - std::optional gracePd; - std::optional flags; + std::optional setFee{}; + std::optional originationFee{}; + std::optional serviceFee{}; + std::optional lateFee{}; + std::optional closeFee{}; + std::optional overFee{}; + std::optional interest{}; + std::optional lateInterest{}; + std::optional closeInterest{}; + std::optional overpaymentInterest{}; + std::optional payTotal{}; + std::optional payInterval{}; + std::optional gracePd{}; + std::optional flags{}; template jtx::JTx - operator()(jtx::Env& env, FN const&... fN) const + operator()(jtx::Env& env, BrokerInfo const& broker, FN const&... fN) + const { using namespace jtx; using namespace jtx::loan; JTx jt{loan::set( - account, broker.brokerID, principalRequest, flags.value_or(0))}; + account, + broker.brokerID, + broker.asset(principalRequest).number(), + flags.value_or(0))}; + sig(sfCounterpartySignature, counter)(env, jt); - fee{setFee}(env, jt); + + fee{setFee.value_or(env.current()->fees().base * 2)}(env, jt); + if (counterpartyExplicit) counterparty(counter)(env, jt); if (originationFee) - loanOriginationFee (*originationFee)(env, jt); + loanOriginationFee(broker.asset(*originationFee).number())( + env, jt); if (serviceFee) - loanServiceFee (*serviceFee)(env, jt); + loanServiceFee(broker.asset(*serviceFee).number())(env, jt); if (lateFee) - latePaymentFee (*lateFee)(env, jt); + latePaymentFee(broker.asset(*lateFee).number())(env, jt); if (closeFee) - closePaymentFee (*closeFee)(env, jt); + closePaymentFee(broker.asset(*closeFee).number())(env, jt); if (overFee) overpaymentFee (*overFee)(env, jt); if (interest) @@ -204,21 +235,18 @@ class Loan_test : public beast::unit_test::suite public: jtx::Env const& env; BrokerInfo const& broker; - Number const& loanAmount; jtx::Account const& pseudoAccount; - Keylet const& keylet; + Keylet const& loanKeylet; VerifyLoanStatus( jtx::Env const& env_, BrokerInfo const& broker_, - Number const& loanAmount_, jtx::Account const& pseudo_, Keylet const& keylet_) : env(env_) , broker(broker_) - , loanAmount(loanAmount_) , pseudoAccount(pseudo_) - , keylet(keylet_) + , loanKeylet(keylet_) { } @@ -316,7 +344,7 @@ class Loan_test : public beast::unit_test::suite std::uint32_t flags) const { using namespace jtx; - if (auto loan = env.le(keylet); env.test.BEAST_EXPECT(loan)) + if (auto loan = env.le(loanKeylet); env.test.BEAST_EXPECT(loan)) { env.test.BEAST_EXPECT( loan->at(sfPreviousPaymentDate) == previousPaymentDate); @@ -398,22 +426,17 @@ class Loan_test : public beast::unit_test::suite jtx::Env& env, jtx::PrettyAsset const& asset, jtx::Account const& lender, - std::optional debtMax = std::nullopt, - std::optional coverRateMin = std::nullopt) + BrokerParameters const& params = BrokerParameters::defaults()) { using namespace jtx; Vault vault{env}; - auto const deposit = asset(vaultDeposit); - auto const debtMaximumValue = debtMax - ? STAmount{asset.raw(), *debtMax} - : asset(debtMaximumParameter).value(); - auto const coverDepositValue = asset(coverDepositParameter).value(); + auto const deposit = asset(params.vaultDeposit); + auto const debtMaximumValue = asset(params.debtMax).value(); + auto const coverDepositValue = asset(params.coverDeposit).value(); - auto const coverRateMinValue = coverRateMin - ? TenthBips32(*coverRateMin) - : TenthBips32(coverRateMinParameter); + auto const coverRateMinValue = params.coverRateMin; auto [tx, vaultKeylet] = vault.create({.owner = lender, .asset = asset}); @@ -431,21 +454,20 @@ class Loan_test : public beast::unit_test::suite } auto const keylet = keylet::loanbroker(lender.id(), env.seq(lender)); - auto const testData = "spam spam spam spam"; using namespace loanBroker; env(set(lender, vaultKeylet.key), - data(testData), - managementFeeRate(managementFeeRateParameter), + data(params.data), + managementFeeRate(params.managementFeeRate), debtMaximum(debtMaximumValue), coverRateMinimum(coverRateMinValue), - coverRateLiquidation(TenthBips32(coverRateLiquidationParameter))); + coverRateLiquidation(TenthBips32(params.coverRateLiquidation))); env(coverDeposit(lender, keylet.key, coverDepositValue)); env.close(); - return {asset, keylet.key, managementFeeRateParameter}; + return {asset, keylet.key, params}; } /// Get the state without checking anything @@ -514,7 +536,7 @@ class Loan_test : public beast::unit_test::suite computeFee( broker.asset, state.totalValue - state.principalOutstanding, - managementFeeRateParameter, + broker.params.managementFeeRate, state.loanScale)); verifyLoanStatus(state); @@ -597,7 +619,7 @@ class Loan_test : public beast::unit_test::suite }(); VerifyLoanStatus const verifyLoanStatus( - env, broker, loanAmount, pseudoAcct, keylet); + env, broker, pseudoAcct, keylet); // No loans yet verifyLoanStatus.checkBroker(0, 0, TenthBips32{0}, 1, 0, 0); @@ -611,15 +633,6 @@ class Loan_test : public beast::unit_test::suite using namespace loan; using namespace std::chrono_literals; - auto const borrowerOwnerCount = env.ownerCount(borrower); - - auto const loanSetFee = env.current()->fees().base * 2; - Number const principalRequest = broker.asset(loanAmount).value(); - auto const originationFee = broker.asset(1).value(); - auto const serviceFee = broker.asset(2).value(); - auto const lateFee = broker.asset(3).value(); - auto const closeFee = broker.asset(4).value(); - auto applyExponent = [interestExponent, this](TenthBips32 value) mutable { BEAST_EXPECT(value > TenthBips32(0)); @@ -640,43 +653,43 @@ class Loan_test : public beast::unit_test::suite return value; }; - auto const overFee = applyExponent(percentageToTenthBips(5) / 10); - auto const interest = applyExponent(percentageToTenthBips(12)); - // 2.4% - auto const lateInterest = applyExponent(percentageToTenthBips(24) / 10); - auto const closeInterest = - applyExponent(percentageToTenthBips(36) / 10); - auto const overpaymentInterest = - applyExponent(percentageToTenthBips(48) / 10); - auto const total = 12; - auto const interval = 600; - auto const grace = 60; + auto const borrowerOwnerCount = env.ownerCount(borrower); - auto const borrowerStartbalance = env.balance(borrower, broker.asset); - - // Use the defined values + auto const loanSetFee = env.current()->fees().base * 2; LoanParameters const loanParams{ - .broker = broker, .account = borrower, .counter = lender, .counterpartyExplicit = false, - .principalRequest = principalRequest, + .principalRequest = loanAmount, .setFee = loanSetFee, - .originationFee = originationFee, - .serviceFee = serviceFee, - .lateFee = lateFee, - .closeFee = closeFee, - .overFee = overFee, - .interest = interest, - .lateInterest = lateInterest, - .closeInterest = closeInterest, - .overpaymentInterest = overpaymentInterest, - .payTotal = total, - .payInterval = interval, - .gracePd = grace, + .originationFee = 1, + .serviceFee = 2, + .lateFee = 3, + .closeFee = 4, + .overFee = applyExponent(percentageToTenthBips(5) / 10), + .interest = applyExponent(percentageToTenthBips(12)), + // 2.4% + .lateInterest = applyExponent(percentageToTenthBips(24) / 10), + .closeInterest = applyExponent(percentageToTenthBips(36) / 10), + .overpaymentInterest = + applyExponent(percentageToTenthBips(48) / 10), + .payTotal = 12, + .payInterval = 600, + .gracePd = 60, .flags = flags, }; - auto createJtx = loanParams(env); + Number const principalRequestAmount = + broker.asset(loanParams.principalRequest).value(); + auto const originationFeeAmount = + broker.asset(*loanParams.originationFee).value(); + auto const serviceFeeAmount = + broker.asset(*loanParams.serviceFee).value(); + auto const lateFeeAmount = broker.asset(*loanParams.lateFee).value(); + auto const closeFeeAmount = broker.asset(*loanParams.closeFee).value(); + + auto const borrowerStartbalance = env.balance(borrower, broker.asset); + + auto createJtx = loanParams(env, broker); #if LOANCOMPLETE { auto createJtxOld = env.jt( @@ -723,8 +736,8 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT( env.balance(borrower, broker.asset).value() == - borrowerStartbalance.value() + principalRequest - - originationFee - adjustment.value()); + borrowerStartbalance.value() + principalRequestAmount - + originationFeeAmount - adjustment.value()); } auto const loanFlags = createJtx.stx->isFlag(tfLoanOverpayment) @@ -741,27 +754,35 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT(loan->at(sfLoanSequence) == loanSequence); BEAST_EXPECT(loan->at(sfBorrower) == borrower.id()); BEAST_EXPECT(loan->at(sfLoanBrokerID) == broker.brokerID); - BEAST_EXPECT(loan->at(sfLoanOriginationFee) == originationFee); - BEAST_EXPECT(loan->at(sfLoanServiceFee) == serviceFee); - BEAST_EXPECT(loan->at(sfLatePaymentFee) == lateFee); - BEAST_EXPECT(loan->at(sfClosePaymentFee) == closeFee); - BEAST_EXPECT(loan->at(sfOverpaymentFee) == overFee); - BEAST_EXPECT(loan->at(sfInterestRate) == interest); - BEAST_EXPECT(loan->at(sfLateInterestRate) == lateInterest); - BEAST_EXPECT(loan->at(sfCloseInterestRate) == closeInterest); BEAST_EXPECT( - loan->at(sfOverpaymentInterestRate) == overpaymentInterest); + loan->at(sfLoanOriginationFee) == originationFeeAmount); + BEAST_EXPECT(loan->at(sfLoanServiceFee) == serviceFeeAmount); + BEAST_EXPECT(loan->at(sfLatePaymentFee) == lateFeeAmount); + BEAST_EXPECT(loan->at(sfClosePaymentFee) == closeFeeAmount); + BEAST_EXPECT(loan->at(sfOverpaymentFee) == *loanParams.overFee); + BEAST_EXPECT(loan->at(sfInterestRate) == *loanParams.interest); + BEAST_EXPECT( + loan->at(sfLateInterestRate) == *loanParams.lateInterest); + BEAST_EXPECT( + loan->at(sfCloseInterestRate) == *loanParams.closeInterest); + BEAST_EXPECT( + loan->at(sfOverpaymentInterestRate) == + *loanParams.overpaymentInterest); BEAST_EXPECT(loan->at(sfStartDate) == startDate); - BEAST_EXPECT(loan->at(sfPaymentInterval) == interval); - BEAST_EXPECT(loan->at(sfGracePeriod) == grace); + BEAST_EXPECT( + loan->at(sfPaymentInterval) == *loanParams.payInterval); + BEAST_EXPECT(loan->at(sfGracePeriod) == *loanParams.gracePd); BEAST_EXPECT(loan->at(sfPreviousPaymentDate) == 0); BEAST_EXPECT( - loan->at(sfNextPaymentDueDate) == startDate + interval); - BEAST_EXPECT(loan->at(sfPaymentRemaining) == total); + loan->at(sfNextPaymentDueDate) == + startDate + *loanParams.payInterval); + BEAST_EXPECT(loan->at(sfPaymentRemaining) == *loanParams.payTotal); BEAST_EXPECT( loan->at(sfLoanScale) == - (broker.asset.integral() ? 0 : principalRequest.exponent())); - BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == principalRequest); + (broker.asset.integral() ? 0 + : principalRequestAmount.exponent())); + BEAST_EXPECT( + loan->at(sfPrincipalOutstanding) == principalRequestAmount); } auto state = getCurrentState(env, broker, keylet, verifyLoanStatus); @@ -772,15 +793,15 @@ class Loan_test : public beast::unit_test::suite state.interestRate, state.paymentInterval, state.paymentRemaining, - managementFeeRateParameter); + broker.params.managementFeeRate); verifyLoanStatus( 0, - startDate + interval, - total, - broker.asset.integral() ? 0 : principalRequest.exponent(), + startDate + *loanParams.payInterval, + *loanParams.payTotal, + broker.asset.integral() ? 0 : principalRequestAmount.exponent(), loanProperties.totalValueOutstanding, - principalRequest, + principalRequestAmount, loanProperties.managementFeeOwedToBroker, loanProperties.periodicPayment, loanFlags | 0); @@ -827,17 +848,17 @@ class Loan_test : public beast::unit_test::suite env(manage(lender, keylet.key, tfLoanUnimpair), canImpair ? ter(tesSUCCESS) : ter(tecNO_PERMISSION)); - auto const nextDueDate = startDate + interval; + auto const nextDueDate = startDate + *loanParams.payInterval; env.close(); verifyLoanStatus( 0, nextDueDate, - total, - broker.asset.integral() ? 0 : principalRequest.exponent(), + *loanParams.payTotal, + broker.asset.integral() ? 0 : principalRequestAmount.exponent(), loanProperties.totalValueOutstanding, - principalRequest, + principalRequestAmount, loanProperties.managementFeeOwedToBroker, loanProperties.periodicPayment, loanFlags | 0); @@ -888,7 +909,7 @@ class Loan_test : public beast::unit_test::suite } // No loans left - verifyLoanStatus.checkBroker(0, 0, interest, 1, 0, 0); + verifyLoanStatus.checkBroker(0, 0, *loanParams.interest, 1, 0, 0); BEAST_EXPECT( env.balance(borrower, broker.asset).value() == @@ -902,6 +923,16 @@ class Loan_test : public beast::unit_test::suite } } + std::string + getCurrencyLabel(Asset const& asset) + { + return ( + asset.native() ? "XRP" + : asset.holds() ? "IOU" + : asset.holds() ? "MPT" + : "Unknown"); + } + /** Wrapper to run a series of lifecycle tests for a given asset and loan * amount * @@ -923,13 +954,10 @@ class Loan_test : public beast::unit_test::suite using namespace jtx; auto const& asset = broker.asset.raw(); + auto const currencyLabel = getCurrencyLabel(asset); auto const caseLabel = [&]() { std::stringstream ss; - ss << "Lifecycle: " << loanAmount << " " - << (asset.native() ? "XRP" - : asset.holds() ? "IOU" - : asset.holds() ? "MPT" - : "Unknown") + ss << "Lifecycle: " << loanAmount << " " << currencyLabel << " Scale interest to: " << interestExponent << " "; return ss.str(); }(); @@ -952,11 +980,14 @@ class Loan_test : public beast::unit_test::suite Account const alice{"alice"}; Number const principalRequest = broker.asset(loanAmount).value(); + Number const maxCoveredLoanValue = broker.params.maxCoveredLoanValue(0); + BEAST_EXPECT(maxCoveredLoanValue == 1000 * 100 / 10); Number const maxCoveredLoanRequest = broker.asset(maxCoveredLoanValue).value(); - Number const totalVaultRequest = broker.asset(vaultDeposit).value(); + Number const totalVaultRequest = + broker.asset(broker.params.vaultDeposit).value(); Number const debtMaximumRequest = - broker.asset(debtMaximumParameter).value(); + broker.asset(broker.params.debtMax).value(); auto const loanSetFee = fee(env.current()->fees().base * 2); @@ -1392,8 +1423,8 @@ class Loan_test : public beast::unit_test::suite tenthBipsOfValue( tenthBipsOfValue( brokerSle->at(sfDebtTotal), - coverRateMinParameter), - coverRateLiquidationParameter), + broker.params.coverRateMin), + broker.params.coverRateLiquidation), state.totalValue - state.managementFeeOutstanding), state.loanScale); return std::make_pair(defaultAmount, brokerSle->at(sfOwner)); @@ -1432,7 +1463,7 @@ class Loan_test : public beast::unit_test::suite auto const& broker = verifyLoanStatus.broker; auto const startingCoverAvailable = coverAvailable( broker.brokerID, - broker.asset(coverDepositParameter).number()); + broker.asset(broker.params.coverDeposit).number()); if (impair) { @@ -1892,7 +1923,7 @@ class Loan_test : public beast::unit_test::suite broker.asset, state.periodicPayment, state.loanScale)}; testcase - << "\tPayment components: " + << currencyLabel << " Payment components: " << "Payments remaining, rawInterest, rawPrincipal, " "rawMFee, trackedValueDelta, trackedPrincipalDelta, " "trackedInterestDelta, trackedMgmtFeeDelta, special"; @@ -1927,19 +1958,20 @@ class Loan_test : public beast::unit_test::suite state.periodicPayment, periodicRate, state.paymentRemaining, - managementFeeRateParameter); + broker.params.managementFeeRate); auto const rounded = calculateRoundedLoanState( state.totalValue, state.principalOutstanding, state.managementFeeOutstanding); testcase - << "\tLoan starting state: " << state.paymentRemaining + << currencyLabel + << " Loan starting state: " << state.paymentRemaining << ", " << raw.interestDue << ", " << raw.principalOutstanding << ", " << raw.managementFeeDue << ", " << rounded.valueOutstanding << ", " << rounded.principalOutstanding << ", " - << rounded.interestOutstanding << ", " + << rounded.interestDue << ", " << rounded.managementFeeDue; } @@ -1963,12 +1995,13 @@ class Loan_test : public beast::unit_test::suite .trackedPrincipalDelta = 0, .trackedManagementFeeDelta = 0}; Number totalInterestPaid = 0; + std::size_t totalPaymentsMade = 0; ripple::LoanState currentTrueState = calculateRawLoanState( state.periodicPayment, periodicRate, state.paymentRemaining, - managementFeeRateParameter); + broker.params.managementFeeRate); while (state.paymentRemaining > 0) { @@ -1983,23 +2016,28 @@ class Loan_test : public beast::unit_test::suite state.periodicPayment, periodicRate, state.paymentRemaining, - managementFeeRateParameter); + broker.params.managementFeeRate); BEAST_EXPECT( - paymentComponents.trackedValueDelta <= - roundedPeriodicPayment); + paymentComponents.trackedValueDelta == + roundedPeriodicPayment || + (paymentComponents.specialCase == + detail::PaymentSpecialCase::final && + paymentComponents.trackedValueDelta < + roundedPeriodicPayment)); ripple::LoanState const nextTrueState = calculateRawLoanState( state.periodicPayment, periodicRate, state.paymentRemaining - 1, - managementFeeRateParameter); + broker.params.managementFeeRate); detail::LoanDeltas const deltas = currentTrueState - nextTrueState; testcase - << "\tPayment components: " << state.paymentRemaining + << currencyLabel + << " Payment components: " << state.paymentRemaining << ", " << deltas.interestDueDelta << ", " << deltas.principalDelta << ", " << deltas.managementFeeDueDelta << ", " @@ -2034,7 +2072,7 @@ class Loan_test : public beast::unit_test::suite (diff > beast::zero && ((broker.asset.integral() && (static_cast(diff) < 3)) || - (totalDue.exponent() - diff.exponent() > 8)))); + (state.loanScale - diff.exponent() > 13)))); BEAST_EXPECT( paymentComponents.trackedValueDelta == @@ -2132,6 +2170,7 @@ class Loan_test : public beast::unit_test::suite paymentComponents.trackedManagementFeeDelta; totalInterestPaid += paymentComponents.trackedInterestPart(); + ++totalPaymentsMade; currentTrueState = nextTrueState; } @@ -2156,6 +2195,8 @@ class Loan_test : public beast::unit_test::suite initialState.totalValue - (initialState.principalOutstanding + initialState.managementFeeOutstanding)); + BEAST_EXPECT( + totalPaymentsMade == initialState.paymentRemaining); // Can't impair or default a paid off loan env(manage(lender, loanKeylet.key, tfLoanImpair), @@ -2164,9 +2205,17 @@ class Loan_test : public beast::unit_test::suite ter(tecNO_PERMISSION)); }); -#if LOANCOMPLETE +#if LOANTODO // TODO + /* + LoanPay fails with tecINVARIANT_FAILED error when loan_broker(also + borrower) tries to do the payment. Here's the sceanrio: Create a XRP + loan with loan broker as borrower, loan origination fee and loan service + fee. Loan broker makes the first payment with periodic payment and loan + service fee. + */ + auto time = [&](std::string label, std::function timed) { if (!BEAST_EXPECT(timed)) return; @@ -2886,7 +2935,11 @@ class Loan_test : public beast::unit_test::suite std::vector brokers; for (auto const& asset : assets) { - brokers.emplace_back(createVaultAndBroker(env, asset, lender)); + brokers.emplace_back(createVaultAndBroker( + env, + asset, + lender, + BrokerParameters{.data = "spam spam spam spam"})); } // Create and update Loans @@ -2930,6 +2983,309 @@ class Loan_test : public beast::unit_test::suite } } + void + makeLoanPayments( + jtx::Env& env, + BrokerInfo const& broker, + LoanParameters const& loanParams, + Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus) + { + // Make all the individual payments + using namespace jtx; + using namespace jtx::loan; + using namespace std::chrono_literals; + using d = NetClock::duration; + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + // Account const evan{"evan"}; + // Account const alice{"alice"}; + + auto const currencyLabel = getCurrencyLabel(broker.asset); + + auto const baseFee = env.current()->fees().base; + + env.close(); + auto state = getCurrentState(env, broker, loanKeylet); + + verifyLoanStatus(state); + + STAmount const serviceFee = + broker.asset(loanParams.serviceFee.value_or(0)); + + // Ensure the borrower has enough funds to make the payments (including + // tx fees, if necessary) + { + auto const borrowerBalance = env.balance(borrower, broker.asset); + + // Add extra for transaction fees and reserves, if appropriate, or a + // tiny amount for the extra paid in each transaction + auto const totalNeeded = state.totalValue + + (serviceFee * state.paymentRemaining) + + (broker.asset.native() + ? Number( + baseFee * state.paymentRemaining + + env.current()->fees().accountReserve( + env.ownerCount(borrower))) + : broker.asset(15).number()); + + auto const shortage = totalNeeded - borrowerBalance.number(); + + if (shortage > beast::zero) + env( + pay((broker.asset.native() ? env.master : issuer), + borrower, + STAmount{broker.asset, shortage})); + } + + // Periodic payment amount will consist of + // 1. principal outstanding (1000) + // 2. interest interest rate (at 12%) + // 3. payment interval (600s) + // 4. loan service fee (2) + // Calculate these values without the helper functions + // to verify they're working correctly The numbers in + // the below BEAST_EXPECTs may not hold across assets. + auto const periodicRate = + loanPeriodicRate(state.interestRate, state.paymentInterval); + STAmount const roundedPeriodicPayment{ + broker.asset, + roundPeriodicPayment( + broker.asset, state.periodicPayment, state.loanScale)}; + + log << currencyLabel << " Payment components: " + << "Payments remaining, rawInterest, rawPrincipal, " + "rawMFee, trackedValueDelta, trackedPrincipalDelta, " + "trackedInterestDelta, trackedMgmtFeeDelta, special" + << std::endl; + + // Include the service fee + STAmount const totalDue = roundToScale( + roundedPeriodicPayment + serviceFee, + state.loanScale, + Number::upward); + + { + auto const raw = calculateRawLoanState( + state.periodicPayment, + periodicRate, + state.paymentRemaining, + broker.params.managementFeeRate); + auto const rounded = calculateRoundedLoanState( + state.totalValue, + state.principalOutstanding, + state.managementFeeOutstanding); + log << currencyLabel + << " Loan starting state: " << state.paymentRemaining << ", " + << raw.interestDue << ", " << raw.principalOutstanding << ", " + << raw.managementFeeDue << ", " << rounded.valueOutstanding + << ", " << rounded.principalOutstanding << ", " + << rounded.interestDue << ", " << rounded.managementFeeDue + << std::endl; + } + + // Try to pay a little extra to show that it's _not_ + // taken + STAmount const transactionAmount = STAmount{broker.asset, totalDue} + + std::min(broker.asset(10).value(), + STAmount{broker.asset, totalDue / 20}); + + auto const borrowerInitialBalance = + env.balance(borrower, broker.asset).number(); + auto const initialState = state; + detail::PaymentComponents totalPaid{ + .trackedValueDelta = 0, + .trackedPrincipalDelta = 0, + .trackedManagementFeeDelta = 0}; + Number totalInterestPaid = 0; + Number totalFeesPaid = 0; + std::size_t totalPaymentsMade = 0; + + ripple::LoanState currentTrueState = calculateRawLoanState( + state.periodicPayment, + periodicRate, + state.paymentRemaining, + broker.params.managementFeeRate); + + auto validateBorrowerBalance = [&]() { + auto const totalSpent = + (totalPaid.trackedValueDelta + totalFeesPaid + + (broker.asset.native() ? Number(baseFee) * totalPaymentsMade + : numZero)); + BEAST_EXPECT( + env.balance(borrower, broker.asset).number() == + borrowerInitialBalance - totalSpent); + }; + while (state.paymentRemaining > 0) + { + validateBorrowerBalance(); + // Compute the expected principal amount + auto const paymentComponents = detail::computePaymentComponents( + broker.asset.raw(), + state.loanScale, + state.totalValue, + state.principalOutstanding, + state.managementFeeOutstanding, + state.periodicPayment, + periodicRate, + state.paymentRemaining, + broker.params.managementFeeRate); + + BEAST_EXPECT( + paymentComponents.trackedValueDelta == roundedPeriodicPayment || + (paymentComponents.specialCase == + detail::PaymentSpecialCase::final && + paymentComponents.trackedValueDelta < roundedPeriodicPayment)); + BEAST_EXPECT( + paymentComponents.trackedValueDelta == + paymentComponents.trackedPrincipalDelta + + paymentComponents.trackedInterestPart() + + paymentComponents.trackedManagementFeeDelta); + + ripple::LoanState const nextTrueState = calculateRawLoanState( + state.periodicPayment, + periodicRate, + state.paymentRemaining - 1, + broker.params.managementFeeRate); + detail::LoanDeltas const deltas = currentTrueState - nextTrueState; + BEAST_EXPECT( + deltas.valueDelta() == + deltas.principalDelta + deltas.interestDueDelta + + deltas.managementFeeDueDelta); + BEAST_EXPECT( + paymentComponents.specialCase == + detail::PaymentSpecialCase::final || + deltas.valueDelta() == state.periodicPayment || + (state.loanScale - + (deltas.valueDelta() - state.periodicPayment).exponent()) > + 14); + + log << currencyLabel + << " Payment components: " << state.paymentRemaining << ", " + << deltas.interestDueDelta << ", " << deltas.principalDelta + << ", " << deltas.managementFeeDueDelta << ", " + << paymentComponents.trackedValueDelta << ", " + << paymentComponents.trackedPrincipalDelta << ", " + << paymentComponents.trackedInterestPart() << ", " + << paymentComponents.trackedManagementFeeDelta << ", " + << (paymentComponents.specialCase == + detail::PaymentSpecialCase::final + ? "final" + : paymentComponents.specialCase == + detail::PaymentSpecialCase::extra + ? "extra" + : "none") + << std::endl; + + auto const totalDueAmount = STAmount{ + broker.asset, paymentComponents.trackedValueDelta + serviceFee}; + + // Due to the rounding algorithms to keep the interest and + // principal in sync with "true" values, the computed amount + // may be a little less than the rounded fixed payment + // amount. For integral types, the difference should be < 3 + // (1 unit for each of the interest and management fee). For + // IOUs, the difference should be dust. + Number const diff = totalDue - totalDueAmount; + BEAST_EXPECT( + paymentComponents.specialCase == + detail::PaymentSpecialCase::final || + diff == beast::zero || + (diff > beast::zero && + ((broker.asset.integral() && + (static_cast(diff) < 3)) || + (state.loanScale - diff.exponent() > 13)))); + + BEAST_EXPECT( + paymentComponents.trackedPrincipalDelta >= beast::zero && + paymentComponents.trackedPrincipalDelta <= + state.principalOutstanding); + BEAST_EXPECT( + paymentComponents.specialCase != + detail::PaymentSpecialCase::final || + paymentComponents.trackedPrincipalDelta == + state.principalOutstanding); + + auto const borrowerBalanceBeforePayment = + env.balance(borrower, broker.asset); + + // Make the payment + env(pay(borrower, loanKeylet.key, transactionAmount)); + + env.close( + d{(state.previousPaymentDate + state.nextPaymentDate) / 2}); + + // Need to account for fees if the loan is in XRP + PrettyAmount adjustment = broker.asset(0); + if (broker.asset.native()) + { + adjustment = env.current()->fees().base; + } + + // Check the result + verifyLoanStatus.checkPayment( + state.loanScale, + borrower, + borrowerBalanceBeforePayment, + totalDueAmount, + adjustment); + + --state.paymentRemaining; + state.previousPaymentDate = state.nextPaymentDate; + if (paymentComponents.specialCase == + detail::PaymentSpecialCase::final) + { + state.paymentRemaining = 0; + } + else + { + state.nextPaymentDate += state.paymentInterval; + } + state.principalOutstanding -= + paymentComponents.trackedPrincipalDelta; + state.managementFeeOutstanding -= + paymentComponents.trackedManagementFeeDelta; + state.totalValue -= paymentComponents.trackedValueDelta; + + verifyLoanStatus(state); + + totalPaid.trackedValueDelta += paymentComponents.trackedValueDelta; + totalPaid.trackedPrincipalDelta += + paymentComponents.trackedPrincipalDelta; + totalPaid.trackedManagementFeeDelta += + paymentComponents.trackedManagementFeeDelta; + totalInterestPaid += paymentComponents.trackedInterestPart(); + totalFeesPaid += serviceFee; + ++totalPaymentsMade; + + currentTrueState = nextTrueState; + } + validateBorrowerBalance(); + + // Loan is paid off + BEAST_EXPECT(state.paymentRemaining == 0); + BEAST_EXPECT(state.principalOutstanding == 0); + + // Make sure all the payments add up + BEAST_EXPECT(totalPaid.trackedValueDelta == initialState.totalValue); + BEAST_EXPECT( + totalPaid.trackedPrincipalDelta == + initialState.principalOutstanding); + BEAST_EXPECT( + totalPaid.trackedManagementFeeDelta == + initialState.managementFeeOutstanding); + // This is almost a tautology given the previous checks, but + // check it anyway for completeness. + BEAST_EXPECT( + totalInterestPaid == + initialState.totalValue - + (initialState.principalOutstanding + + initialState.managementFeeOutstanding)); + BEAST_EXPECT(totalPaymentsMade == initialState.paymentRemaining); + } + void testSelfLoan() { @@ -3088,12 +3444,14 @@ class Loan_test : public beast::unit_test::suite Account const lender{"lender"}; Account const borrower{"borrower"}; - env.fund(XRP(vaultDeposit * 100), lender, borrower); + BrokerParameters brokerParams; + env.fund(XRP(brokerParams.vaultDeposit * 100), lender, borrower); env.close(); PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; - BrokerInfo broker{createVaultAndBroker(env, xrpAsset, lender)}; + BrokerInfo broker{ + createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; using namespace loan; @@ -3139,15 +3497,6 @@ class Loan_test : public beast::unit_test::suite } } - BrokerInfo - createVaultAndBrokerNoMaxDebt( - jtx::Env& env, - jtx::PrettyAsset const& asset, - jtx::Account const& lender) - { - return createVaultAndBroker(env, asset, lender, Number(0)); - } - void testWrongMaxDebtBehavior() { @@ -3161,12 +3510,15 @@ class Loan_test : public beast::unit_test::suite Account const issuer{"issuer"}; Account const lender{"lender"}; - env.fund(XRP(vaultDeposit * 100), issuer, noripple(lender)); + BrokerParameters brokerParams{.debtMax = 0}; + env.fund( + XRP(brokerParams.vaultDeposit * 100), issuer, noripple(lender)); env.close(); PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; - BrokerInfo broker{createVaultAndBrokerNoMaxDebt(env, xrpAsset, lender)}; + BrokerInfo broker{ + createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); BEAST_EXPECT(brokerSle)) @@ -3210,11 +3562,14 @@ class Loan_test : public beast::unit_test::suite Account const lender{"lender"}; Account const borrower{"borrower"}; - env.fund(XRP(vaultDeposit * 100), issuer, lender, borrower); + BrokerParameters brokerParams; + env.fund( + XRP(brokerParams.vaultDeposit * 100), issuer, lender, borrower); env.close(); PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; - BrokerInfo broker{createVaultAndBroker(env, xrpAsset, lender)}; + BrokerInfo broker{ + createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; using namespace loan; @@ -3246,8 +3601,8 @@ class Loan_test : public beast::unit_test::suite auto const keylet = keylet::loan(broker.brokerID, loanSequence); createJson = env.json(createJson, sig(sfCounterpartySignature, lender)); - // Fails in preclaim because principal requested can't be represented as - // XRP + // Fails in preclaim because principal requested can't be + // represented as XRP env(createJson, ter(tecPRECISION_LOSS)); env.close(); @@ -3258,8 +3613,8 @@ class Loan_test : public beast::unit_test::suite createJson[sfPrincipalRequested] = actualPrincipal; createJson.removeMember(sfSequence.jsonName); createJson = env.json(createJson, sig(sfCounterpartySignature, lender)); - // Fails in doApply because the payment is too small to be represented - // as XRP. + // Fails in doApply because the payment is too small to be + // represented as XRP. env(createJson, ter(tecPRECISION_LOSS)); env.close(); } @@ -3369,7 +3724,8 @@ class Loan_test : public beast::unit_test::suite txJson[sfAccount] = borrower.human(); txJson[sfCounterparty] = lender.human(); txJson[sfLoanBrokerID] = - "FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FECF83F" + "FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FEC" + "F83F" "5C"; txJson[sfPrincipalRequested] = "100000000"; txJson[sfPaymentTotal] = 10000; @@ -3487,7 +3843,8 @@ class Loan_test : public beast::unit_test::suite txJson[sfAccount] = lender.human(); txJson[sfCounterparty] = borrower.human(); txJson[sfLoanBrokerID] = - "FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FECF83F" + "FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FEC" + "F83F" "5C"; txJson[sfPrincipalRequested] = "100000000"; txJson[sfPaymentTotal] = 10000; @@ -3652,8 +4009,8 @@ class Loan_test : public beast::unit_test::suite env.close(); env(trust(borrower, IOU(20'000'000))); - // The borrower increases their limit and acquires some IOU so they - // can pay interest + // The borrower increases their limit and acquires some IOU so + // they can pay interest env(pay(issuer, borrower, IOU(500))); env.close(); @@ -3722,7 +4079,11 @@ class Loan_test : public beast::unit_test::suite .holders = {lender}, .pay = issuerBalance}); - auto const broker = createVaultAndBroker(env, asset, lender, 200); + BrokerParameters const brokerParams{ + .debtMax = 200, + }; + auto const broker = + createVaultAndBroker(env, asset, lender, brokerParams); auto const loanSetFee = fee(env.current()->fees().base * 2); // Create Loan env(set(borrower, broker.brokerID, 200), @@ -4025,8 +4386,8 @@ class Loan_test : public beast::unit_test::suite testLoanPayDebtDecreaseInvariant() { // From FIND-007 - testcase - << "LoanPay ripple::LoanPay::doApply : debtDecrease rounding good"; + testcase << "LoanPay ripple::LoanPay::doApply : debtDecrease " + "rounding good"; using namespace jtx; using namespace std::chrono_literals; @@ -4092,8 +4453,7 @@ class Loan_test : public beast::unit_test::suite return Account("Broker pseudo-account", brokerPseudo); }(); - VerifyLoanStatus verifyLoanStatus( - env, broker, Number{992481, -2}, pseudoAcct, keylet); + VerifyLoanStatus verifyLoanStatus(env, broker, pseudoAcct, keylet); auto const originalState = getCurrentState(env, broker, keylet); verifyLoanStatus(originalState); @@ -4282,8 +4642,8 @@ class Loan_test : public beast::unit_test::suite testLoanPayComputePeriodicPaymentValidTotalPrincipalPaidInvariant() { // From FIND-009 - testcase - << "ripple::loanComputePaymentParts : totalPrincipalPaid rounded"; + testcase << "ripple::loanComputePaymentParts : totalPrincipalPaid " + "rounded"; using namespace jtx; using namespace std::chrono_literals; @@ -4423,11 +4783,9 @@ class Loan_test : public beast::unit_test::suite BrokerInfo broker{createVaultAndBroker(env, iouAsset, lender)}; { auto const coverDepositValue = - broker.asset(coverDepositParameter).value(); + broker.asset(broker.params.coverDeposit * 10).value(); env(loanBroker::coverDeposit( - lender, - broker.brokerID, - STAmount{broker.asset, coverDepositValue * 10})); + lender, broker.brokerID, coverDepositValue)); env.close(); } @@ -4516,8 +4874,10 @@ class Loan_test : public beast::unit_test::suite env(payIssuerTx); env.close(); + BrokerParameters const brokerParams{ + .debtMax = Number{0}, .coverRateMin = TenthBips32{1}}; BrokerInfo broker{ - createVaultAndBroker(env, iouAsset, lender, Number(0), 1)}; + createVaultAndBroker(env, iouAsset, lender, brokerParams)}; using namespace loan; @@ -4724,10 +5084,593 @@ class Loan_test : public beast::unit_test::suite } } +#if LOANTODO + void + testCoverDepositAllowsNonTransferableMPT() + { + testcase("CoverDeposit accepts MPT without CanTransfer"); + using namespace jtx; + using namespace loanBroker; + + Env env(*this, all); + + Account const issuer{"issuer"}; + Account const alice{"alice"}; + + env.fund(XRP(100'000), issuer, alice); + env.close(); + + MPTTester mpt{env, issuer, mptInitNoFund}; + + mpt.create( + {.flags = tfMPTCanTransfer, + .mutableFlags = tmfMPTCanMutateCanTransfer}); + + env.close(); + + PrettyAsset const asset = mpt["BUG"]; + mpt.authorize({.account = alice}); + env.close(); + + // Issuer can fund the holder even if CanTransfer is not set. + env(pay(issuer, alice, asset(100))); + env.close(); + + Vault vault{env}; + auto const [createTx, vaultKeylet] = + vault.create({.owner = alice, .asset = asset}); + env(createTx); + env.close(); + + auto const brokerKeylet = + keylet::loanbroker(alice.id(), env.seq(alice)); + env(set(alice, vaultKeylet.key)); + env.close(); + + auto const brokerSle = env.le(brokerKeylet); + if (!BEAST_EXPECT(brokerSle)) + return; + + Account const pseudoAccount{ + "Loan Broker pseudo-account", brokerSle->at(sfAccount)}; + + // Remove CanTransfer after the broker is set up. + mpt.set({.mutableFlags = tmfMPTClearCanTransfer}); + env.close(); + + // Standard Payment path should forbid third-party transfers. + env(pay(alice, pseudoAccount, asset(1)), ter(tecNO_AUTH)); + env.close(); + + auto const depositAmount = asset(1); + env(coverDeposit(alice, brokerKeylet.key, depositAmount)); + BEAST_EXPECT(env.ter() == tesSUCCESS); + env.close(); + + if (auto const refreshed = env.le(brokerKeylet); + BEAST_EXPECT(refreshed)) + { + // with an MPT that cannot be transferred the covrAvailable should + // remain zero + BEAST_EXPECT(refreshed->at(sfCoverAvailable) == 0); + env.require(balance(pseudoAccount, depositAmount)); + } + } + + void + testLoanPayLateFullPaymentBypassesPenalties() + { + testcase("LoanPay full payment skips late penalties"); + using namespace jtx; + using namespace loan; + using namespace std::chrono_literals; + + Env env(*this, all); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1'000'000), issuer, lender, borrower); + env.close(); + + PrettyAsset const asset = issuer[iouCurrency]; + env(trust(lender, asset(100'000'000))); + env(trust(borrower, asset(100'000'000))); + env(pay(issuer, lender, asset(50'000'000))); + env(pay(issuer, borrower, asset(5'000'000))); + env.close(); + + BrokerInfo broker{createVaultAndBroker(env, asset, lender)}; + + auto const loanSetFee = fee(env.current()->fees().base * 2); + + auto const brokerPreLoan = env.le(keylet::loanbroker(broker.brokerID)); + if (!BEAST_EXPECT(brokerPreLoan)) + return; + + auto const loanSequence = brokerPreLoan->at(sfLoanSequence); + auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + + Number const principal = asset(1'000).value(); + Number const serviceFee = asset(2).value(); + Number const lateFee = asset(5).value(); + Number const closeFee = asset(4).value(); + + env(set(borrower, broker.brokerID, principal), + sig(sfCounterpartySignature, lender), + loanServiceFee(serviceFee), + latePaymentFee(lateFee), + closePaymentFee(closeFee), + interestRate(percentageToTenthBips(12)), + lateInterestRate(percentageToTenthBips(24) / 10), + closeInterestRate(percentageToTenthBips(5)), + paymentTotal(12), + paymentInterval(600), + gracePeriod(0), + fee(loanSetFee)); + env.close(); + + auto state1 = getCurrentState(env, broker, loanKeylet); + if (!BEAST_EXPECT(state1.paymentRemaining > 1)) + return; + + using d = NetClock::duration; + using tp = NetClock::time_point; + auto const overdueClose = + tp{d{state1.nextPaymentDate + state1.paymentInterval}}; + env.close(overdueClose); + + auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); + auto const loanSle = env.le(loanKeylet); + if (!BEAST_EXPECT(brokerSle && loanSle)) + return; + + auto state = getCurrentState(env, broker, loanKeylet); + + TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; + TenthBips32 const interestRateValue{loanSle->at(sfInterestRate)}; + TenthBips32 const lateInterestRateValue{ + loanSle->at(sfLateInterestRate)}; + TenthBips32 const closeInterestRateValue{ + loanSle->at(sfCloseInterestRate)}; + + Number const closePaymentFeeRounded = roundToAsset( + broker.asset, loanSle->at(sfClosePaymentFee), state.loanScale); + Number const latePaymentFeeRounded = roundToAsset( + broker.asset, loanSle->at(sfLatePaymentFee), state.loanScale); + + auto const roundedLoanState = calculateRoundedLoanState( + state.totalValue, + state.principalOutstanding, + state.managementFeeOutstanding); + Number const totalInterestOutstanding = roundedLoanState.interestDue; + + auto const periodicRate = + loanPeriodicRate(interestRateValue, state.paymentInterval); + auto const rawLoanState = calculateRawLoanState( + state.periodicPayment, + periodicRate, + state.paymentRemaining, + managementFeeRate); + + auto const parentCloseTime = env.current()->parentCloseTime(); + auto const startDateSeconds = static_cast( + state.startDate.time_since_epoch().count()); + + Number const fullPaymentInterest = calculateFullPaymentInterest( + rawLoanState.principalOutstanding, + periodicRate, + parentCloseTime, + state.paymentInterval, + state.previousPaymentDate, + startDateSeconds, + closeInterestRateValue); + + Number const roundedFullInterestAmount = + roundToAsset(broker.asset, fullPaymentInterest, state.loanScale); + Number const roundedFullManagementFee = computeFee( + broker.asset, + roundedFullInterestAmount, + managementFeeRate, + state.loanScale); + Number const roundedFullInterest = + roundedFullInterestAmount - roundedFullManagementFee; + + Number const trackedValueDelta = state.principalOutstanding + + totalInterestOutstanding + state.managementFeeOutstanding; + Number const untrackedManagementFee = closePaymentFeeRounded + + roundedFullManagementFee - state.managementFeeOutstanding; + Number const untrackedInterest = + roundedFullInterest - totalInterestOutstanding; + + Number const baseFullDue = + trackedValueDelta + untrackedInterest + untrackedManagementFee; + BEAST_EXPECT( + baseFullDue == + roundToAsset(broker.asset, baseFullDue, state.loanScale)); + + auto const overdueSeconds = + parentCloseTime.time_since_epoch().count() - state.nextPaymentDate; + if (!BEAST_EXPECT(overdueSeconds > 0)) + return; + + Number const overdueRate = + loanPeriodicRate(lateInterestRateValue, overdueSeconds); + Number const lateInterestRaw = state.principalOutstanding * overdueRate; + Number const lateInterestRounded = + roundToAsset(broker.asset, lateInterestRaw, state.loanScale); + Number const lateManagementFeeRounded = computeFee( + broker.asset, + lateInterestRounded, + managementFeeRate, + state.loanScale); + Number const penaltyDue = lateInterestRounded + + lateManagementFeeRounded + latePaymentFeeRounded; + BEAST_EXPECT(penaltyDue > Number{}); + + auto const balanceBefore = env.balance(borrower, broker.asset).number(); + + STAmount const paymentAmount{broker.asset.raw(), baseFullDue}; + env(pay(borrower, loanKeylet.key, paymentAmount, tfLoanFullPayment)); + env.close(); + + if (auto const meta = env.meta(); BEAST_EXPECT(meta)) + BEAST_EXPECT(meta->at(sfTransactionResult) == tesSUCCESS); + + auto const balanceAfter = env.balance(borrower, broker.asset).number(); + Number const actualPaid = balanceBefore - balanceAfter; + BEAST_EXPECT(actualPaid == baseFullDue); + + Number const expectedWithPenalty = baseFullDue + penaltyDue; + BEAST_EXPECT(expectedWithPenalty > actualPaid); + BEAST_EXPECT(expectedWithPenalty - actualPaid == penaltyDue); + } + + void + testPoC_UnsignedUnderflowOnFullPayAfterEarlyPeriodic() + { + // --- PoC Summary ---------------------------------------------------- + // Scenario: Borrower makes one periodic payment early (before next due) + // so doPayment sets sfPreviousPaymentDate to the (future) + // sfNextPaymentDueDate and advances sfNextPaymentDueDate by one + // interval. Borrower then immediately performs a full-payment + // (tfLoanFullPayment). Why it matters: Full-payment interest accrual + // uses + // delta = now - max(prevPaymentDate, startDate) + // with an unsigned clock representation (uint32). If prevPaymentDate is + // in the future, the subtraction underflows to a very large positive + // number. This inflates roundedFullInterest and total full-close due, + // and LoanPay applies the inflated valueChange to the vault + // (sfAssetsTotal), increasing NAV. + // -------------------------------------------------------------------- + testcase( + "PoC: Unsigned-underflow full-pay accrual after early periodic"); + + using namespace jtx; + using namespace loan; + using namespace std::chrono_literals; + + Env env(*this, all); + + Account const lender{"poc_lender4"}; + Account const borrower{"poc_borrower4"}; + env.fund(XRP(3'000'000), lender, borrower); + env.close(); + + PrettyAsset const asset{xrpIssue(), 1'000'000}; + BrokerParameters brokerParams{}; + auto const broker = + createVaultAndBroker(env, asset, lender, brokerParams); + + // Create a 3-payment loan so full-payment path is enabled after 1 + // periodic payment. + auto const loanSetFee = fee(env.current()->fees().base * 2); + Number const principalRequest = asset(1000).value(); + auto const originationFee = asset(0).value(); + auto const serviceFee = asset(1).value(); + auto const serviceFeePA = asset(1); + auto const lateFee = asset(0).value(); + auto const closeFee = asset(0).value(); + auto const interest = percentageToTenthBips(12); + auto const lateInterest = percentageToTenthBips(12) / 10; + auto const closeInterest = percentageToTenthBips(12) / 10; + auto const overpaymentInterest = percentageToTenthBips(12) / 10; + auto const total = 3u; + auto const interval = 600u; + auto const grace = 60u; + + auto createJtx = env.jt( + set(borrower, broker.brokerID, principalRequest, 0), + sig(sfCounterpartySignature, lender), + loanOriginationFee(originationFee), + loanServiceFee(serviceFee), + latePaymentFee(lateFee), + closePaymentFee(closeFee), + overpaymentFee(percentageToTenthBips(5) / 10), + interestRate(interest), + lateInterestRate(lateInterest), + closeInterestRate(closeInterest), + overpaymentInterestRate(overpaymentInterest), + paymentTotal(total), + paymentInterval(interval), + gracePeriod(grace), + fee(loanSetFee)); + + auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); + BEAST_EXPECT(brokerSle); + auto const loanSequence = brokerSle ? brokerSle->at(sfLoanSequence) : 0; + auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + + env(createJtx); + env.close(); + + // Compute a regular periodic due and pay it early (before next due). + auto state = getCurrentState(env, broker, loanKeylet); + Number const periodicRate = + loanPeriodicRate(state.interestRate, state.paymentInterval); + auto const components = detail::computePaymentComponents( + asset.raw(), + state.loanScale, + state.totalValue, + state.principalOutstanding, + state.managementFeeOutstanding, + state.periodicPayment, + periodicRate, + state.paymentRemaining, + brokerParams.managementFeeRate); + STAmount const regularDue{ + asset, components.trackedValueDelta + serviceFeePA.number()}; + // now < nextDue immediately after creation, so this is an early pay. + env(pay(borrower, loanKeylet.key, regularDue)); + env.close(); + + // Immediately attempt a full payoff. Compute the exact full-payment + // due to ensure the tx applies. + auto after = getCurrentState(env, broker, loanKeylet); + auto const loanSle = env.le(loanKeylet); + BEAST_EXPECT(loanSle); + auto const brokerSle2 = env.le(keylet::loanbroker(broker.brokerID)); + BEAST_EXPECT(brokerSle2); + + auto const closePaymentFee = + loanSle ? loanSle->at(sfClosePaymentFee) : Number{}; + auto const closeInterestRate = loanSle + ? TenthBips32{loanSle->at(sfCloseInterestRate)} + : TenthBips32{}; + auto const managementFeeRate = brokerSle2 + ? TenthBips16{brokerSle2->at(sfManagementFeeRate)} + : TenthBips16{}; + + Number const periodicRate2 = + loanPeriodicRate(after.interestRate, after.paymentInterval); + // Accrued + prepayment-penalty interest based on current periodic + // schedule + auto const fullPaymentInterest = calculateFullPaymentInterest( + after.periodicPayment, + periodicRate2, + after.paymentRemaining, + env.current()->parentCloseTime(), + after.paymentInterval, + after.previousPaymentDate, + static_cast( + after.startDate.time_since_epoch().count()), + closeInterestRate); + // Round to asset scale and split interest/fee parts + auto const roundedInterest = + roundToAsset(asset.raw(), fullPaymentInterest, after.loanScale); + Number const roundedFullMgmtFee = computeFee( + asset.raw(), roundedInterest, managementFeeRate, after.loanScale); + Number const roundedFullInterest = roundedInterest - roundedFullMgmtFee; + + // Show both signed and unsigned deltas to highlight the underflow. + auto const nowSecs = static_cast( + env.current()->parentCloseTime().time_since_epoch().count()); + auto const startSecs = static_cast( + after.startDate.time_since_epoch().count()); + auto const lastPaymentDate = + std::max(after.previousPaymentDate, startSecs); + auto const signedDelta = static_cast(nowSecs) - + static_cast(lastPaymentDate); + auto const unsignedDelta = + static_cast(nowSecs - lastPaymentDate); + log << "PoC window: prev=" << after.previousPaymentDate + << " start=" << startSecs << " now=" << nowSecs + << " signedDelta=" << signedDelta + << " unsignedDelta=" << unsignedDelta << std::endl; + + // Reference (clamped) computation: emulate a non-negative accrual + // window by clamping prevPaymentDate to 'now' for the full-pay path. + auto const prevClamped = std::min(after.previousPaymentDate, nowSecs); + auto const fullPaymentInterestClamped = calculateFullPaymentInterest( + after.periodicPayment, + periodicRate2, + after.paymentRemaining, + env.current()->parentCloseTime(), + after.paymentInterval, + prevClamped, + startSecs, + closeInterestRate); + auto const roundedInterestClamped = roundToAsset( + asset.raw(), fullPaymentInterestClamped, after.loanScale); + Number const roundedFullMgmtFeeClamped = computeFee( + asset.raw(), + roundedInterestClamped, + managementFeeRate, + after.loanScale); + Number const roundedFullInterestClamped = + roundedInterestClamped - roundedFullMgmtFeeClamped; + STAmount const fullDueClamped{ + asset, + after.principalOutstanding + roundedFullInterestClamped + + roundedFullMgmtFeeClamped + closePaymentFee}; + + // Collect vault NAV before closing payment + auto const vaultId2 = + brokerSle2 ? brokerSle2->at(sfVaultID) : uint256{}; + auto const vaultKey2 = keylet::vault(vaultId2); + auto const vaultBefore = env.le(vaultKey2); + BEAST_EXPECT(vaultBefore); + Number const assetsTotalBefore = + vaultBefore ? vaultBefore->at(sfAssetsTotal) : Number{}; + + STAmount const fullDue{ + asset, + after.principalOutstanding + roundedFullInterest + + roundedFullMgmtFee + closePaymentFee}; + + log << "PoC payoff: principalOutstanding=" << after.principalOutstanding + << " roundedFullInterest=" << roundedFullInterest + << " roundedFullMgmtFee=" << roundedFullMgmtFee + << " closeFee=" << closePaymentFee + << " fullDue=" << to_string(fullDue.getJson()) << std::endl; + log << "PoC reference (clamped): roundedFullInterestClamped=" + << roundedFullInterestClamped + << " roundedFullMgmtFeeClamped=" << roundedFullMgmtFeeClamped + << " fullDueClamped=" << to_string(fullDueClamped.getJson()) + << std::endl; + + env(pay(borrower, loanKeylet.key, fullDue), txflags(tfLoanFullPayment)); + env.close(); + + // Sanity: underflow present (unsigned delta very large relative to + // interval) + BEAST_EXPECT(unsignedDelta > after.paymentInterval); + + // Compare vault NAV before/after the full close + auto const vaultAfter = env.le(vaultKey2); + BEAST_EXPECT(vaultAfter); + if (vaultAfter) + { + auto const assetsTotalAfter = vaultAfter->at(sfAssetsTotal); + log << "PoC NAV: assetsTotalBefore=" << assetsTotalBefore + << " assetsTotalAfter=" << assetsTotalAfter + << " delta=" << (assetsTotalAfter - assetsTotalBefore) + << std::endl; + + // Value-based proof: underflowed window yields a payoff larger than + // the clamped (non-underflow) reference. + BEAST_EXPECT(fullDue != fullDueClamped); + BEAST_EXPECT(fullDue > fullDueClamped); + if (fullDue > fullDueClamped) + log << "PoC delta: overcharge (fullDue > clamped)" << std::endl; + } + + // Loan should be paid off + auto const finalLoan = env.le(loanKeylet); + BEAST_EXPECT(finalLoan); + if (finalLoan) + { + BEAST_EXPECT(finalLoan->at(sfPaymentRemaining) == 0); + BEAST_EXPECT(finalLoan->at(sfPrincipalOutstanding) == 0); + } + } + + void + testLoanCoverMinimumRoundingExploit() + { + auto testLoanCoverMinimumRoundingExploit = + [&, this](Number const& principalRequest) { + testcase << "LoanBrokerCoverClawback drains cover via rounding" + << " principalRequested=" + << to_string(principalRequest); + + using namespace jtx; + using namespace loan; + using namespace loanBroker; + + Env env(*this, all); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1'000'000'000), issuer, lender, borrower); + env.close(); + + env(fset(issuer, asfAllowTrustLineClawback)); + env.close(); + + PrettyAsset const asset = issuer[iouCurrency]; + env(trust(lender, asset(2'000'0000))); + env(trust(borrower, asset(2'000'0000))); + env.close(); + + env(pay(issuer, lender, asset(2'000'0000))); + env.close(); + + BrokerParameters brokerParams{ + .debtMax = 0, .coverRateMin = TenthBips32{10'000}}; + BrokerInfo broker{ + createVaultAndBroker(env, asset, lender, brokerParams)}; + + auto const loanSetFee = fee(env.current()->fees().base * 2); + auto createTx = env.jt( + set(borrower, broker.brokerID, principalRequest), + sig(sfCounterpartySignature, lender), + loanSetFee, + paymentInterval(600), + paymentTotal(1), + gracePeriod(60)); + env(createTx); + env.close(); + + auto const brokerBefore = + env.le(keylet::loanbroker(broker.brokerID)); + BEAST_EXPECT(brokerBefore); + if (!brokerBefore) + return; + + Number const debtOutstanding = brokerBefore->at(sfDebtTotal); + Number const coverAvailableBefore = + brokerBefore->at(sfCoverAvailable); + + BEAST_EXPECT(debtOutstanding > Number{}); + BEAST_EXPECT(coverAvailableBefore > Number{}); + + log << "debt=" << to_string(debtOutstanding) + << " cover_available=" << to_string(coverAvailableBefore); + + env(coverClawback(issuer, 0), loanBrokerID(broker.brokerID)); + env.close(); + + auto const brokerAfter = + env.le(keylet::loanbroker(broker.brokerID)); + BEAST_EXPECT(brokerAfter); + if (!brokerAfter) + return; + + Number const debtAfter = brokerAfter->at(sfDebtTotal); + // the debt has not changed + BEAST_EXPECT(debtAfter == debtOutstanding); + + Number const coverAvailableAfter = + brokerAfter->at(sfCoverAvailable); + + // since the cover rate min != 0, the cover available should not + // be zero + BEAST_EXPECT(coverAvailableAfter != Number{}); + }; + + // Call the lambda with different principal values + testLoanCoverMinimumRoundingExploit(Number{1, -30}); // 1e-30 units + testLoanCoverMinimumRoundingExploit(Number{1, -20}); // 1e-20 units + testLoanCoverMinimumRoundingExploit(Number{1, -10}); // 1e-10 units + testLoanCoverMinimumRoundingExploit(Number{1, 1}); // 1e-10 units + } +#endif + public: void run() override { +#if LOANTODO + testCoverDepositAllowsNonTransferableMPT(); + testLoanPayLateFullPaymentBypassesPenalties(); + testPoC_UnsignedUnderflowOnFullPayAfterEarlyPeriodic(); + testLoanCoverMinimumRoundingExploit(); +#endif + testIssuerLoan(); testDisabled(); testSelfLoan(); @@ -4756,7 +5699,295 @@ public: } }; +class LoanBatch_test : public Loan_test +{ +protected: + enum class AssetType { XRP = 0, IOU = 1, MPT = 2 }; + + beast::xor_shift_engine engine_; + + std::uniform_int_distribution<> assetDist{0, 2}; + std::uniform_int_distribution principalDist{ + 100'000, + 1'000'000'000}; + std::uniform_int_distribution interestRateDist{0, 10000}; + std::uniform_int_distribution<> paymentTotalDist{12, 10000}; + std::uniform_int_distribution<> paymentIntervalDist{60, 3600 * 24 * 30}; + std::uniform_int_distribution managementFeeRateDist{ + 0, + 10'000}; + std::uniform_int_distribution<> serviceFeeDist{0, 20}; + /* + # Generate parameters that are more likely to be valid + principal = Decimal(str(rand.randint(100000, + 100'000'000))).quantize(ROUND_TARGET) + + interest_rate = Decimal(rand.randint(1, 10000)) / + Decimal(100000) + + payment_total = rand.randint(12, 10000) + + payment_interval = Decimal(str(rand.randint(60, 2629746))) + + interest_fee = Decimal(rand.randint(0, 100000)) / + Decimal(100000) +*/ + + void + runLoan( + AssetType assetType, + BrokerParameters const& brokerParams, + LoanParameters const& loanParams) + { + using namespace jtx; + + Account const issuer("issuer"); + Account const lender("lender"); + Account const borrower("borrower"); + + Env env(*this, all); + + // Enough to cover initial fees + env.fund( + env.current()->fees().accountReserve(10) * 10, + issuer, + noripple(lender, borrower)); + + // Make the asset + auto const asset = [&]() { + switch (assetType) + { + case AssetType::XRP: + // TODO: remove the factor, and set up loans in drops + return PrettyAsset{xrpIssue(), 1'000'000}; + + case AssetType::IOU: { + PrettyAsset const asset{issuer[iouCurrency]}; + + env(trust(lender, asset(brokerParams.vaultDeposit))); + env(trust(borrower, asset(brokerParams.vaultDeposit))); + + return asset; + } + + case AssetType::MPT: { + MPTTester mptt{env, issuer, mptInitNoFund}; + mptt.create( + {.flags = tfMPTCanClawback | tfMPTCanTransfer | + tfMPTCanLock}); + // Scale the MPT asset so interest is interesting + PrettyAsset const asset{mptt.issuanceID(), 10'000}; + // Need to do the authorization here because mptt isn't + // accessible outside + mptt.authorize({.account = lender}); + mptt.authorize({.account = borrower}); + + env.close(); + + return asset; + } + + default: + throw std::runtime_error("Unknown asset type"); + } + }(); + + env.close(); + env( + pay((asset.native() ? env.master : issuer), + lender, + asset(brokerParams.vaultDeposit))); + // Fund the borrower later once we know the total loan + // size + + BrokerInfo const broker = + createVaultAndBroker(env, asset, lender, brokerParams); + + auto const pseudoAcctOpt = [&]() -> std::optional { + auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); + if (!BEAST_EXPECT(brokerSle)) + return std::nullopt; + auto const brokerPseudo = brokerSle->at(sfAccount); + return Account("Broker pseudo-account", brokerPseudo); + }(); + if (!pseudoAcctOpt) + return; + Account const& pseudoAcct = *pseudoAcctOpt; + + auto const loanKeyletOpt = [&]() -> std::optional { + auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); + if (!BEAST_EXPECT(brokerSle)) + return std::nullopt; + + // Broker has no loans + BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); + + // The loan keylet is based on the LoanSequence of the + // _LOAN_BROKER_ object. + auto const loanSequence = brokerSle->at(sfLoanSequence); + return keylet::loan(broker.brokerID, loanSequence); + }(); + if (!loanKeyletOpt) + return; + Keylet const& loanKeylet = *loanKeyletOpt; + + env(loanParams(env, broker)); + + env.close(); + + VerifyLoanStatus verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); + + makeLoanPayments(env, broker, loanParams, loanKeylet, verifyLoanStatus); + } + + void + testRandomLoan() + { + using namespace jtx; + + Account const issuer("issuer"); + Account const lender("lender"); + Account const borrower("borrower"); + + // Determine all the random parameters at once + AssetType assetType = static_cast(assetDist(engine_)); + auto const principalRequest = principalDist(engine_); + TenthBips16 managementFeeRate{managementFeeRateDist(engine_)}; + auto const serviceFee = serviceFeeDist(engine_); + TenthBips32 interest{interestRateDist(engine_)}; + auto const payTotal = paymentTotalDist(engine_); + auto const payInterval = paymentIntervalDist(engine_); + + BrokerParameters brokerParams{ + .vaultDeposit = principalRequest * 10, + .debtMax = 0, + .coverRateMin = TenthBips32{0}, + .managementFeeRate = managementFeeRate}; + LoanParameters loanParams{ + .account = lender, + .counter = borrower, + .principalRequest = principalRequest, + .serviceFee = serviceFee, + .interest = interest, + .payTotal = payTotal, + .payInterval = payInterval, + }; + + runLoan(assetType, brokerParams, loanParams); + } + +public: + void + run() override + { + auto const argument = arg(); + auto const numIterations = [s = arg()]() -> int { + int defaultNum = 5; + if (s.empty()) + return defaultNum; + try + { + std::size_t pos; + auto const r = stoi(s, &pos); + if (pos != s.size()) + return defaultNum; + return r; + } + catch (...) + { + return defaultNum; + } + }(); + + using namespace jtx; + + auto const updateInterval = std::min(numIterations / 5, 100); + + for (int i = 0; i < numIterations; ++i) + { + if (i % updateInterval == 0) + testcase << "Random Loan Test iteration " << (i + 1) << "/" + << numIterations; + testRandomLoan(); + } + } +}; + +class LoanArbitrary_test : public LoanBatch_test +{ + void + run() override + { + using namespace jtx; + +#if LOANCOMPLETE + BEAST_EXPECT( + std::numeric_limits::max() > INITIAL_XRP.drops()); + BEAST_EXPECT(Number::maxMantissa > INITIAL_XRP.drops()); + Number initalXrp{INITIAL_XRP}; + BEAST_EXPECT(initalXrp.exponent() <= 0); +#endif + /* + Progress: 27 completed, 50 total attempts | Rejected: periodic=0, +interest=23, +duration=0LoanParameters(principal=Decimal('1255438.00000000000000000000000'), +interest_rate=Decimal('0.01922'), payment_total=5816, +payment_interval=Decimal('29193'), interest_fee=Decimal('0.59195')) Single test +failed with assertion error: Both principal and interest rounded are zero 0 + 0 ++ 0 + */ + + BrokerParameters const brokerParams{ + .vaultDeposit = 1'000'000'000, + .debtMax = 0, + .coverRateMin = TenthBips32{0}, + .managementFeeRate = TenthBips16{5919}, + .coverRateLiquidation = TenthBips32{0}}; + LoanParameters const loanParams{ + .account = Account("lender"), + .counter = Account("borrower"), + .principalRequest = Number{1255438, -6}, + .interest = TenthBips32{1922}, + .payTotal = 5816, + .payInterval = 29193}; + + { + Env env(*this, beast::severities::kWarning); + + auto const asset = PrettyAsset{xrpIssue(), 1'000'000}; + auto const props = computeLoanProperties( + asset, + asset(loanParams.principalRequest).number(), + *loanParams.interest, + *loanParams.payInterval, + *loanParams.payTotal, + brokerParams.managementFeeRate); + log << "Loan properties:\n" + << "\tPeriodic Payment: " << props.periodicPayment << std::endl + << "\tTotal Value: " << props.totalValueOutstanding << std::endl + << "\tManagement Fee: " << props.managementFeeOwedToBroker + << std::endl + << "\tLoan Scale: " << props.loanScale << std::endl + << "\tFirst payment principal: " << props.firstPaymentPrincipal + << std::endl; + + // checkGuards returns a TER, so success is 0 + BEAST_EXPECT(!LoanSet::checkGuards( + asset, + loanParams.principalRequest, + *loanParams.interest, + *loanParams.payTotal, + props, + env.journal)); + } + + runLoan(AssetType::XRP, brokerParams, loanParams); + } +}; + BEAST_DEFINE_TESTSUITE(Loan, tx, ripple); +BEAST_DEFINE_TESTSUITE_MANUAL(LoanBatch, tx, ripple); +BEAST_DEFINE_TESTSUITE_MANUAL(LoanArbitrary, tx, ripple); } // namespace test } // namespace ripple diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index 7181ec2a3e..d5a7185dbe 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -251,7 +251,9 @@ public: * * @param suite_ the current unit_test::suite */ - Env(beast::unit_test::suite& suite_) : Env(suite_, envconfig()) + Env(beast::unit_test::suite& suite_, + beast::severities::Severity thresh = beast::severities::kError) + : Env(suite_, envconfig(), nullptr, thresh) { } diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h index f25f292531..c2bb9281a3 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -295,6 +295,21 @@ struct XRP_t return {TOut{v} * dropsPerXRP}; } + /** Returns an amount of XRP as PrettyAmount, + which is trivially convertable to STAmount + + @param v The Number of XRP (not drops). May be fractional. + */ + PrettyAmount + operator()(Number v) const + { + auto const c = dropsPerXRP.drops(); + auto const d = std::int64_t(v * c); + if (Number(d) / c != v) + Throw("unrepresentable"); + return {d}; + } + PrettyAmount operator()(double v) const { diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 2e48ab86b0..683f1ceaab 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -98,14 +98,24 @@ struct LoanState Number valueOutstanding; /// Prinicipal still due to be paid by the borrower. Number principalOutstanding; - /// Interest still due to be paid by the borrower. - Number interestOutstanding; /// Interest still due to be paid TO the Vault. // This is a portion of interestOutstanding Number interestDue; /// Management fee still due to be paid TO the broker. // This is a portion of interestOutstanding Number managementFeeDue; + + /// Interest still due to be paid by the borrower. + Number + interestOutstanding() const + { + XRPL_ASSERT_PARTS( + interestDue + managementFeeDue == + valueOutstanding - principalOutstanding, + "ripple::LoanState::interestOutstanding", + "other values add up correctly"); + return interestDue + managementFeeDue; + } }; LoanState @@ -113,7 +123,7 @@ calculateRawLoanState( Number const& periodicPayment, Number const& periodicRate, std::uint32_t const paymentRemaining, - TenthBips16 const managementFeeRate); + TenthBips32 const managementFeeRate); LoanState calculateRawLoanState( @@ -121,7 +131,7 @@ calculateRawLoanState( TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t const paymentRemaining, - TenthBips16 const managementFeeRate); + TenthBips32 const managementFeeRate); LoanState calculateRoundedLoanState( @@ -136,7 +146,7 @@ Number computeFee( Asset const& asset, Number const& value, - TenthBips16 managementFeeRate, + TenthBips32 managementFeeRate, std::int32_t scale); Number @@ -195,10 +205,18 @@ struct PaymentComponents struct LoanDeltas { - Number valueDelta; Number principalDelta; Number interestDueDelta; Number managementFeeDueDelta; + + Number + valueDelta() const + { + return principalDelta + interestDueDelta + managementFeeDueDelta; + } + + void + nonNegative(); }; PaymentComponents @@ -218,6 +236,9 @@ computePaymentComponents( detail::LoanDeltas operator-(LoanState const& lhs, LoanState const& rhs); +LoanState +operator-(LoanState const& lhs, detail::LoanDeltas const& rhs); + Number valueMinusFee( Asset const& asset, @@ -232,7 +253,7 @@ computeLoanProperties( TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t paymentsRemaining, - TenthBips16 managementFeeRate); + TenthBips32 managementFeeRate); bool isRounded(Asset const& asset, Number const& value, std::int32_t scale); diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index 2fd61aa7ff..eacaaceb3a 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -467,7 +467,7 @@ struct PaymentComponentsPlus : public PaymentComponents PaymentComponentsPlus( PaymentComponents const& p, Number f, - Number v = Number{}) + Number v = numZero) : PaymentComponents(p) , untrackedManagementFee(f) , untrackedInterest(v) @@ -673,7 +673,7 @@ tryOverpayment( auto const newRounded = calculateRoundedLoanState( totalValueOutstanding, principalOutstanding, managementFeeOutstanding); auto const valueChange = - newRounded.interestOutstanding - rounded.interestOutstanding; + newRounded.interestOutstanding() - rounded.interestOutstanding(); XRPL_ASSERT_PARTS( valueChange < beast::zero, "ripple::detail::tryOverpayment", @@ -999,6 +999,17 @@ PaymentComponents::trackedInterestPart() const (trackedPrincipalDelta + trackedManagementFeeDelta); } +void +LoanDeltas::nonNegative() +{ + if (principalDelta < beast::zero) + principalDelta = numZero; + if (interestDueDelta < beast::zero) + interestDueDelta = numZero; + if (managementFeeDueDelta < beast::zero) + managementFeeDueDelta = numZero; +} + PaymentComponents computePaymentComponents( Asset const& asset, @@ -1035,8 +1046,6 @@ computePaymentComponents( roundToAsset(asset, trueTarget.valueOutstanding, scale), .principalOutstanding = roundToAsset(asset, trueTarget.principalOutstanding, scale), - .interestOutstanding = - roundToAsset(asset, trueTarget.interestOutstanding, scale), .interestDue = roundToAsset(asset, trueTarget.interestDue, scale), .managementFeeDue = roundToAsset(asset, trueTarget.managementFeeDue, scale)}; @@ -1044,67 +1053,38 @@ computePaymentComponents( totalValueOutstanding, principalOutstanding, managementFeeOutstanding); LoanDeltas deltas = currentLedgerState - roundedTarget; - - // It should be impossible for any of the deltas to be negative, but do - // defensive checks - if (deltas.principalDelta < beast::zero) - { - // LCOV_EXCL_START - UNREACHABLE( - "ripple::detail::computePaymentComponents : negative principal " - "delta"); - deltas.principalDelta = Number::zero; - // LCOV_EXCL_STOP - } - if (deltas.interestDueDelta < beast::zero) - { - // LCOV_EXCL_START - UNREACHABLE( - "ripple::detail::computePaymentComponents : negative interest " - "delta"); - deltas.interestDueDelta = Number::zero; - // LCOV_EXCL_STOP - } - if (deltas.managementFeeDueDelta < beast::zero) - { - // LCOV_EXCL_START - UNREACHABLE( - "ripple::detail::computePaymentComponents : negative management " - "fee delta"); - deltas.managementFeeDueDelta = Number::zero; - // LCOV_EXCL_STOP - } + deltas.nonNegative(); // Adjust the deltas if necessary for data integrity XRPL_ASSERT_PARTS( deltas.principalDelta <= currentLedgerState.principalOutstanding, "ripple::detail::computePaymentComponents", "principal delta not greater than outstanding"); + deltas.principalDelta = std::min( deltas.principalDelta, currentLedgerState.principalOutstanding); + XRPL_ASSERT_PARTS( deltas.interestDueDelta <= currentLedgerState.interestDue, "ripple::detail::computePaymentComponents", "interest due delta not greater than outstanding"); + deltas.interestDueDelta = std::min( {deltas.interestDueDelta, - std::max(Number::zero, roundedPeriodicPayment - deltas.principalDelta), + std::max(numZero, roundedPeriodicPayment - deltas.principalDelta), currentLedgerState.interestDue}); + XRPL_ASSERT_PARTS( deltas.managementFeeDueDelta <= currentLedgerState.managementFeeDue, "ripple::detail::computePaymentComponents", "management fee due delta not greater than outstanding"); + deltas.managementFeeDueDelta = std::min( {deltas.managementFeeDueDelta, roundedPeriodicPayment - (deltas.principalDelta + deltas.interestDueDelta), currentLedgerState.managementFeeDue}); - // In case any adjustments were made (or if the original rounding didn't - // quite add up right), recompute the total value delta - deltas.valueDelta = deltas.principalDelta + deltas.interestDueDelta + - deltas.managementFeeDueDelta; - if (paymentRemaining == 1 || totalValueOutstanding <= roundedPeriodicPayment) { @@ -1112,15 +1092,15 @@ computePaymentComponents( // parts. XRPL_ASSERT_PARTS( - deltas.valueDelta == totalValueOutstanding, + deltas.valueDelta() <= totalValueOutstanding, "ripple::detail::computePaymentComponents", "last payment total value agrees"); XRPL_ASSERT_PARTS( - deltas.principalDelta == principalOutstanding, + deltas.principalDelta <= principalOutstanding, "ripple::detail::computePaymentComponents", "last payment principal agrees"); XRPL_ASSERT_PARTS( - deltas.managementFeeDueDelta == managementFeeOutstanding, + deltas.managementFeeDueDelta <= managementFeeOutstanding, "ripple::detail::computePaymentComponents", "last payment management fee agrees"); @@ -1231,13 +1211,12 @@ computePaymentComponents( // trying to take more than the whole payment. The excess can be positive, // which indicates that we're not going to take the whole payment amount, // but if so, it must be small. - auto takeFrom = [](Number& total, Number& component, Number& excess) { + auto takeFrom = [](Number& component, Number& excess) { if (excess > beast::zero) { // Take as much of the excess as we can out of the provided part and // the total auto part = std::min(component, excess); - total -= part; component -= part; excess -= part; } @@ -1248,14 +1227,41 @@ computePaymentComponents( "ripple::detail::computePaymentComponents", "excess non-negative"); }; + auto giveTo = + [](Number& component, Number& shortage, Number const& maximum) { + if (shortage > beast::zero) + { + // Put as much of the shortage as we can into the provided part + // and the total + auto part = std::min(maximum - component, shortage); + component += part; + shortage -= part; + } + // If the shortage goes negative, we put too much, which should be + // impossible + XRPL_ASSERT_PARTS( + shortage >= beast::zero, + "ripple::detail::computePaymentComponents", + "excess non-negative"); + }; auto addressExcess = [&takeFrom](LoanDeltas& deltas, Number& excess) { - takeFrom(deltas.valueDelta, deltas.interestDueDelta, excess); - takeFrom(deltas.valueDelta, deltas.managementFeeDueDelta, excess); - takeFrom(deltas.valueDelta, deltas.principalDelta, excess); + // This order is based on where errors are the least problematic + takeFrom(deltas.interestDueDelta, excess); + takeFrom(deltas.managementFeeDueDelta, excess); + takeFrom(deltas.principalDelta, excess); + }; + auto addressShortage = [&giveTo]( + LoanDeltas& deltas, + Number& shortage, + LoanState const& current) { + giveTo(deltas.interestDueDelta, shortage, current.interestDue); + giveTo(deltas.principalDelta, shortage, current.principalOutstanding); + giveTo( + deltas.managementFeeDueDelta, shortage, current.managementFeeDue); }; Number totalOverpayment = - deltas.valueDelta - currentLedgerState.valueOutstanding; - if (totalOverpayment > 0) + deltas.valueDelta() - currentLedgerState.valueOutstanding; + if (totalOverpayment > beast::zero) { // LCOV_EXCL_START UNREACHABLE( @@ -1264,8 +1270,9 @@ computePaymentComponents( addressExcess(deltas, totalOverpayment); // LCOV_EXCL_STOP } + // Make sure the parts don't add up to too much - Number shortage = roundedPeriodicPayment - deltas.valueDelta; + Number shortage = roundedPeriodicPayment - deltas.valueDelta(); XRPL_ASSERT_PARTS( isRounded(asset, shortage, scale), @@ -1280,38 +1287,63 @@ computePaymentComponents( shortage = -excess; } + else if (shortage > beast::zero && totalOverpayment < beast::zero) + { + // If there's a shortage, and there's room in the loan itself, we can + // top up the parts to make the payment correct. + shortage = std::min(-totalOverpayment, shortage); + addressShortage(deltas, shortage, currentLedgerState); + } // The shortage should never be negative, which indicates that the parts are - // trying to take more than the whole payment. The shortage can be positive, - // which indicates that we're not going to take the whole payment amount, - // but if so, it must be small. + // trying to take more than the whole payment. The shortage should not be + // positive, either, which indicates that we're not going to take the whole + // payment amount. Only the last payment should be allowed to have a + // shortage, and that's handled in a special case above. XRPL_ASSERT_PARTS( - shortage == beast::zero || - (shortage > beast::zero && - ((asset.integral() && shortage < 3) || - (scale - shortage.exponent() > 14))), + shortage == beast::zero, "ripple::detail::computePaymentComponents", - "excess is extremely small"); + "no shortage or excess"); +#if LOANCOMPLETE + /* + // This used to be part of the above assert. It will eventually be removed + // if proved accurate + || + (shortage > beast::zero && + ((asset.integral() && shortage < 3) || + (scale - shortage.exponent() > 14))) + */ +#endif XRPL_ASSERT_PARTS( - deltas.valueDelta == + deltas.valueDelta() == deltas.principalDelta + deltas.interestDueDelta + deltas.managementFeeDueDelta, "ripple::detail::computePaymentComponents", "total value adds up"); XRPL_ASSERT_PARTS( - deltas.principalDelta >= beast::zero, + deltas.principalDelta >= beast::zero && + deltas.principalDelta <= currentLedgerState.principalOutstanding, "ripple::detail::computePaymentComponents", - "non-negative principal"); + "valid principal result"); XRPL_ASSERT_PARTS( - deltas.interestDueDelta >= beast::zero, + deltas.interestDueDelta >= beast::zero && + deltas.interestDueDelta <= currentLedgerState.interestDue, "ripple::detail::computePaymentComponents", - "non-negative interest"); + "valid interest result"); XRPL_ASSERT_PARTS( - deltas.managementFeeDueDelta >= beast::zero, + deltas.managementFeeDueDelta >= beast::zero && + deltas.managementFeeDueDelta <= currentLedgerState.managementFeeDue, "ripple::detail::computePaymentComponents", - "non-negative fee"); + "valid fee result"); + + XRPL_ASSERT_PARTS( + deltas.principalDelta + deltas.interestDueDelta + + deltas.managementFeeDueDelta > + beast::zero, + "ripple::detail::computePaymentComponents", + "payment parts add to payment"); return PaymentComponents{ #if LOANCOMPLETE @@ -1319,11 +1351,18 @@ computePaymentComponents( .rawPrincipal = rawPrincipal, .rawManagementFee = rawFee, #endif - // As a final safety check, don't return any negative values - .trackedValueDelta = std::max(deltas.valueDelta, Number::zero), - .trackedPrincipalDelta = std::max(deltas.principalDelta, Number::zero), - .trackedManagementFeeDelta = - std::max(deltas.managementFeeDueDelta, Number::zero), + // As a final safety check, ensure the value is non-negative, and won't + // make the corresponding item negative + .trackedValueDelta = std::clamp( + deltas.valueDelta(), numZero, currentLedgerState.valueOutstanding), + .trackedPrincipalDelta = std::clamp( + deltas.principalDelta, + numZero, + currentLedgerState.principalOutstanding), + .trackedManagementFeeDelta = std::clamp( + deltas.managementFeeDueDelta, + numZero, + currentLedgerState.managementFeeDue), }; } @@ -1381,28 +1420,23 @@ detail::LoanDeltas operator-(LoanState const& lhs, LoanState const& rhs) { detail::LoanDeltas result{ - .valueDelta = lhs.valueOutstanding - rhs.valueOutstanding, .principalDelta = lhs.principalOutstanding - rhs.principalOutstanding, .interestDueDelta = lhs.interestDue - rhs.interestDue, .managementFeeDueDelta = lhs.managementFeeDue - rhs.managementFeeDue, }; - XRPL_ASSERT_PARTS( - result.valueDelta >= 0, - "ripple::operator-(LoanState,LoanState)", - "valueDelta difference non-negative"); - XRPL_ASSERT_PARTS( - result.principalDelta >= 0, - "ripple::operator-(LoanState,LoanState)", - "principalDelta difference non-negative"); - XRPL_ASSERT_PARTS( - result.interestDueDelta >= 0, - "ripple::operator-(LoanState,LoanState)", - "interestDueDelta difference non-negative"); - XRPL_ASSERT_PARTS( - result.managementFeeDueDelta >= 0, - "ripple::operator-(LoanState,LoanState)", - "managementFeeDueDelta difference non-negative"); + return result; +} + +LoanState +operator-(LoanState const& lhs, detail::LoanDeltas const& rhs) +{ + LoanState result{ + .valueOutstanding = lhs.valueOutstanding - rhs.valueDelta(), + .principalOutstanding = lhs.principalOutstanding - rhs.principalDelta, + .interestDue = lhs.interestDue - rhs.interestDueDelta, + .managementFeeDue = lhs.managementFeeDue - rhs.managementFeeDueDelta, + }; return result; } @@ -1470,14 +1504,13 @@ calculateRawLoanState( Number const& periodicPayment, Number const& periodicRate, std::uint32_t const paymentRemaining, - TenthBips16 const managementFeeRate) + TenthBips32 const managementFeeRate) { if (paymentRemaining == 0) { return LoanState{ .valueOutstanding = 0, .principalOutstanding = 0, - .interestOutstanding = 0, .interestDue = 0, .managementFeeDue = 0}; } @@ -1493,7 +1526,6 @@ calculateRawLoanState( return LoanState{ .valueOutstanding = rawValueOutstanding, .principalOutstanding = rawPrincipalOutstanding, - .interestOutstanding = rawInterestOutstanding, .interestDue = rawInterestOutstanding - rawManagementFeeOutstanding, .managementFeeDue = rawManagementFeeOutstanding}; }; @@ -1504,7 +1536,7 @@ calculateRawLoanState( TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t const paymentRemaining, - TenthBips16 const managementFeeRate) + TenthBips32 const managementFeeRate) { return calculateRawLoanState( periodicPayment, @@ -1521,13 +1553,11 @@ calculateRoundedLoanState( { // This implementation is pretty trivial, but ensures the calculations are // consistent everywhere, and reduces copy/paste errors. - Number const interestOutstanding = - totalValueOutstanding - principalOutstanding; return { .valueOutstanding = totalValueOutstanding, .principalOutstanding = principalOutstanding, - .interestOutstanding = interestOutstanding, - .interestDue = interestOutstanding - managementFeeOutstanding, + .interestDue = totalValueOutstanding - principalOutstanding - + managementFeeOutstanding, .managementFeeDue = managementFeeOutstanding}; } @@ -1544,7 +1574,7 @@ Number computeFee( Asset const& asset, Number const& value, - TenthBips16 managementFeeRate, + TenthBips32 managementFeeRate, std::int32_t scale) { return roundToAsset( @@ -1571,7 +1601,7 @@ computeLoanProperties( TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t paymentsRemaining, - TenthBips16 managementFeeRate) + TenthBips32 managementFeeRate) { auto const periodicRate = loanPeriodicRate(interestRate, paymentInterval); XRPL_ASSERT( diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index ae39b6ccdb..e994227064 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -374,6 +374,10 @@ LoanPay::doApply() paymentParts->principalPaid + paymentParts->interestPaid; auto const totalPaidToVaultRounded = roundToAsset(asset, totalPaidToVaultRaw, vaultScale, Number::downward); + XRPL_ASSERT_PARTS( + !asset.integral() || totalPaidToVaultRaw == totalPaidToVaultRounded, + "ripple::LoanPay::doApply", + "rounding does nothing for integral asset"); auto const totalPaidToVaultForDebt = totalPaidToVaultRaw - paymentParts->valueChange; @@ -405,7 +409,21 @@ LoanPay::doApply() // Vault object state changes view.update(vaultSle); + Number const assetsAvailableBefore = *assetsAvailableProxy; + Number const pseudoAccountBalanceBefore = accountHolds( + view, + vaultPseudoAccount, + asset, + FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, + j_); + { + XRPL_ASSERT_PARTS( + assetsAvailableBefore == pseudoAccountBalanceBefore, + "ripple::LoanPay::doApply", + "vault pseudo balance agrees before"); + auto assetsTotalProxy = vaultSle->at(sfAssetsTotal); assetsAvailableProxy += totalPaidToVaultRounded; @@ -415,6 +433,13 @@ LoanPay::doApply() *assetsAvailableProxy <= *assetsTotalProxy, "ripple::LoanPay::doApply", "assets available must not be greater than assets outstanding"); + + if (*assetsAvailableProxy > *assetsTotalProxy) + { + // LCOV_EXCL_START + return tecINTERNAL; + // LCOV_EXCL_STOP + } } // Move funds @@ -488,6 +513,19 @@ LoanPay::doApply() WaiveTransferFee::Yes)) return ter; + Number const assetsAvailableAfter = *assetsAvailableProxy; + Number const pseudoAccountBalanceAfter = accountHolds( + view, + vaultPseudoAccount, + asset, + FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, + j_); + XRPL_ASSERT_PARTS( + assetsAvailableAfter == pseudoAccountBalanceAfter, + "ripple::LoanPay::doApply", + "vault pseudo balance agrees after"); + #if !NDEBUG auto const accountBalanceAfter = accountCanSend( view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index d8b0441b21..329d400f89 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -318,6 +318,87 @@ LoanSet::preclaim(PreclaimContext const& ctx) return tesSUCCESS; } +TER +LoanSet::checkGuards( + Asset const& vaultAsset, + Number const& principalRequested, + TenthBips32 interestRate, + std::uint32_t paymentTotal, + LoanProperties const& properties, + beast::Journal j) +{ + auto const totalInterestOutstanding = + properties.totalValueOutstanding - principalRequested; + // Guard 1: if there is no computed total interest over the life of the + // loan for a non-zero interest rate, we cannot properly amortize the + // loan + if (interestRate > TenthBips32{0} && totalInterestOutstanding <= 0) + { + // Unless this is a zero-interest loan, there must be some interest + // due on the loan, even if it's (measurable) dust + JLOG(j.warn()) << "Loan for " << principalRequested << " with " + << interestRate << "% interest has no interest due"; + return tecPRECISION_LOSS; + } + // Guard 1a: If there is any interest computed over the life of the + // loan, for a zero interest rate, something went sideways. + if (interestRate == TenthBips32{0} && totalInterestOutstanding > 0) + { + // LCOV_EXCL_START + JLOG(j.warn()) << "Loan for " << principalRequested + << " with 0% interest has interest due"; + return tecINTERNAL; + // LCOV_EXCL_STOP + } + + // Guard 2: if the principal portion of the first periodic payment is + // too small to be accurately represented with the given rounding mode, + // raise an error + if (properties.firstPaymentPrincipal <= 0) + { + // Check that some true (unrounded) principal is paid each period. + // Since the first payment pays the least principal, if it's good, + // they'll all be good. Note that the outstanding principal is + // rounded, and may not change right away. + JLOG(j.warn()) << "Loan is unable to pay principal."; + return tecPRECISION_LOSS; + } + + // Guard 3: If the periodic payment is so small that it can't even be + // rounded to a representable value, then the loan can't be paid. Also, + // avoids dividing by 0. + auto const roundedPayment = roundPeriodicPayment( + vaultAsset, properties.periodicPayment, properties.loanScale); + if (roundedPayment == beast::zero) + { + JLOG(j.warn()) << "Loan Periodic payment (" + << properties.periodicPayment << ") rounds to 0. "; + return tecPRECISION_LOSS; + } + + // Guard 4: if the rounded periodic payment is large enough that the + // loan can't be amortized in the specified number of payments, raise an + // error + { + NumberRoundModeGuard mg(Number::upward); + + if (std::int64_t const computedPayments{ + properties.totalValueOutstanding / roundedPayment}; + computedPayments != paymentTotal) + { + JLOG(j.warn()) << "Loan Periodic payment (" + << properties.periodicPayment << ") rounding (" + << roundedPayment << ") on a total value of " + << properties.totalValueOutstanding + << " can not complete the loan in the specified " + "number of payments (" + << computedPayments << " != " << paymentTotal << ")"; + return tecPRECISION_LOSS; + } + } + return tesSUCCESS; +} + TER LoanSet::doApply() { @@ -396,72 +477,14 @@ LoanSet::doApply() } } - auto const totalInterestOutstanding = - properties.totalValueOutstanding - principalRequested; - // Guard 1: if there is no computed total interest over the life of the loan - // for a non-zero interest rate, we cannot properly amortize the loan - if (interestRate > TenthBips32{0} && totalInterestOutstanding <= 0) - { - // Unless this is a zero-interest loan, there must be some interest due - // on the loan, even if it's (measurable) dust - JLOG(j_.warn()) << "Loan for " << principalRequested << " with " - << interestRate << "% interest has no interest due"; - return tecPRECISION_LOSS; - } - // Guard 1a: If there is any interest computed over the life of the loan, - // for a zero interest rate, something went sideways. - if (interestRate == TenthBips32{0} && totalInterestOutstanding > 0) - { - // LCOV_EXCL_START - JLOG(j_.warn()) << "Loan for " << principalRequested - << " with 0% interest has interest due"; - return tecINTERNAL; - // LCOV_EXCL_STOP - } - - // Guard 2: if the principal portion of the first periodic payment is too - // small to be accurately represented with the given rounding mode, raise an - // error - if (properties.firstPaymentPrincipal <= 0) - { - // Check that some true (unrounded) principal is paid each period. Since - // the first payment pays the least principal, if it's good, they'll - // all be good. Note that the outstanding principal is rounded, and - // may not change right away. - JLOG(j_.warn()) << "Loan is unable to pay principal."; - return tecPRECISION_LOSS; - } - - // Guard 3: If the periodic payment is so small that it can't even be - // rounded to a representable value, then the loan can't be paid. Also, - // avoids dividing by 0. - auto const roundedPayment = roundPeriodicPayment( - vaultAsset, properties.periodicPayment, properties.loanScale); - if (roundedPayment == beast::zero) - { - JLOG(j_.warn()) << "Loan Periodic payment (" - << properties.periodicPayment << ") rounds to 0. "; - return tecPRECISION_LOSS; - } - - // Guard 4: if the rounded periodic payment is large enough that the loan - // can't be amortized in the specified number of payments, raise an error - { - NumberRoundModeGuard mg(Number::upward); - - if (std::int64_t const computedPayments{ - properties.totalValueOutstanding / roundedPayment}; - computedPayments != paymentTotal) - { - JLOG(j_.warn()) - << "Loan Periodic payment (" << properties.periodicPayment - << ") rounding (" << roundedPayment - << ") will complete the " - "loan in less than the specified number of payments (" - << computedPayments << " < " << paymentTotal << ")"; - return tecPRECISION_LOSS; - } - } + if (auto const ret = checkGuards( + vaultAsset, + principalRequested, + interestRate, + paymentTotal, + properties, + j_)) + return ret; // Check that the other computed values are valid if (properties.managementFeeOwedToBroker < 0 || diff --git a/src/xrpld/app/tx/detail/LoanSet.h b/src/xrpld/app/tx/detail/LoanSet.h index 68c7f4ca3c..04d9eccaf2 100644 --- a/src/xrpld/app/tx/detail/LoanSet.h +++ b/src/xrpld/app/tx/detail/LoanSet.h @@ -20,6 +20,7 @@ #ifndef RIPPLE_TX_LOANSET_H_INCLUDED #define RIPPLE_TX_LOANSET_H_INCLUDED +#include #include namespace ripple { @@ -54,6 +55,15 @@ public: static TER preclaim(PreclaimContext const& ctx); + static TER + checkGuards( + Asset const& vaultAsset, + Number const& principalRequested, + TenthBips32 interestRate, + std::uint32_t paymentTotal, + LoanProperties const& properties, + beast::Journal j); + TER doApply() override; From 34db04ffc1468a46b87d12f793c7157f0dc5b6ce Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 4 Nov 2025 19:33:27 -0500 Subject: [PATCH 182/291] Update Lending files for the xrpl rename --- src/test/app/LoanBroker_test.cpp | 19 -------------- src/test/app/Loan_test.cpp | 19 -------------- src/xrpld/app/misc/LendingHelpers.h | 25 +++---------------- src/xrpld/app/misc/detail/LendingHelpers.cpp | 19 -------------- .../app/tx/detail/LoanBrokerCoverClawback.cpp | 19 -------------- .../app/tx/detail/LoanBrokerCoverClawback.h | 23 ++--------------- .../app/tx/detail/LoanBrokerCoverDeposit.cpp | 19 -------------- .../app/tx/detail/LoanBrokerCoverDeposit.h | 23 ++--------------- .../app/tx/detail/LoanBrokerCoverWithdraw.cpp | 19 -------------- .../app/tx/detail/LoanBrokerCoverWithdraw.h | 23 ++--------------- src/xrpld/app/tx/detail/LoanBrokerDelete.cpp | 19 -------------- src/xrpld/app/tx/detail/LoanBrokerDelete.h | 23 ++--------------- src/xrpld/app/tx/detail/LoanBrokerSet.cpp | 19 -------------- src/xrpld/app/tx/detail/LoanBrokerSet.h | 23 ++--------------- src/xrpld/app/tx/detail/LoanDelete.cpp | 19 -------------- src/xrpld/app/tx/detail/LoanDelete.h | 23 ++--------------- src/xrpld/app/tx/detail/LoanManage.cpp | 19 -------------- src/xrpld/app/tx/detail/LoanManage.h | 23 ++--------------- src/xrpld/app/tx/detail/LoanPay.cpp | 19 -------------- src/xrpld/app/tx/detail/LoanPay.h | 23 ++--------------- src/xrpld/app/tx/detail/LoanSet.cpp | 19 -------------- src/xrpld/app/tx/detail/LoanSet.h | 23 ++--------------- 22 files changed, 21 insertions(+), 439 deletions(-) diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 4be39f4954..362d23ff15 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -1,22 +1,3 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2025 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - #include #include diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 261a139c72..cddfb0135a 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -1,22 +1,3 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2025 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - #include // #include diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 683f1ceaab..1c1ad916a1 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -1,24 +1,5 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2025 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#ifndef RIPPLE_APP_MISC_LENDINGHELPERS_H_INCLUDED -#define RIPPLE_APP_MISC_LENDINGHELPERS_H_INCLUDED +#ifndef XRPL_APP_MISC_LENDINGHELPERS_H_INCLUDED +#define XRPL_APP_MISC_LENDINGHELPERS_H_INCLUDED #include #include @@ -280,4 +261,4 @@ loanMakePayment( } // namespace ripple -#endif // RIPPLE_APP_MISC_LENDINGHELPERS_H_INCLUDED +#endif // XRPL_APP_MISC_LENDINGHELPERS_H_INCLUDED diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index eacaaceb3a..99a1d619b1 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -1,22 +1,3 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2025 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - #include // #include diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp index d278f733d6..453431964e 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp @@ -1,22 +1,3 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2025 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - #include // #include diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.h b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.h index 500e8c243d..183d3c4479 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.h +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.h @@ -1,24 +1,5 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2025 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#ifndef RIPPLE_TX_LOANBROKERCOVERCLAWBACK_H_INCLUDED -#define RIPPLE_TX_LOANBROKERCOVERCLAWBACK_H_INCLUDED +#ifndef XRPL_TX_LOANBROKERCOVERCLAWBACK_H_INCLUDED +#define XRPL_TX_LOANBROKERCOVERCLAWBACK_H_INCLUDED #include diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp index 7263628185..e9ba2cda64 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp @@ -1,22 +1,3 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2025 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - #include // #include diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.h b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.h index d3b88dc089..23863b479c 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.h +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.h @@ -1,24 +1,5 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2025 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#ifndef RIPPLE_TX_LOANBROKERCOVERDEPOSIT_H_INCLUDED -#define RIPPLE_TX_LOANBROKERCOVERDEPOSIT_H_INCLUDED +#ifndef XRPL_TX_LOANBROKERCOVERDEPOSIT_H_INCLUDED +#define XRPL_TX_LOANBROKERCOVERDEPOSIT_H_INCLUDED #include diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp index e7143bcf50..97d8c48b74 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp @@ -1,22 +1,3 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2025 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - #include // #include diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h index 27f6e7fe5b..eab2c9e60f 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h @@ -1,24 +1,5 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2025 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#ifndef RIPPLE_TX_LOANBROKERCOVERWITHDRAW_H_INCLUDED -#define RIPPLE_TX_LOANBROKERCOVERWITHDRAW_H_INCLUDED +#ifndef XRPL_TX_LOANBROKERCOVERWITHDRAW_H_INCLUDED +#define XRPL_TX_LOANBROKERCOVERWITHDRAW_H_INCLUDED #include diff --git a/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp b/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp index 07e05ec073..bae806b45e 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp @@ -1,22 +1,3 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2025 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - #include // #include diff --git a/src/xrpld/app/tx/detail/LoanBrokerDelete.h b/src/xrpld/app/tx/detail/LoanBrokerDelete.h index 21248a0331..8466fe4f95 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerDelete.h +++ b/src/xrpld/app/tx/detail/LoanBrokerDelete.h @@ -1,24 +1,5 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2025 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#ifndef RIPPLE_TX_LOANBROKERDELETE_H_INCLUDED -#define RIPPLE_TX_LOANBROKERDELETE_H_INCLUDED +#ifndef XRPL_TX_LOANBROKERDELETE_H_INCLUDED +#define XRPL_TX_LOANBROKERDELETE_H_INCLUDED #include diff --git a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp index 4b9ffff291..c575472e79 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp @@ -1,22 +1,3 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2025 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - #include // #include diff --git a/src/xrpld/app/tx/detail/LoanBrokerSet.h b/src/xrpld/app/tx/detail/LoanBrokerSet.h index c7a12875a2..39ed9bcd61 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerSet.h +++ b/src/xrpld/app/tx/detail/LoanBrokerSet.h @@ -1,24 +1,5 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2025 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#ifndef RIPPLE_TX_LOANBROKERSET_H_INCLUDED -#define RIPPLE_TX_LOANBROKERSET_H_INCLUDED +#ifndef XRPL_TX_LOANBROKERSET_H_INCLUDED +#define XRPL_TX_LOANBROKERSET_H_INCLUDED #include diff --git a/src/xrpld/app/tx/detail/LoanDelete.cpp b/src/xrpld/app/tx/detail/LoanDelete.cpp index ff91039da9..084de2cf3f 100644 --- a/src/xrpld/app/tx/detail/LoanDelete.cpp +++ b/src/xrpld/app/tx/detail/LoanDelete.cpp @@ -1,22 +1,3 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2025 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - #include // #include diff --git a/src/xrpld/app/tx/detail/LoanDelete.h b/src/xrpld/app/tx/detail/LoanDelete.h index 4013fb082a..cbc37dec14 100644 --- a/src/xrpld/app/tx/detail/LoanDelete.h +++ b/src/xrpld/app/tx/detail/LoanDelete.h @@ -1,24 +1,5 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2025 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#ifndef RIPPLE_TX_LOANDELETE_H_INCLUDED -#define RIPPLE_TX_LOANDELETE_H_INCLUDED +#ifndef XRPL_TX_LOANDELETE_H_INCLUDED +#define XRPL_TX_LOANDELETE_H_INCLUDED #include diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index 7b3dcd205b..839bc19ab1 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -1,22 +1,3 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2025 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - #include // #include diff --git a/src/xrpld/app/tx/detail/LoanManage.h b/src/xrpld/app/tx/detail/LoanManage.h index 0a043ca48c..dde1023cad 100644 --- a/src/xrpld/app/tx/detail/LoanManage.h +++ b/src/xrpld/app/tx/detail/LoanManage.h @@ -1,24 +1,5 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2025 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#ifndef RIPPLE_TX_LOANMANAGE_H_INCLUDED -#define RIPPLE_TX_LOANMANAGE_H_INCLUDED +#ifndef XRPL_TX_LOANMANAGE_H_INCLUDED +#define XRPL_TX_LOANMANAGE_H_INCLUDED #include diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index e994227064..5f0b34d5ca 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -1,22 +1,3 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2025 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - #include // #include diff --git a/src/xrpld/app/tx/detail/LoanPay.h b/src/xrpld/app/tx/detail/LoanPay.h index f6255e5bc7..3f8eb16d04 100644 --- a/src/xrpld/app/tx/detail/LoanPay.h +++ b/src/xrpld/app/tx/detail/LoanPay.h @@ -1,24 +1,5 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2025 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#ifndef RIPPLE_TX_LOANPAY_H_INCLUDED -#define RIPPLE_TX_LOANPAY_H_INCLUDED +#ifndef XRPL_TX_LOANPAY_H_INCLUDED +#define XRPL_TX_LOANPAY_H_INCLUDED #include diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 329d400f89..f25a536be4 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -1,22 +1,3 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2025 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - #include // #include diff --git a/src/xrpld/app/tx/detail/LoanSet.h b/src/xrpld/app/tx/detail/LoanSet.h index 04d9eccaf2..e7f4ef5503 100644 --- a/src/xrpld/app/tx/detail/LoanSet.h +++ b/src/xrpld/app/tx/detail/LoanSet.h @@ -1,24 +1,5 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2025 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#ifndef RIPPLE_TX_LOANSET_H_INCLUDED -#define RIPPLE_TX_LOANSET_H_INCLUDED +#ifndef XRPL_TX_LOANSET_H_INCLUDED +#define XRPL_TX_LOANSET_H_INCLUDED #include #include From 95fdbe520f71b8412ed2fc03f4cd0891e74a223f Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 5 Nov 2025 12:45:31 -0500 Subject: [PATCH 183/291] Partially revert aed8e2b166 Fill in payment computation shortages (#5941) - Do not attempt to fill in payment computation shortages. Fixes, tests, and optimizations are left intact. --- src/test/app/Loan_test.cpp | 6 ++++++ src/xrpld/app/misc/detail/LendingHelpers.cpp | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index cddfb0135a..157873872f 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -1999,6 +1999,7 @@ protected: state.paymentRemaining, broker.params.managementFeeRate); +#if LOANFILLSHORTAGE BEAST_EXPECT( paymentComponents.trackedValueDelta == roundedPeriodicPayment || @@ -2006,6 +2007,11 @@ protected: detail::PaymentSpecialCase::final && paymentComponents.trackedValueDelta < roundedPeriodicPayment)); +#else + BEAST_EXPECT( + paymentComponents.trackedValueDelta <= + roundedPeriodicPayment); +#endif ripple::LoanState const nextTrueState = calculateRawLoanState( diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index 99a1d619b1..08683632a6 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -1208,6 +1208,7 @@ computePaymentComponents( "ripple::detail::computePaymentComponents", "excess non-negative"); }; +#if LOANFILLSHORTAGE auto giveTo = [](Number& component, Number& shortage, Number const& maximum) { if (shortage > beast::zero) @@ -1225,12 +1226,14 @@ computePaymentComponents( "ripple::detail::computePaymentComponents", "excess non-negative"); }; +#endif auto addressExcess = [&takeFrom](LoanDeltas& deltas, Number& excess) { // This order is based on where errors are the least problematic takeFrom(deltas.interestDueDelta, excess); takeFrom(deltas.managementFeeDueDelta, excess); takeFrom(deltas.principalDelta, excess); }; +#if LOANFILLSHORTAGE auto addressShortage = [&giveTo]( LoanDeltas& deltas, Number& shortage, @@ -1240,6 +1243,7 @@ computePaymentComponents( giveTo( deltas.managementFeeDueDelta, shortage, current.managementFeeDue); }; +#endif Number totalOverpayment = deltas.valueDelta() - currentLedgerState.valueOutstanding; if (totalOverpayment > beast::zero) @@ -1268,6 +1272,7 @@ computePaymentComponents( shortage = -excess; } +#if LOANFILLSHORTAGE else if (shortage > beast::zero && totalOverpayment < beast::zero) { // If there's a shortage, and there's room in the loan itself, we can @@ -1285,6 +1290,16 @@ computePaymentComponents( shortage == beast::zero, "ripple::detail::computePaymentComponents", "no shortage or excess"); +#else + // The shortage should never be negative, which indicates that the + // parts are trying to take more than the whole payment. The + // shortage may be positive, which indicates that we're not going to + // take the whole payment amount. + XRPL_ASSERT_PARTS( + shortage >= beast::zero, + "ripple::detail::computePaymentComponents", + "no shortage or excess"); +#endif #if LOANCOMPLETE /* // This used to be part of the above assert. It will eventually be removed From 165478b929e736f037bfff6c69a6fa14724fd56a Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 5 Nov 2025 13:28:03 -0500 Subject: [PATCH 184/291] Remove the shortage code completely --- src/test/app/Loan_test.cpp | 10 ---- src/xrpld/app/misc/detail/LendingHelpers.cpp | 50 -------------------- 2 files changed, 60 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 157873872f..d52f057135 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -1999,19 +1999,9 @@ protected: state.paymentRemaining, broker.params.managementFeeRate); -#if LOANFILLSHORTAGE - BEAST_EXPECT( - paymentComponents.trackedValueDelta == - roundedPeriodicPayment || - (paymentComponents.specialCase == - detail::PaymentSpecialCase::final && - paymentComponents.trackedValueDelta < - roundedPeriodicPayment)); -#else BEAST_EXPECT( paymentComponents.trackedValueDelta <= roundedPeriodicPayment); -#endif ripple::LoanState const nextTrueState = calculateRawLoanState( diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index 08683632a6..2887096504 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -1208,42 +1208,12 @@ computePaymentComponents( "ripple::detail::computePaymentComponents", "excess non-negative"); }; -#if LOANFILLSHORTAGE - auto giveTo = - [](Number& component, Number& shortage, Number const& maximum) { - if (shortage > beast::zero) - { - // Put as much of the shortage as we can into the provided part - // and the total - auto part = std::min(maximum - component, shortage); - component += part; - shortage -= part; - } - // If the shortage goes negative, we put too much, which should be - // impossible - XRPL_ASSERT_PARTS( - shortage >= beast::zero, - "ripple::detail::computePaymentComponents", - "excess non-negative"); - }; -#endif auto addressExcess = [&takeFrom](LoanDeltas& deltas, Number& excess) { // This order is based on where errors are the least problematic takeFrom(deltas.interestDueDelta, excess); takeFrom(deltas.managementFeeDueDelta, excess); takeFrom(deltas.principalDelta, excess); }; -#if LOANFILLSHORTAGE - auto addressShortage = [&giveTo]( - LoanDeltas& deltas, - Number& shortage, - LoanState const& current) { - giveTo(deltas.interestDueDelta, shortage, current.interestDue); - giveTo(deltas.principalDelta, shortage, current.principalOutstanding); - giveTo( - deltas.managementFeeDueDelta, shortage, current.managementFeeDue); - }; -#endif Number totalOverpayment = deltas.valueDelta() - currentLedgerState.valueOutstanding; if (totalOverpayment > beast::zero) @@ -1272,25 +1242,6 @@ computePaymentComponents( shortage = -excess; } -#if LOANFILLSHORTAGE - else if (shortage > beast::zero && totalOverpayment < beast::zero) - { - // If there's a shortage, and there's room in the loan itself, we can - // top up the parts to make the payment correct. - shortage = std::min(-totalOverpayment, shortage); - addressShortage(deltas, shortage, currentLedgerState); - } - - // The shortage should never be negative, which indicates that the parts are - // trying to take more than the whole payment. The shortage should not be - // positive, either, which indicates that we're not going to take the whole - // payment amount. Only the last payment should be allowed to have a - // shortage, and that's handled in a special case above. - XRPL_ASSERT_PARTS( - shortage == beast::zero, - "ripple::detail::computePaymentComponents", - "no shortage or excess"); -#else // The shortage should never be negative, which indicates that the // parts are trying to take more than the whole payment. The // shortage may be positive, which indicates that we're not going to @@ -1299,7 +1250,6 @@ computePaymentComponents( shortage >= beast::zero, "ripple::detail::computePaymentComponents", "no shortage or excess"); -#endif #if LOANCOMPLETE /* // This used to be part of the above assert. It will eventually be removed From d10a5786633124322f0cd0417c9ead8f9da9dfe3 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 4 Nov 2025 20:44:45 -0500 Subject: [PATCH 185/291] Add optional enforcement of valid integer range to Number --- include/xrpl/basics/Number.h | 116 ++++++++++++++++++++-- src/libxrpl/basics/Number.cpp | 40 ++++++++ src/test/basics/Number_test.cpp | 168 ++++++++++++++++++++++++++++++++ 3 files changed, 317 insertions(+), 7 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index e34cc61b5b..59f3a212a6 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -13,16 +13,47 @@ class Number; std::string to_string(Number const& amount); +template +constexpr bool +isPowerOfTen(T value) +{ + while (value >= 10 && value % 10 == 0) + value /= 10; + return value == 1; +} + class Number { +public: + /** Describes whether and how to enforce this number as an integer. + * + * - none: No enforcement. The value may vary freely. This is the default. + * - weak: If the absolute value is greater than maxIntValue, valid() will + * return false. + * - strong: Assignment operations will throw if the absolute value is above + * maxIntValue. + */ + enum EnforceInteger { none, weak, strong }; + +private: using rep = std::int64_t; rep mantissa_{0}; int exponent_{std::numeric_limits::lowest()}; + // The enforcement setting is not serialized, and does not affect the + // ledger. If not "none", the value is checked to be within the valid + // integer range. With "strong", the checks will be made as automatic as + // possible. + EnforceInteger enforceInteger_ = none; + public: // The range for the mantissa when normalized - constexpr static std::int64_t minMantissa = 1'000'000'000'000'000LL; - constexpr static std::int64_t maxMantissa = 9'999'999'999'999'999LL; + constexpr static rep minMantissa = 1'000'000'000'000'000LL; + static_assert(isPowerOfTen(minMantissa)); + constexpr static rep maxMantissa = minMantissa * 10 - 1; + static_assert(maxMantissa == 9'999'999'999'999'999LL); + + constexpr static rep maxIntValue = minMantissa / 10; // The range for the exponent when normalized constexpr static int minExponent = -32768; @@ -35,15 +66,33 @@ public: explicit constexpr Number() = default; - Number(rep mantissa); - explicit Number(rep mantissa, int exponent); + Number(rep mantissa, EnforceInteger enforce = none); + explicit Number(rep mantissa, int exponent, EnforceInteger enforce = none); explicit constexpr Number(rep mantissa, int exponent, unchecked) noexcept; + constexpr Number(Number const& other) = default; + constexpr Number(Number&& other) = default; + + ~Number() = default; + + constexpr Number& + operator=(Number const& other); + constexpr Number& + operator=(Number&& other); constexpr rep mantissa() const noexcept; constexpr int exponent() const noexcept; + void + setIntegerEnforcement(EnforceInteger enforce); + + EnforceInteger + integerEnforcement() const noexcept; + + bool + valid() const noexcept; + constexpr Number operator+() const noexcept; constexpr Number @@ -184,6 +233,9 @@ public: private: static thread_local rounding_mode mode_; + void + checkInteger(char const* what) const; + void normalize(); constexpr bool @@ -197,16 +249,52 @@ inline constexpr Number::Number(rep mantissa, int exponent, unchecked) noexcept { } -inline Number::Number(rep mantissa, int exponent) - : mantissa_{mantissa}, exponent_{exponent} +inline Number::Number(rep mantissa, int exponent, EnforceInteger enforce) + : mantissa_{mantissa}, exponent_{exponent}, enforceInteger_(enforce) { normalize(); + + checkInteger("Number::Number integer overflow"); } -inline Number::Number(rep mantissa) : Number{mantissa, 0} +inline Number::Number(rep mantissa, EnforceInteger enforce) + : Number{mantissa, 0, enforce} { } +constexpr Number& +Number::operator=(Number const& other) +{ + if (this != &other) + { + mantissa_ = other.mantissa_; + exponent_ = other.exponent_; + enforceInteger_ = std::max(enforceInteger_, other.enforceInteger_); + + checkInteger("Number::operator= integer overflow"); + } + + return *this; +} + +constexpr Number& +Number::operator=(Number&& other) +{ + if (this != &other) + { + // std::move doesn't really do anything for these types, but + // this is future-proof in case the types ever change + mantissa_ = std::move(other.mantissa_); + exponent_ = std::move(other.exponent_); + if (other.enforceInteger_ > enforceInteger_) + enforceInteger_ = std::move(other.enforceInteger_); + + checkInteger("Number::operator= integer overflow"); + } + + return *this; +} + inline constexpr Number::rep Number::mantissa() const noexcept { @@ -219,6 +307,20 @@ Number::exponent() const noexcept return exponent_; } +inline void +Number::setIntegerEnforcement(EnforceInteger enforce) +{ + enforceInteger_ = enforce; + + checkInteger("Number::setIntegerEnforcement integer overflow"); +} + +inline Number::EnforceInteger +Number::integerEnforcement() const noexcept +{ + return enforceInteger_; +} + inline constexpr Number Number::operator+() const noexcept { diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 89f7937e06..e3789de90a 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -155,6 +155,13 @@ Number::Guard::round() noexcept constexpr Number one{1000000000000000, -15, Number::unchecked{}}; +void +Number::checkInteger(char const* what) const +{ + if (enforceInteger_ == strong && !valid()) + throw std::overflow_error(what); +} + void Number::normalize() { @@ -207,9 +214,27 @@ Number::normalize() mantissa_ = -mantissa_; } +bool +Number::valid() const noexcept +{ + if (enforceInteger_ != none) + { + static Number const max = maxIntValue; + static Number const maxNeg = -maxIntValue; + // Avoid making a copy + if (mantissa_ < 0) + return *this >= maxNeg; + return *this <= max; + } + return true; +} + Number& Number::operator+=(Number const& y) { + // The strictest setting prevails + enforceInteger_ = std::max(enforceInteger_, y.enforceInteger_); + if (y == Number{}) return *this; if (*this == Number{}) @@ -322,6 +347,9 @@ Number::operator+=(Number const& y) } mantissa_ = xm * xn; exponent_ = xe; + + checkInteger("Number::addition integer overflow"); + return *this; } @@ -356,6 +384,9 @@ divu10(uint128_t& u) Number& Number::operator*=(Number const& y) { + // The strictest setting prevails + enforceInteger_ = std::max(enforceInteger_, y.enforceInteger_); + if (*this == Number{}) return *this; if (y == Number{}) @@ -422,12 +453,18 @@ Number::operator*=(Number const& y) XRPL_ASSERT( isnormal() || *this == Number{}, "ripple::Number::operator*=(Number) : result is normal"); + + checkInteger("Number::multiplication integer overflow"); + return *this; } Number& Number::operator/=(Number const& y) { + // The strictest setting prevails + enforceInteger_ = std::max(enforceInteger_, y.enforceInteger_); + if (y == Number{}) throw std::overflow_error("Number: divide by 0"); if (*this == Number{}) @@ -455,6 +492,9 @@ Number::operator/=(Number const& y) exponent_ = ne - de - 17; mantissa_ *= np * dp; normalize(); + + checkInteger("Number::division integer overflow"); + return *this; } diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 06203a4c2a..78c9b28952 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -725,6 +726,172 @@ public: BEAST_EXPECT(Number(-100, -30000).truncate() == Number(0, 0)); } + void + testInteger() + { + testcase("Integer enforcement"); + + using namespace std::string_literals; + + { + Number a{100}; + BEAST_EXPECT(a.integerEnforcement() == Number::none); + BEAST_EXPECT(a.valid()); + a = Number{1, 30}; + BEAST_EXPECT(a.valid()); + a = -100; + BEAST_EXPECT(a.valid()); + } + { + Number a{100, Number::weak}; + BEAST_EXPECT(a.integerEnforcement() == Number::weak); + BEAST_EXPECT(a.valid()); + a = Number{1, 30, Number::none}; + BEAST_EXPECT(!a.valid()); + a = -100; + BEAST_EXPECT(a.integerEnforcement() == Number::weak); + BEAST_EXPECT(a.valid()); + a = Number{5, Number::strong}; + BEAST_EXPECT(a.integerEnforcement() == Number::strong); + BEAST_EXPECT(a.valid()); + } + { + Number a{100, Number::strong}; + BEAST_EXPECT(a.integerEnforcement() == Number::strong); + BEAST_EXPECT(a.valid()); + try + { + a = Number{1, 30}; + BEAST_EXPECT(false); + } + catch (std::overflow_error const& e) + { + BEAST_EXPECT(e.what() == "Number::operator= integer overflow"s); + // The throw is done _after_ the number is updated. + BEAST_EXPECT((a == Number{1, 30})); + } + BEAST_EXPECT(!a.valid()); + a = -100; + BEAST_EXPECT(a.integerEnforcement() == Number::strong); + BEAST_EXPECT(a.valid()); + } + { + Number a{INITIAL_XRP.drops(), Number::weak}; + BEAST_EXPECT(!a.valid()); + a = -a; + BEAST_EXPECT(!a.valid()); + + try + { + a.setIntegerEnforcement(Number::strong); + BEAST_EXPECT(false); + } + catch (std::overflow_error const& e) + { + BEAST_EXPECT( + e.what() == + "Number::setIntegerEnforcement integer overflow"s); + // The throw is internal to the operator before the result is + // assigned to the Number + BEAST_EXPECT(a == -INITIAL_XRP); + BEAST_EXPECT(!a.valid()); + } + try + { + ++a; + BEAST_EXPECT(false); + } + catch (std::overflow_error const& e) + { + BEAST_EXPECT(e.what() == "Number::addition integer overflow"s); + // The throw is internal to the operator before the result is + // assigned to the Number + BEAST_EXPECT(a == -INITIAL_XRP); + BEAST_EXPECT(!a.valid()); + } + a = Number::maxIntValue; + try + { + ++a; + BEAST_EXPECT(false); + } + catch (std::overflow_error const& e) + { + BEAST_EXPECT(e.what() == "Number::addition integer overflow"s); + // This time, the throw is done _after_ the number is updated. + BEAST_EXPECT(a == Number::maxIntValue + 1); + BEAST_EXPECT(!a.valid()); + } + a = -Number::maxIntValue; + try + { + --a; + BEAST_EXPECT(false); + } + catch (std::overflow_error const& e) + { + BEAST_EXPECT(e.what() == "Number::addition integer overflow"s); + // This time, the throw is done _after_ the number is updated. + BEAST_EXPECT(a == -Number::maxIntValue - 1); + BEAST_EXPECT(!a.valid()); + } + a = Number(1, 10); + try + { + a *= Number(1, 10); + BEAST_EXPECT(false); + } + catch (std::overflow_error const& e) + { + BEAST_EXPECT( + e.what() == "Number::multiplication integer overflow"s); + // The throw is done _after_ the number is updated. + BEAST_EXPECT((a == Number{1, 20})); + BEAST_EXPECT(!a.valid()); + } + try + { + a = Number::maxIntValue * 2; + BEAST_EXPECT(false); + } + catch (std::overflow_error const& e) + { + BEAST_EXPECT(e.what() == "Number::operator= integer overflow"s); + // The throw is done _after_ the number is updated. + BEAST_EXPECT((a == Number{2, 14})); + BEAST_EXPECT(!a.valid()); + } + try + { + a = Number(3, 15, Number::strong); + BEAST_EXPECT(false); + } + catch (std::overflow_error const& e) + { + BEAST_EXPECT(e.what() == "Number::Number integer overflow"s); + // The Number doesn't get updated because the ctor throws + BEAST_EXPECT((a == Number{2, 14})); + BEAST_EXPECT(!a.valid()); + } + a = Number(1, 10); + try + { + a /= Number(1, -10); + BEAST_EXPECT(false); + } + catch (std::overflow_error const& e) + { + BEAST_EXPECT(e.what() == "Number::division integer overflow"s); + // The throw is done _after_ the number is updated. + BEAST_EXPECT((a == Number{1, 20})); + BEAST_EXPECT(!a.valid()); + } + a /= Number(1, 15); + BEAST_EXPECT((a == Number{1, 5})); + BEAST_EXPECT(a.valid()); + } + } + void run() override { @@ -746,6 +913,7 @@ public: test_inc_dec(); test_toSTAmount(); test_truncate(); + testInteger(); } }; From 93d99a671cd83aa0e0ac2b4bcf0fa2a32fab9797 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 4 Nov 2025 21:02:39 -0500 Subject: [PATCH 186/291] Make all STNumber fields "soeDEFAULT" --- include/xrpl/protocol/detail/ledger_entries.macro | 6 +++--- src/test/app/Vault_test.cpp | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 53110f09f5..5aae9a9322 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -479,10 +479,10 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({ {sfAccount, soeREQUIRED}, {sfData, soeOPTIONAL}, {sfAsset, soeREQUIRED}, - {sfAssetsTotal, soeREQUIRED}, - {sfAssetsAvailable, soeREQUIRED}, + {sfAssetsTotal, soeDEFAULT}, + {sfAssetsAvailable, soeDEFAULT}, {sfAssetsMaximum, soeDEFAULT}, - {sfLossUnrealized, soeREQUIRED}, + {sfLossUnrealized, soeDEFAULT}, {sfShareMPTID, soeREQUIRED}, {sfWithdrawalPolicy, soeREQUIRED}, {sfScale, soeDEFAULT}, diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index ccbf0fed42..58b929f1a2 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -4438,7 +4438,8 @@ class Vault_test : public beast::unit_test::suite BEAST_EXPECT(checkString(vault, sfAssetsAvailable, "50")); BEAST_EXPECT(checkString(vault, sfAssetsMaximum, "1000")); BEAST_EXPECT(checkString(vault, sfAssetsTotal, "50")); - BEAST_EXPECT(checkString(vault, sfLossUnrealized, "0")); + // Since this field is default, it is not returned. + BEAST_EXPECT(!vault.isMember(sfLossUnrealized.getJsonName())); auto const strShareID = strHex(sle->at(sfShareMPTID)); BEAST_EXPECT(checkString(vault, sfShareMPTID, strShareID)); From 16609ccaad52a140d4b2ce8a127243bc3ed206fe Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 4 Nov 2025 21:07:01 -0500 Subject: [PATCH 187/291] Add integer enforcement when converting to XRP/MPTAmount to Number --- include/xrpl/protocol/MPTAmount.h | 2 +- include/xrpl/protocol/XRPAmount.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/xrpl/protocol/MPTAmount.h b/include/xrpl/protocol/MPTAmount.h index af14786501..1c7c7a25e7 100644 --- a/include/xrpl/protocol/MPTAmount.h +++ b/include/xrpl/protocol/MPTAmount.h @@ -64,7 +64,7 @@ public: operator Number() const noexcept { - return value(); + return {value(), Number::strong}; } /** Return the sign of the amount */ diff --git a/include/xrpl/protocol/XRPAmount.h b/include/xrpl/protocol/XRPAmount.h index e102a3707f..f26bb7366f 100644 --- a/include/xrpl/protocol/XRPAmount.h +++ b/include/xrpl/protocol/XRPAmount.h @@ -143,7 +143,7 @@ public: operator Number() const noexcept { - return drops(); + return {drops(), Number::weak}; } /** Return the sign of the amount */ From 82f68496b8832a941394d60d24737de6cb5ea697 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 5 Nov 2025 09:21:12 -0500 Subject: [PATCH 188/291] Fix build error - avoid copy --- src/test/app/AMM_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index 5a1816ebae..66bceec327 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -1384,7 +1384,7 @@ private: // equal asset deposit: unit test to exercise the rounding-down of // LPTokens in the AMMHelpers.cpp: adjustLPTokens calculations // The LPTokens need to have 16 significant digits and a fractional part - for (Number const deltaLPTokens : + for (Number const& deltaLPTokens : {Number{UINT64_C(100000'0000000009), -10}, Number{UINT64_C(100000'0000000001), -10}}) { From bd196c7609ec49efe2ae9ef9a4b0517c00ec5060 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 5 Nov 2025 18:15:49 -0500 Subject: [PATCH 189/291] Catch up the consequences of Number changes - Change the Number::maxIntValue to all 9's. - Add integral() to Asset (copied from Lending) - Add toNumber() functions to STAmount, MPTAmount, XRPAmount to allow explicit conversions with enforcement options. - Add optional Number::EnforceInteger options to STAmount and STNumber ctors, conversions, etc. IOUs are never checked. - Update Vault transactors, and helper functions, to check restrictions. - Fix and add Vault tests. --- include/xrpl/basics/Number.h | 3 +- include/xrpl/protocol/Asset.h | 6 ++ include/xrpl/protocol/MPTAmount.h | 8 ++- include/xrpl/protocol/STAmount.h | 67 ++++++++++++++++++- include/xrpl/protocol/STNumber.h | 12 ++++ include/xrpl/protocol/SystemParameters.h | 1 + include/xrpl/protocol/XRPAmount.h | 6 ++ src/libxrpl/ledger/View.cpp | 23 +++++-- src/libxrpl/protocol/STAmount.cpp | 19 ++++++ src/libxrpl/protocol/STNumber.cpp | 18 ++++++ src/test/app/Vault_test.cpp | 78 +++++++++++++++++++++-- src/test/basics/Number_test.cpp | 4 +- src/xrpld/app/tx/detail/VaultClawback.cpp | 19 +++++- src/xrpld/app/tx/detail/VaultDeposit.cpp | 23 +++++-- src/xrpld/app/tx/detail/VaultWithdraw.cpp | 11 +++- 15 files changed, 274 insertions(+), 24 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index 59f3a212a6..4f447d5f98 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -53,7 +53,8 @@ public: constexpr static rep maxMantissa = minMantissa * 10 - 1; static_assert(maxMantissa == 9'999'999'999'999'999LL); - constexpr static rep maxIntValue = minMantissa / 10; + constexpr static rep maxIntValue = maxMantissa / 10; + static_assert(maxIntValue == 999'999'999'999'999LL); // The range for the exponent when normalized constexpr static int minExponent = -32768; diff --git a/include/xrpl/protocol/Asset.h b/include/xrpl/protocol/Asset.h index d0efe814a9..6765f7cf7d 100644 --- a/include/xrpl/protocol/Asset.h +++ b/include/xrpl/protocol/Asset.h @@ -84,6 +84,12 @@ public: return holds() && get().native(); } + bool + integral() const + { + return !holds() || get().native(); + } + friend constexpr bool operator==(Asset const& lhs, Asset const& rhs); diff --git a/include/xrpl/protocol/MPTAmount.h b/include/xrpl/protocol/MPTAmount.h index 1c7c7a25e7..5f552d839b 100644 --- a/include/xrpl/protocol/MPTAmount.h +++ b/include/xrpl/protocol/MPTAmount.h @@ -62,11 +62,17 @@ public: explicit constexpr operator bool() const noexcept; - operator Number() const noexcept + operator Number() const { return {value(), Number::strong}; } + Number + toNumber(Number::EnforceInteger enforce) const + { + return {value(), enforce}; + } + /** Return the sign of the amount */ constexpr int signum() const noexcept; diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index 83493efcdd..d0092fdbec 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -40,6 +40,12 @@ private: exponent_type mOffset; bool mIsNegative; + // The Enforce integer setting is not stored or serialized. If set, it is + // used during automatic conversions to Number. If not set, the default + // behavior is used. It can also be overridden when coverting by using + // toNumber(). + std::optional enforceConversion_; + public: using value_type = STAmount; @@ -135,9 +141,28 @@ public: STAmount(A const& asset, int mantissa, int exponent = 0); template - STAmount(A const& asset, Number const& number) + STAmount( + A const& asset, + Number const& number, + std::optional enforce = std::nullopt) : STAmount(asset, number.mantissa(), number.exponent()) { + enforceConversion_ = enforce; + if (!enforce) + { + // Use the default conversion behavior + [[maybe_unused]] + Number const n = *this; + } + else if (enforce == Number::strong) + { + // Throw if it's not valid + if (!validNumber()) + { + Throw( + "STAmount::STAmount integer Number lost precision"); + } + } } // Legacy support for new-style amounts @@ -145,6 +170,17 @@ public: STAmount(XRPAmount const& amount); STAmount(MPTAmount const& amount, MPTIssue const& mptIssue); operator Number() const; + Number + toNumber(Number::EnforceInteger enforce) const; + + void + setIntegerEnforcement(std::optional enforce); + + std::optional + integerEnforcement() const noexcept; + + bool + validNumber() const noexcept; //-------------------------------------------------------------------------- // @@ -155,6 +191,9 @@ public: int exponent() const noexcept; + bool + integral() const noexcept; + bool native() const noexcept; @@ -435,6 +474,12 @@ STAmount::exponent() const noexcept return mOffset; } +inline bool +STAmount::integral() const noexcept +{ + return mAsset.integral(); +} + inline bool STAmount::native() const noexcept { @@ -510,6 +555,8 @@ inline STAmount::operator bool() const noexcept inline STAmount::operator Number() const { + if (enforceConversion_) + return toNumber(*enforceConversion_); if (native()) return xrp(); if (mAsset.holds()) @@ -517,6 +564,17 @@ inline STAmount::operator Number() const return iou(); } +inline Number +STAmount::toNumber(Number::EnforceInteger enforce) const +{ + if (native()) + return xrp().toNumber(enforce); + if (mAsset.holds()) + return mpt().toNumber(enforce); + // It doesn't make sense to enforce limits on IOUs + return iou(); +} + inline STAmount& STAmount::operator=(beast::Zero) { @@ -538,6 +596,11 @@ STAmount::operator=(Number const& number) mValue = mIsNegative ? -number.mantissa() : number.mantissa(); mOffset = number.exponent(); canonicalize(); + + // Convert it back to a Number to check that it's valid + [[maybe_unused]] + Number n = *this; + return *this; } @@ -553,7 +616,7 @@ STAmount::clear() { // The -100 is used to allow 0 to sort less than a small positive values // which have a negative exponent. - mOffset = native() ? 0 : -100; + mOffset = integral() ? 0 : -100; mValue = 0; mIsNegative = false; } diff --git a/include/xrpl/protocol/STNumber.h b/include/xrpl/protocol/STNumber.h index 2ec3d66fd1..43b96a2b46 100644 --- a/include/xrpl/protocol/STNumber.h +++ b/include/xrpl/protocol/STNumber.h @@ -56,6 +56,18 @@ public: bool isDefault() const override; + /// Sets the flag on the underlying number + void + setIntegerEnforcement(Number::EnforceInteger enforce); + + /// Gets the flag value on the underlying number + Number::EnforceInteger + integerEnforcement() const noexcept; + + /// Checks the underlying number + bool + valid() const noexcept; + operator Number() const { return value_; diff --git a/include/xrpl/protocol/SystemParameters.h b/include/xrpl/protocol/SystemParameters.h index de78b65265..7a2c5a7f63 100644 --- a/include/xrpl/protocol/SystemParameters.h +++ b/include/xrpl/protocol/SystemParameters.h @@ -23,6 +23,7 @@ systemName() /** Number of drops in the genesis account. */ constexpr XRPAmount INITIAL_XRP{100'000'000'000 * DROPS_PER_XRP}; +static_assert(INITIAL_XRP.drops() == 100'000'000'000'000'000); /** Returns true if the amount does not exceed the initial XRP in existence. */ inline bool diff --git a/include/xrpl/protocol/XRPAmount.h b/include/xrpl/protocol/XRPAmount.h index f26bb7366f..159174accc 100644 --- a/include/xrpl/protocol/XRPAmount.h +++ b/include/xrpl/protocol/XRPAmount.h @@ -146,6 +146,12 @@ public: return {drops(), Number::weak}; } + Number + toNumber(Number::EnforceInteger enforce) const + { + return {value(), enforce}; + } + /** Return the sign of the amount */ constexpr int signum() const noexcept diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index 0175b099ea..92fd5ccc97 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -2878,13 +2878,17 @@ assetsToSharesDeposit( Number const assetTotal = vault->at(sfAssetsTotal); STAmount shares{vault->at(sfShareMPTID)}; + shares.setIntegerEnforcement(Number::weak); if (assetTotal == 0) return STAmount{ shares.asset(), Number(assets.mantissa(), assets.exponent() + vault->at(sfScale)) - .truncate()}; + .truncate(), + Number::weak}; - Number const shareTotal = issuance->at(sfOutstandingAmount); + Number const shareTotal{ + unsafe_cast(issuance->at(sfOutstandingAmount)), + Number::strong}; shares = (shareTotal * (assets / assetTotal)).truncate(); return shares; } @@ -2906,6 +2910,7 @@ sharesToAssetsDeposit( Number const assetTotal = vault->at(sfAssetsTotal); STAmount assets{vault->at(sfAsset)}; + assets.setIntegerEnforcement(Number::weak); if (assetTotal == 0) return STAmount{ assets.asset(), @@ -2913,7 +2918,9 @@ sharesToAssetsDeposit( shares.exponent() - vault->at(sfScale), false}; - Number const shareTotal = issuance->at(sfOutstandingAmount); + Number const shareTotal{ + unsafe_cast(issuance->at(sfOutstandingAmount)), + Number::strong}; assets = assetTotal * (shares / shareTotal); return assets; } @@ -2937,9 +2944,12 @@ assetsToSharesWithdraw( Number assetTotal = vault->at(sfAssetsTotal); assetTotal -= vault->at(sfLossUnrealized); STAmount shares{vault->at(sfShareMPTID)}; + shares.setIntegerEnforcement(Number::weak); if (assetTotal == 0) return shares; - Number const shareTotal = issuance->at(sfOutstandingAmount); + Number const shareTotal{ + unsafe_cast(issuance->at(sfOutstandingAmount)), + Number::strong}; Number result = shareTotal * (assets / assetTotal); if (truncate == TruncateShares::yes) result = result.truncate(); @@ -2965,9 +2975,12 @@ sharesToAssetsWithdraw( Number assetTotal = vault->at(sfAssetsTotal); assetTotal -= vault->at(sfLossUnrealized); STAmount assets{vault->at(sfAsset)}; + assets.setIntegerEnforcement(Number::weak); if (assetTotal == 0) return assets; - Number const shareTotal = issuance->at(sfOutstandingAmount); + Number const shareTotal{ + unsafe_cast(issuance->at(sfOutstandingAmount)), + Number::strong}; assets = assetTotal * (shares / shareTotal); return assets; } diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 4aac551003..6f6355a145 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -255,6 +255,25 @@ STAmount::move(std::size_t n, void* buf) return emplace(n, buf, std::move(*this)); } +void +STAmount::setIntegerEnforcement(std::optional enforce) +{ + enforceConversion_ = enforce; +} + +std::optional +STAmount::integerEnforcement() const noexcept +{ + return enforceConversion_; +} + +bool +STAmount::validNumber() const noexcept +{ + Number n = toNumber(Number::EnforceInteger::weak); + return n.valid(); +} + //------------------------------------------------------------------------------ // // Conversion diff --git a/src/libxrpl/protocol/STNumber.cpp b/src/libxrpl/protocol/STNumber.cpp index 889dd9ee68..5486864e95 100644 --- a/src/libxrpl/protocol/STNumber.cpp +++ b/src/libxrpl/protocol/STNumber.cpp @@ -94,6 +94,24 @@ STNumber::isDefault() const return value_ == Number(); } +void +STNumber::setIntegerEnforcement(Number::EnforceInteger enforce) +{ + value_.setIntegerEnforcement(enforce); +} + +Number::EnforceInteger +STNumber::integerEnforcement() const noexcept +{ + return value_.integerEnforcement(); +} + +bool +STNumber::valid() const noexcept +{ + return value_.valid(); +} + std::ostream& operator<<(std::ostream& out, STNumber const& rhs) { diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 58b929f1a2..5b36ffaf8e 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -3597,7 +3597,32 @@ class Vault_test : public beast::unit_test::suite }); testCase(18, [&, this](Env& env, Data d) { - testcase("Scale deposit overflow on second deposit"); + testcase("MPT scale deposit overflow"); + // The computed number of shares can not be represented as an MPT + // without truncation + + { + auto tx = d.vault.deposit( + {.depositor = d.depositor, + .id = d.keylet.key, + .amount = d.asset(5)}); + env(tx, ter{tecPRECISION_LOSS}); + env.close(); + } + }); + + testCase(14, [&, this](Env& env, Data d) { + testcase("MPT scale deposit overflow on first deposit"); + auto tx = d.vault.deposit( + {.depositor = d.depositor, + .id = d.keylet.key, + .amount = d.asset(10)}); + env(tx, ter{tecPRECISION_LOSS}); + env.close(); + }); + + testCase(14, [&, this](Env& env, Data d) { + testcase("MPT scale deposit overflow on second deposit"); { auto tx = d.vault.deposit( @@ -3618,8 +3643,8 @@ class Vault_test : public beast::unit_test::suite } }); - testCase(18, [&, this](Env& env, Data d) { - testcase("Scale deposit overflow on total shares"); + testCase(14, [&, this](Env& env, Data d) { + testcase("No MPT scale deposit overflow on total shares"); { auto tx = d.vault.deposit( @@ -3635,7 +3660,7 @@ class Vault_test : public beast::unit_test::suite {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(5)}); - env(tx, ter{tecPATH_DRY}); + env(tx); env.close(); } }); @@ -3919,6 +3944,28 @@ class Vault_test : public beast::unit_test::suite testCase(18, [&, this](Env& env, Data d) { testcase("Scale withdraw overflow"); + { + auto tx = d.vault.deposit( + {.depositor = d.depositor, + .id = d.keylet.key, + .amount = d.asset(5)}); + env(tx, ter{tecPRECISION_LOSS}); + env.close(); + } + + { + auto tx = d.vault.withdraw( + {.depositor = d.depositor, + .id = d.keylet.key, + .amount = STAmount(d.asset, Number(10, 0))}); + env(tx, ter{tecPRECISION_LOSS}); + env.close(); + } + }); + + testCase(14, [&, this](Env& env, Data d) { + testcase("MPT scale withdraw overflow"); + { auto tx = d.vault.deposit( {.depositor = d.depositor, @@ -4137,6 +4184,29 @@ class Vault_test : public beast::unit_test::suite testCase(18, [&, this](Env& env, Data d) { testcase("Scale clawback overflow"); + { + auto tx = d.vault.deposit( + {.depositor = d.depositor, + .id = d.keylet.key, + .amount = d.asset(5)}); + env(tx, ter(tecPRECISION_LOSS)); + env.close(); + } + + { + auto tx = d.vault.clawback( + {.issuer = d.issuer, + .id = d.keylet.key, + .holder = d.depositor, + .amount = STAmount(d.asset, Number(10, 0))}); + env(tx, ter{tecPRECISION_LOSS}); + env.close(); + } + }); + + testCase(14, [&, this](Env& env, Data d) { + testcase("MPT Scale clawback overflow"); + { auto tx = d.vault.deposit( {.depositor = d.depositor, diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 78c9b28952..a5fba8ef7d 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -858,7 +858,7 @@ public: { BEAST_EXPECT(e.what() == "Number::operator= integer overflow"s); // The throw is done _after_ the number is updated. - BEAST_EXPECT((a == Number{2, 14})); + BEAST_EXPECT((a == Number::maxIntValue * 2)); BEAST_EXPECT(!a.valid()); } try @@ -870,7 +870,7 @@ public: { BEAST_EXPECT(e.what() == "Number::Number integer overflow"s); // The Number doesn't get updated because the ctor throws - BEAST_EXPECT((a == Number{2, 14})); + BEAST_EXPECT((a == Number::maxIntValue * 2)); BEAST_EXPECT(!a.valid()); } a = Number(1, 10); diff --git a/src/xrpld/app/tx/detail/VaultClawback.cpp b/src/xrpld/app/tx/detail/VaultClawback.cpp index 7c56ca1d60..1c73d36bb0 100644 --- a/src/xrpld/app/tx/detail/VaultClawback.cpp +++ b/src/xrpld/app/tx/detail/VaultClawback.cpp @@ -71,9 +71,13 @@ VaultClawback::preclaim(PreclaimContext const& ctx) } Asset const vaultAsset = vault->at(sfAsset); - if (auto const amount = ctx.tx[~sfAmount]; - amount && vaultAsset != amount->asset()) - return tecWRONG_ASSET; + if (auto const amount = ctx.tx[~sfAmount]) + { + if (vaultAsset != amount->asset()) + return tecWRONG_ASSET; + else if (!amount->validNumber()) + return tecPRECISION_LOSS; + } if (vaultAsset.native()) { @@ -157,6 +161,8 @@ VaultClawback::doApply() MPTIssue const share{mptIssuanceID}; STAmount sharesDestroyed = {share}; STAmount assetsRecovered; + assetsRecovered.setIntegerEnforcement(Number::weak); + sharesDestroyed.setIntegerEnforcement(Number::weak); try { if (amount == beast::zero) @@ -169,6 +175,9 @@ VaultClawback::doApply() AuthHandling::ahIGNORE_AUTH, j_); + if (!sharesDestroyed.validNumber()) + return tecPRECISION_LOSS; + auto const maybeAssets = sharesToAssetsWithdraw(vault, sleIssuance, sharesDestroyed); if (!maybeAssets) @@ -184,6 +193,8 @@ VaultClawback::doApply() if (!maybeShares) return tecINTERNAL; // LCOV_EXCL_LINE sharesDestroyed = *maybeShares; + if (!sharesDestroyed.validNumber()) + return tecPRECISION_LOSS; } auto const maybeAssets = @@ -192,6 +203,8 @@ VaultClawback::doApply() return tecINTERNAL; // LCOV_EXCL_LINE assetsRecovered = *maybeAssets; } + if (!assetsRecovered.validNumber()) + return tecPRECISION_LOSS; // Clamp to maximum. if (assetsRecovered > *assetsAvailable) diff --git a/src/xrpld/app/tx/detail/VaultDeposit.cpp b/src/xrpld/app/tx/detail/VaultDeposit.cpp index 3e5ae741e3..66e144312f 100644 --- a/src/xrpld/app/tx/detail/VaultDeposit.cpp +++ b/src/xrpld/app/tx/detail/VaultDeposit.cpp @@ -42,6 +42,9 @@ VaultDeposit::preclaim(PreclaimContext const& ctx) if (assets.asset() != vaultAsset) return tecWRONG_ASSET; + if (!assets.validNumber()) + return tecPRECISION_LOSS; + if (vaultAsset.native()) ; // No special checks for XRP else if (vaultAsset.holds()) @@ -217,6 +220,7 @@ VaultDeposit::doApply() } STAmount sharesCreated = {vault->at(sfShareMPTID)}, assetsDeposited; + sharesCreated.setIntegerEnforcement(Number::weak); try { // Compute exchange before transferring any amounts. @@ -227,14 +231,14 @@ VaultDeposit::doApply() return tecINTERNAL; // LCOV_EXCL_LINE sharesCreated = *maybeShares; } - if (sharesCreated == beast::zero) + if (sharesCreated == beast::zero || !sharesCreated.validNumber()) return tecPRECISION_LOSS; auto const maybeAssets = sharesToAssetsDeposit(vault, sleIssuance, sharesCreated); if (!maybeAssets) return tecINTERNAL; // LCOV_EXCL_LINE - else if (*maybeAssets > amount) + else if (*maybeAssets > amount || !maybeAssets->validNumber()) { // LCOV_EXCL_START JLOG(j_.error()) << "VaultDeposit: would take more than offered."; @@ -260,13 +264,22 @@ VaultDeposit::doApply() sharesCreated.asset() != assetsDeposited.asset(), "ripple::VaultDeposit::doApply : assets are not shares"); - vault->at(sfAssetsTotal) += assetsDeposited; - vault->at(sfAssetsAvailable) += assetsDeposited; + auto assetsTotalProxy = vault->at(sfAssetsTotal); + auto assetsAvailableProxy = vault->at(sfAssetsAvailable); + if (vault->at(sfAsset).value().integral()) + { + assetsTotalProxy.value().setIntegerEnforcement(Number::weak); + assetsAvailableProxy.value().setIntegerEnforcement(Number::weak); + } + assetsTotalProxy += assetsDeposited; + assetsAvailableProxy += assetsDeposited; + if (!assetsTotalProxy->valid() || !assetsAvailableProxy->valid()) + return tecLIMIT_EXCEEDED; view().update(vault); // A deposit must not push the vault over its limit. auto const maximum = *vault->at(sfAssetsMaximum); - if (maximum != 0 && *vault->at(sfAssetsTotal) > maximum) + if (maximum != 0 && *assetsTotalProxy > maximum) return tecLIMIT_EXCEEDED; // Transfer assets from depositor to vault. diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.cpp b/src/xrpld/app/tx/detail/VaultWithdraw.cpp index 8cd3f5cd97..8806f7b236 100644 --- a/src/xrpld/app/tx/detail/VaultWithdraw.cpp +++ b/src/xrpld/app/tx/detail/VaultWithdraw.cpp @@ -50,6 +50,9 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx) if (assets.asset() != vaultAsset && assets.asset() != vaultShare) return tecWRONG_ASSET; + if (!assets.validNumber()) + return tecPRECISION_LOSS; + if (vaultAsset.native()) ; // No special checks for XRP else if (vaultAsset.holds()) @@ -154,6 +157,8 @@ VaultWithdraw::doApply() MPTIssue const share{mptIssuanceID}; STAmount sharesRedeemed = {share}; STAmount assetsWithdrawn; + assetsWithdrawn.setIntegerEnforcement(Number::weak); + sharesRedeemed.setIntegerEnforcement(Number::weak); try { if (amount.asset() == vaultAsset) @@ -167,13 +172,15 @@ VaultWithdraw::doApply() sharesRedeemed = *maybeShares; } - if (sharesRedeemed == beast::zero) + if (sharesRedeemed == beast::zero || !sharesRedeemed.validNumber()) return tecPRECISION_LOSS; auto const maybeAssets = sharesToAssetsWithdraw(vault, sleIssuance, sharesRedeemed); if (!maybeAssets) return tecINTERNAL; // LCOV_EXCL_LINE assetsWithdrawn = *maybeAssets; + if (!assetsWithdrawn.validNumber()) + return tecPRECISION_LOSS; } else if (amount.asset() == share) { @@ -184,6 +191,8 @@ VaultWithdraw::doApply() if (!maybeAssets) return tecINTERNAL; // LCOV_EXCL_LINE assetsWithdrawn = *maybeAssets; + if (!assetsWithdrawn.validNumber()) + return tecPRECISION_LOSS; } else return tefINTERNAL; // LCOV_EXCL_LINE From 07ec253f2e47bd33b5b96bbf9651e17679da88f1 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 6 Nov 2025 12:48:02 -0500 Subject: [PATCH 190/291] Add testDustManipulation test, which is expected to fail --- src/test/app/Loan_test.cpp | 157 +++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index d52f057135..507b9117ce 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -5637,6 +5637,162 @@ protected: } #endif + void + testDustManipulation() + { + using namespace jtx; + using namespace std::chrono_literals; + Env env(*this, all); + + // Setup: Create accounts + Account issuer{"issuer"}; + Account lender{"lender"}; + Account borrower{"borrower"}; + Account victim{"victim"}; + + env.fund(XRP(1'000'000'00), issuer, lender, borrower, victim); + env.close(); + + // Step 1: Create vault with IOU asset + auto asset = issuer["USD"]; + env(trust(lender, asset(100000))); + env(trust(borrower, asset(100000))); + env(trust(victim, asset(100000))); + env(pay(issuer, lender, asset(50000))); + env(pay(issuer, borrower, asset(50000))); + env(pay(issuer, victim, asset(50000))); + env.close(); + + BrokerParameters brokerParams{ + .vaultDeposit = 10000, + .debtMax = Number{0}, + .coverRateMin = TenthBips32{1000}, + .coverRateLiquidation = TenthBips32{2500}}; + + auto broker = createVaultAndBroker(env, asset, lender, brokerParams); + + auto const loanKeyletOpt = [&]() -> std::optional { + auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); + if (!BEAST_EXPECT(brokerSle)) + return std::nullopt; + + // Broker has no loans + BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); + + // The loan keylet is based on the LoanSequence of the + // _LOAN_BROKER_ object. + auto const loanSequence = brokerSle->at(sfLoanSequence); + return keylet::loan(broker.brokerID, loanSequence); + }(); + if (!loanKeyletOpt) + return; + + auto const vaultKeyletOpt = [&]() -> std::optional { + auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); + if (!BEAST_EXPECT(brokerSle)) + return std::nullopt; + return keylet::vault(brokerSle->at(sfVaultID)); + }(); + if (!BEAST_EXPECT(vaultKeyletOpt)) + return; + auto const& vaultKeylet = *vaultKeyletOpt; + + { + auto const vaultSle = env.le(vaultKeylet); + Number assetsTotal = vaultSle->at(sfAssetsTotal); + Number assetsAvail = vaultSle->at(sfAssetsAvailable); + + log << "Before loan creation:" << std::endl; + log << " AssetsTotal: " << assetsTotal << std::endl; + log << " AssetsAvailable: " << assetsAvail << std::endl; + log << " Difference: " << (assetsTotal - assetsAvail) << std::endl; + + // before the loan the assets total and available should be equal + BEAST_EXPECT(assetsAvail == assetsTotal); + BEAST_EXPECT( + assetsAvail == + broker.asset(brokerParams.vaultDeposit).number()); + } + + Keylet const& loanKeylet = *loanKeyletOpt; + + LoanParameters const loanParams{ + .account = lender, + .counter = borrower, + .principalRequest = Number{100}, + .interest = TenthBips32{1922}, + .payTotal = 5816, + .payInterval = 86400 * 6, + .gracePd = 86400 * 5, + }; + + env(loanParams(env, broker)); + env.close(); + + // Wait for loan to be late enough to default + env.close(std::chrono::seconds(86400 * 40)); // 40 days + + { + auto const vaultSle = env.le(vaultKeylet); + Number assetsTotal = vaultSle->at(sfAssetsTotal); + Number assetsAvail = vaultSle->at(sfAssetsAvailable); + + log << "After loan creation:" << std::endl; + log << " AssetsTotal: " << assetsTotal << std::endl; + log << " AssetsAvailable: " << assetsAvail << std::endl; + log << " Difference: " << (assetsTotal - assetsAvail) << std::endl; + + auto const loanSle = env.le(loanKeylet); + if (!BEAST_EXPECT(loanSle)) + return; + auto const state = calculateRoundedLoanState(loanSle); + + log << "Loan state:" << std::endl; + log << " ValueOutstanding: " << state.valueOutstanding + << std::endl; + log << " PrincipalOutstanding: " << state.principalOutstanding + << std::endl; + log << " InterestOutstanding: " << state.interestOutstanding() + << std::endl; + log << " InterestDue: " << state.interestDue << std::endl; + log << " FeeDue: " << state.managementFeeDue << std::endl; + + // after loan creation the assets total and available should + // reflect the value of the loan + BEAST_EXPECT(assetsAvail < assetsTotal); + BEAST_EXPECT( + assetsAvail == + broker + .asset( + brokerParams.vaultDeposit - loanParams.principalRequest) + .number()); + BEAST_EXPECT( + assetsTotal == + broker.asset(brokerParams.vaultDeposit + state.interestDue) + .number()); + } + + // Step 7: Trigger default (dust adjustment will occur) + env(jtx::loan::manage(lender, loanKeylet.key, tfLoanDefault)); + env.close(); + + // Step 8: Verify phantom assets created + { + auto const vaultSle2 = env.le(vaultKeylet); + Number assetsTotal2 = vaultSle2->at(sfAssetsTotal); + Number assetsAvail2 = vaultSle2->at(sfAssetsAvailable); + + log << "After default:" << std::endl; + log << " AssetsTotal: " << assetsTotal2 << std::endl; + log << " AssetsAvailable: " << assetsAvail2 << std::endl; + log << " Difference: " << (assetsTotal2 - assetsAvail2) + << std::endl; + + // after a default the assets total and available should be equal + BEAST_EXPECT(assetsAvail2 == assetsTotal2); + } + } + public: void run() override @@ -5647,6 +5803,7 @@ public: testPoC_UnsignedUnderflowOnFullPayAfterEarlyPeriodic(); testLoanCoverMinimumRoundingExploit(); #endif + testDustManipulation(); testIssuerLoan(); testDisabled(); From 560ff824dfa549462d56dfb3a905e2a0ccadcc54 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 6 Nov 2025 19:12:02 -0500 Subject: [PATCH 191/291] Another arbitrary test case to try --- src/test/app/Loan_test.cpp | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 507b9117ce..610f9a39d5 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -6072,18 +6072,18 @@ failed with assertion error: Both principal and interest rounded are zero 0 + 0 */ BrokerParameters const brokerParams{ - .vaultDeposit = 1'000'000'000, + .vaultDeposit = 10000, .debtMax = 0, .coverRateMin = TenthBips32{0}, - .managementFeeRate = TenthBips16{5919}, + // .managementFeeRate = TenthBips16{5919}, .coverRateLiquidation = TenthBips32{0}}; LoanParameters const loanParams{ .account = Account("lender"), .counter = Account("borrower"), - .principalRequest = Number{1255438, -6}, - .interest = TenthBips32{1922}, - .payTotal = 5816, - .payInterval = 29193}; + .principalRequest = Number{10000, 0}, + // .interest = TenthBips32{0}, + // .payTotal = 5816, + .payInterval = 150}; { Env env(*this, beast::severities::kWarning); @@ -6092,9 +6092,10 @@ failed with assertion error: Both principal and interest rounded are zero 0 + 0 auto const props = computeLoanProperties( asset, asset(loanParams.principalRequest).number(), - *loanParams.interest, - *loanParams.payInterval, - *loanParams.payTotal, + loanParams.interest.value_or(TenthBips32{}), + loanParams.payInterval.value_or( + LoanSet::defaultPaymentInterval), + loanParams.payTotal.value_or(LoanSet::defaultPaymentTotal), brokerParams.managementFeeRate); log << "Loan properties:\n" << "\tPeriodic Payment: " << props.periodicPayment << std::endl @@ -6108,9 +6109,9 @@ failed with assertion error: Both principal and interest rounded are zero 0 + 0 // checkGuards returns a TER, so success is 0 BEAST_EXPECT(!LoanSet::checkGuards( asset, - loanParams.principalRequest, - *loanParams.interest, - *loanParams.payTotal, + asset(loanParams.principalRequest).number(), + loanParams.interest.value_or(TenthBips32{}), + loanParams.payTotal.value_or(LoanSet::defaultPaymentTotal), props, env.journal)); } From 2264713d528db5c6a3debfcae1b02a1201d8851b Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 6 Nov 2025 19:20:53 -0500 Subject: [PATCH 192/291] fix: Improve rounding for IOU loans - Scale the loan to the Vault, so that amounts moving to the vault are less likely to have rounding errors. - Similar to LoanPay, when LoanManage defaults a loan, round the amounts to the Vault scale (because the Vault scale can change) before applying them to the Vault. --- src/test/app/Loan_test.cpp | 84 ++++++++++++++------ src/xrpld/app/misc/LendingHelpers.h | 3 +- src/xrpld/app/misc/detail/LendingHelpers.cpp | 37 +++++---- src/xrpld/app/tx/detail/LoanManage.cpp | 43 +++++----- src/xrpld/app/tx/detail/LoanPay.cpp | 6 +- src/xrpld/app/tx/detail/LoanSet.cpp | 15 ++-- 6 files changed, 120 insertions(+), 68 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 610f9a39d5..9239603fc0 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -107,14 +107,43 @@ protected: { jtx::PrettyAsset asset; uint256 brokerID; + uint256 vaultID; BrokerParameters params; BrokerInfo( jtx::PrettyAsset const& asset_, - uint256 const& brokerID_, + Keylet const& brokerKeylet_, + Keylet const& vaultKeylet_, BrokerParameters const& p) - : asset(asset_), brokerID(brokerID_), params(p) + : asset(asset_) + , brokerID(brokerKeylet_.key) + , vaultID(vaultKeylet_.key) + , params(p) { } + + Keylet + brokerKeylet() const + { + return keylet::loanbroker(brokerID); + } + Keylet + vaultKeylet() const + { + return keylet::vault(vaultID); + } + + int + vaultScale(jtx::Env const& env) const + { + using namespace jtx; + + auto const vaultSle = env.le(keylet::vault(vaultID)); + if (!vaultSle) + // This function is not important enough to return an optional. + // Return an impossibly small number + return STAmount::cMinOffset - 1; + return vaultSle->at(sfAssetsTotal).exponent(); + } }; struct LoanParameters @@ -448,7 +477,7 @@ protected: env.close(); - return {asset, keylet.key, params}; + return {asset, keylet, vaultKeylet, params}; } /// Get the state without checking anything @@ -502,9 +531,12 @@ protected: BEAST_EXPECT(state.paymentRemaining == 12); BEAST_EXPECT(state.principalOutstanding == broker.asset(1000).value()); BEAST_EXPECT( - state.loanScale == - (broker.asset.integral() ? 0 - : state.principalOutstanding.exponent())); + state.loanScale >= + (broker.asset.integral() + ? 0 + : std::max( + broker.vaultScale(env), + state.principalOutstanding.exponent()))); BEAST_EXPECT(state.paymentInterval == 600); BEAST_EXPECT( state.totalValue == @@ -759,9 +791,12 @@ protected: startDate + *loanParams.payInterval); BEAST_EXPECT(loan->at(sfPaymentRemaining) == *loanParams.payTotal); BEAST_EXPECT( - loan->at(sfLoanScale) == - (broker.asset.integral() ? 0 - : principalRequestAmount.exponent())); + loan->at(sfLoanScale) >= + (broker.asset.integral() + ? 0 + : std::max( + broker.vaultScale(env), + principalRequestAmount.exponent()))); BEAST_EXPECT( loan->at(sfPrincipalOutstanding) == principalRequestAmount); } @@ -774,13 +809,14 @@ protected: state.interestRate, state.paymentInterval, state.paymentRemaining, - broker.params.managementFeeRate); + broker.params.managementFeeRate, + state.loanScale); verifyLoanStatus( 0, startDate + *loanParams.payInterval, *loanParams.payTotal, - broker.asset.integral() ? 0 : principalRequestAmount.exponent(), + state.loanScale, loanProperties.totalValueOutstanding, principalRequestAmount, loanProperties.managementFeeOwedToBroker, @@ -837,7 +873,7 @@ protected: 0, nextDueDate, *loanParams.payTotal, - broker.asset.integral() ? 0 : principalRequestAmount.exponent(), + loanProperties.loanScale, loanProperties.totalValueOutstanding, principalRequestAmount, loanProperties.managementFeeOwedToBroker, @@ -1394,10 +1430,12 @@ protected: BEAST_EXPECT(brokerSle)) { BEAST_EXPECT( - state.loanScale == + state.loanScale >= (broker.asset.integral() ? 0 - : state.principalOutstanding.exponent())); + : std::max( + broker.vaultScale(env), + state.principalOutstanding.exponent()))); auto const defaultAmount = roundToAsset( broker.asset, std::min( @@ -1688,7 +1726,10 @@ protected: state.loanScale); BEAST_EXPECT( payoffAmount == - broker.asset(Number(1040000114155251, -12))); + roundToAsset( + broker.asset, + broker.asset(Number(1040000114155251, -12)).number(), + state.loanScale)); // The terms of this loan actually make the early payoff // more expensive than just making payments @@ -5687,15 +5728,7 @@ protected: if (!loanKeyletOpt) return; - auto const vaultKeyletOpt = [&]() -> std::optional { - auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - return std::nullopt; - return keylet::vault(brokerSle->at(sfVaultID)); - }(); - if (!BEAST_EXPECT(vaultKeyletOpt)) - return; - auto const& vaultKeylet = *vaultKeyletOpt; + auto const& vaultKeylet = broker.vaultKeylet(); { auto const vaultSle = env.le(vaultKeylet); @@ -6096,7 +6129,8 @@ failed with assertion error: Both principal and interest rounded are zero 0 + 0 loanParams.payInterval.value_or( LoanSet::defaultPaymentInterval), loanParams.payTotal.value_or(LoanSet::defaultPaymentTotal), - brokerParams.managementFeeRate); + brokerParams.managementFeeRate, + asset(brokerParams.vaultDeposit).number().exponent()); log << "Loan properties:\n" << "\tPeriodic Payment: " << props.periodicPayment << std::endl << "\tTotal Value: " << props.totalValueOutstanding << std::endl diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 1c1ad916a1..d9e27414fa 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -234,7 +234,8 @@ computeLoanProperties( TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t paymentsRemaining, - TenthBips32 managementFeeRate); + TenthBips32 managementFeeRate, + std::int32_t minimumScale); bool isRounded(Asset const& asset, Number const& value, std::int32_t scale); diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index 2887096504..f8ed477b9b 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -601,7 +601,8 @@ tryOverpayment( interestRate, paymentInterval, paymentRemaining, - managementFeeRate); + managementFeeRate, + loanScale); auto const newRaw = calculateRawLoanState( newLoanProperties.periodicPayment, @@ -1547,7 +1548,8 @@ computeLoanProperties( TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t paymentsRemaining, - TenthBips32 managementFeeRate) + TenthBips32 managementFeeRate, + std::int32_t minimumScale) { auto const periodicRate = loanPeriodicRate(interestRate, paymentInterval); XRPL_ASSERT( @@ -1556,12 +1558,12 @@ computeLoanProperties( auto const periodicPayment = detail::loanPeriodicPayment( principalOutstanding, periodicRate, paymentsRemaining); - STAmount const totalValueOutstanding = [&]() { + auto const [totalValueOutstanding, loanScale] = [&]() { NumberRoundModeGuard mg(Number::to_nearest); // Use STAmount's internal rounding instead of roundToAsset, because // we're going to use this result to determine the scale for all the // other rounding. - return STAmount{ + STAmount amount{ asset, /* * This formula is from the XLS-66 spec, section 3.2.4.2 (Total @@ -1569,18 +1571,23 @@ computeLoanProperties( * = ..." */ periodicPayment * paymentsRemaining}; + + // Base the loan scale on the total value, since that's going to be the + // biggest number involved (barring unusual parameters for late, full, + // or over payments) + auto const loanScale = std::max(minimumScale, amount.exponent()); + XRPL_ASSERT_PARTS( + (amount.integral() && loanScale == 0) || + (!amount.integral() && + loanScale >= static_cast(amount).exponent()), + "ripple::computeLoanProperties", + "loanScale value fits expectations"); + + // We may need to truncate the total value because of the minimum scale + amount = roundToAsset(asset, amount, loanScale, Number::to_nearest); + + return std::make_pair(amount, loanScale); }(); - // Base the loan scale on the total value, since that's going to be the - // biggest number involved (barring unusual parameters for late, full, or - // over payments) - auto const loanScale = totalValueOutstanding.exponent(); - XRPL_ASSERT_PARTS( - (totalValueOutstanding.integral() && loanScale == 0) || - (!totalValueOutstanding.integral() && - loanScale == - static_cast(totalValueOutstanding).exponent()), - "ripple::computeLoanProperties", - "loanScale value fits expectations"); // Since we just figured out the loan scale, we haven't been able to // validate that the principal fits in it, so to allow this function to diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index 839bc19ab1..4d20a3f6f0 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -169,9 +169,15 @@ LoanManage::defaultLoan( { // Decrease the Total Value of the Vault: - auto vaultAssetsTotalProxy = vaultSle->at(sfAssetsTotal); - auto vaultAssetsAvailableProxy = vaultSle->at(sfAssetsAvailable); - if (vaultAssetsTotalProxy < vaultDefaultAmount) + auto vaultTotalProxy = vaultSle->at(sfAssetsTotal); + auto vaultAvailableProxy = vaultSle->at(sfAssetsAvailable); + + // The vault may be at a different scale than the loan. Reduce rounding + // errors during the accounting by rounding some of the values to that + // scale. + auto const vaultScale = vaultTotalProxy->value().exponent(); + + if (vaultTotalProxy < vaultDefaultAmount) { // LCOV_EXCL_START JLOG(j.warn()) @@ -179,23 +185,24 @@ LoanManage::defaultLoan( return tefBAD_LEDGER; // LCOV_EXCL_STOP } - vaultAssetsTotalProxy -= vaultDefaultAmount; + + auto const vaultDefaultRounded = roundToAsset( + vaultAsset, vaultDefaultAmount, vaultScale, Number::downward); + vaultTotalProxy -= vaultDefaultRounded; // Increase the Asset Available of the Vault by liquidated First-Loss // Capital and any unclaimed funds amount: - vaultAssetsAvailableProxy += defaultCovered; - if (*vaultAssetsAvailableProxy > *vaultAssetsTotalProxy && - !vaultAsset.integral()) + vaultAvailableProxy += defaultCovered; + if (*vaultAvailableProxy > *vaultTotalProxy && !vaultAsset.integral()) { - auto const difference = - vaultAssetsAvailableProxy - vaultAssetsTotalProxy; + auto const difference = vaultAvailableProxy - vaultTotalProxy; JLOG(j.debug()) - << "Vault assets available: " << *vaultAssetsAvailableProxy - << "(" << vaultAssetsAvailableProxy->value().exponent() - << "), Total: " << *vaultAssetsTotalProxy << "(" - << vaultAssetsTotalProxy->value().exponent() + << "Vault assets available: " << *vaultAvailableProxy << "(" + << vaultAvailableProxy->value().exponent() + << "), Total: " << *vaultTotalProxy << "(" + << vaultTotalProxy->value().exponent() << "), Difference: " << difference << "(" << difference.exponent() << ")"; - if (vaultAssetsAvailableProxy->value().exponent() - + if (vaultAvailableProxy->value().exponent() - difference.exponent() > 13) { @@ -204,15 +211,15 @@ LoanManage::defaultLoan( JLOG(j.debug()) << "Difference between vault assets available and total is " "dust. Set both to the larger value."; - vaultAssetsTotalProxy = vaultAssetsAvailableProxy; + vaultTotalProxy = vaultAvailableProxy; } } - if (*vaultAssetsAvailableProxy > *vaultAssetsTotalProxy) + if (*vaultAvailableProxy > *vaultTotalProxy) { JLOG(j.warn()) << "Vault assets available must not be greater " "than assets outstanding. Available: " - << *vaultAssetsAvailableProxy - << ", Total: " << *vaultAssetsTotalProxy; + << *vaultAvailableProxy + << ", Total: " << *vaultTotalProxy; return tecLIMIT_EXCEEDED; } diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 5f0b34d5ca..096cc1b276 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -347,9 +347,11 @@ LoanPay::doApply() view.update(brokerSle); auto assetsAvailableProxy = vaultSle->at(sfAssetsAvailable); + auto assetsTotalProxy = vaultSle->at(sfAssetsTotal); + // The vault may be at a different scale than the loan. Reduce rounding // errors during the payment by rounding some of the values to that scale. - auto const vaultScale = assetsAvailableProxy->value().exponent(); + auto const vaultScale = assetsTotalProxy->value().exponent(); auto const totalPaidToVaultRaw = paymentParts->principalPaid + paymentParts->interestPaid; @@ -405,8 +407,6 @@ LoanPay::doApply() "ripple::LoanPay::doApply", "vault pseudo balance agrees before"); - auto assetsTotalProxy = vaultSle->at(sfAssetsTotal); - assetsAvailableProxy += totalPaidToVaultRounded; assetsTotalProxy += paymentParts->valueChange; diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index f25a536be4..b633281da9 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -418,8 +418,10 @@ LoanSet::doApply() } auto const principalRequested = tx[sfPrincipalRequested]; - if (auto const assetsAvailable = vaultSle->at(sfAssetsAvailable); - assetsAvailable < principalRequested) + auto vaultAvailableProxy = vaultSle->at(sfAssetsAvailable); + auto vaultTotalProxy = vaultSle->at(sfAssetsTotal); + auto const vaultScale = vaultTotalProxy->value().exponent(); + if (vaultAvailableProxy < principalRequested) { JLOG(j_.warn()) << "Insufficient assets available in the Vault to fund the loan."; @@ -438,7 +440,8 @@ LoanSet::doApply() interestRate, paymentInterval, paymentTotal, - TenthBips16{brokerSle->at(sfManagementFeeRate)}); + TenthBips16{brokerSle->at(sfManagementFeeRate)}, + vaultScale); // Check that relevant values won't lose precision. This is mostly only // relevant for IOU assets. @@ -623,10 +626,10 @@ LoanSet::doApply() view.insert(loan); // Update the balances in the vault - vaultSle->at(sfAssetsAvailable) -= principalRequested; - vaultSle->at(sfAssetsTotal) += state.interestDue; + vaultAvailableProxy -= principalRequested; + vaultTotalProxy += state.interestDue; XRPL_ASSERT_PARTS( - *vaultSle->at(sfAssetsAvailable) <= *vaultSle->at(sfAssetsTotal), + *vaultAvailableProxy <= *vaultTotalProxy, "ripple::LoanSet::doApply", "assets available must not be greater than assets outstanding"); view.update(vaultSle); From 74e331f3e933747eeffd06f74d4861921cf0a65c Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 6 Nov 2025 19:36:40 -0500 Subject: [PATCH 193/291] Handle zero rate in `computePaymentFactor` --- src/xrpld/app/misc/detail/LendingHelpers.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index f8ed477b9b..ad0e98b07e 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -73,6 +73,12 @@ computePaymentFactor( Number const& periodicRate, std::uint32_t paymentsRemaining) { + // periodicRate should never be zero in this function, but if it is, + // then 1/paymentRemaining is the most accurate factor that avoids + // divide by 0. + if (periodicRate == beast::zero) + return Number{1} / paymentsRemaining; + /* * This formula is from the XLS-66 spec, section 3.2.4.1.1 (Regular * Payment), though "raisedRate" is computed only once and used twice. From fdb659dc72e11bdecb639a23f32dec08be8f0057 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 6 Nov 2025 20:08:11 -0500 Subject: [PATCH 194/291] fix: Use ".value()" instead of "->" when with STObject::Proxy objects - Turns out that "Proxy::operator->" is not a safe substitute for "Proxy::value()." if the field is not required. The implementation is different such that "operator->" will return a null ptr if the field is not present. This includes default fields with a value of zero! --- include/xrpl/protocol/STObject.h | 4 ++++ src/xrpld/app/tx/detail/LoanManage.cpp | 9 ++++----- src/xrpld/app/tx/detail/LoanPay.cpp | 2 +- src/xrpld/app/tx/detail/LoanSet.cpp | 4 ++-- src/xrpld/app/tx/detail/VaultDeposit.cpp | 3 ++- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index 359f7fa368..1a553e0fbf 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -482,6 +482,8 @@ public: value_type operator*() const; + /// Do not use operator->() unless the field is required, or you've checked + /// that it's set. T const* operator->() const; @@ -739,6 +741,8 @@ STObject::Proxy::operator*() const -> value_type return this->value(); } +/// Do not use operator->() unless the field is required, or you've checked that +/// it's set. template T const* STObject::Proxy::operator->() const diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index 4d20a3f6f0..75d7b9552c 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -175,7 +175,7 @@ LoanManage::defaultLoan( // The vault may be at a different scale than the loan. Reduce rounding // errors during the accounting by rounding some of the values to that // scale. - auto const vaultScale = vaultTotalProxy->value().exponent(); + auto const vaultScale = vaultTotalProxy.value().exponent(); if (vaultTotalProxy < vaultDefaultAmount) { @@ -197,13 +197,12 @@ LoanManage::defaultLoan( auto const difference = vaultAvailableProxy - vaultTotalProxy; JLOG(j.debug()) << "Vault assets available: " << *vaultAvailableProxy << "(" - << vaultAvailableProxy->value().exponent() + << vaultAvailableProxy.value().exponent() << "), Total: " << *vaultTotalProxy << "(" - << vaultTotalProxy->value().exponent() + << vaultTotalProxy.value().exponent() << "), Difference: " << difference << "(" << difference.exponent() << ")"; - if (vaultAvailableProxy->value().exponent() - - difference.exponent() > + if (vaultAvailableProxy.value().exponent() - difference.exponent() > 13) { // If the difference is dust, bring the total up to match diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 096cc1b276..d99d2f4544 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -351,7 +351,7 @@ LoanPay::doApply() // The vault may be at a different scale than the loan. Reduce rounding // errors during the payment by rounding some of the values to that scale. - auto const vaultScale = assetsTotalProxy->value().exponent(); + auto const vaultScale = assetsTotalProxy.value().exponent(); auto const totalPaidToVaultRaw = paymentParts->principalPaid + paymentParts->interestPaid; diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index b633281da9..2b5d759e67 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -420,7 +420,7 @@ LoanSet::doApply() auto vaultAvailableProxy = vaultSle->at(sfAssetsAvailable); auto vaultTotalProxy = vaultSle->at(sfAssetsTotal); - auto const vaultScale = vaultTotalProxy->value().exponent(); + auto const vaultScale = vaultTotalProxy.value().exponent(); if (vaultAvailableProxy < principalRequested) { JLOG(j_.warn()) @@ -513,7 +513,7 @@ LoanSet::doApply() auto const ownerCount = borrowerSle->at(sfOwnerCount); auto const balance = account_ == borrower ? mPriorBalance - : borrowerSle->at(sfBalance)->xrp(); + : borrowerSle->at(sfBalance).value().xrp(); if (balance < view.fees().accountReserve(ownerCount)) return tecINSUFFICIENT_RESERVE; } diff --git a/src/xrpld/app/tx/detail/VaultDeposit.cpp b/src/xrpld/app/tx/detail/VaultDeposit.cpp index 66e144312f..8e42fd188d 100644 --- a/src/xrpld/app/tx/detail/VaultDeposit.cpp +++ b/src/xrpld/app/tx/detail/VaultDeposit.cpp @@ -273,7 +273,8 @@ VaultDeposit::doApply() } assetsTotalProxy += assetsDeposited; assetsAvailableProxy += assetsDeposited; - if (!assetsTotalProxy->valid() || !assetsAvailableProxy->valid()) + if (!assetsTotalProxy.value().valid() || + !assetsAvailableProxy.value().valid()) return tecLIMIT_EXCEEDED; view().update(vault); From c56387d124d785583871ef967741fec38007d70e Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Thu, 6 Nov 2025 21:05:08 -0500 Subject: [PATCH 195/291] Fix empty/zero broker ID in LoanBrokerSet and LoanSet (#5977) - Add checks for empty/zero brokerID where needed - Extend unit-tests. --- src/test/app/LoanBroker_test.cpp | 58 ++++++++++++++++------- src/test/app/Loan_test.cpp | 22 +++++++++ src/xrpld/app/tx/detail/LoanBrokerSet.cpp | 3 ++ src/xrpld/app/tx/detail/LoanSet.cpp | 4 ++ 4 files changed, 69 insertions(+), 18 deletions(-) diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 362d23ff15..171b699bbf 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -904,9 +904,26 @@ class LoanBroker_test : public beast::unit_test::suite if (!BEAST_EXPECT(broker)) return; + auto testZeroBrokerID = [&](auto&& getTxJv) { + auto jv = getTxJv(); + // empty broker ID + jv[sfLoanBrokerID] = ""; + env(jv, ter(temINVALID)); + // zero broker ID + jv[sfLoanBrokerID] = to_string(uint256{}); + // needs a flag to distinguish the parsed STTx from the prior + // test + env(jv, txflags(tfFullyCanonicalSig), ter(temINVALID)); + }; + if (brokerTest == CoverDeposit) { - // preclaim: tecWRONG_ASET + // preflight: temINVALID (empty/zero broker id) + testZeroBrokerID([&]() { + return coverDeposit(alice, brokerKeylet.key, asset(10)); + }); + + // preclaim: tecWRONG_ASSET env(coverDeposit(alice, brokerKeylet.key, issuer["BAD"](10)), ter(tecWRONG_ASSET)); @@ -929,6 +946,11 @@ class LoanBroker_test : public beast::unit_test::suite if (brokerTest == CoverWithdraw) { + // preflight: temINVALID (empty/zero broker id) + testZeroBrokerID([&]() { + return coverWithdraw(alice, brokerKeylet.key, asset(10)); + }); + // preclaim: tecWRONG_ASSSET env(coverWithdraw(alice, brokerKeylet.key, issuer["BAD"](10)), ter(tecWRONG_ASSET)); @@ -976,6 +998,14 @@ class LoanBroker_test : public beast::unit_test::suite if (brokerTest == CoverClawback) { + // preflight: temINVALID (empty/zero broker id) + testZeroBrokerID([&]() { + return env.json( + coverClawback(alice), + loanBrokerID(brokerKeylet.key), + amount(vaultInfo.asset(2))); + }); + if (asset.holds()) { // preclaim: AllowTrustLineClaback is not set @@ -1011,23 +1041,8 @@ class LoanBroker_test : public beast::unit_test::suite sig(sfCounterpartySignature, alice), fee(env.current()->fees().base * 2)); - // preflight: temINVALID (empty broker id) - { - auto jv = del(alice, brokerKeylet.key); - jv[sfLoanBrokerID] = ""; - env(jv, ter(temINVALID)); - } - // preflight: temINVALID (zero broker id) - { - // needs a flag to distinguish the parsed STTx from the prior - // test - auto jv = del(alice, uint256{}, tfFullyCanonicalSig); - BEAST_EXPECT( - jv[sfLoanBrokerID] == - "0000000000000000000000000000000000000000000000000000000000" - "000000"); - env(jv, ter(temINVALID)); - } + // preflight: temINVALID (empty/zero broker id) + testZeroBrokerID([&]() { return del(alice, brokerKeylet.key); }); // preclaim: tecHAS_OBLIGATIONS env(del(alice, brokerKeylet.key), ter(tecHAS_OBLIGATIONS)); @@ -1037,6 +1052,13 @@ class LoanBroker_test : public beast::unit_test::suite if (brokerTest == Set) { + // preflight: temINVALID (empty/zero broker id) + testZeroBrokerID([&]() { + return env.json( + set(alice, vaultInfo.vaultID), + loanBrokerID(brokerKeylet.key)); + }); + if (asset.holds()) { env(fclear(issuer, asfDefaultRipple)); diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 9239603fc0..b2e96fb46d 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -4271,6 +4271,28 @@ protected: Number const& debtMaximumRequest) { // first temBAD_SIGNER: TODO + // empty/zero broker ID + { + auto jv = set(borrower, uint256{}, debtMaximumRequest); + + auto testZeroBrokerID = [&](std::string const& id, + std::uint32_t flags = 0) { + // empty broker ID + jv[sfLoanBrokerID] = id; + env(jv, + sig(sfCounterpartySignature, lender), + loanSetFee, + txflags(flags), + ter(temINVALID)); + }; + // empty broker ID + testZeroBrokerID(std::string("")); + // zero broker ID + // needs a flag to distinguish the parsed STTx from the prior + // test + testZeroBrokerID(to_string(uint256{}), tfFullyCanonicalSig); + } + // preflightCheckSigningKey() failure: // can it happen? the signature is checked before transactor // executes diff --git a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp index c575472e79..c9843d6c6e 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp @@ -35,6 +35,9 @@ LoanBrokerSet::preflight(PreflightContext const& ctx) tx.isFieldPresent(sfCoverRateMinimum) || tx.isFieldPresent(sfCoverRateLiquidation)) return temINVALID; + + if (tx[sfLoanBrokerID] == beast::zero) + return temINVALID; } { diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 2b5d759e67..d8366dc14a 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -100,6 +100,10 @@ LoanSet::preflight(PreflightContext const& ctx) return *ret; } + if (auto const brokerID = ctx.tx[~sfLoanBrokerID]; + brokerID && *brokerID == beast::zero) + return temINVALID; + return tesSUCCESS; } From 2cb0ca5ad29d104cd6bad9ff2b4e5f796ae83d95 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 6 Nov 2025 23:37:30 -0500 Subject: [PATCH 196/291] Refactor loan test helpers into reusable functions - Try to reproduce RIPD-3831 --- src/test/app/Loan_test.cpp | 1065 +++++++++++++++++++++--------------- 1 file changed, 612 insertions(+), 453 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index b2e96fb46d..575d687dfa 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -585,6 +585,518 @@ protected: return true; } + enum class AssetType { XRP = 0, IOU = 1, MPT = 2 }; + + // Specify the accounts as params to allow other accounts to be used + jtx::PrettyAsset + createAsset( + jtx::Env& env, + AssetType assetType, + BrokerParameters const& brokerParams, + jtx::Account const& issuer, + jtx::Account const& lender, + jtx::Account const& borrower) + { + using namespace jtx; + + switch (assetType) + { + case AssetType::XRP: + // TODO: remove the factor, and set up loans in drops + return PrettyAsset{xrpIssue(), 1'000'000}; + + case AssetType::IOU: { + PrettyAsset const asset{issuer[iouCurrency]}; + + env(trust(lender, asset(brokerParams.vaultDeposit))); + env(trust(borrower, asset(brokerParams.vaultDeposit))); + + return asset; + } + + case AssetType::MPT: { + MPTTester mptt{env, issuer, mptInitNoFund}; + mptt.create( + {.flags = + tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); + // Scale the MPT asset so interest is interesting + PrettyAsset const asset{mptt.issuanceID(), 10'000}; + // Need to do the authorization here because mptt isn't + // accessible outside + mptt.authorize({.account = lender}); + mptt.authorize({.account = borrower}); + + env.close(); + + return asset; + } + + default: + throw std::runtime_error("Unknown asset type"); + } + } + + void + describeLoan( + BrokerParameters const& brokerParams, + LoanParameters const& loanParams, + AssetType assetType) + { + using namespace jtx; + + Env env(*this, beast::severities::kWarning); + + auto const asset = createAsset( + env, + assetType, + brokerParams, + Account("issuer"), + Account("lender"), + Account("borrower")); + auto const props = computeLoanProperties( + asset, + asset(loanParams.principalRequest).number(), + loanParams.interest.value_or(TenthBips32{}), + loanParams.payInterval.value_or(LoanSet::defaultPaymentInterval), + loanParams.payTotal.value_or(LoanSet::defaultPaymentTotal), + brokerParams.managementFeeRate, + asset(brokerParams.vaultDeposit).number().exponent()); + log << "Loan properties:\n" + << "\tPeriodic Payment: " << props.periodicPayment << std::endl + << "\tTotal Value: " << props.totalValueOutstanding << std::endl + << "\tManagement Fee: " << props.managementFeeOwedToBroker + << std::endl + << "\tLoan Scale: " << props.loanScale << std::endl + << "\tFirst payment principal: " << props.firstPaymentPrincipal + << std::endl; + + // checkGuards returns a TER, so success is 0 + BEAST_EXPECT(!LoanSet::checkGuards( + asset, + asset(loanParams.principalRequest).number(), + loanParams.interest.value_or(TenthBips32{}), + loanParams.payTotal.value_or(LoanSet::defaultPaymentTotal), + props, + env.journal)); + } + + std::optional< + std::tuple> + createLoan( + jtx::Env& env, + AssetType assetType, + BrokerParameters const& brokerParams, + LoanParameters const& loanParams, + jtx::Account const& issuer, + jtx::Account const& lender, + jtx::Account const& borrower) + { + using namespace jtx; + + // Enough to cover initial fees + env.fund( + env.current()->fees().accountReserve(10) * 10, + issuer, + noripple(lender, borrower)); + + // Make the asset + auto const asset = + createAsset(env, assetType, brokerParams, issuer, lender, borrower); + + env.close(); + env( + pay((asset.native() ? env.master : issuer), + lender, + asset(brokerParams.vaultDeposit))); + // Fund the borrower later once we know the total loan + // size + + BrokerInfo const broker = + createVaultAndBroker(env, asset, lender, brokerParams); + + auto const pseudoAcctOpt = [&]() -> std::optional { + auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); + if (!BEAST_EXPECT(brokerSle)) + return std::nullopt; + auto const brokerPseudo = brokerSle->at(sfAccount); + return Account("Broker pseudo-account", brokerPseudo); + }(); + if (!pseudoAcctOpt) + return std::nullopt; + Account const& pseudoAcct = *pseudoAcctOpt; + + auto const loanKeyletOpt = [&]() -> std::optional { + auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); + if (!BEAST_EXPECT(brokerSle)) + return std::nullopt; + + // Broker has no loans + BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); + + // The loan keylet is based on the LoanSequence of the + // _LOAN_BROKER_ object. + auto const loanSequence = brokerSle->at(sfLoanSequence); + return keylet::loan(broker.brokerID, loanSequence); + }(); + if (!loanKeyletOpt) + return std::nullopt; + Keylet const& loanKeylet = *loanKeyletOpt; + + env(loanParams(env, broker)); + + env.close(); + + VerifyLoanStatus verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); + + return std::make_tuple( + broker, loanKeylet, verifyLoanStatus, pseudoAcct); + } + + void + topUpBorrower( + jtx::Env& env, + BrokerInfo const& broker, + jtx::Account const& issuer, + jtx::Account const& borrower, + LoanState const& state, + std::optional const& servFee) + { + using namespace jtx; + + STAmount const serviceFee = broker.asset(servFee.value_or(0)); + + // Ensure the borrower has enough funds to make the payments + // (including tx fees, if necessary) + auto const borrowerBalance = env.balance(borrower, broker.asset); + + auto const baseFee = env.current()->fees().base; + + // Add extra for transaction fees and reserves, if appropriate, or a + // tiny amount for the extra paid in each transaction + auto const totalNeeded = state.totalValue + + (serviceFee * state.paymentRemaining) + + (broker.asset.native() ? Number( + baseFee * state.paymentRemaining + + env.current()->fees().accountReserve( + env.ownerCount(borrower))) + : broker.asset(15).number()); + + auto const shortage = totalNeeded - borrowerBalance.number(); + + if (shortage > beast::zero) + env( + pay((broker.asset.native() ? env.master : issuer), + borrower, + STAmount{broker.asset, shortage})); + } + + void + makeLoanPayments( + jtx::Env& env, + BrokerInfo const& broker, + LoanParameters const& loanParams, + Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus) + { + // Make all the individual payments + using namespace jtx; + using namespace jtx::loan; + using namespace std::chrono_literals; + using d = NetClock::duration; + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + // Account const evan{"evan"}; + // Account const alice{"alice"}; + + auto const currencyLabel = getCurrencyLabel(broker.asset); + + auto const baseFee = env.current()->fees().base; + + env.close(); + auto state = getCurrentState(env, broker, loanKeylet); + + verifyLoanStatus(state); + + STAmount const serviceFee = + broker.asset(loanParams.serviceFee.value_or(0)); + + topUpBorrower( + env, broker, issuer, borrower, state, loanParams.serviceFee); + + // Periodic payment amount will consist of + // 1. principal outstanding (1000) + // 2. interest interest rate (at 12%) + // 3. payment interval (600s) + // 4. loan service fee (2) + // Calculate these values without the helper functions + // to verify they're working correctly The numbers in + // the below BEAST_EXPECTs may not hold across assets. + auto const periodicRate = + loanPeriodicRate(state.interestRate, state.paymentInterval); + STAmount const roundedPeriodicPayment{ + broker.asset, + roundPeriodicPayment( + broker.asset, state.periodicPayment, state.loanScale)}; + + log << currencyLabel << " Payment components: " + << "Payments remaining, rawInterest, rawPrincipal, " + "rawMFee, trackedValueDelta, trackedPrincipalDelta, " + "trackedInterestDelta, trackedMgmtFeeDelta, special" + << std::endl; + + // Include the service fee + STAmount const totalDue = roundToScale( + roundedPeriodicPayment + serviceFee, + state.loanScale, + Number::upward); + + { + auto const raw = calculateRawLoanState( + state.periodicPayment, + periodicRate, + state.paymentRemaining, + broker.params.managementFeeRate); + auto const rounded = calculateRoundedLoanState( + state.totalValue, + state.principalOutstanding, + state.managementFeeOutstanding); + log << currencyLabel + << " Loan starting state: " << state.paymentRemaining << ", " + << raw.interestDue << ", " << raw.principalOutstanding << ", " + << raw.managementFeeDue << ", " << rounded.valueOutstanding + << ", " << rounded.principalOutstanding << ", " + << rounded.interestDue << ", " << rounded.managementFeeDue + << std::endl; + } + + // Try to pay a little extra to show that it's _not_ + // taken + STAmount const transactionAmount = STAmount{broker.asset, totalDue} + + std::min(broker.asset(10).value(), + STAmount{broker.asset, totalDue / 20}); + + auto const borrowerInitialBalance = + env.balance(borrower, broker.asset).number(); + auto const initialState = state; + detail::PaymentComponents totalPaid{ + .trackedValueDelta = 0, + .trackedPrincipalDelta = 0, + .trackedManagementFeeDelta = 0}; + Number totalInterestPaid = 0; + Number totalFeesPaid = 0; + std::size_t totalPaymentsMade = 0; + + ripple::LoanState currentTrueState = calculateRawLoanState( + state.periodicPayment, + periodicRate, + state.paymentRemaining, + broker.params.managementFeeRate); + + auto validateBorrowerBalance = [&]() { + auto const totalSpent = + (totalPaid.trackedValueDelta + totalFeesPaid + + (broker.asset.native() ? Number(baseFee) * totalPaymentsMade + : numZero)); + BEAST_EXPECT( + env.balance(borrower, broker.asset).number() == + borrowerInitialBalance - totalSpent); + }; + while (state.paymentRemaining > 0) + { + validateBorrowerBalance(); + // Compute the expected principal amount + auto const paymentComponents = detail::computePaymentComponents( + broker.asset.raw(), + state.loanScale, + state.totalValue, + state.principalOutstanding, + state.managementFeeOutstanding, + state.periodicPayment, + periodicRate, + state.paymentRemaining, + broker.params.managementFeeRate); + + BEAST_EXPECT( + paymentComponents.trackedValueDelta == roundedPeriodicPayment || + (paymentComponents.specialCase == + detail::PaymentSpecialCase::final && + paymentComponents.trackedValueDelta < roundedPeriodicPayment)); + BEAST_EXPECT( + paymentComponents.trackedValueDelta == + paymentComponents.trackedPrincipalDelta + + paymentComponents.trackedInterestPart() + + paymentComponents.trackedManagementFeeDelta); + + ripple::LoanState const nextTrueState = calculateRawLoanState( + state.periodicPayment, + periodicRate, + state.paymentRemaining - 1, + broker.params.managementFeeRate); + detail::LoanDeltas const deltas = currentTrueState - nextTrueState; + BEAST_EXPECT( + deltas.valueDelta() == + deltas.principalDelta + deltas.interestDueDelta + + deltas.managementFeeDueDelta); + BEAST_EXPECT( + paymentComponents.specialCase == + detail::PaymentSpecialCase::final || + deltas.valueDelta() == state.periodicPayment || + (state.loanScale - + (deltas.valueDelta() - state.periodicPayment).exponent()) > + 14); + + log << currencyLabel + << " Payment components: " << state.paymentRemaining << ", " + << deltas.interestDueDelta << ", " << deltas.principalDelta + << ", " << deltas.managementFeeDueDelta << ", " + << paymentComponents.trackedValueDelta << ", " + << paymentComponents.trackedPrincipalDelta << ", " + << paymentComponents.trackedInterestPart() << ", " + << paymentComponents.trackedManagementFeeDelta << ", " + << (paymentComponents.specialCase == + detail::PaymentSpecialCase::final + ? "final" + : paymentComponents.specialCase == + detail::PaymentSpecialCase::extra + ? "extra" + : "none") + << std::endl; + + auto const totalDueAmount = STAmount{ + broker.asset, paymentComponents.trackedValueDelta + serviceFee}; + + // Due to the rounding algorithms to keep the interest and + // principal in sync with "true" values, the computed amount + // may be a little less than the rounded fixed payment + // amount. For integral types, the difference should be < 3 + // (1 unit for each of the interest and management fee). For + // IOUs, the difference should be dust. + Number const diff = totalDue - totalDueAmount; + BEAST_EXPECT( + paymentComponents.specialCase == + detail::PaymentSpecialCase::final || + diff == beast::zero || + (diff > beast::zero && + ((broker.asset.integral() && + (static_cast(diff) < 3)) || + (state.loanScale - diff.exponent() > 13)))); + + BEAST_EXPECT( + paymentComponents.trackedPrincipalDelta >= beast::zero && + paymentComponents.trackedPrincipalDelta <= + state.principalOutstanding); + BEAST_EXPECT( + paymentComponents.specialCase != + detail::PaymentSpecialCase::final || + paymentComponents.trackedPrincipalDelta == + state.principalOutstanding); + + auto const borrowerBalanceBeforePayment = + env.balance(borrower, broker.asset); + + // Make the payment + env(pay(borrower, loanKeylet.key, transactionAmount)); + + env.close( + d{(state.previousPaymentDate + state.nextPaymentDate) / 2}); + + // Need to account for fees if the loan is in XRP + PrettyAmount adjustment = broker.asset(0); + if (broker.asset.native()) + { + adjustment = env.current()->fees().base; + } + + // Check the result + verifyLoanStatus.checkPayment( + state.loanScale, + borrower, + borrowerBalanceBeforePayment, + totalDueAmount, + adjustment); + + --state.paymentRemaining; + state.previousPaymentDate = state.nextPaymentDate; + if (paymentComponents.specialCase == + detail::PaymentSpecialCase::final) + { + state.paymentRemaining = 0; + } + else + { + state.nextPaymentDate += state.paymentInterval; + } + state.principalOutstanding -= + paymentComponents.trackedPrincipalDelta; + state.managementFeeOutstanding -= + paymentComponents.trackedManagementFeeDelta; + state.totalValue -= paymentComponents.trackedValueDelta; + + verifyLoanStatus(state); + + totalPaid.trackedValueDelta += paymentComponents.trackedValueDelta; + totalPaid.trackedPrincipalDelta += + paymentComponents.trackedPrincipalDelta; + totalPaid.trackedManagementFeeDelta += + paymentComponents.trackedManagementFeeDelta; + totalInterestPaid += paymentComponents.trackedInterestPart(); + totalFeesPaid += serviceFee; + ++totalPaymentsMade; + + currentTrueState = nextTrueState; + } + validateBorrowerBalance(); + + // Loan is paid off + BEAST_EXPECT(state.paymentRemaining == 0); + BEAST_EXPECT(state.principalOutstanding == 0); + + // Make sure all the payments add up + BEAST_EXPECT(totalPaid.trackedValueDelta == initialState.totalValue); + BEAST_EXPECT( + totalPaid.trackedPrincipalDelta == + initialState.principalOutstanding); + BEAST_EXPECT( + totalPaid.trackedManagementFeeDelta == + initialState.managementFeeOutstanding); + // This is almost a tautology given the previous checks, but + // check it anyway for completeness. + BEAST_EXPECT( + totalInterestPaid == + initialState.totalValue - + (initialState.principalOutstanding + + initialState.managementFeeOutstanding)); + BEAST_EXPECT(totalPaymentsMade == initialState.paymentRemaining); + } + + void + runLoan( + AssetType assetType, + BrokerParameters const& brokerParams, + LoanParameters const& loanParams) + { + using namespace jtx; + + Account const issuer("issuer"); + Account const lender("lender"); + Account const borrower("borrower"); + + Env env(*this, all); + + auto loanResult = createLoan( + env, assetType, brokerParams, loanParams, issuer, lender, borrower); + if (!BEAST_EXPECT(loanResult)) + return; + + auto broker = std::get(*loanResult); + auto loanKeylet = std::get(*loanResult); + auto verifyLoanStatus = std::get(*loanResult); + + makeLoanPayments(env, broker, loanParams, loanKeylet, verifyLoanStatus); + } + /** Runs through the complete lifecycle of a loan * * 1. Create a loan. @@ -3001,309 +3513,6 @@ protected: } } - void - makeLoanPayments( - jtx::Env& env, - BrokerInfo const& broker, - LoanParameters const& loanParams, - Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus) - { - // Make all the individual payments - using namespace jtx; - using namespace jtx::loan; - using namespace std::chrono_literals; - using d = NetClock::duration; - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - // Account const evan{"evan"}; - // Account const alice{"alice"}; - - auto const currencyLabel = getCurrencyLabel(broker.asset); - - auto const baseFee = env.current()->fees().base; - - env.close(); - auto state = getCurrentState(env, broker, loanKeylet); - - verifyLoanStatus(state); - - STAmount const serviceFee = - broker.asset(loanParams.serviceFee.value_or(0)); - - // Ensure the borrower has enough funds to make the payments (including - // tx fees, if necessary) - { - auto const borrowerBalance = env.balance(borrower, broker.asset); - - // Add extra for transaction fees and reserves, if appropriate, or a - // tiny amount for the extra paid in each transaction - auto const totalNeeded = state.totalValue + - (serviceFee * state.paymentRemaining) + - (broker.asset.native() - ? Number( - baseFee * state.paymentRemaining + - env.current()->fees().accountReserve( - env.ownerCount(borrower))) - : broker.asset(15).number()); - - auto const shortage = totalNeeded - borrowerBalance.number(); - - if (shortage > beast::zero) - env( - pay((broker.asset.native() ? env.master : issuer), - borrower, - STAmount{broker.asset, shortage})); - } - - // Periodic payment amount will consist of - // 1. principal outstanding (1000) - // 2. interest interest rate (at 12%) - // 3. payment interval (600s) - // 4. loan service fee (2) - // Calculate these values without the helper functions - // to verify they're working correctly The numbers in - // the below BEAST_EXPECTs may not hold across assets. - auto const periodicRate = - loanPeriodicRate(state.interestRate, state.paymentInterval); - STAmount const roundedPeriodicPayment{ - broker.asset, - roundPeriodicPayment( - broker.asset, state.periodicPayment, state.loanScale)}; - - log << currencyLabel << " Payment components: " - << "Payments remaining, rawInterest, rawPrincipal, " - "rawMFee, trackedValueDelta, trackedPrincipalDelta, " - "trackedInterestDelta, trackedMgmtFeeDelta, special" - << std::endl; - - // Include the service fee - STAmount const totalDue = roundToScale( - roundedPeriodicPayment + serviceFee, - state.loanScale, - Number::upward); - - { - auto const raw = calculateRawLoanState( - state.periodicPayment, - periodicRate, - state.paymentRemaining, - broker.params.managementFeeRate); - auto const rounded = calculateRoundedLoanState( - state.totalValue, - state.principalOutstanding, - state.managementFeeOutstanding); - log << currencyLabel - << " Loan starting state: " << state.paymentRemaining << ", " - << raw.interestDue << ", " << raw.principalOutstanding << ", " - << raw.managementFeeDue << ", " << rounded.valueOutstanding - << ", " << rounded.principalOutstanding << ", " - << rounded.interestDue << ", " << rounded.managementFeeDue - << std::endl; - } - - // Try to pay a little extra to show that it's _not_ - // taken - STAmount const transactionAmount = STAmount{broker.asset, totalDue} + - std::min(broker.asset(10).value(), - STAmount{broker.asset, totalDue / 20}); - - auto const borrowerInitialBalance = - env.balance(borrower, broker.asset).number(); - auto const initialState = state; - detail::PaymentComponents totalPaid{ - .trackedValueDelta = 0, - .trackedPrincipalDelta = 0, - .trackedManagementFeeDelta = 0}; - Number totalInterestPaid = 0; - Number totalFeesPaid = 0; - std::size_t totalPaymentsMade = 0; - - ripple::LoanState currentTrueState = calculateRawLoanState( - state.periodicPayment, - periodicRate, - state.paymentRemaining, - broker.params.managementFeeRate); - - auto validateBorrowerBalance = [&]() { - auto const totalSpent = - (totalPaid.trackedValueDelta + totalFeesPaid + - (broker.asset.native() ? Number(baseFee) * totalPaymentsMade - : numZero)); - BEAST_EXPECT( - env.balance(borrower, broker.asset).number() == - borrowerInitialBalance - totalSpent); - }; - while (state.paymentRemaining > 0) - { - validateBorrowerBalance(); - // Compute the expected principal amount - auto const paymentComponents = detail::computePaymentComponents( - broker.asset.raw(), - state.loanScale, - state.totalValue, - state.principalOutstanding, - state.managementFeeOutstanding, - state.periodicPayment, - periodicRate, - state.paymentRemaining, - broker.params.managementFeeRate); - - BEAST_EXPECT( - paymentComponents.trackedValueDelta == roundedPeriodicPayment || - (paymentComponents.specialCase == - detail::PaymentSpecialCase::final && - paymentComponents.trackedValueDelta < roundedPeriodicPayment)); - BEAST_EXPECT( - paymentComponents.trackedValueDelta == - paymentComponents.trackedPrincipalDelta + - paymentComponents.trackedInterestPart() + - paymentComponents.trackedManagementFeeDelta); - - ripple::LoanState const nextTrueState = calculateRawLoanState( - state.periodicPayment, - periodicRate, - state.paymentRemaining - 1, - broker.params.managementFeeRate); - detail::LoanDeltas const deltas = currentTrueState - nextTrueState; - BEAST_EXPECT( - deltas.valueDelta() == - deltas.principalDelta + deltas.interestDueDelta + - deltas.managementFeeDueDelta); - BEAST_EXPECT( - paymentComponents.specialCase == - detail::PaymentSpecialCase::final || - deltas.valueDelta() == state.periodicPayment || - (state.loanScale - - (deltas.valueDelta() - state.periodicPayment).exponent()) > - 14); - - log << currencyLabel - << " Payment components: " << state.paymentRemaining << ", " - << deltas.interestDueDelta << ", " << deltas.principalDelta - << ", " << deltas.managementFeeDueDelta << ", " - << paymentComponents.trackedValueDelta << ", " - << paymentComponents.trackedPrincipalDelta << ", " - << paymentComponents.trackedInterestPart() << ", " - << paymentComponents.trackedManagementFeeDelta << ", " - << (paymentComponents.specialCase == - detail::PaymentSpecialCase::final - ? "final" - : paymentComponents.specialCase == - detail::PaymentSpecialCase::extra - ? "extra" - : "none") - << std::endl; - - auto const totalDueAmount = STAmount{ - broker.asset, paymentComponents.trackedValueDelta + serviceFee}; - - // Due to the rounding algorithms to keep the interest and - // principal in sync with "true" values, the computed amount - // may be a little less than the rounded fixed payment - // amount. For integral types, the difference should be < 3 - // (1 unit for each of the interest and management fee). For - // IOUs, the difference should be dust. - Number const diff = totalDue - totalDueAmount; - BEAST_EXPECT( - paymentComponents.specialCase == - detail::PaymentSpecialCase::final || - diff == beast::zero || - (diff > beast::zero && - ((broker.asset.integral() && - (static_cast(diff) < 3)) || - (state.loanScale - diff.exponent() > 13)))); - - BEAST_EXPECT( - paymentComponents.trackedPrincipalDelta >= beast::zero && - paymentComponents.trackedPrincipalDelta <= - state.principalOutstanding); - BEAST_EXPECT( - paymentComponents.specialCase != - detail::PaymentSpecialCase::final || - paymentComponents.trackedPrincipalDelta == - state.principalOutstanding); - - auto const borrowerBalanceBeforePayment = - env.balance(borrower, broker.asset); - - // Make the payment - env(pay(borrower, loanKeylet.key, transactionAmount)); - - env.close( - d{(state.previousPaymentDate + state.nextPaymentDate) / 2}); - - // Need to account for fees if the loan is in XRP - PrettyAmount adjustment = broker.asset(0); - if (broker.asset.native()) - { - adjustment = env.current()->fees().base; - } - - // Check the result - verifyLoanStatus.checkPayment( - state.loanScale, - borrower, - borrowerBalanceBeforePayment, - totalDueAmount, - adjustment); - - --state.paymentRemaining; - state.previousPaymentDate = state.nextPaymentDate; - if (paymentComponents.specialCase == - detail::PaymentSpecialCase::final) - { - state.paymentRemaining = 0; - } - else - { - state.nextPaymentDate += state.paymentInterval; - } - state.principalOutstanding -= - paymentComponents.trackedPrincipalDelta; - state.managementFeeOutstanding -= - paymentComponents.trackedManagementFeeDelta; - state.totalValue -= paymentComponents.trackedValueDelta; - - verifyLoanStatus(state); - - totalPaid.trackedValueDelta += paymentComponents.trackedValueDelta; - totalPaid.trackedPrincipalDelta += - paymentComponents.trackedPrincipalDelta; - totalPaid.trackedManagementFeeDelta += - paymentComponents.trackedManagementFeeDelta; - totalInterestPaid += paymentComponents.trackedInterestPart(); - totalFeesPaid += serviceFee; - ++totalPaymentsMade; - - currentTrueState = nextTrueState; - } - validateBorrowerBalance(); - - // Loan is paid off - BEAST_EXPECT(state.paymentRemaining == 0); - BEAST_EXPECT(state.principalOutstanding == 0); - - // Make sure all the payments add up - BEAST_EXPECT(totalPaid.trackedValueDelta == initialState.totalValue); - BEAST_EXPECT( - totalPaid.trackedPrincipalDelta == - initialState.principalOutstanding); - BEAST_EXPECT( - totalPaid.trackedManagementFeeDelta == - initialState.managementFeeOutstanding); - // This is almost a tautology given the previous checks, but - // check it anyway for completeness. - BEAST_EXPECT( - totalInterestPaid == - initialState.totalValue - - (initialState.principalOutstanding + - initialState.managementFeeOutstanding)); - BEAST_EXPECT(totalPaymentsMade == initialState.paymentRemaining); - } - void testSelfLoan() { @@ -5703,6 +5912,8 @@ protected: void testDustManipulation() { + testcase("Dust manipulation"); + using namespace jtx; using namespace std::chrono_literals; Env env(*this, all); @@ -5848,10 +6059,107 @@ protected: } } + void + testRIPD3831() + { + using namespace jtx; + + testcase("RIPD-3831"); + + Account const issuer("issuer"); + Account const lender("lender"); + Account const borrower("borrower"); + + BrokerParameters const brokerParams{ + .vaultDeposit = 100000, + .debtMax = 0, + .coverRateMin = TenthBips32{0}, + // .managementFeeRate = TenthBips16{5919}, + .coverRateLiquidation = TenthBips32{0}}; + LoanParameters const loanParams{ + .account = lender, + .counter = borrower, + .principalRequest = Number{200'000, -6}, + .lateFee = Number{200, -6}, + .interest = TenthBips32{50'000}, + .payTotal = 10, + .payInterval = 150, + .gracePd = 0}; + + describeLoan(brokerParams, loanParams, AssetType::XRP); + + Env env(*this, all); + + auto loanResult = createLoan( + env, + AssetType::XRP, + brokerParams, + loanParams, + issuer, + lender, + borrower); + + if (!BEAST_EXPECT(loanResult)) + return; + + auto broker = std::get(*loanResult); + auto loanKeylet = std::get(*loanResult); + // auto verifyLoanStatus = std::get(*loanResult); + + using tp = NetClock::time_point; + using d = NetClock::duration; + + auto state = getCurrentState(env, broker, loanKeylet); + if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan)) + { + log << "loan after create: " << to_string(loan->getJson()) + << std::endl; + + env.close( + tp{ + d{loan->at(sfNextPaymentDueDate) + loan->at(sfGracePeriod) + + 1}}); + } + + topUpBorrower( + env, broker, issuer, borrower, state, loanParams.serviceFee); + + using namespace jtx::loan; + + auto jv = + pay(borrower, loanKeylet.key, drops(XRPAmount(state.totalValue))); + + { + auto const submitParam = to_string(jv); + log << "about to submit: " << submitParam << std::endl; + auto const jr = env.rpc("submit", borrower.name(), submitParam); + + log << jr << std::endl; + BEAST_EXPECT(jr.isMember(jss::result)); + auto const jResult = jr[jss::result]; + // BEAST_EXPECT(jResult[jss::error] == "invalidTransaction"); + // BEAST_EXPECT( + // jResult[jss::error_exception] == + // "fails local checks: Transaction has bad signature."); + } + + env.close(); + + // Make sure the system keeps responding + env(noop(borrower)); + env.close(); + env(noop(issuer)); + env.close(); + env(noop(lender)); + env.close(); + } + public: void run() override { + testRIPD3831(); + #if LOANTODO testCoverDepositAllowsNonTransferableMPT(); testLoanPayLateFullPaymentBypassesPenalties(); @@ -5891,8 +6199,6 @@ public: class LoanBatch_test : public Loan_test { protected: - enum class AssetType { XRP = 0, IOU = 1, MPT = 2 }; - beast::xor_shift_engine engine_; std::uniform_int_distribution<> assetDist{0, 2}; @@ -5922,113 +6228,6 @@ protected: Decimal(100000) */ - void - runLoan( - AssetType assetType, - BrokerParameters const& brokerParams, - LoanParameters const& loanParams) - { - using namespace jtx; - - Account const issuer("issuer"); - Account const lender("lender"); - Account const borrower("borrower"); - - Env env(*this, all); - - // Enough to cover initial fees - env.fund( - env.current()->fees().accountReserve(10) * 10, - issuer, - noripple(lender, borrower)); - - // Make the asset - auto const asset = [&]() { - switch (assetType) - { - case AssetType::XRP: - // TODO: remove the factor, and set up loans in drops - return PrettyAsset{xrpIssue(), 1'000'000}; - - case AssetType::IOU: { - PrettyAsset const asset{issuer[iouCurrency]}; - - env(trust(lender, asset(brokerParams.vaultDeposit))); - env(trust(borrower, asset(brokerParams.vaultDeposit))); - - return asset; - } - - case AssetType::MPT: { - MPTTester mptt{env, issuer, mptInitNoFund}; - mptt.create( - {.flags = tfMPTCanClawback | tfMPTCanTransfer | - tfMPTCanLock}); - // Scale the MPT asset so interest is interesting - PrettyAsset const asset{mptt.issuanceID(), 10'000}; - // Need to do the authorization here because mptt isn't - // accessible outside - mptt.authorize({.account = lender}); - mptt.authorize({.account = borrower}); - - env.close(); - - return asset; - } - - default: - throw std::runtime_error("Unknown asset type"); - } - }(); - - env.close(); - env( - pay((asset.native() ? env.master : issuer), - lender, - asset(brokerParams.vaultDeposit))); - // Fund the borrower later once we know the total loan - // size - - BrokerInfo const broker = - createVaultAndBroker(env, asset, lender, brokerParams); - - auto const pseudoAcctOpt = [&]() -> std::optional { - auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - return std::nullopt; - auto const brokerPseudo = brokerSle->at(sfAccount); - return Account("Broker pseudo-account", brokerPseudo); - }(); - if (!pseudoAcctOpt) - return; - Account const& pseudoAcct = *pseudoAcctOpt; - - auto const loanKeyletOpt = [&]() -> std::optional { - auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - return std::nullopt; - - // Broker has no loans - BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); - - // The loan keylet is based on the LoanSequence of the - // _LOAN_BROKER_ object. - auto const loanSequence = brokerSle->at(sfLoanSequence); - return keylet::loan(broker.brokerID, loanSequence); - }(); - if (!loanKeyletOpt) - return; - Keylet const& loanKeylet = *loanKeyletOpt; - - env(loanParams(env, broker)); - - env.close(); - - VerifyLoanStatus verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); - - makeLoanPayments(env, broker, loanParams, loanKeylet, verifyLoanStatus); - } - void testRandomLoan() { @@ -6116,16 +6315,6 @@ class LoanArbitrary_test : public LoanBatch_test Number initalXrp{INITIAL_XRP}; BEAST_EXPECT(initalXrp.exponent() <= 0); #endif - /* - Progress: 27 completed, 50 total attempts | Rejected: periodic=0, -interest=23, -duration=0LoanParameters(principal=Decimal('1255438.00000000000000000000000'), -interest_rate=Decimal('0.01922'), payment_total=5816, -payment_interval=Decimal('29193'), interest_fee=Decimal('0.59195')) Single test -failed with assertion error: Both principal and interest rounded are zero 0 + 0 -+ 0 - */ - BrokerParameters const brokerParams{ .vaultDeposit = 10000, .debtMax = 0, @@ -6140,37 +6329,7 @@ failed with assertion error: Both principal and interest rounded are zero 0 + 0 // .payTotal = 5816, .payInterval = 150}; - { - Env env(*this, beast::severities::kWarning); - - auto const asset = PrettyAsset{xrpIssue(), 1'000'000}; - auto const props = computeLoanProperties( - asset, - asset(loanParams.principalRequest).number(), - loanParams.interest.value_or(TenthBips32{}), - loanParams.payInterval.value_or( - LoanSet::defaultPaymentInterval), - loanParams.payTotal.value_or(LoanSet::defaultPaymentTotal), - brokerParams.managementFeeRate, - asset(brokerParams.vaultDeposit).number().exponent()); - log << "Loan properties:\n" - << "\tPeriodic Payment: " << props.periodicPayment << std::endl - << "\tTotal Value: " << props.totalValueOutstanding << std::endl - << "\tManagement Fee: " << props.managementFeeOwedToBroker - << std::endl - << "\tLoan Scale: " << props.loanScale << std::endl - << "\tFirst payment principal: " << props.firstPaymentPrincipal - << std::endl; - - // checkGuards returns a TER, so success is 0 - BEAST_EXPECT(!LoanSet::checkGuards( - asset, - asset(loanParams.principalRequest).number(), - loanParams.interest.value_or(TenthBips32{}), - loanParams.payTotal.value_or(LoanSet::defaultPaymentTotal), - props, - env.journal)); - } + describeLoan(brokerParams, loanParams, AssetType::XRP); runLoan(AssetType::XRP, brokerParams, loanParams); } From 3cb447a4fe7866a533ed9a577ca392d3898cebe5 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 4 Nov 2025 20:44:45 -0500 Subject: [PATCH 197/291] Add optional enforcement of valid integer range to Number --- include/xrpl/basics/Number.h | 116 ++++++++++++++++++++-- src/libxrpl/basics/Number.cpp | 40 ++++++++ src/test/basics/Number_test.cpp | 168 ++++++++++++++++++++++++++++++++ 3 files changed, 317 insertions(+), 7 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index e34cc61b5b..59f3a212a6 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -13,16 +13,47 @@ class Number; std::string to_string(Number const& amount); +template +constexpr bool +isPowerOfTen(T value) +{ + while (value >= 10 && value % 10 == 0) + value /= 10; + return value == 1; +} + class Number { +public: + /** Describes whether and how to enforce this number as an integer. + * + * - none: No enforcement. The value may vary freely. This is the default. + * - weak: If the absolute value is greater than maxIntValue, valid() will + * return false. + * - strong: Assignment operations will throw if the absolute value is above + * maxIntValue. + */ + enum EnforceInteger { none, weak, strong }; + +private: using rep = std::int64_t; rep mantissa_{0}; int exponent_{std::numeric_limits::lowest()}; + // The enforcement setting is not serialized, and does not affect the + // ledger. If not "none", the value is checked to be within the valid + // integer range. With "strong", the checks will be made as automatic as + // possible. + EnforceInteger enforceInteger_ = none; + public: // The range for the mantissa when normalized - constexpr static std::int64_t minMantissa = 1'000'000'000'000'000LL; - constexpr static std::int64_t maxMantissa = 9'999'999'999'999'999LL; + constexpr static rep minMantissa = 1'000'000'000'000'000LL; + static_assert(isPowerOfTen(minMantissa)); + constexpr static rep maxMantissa = minMantissa * 10 - 1; + static_assert(maxMantissa == 9'999'999'999'999'999LL); + + constexpr static rep maxIntValue = minMantissa / 10; // The range for the exponent when normalized constexpr static int minExponent = -32768; @@ -35,15 +66,33 @@ public: explicit constexpr Number() = default; - Number(rep mantissa); - explicit Number(rep mantissa, int exponent); + Number(rep mantissa, EnforceInteger enforce = none); + explicit Number(rep mantissa, int exponent, EnforceInteger enforce = none); explicit constexpr Number(rep mantissa, int exponent, unchecked) noexcept; + constexpr Number(Number const& other) = default; + constexpr Number(Number&& other) = default; + + ~Number() = default; + + constexpr Number& + operator=(Number const& other); + constexpr Number& + operator=(Number&& other); constexpr rep mantissa() const noexcept; constexpr int exponent() const noexcept; + void + setIntegerEnforcement(EnforceInteger enforce); + + EnforceInteger + integerEnforcement() const noexcept; + + bool + valid() const noexcept; + constexpr Number operator+() const noexcept; constexpr Number @@ -184,6 +233,9 @@ public: private: static thread_local rounding_mode mode_; + void + checkInteger(char const* what) const; + void normalize(); constexpr bool @@ -197,16 +249,52 @@ inline constexpr Number::Number(rep mantissa, int exponent, unchecked) noexcept { } -inline Number::Number(rep mantissa, int exponent) - : mantissa_{mantissa}, exponent_{exponent} +inline Number::Number(rep mantissa, int exponent, EnforceInteger enforce) + : mantissa_{mantissa}, exponent_{exponent}, enforceInteger_(enforce) { normalize(); + + checkInteger("Number::Number integer overflow"); } -inline Number::Number(rep mantissa) : Number{mantissa, 0} +inline Number::Number(rep mantissa, EnforceInteger enforce) + : Number{mantissa, 0, enforce} { } +constexpr Number& +Number::operator=(Number const& other) +{ + if (this != &other) + { + mantissa_ = other.mantissa_; + exponent_ = other.exponent_; + enforceInteger_ = std::max(enforceInteger_, other.enforceInteger_); + + checkInteger("Number::operator= integer overflow"); + } + + return *this; +} + +constexpr Number& +Number::operator=(Number&& other) +{ + if (this != &other) + { + // std::move doesn't really do anything for these types, but + // this is future-proof in case the types ever change + mantissa_ = std::move(other.mantissa_); + exponent_ = std::move(other.exponent_); + if (other.enforceInteger_ > enforceInteger_) + enforceInteger_ = std::move(other.enforceInteger_); + + checkInteger("Number::operator= integer overflow"); + } + + return *this; +} + inline constexpr Number::rep Number::mantissa() const noexcept { @@ -219,6 +307,20 @@ Number::exponent() const noexcept return exponent_; } +inline void +Number::setIntegerEnforcement(EnforceInteger enforce) +{ + enforceInteger_ = enforce; + + checkInteger("Number::setIntegerEnforcement integer overflow"); +} + +inline Number::EnforceInteger +Number::integerEnforcement() const noexcept +{ + return enforceInteger_; +} + inline constexpr Number Number::operator+() const noexcept { diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 89f7937e06..e3789de90a 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -155,6 +155,13 @@ Number::Guard::round() noexcept constexpr Number one{1000000000000000, -15, Number::unchecked{}}; +void +Number::checkInteger(char const* what) const +{ + if (enforceInteger_ == strong && !valid()) + throw std::overflow_error(what); +} + void Number::normalize() { @@ -207,9 +214,27 @@ Number::normalize() mantissa_ = -mantissa_; } +bool +Number::valid() const noexcept +{ + if (enforceInteger_ != none) + { + static Number const max = maxIntValue; + static Number const maxNeg = -maxIntValue; + // Avoid making a copy + if (mantissa_ < 0) + return *this >= maxNeg; + return *this <= max; + } + return true; +} + Number& Number::operator+=(Number const& y) { + // The strictest setting prevails + enforceInteger_ = std::max(enforceInteger_, y.enforceInteger_); + if (y == Number{}) return *this; if (*this == Number{}) @@ -322,6 +347,9 @@ Number::operator+=(Number const& y) } mantissa_ = xm * xn; exponent_ = xe; + + checkInteger("Number::addition integer overflow"); + return *this; } @@ -356,6 +384,9 @@ divu10(uint128_t& u) Number& Number::operator*=(Number const& y) { + // The strictest setting prevails + enforceInteger_ = std::max(enforceInteger_, y.enforceInteger_); + if (*this == Number{}) return *this; if (y == Number{}) @@ -422,12 +453,18 @@ Number::operator*=(Number const& y) XRPL_ASSERT( isnormal() || *this == Number{}, "ripple::Number::operator*=(Number) : result is normal"); + + checkInteger("Number::multiplication integer overflow"); + return *this; } Number& Number::operator/=(Number const& y) { + // The strictest setting prevails + enforceInteger_ = std::max(enforceInteger_, y.enforceInteger_); + if (y == Number{}) throw std::overflow_error("Number: divide by 0"); if (*this == Number{}) @@ -455,6 +492,9 @@ Number::operator/=(Number const& y) exponent_ = ne - de - 17; mantissa_ *= np * dp; normalize(); + + checkInteger("Number::division integer overflow"); + return *this; } diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 06203a4c2a..78c9b28952 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -725,6 +726,172 @@ public: BEAST_EXPECT(Number(-100, -30000).truncate() == Number(0, 0)); } + void + testInteger() + { + testcase("Integer enforcement"); + + using namespace std::string_literals; + + { + Number a{100}; + BEAST_EXPECT(a.integerEnforcement() == Number::none); + BEAST_EXPECT(a.valid()); + a = Number{1, 30}; + BEAST_EXPECT(a.valid()); + a = -100; + BEAST_EXPECT(a.valid()); + } + { + Number a{100, Number::weak}; + BEAST_EXPECT(a.integerEnforcement() == Number::weak); + BEAST_EXPECT(a.valid()); + a = Number{1, 30, Number::none}; + BEAST_EXPECT(!a.valid()); + a = -100; + BEAST_EXPECT(a.integerEnforcement() == Number::weak); + BEAST_EXPECT(a.valid()); + a = Number{5, Number::strong}; + BEAST_EXPECT(a.integerEnforcement() == Number::strong); + BEAST_EXPECT(a.valid()); + } + { + Number a{100, Number::strong}; + BEAST_EXPECT(a.integerEnforcement() == Number::strong); + BEAST_EXPECT(a.valid()); + try + { + a = Number{1, 30}; + BEAST_EXPECT(false); + } + catch (std::overflow_error const& e) + { + BEAST_EXPECT(e.what() == "Number::operator= integer overflow"s); + // The throw is done _after_ the number is updated. + BEAST_EXPECT((a == Number{1, 30})); + } + BEAST_EXPECT(!a.valid()); + a = -100; + BEAST_EXPECT(a.integerEnforcement() == Number::strong); + BEAST_EXPECT(a.valid()); + } + { + Number a{INITIAL_XRP.drops(), Number::weak}; + BEAST_EXPECT(!a.valid()); + a = -a; + BEAST_EXPECT(!a.valid()); + + try + { + a.setIntegerEnforcement(Number::strong); + BEAST_EXPECT(false); + } + catch (std::overflow_error const& e) + { + BEAST_EXPECT( + e.what() == + "Number::setIntegerEnforcement integer overflow"s); + // The throw is internal to the operator before the result is + // assigned to the Number + BEAST_EXPECT(a == -INITIAL_XRP); + BEAST_EXPECT(!a.valid()); + } + try + { + ++a; + BEAST_EXPECT(false); + } + catch (std::overflow_error const& e) + { + BEAST_EXPECT(e.what() == "Number::addition integer overflow"s); + // The throw is internal to the operator before the result is + // assigned to the Number + BEAST_EXPECT(a == -INITIAL_XRP); + BEAST_EXPECT(!a.valid()); + } + a = Number::maxIntValue; + try + { + ++a; + BEAST_EXPECT(false); + } + catch (std::overflow_error const& e) + { + BEAST_EXPECT(e.what() == "Number::addition integer overflow"s); + // This time, the throw is done _after_ the number is updated. + BEAST_EXPECT(a == Number::maxIntValue + 1); + BEAST_EXPECT(!a.valid()); + } + a = -Number::maxIntValue; + try + { + --a; + BEAST_EXPECT(false); + } + catch (std::overflow_error const& e) + { + BEAST_EXPECT(e.what() == "Number::addition integer overflow"s); + // This time, the throw is done _after_ the number is updated. + BEAST_EXPECT(a == -Number::maxIntValue - 1); + BEAST_EXPECT(!a.valid()); + } + a = Number(1, 10); + try + { + a *= Number(1, 10); + BEAST_EXPECT(false); + } + catch (std::overflow_error const& e) + { + BEAST_EXPECT( + e.what() == "Number::multiplication integer overflow"s); + // The throw is done _after_ the number is updated. + BEAST_EXPECT((a == Number{1, 20})); + BEAST_EXPECT(!a.valid()); + } + try + { + a = Number::maxIntValue * 2; + BEAST_EXPECT(false); + } + catch (std::overflow_error const& e) + { + BEAST_EXPECT(e.what() == "Number::operator= integer overflow"s); + // The throw is done _after_ the number is updated. + BEAST_EXPECT((a == Number{2, 14})); + BEAST_EXPECT(!a.valid()); + } + try + { + a = Number(3, 15, Number::strong); + BEAST_EXPECT(false); + } + catch (std::overflow_error const& e) + { + BEAST_EXPECT(e.what() == "Number::Number integer overflow"s); + // The Number doesn't get updated because the ctor throws + BEAST_EXPECT((a == Number{2, 14})); + BEAST_EXPECT(!a.valid()); + } + a = Number(1, 10); + try + { + a /= Number(1, -10); + BEAST_EXPECT(false); + } + catch (std::overflow_error const& e) + { + BEAST_EXPECT(e.what() == "Number::division integer overflow"s); + // The throw is done _after_ the number is updated. + BEAST_EXPECT((a == Number{1, 20})); + BEAST_EXPECT(!a.valid()); + } + a /= Number(1, 15); + BEAST_EXPECT((a == Number{1, 5})); + BEAST_EXPECT(a.valid()); + } + } + void run() override { @@ -746,6 +913,7 @@ public: test_inc_dec(); test_toSTAmount(); test_truncate(); + testInteger(); } }; From 24f37d73f614dafbd522068781532e06584ce68e Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 4 Nov 2025 21:02:39 -0500 Subject: [PATCH 198/291] Make all STNumber fields "soeDEFAULT" --- include/xrpl/protocol/detail/ledger_entries.macro | 6 +++--- src/test/app/Vault_test.cpp | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 53110f09f5..5aae9a9322 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -479,10 +479,10 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({ {sfAccount, soeREQUIRED}, {sfData, soeOPTIONAL}, {sfAsset, soeREQUIRED}, - {sfAssetsTotal, soeREQUIRED}, - {sfAssetsAvailable, soeREQUIRED}, + {sfAssetsTotal, soeDEFAULT}, + {sfAssetsAvailable, soeDEFAULT}, {sfAssetsMaximum, soeDEFAULT}, - {sfLossUnrealized, soeREQUIRED}, + {sfLossUnrealized, soeDEFAULT}, {sfShareMPTID, soeREQUIRED}, {sfWithdrawalPolicy, soeREQUIRED}, {sfScale, soeDEFAULT}, diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index ccbf0fed42..58b929f1a2 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -4438,7 +4438,8 @@ class Vault_test : public beast::unit_test::suite BEAST_EXPECT(checkString(vault, sfAssetsAvailable, "50")); BEAST_EXPECT(checkString(vault, sfAssetsMaximum, "1000")); BEAST_EXPECT(checkString(vault, sfAssetsTotal, "50")); - BEAST_EXPECT(checkString(vault, sfLossUnrealized, "0")); + // Since this field is default, it is not returned. + BEAST_EXPECT(!vault.isMember(sfLossUnrealized.getJsonName())); auto const strShareID = strHex(sle->at(sfShareMPTID)); BEAST_EXPECT(checkString(vault, sfShareMPTID, strShareID)); From b605a2cdcc2ac8ca27e3b44036b2ea375069ac89 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 4 Nov 2025 21:07:01 -0500 Subject: [PATCH 199/291] Add integer enforcement when converting to XRP/MPTAmount to Number --- include/xrpl/protocol/MPTAmount.h | 2 +- include/xrpl/protocol/XRPAmount.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/xrpl/protocol/MPTAmount.h b/include/xrpl/protocol/MPTAmount.h index af14786501..1c7c7a25e7 100644 --- a/include/xrpl/protocol/MPTAmount.h +++ b/include/xrpl/protocol/MPTAmount.h @@ -64,7 +64,7 @@ public: operator Number() const noexcept { - return value(); + return {value(), Number::strong}; } /** Return the sign of the amount */ diff --git a/include/xrpl/protocol/XRPAmount.h b/include/xrpl/protocol/XRPAmount.h index e102a3707f..f26bb7366f 100644 --- a/include/xrpl/protocol/XRPAmount.h +++ b/include/xrpl/protocol/XRPAmount.h @@ -143,7 +143,7 @@ public: operator Number() const noexcept { - return drops(); + return {drops(), Number::weak}; } /** Return the sign of the amount */ From cb6df196dc00dad4ce4e48d6623b88b765e9a27c Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 5 Nov 2025 09:21:12 -0500 Subject: [PATCH 200/291] Fix build error - avoid copy --- src/test/app/AMM_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index 5a1816ebae..66bceec327 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -1384,7 +1384,7 @@ private: // equal asset deposit: unit test to exercise the rounding-down of // LPTokens in the AMMHelpers.cpp: adjustLPTokens calculations // The LPTokens need to have 16 significant digits and a fractional part - for (Number const deltaLPTokens : + for (Number const& deltaLPTokens : {Number{UINT64_C(100000'0000000009), -10}, Number{UINT64_C(100000'0000000001), -10}}) { From 0175dd70dbc6a962e93c1fd4ec4cbadc1bcfd903 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 5 Nov 2025 18:15:49 -0500 Subject: [PATCH 201/291] Catch up the consequences of Number changes - Change the Number::maxIntValue to all 9's. - Add integral() to Asset (copied from Lending) - Add toNumber() functions to STAmount, MPTAmount, XRPAmount to allow explicit conversions with enforcement options. - Add optional Number::EnforceInteger options to STAmount and STNumber ctors, conversions, etc. IOUs are never checked. - Update Vault transactors, and helper functions, to check restrictions. - Fix and add Vault tests. --- include/xrpl/basics/Number.h | 3 +- include/xrpl/protocol/Asset.h | 6 ++ include/xrpl/protocol/MPTAmount.h | 8 ++- include/xrpl/protocol/STAmount.h | 67 ++++++++++++++++++- include/xrpl/protocol/STNumber.h | 12 ++++ include/xrpl/protocol/SystemParameters.h | 1 + include/xrpl/protocol/XRPAmount.h | 6 ++ src/libxrpl/ledger/View.cpp | 23 +++++-- src/libxrpl/protocol/STAmount.cpp | 19 ++++++ src/libxrpl/protocol/STNumber.cpp | 18 ++++++ src/test/app/Vault_test.cpp | 78 +++++++++++++++++++++-- src/test/basics/Number_test.cpp | 4 +- src/xrpld/app/tx/detail/VaultClawback.cpp | 19 +++++- src/xrpld/app/tx/detail/VaultDeposit.cpp | 23 +++++-- src/xrpld/app/tx/detail/VaultWithdraw.cpp | 11 +++- 15 files changed, 274 insertions(+), 24 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index 59f3a212a6..4f447d5f98 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -53,7 +53,8 @@ public: constexpr static rep maxMantissa = minMantissa * 10 - 1; static_assert(maxMantissa == 9'999'999'999'999'999LL); - constexpr static rep maxIntValue = minMantissa / 10; + constexpr static rep maxIntValue = maxMantissa / 10; + static_assert(maxIntValue == 999'999'999'999'999LL); // The range for the exponent when normalized constexpr static int minExponent = -32768; diff --git a/include/xrpl/protocol/Asset.h b/include/xrpl/protocol/Asset.h index d0efe814a9..6765f7cf7d 100644 --- a/include/xrpl/protocol/Asset.h +++ b/include/xrpl/protocol/Asset.h @@ -84,6 +84,12 @@ public: return holds() && get().native(); } + bool + integral() const + { + return !holds() || get().native(); + } + friend constexpr bool operator==(Asset const& lhs, Asset const& rhs); diff --git a/include/xrpl/protocol/MPTAmount.h b/include/xrpl/protocol/MPTAmount.h index 1c7c7a25e7..5f552d839b 100644 --- a/include/xrpl/protocol/MPTAmount.h +++ b/include/xrpl/protocol/MPTAmount.h @@ -62,11 +62,17 @@ public: explicit constexpr operator bool() const noexcept; - operator Number() const noexcept + operator Number() const { return {value(), Number::strong}; } + Number + toNumber(Number::EnforceInteger enforce) const + { + return {value(), enforce}; + } + /** Return the sign of the amount */ constexpr int signum() const noexcept; diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index 83493efcdd..d0092fdbec 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -40,6 +40,12 @@ private: exponent_type mOffset; bool mIsNegative; + // The Enforce integer setting is not stored or serialized. If set, it is + // used during automatic conversions to Number. If not set, the default + // behavior is used. It can also be overridden when coverting by using + // toNumber(). + std::optional enforceConversion_; + public: using value_type = STAmount; @@ -135,9 +141,28 @@ public: STAmount(A const& asset, int mantissa, int exponent = 0); template - STAmount(A const& asset, Number const& number) + STAmount( + A const& asset, + Number const& number, + std::optional enforce = std::nullopt) : STAmount(asset, number.mantissa(), number.exponent()) { + enforceConversion_ = enforce; + if (!enforce) + { + // Use the default conversion behavior + [[maybe_unused]] + Number const n = *this; + } + else if (enforce == Number::strong) + { + // Throw if it's not valid + if (!validNumber()) + { + Throw( + "STAmount::STAmount integer Number lost precision"); + } + } } // Legacy support for new-style amounts @@ -145,6 +170,17 @@ public: STAmount(XRPAmount const& amount); STAmount(MPTAmount const& amount, MPTIssue const& mptIssue); operator Number() const; + Number + toNumber(Number::EnforceInteger enforce) const; + + void + setIntegerEnforcement(std::optional enforce); + + std::optional + integerEnforcement() const noexcept; + + bool + validNumber() const noexcept; //-------------------------------------------------------------------------- // @@ -155,6 +191,9 @@ public: int exponent() const noexcept; + bool + integral() const noexcept; + bool native() const noexcept; @@ -435,6 +474,12 @@ STAmount::exponent() const noexcept return mOffset; } +inline bool +STAmount::integral() const noexcept +{ + return mAsset.integral(); +} + inline bool STAmount::native() const noexcept { @@ -510,6 +555,8 @@ inline STAmount::operator bool() const noexcept inline STAmount::operator Number() const { + if (enforceConversion_) + return toNumber(*enforceConversion_); if (native()) return xrp(); if (mAsset.holds()) @@ -517,6 +564,17 @@ inline STAmount::operator Number() const return iou(); } +inline Number +STAmount::toNumber(Number::EnforceInteger enforce) const +{ + if (native()) + return xrp().toNumber(enforce); + if (mAsset.holds()) + return mpt().toNumber(enforce); + // It doesn't make sense to enforce limits on IOUs + return iou(); +} + inline STAmount& STAmount::operator=(beast::Zero) { @@ -538,6 +596,11 @@ STAmount::operator=(Number const& number) mValue = mIsNegative ? -number.mantissa() : number.mantissa(); mOffset = number.exponent(); canonicalize(); + + // Convert it back to a Number to check that it's valid + [[maybe_unused]] + Number n = *this; + return *this; } @@ -553,7 +616,7 @@ STAmount::clear() { // The -100 is used to allow 0 to sort less than a small positive values // which have a negative exponent. - mOffset = native() ? 0 : -100; + mOffset = integral() ? 0 : -100; mValue = 0; mIsNegative = false; } diff --git a/include/xrpl/protocol/STNumber.h b/include/xrpl/protocol/STNumber.h index 2ec3d66fd1..43b96a2b46 100644 --- a/include/xrpl/protocol/STNumber.h +++ b/include/xrpl/protocol/STNumber.h @@ -56,6 +56,18 @@ public: bool isDefault() const override; + /// Sets the flag on the underlying number + void + setIntegerEnforcement(Number::EnforceInteger enforce); + + /// Gets the flag value on the underlying number + Number::EnforceInteger + integerEnforcement() const noexcept; + + /// Checks the underlying number + bool + valid() const noexcept; + operator Number() const { return value_; diff --git a/include/xrpl/protocol/SystemParameters.h b/include/xrpl/protocol/SystemParameters.h index de78b65265..7a2c5a7f63 100644 --- a/include/xrpl/protocol/SystemParameters.h +++ b/include/xrpl/protocol/SystemParameters.h @@ -23,6 +23,7 @@ systemName() /** Number of drops in the genesis account. */ constexpr XRPAmount INITIAL_XRP{100'000'000'000 * DROPS_PER_XRP}; +static_assert(INITIAL_XRP.drops() == 100'000'000'000'000'000); /** Returns true if the amount does not exceed the initial XRP in existence. */ inline bool diff --git a/include/xrpl/protocol/XRPAmount.h b/include/xrpl/protocol/XRPAmount.h index f26bb7366f..159174accc 100644 --- a/include/xrpl/protocol/XRPAmount.h +++ b/include/xrpl/protocol/XRPAmount.h @@ -146,6 +146,12 @@ public: return {drops(), Number::weak}; } + Number + toNumber(Number::EnforceInteger enforce) const + { + return {value(), enforce}; + } + /** Return the sign of the amount */ constexpr int signum() const noexcept diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index 0175b099ea..92fd5ccc97 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -2878,13 +2878,17 @@ assetsToSharesDeposit( Number const assetTotal = vault->at(sfAssetsTotal); STAmount shares{vault->at(sfShareMPTID)}; + shares.setIntegerEnforcement(Number::weak); if (assetTotal == 0) return STAmount{ shares.asset(), Number(assets.mantissa(), assets.exponent() + vault->at(sfScale)) - .truncate()}; + .truncate(), + Number::weak}; - Number const shareTotal = issuance->at(sfOutstandingAmount); + Number const shareTotal{ + unsafe_cast(issuance->at(sfOutstandingAmount)), + Number::strong}; shares = (shareTotal * (assets / assetTotal)).truncate(); return shares; } @@ -2906,6 +2910,7 @@ sharesToAssetsDeposit( Number const assetTotal = vault->at(sfAssetsTotal); STAmount assets{vault->at(sfAsset)}; + assets.setIntegerEnforcement(Number::weak); if (assetTotal == 0) return STAmount{ assets.asset(), @@ -2913,7 +2918,9 @@ sharesToAssetsDeposit( shares.exponent() - vault->at(sfScale), false}; - Number const shareTotal = issuance->at(sfOutstandingAmount); + Number const shareTotal{ + unsafe_cast(issuance->at(sfOutstandingAmount)), + Number::strong}; assets = assetTotal * (shares / shareTotal); return assets; } @@ -2937,9 +2944,12 @@ assetsToSharesWithdraw( Number assetTotal = vault->at(sfAssetsTotal); assetTotal -= vault->at(sfLossUnrealized); STAmount shares{vault->at(sfShareMPTID)}; + shares.setIntegerEnforcement(Number::weak); if (assetTotal == 0) return shares; - Number const shareTotal = issuance->at(sfOutstandingAmount); + Number const shareTotal{ + unsafe_cast(issuance->at(sfOutstandingAmount)), + Number::strong}; Number result = shareTotal * (assets / assetTotal); if (truncate == TruncateShares::yes) result = result.truncate(); @@ -2965,9 +2975,12 @@ sharesToAssetsWithdraw( Number assetTotal = vault->at(sfAssetsTotal); assetTotal -= vault->at(sfLossUnrealized); STAmount assets{vault->at(sfAsset)}; + assets.setIntegerEnforcement(Number::weak); if (assetTotal == 0) return assets; - Number const shareTotal = issuance->at(sfOutstandingAmount); + Number const shareTotal{ + unsafe_cast(issuance->at(sfOutstandingAmount)), + Number::strong}; assets = assetTotal * (shares / shareTotal); return assets; } diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 4aac551003..6f6355a145 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -255,6 +255,25 @@ STAmount::move(std::size_t n, void* buf) return emplace(n, buf, std::move(*this)); } +void +STAmount::setIntegerEnforcement(std::optional enforce) +{ + enforceConversion_ = enforce; +} + +std::optional +STAmount::integerEnforcement() const noexcept +{ + return enforceConversion_; +} + +bool +STAmount::validNumber() const noexcept +{ + Number n = toNumber(Number::EnforceInteger::weak); + return n.valid(); +} + //------------------------------------------------------------------------------ // // Conversion diff --git a/src/libxrpl/protocol/STNumber.cpp b/src/libxrpl/protocol/STNumber.cpp index 889dd9ee68..5486864e95 100644 --- a/src/libxrpl/protocol/STNumber.cpp +++ b/src/libxrpl/protocol/STNumber.cpp @@ -94,6 +94,24 @@ STNumber::isDefault() const return value_ == Number(); } +void +STNumber::setIntegerEnforcement(Number::EnforceInteger enforce) +{ + value_.setIntegerEnforcement(enforce); +} + +Number::EnforceInteger +STNumber::integerEnforcement() const noexcept +{ + return value_.integerEnforcement(); +} + +bool +STNumber::valid() const noexcept +{ + return value_.valid(); +} + std::ostream& operator<<(std::ostream& out, STNumber const& rhs) { diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 58b929f1a2..5b36ffaf8e 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -3597,7 +3597,32 @@ class Vault_test : public beast::unit_test::suite }); testCase(18, [&, this](Env& env, Data d) { - testcase("Scale deposit overflow on second deposit"); + testcase("MPT scale deposit overflow"); + // The computed number of shares can not be represented as an MPT + // without truncation + + { + auto tx = d.vault.deposit( + {.depositor = d.depositor, + .id = d.keylet.key, + .amount = d.asset(5)}); + env(tx, ter{tecPRECISION_LOSS}); + env.close(); + } + }); + + testCase(14, [&, this](Env& env, Data d) { + testcase("MPT scale deposit overflow on first deposit"); + auto tx = d.vault.deposit( + {.depositor = d.depositor, + .id = d.keylet.key, + .amount = d.asset(10)}); + env(tx, ter{tecPRECISION_LOSS}); + env.close(); + }); + + testCase(14, [&, this](Env& env, Data d) { + testcase("MPT scale deposit overflow on second deposit"); { auto tx = d.vault.deposit( @@ -3618,8 +3643,8 @@ class Vault_test : public beast::unit_test::suite } }); - testCase(18, [&, this](Env& env, Data d) { - testcase("Scale deposit overflow on total shares"); + testCase(14, [&, this](Env& env, Data d) { + testcase("No MPT scale deposit overflow on total shares"); { auto tx = d.vault.deposit( @@ -3635,7 +3660,7 @@ class Vault_test : public beast::unit_test::suite {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(5)}); - env(tx, ter{tecPATH_DRY}); + env(tx); env.close(); } }); @@ -3919,6 +3944,28 @@ class Vault_test : public beast::unit_test::suite testCase(18, [&, this](Env& env, Data d) { testcase("Scale withdraw overflow"); + { + auto tx = d.vault.deposit( + {.depositor = d.depositor, + .id = d.keylet.key, + .amount = d.asset(5)}); + env(tx, ter{tecPRECISION_LOSS}); + env.close(); + } + + { + auto tx = d.vault.withdraw( + {.depositor = d.depositor, + .id = d.keylet.key, + .amount = STAmount(d.asset, Number(10, 0))}); + env(tx, ter{tecPRECISION_LOSS}); + env.close(); + } + }); + + testCase(14, [&, this](Env& env, Data d) { + testcase("MPT scale withdraw overflow"); + { auto tx = d.vault.deposit( {.depositor = d.depositor, @@ -4137,6 +4184,29 @@ class Vault_test : public beast::unit_test::suite testCase(18, [&, this](Env& env, Data d) { testcase("Scale clawback overflow"); + { + auto tx = d.vault.deposit( + {.depositor = d.depositor, + .id = d.keylet.key, + .amount = d.asset(5)}); + env(tx, ter(tecPRECISION_LOSS)); + env.close(); + } + + { + auto tx = d.vault.clawback( + {.issuer = d.issuer, + .id = d.keylet.key, + .holder = d.depositor, + .amount = STAmount(d.asset, Number(10, 0))}); + env(tx, ter{tecPRECISION_LOSS}); + env.close(); + } + }); + + testCase(14, [&, this](Env& env, Data d) { + testcase("MPT Scale clawback overflow"); + { auto tx = d.vault.deposit( {.depositor = d.depositor, diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 78c9b28952..a5fba8ef7d 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -858,7 +858,7 @@ public: { BEAST_EXPECT(e.what() == "Number::operator= integer overflow"s); // The throw is done _after_ the number is updated. - BEAST_EXPECT((a == Number{2, 14})); + BEAST_EXPECT((a == Number::maxIntValue * 2)); BEAST_EXPECT(!a.valid()); } try @@ -870,7 +870,7 @@ public: { BEAST_EXPECT(e.what() == "Number::Number integer overflow"s); // The Number doesn't get updated because the ctor throws - BEAST_EXPECT((a == Number{2, 14})); + BEAST_EXPECT((a == Number::maxIntValue * 2)); BEAST_EXPECT(!a.valid()); } a = Number(1, 10); diff --git a/src/xrpld/app/tx/detail/VaultClawback.cpp b/src/xrpld/app/tx/detail/VaultClawback.cpp index 7c56ca1d60..1c73d36bb0 100644 --- a/src/xrpld/app/tx/detail/VaultClawback.cpp +++ b/src/xrpld/app/tx/detail/VaultClawback.cpp @@ -71,9 +71,13 @@ VaultClawback::preclaim(PreclaimContext const& ctx) } Asset const vaultAsset = vault->at(sfAsset); - if (auto const amount = ctx.tx[~sfAmount]; - amount && vaultAsset != amount->asset()) - return tecWRONG_ASSET; + if (auto const amount = ctx.tx[~sfAmount]) + { + if (vaultAsset != amount->asset()) + return tecWRONG_ASSET; + else if (!amount->validNumber()) + return tecPRECISION_LOSS; + } if (vaultAsset.native()) { @@ -157,6 +161,8 @@ VaultClawback::doApply() MPTIssue const share{mptIssuanceID}; STAmount sharesDestroyed = {share}; STAmount assetsRecovered; + assetsRecovered.setIntegerEnforcement(Number::weak); + sharesDestroyed.setIntegerEnforcement(Number::weak); try { if (amount == beast::zero) @@ -169,6 +175,9 @@ VaultClawback::doApply() AuthHandling::ahIGNORE_AUTH, j_); + if (!sharesDestroyed.validNumber()) + return tecPRECISION_LOSS; + auto const maybeAssets = sharesToAssetsWithdraw(vault, sleIssuance, sharesDestroyed); if (!maybeAssets) @@ -184,6 +193,8 @@ VaultClawback::doApply() if (!maybeShares) return tecINTERNAL; // LCOV_EXCL_LINE sharesDestroyed = *maybeShares; + if (!sharesDestroyed.validNumber()) + return tecPRECISION_LOSS; } auto const maybeAssets = @@ -192,6 +203,8 @@ VaultClawback::doApply() return tecINTERNAL; // LCOV_EXCL_LINE assetsRecovered = *maybeAssets; } + if (!assetsRecovered.validNumber()) + return tecPRECISION_LOSS; // Clamp to maximum. if (assetsRecovered > *assetsAvailable) diff --git a/src/xrpld/app/tx/detail/VaultDeposit.cpp b/src/xrpld/app/tx/detail/VaultDeposit.cpp index 3e5ae741e3..66e144312f 100644 --- a/src/xrpld/app/tx/detail/VaultDeposit.cpp +++ b/src/xrpld/app/tx/detail/VaultDeposit.cpp @@ -42,6 +42,9 @@ VaultDeposit::preclaim(PreclaimContext const& ctx) if (assets.asset() != vaultAsset) return tecWRONG_ASSET; + if (!assets.validNumber()) + return tecPRECISION_LOSS; + if (vaultAsset.native()) ; // No special checks for XRP else if (vaultAsset.holds()) @@ -217,6 +220,7 @@ VaultDeposit::doApply() } STAmount sharesCreated = {vault->at(sfShareMPTID)}, assetsDeposited; + sharesCreated.setIntegerEnforcement(Number::weak); try { // Compute exchange before transferring any amounts. @@ -227,14 +231,14 @@ VaultDeposit::doApply() return tecINTERNAL; // LCOV_EXCL_LINE sharesCreated = *maybeShares; } - if (sharesCreated == beast::zero) + if (sharesCreated == beast::zero || !sharesCreated.validNumber()) return tecPRECISION_LOSS; auto const maybeAssets = sharesToAssetsDeposit(vault, sleIssuance, sharesCreated); if (!maybeAssets) return tecINTERNAL; // LCOV_EXCL_LINE - else if (*maybeAssets > amount) + else if (*maybeAssets > amount || !maybeAssets->validNumber()) { // LCOV_EXCL_START JLOG(j_.error()) << "VaultDeposit: would take more than offered."; @@ -260,13 +264,22 @@ VaultDeposit::doApply() sharesCreated.asset() != assetsDeposited.asset(), "ripple::VaultDeposit::doApply : assets are not shares"); - vault->at(sfAssetsTotal) += assetsDeposited; - vault->at(sfAssetsAvailable) += assetsDeposited; + auto assetsTotalProxy = vault->at(sfAssetsTotal); + auto assetsAvailableProxy = vault->at(sfAssetsAvailable); + if (vault->at(sfAsset).value().integral()) + { + assetsTotalProxy.value().setIntegerEnforcement(Number::weak); + assetsAvailableProxy.value().setIntegerEnforcement(Number::weak); + } + assetsTotalProxy += assetsDeposited; + assetsAvailableProxy += assetsDeposited; + if (!assetsTotalProxy->valid() || !assetsAvailableProxy->valid()) + return tecLIMIT_EXCEEDED; view().update(vault); // A deposit must not push the vault over its limit. auto const maximum = *vault->at(sfAssetsMaximum); - if (maximum != 0 && *vault->at(sfAssetsTotal) > maximum) + if (maximum != 0 && *assetsTotalProxy > maximum) return tecLIMIT_EXCEEDED; // Transfer assets from depositor to vault. diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.cpp b/src/xrpld/app/tx/detail/VaultWithdraw.cpp index 8cd3f5cd97..8806f7b236 100644 --- a/src/xrpld/app/tx/detail/VaultWithdraw.cpp +++ b/src/xrpld/app/tx/detail/VaultWithdraw.cpp @@ -50,6 +50,9 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx) if (assets.asset() != vaultAsset && assets.asset() != vaultShare) return tecWRONG_ASSET; + if (!assets.validNumber()) + return tecPRECISION_LOSS; + if (vaultAsset.native()) ; // No special checks for XRP else if (vaultAsset.holds()) @@ -154,6 +157,8 @@ VaultWithdraw::doApply() MPTIssue const share{mptIssuanceID}; STAmount sharesRedeemed = {share}; STAmount assetsWithdrawn; + assetsWithdrawn.setIntegerEnforcement(Number::weak); + sharesRedeemed.setIntegerEnforcement(Number::weak); try { if (amount.asset() == vaultAsset) @@ -167,13 +172,15 @@ VaultWithdraw::doApply() sharesRedeemed = *maybeShares; } - if (sharesRedeemed == beast::zero) + if (sharesRedeemed == beast::zero || !sharesRedeemed.validNumber()) return tecPRECISION_LOSS; auto const maybeAssets = sharesToAssetsWithdraw(vault, sleIssuance, sharesRedeemed); if (!maybeAssets) return tecINTERNAL; // LCOV_EXCL_LINE assetsWithdrawn = *maybeAssets; + if (!assetsWithdrawn.validNumber()) + return tecPRECISION_LOSS; } else if (amount.asset() == share) { @@ -184,6 +191,8 @@ VaultWithdraw::doApply() if (!maybeAssets) return tecINTERNAL; // LCOV_EXCL_LINE assetsWithdrawn = *maybeAssets; + if (!assetsWithdrawn.validNumber()) + return tecPRECISION_LOSS; } else return tefINTERNAL; // LCOV_EXCL_LINE From ebfca636fc82b4f41158a63c19f6d2afe7fed576 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Fri, 7 Nov 2025 00:39:34 -0500 Subject: [PATCH 202/291] Implicitly authorize Vault and LoanBroker pseudo-accounts (#5976) - Vault and LoanBroker pseudo-accounts can hold MPTs, regardless of MPTRequireAuth setting. - Add requireAuth check in LoanBrokerCoverDeposit and LoanPay. - Fail attempts to unauthorize pseudo-accounts by MPT issuers. --- include/xrpl/ledger/View.h | 18 ++- src/libxrpl/ledger/View.cpp | 27 +++- src/test/app/LoanBroker_test.cpp | 121 ++++++++++++++++++ src/test/app/Loan_test.cpp | 61 +++++++++ .../app/tx/detail/LoanBrokerCoverDeposit.cpp | 4 + src/xrpld/app/tx/detail/LoanPay.cpp | 5 + src/xrpld/app/tx/detail/MPTokenAuthorize.cpp | 9 +- 7 files changed, 234 insertions(+), 11 deletions(-) diff --git a/include/xrpl/ledger/View.h b/include/xrpl/ledger/View.h index c31502be98..ece574e52d 100644 --- a/include/xrpl/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -654,14 +654,17 @@ createPseudoAccount( uint256 const& pseudoOwnerKey, SField const& ownerField); -// Returns true iff sleAcct is a pseudo-account. +// Returns true iff sleAcct is a pseudo-account or specific +// pseudo-accounts in pseudoFieldFilter. // // Returns false if sleAcct is // * NOT a pseudo-account OR // * NOT a ltACCOUNT_ROOT OR // * null pointer [[nodiscard]] bool -isPseudoAccount(std::shared_ptr sleAcct); +isPseudoAccount( + std::shared_ptr sleAcct, + std::set const& pseudoFieldFilter = {}); // Returns the list of fields that define an ACCOUNT_ROOT as a pseudo-account if // set @@ -675,9 +678,13 @@ isPseudoAccount(std::shared_ptr sleAcct); getPseudoAccountFields(); [[nodiscard]] inline bool -isPseudoAccount(ReadView const& view, AccountID const& accountId) +isPseudoAccount( + ReadView const& view, + AccountID const& accountId, + std::set const& pseudoFieldFilter = {}) { - return isPseudoAccount(view.read(keylet::account(accountId))); + return isPseudoAccount( + view.read(keylet::account(accountId)), pseudoFieldFilter); } [[nodiscard]] TER @@ -1001,7 +1008,8 @@ requireAuth( * purely defensive, as we currently do not allow such vaults to be created. * * If StrongAuth then return tecNO_AUTH if MPToken doesn't exist or - * lsfMPTRequireAuth is set and MPToken is not authorized. + * lsfMPTRequireAuth is set and MPToken is not authorized. Vault and LoanBroker + * pseudo-accounts are implicitly authorized. * * If WeakAuth then return tecNO_AUTH if lsfMPTRequireAuth is set and MPToken * doesn't exist or is not authorized (explicitly or via credentials, if diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index 78e1663773..1e57bc663b 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -1204,7 +1204,8 @@ getPseudoAccountFields() { // LCOV_EXCL_START LogicError( - "ripple::isPseudoAccount : unable to find account root ledger " + "ripple::getPseudoAccountFields : unable to find account root " + "ledger " "format"); // LCOV_EXCL_STOP } @@ -1222,7 +1223,9 @@ getPseudoAccountFields() } [[nodiscard]] bool -isPseudoAccount(std::shared_ptr sleAcct) +isPseudoAccount( + std::shared_ptr sleAcct, + std::set const& pseudoFieldFilter) { auto const& fields = getPseudoAccountFields(); @@ -1230,8 +1233,12 @@ isPseudoAccount(std::shared_ptr sleAcct) // semantics of true return value clean. return sleAcct && sleAcct->getType() == ltACCOUNT_ROOT && std::count_if( - fields.begin(), fields.end(), [&sleAcct](SField const* sf) -> bool { - return sleAcct->isFieldPresent(*sf); + fields.begin(), + fields.end(), + [&sleAcct, &pseudoFieldFilter](SField const* sf) -> bool { + return sleAcct->isFieldPresent(*sf) && + (pseudoFieldFilter.empty() || + pseudoFieldFilter.contains(sf)); }) > 0; } @@ -3099,7 +3106,10 @@ requireAuth( if (mptIssuer == account) // Issuer won't have MPToken return tesSUCCESS; - if (view.rules().enabled(featureSingleAssetVault)) + bool const featureSAVEnabled = + view.rules().enabled(featureSingleAssetVault); + + if (featureSAVEnabled) { if (depth >= maxAssetCheckDepth) return tecINTERNAL; // LCOV_EXCL_LINE @@ -3158,6 +3168,13 @@ requireAuth( // belong to someone who is explicitly authorized e.g. a vault owner. } + if (featureSAVEnabled) + { + // Implicitly authorize Vault and LoanBroker pseudo-accounts + if (isPseudoAccount(view, account, {&sfVaultID, &sfLoanBrokerID})) + return tesSUCCESS; + } + // mptoken must be authorized if issuance enabled requireAuth if (sleIssuance->isFlag(lsfMPTRequireAuth) && (!sleToken || !sleToken->isFlag(lsfMPTAuthorized))) diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 171b699bbf..ff70290331 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -1265,6 +1265,126 @@ class LoanBroker_test : public beast::unit_test::suite (void)LoanBrokerCoverDeposit::preclaim(pctx); } + void + testRequireAuth() + { + testcase("Require Auth - Implicit Pseudo-account authorization"); + using namespace jtx; + using namespace loanBroker; + + Account const issuer{"issuer"}; + Account const alice{"alice"}; + Env env(*this); + Vault vault{env}; + + env.fund(XRP(100'000), issuer, alice); + env.close(); + + auto asset = MPTTester({ + .env = env, + .issuer = issuer, + .holders = {alice}, + .flags = MPTDEXFlags | tfMPTRequireAuth | tfMPTCanClawback | + tfMPTCanLock, + .authHolder = true, + }); + + env(pay(issuer, alice, asset(100'000))); + env.close(); + + // Alice is not authorized, can still create the vault + asset.authorize( + {.account = issuer, .holder = alice, .flags = tfMPTUnauthorize}); + auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = asset}); + env(tx); + env.close(); + + auto const le = env.le(vaultKeylet); + VaultInfo vaultInfo = [&]() { + if (BEAST_EXPECT(le)) + return VaultInfo{asset, vaultKeylet.key, le->at(sfAccount)}; + return VaultInfo{asset, {}, {}}; + }(); + if (vaultInfo.vaultID == uint256{}) + return; + + // Can't unauthorize Vault pseudo-account + asset.authorize( + {.account = issuer, + .holder = vaultInfo.pseudoAccount, + .flags = tfMPTUnauthorize, + .err = tecNO_PERMISSION}); + + auto forUnauthAuth = [&](auto&& doTx) { + for (auto const flag : {tfMPTUnauthorize, 0u}) + { + asset.authorize( + {.account = issuer, .holder = alice, .flags = flag}); + env.close(); + doTx(flag == 0); + env.close(); + } + }; + + // Can't deposit into Vault if the vault owner is not authorized + forUnauthAuth([&](bool authorized) { + auto const err = !authorized ? ter(tecNO_AUTH) : ter(tesSUCCESS); + env(vault.deposit( + {.depositor = alice, + .id = vaultKeylet.key, + .amount = asset(51)}), + err); + }); + + // Can't withdraw from Vault if the vault owner is not authorized + forUnauthAuth([&](bool authorized) { + auto const err = !authorized ? ter(tecNO_AUTH) : ter(tesSUCCESS); + env(vault.withdraw( + {.depositor = alice, + .id = vaultKeylet.key, + .amount = asset(1)}), + err); + }); + + auto const brokerKeylet = + keylet::loanbroker(alice.id(), env.seq(alice)); + // Can create LoanBroker if the vault owner is not authorized + forUnauthAuth([&](auto) { env(set(alice, vaultInfo.vaultID)); }); + + auto const broker = env.le(brokerKeylet); + if (!BEAST_EXPECT(broker)) + return; + Account brokerPseudo("pseudo", broker->at(sfAccount)); + + // Can't unauthorize LoanBroker pseudo-account + asset.authorize( + {.account = issuer, + .holder = brokerPseudo, + .flags = tfMPTUnauthorize, + .err = tecNO_PERMISSION}); + + // Can't cover deposit into Vault if the vault owner is not authorized + forUnauthAuth([&](bool authorized) { + auto const err = !authorized ? ter(tecNO_AUTH) : ter(tesSUCCESS); + env(coverDeposit(alice, brokerKeylet.key, vaultInfo.asset(10)), + err); + }); + + // Can't cover withdraw from Vault if the vault owner is not authorized + forUnauthAuth([&](bool authorized) { + auto const err = !authorized ? ter(tecNO_AUTH) : ter(tesSUCCESS); + env(coverWithdraw(alice, brokerKeylet.key, vaultInfo.asset(5)), + err); + }); + + // Issuer can always cover clawback. The holder authorization is n/a. + forUnauthAuth([&](bool) { + env(coverClawback(issuer), + loanBrokerID(brokerKeylet.key), + amount(vaultInfo.asset(1))); + }); + } + public: void run() override @@ -1278,6 +1398,7 @@ public: testInvalidLoanBrokerCoverWithdraw(); testInvalidLoanBrokerDelete(); testInvalidLoanBrokerSet(); + testRequireAuth(); // TODO: Write clawback failure tests with an issuer / MPT that doesn't // have the right flags set. diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 575d687dfa..67bc49f076 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -5333,6 +5333,65 @@ protected: } } + void + testRequireAuth() + { + testcase("Require Auth - Implicit Pseudo-account authorization"); + using namespace jtx; + using namespace loan; + Account const lender{"lender"}; + Account const issuer{"issuer"}; + Account const borrower{"borrower"}; + Env env(*this); + + env.fund(XRP(100'000), issuer, lender, borrower); + env.close(); + + auto asset = MPTTester({ + .env = env, + .issuer = issuer, + .holders = {lender, borrower}, + .flags = MPTDEXFlags | tfMPTRequireAuth | tfMPTCanClawback | + tfMPTCanLock, + .authHolder = true, + }); + + env(pay(issuer, lender, asset(5'000'000))); + BrokerInfo brokerInfo{createVaultAndBroker(env, asset, lender)}; + + auto const loanSetFee = fee(env.current()->fees().base * 2); + STAmount const debtMaximumRequest = brokerInfo.asset(1'000).value(); + + auto forUnauthAuth = [&](auto&& doTx) { + for (auto const flag : {tfMPTUnauthorize, 0u}) + { + asset.authorize( + {.account = issuer, .holder = borrower, .flags = flag}); + env.close(); + doTx(flag == 0); + env.close(); + } + }; + + // Can't create a loan if the borrower is not authorized + forUnauthAuth([&](bool authorized) { + auto const err = !authorized ? ter(tecNO_AUTH) : ter(tesSUCCESS); + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + sig(sfCounterpartySignature, lender), + loanSetFee, + err); + }); + + std::uint32_t constexpr loanSequence = 1; + auto const loanKeylet = keylet::loan(brokerInfo.brokerID, loanSequence); + + // Can't loan pay if the borrower is not authorized + forUnauthAuth([&](bool authorized) { + auto const err = !authorized ? ter(tecNO_AUTH) : ter(tesSUCCESS); + env(pay(borrower, loanKeylet.key, debtMaximumRequest), err); + }); + } + #if LOANTODO void testCoverDepositAllowsNonTransferableMPT() @@ -6193,6 +6252,8 @@ public: testLoanPayComputePeriodicPaymentValidTotalPrincipalPaidInvariant(); testLoanPayComputePeriodicPaymentValidTotalInterestPaidInvariant(); testLoanNextPaymentDueDateOverflow(); + + testRequireAuth(); } }; diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp index e9ba2cda64..25075582c5 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp @@ -65,6 +65,10 @@ LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx) // Pseudo-account cannot receive if asset is deep frozen if (auto const ret = checkDeepFrozen(ctx.view, pseudoAccountID, vaultAsset)) return ret; + // Cannot transfer unauthorized asset + if (auto const ret = + requireAuth(ctx.view, vaultAsset, account, AuthType::StrongAuth)) + return ret; if (accountHolds( ctx.view, diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index d99d2f4544..3edc109c80 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -187,6 +187,11 @@ LoanPay::preclaim(PreclaimContext const& ctx) << "Vault pseudo-account can not receive funds (deep frozen)."; return ret; } + if (auto const ret = requireAuth(ctx.view, asset, account)) + { + JLOG(ctx.j.warn()) << "Borrower account is not authorized."; + return ret; + } // Make sure the borrower has enough funds to make the payment! // Do not support "partial payments" - if the transaction says to pay X, // then the account must have X available, even if the loan payment takes diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp index 858fd6d0d6..b6eaacb372 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp @@ -94,7 +94,8 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx) return tesSUCCESS; } - if (!ctx.view.exists(keylet::account(*holderID))) + auto const sleHolder = ctx.view.read(keylet::account(*holderID)); + if (!sleHolder) return tecNO_DST; auto const sleMptIssuance = @@ -124,6 +125,12 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx) keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], *holderID))) return tecOBJECT_NOT_FOUND; + // Can't unauthorize the pseudo-accounts because they are implicitly + // always authorized. No need to amendment gate since Vault and LoanBroker + // can only be created if the Vault amendment is enabled. + if (isPseudoAccount(ctx.view, *holderID, {&sfVaultID, &sfLoanBrokerID})) + return tecNO_PERMISSION; + return tesSUCCESS; } From 77d5826297acca405957cac1a004bc52c0ea0dd3 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 7 Nov 2025 15:37:32 -0500 Subject: [PATCH 203/291] Add Grace Period to testLoanNextPaymentDueDateOverflow - Demonstrates that Grace Period can overflow. - Expected to fail. --- src/test/app/Loan_test.cpp | 91 +++++++++++++++++++++++++++++--------- 1 file changed, 71 insertions(+), 20 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 67bc49f076..0ff5e1fbef 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -5154,11 +5154,6 @@ protected: return createJson; }(); - auto const brokerStateBefore = - env.le(keylet::loanbroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - auto const baseFee = env.current()->fees().base; auto parentCloseTime = [&]() { @@ -5173,7 +5168,7 @@ protected: }; { - // straight-up overflow + // straight-up overflow: interval auto const interval = maxLoanTime() + 1; auto const total = 1; auto createJson = env.json( @@ -5185,7 +5180,7 @@ protected: env.close(); } { - // straight-up overflow + // straight-up overflow: total // min interval is 60 auto const interval = 60; auto const total = maxLoanTime() + 1; @@ -5197,6 +5192,24 @@ protected: ter(tecKILLED)); env.close(); } + { + // straight-up overflow: grace period + // min interval is 60 + auto const interval = maxLoanTime() + 1; + auto const total = 1; + auto const grace = interval; + auto createJson = env.json( + baseJson, + paymentInterval(interval), + paymentTotal(total), + gracePeriod(grace)); + + // The grace period can't be larger than the interval. + env(createJson, + sig(sfCounterpartySignature, lender), + ter(tecKILLED)); + env.close(); + } { // Overflow with multiplication of a few large intervals auto const interval = 1'000'000'000; @@ -5222,12 +5235,38 @@ protected: ter(tecKILLED)); env.close(); } + { + // Overflow with an absurdly large grace period + // min interval is 60 + auto const total = 60; + auto const interval = (maxLoanTime() - total) / total; + auto const grace = interval; + auto createJson = env.json( + baseJson, + paymentInterval(interval), + paymentTotal(total), + gracePeriod(grace)); + + env(createJson, + sig(sfCounterpartySignature, lender), + ter(tecKILLED)); + env.close(); + } { // Start date when the ledger is closed will be larger - auto const interval = maxLoanTime(); + auto const brokerStateBefore = + env.le(keylet::loanbroker(broker.brokerID)); + auto const loanSequence = brokerStateBefore->at(sfLoanSequence); + auto const keylet = keylet::loan(broker.brokerID, loanSequence); + + auto const grace = 100; + auto const interval = maxLoanTime() - grace; auto const total = 1; auto createJson = env.json( - baseJson, paymentInterval(interval), paymentTotal(total)); + baseJson, + paymentInterval(interval), + paymentTotal(total), + gracePeriod(grace)); env(createJson, sig(sfCounterpartySignature, lender), @@ -5248,11 +5287,20 @@ protected: } { // Start date when the ledger is closed will be larger + auto const brokerStateBefore = + env.le(keylet::loanbroker(broker.brokerID)); + auto const loanSequence = brokerStateBefore->at(sfLoanSequence); + auto const keylet = keylet::loan(broker.brokerID, loanSequence); + auto const closeStartDate = (parentCloseTime() / 10 + 1) * 10; - auto const interval = maxTime - closeStartDate; + auto const grace = 5'000; + auto const interval = maxTime - closeStartDate - grace; auto const total = 1; auto createJson = env.json( - baseJson, paymentInterval(interval), paymentTotal(total)); + baseJson, + paymentInterval(interval), + paymentTotal(total), + gracePeriod(grace)); env(createJson, sig(sfCounterpartySignature, lender), @@ -5268,7 +5316,7 @@ protected: // This loan exists auto const afterState = getCurrentState(env, broker, keylet); - BEAST_EXPECT(afterState.nextPaymentDate == maxTime); + BEAST_EXPECT(afterState.nextPaymentDate == maxTime - grace); BEAST_EXPECT(afterState.previousPaymentDate == 0); BEAST_EXPECT(afterState.paymentRemaining == 1); } @@ -5279,7 +5327,8 @@ protected: // Start date when the ledger is closed will be larger auto const closeStartDate = (parentCloseTime() / 10 + 1) * 10; - auto const maxLoanTime = maxTime - closeStartDate; + auto const grace = 5'000; + auto const maxLoanTime = maxTime - closeStartDate - grace; auto const total = [&]() { if (maxLoanTime % 5 == 0) return 5; @@ -5300,7 +5349,10 @@ protected: auto const interval = maxLoanTime / total; auto createJson = env.json( - baseJson, paymentInterval(interval), paymentTotal(total)); + baseJson, + paymentInterval(interval), + paymentTotal(total), + gracePeriod(grace)); env(createJson, sig(sfCounterpartySignature, lender), @@ -5327,8 +5379,9 @@ protected: // The loan is on the last payment auto const afterState = getCurrentState(env, broker, keylet); - BEAST_EXPECT(afterState.nextPaymentDate == maxTime); - BEAST_EXPECT(afterState.previousPaymentDate == maxTime - interval); + BEAST_EXPECT(afterState.nextPaymentDate == maxTime - grace); + BEAST_EXPECT( + afterState.previousPaymentDate == maxTime - grace - interval); BEAST_EXPECT(afterState.paymentRemaining == 1); } } @@ -6174,10 +6227,8 @@ protected: log << "loan after create: " << to_string(loan->getJson()) << std::endl; - env.close( - tp{ - d{loan->at(sfNextPaymentDueDate) + loan->at(sfGracePeriod) + - 1}}); + env.close(tp{d{ + loan->at(sfNextPaymentDueDate) + loan->at(sfGracePeriod) + 1}}); } topUpBorrower( From 972841ae29da9bd2838bf2bc3b96c725c68af08b Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 7 Nov 2025 15:40:50 -0500 Subject: [PATCH 204/291] Check for Grace Period overflow in LoanSet --- src/xrpld/app/tx/detail/LoanSet.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index d8366dc14a..1ef78b11b6 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -192,8 +192,8 @@ LoanSet::preclaim(PreclaimContext const& ctx) { // Check for numeric overflow of the schedule before we load any - // objects. NextPaymentDue date for the last payment is: - // startDate + (paymentInterval * paymentTotal). + // objects. The Grace Period for the last payment ends at: + // startDate + (paymentInterval * paymentTotal) + gracePeriod. // If that value is larger than "maxTime", the value // overflows, and we kill the transaction. using timeType = decltype(sfNextPaymentDueDate)::type::value_type; @@ -207,6 +207,13 @@ LoanSet::preclaim(PreclaimContext const& ctx) ctx.tx.at(~sfPaymentInterval).value_or(defaultPaymentInterval); auto const total = ctx.tx.at(~sfPaymentTotal).value_or(defaultPaymentTotal); + auto const grace = + ctx.tx.at(~sfGracePeriod).value_or(defaultGracePeriod); + + // The grace period can't be larger than the interval. Check it first, + // mostly so that unit tests can test that specific case. + if (grace > timeAvailable) + return tecKILLED; if (interval > timeAvailable) return tecKILLED; @@ -214,7 +221,9 @@ LoanSet::preclaim(PreclaimContext const& ctx) if (total > timeAvailable) return tecKILLED; - if (timeAvailable / interval < total) + auto const timeLastPayment = timeAvailable - grace; + + if (timeLastPayment / interval < total) return tecKILLED; } From 8e56af20ee9e9e35b50c66d9ab10e63ef841955d Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 7 Nov 2025 18:30:09 -0500 Subject: [PATCH 205/291] Add a distinction between a "valid" and a "representable" Number - "valid" means the value is <= Number::maxIntValue, which has been changed to maxMantissa / 100. A valid number could get bigger and be ok - such as when paying late interest on a loan. - "representable" means the value is <= Number::maxMantissa. An unrepresentable number WILL be rounded or truncated. - Adds a fourth level of enforcement: "compatible". It is used for converting XRP to Number (for AMM), and when doing explicit checks. - "weak" will now throw if the number is unrepresentable. --- include/xrpl/basics/Number.h | 31 ++++++--- include/xrpl/protocol/XRPAmount.h | 2 +- src/libxrpl/basics/Number.cpp | 32 ++++++++- src/libxrpl/ledger/View.cpp | 8 +++ src/libxrpl/protocol/STAmount.cpp | 3 +- src/test/app/Vault_test.cpp | 10 +-- src/test/basics/Number_test.cpp | 76 +++++++++++++++++++++- src/xrpld/app/tx/detail/InvariantCheck.cpp | 20 ++++++ src/xrpld/app/tx/detail/VaultClawback.cpp | 5 ++ src/xrpld/app/tx/detail/VaultCreate.cpp | 7 ++ src/xrpld/app/tx/detail/VaultDeposit.cpp | 2 + src/xrpld/app/tx/detail/VaultSet.cpp | 5 ++ src/xrpld/app/tx/detail/VaultWithdraw.cpp | 2 + 13 files changed, 184 insertions(+), 19 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index 4f447d5f98..3b124e3166 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -28,12 +28,17 @@ public: /** Describes whether and how to enforce this number as an integer. * * - none: No enforcement. The value may vary freely. This is the default. - * - weak: If the absolute value is greater than maxIntValue, valid() will - * return false. - * - strong: Assignment operations will throw if the absolute value is above - * maxIntValue. + * - compatible: If the absolute value is greater than maxIntValue, valid() + * will return false. Needed for backward compatibility with XRP used in + * AMMs, and available for functions that will do their own checking. This + * is the default for automatic conversions from XRPAmount to Number. + * - weak: Like compatible, plus, if the value is unrepresentable (larger + * than maxMantissa), assignment and other operations will throw. + * - strong: Like weak, plus, if the absolute value is invalid (larger than + * maxIntValue), assignment and other operations will throw. This is the + * defalut for automatic conversions from MPTAmount to Number. */ - enum EnforceInteger { none, weak, strong }; + enum EnforceInteger { none, compatible, weak, strong }; private: using rep = std::int64_t; @@ -42,8 +47,7 @@ private: // The enforcement setting is not serialized, and does not affect the // ledger. If not "none", the value is checked to be within the valid - // integer range. With "strong", the checks will be made as automatic as - // possible. + // integer range. See the enum description for more detail. EnforceInteger enforceInteger_ = none; public: @@ -53,8 +57,8 @@ public: constexpr static rep maxMantissa = minMantissa * 10 - 1; static_assert(maxMantissa == 9'999'999'999'999'999LL); - constexpr static rep maxIntValue = maxMantissa / 10; - static_assert(maxIntValue == 999'999'999'999'999LL); + constexpr static rep maxIntValue = maxMantissa / 100; + static_assert(maxIntValue == 99'999'999'999'999LL); // The range for the exponent when normalized constexpr static int minExponent = -32768; @@ -93,6 +97,15 @@ public: bool valid() const noexcept; + bool + representable() const noexcept; + /// Combines setIntegerEnforcement(EnforceInteger) and valid() + bool + valid(EnforceInteger enforce); + /// Because this function is const, it should only be used for one-off + /// checks + bool + valid(EnforceInteger enforce) const; constexpr Number operator+() const noexcept; diff --git a/include/xrpl/protocol/XRPAmount.h b/include/xrpl/protocol/XRPAmount.h index 159174accc..8f553a6083 100644 --- a/include/xrpl/protocol/XRPAmount.h +++ b/include/xrpl/protocol/XRPAmount.h @@ -143,7 +143,7 @@ public: operator Number() const noexcept { - return {drops(), Number::weak}; + return {drops(), Number::compatible}; } Number diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index e3789de90a..489e27c79f 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -160,6 +160,8 @@ Number::checkInteger(char const* what) const { if (enforceInteger_ == strong && !valid()) throw std::overflow_error(what); + if (enforceInteger_ == weak && !representable()) + throw std::overflow_error(what); } void @@ -217,7 +219,20 @@ Number::normalize() bool Number::valid() const noexcept { - if (enforceInteger_ != none) + return valid(enforceInteger_); +} + +bool +Number::valid(EnforceInteger enforce) +{ + setIntegerEnforcement(enforce); + return valid(); +} + +bool +Number::valid(EnforceInteger enforce) const +{ + if (enforce != none) { static Number const max = maxIntValue; static Number const maxNeg = -maxIntValue; @@ -229,6 +244,21 @@ Number::valid() const noexcept return true; } +bool +Number::representable() const noexcept +{ + if (enforceInteger_ != none) + { + static Number const max = maxMantissa; + static Number const maxNeg = -maxMantissa; + // Avoid making a copy + if (mantissa_ < 0) + return *this >= maxNeg; + return *this <= max; + } + return true; +} + Number& Number::operator+=(Number const& y) { diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index 92fd5ccc97..43fcae04f7 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -2878,6 +2878,8 @@ assetsToSharesDeposit( Number const assetTotal = vault->at(sfAssetsTotal); STAmount shares{vault->at(sfShareMPTID)}; + // STAmount will ignore enforcement for IOUs, so we can set it regardless of + // type. shares.setIntegerEnforcement(Number::weak); if (assetTotal == 0) return STAmount{ @@ -2910,6 +2912,8 @@ sharesToAssetsDeposit( Number const assetTotal = vault->at(sfAssetsTotal); STAmount assets{vault->at(sfAsset)}; + // STAmount will ignore enforcement for IOUs, so we can set it regardless of + // type. assets.setIntegerEnforcement(Number::weak); if (assetTotal == 0) return STAmount{ @@ -2944,6 +2948,8 @@ assetsToSharesWithdraw( Number assetTotal = vault->at(sfAssetsTotal); assetTotal -= vault->at(sfLossUnrealized); STAmount shares{vault->at(sfShareMPTID)}; + // STAmount will ignore enforcement for IOUs, so we can set it regardless of + // type. shares.setIntegerEnforcement(Number::weak); if (assetTotal == 0) return shares; @@ -2975,6 +2981,8 @@ sharesToAssetsWithdraw( Number assetTotal = vault->at(sfAssetsTotal); assetTotal -= vault->at(sfLossUnrealized); STAmount assets{vault->at(sfAsset)}; + // STAmount will ignore enforcement for IOUs, so we can set it regardless of + // type. assets.setIntegerEnforcement(Number::weak); if (assetTotal == 0) return assets; diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 6f6355a145..1df3d3c18b 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -270,7 +270,8 @@ STAmount::integerEnforcement() const noexcept bool STAmount::validNumber() const noexcept { - Number n = toNumber(Number::EnforceInteger::weak); + // compatible will not throw. + Number n = toNumber(Number::EnforceInteger::compatible); return n.valid(); } diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 5b36ffaf8e..4f81118147 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -3611,7 +3611,7 @@ class Vault_test : public beast::unit_test::suite } }); - testCase(14, [&, this](Env& env, Data d) { + testCase(13, [&, this](Env& env, Data d) { testcase("MPT scale deposit overflow on first deposit"); auto tx = d.vault.deposit( {.depositor = d.depositor, @@ -3621,7 +3621,7 @@ class Vault_test : public beast::unit_test::suite env.close(); }); - testCase(14, [&, this](Env& env, Data d) { + testCase(13, [&, this](Env& env, Data d) { testcase("MPT scale deposit overflow on second deposit"); { @@ -3643,7 +3643,7 @@ class Vault_test : public beast::unit_test::suite } }); - testCase(14, [&, this](Env& env, Data d) { + testCase(13, [&, this](Env& env, Data d) { testcase("No MPT scale deposit overflow on total shares"); { @@ -3963,7 +3963,7 @@ class Vault_test : public beast::unit_test::suite } }); - testCase(14, [&, this](Env& env, Data d) { + testCase(13, [&, this](Env& env, Data d) { testcase("MPT scale withdraw overflow"); { @@ -4204,7 +4204,7 @@ class Vault_test : public beast::unit_test::suite } }); - testCase(14, [&, this](Env& env, Data d) { + testCase(13, [&, this](Env& env, Data d) { testcase("MPT Scale clawback overflow"); { diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index a5fba8ef7d..1efafec65f 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -737,28 +737,87 @@ public: Number a{100}; BEAST_EXPECT(a.integerEnforcement() == Number::none); BEAST_EXPECT(a.valid()); + BEAST_EXPECT(a.representable()); a = Number{1, 30}; BEAST_EXPECT(a.valid()); + BEAST_EXPECT(a.representable()); a = -100; BEAST_EXPECT(a.valid()); + BEAST_EXPECT(a.representable()); + } + { + Number a{100, Number::compatible}; + BEAST_EXPECT(a.integerEnforcement() == Number::compatible); + BEAST_EXPECT(a.valid()); + BEAST_EXPECT(a.representable()); + a = Number{1, 15}; + BEAST_EXPECT(!a.valid()); + BEAST_EXPECT(a.representable()); + a = Number{1, 30, Number::none}; + BEAST_EXPECT(!a.valid()); + BEAST_EXPECT(!a.representable()); + a = -100; + BEAST_EXPECT(a.integerEnforcement() == Number::compatible); + BEAST_EXPECT(a.valid()); + BEAST_EXPECT(a.representable()); + a = Number{5, Number::weak}; + BEAST_EXPECT(a.integerEnforcement() == Number::weak); + BEAST_EXPECT(a.valid()); + BEAST_EXPECT(a.representable()); + a = Number{5, Number::strong}; + BEAST_EXPECT(a.integerEnforcement() == Number::strong); + BEAST_EXPECT(a.valid()); + BEAST_EXPECT(a.representable()); } { Number a{100, Number::weak}; BEAST_EXPECT(a.integerEnforcement() == Number::weak); BEAST_EXPECT(a.valid()); - a = Number{1, 30, Number::none}; + BEAST_EXPECT(a.representable()); + a = Number{1, 15}; BEAST_EXPECT(!a.valid()); + BEAST_EXPECT(a.representable()); + try + { + a = Number{1, 30, Number::compatible}; + BEAST_EXPECT(false); + } + catch (std::overflow_error const& e) + { + BEAST_EXPECT(e.what() == "Number::operator= integer overflow"s); + // The throw is done _after_ the number is updated. + BEAST_EXPECT((a == Number{1, 30})); + } + BEAST_EXPECT(a.integerEnforcement() == Number::weak); + BEAST_EXPECT(!a.valid()); + BEAST_EXPECT(!a.representable()); a = -100; BEAST_EXPECT(a.integerEnforcement() == Number::weak); BEAST_EXPECT(a.valid()); + BEAST_EXPECT(a.representable()); a = Number{5, Number::strong}; BEAST_EXPECT(a.integerEnforcement() == Number::strong); BEAST_EXPECT(a.valid()); + BEAST_EXPECT(a.representable()); } { Number a{100, Number::strong}; BEAST_EXPECT(a.integerEnforcement() == Number::strong); BEAST_EXPECT(a.valid()); + BEAST_EXPECT(a.representable()); + try + { + a = Number{1, 15, Number::compatible}; + BEAST_EXPECT(false); + } + catch (std::overflow_error const& e) + { + BEAST_EXPECT(e.what() == "Number::operator= integer overflow"s); + // The throw is done _after_ the number is updated. + BEAST_EXPECT((a == Number{1, 15})); + } + BEAST_EXPECT(!a.valid()); + BEAST_EXPECT(a.representable()); try { a = Number{1, 30}; @@ -771,15 +830,19 @@ public: BEAST_EXPECT((a == Number{1, 30})); } BEAST_EXPECT(!a.valid()); + BEAST_EXPECT(!a.representable()); a = -100; BEAST_EXPECT(a.integerEnforcement() == Number::strong); BEAST_EXPECT(a.valid()); + BEAST_EXPECT(a.representable()); } { - Number a{INITIAL_XRP.drops(), Number::weak}; + Number a{INITIAL_XRP.drops(), Number::compatible}; BEAST_EXPECT(!a.valid()); + BEAST_EXPECT(!a.representable()); a = -a; BEAST_EXPECT(!a.valid()); + BEAST_EXPECT(!a.representable()); try { @@ -795,6 +858,7 @@ public: // assigned to the Number BEAST_EXPECT(a == -INITIAL_XRP); BEAST_EXPECT(!a.valid()); + BEAST_EXPECT(!a.representable()); } try { @@ -808,6 +872,7 @@ public: // assigned to the Number BEAST_EXPECT(a == -INITIAL_XRP); BEAST_EXPECT(!a.valid()); + BEAST_EXPECT(!a.representable()); } a = Number::maxIntValue; try @@ -821,6 +886,7 @@ public: // This time, the throw is done _after_ the number is updated. BEAST_EXPECT(a == Number::maxIntValue + 1); BEAST_EXPECT(!a.valid()); + BEAST_EXPECT(a.representable()); } a = -Number::maxIntValue; try @@ -834,6 +900,7 @@ public: // This time, the throw is done _after_ the number is updated. BEAST_EXPECT(a == -Number::maxIntValue - 1); BEAST_EXPECT(!a.valid()); + BEAST_EXPECT(a.representable()); } a = Number(1, 10); try @@ -848,6 +915,7 @@ public: // The throw is done _after_ the number is updated. BEAST_EXPECT((a == Number{1, 20})); BEAST_EXPECT(!a.valid()); + BEAST_EXPECT(!a.representable()); } try { @@ -860,6 +928,7 @@ public: // The throw is done _after_ the number is updated. BEAST_EXPECT((a == Number::maxIntValue * 2)); BEAST_EXPECT(!a.valid()); + BEAST_EXPECT(a.representable()); } try { @@ -872,6 +941,7 @@ public: // The Number doesn't get updated because the ctor throws BEAST_EXPECT((a == Number::maxIntValue * 2)); BEAST_EXPECT(!a.valid()); + BEAST_EXPECT(a.representable()); } a = Number(1, 10); try @@ -885,10 +955,12 @@ public: // The throw is done _after_ the number is updated. BEAST_EXPECT((a == Number{1, 20})); BEAST_EXPECT(!a.valid()); + BEAST_EXPECT(!a.representable()); } a /= Number(1, 15); BEAST_EXPECT((a == Number{1, 5})); BEAST_EXPECT(a.valid()); + BEAST_EXPECT(a.representable()); } } diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index c15f2b64a5..0dbbd4f24a 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -2180,6 +2180,13 @@ ValidVault::Vault::make(SLE const& from) self.assetsAvailable = from.at(sfAssetsAvailable); self.assetsMaximum = from.at(sfAssetsMaximum); self.lossUnrealized = from.at(sfLossUnrealized); + if (self.asset.integral()) + { + self.assetsTotal.setIntegerEnforcement(Number::compatible); + self.assetsAvailable.setIntegerEnforcement(Number::compatible); + self.assetsMaximum.setIntegerEnforcement(Number::compatible); + self.lossUnrealized.setIntegerEnforcement(Number::compatible); + } return self; } @@ -2413,6 +2420,19 @@ ValidVault::finalize( beforeVault_.empty() || beforeVault_[0].key == afterVault.key, "ripple::ValidVault::finalize : single vault operation"); + if (!afterVault.assetsTotal.representable() || + !afterVault.assetsAvailable.representable() || + !afterVault.assetsMaximum.representable() || + !afterVault.lossUnrealized.representable()) + { + JLOG(j.fatal()) << "Invariant failed: vault overflowed maximum current " + "representable integer value"; + XRPL_ASSERT( + enforce, + "ripple::ValidVault::finalize : vault integer limit invariant"); + return !enforce; // That's all we can do here + } + auto const updatedShares = [&]() -> std::optional { // At this moment we only know that a vault is being updated and there // might be some MPTokenIssuance objects which are also updated in the diff --git a/src/xrpld/app/tx/detail/VaultClawback.cpp b/src/xrpld/app/tx/detail/VaultClawback.cpp index 1c73d36bb0..b464c1eb28 100644 --- a/src/xrpld/app/tx/detail/VaultClawback.cpp +++ b/src/xrpld/app/tx/detail/VaultClawback.cpp @@ -150,8 +150,11 @@ VaultClawback::doApply() amount.asset() == vaultAsset, "ripple::VaultClawback::doApply : matching asset"); + // Both of these values are going to be decreased in this transaction, + // so the limit doesn't really matter. auto assetsAvailable = vault->at(sfAssetsAvailable); auto assetsTotal = vault->at(sfAssetsTotal); + [[maybe_unused]] auto const lossUnrealized = vault->at(sfLossUnrealized); XRPL_ASSERT( lossUnrealized <= (assetsTotal - assetsAvailable), @@ -161,6 +164,8 @@ VaultClawback::doApply() MPTIssue const share{mptIssuanceID}; STAmount sharesDestroyed = {share}; STAmount assetsRecovered; + // STAmount will ignore enforcement for IOUs, so we can set it regardless of + // type. assetsRecovered.setIntegerEnforcement(Number::weak); sharesDestroyed.setIntegerEnforcement(Number::weak); try diff --git a/src/xrpld/app/tx/detail/VaultCreate.cpp b/src/xrpld/app/tx/detail/VaultCreate.cpp index 8acb40ad41..6c30739369 100644 --- a/src/xrpld/app/tx/detail/VaultCreate.cpp +++ b/src/xrpld/app/tx/detail/VaultCreate.cpp @@ -210,6 +210,13 @@ VaultCreate::doApply() vault->at(sfWithdrawalPolicy) = vaultStrategyFirstComeFirstServe; if (scale) vault->at(sfScale) = scale; + if (asset.integral()) + { + // Only the Maximum can be a non-zero value, so only it needs to be + // checked. + if (!vault->at(sfAssetsMaximum).value().valid(Number::compatible)) + return tecLIMIT_EXCEEDED; + } view().insert(vault); // Explicitly create MPToken for the vault owner diff --git a/src/xrpld/app/tx/detail/VaultDeposit.cpp b/src/xrpld/app/tx/detail/VaultDeposit.cpp index 66e144312f..609379d4c2 100644 --- a/src/xrpld/app/tx/detail/VaultDeposit.cpp +++ b/src/xrpld/app/tx/detail/VaultDeposit.cpp @@ -220,6 +220,8 @@ VaultDeposit::doApply() } STAmount sharesCreated = {vault->at(sfShareMPTID)}, assetsDeposited; + // STAmount will ignore enforcement for IOUs, so we can set it regardless of + // type. sharesCreated.setIntegerEnforcement(Number::weak); try { diff --git a/src/xrpld/app/tx/detail/VaultSet.cpp b/src/xrpld/app/tx/detail/VaultSet.cpp index 38ab6296ef..75f1689d7f 100644 --- a/src/xrpld/app/tx/detail/VaultSet.cpp +++ b/src/xrpld/app/tx/detail/VaultSet.cpp @@ -144,6 +144,11 @@ VaultSet::doApply() tx[sfAssetsMaximum] < *vault->at(sfAssetsTotal)) return tecLIMIT_EXCEEDED; vault->at(sfAssetsMaximum) = tx[sfAssetsMaximum]; + if (vault->at(sfAsset).value().integral()) + { + if (!vault->at(sfAssetsMaximum).value().valid(Number::compatible)) + return tecLIMIT_EXCEEDED; + } } if (auto const domainId = tx[~sfDomainID]; domainId) diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.cpp b/src/xrpld/app/tx/detail/VaultWithdraw.cpp index 8806f7b236..e28c54676c 100644 --- a/src/xrpld/app/tx/detail/VaultWithdraw.cpp +++ b/src/xrpld/app/tx/detail/VaultWithdraw.cpp @@ -222,6 +222,8 @@ VaultWithdraw::doApply() return tecINSUFFICIENT_FUNDS; } + // These values are only going to decrease, and can't be less than 0, so + // there's no need for integer range enforcement. auto assetsAvailable = vault->at(sfAssetsAvailable); auto assetsTotal = vault->at(sfAssetsTotal); [[maybe_unused]] auto const lossUnrealized = vault->at(sfLossUnrealized); From e3ea23cff5253ddac0b7392e2f828484e5001910 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sat, 8 Nov 2025 19:04:32 -0500 Subject: [PATCH 206/291] Add test case to reproduce RIPD-3459 - Improve a few loan test helper functions. - Make Loan.GracePeriod a default field. --- .../xrpl/protocol/detail/ledger_entries.macro | 2 +- src/test/app/Loan_test.cpp | 142 ++++++++++++++---- 2 files changed, 117 insertions(+), 27 deletions(-) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 3904d9f239..9e9a0c17a4 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -540,7 +540,7 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({ {sfOverpaymentInterestRate, soeDEFAULT}, {sfStartDate, soeREQUIRED}, {sfPaymentInterval, soeREQUIRED}, - {sfGracePeriod, soeREQUIRED}, + {sfGracePeriod, soeDEFAULT}, {sfPreviousPaymentDate, soeDEFAULT}, {sfNextPaymentDueDate, soeOPTIONAL}, // The loan object tracks these values: diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 0ff5e1fbef..a6f746ad20 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -615,6 +615,19 @@ protected: } case AssetType::MPT: { + // Enough to cover initial fees + if (!env.le(keylet::account(issuer))) + env.fund( + env.current()->fees().accountReserve(10) * 10, issuer); + if (!env.le(keylet::account(lender))) + env.fund( + env.current()->fees().accountReserve(10) * 10, + noripple(lender)); + if (!env.le(keylet::account(borrower))) + env.fund( + env.current()->fees().accountReserve(10) * 10, + noripple(borrower)); + MPTTester mptt{env, issuer, mptInitNoFund}; mptt.create( {.flags = @@ -644,7 +657,7 @@ protected: { using namespace jtx; - Env env(*this, beast::severities::kWarning); + Env env(*this); auto const asset = createAsset( env, @@ -653,15 +666,25 @@ protected: Account("issuer"), Account("lender"), Account("borrower")); + auto const principal = asset(loanParams.principalRequest).number(); + auto const interest = loanParams.interest.value_or(TenthBips32{}); + auto const interval = + loanParams.payInterval.value_or(LoanSet::defaultPaymentInterval); + auto const total = + loanParams.payTotal.value_or(LoanSet::defaultPaymentTotal); auto const props = computeLoanProperties( asset, - asset(loanParams.principalRequest).number(), - loanParams.interest.value_or(TenthBips32{}), - loanParams.payInterval.value_or(LoanSet::defaultPaymentInterval), - loanParams.payTotal.value_or(LoanSet::defaultPaymentTotal), + principal, + interest, + interval, + total, brokerParams.managementFeeRate, asset(brokerParams.vaultDeposit).number().exponent()); log << "Loan properties:\n" + << "\tPrincipal: " << principal << std::endl + << "\tInterest rate: " << interest << std::endl + << "\tPayment interval: " << interval << std::endl + << "\tTotal Payments: " << total << std::endl << "\tPeriodic Payment: " << props.periodicPayment << std::endl << "\tTotal Value: " << props.totalValueOutstanding << std::endl << "\tManagement Fee: " << props.managementFeeOwedToBroker @@ -680,8 +703,7 @@ protected: env.journal)); } - std::optional< - std::tuple> + std::optional> createLoan( jtx::Env& env, AssetType assetType, @@ -707,7 +729,7 @@ protected: env( pay((asset.native() ? env.master : issuer), lender, - asset(brokerParams.vaultDeposit))); + asset(brokerParams.vaultDeposit + brokerParams.coverDeposit))); // Fund the borrower later once we know the total loan // size @@ -746,10 +768,7 @@ protected: env.close(); - VerifyLoanStatus verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); - - return std::make_tuple( - broker, loanKeylet, verifyLoanStatus, pseudoAcct); + return std::make_tuple(broker, loanKeylet, pseudoAcct); } void @@ -919,10 +938,11 @@ protected: broker.params.managementFeeRate); BEAST_EXPECT( - paymentComponents.trackedValueDelta == roundedPeriodicPayment || + paymentComponents.trackedValueDelta <= roundedPeriodicPayment || (paymentComponents.specialCase == detail::PaymentSpecialCase::final && - paymentComponents.trackedValueDelta < roundedPeriodicPayment)); + paymentComponents.trackedValueDelta >= + roundedPeriodicPayment)); BEAST_EXPECT( paymentComponents.trackedValueDelta == paymentComponents.trackedPrincipalDelta + @@ -1092,7 +1112,9 @@ protected: auto broker = std::get(*loanResult); auto loanKeylet = std::get(*loanResult); - auto verifyLoanStatus = std::get(*loanResult); + auto pseudoAcct = std::get(*loanResult); + + VerifyLoanStatus verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); makeLoanPayments(env, broker, loanParams, loanKeylet, verifyLoanStatus); } @@ -6198,25 +6220,20 @@ protected: .payInterval = 150, .gracePd = 0}; - describeLoan(brokerParams, loanParams, AssetType::XRP); + auto const assetType = AssetType::XRP; + + describeLoan(brokerParams, loanParams, assetType); Env env(*this, all); auto loanResult = createLoan( - env, - AssetType::XRP, - brokerParams, - loanParams, - issuer, - lender, - borrower); + env, assetType, brokerParams, loanParams, issuer, lender, borrower); if (!BEAST_EXPECT(loanResult)) return; auto broker = std::get(*loanResult); auto loanKeylet = std::get(*loanResult); - // auto verifyLoanStatus = std::get(*loanResult); using tp = NetClock::time_point; using d = NetClock::duration; @@ -6264,12 +6281,82 @@ protected: env.close(); } + void + testRIPD3459() + { + testcase("RIPD-3459 - LoanBroker incorrect debt total"); + + using namespace jtx; + + Account const issuer("issuer"); + Account const lender("lender"); + Account const borrower("borrower"); + + BrokerParameters const brokerParams{ + .vaultDeposit = 200'000, + .debtMax = 0, + .coverRateMin = TenthBips32{0}, + // .managementFeeRate = TenthBips16{5919}, + .coverRateLiquidation = TenthBips32{0}}; + LoanParameters const loanParams{ + .account = lender, + .counter = borrower, + .principalRequest = Number{100'000, -4}, + .interest = TenthBips32{100'000}, + .payTotal = 10, + // Guess + // .payInterval = 10, + .gracePd = 0}; + + auto const assetType = AssetType::MPT; + + describeLoan(brokerParams, loanParams, assetType); + + Env env(*this, all); + + auto loanResult = createLoan( + env, assetType, brokerParams, loanParams, issuer, lender, borrower); + + if (!BEAST_EXPECT(loanResult)) + return; + + auto broker = std::get(*loanResult); + auto loanKeylet = std::get(*loanResult); + auto pseudoAcct = std::get(*loanResult); + + VerifyLoanStatus verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); + + if (auto const brokerSle = env.le(broker.brokerKeylet()); + BEAST_EXPECT(brokerSle)) + { + if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle)) + { + BEAST_EXPECT( + brokerSle->at(sfDebtTotal) == + loanSle->at(sfTotalValueOutstanding)); + } + } + + makeLoanPayments(env, broker, loanParams, loanKeylet, verifyLoanStatus); + + if (auto const brokerSle = env.le(broker.brokerKeylet()); + BEAST_EXPECT(brokerSle)) + { + if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle)) + { + log << pretty(brokerSle->getJson()) << std::endl + << pretty(loanSle->getJson()) << std::endl; + BEAST_EXPECT( + brokerSle->at(sfDebtTotal) == + loanSle->at(sfTotalValueOutstanding)); + } + } + } + public: void run() override { - testRIPD3831(); - #if LOANTODO testCoverDepositAllowsNonTransferableMPT(); testLoanPayLateFullPaymentBypassesPenalties(); @@ -6305,6 +6392,9 @@ public: testLoanNextPaymentDueDateOverflow(); testRequireAuth(); + + testRIPD3831(); + testRIPD3459(); } }; From e00fea9934e4ff6e50a5d51c20fff5e95bab90ef Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sat, 8 Nov 2025 22:47:50 -0500 Subject: [PATCH 207/291] Update the test case for RIPD-3459 with more detailed output data --- src/test/app/Loan_test.cpp | 184 +++++++++++++++++++++++++++---------- 1 file changed, 133 insertions(+), 51 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index a6f746ad20..357e233cea 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -651,14 +651,13 @@ protected: void describeLoan( + jtx::Env& env, BrokerParameters const& brokerParams, LoanParameters const& loanParams, AssetType assetType) { using namespace jtx; - Env env(*this); - auto const asset = createAsset( env, assetType, @@ -672,18 +671,20 @@ protected: loanParams.payInterval.value_or(LoanSet::defaultPaymentInterval); auto const total = loanParams.payTotal.value_or(LoanSet::defaultPaymentTotal); + auto const feeRate = brokerParams.managementFeeRate; auto const props = computeLoanProperties( asset, principal, interest, interval, total, - brokerParams.managementFeeRate, + feeRate, asset(brokerParams.vaultDeposit).number().exponent()); log << "Loan properties:\n" << "\tPrincipal: " << principal << std::endl << "\tInterest rate: " << interest << std::endl << "\tPayment interval: " << interval << std::endl + << "\tManagement Fee Rate: " << feeRate << std::endl << "\tTotal Payments: " << total << std::endl << "\tPeriodic Payment: " << props.periodicPayment << std::endl << "\tTotal Value: " << props.totalValueOutstanding << std::endl @@ -721,6 +722,8 @@ protected: issuer, noripple(lender, borrower)); + describeLoan(env, brokerParams, loanParams, assetType); + // Make the asset auto const asset = createAsset(env, assetType, brokerParams, issuer, lender, borrower); @@ -815,7 +818,8 @@ protected: BrokerInfo const& broker, LoanParameters const& loanParams, Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus) + VerifyLoanStatus const& verifyLoanStatus, + bool showStepBalances = false) { // Make all the individual payments using namespace jtx; @@ -859,11 +863,14 @@ protected: roundPeriodicPayment( broker.asset, state.periodicPayment, state.loanScale)}; - log << currencyLabel << " Payment components: " - << "Payments remaining, rawInterest, rawPrincipal, " - "rawMFee, trackedValueDelta, trackedPrincipalDelta, " - "trackedInterestDelta, trackedMgmtFeeDelta, special" - << std::endl; + if (!showStepBalances) + log << currencyLabel << " Payment components: " + << "Payments remaining, " + << "rawInterest, rawPrincipal, " + "rawMFee, " + << "trackedValueDelta, trackedPrincipalDelta, " + "trackedInterestDelta, trackedMgmtFeeDelta, special" + << std::endl; // Include the service fee STAmount const totalDue = roundToScale( @@ -881,13 +888,28 @@ protected: state.totalValue, state.principalOutstanding, state.managementFeeOutstanding); - log << currencyLabel - << " Loan starting state: " << state.paymentRemaining << ", " - << raw.interestDue << ", " << raw.principalOutstanding << ", " - << raw.managementFeeDue << ", " << rounded.valueOutstanding - << ", " << rounded.principalOutstanding << ", " - << rounded.interestDue << ", " << rounded.managementFeeDue - << std::endl; + + if (showStepBalances) + { + log << currencyLabel << " Starting loan balances: " + << "\n\tTotal value: " << rounded.valueOutstanding + << "\n\tPrincipal: " << rounded.principalOutstanding + << "\n\tInterest: " << rounded.interestDue + << "\n\tMgmt fee: " << rounded.managementFeeDue + << "\n\tPayments remaining " << state.paymentRemaining + << std::endl; + } + else + { + log << currencyLabel + << " Loan starting state: " << state.paymentRemaining + << ", " << raw.interestDue << ", " + << raw.principalOutstanding << ", " << raw.managementFeeDue + << ", " << rounded.valueOutstanding << ", " + << rounded.principalOutstanding << ", " + << rounded.interestDue << ", " << rounded.managementFeeDue + << std::endl; + } } // Try to pay a little extra to show that it's _not_ @@ -922,6 +944,11 @@ protected: env.balance(borrower, broker.asset).number() == borrowerInitialBalance - totalSpent); }; + + auto truncate = [](Number const& n, int places = 3) { + auto const factor = Number{1, places}; + return (n * factor).truncate() / factor; + }; while (state.paymentRemaining > 0) { validateBorrowerBalance(); @@ -967,22 +994,24 @@ protected: (deltas.valueDelta() - state.periodicPayment).exponent()) > 14); - log << currencyLabel - << " Payment components: " << state.paymentRemaining << ", " - << deltas.interestDueDelta << ", " << deltas.principalDelta - << ", " << deltas.managementFeeDueDelta << ", " - << paymentComponents.trackedValueDelta << ", " - << paymentComponents.trackedPrincipalDelta << ", " - << paymentComponents.trackedInterestPart() << ", " - << paymentComponents.trackedManagementFeeDelta << ", " - << (paymentComponents.specialCase == - detail::PaymentSpecialCase::final - ? "final" - : paymentComponents.specialCase == - detail::PaymentSpecialCase::extra - ? "extra" - : "none") - << std::endl; + if (!showStepBalances) + log << currencyLabel + << " Payment components: " << state.paymentRemaining << ", " + + << deltas.interestDueDelta << ", " << deltas.principalDelta + << ", " << deltas.managementFeeDueDelta << ", " + << paymentComponents.trackedValueDelta << ", " + << paymentComponents.trackedPrincipalDelta << ", " + << paymentComponents.trackedInterestPart() << ", " + << paymentComponents.trackedManagementFeeDelta << ", " + << (paymentComponents.specialCase == + detail::PaymentSpecialCase::final + ? "final" + : paymentComponents.specialCase == + detail::PaymentSpecialCase::extra + ? "extra" + : "none") + << std::endl; auto const totalDueAmount = STAmount{ broker.asset, paymentComponents.trackedValueDelta + serviceFee}; @@ -1037,6 +1066,34 @@ protected: totalDueAmount, adjustment); + if (showStepBalances) + { + auto const loanSle = env.le(loanKeylet); + if (!BEAST_EXPECT(loanSle)) + // No reason for this not to exist + return; + auto const current = calculateRoundedLoanState(loanSle); + auto const errors = nextTrueState - current; + log << currencyLabel << " Loan balances: " + << "\n\tAmount taken: " + << paymentComponents.trackedValueDelta + << "\n\tTotal value: " << current.valueOutstanding + << " (true: " << truncate(nextTrueState.valueOutstanding) + << ", error: " << truncate(errors.valueDelta()) + << ")\n\tPrincipal: " << current.principalOutstanding + << " (true: " + << truncate(nextTrueState.principalOutstanding) + << ", error: " << truncate(errors.principalDelta) + << ")\n\tInterest: " << current.interestDue + << " (true: " << truncate(nextTrueState.interestDue) + << ", error: " << truncate(errors.interestDueDelta) + << ")\n\tMgmt fee: " << current.managementFeeDue + << " (true: " << truncate(nextTrueState.managementFeeDue) + << ", error: " << truncate(errors.managementFeeDueDelta) + << ")\n\tPayments remaining " + << loanSle->at(sfPaymentRemaining) << std::endl; + } + --state.paymentRemaining; state.previousPaymentDate = state.nextPaymentDate; if (paymentComponents.specialCase == @@ -1083,12 +1140,43 @@ protected: initialState.managementFeeOutstanding); // This is almost a tautology given the previous checks, but // check it anyway for completeness. - BEAST_EXPECT( - totalInterestPaid == - initialState.totalValue - - (initialState.principalOutstanding + - initialState.managementFeeOutstanding)); + auto const initialInterestDue = initialState.totalValue - + (initialState.principalOutstanding + + initialState.managementFeeOutstanding); + BEAST_EXPECT(totalInterestPaid == initialInterestDue); BEAST_EXPECT(totalPaymentsMade == initialState.paymentRemaining); + + if (showStepBalances) + { + auto const loanSle = env.le(loanKeylet); + if (!BEAST_EXPECT(loanSle)) + // No reason for this not to exist + return; + log << currencyLabel << " Total amounts paid: " + << "\n\tTotal value: " << totalPaid.trackedValueDelta + << " (initial: " << truncate(initialState.totalValue) + << ", error: " + << truncate( + initialState.totalValue - totalPaid.trackedValueDelta) + << ")\n\tPrincipal: " << totalPaid.trackedPrincipalDelta + << " (initial: " << truncate(initialState.principalOutstanding) + << ", error: " + << truncate( + initialState.principalOutstanding - + totalPaid.trackedPrincipalDelta) + << ")\n\tInterest: " << totalInterestPaid + << " (initial: " << truncate(initialInterestDue) << ", error: " + << truncate(initialInterestDue - totalInterestPaid) + << ")\n\tMgmt fee: " << totalPaid.trackedManagementFeeDelta + << " (initial: " + << truncate(initialState.managementFeeOutstanding) + << ", error: " + << truncate( + initialState.managementFeeOutstanding - + totalPaid.trackedManagementFeeDelta) + << ")\n\tTotal payments made: " << totalPaymentsMade + << std::endl; + } } void @@ -6222,8 +6310,6 @@ protected: auto const assetType = AssetType::XRP; - describeLoan(brokerParams, loanParams, assetType); - Env env(*this, all); auto loanResult = createLoan( @@ -6241,8 +6327,8 @@ protected: auto state = getCurrentState(env, broker, loanKeylet); if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan)) { - log << "loan after create: " << to_string(loan->getJson()) - << std::endl; + // log << "loan after create: " << to_string(loan->getJson()) + // << std::endl; env.close(tp{d{ loan->at(sfNextPaymentDueDate) + loan->at(sfGracePeriod) + 1}}); @@ -6258,10 +6344,10 @@ protected: { auto const submitParam = to_string(jv); - log << "about to submit: " << submitParam << std::endl; + // log << "about to submit: " << submitParam << std::endl; auto const jr = env.rpc("submit", borrower.name(), submitParam); - log << jr << std::endl; + // log << jr << std::endl; BEAST_EXPECT(jr.isMember(jss::result)); auto const jResult = jr[jss::result]; // BEAST_EXPECT(jResult[jss::error] == "invalidTransaction"); @@ -6296,7 +6382,7 @@ protected: .vaultDeposit = 200'000, .debtMax = 0, .coverRateMin = TenthBips32{0}, - // .managementFeeRate = TenthBips16{5919}, + .managementFeeRate = TenthBips16{500}, .coverRateLiquidation = TenthBips32{0}}; LoanParameters const loanParams{ .account = lender, @@ -6310,8 +6396,6 @@ protected: auto const assetType = AssetType::MPT; - describeLoan(brokerParams, loanParams, assetType); - Env env(*this, all); auto loanResult = createLoan( @@ -6337,18 +6421,18 @@ protected: } } - makeLoanPayments(env, broker, loanParams, loanKeylet, verifyLoanStatus); + makeLoanPayments( + env, broker, loanParams, loanKeylet, verifyLoanStatus, true); if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle)) { if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle)) { - log << pretty(brokerSle->getJson()) << std::endl - << pretty(loanSle->getJson()) << std::endl; BEAST_EXPECT( brokerSle->at(sfDebtTotal) == loanSle->at(sfTotalValueOutstanding)); + BEAST_EXPECT(brokerSle->at(sfDebtTotal) == beast::zero); } } } @@ -6531,8 +6615,6 @@ class LoanArbitrary_test : public LoanBatch_test // .payTotal = 5816, .payInterval = 150}; - describeLoan(brokerParams, loanParams, AssetType::XRP); - runLoan(AssetType::XRP, brokerParams, loanParams); } }; From b5b31efe0ba37f82d4fb9e7c7f1ce51df8ce9c68 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sun, 9 Nov 2025 17:37:46 -0500 Subject: [PATCH 208/291] Fix RIPD-3901 - faulty assert - Assert requires that an overpayment reduces the value of a loan. If the overall loan interest is low enough, it could leave it unchanged. Update the assert to require that the overpayment does not increase the value of the loan. - Adds a unit test provided by @gregtatcam to demonstrate this issue. --- src/test/app/Loan_test.cpp | 62 ++++++++++++++++++++ src/xrpld/app/misc/detail/LendingHelpers.cpp | 4 +- src/xrpld/app/tx/detail/LoanPay.cpp | 2 +- 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 357e233cea..ea05c6486b 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -6437,6 +6437,67 @@ protected: } } + void + testRIPD3901() + { + testcase("Crash with tfLoanOverpayment"); + using namespace jtx; + using namespace loan; + Account const lender{"lender"}; + Account const issuer{"issuer"}; + Account const borrower{"borrower"}; + Account const depositor{"depositor"}; + auto const txfee = fee(XRP(100)); + + Env env(*this); + Vault vault(env); + + env.fund(XRP(10'000), lender, issuer, borrower, depositor); + env.close(); + + auto [tx, vaultKeyLet] = + vault.create({.owner = lender, .asset = xrpIssue()}); + env(tx, txfee); + env.close(); + + env(vault.deposit( + {.depositor = depositor, + .id = vaultKeyLet.key, + .amount = XRP(1'000)}), + txfee); + env.close(); + + auto const brokerKeyLet = + keylet::loanbroker(lender.id(), env.seq(lender)); + + env(loanBroker::set(lender, vaultKeyLet.key), txfee); + env.close(); + + // BrokerInfo brokerInfo{xrpIssue(), keylet, vaultKeyLet, {}}; + + STAmount const debtMaximumRequest = XRPAmount(200'000); + + env(set(borrower, brokerKeyLet.key, debtMaximumRequest), + sig(sfCounterpartySignature, lender), + interestRate(TenthBips32(50'000)), + paymentTotal(2), + paymentInterval(150), + txflags(tfLoanOverpayment), + txfee); + env.close(); + + std::uint32_t const loanSequence = 1; + auto const loanKeylet = keylet::loan(brokerKeyLet.key, loanSequence); + + if (auto loan = env.le(loanKeylet); env.test.BEAST_EXPECT(loan)) + { + env(loan::pay(borrower, loanKeylet.key, XRPAmount(150'001)), + txflags(tfLoanOverpayment), + txfee); + env.close(); + } + } + public: void run() override @@ -6479,6 +6540,7 @@ public: testRIPD3831(); testRIPD3459(); + testRIPD3901(); } }; diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index ad0e98b07e..8c349aafa2 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -663,9 +663,9 @@ tryOverpayment( auto const valueChange = newRounded.interestOutstanding() - rounded.interestOutstanding(); XRPL_ASSERT_PARTS( - valueChange < beast::zero, + valueChange <= beast::zero, "ripple::detail::tryOverpayment", - "principal overpayment reduced value of loan"); + "principal overpayment did not increase value of loan"); return LoanPaymentParts{ .principalPaid = diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 3edc109c80..4cad02d762 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -332,7 +332,7 @@ LoanPay::doApply() // It should not be possible to pay 0 total paymentParts->principalPaid + paymentParts->interestPaid > 0, "ripple::LoanPay::doApply", - "valid principal paid"); + "valid total paid"); XRPL_ASSERT_PARTS( paymentParts->feePaid >= 0, "ripple::LoanPay::doApply", From a9796d0210d24fcb27ffd1754ecc87179e4981e7 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sun, 9 Nov 2025 20:27:59 -0500 Subject: [PATCH 209/291] Minor test fixes - Don't deposit cover if the amount is 0. - Handle missing next payment date in getCurrentState. --- src/test/app/Loan_test.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index ea05c6486b..57da3e28e7 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -473,7 +473,8 @@ protected: coverRateMinimum(coverRateMinValue), coverRateLiquidation(TenthBips32(params.coverRateLiquidation))); - env(coverDeposit(lender, keylet.key, coverDepositValue)); + if (coverDepositValue != beast::zero) + env(coverDeposit(lender, keylet.key, coverDepositValue)); env.close(); @@ -496,7 +497,7 @@ protected: return LoanState{ .previousPaymentDate = loan->at(sfPreviousPaymentDate), .startDate = tp{d{loan->at(sfStartDate)}}, - .nextPaymentDate = loan->at(sfNextPaymentDueDate), + .nextPaymentDate = loan->at(~sfNextPaymentDueDate).value_or(0), .paymentRemaining = loan->at(sfPaymentRemaining), .loanScale = loan->at(sfLoanScale), .totalValue = loan->at(sfTotalValueOutstanding), From 0ae835b356ca3ef3c99b4cf6ffdaa3b007154d89 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 10 Nov 2025 13:05:51 -0500 Subject: [PATCH 210/291] doc: Expand explanation for LoanBrokerCoverClawback::determineBrokerID --- .../app/tx/detail/LoanBrokerCoverClawback.cpp | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp index 453431964e..f4a6f6ba20 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp @@ -57,26 +57,44 @@ LoanBrokerCoverClawback::preflight(PreflightContext const& ctx) Expected determineBrokerID(ReadView const& view, STTx const& tx) { + // If the broker ID was provided in the transaction, that's all we + // need. if (auto const brokerID = tx[~sfLoanBrokerID]) return *brokerID; + // If the broker ID was not provided, and the amount is either + // absent or holds a non-IOU - including MPT, something went wrong, + // because that should have been rejected in preflight(). auto const dstAmount = tx[~sfAmount]; if (!dstAmount || !dstAmount->holds()) return Unexpected{tecINTERNAL}; // LCOV_EXCL_LINE - // Since we don't have a LoanBrokerID, holder _should_ be the loan broker's - // pseudo-account, but we don't know yet whether it is, so use a generic - // placeholder name. - auto const holder = dstAmount->getIssuer(); - auto const sle = view.read(keylet::account(holder)); + // Every trust line is bidirectional. Both sides are simultaneously + // issuer and holder. For this transaction, the Account is acting as + // a holder, and clawing back funds from the LoanBroker + // Pseudo-account acting as holder. If the Amount is an IOU, and the + // `issuer` field specified in that Amount is a LoanBroker + // Pseudo-account, we can get the LoanBrokerID from there. + // + // Thus, Amount.issuer _should_ be the loan broker's + // pseudo-account, but we don't know yet whether it is. + auto const maybePseudo = dstAmount->getIssuer(); + auto const sle = view.read(keylet::account(maybePseudo)); + + // If the account was not found, the transaction can't go further. if (!sle) return Unexpected{tecNO_ENTRY}; + // If the account was found, and has a LoanBrokerID (and therefore + // is a pseudo-account), that's the + // answer we need. if (auto const brokerID = sle->at(~sfLoanBrokerID)) return *brokerID; - // Or tecWRONG_ASSET? + // If the account does not have a LoanBrokerID, the transaction + // can't go further, even if it's a different type of Pseudo-account. return Unexpected{tecOBJECT_NOT_FOUND}; + // Or tecWRONG_ASSET? } Expected From 9e25b4753dee30db2ba093b4d68c5bd3edd61d94 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 10 Nov 2025 15:32:32 -0500 Subject: [PATCH 211/291] fix: Check for empty or zero VaultID in LoanBrokerSet - Resolves RIPD-4067. --- src/test/app/LoanBroker_test.cpp | 17 +++++++++++++++++ src/xrpld/app/tx/detail/LoanBrokerSet.cpp | 6 ++++++ 2 files changed, 23 insertions(+) diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index ff70290331..aa5b5519b9 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -915,6 +915,17 @@ class LoanBroker_test : public beast::unit_test::suite // test env(jv, txflags(tfFullyCanonicalSig), ter(temINVALID)); }; + auto testZeroVaultID = [&](auto&& getTxJv) { + auto jv = getTxJv(); + // empty broker ID + jv[sfVaultID] = ""; + env(jv, ter(temINVALID)); + // zero broker ID + jv[sfVaultID] = to_string(uint256{}); + // needs a flag to distinguish the parsed STTx from the prior + // test + env(jv, txflags(tfFullyCanonicalSig), ter(temINVALID)); + }; if (brokerTest == CoverDeposit) { @@ -1058,6 +1069,12 @@ class LoanBroker_test : public beast::unit_test::suite set(alice, vaultInfo.vaultID), loanBrokerID(brokerKeylet.key)); }); + // preflight: temINVALID (empty/zero vault id) + testZeroVaultID([&]() { + return env.json( + set(alice, vaultInfo.vaultID), + loanBrokerID(brokerKeylet.key)); + }); if (asset.holds()) { diff --git a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp index c9843d6c6e..c5b6891cb8 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp @@ -40,6 +40,12 @@ LoanBrokerSet::preflight(PreflightContext const& ctx) return temINVALID; } + if (auto const vaultID = tx.at(~sfVaultID)) + { + if (*vaultID == beast::zero) + return temINVALID; + } + { auto const minimumZero = tx[~sfCoverRateMinimum].value_or(0) == 0; auto const liquidationZero = From 6ad4b29878d7390daf39619cf2f1b861ea5a7c26 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 10 Nov 2025 17:59:10 -0500 Subject: [PATCH 212/291] Always round cover minimum calculations up - Addresses RIPD-4016. - Add and update testRoundingAllowsUndercoverage() unit test from ticket. --- src/test/app/Loan_test.cpp | 91 +++++++++++++++++++ .../app/tx/detail/LoanBrokerCoverClawback.cpp | 12 ++- .../app/tx/detail/LoanBrokerCoverWithdraw.cpp | 16 +++- src/xrpld/app/tx/detail/LoanManage.cpp | 32 ++++--- src/xrpld/app/tx/detail/LoanPay.cpp | 16 ++-- src/xrpld/app/tx/detail/LoanSet.cpp | 13 ++- 6 files changed, 148 insertions(+), 32 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 57da3e28e7..78e44d0f13 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -86,6 +86,7 @@ protected: Number maxCoveredLoanValue(Number const& currentDebt) const { + NumberRoundModeGuard mg(Number::downward); auto debtLimit = coverDeposit * tenthBipsPerUnity.value() / coverRateMin.value(); @@ -2059,6 +2060,7 @@ protected: : std::max( broker.vaultScale(env), state.principalOutstanding.exponent()))); + NumberRoundModeGuard mg(Number::upward); auto const defaultAmount = roundToAsset( broker.asset, std::min( @@ -6499,6 +6501,94 @@ protected: } } + void + testRoundingAllowsUndercoverage() + { + testcase("Minimum cover rounding allows undercoverage (XRP)"); + + using namespace jtx; + using namespace loanBroker; + + Env env(*this, all); + + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + auto const asset = xrpIssue(); + + env.fund(XRP(200'000), lender, borrower); + env.close(); + + // Vault with XRP asset + Vault vault{env}; + auto [vaultCreate, vaultKeylet] = + vault.create({.owner = lender, .asset = xrpIssue()}); + env(vaultCreate); + env.close(); + BEAST_EXPECT(env.le(vaultKeylet)); + + // Seed the vault with XRP so it can fund the loan principal + PrettyAsset const xrpAsset{xrpIssue(), 1}; + + BrokerParameters const brokerParams{ + .vaultDeposit = 1'000, + .debtMax = Number{0}, + .coverRateMin = TenthBips32{10'000}, + .coverDeposit = 82, + }; + + auto const brokerInfo = + createVaultAndBroker(env, xrpAsset, lender, brokerParams); + // Create a loan with principal 804 XRP and 0% interest (so + // DebtTotal increases by exactly 804) + env(loan::set(borrower, brokerInfo.brokerID, xrpAsset(804).value()), + loan::interestRate(TenthBips32(0)), + sig(sfCounterpartySignature, lender), + fee(env.current()->fees().base * 2)); + BEAST_EXPECT(env.ter() == tesSUCCESS); + env.close(); + + // Verify DebtTotal is exactly 804 + if (auto const brokerSle = + env.le(keylet::loanbroker(brokerInfo.brokerID)); + BEAST_EXPECT(brokerSle)) + { + std::cout << *brokerSle << std::endl; + BEAST_EXPECT(brokerSle->at(sfDebtTotal) == Number(804)); + } + + // Attempt to withdraw 2 XRP to self, leaving 80 XRP CoverAvailable. + // The minimum is 80.4 XRP, which rounds up to 81 XRP, so this fails. + env(coverWithdraw(lender, brokerInfo.brokerID, xrpAsset(2).value()), + ter(tecINSUFFICIENT_FUNDS)); + BEAST_EXPECT(env.ter() == tecINSUFFICIENT_FUNDS); + env.close(); + + // Attempt to withdraw 1 XRP to self, leaving 81 XRP CoverAvailable. + // because that leaves sufficient cover, this succeeds + env(coverWithdraw(lender, brokerInfo.brokerID, xrpAsset(1).value())); + BEAST_EXPECT(env.ter() == tesSUCCESS); + env.close(); + + // Validate CoverAvailable == 80 XRP and DebtTotal remains 804 + if (auto const brokerSle = + env.le(keylet::loanbroker(brokerInfo.brokerID)); + BEAST_EXPECT(brokerSle)) + { + std::cout << *brokerSle << std::endl; + BEAST_EXPECT( + brokerSle->at(sfCoverAvailable) == xrpAsset(81).value()); + BEAST_EXPECT(brokerSle->at(sfDebtTotal) == Number(804)); + + // Also demonstrate that the true minimum (804 * 10%) exceeds 80 + auto const theoreticalMin = + tenthBipsOfValue(Number(804), TenthBips32(10'000)); + std::cout << "Theoretical min cover: " << theoreticalMin + << std::endl; + BEAST_EXPECT(Number(804, -1) == theoreticalMin); + } + } + public: void run() override @@ -6542,6 +6632,7 @@ public: testRIPD3831(); testRIPD3459(); testRIPD3901(); + testRoundingAllowsUndercoverage(); } }; diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp index f4a6f6ba20..f31520e42b 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp @@ -133,9 +133,15 @@ determineClawAmount( Asset const& vaultAsset, std::optional const& amount) { - auto const maxClawAmount = sleBroker[sfCoverAvailable] - - tenthBipsOfValue(sleBroker[sfDebtTotal], - TenthBips32(sleBroker[sfCoverRateMinimum])); + auto const maxClawAmount = [&]() { + // Always round the minimum required up + NumberRoundModeGuard mg1(Number::upward); + auto const minRequiredCover = tenthBipsOfValue( + sleBroker[sfDebtTotal], TenthBips32(sleBroker[sfCoverRateMinimum])); + // The subtraction probably won't round, but round down if it does. + NumberRoundModeGuard mg2(Number::downward); + return sleBroker[sfCoverAvailable] - minRequiredCover; + }(); if (maxClawAmount <= beast::zero) return Unexpected(tecINSUFFICIENT_FUNDS); diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp index 97d8c48b74..aebda09e0b 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp @@ -99,11 +99,17 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) auto const coverAvail = sleBroker->at(sfCoverAvailable); // Cover Rate is in 1/10 bips units auto const currentDebtTotal = sleBroker->at(sfDebtTotal); - auto const minimumCover = roundToAsset( - vaultAsset, - tenthBipsOfValue( - currentDebtTotal, TenthBips32(sleBroker->at(sfCoverRateMinimum))), - currentDebtTotal.exponent()); + auto const minimumCover = [&]() { + // Always round the minimum required up. + // Applies to `tenthBipsOfValue` as well as `roundToAsset`. + NumberRoundModeGuard mg(Number::upward); + return roundToAsset( + vaultAsset, + tenthBipsOfValue( + currentDebtTotal, + TenthBips32(sleBroker->at(sfCoverRateMinimum))), + currentDebtTotal.exponent()); + }(); if (coverAvail < amount) return tecINSUFFICIENT_FUNDS; if ((coverAvail - amount) < minimumCover) diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index 75d7b9552c..c45d317dea 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -148,20 +148,24 @@ LoanManage::defaultLoan( TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)}; TenthBips32 const coverRateLiquidation{ brokerSle->at(sfCoverRateLiquidation)}; - auto const defaultCovered = roundToAsset( - vaultAsset, - /* - * This formula is from the XLS-66 spec, section 3.2.3.2 (State - * Changes), specifically "if the `tfLoanDefault` flag is set" / "Apply - * the First-Loss Capital to the Default Amount" - */ - std::min( - tenthBipsOfValue( - tenthBipsOfValue( - brokerDebtTotalProxy.value(), coverRateMinimum), - coverRateLiquidation), - totalDefaultAmount), - loanScale); + auto const defaultCovered = [&]() { + // Always round the minimum required up. + NumberRoundModeGuard mg(Number::upward); + auto const minimumCover = + tenthBipsOfValue(brokerDebtTotalProxy.value(), coverRateMinimum); + // Round the liquidation amount up, too + return roundToAsset( + vaultAsset, + /* + * This formula is from the XLS-66 spec, section 3.2.3.2 (State + * Changes), specifically "if the `tfLoanDefault` flag is set" / + * "Apply the First-Loss Capital to the Default Amount" + */ + std::min( + tenthBipsOfValue(minimumCover, coverRateLiquidation), + totalDefaultAmount), + loanScale); + }(); auto const vaultDefaultAmount = totalDefaultAmount - defaultCovered; diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 4cad02d762..fe19ed1c0d 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -253,12 +253,16 @@ LoanPay::doApply() // // Normally freeze status is checked in preflight, but we do it here to // avoid duplicating the check. It'll claim a fee either way. - bool const sendBrokerFeeToOwner = coverAvailableProxy >= - roundToAsset(asset, - tenthBipsOfValue( - debtTotalProxy.value(), coverRateMinimum), - loanScale) && - !isDeepFrozen(view, brokerOwner, asset); + bool const sendBrokerFeeToOwner = [&]() { + // Always round the minimum required up. + NumberRoundModeGuard mg(Number::upward); + return coverAvailableProxy >= + roundToAsset( + asset, + tenthBipsOfValue(debtTotalProxy.value(), coverRateMinimum), + loanScale) && + !isDeepFrozen(view, brokerOwner, asset); + }(); auto const brokerPayee = sendBrokerFeeToOwner ? brokerOwner : brokerPseudoAccount; diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 1ef78b11b6..c129673222 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -514,11 +514,16 @@ LoanSet::doApply() return tecLIMIT_EXCEEDED; } TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)}; - if (brokerSle->at(sfCoverAvailable) < - tenthBipsOfValue(newDebtTotal, coverRateMinimum)) { - JLOG(j_.warn()) << "Insufficient first-loss capital to cover the loan."; - return tecINSUFFICIENT_FUNDS; + // Always round the minimum required up. + NumberRoundModeGuard mg(Number::upward); + if (brokerSle->at(sfCoverAvailable) < + tenthBipsOfValue(newDebtTotal, coverRateMinimum)) + { + JLOG(j_.warn()) + << "Insufficient first-loss capital to cover the loan."; + return tecINSUFFICIENT_FUNDS; + } } adjustOwnerCount(view, borrowerSle, 1, j_); From 2f064893bb18df0379d547373768562f01498ae7 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 10 Nov 2025 19:50:15 -0500 Subject: [PATCH 213/291] test: Add a test case reproducing the issue in RIPD-3902 --- src/test/app/Loan_test.cpp | 64 +++++++++++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 4 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 78e44d0f13..1778e60b37 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -610,8 +610,11 @@ protected: case AssetType::IOU: { PrettyAsset const asset{issuer[iouCurrency]}; - env(trust(lender, asset(brokerParams.vaultDeposit))); - env(trust(borrower, asset(brokerParams.vaultDeposit))); + auto const limit = asset( + 100 * + (brokerParams.vaultDeposit + brokerParams.coverDeposit)); + env(trust(lender, limit)); + env(trust(borrower, limit)); return asset; } @@ -947,8 +950,14 @@ protected: borrowerInitialBalance - totalSpent); }; - auto truncate = [](Number const& n, int places = 3) { - auto const factor = Number{1, places}; + auto const defaultRound = broker.asset.integral() ? 3 : 0; + auto truncate = [defaultRound]( + Number const& n, + std::optional places = std::nullopt) { + auto const p = places.value_or(defaultRound); + if (p == 0) + return n; + auto const factor = Number{1, p}; return (n * factor).truncate() / factor; }; while (state.paymentRemaining > 0) @@ -6589,6 +6598,52 @@ protected: } } + void + testRIPD3902() + { + testcase("RIPD-3902 - 1 IOU loan payments"); + + using namespace jtx; + + Account const issuer("issuer"); + Account const lender("lender"); + Account const borrower("borrower"); + + BrokerParameters const brokerParams{ + .vaultDeposit = 10, + .debtMax = 0, + .coverRateMin = TenthBips32{0}, + .managementFeeRate = TenthBips16{0}, + .coverRateLiquidation = TenthBips32{0}}; + LoanParameters const loanParams{ + .account = lender, + .counter = borrower, + .principalRequest = Number{1, 0}, + .interest = TenthBips32{100'000}, + .payTotal = 5, + .payInterval = 150, + .gracePd = 60}; + + auto const assetType = AssetType::IOU; + + Env env(*this, all); + + auto loanResult = createLoan( + env, assetType, brokerParams, loanParams, issuer, lender, borrower); + + if (!BEAST_EXPECT(loanResult)) + return; + + auto broker = std::get(*loanResult); + auto loanKeylet = std::get(*loanResult); + auto pseudoAcct = std::get(*loanResult); + + VerifyLoanStatus verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); + + makeLoanPayments( + env, broker, loanParams, loanKeylet, verifyLoanStatus, true); + } + public: void run() override @@ -6632,6 +6687,7 @@ public: testRIPD3831(); testRIPD3459(); testRIPD3901(); + testRIPD3902(); testRoundingAllowsUndercoverage(); } }; From 7712cbdfcc1b1bfc7ebe11373a20b08fd30e3c84 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 10 Nov 2025 21:02:01 -0500 Subject: [PATCH 214/291] Fix build issue - unused variable --- src/test/app/Loan_test.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 1778e60b37..ef7a47c3ee 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -6523,8 +6523,6 @@ protected: Account const lender{"lender"}; Account const borrower{"borrower"}; - auto const asset = xrpIssue(); - env.fund(XRP(200'000), lender, borrower); env.close(); From 1c99243ec2abcf63c14b97067cedea64e54beb6d Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 10 Nov 2025 21:10:13 -0500 Subject: [PATCH 215/291] Fix service fee accounting when a borrower is the broker (#6016) - Add unit-test to verify the fix. --- src/libxrpl/ledger/View.cpp | 9 +-- src/test/app/Loan_test.cpp | 126 ++++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 6 deletions(-) diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index deaf53c05b..84e3073465 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -2376,8 +2376,6 @@ accountSendMultiIOU( auto const& receiverID = r.first; STAmount amount{asset, r.second}; - takeFromSender += amount; - if (view.rules().enabled(fixAMMv1_1)) { if (amount < beast::zero) @@ -2423,6 +2421,9 @@ accountSendMultiIOU( view.creditHook(xrpAccount(), receiverID, amount, -rcvBal); view.update(receiver); + + // Take what is actually sent + takeFromSender += amount; } if (auto stream = j.trace()) @@ -2626,10 +2627,6 @@ rippleSendMultiMPT( auto const& receiverID = r.first; STAmount amount{asset, r.second}; - XRPL_ASSERT( - senderID != receiverID, - "ripple::rippleSendMultiMPT : sender is not receiver"); - XRPL_ASSERT( amount >= beast::zero, "ripple::rippleSendMultiMPT : minimum amount "); diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index ef7a47c3ee..f425778a7e 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -6642,6 +6642,131 @@ protected: env, broker, loanParams, loanKeylet, verifyLoanStatus, true); } + void + testBorrowerIsBroker() + { + testcase("Test Borrower is Broker"); + using namespace jtx; + using namespace loan; + Account const broker{"broker"}; + Account const issuer{"issuer"}; + Account const borrower_{"borrower"}; + Account const depositor{"depositor"}; + + auto testLoanAsset = [&](auto&& getMaxDebt, auto const& borrower) { + Env env(*this); + Vault vault(env); + + if (borrower == broker) + env.fund(XRP(10'000), broker, issuer, depositor); + else + env.fund(XRP(10'000), broker, borrower, issuer, depositor); + env.close(); + + auto const xrpFee = XRP(100); + auto const txFee = fee(xrpFee); + + STAmount const debtMaximumRequest = getMaxDebt(env); + + auto const& asset = debtMaximumRequest.asset(); + auto const initialVault = asset(debtMaximumRequest * 100); + + auto [tx, vaultKeylet] = + vault.create({.owner = broker, .asset = asset}); + env(tx, txFee); + env.close(); + + env(vault.deposit( + {.depositor = depositor, + .id = vaultKeylet.key, + .amount = initialVault}), + txFee); + env.close(); + + auto const brokerKeylet = + keylet::loanbroker(broker.id(), env.seq(broker)); + + env(loanBroker::set(broker, vaultKeylet.key), txFee); + env.close(); + + auto const serviceFee = 101; + + env(set(broker, brokerKeylet.key, debtMaximumRequest), + counterparty(borrower), + sig(sfCounterpartySignature, borrower), + loanServiceFee(serviceFee), + paymentTotal(10), + txFee); + env.close(); + + std::uint32_t const loanSequence = 1; + auto const loanKeylet = + keylet::loan(brokerKeylet.key, loanSequence); + + auto const brokerBalanceBefore = env.balance(broker, asset); + + if (auto const loanSle = env.le(loanKeylet); + env.test.BEAST_EXPECT(loanSle)) + { + auto const payment = loanSle->at(sfPeriodicPayment); + auto const totalPayment = payment + serviceFee; + env(loan::pay(borrower, loanKeylet.key, asset(totalPayment)), + txFee); + env.close(); + if (auto const vaultSle = env.le(vaultKeylet); + BEAST_EXPECT(vaultSle)) + { + auto const expected = [&]() { + // The service fee is transferred to the broker if + // a borrower is not the broker + if (borrower != broker) + return brokerBalanceBefore.number() + serviceFee; + // Since a borrower is the broker, the payment is + // transferred to the Vault from the broker but not + // the service fee. + // If the asset is XRP then the broker pays the txfee. + if (asset.native()) + return brokerBalanceBefore.number() - payment - + xrpFee.number(); + return brokerBalanceBefore.number() - payment; + }(); + BEAST_EXPECT( + env.balance(broker, asset).value() == + asset(expected).value()); + } + } + }; + // Test when a borrower is the broker and is not to verify correct + // service fee transfer in both cases. + for (auto const& borrowerAcct : {broker, borrower_}) + { + testLoanAsset( + [&](Env&) -> STAmount { return STAmount{XRPAmount{200'000}}; }, + borrowerAcct); + testLoanAsset( + [&](Env& env) -> STAmount { + auto const IOU = issuer["USD"]; + env(trust(broker, IOU(1'000'000'000))); + env(trust(depositor, IOU(1'000'000'000))); + env(pay(issuer, broker, IOU(100'000'000))); + env(pay(issuer, depositor, IOU(100'000'000))); + env.close(); + return IOU(200'000); + }, + borrowerAcct); + testLoanAsset( + [&](Env& env) -> STAmount { + MPTTester mpt( + {.env = env, + .issuer = issuer, + .holders = {broker, depositor}, + .pay = 100'000'000}); + return mpt(200'000); + }, + borrowerAcct); + } + } + public: void run() override @@ -6687,6 +6812,7 @@ public: testRIPD3901(); testRIPD3902(); testRoundingAllowsUndercoverage(); + testBorrowerIsBroker(); } }; From 8d22409ab507542fa4e23e9060c31d825b8ccb2c Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 10 Nov 2025 20:25:57 -0500 Subject: [PATCH 216/291] review feedback: Use the specific type in the "SendMulti" functions --- src/libxrpl/ledger/View.cpp | 72 +++++++++++++------------------------ 1 file changed, 24 insertions(+), 48 deletions(-) diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index 84e3073465..0ddea91c5d 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -2113,26 +2113,26 @@ static TER rippleSendMultiIOU( ApplyView& view, AccountID const& senderID, - Asset const& asset, + Issue const& issue, MultiplePaymentDestinations const& receivers, STAmount& actual, beast::Journal j, WaiveTransferFee waiveFee) { - auto const issuer = asset.getIssuer(); + auto const issuer = issue.getIssuer(); XRPL_ASSERT( !isXRP(senderID), "ripple::rippleSendMultiIOU : sender is not XRP"); // These may diverge - STAmount takeFromSender{asset}; + STAmount takeFromSender{issue}; actual = takeFromSender; // Failures return immediately. for (auto const& r : receivers) { auto const& receiverID = r.first; - STAmount amount{asset, r.second}; + STAmount amount{issue, r.second}; /* If we aren't sending anything or if the sender is the same as the * receiver then we don't need to do anything. @@ -2315,7 +2315,7 @@ static TER accountSendMultiIOU( ApplyView& view, AccountID const& senderID, - Asset const& asset, + Issue const& issue, MultiplePaymentDestinations const& receivers, beast::Journal j, WaiveTransferFee waiveFee) @@ -2325,27 +2325,14 @@ accountSendMultiIOU( "ripple::accountSendMultiIOU", "multiple recipients provided"); - if (view.rules().enabled(fixAMMv1_1)) - { - if (asset.holds()) - { - return tecINTERNAL; - } - } - else - { - XRPL_ASSERT( - !asset.holds(), "ripple::accountSendMultiIOU : not MPT"); - } - - if (!asset.native()) + if (!issue.native()) { STAmount actual; JLOG(j.trace()) << "accountSendMultiIOU: " << to_string(senderID) << " sending " << receivers.size() << " IOUs"; return rippleSendMultiIOU( - view, senderID, asset, receivers, actual, j, waiveFee); + view, senderID, issue, receivers, actual, j, waiveFee); } /* XRP send which does not check reserve and can do pure adjustment. @@ -2370,24 +2357,15 @@ accountSendMultiIOU( } // Failures return immediately. - STAmount takeFromSender{asset}; + STAmount takeFromSender{issue}; for (auto const& r : receivers) { auto const& receiverID = r.first; - STAmount amount{asset, r.second}; + STAmount amount{issue, r.second}; - if (view.rules().enabled(fixAMMv1_1)) + if (amount < beast::zero) { - if (amount < beast::zero) - { - return tecINTERNAL; - } - } - else - { - XRPL_ASSERT( - amount >= beast::zero, - "ripple::accountSendMultiIOU : minimum amount"); + return tecINTERNAL; // LCOV_EXCL_LINE } /* If we aren't sending anything or if the sender is the same as the @@ -2603,7 +2581,7 @@ static TER rippleSendMultiMPT( ApplyView& view, AccountID const& senderID, - Asset const& asset, + MPTIssue const& mptIssue, MultiplePaymentDestinations const& receivers, STAmount& actual, beast::Journal j, @@ -2611,25 +2589,25 @@ rippleSendMultiMPT( { // Safe to get MPT since rippleSendMultiMPT is only called by // accountSendMultiMPT - auto const issuer = asset.getIssuer(); + auto const issuer = mptIssue.getIssuer(); - auto const sle = - view.read(keylet::mptIssuance(asset.get().getMptID())); + auto const sle = view.read(keylet::mptIssuance(mptIssue.getMptID())); if (!sle) return tecOBJECT_NOT_FOUND; // These may diverge - STAmount takeFromSender{asset}; + STAmount takeFromSender{mptIssue}; actual = takeFromSender; for (auto const& r : receivers) { auto const& receiverID = r.first; - STAmount amount{asset, r.second}; + STAmount amount{mptIssue, r.second}; - XRPL_ASSERT( - amount >= beast::zero, - "ripple::rippleSendMultiMPT : minimum amount "); + if (amount < beast::zero) + { + return tecINTERNAL; // LCOV_EXCL_LINE + } /* If we aren't sending anything or if the sender is the same as the * receiver then we don't need to do anything. @@ -2720,17 +2698,15 @@ static TER accountSendMultiMPT( ApplyView& view, AccountID const& senderID, - Asset const& asset, + MPTIssue const& mptIssue, MultiplePaymentDestinations const& receivers, beast::Journal j, WaiveTransferFee waiveFee) { - XRPL_ASSERT(asset.holds(), "ripple::accountSendMultiMPT : MPT"); - STAmount actual; return rippleSendMultiMPT( - view, senderID, asset, receivers, actual, j, waiveFee); + view, senderID, mptIssue, receivers, actual, j, waiveFee); } TER @@ -2771,10 +2747,10 @@ accountSendMulti( [&](TIss const& issue) { if constexpr (std::is_same_v) return accountSendMultiIOU( - view, senderID, asset, receivers, j, waiveFee); + view, senderID, issue, receivers, j, waiveFee); else return accountSendMultiMPT( - view, senderID, asset, receivers, j, waiveFee); + view, senderID, issue, receivers, j, waiveFee); }, asset.value()); } From 4396b77c4b9d6587233f10d18b918a635f00ff2d Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 10 Nov 2025 23:17:42 -0500 Subject: [PATCH 217/291] Add tfLoanLatePayment flag; full payment is no longer a special case - A regular payment that is late, or a tfLoanLatePayment that is not late will fail. - Flags are mutually exclusive. - Add a few interest computation shortcuts and overflow prevention checks that return 0 if there's no time to compute for. --- include/xrpl/protocol/TxFlags.h | 3 +- src/test/app/Loan_test.cpp | 34 +- src/xrpld/app/misc/LendingHelpers.h | 27 +- src/xrpld/app/misc/detail/LendingHelpers.cpp | 386 +++++++++---------- src/xrpld/app/tx/detail/LoanPay.cpp | 54 +-- 5 files changed, 260 insertions(+), 244 deletions(-) diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index d3a2fecf45..0f3ea94b21 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -274,10 +274,11 @@ constexpr std::uint32_t const tfLoanOverpayment = 0x00010000; // LoanPay exclusive flags: // tfLoanFullPayment: True, indicates that the payment is constexpr std::uint32_t const tfLoanFullPayment = 0x00020000; +constexpr std::uint32_t const tfLoanLatePayment = 0x00040000; constexpr std::uint32_t const tfLoanSetMask = ~(tfUniversal | tfLoanOverpayment); constexpr std::uint32_t const tfLoanPayMask = ~(tfUniversal | - tfLoanOverpayment | tfLoanFullPayment); + tfLoanOverpayment | tfLoanFullPayment | tfLoanLatePayment); // LoanManage flags: constexpr std::uint32_t const tfLoanDefault = 0x00010000; diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index f425778a7e..536ac31eda 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -1059,8 +1059,7 @@ protected: // Make the payment env(pay(borrower, loanKeylet.key, transactionAmount)); - env.close( - d{(state.previousPaymentDate + state.nextPaymentDate) / 2}); + env.close(d{state.paymentInterval / 2}); // Need to account for fees if the loan is in XRP PrettyAmount adjustment = broker.asset(0); @@ -2227,14 +2226,29 @@ protected: (Number{15, -1} / loanPaymentsPerFeeIncrement + 1)}), ter(temINVALID_FLAG)); } - // Try to send a payment marked as both full payment and - // overpayment. Do not include `txFlags`, so we don't duplicate the - // prior test transaction. + // Try to send a payment marked as multiple mutually exclusive + // payment types. Do not include `txFlags`, so we don't duplicate + // the prior test transaction. + env(pay(borrower, + loanKeylet.key, + broker.asset(state.periodicPayment * 2), + tfLoanLatePayment | tfLoanFullPayment), + ter(temINVALID_FLAG)); + env(pay(borrower, + loanKeylet.key, + broker.asset(state.periodicPayment * 2), + tfLoanLatePayment | tfLoanOverpayment), + ter(temINVALID_FLAG)); env(pay(borrower, loanKeylet.key, broker.asset(state.periodicPayment * 2), tfLoanOverpayment | tfLoanFullPayment), ter(temINVALID_FLAG)); + env(pay(borrower, + loanKeylet.key, + broker.asset(state.periodicPayment * 2), + tfLoanLatePayment | tfLoanOverpayment | tfLoanFullPayment), + ter(temINVALID_FLAG)); { auto const otherAsset = broker.asset.raw() == assets[0].raw() @@ -4562,7 +4576,11 @@ protected: issuer, lender["IOU"](1'000), tfClearFreeze | tfClearDeepFreeze)); env.close(); - env(pay(borrower, loanKeylet.key, debtMaximumRequest)); + // The payment is late by this point + env(pay(borrower, loanKeylet.key, debtMaximumRequest), ter(tecEXPIRED)); + env.close(); + env(pay( + borrower, loanKeylet.key, debtMaximumRequest, tfLoanLatePayment)); env.close(); // preclaim: tecKILLED @@ -6777,11 +6795,10 @@ public: testPoC_UnsignedUnderflowOnFullPayAfterEarlyPeriodic(); testLoanCoverMinimumRoundingExploit(); #endif - testDustManipulation(); - testIssuerLoan(); testDisabled(); testSelfLoan(); + testIssuerLoan(); testLoanSet(); testLifecycle(); testServiceFeeOnBrokerDeepFreeze(); @@ -6806,6 +6823,7 @@ public: testLoanNextPaymentDueDateOverflow(); testRequireAuth(); + testDustManipulation(); testRIPD3831(); testRIPD3459(); diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index d9e27414fa..c32b578d0b 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -30,21 +30,24 @@ roundPeriodicPayment( struct LoanPaymentParts { /// principal_paid is the amount of principal that the payment covered. - Number principalPaid; + Number principalPaid = numZero; /// interest_paid is the amount of interest that the payment covered. - Number interestPaid; + Number interestPaid = numZero; /** * value_change is the amount by which the total value of the Loan changed. * If value_change < 0, Loan value decreased. * If value_change > 0, Loan value increased. * This is 0 for regular payments. */ - Number valueChange; + Number valueChange = numZero; /// feePaid is amount of fee that is paid to the broker - Number feePaid; + Number feePaid = numZero; LoanPaymentParts& operator+=(LoanPaymentParts const& other); + + bool + operator==(LoanPaymentParts const& other) const; }; /** This structure describes the initial "computed" properties of a loan. @@ -240,15 +243,11 @@ computeLoanProperties( bool isRounded(Asset const& asset, Number const& value, std::int32_t scale); -Expected -loanMakeFullPayment( - Asset const& asset, - ApplyView& view, - SLE::ref loan, - SLE::const_ref brokerSle, - STAmount const& amount, - bool const overpaymentAllowed, - beast::Journal j); +// Indicates what type of payment is being made. +// regular, late, and full are mutually exclusive. +// overpayment is an "add on" to a regular payment, and follows that path with +// potential extra work at the end. +enum class LoanPaymentType { regular = 0, late, full, overpayment }; Expected loanMakePayment( @@ -257,7 +256,7 @@ loanMakePayment( SLE::ref loan, SLE::const_ref brokerSle, STAmount const& amount, - bool const overpaymentAllowed, + LoanPaymentType const paymentType, beast::Journal j); } // namespace ripple diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index 8c349aafa2..b980e73353 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -35,6 +35,14 @@ LoanPaymentParts::operator+=(LoanPaymentParts const& other) return *this; } +bool +LoanPaymentParts::operator==(LoanPaymentParts const& other) const +{ + return principalPaid == other.principalPaid && + interestPaid == other.interestPaid && + valueChange == other.valueChange && feePaid == other.feePaid; +} + Number loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval) { @@ -170,6 +178,12 @@ loanLatePaymentInterest( * * The spec is to be updated to base the duration on the next due date */ + + // If the payment is not late by any amount of time, then there's no late + // interest + if (parentCloseTime.time_since_epoch().count() <= nextPaymentDueDate) + return 0; + auto const secondsOverdue = parentCloseTime.time_since_epoch().count() - nextPaymentDueDate; @@ -193,6 +207,11 @@ loanAccruedInterest( */ auto const lastPaymentDate = std::max(prevPaymentDate, startDate); + // If the loan has been paid ahead, then "lastPaymentDate" is in the future, + // and no interest has accrued. + if (parentCloseTime.time_since_epoch().count() <= lastPaymentDate) + return 0; + auto const secondsSinceLastPayment = parentCloseTime.time_since_epoch().count() - lastPaymentDate; @@ -818,7 +837,7 @@ computeLatePayment( beast::Journal j) { if (!hasExpired(view, nextDueDate)) - return Unexpected(tesSUCCESS); + return Unexpected(tecTOO_SOON); // the payment is late // Late payment interest is only the part of the interest that comes @@ -972,9 +991,19 @@ computeFullPayment( "ripple::detail::computeFullPayment", "total due is rounded"); + JLOG(j.trace()) << "computeFullPayment result: periodicPayment: " + << periodicPayment << ", periodicRate: " << periodicRate + << ", paymentRemaining: " << paymentRemaining + << ", rawPrincipalOutstanding: " << rawPrincipalOutstanding + << ", fullPaymentInterest: " << fullPaymentInterest + << ", roundedFullInterest: " << roundedFullInterest + << ", roundedFullManagementFee: " + << roundedFullManagementFee + << ", untrackedInterest: " << full.untrackedInterest; + if (amount < full.totalDue) // If the payment is less than the full payment amount, it's not - // sufficient to be a full payment, but that's not an error. + // sufficient to be a full payment. return Unexpected(tecINSUFFICIENT_PAYMENT); return full; @@ -1639,111 +1668,6 @@ computeLoanProperties( .firstPaymentPrincipal = firstPaymentPrincipal}; } -Expected -loanMakeFullPayment( - Asset const& asset, - ApplyView& view, - SLE::ref loan, - SLE::const_ref brokerSle, - STAmount const& amount, - bool const overpaymentAllowed, - beast::Journal j) -{ - auto principalOutstandingProxy = loan->at(sfPrincipalOutstanding); - auto paymentRemainingProxy = loan->at(sfPaymentRemaining); - - if (paymentRemainingProxy == 0 || principalOutstandingProxy == 0) - { - // Loan complete - JLOG(j.warn()) << "Loan is already paid off."; - return Unexpected(tecKILLED); - } - - auto totalValueOutstandingProxy = loan->at(sfTotalValueOutstanding); - auto managementFeeOutstandingProxy = loan->at(sfManagementFeeOutstanding); - - // Next payment due date must be set unless the loan is complete - auto nextDueDateProxy = loan->at(~sfNextPaymentDueDate); - if (!nextDueDateProxy) - { - JLOG(j.warn()) << "Loan next payment due date is not set."; - return Unexpected(tecINTERNAL); - } - - std::int32_t const loanScale = loan->at(sfLoanScale); - - TenthBips32 const interestRate{loan->at(sfInterestRate)}; - TenthBips32 const closeInterestRate{loan->at(sfCloseInterestRate)}; - - Number const closePaymentFee = - roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale); - TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; - - auto const periodicPayment = loan->at(sfPeriodicPayment); - - auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDate); - std::uint32_t const startDate = loan->at(sfStartDate); - - std::uint32_t const paymentInterval = loan->at(sfPaymentInterval); - // Compute the normal periodic rate, payment, etc. - // We'll need it in the remaining calculations - Number const periodicRate = loanPeriodicRate(interestRate, paymentInterval); - XRPL_ASSERT( - interestRate == 0 || periodicRate > 0, - "ripple::loanMakeFullPayment : valid rate"); - - XRPL_ASSERT( - *totalValueOutstandingProxy > 0, - "ripple::loanMakeFullPayment : valid total value"); - - view.update(loan); - - // ------------------------------------------------------------- - // full payment handling - LoanState const roundedLoanState = calculateRoundedLoanState( - totalValueOutstandingProxy, - principalOutstandingProxy, - managementFeeOutstandingProxy); - - if (auto const fullPaymentComponents = detail::computeFullPayment( - asset, - view, - principalOutstandingProxy, - managementFeeOutstandingProxy, - periodicPayment, - paymentRemainingProxy, - prevPaymentDateProxy, - startDate, - paymentInterval, - closeInterestRate, - loanScale, - roundedLoanState.interestDue, - periodicRate, - closePaymentFee, - amount, - managementFeeRate, - j)) - return doPayment( - *fullPaymentComponents, - totalValueOutstandingProxy, - principalOutstandingProxy, - managementFeeOutstandingProxy, - paymentRemainingProxy, - prevPaymentDateProxy, - nextDueDateProxy, - paymentInterval); - else if (fullPaymentComponents.error()) - // error() will be the TER returned if a payment is not made. It - // will only evaluate to true if it's unsuccessful. Otherwise, - // tesSUCCESS means nothing was done, so continue. - return Unexpected(fullPaymentComponents.error()); - - // LCOV_EXCL_START - UNREACHABLE("ripple::loanMakeFullPayment : invalid result"); - return Unexpected(tecINTERNAL); - // LCOV_EXCL_STOP -} - Expected loanMakePayment( Asset const& asset, @@ -1751,7 +1675,7 @@ loanMakePayment( SLE::ref loan, SLE::const_ref brokerSle, STAmount const& amount, - bool const overpaymentAllowed, + LoanPaymentType const paymentType, beast::Journal j) { /* @@ -1785,16 +1709,14 @@ loanMakePayment( std::int32_t const loanScale = loan->at(sfLoanScale); TenthBips32 const interestRate{loan->at(sfInterestRate)}; - TenthBips32 const lateInterestRate{loan->at(sfLateInterestRate)}; - TenthBips32 const closeInterestRate{loan->at(sfCloseInterestRate)}; Number const serviceFee = loan->at(sfLoanServiceFee); - Number const latePaymentFee = loan->at(sfLatePaymentFee); TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; Number const periodicPayment = loan->at(sfPeriodicPayment); auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDate); + std::uint32_t const startDate = loan->at(sfStartDate); std::uint32_t const paymentInterval = loan->at(sfPaymentInterval); // Compute the normal periodic rate, payment, etc. @@ -1810,7 +1732,7 @@ loanMakePayment( view.update(loan); - detail::PaymentComponentsPlus const periodic{ + detail::PaymentComponentsPlus periodic{ detail::computePaymentComponents( asset, loanScale, @@ -1829,21 +1751,141 @@ loanMakePayment( // ------------------------------------------------------------- // late payment handling - if (auto const latePaymentComponents = detail::computeLatePayment( - asset, - view, - principalOutstandingProxy, - *nextDueDateProxy, - periodic, - lateInterestRate, - loanScale, - latePaymentFee, - amount, - managementFeeRate, - j)) + if (paymentType == LoanPaymentType::late) { - return doPayment( - *latePaymentComponents, + TenthBips32 const lateInterestRate{loan->at(sfLateInterestRate)}; + Number const latePaymentFee = loan->at(sfLatePaymentFee); + + if (auto const latePaymentComponents = detail::computeLatePayment( + asset, + view, + principalOutstandingProxy, + *nextDueDateProxy, + periodic, + lateInterestRate, + loanScale, + latePaymentFee, + amount, + managementFeeRate, + j)) + { + return doPayment( + *latePaymentComponents, + totalValueOutstandingProxy, + principalOutstandingProxy, + managementFeeOutstandingProxy, + paymentRemainingProxy, + prevPaymentDateProxy, + nextDueDateProxy, + paymentInterval); + } + else if (latePaymentComponents.error()) + { + // error() will be the TER returned if a payment is not made. It + // will only evaluate to true if it's unsuccessful. + return Unexpected(latePaymentComponents.error()); + } + + // LCOV_EXCL_START + UNREACHABLE("ripple::loanMakePayment : invalid late payment result"); + return Unexpected(tecINTERNAL); + // LCOV_EXCL_STOP + } + else if (hasExpired(view, nextDueDateProxy)) + { + // If the payment is late, and the late flag was not set, it's not valid + JLOG(j.warn()) + << "Loan payment is overdue. Use the tfLoanLatePayment transaction " + "flag to make a late payment. Loan was created on " + << startDate << ", prev payment due date is " + << prevPaymentDateProxy << ", next payment due date is " + << *nextDueDateProxy << ", ledger time is " + << view.parentCloseTime().time_since_epoch().count(); + return Unexpected(tecEXPIRED); + } + + // ------------------------------------------------------------- + // full payment handling + if (paymentType == LoanPaymentType::full) + { + TenthBips32 const closeInterestRate{loan->at(sfCloseInterestRate)}; + Number const closePaymentFee = + roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale); + + LoanState const roundedLoanState = calculateRoundedLoanState( + totalValueOutstandingProxy, + principalOutstandingProxy, + managementFeeOutstandingProxy); + + if (auto const fullPaymentComponents = detail::computeFullPayment( + asset, + view, + principalOutstandingProxy, + managementFeeOutstandingProxy, + periodicPayment, + paymentRemainingProxy, + prevPaymentDateProxy, + startDate, + paymentInterval, + closeInterestRate, + loanScale, + roundedLoanState.interestDue, + periodicRate, + closePaymentFee, + amount, + managementFeeRate, + j)) + { + return doPayment( + *fullPaymentComponents, + totalValueOutstandingProxy, + principalOutstandingProxy, + managementFeeOutstandingProxy, + paymentRemainingProxy, + prevPaymentDateProxy, + nextDueDateProxy, + paymentInterval); + } + else if (fullPaymentComponents.error()) + // error() will be the TER returned if a payment is not made. It + // will only evaluate to true if it's unsuccessful. Otherwise, + // tesSUCCESS means nothing was done, so continue. + return Unexpected(fullPaymentComponents.error()); + + // LCOV_EXCL_START + UNREACHABLE("ripple::loanMakePayment : invalid full payment result"); + return Unexpected(tecINTERNAL); + // LCOV_EXCL_STOP + } + + // ------------------------------------------------------------- + // regular periodic payment handling + + XRPL_ASSERT_PARTS( + paymentType == LoanPaymentType::regular || + paymentType == LoanPaymentType::overpayment, + "ripple::loanMakePayment", + "regular payment type"); + + // This will keep a running total of what is actually paid, if the payment + // is sufficient for any payment + LoanPaymentParts totalParts; + Number totalPaid; + std::size_t numPayments = 0; + + while (amount >= totalPaid + periodic.totalDue && + paymentRemainingProxy > 0 && + numPayments < loanMaximumPaymentsPerTransaction) + { + // Try to make more payments + XRPL_ASSERT_PARTS( + periodic.trackedPrincipalDelta >= 0, + "ripple::loanMakePayment", + "payment pays non-negative principal"); + + totalPaid += periodic.totalDue; + totalParts += detail::doPayment( + periodic, totalValueOutstandingProxy, principalOutstandingProxy, managementFeeOutstandingProxy, @@ -1851,47 +1893,19 @@ loanMakePayment( prevPaymentDateProxy, nextDueDateProxy, paymentInterval); - } - else if (latePaymentComponents.error()) - // error() will be the TER returned if a payment is not made. It will - // only evaluate to true if it's unsuccessful. Otherwise, tesSUCCESS - // means nothing was done, so continue. - return Unexpected(latePaymentComponents.error()); + ++numPayments; - // ------------------------------------------------------------- - // regular periodic payment handling + XRPL_ASSERT_PARTS( + (periodic.specialCase == detail::PaymentSpecialCase::final) == + (paymentRemainingProxy == 0), + "ripple::loanMakePayment", + "final payment is the final payment"); - // if the payment is not late nor if it's a full payment, then it must - // be a periodic one, with possible overpayments + // Don't compute the next payment if this was the last payment + if (periodic.specialCase == detail::PaymentSpecialCase::final) + break; - // This will keep a running total of what is actually paid, if the payment - // is sufficient for a single payment - Number totalPaid = periodic.totalDue; - - if (amount < totalPaid) - { - JLOG(j.warn()) << "Periodic loan payment amount is insufficient. Due: " - << totalPaid << ", paid: " << amount; - return Unexpected(tecINSUFFICIENT_PAYMENT); - } - - LoanPaymentParts totalParts = detail::doPayment( - periodic, - totalValueOutstandingProxy, - principalOutstandingProxy, - managementFeeOutstandingProxy, - paymentRemainingProxy, - prevPaymentDateProxy, - nextDueDateProxy, - paymentInterval); - - std::size_t numPayments = 1; - - while (totalPaid < amount && paymentRemainingProxy > 0 && - numPayments < loanMaximumPaymentsPerTransaction) - { - // Try to make more payments - detail::PaymentComponentsPlus const nextPayment{ + periodic = detail::PaymentComponentsPlus{ detail::computePaymentComponents( asset, loanScale, @@ -1903,40 +1917,13 @@ loanMakePayment( paymentRemainingProxy, managementFeeRate), serviceFee}; - XRPL_ASSERT_PARTS( - nextPayment.trackedPrincipalDelta >= 0, - "ripple::loanMakePayment", - "additional payment pays non-negative principal"); -#if LOANCOMPLETE - XRPL_ASSERT( - nextPayment.rawInterest <= periodic.rawInterest, - "ripple::loanMakePayment : decreasing interest"); - XRPL_ASSERT( - nextPayment.rawPrinicpal >= periodic.rawPrincipal, - "ripple::loanMakePayment : increasing principal"); -#endif + } - if (amount < totalPaid + nextPayment.totalDue) - // We're done making payments. - break; - - totalPaid += nextPayment.totalDue; - totalParts += detail::doPayment( - nextPayment, - totalValueOutstandingProxy, - principalOutstandingProxy, - managementFeeOutstandingProxy, - paymentRemainingProxy, - prevPaymentDateProxy, - nextDueDateProxy, - paymentInterval); - ++numPayments; - - XRPL_ASSERT_PARTS( - (nextPayment.specialCase == detail::PaymentSpecialCase::final) == - (paymentRemainingProxy == 0), - "ripple::loanMakePayment", - "final payment is the final payment"); + if (numPayments == 0) + { + JLOG(j.warn()) << "Regular loan payment amount is insufficient. Due: " + << periodic.totalDue << ", paid: " << amount; + return Unexpected(tecINSUFFICIENT_PAYMENT); } XRPL_ASSERT_PARTS( @@ -1952,8 +1939,9 @@ loanMakePayment( // ------------------------------------------------------------- // overpayment handling - if (overpaymentAllowed && loan->isFlag(lsfLoanOverpayment) && - paymentRemainingProxy > 0 && nextDueDateProxy && totalPaid < amount && + if (paymentType == LoanPaymentType::overpayment && + loan->isFlag(lsfLoanOverpayment) && paymentRemainingProxy > 0 && + nextDueDateProxy && totalPaid < amount && numPayments < loanMaximumPaymentsPerTransaction) { TenthBips32 const overpaymentInterestRate{ diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index fe19ed1c0d..64e18016e1 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -30,9 +30,21 @@ LoanPay::preflight(PreflightContext const& ctx) if (ctx.tx[sfAmount] <= beast::zero) return temBAD_AMOUNT; - // isFlag requires an exact match - all flags to be set - to return true. - if (ctx.tx.isFlag(tfLoanOverpayment | tfLoanFullPayment)) + // The loan payment flags are all mutually exclusive. If more than one is + // set, the tx is malformed. + int flagsSet = 0; + for (auto const flag : + {tfLoanLatePayment, tfLoanFullPayment, tfLoanOverpayment}) + { + if (ctx.tx.isFlag(flag)) + ++flagsSet; + } + if (flagsSet > 1) + { + JLOG(ctx.j.warn()) << "Only one LoanPay flag can be set per tx. " + << flagsSet << " is too many."; return temINVALID_FLAG; + } return tesSUCCESS; } @@ -42,8 +54,8 @@ LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx) { auto const normalCost = Transactor::calculateBaseFee(view, tx); - if (tx.isFlag(tfLoanFullPayment)) - // The loan will be making one set of calculations for one (large) + if (tx.isFlag(tfLoanFullPayment) || tx.isFlag(tfLoanLatePayment)) + // The loan will be making one set of calculations for one full or late // payment return normalCost; @@ -65,7 +77,8 @@ LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx) } if (hasExpired(view, loanSle->at(sfNextPaymentDueDate))) - // If the payment is late, it'll only make one payment + // If the payment is late, and the late payment flag is not set, it'll + // fail return normalCost; auto const brokerSle = @@ -97,6 +110,7 @@ LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx) // Estimate how many payments will be made Number const numPaymentEstimate = static_cast(amount / regularPayment); + // Charge one base fee per paymentsPerFeeIncrement payments, rounding up. Number::setround(Number::upward); auto const feeIncrements = std::max( @@ -291,23 +305,19 @@ LoanPay::doApply() LoanManage::unimpairLoan(view, loanSle, vaultSle, j_); } - Expected const paymentParts = - tx.isFlag(tfLoanFullPayment) ? loanMakeFullPayment( - asset, - view, - loanSle, - brokerSle, - amount, - tx.isFlag(tfLoanOverpayment), - j_) - : loanMakePayment( - asset, - view, - loanSle, - brokerSle, - amount, - tx.isFlag(tfLoanOverpayment), - j_); + LoanPaymentType const paymentType = [&tx]() { + // preflight already checked that at most one flag is set. + if (tx.isFlag(tfLoanLatePayment)) + return LoanPaymentType::late; + if (tx.isFlag(tfLoanFullPayment)) + return LoanPaymentType::full; + if (tx.isFlag(tfLoanOverpayment)) + return LoanPaymentType::overpayment; + return LoanPaymentType::regular; + }(); + + Expected const paymentParts = loanMakePayment( + asset, view, loanSle, brokerSle, amount, paymentType, j_); if (!paymentParts) { From 21eb13de0c7075d8cd5d8d2deb13b94277668d61 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 10 Nov 2025 23:28:25 -0500 Subject: [PATCH 218/291] Reorder payment options to do full early payment first - Since all the special cases are now specified with flags, the order is less important. - Avoids computing the periodic payment parts that are not needed for full payment computation. - A late payment without the late payment flag will override everything else, though. --- src/xrpld/app/misc/detail/LendingHelpers.cpp | 125 ++++++++++--------- 1 file changed, 66 insertions(+), 59 deletions(-) diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index b980e73353..325fb0954a 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -1732,66 +1732,10 @@ loanMakePayment( view.update(loan); - detail::PaymentComponentsPlus periodic{ - detail::computePaymentComponents( - asset, - loanScale, - totalValueOutstandingProxy, - principalOutstandingProxy, - managementFeeOutstandingProxy, - periodicPayment, - periodicRate, - paymentRemainingProxy, - managementFeeRate), - serviceFee}; - XRPL_ASSERT_PARTS( - periodic.trackedPrincipalDelta >= 0, - "ripple::loanMakePayment", - "regular payment valid principal"); - // ------------------------------------------------------------- - // late payment handling - if (paymentType == LoanPaymentType::late) - { - TenthBips32 const lateInterestRate{loan->at(sfLateInterestRate)}; - Number const latePaymentFee = loan->at(sfLatePaymentFee); - - if (auto const latePaymentComponents = detail::computeLatePayment( - asset, - view, - principalOutstandingProxy, - *nextDueDateProxy, - periodic, - lateInterestRate, - loanScale, - latePaymentFee, - amount, - managementFeeRate, - j)) - { - return doPayment( - *latePaymentComponents, - totalValueOutstandingProxy, - principalOutstandingProxy, - managementFeeOutstandingProxy, - paymentRemainingProxy, - prevPaymentDateProxy, - nextDueDateProxy, - paymentInterval); - } - else if (latePaymentComponents.error()) - { - // error() will be the TER returned if a payment is not made. It - // will only evaluate to true if it's unsuccessful. - return Unexpected(latePaymentComponents.error()); - } - - // LCOV_EXCL_START - UNREACHABLE("ripple::loanMakePayment : invalid late payment result"); - return Unexpected(tecINTERNAL); - // LCOV_EXCL_STOP - } - else if (hasExpired(view, nextDueDateProxy)) + // A late payment not flagged as late overrides all other options. + if (paymentType != LoanPaymentType::late && + hasExpired(view, nextDueDateProxy)) { // If the payment is late, and the late flag was not set, it's not valid JLOG(j.warn()) @@ -1858,6 +1802,69 @@ loanMakePayment( // LCOV_EXCL_STOP } + // ------------------------------------------------------------- + // compute the periodic payment info that will be needed whether the payment + // is late or regular + detail::PaymentComponentsPlus periodic{ + detail::computePaymentComponents( + asset, + loanScale, + totalValueOutstandingProxy, + principalOutstandingProxy, + managementFeeOutstandingProxy, + periodicPayment, + periodicRate, + paymentRemainingProxy, + managementFeeRate), + serviceFee}; + XRPL_ASSERT_PARTS( + periodic.trackedPrincipalDelta >= 0, + "ripple::loanMakePayment", + "regular payment valid principal"); + + // ------------------------------------------------------------- + // late payment handling + if (paymentType == LoanPaymentType::late) + { + TenthBips32 const lateInterestRate{loan->at(sfLateInterestRate)}; + Number const latePaymentFee = loan->at(sfLatePaymentFee); + + if (auto const latePaymentComponents = detail::computeLatePayment( + asset, + view, + principalOutstandingProxy, + *nextDueDateProxy, + periodic, + lateInterestRate, + loanScale, + latePaymentFee, + amount, + managementFeeRate, + j)) + { + return doPayment( + *latePaymentComponents, + totalValueOutstandingProxy, + principalOutstandingProxy, + managementFeeOutstandingProxy, + paymentRemainingProxy, + prevPaymentDateProxy, + nextDueDateProxy, + paymentInterval); + } + else if (latePaymentComponents.error()) + { + // error() will be the TER returned if a payment is not made. It + // will only evaluate to true if it's unsuccessful. + return Unexpected(latePaymentComponents.error()); + } + + // LCOV_EXCL_START + UNREACHABLE("ripple::loanMakePayment : invalid late payment result"); + return Unexpected(tecINTERNAL); + // LCOV_EXCL_STOP + } + // ------------------------------------------------------------- // regular periodic payment handling From 6c375f1346e2227f1e9c4536a1f907a9b82db549 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 11 Nov 2025 12:02:58 -0500 Subject: [PATCH 219/291] Convert sfNextPaymentDueDate from optional to default - Simplifies some of the updates and checks --- .../xrpl/protocol/detail/ledger_entries.macro | 2 +- src/test/app/Loan_test.cpp | 13 ++++++----- src/xrpld/app/misc/detail/LendingHelpers.cpp | 23 ++++++++----------- src/xrpld/app/tx/detail/LoanManage.cpp | 4 +++- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 9e9a0c17a4..5f6d1b844e 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -542,7 +542,7 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({ {sfPaymentInterval, soeREQUIRED}, {sfGracePeriod, soeDEFAULT}, {sfPreviousPaymentDate, soeDEFAULT}, - {sfNextPaymentDueDate, soeOPTIONAL}, + {sfNextPaymentDueDate, soeDEFAULT}, // The loan object tracks these values: // // - PaymentRemaining: The number of payments left in the loan. When it diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 536ac31eda..47b76d2a8a 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -361,11 +361,8 @@ protected: loan->at(sfPreviousPaymentDate) == previousPaymentDate); env.test.BEAST_EXPECT( loan->at(sfPaymentRemaining) == paymentRemaining); - if (paymentRemaining == 0) - env.test.BEAST_EXPECT(!loan->at(~sfNextPaymentDueDate)); - else - env.test.BEAST_EXPECT( - loan->at(sfNextPaymentDueDate) == nextPaymentDate); + env.test.BEAST_EXPECT( + loan->at(sfNextPaymentDueDate) == nextPaymentDate); env.test.BEAST_EXPECT(loan->at(sfLoanScale) == loanScale); env.test.BEAST_EXPECT( loan->at(sfTotalValueOutstanding) == totalValue); @@ -498,7 +495,7 @@ protected: return LoanState{ .previousPaymentDate = loan->at(sfPreviousPaymentDate), .startDate = tp{d{loan->at(sfStartDate)}}, - .nextPaymentDate = loan->at(~sfNextPaymentDueDate).value_or(0), + .nextPaymentDate = loan->at(sfNextPaymentDueDate), .paymentRemaining = loan->at(sfPaymentRemaining), .loanScale = loan->at(sfLoanScale), .totalValue = loan->at(sfTotalValueOutstanding), @@ -1110,6 +1107,7 @@ protected: detail::PaymentSpecialCase::final) { state.paymentRemaining = 0; + state.nextPaymentDate = 0; } else { @@ -2169,6 +2167,7 @@ protected: state.totalValue = 0; state.principalOutstanding = 0; state.managementFeeOutstanding = 0; + state.nextPaymentDate = 0; verifyLoanStatus(state); // Once a loan is defaulted, it can't be managed @@ -2313,6 +2312,7 @@ protected: state.managementFeeOutstanding = 0; state.previousPaymentDate = state.nextPaymentDate + state.paymentInterval * (numPayments - 1); + state.nextPaymentDate = 0; verifyLoanStatus(state); verifyLoanStatus.checkPayment( @@ -2815,6 +2815,7 @@ protected: detail::PaymentSpecialCase::final) { state.paymentRemaining = 0; + state.nextPaymentDate = 0; } else { diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index 325fb0954a..eac586d1a0 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -518,9 +518,9 @@ doPayment( paymentRemainingProxy = 0; prevPaymentDateProxy = *nextDueDateProxy; - // Remove the field. This is the only condition where nextDueDate is - // allowed to be removed. - nextDueDateProxy = std::nullopt; + // Zero out the next due date. Since it's default, it'll be removed from + // the object. + nextDueDateProxy = 0; // Always zero out the the tracked values on a final payment principalOutstandingProxy = 0; @@ -533,10 +533,8 @@ doPayment( { paymentRemainingProxy -= 1; - prevPaymentDateProxy = *nextDueDateProxy; - // STObject::OptionalField does not define operator+=, so do it the - // old-fashioned way. - nextDueDateProxy = *nextDueDateProxy + paymentInterval; + prevPaymentDateProxy = nextDueDateProxy; + nextDueDateProxy += paymentInterval; } XRPL_ASSERT_PARTS( principalOutstandingProxy > payment.trackedPrincipalDelta, @@ -1699,8 +1697,8 @@ loanMakePayment( auto managementFeeOutstandingProxy = loan->at(sfManagementFeeOutstanding); // Next payment due date must be set unless the loan is complete - auto nextDueDateProxy = loan->at(~sfNextPaymentDueDate); - if (!nextDueDateProxy) + auto nextDueDateProxy = loan->at(sfNextPaymentDueDate); + if (*nextDueDateProxy == 0) { JLOG(j.warn()) << "Loan next payment due date is not set."; return Unexpected(tecINTERNAL); @@ -1743,7 +1741,7 @@ loanMakePayment( "flag to make a late payment. Loan was created on " << startDate << ", prev payment due date is " << prevPaymentDateProxy << ", next payment due date is " - << *nextDueDateProxy << ", ledger time is " + << nextDueDateProxy << ", ledger time is " << view.parentCloseTime().time_since_epoch().count(); return Unexpected(tecEXPIRED); } @@ -1833,7 +1831,7 @@ loanMakePayment( asset, view, principalOutstandingProxy, - *nextDueDateProxy, + nextDueDateProxy, periodic, lateInterestRate, loanScale, @@ -1948,8 +1946,7 @@ loanMakePayment( // overpayment handling if (paymentType == LoanPaymentType::overpayment && loan->isFlag(lsfLoanOverpayment) && paymentRemainingProxy > 0 && - nextDueDateProxy && totalPaid < amount && - numPayments < loanMaximumPaymentsPerTransaction) + totalPaid < amount && numPayments < loanMaximumPaymentsPerTransaction) { TenthBips32 const overpaymentInterestRate{ loan->at(sfOverpaymentInterestRate)}; diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index c45d317dea..73754404e5 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -276,7 +276,9 @@ LoanManage::defaultLoan( paymentRemainingProxy = 0; principalOutstandingProxy = 0; managementFeeOutstandingProxy = 0; - loanSle->at(~sfNextPaymentDueDate) = std::nullopt; + // Zero out the next due date. Since it's default, it'll be removed from + // the object. + loanSle->at(sfNextPaymentDueDate) = 0; view.update(loanSle); // Return funds from the LoanBroker pseudo-account to the From ef66a1cc0eee294fbe73ae8734672d69c926d501 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 12 Nov 2025 13:46:11 -0500 Subject: [PATCH 220/291] Disable inner Batch transactions for Vault and Loan types --- src/test/app/Batch_test.cpp | 21 ++++++++++++++++----- src/test/app/Loan_test.cpp | 9 ++++++++- src/xrpld/app/tx/detail/Batch.cpp | 11 ++++++++++- src/xrpld/app/tx/detail/Batch.h | 18 ++++++++++++++++++ 4 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/test/app/Batch_test.cpp b/src/test/app/Batch_test.cpp index b74d492cc1..2f3b9337e0 100644 --- a/src/test/app/Batch_test.cpp +++ b/src/test/app/Batch_test.cpp @@ -2543,6 +2543,11 @@ class Batch_test : public beast::unit_test::suite { testcase("loan"); + bool const lendingBatchEnabled = !std::any_of( + Batch::disabledTxTypes.begin(), + Batch::disabledTxTypes.end(), + [](auto const& disabled) { return disabled == ttLOAN_BROKER_SET; }); + using namespace test::jtx; test::jtx::Env env{ @@ -2609,7 +2614,8 @@ class Batch_test : public beast::unit_test::suite { auto const [txIDs, batchID] = submitBatch( env, - temBAD_SIGNATURE, + lendingBatchEnabled ? temBAD_SIGNATURE + : temINVALID_INNER_BATCH, batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing), batch::inner( env.json( @@ -2648,7 +2654,8 @@ class Batch_test : public beast::unit_test::suite { auto const [txIDs, batchID] = submitBatch( env, - temBAD_SIGNER, + lendingBatchEnabled ? temBAD_SIGNER + : temINVALID_INNER_BATCH, batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing), batch::inner( env.json( @@ -2672,7 +2679,8 @@ class Batch_test : public beast::unit_test::suite auto const batchFee = batch::calcBatchFee(env, 1, 2); auto const [txIDs, batchID] = submitBatch( env, - tesSUCCESS, + lendingBatchEnabled ? TER(tesSUCCESS) + : TER(temINVALID_INNER_BATCH), batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing), batch::inner( env.json( @@ -2704,7 +2712,8 @@ class Batch_test : public beast::unit_test::suite auto const batchFee = batch::calcBatchFee(env, 1, 2); auto const [txIDs, batchID] = submitBatch( env, - tesSUCCESS, + lendingBatchEnabled ? TER(tesSUCCESS) + : TER(temINVALID_INNER_BATCH), batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing), batch::inner( env.json( @@ -2721,7 +2730,9 @@ class Batch_test : public beast::unit_test::suite } env.close(); BEAST_EXPECT(env.le(brokerKeylet)); - if (auto const sleLoan = env.le(loanKeylet); BEAST_EXPECT(sleLoan)) + if (auto const sleLoan = env.le(loanKeylet); lendingBatchEnabled + ? BEAST_EXPECT(sleLoan) + : !BEAST_EXPECT(!sleLoan)) { BEAST_EXPECT(sleLoan->isFlag(lsfLoanImpaired)); } diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 47b76d2a8a..da8b964c94 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -3801,6 +3802,11 @@ protected: // From FIND-001 testcase << "Batch Bypass Counterparty"; + bool const lendingBatchEnabled = !std::any_of( + Batch::disabledTxTypes.begin(), + Batch::disabledTxTypes.end(), + [](auto const& disabled) { return disabled == ttLOAN_BROKER_SET; }); + using namespace jtx; using namespace std::chrono_literals; Env env(*this, all); @@ -3846,7 +3852,8 @@ protected: env(batch::outer(borrower, seq, batchFee, tfAllOrNothing), batch::inner(forgedLoanSet, seq + 1), batch::inner(pay(borrower, lender, XRP(1)), seq + 2), - ter(temBAD_SIGNATURE)); + ter(lendingBatchEnabled ? temBAD_SIGNATURE + : temINVALID_INNER_BATCH)); env.close(); // ? Check that the loan was NOT created diff --git a/src/xrpld/app/tx/detail/Batch.cpp b/src/xrpld/app/tx/detail/Batch.cpp index 052e90e19e..9d2fe4e47d 100644 --- a/src/xrpld/app/tx/detail/Batch.cpp +++ b/src/xrpld/app/tx/detail/Batch.cpp @@ -263,7 +263,8 @@ Batch::preflight(PreflightContext const& ctx) return temREDUNDANT; } - if (stx.getFieldU16(sfTransactionType) == ttBATCH) + auto const txType = stx.getFieldU16(sfTransactionType); + if (txType == ttBATCH) { JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " << "batch cannot have an inner batch txn. " @@ -271,6 +272,14 @@ Batch::preflight(PreflightContext const& ctx) return temINVALID; } + if (std::any_of( + disabledTxTypes.begin(), + disabledTxTypes.end(), + [txType](auto const& disabled) { return txType == disabled; })) + { + return temINVALID_INNER_BATCH; + } + if (!(stx.getFlags() & tfInnerBatchTxn)) { JLOG(ctx.j.debug()) diff --git a/src/xrpld/app/tx/detail/Batch.h b/src/xrpld/app/tx/detail/Batch.h index 1b1d7614d5..7889e91bdc 100644 --- a/src/xrpld/app/tx/detail/Batch.h +++ b/src/xrpld/app/tx/detail/Batch.h @@ -35,6 +35,24 @@ public: TER doApply() override; + + static constexpr auto disabledTxTypes = std::to_array({ + ttVAULT_CREATE, + ttVAULT_SET, + ttVAULT_DELETE, + ttVAULT_DEPOSIT, + ttVAULT_WITHDRAW, + ttVAULT_CLAWBACK, + ttLOAN_BROKER_SET, + ttLOAN_BROKER_DELETE, + ttLOAN_BROKER_COVER_DEPOSIT, + ttLOAN_BROKER_COVER_WITHDRAW, + ttLOAN_BROKER_COVER_CLAWBACK, + ttLOAN_SET, + ttLOAN_DELETE, + ttLOAN_MANAGE, + ttLOAN_PAY, + }); }; } // namespace ripple From b7ed35cb0420d95fe7609ceabe6a79e2fbb90044 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 12 Nov 2025 16:34:41 -0500 Subject: [PATCH 221/291] Fix merge issue: references to retired features --- src/xrpld/app/tx/detail/Payment.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index 931048d1a9..9d6a5e5783 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -567,9 +567,6 @@ Payment::makeRipplePayment(Payment::RipplePaymentParams const& p) beast::Journal j_ = p.j; auto view = [&p]() -> ApplyView& { return p.ctx.view(); }; - bool const depositPreauth = - p.ctx.view().rules().enabled(featureDepositPreauth); - bool const depositAuth = p.ctx.view().rules().enabled(featureDepositAuth); // Below this line, copied straight from Payment::doApply // except `ctx_.tx.getFieldPathSet(sfPaths)` replaced with `paths`, From 2e34506835a3487313194ba01b3f43b2c00b3548 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 12 Nov 2025 19:14:54 -0500 Subject: [PATCH 222/291] Revert "Add a distinction between a "valid" and a "representable" Number" This reverts commit 8e56af20ee9e9e35b50c66d9ab10e63ef841955d. --- include/xrpl/basics/Number.h | 31 +++------ include/xrpl/protocol/XRPAmount.h | 2 +- src/libxrpl/basics/Number.cpp | 32 +-------- src/libxrpl/ledger/View.cpp | 8 --- src/libxrpl/protocol/STAmount.cpp | 4 +- src/test/app/Vault_test.cpp | 10 +-- src/test/basics/Number_test.cpp | 76 +--------------------- src/xrpld/app/tx/detail/InvariantCheck.cpp | 20 ------ src/xrpld/app/tx/detail/VaultClawback.cpp | 5 -- src/xrpld/app/tx/detail/VaultCreate.cpp | 7 -- src/xrpld/app/tx/detail/VaultDeposit.cpp | 2 - src/xrpld/app/tx/detail/VaultSet.cpp | 5 -- src/xrpld/app/tx/detail/VaultWithdraw.cpp | 2 - 13 files changed, 19 insertions(+), 185 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index 342184df8d..fd04c8b788 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -28,17 +28,12 @@ public: /** Describes whether and how to enforce this number as an integer. * * - none: No enforcement. The value may vary freely. This is the default. - * - compatible: If the absolute value is greater than maxIntValue, valid() - * will return false. Needed for backward compatibility with XRP used in - * AMMs, and available for functions that will do their own checking. This - * is the default for automatic conversions from XRPAmount to Number. - * - weak: Like compatible, plus, if the value is unrepresentable (larger - * than maxMantissa), assignment and other operations will throw. - * - strong: Like weak, plus, if the absolute value is invalid (larger than - * maxIntValue), assignment and other operations will throw. This is the - * defalut for automatic conversions from MPTAmount to Number. + * - weak: If the absolute value is greater than maxIntValue, valid() will + * return false. + * - strong: Assignment operations will throw if the absolute value is above + * maxIntValue. */ - enum EnforceInteger { none, compatible, weak, strong }; + enum EnforceInteger { none, weak, strong }; private: using rep = std::int64_t; @@ -47,7 +42,8 @@ private: // The enforcement setting is not serialized, and does not affect the // ledger. If not "none", the value is checked to be within the valid - // integer range. See the enum description for more detail. + // integer range. With "strong", the checks will be made as automatic as + // possible. EnforceInteger enforceInteger_ = none; public: @@ -57,8 +53,8 @@ public: constexpr static rep maxMantissa = minMantissa * 10 - 1; static_assert(maxMantissa == 9'999'999'999'999'999LL); - constexpr static rep maxIntValue = maxMantissa / 100; - static_assert(maxIntValue == 99'999'999'999'999LL); + constexpr static rep maxIntValue = maxMantissa / 10; + static_assert(maxIntValue == 999'999'999'999'999LL); // The range for the exponent when normalized constexpr static int minExponent = -32768; @@ -97,15 +93,6 @@ public: bool valid() const noexcept; - bool - representable() const noexcept; - /// Combines setIntegerEnforcement(EnforceInteger) and valid() - bool - valid(EnforceInteger enforce); - /// Because this function is const, it should only be used for one-off - /// checks - bool - valid(EnforceInteger enforce) const; constexpr Number operator+() const noexcept; diff --git a/include/xrpl/protocol/XRPAmount.h b/include/xrpl/protocol/XRPAmount.h index 8f553a6083..159174accc 100644 --- a/include/xrpl/protocol/XRPAmount.h +++ b/include/xrpl/protocol/XRPAmount.h @@ -143,7 +143,7 @@ public: operator Number() const noexcept { - return {drops(), Number::compatible}; + return {drops(), Number::weak}; } Number diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index d0837860be..2587af2e70 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -227,8 +227,6 @@ Number::checkInteger(char const* what) const { if (enforceInteger_ == strong && !valid()) throw std::overflow_error(what); - if (enforceInteger_ == weak && !representable()) - throw std::overflow_error(what); } void @@ -275,20 +273,7 @@ Number::normalize() bool Number::valid() const noexcept { - return valid(enforceInteger_); -} - -bool -Number::valid(EnforceInteger enforce) -{ - setIntegerEnforcement(enforce); - return valid(); -} - -bool -Number::valid(EnforceInteger enforce) const -{ - if (enforce != none) + if (enforceInteger_ != none) { static Number const max = maxIntValue; static Number const maxNeg = -maxIntValue; @@ -300,21 +285,6 @@ Number::valid(EnforceInteger enforce) const return true; } -bool -Number::representable() const noexcept -{ - if (enforceInteger_ != none) - { - static Number const max = maxMantissa; - static Number const maxNeg = -maxMantissa; - // Avoid making a copy - if (mantissa_ < 0) - return *this >= maxNeg; - return *this <= max; - } - return true; -} - Number& Number::operator+=(Number const& y) { diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index fa9e71f113..7c158d4448 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -3454,8 +3454,6 @@ assetsToSharesDeposit( Number const assetTotal = vault->at(sfAssetsTotal); STAmount shares{vault->at(sfShareMPTID)}; - // STAmount will ignore enforcement for IOUs, so we can set it regardless of - // type. shares.setIntegerEnforcement(Number::weak); if (assetTotal == 0) return STAmount{ @@ -3488,8 +3486,6 @@ sharesToAssetsDeposit( Number const assetTotal = vault->at(sfAssetsTotal); STAmount assets{vault->at(sfAsset)}; - // STAmount will ignore enforcement for IOUs, so we can set it regardless of - // type. assets.setIntegerEnforcement(Number::weak); if (assetTotal == 0) return STAmount{ @@ -3524,8 +3520,6 @@ assetsToSharesWithdraw( Number assetTotal = vault->at(sfAssetsTotal); assetTotal -= vault->at(sfLossUnrealized); STAmount shares{vault->at(sfShareMPTID)}; - // STAmount will ignore enforcement for IOUs, so we can set it regardless of - // type. shares.setIntegerEnforcement(Number::weak); if (assetTotal == 0) return shares; @@ -3557,8 +3551,6 @@ sharesToAssetsWithdraw( Number assetTotal = vault->at(sfAssetsTotal); assetTotal -= vault->at(sfLossUnrealized); STAmount assets{vault->at(sfAsset)}; - // STAmount will ignore enforcement for IOUs, so we can set it regardless of - // type. assets.setIntegerEnforcement(Number::weak); if (assetTotal == 0) return assets; diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 26335c12bf..545a09a625 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -270,9 +270,7 @@ STAmount::integerEnforcement() const noexcept bool STAmount::validNumber() const noexcept { - // compatible will not throw. IOUs will ignore the flag, and will - // always be valid. - Number n = toNumber(Number::EnforceInteger::compatible); + Number n = toNumber(Number::EnforceInteger::weak); return n.valid(); } diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index f823c56a72..893a50f605 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -3727,7 +3727,7 @@ class Vault_test : public beast::unit_test::suite } }); - testCase(13, [&, this](Env& env, Data d) { + testCase(14, [&, this](Env& env, Data d) { testcase("MPT scale deposit overflow on first deposit"); auto tx = d.vault.deposit( {.depositor = d.depositor, @@ -3737,7 +3737,7 @@ class Vault_test : public beast::unit_test::suite env.close(); }); - testCase(13, [&, this](Env& env, Data d) { + testCase(14, [&, this](Env& env, Data d) { testcase("MPT scale deposit overflow on second deposit"); { @@ -3759,7 +3759,7 @@ class Vault_test : public beast::unit_test::suite } }); - testCase(13, [&, this](Env& env, Data d) { + testCase(14, [&, this](Env& env, Data d) { testcase("No MPT scale deposit overflow on total shares"); { @@ -4079,7 +4079,7 @@ class Vault_test : public beast::unit_test::suite } }); - testCase(13, [&, this](Env& env, Data d) { + testCase(14, [&, this](Env& env, Data d) { testcase("MPT scale withdraw overflow"); { @@ -4320,7 +4320,7 @@ class Vault_test : public beast::unit_test::suite } }); - testCase(13, [&, this](Env& env, Data d) { + testCase(14, [&, this](Env& env, Data d) { testcase("MPT Scale clawback overflow"); { diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index c814f5f7a3..a505ff522d 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -846,87 +846,28 @@ public: Number a{100}; BEAST_EXPECT(a.integerEnforcement() == Number::none); BEAST_EXPECT(a.valid()); - BEAST_EXPECT(a.representable()); a = Number{1, 30}; BEAST_EXPECT(a.valid()); - BEAST_EXPECT(a.representable()); a = -100; BEAST_EXPECT(a.valid()); - BEAST_EXPECT(a.representable()); - } - { - Number a{100, Number::compatible}; - BEAST_EXPECT(a.integerEnforcement() == Number::compatible); - BEAST_EXPECT(a.valid()); - BEAST_EXPECT(a.representable()); - a = Number{1, 15}; - BEAST_EXPECT(!a.valid()); - BEAST_EXPECT(a.representable()); - a = Number{1, 30, Number::none}; - BEAST_EXPECT(!a.valid()); - BEAST_EXPECT(!a.representable()); - a = -100; - BEAST_EXPECT(a.integerEnforcement() == Number::compatible); - BEAST_EXPECT(a.valid()); - BEAST_EXPECT(a.representable()); - a = Number{5, Number::weak}; - BEAST_EXPECT(a.integerEnforcement() == Number::weak); - BEAST_EXPECT(a.valid()); - BEAST_EXPECT(a.representable()); - a = Number{5, Number::strong}; - BEAST_EXPECT(a.integerEnforcement() == Number::strong); - BEAST_EXPECT(a.valid()); - BEAST_EXPECT(a.representable()); } { Number a{100, Number::weak}; BEAST_EXPECT(a.integerEnforcement() == Number::weak); BEAST_EXPECT(a.valid()); - BEAST_EXPECT(a.representable()); - a = Number{1, 15}; + a = Number{1, 30, Number::none}; BEAST_EXPECT(!a.valid()); - BEAST_EXPECT(a.representable()); - try - { - a = Number{1, 30, Number::compatible}; - BEAST_EXPECT(false); - } - catch (std::overflow_error const& e) - { - BEAST_EXPECT(e.what() == "Number::operator= integer overflow"s); - // The throw is done _after_ the number is updated. - BEAST_EXPECT((a == Number{1, 30})); - } - BEAST_EXPECT(a.integerEnforcement() == Number::weak); - BEAST_EXPECT(!a.valid()); - BEAST_EXPECT(!a.representable()); a = -100; BEAST_EXPECT(a.integerEnforcement() == Number::weak); BEAST_EXPECT(a.valid()); - BEAST_EXPECT(a.representable()); a = Number{5, Number::strong}; BEAST_EXPECT(a.integerEnforcement() == Number::strong); BEAST_EXPECT(a.valid()); - BEAST_EXPECT(a.representable()); } { Number a{100, Number::strong}; BEAST_EXPECT(a.integerEnforcement() == Number::strong); BEAST_EXPECT(a.valid()); - BEAST_EXPECT(a.representable()); - try - { - a = Number{1, 15, Number::compatible}; - BEAST_EXPECT(false); - } - catch (std::overflow_error const& e) - { - BEAST_EXPECT(e.what() == "Number::operator= integer overflow"s); - // The throw is done _after_ the number is updated. - BEAST_EXPECT((a == Number{1, 15})); - } - BEAST_EXPECT(!a.valid()); - BEAST_EXPECT(a.representable()); try { a = Number{1, 30}; @@ -939,19 +880,15 @@ public: BEAST_EXPECT((a == Number{1, 30})); } BEAST_EXPECT(!a.valid()); - BEAST_EXPECT(!a.representable()); a = -100; BEAST_EXPECT(a.integerEnforcement() == Number::strong); BEAST_EXPECT(a.valid()); - BEAST_EXPECT(a.representable()); } { - Number a{INITIAL_XRP.drops(), Number::compatible}; + Number a{INITIAL_XRP.drops(), Number::weak}; BEAST_EXPECT(!a.valid()); - BEAST_EXPECT(!a.representable()); a = -a; BEAST_EXPECT(!a.valid()); - BEAST_EXPECT(!a.representable()); try { @@ -967,7 +904,6 @@ public: // assigned to the Number BEAST_EXPECT(a == -INITIAL_XRP); BEAST_EXPECT(!a.valid()); - BEAST_EXPECT(!a.representable()); } try { @@ -981,7 +917,6 @@ public: // assigned to the Number BEAST_EXPECT(a == -INITIAL_XRP); BEAST_EXPECT(!a.valid()); - BEAST_EXPECT(!a.representable()); } a = Number::maxIntValue; try @@ -995,7 +930,6 @@ public: // This time, the throw is done _after_ the number is updated. BEAST_EXPECT(a == Number::maxIntValue + 1); BEAST_EXPECT(!a.valid()); - BEAST_EXPECT(a.representable()); } a = -Number::maxIntValue; try @@ -1009,7 +943,6 @@ public: // This time, the throw is done _after_ the number is updated. BEAST_EXPECT(a == -Number::maxIntValue - 1); BEAST_EXPECT(!a.valid()); - BEAST_EXPECT(a.representable()); } a = Number(1, 10); try @@ -1024,7 +957,6 @@ public: // The throw is done _after_ the number is updated. BEAST_EXPECT((a == Number{1, 20})); BEAST_EXPECT(!a.valid()); - BEAST_EXPECT(!a.representable()); } try { @@ -1037,7 +969,6 @@ public: // The throw is done _after_ the number is updated. BEAST_EXPECT((a == Number::maxIntValue * 2)); BEAST_EXPECT(!a.valid()); - BEAST_EXPECT(a.representable()); } try { @@ -1050,7 +981,6 @@ public: // The Number doesn't get updated because the ctor throws BEAST_EXPECT((a == Number::maxIntValue * 2)); BEAST_EXPECT(!a.valid()); - BEAST_EXPECT(a.representable()); } a = Number(1, 10); try @@ -1064,12 +994,10 @@ public: // The throw is done _after_ the number is updated. BEAST_EXPECT((a == Number{1, 20})); BEAST_EXPECT(!a.valid()); - BEAST_EXPECT(!a.representable()); } a /= Number(1, 15); BEAST_EXPECT((a == Number{1, 5})); BEAST_EXPECT(a.valid()); - BEAST_EXPECT(a.representable()); } } diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index b6ddfde24f..a23b4bcb47 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -2532,13 +2532,6 @@ ValidVault::Vault::make(SLE const& from) self.assetsAvailable = from.at(sfAssetsAvailable); self.assetsMaximum = from.at(sfAssetsMaximum); self.lossUnrealized = from.at(sfLossUnrealized); - if (self.asset.integral()) - { - self.assetsTotal.setIntegerEnforcement(Number::compatible); - self.assetsAvailable.setIntegerEnforcement(Number::compatible); - self.assetsMaximum.setIntegerEnforcement(Number::compatible); - self.lossUnrealized.setIntegerEnforcement(Number::compatible); - } return self; } @@ -2773,19 +2766,6 @@ ValidVault::finalize( beforeVault_.empty() || beforeVault_[0].key == afterVault.key, "ripple::ValidVault::finalize : single vault operation"); - if (!afterVault.assetsTotal.representable() || - !afterVault.assetsAvailable.representable() || - !afterVault.assetsMaximum.representable() || - !afterVault.lossUnrealized.representable()) - { - JLOG(j.fatal()) << "Invariant failed: vault overflowed maximum current " - "representable integer value"; - XRPL_ASSERT( - enforce, - "ripple::ValidVault::finalize : vault integer limit invariant"); - return !enforce; // That's all we can do here - } - auto const updatedShares = [&]() -> std::optional { // At this moment we only know that a vault is being updated and there // might be some MPTokenIssuance objects which are also updated in the diff --git a/src/xrpld/app/tx/detail/VaultClawback.cpp b/src/xrpld/app/tx/detail/VaultClawback.cpp index b464c1eb28..1c73d36bb0 100644 --- a/src/xrpld/app/tx/detail/VaultClawback.cpp +++ b/src/xrpld/app/tx/detail/VaultClawback.cpp @@ -150,11 +150,8 @@ VaultClawback::doApply() amount.asset() == vaultAsset, "ripple::VaultClawback::doApply : matching asset"); - // Both of these values are going to be decreased in this transaction, - // so the limit doesn't really matter. auto assetsAvailable = vault->at(sfAssetsAvailable); auto assetsTotal = vault->at(sfAssetsTotal); - [[maybe_unused]] auto const lossUnrealized = vault->at(sfLossUnrealized); XRPL_ASSERT( lossUnrealized <= (assetsTotal - assetsAvailable), @@ -164,8 +161,6 @@ VaultClawback::doApply() MPTIssue const share{mptIssuanceID}; STAmount sharesDestroyed = {share}; STAmount assetsRecovered; - // STAmount will ignore enforcement for IOUs, so we can set it regardless of - // type. assetsRecovered.setIntegerEnforcement(Number::weak); sharesDestroyed.setIntegerEnforcement(Number::weak); try diff --git a/src/xrpld/app/tx/detail/VaultCreate.cpp b/src/xrpld/app/tx/detail/VaultCreate.cpp index ea5b1d3e76..d84205b13f 100644 --- a/src/xrpld/app/tx/detail/VaultCreate.cpp +++ b/src/xrpld/app/tx/detail/VaultCreate.cpp @@ -213,13 +213,6 @@ VaultCreate::doApply() vault->at(sfWithdrawalPolicy) = vaultStrategyFirstComeFirstServe; if (scale) vault->at(sfScale) = scale; - if (asset.integral()) - { - // Only the Maximum can be a non-zero value, so only it needs to be - // checked. - if (!vault->at(sfAssetsMaximum).value().valid(Number::compatible)) - return tecLIMIT_EXCEEDED; - } view().insert(vault); // Explicitly create MPToken for the vault owner diff --git a/src/xrpld/app/tx/detail/VaultDeposit.cpp b/src/xrpld/app/tx/detail/VaultDeposit.cpp index 53b7fd67c0..8e42fd188d 100644 --- a/src/xrpld/app/tx/detail/VaultDeposit.cpp +++ b/src/xrpld/app/tx/detail/VaultDeposit.cpp @@ -220,8 +220,6 @@ VaultDeposit::doApply() } STAmount sharesCreated = {vault->at(sfShareMPTID)}, assetsDeposited; - // STAmount will ignore enforcement for IOUs, so we can set it regardless of - // type. sharesCreated.setIntegerEnforcement(Number::weak); try { diff --git a/src/xrpld/app/tx/detail/VaultSet.cpp b/src/xrpld/app/tx/detail/VaultSet.cpp index 439bc598e0..97522dad12 100644 --- a/src/xrpld/app/tx/detail/VaultSet.cpp +++ b/src/xrpld/app/tx/detail/VaultSet.cpp @@ -147,11 +147,6 @@ VaultSet::doApply() tx[sfAssetsMaximum] < *vault->at(sfAssetsTotal)) return tecLIMIT_EXCEEDED; vault->at(sfAssetsMaximum) = tx[sfAssetsMaximum]; - if (vault->at(sfAsset).value().integral()) - { - if (!vault->at(sfAssetsMaximum).value().valid(Number::compatible)) - return tecLIMIT_EXCEEDED; - } } if (auto const domainId = tx[~sfDomainID]; domainId) diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.cpp b/src/xrpld/app/tx/detail/VaultWithdraw.cpp index fc2ee114e3..04857d400e 100644 --- a/src/xrpld/app/tx/detail/VaultWithdraw.cpp +++ b/src/xrpld/app/tx/detail/VaultWithdraw.cpp @@ -207,8 +207,6 @@ VaultWithdraw::doApply() return tecINSUFFICIENT_FUNDS; } - // These values are only going to decrease, and can't be less than 0, so - // there's no need for integer range enforcement. auto assetsAvailable = vault->at(sfAssetsAvailable); auto assetsTotal = vault->at(sfAssetsTotal); [[maybe_unused]] auto const lossUnrealized = vault->at(sfLossUnrealized); From 694abd1c7917a89b826f196d8560bf3c0002dce0 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 12 Nov 2025 19:15:00 -0500 Subject: [PATCH 223/291] Revert "Catch up the consequences of Number changes" This reverts commit 0175dd70dbc6a962e93c1fd4ec4cbadc1bcfd903. --- include/xrpl/basics/Number.h | 3 +- include/xrpl/protocol/Asset.h | 6 -- include/xrpl/protocol/MPTAmount.h | 8 +-- include/xrpl/protocol/STAmount.h | 67 +------------------ include/xrpl/protocol/STNumber.h | 12 ---- include/xrpl/protocol/SystemParameters.h | 1 - include/xrpl/protocol/XRPAmount.h | 6 -- src/libxrpl/ledger/View.cpp | 23 ++----- src/libxrpl/protocol/STAmount.cpp | 19 ------ src/libxrpl/protocol/STNumber.cpp | 18 ------ src/test/app/Vault_test.cpp | 78 ++--------------------- src/test/basics/Number_test.cpp | 4 +- src/xrpld/app/tx/detail/VaultClawback.cpp | 19 +----- src/xrpld/app/tx/detail/VaultDeposit.cpp | 24 ++----- src/xrpld/app/tx/detail/VaultWithdraw.cpp | 11 +--- 15 files changed, 24 insertions(+), 275 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index fd04c8b788..1ea43a1e93 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -53,8 +53,7 @@ public: constexpr static rep maxMantissa = minMantissa * 10 - 1; static_assert(maxMantissa == 9'999'999'999'999'999LL); - constexpr static rep maxIntValue = maxMantissa / 10; - static_assert(maxIntValue == 999'999'999'999'999LL); + constexpr static rep maxIntValue = minMantissa / 10; // The range for the exponent when normalized constexpr static int minExponent = -32768; diff --git a/include/xrpl/protocol/Asset.h b/include/xrpl/protocol/Asset.h index 6765f7cf7d..d0efe814a9 100644 --- a/include/xrpl/protocol/Asset.h +++ b/include/xrpl/protocol/Asset.h @@ -84,12 +84,6 @@ public: return holds() && get().native(); } - bool - integral() const - { - return !holds() || get().native(); - } - friend constexpr bool operator==(Asset const& lhs, Asset const& rhs); diff --git a/include/xrpl/protocol/MPTAmount.h b/include/xrpl/protocol/MPTAmount.h index 5f552d839b..1c7c7a25e7 100644 --- a/include/xrpl/protocol/MPTAmount.h +++ b/include/xrpl/protocol/MPTAmount.h @@ -62,17 +62,11 @@ public: explicit constexpr operator bool() const noexcept; - operator Number() const + operator Number() const noexcept { return {value(), Number::strong}; } - Number - toNumber(Number::EnforceInteger enforce) const - { - return {value(), enforce}; - } - /** Return the sign of the amount */ constexpr int signum() const noexcept; diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index 11df94518f..d12e218250 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -40,12 +40,6 @@ private: exponent_type mOffset; bool mIsNegative; - // The Enforce integer setting is not stored or serialized. If set, it is - // used during automatic conversions to Number. If not set, the default - // behavior is used. It can also be overridden when coverting by using - // toNumber(). - std::optional enforceConversion_; - public: using value_type = STAmount; @@ -143,28 +137,9 @@ public: STAmount(A const& asset, int mantissa, int exponent = 0); template - STAmount( - A const& asset, - Number const& number, - std::optional enforce = std::nullopt) + STAmount(A const& asset, Number const& number) : STAmount(asset, number.mantissa(), number.exponent()) { - enforceConversion_ = enforce; - if (!enforce) - { - // Use the default conversion behavior - [[maybe_unused]] - Number const n = *this; - } - else if (enforce == Number::strong) - { - // Throw if it's not valid - if (!validNumber()) - { - Throw( - "STAmount::STAmount integer Number lost precision"); - } - } } // Legacy support for new-style amounts @@ -172,17 +147,6 @@ public: STAmount(XRPAmount const& amount); STAmount(MPTAmount const& amount, MPTIssue const& mptIssue); operator Number() const; - Number - toNumber(Number::EnforceInteger enforce) const; - - void - setIntegerEnforcement(std::optional enforce); - - std::optional - integerEnforcement() const noexcept; - - bool - validNumber() const noexcept; //-------------------------------------------------------------------------- // @@ -193,9 +157,6 @@ public: int exponent() const noexcept; - bool - integral() const noexcept; - bool native() const noexcept; @@ -476,12 +437,6 @@ STAmount::exponent() const noexcept return mOffset; } -inline bool -STAmount::integral() const noexcept -{ - return mAsset.integral(); -} - inline bool STAmount::native() const noexcept { @@ -557,8 +512,6 @@ inline STAmount::operator bool() const noexcept inline STAmount::operator Number() const { - if (enforceConversion_) - return toNumber(*enforceConversion_); if (native()) return xrp(); if (mAsset.holds()) @@ -566,17 +519,6 @@ inline STAmount::operator Number() const return iou(); } -inline Number -STAmount::toNumber(Number::EnforceInteger enforce) const -{ - if (native()) - return xrp().toNumber(enforce); - if (mAsset.holds()) - return mpt().toNumber(enforce); - // It doesn't make sense to enforce limits on IOUs - return iou(); -} - inline STAmount& STAmount::operator=(beast::Zero) { @@ -598,11 +540,6 @@ STAmount::operator=(Number const& number) mValue = mIsNegative ? -number.mantissa() : number.mantissa(); mOffset = number.exponent(); canonicalize(); - - // Convert it back to a Number to check that it's valid - [[maybe_unused]] - Number n = *this; - return *this; } @@ -618,7 +555,7 @@ STAmount::clear() { // The -100 is used to allow 0 to sort less than a small positive values // which have a negative exponent. - mOffset = integral() ? 0 : -100; + mOffset = native() ? 0 : -100; mValue = 0; mIsNegative = false; } diff --git a/include/xrpl/protocol/STNumber.h b/include/xrpl/protocol/STNumber.h index 43b96a2b46..2ec3d66fd1 100644 --- a/include/xrpl/protocol/STNumber.h +++ b/include/xrpl/protocol/STNumber.h @@ -56,18 +56,6 @@ public: bool isDefault() const override; - /// Sets the flag on the underlying number - void - setIntegerEnforcement(Number::EnforceInteger enforce); - - /// Gets the flag value on the underlying number - Number::EnforceInteger - integerEnforcement() const noexcept; - - /// Checks the underlying number - bool - valid() const noexcept; - operator Number() const { return value_; diff --git a/include/xrpl/protocol/SystemParameters.h b/include/xrpl/protocol/SystemParameters.h index 7a2c5a7f63..de78b65265 100644 --- a/include/xrpl/protocol/SystemParameters.h +++ b/include/xrpl/protocol/SystemParameters.h @@ -23,7 +23,6 @@ systemName() /** Number of drops in the genesis account. */ constexpr XRPAmount INITIAL_XRP{100'000'000'000 * DROPS_PER_XRP}; -static_assert(INITIAL_XRP.drops() == 100'000'000'000'000'000); /** Returns true if the amount does not exceed the initial XRP in existence. */ inline bool diff --git a/include/xrpl/protocol/XRPAmount.h b/include/xrpl/protocol/XRPAmount.h index 159174accc..f26bb7366f 100644 --- a/include/xrpl/protocol/XRPAmount.h +++ b/include/xrpl/protocol/XRPAmount.h @@ -146,12 +146,6 @@ public: return {drops(), Number::weak}; } - Number - toNumber(Number::EnforceInteger enforce) const - { - return {value(), enforce}; - } - /** Return the sign of the amount */ constexpr int signum() const noexcept diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index 7c158d4448..9b6a8723c5 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -3454,17 +3454,13 @@ assetsToSharesDeposit( Number const assetTotal = vault->at(sfAssetsTotal); STAmount shares{vault->at(sfShareMPTID)}; - shares.setIntegerEnforcement(Number::weak); if (assetTotal == 0) return STAmount{ shares.asset(), Number(assets.mantissa(), assets.exponent() + vault->at(sfScale)) - .truncate(), - Number::weak}; + .truncate()}; - Number const shareTotal{ - unsafe_cast(issuance->at(sfOutstandingAmount)), - Number::strong}; + Number const shareTotal = issuance->at(sfOutstandingAmount); shares = ((shareTotal * assets) / assetTotal).truncate(); return shares; } @@ -3486,7 +3482,6 @@ sharesToAssetsDeposit( Number const assetTotal = vault->at(sfAssetsTotal); STAmount assets{vault->at(sfAsset)}; - assets.setIntegerEnforcement(Number::weak); if (assetTotal == 0) return STAmount{ assets.asset(), @@ -3494,9 +3489,7 @@ sharesToAssetsDeposit( shares.exponent() - vault->at(sfScale), false}; - Number const shareTotal{ - unsafe_cast(issuance->at(sfOutstandingAmount)), - Number::strong}; + Number const shareTotal = issuance->at(sfOutstandingAmount); assets = (assetTotal * shares) / shareTotal; return assets; } @@ -3520,12 +3513,9 @@ assetsToSharesWithdraw( Number assetTotal = vault->at(sfAssetsTotal); assetTotal -= vault->at(sfLossUnrealized); STAmount shares{vault->at(sfShareMPTID)}; - shares.setIntegerEnforcement(Number::weak); if (assetTotal == 0) return shares; - Number const shareTotal{ - unsafe_cast(issuance->at(sfOutstandingAmount)), - Number::strong}; + Number const shareTotal = issuance->at(sfOutstandingAmount); Number result = (shareTotal * assets) / assetTotal; if (truncate == TruncateShares::yes) result = result.truncate(); @@ -3551,12 +3541,9 @@ sharesToAssetsWithdraw( Number assetTotal = vault->at(sfAssetsTotal); assetTotal -= vault->at(sfLossUnrealized); STAmount assets{vault->at(sfAsset)}; - assets.setIntegerEnforcement(Number::weak); if (assetTotal == 0) return assets; - Number const shareTotal{ - unsafe_cast(issuance->at(sfOutstandingAmount)), - Number::strong}; + Number const shareTotal = issuance->at(sfOutstandingAmount); assets = (assetTotal * shares) / shareTotal; return assets; } diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 545a09a625..566957509c 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -255,25 +255,6 @@ STAmount::move(std::size_t n, void* buf) return emplace(n, buf, std::move(*this)); } -void -STAmount::setIntegerEnforcement(std::optional enforce) -{ - enforceConversion_ = enforce; -} - -std::optional -STAmount::integerEnforcement() const noexcept -{ - return enforceConversion_; -} - -bool -STAmount::validNumber() const noexcept -{ - Number n = toNumber(Number::EnforceInteger::weak); - return n.valid(); -} - //------------------------------------------------------------------------------ // // Conversion diff --git a/src/libxrpl/protocol/STNumber.cpp b/src/libxrpl/protocol/STNumber.cpp index bab99c161d..c353f6b795 100644 --- a/src/libxrpl/protocol/STNumber.cpp +++ b/src/libxrpl/protocol/STNumber.cpp @@ -94,24 +94,6 @@ STNumber::isDefault() const return value_ == Number(); } -void -STNumber::setIntegerEnforcement(Number::EnforceInteger enforce) -{ - value_.setIntegerEnforcement(enforce); -} - -Number::EnforceInteger -STNumber::integerEnforcement() const noexcept -{ - return value_.integerEnforcement(); -} - -bool -STNumber::valid() const noexcept -{ - return value_.valid(); -} - std::ostream& operator<<(std::ostream& out, STNumber const& rhs) { diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 893a50f605..fbc4a5ff9b 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -3713,32 +3713,7 @@ class Vault_test : public beast::unit_test::suite }); testCase(18, [&, this](Env& env, Data d) { - testcase("MPT scale deposit overflow"); - // The computed number of shares can not be represented as an MPT - // without truncation - - { - auto tx = d.vault.deposit( - {.depositor = d.depositor, - .id = d.keylet.key, - .amount = d.asset(5)}); - env(tx, ter{tecPRECISION_LOSS}); - env.close(); - } - }); - - testCase(14, [&, this](Env& env, Data d) { - testcase("MPT scale deposit overflow on first deposit"); - auto tx = d.vault.deposit( - {.depositor = d.depositor, - .id = d.keylet.key, - .amount = d.asset(10)}); - env(tx, ter{tecPRECISION_LOSS}); - env.close(); - }); - - testCase(14, [&, this](Env& env, Data d) { - testcase("MPT scale deposit overflow on second deposit"); + testcase("Scale deposit overflow on second deposit"); { auto tx = d.vault.deposit( @@ -3759,8 +3734,8 @@ class Vault_test : public beast::unit_test::suite } }); - testCase(14, [&, this](Env& env, Data d) { - testcase("No MPT scale deposit overflow on total shares"); + testCase(18, [&, this](Env& env, Data d) { + testcase("Scale deposit overflow on total shares"); { auto tx = d.vault.deposit( @@ -3776,7 +3751,7 @@ class Vault_test : public beast::unit_test::suite {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(5)}); - env(tx); + env(tx, ter{tecPATH_DRY}); env.close(); } }); @@ -4060,28 +4035,6 @@ class Vault_test : public beast::unit_test::suite testCase(18, [&, this](Env& env, Data d) { testcase("Scale withdraw overflow"); - { - auto tx = d.vault.deposit( - {.depositor = d.depositor, - .id = d.keylet.key, - .amount = d.asset(5)}); - env(tx, ter{tecPRECISION_LOSS}); - env.close(); - } - - { - auto tx = d.vault.withdraw( - {.depositor = d.depositor, - .id = d.keylet.key, - .amount = STAmount(d.asset, Number(10, 0))}); - env(tx, ter{tecPRECISION_LOSS}); - env.close(); - } - }); - - testCase(14, [&, this](Env& env, Data d) { - testcase("MPT scale withdraw overflow"); - { auto tx = d.vault.deposit( {.depositor = d.depositor, @@ -4300,29 +4253,6 @@ class Vault_test : public beast::unit_test::suite testCase(18, [&, this](Env& env, Data d) { testcase("Scale clawback overflow"); - { - auto tx = d.vault.deposit( - {.depositor = d.depositor, - .id = d.keylet.key, - .amount = d.asset(5)}); - env(tx, ter(tecPRECISION_LOSS)); - env.close(); - } - - { - auto tx = d.vault.clawback( - {.issuer = d.issuer, - .id = d.keylet.key, - .holder = d.depositor, - .amount = STAmount(d.asset, Number(10, 0))}); - env(tx, ter{tecPRECISION_LOSS}); - env.close(); - } - }); - - testCase(14, [&, this](Env& env, Data d) { - testcase("MPT Scale clawback overflow"); - { auto tx = d.vault.deposit( {.depositor = d.depositor, diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index a505ff522d..968e621657 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -967,7 +967,7 @@ public: { BEAST_EXPECT(e.what() == "Number::operator= integer overflow"s); // The throw is done _after_ the number is updated. - BEAST_EXPECT((a == Number::maxIntValue * 2)); + BEAST_EXPECT((a == Number{2, 14})); BEAST_EXPECT(!a.valid()); } try @@ -979,7 +979,7 @@ public: { BEAST_EXPECT(e.what() == "Number::Number integer overflow"s); // The Number doesn't get updated because the ctor throws - BEAST_EXPECT((a == Number::maxIntValue * 2)); + BEAST_EXPECT((a == Number{2, 14})); BEAST_EXPECT(!a.valid()); } a = Number(1, 10); diff --git a/src/xrpld/app/tx/detail/VaultClawback.cpp b/src/xrpld/app/tx/detail/VaultClawback.cpp index 1c73d36bb0..7c56ca1d60 100644 --- a/src/xrpld/app/tx/detail/VaultClawback.cpp +++ b/src/xrpld/app/tx/detail/VaultClawback.cpp @@ -71,13 +71,9 @@ VaultClawback::preclaim(PreclaimContext const& ctx) } Asset const vaultAsset = vault->at(sfAsset); - if (auto const amount = ctx.tx[~sfAmount]) - { - if (vaultAsset != amount->asset()) - return tecWRONG_ASSET; - else if (!amount->validNumber()) - return tecPRECISION_LOSS; - } + if (auto const amount = ctx.tx[~sfAmount]; + amount && vaultAsset != amount->asset()) + return tecWRONG_ASSET; if (vaultAsset.native()) { @@ -161,8 +157,6 @@ VaultClawback::doApply() MPTIssue const share{mptIssuanceID}; STAmount sharesDestroyed = {share}; STAmount assetsRecovered; - assetsRecovered.setIntegerEnforcement(Number::weak); - sharesDestroyed.setIntegerEnforcement(Number::weak); try { if (amount == beast::zero) @@ -175,9 +169,6 @@ VaultClawback::doApply() AuthHandling::ahIGNORE_AUTH, j_); - if (!sharesDestroyed.validNumber()) - return tecPRECISION_LOSS; - auto const maybeAssets = sharesToAssetsWithdraw(vault, sleIssuance, sharesDestroyed); if (!maybeAssets) @@ -193,8 +184,6 @@ VaultClawback::doApply() if (!maybeShares) return tecINTERNAL; // LCOV_EXCL_LINE sharesDestroyed = *maybeShares; - if (!sharesDestroyed.validNumber()) - return tecPRECISION_LOSS; } auto const maybeAssets = @@ -203,8 +192,6 @@ VaultClawback::doApply() return tecINTERNAL; // LCOV_EXCL_LINE assetsRecovered = *maybeAssets; } - if (!assetsRecovered.validNumber()) - return tecPRECISION_LOSS; // Clamp to maximum. if (assetsRecovered > *assetsAvailable) diff --git a/src/xrpld/app/tx/detail/VaultDeposit.cpp b/src/xrpld/app/tx/detail/VaultDeposit.cpp index 8e42fd188d..3e5ae741e3 100644 --- a/src/xrpld/app/tx/detail/VaultDeposit.cpp +++ b/src/xrpld/app/tx/detail/VaultDeposit.cpp @@ -42,9 +42,6 @@ VaultDeposit::preclaim(PreclaimContext const& ctx) if (assets.asset() != vaultAsset) return tecWRONG_ASSET; - if (!assets.validNumber()) - return tecPRECISION_LOSS; - if (vaultAsset.native()) ; // No special checks for XRP else if (vaultAsset.holds()) @@ -220,7 +217,6 @@ VaultDeposit::doApply() } STAmount sharesCreated = {vault->at(sfShareMPTID)}, assetsDeposited; - sharesCreated.setIntegerEnforcement(Number::weak); try { // Compute exchange before transferring any amounts. @@ -231,14 +227,14 @@ VaultDeposit::doApply() return tecINTERNAL; // LCOV_EXCL_LINE sharesCreated = *maybeShares; } - if (sharesCreated == beast::zero || !sharesCreated.validNumber()) + if (sharesCreated == beast::zero) return tecPRECISION_LOSS; auto const maybeAssets = sharesToAssetsDeposit(vault, sleIssuance, sharesCreated); if (!maybeAssets) return tecINTERNAL; // LCOV_EXCL_LINE - else if (*maybeAssets > amount || !maybeAssets->validNumber()) + else if (*maybeAssets > amount) { // LCOV_EXCL_START JLOG(j_.error()) << "VaultDeposit: would take more than offered."; @@ -264,23 +260,13 @@ VaultDeposit::doApply() sharesCreated.asset() != assetsDeposited.asset(), "ripple::VaultDeposit::doApply : assets are not shares"); - auto assetsTotalProxy = vault->at(sfAssetsTotal); - auto assetsAvailableProxy = vault->at(sfAssetsAvailable); - if (vault->at(sfAsset).value().integral()) - { - assetsTotalProxy.value().setIntegerEnforcement(Number::weak); - assetsAvailableProxy.value().setIntegerEnforcement(Number::weak); - } - assetsTotalProxy += assetsDeposited; - assetsAvailableProxy += assetsDeposited; - if (!assetsTotalProxy.value().valid() || - !assetsAvailableProxy.value().valid()) - return tecLIMIT_EXCEEDED; + vault->at(sfAssetsTotal) += assetsDeposited; + vault->at(sfAssetsAvailable) += assetsDeposited; view().update(vault); // A deposit must not push the vault over its limit. auto const maximum = *vault->at(sfAssetsMaximum); - if (maximum != 0 && *assetsTotalProxy > maximum) + if (maximum != 0 && *vault->at(sfAssetsTotal) > maximum) return tecLIMIT_EXCEEDED; // Transfer assets from depositor to vault. diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.cpp b/src/xrpld/app/tx/detail/VaultWithdraw.cpp index 04857d400e..dc8cbde7c0 100644 --- a/src/xrpld/app/tx/detail/VaultWithdraw.cpp +++ b/src/xrpld/app/tx/detail/VaultWithdraw.cpp @@ -47,9 +47,6 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx) if (assets.asset() != vaultAsset && assets.asset() != vaultShare) return tecWRONG_ASSET; - if (!assets.validNumber()) - return tecPRECISION_LOSS; - if (vaultAsset.native()) ; // No special checks for XRP else if (vaultAsset.holds()) @@ -142,8 +139,6 @@ VaultWithdraw::doApply() MPTIssue const share{mptIssuanceID}; STAmount sharesRedeemed = {share}; STAmount assetsWithdrawn; - assetsWithdrawn.setIntegerEnforcement(Number::weak); - sharesRedeemed.setIntegerEnforcement(Number::weak); try { if (amount.asset() == vaultAsset) @@ -157,15 +152,13 @@ VaultWithdraw::doApply() sharesRedeemed = *maybeShares; } - if (sharesRedeemed == beast::zero || !sharesRedeemed.validNumber()) + if (sharesRedeemed == beast::zero) return tecPRECISION_LOSS; auto const maybeAssets = sharesToAssetsWithdraw(vault, sleIssuance, sharesRedeemed); if (!maybeAssets) return tecINTERNAL; // LCOV_EXCL_LINE assetsWithdrawn = *maybeAssets; - if (!assetsWithdrawn.validNumber()) - return tecPRECISION_LOSS; } else if (amount.asset() == share) { @@ -176,8 +169,6 @@ VaultWithdraw::doApply() if (!maybeAssets) return tecINTERNAL; // LCOV_EXCL_LINE assetsWithdrawn = *maybeAssets; - if (!assetsWithdrawn.validNumber()) - return tecPRECISION_LOSS; } else return tefINTERNAL; // LCOV_EXCL_LINE From 814577758fc2f62ac5093b331836bf1de7855f50 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 12 Nov 2025 19:15:03 -0500 Subject: [PATCH 224/291] Revert "Fix build error - avoid copy" This reverts commit cb6df196dc00dad4ce4e48d6623b88b765e9a27c. --- src/test/app/AMM_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index 66bceec327..5a1816ebae 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -1384,7 +1384,7 @@ private: // equal asset deposit: unit test to exercise the rounding-down of // LPTokens in the AMMHelpers.cpp: adjustLPTokens calculations // The LPTokens need to have 16 significant digits and a fractional part - for (Number const& deltaLPTokens : + for (Number const deltaLPTokens : {Number{UINT64_C(100000'0000000009), -10}, Number{UINT64_C(100000'0000000001), -10}}) { From 1b4e18a1a7c9e58dfe87c2d7d5d3895f37a17511 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 12 Nov 2025 19:15:06 -0500 Subject: [PATCH 225/291] Revert "Add integer enforcement when converting to XRP/MPTAmount to Number" This reverts commit b605a2cdcc2ac8ca27e3b44036b2ea375069ac89. --- include/xrpl/protocol/MPTAmount.h | 2 +- include/xrpl/protocol/XRPAmount.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/xrpl/protocol/MPTAmount.h b/include/xrpl/protocol/MPTAmount.h index 1c7c7a25e7..af14786501 100644 --- a/include/xrpl/protocol/MPTAmount.h +++ b/include/xrpl/protocol/MPTAmount.h @@ -64,7 +64,7 @@ public: operator Number() const noexcept { - return {value(), Number::strong}; + return value(); } /** Return the sign of the amount */ diff --git a/include/xrpl/protocol/XRPAmount.h b/include/xrpl/protocol/XRPAmount.h index f26bb7366f..e102a3707f 100644 --- a/include/xrpl/protocol/XRPAmount.h +++ b/include/xrpl/protocol/XRPAmount.h @@ -143,7 +143,7 @@ public: operator Number() const noexcept { - return {drops(), Number::weak}; + return drops(); } /** Return the sign of the amount */ From 9b0b7b5a91276903842c42d30c12abebd5bd87e5 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 12 Nov 2025 19:15:07 -0500 Subject: [PATCH 226/291] Revert "Make all STNumber fields "soeDEFAULT"" This reverts commit 24f37d73f614dafbd522068781532e06584ce68e. --- include/xrpl/protocol/detail/ledger_entries.macro | 6 +++--- src/test/app/Vault_test.cpp | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 5f6d1b844e..011b3f5ea6 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -480,10 +480,10 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({ {sfAccount, soeREQUIRED}, {sfData, soeOPTIONAL}, {sfAsset, soeREQUIRED}, - {sfAssetsTotal, soeDEFAULT}, - {sfAssetsAvailable, soeDEFAULT}, + {sfAssetsTotal, soeREQUIRED}, + {sfAssetsAvailable, soeREQUIRED}, {sfAssetsMaximum, soeDEFAULT}, - {sfLossUnrealized, soeDEFAULT}, + {sfLossUnrealized, soeREQUIRED}, {sfShareMPTID, soeREQUIRED}, {sfWithdrawalPolicy, soeREQUIRED}, {sfScale, soeDEFAULT}, diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index fbc4a5ff9b..dec47f8835 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -4554,8 +4554,7 @@ class Vault_test : public beast::unit_test::suite BEAST_EXPECT(checkString(vault, sfAssetsAvailable, "50")); BEAST_EXPECT(checkString(vault, sfAssetsMaximum, "1000")); BEAST_EXPECT(checkString(vault, sfAssetsTotal, "50")); - // Since this field is default, it is not returned. - BEAST_EXPECT(!vault.isMember(sfLossUnrealized.getJsonName())); + BEAST_EXPECT(checkString(vault, sfLossUnrealized, "0")); auto const strShareID = strHex(sle->at(sfShareMPTID)); BEAST_EXPECT(checkString(vault, sfShareMPTID, strShareID)); From 398170ef3d97787a9ee8e67afdfee522548c404b Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 12 Nov 2025 19:15:14 -0500 Subject: [PATCH 227/291] Revert "Add optional enforcement of valid integer range to Number" This reverts commit 3cb447a4fe7866a533ed9a577ca392d3898cebe5. --- include/xrpl/basics/Number.h | 116 ++-------------------- src/libxrpl/basics/Number.cpp | 40 -------- src/test/basics/Number_test.cpp | 169 -------------------------------- 3 files changed, 7 insertions(+), 318 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index 1ea43a1e93..080a807909 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -13,47 +13,16 @@ class Number; std::string to_string(Number const& amount); -template -constexpr bool -isPowerOfTen(T value) -{ - while (value >= 10 && value % 10 == 0) - value /= 10; - return value == 1; -} - class Number { -public: - /** Describes whether and how to enforce this number as an integer. - * - * - none: No enforcement. The value may vary freely. This is the default. - * - weak: If the absolute value is greater than maxIntValue, valid() will - * return false. - * - strong: Assignment operations will throw if the absolute value is above - * maxIntValue. - */ - enum EnforceInteger { none, weak, strong }; - -private: using rep = std::int64_t; rep mantissa_{0}; int exponent_{std::numeric_limits::lowest()}; - // The enforcement setting is not serialized, and does not affect the - // ledger. If not "none", the value is checked to be within the valid - // integer range. With "strong", the checks will be made as automatic as - // possible. - EnforceInteger enforceInteger_ = none; - public: // The range for the mantissa when normalized - constexpr static rep minMantissa = 1'000'000'000'000'000LL; - static_assert(isPowerOfTen(minMantissa)); - constexpr static rep maxMantissa = minMantissa * 10 - 1; - static_assert(maxMantissa == 9'999'999'999'999'999LL); - - constexpr static rep maxIntValue = minMantissa / 10; + constexpr static std::int64_t minMantissa = 1'000'000'000'000'000LL; + constexpr static std::int64_t maxMantissa = 9'999'999'999'999'999LL; // The range for the exponent when normalized constexpr static int minExponent = -32768; @@ -66,33 +35,15 @@ public: explicit constexpr Number() = default; - Number(rep mantissa, EnforceInteger enforce = none); - explicit Number(rep mantissa, int exponent, EnforceInteger enforce = none); + Number(rep mantissa); + explicit Number(rep mantissa, int exponent); explicit constexpr Number(rep mantissa, int exponent, unchecked) noexcept; - constexpr Number(Number const& other) = default; - constexpr Number(Number&& other) = default; - - ~Number() = default; - - constexpr Number& - operator=(Number const& other); - constexpr Number& - operator=(Number&& other); constexpr rep mantissa() const noexcept; constexpr int exponent() const noexcept; - void - setIntegerEnforcement(EnforceInteger enforce); - - EnforceInteger - integerEnforcement() const noexcept; - - bool - valid() const noexcept; - constexpr Number operator+() const noexcept; constexpr Number @@ -218,9 +169,6 @@ public: private: static thread_local rounding_mode mode_; - void - checkInteger(char const* what) const; - void normalize(); constexpr bool @@ -236,52 +184,16 @@ inline constexpr Number::Number(rep mantissa, int exponent, unchecked) noexcept { } -inline Number::Number(rep mantissa, int exponent, EnforceInteger enforce) - : mantissa_{mantissa}, exponent_{exponent}, enforceInteger_(enforce) +inline Number::Number(rep mantissa, int exponent) + : mantissa_{mantissa}, exponent_{exponent} { normalize(); - - checkInteger("Number::Number integer overflow"); } -inline Number::Number(rep mantissa, EnforceInteger enforce) - : Number{mantissa, 0, enforce} +inline Number::Number(rep mantissa) : Number{mantissa, 0} { } -constexpr Number& -Number::operator=(Number const& other) -{ - if (this != &other) - { - mantissa_ = other.mantissa_; - exponent_ = other.exponent_; - enforceInteger_ = std::max(enforceInteger_, other.enforceInteger_); - - checkInteger("Number::operator= integer overflow"); - } - - return *this; -} - -constexpr Number& -Number::operator=(Number&& other) -{ - if (this != &other) - { - // std::move doesn't really do anything for these types, but - // this is future-proof in case the types ever change - mantissa_ = std::move(other.mantissa_); - exponent_ = std::move(other.exponent_); - if (other.enforceInteger_ > enforceInteger_) - enforceInteger_ = std::move(other.enforceInteger_); - - checkInteger("Number::operator= integer overflow"); - } - - return *this; -} - inline constexpr Number::rep Number::mantissa() const noexcept { @@ -294,20 +206,6 @@ Number::exponent() const noexcept return exponent_; } -inline void -Number::setIntegerEnforcement(EnforceInteger enforce) -{ - enforceInteger_ = enforce; - - checkInteger("Number::setIntegerEnforcement integer overflow"); -} - -inline Number::EnforceInteger -Number::integerEnforcement() const noexcept -{ - return enforceInteger_; -} - inline constexpr Number Number::operator+() const noexcept { diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 2587af2e70..797ca83b67 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -222,13 +222,6 @@ Number::Guard::doRound(rep& drops) constexpr Number one{1000000000000000, -15, Number::unchecked{}}; -void -Number::checkInteger(char const* what) const -{ - if (enforceInteger_ == strong && !valid()) - throw std::overflow_error(what); -} - void Number::normalize() { @@ -270,27 +263,9 @@ Number::normalize() mantissa_ = -mantissa_; } -bool -Number::valid() const noexcept -{ - if (enforceInteger_ != none) - { - static Number const max = maxIntValue; - static Number const maxNeg = -maxIntValue; - // Avoid making a copy - if (mantissa_ < 0) - return *this >= maxNeg; - return *this <= max; - } - return true; -} - Number& Number::operator+=(Number const& y) { - // The strictest setting prevails - enforceInteger_ = std::max(enforceInteger_, y.enforceInteger_); - if (y == Number{}) return *this; if (*this == Number{}) @@ -378,9 +353,6 @@ Number::operator+=(Number const& y) } mantissa_ = xm * xn; exponent_ = xe; - - checkInteger("Number::addition integer overflow"); - return *this; } @@ -415,9 +387,6 @@ divu10(uint128_t& u) Number& Number::operator*=(Number const& y) { - // The strictest setting prevails - enforceInteger_ = std::max(enforceInteger_, y.enforceInteger_); - if (*this == Number{}) return *this; if (y == Number{}) @@ -469,18 +438,12 @@ Number::operator*=(Number const& y) XRPL_ASSERT( isnormal() || *this == Number{}, "ripple::Number::operator*=(Number) : result is normal"); - - checkInteger("Number::multiplication integer overflow"); - return *this; } Number& Number::operator/=(Number const& y) { - // The strictest setting prevails - enforceInteger_ = std::max(enforceInteger_, y.enforceInteger_); - if (y == Number{}) throw std::overflow_error("Number: divide by 0"); if (*this == Number{}) @@ -508,9 +471,6 @@ Number::operator/=(Number const& y) exponent_ = ne - de - 17; mantissa_ *= np * dp; normalize(); - - checkInteger("Number::division integer overflow"); - return *this; } diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 968e621657..cec69fe05f 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -2,7 +2,6 @@ #include #include #include -#include #include #include @@ -835,173 +834,6 @@ public: } } - void - testInteger() - { - testcase("Integer enforcement"); - - using namespace std::string_literals; - - { - Number a{100}; - BEAST_EXPECT(a.integerEnforcement() == Number::none); - BEAST_EXPECT(a.valid()); - a = Number{1, 30}; - BEAST_EXPECT(a.valid()); - a = -100; - BEAST_EXPECT(a.valid()); - } - { - Number a{100, Number::weak}; - BEAST_EXPECT(a.integerEnforcement() == Number::weak); - BEAST_EXPECT(a.valid()); - a = Number{1, 30, Number::none}; - BEAST_EXPECT(!a.valid()); - a = -100; - BEAST_EXPECT(a.integerEnforcement() == Number::weak); - BEAST_EXPECT(a.valid()); - a = Number{5, Number::strong}; - BEAST_EXPECT(a.integerEnforcement() == Number::strong); - BEAST_EXPECT(a.valid()); - } - { - Number a{100, Number::strong}; - BEAST_EXPECT(a.integerEnforcement() == Number::strong); - BEAST_EXPECT(a.valid()); - try - { - a = Number{1, 30}; - BEAST_EXPECT(false); - } - catch (std::overflow_error const& e) - { - BEAST_EXPECT(e.what() == "Number::operator= integer overflow"s); - // The throw is done _after_ the number is updated. - BEAST_EXPECT((a == Number{1, 30})); - } - BEAST_EXPECT(!a.valid()); - a = -100; - BEAST_EXPECT(a.integerEnforcement() == Number::strong); - BEAST_EXPECT(a.valid()); - } - { - Number a{INITIAL_XRP.drops(), Number::weak}; - BEAST_EXPECT(!a.valid()); - a = -a; - BEAST_EXPECT(!a.valid()); - - try - { - a.setIntegerEnforcement(Number::strong); - BEAST_EXPECT(false); - } - catch (std::overflow_error const& e) - { - BEAST_EXPECT( - e.what() == - "Number::setIntegerEnforcement integer overflow"s); - // The throw is internal to the operator before the result is - // assigned to the Number - BEAST_EXPECT(a == -INITIAL_XRP); - BEAST_EXPECT(!a.valid()); - } - try - { - ++a; - BEAST_EXPECT(false); - } - catch (std::overflow_error const& e) - { - BEAST_EXPECT(e.what() == "Number::addition integer overflow"s); - // The throw is internal to the operator before the result is - // assigned to the Number - BEAST_EXPECT(a == -INITIAL_XRP); - BEAST_EXPECT(!a.valid()); - } - a = Number::maxIntValue; - try - { - ++a; - BEAST_EXPECT(false); - } - catch (std::overflow_error const& e) - { - BEAST_EXPECT(e.what() == "Number::addition integer overflow"s); - // This time, the throw is done _after_ the number is updated. - BEAST_EXPECT(a == Number::maxIntValue + 1); - BEAST_EXPECT(!a.valid()); - } - a = -Number::maxIntValue; - try - { - --a; - BEAST_EXPECT(false); - } - catch (std::overflow_error const& e) - { - BEAST_EXPECT(e.what() == "Number::addition integer overflow"s); - // This time, the throw is done _after_ the number is updated. - BEAST_EXPECT(a == -Number::maxIntValue - 1); - BEAST_EXPECT(!a.valid()); - } - a = Number(1, 10); - try - { - a *= Number(1, 10); - BEAST_EXPECT(false); - } - catch (std::overflow_error const& e) - { - BEAST_EXPECT( - e.what() == "Number::multiplication integer overflow"s); - // The throw is done _after_ the number is updated. - BEAST_EXPECT((a == Number{1, 20})); - BEAST_EXPECT(!a.valid()); - } - try - { - a = Number::maxIntValue * 2; - BEAST_EXPECT(false); - } - catch (std::overflow_error const& e) - { - BEAST_EXPECT(e.what() == "Number::operator= integer overflow"s); - // The throw is done _after_ the number is updated. - BEAST_EXPECT((a == Number{2, 14})); - BEAST_EXPECT(!a.valid()); - } - try - { - a = Number(3, 15, Number::strong); - BEAST_EXPECT(false); - } - catch (std::overflow_error const& e) - { - BEAST_EXPECT(e.what() == "Number::Number integer overflow"s); - // The Number doesn't get updated because the ctor throws - BEAST_EXPECT((a == Number{2, 14})); - BEAST_EXPECT(!a.valid()); - } - a = Number(1, 10); - try - { - a /= Number(1, -10); - BEAST_EXPECT(false); - } - catch (std::overflow_error const& e) - { - BEAST_EXPECT(e.what() == "Number::division integer overflow"s); - // The throw is done _after_ the number is updated. - BEAST_EXPECT((a == Number{1, 20})); - BEAST_EXPECT(!a.valid()); - } - a /= Number(1, 15); - BEAST_EXPECT((a == Number{1, 5})); - BEAST_EXPECT(a.valid()); - } - } - - void run() override { testZero(); @@ -1023,7 +855,6 @@ public: test_toSTAmount(); test_truncate(); testRounding(); - testInteger(); } }; From a4aa72eada60c4d0aaf90ead3d2d62b3c0c9136e Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 12 Nov 2025 19:19:06 -0500 Subject: [PATCH 228/291] Fix revert issues --- include/xrpl/basics/Number.h | 13 ++++++++++++- include/xrpl/protocol/Asset.h | 6 ++++++ include/xrpl/protocol/STAmount.h | 9 +++++++++ src/test/basics/Number_test.cpp | 1 + 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index 080a807909..4a4ee1cacb 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -13,6 +13,15 @@ class Number; std::string to_string(Number const& amount); +template +constexpr bool +isPowerOfTen(T value) +{ + while (value >= 10 && value % 10 == 0) + value /= 10; + return value == 1; +} + class Number { using rep = std::int64_t; @@ -22,7 +31,9 @@ class Number public: // The range for the mantissa when normalized constexpr static std::int64_t minMantissa = 1'000'000'000'000'000LL; - constexpr static std::int64_t maxMantissa = 9'999'999'999'999'999LL; + static_assert(isPowerOfTen(minMantissa)); + constexpr static std::int64_t maxMantissa = minMantissa * 10 - 1; + static_assert(maxMantissa == 9'999'999'999'999'999LL); // The range for the exponent when normalized constexpr static int minExponent = -32768; diff --git a/include/xrpl/protocol/Asset.h b/include/xrpl/protocol/Asset.h index d0efe814a9..6765f7cf7d 100644 --- a/include/xrpl/protocol/Asset.h +++ b/include/xrpl/protocol/Asset.h @@ -84,6 +84,12 @@ public: return holds() && get().native(); } + bool + integral() const + { + return !holds() || get().native(); + } + friend constexpr bool operator==(Asset const& lhs, Asset const& rhs); diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index d12e218250..f3f285249c 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -157,6 +157,9 @@ public: int exponent() const noexcept; + bool + integral() const noexcept; + bool native() const noexcept; @@ -437,6 +440,12 @@ STAmount::exponent() const noexcept return mOffset; } +inline bool +STAmount::integral() const noexcept +{ + return mAsset.integral(); +} + inline bool STAmount::native() const noexcept { diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index cec69fe05f..bb5262d028 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -834,6 +834,7 @@ public: } } + void run() override { testZero(); From ad124ce7b24c5824c0536232f74b0b48216b3d11 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 13 Nov 2025 01:51:07 -0500 Subject: [PATCH 229/291] Fix balance checking assertion in LoanPay when issuer is borrower - Resolves RIPD-4096 --- src/test/app/Loan_test.cpp | 140 ++++++++++++++++++++++------ src/xrpld/app/tx/detail/LoanPay.cpp | 3 +- 2 files changed, 114 insertions(+), 29 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index da8b964c94..6e1485abdf 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -611,8 +611,10 @@ protected: auto const limit = asset( 100 * (brokerParams.vaultDeposit + brokerParams.coverDeposit)); - env(trust(lender, limit)); - env(trust(borrower, limit)); + if (lender != issuer) + env(trust(lender, limit)); + if (borrower != issuer) + env(trust(borrower, limit)); return asset; } @@ -639,8 +641,10 @@ protected: PrettyAsset const asset{mptt.issuanceID(), 10'000}; // Need to do the authorization here because mptt isn't // accessible outside - mptt.authorize({.account = lender}); - mptt.authorize({.account = borrower}); + if (lender != issuer) + mptt.authorize({.account = lender}); + if (borrower != issuer) + mptt.authorize({.account = borrower}); env.close(); @@ -657,17 +661,15 @@ protected: jtx::Env& env, BrokerParameters const& brokerParams, LoanParameters const& loanParams, - AssetType assetType) + AssetType assetType, + jtx::Account const& issuer, + jtx::Account const& lender, + jtx::Account const& borrower) { using namespace jtx; - auto const asset = createAsset( - env, - assetType, - brokerParams, - Account("issuer"), - Account("lender"), - Account("borrower")); + auto const asset = + createAsset(env, assetType, brokerParams, issuer, lender, borrower); auto const principal = asset(loanParams.principalRequest).number(); auto const interest = loanParams.interest.value_or(TenthBips32{}); auto const interval = @@ -720,20 +722,27 @@ protected: using namespace jtx; // Enough to cover initial fees - env.fund( - env.current()->fees().accountReserve(10) * 10, - issuer, - noripple(lender, borrower)); + env.fund(env.current()->fees().accountReserve(10) * 10, issuer); + if (lender != issuer) + env.fund( + env.current()->fees().accountReserve(10) * 10, + noripple(lender)); + if (borrower != issuer && borrower != lender) + env.fund( + env.current()->fees().accountReserve(10) * 10, + noripple(borrower)); - describeLoan(env, brokerParams, loanParams, assetType); + describeLoan( + env, brokerParams, loanParams, assetType, issuer, lender, borrower); // Make the asset auto const asset = createAsset(env, assetType, brokerParams, issuer, lender, borrower); env.close(); - env( - pay((asset.native() ? env.master : issuer), + if (asset.native() || lender != issuer) + env(pay( + (asset.native() ? env.master : issuer), lender, asset(brokerParams.vaultDeposit + brokerParams.coverDeposit))); // Fund the borrower later once we know the total loan @@ -808,7 +817,8 @@ protected: auto const shortage = totalNeeded - borrowerBalance.number(); - if (shortage > beast::zero) + if (shortage > beast::zero && + (broker.asset.native() || issuer != borrower)) env( pay((broker.asset.native() ? env.master : issuer), borrower, @@ -822,6 +832,9 @@ protected: LoanParameters const& loanParams, Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus, + jtx::Account const& issuer, + jtx::Account const& lender, + jtx::Account const& borrower, bool showStepBalances = false) { // Make all the individual payments @@ -830,9 +843,6 @@ protected: using namespace std::chrono_literals; using d = NetClock::duration; - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; // Account const evan{"evan"}; // Account const alice{"alice"}; @@ -939,6 +949,8 @@ protected: broker.params.managementFeeRate); auto validateBorrowerBalance = [&]() { + if (borrower == issuer) + return; auto const totalSpent = (totalPaid.trackedValueDelta + totalFeesPaid + (broker.asset.native() ? Number(baseFee) * totalPaymentsMade @@ -1213,7 +1225,15 @@ protected: VerifyLoanStatus verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); - makeLoanPayments(env, broker, loanParams, loanKeylet, verifyLoanStatus); + makeLoanPayments( + env, + broker, + loanParams, + loanKeylet, + verifyLoanStatus, + issuer, + lender, + borrower); } /** Runs through the complete lifecycle of a loan @@ -6428,8 +6448,6 @@ protected: .principalRequest = Number{100'000, -4}, .interest = TenthBips32{100'000}, .payTotal = 10, - // Guess - // .payInterval = 10, .gracePd = 0}; auto const assetType = AssetType::MPT; @@ -6460,7 +6478,15 @@ protected: } makeLoanPayments( - env, broker, loanParams, loanKeylet, verifyLoanStatus, true); + env, + broker, + loanParams, + loanKeylet, + verifyLoanStatus, + issuer, + lender, + borrower, + true); if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle)) @@ -6665,7 +6691,15 @@ protected: VerifyLoanStatus verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); makeLoanPayments( - env, broker, loanParams, loanKeylet, verifyLoanStatus, true); + env, + broker, + loanParams, + loanKeylet, + verifyLoanStatus, + issuer, + lender, + borrower, + true); } void @@ -6793,6 +6827,55 @@ protected: } } + void + testIssuerIsBorrower() + { + testcase("RIPD-4096 - Issuer as borrower"); + + using namespace jtx; + + Account const issuer("issuer"); + Account const lender("lender"); + + BrokerParameters const brokerParams{ + .vaultDeposit = 100'000, + .debtMax = 0, + .coverRateMin = TenthBips32{0}, + .managementFeeRate = TenthBips16{0}, + .coverRateLiquidation = TenthBips32{0}}; + LoanParameters const loanParams{ + .account = lender, + .counter = issuer, + .principalRequest = Number{10000}}; + + auto const assetType = AssetType::IOU; + + Env env(*this, all); + + auto loanResult = createLoan( + env, assetType, brokerParams, loanParams, issuer, lender, issuer); + + if (!BEAST_EXPECT(loanResult)) + return; + + auto broker = std::get(*loanResult); + auto loanKeylet = std::get(*loanResult); + auto pseudoAcct = std::get(*loanResult); + + VerifyLoanStatus verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); + + makeLoanPayments( + env, + broker, + loanParams, + loanKeylet, + verifyLoanStatus, + issuer, + lender, + issuer, + true); + } + public: void run() override @@ -6839,6 +6922,7 @@ public: testRIPD3902(); testRoundingAllowsUndercoverage(); testBorrowerIsBroker(); + testIssuerIsBorrower(); } }; diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 64e18016e1..0f3a6a3819 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -553,7 +553,8 @@ LoanPay::doApply() "ripple::LoanPay::doApply", "positive account balance"); XRPL_ASSERT_PARTS( - accountBalanceAfter < accountBalanceBefore, + accountBalanceAfter < accountBalanceBefore || + account_ == asset.getIssuer(), "ripple::LoanPay::doApply", "account balance decreased"); XRPL_ASSERT_PARTS( From 1c1cd1a391e25c833b66c00345aa99480df5e914 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 13 Nov 2025 12:15:39 -0500 Subject: [PATCH 230/291] Change Vault asset fields back to default - Was unintentionally rolled back with `Number` changes. --- include/xrpl/protocol/detail/ledger_entries.macro | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 011b3f5ea6..5f6d1b844e 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -480,10 +480,10 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({ {sfAccount, soeREQUIRED}, {sfData, soeOPTIONAL}, {sfAsset, soeREQUIRED}, - {sfAssetsTotal, soeREQUIRED}, - {sfAssetsAvailable, soeREQUIRED}, + {sfAssetsTotal, soeDEFAULT}, + {sfAssetsAvailable, soeDEFAULT}, {sfAssetsMaximum, soeDEFAULT}, - {sfLossUnrealized, soeREQUIRED}, + {sfLossUnrealized, soeDEFAULT}, {sfShareMPTID, soeREQUIRED}, {sfWithdrawalPolicy, soeREQUIRED}, {sfScale, soeDEFAULT}, From a868b1938d37f332f11f4d94e7f58a3ecbf7fb0c Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 13 Nov 2025 14:21:41 -0500 Subject: [PATCH 231/291] Fix Vault test failure due to default field not being in RPC Json --- src/test/app/Vault_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index dec47f8835..8566f82bd7 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -4554,7 +4554,7 @@ class Vault_test : public beast::unit_test::suite BEAST_EXPECT(checkString(vault, sfAssetsAvailable, "50")); BEAST_EXPECT(checkString(vault, sfAssetsMaximum, "1000")); BEAST_EXPECT(checkString(vault, sfAssetsTotal, "50")); - BEAST_EXPECT(checkString(vault, sfLossUnrealized, "0")); + BEAST_EXPECT(!vault.isMember(sfLossUnrealized.getJsonName())); auto const strShareID = strHex(sle->at(sfShareMPTID)); BEAST_EXPECT(checkString(vault, sfShareMPTID, strShareID)); From c65a311ad19da2526433048f6f93b3da48761f92 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 13 Nov 2025 14:26:47 -0500 Subject: [PATCH 232/291] Remove a bunch of obsolete code, marked by "#if LOANCOMPLETE" --- src/test/app/Loan_test.cpp | 29 -- src/xrpld/app/misc/LendingHelpers.h | 6 - src/xrpld/app/misc/detail/LendingHelpers.cpp | 370 ------------------- 3 files changed, 405 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 6e1485abdf..09031e0c53 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -1354,28 +1354,6 @@ protected: auto const borrowerStartbalance = env.balance(borrower, broker.asset); auto createJtx = loanParams(env, broker); -#if LOANCOMPLETE - { - auto createJtxOld = env.jt( - set(borrower, broker.brokerID, principalRequest, flags), - sig(sfCounterpartySignature, lender), - loanOriginationFee(originationFee), - loanServiceFee(serviceFee), - latePaymentFee(lateFee), - closePaymentFee(closeFee), - overpaymentFee(overFee), - interestRate(interest), - lateInterestRate(lateInterest), - closeInterestRate(closeInterest), - overpaymentInterestRate(overpaymentInterest), - paymentTotal(total), - paymentInterval(interval), - gracePeriod(grace), - fee(loanSetFee)); - BEAST_EXPECT( - createJtx.stx->getJson(0) == createJtxOld.stx->getJson(0)); - } -#endif // Successfully create a Loan env(createJtx); @@ -7038,13 +7016,6 @@ class LoanArbitrary_test : public LoanBatch_test { using namespace jtx; -#if LOANCOMPLETE - BEAST_EXPECT( - std::numeric_limits::max() > INITIAL_XRP.drops()); - BEAST_EXPECT(Number::maxMantissa > INITIAL_XRP.drops()); - Number initalXrp{INITIAL_XRP}; - BEAST_EXPECT(initalXrp.exponent() <= 0); -#endif BrokerParameters const brokerParams{ .vaultDeposit = 10000, .debtMax = 0, diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index c32b578d0b..459c2ecf40 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -164,12 +164,6 @@ enum class PaymentSpecialCase { none, final, extra }; /// single loan payment struct PaymentComponents { -#if LOANCOMPLETE - // raw values are unrounded, and are based on pure math - Number rawInterest; - Number rawPrincipal; - Number rawManagementFee; -#endif // tracked values are rounded to the asset and loan scale, and correspond to // fields in the Loan ledger object. // trackedValueDelta modifies sfTotalValueOutstanding. diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index eac586d1a0..551842efb0 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -219,244 +219,6 @@ loanAccruedInterest( paymentInterval; } -#if LOANCOMPLETE -Number -computeRoundedPrincipalComponent( - Asset const& asset, - Number const& principalOutstanding, - Number const& rawPrincipalOutstanding, - Number const& rawPrincipal, - Number const& roundedPeriodicPayment, - std::int32_t scale) -{ - // Adjust the principal payment by the rounding error between the true - // and rounded principal outstanding - auto const diff = roundToAsset( - asset, - principalOutstanding - rawPrincipalOutstanding, - scale, - asset.integral() ? Number::downward : Number::towards_zero); - - // If the rounded principal outstanding is greater than the true - // principal outstanding, we need to pay more principal to reduce - // the rounded principal outstanding - // - // If the rounded principal outstanding is less than the true - // principal outstanding, we need to pay less principal to allow the - // rounded principal outstanding to catch up - - auto const p = - roundToAsset(asset, rawPrincipal + diff, scale, Number::downward); - - // For particular loans, it's entirely possible for many of the first - // rounded payments to be all interest. - XRPL_ASSERT_PARTS( - p >= 0, - "rippled::detail::computeRoundedPrincipalComponent", - "principal part not negative"); - XRPL_ASSERT_PARTS( - p <= principalOutstanding, - "rippled::detail::computeRoundedPrincipalComponent", - "principal part not larger than outstanding principal"); - XRPL_ASSERT_PARTS( - !asset.integral() || abs(p - rawPrincipal) <= 1, - "rippled::detail::computeRoundedPrincipalComponent", - "principal part not larger than outstanding principal"); - XRPL_ASSERT_PARTS( - p <= roundedPeriodicPayment, - "rippled::detail::computeRoundedPrincipalComponent", - "principal part not larger than total payment"); - - // The asserts will be skipped in release builds, so check here to make - // sure nothing goes negative - if (p > roundedPeriodicPayment || p > principalOutstanding) - return std::min(roundedPeriodicPayment, principalOutstanding); - else if (p < 0) - return Number{}; - - return p; -} - -/** Returns the interest component of a payment WITHOUT accounting for - ** management fees - * - * In other words, it returns the combined value of the interest part that will - * go to the Vault and the management fee that will go to the Broker. - */ - -Number -computeRoundedInterestComponent( - Asset const& asset, - Number const& interestOutstanding, - Number const& roundedPrincipal, - Number const& rawInterestOutstanding, - Number const& roundedPeriodicPayment, - std::int32_t scale) -{ - // Start by just using the non-principal part of the payment for interest - Number roundedInterest = roundedPeriodicPayment - roundedPrincipal; - XRPL_ASSERT_PARTS( - isRounded(asset, roundedInterest, scale), - "ripple::detail::computeRoundedInterestComponent", - "initial interest computation is rounded"); - - { - // Adjust the interest payment by the rounding error between the true - // and rounded interest outstanding - // - // If the rounded interest outstanding is greater than the true interest - // outstanding, we need to pay more interest to reduce the rounded - // interest outstanding - // - // If the rounded interest outstanding is less than the true interest - // outstanding, we need to pay less interest to allow the rounded - // interest outstanding to catch up - auto const diff = roundToAsset( - asset, - interestOutstanding - rawInterestOutstanding, - scale, - asset.integral() ? Number::downward : Number::towards_zero); - roundedInterest += diff; - } - - // However, we cannot allow negative interest payments, therefore we need to - // cap the interest payment at 0. - // - // Ensure interest payment is non-negative and does not exceed the remaining - // payment after principal - return std::max(Number{}, roundedInterest); -} - -// The Interest and Fee components need to be calculated together, because they -// can affect each other during computation in both directions. - -std::pair -computeRoundedInterestAndFeeComponents( - Asset const& asset, - Number const& interestOutstanding, - Number const& managementFeeOutstanding, - Number const& roundedPrincipal, - Number const& rawInterestOutstanding, - Number const& rawManagementFeeOutstanding, - Number const& roundedPeriodicPayment, - Number const& periodicRate, - TenthBips16 managementFeeRate, - std::int32_t scale) -{ - // Zero interest means ZERO interest - if (periodicRate == 0) - return std::make_pair(Number{}, Number{}); - - Number roundedInterest = computeRoundedInterestComponent( - asset, - interestOutstanding, - roundedPrincipal, - rawInterestOutstanding, - roundedPeriodicPayment, - scale); - - Number roundedFee = - computeFee(asset, roundedInterest, managementFeeRate, scale); - - { - // Adjust the interest fee by the rounding error between the true and - // rounded interest fee outstanding - auto const diff = roundToAsset( - asset, - managementFeeOutstanding - rawManagementFeeOutstanding, - scale, - asset.integral() ? Number::downward : Number::towards_zero); - - roundedFee += diff; - - // But again, we cannot allow negative interest fees, therefore we need - // to cap the interest fee at 0 - roundedFee = std::max(Number{}, roundedFee); - - // Finally, the rounded interest fee cannot exceed the outstanding - // interest fee - roundedFee = std::min(roundedFee, managementFeeOutstanding); - } - - // Remove the fee portion from the interest payment, as the fee is paid - // separately - - // Ensure that the interest payment does not become negative, this may - // happen with high interest fees - roundedInterest = std::max(Number{}, roundedInterest - roundedFee); - - // Finally, ensure that the interest payment does not exceed the - // interest outstanding - roundedInterest = std::min(interestOutstanding, roundedInterest); - - // Make sure the parts don't add up to too much - auto const initialTotal = roundedPrincipal + roundedInterest + roundedFee; - Number excess = roundedPeriodicPayment - initialTotal; - - XRPL_ASSERT_PARTS( - isRounded(asset, excess, scale), - "ripple::detail::computeRoundedInterestAndFeeComponents", - "excess is rounded"); - -#if LOANCOMPLETE - if (excess != beast::zero) - std::cout << "computeRoundedInterestAndFeeComponents excess is " - << excess << std::endl; -#endif - - if (excess < beast::zero) - { - // Take as much of the excess as we can out of the interest -#if LOANCOMPLETE - std::cout << "\tApplying excess to interest\n"; -#endif - auto part = std::min(roundedInterest, -excess); - roundedInterest -= part; - excess += part; - - XRPL_ASSERT_PARTS( - excess <= beast::zero, - "ripple::detail::computeRoundedInterestAndFeeComponents", - "excess not positive (interest)"); - } - if (excess < beast::zero) - { - // If there's any left, take as much of the excess as we can out of the - // fee -#if LOANCOMPLETE - std::cout << "\tApplying excess to fee\n"; -#endif - auto part = std::min(roundedFee, -excess); - roundedFee -= part; - excess += part; - } - - // The excess should never be negative, which indicates that the parts are - // trying to take more than the whole payment. The excess can be positive, - // which indicates that we're not going to take the whole payment amount, - // but if so, it must be small. - XRPL_ASSERT_PARTS( - excess == beast::zero || - (excess > beast::zero && - ((asset.integral() && excess < 3) || - (roundedPeriodicPayment.exponent() - excess.exponent() > 6))), - "ripple::detail::computeRoundedInterestAndFeeComponents", - "excess is extremely small (fee)"); - - XRPL_ASSERT_PARTS( - roundedFee >= beast::zero, - "ripple::detail::computeRoundedInterestAndFeeComponents", - "non-negative fee"); - XRPL_ASSERT_PARTS( - roundedInterest >= beast::zero, - "ripple::detail::computeRoundedInterestAndFeeComponents", - "non-negative interest"); - - return std::make_pair( - std::max(Number{}, roundedInterest), std::max(Number{}, roundedFee)); -} -#endif - struct PaymentComponentsPlus : public PaymentComponents { // untrackedManagementFeeDelta includes any fees that go directly to the @@ -846,10 +608,6 @@ computeLatePayment( view.parentCloseTime(), nextDueDate); -#if LOANCOMPLETE - auto const [rawLateInterest, rawLateManagementFee] = - computeInterestAndFeeParts(latePaymentInterest, managementFeeRate); -#endif auto const [roundedLateInterest, roundedLateManagementFee] = [&]() { auto const interest = roundToAsset(asset, latePaymentInterest, loanScale); @@ -868,9 +626,6 @@ computeLatePayment( // This preserves all the other fields without having to enumerate them. PaymentComponentsPlus const late = [&]() { auto inner = periodic; -#if LOANCOMPLETE - inner.rawInterest += rawLateInterest; -#endif return PaymentComponentsPlus{ inner, @@ -943,11 +698,6 @@ computeFullPayment( startDate, closeInterestRate); -#if LOANCOMPLETE - auto const [rawFullInterest, rawFullManagementFee] = - computeInterestAndFeeParts(fullPaymentInterest, managementFeeRate); -#endif - auto const [roundedFullInterest, roundedFullManagementFee] = [&]() { auto const interest = roundToAsset(asset, fullPaymentInterest, loanScale); @@ -960,11 +710,6 @@ computeFullPayment( PaymentComponentsPlus const full{ PaymentComponents{ -#if LOANCOMPLETE - .rawInterest = rawFullInterest, - .rawPrincipal = rawPrincipalOutstanding, - .rawManagementFee = rawFullManagementFee, -#endif .trackedValueDelta = principalOutstanding + totalInterestOutstanding + managementFeeOutstanding, .trackedPrincipalDelta = principalOutstanding, @@ -1121,107 +866,12 @@ computePaymentComponents( // Pay everything off return PaymentComponents{ -#if LOANCOMPLETE - .rawInterest = raw.interestOutstanding, - .rawPrincipal = raw.principalOutstanding, - .rawManagementFee = raw.managementFeeDue, -#endif .trackedValueDelta = totalValueOutstanding, .trackedPrincipalDelta = principalOutstanding, .trackedManagementFeeDelta = managementFeeOutstanding, .specialCase = PaymentSpecialCase::final}; } -#if LOANCOMPLETE - /* - * From the spec, once the periodicPayment is computed: - * - * The principal and interest portions can be derived as follows: - * interest = principalOutstanding * periodicRate - * principal = periodicPayment - interest - */ - Number const rawInterest = raw.principalOutstanding * periodicRate; - Number const rawPrincipal = periodicPayment - rawInterest; - Number const rawFee = tenthBipsOfValue(rawInterest, managementFeeRate); - XRPL_ASSERT_PARTS( - rawInterest >= 0, - "ripple::detail::computePaymentComponents", - "valid raw interest"); - XRPL_ASSERT_PARTS( - rawPrincipal >= 0 && rawPrincipal <= raw.principalOutstanding, - "ripple::detail::computePaymentComponents", - "valid raw principal"); - XRPL_ASSERT_PARTS( - rawFee >= 0 && rawFee <= raw.managementFeeDue, - "ripple::detail::computePaymentComponents", - "valid raw fee"); - - /* - Critical Calculation: Balancing Principal and Interest Outstanding - - This calculation maintains a delicate balance between keeping - principal outstanding and interest outstanding as close as possible to - reference values. However, we cannot perfectly match the reference - values due to rounding issues. - - Key considerations: - 1. Since the periodic payment is rounded up, we have excess funds - that can be used to pay down the loan faster than the reference - calculation. - - 2. We must ensure that loan repayment is not too fast, otherwise we - will end up with negative principal outstanding or negative - interest outstanding. - - 3. We cannot allow the borrower to repay interest ahead of schedule. - If the borrower makes an overpayment, the interest portion could - go negative, requiring complex recalculation to refund the borrower by - reflecting the overpayment in the principal portion of the loan. - */ - - Number const roundedPrincipal = detail::computeRoundedPrincipalComponent( - asset, - principalOutstanding, - raw.principalOutstanding, - rawPrincipal, - roundedPeriodicPayment, - scale); - - auto const [roundedInterest, roundedFee] = - detail::computeRoundedInterestAndFeeComponents( - asset, - totalValueOutstanding - principalOutstanding, - managementFeeOutstanding, - roundedPrincipal, - raw.interestOutstanding, - raw.managementFeeDue, - roundedPeriodicPayment, - periodicRate, - managementFeeRate, - scale); - - XRPL_ASSERT_PARTS( - roundedInterest >= 0 && isRounded(asset, roundedInterest, scale), - "ripple::detail::computePaymentComponents", - "valid rounded interest"); - XRPL_ASSERT_PARTS( - roundedFee >= 0 && roundedFee <= managementFeeOutstanding && - isRounded(asset, roundedFee, scale), - "ripple::detail::computePaymentComponents", - "valid rounded fee"); - XRPL_ASSERT_PARTS( - roundedPrincipal >= 0 && roundedPrincipal <= principalOutstanding && - roundedPrincipal <= roundedPeriodicPayment && - isRounded(asset, roundedPrincipal, scale), - "ripple::detail::computePaymentComponents", - "valid rounded principal"); - XRPL_ASSERT_PARTS( - roundedPrincipal + roundedInterest + roundedFee <= - roundedPeriodicPayment, - "ripple::detail::computePaymentComponents", - "payment parts fit within payment limit"); -#endif - // The shortage must never be negative, which indicates that the parts are // trying to take more than the whole payment. The excess can be positive, // which indicates that we're not going to take the whole payment amount, @@ -1284,16 +934,6 @@ computePaymentComponents( shortage >= beast::zero, "ripple::detail::computePaymentComponents", "no shortage or excess"); -#if LOANCOMPLETE - /* - // This used to be part of the above assert. It will eventually be removed - // if proved accurate - || - (shortage > beast::zero && - ((asset.integral() && shortage < 3) || - (scale - shortage.exponent() > 14))) - */ -#endif XRPL_ASSERT_PARTS( deltas.valueDelta() == @@ -1326,11 +966,6 @@ computePaymentComponents( "payment parts add to payment"); return PaymentComponents{ -#if LOANCOMPLETE - .rawInterest = rawInterest - rawFee, - .rawPrincipal = rawPrincipal, - .rawManagementFee = rawFee, -#endif // As a final safety check, ensure the value is non-negative, and won't // make the corresponding item negative .trackedValueDelta = std::clamp( @@ -1380,11 +1015,6 @@ computeOverpaymentComponents( return detail::PaymentComponentsPlus{ detail::PaymentComponents{ -#if LOANCOMPLETE - .rawInterest = rawOverpaymentInterest, - .rawPrincipal = payment - rawOverpaymentInterest, - .rawManagementFee = 0, -#endif .trackedValueDelta = payment, .trackedPrincipalDelta = payment - roundedOverpaymentInterest - roundedOverpaymentManagementFee, From b9fdf9e564bda4c136ee61c36c1fb86fa627d2ef Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 14 Nov 2025 12:52:27 -0500 Subject: [PATCH 233/291] Add the testPoC_UnsignedUnderflowOnFullPayAfterEarlyPeriodic - Updated to reflect that the issue in RIPD-3971 has been resolved by commit 4396b77c4b9 --- src/test/app/Loan_test.cpp | 193 ++++++++++++++++++------------------- 1 file changed, 96 insertions(+), 97 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 09031e0c53..6e13ee5731 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -5834,6 +5834,100 @@ protected: BEAST_EXPECT(expectedWithPenalty - actualPaid == penaltyDue); } + void + testLoanCoverMinimumRoundingExploit() + { + auto testLoanCoverMinimumRoundingExploit = + [&, this](Number const& principalRequest) { + testcase << "LoanBrokerCoverClawback drains cover via rounding" + << " principalRequested=" + << to_string(principalRequest); + + using namespace jtx; + using namespace loan; + using namespace loanBroker; + + Env env(*this, all); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1'000'000'000), issuer, lender, borrower); + env.close(); + + env(fset(issuer, asfAllowTrustLineClawback)); + env.close(); + + PrettyAsset const asset = issuer[iouCurrency]; + env(trust(lender, asset(2'000'0000))); + env(trust(borrower, asset(2'000'0000))); + env.close(); + + env(pay(issuer, lender, asset(2'000'0000))); + env.close(); + + BrokerParameters brokerParams{ + .debtMax = 0, .coverRateMin = TenthBips32{10'000}}; + BrokerInfo broker{ + createVaultAndBroker(env, asset, lender, brokerParams)}; + + auto const loanSetFee = fee(env.current()->fees().base * 2); + auto createTx = env.jt( + set(borrower, broker.brokerID, principalRequest), + sig(sfCounterpartySignature, lender), + loanSetFee, + paymentInterval(600), + paymentTotal(1), + gracePeriod(60)); + env(createTx); + env.close(); + + auto const brokerBefore = + env.le(keylet::loanbroker(broker.brokerID)); + BEAST_EXPECT(brokerBefore); + if (!brokerBefore) + return; + + Number const debtOutstanding = brokerBefore->at(sfDebtTotal); + Number const coverAvailableBefore = + brokerBefore->at(sfCoverAvailable); + + BEAST_EXPECT(debtOutstanding > Number{}); + BEAST_EXPECT(coverAvailableBefore > Number{}); + + log << "debt=" << to_string(debtOutstanding) + << " cover_available=" << to_string(coverAvailableBefore); + + env(coverClawback(issuer, 0), loanBrokerID(broker.brokerID)); + env.close(); + + auto const brokerAfter = + env.le(keylet::loanbroker(broker.brokerID)); + BEAST_EXPECT(brokerAfter); + if (!brokerAfter) + return; + + Number const debtAfter = brokerAfter->at(sfDebtTotal); + // the debt has not changed + BEAST_EXPECT(debtAfter == debtOutstanding); + + Number const coverAvailableAfter = + brokerAfter->at(sfCoverAvailable); + + // since the cover rate min != 0, the cover available should not + // be zero + BEAST_EXPECT(coverAvailableAfter != Number{}); + }; + + // Call the lambda with different principal values + testLoanCoverMinimumRoundingExploit(Number{1, -30}); // 1e-30 units + testLoanCoverMinimumRoundingExploit(Number{1, -20}); // 1e-20 units + testLoanCoverMinimumRoundingExploit(Number{1, -10}); // 1e-10 units + testLoanCoverMinimumRoundingExploit(Number{1, 1}); // 1e-10 units + } +#endif + void testPoC_UnsignedUnderflowOnFullPayAfterEarlyPeriodic() { @@ -6057,8 +6151,7 @@ protected: // Value-based proof: underflowed window yields a payoff larger than // the clamped (non-underflow) reference. - BEAST_EXPECT(fullDue != fullDueClamped); - BEAST_EXPECT(fullDue > fullDueClamped); + BEAST_EXPECT(fullDue == fullDueClamped); if (fullDue > fullDueClamped) log << "PoC delta: overcharge (fullDue > clamped)" << std::endl; } @@ -6073,100 +6166,6 @@ protected: } } - void - testLoanCoverMinimumRoundingExploit() - { - auto testLoanCoverMinimumRoundingExploit = - [&, this](Number const& principalRequest) { - testcase << "LoanBrokerCoverClawback drains cover via rounding" - << " principalRequested=" - << to_string(principalRequest); - - using namespace jtx; - using namespace loan; - using namespace loanBroker; - - Env env(*this, all); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000'000), issuer, lender, borrower); - env.close(); - - env(fset(issuer, asfAllowTrustLineClawback)); - env.close(); - - PrettyAsset const asset = issuer[iouCurrency]; - env(trust(lender, asset(2'000'0000))); - env(trust(borrower, asset(2'000'0000))); - env.close(); - - env(pay(issuer, lender, asset(2'000'0000))); - env.close(); - - BrokerParameters brokerParams{ - .debtMax = 0, .coverRateMin = TenthBips32{10'000}}; - BrokerInfo broker{ - createVaultAndBroker(env, asset, lender, brokerParams)}; - - auto const loanSetFee = fee(env.current()->fees().base * 2); - auto createTx = env.jt( - set(borrower, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, lender), - loanSetFee, - paymentInterval(600), - paymentTotal(1), - gracePeriod(60)); - env(createTx); - env.close(); - - auto const brokerBefore = - env.le(keylet::loanbroker(broker.brokerID)); - BEAST_EXPECT(brokerBefore); - if (!brokerBefore) - return; - - Number const debtOutstanding = brokerBefore->at(sfDebtTotal); - Number const coverAvailableBefore = - brokerBefore->at(sfCoverAvailable); - - BEAST_EXPECT(debtOutstanding > Number{}); - BEAST_EXPECT(coverAvailableBefore > Number{}); - - log << "debt=" << to_string(debtOutstanding) - << " cover_available=" << to_string(coverAvailableBefore); - - env(coverClawback(issuer, 0), loanBrokerID(broker.brokerID)); - env.close(); - - auto const brokerAfter = - env.le(keylet::loanbroker(broker.brokerID)); - BEAST_EXPECT(brokerAfter); - if (!brokerAfter) - return; - - Number const debtAfter = brokerAfter->at(sfDebtTotal); - // the debt has not changed - BEAST_EXPECT(debtAfter == debtOutstanding); - - Number const coverAvailableAfter = - brokerAfter->at(sfCoverAvailable); - - // since the cover rate min != 0, the cover available should not - // be zero - BEAST_EXPECT(coverAvailableAfter != Number{}); - }; - - // Call the lambda with different principal values - testLoanCoverMinimumRoundingExploit(Number{1, -30}); // 1e-30 units - testLoanCoverMinimumRoundingExploit(Number{1, -20}); // 1e-20 units - testLoanCoverMinimumRoundingExploit(Number{1, -10}); // 1e-10 units - testLoanCoverMinimumRoundingExploit(Number{1, 1}); // 1e-10 units - } -#endif - void testDustManipulation() { @@ -6861,9 +6860,9 @@ public: #if LOANTODO testCoverDepositAllowsNonTransferableMPT(); testLoanPayLateFullPaymentBypassesPenalties(); - testPoC_UnsignedUnderflowOnFullPayAfterEarlyPeriodic(); testLoanCoverMinimumRoundingExploit(); #endif + testPoC_UnsignedUnderflowOnFullPayAfterEarlyPeriodic(); testDisabled(); testSelfLoan(); From 31d6ad727ec64a7b31155af18a5f0d4be970532d Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Sat, 15 Nov 2025 09:48:38 +0100 Subject: [PATCH 234/291] Refactors method and struct names (#6039) - remove unused method - rename computeFee to computeManagementFee - rename calculateRounded to constructRounded - remove redundancies in LoanDeltas struct - rename PaymentComponentsPlus to ExtendedPaymentComponents - additional logging --- src/test/app/Loan_test.cpp | 55 +++--- src/xrpld/app/misc/LendingHelpers.h | 25 +-- src/xrpld/app/misc/detail/LendingHelpers.cpp | 171 +++++++++---------- src/xrpld/app/tx/detail/LoanSet.cpp | 2 +- 4 files changed, 116 insertions(+), 137 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 6e13ee5731..37490906ff 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -376,7 +376,7 @@ protected: loan->at(sfPeriodicPayment) == periodicPayment); env.test.BEAST_EXPECT(loan->at(sfFlags) == flags); - auto const ls = calculateRoundedLoanState(loan); + auto const ls = constructRoundedLoanState(loan); auto const interestRate = TenthBips32{loan->at(sfInterestRate)}; auto const paymentInterval = loan->at(sfPaymentInterval); @@ -546,7 +546,7 @@ protected: state.loanScale)); BEAST_EXPECT( state.managementFeeOutstanding == - computeFee( + computeManagementFee( broker.asset, state.totalValue - state.principalOutstanding, broker.params.managementFeeRate, @@ -897,7 +897,7 @@ protected: periodicRate, state.paymentRemaining, broker.params.managementFeeRate); - auto const rounded = calculateRoundedLoanState( + auto const rounded = constructRoundedLoanState( state.totalValue, state.principalOutstanding, state.managementFeeOutstanding); @@ -1004,23 +1004,21 @@ protected: broker.params.managementFeeRate); detail::LoanDeltas const deltas = currentTrueState - nextTrueState; BEAST_EXPECT( - deltas.valueDelta() == - deltas.principalDelta + deltas.interestDueDelta + - deltas.managementFeeDueDelta); + deltas.total() == + deltas.principal + deltas.interest + deltas.managementFee); BEAST_EXPECT( paymentComponents.specialCase == detail::PaymentSpecialCase::final || - deltas.valueDelta() == state.periodicPayment || + deltas.total() == state.periodicPayment || (state.loanScale - - (deltas.valueDelta() - state.periodicPayment).exponent()) > - 14); + (deltas.total() - state.periodicPayment).exponent()) > 14); if (!showStepBalances) log << currencyLabel << " Payment components: " << state.paymentRemaining << ", " - << deltas.interestDueDelta << ", " << deltas.principalDelta - << ", " << deltas.managementFeeDueDelta << ", " + << deltas.interest << ", " << deltas.principal << ", " + << deltas.managementFee << ", " << paymentComponents.trackedValueDelta << ", " << paymentComponents.trackedPrincipalDelta << ", " << paymentComponents.trackedInterestPart() << ", " @@ -1092,24 +1090,24 @@ protected: if (!BEAST_EXPECT(loanSle)) // No reason for this not to exist return; - auto const current = calculateRoundedLoanState(loanSle); + auto const current = constructRoundedLoanState(loanSle); auto const errors = nextTrueState - current; log << currencyLabel << " Loan balances: " << "\n\tAmount taken: " << paymentComponents.trackedValueDelta << "\n\tTotal value: " << current.valueOutstanding << " (true: " << truncate(nextTrueState.valueOutstanding) - << ", error: " << truncate(errors.valueDelta()) + << ", error: " << truncate(errors.total()) << ")\n\tPrincipal: " << current.principalOutstanding << " (true: " << truncate(nextTrueState.principalOutstanding) - << ", error: " << truncate(errors.principalDelta) + << ", error: " << truncate(errors.principal) << ")\n\tInterest: " << current.interestDue << " (true: " << truncate(nextTrueState.interestDue) - << ", error: " << truncate(errors.interestDueDelta) + << ", error: " << truncate(errors.interest) << ")\n\tMgmt fee: " << current.managementFeeDue << " (true: " << truncate(nextTrueState.managementFeeDue) - << ", error: " << truncate(errors.managementFeeDueDelta) + << ", error: " << truncate(errors.managementFee) << ")\n\tPayments remaining " << loanSle->at(sfPaymentRemaining) << std::endl; } @@ -2628,7 +2626,7 @@ protected: periodicRate, state.paymentRemaining, broker.params.managementFeeRate); - auto const rounded = calculateRoundedLoanState( + auto const rounded = constructRoundedLoanState( state.totalValue, state.principalOutstanding, state.managementFeeOutstanding); @@ -2703,9 +2701,8 @@ protected: testcase << currencyLabel << " Payment components: " << state.paymentRemaining - << ", " << deltas.interestDueDelta << ", " - << deltas.principalDelta << ", " - << deltas.managementFeeDueDelta << ", " + << ", " << deltas.interest << ", " << deltas.principal + << ", " << deltas.managementFee << ", " << paymentComponents.trackedValueDelta << ", " << paymentComponents.trackedPrincipalDelta << ", " << paymentComponents.trackedInterestPart() << ", " @@ -2752,7 +2749,7 @@ protected: state.paymentRemaining < 12 || roundToAsset( broker.asset, - deltas.principalDelta, + deltas.principal, state.loanScale, Number::upward) == roundToScale( @@ -2775,8 +2772,8 @@ protected: paymentComponents.specialCase == detail::PaymentSpecialCase::final || (state.periodicPayment.exponent() - - (deltas.principalDelta + deltas.interestDueDelta + - deltas.managementFeeDueDelta - state.periodicPayment) + (deltas.principal + deltas.interest + + deltas.managementFee - state.periodicPayment) .exponent()) > 14); auto const borrowerBalanceBeforePayment = @@ -5747,7 +5744,7 @@ protected: Number const latePaymentFeeRounded = roundToAsset( broker.asset, loanSle->at(sfLatePaymentFee), state.loanScale); - auto const roundedLoanState = calculateRoundedLoanState( + auto const roundedLoanState = constructRoundedLoanState( state.totalValue, state.principalOutstanding, state.managementFeeOutstanding); @@ -5776,7 +5773,7 @@ protected: Number const roundedFullInterestAmount = roundToAsset(broker.asset, fullPaymentInterest, state.loanScale); - Number const roundedFullManagementFee = computeFee( + Number const roundedFullManagementFee = computeManagementFee( broker.asset, roundedFullInterestAmount, managementFeeRate, @@ -5807,7 +5804,7 @@ protected: Number const lateInterestRaw = state.principalOutstanding * overdueRate; Number const lateInterestRounded = roundToAsset(broker.asset, lateInterestRaw, state.loanScale); - Number const lateManagementFeeRounded = computeFee( + Number const lateManagementFeeRounded = computeManagementFee( broker.asset, lateInterestRounded, managementFeeRate, @@ -6060,7 +6057,7 @@ protected: // Round to asset scale and split interest/fee parts auto const roundedInterest = roundToAsset(asset.raw(), fullPaymentInterest, after.loanScale); - Number const roundedFullMgmtFee = computeFee( + Number const roundedFullMgmtFee = computeManagementFee( asset.raw(), roundedInterest, managementFeeRate, after.loanScale); Number const roundedFullInterest = roundedInterest - roundedFullMgmtFee; @@ -6094,7 +6091,7 @@ protected: closeInterestRate); auto const roundedInterestClamped = roundToAsset( asset.raw(), fullPaymentInterestClamped, after.loanScale); - Number const roundedFullMgmtFeeClamped = computeFee( + Number const roundedFullMgmtFeeClamped = computeManagementFee( asset.raw(), roundedInterestClamped, managementFeeRate, @@ -6268,7 +6265,7 @@ protected: auto const loanSle = env.le(loanKeylet); if (!BEAST_EXPECT(loanSle)) return; - auto const state = calculateRoundedLoanState(loanSle); + auto const state = constructRoundedLoanState(loanSle); log << "Loan state:" << std::endl; log << " ValueOutstanding: " << state.valueOutstanding diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 459c2ecf40..f430a0f0d5 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -118,18 +118,18 @@ calculateRawLoanState( TenthBips32 const managementFeeRate); LoanState -calculateRoundedLoanState( +constructRoundedLoanState( Number const& totalValueOutstanding, Number const& principalOutstanding, Number const& managementFeeOutstanding); LoanState -calculateRoundedLoanState(SLE::const_ref loan); +constructRoundedLoanState(SLE::const_ref loan); Number -computeFee( +computeManagementFee( Asset const& asset, - Number const& value, + Number const& interest, TenthBips32 managementFeeRate, std::int32_t scale); @@ -183,14 +183,14 @@ struct PaymentComponents struct LoanDeltas { - Number principalDelta; - Number interestDueDelta; - Number managementFeeDueDelta; + Number principal; + Number interest; + Number managementFee; Number - valueDelta() const + total() const { - return principalDelta + interestDueDelta + managementFeeDueDelta; + return principal + interest + managementFee; } void @@ -217,13 +217,6 @@ operator-(LoanState const& lhs, LoanState const& rhs); LoanState operator-(LoanState const& lhs, detail::LoanDeltas const& rhs); -Number -valueMinusFee( - Asset const& asset, - Number const& value, - TenthBips16 managementFeeRate, - std::int32_t scale); - LoanProperties computeLoanProperties( Asset const& asset, diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index 551842efb0..62f567e7c0 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -219,7 +219,7 @@ loanAccruedInterest( paymentInterval; } -struct PaymentComponentsPlus : public PaymentComponents +struct ExtendedPaymentComponents : public PaymentComponents { // untrackedManagementFeeDelta includes any fees that go directly to the // Broker, such as late fees. This value may be negative, though the final @@ -232,7 +232,7 @@ struct PaymentComponentsPlus : public PaymentComponents Number untrackedInterest; Number totalDue; - PaymentComponentsPlus( + ExtendedPaymentComponents( PaymentComponents const& p, Number f, Number v = numZero) @@ -248,7 +248,7 @@ struct PaymentComponentsPlus : public PaymentComponents template LoanPaymentParts doPayment( - PaymentComponentsPlus const& payment, + ExtendedPaymentComponents const& payment, NumberProxy& totalValueOutstandingProxy, NumberProxy& principalOutstandingProxy, NumberProxy& managementFeeOutstandingProxy, @@ -353,7 +353,7 @@ Expected tryOverpayment( Asset const& asset, std::int32_t loanScale, - PaymentComponentsPlus const& overpaymentComponents, + ExtendedPaymentComponents const& overpaymentComponents, Number& totalValueOutstanding, Number& principalOutstanding, Number& managementFeeOutstanding, @@ -369,7 +369,7 @@ tryOverpayment( { auto const raw = calculateRawLoanState( periodicPayment, periodicRate, paymentRemaining, managementFeeRate); - auto const rounded = calculateRoundedLoanState( + auto const rounded = constructRoundedLoanState( totalValueOutstanding, principalOutstanding, managementFeeOutstanding); auto const totalValueError = totalValueOutstanding - raw.valueOutstanding; @@ -437,7 +437,7 @@ tryOverpayment( // LCOV_EXCL_STOP } - auto const newRounded = calculateRoundedLoanState( + auto const newRounded = constructRoundedLoanState( totalValueOutstanding, principalOutstanding, managementFeeOutstanding); auto const valueChange = newRounded.interestOutstanding() - rounded.interestOutstanding(); @@ -460,7 +460,7 @@ Expected doOverpayment( Asset const& asset, std::int32_t loanScale, - PaymentComponentsPlus const& overpaymentComponents, + ExtendedPaymentComponents const& overpaymentComponents, NumberProxy& totalValueOutstandingProxy, NumberProxy& principalOutstandingProxy, NumberProxy& managementFeeOutstandingProxy, @@ -563,7 +563,8 @@ computeInterestAndFeeParts( TenthBips16 managementFeeRate, std::int32_t loanScale) { - auto const fee = computeFee(asset, interest, managementFeeRate, loanScale); + auto const fee = + computeManagementFee(asset, interest, managementFeeRate, loanScale); return std::make_pair(interest - fee, fee); } @@ -582,13 +583,13 @@ computeInterestAndFeeParts( * * section 3.2.4.1.2 (Late Payment) */ -Expected +Expected computeLatePayment( Asset const& asset, ApplyView const& view, Number const& principalOutstanding, std::int32_t nextDueDate, - PaymentComponentsPlus const& periodic, + ExtendedPaymentComponents const& periodic, TenthBips32 lateInterestRate, std::int32_t loanScale, Number const& latePaymentFee, @@ -624,10 +625,10 @@ computeLatePayment( "no extra parts to this payment"); // Copy the periodic payment values, and add on the late interest. // This preserves all the other fields without having to enumerate them. - PaymentComponentsPlus const late = [&]() { + ExtendedPaymentComponents const late = [&]() { auto inner = periodic; - return PaymentComponentsPlus{ + return ExtendedPaymentComponents{ inner, // A late payment pays both the normal fee, and the extra fees periodic.untrackedManagementFee + latePaymentFee + @@ -655,12 +656,12 @@ computeLatePayment( /* Handle possible full payments. * * If this function processed a full payment, the return value will be - * a PaymentComponentsPlus object. Otherwise, it'll be an Unexpected with the - * error code the caller is expected to return. It should NEVER return + * an ExtendedPaymentComponents object. Otherwise, it'll be an Unexpected with + * the error code the caller is expected to return. It should NEVER return * tesSUCCESS */ -Expected +Expected computeFullPayment( Asset const& asset, ApplyView& view, @@ -681,8 +682,12 @@ computeFullPayment( beast::Journal j) { if (paymentRemaining <= 1) + { // If this is the last payment, it has to be a regular payment + JLOG(j.warn()) << "Full payment requested when only final " + << "payment remains."; return Unexpected(tecKILLED); + } Number const rawPrincipalOutstanding = loanPrincipalFromPeriodicPayment( periodicPayment, periodicRate, paymentRemaining); @@ -708,7 +713,7 @@ computeFullPayment( return std::make_tuple(parts.first, parts.second); }(); - PaymentComponentsPlus const full{ + ExtendedPaymentComponents const full{ PaymentComponents{ .trackedValueDelta = principalOutstanding + totalInterestOutstanding + managementFeeOutstanding, @@ -762,12 +767,12 @@ PaymentComponents::trackedInterestPart() const void LoanDeltas::nonNegative() { - if (principalDelta < beast::zero) - principalDelta = numZero; - if (interestDueDelta < beast::zero) - interestDueDelta = numZero; - if (managementFeeDueDelta < beast::zero) - managementFeeDueDelta = numZero; + if (principal < beast::zero) + principal = numZero; + if (interest < beast::zero) + interest = numZero; + if (managementFee < beast::zero) + managementFee = numZero; } PaymentComponents @@ -809,7 +814,7 @@ computePaymentComponents( .interestDue = roundToAsset(asset, trueTarget.interestDue, scale), .managementFeeDue = roundToAsset(asset, trueTarget.managementFeeDue, scale)}; - LoanState const currentLedgerState = calculateRoundedLoanState( + LoanState const currentLedgerState = constructRoundedLoanState( totalValueOutstanding, principalOutstanding, managementFeeOutstanding); LoanDeltas deltas = currentLedgerState - roundedTarget; @@ -817,32 +822,31 @@ computePaymentComponents( // Adjust the deltas if necessary for data integrity XRPL_ASSERT_PARTS( - deltas.principalDelta <= currentLedgerState.principalOutstanding, + deltas.principal <= currentLedgerState.principalOutstanding, "ripple::detail::computePaymentComponents", "principal delta not greater than outstanding"); - deltas.principalDelta = std::min( - deltas.principalDelta, currentLedgerState.principalOutstanding); + deltas.principal = + std::min(deltas.principal, currentLedgerState.principalOutstanding); XRPL_ASSERT_PARTS( - deltas.interestDueDelta <= currentLedgerState.interestDue, + deltas.interest <= currentLedgerState.interestDue, "ripple::detail::computePaymentComponents", "interest due delta not greater than outstanding"); - deltas.interestDueDelta = std::min( - {deltas.interestDueDelta, - std::max(numZero, roundedPeriodicPayment - deltas.principalDelta), + deltas.interest = std::min( + {deltas.interest, + std::max(numZero, roundedPeriodicPayment - deltas.principal), currentLedgerState.interestDue}); XRPL_ASSERT_PARTS( - deltas.managementFeeDueDelta <= currentLedgerState.managementFeeDue, + deltas.managementFee <= currentLedgerState.managementFeeDue, "ripple::detail::computePaymentComponents", "management fee due delta not greater than outstanding"); - deltas.managementFeeDueDelta = std::min( - {deltas.managementFeeDueDelta, - roundedPeriodicPayment - - (deltas.principalDelta + deltas.interestDueDelta), + deltas.managementFee = std::min( + {deltas.managementFee, + roundedPeriodicPayment - (deltas.principal + deltas.interest), currentLedgerState.managementFeeDue}); if (paymentRemaining == 1 || @@ -852,15 +856,15 @@ computePaymentComponents( // parts. XRPL_ASSERT_PARTS( - deltas.valueDelta() <= totalValueOutstanding, + deltas.total() <= totalValueOutstanding, "ripple::detail::computePaymentComponents", "last payment total value agrees"); XRPL_ASSERT_PARTS( - deltas.principalDelta <= principalOutstanding, + deltas.principal <= principalOutstanding, "ripple::detail::computePaymentComponents", "last payment principal agrees"); XRPL_ASSERT_PARTS( - deltas.managementFeeDueDelta <= managementFeeOutstanding, + deltas.managementFee <= managementFeeOutstanding, "ripple::detail::computePaymentComponents", "last payment management fee agrees"); @@ -894,12 +898,12 @@ computePaymentComponents( }; auto addressExcess = [&takeFrom](LoanDeltas& deltas, Number& excess) { // This order is based on where errors are the least problematic - takeFrom(deltas.interestDueDelta, excess); - takeFrom(deltas.managementFeeDueDelta, excess); - takeFrom(deltas.principalDelta, excess); + takeFrom(deltas.interest, excess); + takeFrom(deltas.managementFee, excess); + takeFrom(deltas.principal, excess); }; Number totalOverpayment = - deltas.valueDelta() - currentLedgerState.valueOutstanding; + deltas.total() - currentLedgerState.valueOutstanding; if (totalOverpayment > beast::zero) { // LCOV_EXCL_START @@ -911,7 +915,7 @@ computePaymentComponents( } // Make sure the parts don't add up to too much - Number shortage = roundedPeriodicPayment - deltas.valueDelta(); + Number shortage = roundedPeriodicPayment - deltas.total(); XRPL_ASSERT_PARTS( isRounded(asset, shortage, scale), @@ -936,32 +940,29 @@ computePaymentComponents( "no shortage or excess"); XRPL_ASSERT_PARTS( - deltas.valueDelta() == - deltas.principalDelta + deltas.interestDueDelta + - deltas.managementFeeDueDelta, + deltas.total() == + deltas.principal + deltas.interest + deltas.managementFee, "ripple::detail::computePaymentComponents", "total value adds up"); XRPL_ASSERT_PARTS( - deltas.principalDelta >= beast::zero && - deltas.principalDelta <= currentLedgerState.principalOutstanding, + deltas.principal >= beast::zero && + deltas.principal <= currentLedgerState.principalOutstanding, "ripple::detail::computePaymentComponents", "valid principal result"); XRPL_ASSERT_PARTS( - deltas.interestDueDelta >= beast::zero && - deltas.interestDueDelta <= currentLedgerState.interestDue, + deltas.interest >= beast::zero && + deltas.interest <= currentLedgerState.interestDue, "ripple::detail::computePaymentComponents", "valid interest result"); XRPL_ASSERT_PARTS( - deltas.managementFeeDueDelta >= beast::zero && - deltas.managementFeeDueDelta <= currentLedgerState.managementFeeDue, + deltas.managementFee >= beast::zero && + deltas.managementFee <= currentLedgerState.managementFeeDue, "ripple::detail::computePaymentComponents", "valid fee result"); XRPL_ASSERT_PARTS( - deltas.principalDelta + deltas.interestDueDelta + - deltas.managementFeeDueDelta > - beast::zero, + deltas.principal + deltas.interest + deltas.managementFee > beast::zero, "ripple::detail::computePaymentComponents", "payment parts add to payment"); @@ -969,19 +970,15 @@ computePaymentComponents( // As a final safety check, ensure the value is non-negative, and won't // make the corresponding item negative .trackedValueDelta = std::clamp( - deltas.valueDelta(), numZero, currentLedgerState.valueOutstanding), + deltas.total(), numZero, currentLedgerState.valueOutstanding), .trackedPrincipalDelta = std::clamp( - deltas.principalDelta, - numZero, - currentLedgerState.principalOutstanding), + deltas.principal, numZero, currentLedgerState.principalOutstanding), .trackedManagementFeeDelta = std::clamp( - deltas.managementFeeDueDelta, - numZero, - currentLedgerState.managementFeeDue), + deltas.managementFee, numZero, currentLedgerState.managementFeeDue), }; } -PaymentComponentsPlus +ExtendedPaymentComponents computeOverpaymentComponents( Asset const& asset, int32_t const loanScale, @@ -1013,7 +1010,7 @@ computeOverpaymentComponents( asset, interest, managementFeeRate, loanScale); }(); - return detail::PaymentComponentsPlus{ + return detail::ExtendedPaymentComponents{ detail::PaymentComponents{ .trackedValueDelta = payment, .trackedPrincipalDelta = payment - roundedOverpaymentInterest - @@ -1030,9 +1027,9 @@ detail::LoanDeltas operator-(LoanState const& lhs, LoanState const& rhs) { detail::LoanDeltas result{ - .principalDelta = lhs.principalOutstanding - rhs.principalOutstanding, - .interestDueDelta = lhs.interestDue - rhs.interestDue, - .managementFeeDueDelta = lhs.managementFeeDue - rhs.managementFeeDue, + .principal = lhs.principalOutstanding - rhs.principalOutstanding, + .interest = lhs.interestDue - rhs.interestDue, + .managementFee = lhs.managementFeeDue - rhs.managementFeeDue, }; return result; @@ -1042,10 +1039,10 @@ LoanState operator-(LoanState const& lhs, detail::LoanDeltas const& rhs) { LoanState result{ - .valueOutstanding = lhs.valueOutstanding - rhs.valueDelta(), - .principalOutstanding = lhs.principalOutstanding - rhs.principalDelta, - .interestDue = lhs.interestDue - rhs.interestDueDelta, - .managementFeeDue = lhs.managementFeeDue - rhs.managementFeeDueDelta, + .valueOutstanding = lhs.valueOutstanding - rhs.total(), + .principalOutstanding = lhs.principalOutstanding - rhs.principal, + .interestDue = lhs.interestDue - rhs.interest, + .managementFeeDue = lhs.managementFeeDue - rhs.managementFee, }; return result; @@ -1156,14 +1153,14 @@ calculateRawLoanState( } LoanState -calculateRoundedLoanState( +constructRoundedLoanState( Number const& totalValueOutstanding, Number const& principalOutstanding, Number const& managementFeeOutstanding) { // This implementation is pretty trivial, but ensures the calculations are // consistent everywhere, and reduces copy/paste errors. - return { + return LoanState{ .valueOutstanding = totalValueOutstanding, .principalOutstanding = principalOutstanding, .interestDue = totalValueOutstanding - principalOutstanding - @@ -1172,16 +1169,16 @@ calculateRoundedLoanState( } LoanState -calculateRoundedLoanState(SLE::const_ref loan) +constructRoundedLoanState(SLE::const_ref loan) { - return calculateRoundedLoanState( + return constructRoundedLoanState( loan->at(sfTotalValueOutstanding), loan->at(sfPrincipalOutstanding), loan->at(sfManagementFeeOutstanding)); } Number -computeFee( +computeManagementFee( Asset const& asset, Number const& value, TenthBips32 managementFeeRate, @@ -1194,16 +1191,6 @@ computeFee( Number::downward); } -Number -valueMinusFee( - Asset const& asset, - Number const& value, - TenthBips16 managementFeeRate, - std::int32_t scale) -{ - return value - computeFee(asset, value, managementFeeRate, scale); -} - LoanProperties computeLoanProperties( Asset const& asset, @@ -1258,7 +1245,7 @@ computeLoanProperties( principalOutstanding = roundToAsset( asset, principalOutstanding, loanScale, Number::to_nearest); - auto const feeOwedToBroker = computeFee( + auto const feeOwedToBroker = computeManagementFee( asset, /* * This formula is from the XLS-66 spec, section 3.2.4.2 (Total Loan @@ -1384,7 +1371,7 @@ loanMakePayment( Number const closePaymentFee = roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale); - LoanState const roundedLoanState = calculateRoundedLoanState( + LoanState const roundedLoanState = constructRoundedLoanState( totalValueOutstandingProxy, principalOutstandingProxy, managementFeeOutstandingProxy); @@ -1426,6 +1413,7 @@ loanMakePayment( // LCOV_EXCL_START UNREACHABLE("ripple::loanMakePayment : invalid full payment result"); + JLOG(j.error()) << "Full payment computation failed unexpectedly."; return Unexpected(tecINTERNAL); // LCOV_EXCL_STOP } @@ -1433,7 +1421,7 @@ loanMakePayment( // ------------------------------------------------------------- // compute the periodic payment info that will be needed whether the payment // is late or regular - detail::PaymentComponentsPlus periodic{ + detail::ExtendedPaymentComponents periodic{ detail::computePaymentComponents( asset, loanScale, @@ -1489,6 +1477,7 @@ loanMakePayment( // LCOV_EXCL_START UNREACHABLE("ripple::loanMakePayment : invalid late payment result"); + JLOG(j.error()) << "Late payment computation failed unexpectedly."; return Unexpected(tecINTERNAL); // LCOV_EXCL_STOP } @@ -1540,7 +1529,7 @@ loanMakePayment( if (periodic.specialCase == detail::PaymentSpecialCase::final) break; - periodic = detail::PaymentComponentsPlus{ + periodic = detail::ExtendedPaymentComponents{ detail::computePaymentComponents( asset, loanScale, @@ -1588,7 +1577,7 @@ loanMakePayment( Number const overpayment = std::min(amount - totalPaid, *totalValueOutstandingProxy); - detail::PaymentComponentsPlus const overpaymentComponents = + detail::ExtendedPaymentComponents const overpaymentComponents = detail::computeOverpaymentComponents( asset, loanScale, diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index c129673222..f72ad33cee 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -495,7 +495,7 @@ LoanSet::doApply() // LCOV_EXCL_STOP } - LoanState const state = calculateRoundedLoanState( + LoanState const state = constructRoundedLoanState( properties.totalValueOutstanding, principalRequested, properties.managementFeeOwedToBroker); From d8b944c21820e33bc5170c3df537b046b685baa3 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Tue, 18 Nov 2025 23:28:59 +0000 Subject: [PATCH 235/291] Enforce assets are transferable (#6017) - Includes unit tests for terNO_RIPPLE. - Switch auth mode to weak when submitter is destination. - Require rippling enabled for vault deposits or withdrawals. --- include/xrpl/ledger/View.h | 21 +++ src/libxrpl/ledger/View.cpp | 36 ++++ src/test/app/Loan_test.cpp | 55 ++++++- src/test/app/Vault_test.cpp | 155 +++++++++++++++++- .../app/tx/detail/LoanBrokerCoverDeposit.cpp | 7 +- .../app/tx/detail/LoanBrokerCoverWithdraw.cpp | 24 ++- src/xrpld/app/tx/detail/VaultDeposit.cpp | 36 +--- src/xrpld/app/tx/detail/VaultWithdraw.cpp | 45 ++--- 8 files changed, 297 insertions(+), 82 deletions(-) diff --git a/include/xrpl/ledger/View.h b/include/xrpl/ledger/View.h index ece574e52d..5e6fb8f5ce 100644 --- a/include/xrpl/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -1082,6 +1083,26 @@ canTransfer( AccountID const& from, AccountID const& to); +[[nodiscard]] TER +canTransfer( + ReadView const& view, + Issue const& issue, + AccountID const& from, + AccountID const& to); + +[[nodiscard]] TER inline canTransfer( + ReadView const& view, + Asset const& asset, + AccountID const& from, + AccountID const& to) +{ + return std::visit( + [&](TIss const& issue) -> TER { + return canTransfer(view, issue, from, to); + }, + asset.value()); +} + /** Deleter function prototype. Returns the status of the entry deletion * (if should not be skipped) and if the entry should be skipped. The status * is always tesSUCCESS if the entry should be skipped. diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index 9b6a8723c5..88bffae413 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -3277,6 +3277,42 @@ canTransfer( return tesSUCCESS; } +[[nodiscard]] TER +canTransfer( + ReadView const& view, + Issue const& issue, + AccountID const& from, + AccountID const& to) +{ + if (issue.native()) + return tesSUCCESS; + + auto const issuerId = issue.getIssuer(); + auto const sleIssuer = view.read(keylet::account(issuerId)); + if (sleIssuer == nullptr) + return tefINTERNAL; // LCOV_EXCL_LINE + if (issuerId == from || issuerId == to) + return tesSUCCESS; + + auto const isRippleDisabled = [&](AccountID account) -> bool { + // Line might not exist, but some transfers can create it. If this + // is the case, just check the default ripple on the issuer account. + auto const line = view.read(keylet::line(account, issue)); + if (line) + { + bool const issuerHigh = issuerId > account; + return line->isFlag(issuerHigh ? lsfHighNoRipple : lsfLowNoRipple); + } + return sleIssuer->isFlag(lsfDefaultRipple) == false; + }; + + // Fail if rippling disabled on either trust line + if (isRippleDisabled(from) || isRippleDisabled(to)) + return terNO_RIPPLE; + + return tesSUCCESS; +} + TER cleanupOnAccountDelete( ApplyView& view, diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 37490906ff..24cb63a3cf 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -5588,11 +5588,11 @@ protected: }); } -#if LOANTODO void - testCoverDepositAllowsNonTransferableMPT() + testCoverDepositWithdrawNonTransferableMPT() { - testcase("CoverDeposit accepts MPT without CanTransfer"); + testcase( + "CoverDeposit and CoverWithdraw reject MPT without CanTransfer"); using namespace jtx; using namespace loanBroker; @@ -5612,7 +5612,7 @@ protected: env.close(); - PrettyAsset const asset = mpt["BUG"]; + PrettyAsset const asset = mpt["MPT"]; mpt.authorize({.account = alice}); env.close(); @@ -5646,21 +5646,58 @@ protected: env(pay(alice, pseudoAccount, asset(1)), ter(tecNO_AUTH)); env.close(); + // Cover cannot be transferred to broker account auto const depositAmount = asset(1); - env(coverDeposit(alice, brokerKeylet.key, depositAmount)); - BEAST_EXPECT(env.ter() == tesSUCCESS); + env(coverDeposit(alice, brokerKeylet.key, depositAmount), + ter{tecNO_AUTH}); env.close(); if (auto const refreshed = env.le(brokerKeylet); BEAST_EXPECT(refreshed)) { - // with an MPT that cannot be transferred the covrAvailable should - // remain zero BEAST_EXPECT(refreshed->at(sfCoverAvailable) == 0); + env.require(balance(pseudoAccount, asset(0))); + } + + // Set CanTransfer again and transfer some deposit + mpt.set({.mutableFlags = tmfMPTSetCanTransfer}); + env.close(); + + env(coverDeposit(alice, brokerKeylet.key, depositAmount)); + env.close(); + + if (auto const refreshed = env.le(brokerKeylet); + BEAST_EXPECT(refreshed)) + { + BEAST_EXPECT(refreshed->at(sfCoverAvailable) == 1); env.require(balance(pseudoAccount, depositAmount)); } + + // Remove CanTransfer after the deposit + mpt.set({.mutableFlags = tmfMPTClearCanTransfer}); + env.close(); + + // Cover cannot be transferred from broker account + env(coverWithdraw(alice, brokerKeylet.key, depositAmount), + ter{tecNO_AUTH}); + env.close(); + + // Set CanTransfer again and withdraw + mpt.set({.mutableFlags = tmfMPTSetCanTransfer}); + env.close(); + + env(coverWithdraw(alice, brokerKeylet.key, depositAmount)); + env.close(); + + if (auto const refreshed = env.le(brokerKeylet); + BEAST_EXPECT(refreshed)) + { + BEAST_EXPECT(refreshed->at(sfCoverAvailable) == 0); + env.require(balance(pseudoAccount, asset(0))); + } } +#if LOANTODO void testLoanPayLateFullPaymentBypassesPenalties() { @@ -6855,10 +6892,10 @@ public: run() override { #if LOANTODO - testCoverDepositAllowsNonTransferableMPT(); testLoanPayLateFullPaymentBypassesPenalties(); testLoanCoverMinimumRoundingExploit(); #endif + testCoverDepositWithdrawNonTransferableMPT(); testPoC_UnsignedUnderflowOnFullPayAfterEarlyPeriodic(); testDisabled(); diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index a4a3fdd26a..499584a43d 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -587,6 +588,7 @@ class Vault_test : public beast::unit_test::suite Vault vault{env}; env.fund(XRP(1000), issuer, owner, depositor, charlie, dave); env.close(); + env(fset(issuer, asfDefaultRipple)); env(fset(issuer, asfAllowTrustLineClawback)); env(fset(issuer, asfRequireAuth)); env(fset(dave, asfRequireDest)); @@ -657,6 +659,7 @@ class Vault_test : public beast::unit_test::suite env.fund(XRP(1000), issuer, owner); env.close(); + env(fset(issuer, asfDefaultRipple)); env(fset(issuer, asfAllowTrustLineClawback)); env(fset(issuer, asfRequireAuth)); env.close(); @@ -1765,7 +1768,8 @@ class Vault_test : public beast::unit_test::suite mptt.create( {.flags = tfMPTCanTransfer | tfMPTCanLock | (args.enableClawback ? tfMPTCanClawback : none) | - (args.requireAuth ? tfMPTRequireAuth : none)}); + (args.requireAuth ? tfMPTRequireAuth : none), + .mutableFlags = tmfMPTCanMutateCanTransfer}); PrettyAsset asset = mptt.issuanceID(); mptt.authorize({.account = owner}); mptt.authorize({.account = depositor}); @@ -2483,6 +2487,53 @@ class Vault_test : public beast::unit_test::suite env(tx2, ter{tecWRONG_ASSET}); env.close(); } + + testCase([this]( + Env& env, + Account const&, + Account const& owner, + Account const& depositor, + PrettyAsset const& asset, + Vault& vault, + MPTTester& mptt) { + testcase("MPT non-transferable"); + + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + + tx = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(100)}); + env(tx); + env.close(); + + // Remove CanTransfer + mptt.set({.mutableFlags = tmfMPTClearCanTransfer}); + env.close(); + + env(tx, ter{tecNO_AUTH}); + env.close(); + + tx = vault.withdraw( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(100)}); + + env(tx, ter{tecNO_AUTH}); + env.close(); + + // Restore CanTransfer + mptt.set({.mutableFlags = tmfMPTSetCanTransfer}); + env.close(); + + env(tx); + env.close(); + + // Delete vault with zero balance + env(vault.del({.owner = owner, .id = keylet.key})); + }); } void @@ -2495,6 +2546,7 @@ class Vault_test : public beast::unit_test::suite int initialXRP = 1000; Number initialIOU = 200; double transferRate = 1.0; + bool charlieRipple = true; }; auto testCase = @@ -2515,13 +2567,27 @@ class Vault_test : public beast::unit_test::suite Account const charlie{"charlie"}; Vault vault{env}; env.fund(XRP(args.initialXRP), issuer, owner, charlie); + env(fset(issuer, asfDefaultRipple)); env(fset(issuer, asfAllowTrustLineClawback)); env.close(); PrettyAsset const asset = issuer["IOU"]; env.trust(asset(1000), owner); - env.trust(asset(1000), charlie); env(pay(issuer, owner, asset(args.initialIOU))); + env.close(); + if (!args.charlieRipple) + { + env(fset(issuer, 0, asfDefaultRipple)); + env.close(); + env.trust(asset(1000), charlie); + env.close(); + env(pay(issuer, charlie, asset(args.initialIOU))); + env.close(); + env(fset(issuer, asfDefaultRipple)); + } + else + env.trust(asset(1000), charlie); + env.close(); env(rate(issuer, args.transferRate)); env.close(); @@ -2899,6 +2965,89 @@ class Vault_test : public beast::unit_test::suite env(tx1); }); + testCase( + [&, this]( + Env& env, + Account const& owner, + Account const& issuer, + Account const& charlie, + auto, + Vault& vault, + PrettyAsset const& asset, + std::function issuanceId) { + testcase("IOU non-transferable"); + + auto [tx, keylet] = + vault.create({.owner = owner, .asset = asset}); + tx[sfScale] = 0; + env(tx); + env.close(); + + { + // Charlie cannot deposit + auto tx = vault.deposit( + {.depositor = charlie, + .id = keylet.key, + .amount = asset(100)}); + env(tx, ter{terNO_RIPPLE}); + env.close(); + } + + { + PrettyAsset shares = issuanceId(keylet); + auto tx1 = vault.deposit( + {.depositor = owner, + .id = keylet.key, + .amount = asset(100)}); + env(tx1); + env.close(); + + // Charlie cannot receive funds + auto tx2 = vault.withdraw( + {.depositor = owner, + .id = keylet.key, + .amount = shares(100)}); + tx2[sfDestination] = charlie.human(); + env(tx2, ter{terNO_RIPPLE}); + env.close(); + + { + // Create MPToken for shares held by Charlie + Json::Value tx{Json::objectValue}; + tx[sfAccount] = charlie.human(); + tx[sfMPTokenIssuanceID] = + to_string(shares.raw().get().getMptID()); + tx[sfTransactionType] = jss::MPTokenAuthorize; + env(tx); + env.close(); + } + env(pay(owner, charlie, shares(100))); + env.close(); + + // Charlie cannot withdraw + auto tx3 = vault.withdraw( + {.depositor = charlie, + .id = keylet.key, + .amount = shares(100)}); + env(tx3, ter{terNO_RIPPLE}); + env.close(); + + env(pay(charlie, owner, shares(100))); + env.close(); + } + + tx = vault.withdraw( + {.depositor = owner, + .id = keylet.key, + .amount = asset(100)}); + env(tx); + env.close(); + + // Delete vault with zero balance + env(vault.del({.owner = owner, .id = keylet.key})); + }, + {.charlieRipple = false}); + testCase( [&, this]( Env& env, @@ -3217,6 +3366,7 @@ class Vault_test : public beast::unit_test::suite credIssuer1, credIssuer2); env.close(); + env(fset(issuer, asfDefaultRipple)); env(fset(issuer, asfAllowTrustLineClawback)); env.close(); env.require(flags(issuer, asfAllowTrustLineClawback)); @@ -3646,6 +3796,7 @@ class Vault_test : public beast::unit_test::suite Account const depositor{"depositor"}; Vault vault{env}; env.fund(XRP(1000), issuer, owner, depositor); + env(fset(issuer, asfDefaultRipple)); env(fset(issuer, asfAllowTrustLineClawback)); env.close(); diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp index 25075582c5..974bb36a93 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp @@ -52,13 +52,16 @@ LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx) JLOG(ctx.j.fatal()) << "Vault is missing for Broker " << brokerID; return tefBAD_LEDGER; } - auto const vaultAsset = vault->at(sfAsset); + auto const vaultAsset = vault->at(sfAsset); if (amount.asset() != vaultAsset) return tecWRONG_ASSET; auto const pseudoAccountID = sleBroker->at(sfAccount); - + // Cannot transfer a non-transferable Asset + if (auto const ret = + canTransfer(ctx.view, vaultAsset, account, pseudoAccountID)) + return ret; // Cannot transfer a frozen Asset if (auto const ret = checkFrozen(ctx.view, account, vaultAsset)) return ret; diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp index aebda09e0b..58204b70d5 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp @@ -60,14 +60,21 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) auto const vault = ctx.view.read(keylet::vault(sleBroker->at(sfVaultID))); if (!vault) return tefBAD_LEDGER; // LCOV_EXCL_LINE - auto const vaultAsset = vault->at(sfAsset); + auto const vaultAsset = vault->at(sfAsset); if (amount.asset() != vaultAsset) return tecWRONG_ASSET; + // The broker's pseudo-account is the source of funds. + auto const pseudoAccountID = sleBroker->at(sfAccount); + // Cannot transfer a non-transferable Asset + if (auto const ret = + canTransfer(ctx.view, vaultAsset, pseudoAccountID, dstAcct)) + return ret; + // Withdrawal to a 3rd party destination account is essentially a transfer. // Enforce all the usual asset transfer checks. - AuthType authType = AuthType::Legacy; + AuthType authType = AuthType::WeakAuth; if (account != dstAcct) { if (auto const ret = canWithdraw(ctx.view, tx)) @@ -82,9 +89,6 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) if (auto const ter = requireAuth(ctx.view, vaultAsset, dstAcct, authType)) return ter; - // The broker's pseudo-account is the source of funds. - auto const pseudoAccountID = sleBroker->at(sfAccount); - // Check for freezes, unless sending directly to the issuer if (dstAcct != vaultAsset.getIssuer()) { @@ -146,8 +150,16 @@ LoanBrokerCoverWithdraw::doApply() broker->at(sfCoverAvailable) -= amount; view().update(broker); - // Move the funds from the broker's pseudo-account to the dstAcct + // Create trust line or MPToken for the receiving account + if (dstAcct == account_) + { + if (auto const ter = addEmptyHolding( + view(), account_, mPriorBalance, amount.asset(), j_); + !isTesSuccess(ter) && ter != tecDUPLICATE) + return ter; + } + // Move the funds from the broker's pseudo-account to the dstAcct if (dstAcct == account_ || amount.native()) { // Transfer assets directly from pseudo-account to depositor. diff --git a/src/xrpld/app/tx/detail/VaultDeposit.cpp b/src/xrpld/app/tx/detail/VaultDeposit.cpp index 3e5ae741e3..aeaf890126 100644 --- a/src/xrpld/app/tx/detail/VaultDeposit.cpp +++ b/src/xrpld/app/tx/detail/VaultDeposit.cpp @@ -36,41 +36,19 @@ VaultDeposit::preclaim(PreclaimContext const& ctx) if (!vault) return tecNO_ENTRY; - auto const account = ctx.tx[sfAccount]; + auto const& account = ctx.tx[sfAccount]; auto const assets = ctx.tx[sfAmount]; auto const vaultAsset = vault->at(sfAsset); if (assets.asset() != vaultAsset) return tecWRONG_ASSET; - if (vaultAsset.native()) - ; // No special checks for XRP - else if (vaultAsset.holds()) + auto const& vaultAccount = vault->at(sfAccount); + if (auto ter = canTransfer(ctx.view, vaultAsset, account, vaultAccount); + !isTesSuccess(ter)) { - auto mptID = vaultAsset.get().getMptID(); - auto issuance = ctx.view.read(keylet::mptIssuance(mptID)); - if (!issuance) - return tecOBJECT_NOT_FOUND; - if (!issuance->isFlag(lsfMPTCanTransfer)) - { - // LCOV_EXCL_START - JLOG(ctx.j.error()) - << "VaultDeposit: vault assets are non-transferable."; - return tecNO_AUTH; - // LCOV_EXCL_STOP - } - } - else if (vaultAsset.holds()) - { - auto const issuer = - ctx.view.read(keylet::account(vaultAsset.getIssuer())); - if (!issuer) - { - // LCOV_EXCL_START - JLOG(ctx.j.error()) - << "VaultDeposit: missing issuer of vault assets."; - return tefINTERNAL; - // LCOV_EXCL_STOP - } + JLOG(ctx.j.debug()) + << "VaultDeposit: vault assets are non-transferable."; + return ter; } auto const mptIssuanceID = vault->at(sfShareMPTID); diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.cpp b/src/xrpld/app/tx/detail/VaultWithdraw.cpp index dc8cbde7c0..de84a372e7 100644 --- a/src/xrpld/app/tx/detail/VaultWithdraw.cpp +++ b/src/xrpld/app/tx/detail/VaultWithdraw.cpp @@ -47,35 +47,15 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx) if (assets.asset() != vaultAsset && assets.asset() != vaultShare) return tecWRONG_ASSET; - if (vaultAsset.native()) - ; // No special checks for XRP - else if (vaultAsset.holds()) + auto const& vaultAccount = vault->at(sfAccount); + auto const& account = ctx.tx[sfAccount]; + auto const& dstAcct = ctx.tx[~sfDestination].value_or(account); + if (auto ter = canTransfer(ctx.view, vaultAsset, vaultAccount, dstAcct); + !isTesSuccess(ter)) { - auto mptID = vaultAsset.get().getMptID(); - auto issuance = ctx.view.read(keylet::mptIssuance(mptID)); - if (!issuance) - return tecOBJECT_NOT_FOUND; - if (!issuance->isFlag(lsfMPTCanTransfer)) - { - // LCOV_EXCL_START - JLOG(ctx.j.error()) - << "VaultWithdraw: vault assets are non-transferable."; - return tecNO_AUTH; - // LCOV_EXCL_STOP - } - } - else if (vaultAsset.holds()) - { - auto const issuer = - ctx.view.read(keylet::account(vaultAsset.getIssuer())); - if (!issuer) - { - // LCOV_EXCL_START - JLOG(ctx.j.error()) - << "VaultWithdraw: missing issuer of vault assets."; - return tefINTERNAL; - // LCOV_EXCL_STOP - } + JLOG(ctx.j.debug()) + << "VaultWithdraw: vault assets are non-transferable."; + return ter; } // Enforce valid withdrawal policy @@ -87,9 +67,6 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx) // LCOV_EXCL_STOP } - auto const account = ctx.tx[sfAccount]; - auto const dstAcct = ctx.tx[~sfDestination].value_or(account); - if (auto const ret = canWithdraw(ctx.view, ctx.tx)) return ret; @@ -259,9 +236,9 @@ VaultWithdraw::doApply() } auto const dstAcct = ctx_.tx[~sfDestination].value_or(account_); - if (!vaultAsset.native() && // - dstAcct != vaultAsset.getIssuer() && // - dstAcct == account_) + + // Create trust line or MPToken for the receiving account + if (dstAcct == account_) { if (auto const ter = addEmptyHolding( view(), account_, mPriorBalance, vaultAsset, j_); From fd115cfc131ba484c152efd59aed2c2fee6f6ff7 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 18 Nov 2025 18:11:16 -0500 Subject: [PATCH 236/291] Address my review feedback on (#6017) - Shortcut on issuer match in canTransfer. - An asset can transfer if either trustline enabled rippling, so both must have it disabled for the transfer to fail with terNO_RIPPLE. - Remove unnecessary asfDefaultRipple sets in unit tests. - Add a new unit test helper class: testline, and a macro: THISLINE. When included as a parameter to Env::operator(), will include the line number of the transaction that didn't get the expected result. Works similarly to BEAST_EXPECT. I didn't do the same for the file name, because that can be deduced from the testcase name. --- src/libxrpl/ledger/View.cpp | 8 ++--- src/test/app/Loan_test.cpp | 57 ++++++++++++++++++++++------------ src/test/app/Vault_test.cpp | 27 ++++++++-------- src/test/jtx.h | 1 + src/test/jtx/JTx.h | 3 ++ src/test/jtx/impl/Env.cpp | 28 +++++++++-------- src/test/jtx/impl/testline.cpp | 15 +++++++++ src/test/jtx/testline.h | 34 ++++++++++++++++++++ 8 files changed, 123 insertions(+), 50 deletions(-) create mode 100644 src/test/jtx/impl/testline.cpp create mode 100644 src/test/jtx/testline.h diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index 88bffae413..28f6bb89be 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -3288,11 +3288,11 @@ canTransfer( return tesSUCCESS; auto const issuerId = issue.getIssuer(); + if (issuerId == from || issuerId == to) + return tesSUCCESS; auto const sleIssuer = view.read(keylet::account(issuerId)); if (sleIssuer == nullptr) return tefINTERNAL; // LCOV_EXCL_LINE - if (issuerId == from || issuerId == to) - return tesSUCCESS; auto const isRippleDisabled = [&](AccountID account) -> bool { // Line might not exist, but some transfers can create it. If this @@ -3306,8 +3306,8 @@ canTransfer( return sleIssuer->isFlag(lsfDefaultRipple) == false; }; - // Fail if rippling disabled on either trust line - if (isRippleDisabled(from) || isRippleDisabled(to)) + // Fail if rippling disabled on both trust lines + if (isRippleDisabled(from) && isRippleDisabled(to)) return terNO_RIPPLE; return tesSUCCESS; diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 24cb63a3cf..1db46cf91d 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -4516,8 +4516,8 @@ protected: // preclaim Env env(*this); env.fund(XRP(1'000), lender, issuer, borrower); - env(trust(lender, IOU(10'000'000))); - env(pay(issuer, lender, IOU(5'000'000))); + env(trust(lender, IOU(10'000'000)), THISLINE); + env(pay(issuer, lender, IOU(5'000'000)), THISLINE); BrokerInfo brokerInfo{createVaultAndBroker(env, issuer["IOU"], lender)}; auto const loanSetFee = fee(env.current()->fees().base * 2); @@ -4525,21 +4525,24 @@ protected: env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), sig(sfCounterpartySignature, lender), - loanSetFee); + loanSetFee, + THISLINE); env.close(); std::uint32_t const loanSequence = 1; auto const loanKeylet = keylet::loan(brokerInfo.brokerID, loanSequence); - env(fset(issuer, asfGlobalFreeze)); + env(fset(issuer, asfGlobalFreeze), THISLINE); env.close(); // preclaim: tecFROZEN - env(pay(borrower, loanKeylet.key, debtMaximumRequest), ter(tecFROZEN)); + env(pay(borrower, loanKeylet.key, debtMaximumRequest), + ter(tecFROZEN), + THISLINE); env.close(); - env(fclear(issuer, asfGlobalFreeze)); + env(fclear(issuer, asfGlobalFreeze), THISLINE); env.close(); auto const pseudoBroker = [&]() -> std::optional { @@ -4559,37 +4562,51 @@ protected: // Lender and pseudoaccount must both be frozen env(trust( - issuer, - lender["IOU"](1'000), - lender, - tfSetFreeze | tfSetDeepFreeze)); + issuer, + lender["IOU"](1'000), + lender, + tfSetFreeze | tfSetDeepFreeze), + THISLINE); env(trust( - issuer, - (*pseudoBroker)["IOU"](1'000), - *pseudoBroker, - tfSetFreeze | tfSetDeepFreeze)); + issuer, + (*pseudoBroker)["IOU"](1'000), + *pseudoBroker, + tfSetFreeze | tfSetDeepFreeze), + THISLINE); env.close(); // preclaim: tecFROZEN due to deep frozen - env(pay(borrower, loanKeylet.key, debtMaximumRequest), ter(tecFROZEN)); + env(pay(borrower, loanKeylet.key, debtMaximumRequest), + ter(tecFROZEN), + THISLINE); env.close(); // Only one needs to be unfrozen env(trust( - issuer, lender["IOU"](1'000), tfClearFreeze | tfClearDeepFreeze)); + issuer, + lender["IOU"](1'000), + tfClearFreeze | tfClearDeepFreeze), + THISLINE); env.close(); // The payment is late by this point - env(pay(borrower, loanKeylet.key, debtMaximumRequest), ter(tecEXPIRED)); + env(pay(borrower, loanKeylet.key, debtMaximumRequest), + ter(tecEXPIRED), + THISLINE); env.close(); - env(pay( - borrower, loanKeylet.key, debtMaximumRequest, tfLoanLatePayment)); + env(pay(borrower, + loanKeylet.key, + debtMaximumRequest, + tfLoanLatePayment), + THISLINE); env.close(); // preclaim: tecKILLED // note that tecKILLED in loanMakePayment() // doesn't happen because of the preclaim check. - env(pay(borrower, loanKeylet.key, debtMaximumRequest), ter(tecKILLED)); + env(pay(borrower, loanKeylet.key, debtMaximumRequest), + ter(tecKILLED), + THISLINE); } void diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 499584a43d..ce76e06912 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -588,7 +589,6 @@ class Vault_test : public beast::unit_test::suite Vault vault{env}; env.fund(XRP(1000), issuer, owner, depositor, charlie, dave); env.close(); - env(fset(issuer, asfDefaultRipple)); env(fset(issuer, asfAllowTrustLineClawback)); env(fset(issuer, asfRequireAuth)); env(fset(dave, asfRequireDest)); @@ -659,7 +659,6 @@ class Vault_test : public beast::unit_test::suite env.fund(XRP(1000), issuer, owner); env.close(); - env(fset(issuer, asfDefaultRipple)); env(fset(issuer, asfAllowTrustLineClawback)); env(fset(issuer, asfRequireAuth)); env.close(); @@ -2567,7 +2566,6 @@ class Vault_test : public beast::unit_test::suite Account const charlie{"charlie"}; Vault vault{env}; env.fund(XRP(args.initialXRP), issuer, owner, charlie); - env(fset(issuer, asfDefaultRipple)); env(fset(issuer, asfAllowTrustLineClawback)); env.close(); @@ -2971,7 +2969,7 @@ class Vault_test : public beast::unit_test::suite Account const& owner, Account const& issuer, Account const& charlie, - auto, + auto vaultAccount, Vault& vault, PrettyAsset const& asset, std::function issuanceId) { @@ -2983,13 +2981,18 @@ class Vault_test : public beast::unit_test::suite env(tx); env.close(); + // Turn on noripple on the pseudo account's trust line. + // Charlie's is already set. + env(trust(issuer, vaultAccount(keylet)["IOU"], tfSetNoRipple), + THISLINE); + { // Charlie cannot deposit auto tx = vault.deposit( {.depositor = charlie, .id = keylet.key, .amount = asset(100)}); - env(tx, ter{terNO_RIPPLE}); + env(tx, ter{terNO_RIPPLE}, THISLINE); env.close(); } @@ -2999,7 +3002,7 @@ class Vault_test : public beast::unit_test::suite {.depositor = owner, .id = keylet.key, .amount = asset(100)}); - env(tx1); + env(tx1, THISLINE); env.close(); // Charlie cannot receive funds @@ -3008,7 +3011,7 @@ class Vault_test : public beast::unit_test::suite .id = keylet.key, .amount = shares(100)}); tx2[sfDestination] = charlie.human(); - env(tx2, ter{terNO_RIPPLE}); + env(tx2, ter{terNO_RIPPLE}, THISLINE); env.close(); { @@ -3021,7 +3024,7 @@ class Vault_test : public beast::unit_test::suite env(tx); env.close(); } - env(pay(owner, charlie, shares(100))); + env(pay(owner, charlie, shares(100)), THISLINE); env.close(); // Charlie cannot withdraw @@ -3032,7 +3035,7 @@ class Vault_test : public beast::unit_test::suite env(tx3, ter{terNO_RIPPLE}); env.close(); - env(pay(charlie, owner, shares(100))); + env(pay(charlie, owner, shares(100)), THISLINE); env.close(); } @@ -3040,11 +3043,11 @@ class Vault_test : public beast::unit_test::suite {.depositor = owner, .id = keylet.key, .amount = asset(100)}); - env(tx); + env(tx, THISLINE); env.close(); // Delete vault with zero balance - env(vault.del({.owner = owner, .id = keylet.key})); + env(vault.del({.owner = owner, .id = keylet.key}), THISLINE); }, {.charlieRipple = false}); @@ -3366,7 +3369,6 @@ class Vault_test : public beast::unit_test::suite credIssuer1, credIssuer2); env.close(); - env(fset(issuer, asfDefaultRipple)); env(fset(issuer, asfAllowTrustLineClawback)); env.close(); env.require(flags(issuer, asfAllowTrustLineClawback)); @@ -3796,7 +3798,6 @@ class Vault_test : public beast::unit_test::suite Account const depositor{"depositor"}; Vault vault{env}; env.fund(XRP(1000), issuer, owner, depositor); - env(fset(issuer, asfDefaultRipple)); env(fset(issuer, asfAllowTrustLineClawback)); env.close(); diff --git a/src/test/jtx.h b/src/test/jtx.h index 4c33495ad8..1d7f38ff54 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -52,6 +52,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/jtx/JTx.h b/src/test/jtx/JTx.h index d1160b0f34..2baaf8cd10 100644 --- a/src/test/jtx/JTx.h +++ b/src/test/jtx/JTx.h @@ -40,6 +40,9 @@ struct JTx // Functions that sign something else after the mainSigners, such as // sfCounterpartySignature std::vector> postSigners; + // Metadata about the unit test itself + // The line where the JTx was constructed + std::optional testLine = std::nullopt; JTx() = default; JTx(JTx const&) = default; diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index 2ef7879c97..9ce76d01c9 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -427,14 +427,16 @@ Env::postconditions( ParsedResult const& parsed, Json::Value const& jr) { - bool bad = !test.expect(parsed.ter, "apply: No ter result!"); + auto const line = jt.testLine ? " (" + to_string(*jt.testLine) + ")" : ""; + bool bad = !test.expect(parsed.ter, "apply: No ter result!" + line); bad = (jt.ter && parsed.ter && !test.expect( *parsed.ter == *jt.ter, "apply: Got " + transToken(*parsed.ter) + " (" + transHuman(*parsed.ter) + "); Expected " + - transToken(*jt.ter) + " (" + transHuman(*jt.ter) + ")")); + transToken(*jt.ter) + " (" + transHuman(*jt.ter) + ")" + + line)); using namespace std::string_literals; bad = (jt.rpcCode && !test.expect( @@ -446,21 +448,21 @@ Env::postconditions( : "NO RESULT") + " (" + parsed.rpcMessage + "); Expected " + RPC::get_error_info(jt.rpcCode->first).token.c_str() + " (" + - jt.rpcCode->second + ")")) || + jt.rpcCode->second + ")" + line)) || bad; // If we have an rpcCode (just checked), then the rpcException check is // optional - the 'error' field may not be defined, but if it is, it must // match rpcError. - bad = - (jt.rpcException && - !test.expect( - (jt.rpcCode && parsed.rpcError.empty()) || - (parsed.rpcError == jt.rpcException->first && - (!jt.rpcException->second || - parsed.rpcException == *jt.rpcException->second)), - "apply: Got RPC result "s + parsed.rpcError + " (" + - parsed.rpcException + "); Expected " + jt.rpcException->first + - " (" + jt.rpcException->second.value_or("n/a") + ")")) || + bad = (jt.rpcException && + !test.expect( + (jt.rpcCode && parsed.rpcError.empty()) || + (parsed.rpcError == jt.rpcException->first && + (!jt.rpcException->second || + parsed.rpcException == *jt.rpcException->second)), + "apply: Got RPC result "s + parsed.rpcError + " (" + + parsed.rpcException + "); Expected " + + jt.rpcException->first + " (" + + jt.rpcException->second.value_or("n/a") + ")" + line)) || bad; if (bad) { diff --git a/src/test/jtx/impl/testline.cpp b/src/test/jtx/impl/testline.cpp new file mode 100644 index 0000000000..722dc33bff --- /dev/null +++ b/src/test/jtx/impl/testline.cpp @@ -0,0 +1,15 @@ +#include + +namespace ripple { +namespace test { +namespace jtx { + +void +testline::operator()(Env&, JTx& jt) const +{ + jt.testLine = line_; +} + +} // namespace jtx +} // namespace test +} // namespace ripple diff --git a/src/test/jtx/testline.h b/src/test/jtx/testline.h new file mode 100644 index 0000000000..65dd3b7d92 --- /dev/null +++ b/src/test/jtx/testline.h @@ -0,0 +1,34 @@ +#ifndef XRPL_TEST_JTX_TESTLINE_H_INCLUDED +#define XRPL_TEST_JTX_TESTLINE_H_INCLUDED + +#include + +namespace ripple { +namespace test { +namespace jtx { + +/** Store the line number of the current test in a JTx. + + Intended to help debug failing transaction submission tests. +*/ +class testline +{ +private: + int line_; + +public: + explicit testline(int line) : line_(line) + { + } + + void + operator()(Env&, JTx& jt) const; +}; + +#define THISLINE testline(__LINE__) + +} // namespace jtx +} // namespace test +} // namespace ripple + +#endif From 8a1f244682d7bb4554d7cc0b4801ecb2d4ea9a9f Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 14 Nov 2025 17:34:53 -0500 Subject: [PATCH 237/291] Updated overpayment processing - Added a test for RIPD-4125 --- src/test/app/Loan_test.cpp | 275 +++++++++++++------ src/xrpld/app/misc/LendingHelpers.h | 12 + src/xrpld/app/misc/detail/LendingHelpers.cpp | 260 ++++++++++++++---- src/xrpld/app/tx/detail/LoanPay.cpp | 12 + src/xrpld/app/tx/detail/LoanSet.cpp | 85 +----- src/xrpld/app/tx/detail/LoanSet.h | 9 - 6 files changed, 436 insertions(+), 217 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 1db46cf91d..fa4ed233b4 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -83,6 +83,7 @@ protected: TenthBips16 managementFeeRate{100}; TenthBips32 coverRateLiquidation = percentageToTenthBips(25); std::string data{}; + std::uint32_t flags = 0; Number maxCoveredLoanValue(Number const& currentDebt) const @@ -223,6 +224,22 @@ protected: } }; + struct PaymentParameters + { + Number overpaymentFactor = Number{1}; + std::optional overpaymentExtra = std::nullopt; + std::uint32_t flags = 0; + bool showStepBalances = false; + bool validateBalances = true; + + static PaymentParameters const& + defaults() + { + static PaymentParameters const result{}; + return result; + } + }; + struct LoanState { std::uint32_t previousPaymentDate = 0; @@ -465,7 +482,7 @@ protected: auto const keylet = keylet::loanbroker(lender.id(), env.seq(lender)); using namespace loanBroker; - env(set(lender, vaultKeylet.key), + env(set(lender, vaultKeylet.key, params.flags), data(params.data), managementFeeRate(params.managementFeeRate), debtMaximum(debtMaximumValue), @@ -700,10 +717,10 @@ protected: << std::endl; // checkGuards returns a TER, so success is 0 - BEAST_EXPECT(!LoanSet::checkGuards( + BEAST_EXPECT(!checkLoanGuards( asset, asset(loanParams.principalRequest).number(), - loanParams.interest.value_or(TenthBips32{}), + loanParams.interest.value_or(TenthBips32{}) != beast::zero, loanParams.payTotal.value_or(LoanSet::defaultPaymentTotal), props, env.journal)); @@ -835,7 +852,7 @@ protected: jtx::Account const& issuer, jtx::Account const& lender, jtx::Account const& borrower, - bool showStepBalances = false) + PaymentParameters const& paymentParams = PaymentParameters::defaults()) { // Make all the individual payments using namespace jtx; @@ -846,6 +863,8 @@ protected: // Account const evan{"evan"}; // Account const alice{"alice"}; + bool const showStepBalances = paymentParams.showStepBalances; + auto const currencyLabel = getCurrencyLabel(broker.asset); auto const baseFee = env.current()->fees().base; @@ -891,24 +910,25 @@ protected: state.loanScale, Number::upward); + auto currentRoundedState = constructRoundedLoanState( + state.totalValue, + state.principalOutstanding, + state.managementFeeOutstanding); { auto const raw = calculateRawLoanState( state.periodicPayment, periodicRate, state.paymentRemaining, broker.params.managementFeeRate); - auto const rounded = constructRoundedLoanState( - state.totalValue, - state.principalOutstanding, - state.managementFeeOutstanding); if (showStepBalances) { log << currencyLabel << " Starting loan balances: " - << "\n\tTotal value: " << rounded.valueOutstanding - << "\n\tPrincipal: " << rounded.principalOutstanding - << "\n\tInterest: " << rounded.interestDue - << "\n\tMgmt fee: " << rounded.managementFeeDue + << "\n\tTotal value: " + << currentRoundedState.valueOutstanding << "\n\tPrincipal: " + << currentRoundedState.principalOutstanding + << "\n\tInterest: " << currentRoundedState.interestDue + << "\n\tMgmt fee: " << currentRoundedState.managementFeeDue << "\n\tPayments remaining " << state.paymentRemaining << std::endl; } @@ -918,18 +938,24 @@ protected: << " Loan starting state: " << state.paymentRemaining << ", " << raw.interestDue << ", " << raw.principalOutstanding << ", " << raw.managementFeeDue - << ", " << rounded.valueOutstanding << ", " - << rounded.principalOutstanding << ", " - << rounded.interestDue << ", " << rounded.managementFeeDue - << std::endl; + << ", " << currentRoundedState.valueOutstanding << ", " + << currentRoundedState.principalOutstanding << ", " + << currentRoundedState.interestDue << ", " + << currentRoundedState.managementFeeDue << std::endl; } } // Try to pay a little extra to show that it's _not_ // taken - STAmount const transactionAmount = STAmount{broker.asset, totalDue} + - std::min(broker.asset(10).value(), - STAmount{broker.asset, totalDue / 20}); + auto const extraAmount = paymentParams.overpaymentExtra + ? broker.asset(*paymentParams.overpaymentExtra).value() + : std::min( + broker.asset(10).value(), + STAmount{broker.asset, totalDue / 20}); + + STAmount const transactionAmount = + STAmount{broker.asset, totalDue * paymentParams.overpaymentFactor} + + extraAmount; auto const borrowerInitialBalance = env.balance(borrower, broker.asset).number(); @@ -949,7 +975,7 @@ protected: broker.params.managementFeeRate); auto validateBorrowerBalance = [&]() { - if (borrower == issuer) + if (borrower == issuer || !paymentParams.validateBalances) return; auto const totalSpent = (totalPaid.trackedValueDelta + totalFeesPaid + @@ -1035,54 +1061,64 @@ protected: auto const totalDueAmount = STAmount{ broker.asset, paymentComponents.trackedValueDelta + serviceFee}; - // Due to the rounding algorithms to keep the interest and - // principal in sync with "true" values, the computed amount - // may be a little less than the rounded fixed payment - // amount. For integral types, the difference should be < 3 - // (1 unit for each of the interest and management fee). For - // IOUs, the difference should be dust. - Number const diff = totalDue - totalDueAmount; - BEAST_EXPECT( - paymentComponents.specialCase == - detail::PaymentSpecialCase::final || - diff == beast::zero || - (diff > beast::zero && - ((broker.asset.integral() && - (static_cast(diff) < 3)) || - (state.loanScale - diff.exponent() > 13)))); + if (paymentParams.validateBalances) + { + // Due to the rounding algorithms to keep the interest and + // principal in sync with "true" values, the computed amount + // may be a little less than the rounded fixed payment + // amount. For integral types, the difference should be < 3 + // (1 unit for each of the interest and management fee). For + // IOUs, the difference should be dust. + Number const diff = totalDue - totalDueAmount; + BEAST_EXPECT( + paymentComponents.specialCase == + detail::PaymentSpecialCase::final || + diff == beast::zero || + (diff > beast::zero && + ((broker.asset.integral() && + (static_cast(diff) < 3)) || + (state.loanScale - diff.exponent() > 13)))); - BEAST_EXPECT( - paymentComponents.trackedPrincipalDelta >= beast::zero && - paymentComponents.trackedPrincipalDelta <= - state.principalOutstanding); - BEAST_EXPECT( - paymentComponents.specialCase != - detail::PaymentSpecialCase::final || - paymentComponents.trackedPrincipalDelta == - state.principalOutstanding); + BEAST_EXPECT( + paymentComponents.trackedPrincipalDelta >= beast::zero && + paymentComponents.trackedPrincipalDelta <= + state.principalOutstanding); + BEAST_EXPECT( + paymentComponents.specialCase != + detail::PaymentSpecialCase::final || + paymentComponents.trackedPrincipalDelta == + state.principalOutstanding); + } auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset); // Make the payment - env(pay(borrower, loanKeylet.key, transactionAmount)); + env( + pay(borrower, + loanKeylet.key, + transactionAmount, + paymentParams.flags)); env.close(d{state.paymentInterval / 2}); - // Need to account for fees if the loan is in XRP - PrettyAmount adjustment = broker.asset(0); - if (broker.asset.native()) + if (paymentParams.validateBalances) { - adjustment = env.current()->fees().base; - } + // Need to account for fees if the loan is in XRP + PrettyAmount adjustment = broker.asset(0); + if (broker.asset.native()) + { + adjustment = env.current()->fees().base; + } - // Check the result - verifyLoanStatus.checkPayment( - state.loanScale, - borrower, - borrowerBalanceBeforePayment, - totalDueAmount, - adjustment); + // Check the result + verifyLoanStatus.checkPayment( + state.loanScale, + borrower, + borrowerBalanceBeforePayment, + totalDueAmount, + adjustment); + } if (showStepBalances) { @@ -1110,6 +1146,8 @@ protected: << ", error: " << truncate(errors.managementFee) << ")\n\tPayments remaining " << loanSle->at(sfPaymentRemaining) << std::endl; + + currentRoundedState = current; } --state.paymentRemaining; @@ -1130,7 +1168,8 @@ protected: paymentComponents.trackedManagementFeeDelta; state.totalValue -= paymentComponents.trackedValueDelta; - verifyLoanStatus(state); + if (paymentParams.validateBalances) + verifyLoanStatus(state); totalPaid.trackedValueDelta += paymentComponents.trackedValueDelta; totalPaid.trackedPrincipalDelta += @@ -1149,21 +1188,25 @@ protected: BEAST_EXPECT(state.paymentRemaining == 0); BEAST_EXPECT(state.principalOutstanding == 0); - // Make sure all the payments add up - BEAST_EXPECT(totalPaid.trackedValueDelta == initialState.totalValue); - BEAST_EXPECT( - totalPaid.trackedPrincipalDelta == - initialState.principalOutstanding); - BEAST_EXPECT( - totalPaid.trackedManagementFeeDelta == - initialState.managementFeeOutstanding); - // This is almost a tautology given the previous checks, but - // check it anyway for completeness. auto const initialInterestDue = initialState.totalValue - (initialState.principalOutstanding + initialState.managementFeeOutstanding); - BEAST_EXPECT(totalInterestPaid == initialInterestDue); - BEAST_EXPECT(totalPaymentsMade == initialState.paymentRemaining); + if (paymentParams.validateBalances) + { + // Make sure all the payments add up + BEAST_EXPECT( + totalPaid.trackedValueDelta == initialState.totalValue); + BEAST_EXPECT( + totalPaid.trackedPrincipalDelta == + initialState.principalOutstanding); + BEAST_EXPECT( + totalPaid.trackedManagementFeeDelta == + initialState.managementFeeOutstanding); + // This is almost a tautology given the previous checks, but + // check it anyway for completeness. + BEAST_EXPECT(totalInterestPaid == initialInterestDue); + BEAST_EXPECT(totalPaymentsMade == initialState.paymentRemaining); + } if (showStepBalances) { @@ -6514,7 +6557,7 @@ protected: issuer, lender, borrower, - true); + PaymentParameters{.showStepBalances = true}); if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle)) @@ -6640,7 +6683,7 @@ protected: env.le(keylet::loanbroker(brokerInfo.brokerID)); BEAST_EXPECT(brokerSle)) { - std::cout << *brokerSle << std::endl; + log << *brokerSle << std::endl; BEAST_EXPECT(brokerSle->at(sfDebtTotal) == Number(804)); } @@ -6662,7 +6705,7 @@ protected: env.le(keylet::loanbroker(brokerInfo.brokerID)); BEAST_EXPECT(brokerSle)) { - std::cout << *brokerSle << std::endl; + log << *brokerSle << std::endl; BEAST_EXPECT( brokerSle->at(sfCoverAvailable) == xrpAsset(81).value()); BEAST_EXPECT(brokerSle->at(sfDebtTotal) == Number(804)); @@ -6670,8 +6713,7 @@ protected: // Also demonstrate that the true minimum (804 * 10%) exceeds 80 auto const theoreticalMin = tenthBipsOfValue(Number(804), TenthBips32(10'000)); - std::cout << "Theoretical min cover: " << theoreticalMin - << std::endl; + log << "Theoretical min cover: " << theoreticalMin << std::endl; BEAST_EXPECT(Number(804, -1) == theoreticalMin); } } @@ -6727,7 +6769,7 @@ protected: issuer, lender, borrower, - true); + PaymentParameters{.showStepBalances = true}); } void @@ -6901,7 +6943,85 @@ protected: issuer, lender, issuer, - true); + PaymentParameters{.showStepBalances = true}); + } + + void + testLimitExceeded() + { + testcase("RIPD-4125 - overpayment"); + + using namespace jtx; + + Account const issuer("issuer"); + Account const lender("lender"); + Account const borrower("borrower"); + + BrokerParameters const brokerParams{ + .vaultDeposit = 100'000, + .debtMax = 0, + .coverRateMin = TenthBips32{0}, + .managementFeeRate = TenthBips16{0}, + .coverRateLiquidation = TenthBips32{0}}; + LoanParameters const loanParams{ + .account = lender, + .counter = borrower, + .principalRequest = Number{200000, -6}, + .interest = TenthBips32{50000}, + .payTotal = 3, + .payInterval = 200, + .gracePd = 60, + .flags = tfLoanOverpayment, + }; + + auto const assetType = AssetType::XRP; + + Env env( + *this, + makeConfig(), + all, + nullptr, + beast::severities::Severity::kWarning); + + auto loanResult = createLoan( + env, assetType, brokerParams, loanParams, issuer, lender, borrower); + + if (!BEAST_EXPECT(loanResult)) + return; + + auto broker = std::get(*loanResult); + auto loanKeylet = std::get(*loanResult); + auto pseudoAcct = std::get(*loanResult); + + VerifyLoanStatus verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); + + auto const state = getCurrentState(env, broker, loanKeylet); + + env(loan::pay( + borrower, + loanKeylet.key, + STAmount{broker.asset, state.periodicPayment * 3 / 2 + 1}, + tfLoanOverpayment)); + env.close(); + + PaymentParameters paymentParams{ + //.overpaymentFactor = Number{15, -1}, + //.overpaymentExtra = Number{1, -6}, + //.flags = tfLoanOverpayment, + .showStepBalances = true, + //.validateBalances = false, + }; + + makeLoanPayments( + env, + broker, + loanParams, + loanKeylet, + verifyLoanStatus, + issuer, + lender, + borrower, + paymentParams); } public: @@ -6951,6 +7071,7 @@ public: testRoundingAllowsUndercoverage(); testBorrowerIsBroker(); testIssuerIsBorrower(); + testLimitExceeded(); } }; diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index f430a0f0d5..28a8ad0829 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -102,6 +102,15 @@ struct LoanState } }; +TER +checkLoanGuards( + Asset const& vaultAsset, + Number const& principalRequested, + bool expectInterest, + std::uint32_t paymentTotal, + LoanProperties const& properties, + beast::Journal j); + LoanState calculateRawLoanState( Number const& periodicPayment, @@ -217,6 +226,9 @@ operator-(LoanState const& lhs, LoanState const& rhs); LoanState operator-(LoanState const& lhs, detail::LoanDeltas const& rhs); +LoanState +operator+(LoanState const& lhs, detail::LoanDeltas const& rhs); + LoanProperties computeLoanProperties( Asset const& asset, diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index 62f567e7c0..e5eac77c1b 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -372,9 +372,7 @@ tryOverpayment( auto const rounded = constructRoundedLoanState( totalValueOutstanding, principalOutstanding, managementFeeOutstanding); - auto const totalValueError = totalValueOutstanding - raw.valueOutstanding; - auto const principalError = principalOutstanding - raw.principalOutstanding; - auto const feeError = managementFeeOutstanding - raw.managementFeeDue; + auto const errors = rounded - raw; auto const newRawPrincipal = std::max( raw.principalOutstanding - overpaymentComponents.trackedPrincipalDelta, @@ -389,33 +387,68 @@ tryOverpayment( managementFeeRate, loanScale); - auto const newRaw = calculateRawLoanState( - newLoanProperties.periodicPayment, - periodicRate, - paymentRemaining, - managementFeeRate); + JLOG(j.debug()) << "new periodic payment: " + << newLoanProperties.periodicPayment + << ", new total value: " + << newLoanProperties.totalValueOutstanding + << ", first payment principal: " + << newLoanProperties.firstPaymentPrincipal; - totalValueOutstanding = roundToAsset( - asset, newRaw.valueOutstanding + totalValueError, loanScale); - principalOutstanding = roundToAsset( - asset, - newRaw.principalOutstanding + principalError, - loanScale, - Number::downward); - managementFeeOutstanding = - roundToAsset(asset, newRaw.managementFeeDue + feeError, loanScale); + auto const newRaw = calculateRawLoanState( + newLoanProperties.periodicPayment, + periodicRate, + paymentRemaining, + managementFeeRate) + + errors; + + JLOG(j.debug()) << "new raw value: " << newRaw.valueOutstanding + << ", principal: " << newRaw.principalOutstanding + << ", interest gross: " << newRaw.interestOutstanding(); + + principalOutstanding = std::clamp( + roundToAsset( + asset, newRaw.principalOutstanding, loanScale, Number::upward), + numZero, + rounded.principalOutstanding); + totalValueOutstanding = std::clamp( + roundToAsset( + asset, + principalOutstanding + newRaw.interestOutstanding(), + loanScale, + Number::upward), + numZero, + rounded.valueOutstanding); + managementFeeOutstanding = std::clamp( + roundToAsset(asset, newRaw.managementFeeDue, loanScale), + numZero, + rounded.managementFeeDue); + + auto const newRounded = constructRoundedLoanState( + totalValueOutstanding, principalOutstanding, managementFeeOutstanding); + + newLoanProperties.totalValueOutstanding = newRounded.valueOutstanding; + + JLOG(j.debug()) << "new rounded value: " << newRounded.valueOutstanding + << ", principal: " << newRounded.principalOutstanding + << ", interest gross: " << newRounded.interestOutstanding(); periodicPayment = newLoanProperties.periodicPayment; // check that the loan is still valid - if (newLoanProperties.firstPaymentPrincipal <= 0 && - principalOutstanding > 0) + if (auto const ter = checkLoanGuards( + asset, + principalOutstanding, + // The loan may have been created with interest, but for + // small interest amounts, that may have already been paid + // off. Check what's still outstanding. This should + // guarantee that the interest checks pass. + newRounded.interestOutstanding() != beast::zero, + paymentRemaining, + newLoanProperties, + j)) { - // The overpayment has caused the loan to be in a state - // where no further principal can be paid. - JLOG(j.warn()) - << "Loan overpayment would cause loan to be stuck. " - "Rejecting overpayment, but normal payments are unaffected."; + JLOG(j.warn()) << "Principal overpayment would cause the loan to be in " + "an invalid state. Ignore the overpayment"; return Unexpected(tesSUCCESS); } @@ -437,21 +470,27 @@ tryOverpayment( // LCOV_EXCL_STOP } - auto const newRounded = constructRoundedLoanState( - totalValueOutstanding, principalOutstanding, managementFeeOutstanding); + auto const deltas = rounded - newRounded; + + auto const hypotheticalValueOutstanding = + rounded.valueOutstanding - deltas.principal; + auto const valueChange = - newRounded.interestOutstanding() - rounded.interestOutstanding(); - XRPL_ASSERT_PARTS( - valueChange <= beast::zero, - "ripple::detail::tryOverpayment", - "principal overpayment did not increase value of loan"); + newRounded.valueOutstanding - hypotheticalValueOutstanding; + if (valueChange > 0) + { + JLOG(j.warn()) << "Principal overpayment would increase the value of " + "the loan. Ignore the overpayment"; + return Unexpected(tesSUCCESS); + } return LoanPaymentParts{ - .principalPaid = - rounded.principalOutstanding - newRounded.principalOutstanding, - .interestPaid = rounded.interestDue - newRounded.interestDue, - .valueChange = valueChange + overpaymentComponents.untrackedInterest, - .feePaid = rounded.managementFeeDue - newRounded.managementFeeDue + + .principalPaid = deltas.principal, + .interestPaid = + deltas.interest + overpaymentComponents.untrackedInterest, + .valueChange = + valueChange + overpaymentComponents.trackedInterestPart(), + .feePaid = deltas.managementFee + overpaymentComponents.untrackedManagementFee}; } @@ -481,6 +520,17 @@ doOverpayment( Number managementFeeOutstanding = managementFeeOutstandingProxy; Number periodicPayment = periodicPaymentProxy; + JLOG(j.debug()) + << "overpayment components:" + << ", totalValue before: " << *totalValueOutstandingProxy + << ", valueDelta: " << overpaymentComponents.trackedValueDelta + << ", principalDelta: " << overpaymentComponents.trackedPrincipalDelta + << ", managementFeeDelta: " + << overpaymentComponents.trackedManagementFeeDelta + << ", interestPart: " << overpaymentComponents.trackedInterestPart() + << ", untrackedInterest: " << overpaymentComponents.untrackedInterest + << ", totalDue: " << overpaymentComponents.totalDue + << ", payments remaining :" << paymentRemaining; auto const ret = tryOverpayment( asset, loanScale, @@ -527,12 +577,29 @@ doOverpayment( "ripple::detail::doOverpayment", "no fee change"); + // I'm not 100% sure the following asserts are correct. If in doubt, and + // everything else works, remove any that cause trouble. + + JLOG(j.debug()) << "valueChange: " << loanPaymentParts.valueChange + << ", totalValue before: " << *totalValueOutstandingProxy + << ", totalValue after: " << totalValueOutstanding + << ", totalValue delta: " + << (totalValueOutstandingProxy - totalValueOutstanding) + << ", principalDelta: " + << overpaymentComponents.trackedPrincipalDelta + << ", principalPaid: " << loanPaymentParts.principalPaid + << ", Computed difference: " + << overpaymentComponents.trackedPrincipalDelta - + (totalValueOutstandingProxy - totalValueOutstanding); + XRPL_ASSERT_PARTS( - overpaymentComponents.untrackedInterest == - totalValueOutstandingProxy - totalValueOutstanding - - overpaymentComponents.trackedPrincipalDelta, + loanPaymentParts.valueChange == + totalValueOutstanding - + (totalValueOutstandingProxy - + overpaymentComponents.trackedPrincipalDelta) + + overpaymentComponents.trackedInterestPart(), "ripple::detail::doOverpayment", - "value change agrees"); + "interest paid agrees"); XRPL_ASSERT_PARTS( overpaymentComponents.trackedPrincipalDelta == @@ -995,11 +1062,9 @@ computeOverpaymentComponents( Number const fee = roundToAsset( asset, tenthBipsOfValue(overpayment, overpaymentFeeRate), loanScale); - Number const payment = overpayment - fee; - - auto const [rawOverpaymentInterest, rawOverpaymentManagementFee] = [&]() { + auto const [rawOverpaymentInterest, _] = [&]() { Number const interest = - tenthBipsOfValue(payment, overpaymentInterestRate); + tenthBipsOfValue(overpayment, overpaymentInterestRate); return detail::computeInterestAndFeeParts(interest, managementFeeRate); }(); auto const [roundedOverpaymentInterest, roundedOverpaymentManagementFee] = @@ -1010,15 +1075,20 @@ computeOverpaymentComponents( asset, interest, managementFeeRate, loanScale); }(); - return detail::ExtendedPaymentComponents{ + auto const result = detail::ExtendedPaymentComponents{ detail::PaymentComponents{ - .trackedValueDelta = payment, - .trackedPrincipalDelta = payment - roundedOverpaymentInterest - - roundedOverpaymentManagementFee, + .trackedValueDelta = overpayment - fee, + .trackedPrincipalDelta = overpayment - roundedOverpaymentInterest - + roundedOverpaymentManagementFee - fee, .trackedManagementFeeDelta = roundedOverpaymentManagementFee, .specialCase = detail::PaymentSpecialCase::extra}, fee, roundedOverpaymentInterest}; + XRPL_ASSERT_PARTS( + result.trackedInterestPart() == roundedOverpaymentInterest, + "ripple::detail::computeOverpaymentComponents", + "valid interest computation"); + return result; } } // namespace detail @@ -1048,6 +1118,100 @@ operator-(LoanState const& lhs, detail::LoanDeltas const& rhs) return result; } +LoanState +operator+(LoanState const& lhs, detail::LoanDeltas const& rhs) +{ + LoanState result{ + .valueOutstanding = lhs.valueOutstanding + rhs.total(), + .principalOutstanding = lhs.principalOutstanding + rhs.principal, + .interestDue = lhs.interestDue + rhs.interest, + .managementFeeDue = lhs.managementFeeDue + rhs.managementFee, + }; + + return result; +} + +TER +checkLoanGuards( + Asset const& vaultAsset, + Number const& principalRequested, + bool expectInterest, + std::uint32_t paymentTotal, + LoanProperties const& properties, + beast::Journal j) +{ + auto const totalInterestOutstanding = + properties.totalValueOutstanding - principalRequested; + // Guard 1: if there is no computed total interest over the life of the + // loan for a non-zero interest rate, we cannot properly amortize the + // loan + if (expectInterest && totalInterestOutstanding <= 0) + { + // Unless this is a zero-interest loan, there must be some interest + // due on the loan, even if it's (measurable) dust + JLOG(j.warn()) << "Loan for " << principalRequested + << " with interest has no interest due"; + return tecPRECISION_LOSS; + } + // Guard 1a: If there is any interest computed over the life of the + // loan, for a zero interest rate, something went sideways. + if (!expectInterest && totalInterestOutstanding > 0) + { + // LCOV_EXCL_START + JLOG(j.warn()) << "Loan for " << principalRequested + << " with no interest has interest due"; + return tecINTERNAL; + // LCOV_EXCL_STOP + } + + // Guard 2: if the principal portion of the first periodic payment is + // too small to be accurately represented with the given rounding mode, + // raise an error + if (properties.firstPaymentPrincipal <= 0) + { + // Check that some true (unrounded) principal is paid each period. + // Since the first payment pays the least principal, if it's good, + // they'll all be good. Note that the outstanding principal is + // rounded, and may not change right away. + JLOG(j.warn()) << "Loan is unable to pay principal."; + return tecPRECISION_LOSS; + } + + // Guard 3: If the periodic payment is so small that it can't even be + // rounded to a representable value, then the loan can't be paid. Also, + // avoids dividing by 0. + auto const roundedPayment = roundPeriodicPayment( + vaultAsset, properties.periodicPayment, properties.loanScale); + if (roundedPayment == beast::zero) + { + JLOG(j.warn()) << "Loan Periodic payment (" + << properties.periodicPayment << ") rounds to 0. "; + return tecPRECISION_LOSS; + } + + // Guard 4: if the rounded periodic payment is large enough that the + // loan can't be amortized in the specified number of payments, raise an + // error + { + NumberRoundModeGuard mg(Number::upward); + + if (std::int64_t const computedPayments{ + properties.totalValueOutstanding / roundedPayment}; + computedPayments != paymentTotal) + { + JLOG(j.warn()) << "Loan Periodic payment (" + << properties.periodicPayment << ") rounding (" + << roundedPayment << ") on a total value of " + << properties.totalValueOutstanding + << " can not complete the loan in the specified " + "number of payments (" + << computedPayments << " != " << paymentTotal << ")"; + return tecPRECISION_LOSS; + } + } + return tesSUCCESS; +} + Number calculateFullPaymentInterest( Number const& rawPrincipalOutstanding, diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 0f3a6a3819..39a93f634a 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -361,6 +361,12 @@ LoanPay::doApply() // LCOV_EXCL_STOP } + JLOG(j_.debug()) << "Loan Pay: principal paid: " + << paymentParts->principalPaid + << ", interest paid: " << paymentParts->interestPaid + << ", fee paid: " << paymentParts->feePaid + << ", value change: " << paymentParts->valueChange; + //------------------------------------------------------ // LoanBroker object state changes view.update(brokerSle); @@ -442,6 +448,12 @@ LoanPay::doApply() } } + JLOG(j_.debug()) << "total paid to vault raw: " << totalPaidToVaultRaw + << ", total paid to vault rounded: " + << totalPaidToVaultRounded + << ", total paid to broker: " << totalPaidToBroker + << ", amount from transaction: " << amount; + // Move funds XRPL_ASSERT_PARTS( totalPaidToVaultRounded + totalPaidToBroker <= amount, diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index f72ad33cee..d9e5f8b981 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -312,87 +312,6 @@ LoanSet::preclaim(PreclaimContext const& ctx) return tesSUCCESS; } -TER -LoanSet::checkGuards( - Asset const& vaultAsset, - Number const& principalRequested, - TenthBips32 interestRate, - std::uint32_t paymentTotal, - LoanProperties const& properties, - beast::Journal j) -{ - auto const totalInterestOutstanding = - properties.totalValueOutstanding - principalRequested; - // Guard 1: if there is no computed total interest over the life of the - // loan for a non-zero interest rate, we cannot properly amortize the - // loan - if (interestRate > TenthBips32{0} && totalInterestOutstanding <= 0) - { - // Unless this is a zero-interest loan, there must be some interest - // due on the loan, even if it's (measurable) dust - JLOG(j.warn()) << "Loan for " << principalRequested << " with " - << interestRate << "% interest has no interest due"; - return tecPRECISION_LOSS; - } - // Guard 1a: If there is any interest computed over the life of the - // loan, for a zero interest rate, something went sideways. - if (interestRate == TenthBips32{0} && totalInterestOutstanding > 0) - { - // LCOV_EXCL_START - JLOG(j.warn()) << "Loan for " << principalRequested - << " with 0% interest has interest due"; - return tecINTERNAL; - // LCOV_EXCL_STOP - } - - // Guard 2: if the principal portion of the first periodic payment is - // too small to be accurately represented with the given rounding mode, - // raise an error - if (properties.firstPaymentPrincipal <= 0) - { - // Check that some true (unrounded) principal is paid each period. - // Since the first payment pays the least principal, if it's good, - // they'll all be good. Note that the outstanding principal is - // rounded, and may not change right away. - JLOG(j.warn()) << "Loan is unable to pay principal."; - return tecPRECISION_LOSS; - } - - // Guard 3: If the periodic payment is so small that it can't even be - // rounded to a representable value, then the loan can't be paid. Also, - // avoids dividing by 0. - auto const roundedPayment = roundPeriodicPayment( - vaultAsset, properties.periodicPayment, properties.loanScale); - if (roundedPayment == beast::zero) - { - JLOG(j.warn()) << "Loan Periodic payment (" - << properties.periodicPayment << ") rounds to 0. "; - return tecPRECISION_LOSS; - } - - // Guard 4: if the rounded periodic payment is large enough that the - // loan can't be amortized in the specified number of payments, raise an - // error - { - NumberRoundModeGuard mg(Number::upward); - - if (std::int64_t const computedPayments{ - properties.totalValueOutstanding / roundedPayment}; - computedPayments != paymentTotal) - { - JLOG(j.warn()) << "Loan Periodic payment (" - << properties.periodicPayment << ") rounding (" - << roundedPayment << ") on a total value of " - << properties.totalValueOutstanding - << " can not complete the loan in the specified " - "number of payments (" - << computedPayments << " != " << paymentTotal << ")"; - return tecPRECISION_LOSS; - } - } - return tesSUCCESS; -} - TER LoanSet::doApply() { @@ -474,10 +393,10 @@ LoanSet::doApply() } } - if (auto const ret = checkGuards( + if (auto const ret = checkLoanGuards( vaultAsset, principalRequested, - interestRate, + interestRate != beast::zero, paymentTotal, properties, j_)) diff --git a/src/xrpld/app/tx/detail/LoanSet.h b/src/xrpld/app/tx/detail/LoanSet.h index e7f4ef5503..91f3960891 100644 --- a/src/xrpld/app/tx/detail/LoanSet.h +++ b/src/xrpld/app/tx/detail/LoanSet.h @@ -36,15 +36,6 @@ public: static TER preclaim(PreclaimContext const& ctx); - static TER - checkGuards( - Asset const& vaultAsset, - Number const& principalRequested, - TenthBips32 interestRate, - std::uint32_t paymentTotal, - LoanProperties const& properties, - beast::Journal j); - TER doApply() override; From cc9b6fc68c2d803c6c0cd8a7b0c84b8d7229e9ed Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 21 Nov 2025 14:46:34 -0500 Subject: [PATCH 238/291] Restore code that needs to be amendment gated --- src/xrpld/app/tx/detail/apply.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/xrpld/app/tx/detail/apply.cpp b/src/xrpld/app/tx/detail/apply.cpp index d6967e333f..e9709c20f5 100644 --- a/src/xrpld/app/tx/detail/apply.cpp +++ b/src/xrpld/app/tx/detail/apply.cpp @@ -40,6 +40,16 @@ checkValidity( return { Validity::SigBad, "Malformed: Invalid inner batch transaction."}; + + std::string reason; + if (!passesLocalChecks(tx, reason)) + { + router.setFlags(id, SF_LOCALBAD); + return {Validity::SigGoodOnly, reason}; + } + + router.setFlags(id, SF_SIGGOOD); + return {Validity::Valid, ""}; } if (any(flags & SF_SIGBAD)) From b76cd60a127b5d73dbce985004e47973bbf15cde Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 21 Nov 2025 16:39:44 -0500 Subject: [PATCH 239/291] fixup! Restore code that needs to be amendment gated --- src/test/app/Batch_test.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/test/app/Batch_test.cpp b/src/test/app/Batch_test.cpp index 2f3b9337e0..f2fae58a9b 100644 --- a/src/test/app/Batch_test.cpp +++ b/src/test/app/Batch_test.cpp @@ -2295,12 +2295,9 @@ class Batch_test : public beast::unit_test::suite Serializer s; parsed.object->add(s); auto const jrr = env.rpc("submit", strHex(s.slice()))[jss::result]; - BEAST_EXPECTS( - jrr[jss::status] == "error" && - jrr[jss::error] == "invalidTransaction" && - jrr[jss::error_exception] == - "fails local checks: Empty SigningPubKey.", - to_string(jrr)); + BEAST_EXPECT( + jrr[jss::status] == "success" && + jrr[jss::engine_result] == "temINVALID_FLAG"); env.close(); } From 1cec91e72d94508d881f6a4a396aa8a8ac8cd82a Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 21 Nov 2025 17:20:43 -0500 Subject: [PATCH 240/291] Move protocol.h values into namespace Lending --- include/xrpl/protocol/Protocol.h | 2 ++ src/test/app/LoanBroker_test.cpp | 1 + src/test/app/Loan_test.cpp | 6 ++++++ src/xrpld/app/misc/detail/LendingHelpers.cpp | 2 ++ src/xrpld/app/tx/detail/LoanBrokerSet.cpp | 2 ++ src/xrpld/app/tx/detail/LoanPay.cpp | 2 ++ src/xrpld/app/tx/detail/LoanSet.cpp | 2 ++ 7 files changed, 17 insertions(+) diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 2487d29c9b..ded16cad6d 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -110,6 +110,7 @@ tenthBipsOfValue(T value, TenthBips bips) return value * bips.value() / tenthBipsPerUnity.value(); } +namespace Lending { /** The maximum management fee rate allowed by a loan broker in 1/10 bips. Valid values are between 0 and 10% inclusive. @@ -197,6 +198,7 @@ static constexpr int loanPaymentsPerFeeIncrement = 5; * without an amendment */ static constexpr int loanMaximumPaymentsPerTransaction = 100; +} // namespace Lending /** The maximum length of a URI inside an NFT */ std::size_t constexpr maxTokenURILength = 256; diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index aa5b5519b9..a73621aa85 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -638,6 +638,7 @@ class LoanBroker_test : public beast::unit_test::suite } using namespace loanBroker; + using namespace ripple::Lending; TenthBips32 const tenthBipsZero{0}; diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index fa4ed233b4..584f8d6890 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -1639,6 +1639,7 @@ protected: int interestExponent) { using namespace jtx; + using namespace Lending; auto const& asset = broker.asset.raw(); auto const currencyLabel = getCurrencyLabel(asset); @@ -4843,6 +4844,7 @@ protected: using namespace jtx; using namespace std::chrono_literals; + using namespace Lending; Env env(*this, all); Account const issuer{"issuer"}; @@ -5016,6 +5018,7 @@ protected: using namespace jtx; using namespace std::chrono_literals; + using namespace Lending; Env env(*this, all); Account const issuer{"issuer"}; @@ -5099,6 +5102,7 @@ protected: using namespace jtx; using namespace std::chrono_literals; + using namespace Lending; Env env(*this, all); Account const issuer{"issuer"}; @@ -5211,6 +5215,7 @@ protected: using namespace jtx; using namespace std::chrono_literals; + using namespace Lending; Env env(*this, all); Account const issuer{"issuer"}; @@ -5305,6 +5310,7 @@ protected: using namespace jtx; using namespace std::chrono_literals; + using namespace Lending; Env env(*this, all); Account const issuer{"issuer"}; diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index e5eac77c1b..b558a24a15 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -1457,6 +1457,8 @@ loanMakePayment( LoanPaymentType const paymentType, beast::Journal j) { + using namespace Lending; + /* * This function is an implementation of the XLS-66 spec, * section 3.2.4.3 (Transaction Pseudo-code) diff --git a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp index c5b6891cb8..0e89cc6533 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp @@ -13,6 +13,8 @@ LoanBrokerSet::checkExtraFeatures(PreflightContext const& ctx) NotTEC LoanBrokerSet::preflight(PreflightContext const& ctx) { + using namespace Lending; + auto const& tx = ctx.tx; if (auto const data = tx[~sfData]; data && !data->empty() && !validDataLength(tx[~sfData], maxDataPayloadLength)) diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 39a93f634a..58fb2739d6 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -52,6 +52,8 @@ LoanPay::preflight(PreflightContext const& ctx) XRPAmount LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx) { + using namespace Lending; + auto const normalCost = Transactor::calculateBaseFee(view, tx); if (tx.isFlag(tfLoanFullPayment) || tx.isFlag(tfLoanLatePayment)) diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index d9e5f8b981..2f174ff0c8 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -21,6 +21,8 @@ LoanSet::getFlagsMask(PreflightContext const& ctx) NotTEC LoanSet::preflight(PreflightContext const& ctx) { + using namespace Lending; + auto const& tx = ctx.tx; // Special case for Batch inner transactions From 973a1059594266c8bbb1a6160b181c30e37b3368 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sat, 22 Nov 2025 15:28:27 -0500 Subject: [PATCH 241/291] Review feedback: test coverage, round debt total - Add some test cases to improve coverage, and exclude some lines from coverage. - Rounds the Broker.DebtTotal to the Vault scale any time it is modified. This should reduce rounding errors. - Ensure that no debt is left for the Broker after the last loan is deleted. This ensures that any accumulated rounding errors are resolved. --- src/test/app/LoanBroker_test.cpp | 7 +++++- src/test/app/Loan_test.cpp | 6 +---- src/xrpld/app/misc/LendingHelpers.h | 25 ++++++++++++++++++++ src/xrpld/app/tx/detail/LoanBrokerDelete.cpp | 24 +++++++++++++++++++ src/xrpld/app/tx/detail/LoanBrokerSet.cpp | 6 ++--- src/xrpld/app/tx/detail/LoanDelete.cpp | 20 ++++++++++++++++ src/xrpld/app/tx/detail/LoanManage.cpp | 23 +++++++----------- src/xrpld/app/tx/detail/LoanPay.cpp | 6 ++--- src/xrpld/app/tx/detail/LoanSet.cpp | 5 ++-- 9 files changed, 93 insertions(+), 29 deletions(-) diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index a73621aa85..ca64b5e0d6 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -673,11 +673,16 @@ class LoanBroker_test : public beast::unit_test::suite coverRateMinimum(maxCoverRate), coverRateLiquidation(maxCoverRate), ter(tecNO_PERMISSION)); - // Cover: too big + // CoverMinimum: too big env(set(evan, vault.vaultID), coverRateMinimum(maxCoverRate + 1), coverRateLiquidation(maxCoverRate + 1), ter(temINVALID)); + // CoverLiquidation: too big + env(set(evan, vault.vaultID), + coverRateMinimum(maxCoverRate / 2), + coverRateLiquidation(maxCoverRate + 1), + ter(temINVALID)); // Cover: zero min, non-zero liquidation - implicit and // explicit zero values. env(set(evan, vault.vaultID), diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 584f8d6890..97078f8d95 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -141,11 +141,7 @@ protected: using namespace jtx; auto const vaultSle = env.le(keylet::vault(vaultID)); - if (!vaultSle) - // This function is not important enough to return an optional. - // Return an impossibly small number - return STAmount::cMinOffset - 1; - return vaultSle->at(sfAssetsTotal).exponent(); + return getVaultScale(vaultSle); } }; diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 28a8ad0829..89ac0fb041 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -102,6 +102,31 @@ struct LoanState } }; +// Some values get re-rounded to the vault scale any time they are adjusted. In +// addition, they are prevented from ever going below zero. This helps avoid +// accumulated rounding errors and leftover dust amounts. +template +void +adjustImpreciseNumber( + NumberProxy value, + Number const& adjustment, + Asset const& asset, + int vaultScale) +{ + value = roundToAsset(asset, value + adjustment, vaultScale); + + if (*value < beast::zero) + value = 0; +} + +inline int +getVaultScale(SLE::const_ref vaultSle) +{ + if (!vaultSle) + return Number::minExponent - 1; // LCOV_EXCL_LINE + return vaultSle->at(sfAssetsTotal).exponent(); +} + TER checkLoanGuards( Asset const& vaultAsset, diff --git a/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp b/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp index bae806b45e..d3c4092d21 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp @@ -43,6 +43,30 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx) JLOG(ctx.j.warn()) << "LoanBrokerDelete: Owner count is " << ownerCount; return tecHAS_OBLIGATIONS; } + if (auto const debtTotal = sleBroker->at(sfDebtTotal); + debtTotal != beast::zero) + { + // Any remaining debt should have been wiped out by the last Loan + // Delete. This check is purely defensive. + auto const vault = + ctx.view.read(keylet::vault(sleBroker->at(sfVaultID))); + if (!vault) + return tefINTERNAL; // LCOV_EXCL_LINE + auto const asset = vault->at(sfAsset); + auto const scale = getVaultScale(vault); + + auto const rounded = + roundToAsset(asset, debtTotal, scale, Number::towards_zero); + + if (rounded != beast::zero) + { + // LCOV_EXCL_START + JLOG(ctx.j.warn()) << "LoanBrokerDelete: Debt total is " + << debtTotal << ", which rounds to " << rounded; + return tecHAS_OBLIGATIONS; + // LCOV_EXCL_START + } + } return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp index 0e89cc6533..196d86d091 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp @@ -144,9 +144,9 @@ LoanBrokerSet::doApply() std::make_shared(keylet::loanbroker(account_, sequence)); if (auto const ter = dirLink(view, account_, broker)) - return ter; + return ter; // LCOV_EXCL_LINE if (auto const ter = dirLink(view, vaultPseudoID, broker, sfVaultNode)) - return ter; + return ter; // LCOV_EXCL_LINE adjustOwnerCount(view, owner, 2, j_); auto const ownerCount = owner->at(sfOwnerCount); @@ -156,7 +156,7 @@ LoanBrokerSet::doApply() auto maybePseudo = createPseudoAccount(view, broker->key(), sfLoanBrokerID); if (!maybePseudo) - return maybePseudo.error(); + return maybePseudo.error(); // LCOV_EXCL_LINE auto& pseudo = *maybePseudo; auto pseudoId = pseudo->at(sfAccount); diff --git a/src/xrpld/app/tx/detail/LoanDelete.cpp b/src/xrpld/app/tx/detail/LoanDelete.cpp index 084de2cf3f..87ff4d594b 100644 --- a/src/xrpld/app/tx/detail/LoanDelete.cpp +++ b/src/xrpld/app/tx/detail/LoanDelete.cpp @@ -101,7 +101,27 @@ LoanDelete::doApply() view.erase(loanSle); // Decrement the LoanBroker's owner count. + // The broker's owner count is solely for the number of outstanding loans, + // and is distinct from the broker's pseudo-account's owner count adjustOwnerCount(view, brokerSle, -1, j_); + // If there are no loans left, then any remaining debt must be forgiven, + // because there is no other way to pay it back. + if (brokerSle->at(sfOwnerCount) == 0) + { + auto debtTotalProxy = brokerSle->at(sfDebtTotal); + if (*debtTotalProxy != beast::zero) + { + XRPL_ASSERT_PARTS( + roundToAsset( + vaultSle->at(sfAsset), + debtTotalProxy, + getVaultScale(vaultSle), + Number::towards_zero) == beast::zero, + "ripple::LoanDelete::doApply", + "last loan, remaining debt rounds to zero"); + debtTotalProxy = 0; + } + } // Decrement the borrower's owner count adjustOwnerCount(view, borrowerSle, -1, j_); diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index 73754404e5..4ba8808816 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -171,16 +171,16 @@ LoanManage::defaultLoan( // Update the Vault object: + // The vault may be at a different scale than the loan. Reduce rounding + // errors during the accounting by rounding some of the values to that + // scale. + auto const vaultScale = getVaultScale(vaultSle); + { // Decrease the Total Value of the Vault: auto vaultTotalProxy = vaultSle->at(sfAssetsTotal); auto vaultAvailableProxy = vaultSle->at(sfAssetsAvailable); - // The vault may be at a different scale than the loan. Reduce rounding - // errors during the accounting by rounding some of the values to that - // scale. - auto const vaultScale = vaultTotalProxy.value().exponent(); - if (vaultTotalProxy < vaultDefaultAmount) { // LCOV_EXCL_START @@ -246,16 +246,11 @@ LoanManage::defaultLoan( // Update the LoanBroker object: { + auto const asset = *vaultSle->at(sfAsset); + // Decrease the Debt of the LoanBroker: - if (brokerDebtTotalProxy < totalDefaultAmount) - { - // LCOV_EXCL_START - JLOG(j.warn()) - << "LoanBroker debt total is less than the default amount"; - return tefBAD_LEDGER; - // LCOV_EXCL_STOP - } - brokerDebtTotalProxy -= totalDefaultAmount; + adjustImpreciseNumber( + brokerDebtTotalProxy, -totalDefaultAmount, asset, vaultScale); // Decrease the First-Loss Capital Cover Available: auto coverAvailableProxy = brokerSle->at(sfCoverAvailable); if (coverAvailableProxy < defaultCovered) diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 58fb2739d6..11f23848ab 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -410,10 +410,8 @@ LoanPay::doApply() // Despite our best efforts, it's possible for rounding errors to accumulate // in the loan broker's debt total. This is because the broker may have more // that one loan with significantly different scales. - if (totalPaidToVaultForDebt >= debtTotalProxy) - debtTotalProxy = 0; - else - debtTotalProxy -= totalPaidToVaultForDebt; + adjustImpreciseNumber( + debtTotalProxy, -totalPaidToVaultForDebt, asset, vaultScale); //------------------------------------------------------ // Vault object state changes diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 2f174ff0c8..1da06851a5 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -354,7 +354,7 @@ LoanSet::doApply() auto vaultAvailableProxy = vaultSle->at(sfAssetsAvailable); auto vaultTotalProxy = vaultSle->at(sfAssetsTotal); - auto const vaultScale = vaultTotalProxy.value().exponent(); + auto const vaultScale = getVaultScale(vaultSle); if (vaultAvailableProxy < principalRequested) { JLOG(j_.warn()) @@ -574,7 +574,8 @@ LoanSet::doApply() view.update(vaultSle); // Update the balances in the loan broker - brokerSle->at(sfDebtTotal) += newDebtDelta; + adjustImpreciseNumber( + brokerSle->at(sfDebtTotal), newDebtDelta, vaultAsset, vaultScale); // The broker's owner count is solely for the number of outstanding loans, // and is distinct from the broker's pseudo-account's owner count adjustOwnerCount(view, brokerSle, 1, j_); From d353b0ec040769518c2c752abf92b84382ed3e17 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sat, 22 Nov 2025 16:14:54 -0500 Subject: [PATCH 242/291] Review feedback - Use a lambda to defensively guarantee that "tx" can not affect the "signerCount" in "LoanSet::calculateBaseFee" # Please enter the commit message for your changes. Lines starting --- src/xrpld/app/tx/detail/LoanSet.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 1da06851a5..e9eff856a6 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -159,9 +159,13 @@ LoanSet::calculateBaseFee(ReadView const& view, STTx const& tx) // for the transaction. Note that unlike the base class, the single signer // is counted if present. It will only be absent in a batch inner // transaction. - std::size_t const signerCount = counterSig.isFieldPresent(sfSigners) - ? counterSig.getFieldArray(sfSigners).size() - : (counterSig.isFieldPresent(sfTxnSignature) ? 1 : 0); + std::size_t const signerCount = [&counterSig]() { + // Compute defensively. Assure that "tx" cannot be accessed and cause + // confusion or miscalculations. + return counterSig.isFieldPresent(sfSigners) + ? counterSig.getFieldArray(sfSigners).size() + : (counterSig.isFieldPresent(sfTxnSignature) ? 1 : 0); + }(); return normalCost + (signerCount * baseFee); } From 8bb162ce52bb46e2b855e0e7079fa49c65a433d0 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sat, 22 Nov 2025 19:36:50 -0500 Subject: [PATCH 243/291] Review feedback from @gregtatcam - Rewrite "Asset::native()" and "integral()" to use std::visit. - Improve documentation for the LoanPay transaction flags: tfLoanFullPayment, tfLoanLatePayment --- include/xrpl/protocol/Asset.h | 18 ++++++++++++++++-- include/xrpl/protocol/TxFlags.h | 8 +++++++- .../xrpl/protocol/detail/ledger_entries.macro | 2 +- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/include/xrpl/protocol/Asset.h b/include/xrpl/protocol/Asset.h index 6765f7cf7d..7ad1e70256 100644 --- a/include/xrpl/protocol/Asset.h +++ b/include/xrpl/protocol/Asset.h @@ -81,13 +81,27 @@ public: bool native() const { - return holds() && get().native(); + return std::visit( + [&](TIss const& issue) { + if constexpr (std::is_same_v) + return issue.native(); + if constexpr (std::is_same_v) + return false; + }, + issue_); } bool integral() const { - return !holds() || get().native(); + return std::visit( + [&](TIss const& issue) { + if constexpr (std::is_same_v) + return issue.native(); + if constexpr (std::is_same_v) + return true; + }, + issue_); } friend constexpr bool diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index 0f3ea94b21..d4faed192c 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -272,8 +272,14 @@ constexpr std::uint32_t const tfBatchMask = // as an overpayment. False, no overpayments will be taken. constexpr std::uint32_t const tfLoanOverpayment = 0x00010000; // LoanPay exclusive flags: -// tfLoanFullPayment: True, indicates that the payment is +// tfLoanFullPayment: True, indicates that the payment is an early +// full payment. It must pay the entire loan including close +// interest and fees, or it will fail. False: Not a full payment. constexpr std::uint32_t const tfLoanFullPayment = 0x00020000; +// tfLoanLatePayment: True, indicates that the payment is late, +// and includes late iterest and fees. If the loan is not late, +// it will fail. False: not a late payment. If the current payment +// is overdue, the transaction will fail. constexpr std::uint32_t const tfLoanLatePayment = 0x00040000; constexpr std::uint32_t const tfLoanSetMask = ~(tfUniversal | tfLoanOverpayment); diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 5f6d1b844e..1034c35895 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -572,7 +572,7 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({ // // - InterestOwedToVault = InterestOutstanding - ManagementFeeOutstanding // The amount of the total interest that is owed to the vault, and - // will be sent to as part of a payment. + // will be sent to it as part of a payment. // // - TrueTotalLoanValue = PaymentRemaining * PeriodicPayment // The unrounded true total value of the loan. From b82481858af3cb0d7f4d06c382d31b7099d37402 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sat, 22 Nov 2025 20:57:34 -0500 Subject: [PATCH 244/291] Review feedback from @gregtatcam - Rename "accountCanSpend" to "accountSpendabled". - Change some getIssuer() local vars to use references. --- include/xrpl/ledger/View.h | 8 ++++---- src/libxrpl/ledger/View.cpp | 26 +++++++++++++------------- src/xrpld/app/tx/detail/LoanPay.cpp | 14 +++++++------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/include/xrpl/ledger/View.h b/include/xrpl/ledger/View.h index 5e6fb8f5ce..3bad22c321 100644 --- a/include/xrpl/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -352,7 +352,7 @@ accountHolds( // // <-- saAmount: amount of currency held by account. May be negative. [[nodiscard]] STAmount -accountCanSend( +accountSpendable( ReadView const& view, AccountID const& account, Currency const& currency, @@ -361,7 +361,7 @@ accountCanSend( beast::Journal j); [[nodiscard]] STAmount -accountCanSend( +accountSpendable( ReadView const& view, AccountID const& account, Issue const& issue, @@ -369,7 +369,7 @@ accountCanSend( beast::Journal j); [[nodiscard]] STAmount -accountCanSend( +accountSpendable( ReadView const& view, AccountID const& account, MPTIssue const& mptIssue, @@ -378,7 +378,7 @@ accountCanSend( beast::Journal j); [[nodiscard]] STAmount -accountCanSend( +accountSpendable( ReadView const& view, AccountID const& account, Asset const& asset, diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index 28f6bb89be..dc154f13e6 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -564,7 +564,7 @@ accountHolds( } STAmount -accountCanSend( +accountSpendable( ReadView const& view, AccountID const& account, Currency const& currency, @@ -589,19 +589,19 @@ accountCanSend( } STAmount -accountCanSend( +accountSpendable( ReadView const& view, AccountID const& account, Issue const& issue, FreezeHandling zeroIfFrozen, beast::Journal j) { - return accountCanSend( + return accountSpendable( view, account, issue.currency, issue.account, zeroIfFrozen, j); } STAmount -accountCanSend( +accountSpendable( ReadView const& view, AccountID const& account, MPTIssue const& mptIssue, @@ -631,7 +631,7 @@ accountCanSend( } [[nodiscard]] STAmount -accountCanSend( +accountSpendable( ReadView const& view, AccountID const& account, Asset const& asset, @@ -645,9 +645,9 @@ accountCanSend( std::remove_cvref_t, Issue>) { - return accountCanSend(view, account, value, zeroIfFrozen, j); + return accountSpendable(view, account, value, zeroIfFrozen, j); } - return accountCanSend( + return accountSpendable( view, account, value, zeroIfFrozen, zeroIfUnauthorized, j); }, asset.value()); @@ -2064,7 +2064,7 @@ rippleSendIOU( beast::Journal j, WaiveTransferFee waiveFee) { - auto const issuer = saAmount.getIssuer(); + auto const& issuer = saAmount.getIssuer(); XRPL_ASSERT( !isXRP(uSenderID) && !isXRP(uReceiverID), @@ -2119,7 +2119,7 @@ rippleSendMultiIOU( beast::Journal j, WaiveTransferFee waiveFee) { - auto const issuer = issue.getIssuer(); + auto const& issuer = issue.getIssuer(); XRPL_ASSERT( !isXRP(senderID), "ripple::rippleSendMultiIOU : sender is not XRP"); @@ -2459,7 +2459,7 @@ rippleCreditMPT( { // Do not check MPT authorization here - it must have been checked earlier auto const mptID = keylet::mptIssuance(saAmount.get().getMptID()); - auto const issuer = saAmount.getIssuer(); + auto const& issuer = saAmount.getIssuer(); auto sleIssuance = view.peek(mptID); if (!sleIssuance) return tecOBJECT_NOT_FOUND; @@ -2526,7 +2526,7 @@ rippleSendMPT( "ripple::rippleSendMPT : sender is not receiver"); // Safe to get MPT since rippleSendMPT is only called by accountSendMPT - auto const issuer = saAmount.getIssuer(); + auto const& issuer = saAmount.getIssuer(); auto const sle = view.read(keylet::mptIssuance(saAmount.get().getMptID())); @@ -2589,7 +2589,7 @@ rippleSendMultiMPT( { // Safe to get MPT since rippleSendMultiMPT is only called by // accountSendMultiMPT - auto const issuer = mptIssue.getIssuer(); + auto const& issuer = mptIssue.getIssuer(); auto const sle = view.read(keylet::mptIssuance(mptIssue.getMptID())); if (!sle) @@ -3287,7 +3287,7 @@ canTransfer( if (issue.native()) return tesSUCCESS; - auto const issuerId = issue.getIssuer(); + auto const& issuerId = issue.getIssuer(); if (issuerId == from || issuerId == to) return tesSUCCESS; auto const sleIssuer = view.read(keylet::account(issuerId)); diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 11f23848ab..0b7a3ed0dd 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -212,7 +212,7 @@ LoanPay::preclaim(PreclaimContext const& ctx) // Do not support "partial payments" - if the transaction says to pay X, // then the account must have X available, even if the loan payment takes // less. - if (auto const balance = accountCanSend( + if (auto const balance = accountSpendable( ctx.view, account, asset, @@ -470,11 +470,11 @@ LoanPay::doApply() } #if !NDEBUG - auto const accountBalanceBefore = accountCanSend( + auto const accountBalanceBefore = accountSpendable( view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); auto const vaultBalanceBefore = account_ == vaultPseudoAccount ? STAmount{asset, 0} - : accountCanSend( + : accountSpendable( view, vaultPseudoAccount, asset, @@ -483,7 +483,7 @@ LoanPay::doApply() j_); auto const brokerBalanceBefore = account_ == brokerPayee ? STAmount{asset, 0} - : accountCanSend( + : accountSpendable( view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); #endif @@ -539,11 +539,11 @@ LoanPay::doApply() "vault pseudo balance agrees after"); #if !NDEBUG - auto const accountBalanceAfter = accountCanSend( + auto const accountBalanceAfter = accountSpendable( view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); auto const vaultBalanceAfter = account_ == vaultPseudoAccount ? STAmount{asset, 0} - : accountCanSend( + : accountSpendable( view, vaultPseudoAccount, asset, @@ -552,7 +552,7 @@ LoanPay::doApply() j_); auto const brokerBalanceAfter = account_ == brokerPayee ? STAmount{asset, 0} - : accountCanSend( + : accountSpendable( view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); XRPL_ASSERT_PARTS( From 4c9a96a17b7bacf2616b745e8dd51b52a6ee88d2 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sat, 22 Nov 2025 21:11:06 -0500 Subject: [PATCH 245/291] Review feedback from @gregtatcam - No need to subtract takeFromSender in rippleSendMultiMPT if the sender is the issuer. --- src/libxrpl/ledger/View.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index dc154f13e6..682c0cb2ad 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -2621,12 +2621,16 @@ rippleSendMultiMPT( // not exceed MaximumAmount if (senderID == issuer) { + XRPL_ASSERT_PARTS( + takeFromSender == beast::zero, + "rippler::rippleSendMultiMPT", + "sender == issuer, takeFromSender == zero"); auto const sendAmount = amount.mpt().value(); auto const maximumAmount = sle->at(~sfMaximumAmount).value_or(maxMPTokenAmount); - if (sendAmount > maximumAmount - takeFromSender || + if (sendAmount > maximumAmount || sle->getFieldU64(sfOutstandingAmount) > - maximumAmount - sendAmount - takeFromSender) + maximumAmount - sendAmount) return tecPATH_DRY; } From 590f69cd4c340950c49f6b0818e7c54aabea4167 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sat, 22 Nov 2025 22:23:27 -0400 Subject: [PATCH 246/291] Add comment to totalPaidToVaultForDebt in LoanPay.cpp Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com> --- src/xrpld/app/tx/detail/LoanPay.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 0b7a3ed0dd..95a13d2fcc 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -388,6 +388,11 @@ LoanPay::doApply() !asset.integral() || totalPaidToVaultRaw == totalPaidToVaultRounded, "ripple::LoanPay::doApply", "rounding does nothing for integral asset"); + // Account for value changes when reducing the broker's debt: + // - Positive value change (from full/late/overpayments): Subtract from the + // amount credited toward debt to avoid over-reducing the debt. + // - Negative value change (from full/overpayments): Add to the amount + // credited toward debt,effectively increasing the debt reduction. auto const totalPaidToVaultForDebt = totalPaidToVaultRaw - paymentParts->valueChange; From 1c38bbdf45d5af2a1b3d5b23aa8afd727b23025b Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sat, 22 Nov 2025 21:33:30 -0500 Subject: [PATCH 247/291] Review feedback from @gregtatcam - Remove unneeded proxy variables. Set the fields directly. --- src/xrpld/app/tx/detail/LoanManage.cpp | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index 4ba8808816..cf1d143c1b 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -135,13 +135,6 @@ LoanManage::defaultLoan( std::int32_t const loanScale = loanSle->at(sfLoanScale); auto brokerDebtTotalProxy = brokerSle->at(sfDebtTotal); - auto principalOutstandingProxy = loanSle->at(sfPrincipalOutstanding); - auto managementFeeOutstandingProxy = - loanSle->at(sfManagementFeeOutstanding); - - auto totalValueOutstandingProxy = loanSle->at(sfTotalValueOutstanding); - auto paymentRemainingProxy = loanSle->at(sfPaymentRemaining); - Number const totalDefaultAmount = owedToVault(loanSle); // Apply the First-Loss Capital to the Default Amount @@ -267,10 +260,11 @@ LoanManage::defaultLoan( // Update the Loan object: loanSle->setFlag(lsfLoanDefault); - totalValueOutstandingProxy = 0; - paymentRemainingProxy = 0; - principalOutstandingProxy = 0; - managementFeeOutstandingProxy = 0; + + loanSle->at(sfTotalValueOutstanding) = 0; + loanSle->at(sfPaymentRemaining) = 0; + loanSle->at(sfPrincipalOutstanding) = 0; + loanSle->at(sfManagementFeeOutstanding) = 0; // Zero out the next due date. Since it's default, it'll be removed from // the object. loanSle->at(sfNextPaymentDueDate) = 0; From f8ee979ff4c78b479de62fd924c3c172c3e52a18 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sat, 22 Nov 2025 21:46:00 -0500 Subject: [PATCH 248/291] Review feedback from @gregtatcam - Add a comment explaining the formula in LoanManage::owedToVault - Make owedToVault static --- src/xrpld/app/tx/detail/LoanManage.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index cf1d143c1b..adf08d71bf 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -114,9 +114,20 @@ LoanManage::preclaim(PreclaimContext const& ctx) return tesSUCCESS; } -Number +static Number owedToVault(SLE::ref loanSle) { + // Spec section 3.2.3.2, defines the default amount as + // + // DefaultAmount = (Loan.PrincipalOutstanding + Loan.InterestOutstanding) + // + // Loan.InterestOutstanding is not stored directly on ledger. + // It is computed as + // + // Loan.TotalValueOutstanding - Loan.PrincipalOutstanding - + // Loan.ManagementFeeOutstanding + // + // Add that to the original formula, and you get this: return loanSle->at(sfTotalValueOutstanding) - loanSle->at(sfManagementFeeOutstanding); } From fa2dea84c2eb465a9ac7b0bfdd91fd748d1599a4 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sat, 22 Nov 2025 22:30:06 -0500 Subject: [PATCH 249/291] Review feedback from @gregtatcam: STAmount - Fix a couple of typos. - Make fewer unneeded copies in roundToScale. --- include/xrpl/protocol/STAmount.h | 4 ++-- src/libxrpl/protocol/STAmount.cpp | 20 +++++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index f3f285249c..58c420de74 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -701,7 +701,7 @@ getRate(STAmount const& offerOut, STAmount const& offerIn); */ STAmount roundToScale( - STAmount value, + STAmount const& value, std::int32_t scale, Number::rounding_mode rounding = Number::getround()); @@ -729,7 +729,7 @@ roundToAsset( STAmount const ret{asset, value}; if (ret.integral()) return ret; - // Not that the ctor will round integral types (XRP, MPT) via canonicalize, + // Note that the ctor will round integral types (XRP, MPT) via canonicalize, // so no extra work is needed for those. return roundToScale(ret, scale); } diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 566957509c..072b84f2c0 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -1462,9 +1462,12 @@ canonicalizeRoundStrict( } STAmount -roundToScale(STAmount value, std::int32_t scale, Number::rounding_mode rounding) +roundToScale( + STAmount const& value, + std::int32_t scale, + Number::rounding_mode rounding) { - // Nothing to do for intgral types. + // Nothing to do for integral types. if (value.integral()) return value; @@ -1474,16 +1477,15 @@ roundToScale(STAmount value, std::int32_t scale, Number::rounding_mode rounding) if (value.exponent() >= scale) return value; - STAmount referenceValue{ + STAmount const referenceValue{ value.asset(), STAmount::cMinValue, scale, value.negative()}; NumberRoundModeGuard mg(rounding); - // With an IOU, the total will be truncated to the precision of the - // larger value: referenceValue - value += referenceValue; - // Remove the reference value, and we're left with the rounded value. - value -= referenceValue; - return value; + // With an IOU, the the result of addition will be truncated to the + // precision of the larger value, which in this case is referenceValue. Then + // remove the reference value via subtraction, and we're left with the + // rounded value. + return (value + referenceValue) - referenceValue; } namespace { From 2f410148f132f5061f6994895c34cc6621688c73 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sat, 22 Nov 2025 23:40:19 -0400 Subject: [PATCH 250/291] Update src/xrpld/app/misc/detail/LendingHelpers.cpp - Log a warning if a full payment fails because it's the last payment. Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com> --- src/xrpld/app/misc/detail/LendingHelpers.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index b558a24a15..153201c330 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -751,10 +751,13 @@ computeFullPayment( if (paymentRemaining <= 1) { // If this is the last payment, it has to be a regular payment - JLOG(j.warn()) << "Full payment requested when only final " - << "payment remains."; + if (paymentRemaining <= 1) + { + // If this is the last payment, it has to be a regular payment + JLOG(j.warn()) << "Last payment cannot be a full payment."; return Unexpected(tecKILLED); } + } Number const rawPrincipalOutstanding = loanPrincipalFromPeriodicPayment( periodicPayment, periodicRate, paymentRemaining); From 17eaa51378c991a4395d157a0278552b9f520f2f Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sat, 22 Nov 2025 23:49:19 -0400 Subject: [PATCH 251/291] Update src/xrpld/app/misc/detail/LendingHelpers.cpp - Optimization to skip multiplication. Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com> --- src/xrpld/app/misc/detail/LendingHelpers.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index 153201c330..4c73c28f8c 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -1238,7 +1238,10 @@ calculateFullPaymentInterest( accruedInterest >= 0, "ripple::detail::computeFullPaymentInterest : valid accrued interest"); - auto const prepaymentPenalty = + auto const prepaymentPenalty = closeInterestRate == beast::zero + ? Number{} + : tenthBipsOfValue(rawPrincipalOutstanding, closeInterestRate); + tenthBipsOfValue(rawPrincipalOutstanding, closeInterestRate); XRPL_ASSERT( prepaymentPenalty >= 0, From e25bae5e78ab0edf61f4415974c09a7781df362b Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sat, 22 Nov 2025 22:56:40 -0500 Subject: [PATCH 252/291] Review feedback from @Tapanito - Check for zero interest in loanAccruedInterest - Fix a couple of errors from earlier commits --- src/xrpld/app/misc/detail/LendingHelpers.cpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index 4c73c28f8c..9f88001c05 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -205,12 +205,15 @@ loanAccruedInterest( * This formula is from the XLS-66 spec, section 3.2.4.1.4 (Early Full * Repayment), specifically "accruedInterest = ...". */ + if (periodicRate == beast::zero) + return numZero; + auto const lastPaymentDate = std::max(prevPaymentDate, startDate); // If the loan has been paid ahead, then "lastPaymentDate" is in the future, // and no interest has accrued. if (parentCloseTime.time_since_epoch().count() <= lastPaymentDate) - return 0; + return numZero; auto const secondsSinceLastPayment = parentCloseTime.time_since_epoch().count() - lastPaymentDate; @@ -749,15 +752,11 @@ computeFullPayment( beast::Journal j) { if (paymentRemaining <= 1) - { - // If this is the last payment, it has to be a regular payment - if (paymentRemaining <= 1) { // If this is the last payment, it has to be a regular payment JLOG(j.warn()) << "Last payment cannot be a full payment."; return Unexpected(tecKILLED); } - } Number const rawPrincipalOutstanding = loanPrincipalFromPeriodicPayment( periodicPayment, periodicRate, paymentRemaining); @@ -1241,8 +1240,7 @@ calculateFullPaymentInterest( auto const prepaymentPenalty = closeInterestRate == beast::zero ? Number{} : tenthBipsOfValue(rawPrincipalOutstanding, closeInterestRate); - - tenthBipsOfValue(rawPrincipalOutstanding, closeInterestRate); + XRPL_ASSERT( prepaymentPenalty >= 0, "ripple::detail::computeFullPaymentInterest : valid prepayment " From 2fb9a6a2920cd731477c77e7a55358aee9de96bb Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sat, 22 Nov 2025 23:06:12 -0500 Subject: [PATCH 253/291] Review feedback from @Tapanito: tecKILLED logs - Log messages explaining Loan-related tecKILLED returns. --- src/xrpld/app/tx/detail/LoanSet.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index e9eff856a6..0d86ade9dc 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -219,18 +219,32 @@ LoanSet::preclaim(PreclaimContext const& ctx) // The grace period can't be larger than the interval. Check it first, // mostly so that unit tests can test that specific case. if (grace > timeAvailable) + { + JLOG(ctx.j.warn()) << "Grace period exceeds protocol time limit."; return tecKILLED; + } if (interval > timeAvailable) + { + JLOG(ctx.j.warn()) + << "Payment interval exceeds protocol time limit."; return tecKILLED; + } if (total > timeAvailable) + { + JLOG(ctx.j.warn()) << "Payment total exceeds protocol time limit."; return tecKILLED; + } auto const timeLastPayment = timeAvailable - grace; if (timeLastPayment / interval < total) + { + JLOG(ctx.j.warn()) << "Last payment due date, or grace period for " + "last payment exceeds protocol time limit."; return tecKILLED; + } } auto const account = tx[sfAccount]; From 1ceff3dde70b877fdfc30a3ab2322a5f13b56070 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sat, 22 Nov 2025 23:36:17 -0500 Subject: [PATCH 254/291] Review feedback from @greptatcam: getStartDate - Make getStartDate helper in LoanSet static. --- src/xrpld/app/tx/detail/LoanSet.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 0d86ade9dc..6c0ce68386 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -185,7 +185,7 @@ LoanSet::getValueFields() return valueFields; } -std::uint32_t +static std::uint32_t getStartDate(ReadView const& view) { return view.info().closeTime.time_since_epoch().count(); From a3b2ce91defb87b6e870562b27f5b8a37561217c Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sat, 22 Nov 2025 23:45:39 -0500 Subject: [PATCH 255/291] Review feedback from @gregtatcam: frozen broker pseudo-account - LoanSet needs to fail if the broker pseudo-account is deep frozen, because it needs to be able to receive fees if the broker owner can't. --- src/xrpld/app/tx/detail/LoanSet.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 6c0ce68386..95cb48234b 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -266,6 +266,7 @@ LoanSet::preclaim(PreclaimContext const& ctx) "of the LoanBroker."; return tecNO_PERMISSION; } + auto const brokerPseudo = brokerSle->at(sfAccount); auto const borrower = counterparty == brokerOwner ? account : counterparty; if (auto const borrowerSle = ctx.view.read(keylet::account(borrower)); @@ -312,6 +313,16 @@ LoanSet::preclaim(PreclaimContext const& ctx) JLOG(ctx.j.warn()) << "Vault pseudo-account is frozen."; return ret; } + + // brokerPseudo is the fallback account to receive LoanPay fees, even if the + // broker owner is unable to accept them. Don't create the loan if it is + // deep frozen. + if (auto const ret = checkDeepFrozen(ctx.view, brokerPseudo, asset)) + { + JLOG(ctx.j.warn()) << "Broker pseudo-account is frozen."; + return ret; + } + // borrower is eventually going to have to pay back the loan, so it can't be // frozen now. It is also going to receive funds, so it can't be deep // frozen, but being frozen is a prerequisite for being deep frozen, so From 63724e4405b6dc397f92eba980f63298bd19b699 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sun, 23 Nov 2025 00:03:52 -0500 Subject: [PATCH 256/291] Review feedback from @gregtatcam: seconds per year - Define a constexpr secondsInYear in LedgerHelpers.h --- src/test/app/Loan_test.cpp | 4 ++-- src/xrpld/app/misc/LendingHelpers.h | 2 ++ src/xrpld/app/misc/detail/LendingHelpers.cpp | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 97078f8d95..608a384199 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -2388,7 +2388,7 @@ protected: // the below BEAST_EXPECTs may not hold across assets. Number const interval = state.paymentInterval; auto const periodicRate = - interval * Number(12, -2) / (365 * 24 * 60 * 60); + interval * Number(12, -2) / secondsInYear; BEAST_EXPECT( periodicRate == Number(2283105022831050, -21, Number::unchecked{})); @@ -2620,7 +2620,7 @@ protected: // the below BEAST_EXPECTs may not hold across assets. Number const interval = state.paymentInterval; auto const periodicRate = - interval * Number(12, -2) / (365 * 24 * 60 * 60); + interval * Number(12, -2) / secondsInYear; BEAST_EXPECT( periodicRate == Number(2283105022831050, -21, Number::unchecked{})); diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 89ac0fb041..d2ff79d9b5 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -12,6 +12,8 @@ struct PreflightContext; bool checkLendingProtocolDependencies(PreflightContext const& ctx); +static constexpr std::uint32_t secondsInYear = 365 * 24 * 60 * 60; + Number loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval); diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index 9f88001c05..bbee329ff5 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -54,7 +54,7 @@ loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval) * other places. */ return tenthBipsOfValue(Number(paymentInterval), interestRate) / - (365 * 24 * 60 * 60); + secondsInYear; } bool From b8ad6d39502a52c52c8786ad46f7a391e695c63d Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sun, 23 Nov 2025 14:58:45 -0500 Subject: [PATCH 257/291] Review feedback from @a1q123456: LoanDelete - Update the ValidLoan invariant to guarantee that Loan fields are consistent for the paid off and not paid off state. --- src/xrpld/app/tx/detail/InvariantCheck.cpp | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index a23b4bcb47..a82061b180 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -2470,10 +2470,25 @@ ValidLoan::finalize( for (auto const& [before, after] : loans_) { // https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3223-invariants - // If `Loan.PaymentRemaining = 0` then `Loan.PrincipalOutstanding = 0` + // If `Loan.PaymentRemaining = 0` then the loan MUST be fully paid off if (after->at(sfPaymentRemaining) == 0 && - after->at(sfPrincipalOutstanding) != 0) + (after->at(sfTotalValueOutstanding) != beast::zero || + after->at(sfPrincipalOutstanding) != beast::zero || + after->at(sfManagementFeeOutstanding) != beast::zero)) { + JLOG(j.fatal()) << "Invariant failed: Loan with zero payments " + "remaining has not been paid off"; + return false; + } + // If `Loan.PaymentRemaining != 0` then the loan MUST NOT be fully paid + // off + if (after->at(sfPaymentRemaining) != 0 && + after->at(sfTotalValueOutstanding) == beast::zero && + after->at(sfPrincipalOutstanding) == beast::zero && + after->at(sfManagementFeeOutstanding) == beast::zero) + { + JLOG(j.fatal()) << "Invariant failed: Loan with zero payments " + "remaining has not been paid off"; return false; } if (before && From 58497541c72133639b2637e6af0a446c7b942f21 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sun, 23 Nov 2025 15:29:48 -0500 Subject: [PATCH 258/291] Review feedback from @gregtatcam: LoanPay flags - Used std::popcount instead of a loop to check for mutually exclusive flags --- src/xrpld/app/tx/detail/LoanPay.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 95a13d2fcc..eb1c5adc19 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -7,6 +7,8 @@ #include #include +#include + namespace ripple { bool @@ -32,14 +34,11 @@ LoanPay::preflight(PreflightContext const& ctx) // The loan payment flags are all mutually exclusive. If more than one is // set, the tx is malformed. - int flagsSet = 0; - for (auto const flag : - {tfLoanLatePayment, tfLoanFullPayment, tfLoanOverpayment}) - { - if (ctx.tx.isFlag(flag)) - ++flagsSet; - } - if (flagsSet > 1) + static_assert( + (tfLoanLatePayment | tfLoanFullPayment | tfLoanOverpayment) == + ~(tfLoanPayMask | tfUniversal)); + auto const flagsSet = ctx.tx.getFlags() & ~(tfLoanPayMask | tfUniversal); + if (std::popcount(flagsSet) > 1) { JLOG(ctx.j.warn()) << "Only one LoanPay flag can be set per tx. " << flagsSet << " is too many."; From deab24b5802efd592f4349ee1bc15b7c67f2c85b Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sun, 23 Nov 2025 17:10:15 -0500 Subject: [PATCH 259/291] Review feedback from @gregtatcam: full interest - Found computed full interest downward, per spec. --- src/xrpld/app/misc/detail/LendingHelpers.cpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index bbee329ff5..b8fda968d3 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -179,13 +179,14 @@ loanLatePaymentInterest( * The spec is to be updated to base the duration on the next due date */ + auto const now = parentCloseTime.time_since_epoch().count(); + // If the payment is not late by any amount of time, then there's no late // interest - if (parentCloseTime.time_since_epoch().count() <= nextPaymentDueDate) + if (now <= nextPaymentDueDate) return 0; - auto const secondsOverdue = - parentCloseTime.time_since_epoch().count() - nextPaymentDueDate; + auto const secondsOverdue = now - nextPaymentDueDate; auto const rate = loanPeriodicRate(lateInterestRate, secondsOverdue); @@ -209,14 +210,14 @@ loanAccruedInterest( return numZero; auto const lastPaymentDate = std::max(prevPaymentDate, startDate); + auto const now = parentCloseTime.time_since_epoch().count(); // If the loan has been paid ahead, then "lastPaymentDate" is in the future, // and no interest has accrued. - if (parentCloseTime.time_since_epoch().count() <= lastPaymentDate) + if (now <= lastPaymentDate) return numZero; - auto const secondsSinceLastPayment = - parentCloseTime.time_since_epoch().count() - lastPaymentDate; + auto const secondsSinceLastPayment = now - lastPaymentDate; return principalOutstanding * periodicRate * secondsSinceLastPayment / paymentInterval; @@ -773,8 +774,8 @@ computeFullPayment( closeInterestRate); auto const [roundedFullInterest, roundedFullManagementFee] = [&]() { - auto const interest = - roundToAsset(asset, fullPaymentInterest, loanScale); + auto const interest = roundToAsset( + asset, fullPaymentInterest, loanScale, Number::downward); auto const parts = computeInterestAndFeeParts( asset, interest, managementFeeRate, loanScale); // Apply as much of the fee to the outstanding fee, but no From 36c1dd18ee1b5b66a1b2570b19c231f20e72e816 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sun, 23 Nov 2025 17:28:32 -0500 Subject: [PATCH 260/291] Review feedback from @Tapanito: final payment - Optimize final payment by checking and returning that case before doing any other computations. Removes some asserts that weren't really adding value. --- src/xrpld/app/misc/detail/LendingHelpers.cpp | 39 ++++++-------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index b8fda968d3..44b062f300 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -874,6 +874,18 @@ computePaymentComponents( auto const roundedPeriodicPayment = roundPeriodicPayment(asset, periodicPayment, scale); + if (paymentRemaining == 1 || + totalValueOutstanding <= roundedPeriodicPayment) + { + // If there's only one payment left, we need to pay off each of the loan + // parts. + return PaymentComponents{ + .trackedValueDelta = totalValueOutstanding, + .trackedPrincipalDelta = principalOutstanding, + .trackedManagementFeeDelta = managementFeeOutstanding, + .specialCase = PaymentSpecialCase::final}; + } + LoanState const trueTarget = calculateRawLoanState( periodicPayment, periodicRate, paymentRemaining - 1, managementFeeRate); LoanState const roundedTarget = LoanState{ @@ -919,33 +931,6 @@ computePaymentComponents( roundedPeriodicPayment - (deltas.principal + deltas.interest), currentLedgerState.managementFeeDue}); - if (paymentRemaining == 1 || - totalValueOutstanding <= roundedPeriodicPayment) - { - // If there's only one payment left, we need to pay off each of the loan - // parts. - - XRPL_ASSERT_PARTS( - deltas.total() <= totalValueOutstanding, - "ripple::detail::computePaymentComponents", - "last payment total value agrees"); - XRPL_ASSERT_PARTS( - deltas.principal <= principalOutstanding, - "ripple::detail::computePaymentComponents", - "last payment principal agrees"); - XRPL_ASSERT_PARTS( - deltas.managementFee <= managementFeeOutstanding, - "ripple::detail::computePaymentComponents", - "last payment management fee agrees"); - - // Pay everything off - return PaymentComponents{ - .trackedValueDelta = totalValueOutstanding, - .trackedPrincipalDelta = principalOutstanding, - .trackedManagementFeeDelta = managementFeeOutstanding, - .specialCase = PaymentSpecialCase::final}; - } - // The shortage must never be negative, which indicates that the parts are // trying to take more than the whole payment. The excess can be positive, // which indicates that we're not going to take the whole payment amount, From 27a7138f881bc3369b78b39f70a48910f33b888d Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sun, 23 Nov 2025 21:04:22 -0500 Subject: [PATCH 261/291] Review feedback from @gregtatcam: constructRoundedLoanState - Rename the overload of constructRoundedLoanState that takes components as inputs to constructLoanState. The original function did no rounding or enforcement of the inputs being rounded. - Left the overload constructRoundedLoanState(SLE::const_ref loan) alone, because Loan objects are assumed to be rounded. --- src/test/app/Loan_test.cpp | 6 +++--- src/xrpld/app/misc/LendingHelpers.h | 5 ++++- src/xrpld/app/misc/detail/LendingHelpers.cpp | 12 ++++++------ src/xrpld/app/tx/detail/LoanSet.cpp | 2 +- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 608a384199..0ea884e45c 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -906,7 +906,7 @@ protected: state.loanScale, Number::upward); - auto currentRoundedState = constructRoundedLoanState( + auto currentRoundedState = constructLoanState( state.totalValue, state.principalOutstanding, state.managementFeeOutstanding); @@ -2666,7 +2666,7 @@ protected: periodicRate, state.paymentRemaining, broker.params.managementFeeRate); - auto const rounded = constructRoundedLoanState( + auto const rounded = constructLoanState( state.totalValue, state.principalOutstanding, state.managementFeeOutstanding); @@ -5843,7 +5843,7 @@ protected: Number const latePaymentFeeRounded = roundToAsset( broker.asset, loanSle->at(sfLatePaymentFee), state.loanScale); - auto const roundedLoanState = constructRoundedLoanState( + auto const roundedLoanState = constructLoanState( state.totalValue, state.principalOutstanding, state.managementFeeOutstanding); diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index d2ff79d9b5..9d5e42ad7b 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -153,12 +153,15 @@ calculateRawLoanState( std::uint32_t const paymentRemaining, TenthBips32 const managementFeeRate); +// Constructs a valid LoanState object from arbitrary inputs LoanState -constructRoundedLoanState( +constructLoanState( Number const& totalValueOutstanding, Number const& principalOutstanding, Number const& managementFeeOutstanding); +// Constructs a valid LoanState object from a Loan object, which always has +// rounded values LoanState constructRoundedLoanState(SLE::const_ref loan); diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index 44b062f300..555bdf5374 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -373,7 +373,7 @@ tryOverpayment( { auto const raw = calculateRawLoanState( periodicPayment, periodicRate, paymentRemaining, managementFeeRate); - auto const rounded = constructRoundedLoanState( + auto const rounded = constructLoanState( totalValueOutstanding, principalOutstanding, managementFeeOutstanding); auto const errors = rounded - raw; @@ -427,7 +427,7 @@ tryOverpayment( numZero, rounded.managementFeeDue); - auto const newRounded = constructRoundedLoanState( + auto const newRounded = constructLoanState( totalValueOutstanding, principalOutstanding, managementFeeOutstanding); newLoanProperties.totalValueOutstanding = newRounded.valueOutstanding; @@ -896,7 +896,7 @@ computePaymentComponents( .interestDue = roundToAsset(asset, trueTarget.interestDue, scale), .managementFeeDue = roundToAsset(asset, trueTarget.managementFeeDue, scale)}; - LoanState const currentLedgerState = constructRoundedLoanState( + LoanState const currentLedgerState = constructLoanState( totalValueOutstanding, principalOutstanding, managementFeeOutstanding); LoanDeltas deltas = currentLedgerState - roundedTarget; @@ -1307,7 +1307,7 @@ calculateRawLoanState( } LoanState -constructRoundedLoanState( +constructLoanState( Number const& totalValueOutstanding, Number const& principalOutstanding, Number const& managementFeeOutstanding) @@ -1325,7 +1325,7 @@ constructRoundedLoanState( LoanState constructRoundedLoanState(SLE::const_ref loan) { - return constructRoundedLoanState( + return constructLoanState( loan->at(sfTotalValueOutstanding), loan->at(sfPrincipalOutstanding), loan->at(sfManagementFeeOutstanding)); @@ -1527,7 +1527,7 @@ loanMakePayment( Number const closePaymentFee = roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale); - LoanState const roundedLoanState = constructRoundedLoanState( + LoanState const roundedLoanState = constructLoanState( totalValueOutstandingProxy, principalOutstandingProxy, managementFeeOutstandingProxy); diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 95cb48234b..7425515d61 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -445,7 +445,7 @@ LoanSet::doApply() // LCOV_EXCL_STOP } - LoanState const state = constructRoundedLoanState( + LoanState const state = constructLoanState( properties.totalValueOutstanding, principalRequested, properties.managementFeeOwedToBroker); From 4ba2514bfac12e8b90ed5f14f04e0a08167c772a Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sun, 23 Nov 2025 21:28:09 -0500 Subject: [PATCH 262/291] Review feedback from @gregtatcam: LoanDeltas - Renamed to LoanStateDeltas, and document what it's for --- src/test/app/Loan_test.cpp | 5 +++-- src/xrpld/app/misc/LendingHelpers.h | 11 +++++++---- src/xrpld/app/misc/detail/LendingHelpers.cpp | 14 +++++++------- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 0ea884e45c..9864d9bd43 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -1024,7 +1024,8 @@ protected: periodicRate, state.paymentRemaining - 1, broker.params.managementFeeRate); - detail::LoanDeltas const deltas = currentTrueState - nextTrueState; + detail::LoanStateDeltas const deltas = + currentTrueState - nextTrueState; BEAST_EXPECT( deltas.total() == deltas.principal + deltas.interest + deltas.managementFee); @@ -2735,7 +2736,7 @@ protected: periodicRate, state.paymentRemaining - 1, broker.params.managementFeeRate); - detail::LoanDeltas const deltas = + detail::LoanStateDeltas const deltas = currentTrueState - nextTrueState; testcase diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 9d5e42ad7b..4bed3c136b 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -220,7 +220,10 @@ struct PaymentComponents trackedInterestPart() const; }; -struct LoanDeltas +// This structure describes the difference between two LoanState objects so that +// the differences between components don't have to be tracked individually, +// risking more errors. How that difference is used depends on the context. +struct LoanStateDeltas { Number principal; Number interest; @@ -250,14 +253,14 @@ computePaymentComponents( } // namespace detail -detail::LoanDeltas +detail::LoanStateDeltas operator-(LoanState const& lhs, LoanState const& rhs); LoanState -operator-(LoanState const& lhs, detail::LoanDeltas const& rhs); +operator-(LoanState const& lhs, detail::LoanStateDeltas const& rhs); LoanState -operator+(LoanState const& lhs, detail::LoanDeltas const& rhs); +operator+(LoanState const& lhs, detail::LoanStateDeltas const& rhs); LoanProperties computeLoanProperties( diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index 555bdf5374..021690539f 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -835,7 +835,7 @@ PaymentComponents::trackedInterestPart() const } void -LoanDeltas::nonNegative() +LoanStateDeltas::nonNegative() { if (principal < beast::zero) principal = numZero; @@ -899,7 +899,7 @@ computePaymentComponents( LoanState const currentLedgerState = constructLoanState( totalValueOutstanding, principalOutstanding, managementFeeOutstanding); - LoanDeltas deltas = currentLedgerState - roundedTarget; + LoanStateDeltas deltas = currentLedgerState - roundedTarget; deltas.nonNegative(); // Adjust the deltas if necessary for data integrity @@ -951,7 +951,7 @@ computePaymentComponents( "ripple::detail::computePaymentComponents", "excess non-negative"); }; - auto addressExcess = [&takeFrom](LoanDeltas& deltas, Number& excess) { + auto addressExcess = [&takeFrom](LoanStateDeltas& deltas, Number& excess) { // This order is based on where errors are the least problematic takeFrom(deltas.interest, excess); takeFrom(deltas.managementFee, excess); @@ -1081,10 +1081,10 @@ computeOverpaymentComponents( } // namespace detail -detail::LoanDeltas +detail::LoanStateDeltas operator-(LoanState const& lhs, LoanState const& rhs) { - detail::LoanDeltas result{ + detail::LoanStateDeltas result{ .principal = lhs.principalOutstanding - rhs.principalOutstanding, .interest = lhs.interestDue - rhs.interestDue, .managementFee = lhs.managementFeeDue - rhs.managementFeeDue, @@ -1094,7 +1094,7 @@ operator-(LoanState const& lhs, LoanState const& rhs) } LoanState -operator-(LoanState const& lhs, detail::LoanDeltas const& rhs) +operator-(LoanState const& lhs, detail::LoanStateDeltas const& rhs) { LoanState result{ .valueOutstanding = lhs.valueOutstanding - rhs.total(), @@ -1107,7 +1107,7 @@ operator-(LoanState const& lhs, detail::LoanDeltas const& rhs) } LoanState -operator+(LoanState const& lhs, detail::LoanDeltas const& rhs) +operator+(LoanState const& lhs, detail::LoanStateDeltas const& rhs) { LoanState result{ .valueOutstanding = lhs.valueOutstanding + rhs.total(), From 4c28e750ff6ca00a5bfaebd038135a0ff7ee9361 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sun, 23 Nov 2025 21:41:38 -0500 Subject: [PATCH 263/291] Review feedback from @gregtatcam: compute vs. calculate - Rename the LendingHelper functions that started with `calculate` to start with `compute`. --- src/test/app/Loan_test.cpp | 29 ++++++++++---------- src/xrpld/app/misc/LendingHelpers.h | 8 +++--- src/xrpld/app/misc/detail/LendingHelpers.cpp | 24 ++++++++-------- 3 files changed, 30 insertions(+), 31 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 9864d9bd43..b2ad47c2b4 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -911,7 +911,7 @@ protected: state.principalOutstanding, state.managementFeeOutstanding); { - auto const raw = calculateRawLoanState( + auto const raw = computeRawLoanState( state.periodicPayment, periodicRate, state.paymentRemaining, @@ -964,7 +964,7 @@ protected: Number totalFeesPaid = 0; std::size_t totalPaymentsMade = 0; - ripple::LoanState currentTrueState = calculateRawLoanState( + ripple::LoanState currentTrueState = computeRawLoanState( state.periodicPayment, periodicRate, state.paymentRemaining, @@ -1019,7 +1019,7 @@ protected: paymentComponents.trackedInterestPart() + paymentComponents.trackedManagementFeeDelta); - ripple::LoanState const nextTrueState = calculateRawLoanState( + ripple::LoanState const nextTrueState = computeRawLoanState( state.periodicPayment, periodicRate, state.paymentRemaining - 1, @@ -2662,7 +2662,7 @@ protected: Number::upward)); { - auto const raw = calculateRawLoanState( + auto const raw = computeRawLoanState( state.periodicPayment, periodicRate, state.paymentRemaining, @@ -2705,7 +2705,7 @@ protected: Number totalInterestPaid = 0; std::size_t totalPaymentsMade = 0; - ripple::LoanState currentTrueState = calculateRawLoanState( + ripple::LoanState currentTrueState = computeRawLoanState( state.periodicPayment, periodicRate, state.paymentRemaining, @@ -2730,12 +2730,11 @@ protected: paymentComponents.trackedValueDelta <= roundedPeriodicPayment); - ripple::LoanState const nextTrueState = - calculateRawLoanState( - state.periodicPayment, - periodicRate, - state.paymentRemaining - 1, - broker.params.managementFeeRate); + ripple::LoanState const nextTrueState = computeRawLoanState( + state.periodicPayment, + periodicRate, + state.paymentRemaining - 1, + broker.params.managementFeeRate); detail::LoanStateDeltas const deltas = currentTrueState - nextTrueState; @@ -5852,7 +5851,7 @@ protected: auto const periodicRate = loanPeriodicRate(interestRateValue, state.paymentInterval); - auto const rawLoanState = calculateRawLoanState( + auto const rawLoanState = computeRawLoanState( state.periodicPayment, periodicRate, state.paymentRemaining, @@ -5862,7 +5861,7 @@ protected: auto const startDateSeconds = static_cast( state.startDate.time_since_epoch().count()); - Number const fullPaymentInterest = calculateFullPaymentInterest( + Number const fullPaymentInterest = computeFullPaymentInterest( rawLoanState.principalOutstanding, periodicRate, parentCloseTime, @@ -6144,7 +6143,7 @@ protected: loanPeriodicRate(after.interestRate, after.paymentInterval); // Accrued + prepayment-penalty interest based on current periodic // schedule - auto const fullPaymentInterest = calculateFullPaymentInterest( + auto const fullPaymentInterest = computeFullPaymentInterest( after.periodicPayment, periodicRate2, after.paymentRemaining, @@ -6180,7 +6179,7 @@ protected: // Reference (clamped) computation: emulate a non-negative accrual // window by clamping prevPaymentDate to 'now' for the full-pay path. auto const prevClamped = std::min(after.previousPaymentDate, nowSecs); - auto const fullPaymentInterestClamped = calculateFullPaymentInterest( + auto const fullPaymentInterestClamped = computeFullPaymentInterest( after.periodicPayment, periodicRate2, after.paymentRemaining, diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 4bed3c136b..93dc2d7f66 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -139,14 +139,14 @@ checkLoanGuards( beast::Journal j); LoanState -calculateRawLoanState( +computeRawLoanState( Number const& periodicPayment, Number const& periodicRate, std::uint32_t const paymentRemaining, TenthBips32 const managementFeeRate); LoanState -calculateRawLoanState( +computeRawLoanState( Number const& periodicPayment, TenthBips32 interestRate, std::uint32_t paymentInterval, @@ -173,7 +173,7 @@ computeManagementFee( std::int32_t scale); Number -calculateFullPaymentInterest( +computeFullPaymentInterest( Number const& rawPrincipalOutstanding, Number const& periodicRate, NetClock::time_point parentCloseTime, @@ -183,7 +183,7 @@ calculateFullPaymentInterest( TenthBips32 closeInterestRate); Number -calculateFullPaymentInterest( +computeFullPaymentInterest( Number const& periodicPayment, Number const& periodicRate, std::uint32_t paymentRemaining, diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index 021690539f..3393288f50 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -371,7 +371,7 @@ tryOverpayment( TenthBips16 const managementFeeRate, beast::Journal j) { - auto const raw = calculateRawLoanState( + auto const raw = computeRawLoanState( periodicPayment, periodicRate, paymentRemaining, managementFeeRate); auto const rounded = constructLoanState( totalValueOutstanding, principalOutstanding, managementFeeOutstanding); @@ -398,7 +398,7 @@ tryOverpayment( << ", first payment principal: " << newLoanProperties.firstPaymentPrincipal; - auto const newRaw = calculateRawLoanState( + auto const newRaw = computeRawLoanState( newLoanProperties.periodicPayment, periodicRate, paymentRemaining, @@ -764,7 +764,7 @@ computeFullPayment( // Full payment interest consists of accrued normal interest and the // prepayment penalty, as computed by 3.2.4.1.4. - auto const fullPaymentInterest = calculateFullPaymentInterest( + auto const fullPaymentInterest = computeFullPaymentInterest( rawPrincipalOutstanding, periodicRate, view.parentCloseTime(), @@ -886,7 +886,7 @@ computePaymentComponents( .specialCase = PaymentSpecialCase::final}; } - LoanState const trueTarget = calculateRawLoanState( + LoanState const trueTarget = computeRawLoanState( periodicPayment, periodicRate, paymentRemaining - 1, managementFeeRate); LoanState const roundedTarget = LoanState{ .valueOutstanding = @@ -1201,7 +1201,7 @@ checkLoanGuards( } Number -calculateFullPaymentInterest( +computeFullPaymentInterest( Number const& rawPrincipalOutstanding, Number const& periodicRate, NetClock::time_point parentCloseTime, @@ -1236,7 +1236,7 @@ calculateFullPaymentInterest( } Number -calculateFullPaymentInterest( +computeFullPaymentInterest( Number const& periodicPayment, Number const& periodicRate, std::uint32_t paymentRemaining, @@ -1250,7 +1250,7 @@ calculateFullPaymentInterest( detail::loanPrincipalFromPeriodicPayment( periodicPayment, periodicRate, paymentRemaining); - return calculateFullPaymentInterest( + return computeFullPaymentInterest( rawPrincipalOutstanding, periodicRate, parentCloseTime, @@ -1261,7 +1261,7 @@ calculateFullPaymentInterest( } LoanState -calculateRawLoanState( +computeRawLoanState( Number const& periodicPayment, Number const& periodicRate, std::uint32_t const paymentRemaining, @@ -1292,14 +1292,14 @@ calculateRawLoanState( }; LoanState -calculateRawLoanState( +computeRawLoanState( Number const& periodicPayment, TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t const paymentRemaining, TenthBips32 const managementFeeRate) { - return calculateRawLoanState( + return computeRawLoanState( periodicPayment, loanPeriodicRate(interestRate, paymentInterval), paymentRemaining, @@ -1412,12 +1412,12 @@ computeLoanProperties( auto const firstPaymentPrincipal = [&]() { // Compute the parts for the first payment. Ensure that the // principal payment will actually change the principal. - auto const startingState = calculateRawLoanState( + auto const startingState = computeRawLoanState( periodicPayment, periodicRate, paymentsRemaining, managementFeeRate); - auto const firstPaymentState = calculateRawLoanState( + auto const firstPaymentState = computeRawLoanState( periodicPayment, periodicRate, paymentsRemaining - 1, From dd5464d92170a2ce54a3efc7ba90b845c0a033fa Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sun, 23 Nov 2025 23:06:44 -0500 Subject: [PATCH 264/291] From discussion with @gregtatcam: Revert "Payment" - Revert Payment transactor (Payment.*) back to what is in "develop". - Change LoanBrokerCoverWithdraw to do a direct transfer, whether it's too the account or the destination. Similar to VaultWithdraw, and as specified - https://github.com/XRPLF/rippled/pull/5270#discussion_r2554560222 --- .../app/tx/detail/LoanBrokerCoverWithdraw.cpp | 67 +--- src/xrpld/app/tx/detail/Payment.cpp | 373 +++++++----------- src/xrpld/app/tx/detail/Payment.h | 31 -- 3 files changed, 165 insertions(+), 306 deletions(-) diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp index 58204b70d5..ef4cd68420 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp @@ -159,60 +159,25 @@ LoanBrokerCoverWithdraw::doApply() return ter; } - // Move the funds from the broker's pseudo-account to the dstAcct - if (dstAcct == account_ || amount.native()) + // Sanity check + if (accountHolds( + view(), + brokerPseudoID, + amount.asset(), + FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, + j_) < amount) { - // Transfer assets directly from pseudo-account to depositor. - // Because this is either a self-transfer or an XRP payment, there is no - // need to use the payment engine. - return accountSend( - view(), brokerPseudoID, dstAcct, amount, j_, WaiveTransferFee::Yes); + // LCOV_EXCL_START + JLOG(j_.error()) << "LoanBrokerCoverWithdraw: negative balance of " + "broker cover assets."; + return tefINTERNAL; + // LCOV_EXCL_STOP } - { - // If sending the Cover to a different account, then this is - // effectively a payment. Use the Payment transaction code to call - // the payment engine, though only a subset of the functionality is - // supported in this transaction. e.g. No paths, no partial - // payments. - bool const mptDirect = amount.holds(); - STAmount const maxSourceAmount = - Payment::getMaxSourceAmount(brokerPseudoID, amount); - SLE::pointer sleDst = view().peek(keylet::account(dstAcct)); - if (!sleDst) - return tecINTERNAL; // LCOV_EXCL_LINE - - Payment::RipplePaymentParams paymentParams{ - .ctx = ctx_, - .maxSourceAmount = maxSourceAmount, - .srcAccountID = brokerPseudoID, - .dstAccountID = dstAcct, - .sleDst = sleDst, - .dstAmount = amount, - .paths = STPathSet{}, - .deliverMin = std::nullopt, - .j = j_}; - - TER ret; - if (mptDirect) - { - ret = Payment::makeMPTDirectPayment(paymentParams); - } - else - { - ret = Payment::makeRipplePayment(paymentParams); - } - // Always claim a fee - if (!isTesSuccess(ret) && !isTecClaim(ret)) - { - JLOG(j_.info()) - << "LoanBrokerCoverWithdraw: changing result from " - << transToken(ret) - << " to tecPATH_DRY for IOU payment with Destination"; - return tecPATH_DRY; - } - return ret; - } + // Move the funds directly from the broker's pseudo-account to the dstAcct + return accountSend( + view(), brokerPseudoID, dstAcct, amount, j_, WaiveTransferFee::Yes); } //------------------------------------------------------------------------------ diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index 9d6a5e5783..3aff4db5e1 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -29,8 +29,8 @@ Payment::makeTxConsequences(PreflightContext const& ctx) } STAmount -Payment::getMaxSourceAmount( - AccountID const& senderAccount, +getMaxSourceAmount( + AccountID const& account, STAmount const& dstAmount, std::optional const& sendMax) { @@ -40,7 +40,7 @@ Payment::getMaxSourceAmount( return dstAmount; else return STAmount( - Issue{dstAmount.get().currency, senderAccount}, + Issue{dstAmount.get().currency, account}, dstAmount.mantissa(), dstAmount.exponent(), dstAmount < beast::zero); @@ -422,35 +422,155 @@ Payment::doApply() if (ripple) { - return makeRipplePayment(RipplePaymentParams{ - .ctx = ctx_, - .maxSourceAmount = maxSourceAmount, - .srcAccountID = account_, - .dstAccountID = dstAccountID, - .sleDst = sleDst, - .dstAmount = dstAmount, - .paths = ctx_.tx.getFieldPathSet(sfPaths), - .deliverMin = deliverMin, - .partialPaymentAllowed = partialPaymentAllowed, - .defaultPathsAllowed = defaultPathsAllowed, - .limitQuality = limitQuality, - .j = j_}); + // Ripple payment with at least one intermediate step and uses + // transitive balances. + + // An account that requires authorization has two ways to get an + // IOU Payment in: + // 1. If Account == Destination, or + // 2. If Account is deposit preauthorized by destination. + + if (auto err = verifyDepositPreauth( + ctx_.tx, + ctx_.view(), + account_, + dstAccountID, + sleDst, + ctx_.journal); + !isTesSuccess(err)) + return err; + + path::RippleCalc::Input rcInput; + rcInput.partialPaymentAllowed = partialPaymentAllowed; + rcInput.defaultPathsAllowed = defaultPathsAllowed; + rcInput.limitQuality = limitQuality; + rcInput.isLedgerOpen = view().open(); + + path::RippleCalc::Output rc; + { + PaymentSandbox pv(&view()); + JLOG(j_.debug()) << "Entering RippleCalc in payment: " + << ctx_.tx.getTransactionID(); + rc = path::RippleCalc::rippleCalculate( + pv, + maxSourceAmount, + dstAmount, + dstAccountID, + account_, + ctx_.tx.getFieldPathSet(sfPaths), + ctx_.tx[~sfDomainID], + ctx_.app.logs(), + &rcInput); + // VFALCO NOTE We might not need to apply, depending + // on the TER. But always applying *should* + // be safe. + pv.apply(ctx_.rawView()); + } + + // TODO: is this right? If the amount is the correct amount, was + // the delivered amount previously set? + if (rc.result() == tesSUCCESS && rc.actualAmountOut != dstAmount) + { + if (deliverMin && rc.actualAmountOut < *deliverMin) + rc.setResult(tecPATH_PARTIAL); + else + ctx_.deliver(rc.actualAmountOut); + } + + auto terResult = rc.result(); + + // Because of its overhead, if RippleCalc + // fails with a retry code, claim a fee + // instead. Maybe the user will be more + // careful with their path spec next time. + if (isTerRetry(terResult)) + terResult = tecPATH_DRY; + return terResult; } else if (mptDirect) { - return makeMPTDirectPayment(RipplePaymentParams{ - .ctx = ctx_, - .maxSourceAmount = maxSourceAmount, - .srcAccountID = account_, - .dstAccountID = dstAccountID, - .sleDst = sleDst, - .dstAmount = dstAmount, - .paths = ctx_.tx.getFieldPathSet(sfPaths), - .deliverMin = deliverMin, - .partialPaymentAllowed = partialPaymentAllowed, - .defaultPathsAllowed = defaultPathsAllowed, - .limitQuality = limitQuality, - .j = j_}); + JLOG(j_.trace()) << " dstAmount=" << dstAmount.getFullText(); + auto const& mptIssue = dstAmount.get(); + + if (auto const ter = requireAuth(view(), mptIssue, account_); + ter != tesSUCCESS) + return ter; + + if (auto const ter = requireAuth(view(), mptIssue, dstAccountID); + ter != tesSUCCESS) + return ter; + + if (auto const ter = + canTransfer(view(), mptIssue, account_, dstAccountID); + ter != tesSUCCESS) + return ter; + + if (auto err = verifyDepositPreauth( + ctx_.tx, + ctx_.view(), + account_, + dstAccountID, + sleDst, + ctx_.journal); + !isTesSuccess(err)) + return err; + + auto const& issuer = mptIssue.getIssuer(); + + // Transfer rate + Rate rate{QUALITY_ONE}; + // Payment between the holders + if (account_ != issuer && dstAccountID != issuer) + { + // If globally/individually locked then + // - can't send between holders + // - holder can send back to issuer + // - issuer can send to holder + if (isAnyFrozen(view(), {account_, dstAccountID}, mptIssue)) + return tecLOCKED; + + // Get the rate for a payment between the holders. + rate = transferRate(view(), mptIssue.getMptID()); + } + + // Amount to deliver. + STAmount amountDeliver = dstAmount; + // Factor in the transfer rate. + // No rounding. It'll change once MPT integrated into DEX. + STAmount requiredMaxSourceAmount = multiply(dstAmount, rate); + + // Send more than the account wants to pay or less than + // the account wants to deliver (if no SendMax). + // Adjust the amount to deliver. + if (partialPaymentAllowed && requiredMaxSourceAmount > maxSourceAmount) + { + requiredMaxSourceAmount = maxSourceAmount; + // No rounding. It'll change once MPT integrated into DEX. + amountDeliver = divide(maxSourceAmount, rate); + } + + if (requiredMaxSourceAmount > maxSourceAmount || + (deliverMin && amountDeliver < *deliverMin)) + return tecPATH_PARTIAL; + + PaymentSandbox pv(&view()); + auto res = accountSend( + pv, account_, dstAccountID, amountDeliver, ctx_.journal); + if (res == tesSUCCESS) + { + pv.apply(ctx_.rawView()); + + // If the actual amount delivered is different from the original + // amount due to partial payment or transfer fee, we need to update + // DelieveredAmount using the actual delivered amount + if (view().rules().enabled(fixMPTDeliveredAmount) && + amountDeliver != dstAmount) + ctx_.deliver(amountDeliver); + } + else if (res == tecINSUFFICIENT_FUNDS || res == tecPATH_DRY) + res = tecPATH_PARTIAL; + + return res; } XRPL_ASSERT(dstAmount.native(), "ripple::Payment::doApply : amount is XRP"); @@ -543,199 +663,4 @@ Payment::doApply() return tesSUCCESS; } -// Reusable helpers -TER -Payment::makeRipplePayment(Payment::RipplePaymentParams const& p) -{ - // Set up some copies/references so the code can be moved from - // Payment::doApply otherwise unmodified - // - // Note that some of these variable names use trailing '_', which is - // usually reserved for member variables. After, or just before these - // changes are merged, a follow-up can clean up the names for consistency. - ApplyContext& ctx_ = p.ctx; - STAmount const& maxSourceAmount = p.maxSourceAmount; - AccountID const& account_ = p.srcAccountID; - AccountID const& dstAccountID = p.dstAccountID; - SLE::pointer sleDst = p.sleDst; - STAmount const& dstAmount = p.dstAmount; - STPathSet const& paths = p.paths; - std::optional const& deliverMin = p.deliverMin; - bool partialPaymentAllowed = p.partialPaymentAllowed; - bool defaultPathsAllowed = p.defaultPathsAllowed; - bool limitQuality = p.limitQuality; - beast::Journal j_ = p.j; - - auto view = [&p]() -> ApplyView& { return p.ctx.view(); }; - - // Below this line, copied straight from Payment::doApply - // except `ctx_.tx.getFieldPathSet(sfPaths)` replaced with `paths`, - // because not all transactions have that field available, and using - // it will throw - //------------------------------------------------------- - // Ripple payment with at least one intermediate step and uses - // transitive balances. - - // An account that requires authorization has two ways to get an - // IOU Payment in: - // 1. If Account == Destination, or - // 2. If Account is deposit preauthorized by destination. - - if (auto err = verifyDepositPreauth( - ctx_.tx, ctx_.view(), account_, dstAccountID, sleDst, ctx_.journal); - !isTesSuccess(err)) - return err; - - path::RippleCalc::Input rcInput; - rcInput.partialPaymentAllowed = partialPaymentAllowed; - rcInput.defaultPathsAllowed = defaultPathsAllowed; - rcInput.limitQuality = limitQuality; - rcInput.isLedgerOpen = view().open(); - - path::RippleCalc::Output rc; - { - PaymentSandbox pv(&view()); - JLOG(j_.debug()) << "Entering RippleCalc in payment: " - << ctx_.tx.getTransactionID(); - rc = path::RippleCalc::rippleCalculate( - pv, - maxSourceAmount, - dstAmount, - dstAccountID, - account_, - paths, - ctx_.tx[~sfDomainID], - ctx_.app.logs(), - &rcInput); - // VFALCO NOTE We might not need to apply, depending - // on the TER. But always applying *should* - // be safe. - pv.apply(ctx_.rawView()); - } - - // TODO: is this right? If the amount is the correct amount, was - // the delivered amount previously set? - if (rc.result() == tesSUCCESS && rc.actualAmountOut != dstAmount) - { - if (deliverMin && rc.actualAmountOut < *deliverMin) - rc.setResult(tecPATH_PARTIAL); - else - ctx_.deliver(rc.actualAmountOut); - } - - auto terResult = rc.result(); - - // Because of its overhead, if RippleCalc - // fails with a retry code, claim a fee - // instead. Maybe the user will be more - // careful with their path spec next time. - if (isTerRetry(terResult)) - terResult = tecPATH_DRY; - return terResult; -} - -TER -Payment::makeMPTDirectPayment(Payment::RipplePaymentParams const& p) -{ - // Set up some copies/references so the code can be moved from - // Payment::doApply otherwise unmodified - // - // Note that some of these variable names use trailing '_', which is - // usually reserved for member variables. After, or just before these - // changes are merged, a follow-up can clean up the names for consistency. - ApplyContext& ctx_ = p.ctx; - STAmount const& maxSourceAmount = p.maxSourceAmount; - AccountID const& account_ = p.srcAccountID; - AccountID const& dstAccountID = p.dstAccountID; - SLE::pointer sleDst = p.sleDst; - STAmount const& dstAmount = p.dstAmount; - // STPathSet const& paths = p.paths; - std::optional const& deliverMin = p.deliverMin; - bool partialPaymentAllowed = p.partialPaymentAllowed; - // bool defaultPathsAllowed = p.defaultPathsAllowed; - // bool limitQuality = p.limitQuality; - beast::Journal j_ = p.j; - - auto view = [&p]() -> ApplyView& { return p.ctx.view(); }; - - // Below this line, copied straight from Payment::doApply - //------------------------------------------------------- - JLOG(j_.trace()) << " dstAmount=" << dstAmount.getFullText(); - auto const& mptIssue = dstAmount.get(); - - if (auto const ter = requireAuth(view(), mptIssue, account_); - ter != tesSUCCESS) - return ter; - - if (auto const ter = requireAuth(view(), mptIssue, dstAccountID); - ter != tesSUCCESS) - return ter; - - if (auto const ter = canTransfer(view(), mptIssue, account_, dstAccountID); - ter != tesSUCCESS) - return ter; - - if (auto err = verifyDepositPreauth( - ctx_.tx, ctx_.view(), account_, dstAccountID, sleDst, ctx_.journal); - !isTesSuccess(err)) - return err; - - auto const& issuer = mptIssue.getIssuer(); - - // Transfer rate - Rate rate{QUALITY_ONE}; - // Payment between the holders - if (account_ != issuer && dstAccountID != issuer) - { - // If globally/individually locked then - // - can't send between holders - // - holder can send back to issuer - // - issuer can send to holder - if (isAnyFrozen(view(), {account_, dstAccountID}, mptIssue)) - return tecLOCKED; - - // Get the rate for a payment between the holders. - rate = transferRate(view(), mptIssue.getMptID()); - } - - // Amount to deliver. - STAmount amountDeliver = dstAmount; - // Factor in the transfer rate. - // No rounding. It'll change once MPT integrated into DEX. - STAmount requiredMaxSourceAmount = multiply(dstAmount, rate); - - // Send more than the account wants to pay or less than - // the account wants to deliver (if no SendMax). - // Adjust the amount to deliver. - if (partialPaymentAllowed && requiredMaxSourceAmount > maxSourceAmount) - { - requiredMaxSourceAmount = maxSourceAmount; - // No rounding. It'll change once MPT integrated into DEX. - amountDeliver = divide(maxSourceAmount, rate); - } - - if (requiredMaxSourceAmount > maxSourceAmount || - (deliverMin && amountDeliver < *deliverMin)) - return tecPATH_PARTIAL; - - PaymentSandbox pv(&view()); - auto res = - accountSend(pv, account_, dstAccountID, amountDeliver, ctx_.journal); - if (res == tesSUCCESS) - { - pv.apply(ctx_.rawView()); - - // If the actual amount delivered is different from the original - // amount due to partial payment or transfer fee, we need to update - // DelieveredAmount using the actual delivered amount - if (view().rules().enabled(fixMPTDeliveredAmount) && - amountDeliver != dstAmount) - ctx_.deliver(amountDeliver); - } - else if (res == tecINSUFFICIENT_FUNDS || res == tecPATH_DRY) - res = tecPATH_PARTIAL; - - return res; -} - } // namespace ripple diff --git a/src/xrpld/app/tx/detail/Payment.h b/src/xrpld/app/tx/detail/Payment.h index c532b6efb2..f41326ecf6 100644 --- a/src/xrpld/app/tx/detail/Payment.h +++ b/src/xrpld/app/tx/detail/Payment.h @@ -40,37 +40,6 @@ public: TER doApply() override; - - // Helpers for this class and other transactors that make "Payments" - struct RipplePaymentParams - { - ApplyContext& ctx; - STAmount const& maxSourceAmount; - AccountID const& srcAccountID; - AccountID const& dstAccountID; - SLE::pointer sleDst; - STAmount const& dstAmount; - // Paths need to be explicitly included because other transactions don't - // have them defined - STPathSet const& paths; - std::optional const& deliverMin; - bool partialPaymentAllowed = false; - bool defaultPathsAllowed = true; - bool limitQuality = false; - beast::Journal j; - }; - - static STAmount - getMaxSourceAmount( - AccountID const& senderAccount, - STAmount const& dstAmount, - std::optional const& sendMax = {}); - - static TER - makeRipplePayment(RipplePaymentParams const& p); - - static TER - makeMPTDirectPayment(RipplePaymentParams const& p); }; } // namespace ripple From 76766e93dfad792383450d848ed642c67093d0fe Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sun, 23 Nov 2025 23:14:26 -0500 Subject: [PATCH 265/291] Review feedback from @gregtatcam: readability --- src/xrpld/app/misc/detail/LendingHelpers.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index 3393288f50..80423e7269 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -1653,7 +1653,7 @@ loanMakePayment( Number totalPaid; std::size_t numPayments = 0; - while (amount >= totalPaid + periodic.totalDue && + while ((amount >= (totalPaid + periodic.totalDue)) && paymentRemainingProxy > 0 && numPayments < loanMaximumPaymentsPerTransaction) { From 1ba010031d560c485f05e5e74fdbb0c4d2e171ca Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Sun, 23 Nov 2025 23:32:23 -0500 Subject: [PATCH 266/291] Review feedback from @gregtatcam: variable name - Renamed "fee" to "overpaymentFee" in "computeOverpaymentComponents". --- src/xrpld/app/misc/detail/LendingHelpers.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index 80423e7269..8ba49c787c 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -1047,7 +1047,7 @@ computeOverpaymentComponents( "ripple::detail::computeOverpaymentComponents : valid overpayment " "amount"); - Number const fee = roundToAsset( + Number const overpaymentFee = roundToAsset( asset, tenthBipsOfValue(overpayment, overpaymentFeeRate), loanScale); auto const [rawOverpaymentInterest, _] = [&]() { @@ -1065,12 +1065,12 @@ computeOverpaymentComponents( auto const result = detail::ExtendedPaymentComponents{ detail::PaymentComponents{ - .trackedValueDelta = overpayment - fee, + .trackedValueDelta = overpayment - overpaymentFee, .trackedPrincipalDelta = overpayment - roundedOverpaymentInterest - - roundedOverpaymentManagementFee - fee, + roundedOverpaymentManagementFee - overpaymentFee, .trackedManagementFeeDelta = roundedOverpaymentManagementFee, .specialCase = detail::PaymentSpecialCase::extra}, - fee, + overpaymentFee, roundedOverpaymentInterest}; XRPL_ASSERT_PARTS( result.trackedInterestPart() == roundedOverpaymentInterest, From a7af773dd83206fe7bf9140ad218c758f81c5be6 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 24 Nov 2025 16:24:35 -0500 Subject: [PATCH 267/291] Add a LoanBroker invariant to compare CoverAvailable to balance - Ensures that LoanBroker.CoverAvailable is never less than the pseudo-account balance for the Vault asset. --- include/xrpl/protocol/STAmount.h | 2 +- src/libxrpl/protocol/STAmount.cpp | 6 +- src/xrpld/app/tx/detail/InvariantCheck.cpp | 90 +++++++++++++++++++++- src/xrpld/app/tx/detail/InvariantCheck.h | 22 +++++- 4 files changed, 111 insertions(+), 9 deletions(-) diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index 58c420de74..3ea45cc05b 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -564,7 +564,7 @@ STAmount::clear() { // The -100 is used to allow 0 to sort less than a small positive values // which have a negative exponent. - mOffset = native() ? 0 : -100; + mOffset = integral() ? 0 : -100; mValue = 0; mIsNegative = false; } diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 072b84f2c0..8b714193cc 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -830,7 +830,7 @@ STAmount::isDefault() const void STAmount::canonicalize() { - if (native() || mAsset.holds()) + if (integral()) { // native and MPT currency amounts should always have an offset of zero // log(2^64,10) ~ 19.2 @@ -859,8 +859,10 @@ STAmount::canonicalize() }; if (native()) set(XRPAmount{num}); - else + else if (mAsset.holds()) set(MPTAmount{num}); + else + Throw("Unknown integral asset type"); mOffset = 0; } else diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index a82061b180..dd3c605ca9 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -2342,9 +2342,30 @@ ValidLoanBroker::visitEntry( std::shared_ptr const& before, std::shared_ptr const& after) { - if (after && after->getType() == ltLOAN_BROKER) + if (after) { - brokers_.emplace_back(before, after); + if (after->getType() == ltLOAN_BROKER) + { + auto& broker = brokers_[after->key()]; + broker.brokerBefore = before; + broker.brokerAfter = after; + } + else if ( + after->getType() == ltACCOUNT_ROOT && + after->isFieldPresent(sfLoanBrokerID)) + { + auto const& loanBrokerID = after->at(sfLoanBrokerID); + // create an entry if one doesn't already exist + auto& broker = brokers_[loanBrokerID]; + } + else if (after->getType() == ltRIPPLE_STATE) + { + lines_.emplace_back(after); + } + else if (after->getType() == ltMPTOKEN) + { + mpts_.emplace_back(after); + } } } @@ -2403,8 +2424,51 @@ ValidLoanBroker::finalize( // Loan Brokers will not exist on ledger if the Lending Protocol amendment // is not enabled, so there's no need to check it. - for (auto const& [before, after] : brokers_) + for (auto const& line : lines_) { + for (auto const& field : {&sfLowLimit, &sfHighLimit}) + { + auto const account = + view.read(keylet::account(line->at(*field).getIssuer())); + // This Invariant doesn't know about the rules for Trust Lines, so + // if the account is missing, don't treat it as an error. This + // loop is only concerned with finding Broker pseudo-accounts + if (account && account->isFieldPresent(sfLoanBrokerID)) + { + auto const& loanBrokerID = account->at(sfLoanBrokerID); + // create an entry if one doesn't already exist + auto& broker = brokers_[loanBrokerID]; + } + } + } + for (auto const& mpt : mpts_) + { + auto const account = view.read(keylet::account(mpt->at(sfAccount))); + // This Invariant doesn't know about the rules for MPTokens, so + // if the account is missing, don't treat is as an error. This + // loop is only concerned with finding Broker pseudo-accounts + if (account && account->isFieldPresent(sfLoanBrokerID)) + { + auto const& loanBrokerID = account->at(sfLoanBrokerID); + // create an entry if one doesn't already exist + auto& broker = brokers_[loanBrokerID]; + } + } + + for (auto const& [brokerID, broker] : brokers_) + { + auto const& after = broker.brokerAfter + ? broker.brokerAfter + : view.read(keylet::loanbroker(brokerID)); + + if (!after) + { + JLOG(j.fatal()) << "Invariant failed: Loan Broker missing"; + return false; + } + + auto const& before = broker.brokerBefore; + // https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3123-invariants // If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most // one node (the root), which will only hold entries for `RippleState` @@ -2438,6 +2502,26 @@ ValidLoanBroker::finalize( << "Invariant failed: Loan Broker cover available is negative"; return false; } + auto const vault = view.read(keylet::vault(after->at(sfVaultID))); + if (!vault) + { + JLOG(j.fatal()) + << "Invariant failed: Loan Broker vault ID is invalid"; + return false; + } + auto const& vaultAsset = vault->at(sfAsset); + if (after->at(sfCoverAvailable) < accountHolds( + view, + after->at(sfAccount), + vaultAsset, + FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, + j)) + { + JLOG(j.fatal()) << "Invariant failed: Loan Broker cover available " + "is less than pseudo-account asset balance"; + return false; + } } return true; } diff --git a/src/xrpld/app/tx/detail/InvariantCheck.h b/src/xrpld/app/tx/detail/InvariantCheck.h index 4f81141300..ffe8e4ca5e 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.h +++ b/src/xrpld/app/tx/detail/InvariantCheck.h @@ -763,9 +763,25 @@ public: */ class ValidLoanBroker { - // Pair is . After is used for most of the checks, except - // those that check changed values. - std::vector> brokers_; + // Not all of these elements will necessarily be populated. Remaining items + // will be looked up as needed. + struct BrokerInfo + { + SLE::const_pointer brokerBefore = nullptr; + // After is used for most of the checks, except + // those that check changed values. + SLE::const_pointer brokerAfter = nullptr; + }; + // Collect all the LoanBrokers found directly or indirectly through + // pseudo-accounts. Key is the brokerID / index. It will be used to find the + // LoanBroker object if brokerBefore and brokerAfter are nullptr + std::map brokers_; + // Collect all the modified trust lines. Their high and low accounts will be + // loaded to look for LoanBroker pseudo-accounts. + std::vector lines_; + // Collect all the modified MPTokens. Their accounts will be loaded to look + // for LoanBroker pseudo-accounts. + std::vector mpts_; bool goodZeroDirectory( From 4081c9b8e92fd75f2ae12c071547d515d7b771be Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 24 Nov 2025 18:20:00 -0400 Subject: [PATCH 268/291] Update src/xrpld/app/tx/detail/LoanBrokerSet.cpp Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com> --- src/xrpld/app/tx/detail/LoanBrokerSet.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp index 196d86d091..e0d54f2ff0 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp @@ -169,6 +169,7 @@ LoanBrokerSet::doApply() broker->at(sfVaultID) = vaultID; broker->at(sfOwner) = account_; broker->at(sfAccount) = pseudoId; + // The LoanSequence indexes loans created by this broker, starting at 1 broker->at(sfLoanSequence) = 1; if (auto const data = tx[~sfData]) broker->at(sfData) = *data; From e850b9f799bd7e7029d7e72698a9cb58069c7aab Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 24 Nov 2025 18:25:56 -0400 Subject: [PATCH 269/291] Update src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com> --- src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp index 974bb36a93..01bf4102ed 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp @@ -49,8 +49,10 @@ LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx) auto const vault = ctx.view.read(keylet::vault(sleBroker->at(sfVaultID))); if (!vault) { + // LCOV_EXCL_START JLOG(ctx.j.fatal()) << "Vault is missing for Broker " << brokerID; return tefBAD_LEDGER; + // LCOV_EXCL_STOP } auto const vaultAsset = vault->at(sfAsset); From b7b0a6573561113e9402715b7aebf2e126733bd9 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 24 Nov 2025 18:44:16 -0400 Subject: [PATCH 270/291] Update src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com> --- src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp index f31520e42b..b4a71d47d1 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp @@ -226,7 +226,13 @@ LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx) auto const vault = ctx.view.read(keylet::vault(sleBroker->at(sfVaultID))); if (!vault) - return tecINTERNAL; + { + // LCOV_EXCL_START + JLOG(ctx.j.fatal()) << "Vault is missing for Broker " << brokerID; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + auto const vaultAsset = vault->at(sfAsset); From 7e6647e769c33a56eae6c560e1be0de5ed5bbbc0 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 24 Nov 2025 16:40:37 -0500 Subject: [PATCH 271/291] Review feedback from @shawnxie999: broker owner count Annotate loan broker owner set/delete owner count adjustment --- src/xrpld/app/tx/detail/LoanBrokerDelete.cpp | 2 ++ src/xrpld/app/tx/detail/LoanBrokerSet.cpp | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp b/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp index d3c4092d21..191aef71ec 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp @@ -157,6 +157,8 @@ LoanBrokerDelete::doApply() if (!owner) return tefBAD_LEDGER; // LCOV_EXCL_LINE + // Decreases the owner count by two: one for the LoanBroker object, and + // one for the pseudo-account. adjustOwnerCount(view(), owner, -2, j_); } diff --git a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp index e0d54f2ff0..beb8bf141b 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp @@ -148,6 +148,8 @@ LoanBrokerSet::doApply() if (auto const ter = dirLink(view, vaultPseudoID, broker, sfVaultNode)) return ter; // LCOV_EXCL_LINE + // Increases the owner count by two: one for the LoanBroker object, and + // one for the pseudo-account. adjustOwnerCount(view, owner, 2, j_); auto const ownerCount = owner->at(sfOwnerCount); if (mPriorBalance < view.fees().accountReserve(ownerCount)) From bb3bd74457af564956305ebce6d2f9989782351d Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 24 Nov 2025 16:53:52 -0500 Subject: [PATCH 272/291] Review feedback from @shawnxie999: removeEmptyHolding - removeEmptyHolding will succeed if the account is the issuer - If it encounters a trust line in that state, the line will always be deleted. - If it encounters an MPTToken in that state, which should be impossible, the token will be deleted if it doesn't have a balance. --- src/libxrpl/ledger/View.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index 682c0cb2ad..b3625bc596 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -1730,10 +1730,13 @@ removeEmptyHolding( } // `asset` is an IOU. + // If the account is the issuer, then no line should exist. Check anyway. If + // a line does exist, it will get deleted. If not, return success. + bool const accountIsIssuer = accountID == issue.account; auto const line = view.peek(keylet::line(accountID, issue)); if (!line) - return tecOBJECT_NOT_FOUND; - if (line->at(sfBalance)->iou() != beast::zero) + return accountIsIssuer ? (TER)tesSUCCESS : (TER)tecOBJECT_NOT_FOUND; + if (!accountIsIssuer && line->at(sfBalance)->iou() != beast::zero) return tecHAS_OBLIGATIONS; // Adjust the owner count(s) @@ -1782,10 +1785,18 @@ removeEmptyHolding( MPTIssue const& mptIssue, beast::Journal journal) { + // If the account is the issuer, then no token should exist. MPTs do not + // have the legacy ability to create such a situation, but check anyway. If + // a token does exist, it will get deleted. If not, return success. + bool const accountIsIssuer = accountID == mptIssue.getIssuer(); auto const& mptID = mptIssue.getMptID(); auto const mptoken = view.peek(keylet::mptoken(mptID, accountID)); if (!mptoken) - return tecOBJECT_NOT_FOUND; + return accountIsIssuer ? (TER)tesSUCCESS : (TER)tecOBJECT_NOT_FOUND; + // Unlike a trust line, if the account is the issuer, and the token has a + // balance, it can not just be deleted, because that will throw the issuance + // accounting out of balance, so fail. Since this should be impossible + // anyway, I'm not going to put any effort into it. if (mptoken->at(sfMPTAmount) != 0) return tecHAS_OBLIGATIONS; From c5f5edebccfc1a47bbfaa528d70f5bbd83980c43 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 24 Nov 2025 17:45:56 -0500 Subject: [PATCH 273/291] Address a couple of feedback items from @Tapanito and @shawnxie999: - Call verifyDepositAuth in VaultWithdraw and LoanBrokerCoverWithdraw to a destination. - Update a couple of impossible error returns to log a message, but exclude from coverage. - Fail if the LoanBroker.LoanSequence overflows to 0. --- .../app/tx/detail/LoanBrokerCoverWithdraw.cpp | 16 +++++++++++++++- src/xrpld/app/tx/detail/LoanSet.cpp | 4 ++++ src/xrpld/app/tx/detail/VaultWithdraw.cpp | 7 +++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp index ef4cd68420..2b54e28295 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp @@ -3,6 +3,8 @@ #include #include +#include + namespace ripple { bool @@ -59,7 +61,12 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) } auto const vault = ctx.view.read(keylet::vault(sleBroker->at(sfVaultID))); if (!vault) - return tefBAD_LEDGER; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + JLOG(ctx.j.fatal()) << "Vault is missing for Broker " << brokerID; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } auto const vaultAsset = vault->at(sfAsset); if (amount.asset() != vaultAsset) @@ -158,6 +165,13 @@ LoanBrokerCoverWithdraw::doApply() !isTesSuccess(ter) && ter != tecDUPLICATE) return ter; } + else + { + auto dstSle = view().peek(keylet::account(dstAcct)); + if (auto err = + verifyDepositPreauth(tx, view(), account_, dstAcct, dstSle, j_)) + return err; + } // Sanity check if (accountHolds( diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 7425515d61..e73f685bf8 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -609,6 +609,10 @@ LoanSet::doApply() // and is distinct from the broker's pseudo-account's owner count adjustOwnerCount(view, brokerSle, 1, j_); loanSequenceProxy += 1; + // The sequence should be extremely unlikely to roll over, but fail if it + // does + if (loanSequenceProxy == 0) + return tecMAX_SEQUENCE_REACHED; view.update(brokerSle); // Put the loan into the pseudo-account's directory diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.cpp b/src/xrpld/app/tx/detail/VaultWithdraw.cpp index de84a372e7..adab7eb3a6 100644 --- a/src/xrpld/app/tx/detail/VaultWithdraw.cpp +++ b/src/xrpld/app/tx/detail/VaultWithdraw.cpp @@ -245,6 +245,13 @@ VaultWithdraw::doApply() !isTesSuccess(ter) && ter != tecDUPLICATE) return ter; } + else + { + auto dstSle = view().peek(keylet::account(dstAcct)); + if (auto err = verifyDepositPreauth( + ctx_.tx, view(), account_, dstAcct, dstSle, j_)) + return err; + } // Transfer assets from vault to depositor or destination account. if (auto const ter = accountSend( From 3ef65084900b64e8f49f36725447d2df8059d983 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 24 Nov 2025 17:55:29 -0500 Subject: [PATCH 274/291] Fix build error with LoanBroker invariant --- src/xrpld/app/tx/detail/InvariantCheck.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index dd3c605ca9..c5bdbcf6dc 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -2356,7 +2356,7 @@ ValidLoanBroker::visitEntry( { auto const& loanBrokerID = after->at(sfLoanBrokerID); // create an entry if one doesn't already exist - auto& broker = brokers_[loanBrokerID]; + brokers_.emplace(loanBrokerID, BrokerInfo{}); } else if (after->getType() == ltRIPPLE_STATE) { @@ -2437,7 +2437,7 @@ ValidLoanBroker::finalize( { auto const& loanBrokerID = account->at(sfLoanBrokerID); // create an entry if one doesn't already exist - auto& broker = brokers_[loanBrokerID]; + brokers_.emplace(loanBrokerID, BrokerInfo{}); } } } @@ -2451,7 +2451,7 @@ ValidLoanBroker::finalize( { auto const& loanBrokerID = account->at(sfLoanBrokerID); // create an entry if one doesn't already exist - auto& broker = brokers_[loanBrokerID]; + brokers_.emplace(loanBrokerID, BrokerInfo{}); } } From 24586ef3941aaffcfdf9ae596259c0d2fdf961b1 Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Tue, 25 Nov 2025 00:17:34 +0100 Subject: [PATCH 275/291] Add deep freeze check to LoanBrokerDelete (#6053) - Add logging for missing vault - Fix unit test --- src/test/app/LoanBroker_test.cpp | 14 ++++++++++ src/xrpld/app/tx/detail/LoanBrokerDelete.cpp | 29 +++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index ca64b5e0d6..66ce525e8c 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -1063,6 +1063,20 @@ class LoanBroker_test : public beast::unit_test::suite // preclaim: tecHAS_OBLIGATIONS env(del(alice, brokerKeylet.key), ter(tecHAS_OBLIGATIONS)); + + // Repay and delete the loan + auto const loanKeylet = keylet::loan(brokerKeylet.key, 1); + env(loan::pay(borrower, loanKeylet.key, asset(50).value())); + env(loan::del(alice, loanKeylet.key)); + + env(trust(issuer, asset(0), alice, tfSetFreeze | tfSetDeepFreeze)); + // preclaim: tecFROZEN (deep frozen) + env(del(alice, brokerKeylet.key), ter(tecFROZEN)); + env(trust( + issuer, asset(0), alice, tfClearFreeze | tfClearDeepFreeze)); + + // successful delete the loan broker object + env(del(alice, brokerKeylet.key), ter(tesSUCCESS)); } else env(del(alice, brokerKeylet.key)); diff --git a/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp b/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp index 191aef71ec..f3dd781bb5 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp @@ -33,7 +33,10 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx) JLOG(ctx.j.warn()) << "LoanBroker does not exist."; return tecNO_ENTRY; } - if (account != sleBroker->at(sfOwner)) + + auto const brokerOwner = sleBroker->at(sfOwner); + + if (account != brokerOwner) { JLOG(ctx.j.warn()) << "Account is not the owner of the LoanBroker."; return tecNO_PERMISSION; @@ -68,6 +71,30 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx) } } + auto const vault = ctx.view.read(keylet::vault(sleBroker->at(sfVaultID))); + if (!vault) + { + // LCOV_EXCL_START + JLOG(ctx.j.fatal()) << "Vault is missing for Broker " << brokerID; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + + Asset const asset = vault->at(sfAsset); + + auto const coverAvailable = + STAmount{asset, sleBroker->at(sfCoverAvailable)}; + // If there are assets in the cover, broker will receive them on deletion. + // So we need to check if the broker owner is deep frozen for that asset. + if (coverAvailable > beast::zero) + { + if (auto const ret = checkDeepFrozen(ctx.view, brokerOwner, asset)) + { + JLOG(ctx.j.warn()) << "Broker owner account is frozen."; + return ret; + } + } + return tesSUCCESS; } From 005390e26633f7343e2c61f6c14c7b85ed13a7aa Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 24 Nov 2025 19:48:57 -0500 Subject: [PATCH 276/291] Fix formatting --- src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp index 01bf4102ed..4e9e0e9c05 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp @@ -52,7 +52,7 @@ LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx) // LCOV_EXCL_START JLOG(ctx.j.fatal()) << "Vault is missing for Broker " << brokerID; return tefBAD_LEDGER; - // LCOV_EXCL_STOP + // LCOV_EXCL_STOP } auto const vaultAsset = vault->at(sfAsset); From 25e72d7844b4541695a2c00fac81b92d8168dd59 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 24 Nov 2025 21:42:03 -0400 Subject: [PATCH 277/291] Update src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp Co-authored-by: Shawn Xie <35279399+shawnxie999@users.noreply.github.com> --- src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp index b4a71d47d1..566d7a49af 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp @@ -275,7 +275,7 @@ LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx) STAmount const clawAmount = *findClawAmount; // Explicitly check the balance of the trust line / MPT to make sure the - // balance is actually there. It should always match `stCoverAvailable`, so + // balance is actually there. It should always match `sfCoverAvailable`, so // if there isn't, this is an internal error. if (accountHolds( ctx.view, From 71bb08cd78d0718421b68f063ce32ff5bee5f654 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 24 Nov 2025 20:36:57 -0500 Subject: [PATCH 278/291] Review feedback from @Tapanito, @gregtatcam, and @shawnxie999 - Created a common doWithdraw function for VaultWithdraw and LoanBrokerCoverWithdraw. Added verifyDepositPreauth to it, so that both transactions will get the check. - Add a missing null check to LoanBrokerSet, and add log messages to the existing checks. --- include/xrpl/ledger/View.h | 11 ++++ src/libxrpl/ledger/View.cpp | 49 +++++++++++++++++ .../app/tx/detail/LoanBrokerCoverWithdraw.cpp | 44 ++++------------ src/xrpld/app/tx/detail/LoanBrokerSet.cpp | 24 ++++++++- src/xrpld/app/tx/detail/VaultWithdraw.cpp | 52 ++++--------------- 5 files changed, 100 insertions(+), 80 deletions(-) diff --git a/include/xrpl/ledger/View.h b/include/xrpl/ledger/View.h index 3bad22c321..fc16d53e61 100644 --- a/include/xrpl/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -753,6 +753,17 @@ canWithdraw( [[nodiscard]] TER canWithdraw(ReadView const& view, STTx const& tx); +[[nodiscard]] TER +doWithdraw( + ApplyView& view, + STTx const& tx, + AccountID const& senderAcct, + AccountID const& dstAcct, + AccountID const& sourceAcct, + XRPAmount priorBalance, + STAmount const& amount, + beast::Journal j); + /// Any transactors that call addEmptyHolding() in doApply must call /// canAddHolding() in preflight with the same View and Asset [[nodiscard]] TER diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index b3625bc596..a91a0c6468 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -1386,6 +1386,55 @@ canWithdraw(ReadView const& view, STTx const& tx) return canWithdraw(from, view, to, tx.isFieldPresent(sfDestinationTag)); } +TER +doWithdraw( + ApplyView& view, + STTx const& tx, + AccountID const& senderAcct, + AccountID const& dstAcct, + AccountID const& sourceAcct, + XRPAmount priorBalance, + STAmount const& amount, + beast::Journal j) +{ + // Create trust line or MPToken for the receiving account + if (dstAcct == senderAcct) + { + if (auto const ter = addEmptyHolding( + view, senderAcct, priorBalance, amount.asset(), j); + !isTesSuccess(ter) && ter != tecDUPLICATE) + return ter; + } + else + { + auto dstSle = view.peek(keylet::account(dstAcct)); + if (auto err = + verifyDepositPreauth(tx, view, senderAcct, dstAcct, dstSle, j)) + return err; + } + + // Sanity check + if (accountHolds( + view, + sourceAcct, + amount.asset(), + FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, + j) < amount) + { + // LCOV_EXCL_START + JLOG(j.error()) << "LoanBrokerCoverWithdraw: negative balance of " + "broker cover assets."; + return tefINTERNAL; + // LCOV_EXCL_STOP + } + + // Move the funds directly from the broker's pseudo-account to the + // dstAcct + return accountSend( + view, sourceAcct, dstAcct, amount, j, WaiveTransferFee::Yes); +} + [[nodiscard]] TER addEmptyHolding( ApplyView& view, diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp index 2b54e28295..1fd5a1a471 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp @@ -157,41 +157,15 @@ LoanBrokerCoverWithdraw::doApply() broker->at(sfCoverAvailable) -= amount; view().update(broker); - // Create trust line or MPToken for the receiving account - if (dstAcct == account_) - { - if (auto const ter = addEmptyHolding( - view(), account_, mPriorBalance, amount.asset(), j_); - !isTesSuccess(ter) && ter != tecDUPLICATE) - return ter; - } - else - { - auto dstSle = view().peek(keylet::account(dstAcct)); - if (auto err = - verifyDepositPreauth(tx, view(), account_, dstAcct, dstSle, j_)) - return err; - } - - // Sanity check - if (accountHolds( - view(), - brokerPseudoID, - amount.asset(), - FreezeHandling::fhIGNORE_FREEZE, - AuthHandling::ahIGNORE_AUTH, - j_) < amount) - { - // LCOV_EXCL_START - JLOG(j_.error()) << "LoanBrokerCoverWithdraw: negative balance of " - "broker cover assets."; - return tefINTERNAL; - // LCOV_EXCL_STOP - } - - // Move the funds directly from the broker's pseudo-account to the dstAcct - return accountSend( - view(), brokerPseudoID, dstAcct, amount, j_, WaiveTransferFee::Yes); + return doWithdraw( + view(), + tx, + account_, + dstAcct, + brokerPseudoID, + mPriorBalance, + amount, + j_); } //------------------------------------------------------------------------------ diff --git a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp index beb8bf141b..c2e6effd7a 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp @@ -120,7 +120,13 @@ LoanBrokerSet::doApply() // Modify an existing LoanBroker auto broker = view.peek(keylet::loanbroker(*brokerID)); if (!broker) - return tefBAD_LEDGER; // LCOV_EXCL_LINE + { + // This should be impossible + // LCOV_EXCL_START + JLOG(j_.fatal()) << "LoanBroker does not exist."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } if (auto const data = tx[~sfData]) broker->at(sfData) = *data; @@ -134,12 +140,26 @@ LoanBrokerSet::doApply() // Create a new LoanBroker pointing back to the given Vault auto const vaultID = tx[sfVaultID]; auto const sleVault = view.read(keylet::vault(vaultID)); + if (!sleVault) + { + // This should be impossible + // LCOV_EXCL_START + JLOG(j_.fatal()) << "Vault does not exist."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } auto const vaultPseudoID = sleVault->at(sfAccount); auto const sequence = tx.getSeqValue(); auto owner = view.peek(keylet::account(account_)); if (!owner) - return tefBAD_LEDGER; // LCOV_EXCL_LINE + { + // This should be impossible + // LCOV_EXCL_START + JLOG(j_.fatal()) << "Account does not exist."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } auto broker = std::make_shared(keylet::loanbroker(account_, sequence)); diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.cpp b/src/xrpld/app/tx/detail/VaultWithdraw.cpp index adab7eb3a6..99a5f5f721 100644 --- a/src/xrpld/app/tx/detail/VaultWithdraw.cpp +++ b/src/xrpld/app/tx/detail/VaultWithdraw.cpp @@ -237,49 +237,15 @@ VaultWithdraw::doApply() auto const dstAcct = ctx_.tx[~sfDestination].value_or(account_); - // Create trust line or MPToken for the receiving account - if (dstAcct == account_) - { - if (auto const ter = addEmptyHolding( - view(), account_, mPriorBalance, vaultAsset, j_); - !isTesSuccess(ter) && ter != tecDUPLICATE) - return ter; - } - else - { - auto dstSle = view().peek(keylet::account(dstAcct)); - if (auto err = verifyDepositPreauth( - ctx_.tx, view(), account_, dstAcct, dstSle, j_)) - return err; - } - - // Transfer assets from vault to depositor or destination account. - if (auto const ter = accountSend( - view(), - vaultAccount, - dstAcct, - assetsWithdrawn, - j_, - WaiveTransferFee::Yes); - !isTesSuccess(ter)) - return ter; - - // Sanity check - if (accountHolds( - view(), - vaultAccount, - assetsWithdrawn.asset(), - FreezeHandling::fhIGNORE_FREEZE, - AuthHandling::ahIGNORE_AUTH, - j_) < beast::zero) - { - // LCOV_EXCL_START - JLOG(j_.error()) << "VaultWithdraw: negative balance of vault assets."; - return tefINTERNAL; - // LCOV_EXCL_STOP - } - - return tesSUCCESS; + return doWithdraw( + view(), + ctx_.tx, + account_, + dstAcct, + vaultAccount, + mPriorBalance, + assetsWithdrawn, + j_); } } // namespace ripple From dedafbe3268dce10b80e5fc4b49a5f058265020e Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 24 Nov 2025 20:54:31 -0500 Subject: [PATCH 279/291] Review feedback from @gregtatcam: typo --- src/xrpld/app/tx/detail/LoanPay.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index eb1c5adc19..4bc0dc6188 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -413,7 +413,7 @@ LoanPay::doApply() "totalPaidToVaultForDebt rounding good"); // Despite our best efforts, it's possible for rounding errors to accumulate // in the loan broker's debt total. This is because the broker may have more - // that one loan with significantly different scales. + // than one loan with significantly different scales. adjustImpreciseNumber( debtTotalProxy, -totalPaidToVaultForDebt, asset, vaultScale); From b8d830a81294eb9e03c5b54c36ef12a9ba19f1f1 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 24 Nov 2025 21:38:21 -0500 Subject: [PATCH 280/291] Fix formatting --- src/libxrpl/protocol/STTx.cpp | 3 +-- src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index a6f134352e..d3b3ab4a88 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -264,8 +264,7 @@ STTx::checkSign(Rules const& rules) const if (isFieldPresent(sfCounterpartySignature)) { auto const counterSig = getFieldObject(sfCounterpartySignature); - if (auto const ret = checkSign(rules, counterSig); - !ret) + if (auto const ret = checkSign(rules, counterSig); !ret) return Unexpected("Counterparty: " + ret.error()); } return {}; diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp index 566d7a49af..8a1d8eb8f9 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp @@ -230,7 +230,7 @@ LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx) // LCOV_EXCL_START JLOG(ctx.j.fatal()) << "Vault is missing for Broker " << brokerID; return tefBAD_LEDGER; - // LCOV_EXCL_STOP + // LCOV_EXCL_STOP } From 5773f2440a736d6b40145bcd1294190dbf867878 Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Tue, 25 Nov 2025 06:37:04 +0100 Subject: [PATCH 281/291] Add additional documentation to Lending Protocol (#6037) - documents core equations of the lending protocol --- src/xrpld/app/misc/LendingHelpers.h | 232 ++++-- src/xrpld/app/misc/detail/LendingHelpers.cpp | 711 +++++++++++++------ src/xrpld/app/tx/detail/LoanPay.cpp | 4 +- src/xrpld/app/tx/detail/LoanSet.cpp | 4 +- 4 files changed, 681 insertions(+), 270 deletions(-) diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index 93dc2d7f66..559af28a47 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -27,22 +27,54 @@ roundPeriodicPayment( return roundToAsset(asset, periodicPayment, scale, Number::upward); } -/// This structure is explained in the XLS-66 spec, section 3.2.4.4 (Failure -/// Conditions) +/* Represents the breakdown of amounts to be paid and changes applied to the + * Loan object while processing a loan payment. + * + * This structure is returned after processing a loan payment transaction and + * captures the amounts that need to be paid. The actual ledger entry changes + * are made in LoanPay based on this structure values. + * + * The sum of principalPaid, interestPaid, and feePaid represents the total + * amount to be deducted from the borrower's account. The valueChange field + * tracks whether the loan's total value increased or decreased beyond normal + * amortization. + * + * This structure is explained in the XLS-66 spec, section 3.2.4.2 (Payment + * Processing). + */ struct LoanPaymentParts { - /// principal_paid is the amount of principal that the payment covered. + // The amount of principal paid that reduces the loan balance. + // This amount is subtracted from sfPrincipalOutstanding in the Loan object + // and paid to the Vault Number principalPaid = numZero; - /// interest_paid is the amount of interest that the payment covered. + + // The total amount of interest paid to the Vault. + // This includes: + // - Tracked interest from the amortization schedule + // - Untracked interest (e.g., late payment penalty interest) + // This value is always non-negative. Number interestPaid = numZero; - /** - * value_change is the amount by which the total value of the Loan changed. - * If value_change < 0, Loan value decreased. - * If value_change > 0, Loan value increased. - * This is 0 for regular payments. - */ + + // The change in the loan's total value outstanding. + // - If valueChange < 0: Loan value decreased + // - If valueChange > 0: Loan value increased + // - If valueChange = 0: No value adjustment + // + // For regular on-time payments, this is always 0. Non-zero values occur + // when: + // - Overpayments reduce the loan balance beyond the scheduled amount + // - Late payments add penalty interest to the loan value + // - Early full payment may increase or decrease the loan value based on + // terms Number valueChange = numZero; - /// feePaid is amount of fee that is paid to the broker + + /* The total amount of fees paid to the Broker. + * This includes: + * - Tracked management fees from the amortization schedule + * - Untracked fees (e.g., late payment fees, service fees, origination + * fees) This value is always non-negative. + */ Number feePaid = numZero; LoanPaymentParts& @@ -52,46 +84,71 @@ struct LoanPaymentParts operator==(LoanPaymentParts const& other) const; }; -/** This structure describes the initial "computed" properties of a loan. +/* Describes the initial computed properties of a loan. * - * It is used at loan creation and when the terms of a loan change, such as - * after an overpayment. + * This structure contains the fundamental calculated values that define a + * loan's payment structure and amortization schedule. These properties are + * computed: + * - At loan creation (LoanSet transaction) + * - When loan terms change (e.g., after an overpayment that reduces the loan + * balance) */ struct LoanProperties { + // The unrounded amount to be paid at each regular payment period. + // Calculated using the standard amortization formula based on principal, + // interest rate, and number of payments. + // The actual amount paid in the LoanPay transaction must be rounded up to + // the precision of the asset and loan. Number periodicPayment; + + // The total amount the borrower will pay over the life of the loan. + // Equal to periodicPayment * paymentsRemaining. + // This includes principal, interest, and management fees. Number totalValueOutstanding; + + // The total management fee that will be paid to the broker over the + // loan's lifetime. This is a percentage of the total interest (gross) + // as specified by the broker's management fee rate. Number managementFeeOwedToBroker; + + // The scale (decimal places) used for rounding all loan amounts. + // This is the maximum of: + // - The asset's native scale + // - A minimum scale required to represent the periodic payment accurately + // All loan state values (principal, interest, fees) are rounded to this + // scale. std::int32_t loanScale; + + // The principal portion of the first payment. Number firstPaymentPrincipal; }; -/** This structure captures the current state of a loan and all the - relevant parts. - - Whether the values are raw (unrounded) or rounded will - depend on how it was computed. - - Many of the fields can be derived from each other, but they're all provided - here to reduce code duplication and possible mistakes. - e.g. - * interestOutstanding = valueOutstanding - principalOutstanding - * interestDue = interestOutstanding - managementFeeDue +/** This structure captures the parts of a loan state. + * + * Whether the values are raw (unrounded) or rounded will depend on how it was + * computed. + * + * Many of the fields can be derived from each other, but they're all provided + * here to reduce code duplication and possible mistakes. + * e.g. + * * interestOutstanding = valueOutstanding - principalOutstanding + * * interestDue = interestOutstanding - managementFeeDue */ struct LoanState { - /// Total value still due to be paid by the borrower. + // Total value still due to be paid by the borrower. Number valueOutstanding; - /// Prinicipal still due to be paid by the borrower. + // Principal still due to be paid by the borrower. Number principalOutstanding; - /// Interest still due to be paid TO the Vault. + // Interest still due to be paid to the Vault. // This is a portion of interestOutstanding Number interestDue; - /// Management fee still due to be paid TO the broker. + // Management fee still due to be paid to the broker. // This is a portion of interestOutstanding Number managementFeeDue; - /// Interest still due to be paid by the borrower. + // Interest still due to be paid by the borrower. Number interestOutstanding() const { @@ -199,42 +256,133 @@ namespace detail { enum class PaymentSpecialCase { none, final, extra }; -/// This structure is used internally to compute the breakdown of a -/// single loan payment +/* Represents a single loan payment component parts. + +* This structure captures the "delta" (change) values that will be applied to +* the tracked fields in the Loan ledger object when a payment is processed. +* +* These are called "deltas" because they represent the amount by which each +* corresponding field in the Loan object will be reduced. +* They are "tracked" as they change tracked loan values. +*/ struct PaymentComponents { - // tracked values are rounded to the asset and loan scale, and correspond to - // fields in the Loan ledger object. - // trackedValueDelta modifies sfTotalValueOutstanding. + // The change in total value outstanding for this payment. + // This amount will be subtracted from sfTotalValueOutstanding in the Loan + // object. Equal to the sum of trackedPrincipalDelta, + // trackedInterestPart(), and trackedManagementFeeDelta. Number trackedValueDelta; - // trackedPrincipalDelta modifies sfPrincipalOutstanding. + + // The change in principal outstanding for this payment. + // This amount will be subtracted from sfPrincipalOutstanding in the Loan + // object, representing the portion of the payment that reduces the + // original loan amount. Number trackedPrincipalDelta; - // trackedManagementFeeDelta modifies sfManagementFeeOutstanding. It will - // not include any "extra" fees that go directly to the broker, such as late - // fees. + + // The change in management fee outstanding for this payment. + // This amount will be subtracted from sfManagementFeeOutstanding in the + // Loan object. This represents only the tracked management fees from the + // amortization schedule and does not include additional untracked fees + // (such as late payment fees) that go directly to the broker. Number trackedManagementFeeDelta; + // Indicates if this payment has special handling requirements. + // - none: Regular scheduled payment + // - final: The last payment that closes out the loan + // - extra: An additional payment beyond the regular schedule (overpayment) PaymentSpecialCase specialCase = PaymentSpecialCase::none; + // Calculates the tracked interest portion of this payment. + // This is derived from the other components as: + // trackedValueDelta - trackedPrincipalDelta - trackedManagementFeeDelta + // + // @return The amount of tracked interest included in this payment that + // will be paid to the vault. Number trackedInterestPart() const; }; -// This structure describes the difference between two LoanState objects so that -// the differences between components don't have to be tracked individually, -// risking more errors. How that difference is used depends on the context. +/* Extends PaymentComponents with untracked payment amounts. + * + * This structure adds untracked fees and interest to the base + * PaymentComponents, representing amounts that don't affect the Loan object's + * tracked state but are still part of the total payment due from the borrower. + * + * Untracked amounts include: + * - Late payment fees that go directly to the Broker + * - Late payment penalty interest that goes directly to the Vault + * - Service fees + * + * The key distinction is that tracked amounts reduce the Loan object's state + * (sfTotalValueOutstanding, sfPrincipalOutstanding, + * sfManagementFeeOutstanding), while untracked amounts are paid directly to the + * recipient without affecting the loan's amortization schedule. + */ +struct ExtendedPaymentComponents : public PaymentComponents +{ + // Additional management fees that go directly to the Broker. + // This includes fees not part of the standard amortization schedule + // (e.g., late fees, service fees, origination fees). + // This value may be negative, though the final value returned in + // LoanPaymentParts.feePaid will never be negative. + Number untrackedManagementFee; + + // Additional interest that goes directly to the Vault. + // This includes interest not part of the standard amortization schedule + // (e.g., late payment penalty interest). + // This value may be negative, though the final value returned in + // LoanPaymentParts.interestPaid will never be negative. + Number untrackedInterest; + + // The complete amount due from the borrower for this payment. + // Calculated as: trackedValueDelta + untrackedInterest + + // untrackedManagementFee + // + // This value is used to validate that the payment amount provided by the + // borrower is sufficient to cover all components of the payment. + Number totalDue; + + ExtendedPaymentComponents( + PaymentComponents const& p, + Number fee, + Number interest = numZero) + : PaymentComponents(p) + , untrackedManagementFee(fee) + , untrackedInterest(interest) + , totalDue( + trackedValueDelta + untrackedInterest + untrackedManagementFee) + { + } +}; + +/* Represents the differences between two loan states. + * + * This structure is used to capture the change in each component of a loan's + * state, typically when computing the difference between two LoanState objects + * (e.g., before and after a payment). It is a convenient way to capture changes + * in each component. How that difference is used depends on the context. + */ struct LoanStateDeltas { + // The difference in principal outstanding between two loan states. Number principal; + + // The difference in interest due between two loan states. Number interest; + + // The difference in management fee outstanding between two loan states. Number managementFee; + /* Calculates the total change across all components. + * @return The sum of principal, interest, and management fee deltas. + */ Number total() const { return principal + interest + managementFee; } + // Ensures all delta values are non-negative. void nonNegative(); }; diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index 8ba49c787c..8020b47ba9 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -1,5 +1,5 @@ #include -// +// DO NOT REMOVE forces header file include to sort first #include namespace ripple { @@ -43,20 +43,23 @@ LoanPaymentParts::operator==(LoanPaymentParts const& other) const valueChange == other.valueChange && feePaid == other.feePaid; } +/* Converts annualized interest rate to per-payment-period rate. + * The rate is prorated based on the payment interval in seconds. + * + * Equation (1) from XLS-66 spec, Section A-2 Equation Glossary + */ Number loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval) { - // Need floating point math for this one, since we're dividing by some - // large numbers - /* - * This formula is from the XLS-66 spec, section 3.2.4.1.1 (Regular - * Payment), specifically "periodicRate = ...", though it is duplicated in - * other places. - */ + // Need floating point math, since we're dividing by a large number return tenthBipsOfValue(Number(paymentInterval), interestRate) / secondsInYear; } +/* Checks if a value is already rounded to the specified scale. + * Returns true if rounding down and rounding up produce the same result, + * indicating no further precision exists beyond the scale. + */ bool isRounded(Asset const& asset, Number const& value, std::int32_t scale) { @@ -66,37 +69,52 @@ isRounded(Asset const& asset, Number const& value, std::int32_t scale) namespace detail { +void +LoanStateDeltas::nonNegative() +{ + if (principal < beast::zero) + principal = numZero; + if (interest < beast::zero) + interest = numZero; + if (managementFee < beast::zero) + managementFee = numZero; +} + +/* Computes (1 + periodicRate)^paymentsRemaining for amortization calculations. + * + * Equation (5) from XLS-66 spec, Section A-2 Equation Glossary + */ Number computeRaisedRate(Number const& periodicRate, std::uint32_t paymentsRemaining) { - /* - * This formula is from the XLS-66 spec, section 3.2.4.1.1 (Regular - * Payment), though "raisedRate" is computed only once and used twice. - */ return power(1 + periodicRate, paymentsRemaining); } +/* Computes the payment factor used in standard amortization formulas. + * This factor converts principal to periodic payment amount. + * + * Equation (6) from XLS-66 spec, Section A-2 Equation Glossary + */ Number computePaymentFactor( Number const& periodicRate, std::uint32_t paymentsRemaining) { - // periodicRate should never be zero in this function, but if it is, - // then 1/paymentRemaining is the most accurate factor that avoids - // divide by 0. + // For zero interest, payment factor is simply 1/paymentsRemaining if (periodicRate == beast::zero) return Number{1} / paymentsRemaining; - /* - * This formula is from the XLS-66 spec, section 3.2.4.1.1 (Regular - * Payment), though "raisedRate" is computed only once and used twice. - */ Number const raisedRate = computeRaisedRate(periodicRate, paymentsRemaining); return (periodicRate * raisedRate) / (raisedRate - 1); } +/* Calculates the periodic payment amount using standard amortization formula. + * For interest-free loans, returns principal divided equally across payments. + * + * Equation (7) from XLS-66 spec, Section A-2 Equation Glossary + */ Number loanPeriodicPayment( Number const& principalOutstanding, @@ -106,18 +124,19 @@ loanPeriodicPayment( if (principalOutstanding == 0 || paymentsRemaining == 0) return 0; - // Special case for interest free loans - equal payments of the principal. + // Interest-free loans: equal principal payments if (periodicRate == beast::zero) return principalOutstanding / paymentsRemaining; - /* - * This formula is from the XLS-66 spec, section 3.2.4.1.1 (Regular - * Payment). - */ return principalOutstanding * computePaymentFactor(periodicRate, paymentsRemaining); } +/* Calculates the periodic payment amount from annualized interest rate. + * Converts the annual rate to periodic rate before computing payment. + * + * Equation (7) from XLS-66 spec, Section A-2 Equation Glossary + */ Number loanPeriodicPayment( Number const& principalOutstanding, @@ -127,16 +146,18 @@ loanPeriodicPayment( { if (principalOutstanding == 0 || paymentsRemaining == 0) return 0; - /* - * This function is derived from the XLS-66 spec, section 3.2.4.1.1 (Regular - * payment), though it is duplicated in other places. - */ + Number const periodicRate = loanPeriodicRate(interestRate, paymentInterval); return loanPeriodicPayment( principalOutstanding, periodicRate, paymentsRemaining); } +/* Reverse-calculates principal from periodic payment amount. + * Used to determine theoretical principal at any point in the schedule. + * + * Equation (10) from XLS-66 spec, Section A-2 Equation Glossary + */ Number loanPrincipalFromPeriodicPayment( Number const& periodicPayment, @@ -146,14 +167,15 @@ loanPrincipalFromPeriodicPayment( if (periodicRate == 0) return periodicPayment * paymentsRemaining; - /* - * This formula is the reverse of the one from the XLS-66 spec, - * section 3.2.4.1.1 (Regular Payment) used in loanPeriodicPayment - */ return periodicPayment / computePaymentFactor(periodicRate, paymentsRemaining); } +/* Splits gross interest into net interest (to vault) and management fee (to + * broker). Returns pair of (net interest, management fee). + * + * Equation (33) from XLS-66 spec, Section A-2 Equation Glossary + */ std::pair computeInterestAndFeeParts( Number const& interest, @@ -161,10 +183,32 @@ computeInterestAndFeeParts( { auto const fee = tenthBipsOfValue(interest, managementFeeRate); - // No error tracking needed here because this is extra return std::make_pair(interest - fee, fee); } +/* + * Computes the interest and management fee parts from interest amount. + * + * Equation (33) from XLS-66 spec, Section A-2 Equation Glossary + */ +std::pair +computeInterestAndFeeParts( + Asset const& asset, + Number const& interest, + TenthBips16 managementFeeRate, + std::int32_t loanScale) +{ + auto const fee = + computeManagementFee(asset, interest, managementFeeRate, loanScale); + + return std::make_pair(interest - fee, fee); +} + +/* Calculates penalty interest accrued on overdue payments. + * Returns 0 if payment is not late. + * + * Equation (16) from XLS-66 spec, Section A-2 Equation Glossary + */ Number loanLatePaymentInterest( Number const& principalOutstanding, @@ -172,13 +216,6 @@ loanLatePaymentInterest( NetClock::time_point parentCloseTime, std::uint32_t nextPaymentDueDate) { - /* - * This formula is from the XLS-66 spec, section 3.2.4.1.2 (Late payment), - * specifically "latePaymentInterest = ..." - * - * The spec is to be updated to base the duration on the next due date - */ - auto const now = parentCloseTime.time_since_epoch().count(); // If the payment is not late by any amount of time, then there's no late @@ -186,6 +223,7 @@ loanLatePaymentInterest( if (now <= nextPaymentDueDate) return 0; + // Equation (3) from XLS-66 spec, Section A-2 Equation Glossary auto const secondsOverdue = now - nextPaymentDueDate; auto const rate = loanPeriodicRate(lateInterestRate, secondsOverdue); @@ -193,6 +231,11 @@ loanLatePaymentInterest( return principalOutstanding * rate; } +/* Calculates interest accrued since the last payment based on time elapsed. + * Returns 0 if loan is paid ahead of schedule. + * + * Equation (27) from XLS-66 spec, Section A-2 Equation Glossary + */ Number loanAccruedInterest( Number const& principalOutstanding, @@ -202,10 +245,6 @@ loanAccruedInterest( std::uint32_t prevPaymentDate, std::uint32_t paymentInterval) { - /* - * This formula is from the XLS-66 spec, section 3.2.4.1.4 (Early Full - * Repayment), specifically "accruedInterest = ...". - */ if (periodicRate == beast::zero) return numZero; @@ -217,38 +256,25 @@ loanAccruedInterest( if (now <= lastPaymentDate) return numZero; + // Equation (4) from XLS-66 spec, Section A-2 Equation Glossary auto const secondsSinceLastPayment = now - lastPaymentDate; + // Division is more likely to introduce rounding errors, which will then get + // amplified by multiplication. Therefore, we first multiply, and only then + // divide. return principalOutstanding * periodicRate * secondsSinceLastPayment / paymentInterval; } -struct ExtendedPaymentComponents : public PaymentComponents -{ - // untrackedManagementFeeDelta includes any fees that go directly to the - // Broker, such as late fees. This value may be negative, though the final - // value returned in LoanPaymentParts.feePaid will never be negative. - Number untrackedManagementFee; - // untrackedInterest includes any fees that go directly to the Vault, such - // as late payment penalty interest. This value may be negative, though the - // final value returned in LoanPaymentParts.interestPaid will never be - // negative. - Number untrackedInterest; - Number totalDue; - - ExtendedPaymentComponents( - PaymentComponents const& p, - Number f, - Number v = numZero) - : PaymentComponents(p) - , untrackedManagementFee(f) - , untrackedInterest(v) - , totalDue( - trackedValueDelta + untrackedInterest + untrackedManagementFee) - { - } -}; +/* Applies a payment to the loan state and returns the breakdown of amounts + * paid. + * + * This is the core function that updates the Loan ledger object fields based on + * a computed payment. + * The function is templated to work with both direct Number/uint32_t values + * (for testing/simulation) and ValueProxy types (for actual ledger updates). + */ template LoanPaymentParts doPayment( @@ -281,20 +307,26 @@ doPayment( "ripple::detail::doPayment", "Full management fee payment"); + // Mark the loan as complete paymentRemainingProxy = 0; + // Record when the final payment was made prevPaymentDateProxy = *nextDueDateProxy; - // Zero out the next due date. Since it's default, it'll be removed from - // the object. + + // Clear the next due date. Setting it to 0 causes + // it to be removed from the Loan ledger object, saving space. nextDueDateProxy = 0; - // Always zero out the the tracked values on a final payment + // Zero out all tracked loan balances to mark the loan as paid off. + // These will be removed from the Loan object since they're default + // values. principalOutstandingProxy = 0; totalValueOutstandingProxy = 0; managementFeeOutstandingProxy = 0; } else { + // For regular payments (not overpayments), advance the payment schedule if (payment.specialCase != PaymentSpecialCase::extra) { paymentRemainingProxy -= 1; @@ -317,11 +349,13 @@ doPayment( "ripple::detail::doPayment", "Valid management fee"); + // Apply the payment deltas to reduce the outstanding balances principalOutstandingProxy -= payment.trackedPrincipalDelta; totalValueOutstandingProxy -= payment.trackedValueDelta; managementFeeOutstandingProxy -= payment.trackedManagementFeeDelta; } + // Principal can never exceed total value (principal is part of total value) XRPL_ASSERT_PARTS( // Use an explicit cast because the template parameter can be // ValueProxy or Number @@ -329,6 +363,7 @@ doPayment( static_cast(totalValueOutstandingProxy), "ripple::detail::doPayment", "principal does not exceed total"); + XRPL_ASSERT_PARTS( // Use an explicit cast because the template parameter can be // ValueProxy or Number @@ -337,22 +372,37 @@ doPayment( "fee outstanding stays valid"); return LoanPaymentParts{ + // Principal paid is straightforward - it's the tracked delta .principalPaid = payment.trackedPrincipalDelta, - // Now that the Loan object has been updated, the tracked interest - // (computed here) and untracked interest can be combined. + + // Interest paid combines: + // 1. Tracked interest from the amortization schedule + // (derived from the tracked deltas) + // 2. Untracked interest (e.g., late payment penalties) .interestPaid = payment.trackedInterestPart() + payment.untrackedInterest, + + // Value change represents how the loan's total value changed beyond + // normal amortization. .valueChange = payment.untrackedInterest, - // Now that the Loan object has been updated, the fee parts can be - // combined + + // Fee paid combines: + // 1. Tracked management fees from the amortization schedule + // 2. Untracked fees (e.g., late payment fees, service fees) .feePaid = payment.trackedManagementFeeDelta + payment.untrackedManagementFee}; } -// This function mainly exists to guarantee isolation of the "sandbox" -// variables from the real / proxy variables that will affect actual -// ledger data in the caller. - +/* Simulates an overpayment to validate it won't break the loan's amortization. + * + * When a borrower pays more than the scheduled amount, the loan needs to be + * re-amortized with a lower principal. This function performs that calculation + * in a "sandbox" using temporary variables, allowing the caller to validate + * the result before committing changes to the actual ledger. + * + * The function preserves accumulated rounding errors across the re-amortization + * to ensure the loan state remains consistent with its payment history. + */ Expected tryOverpayment( Asset const& asset, @@ -371,17 +421,29 @@ tryOverpayment( TenthBips16 const managementFeeRate, beast::Journal j) { + // Calculate what the loan state SHOULD be theoretically (at full precision) auto const raw = computeRawLoanState( periodicPayment, periodicRate, paymentRemaining, managementFeeRate); + + // Get the actual loan state (with accumulated rounding from past payments) auto const rounded = constructLoanState( totalValueOutstanding, principalOutstanding, managementFeeOutstanding); + // Calculate the accumulated rounding errors. These need to be preserved + // across the re-amortization to maintain consistency with the loan's + // payment history. Without preserving these errors, the loan could end + // up with a different total value than what the borrower has actually paid. auto const errors = rounded - raw; + // Compute the new principal by applying the overpayment to the raw + // (theoretical) principal. Use max with 0 to ensure we never go negative. auto const newRawPrincipal = std::max( raw.principalOutstanding - overpaymentComponents.trackedPrincipalDelta, Number{0}); + // Compute new loan properties based on the reduced principal. This + // recalculates the periodic payment, total value, and management fees + // for the remaining payment schedule. auto newLoanProperties = computeLoanProperties( asset, newRawPrincipal, @@ -398,6 +460,7 @@ tryOverpayment( << ", first payment principal: " << newLoanProperties.firstPaymentPrincipal; + // Calculate what the new loan state should be with the new periodic payment auto const newRaw = computeRawLoanState( newLoanProperties.periodicPayment, periodicRate, @@ -408,6 +471,9 @@ tryOverpayment( JLOG(j.debug()) << "new raw value: " << newRaw.valueOutstanding << ", principal: " << newRaw.principalOutstanding << ", interest gross: " << newRaw.interestOutstanding(); + // Update the loan state variables with the new values PLUS the preserved + // rounding errors. This ensures the loan's tracked state remains + // consistent with its payment history. principalOutstanding = std::clamp( roundToAsset( @@ -430,12 +496,15 @@ tryOverpayment( auto const newRounded = constructLoanState( totalValueOutstanding, principalOutstanding, managementFeeOutstanding); + // Update newLoanProperties so that checkLoanGuards can make an accurate + // evaluation. newLoanProperties.totalValueOutstanding = newRounded.valueOutstanding; JLOG(j.debug()) << "new rounded value: " << newRounded.valueOutstanding << ", principal: " << newRounded.principalOutstanding << ", interest gross: " << newRounded.interestOutstanding(); + // Update the periodic payment to reflect the re-amortized schedule periodicPayment = newLoanProperties.periodicPayment; // check that the loan is still valid @@ -453,10 +522,12 @@ tryOverpayment( { JLOG(j.warn()) << "Principal overpayment would cause the loan to be in " "an invalid state. Ignore the overpayment"; + return Unexpected(tesSUCCESS); } - // Check that the other computed values are valid + // Validate that all computed properties are reasonable. These checks should + // never fail under normal circumstances, but we validate defensively. if (newLoanProperties.periodicPayment <= 0 || newLoanProperties.totalValueOutstanding <= 0 || newLoanProperties.managementFeeOwedToBroker < 0) @@ -479,6 +550,9 @@ tryOverpayment( auto const hypotheticalValueOutstanding = rounded.valueOutstanding - deltas.principal; + // Calculate how the loan's value changed due to the overpayment. + // This should be negative (value decreased) or zero. A principal + // overpayment should never increase the loan's value. auto const valueChange = newRounded.valueOutstanding - hypotheticalValueOutstanding; if (valueChange > 0) @@ -489,15 +563,33 @@ tryOverpayment( } return LoanPaymentParts{ + // Principal paid is the reduction in principal outstanding .principalPaid = deltas.principal, + // Interest paid is the reduction in interest due .interestPaid = deltas.interest + overpaymentComponents.untrackedInterest, + // Value change includes both the reduction from paying down principal + // (negative) and any untracked interest penalties (positive, e.g., if + // the overpayment itself incurs a fee) .valueChange = valueChange + overpaymentComponents.trackedInterestPart(), + // Fee paid includes both the reduction in tracked management fees and + // any untracked fees on the overpayment itself .feePaid = deltas.managementFee + overpaymentComponents.untrackedManagementFee}; } +/* Validates and applies an overpayment to the loan state. + * + * This function acts as a wrapper around tryOverpayment(), performing the + * re-amortization calculation in a sandbox (using temporary copies of the + * loan state), then validating the results before committing them to the + * actual ledger via the proxy objects. + * + * The two-step process (try in sandbox, then commit) ensures that if the + * overpayment would leave the loan in an invalid state, we can reject it + * gracefully without corrupting the ledger data. + */ template Expected doOverpayment( @@ -517,8 +609,9 @@ doOverpayment( TenthBips16 const managementFeeRate, beast::Journal j) { - // Use temp variables to do the payment, so they can be thrown away if - // they don't work + // Create temporary copies of the loan state that can be safely modified + // and discarded if the overpayment doesn't work out. This prevents + // corrupting the actual ledger data if validation fails. Number totalValueOutstanding = totalValueOutstandingProxy; Number principalOutstanding = principalOutstandingProxy; Number managementFeeOutstanding = managementFeeOutstandingProxy; @@ -535,6 +628,9 @@ doOverpayment( << ", untrackedInterest: " << overpaymentComponents.untrackedInterest << ", totalDue: " << overpaymentComponents.totalDue << ", payments remaining :" << paymentRemaining; + + // Attempt to re-amortize the loan with the overpayment applied. + // This modifies the temporary copies, leaving the proxies unchanged. auto const ret = tryOverpayment( asset, loanScale, @@ -556,6 +652,9 @@ doOverpayment( auto const& loanPaymentParts = *ret; + // Safety check: the principal must have decreased. If it didn't (or + // increased!), something went wrong in the calculation and we should + // reject the overpayment. if (principalOutstandingProxy <= principalOutstanding) { // LCOV_EXCL_START @@ -567,8 +666,10 @@ doOverpayment( // LCOV_EXCL_STOP } - // We haven't updated the proxies yet, so they still have the original - // values. Use those to do some checks. + // The proxies still hold the original (pre-overpayment) values, which + // allows us to compute deltas and verify they match what we expect + // from the overpaymentComponents and loanPaymentParts. + XRPL_ASSERT_PARTS( overpaymentComponents.trackedPrincipalDelta == principalOutstandingProxy - principalOutstanding, @@ -618,7 +719,8 @@ doOverpayment( "ripple::detail::doOverpayment", "fee payment matches"); - // Update the loan object (via proxies) + // All validations passed, so update the proxy objects (which will + // modify the actual Loan ledger object) totalValueOutstandingProxy = totalValueOutstanding; principalOutstandingProxy = principalOutstanding; managementFeeOutstandingProxy = managementFeeOutstanding; @@ -627,33 +729,19 @@ doOverpayment( return loanPaymentParts; } -std::pair -computeInterestAndFeeParts( - Asset const& asset, - Number const& interest, - TenthBips16 managementFeeRate, - std::int32_t loanScale) -{ - auto const fee = - computeManagementFee(asset, interest, managementFeeRate, loanScale); - - return std::make_pair(interest - fee, fee); -} - -/** Handle possible late payments. +/* Computes the payment components for a late payment. * - * If this function processed a late payment, the return value will be - * a LoanPaymentParts object. If the loan is not late, the return will be an - * Unexpected(tesSUCCESS). Otherwise, it'll be an Unexpected with the error code - * the caller is expected to return. + * A late payment is made after the grace period has expired and includes: + * 1. All components of a regular periodic payment + * 2. Late payment penalty interest (accrued since the due date) + * 3. Late payment fee charged by the broker * + * The late penalty interest increases the loan's total value (the borrower + * owes more than scheduled), while the regular payment components follow + * the normal amortization schedule. * - * This function is an implementation of the XLS-66 spec, based on - * * section 3.2.4.3 (Transaction Pseudo-code), specifically the bit - * labeled "the payment is late" - * * section 3.2.4.1.2 (Late Payment) + * Implements equation (15) from XLS-66 spec, Section A-2 Equation Glossary */ - Expected computeLatePayment( Asset const& asset, @@ -668,18 +756,21 @@ computeLatePayment( TenthBips16 managementFeeRate, beast::Journal j) { + // Check if the due date has passed. If not, reject the payment as + // being too soon if (!hasExpired(view, nextDueDate)) return Unexpected(tecTOO_SOON); - // the payment is late - // Late payment interest is only the part of the interest that comes - // from being late, as computed by 3.2.4.1.2. + // Calculate the penalty interest based on how long the payment is overdue. auto const latePaymentInterest = loanLatePaymentInterest( principalOutstanding, lateInterestRate, view.parentCloseTime(), nextDueDate); + // Round the late interest and split it between the vault (net interest) + // and the broker (management fee portion). This lambda ensures we + // round before splitting to maintain precision. auto const [roundedLateInterest, roundedLateManagementFee] = [&]() { auto const interest = roundToAsset(asset, latePaymentInterest, loanScale); @@ -694,18 +785,29 @@ computeLatePayment( periodic.specialCase != PaymentSpecialCase::extra, "ripple::detail::computeLatePayment", "no extra parts to this payment"); - // Copy the periodic payment values, and add on the late interest. - // This preserves all the other fields without having to enumerate them. + + // Create the late payment components by copying the regular periodic + // payment and adding the late penalties. We use a lambda to construct + // this to keep the logic clear. This preserves all the other fields without + // having to enumerate them. + ExtendedPaymentComponents const late = [&]() { auto inner = periodic; return ExtendedPaymentComponents{ inner, - // A late payment pays both the normal fee, and the extra fees + // Untracked management fee includes: + // 1. Regular service fee (from periodic.untrackedManagementFee) + // 2. Late payment fee (fixed penalty) + // 3. Management fee portion of late interest periodic.untrackedManagementFee + latePaymentFee + roundedLateManagementFee, - // A late payment increases the value of the loan by the difference - // between periodic and late payment interest + + // Untracked interest includes: + // 1. Any untracked interest from the regular payment (usually 0) + // 2. Late penalty interest (increases loan value) + // This positive value indicates the loan's value increased due + // to the late payment. periodic.untrackedInterest + roundedLateInterest}; }(); @@ -714,6 +816,9 @@ computeLatePayment( "ripple::detail::computeLatePayment", "total due is rounded"); + // Check that the borrower provided enough funds to cover the late payment. + // The late payment is more expensive than a regular payment due to the + // penalties. if (amount < late.totalDue) { JLOG(j.warn()) << "Late loan payment amount is insufficient. Due: " @@ -724,14 +829,25 @@ computeLatePayment( return late; } -/* Handle possible full payments. +/* Computes payment components for paying off a loan early (before final + * payment). * - * If this function processed a full payment, the return value will be - * an ExtendedPaymentComponents object. Otherwise, it'll be an Unexpected with - * the error code the caller is expected to return. It should NEVER return - * tesSUCCESS + * A full payment closes the loan immediately, paying off all outstanding + * balances plus a prepayment penalty and any accrued interest since the last + * payment. This is different from the final scheduled payment, which has no + * prepayment penalty. + * + * The function calculates: + * - Accrued interest since last payment (time-based) + * - Prepayment penalty (percentage of remaining principal) + * - Close payment fee (fixed fee for early closure) + * - All remaining principal and outstanding fees + * + * The loan's value may increase or decrease depending on whether the prepayment + * penalty exceeds the scheduled interest that would have been paid. + * + * Implements equation (26) from XLS-66 spec, Section A-2 Equation Glossary */ - Expected computeFullPayment( Asset const& asset, @@ -752,6 +868,7 @@ computeFullPayment( TenthBips16 managementFeeRate, beast::Journal j) { + // Full payment must be made before the final scheduled payment. if (paymentRemaining <= 1) { // If this is the last payment, it has to be a regular payment @@ -759,11 +876,14 @@ computeFullPayment( return Unexpected(tecKILLED); } + // Calculate the theoretical principal based on the payment schedule. + // This raw (unrounded) value is used to compute interest and penalties + // accurately. Number const rawPrincipalOutstanding = loanPrincipalFromPeriodicPayment( periodicPayment, periodicRate, paymentRemaining); - // Full payment interest consists of accrued normal interest and the - // prepayment penalty, as computed by 3.2.4.1.4. + // Full payment interest includes both accrued interest (time since last + // payment) and prepayment penalty (for closing early). auto const fullPaymentInterest = computeFullPaymentInterest( rawPrincipalOutstanding, periodicRate, @@ -773,36 +893,49 @@ computeFullPayment( startDate, closeInterestRate); + // Split the full payment interest into net interest (to vault) and + // management fee (to broker), applying proper rounding. auto const [roundedFullInterest, roundedFullManagementFee] = [&]() { auto const interest = roundToAsset( asset, fullPaymentInterest, loanScale, Number::downward); auto const parts = computeInterestAndFeeParts( asset, interest, managementFeeRate, loanScale); - // Apply as much of the fee to the outstanding fee, but no - // more return std::make_tuple(parts.first, parts.second); }(); ExtendedPaymentComponents const full{ PaymentComponents{ + // Pay off all tracked outstanding balances: principal, interest, + // and fees. + // This marks the loan as complete (final payment). .trackedValueDelta = principalOutstanding + totalInterestOutstanding + managementFeeOutstanding, .trackedPrincipalDelta = principalOutstanding, - // to make the accounting work later, the tracked part of the fee - // must be paid in full + + // All outstanding management fees are paid. This zeroes out the + // tracked fee balance. .trackedManagementFeeDelta = managementFeeOutstanding, - .specialCase = PaymentSpecialCase::final}, - // A full payment pays the single close payment fee, plus the computed - // management fee part of the interest portion, but for tracking, the - // outstanding part is removed. That could make this value negative, but - // that's ok, because it's not used until it's recombined with - // roundedManagementFee. + .specialCase = PaymentSpecialCase::final, + }, + + // Untracked management fee includes: + // 1. Close payment fee (fixed fee for early closure) + // 2. Management fee on the full payment interest + // 3. Minus the outstanding tracked fee (already accounted for above) + // This can be negative because the outstanding fee is subtracted, but + // it gets combined with trackedManagementFeeDelta in the final + // accounting. closePaymentFee + roundedFullManagementFee - managementFeeOutstanding, - // A full payment changes the value of the loan by the difference - // between expected outstanding interest return and the actual interest - // paid. This value can be positive (increasing the value) or negative - // (decreasing the value). - roundedFullInterest - totalInterestOutstanding}; + + // Value change represents the difference between what the loan was + // expected to earn (totalInterestOutstanding) and what it actually + // earns (roundedFullInterest with prepayment penalty). + // - Positive: Prepayment penalty exceeds scheduled interest (loan value + // increases) + // - Negative: Prepayment penalty is less than scheduled interest (loan + // value decreases) + roundedFullInterest - totalInterestOutstanding, + }; XRPL_ASSERT_PARTS( isRounded(asset, full.totalDue, loanScale), @@ -834,17 +967,22 @@ PaymentComponents::trackedInterestPart() const (trackedPrincipalDelta + trackedManagementFeeDelta); } -void -LoanStateDeltas::nonNegative() -{ - if (principal < beast::zero) - principal = numZero; - if (interest < beast::zero) - interest = numZero; - if (managementFee < beast::zero) - managementFee = numZero; -} - +/* Computes the breakdown of a regular periodic payment into principal, + * interest, and management fee components. + * + * This function determines how a single scheduled payment should be split among + * the three tracked loan components. The calculation accounts for accumulated + * rounding errors. + * + * The algorithm: + * 1. Calculate what the loan state SHOULD be after this payment (target) + * 2. Compare current state to target to get deltas + * 3. Adjust deltas to handle rounding artifacts and edge cases + * 4. Ensure deltas don't exceed available balances or payment amount + * + * Special handling for the final payment: all remaining balances are paid off + * regardless of the periodic payment amount. + */ PaymentComponents computePaymentComponents( Asset const& asset, @@ -857,10 +995,6 @@ computePaymentComponents( std::uint32_t paymentRemaining, TenthBips16 managementFeeRate) { - /* - * This function is derived from the XLS-66 spec, section 3.2.4.1.1 (Regular - * Payment) - */ XRPL_ASSERT_PARTS( isRounded(asset, totalValueOutstanding, scale) && isRounded(asset, principalOutstanding, scale) && @@ -871,9 +1005,12 @@ computePaymentComponents( paymentRemaining > 0, "ripple::detail::computePaymentComponents", "some payments remaining"); + auto const roundedPeriodicPayment = roundPeriodicPayment(asset, periodicPayment, scale); + // Final payment: pay off everything remaining, ignoring the normal + // periodic payment amount. This ensures the loan completes cleanly. if (paymentRemaining == 1 || totalValueOutstanding <= roundedPeriodicPayment) { @@ -886,8 +1023,13 @@ computePaymentComponents( .specialCase = PaymentSpecialCase::final}; } + // Calculate what the loan state SHOULD be after this payment (the target). + // This is computed at full precision using the theoretical amortization. LoanState const trueTarget = computeRawLoanState( periodicPayment, periodicRate, paymentRemaining - 1, managementFeeRate); + + // Round the target to the loan's scale to match how actual loan values + // are stored. LoanState const roundedTarget = LoanState{ .valueOutstanding = roundToAsset(asset, trueTarget.valueOutstanding, scale), @@ -896,18 +1038,26 @@ computePaymentComponents( .interestDue = roundToAsset(asset, trueTarget.interestDue, scale), .managementFeeDue = roundToAsset(asset, trueTarget.managementFeeDue, scale)}; + + // Get the current actual loan state from the ledger values LoanState const currentLedgerState = constructLoanState( totalValueOutstanding, principalOutstanding, managementFeeOutstanding); + // The difference between current and target states gives us the payment + // components. Any discrepancies from accumulated rounding are captured + // here. + LoanStateDeltas deltas = currentLedgerState - roundedTarget; + + // Rounding can occasionally produce negative deltas. Zero them out. deltas.nonNegative(); - // Adjust the deltas if necessary for data integrity XRPL_ASSERT_PARTS( deltas.principal <= currentLedgerState.principalOutstanding, "ripple::detail::computePaymentComponents", "principal delta not greater than outstanding"); + // Cap each component to never exceed what's actually outstanding deltas.principal = std::min(deltas.principal, currentLedgerState.principalOutstanding); @@ -916,6 +1066,8 @@ computePaymentComponents( "ripple::detail::computePaymentComponents", "interest due delta not greater than outstanding"); + // Cap interest to both the outstanding amount AND what's left of the + // periodic payment after principal is paid deltas.interest = std::min( {deltas.interest, std::max(numZero, roundedPeriodicPayment - deltas.principal), @@ -926,6 +1078,8 @@ computePaymentComponents( "ripple::detail::computePaymentComponents", "management fee due delta not greater than outstanding"); + // Cap management fee to both the outstanding amount AND what's left of the + // periodic payment after principal and interest are paid deltas.managementFee = std::min( {deltas.managementFee, roundedPeriodicPayment - (deltas.principal + deltas.interest), @@ -938,27 +1092,30 @@ computePaymentComponents( auto takeFrom = [](Number& component, Number& excess) { if (excess > beast::zero) { - // Take as much of the excess as we can out of the provided part and - // the total auto part = std::min(component, excess); component -= part; excess -= part; } - // If the excess goes negative, we took too much, which should be - // impossible XRPL_ASSERT_PARTS( excess >= beast::zero, "ripple::detail::computePaymentComponents", "excess non-negative"); }; + // Helper to reduce deltas when they collectively exceed a limit. + // Order matters: we prefer to reduce interest first (most flexible), + // then management fee, then principal (least flexible). auto addressExcess = [&takeFrom](LoanStateDeltas& deltas, Number& excess) { // This order is based on where errors are the least problematic takeFrom(deltas.interest, excess); takeFrom(deltas.managementFee, excess); takeFrom(deltas.principal, excess); }; + + // Check if deltas exceed the total outstanding value. This should never + // happen due to earlier caps, but handle it defensively. Number totalOverpayment = deltas.total() - currentLedgerState.valueOutstanding; + if (totalOverpayment > beast::zero) { // LCOV_EXCL_START @@ -969,7 +1126,7 @@ computePaymentComponents( // LCOV_EXCL_STOP } - // Make sure the parts don't add up to too much + // Check if deltas exceed the periodic payment amount. Reduce if needed. Number shortage = roundedPeriodicPayment - deltas.total(); XRPL_ASSERT_PARTS( @@ -979,21 +1136,21 @@ computePaymentComponents( if (shortage < beast::zero) { + // Deltas exceed payment amount - reduce them proportionally Number excess = -shortage; - addressExcess(deltas, excess); - shortage = -excess; } - // The shortage should never be negative, which indicates that the - // parts are trying to take more than the whole payment. The - // shortage may be positive, which indicates that we're not going to - // take the whole payment amount. + + // At this point, shortage >= 0 means we're paying less than the full + // periodic payment (due to rounding or component caps). + // shortage < 0 would mean we're trying to pay more than allowed (bug). XRPL_ASSERT_PARTS( shortage >= beast::zero, "ripple::detail::computePaymentComponents", "no shortage or excess"); + // Final validation that all components are valid XRPL_ASSERT_PARTS( deltas.total() == deltas.principal + deltas.interest + deltas.managementFee, @@ -1021,9 +1178,8 @@ computePaymentComponents( "ripple::detail::computePaymentComponents", "payment parts add to payment"); + // Final safety clamp to ensure no value exceeds its outstanding balance return PaymentComponents{ - // As a final safety check, ensure the value is non-negative, and won't - // make the corresponding item negative .trackedValueDelta = std::clamp( deltas.total(), numZero, currentLedgerState.valueOutstanding), .trackedPrincipalDelta = std::clamp( @@ -1033,6 +1189,24 @@ computePaymentComponents( }; } +/* Computes payment components for an overpayment scenario. + * + * An overpayment occurs when a borrower pays more than the scheduled periodic + * payment amount. The overpayment is treated as extra principal reduction, + * but incurs a fee and potentially a penalty interest charge. + * + * The calculation (Section 3.2.4.2.3 from XLS-66 spec): + * 1. Calculate gross penalty interest on the overpayment amount + * 2. Split the gross interest into net interest and management fee + * 3. Calculate the penalty fee + * 4. Determine the principal portion by subtracting the interest (gross) and + * management fee from the overpayment amount + * + * Unlike regular payments which follow the amortization schedule, overpayments + * apply to principal, reducing the loan balance and future interest costs. + * + * Equations (20), (21) and (22) from XLS-66 spec, Section A-2 Equation Glossary + */ ExtendedPaymentComponents computeOverpaymentComponents( Asset const& asset, @@ -1047,14 +1221,23 @@ computeOverpaymentComponents( "ripple::detail::computeOverpaymentComponents : valid overpayment " "amount"); + // First, deduct the fixed overpayment fee from the total amount. + // This reduces the effective payment that will be applied to the loan. + // Equation (22) from XLS-66 spec, Section A-2 Equation Glossary Number const overpaymentFee = roundToAsset( asset, tenthBipsOfValue(overpayment, overpaymentFeeRate), loanScale); + // Calculate the penalty interest on the effective payment amount. + // This interest doesn't follow the normal amortization schedule - it's + // a one-time charge for paying early. + // Equation (20) and (21) from XLS-66 spec, Section A-2 Equation Glossary auto const [rawOverpaymentInterest, _] = [&]() { Number const interest = tenthBipsOfValue(overpayment, overpaymentInterestRate); return detail::computeInterestAndFeeParts(interest, managementFeeRate); }(); + + // Round the penalty interest components to the loan scale auto const [roundedOverpaymentInterest, roundedOverpaymentManagementFee] = [&]() { Number const interest = @@ -1064,13 +1247,23 @@ computeOverpaymentComponents( }(); auto const result = detail::ExtendedPaymentComponents{ + // Build the payment components, after fees and penalty + // interest are deducted, the remainder goes entirely to principal + // reduction. detail::PaymentComponents{ .trackedValueDelta = overpayment - overpaymentFee, .trackedPrincipalDelta = overpayment - roundedOverpaymentInterest - roundedOverpaymentManagementFee - overpaymentFee, .trackedManagementFeeDelta = roundedOverpaymentManagementFee, .specialCase = detail::PaymentSpecialCase::extra}, + // Untracked management fee is the fixed overpayment fee overpaymentFee, + // Untracked interest is the penalty interest charged for + // overpaying. + // This is positive, representing a one-time cost, but it's + // typically + // much smaller than the interest savings from reducing + // principal. roundedOverpaymentInterest}; XRPL_ASSERT_PARTS( result.trackedInterestPart() == roundedOverpaymentInterest, @@ -1200,6 +1393,12 @@ checkLoanGuards( return tesSUCCESS; } +/* + * This function calculates the full payment interest accrued since the last + * payment, plus any prepayment penalty. + * + * Equations (27) and (28) from XLS-66 spec, Section A-2 Equation Glossary + */ Number computeFullPaymentInterest( Number const& rawPrincipalOutstanding, @@ -1210,8 +1409,6 @@ computeFullPaymentInterest( std::uint32_t startDate, TenthBips32 closeInterestRate) { - // If there is more than one payment remaining, see if enough was - // paid for a full payment auto const accruedInterest = detail::loanAccruedInterest( rawPrincipalOutstanding, periodicRate, @@ -1221,8 +1418,10 @@ computeFullPaymentInterest( paymentInterval); XRPL_ASSERT( accruedInterest >= 0, - "ripple::detail::computeFullPaymentInterest : valid accrued interest"); + "ripple::detail::computeFullPaymentInterest : valid accrued " + "interest"); + // Equation (28) from XLS-66 spec, Section A-2 Equation Glossary auto const prepaymentPenalty = closeInterestRate == beast::zero ? Number{} : tenthBipsOfValue(rawPrincipalOutstanding, closeInterestRate); @@ -1232,6 +1431,7 @@ computeFullPaymentInterest( "ripple::detail::computeFullPaymentInterest : valid prepayment " "interest"); + // Part of equation (27) from XLS-66 spec, Section A-2 Equation Glossary return accruedInterest + prepaymentPenalty; } @@ -1260,6 +1460,25 @@ computeFullPaymentInterest( closeInterestRate); } +/* Calculates the theoretical loan state at maximum precision for a given point + * in the amortization schedule. + * + * This function computes what the loan's outstanding balances should be based + * on the periodic payment amount and number of payments remaining, + * without considering any rounding that may have been applied to the actual + * Loan object's state. This "raw" (unrounded) state is used as a target for + * computing payment components and validating that the loan's tracked state + * hasn't drifted too far from the theoretical values. + * + * The raw state serves several purposes: + * 1. Computing the expected payment breakdown (principal, interest, fees) + * 2. Detecting and correcting rounding errors that accumulate over time + * 3. Validating that overpayments are calculated correctly + * 4. Ensuring the loan will be fully paid off at the end of its term + * + * If paymentRemaining is 0, returns a fully zeroed-out LoanState, + * representing a completely paid-off loan. + */ LoanState computeRawLoanState( Number const& periodicPayment, @@ -1275,19 +1494,30 @@ computeRawLoanState( .interestDue = 0, .managementFeeDue = 0}; } - Number const rawValueOutstanding = periodicPayment * paymentRemaining; + + // Equation (30) from XLS-66 spec, Section A-2 Equation Glossary + Number const rawTotalValueOutstanding = periodicPayment * paymentRemaining; + Number const rawPrincipalOutstanding = detail::loanPrincipalFromPeriodicPayment( periodicPayment, periodicRate, paymentRemaining); - Number const rawInterestOutstanding = - rawValueOutstanding - rawPrincipalOutstanding; + + // Equation (31) from XLS-66 spec, Section A-2 Equation Glossary + Number const rawInterestOutstandingGross = + rawTotalValueOutstanding - rawPrincipalOutstanding; + + // Equation (32) from XLS-66 spec, Section A-2 Equation Glossary Number const rawManagementFeeOutstanding = - tenthBipsOfValue(rawInterestOutstanding, managementFeeRate); + tenthBipsOfValue(rawInterestOutstandingGross, managementFeeRate); + + // Equation (33) from XLS-66 spec, Section A-2 Equation Glossary + Number const rawInterestOutstandingNet = + rawInterestOutstandingGross - rawManagementFeeOutstanding; return LoanState{ - .valueOutstanding = rawValueOutstanding, + .valueOutstanding = rawTotalValueOutstanding, .principalOutstanding = rawPrincipalOutstanding, - .interestDue = rawInterestOutstanding - rawManagementFeeOutstanding, + .interestDue = rawInterestOutstandingNet, .managementFeeDue = rawManagementFeeOutstanding}; }; @@ -1306,14 +1536,33 @@ computeRawLoanState( managementFeeRate); } +/* Constructs a LoanState from rounded Loan ledger object values. + * + * This function creates a LoanState structure from the three tracked values + * stored in a Loan ledger object. Unlike calculateRawLoanState(), which + * computes theoretical unrounded values, this function works with values + * that have already been rounded to the loan's scale. + * + * The key difference from calculateRawLoanState(): + * - calculateRawLoanState: Computes theoretical values at full precision + * - constructRoundedLoanState: Builds state from actual rounded ledger values + * + * The interestDue field is derived from the other three values rather than + * stored directly, since it can be calculated as: + * interestDue = totalValueOutstanding - principalOutstanding - + * managementFeeOutstanding + * + * This ensures consistency across the codebase and prevents copy-paste errors + * when creating LoanState objects from Loan ledger data. + */ LoanState constructLoanState( Number const& totalValueOutstanding, Number const& principalOutstanding, Number const& managementFeeOutstanding) { - // This implementation is pretty trivial, but ensures the calculations are - // consistent everywhere, and reduces copy/paste errors. + // This implementation is pretty trivial, but ensures the calculations + // are consistent everywhere, and reduces copy/paste errors. return LoanState{ .valueOutstanding = totalValueOutstanding, .principalOutstanding = principalOutstanding, @@ -1331,6 +1580,12 @@ constructRoundedLoanState(SLE::const_ref loan) loan->at(sfManagementFeeOutstanding)); } +/* + * This function calculates the fee owed to the broker based on the asset, + * value, and management fee rate. + * + * Equation (32) from XLS-66 spec, Section A-2 Equation Glossary + */ Number computeManagementFee( Asset const& asset, @@ -1345,6 +1600,9 @@ computeManagementFee( Number::downward); } +/* + * Given the loan parameters, compute the derived properties of the loan. + */ LoanProperties computeLoanProperties( Asset const& asset, @@ -1362,23 +1620,19 @@ computeLoanProperties( auto const periodicPayment = detail::loanPeriodicPayment( principalOutstanding, periodicRate, paymentsRemaining); + auto const [totalValueOutstanding, loanScale] = [&]() { NumberRoundModeGuard mg(Number::to_nearest); // Use STAmount's internal rounding instead of roundToAsset, because // we're going to use this result to determine the scale for all the // other rounding. - STAmount amount{ - asset, - /* - * This formula is from the XLS-66 spec, section 3.2.4.2 (Total - * Loan Value Calculation), specifically "totalValueOutstanding - * = ..." - */ - periodicPayment * paymentsRemaining}; - // Base the loan scale on the total value, since that's going to be the - // biggest number involved (barring unusual parameters for late, full, - // or over payments) + // Equation (30) from XLS-66 spec, Section A-2 Equation Glossary + STAmount amount{asset, periodicPayment * paymentsRemaining}; + + // Base the loan scale on the total value, since that's going to be + // the biggest number involved (barring unusual parameters for late, + // full, or over payments) auto const loanScale = std::max(minimumScale, amount.exponent()); XRPL_ASSERT_PARTS( (amount.integral() && loanScale == 0) || @@ -1387,7 +1641,8 @@ computeLoanProperties( "ripple::computeLoanProperties", "loanScale value fits expectations"); - // We may need to truncate the total value because of the minimum scale + // We may need to truncate the total value because of the minimum + // scale amount = roundToAsset(asset, amount, loanScale, Number::to_nearest); return std::make_pair(amount, loanScale); @@ -1399,16 +1654,15 @@ computeLoanProperties( principalOutstanding = roundToAsset( asset, principalOutstanding, loanScale, Number::to_nearest); + // E loanMakePayment( Asset const& asset, @@ -1449,17 +1710,12 @@ loanMakePayment( { using namespace Lending; - /* - * This function is an implementation of the XLS-66 spec, - * section 3.2.4.3 (Transaction Pseudo-code) - */ auto principalOutstandingProxy = loan->at(sfPrincipalOutstanding); auto paymentRemainingProxy = loan->at(sfPaymentRemaining); if (paymentRemainingProxy == 0 || principalOutstandingProxy == 0) { - // Loan complete - // This is already checked in LoanPay::preclaim() + // Loan complete this is already checked in LoanPay::preclaim() // LCOV_EXCL_START JLOG(j.warn()) << "Loan is already paid off."; return Unexpected(tecKILLED); @@ -1490,8 +1746,9 @@ loanMakePayment( std::uint32_t const startDate = loan->at(sfStartDate); std::uint32_t const paymentInterval = loan->at(sfPaymentInterval); - // Compute the normal periodic rate, payment, etc. - // We'll need it in the remaining calculations + + // Compute the periodic rate that will be used for calculations + // throughout Number const periodicRate = loanPeriodicRate(interestRate, paymentInterval); XRPL_ASSERT( interestRate == 0 || periodicRate > 0, @@ -1508,14 +1765,15 @@ loanMakePayment( if (paymentType != LoanPaymentType::late && hasExpired(view, nextDueDateProxy)) { - // If the payment is late, and the late flag was not set, it's not valid - JLOG(j.warn()) - << "Loan payment is overdue. Use the tfLoanLatePayment transaction " - "flag to make a late payment. Loan was created on " - << startDate << ", prev payment due date is " - << prevPaymentDateProxy << ", next payment due date is " - << nextDueDateProxy << ", ledger time is " - << view.parentCloseTime().time_since_epoch().count(); + // If the payment is late, and the late flag was not set, it's not + // valid + JLOG(j.warn()) << "Loan payment is overdue. Use the tfLoanLatePayment " + "transaction " + "flag to make a late payment. Loan was created on " + << startDate << ", prev payment due date is " + << prevPaymentDateProxy << ", next payment due date is " + << nextDueDateProxy << ", ledger time is " + << view.parentCloseTime().time_since_epoch().count(); return Unexpected(tecEXPIRED); } @@ -1575,8 +1833,8 @@ loanMakePayment( } // ------------------------------------------------------------- - // compute the periodic payment info that will be needed whether the payment - // is late or regular + // compute the periodic payment info that will be needed whether the + // payment is late or regular detail::ExtendedPaymentComponents periodic{ detail::computePaymentComponents( asset, @@ -1647,8 +1905,7 @@ loanMakePayment( "ripple::loanMakePayment", "regular payment type"); - // This will keep a running total of what is actually paid, if the payment - // is sufficient for any payment + // Keep a running total of the actual parts paid LoanPaymentParts totalParts; Number totalPaid; std::size_t numPayments = 0; @@ -1750,7 +2007,8 @@ loanMakePayment( overpaymentComponents.untrackedInterest >= beast::zero, "ripple::loanMakePayment", "overpayment penalty did not reduce value of loan"); - // Can't just use `periodicPayment` here, because it might change + // Can't just use `periodicPayment` here, because it might + // change auto periodicPaymentProxy = loan->at(sfPeriodicPayment); if (auto const overResult = detail::doOverpayment( asset, @@ -1770,9 +2028,10 @@ loanMakePayment( j)) totalParts += *overResult; else if (overResult.error()) - // error() will be the TER returned if a payment is not made. It - // will only evaluate to true if it's unsuccessful. Otherwise, - // tesSUCCESS means nothing was done, so continue. + // error() will be the TER returned if a payment is not + // made. It will only evaluate to true if it's unsuccessful. + // Otherwise, tesSUCCESS means nothing was done, so + // continue. return Unexpected(overResult.error()); } } diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 4bc0dc6188..43f19743a7 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -269,7 +269,9 @@ LoanPay::doApply() // Normally freeze status is checked in preflight, but we do it here to // avoid duplicating the check. It'll claim a fee either way. bool const sendBrokerFeeToOwner = [&]() { - // Always round the minimum required up. + // Round the minimum required cover up to be conservative. This ensures + // CoverAvailable never drops below the theoretical minimum, protecting + // the broker's solvency. NumberRoundModeGuard mg(Number::upward); return coverAvailableProxy >= roundToAsset( diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index e73f685bf8..838e774cae 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -465,7 +465,9 @@ LoanSet::doApply() } TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)}; { - // Always round the minimum required up. + // Round the minimum required cover up to be conservative. This ensures + // CoverAvailable never drops below the theoretical minimum, protecting + // the broker's solvency. NumberRoundModeGuard mg(Number::upward); if (brokerSle->at(sfCoverAvailable) < tenthBipsOfValue(newDebtTotal, coverRateMinimum)) From 49ee70ea28af99577f675fea700d142d6db98b4d Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 25 Nov 2025 12:22:15 -0500 Subject: [PATCH 282/291] Fix formatting again --- src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp index 8a1d8eb8f9..2a5a212211 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp @@ -233,7 +233,6 @@ LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx) // LCOV_EXCL_STOP } - auto const vaultAsset = vault->at(sfAsset); if (vaultAsset.native()) From 77e3bbdc8966cebe4b4f58ab6bbf789a0db49527 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 25 Nov 2025 13:25:48 -0500 Subject: [PATCH 283/291] Move the ValidPseudoAccounts class back to its original location - The class didn't actually change much, if at all, but somehow got relocated. - This should make the review easier, and reduce the footprint of the PR. --- src/xrpld/app/tx/detail/InvariantCheck.cpp | 190 +++++++++++---------- src/xrpld/app/tx/detail/InvariantCheck.h | 56 +++--- 2 files changed, 124 insertions(+), 122 deletions(-) diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 04f28352b8..27c87e074c 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -1731,6 +1731,102 @@ ValidPermissionedDomain::finalize( (sleStatus_[1] ? check(*sleStatus_[1], j) : true); } +//------------------------------------------------------------------------------ + +void +ValidPseudoAccounts::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (isDelete) + // Deletion is ignored + return; + + if (after && after->getType() == ltACCOUNT_ROOT) + { + bool const isPseudo = [&]() { + // isPseudoAccount checks that any of the pseudo-account fields are + // set. + if (isPseudoAccount(after)) + return true; + // Not all pseudo-accounts have a zero sequence, but all accounts + // with a zero sequence had better be pseudo-accounts. + if (after->at(sfSequence) == 0) + return true; + + return false; + }(); + if (isPseudo) + { + // Pseudo accounts must have the following properties: + // 1. Exactly one of the pseudo-account fields is set. + // 2. The sequence number is not changed. + // 3. The lsfDisableMaster, lsfDefaultRipple, and lsfDepositAuth + // flags are set. + // 4. The RegularKey is not set. + { + std::vector const& fields = + getPseudoAccountFields(); + + auto const numFields = std::count_if( + fields.begin(), + fields.end(), + [&after](SField const* sf) -> bool { + return after->isFieldPresent(*sf); + }); + if (numFields != 1) + { + std::stringstream error; + error << "pseudo-account has " << numFields + << " pseudo-account fields set"; + errors_.emplace_back(error.str()); + } + } + if (before && before->at(sfSequence) != after->at(sfSequence)) + { + errors_.emplace_back("pseudo-account sequence changed"); + } + if (!after->isFlag( + lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth)) + { + errors_.emplace_back("pseudo-account flags are not set"); + } + if (after->isFieldPresent(sfRegularKey)) + { + errors_.emplace_back("pseudo-account has a regular key"); + } + } + } +} + +bool +ValidPseudoAccounts::finalize( + STTx const& tx, + TER const, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + bool const enforce = view.rules().enabled(featureSingleAssetVault); + XRPL_ASSERT( + errors_.empty() || enforce, + "ripple::ValidPseudoAccounts::finalize : no bad " + "changes or enforce invariant"); + if (!errors_.empty()) + { + for (auto const& error : errors_) + { + JLOG(j.fatal()) << "Invariant failed: " << error; + } + if (enforce) + return false; + } + return true; +} + +//------------------------------------------------------------------------------ + void ValidPermissionedDEX::visitEntry( bool, @@ -2237,100 +2333,6 @@ NoModifiedUnmodifiableFields::finalize( //------------------------------------------------------------------------------ -void -ValidPseudoAccounts::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - if (isDelete) - // Deletion is ignored - return; - - if (after && after->getType() == ltACCOUNT_ROOT) - { - bool const isPseudo = [&]() { - // isPseudoAccount checks that any of the pseudo-account fields are - // set. - if (isPseudoAccount(after)) - return true; - // Not all pseudo-accounts have a zero sequence, but all accounts - // with a zero sequence had better be pseudo-accounts. - if (after->at(sfSequence) == 0) - return true; - - return false; - }(); - if (isPseudo) - { - // Pseudo accounts must have the following properties: - // 1. Exactly one of the pseudo-account fields is set. - // 2. The sequence number is not changed. - // 3. The lsfDisableMaster, lsfDefaultRipple, and lsfDepositAuth - // flags are set. - // 4. The RegularKey is not set. - { - std::vector const& fields = - getPseudoAccountFields(); - - auto const numFields = std::count_if( - fields.begin(), - fields.end(), - [&after](SField const* sf) -> bool { - return after->isFieldPresent(*sf); - }); - if (numFields != 1) - { - std::stringstream error; - error << "pseudo-account has " << numFields - << " pseudo-account fields set"; - errors_.emplace_back(error.str()); - } - } - if (before && before->at(sfSequence) != after->at(sfSequence)) - { - errors_.emplace_back("pseudo-account sequence changed"); - } - if (!after->isFlag( - lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth)) - { - errors_.emplace_back("pseudo-account flags are not set"); - } - if (after->isFieldPresent(sfRegularKey)) - { - errors_.emplace_back("pseudo-account has a regular key"); - } - } - } -} - -bool -ValidPseudoAccounts::finalize( - STTx const& tx, - TER const, - XRPAmount const, - ReadView const& view, - beast::Journal const& j) -{ - bool const enforce = view.rules().enabled(featureSingleAssetVault); - XRPL_ASSERT( - errors_.empty() || enforce, - "ripple::ValidPseudoAccounts::finalize : no bad " - "changes or enforce invariant"); - if (!errors_.empty()) - { - for (auto const& error : errors_) - { - JLOG(j.fatal()) << "Invariant failed: " << error; - } - if (enforce) - return false; - } - return true; -} - -//------------------------------------------------------------------------------ - void ValidLoanBroker::visitEntry( bool isDelete, diff --git a/src/xrpld/app/tx/detail/InvariantCheck.h b/src/xrpld/app/tx/detail/InvariantCheck.h index ffe8e4ca5e..09a84b1ab4 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.h +++ b/src/xrpld/app/tx/detail/InvariantCheck.h @@ -612,6 +612,34 @@ public: beast::Journal const&); }; +/** + * @brief Invariants: Pseudo-accounts have valid and consisent properties + * + * Pseudo-accounts have certain properties, and some of those properties are + * unique to pseudo-accounts. Check that all pseudo-accounts are following the + * rules, and that only pseudo-accounts look like pseudo-accounts. + * + */ +class ValidPseudoAccounts +{ + std::vector errors_; + +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&); +}; + class ValidPermissionedDEX { bool regularOffers_ = false; @@ -725,34 +753,6 @@ public: beast::Journal const&); }; -/** - * @brief Invariants: Pseudo-accounts have valid and consisent properties - * - * Pseudo-accounts have certain properties, and some of those properties are - * unique to pseudo-accounts. Check that all pseudo-accounts are following the - * rules, and that only pseudo-accounts look like pseudo-accounts. - * - */ -class ValidPseudoAccounts -{ - std::vector errors_; - -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&); -}; - /** * @brief Invariants: Loan brokers are internally consistent * From 40cd57355db2f5367184a814367b3634505ab838 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 25 Nov 2025 14:46:46 -0500 Subject: [PATCH 284/291] Review feedback from @shawnxie999: MPT Clawback - MPTs do not require the lsfMPTCanLock flag to be able to clawback --- src/test/app/LoanBroker_test.cpp | 132 ++++++++++-------- .../app/tx/detail/LoanBrokerCoverClawback.cpp | 3 +- 2 files changed, 74 insertions(+), 61 deletions(-) diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 66ce525e8c..597e2ea75b 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -877,16 +877,16 @@ class LoanBroker_test : public beast::unit_test::suite PrettyAsset const asset = [&]() { if (getAsset) return getAsset(env, issuer, alice); - env(trust(alice, issuer["IOU"](1'000'000))); + env(trust(alice, issuer["IOU"](1'000'000)), THISLINE); env.close(); return PrettyAsset(issuer["IOU"]); }(); - env(pay(issuer, alice, asset(100'000))); + env(pay(issuer, alice, asset(100'000)), THISLINE); env.close(); auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = asset}); - env(tx); + env(tx, THISLINE); env.close(); auto const le = env.le(vaultKeylet); VaultInfo vaultInfo = [&]() { @@ -898,12 +898,15 @@ class LoanBroker_test : public beast::unit_test::suite return; env(vault.deposit( - {.depositor = alice, .id = vaultKeylet.key, .amount = asset(50)})); + {.depositor = alice, + .id = vaultKeylet.key, + .amount = asset(50)}), + THISLINE); env.close(); auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice)); - env(set(alice, vaultInfo.vaultID)); + env(set(alice, vaultInfo.vaultID), THISLINE); env.close(); auto broker = env.le(brokerKeylet); @@ -914,23 +917,23 @@ class LoanBroker_test : public beast::unit_test::suite auto jv = getTxJv(); // empty broker ID jv[sfLoanBrokerID] = ""; - env(jv, ter(temINVALID)); + env(jv, ter(temINVALID), THISLINE); // zero broker ID jv[sfLoanBrokerID] = to_string(uint256{}); // needs a flag to distinguish the parsed STTx from the prior // test - env(jv, txflags(tfFullyCanonicalSig), ter(temINVALID)); + env(jv, txflags(tfFullyCanonicalSig), ter(temINVALID), THISLINE); }; auto testZeroVaultID = [&](auto&& getTxJv) { auto jv = getTxJv(); // empty broker ID jv[sfVaultID] = ""; - env(jv, ter(temINVALID)); + env(jv, ter(temINVALID), THISLINE); // zero broker ID jv[sfVaultID] = to_string(uint256{}); // needs a flag to distinguish the parsed STTx from the prior // test - env(jv, txflags(tfFullyCanonicalSig), ter(temINVALID)); + env(jv, txflags(tfFullyCanonicalSig), ter(temINVALID), THISLINE); }; if (brokerTest == CoverDeposit) @@ -942,23 +945,26 @@ class LoanBroker_test : public beast::unit_test::suite // preclaim: tecWRONG_ASSET env(coverDeposit(alice, brokerKeylet.key, issuer["BAD"](10)), - ter(tecWRONG_ASSET)); + ter(tecWRONG_ASSET), + THISLINE); // preclaim: tecINSUFFICIENT_FUNDS - env(pay(alice, issuer, asset(100'000 - 50))); + env(pay(alice, issuer, asset(100'000 - 50)), THISLINE); env.close(); env(coverDeposit(alice, brokerKeylet.key, vaultInfo.asset(10)), ter(tecINSUFFICIENT_FUNDS)); // preclaim: tecFROZEN - env(fset(issuer, asfGlobalFreeze)); + env(fset(issuer, asfGlobalFreeze), THISLINE); env.close(); env(coverDeposit(alice, brokerKeylet.key, vaultInfo.asset(10)), - ter(tecFROZEN)); + ter(tecFROZEN), + THISLINE); } else // Fund the cover deposit - env(coverDeposit(alice, brokerKeylet.key, vaultInfo.asset(10))); + env(coverDeposit(alice, brokerKeylet.key, vaultInfo.asset(10)), + THISLINE); env.close(); if (brokerTest == CoverWithdraw) @@ -970,47 +976,54 @@ class LoanBroker_test : public beast::unit_test::suite // preclaim: tecWRONG_ASSSET env(coverWithdraw(alice, brokerKeylet.key, issuer["BAD"](10)), - ter(tecWRONG_ASSET)); + ter(tecWRONG_ASSET), + THISLINE); // preclaim: tecNO_DST Account const bogus{"bogus"}; env(coverWithdraw(alice, brokerKeylet.key, asset(10)), destination(bogus), - ter(tecNO_DST)); + ter(tecNO_DST), + THISLINE); // preclaim: tecDST_TAG_NEEDED Account const dest{"dest"}; env.fund(XRP(1'000), dest); - env(fset(dest, asfRequireDest)); + env(fset(dest, asfRequireDest), THISLINE); env.close(); env(coverWithdraw(alice, brokerKeylet.key, asset(10)), destination(dest), - ter(tecDST_TAG_NEEDED)); + ter(tecDST_TAG_NEEDED), + THISLINE); // preclaim: tecNO_PERMISSION - env(fclear(dest, asfRequireDest)); - env(fset(dest, asfDepositAuth)); + env(fclear(dest, asfRequireDest), THISLINE); + env(fset(dest, asfDepositAuth), THISLINE); env.close(); env(coverWithdraw(alice, brokerKeylet.key, asset(10)), destination(dest), - ter(tecNO_PERMISSION)); + ter(tecNO_PERMISSION), + THISLINE); // preclaim: tecFROZEN - env(trust(dest, asset(1'000))); - env(fclear(dest, asfDepositAuth)); - env(fset(issuer, asfGlobalFreeze)); + env(trust(dest, asset(1'000)), THISLINE); + env(fclear(dest, asfDepositAuth), THISLINE); + env(fset(issuer, asfGlobalFreeze), THISLINE); env.close(); env(coverWithdraw(alice, brokerKeylet.key, asset(10)), destination(dest), - ter(tecFROZEN)); + ter(tecFROZEN), + THISLINE); // preclaim:: tecFROZEN (deep frozen) - env(fclear(issuer, asfGlobalFreeze)); + env(fclear(issuer, asfGlobalFreeze), THISLINE); env(trust( - issuer, asset(1'000), dest, tfSetFreeze | tfSetDeepFreeze)); + issuer, asset(1'000), dest, tfSetFreeze | tfSetDeepFreeze), + THISLINE); env(coverWithdraw(alice, brokerKeylet.key, asset(10)), destination(dest), - ter(tecFROZEN)); + ter(tecFROZEN), + THISLINE); } if (brokerTest == CoverClawback) @@ -1029,23 +1042,27 @@ class LoanBroker_test : public beast::unit_test::suite env(coverClawback(issuer), loanBrokerID(brokerKeylet.key), amount(vaultInfo.asset(2)), - ter(tecNO_PERMISSION)); + ter(tecNO_PERMISSION), + THISLINE); // preclaim: NoFreeze is set - env(fset(issuer, asfAllowTrustLineClawback | asfNoFreeze)); + env(fset(issuer, asfAllowTrustLineClawback | asfNoFreeze), + THISLINE); env.close(); env(coverClawback(issuer), loanBrokerID(brokerKeylet.key), amount(vaultInfo.asset(2)), - ter(tecNO_PERMISSION)); + ter(tecNO_PERMISSION), + THISLINE); } else { - // preclaim: MPTCanClawback is not set or MPTCAnLock is not set + // preclaim: MPTCanClawback is not set or MPTCanLock is not set env(coverClawback(issuer), loanBrokerID(brokerKeylet.key), amount(vaultInfo.asset(2)), - ter(tecNO_PERMISSION)); + ter(tecNO_PERMISSION), + THISLINE); } env.close(); } @@ -1056,30 +1073,36 @@ class LoanBroker_test : public beast::unit_test::suite env.fund(XRP(1'000), borrower); env(loan::set(borrower, brokerKeylet.key, asset(50).value()), sig(sfCounterpartySignature, alice), - fee(env.current()->fees().base * 2)); + fee(env.current()->fees().base * 2), + THISLINE); // preflight: temINVALID (empty/zero broker id) testZeroBrokerID([&]() { return del(alice, brokerKeylet.key); }); // preclaim: tecHAS_OBLIGATIONS - env(del(alice, brokerKeylet.key), ter(tecHAS_OBLIGATIONS)); + env(del(alice, brokerKeylet.key), + ter(tecHAS_OBLIGATIONS), + THISLINE); // Repay and delete the loan auto const loanKeylet = keylet::loan(brokerKeylet.key, 1); - env(loan::pay(borrower, loanKeylet.key, asset(50).value())); - env(loan::del(alice, loanKeylet.key)); + env(loan::pay(borrower, loanKeylet.key, asset(50).value()), + THISLINE); + env(loan::del(alice, loanKeylet.key), THISLINE); - env(trust(issuer, asset(0), alice, tfSetFreeze | tfSetDeepFreeze)); + env(trust(issuer, asset(0), alice, tfSetFreeze | tfSetDeepFreeze), + THISLINE); // preclaim: tecFROZEN (deep frozen) - env(del(alice, brokerKeylet.key), ter(tecFROZEN)); + env(del(alice, brokerKeylet.key), ter(tecFROZEN), THISLINE); env(trust( - issuer, asset(0), alice, tfClearFreeze | tfClearDeepFreeze)); + issuer, asset(0), alice, tfClearFreeze | tfClearDeepFreeze), + THISLINE); // successful delete the loan broker object - env(del(alice, brokerKeylet.key), ter(tesSUCCESS)); + env(del(alice, brokerKeylet.key), ter(tesSUCCESS), THISLINE); } else - env(del(alice, brokerKeylet.key)); + env(del(alice, brokerKeylet.key), THISLINE); if (brokerTest == Set) { @@ -1098,21 +1121,23 @@ class LoanBroker_test : public beast::unit_test::suite if (asset.holds()) { - env(fclear(issuer, asfDefaultRipple)); + env(fclear(issuer, asfDefaultRipple), THISLINE); env.close(); // preclaim: DefaultRipple is not set - env(set(alice, vaultInfo.vaultID), ter(terNO_RIPPLE)); + env(set(alice, vaultInfo.vaultID), ter(terNO_RIPPLE), THISLINE); - env(fset(issuer, asfDefaultRipple)); + env(fset(issuer, asfDefaultRipple), THISLINE); env.close(); } auto const amt = env.balance(alice) - env.current()->fees().accountReserve(env.ownerCount(alice)); - env(pay(alice, issuer, amt)); + env(pay(alice, issuer, amt), THISLINE); // preclaim:: tecINSUFFICIENT_RESERVE - env(set(alice, vaultInfo.vaultID), ter(tecINSUFFICIENT_RESERVE)); + env(set(alice, vaultInfo.vaultID), + ter(tecINSUFFICIENT_RESERVE), + THISLINE); } } @@ -1135,7 +1160,7 @@ class LoanBroker_test : public beast::unit_test::suite auto jtx = env.jt(coverClawback(alice), amount(USD(100))); // holder == account - env(jtx, ter(temINVALID)); + env(jtx, ter(temINVALID), THISLINE); // holder == beast::zero STAmount bad(Issue{USD.currency, beast::zero}, 100); @@ -1164,17 +1189,6 @@ class LoanBroker_test : public beast::unit_test::suite return mpt; }, CoverClawback); - // MPTCanLock is not set - testLoanBroker( - [&](Env& env, Account const& issuer, Account const& alice) -> MPT { - MPTTester mpt( - {.env = env, - .issuer = issuer, - .holders = {alice}, - .flags = MPTDEXFlags | tfMPTCanClawback}); - return mpt; - }, - CoverClawback); } void diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp index 2a5a212211..26e978697c 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp @@ -192,8 +192,7 @@ preclaimHelper( if (!sleIssuance) return tecOBJECT_NOT_FOUND; - if (!sleIssuance->isFlag(lsfMPTCanClawback) || - !sleIssuance->isFlag(lsfMPTCanLock)) + if (!sleIssuance->isFlag(lsfMPTCanClawback)) return tecNO_PERMISSION; // With all the checking already done, this should be impossible From 86e71daa861ca47fb4336dbb9a7c38d0b99c1d55 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 1 Dec 2025 12:20:02 -0500 Subject: [PATCH 285/291] Placeholder From b772f6281e3e1d0a1994175808731172c065bf47 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 2 Dec 2025 12:20:43 -0500 Subject: [PATCH 286/291] Revert "Implement Lending Protocol (unsupported) (#5270)" This reverts commit 6c67f1f525aa902e7c3cc3ff43b501ec5915da59. --- include/xrpl/basics/Number.h | 32 +- include/xrpl/beast/utility/instrumentation.h | 2 +- include/xrpl/json/json_value.h | 3 - include/xrpl/ledger/ApplyView.h | 39 - include/xrpl/ledger/View.h | 251 +- include/xrpl/protocol/Asset.h | 22 +- include/xrpl/protocol/Indexes.h | 18 - include/xrpl/protocol/LedgerFormats.h | 5 - include/xrpl/protocol/Protocol.h | 135 - include/xrpl/protocol/SField.h | 4 +- include/xrpl/protocol/STAmount.h | 76 +- include/xrpl/protocol/STObject.h | 29 +- include/xrpl/protocol/TxFlags.h | 26 - include/xrpl/protocol/detail/features.macro | 5 - .../xrpl/protocol/detail/ledger_entries.macro | 119 +- include/xrpl/protocol/detail/sfields.macro | 45 +- .../xrpl/protocol/detail/transactions.macro | 133 - include/xrpl/protocol/jss.h | 5 - src/libxrpl/basics/Number.cpp | 158 +- src/libxrpl/json/json_value.cpp | 6 - src/libxrpl/ledger/ApplyView.cpp | 265 +- src/libxrpl/ledger/View.cpp | 842 +- src/libxrpl/protocol/Indexes.cpp | 14 - src/libxrpl/protocol/InnerObjectFormats.cpp | 8 - src/libxrpl/protocol/STAmount.cpp | 35 +- src/libxrpl/protocol/STTx.cpp | 5 +- src/test/app/Batch_test.cpp | 202 - src/test/app/Invariants_test.cpp | 441 +- src/test/app/LoanBroker_test.cpp | 1462 ---- src/test/app/Loan_test.cpp | 7215 ----------------- src/test/app/MPToken_test.cpp | 6 +- src/test/app/Vault_test.cpp | 221 +- src/test/basics/Number_test.cpp | 110 - src/test/jtx.h | 1 - src/test/jtx/Env.h | 4 +- src/test/jtx/JTx.h | 3 - src/test/jtx/TestHelpers.h | 129 - src/test/jtx/amount.h | 34 - src/test/jtx/batch.h | 3 +- src/test/jtx/impl/Env.cpp | 28 +- src/test/jtx/impl/TestHelpers.cpp | 128 - src/test/jtx/impl/mpt.cpp | 264 +- src/test/jtx/impl/testline.cpp | 15 - src/test/jtx/mpt.h | 84 +- src/test/jtx/testline.h | 34 - src/test/protocol/STParsedJSON_test.cpp | 40 +- .../app/ledger/detail/LedgerReplayTask.cpp | 2 +- src/xrpld/app/ledger/detail/OpenLedger.cpp | 5 +- src/xrpld/app/misc/LendingHelpers.h | 444 - src/xrpld/app/misc/NetworkOPs.cpp | 17 +- src/xrpld/app/misc/detail/LendingHelpers.cpp | 2058 ----- src/xrpld/app/tx/detail/Batch.cpp | 15 +- src/xrpld/app/tx/detail/Batch.h | 18 - src/xrpld/app/tx/detail/Change.cpp | 6 +- src/xrpld/app/tx/detail/InvariantCheck.cpp | 500 +- src/xrpld/app/tx/detail/InvariantCheck.h | 123 +- .../app/tx/detail/LoanBrokerCoverClawback.cpp | 340 - .../app/tx/detail/LoanBrokerCoverClawback.h | 34 - .../app/tx/detail/LoanBrokerCoverDeposit.cpp | 123 - .../app/tx/detail/LoanBrokerCoverDeposit.h | 34 - .../app/tx/detail/LoanBrokerCoverWithdraw.cpp | 173 - .../app/tx/detail/LoanBrokerCoverWithdraw.h | 34 - src/xrpld/app/tx/detail/LoanBrokerDelete.cpp | 197 - src/xrpld/app/tx/detail/LoanBrokerDelete.h | 34 - src/xrpld/app/tx/detail/LoanBrokerSet.cpp | 215 - src/xrpld/app/tx/detail/LoanBrokerSet.h | 34 - src/xrpld/app/tx/detail/LoanDelete.cpp | 133 - src/xrpld/app/tx/detail/LoanDelete.h | 34 - src/xrpld/app/tx/detail/LoanManage.cpp | 420 - src/xrpld/app/tx/detail/LoanManage.h | 66 - src/xrpld/app/tx/detail/LoanPay.cpp | 602 -- src/xrpld/app/tx/detail/LoanPay.h | 40 - src/xrpld/app/tx/detail/LoanSet.cpp | 632 -- src/xrpld/app/tx/detail/LoanSet.h | 59 - src/xrpld/app/tx/detail/MPTokenAuthorize.cpp | 9 +- src/xrpld/app/tx/detail/SetTrust.cpp | 4 +- src/xrpld/app/tx/detail/Transactor.cpp | 32 +- src/xrpld/app/tx/detail/Transactor.h | 35 +- src/xrpld/app/tx/detail/VaultCreate.cpp | 7 +- src/xrpld/app/tx/detail/VaultDeposit.cpp | 36 +- src/xrpld/app/tx/detail/VaultSet.cpp | 7 +- src/xrpld/app/tx/detail/VaultWithdraw.cpp | 105 +- src/xrpld/overlay/detail/PeerImp.cpp | 38 +- src/xrpld/rpc/detail/TransactionSign.cpp | 6 +- src/xrpld/rpc/handlers/AccountInfo.cpp | 22 - src/xrpld/rpc/handlers/LedgerEntry.cpp | 40 - 86 files changed, 591 insertions(+), 18848 deletions(-) delete mode 100644 src/test/app/LoanBroker_test.cpp delete mode 100644 src/test/app/Loan_test.cpp delete mode 100644 src/test/jtx/impl/testline.cpp delete mode 100644 src/test/jtx/testline.h delete mode 100644 src/xrpld/app/misc/LendingHelpers.h delete mode 100644 src/xrpld/app/misc/detail/LendingHelpers.cpp delete mode 100644 src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp delete mode 100644 src/xrpld/app/tx/detail/LoanBrokerCoverClawback.h delete mode 100644 src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp delete mode 100644 src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.h delete mode 100644 src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp delete mode 100644 src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h delete mode 100644 src/xrpld/app/tx/detail/LoanBrokerDelete.cpp delete mode 100644 src/xrpld/app/tx/detail/LoanBrokerDelete.h delete mode 100644 src/xrpld/app/tx/detail/LoanBrokerSet.cpp delete mode 100644 src/xrpld/app/tx/detail/LoanBrokerSet.h delete mode 100644 src/xrpld/app/tx/detail/LoanDelete.cpp delete mode 100644 src/xrpld/app/tx/detail/LoanDelete.h delete mode 100644 src/xrpld/app/tx/detail/LoanManage.cpp delete mode 100644 src/xrpld/app/tx/detail/LoanManage.h delete mode 100644 src/xrpld/app/tx/detail/LoanPay.cpp delete mode 100644 src/xrpld/app/tx/detail/LoanPay.h delete mode 100644 src/xrpld/app/tx/detail/LoanSet.cpp delete mode 100644 src/xrpld/app/tx/detail/LoanSet.h diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index 4a4ee1cacb..e34cc61b5b 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -13,15 +13,6 @@ class Number; std::string to_string(Number const& amount); -template -constexpr bool -isPowerOfTen(T value) -{ - while (value >= 10 && value % 10 == 0) - value /= 10; - return value == 1; -} - class Number { using rep = std::int64_t; @@ -31,9 +22,7 @@ class Number public: // The range for the mantissa when normalized constexpr static std::int64_t minMantissa = 1'000'000'000'000'000LL; - static_assert(isPowerOfTen(minMantissa)); - constexpr static std::int64_t maxMantissa = minMantissa * 10 - 1; - static_assert(maxMantissa == 9'999'999'999'999'999LL); + constexpr static std::int64_t maxMantissa = 9'999'999'999'999'999LL; // The range for the exponent when normalized constexpr static int minExponent = -32768; @@ -143,7 +132,22 @@ public: } Number - truncate() const noexcept; + truncate() const noexcept + { + if (exponent_ >= 0 || mantissa_ == 0) + return *this; + + Number ret = *this; + while (ret.exponent_ < 0 && ret.mantissa_ != 0) + { + ret.exponent_ += 1; + ret.mantissa_ /= rep(10); + } + // We are guaranteed that normalize() will never throw an exception + // because exponent is either negative or zero at this point. + ret.normalize(); + return ret; + } friend constexpr bool operator>(Number const& x, Number const& y) noexcept @@ -188,8 +192,6 @@ private: class Guard; }; -constexpr static Number numZero{}; - inline constexpr Number::Number(rep mantissa, int exponent, unchecked) noexcept : mantissa_{mantissa}, exponent_{exponent} { diff --git a/include/xrpl/beast/utility/instrumentation.h b/include/xrpl/beast/utility/instrumentation.h index 3c364b64e6..1234e3075c 100644 --- a/include/xrpl/beast/utility/instrumentation.h +++ b/include/xrpl/beast/utility/instrumentation.h @@ -13,7 +13,7 @@ // The duplication is because Visual Studio 2019 cannot compile that header // even with the option -Zc:__cplusplus added. #define ALWAYS(cond, message, ...) assert((message) && (cond)) -#define ALWAYS_OR_UNREACHABLE(cond, message) assert((message) && (cond)) +#define ALWAYS_OR_UNREACHABLE(cond, message, ...) assert((message) && (cond)) #define SOMETIMES(cond, message, ...) #define REACHABLE(message, ...) #define UNREACHABLE(message, ...) assert((message) && false) diff --git a/include/xrpl/json/json_value.h b/include/xrpl/json/json_value.h index c14d5d5127..6b460ecd3b 100644 --- a/include/xrpl/json/json_value.h +++ b/include/xrpl/json/json_value.h @@ -381,9 +381,6 @@ public: /// Return true if the object has a member named key. bool isMember(std::string const& key) const; - /// Return true if the object has a member named key. - bool - isMember(StaticString const& key) const; /// \brief Return a list of the member names. /// diff --git a/include/xrpl/ledger/ApplyView.h b/include/xrpl/ledger/ApplyView.h index 5e283641da..fc82e94b5c 100644 --- a/include/xrpl/ledger/ApplyView.h +++ b/include/xrpl/ledger/ApplyView.h @@ -368,45 +368,6 @@ public: emptyDirDelete(Keylet const& directory); }; -namespace directory { -/** Helper functions for managing low-level directory operations. - These are not part of the ApplyView interface. - - Don't use them unless you really, really know what you're doing. - Instead use dirAdd, dirInsert, etc. - */ - -std::uint64_t -createRoot( - ApplyView& view, - Keylet const& directory, - uint256 const& key, - std::function const&)> const& describe); - -auto -findPreviousPage(ApplyView& view, Keylet const& directory, SLE::ref start); - -std::uint64_t -insertKey( - ApplyView& view, - SLE::ref node, - std::uint64_t page, - bool preserveOrder, - STVector256& indexes, - uint256 const& key); - -std::optional -insertPage( - ApplyView& view, - std::uint64_t page, - SLE::pointer node, - std::uint64_t nextPage, - SLE::ref next, - uint256 const& key, - Keylet const& directory, - std::function const&)> const& describe); - -} // namespace directory } // namespace ripple #endif diff --git a/include/xrpl/ledger/View.h b/include/xrpl/ledger/View.h index fc16d53e61..9635115f0c 100644 --- a/include/xrpl/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include @@ -224,80 +223,6 @@ isDeepFrozen( Currency const& currency, AccountID const& issuer); -[[nodiscard]] inline bool -isDeepFrozen( - ReadView const& view, - AccountID const& account, - Issue const& issue, - int = 0 /*ignored*/) -{ - return isDeepFrozen(view, account, issue.currency, issue.account); -} - -[[nodiscard]] inline bool -isDeepFrozen( - ReadView const& view, - AccountID const& account, - MPTIssue const& mptIssue, - int depth = 0) -{ - // Unlike IOUs, frozen / locked MPTs are not allowed to send or receive - // funds, so checking "deep frozen" is the same as checking "frozen". - return isFrozen(view, account, mptIssue, depth); -} - -/** - * isFrozen check is recursive for MPT shares in a vault, descending to - * assets in the vault, up to maxAssetCheckDepth recursion depth. This is - * purely defensive, as we currently do not allow such vaults to be created. - */ -[[nodiscard]] inline bool -isDeepFrozen( - ReadView const& view, - AccountID const& account, - Asset const& asset, - int depth = 0) -{ - return std::visit( - [&](auto const& issue) { - return isDeepFrozen(view, account, issue, depth); - }, - asset.value()); -} - -[[nodiscard]] inline TER -checkDeepFrozen( - ReadView const& view, - AccountID const& account, - Issue const& issue) -{ - return isDeepFrozen(view, account, issue) ? (TER)tecFROZEN - : (TER)tesSUCCESS; -} - -[[nodiscard]] inline TER -checkDeepFrozen( - ReadView const& view, - AccountID const& account, - MPTIssue const& mptIssue) -{ - return isDeepFrozen(view, account, mptIssue) ? (TER)tecLOCKED - : (TER)tesSUCCESS; -} - -[[nodiscard]] inline TER -checkDeepFrozen( - ReadView const& view, - AccountID const& account, - Asset const& asset) -{ - return std::visit( - [&](auto const& issue) { - return checkDeepFrozen(view, account, issue); - }, - asset.value()); -} - [[nodiscard]] bool isLPTokenFrozen( ReadView const& view, @@ -343,49 +268,6 @@ accountHolds( AuthHandling zeroIfUnauthorized, beast::Journal j); -// Returns the amount an account can spend total. -// -// These functions use accountHolds, but unlike accountHolds: -// * The account can go into debt. -// * If the account is the asset issuer the only limit is defined by the asset / -// issuance. -// -// <-- saAmount: amount of currency held by account. May be negative. -[[nodiscard]] STAmount -accountSpendable( - ReadView const& view, - AccountID const& account, - Currency const& currency, - AccountID const& issuer, - FreezeHandling zeroIfFrozen, - beast::Journal j); - -[[nodiscard]] STAmount -accountSpendable( - ReadView const& view, - AccountID const& account, - Issue const& issue, - FreezeHandling zeroIfFrozen, - beast::Journal j); - -[[nodiscard]] STAmount -accountSpendable( - ReadView const& view, - AccountID const& account, - MPTIssue const& mptIssue, - FreezeHandling zeroIfFrozen, - AuthHandling zeroIfUnauthorized, - beast::Journal j); - -[[nodiscard]] STAmount -accountSpendable( - ReadView const& view, - AccountID const& account, - Asset const& asset, - FreezeHandling zeroIfFrozen, - AuthHandling zeroIfUnauthorized, - beast::Journal j); - // Returns the amount an account can spend of the currency type saDefault, or // returns saDefault if this account is the issuer of the currency in // question. Should be used in favor of accountHolds when questioning how much @@ -632,11 +514,7 @@ dirNext( describeOwnerDir(AccountID const& account); [[nodiscard]] TER -dirLink( - ApplyView& view, - AccountID const& owner, - std::shared_ptr& object, - SF_UINT64 const& node = sfOwnerNode); +dirLink(ApplyView& view, AccountID const& owner, std::shared_ptr& object); AccountID pseudoAccountAddress(ReadView const& view, uint256 const& pseudoOwnerKey); @@ -655,17 +533,14 @@ createPseudoAccount( uint256 const& pseudoOwnerKey, SField const& ownerField); -// Returns true iff sleAcct is a pseudo-account or specific -// pseudo-accounts in pseudoFieldFilter. +// Returns true iff sleAcct is a pseudo-account. // // Returns false if sleAcct is // * NOT a pseudo-account OR // * NOT a ltACCOUNT_ROOT OR // * null pointer [[nodiscard]] bool -isPseudoAccount( - std::shared_ptr sleAcct, - std::set const& pseudoFieldFilter = {}); +isPseudoAccount(std::shared_ptr sleAcct); // Returns the list of fields that define an ACCOUNT_ROOT as a pseudo-account if // set @@ -679,91 +554,14 @@ isPseudoAccount( getPseudoAccountFields(); [[nodiscard]] inline bool -isPseudoAccount( - ReadView const& view, - AccountID const& accountId, - std::set const& pseudoFieldFilter = {}) +isPseudoAccount(ReadView const& view, AccountID accountId) { - return isPseudoAccount( - view.read(keylet::account(accountId)), pseudoFieldFilter); + return isPseudoAccount(view.read(keylet::account(accountId))); } [[nodiscard]] TER canAddHolding(ReadView const& view, Asset const& asset); -/** Validates that the destination SLE and tag are valid - - - Checks that the SLE is not null. - - If the SLE requires a destination tag, checks that there is a tag. -*/ -[[nodiscard]] TER -checkDestinationAndTag(SLE::const_ref toSle, bool hasDestinationTag); - -/** Checks that can withdraw funds from an object to itself or a destination. - * - * The receiver may be either the submitting account (sfAccount) or a different - * destination account (sfDestination). - * - * - Checks that the receiver account exists. - * - If the receiver requires a destination tag, check that one exists, even - * if withdrawing to self. - * - If withdrawing to self, succeed. - * - If not, checks if the receiver requires deposit authorization, and if - * the sender has it. - */ -[[nodiscard]] TER -canWithdraw( - AccountID const& from, - ReadView const& view, - AccountID const& to, - SLE::const_ref toSle, - bool hasDestinationTag); - -/** Checks that can withdraw funds from an object to itself or a destination. - * - * The receiver may be either the submitting account (sfAccount) or a different - * destination account (sfDestination). - * - * - Checks that the receiver account exists. - * - If the receiver requires a destination tag, check that one exists, even - * if withdrawing to self. - * - If withdrawing to self, succeed. - * - If not, checks if the receiver requires deposit authorization, and if - * the sender has it. - */ -[[nodiscard]] TER -canWithdraw( - AccountID const& from, - ReadView const& view, - AccountID const& to, - bool hasDestinationTag); - -/** Checks that can withdraw funds from an object to itself or a destination. - * - * The receiver may be either the submitting account (sfAccount) or a different - * destination account (sfDestination). - * - * - Checks that the receiver account exists. - * - If the receiver requires a destination tag, check that one exists, even - * if withdrawing to self. - * - If withdrawing to self, succeed. - * - If not, checks if the receiver requires deposit authorization, and if - * the sender has it. - */ -[[nodiscard]] TER -canWithdraw(ReadView const& view, STTx const& tx); - -[[nodiscard]] TER -doWithdraw( - ApplyView& view, - STTx const& tx, - AccountID const& senderAcct, - AccountID const& dstAcct, - AccountID const& sourceAcct, - XRPAmount priorBalance, - STAmount const& amount, - beast::Journal j); - /// Any transactors that call addEmptyHolding() in doApply must call /// canAddHolding() in preflight with the same View and Asset [[nodiscard]] TER @@ -933,22 +731,6 @@ accountSend( beast::Journal j, WaiveTransferFee waiveFee = WaiveTransferFee::No); -using MultiplePaymentDestinations = std::vector>; -/** Like accountSend, except one account is sending multiple payments (with the - * same asset!) simultaneously - * - * Calls static accountSendMultiIOU if saAmount represents Issue. - * Calls static accountSendMultiMPT if saAmount represents MPTIssue. - */ -[[nodiscard]] TER -accountSendMulti( - ApplyView& view, - AccountID const& senderID, - Asset const& asset, - MultiplePaymentDestinations const& receivers, - beast::Journal j, - WaiveTransferFee waiveFee = WaiveTransferFee::No); - [[nodiscard]] TER issueIOU( ApplyView& view, @@ -1020,8 +802,7 @@ requireAuth( * purely defensive, as we currently do not allow such vaults to be created. * * If StrongAuth then return tecNO_AUTH if MPToken doesn't exist or - * lsfMPTRequireAuth is set and MPToken is not authorized. Vault and LoanBroker - * pseudo-accounts are implicitly authorized. + * lsfMPTRequireAuth is set and MPToken is not authorized. * * If WeakAuth then return tecNO_AUTH if lsfMPTRequireAuth is set and MPToken * doesn't exist or is not authorized (explicitly or via credentials, if @@ -1094,26 +875,6 @@ canTransfer( AccountID const& from, AccountID const& to); -[[nodiscard]] TER -canTransfer( - ReadView const& view, - Issue const& issue, - AccountID const& from, - AccountID const& to); - -[[nodiscard]] TER inline canTransfer( - ReadView const& view, - Asset const& asset, - AccountID const& from, - AccountID const& to) -{ - return std::visit( - [&](TIss const& issue) -> TER { - return canTransfer(view, issue, from, to); - }, - asset.value()); -} - /** Deleter function prototype. Returns the status of the entry deletion * (if should not be skipped) and if the entry should be skipped. The status * is always tesSUCCESS if the entry should be skipped. diff --git a/include/xrpl/protocol/Asset.h b/include/xrpl/protocol/Asset.h index 7ad1e70256..d0efe814a9 100644 --- a/include/xrpl/protocol/Asset.h +++ b/include/xrpl/protocol/Asset.h @@ -81,27 +81,7 @@ public: bool native() const { - return std::visit( - [&](TIss const& issue) { - if constexpr (std::is_same_v) - return issue.native(); - if constexpr (std::is_same_v) - return false; - }, - issue_); - } - - bool - integral() const - { - return std::visit( - [&](TIss const& issue) { - if constexpr (std::is_same_v) - return issue.native(); - if constexpr (std::is_same_v) - return true; - }, - issue_); + return holds() && get().native(); } friend constexpr bool diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index 69418fbd25..b48ed64d71 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -327,24 +327,6 @@ vault(uint256 const& vaultKey) return {ltVAULT, vaultKey}; } -Keylet -loanbroker(AccountID const& owner, std::uint32_t seq) noexcept; - -inline Keylet -loanbroker(uint256 const& key) -{ - return {ltLOAN_BROKER, key}; -} - -Keylet -loan(uint256 const& loanBrokerID, std::uint32_t loanSeq) noexcept; - -inline Keylet -loan(uint256 const& key) -{ - return {ltLOAN, key}; -} - Keylet permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept; diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index 64ae604f64..9e76f3bde7 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -186,11 +186,6 @@ enum LedgerSpecificFlags { // ltVAULT lsfVaultPrivate = 0x00010000, - - // ltLOAN - lsfLoanDefault = 0x00010000, - lsfLoanImpaired = 0x00020000, - lsfLoanOverpayment = 0x00040000, // True, loan allows overpayments }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index ded16cad6d..20b73e90c9 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -3,7 +3,6 @@ #include #include -#include #include @@ -66,140 +65,6 @@ std::size_t constexpr maxDeletableTokenOfferEntries = 500; */ std::uint16_t constexpr maxTransferFee = 50000; -/** There are 10,000 basis points (bips) in 100%. - * - * Basis points represent 0.01%. - * - * Given a value X, to find the amount for B bps, - * use X * B / bipsPerUnity - * - * Example: If a loan broker has 999 XRP of debt, and must maintain 1,000 bps of - * that debt as cover (10%), then the minimum cover amount is 999,000,000 drops - * * 1000 / bipsPerUnity = 99,900,00 drops or 99.9 XRP. - * - * Given a percentage P, to find the number of bps that percentage represents, - * use P * bipsPerUnity. - * - * Example: 50% is 0.50 * bipsPerUnity = 5,000 bps. - */ -Bips32 constexpr bipsPerUnity(100 * 100); -static_assert(bipsPerUnity == Bips32{10'000}); -TenthBips32 constexpr tenthBipsPerUnity(bipsPerUnity.value() * 10); -static_assert(tenthBipsPerUnity == TenthBips32(100'000)); - -constexpr Bips32 -percentageToBips(std::uint32_t percentage) -{ - return Bips32(percentage * bipsPerUnity.value() / 100); -} -constexpr TenthBips32 -percentageToTenthBips(std::uint32_t percentage) -{ - return TenthBips32(percentage * tenthBipsPerUnity.value() / 100); -} -template -constexpr T -bipsOfValue(T value, Bips bips) -{ - return value * bips.value() / bipsPerUnity.value(); -} -template -constexpr T -tenthBipsOfValue(T value, TenthBips bips) -{ - return value * bips.value() / tenthBipsPerUnity.value(); -} - -namespace Lending { -/** The maximum management fee rate allowed by a loan broker in 1/10 bips. - - Valid values are between 0 and 10% inclusive. -*/ -TenthBips16 constexpr maxManagementFeeRate( - unsafe_cast(percentageToTenthBips(10).value())); -static_assert(maxManagementFeeRate == TenthBips16(std::uint16_t(10'000u))); - -/** The maximum coverage rate required of a loan broker in 1/10 bips. - - Valid values are between 0 and 100% inclusive. -*/ -TenthBips32 constexpr maxCoverRate = percentageToTenthBips(100); -static_assert(maxCoverRate == TenthBips32(100'000u)); - -/** The maximum overpayment fee on a loan in 1/10 bips. -* - Valid values are between 0 and 100% inclusive. -*/ -TenthBips32 constexpr maxOverpaymentFee = percentageToTenthBips(100); -static_assert(maxOverpaymentFee == TenthBips32(100'000u)); - -/** Annualized interest rate of the Loan in 1/10 bips. - * - * Valid values are between 0 and 100% inclusive. - */ -TenthBips32 constexpr maxInterestRate = percentageToTenthBips(100); -static_assert(maxInterestRate == TenthBips32(100'000u)); - -/** The maximum premium added to the interest rate for late payments on a loan - * in 1/10 bips. - * - * Valid values are between 0 and 100% inclusive. - */ -TenthBips32 constexpr maxLateInterestRate = percentageToTenthBips(100); -static_assert(maxLateInterestRate == TenthBips32(100'000u)); - -/** The maximum close interest rate charged for repaying a loan early in 1/10 - * bips. - * - * Valid values are between 0 and 100% inclusive. - */ -TenthBips32 constexpr maxCloseInterestRate = percentageToTenthBips(100); -static_assert(maxCloseInterestRate == TenthBips32(100'000u)); - -/** The maximum overpayment interest rate charged on loan overpayments in 1/10 - * bips. - * - * Valid values are between 0 and 100% inclusive. - */ -TenthBips32 constexpr maxOverpaymentInterestRate = percentageToTenthBips(100); -static_assert(maxOverpaymentInterestRate == TenthBips32(100'000u)); - -/** LoanPay transaction cost will be one base fee per X combined payments - * - * The number of payments is estimated based on the Amount paid and the Loan's - * Fixed Payment size. Overpayments (indicated with the tfLoanOverpayment flag) - * count as one more payment. - * - * This number was chosen arbitrarily, but should not be changed once released - * without an amendment - */ -static constexpr int loanPaymentsPerFeeIncrement = 5; - -/** Maximum number of combined payments that a LoanPay transaction will process - * - * This limit is enforced during the loan payment process, and thus is not - * estimated. If the limit is hit, no further payments or overpayments will be - * processed, no matter how much of the transation Amount is left, but the - * transaction will succeed with the payments that have been processed up to - * that point. - * - * This limit is independent of loanPaymentsPerFeeIncrement, so a transaction - * could potentially be charged for many more payments than actually get - * processed. Users should take care not to submit a transaction paying more - * than loanMaximumPaymentsPerTransaction * Loan.PeriodicPayment. Because - * overpayments are charged as a payment, if submitting - * loanMaximumPaymentsPerTransaction * Loan.PeriodicPayment, users should not - * set the tfLoanOverpayment flag. - * - * Even though they're independent, loanMaximumPaymentsPerTransaction should be - * a multiple of loanPaymentsPerFeeIncrement. - * - * This number was chosen arbitrarily, but should not be changed once released - * without an amendment - */ -static constexpr int loanMaximumPaymentsPerTransaction = 100; -} // namespace Lending - /** The maximum length of a URI inside an NFT */ std::size_t constexpr maxTokenURILength = 256; diff --git a/include/xrpl/protocol/SField.h b/include/xrpl/protocol/SField.h index 82619b1ced..ea6b51dbab 100644 --- a/include/xrpl/protocol/SField.h +++ b/include/xrpl/protocol/SField.h @@ -120,8 +120,8 @@ field_code(int id, int index) SFields are created at compile time. Each SField, once constructed, lives until program termination, and there - is only one instance per fieldType/fieldValue pair which serves the - entire application. + is only one instance per fieldType/fieldValue pair which serves the entire + application. */ class SField { diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index 3ea45cc05b..83493efcdd 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -47,18 +47,16 @@ public: static int const cMaxOffset = 80; // Maximum native value supported by the code - constexpr static std::uint64_t cMinValue = 1'000'000'000'000'000ull; - static_assert(isPowerOfTen(cMinValue)); - constexpr static std::uint64_t cMaxValue = cMinValue * 10 - 1; - static_assert(cMaxValue == 9'999'999'999'999'999ull); - constexpr static std::uint64_t cMaxNative = 9'000'000'000'000'000'000ull; + static std::uint64_t const cMinValue = 1000000000000000ull; + static std::uint64_t const cMaxValue = 9999999999999999ull; + static std::uint64_t const cMaxNative = 9000000000000000000ull; // Max native value on network. - constexpr static std::uint64_t cMaxNativeN = 100'000'000'000'000'000ull; - constexpr static std::uint64_t cIssuedCurrency = 0x8'000'000'000'000'000ull; - constexpr static std::uint64_t cPositive = 0x4'000'000'000'000'000ull; - constexpr static std::uint64_t cMPToken = 0x2'000'000'000'000'000ull; - constexpr static std::uint64_t cValueMask = ~(cPositive | cMPToken); + static std::uint64_t const cMaxNativeN = 100000000000000000ull; + static std::uint64_t const cIssuedCurrency = 0x8000000000000000ull; + static std::uint64_t const cPositive = 0x4000000000000000ull; + static std::uint64_t const cMPToken = 0x2000000000000000ull; + static std::uint64_t const cValueMask = ~(cPositive | cMPToken); static std::uint64_t const uRateOne; @@ -157,9 +155,6 @@ public: int exponent() const noexcept; - bool - integral() const noexcept; - bool native() const noexcept; @@ -440,12 +435,6 @@ STAmount::exponent() const noexcept return mOffset; } -inline bool -STAmount::integral() const noexcept -{ - return mAsset.integral(); -} - inline bool STAmount::native() const noexcept { @@ -564,7 +553,7 @@ STAmount::clear() { // The -100 is used to allow 0 to sort less than a small positive values // which have a negative exponent. - mOffset = integral() ? 0 : -100; + mOffset = native() ? 0 : -100; mValue = 0; mIsNegative = false; } @@ -687,53 +676,6 @@ divRoundStrict( std::uint64_t getRate(STAmount const& offerOut, STAmount const& offerIn); -/** Round an arbitrary precision Amount to the precision of an STAmount that has - * a given exponent. - * - * This is used to ensure that calculations involving IOU amounts do not collect - * dust beyond the precision of the reference value. - * - * @param value The value to be rounded - * @param scale An exponent value to establish the precision limit of - * `value`. Should be larger than `value.exponent()`. - * @param rounding Optional Number rounding mode - * - */ -STAmount -roundToScale( - STAmount const& value, - std::int32_t scale, - Number::rounding_mode rounding = Number::getround()); - -/** Round an arbitrary precision Number to the precision of a given Asset. - * - * This is used to ensure that calculations do not collect dust beyond the - * precision of the reference value for IOUs, or fractional amounts for the - * integral types XRP and MPT. - * - * @param asset The relevant asset - * @param value The value to be rounded - * @param scale Only relevant to IOU assets. An exponent value to establish the - * precision limit of `value`. Should be larger than `value.exponent()`. - * @param rounding Optional Number rounding mode - */ -template -Number -roundToAsset( - A const& asset, - Number const& value, - std::int32_t scale, - Number::rounding_mode rounding = Number::getround()) -{ - NumberRoundModeGuard mg(rounding); - STAmount const ret{asset, value}; - if (ret.integral()) - return ret; - // Note that the ctor will round integral types (XRP, MPT) via canonicalize, - // so no extra work is needed for those. - return roundToScale(ret, scale); -} - //------------------------------------------------------------------------------ inline bool diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index 1a553e0fbf..9b325f06fe 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -482,8 +482,6 @@ public: value_type operator*() const; - /// Do not use operator->() unless the field is required, or you've checked - /// that it's set. T const* operator->() const; @@ -507,26 +505,7 @@ protected: // Constraint += and -= ValueProxy operators // to value types that support arithmetic operations template -concept IsArithmeticNumber = std::is_arithmetic_v || - std::is_same_v || std::is_same_v; -template < - typename U, - typename Value = typename U::value_type, - typename Unit = typename U::unit_type> -concept IsArithmeticValueUnit = - std::is_same_v> && - IsArithmeticNumber && std::is_class_v; -template -concept IsArithmeticST = !IsArithmeticValueUnit && IsArithmeticNumber; -template -concept IsArithmetic = - IsArithmeticNumber || IsArithmeticST || IsArithmeticValueUnit; - -template -concept Addable = requires(T t, U u) { t = t + u; }; -template -concept IsArithmeticCompatible = - IsArithmetic && Addable; +concept IsArithmetic = std::is_arithmetic_v || std::is_same_v; template class STObject::ValueProxy : public Proxy @@ -546,12 +525,10 @@ public: // Convenience operators for value types supporting // arithmetic operations template - requires IsArithmeticCompatible ValueProxy& operator+=(U const& u); template - requires IsArithmeticCompatible ValueProxy& operator-=(U const& u); @@ -741,8 +718,6 @@ STObject::Proxy::operator*() const -> value_type return this->value(); } -/// Do not use operator->() unless the field is required, or you've checked that -/// it's set. template T const* STObject::Proxy::operator->() const @@ -789,7 +764,6 @@ STObject::ValueProxy::operator=(U&& u) template template - requires IsArithmeticCompatible STObject::ValueProxy& STObject::ValueProxy::operator+=(U const& u) { @@ -799,7 +773,6 @@ STObject::ValueProxy::operator+=(U const& u) template template - requires IsArithmeticCompatible STObject::ValueProxy& STObject::ValueProxy::operator-=(U const& u) { diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index d4faed192c..b067e8487b 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -266,32 +266,6 @@ constexpr std::uint32_t tfIndependent = 0x00080000; constexpr std::uint32_t const tfBatchMask = ~(tfUniversal | tfAllOrNothing | tfOnlyOne | tfUntilFailure | tfIndependent) | tfInnerBatchTxn; -// LoanSet and LoanPay flags: -// LoanSet: True, indicates the loan supports overpayments -// LoanPay: True, indicates any excess in this payment can be used -// as an overpayment. False, no overpayments will be taken. -constexpr std::uint32_t const tfLoanOverpayment = 0x00010000; -// LoanPay exclusive flags: -// tfLoanFullPayment: True, indicates that the payment is an early -// full payment. It must pay the entire loan including close -// interest and fees, or it will fail. False: Not a full payment. -constexpr std::uint32_t const tfLoanFullPayment = 0x00020000; -// tfLoanLatePayment: True, indicates that the payment is late, -// and includes late iterest and fees. If the loan is not late, -// it will fail. False: not a late payment. If the current payment -// is overdue, the transaction will fail. -constexpr std::uint32_t const tfLoanLatePayment = 0x00040000; -constexpr std::uint32_t const tfLoanSetMask = ~(tfUniversal | - tfLoanOverpayment); -constexpr std::uint32_t const tfLoanPayMask = ~(tfUniversal | - tfLoanOverpayment | tfLoanFullPayment | tfLoanLatePayment); - -// LoanManage flags: -constexpr std::uint32_t const tfLoanDefault = 0x00010000; -constexpr std::uint32_t const tfLoanImpair = 0x00020000; -constexpr std::uint32_t const tfLoanUnimpair = 0x00040000; -constexpr std::uint32_t const tfLoanManageMask = ~(tfUniversal | tfLoanDefault | tfLoanImpair | tfLoanUnimpair); - // clang-format on } // namespace ripple diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 5c8d2aa198..5f1eca7afe 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -11,12 +11,9 @@ #error "undefined macro: XRPL_RETIRE_FIX" #endif -// clang-format off - // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. -XRPL_FEATURE(LendingProtocol, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(PermissionDelegationV1_1, Supported::no, VoteBehavior::DefaultNo) XRPL_FIX (DirectoryLimit, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (IncludeKeyletFields, Supported::yes, VoteBehavior::DefaultNo) @@ -140,5 +137,3 @@ XRPL_RETIRE_FEATURE(SortedDirectories) XRPL_RETIRE_FEATURE(TicketBatch) XRPL_RETIRE_FEATURE(TickSize) XRPL_RETIRE_FEATURE(TrustSetAuth) - -// clang-format on diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 1034c35895..53110f09f5 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -149,7 +149,6 @@ 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. @@ -480,10 +479,10 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({ {sfAccount, soeREQUIRED}, {sfData, soeOPTIONAL}, {sfAsset, soeREQUIRED}, - {sfAssetsTotal, soeDEFAULT}, - {sfAssetsAvailable, soeDEFAULT}, + {sfAssetsTotal, soeREQUIRED}, + {sfAssetsAvailable, soeREQUIRED}, {sfAssetsMaximum, soeDEFAULT}, - {sfLossUnrealized, soeDEFAULT}, + {sfLossUnrealized, soeREQUIRED}, {sfShareMPTID, soeREQUIRED}, {sfWithdrawalPolicy, soeREQUIRED}, {sfScale, soeDEFAULT}, @@ -491,117 +490,5 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({ // no PermissionedDomainID ever (use MPTIssuance.sfDomainID) })) -/** Reserve 0x0084-0x0087 for future Vault-related objects. */ - -/** A ledger object representing a loan broker - - \sa keylet::loanbroker - */ -LEDGER_ENTRY(ltLOAN_BROKER, 0x0088, LoanBroker, loan_broker, ({ - {sfPreviousTxnID, soeREQUIRED}, - {sfPreviousTxnLgrSeq, soeREQUIRED}, - {sfSequence, soeREQUIRED}, - {sfOwnerNode, soeREQUIRED}, - {sfVaultNode, soeREQUIRED}, - {sfVaultID, soeREQUIRED}, - {sfAccount, soeREQUIRED}, - {sfOwner, soeREQUIRED}, - {sfLoanSequence, soeREQUIRED}, - {sfData, soeDEFAULT}, - {sfManagementFeeRate, soeDEFAULT}, - {sfOwnerCount, soeDEFAULT}, - {sfDebtTotal, soeDEFAULT}, - {sfDebtMaximum, soeDEFAULT}, - {sfCoverAvailable, soeDEFAULT}, - {sfCoverRateMinimum, soeDEFAULT}, - {sfCoverRateLiquidation, soeDEFAULT}, -})) - -/** A ledger object representing a loan between a Borrower and a Loan Broker - - \sa keylet::loan - */ -LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({ - {sfPreviousTxnID, soeREQUIRED}, - {sfPreviousTxnLgrSeq, soeREQUIRED}, - {sfOwnerNode, soeREQUIRED}, - {sfLoanBrokerNode, soeREQUIRED}, - {sfLoanBrokerID, soeREQUIRED}, - {sfLoanSequence, soeREQUIRED}, - {sfBorrower, soeREQUIRED}, - {sfLoanOriginationFee, soeDEFAULT}, - {sfLoanServiceFee, soeDEFAULT}, - {sfLatePaymentFee, soeDEFAULT}, - {sfClosePaymentFee, soeDEFAULT}, - {sfOverpaymentFee, soeDEFAULT}, - {sfInterestRate, soeDEFAULT}, - {sfLateInterestRate, soeDEFAULT}, - {sfCloseInterestRate, soeDEFAULT}, - {sfOverpaymentInterestRate, soeDEFAULT}, - {sfStartDate, soeREQUIRED}, - {sfPaymentInterval, soeREQUIRED}, - {sfGracePeriod, soeDEFAULT}, - {sfPreviousPaymentDate, soeDEFAULT}, - {sfNextPaymentDueDate, soeDEFAULT}, - // The loan object tracks these values: - // - // - PaymentRemaining: The number of payments left in the loan. When it - // reaches 0, the loan is paid off, and all other relevant values - // must also be 0. - // - // - PeriodicPayment: The fixed, unrounded amount to be paid each - // interval. Stored with as much precision as possible. - // Payment transactions must round this value *UP*. - // - // - TotalValueOutstanding: The rounded total amount owed by the - // borrower to the lender / vault. - // - // - PrincipalOutstanding: The rounded portion of the - // TotalValueOutstanding that is from the principal borrowed. - // - // - ManagementFeeOutstanding: The rounded portion of the - // TotalValueOutstanding that represents management fees - // specifically owed to the broker based on the initial - // loan parameters. - // - // There are additional values that can be computed from these: - // - // - InterestOutstanding = TotalValueOutstanding - PrincipalOutstanding - // The total amount of interest still pending on the loan, - // independent of management fees. - // - // - InterestOwedToVault = InterestOutstanding - ManagementFeeOutstanding - // The amount of the total interest that is owed to the vault, and - // will be sent to it as part of a payment. - // - // - TrueTotalLoanValue = PaymentRemaining * PeriodicPayment - // The unrounded true total value of the loan. - // - // - TrueTotalPrincialOutstanding can be computed using the algorithm - // in the ripple::detail::loanPrincipalFromPeriodicPayment function. - // - // - TrueTotalInterestOutstanding = TrueTotalLoanValue - - // TrueTotalPrincipalOutstanding - // The unrounded true total interest remaining. - // - // - TrueTotalManagementFeeOutstanding = TrueTotalInterestOutstanding * - // LoanBroker.ManagementFeeRate - // The unrounded true total fee still owed to the broker. - // - // Note the the "True" values may differ significantly from the tracked - // rounded values. - {sfPaymentRemaining, soeDEFAULT}, - {sfPeriodicPayment, soeREQUIRED}, - {sfPrincipalOutstanding, soeDEFAULT}, - {sfTotalValueOutstanding, soeDEFAULT}, - {sfManagementFeeOutstanding, soeDEFAULT}, - // Based on the computed total value at creation, used for - // rounding calculated values so they are all on a - // consistent scale - that is, they all have the same - // number of digits after the decimal point (excluding - // trailing zeros). - {sfLoanScale, soeDEFAULT}, -})) - #undef EXPAND #undef LEDGER_ENTRY_DUPLICATE diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index d5c5d9447f..6e3c20267c 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -5,8 +5,6 @@ #error "undefined macro: TYPED_SFIELD" #endif -// clang-format off - // untyped UNTYPED_SFIELD(sfLedgerEntry, LEDGERENTRY, 257) UNTYPED_SFIELD(sfTransaction, TRANSACTION, 257) @@ -42,7 +40,6 @@ 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) // 1/10 basis points (bips) // 32-bit integers (common) TYPED_SFIELD(sfNetworkID, UINT32, 1) @@ -99,21 +96,6 @@ TYPED_SFIELD(sfFirstNFTokenSequence, UINT32, 50) TYPED_SFIELD(sfOracleDocumentID, UINT32, 51) TYPED_SFIELD(sfPermissionValue, UINT32, 52) TYPED_SFIELD(sfMutableFlags, UINT32, 53) -TYPED_SFIELD(sfStartDate, UINT32, 54) -TYPED_SFIELD(sfPaymentInterval, UINT32, 55) -TYPED_SFIELD(sfGracePeriod, UINT32, 56) -TYPED_SFIELD(sfPreviousPaymentDate, UINT32, 57) -TYPED_SFIELD(sfNextPaymentDueDate, UINT32, 58) -TYPED_SFIELD(sfPaymentRemaining, UINT32, 59) -TYPED_SFIELD(sfPaymentTotal, UINT32, 60) -TYPED_SFIELD(sfLoanSequence, UINT32, 61) -TYPED_SFIELD(sfCoverRateMinimum, UINT32, 62) // 1/10 basis points (bips) -TYPED_SFIELD(sfCoverRateLiquidation, UINT32, 63) // 1/10 basis points (bips) -TYPED_SFIELD(sfOverpaymentFee, UINT32, 64) // 1/10 basis points (bips) -TYPED_SFIELD(sfInterestRate, UINT32, 65) // 1/10 basis points (bips) -TYPED_SFIELD(sfLateInterestRate, UINT32, 66) // 1/10 basis points (bips) -TYPED_SFIELD(sfCloseInterestRate, UINT32, 67) // 1/10 basis points (bips) -TYPED_SFIELD(sfOverpaymentInterestRate, UINT32, 68) // 1/10 basis points (bips) // 64-bit integers (common) TYPED_SFIELD(sfIndexNext, UINT64, 1) @@ -145,8 +127,6 @@ TYPED_SFIELD(sfMPTAmount, UINT64, 26, SField::sMD_BaseTen|SFie TYPED_SFIELD(sfIssuerNode, UINT64, 27) TYPED_SFIELD(sfSubjectNode, UINT64, 28) TYPED_SFIELD(sfLockedAmount, UINT64, 29, SField::sMD_BaseTen|SField::sMD_Default) -TYPED_SFIELD(sfVaultNode, UINT64, 30) -TYPED_SFIELD(sfLoanBrokerNode, UINT64, 31) // 128-bit TYPED_SFIELD(sfEmailHash, UINT128, 1) @@ -201,9 +181,6 @@ TYPED_SFIELD(sfDomainID, UINT256, 34) TYPED_SFIELD(sfVaultID, UINT256, 35, SField::sMD_PseudoAccount | SField::sMD_Default) TYPED_SFIELD(sfParentBatchID, UINT256, 36) -TYPED_SFIELD(sfLoanBrokerID, UINT256, 37, - SField::sMD_PseudoAccount | SField::sMD_Default) -TYPED_SFIELD(sfLoanID, UINT256, 38) // number (common) TYPED_SFIELD(sfNumber, NUMBER, 1) @@ -211,21 +188,12 @@ 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, 10) -TYPED_SFIELD(sfLatePaymentFee, NUMBER, 11) -TYPED_SFIELD(sfClosePaymentFee, NUMBER, 12) -TYPED_SFIELD(sfPrincipalOutstanding, NUMBER, 13) -TYPED_SFIELD(sfPrincipalRequested, NUMBER, 14) -TYPED_SFIELD(sfTotalValueOutstanding, NUMBER, 15) -TYPED_SFIELD(sfPeriodicPayment, NUMBER, 16) -TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17) // int32 -TYPED_SFIELD(sfLoanScale, INT32, 1) +// NOTE: Do not use `sfDummyInt32`. It's so far the only use of INT32 +// in this file and has been defined here for test only. +// TODO: Replace `sfDummyInt32` with actually useful field. +TYPED_SFIELD(sfDummyInt32, INT32, 1) // for tests only // currency amount (common) TYPED_SFIELD(sfAmount, AMOUNT, 1) @@ -321,8 +289,6 @@ 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) -TYPED_SFIELD(sfCounterparty, ACCOUNT, 26) // vector of 256-bit TYPED_SFIELD(sfIndexes, VECTOR256, 1, SField::sMD_Never) @@ -386,7 +352,6 @@ UNTYPED_SFIELD(sfCredential, OBJECT, 33) UNTYPED_SFIELD(sfRawTransaction, OBJECT, 34) UNTYPED_SFIELD(sfBatchSigner, OBJECT, 35) UNTYPED_SFIELD(sfBook, OBJECT, 36) -UNTYPED_SFIELD(sfCounterpartySignature, OBJECT, 37, SField::sMD_Default, SField::notSigning) // array of objects (common) // ARRAY/1 is reserved for end of array @@ -421,5 +386,3 @@ UNTYPED_SFIELD(sfAcceptedCredentials, ARRAY, 28) UNTYPED_SFIELD(sfPermissions, ARRAY, 29) UNTYPED_SFIELD(sfRawTransactions, ARRAY, 30) UNTYPED_SFIELD(sfBatchSigners, ARRAY, 31, SField::sMD_Default, SField::notSigning) - -// clang-format on diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 6d2d833440..e551c89e55 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -925,139 +925,6 @@ TRANSACTION(ttBATCH, 71, Batch, {sfBatchSigners, soeOPTIONAL}, })) -/** Reserve 72-73 for future Vault-related transactions */ - -/** This transaction creates and updates a Loan Broker */ -#if TRANSACTION_INCLUDE -# include -#endif -TRANSACTION(ttLOAN_BROKER_SET, 74, LoanBrokerSet, - Delegation::delegatable, - featureLendingProtocol, - createPseudoAcct | mayAuthorizeMPT, ({ - {sfVaultID, soeREQUIRED}, - {sfLoanBrokerID, soeOPTIONAL}, - {sfData, soeOPTIONAL}, - {sfManagementFeeRate, soeOPTIONAL}, - {sfDebtMaximum, soeOPTIONAL}, - {sfCoverRateMinimum, soeOPTIONAL}, - {sfCoverRateLiquidation, soeOPTIONAL}, -})) - -/** This transaction deletes a Loan Broker */ -#if TRANSACTION_INCLUDE -# include -#endif -TRANSACTION(ttLOAN_BROKER_DELETE, 75, LoanBrokerDelete, - Delegation::delegatable, - featureLendingProtocol, - mustDeleteAcct | mayAuthorizeMPT, ({ - {sfLoanBrokerID, soeREQUIRED}, -})) - -/** This transaction deposits First Loss Capital into a Loan Broker */ -#if TRANSACTION_INCLUDE -# include -#endif -TRANSACTION(ttLOAN_BROKER_COVER_DEPOSIT, 76, LoanBrokerCoverDeposit, - Delegation::delegatable, - featureLendingProtocol, - noPriv, ({ - {sfLoanBrokerID, soeREQUIRED}, - {sfAmount, soeREQUIRED, soeMPTSupported}, -})) - -/** This transaction withdraws First Loss Capital from a Loan Broker */ -#if TRANSACTION_INCLUDE -# include -#endif -TRANSACTION(ttLOAN_BROKER_COVER_WITHDRAW, 77, LoanBrokerCoverWithdraw, - Delegation::delegatable, - featureLendingProtocol, - mayAuthorizeMPT, ({ - {sfLoanBrokerID, soeREQUIRED}, - {sfAmount, soeREQUIRED, soeMPTSupported}, - {sfDestination, soeOPTIONAL}, - {sfDestinationTag, soeOPTIONAL}, -})) - -/** This transaction claws back First Loss Capital from a Loan Broker to - the issuer of the capital */ -#if TRANSACTION_INCLUDE -# include -#endif -TRANSACTION(ttLOAN_BROKER_COVER_CLAWBACK, 78, LoanBrokerCoverClawback, - Delegation::delegatable, - featureLendingProtocol, - noPriv, ({ - {sfLoanBrokerID, soeOPTIONAL}, - {sfAmount, soeOPTIONAL, soeMPTSupported}, -})) - -/** This transaction creates a Loan */ -#if TRANSACTION_INCLUDE -# include -#endif -TRANSACTION(ttLOAN_SET, 80, LoanSet, - Delegation::delegatable, - featureLendingProtocol, - mayAuthorizeMPT | mustModifyVault, ({ - {sfLoanBrokerID, soeREQUIRED}, - {sfData, soeOPTIONAL}, - {sfCounterparty, soeOPTIONAL}, - {sfCounterpartySignature, soeOPTIONAL}, - {sfLoanOriginationFee, soeOPTIONAL}, - {sfLoanServiceFee, soeOPTIONAL}, - {sfLatePaymentFee, soeOPTIONAL}, - {sfClosePaymentFee, soeOPTIONAL}, - {sfOverpaymentFee, soeOPTIONAL}, - {sfInterestRate, soeOPTIONAL}, - {sfLateInterestRate, soeOPTIONAL}, - {sfCloseInterestRate, soeOPTIONAL}, - {sfOverpaymentInterestRate, soeOPTIONAL}, - {sfPrincipalRequested, soeREQUIRED}, - {sfPaymentTotal, soeOPTIONAL}, - {sfPaymentInterval, soeOPTIONAL}, - {sfGracePeriod, soeOPTIONAL}, -})) - -/** This transaction deletes an existing Loan */ -#if TRANSACTION_INCLUDE -# include -#endif -TRANSACTION(ttLOAN_DELETE, 81, LoanDelete, - Delegation::delegatable, - featureLendingProtocol, - noPriv, ({ - {sfLoanID, soeREQUIRED}, -})) - -/** This transaction is used to change the delinquency status of an existing Loan */ -#if TRANSACTION_INCLUDE -# include -#endif -TRANSACTION(ttLOAN_MANAGE, 82, LoanManage, - Delegation::delegatable, - featureLendingProtocol, - // All of the LoanManage options will modify the vault, but the - // transaction can succeed without options, essentially making it - // a noop. - mayModifyVault, ({ - {sfLoanID, soeREQUIRED}, -})) - -/** The Borrower uses this transaction to make a Payment on the Loan. */ -#if TRANSACTION_INCLUDE -# include -#endif -TRANSACTION(ttLOAN_PAY, 84, LoanPay, - Delegation::delegatable, - featureLendingProtocol, - mayAuthorizeMPT | mustModifyVault, ({ - {sfLoanID, soeREQUIRED}, - {sfAmount, soeREQUIRED, soeMPTSupported}, -})) - /** This system-generated transaction type is used to update the status of the various amendments. For details, see: https://xrpl.org/amendments.html diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index b9a8945d21..890e979ad4 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -40,8 +40,6 @@ JSS(BaseAsset); // in: Oracle JSS(BidMax); // in: AMM Bid JSS(BidMin); // in: AMM Bid JSS(ClearFlag); // field. -JSS(Counterparty); // field. -JSS(CounterpartySignature);// field. JSS(DeliverMax); // out: alias to Amount JSS(DeliverMin); // in: TransactionSign JSS(Destination); // in: TransactionSign; field. @@ -375,8 +373,6 @@ JSS(load_factor_local); // out: NetworkOPs JSS(load_factor_net); // out: NetworkOPs JSS(load_factor_server); // out: NetworkOPs JSS(load_fee); // out: LoadFeeTrackImp, NetworkOPs -JSS(loan_broker_id); // in: LedgerEntry -JSS(loan_seq); // in: LedgerEntry JSS(local); // out: resource/Logic.h JSS(local_txs); // out: GetCounts JSS(local_static_keys); // out: ValidatorList @@ -489,7 +485,6 @@ JSS(propose_seq); // out: LedgerPropose JSS(proposers); // out: NetworkOPs, LedgerConsensus JSS(protocol); // out: NetworkOPs, PeerImp JSS(proxied); // out: RPC ping -JSS(pseudo_account); // out: AccountInfo JSS(pubkey_node); // out: NetworkOPs JSS(pubkey_publisher); // out: ValidatorList JSS(pubkey_validator); // out: NetworkOPs, ValidatorList diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index 797ca83b67..89f7937e06 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -74,18 +74,6 @@ public: // tie, round towards even. int round() noexcept; - - // Modify the result to the correctly rounded value - void - doRoundUp(rep& mantissa, int& exponent, std::string location); - - // Modify the result to the correctly rounded value - void - doRoundDown(rep& mantissa, int& exponent); - - // Modify the result to the correctly rounded value - void - doRound(rep& drops); }; inline void @@ -163,61 +151,6 @@ Number::Guard::round() noexcept return 0; } -void -Number::Guard::doRoundUp(rep& mantissa, int& exponent, std::string location) -{ - auto r = round(); - if (r == 1 || (r == 0 && (mantissa & 1) == 1)) - { - ++mantissa; - if (mantissa > maxMantissa) - { - mantissa /= 10; - ++exponent; - } - } - if (exponent < minExponent) - { - mantissa = 0; - exponent = Number{}.exponent_; - } - if (exponent > maxExponent) - throw std::overflow_error(location); -} - -void -Number::Guard::doRoundDown(rep& mantissa, int& exponent) -{ - auto r = round(); - if (r == 1 || (r == 0 && (mantissa & 1) == 1)) - { - --mantissa; - if (mantissa < minMantissa) - { - mantissa *= 10; - --exponent; - } - } - if (exponent < minExponent) - { - mantissa = 0; - exponent = Number{}.exponent_; - } -} - -// Modify the result to the correctly rounded value -void -Number::Guard::doRound(rep& drops) -{ - auto r = round(); - if (r == 1 || (r == 0 && (drops & 1) == 1)) - { - ++drops; - } - if (is_negative()) - drops = -drops; -} - // Number constexpr Number one{1000000000000000, -15, Number::unchecked{}}; @@ -257,7 +190,18 @@ Number::normalize() return; } - g.doRoundUp(mantissa_, exponent_, "Number::normalize 2"); + auto r = g.round(); + if (r == 1 || (r == 0 && (mantissa_ & 1) == 1)) + { + ++mantissa_; + if (mantissa_ > maxMantissa) + { + mantissa_ /= 10; + ++exponent_; + } + } + if (exponent_ > maxExponent) + throw std::overflow_error("Number::normalize 2"); if (negative) mantissa_ = -mantissa_; @@ -329,7 +273,18 @@ Number::operator+=(Number const& y) xm /= 10; ++xe; } - g.doRoundUp(xm, xe, "Number::addition overflow"); + auto r = g.round(); + if (r == 1 || (r == 0 && (xm & 1) == 1)) + { + ++xm; + if (xm > maxMantissa) + { + xm /= 10; + ++xe; + } + } + if (xe > maxExponent) + throw std::overflow_error("Number::addition overflow"); } else { @@ -349,7 +304,21 @@ Number::operator+=(Number const& y) xm -= g.pop(); --xe; } - g.doRoundDown(xm, xe); + auto r = g.round(); + if (r == 1 || (r == 0 && (xm & 1) == 1)) + { + --xm; + if (xm < minMantissa) + { + xm *= 10; + --xe; + } + } + if (xe < minExponent) + { + xm = 0; + xe = Number{}.exponent_; + } } mantissa_ = xm * xn; exponent_ = xe; @@ -429,10 +398,25 @@ Number::operator*=(Number const& y) } xm = static_cast(zm); xe = ze; - g.doRoundUp( - xm, - xe, - "Number::multiplication overflow : exponent is " + std::to_string(xe)); + auto r = g.round(); + if (r == 1 || (r == 0 && (xm & 1) == 1)) + { + ++xm; + if (xm > maxMantissa) + { + xm /= 10; + ++xe; + } + } + if (xe < minExponent) + { + xm = 0; + xe = Number{}.exponent_; + } + if (xe > maxExponent) + throw std::overflow_error( + "Number::multiplication overflow : exponent is " + + std::to_string(xe)); mantissa_ = xm * zn; exponent_ = xe; XRPL_ASSERT( @@ -497,29 +481,17 @@ Number::operator rep() const throw std::overflow_error("Number::operator rep() overflow"); drops *= 10; } - g.doRound(drops); + auto r = g.round(); + if (r == 1 || (r == 0 && (drops & 1) == 1)) + { + ++drops; + } + if (g.is_negative()) + drops = -drops; } return drops; } -Number -Number::truncate() const noexcept -{ - if (exponent_ >= 0 || mantissa_ == 0) - return *this; - - Number ret = *this; - while (ret.exponent_ < 0 && ret.mantissa_ != 0) - { - ret.exponent_ += 1; - ret.mantissa_ /= rep(10); - } - // We are guaranteed that normalize() will never throw an exception - // because exponent is either negative or zero at this point. - ret.normalize(); - return ret; -} - std::string to_string(Number const& amount) { diff --git a/src/libxrpl/json/json_value.cpp b/src/libxrpl/json/json_value.cpp index 1580e68255..a9478f51ab 100644 --- a/src/libxrpl/json/json_value.cpp +++ b/src/libxrpl/json/json_value.cpp @@ -1042,12 +1042,6 @@ Value::isMember(std::string const& key) const return isMember(key.c_str()); } -bool -Value::isMember(StaticString const& key) const -{ - return isMember(key.c_str()); -} - Value::Members Value::getMemberNames() const { diff --git a/src/libxrpl/ledger/ApplyView.cpp b/src/libxrpl/ledger/ApplyView.cpp index 911bec1722..19dfd199f9 100644 --- a/src/libxrpl/ledger/ApplyView.cpp +++ b/src/libxrpl/ledger/ApplyView.cpp @@ -8,149 +8,6 @@ namespace ripple { -namespace directory { - -std::uint64_t -createRoot( - ApplyView& view, - Keylet const& directory, - uint256 const& key, - std::function const&)> const& describe) -{ - auto newRoot = std::make_shared(directory); - newRoot->setFieldH256(sfRootIndex, directory.key); - describe(newRoot); - - STVector256 v; - v.push_back(key); - newRoot->setFieldV256(sfIndexes, v); - - view.insert(newRoot); - return std::uint64_t{0}; -} - -auto -findPreviousPage(ApplyView& view, Keylet const& directory, SLE::ref start) -{ - std::uint64_t page = start->getFieldU64(sfIndexPrevious); - - auto node = start; - - if (page) - { - node = view.peek(keylet::page(directory, page)); - if (!node) - { // LCOV_EXCL_START - LogicError("Directory chain: root back-pointer broken."); - // LCOV_EXCL_STOP - } - } - - auto indexes = node->getFieldV256(sfIndexes); - return std::make_tuple(page, node, indexes); -} - -std::uint64_t -insertKey( - ApplyView& view, - SLE::ref node, - std::uint64_t page, - bool preserveOrder, - STVector256& indexes, - uint256 const& key) -{ - if (preserveOrder) - { - if (std::find(indexes.begin(), indexes.end(), key) != indexes.end()) - LogicError("dirInsert: double insertion"); // LCOV_EXCL_LINE - - indexes.push_back(key); - } - else - { - // We can't be sure if this page is already sorted because - // it may be a legacy page we haven't yet touched. Take - // the time to sort it. - std::sort(indexes.begin(), indexes.end()); - - auto pos = std::lower_bound(indexes.begin(), indexes.end(), key); - - if (pos != indexes.end() && key == *pos) - LogicError("dirInsert: double insertion"); // LCOV_EXCL_LINE - - indexes.insert(pos, key); - } - - node->setFieldV256(sfIndexes, indexes); - view.update(node); - return page; -} - -std::optional -insertPage( - ApplyView& view, - std::uint64_t page, - SLE::pointer node, - std::uint64_t nextPage, - SLE::ref next, - uint256 const& key, - Keylet const& directory, - std::function const&)> const& describe) -{ - // We rely on modulo arithmetic of unsigned integers (guaranteed in - // [basic.fundamental] paragraph 2) to detect page representation overflow. - // For signed integers this would be UB, hence static_assert here. - static_assert(std::is_unsigned_v); - // Defensive check against breaking changes in compiler. - static_assert([](std::type_identity) constexpr -> T { - T tmp = std::numeric_limits::max(); - return ++tmp; - }(std::type_identity{}) == 0); - ++page; - // Check whether we're out of pages. - if (page == 0) - return std::nullopt; - if (!view.rules().enabled(fixDirectoryLimit) && - page >= dirNodeMaxPages) // Old pages limit - return std::nullopt; - - // We are about to create a new node; we'll link it to - // the chain first: - node->setFieldU64(sfIndexNext, page); - view.update(node); - - next->setFieldU64(sfIndexPrevious, page); - view.update(next); - - // Insert the new key: - STVector256 indexes; - indexes.push_back(key); - - node = std::make_shared(keylet::page(directory, page)); - node->setFieldH256(sfRootIndex, directory.key); - node->setFieldV256(sfIndexes, indexes); - - // Save some space by not specifying the value 0 since - // it's the default. - if (page != 1) - node->setFieldU64(sfIndexPrevious, page - 1); - XRPL_ASSERT_PARTS( - !nextPage, - "ripple::directory::insertPage", - "nextPage has default value"); - /* Reserved for future use when directory pages may be inserted in - * between two other pages instead of only at the end of the chain. - if (nextPage) - node->setFieldU64(sfIndexNext, nextPage); - */ - describe(node); - view.insert(node); - - return page; -} - -} // namespace directory - std::optional ApplyView::dirAdd( bool preserveOrder, @@ -163,21 +20,102 @@ ApplyView::dirAdd( if (!root) { // No root, make it. - return directory::createRoot(*this, directory, key, describe); + root = std::make_shared(directory); + root->setFieldH256(sfRootIndex, directory.key); + describe(root); + + STVector256 v; + v.push_back(key); + root->setFieldV256(sfIndexes, v); + + insert(root); + return std::uint64_t{0}; } - auto [page, node, indexes] = - directory::findPreviousPage(*this, directory, root); + std::uint64_t page = root->getFieldU64(sfIndexPrevious); + + auto node = root; + + if (page) + { + node = peek(keylet::page(directory, page)); + if (!node) + LogicError("Directory chain: root back-pointer broken."); + } + + auto indexes = node->getFieldV256(sfIndexes); // If there's space, we use it: if (indexes.size() < dirNodeMaxEntries) { - return directory::insertKey( - *this, node, page, preserveOrder, indexes, key); + if (preserveOrder) + { + if (std::find(indexes.begin(), indexes.end(), key) != indexes.end()) + LogicError("dirInsert: double insertion"); + + indexes.push_back(key); + } + else + { + // We can't be sure if this page is already sorted because + // it may be a legacy page we haven't yet touched. Take + // the time to sort it. + std::sort(indexes.begin(), indexes.end()); + + auto pos = std::lower_bound(indexes.begin(), indexes.end(), key); + + if (pos != indexes.end() && key == *pos) + LogicError("dirInsert: double insertion"); + + indexes.insert(pos, key); + } + + node->setFieldV256(sfIndexes, indexes); + update(node); + return page; } - return directory::insertPage( - *this, page, node, 0, root, key, directory, describe); + // We rely on modulo arithmetic of unsigned integers (guaranteed in + // [basic.fundamental] paragraph 2) to detect page representation overflow. + // For signed integers this would be UB, hence static_assert here. + static_assert(std::is_unsigned_v); + // Defensive check against breaking changes in compiler. + static_assert([](std::type_identity) constexpr -> T { + T tmp = std::numeric_limits::max(); + return ++tmp; + }(std::type_identity{}) == 0); + ++page; + // Check whether we're out of pages. + if (page == 0) + return std::nullopt; + if (!rules().enabled(fixDirectoryLimit) && + page >= dirNodeMaxPages) // Old pages limit + return std::nullopt; + + // We are about to create a new node; we'll link it to + // the chain first: + node->setFieldU64(sfIndexNext, page); + update(node); + + root->setFieldU64(sfIndexPrevious, page); + update(root); + + // Insert the new key: + indexes.clear(); + indexes.push_back(key); + + node = std::make_shared(keylet::page(directory, page)); + node->setFieldH256(sfRootIndex, directory.key); + node->setFieldV256(sfIndexes, indexes); + + // Save some space by not specifying the value 0 since + // it's the default. + if (page != 1) + node->setFieldU64(sfIndexPrevious, page - 1); + describe(node); + insert(node); + + return page; } bool @@ -207,10 +145,10 @@ ApplyView::emptyDirDelete(Keylet const& directory) auto nextPage = node->getFieldU64(sfIndexNext); if (nextPage == rootPage && prevPage != rootPage) - LogicError("Directory chain: fwd link broken"); // LCOV_EXCL_LINE + LogicError("Directory chain: fwd link broken"); if (prevPage == rootPage && nextPage != rootPage) - LogicError("Directory chain: rev link broken"); // LCOV_EXCL_LINE + LogicError("Directory chain: rev link broken"); // Older versions of the code would, in some cases, allow the last // page to be empty. Remove such pages: @@ -219,10 +157,7 @@ ApplyView::emptyDirDelete(Keylet const& directory) auto last = peek(keylet::page(directory, nextPage)); if (!last) - { // LCOV_EXCL_START LogicError("Directory chain: fwd link broken."); - // LCOV_EXCL_STOP - } if (!last->getFieldV256(sfIndexes).empty()) return false; @@ -294,16 +229,10 @@ ApplyView::dirRemove( if (page == rootPage) { if (nextPage == page && prevPage != page) - { // LCOV_EXCL_START LogicError("Directory chain: fwd link broken"); - // LCOV_EXCL_STOP - } if (prevPage == page && nextPage != page) - { // LCOV_EXCL_START LogicError("Directory chain: rev link broken"); - // LCOV_EXCL_STOP - } // Older versions of the code would, in some cases, // allow the last page to be empty. Remove such @@ -312,10 +241,7 @@ ApplyView::dirRemove( { auto last = peek(keylet::page(directory, nextPage)); if (!last) - { // LCOV_EXCL_START LogicError("Directory chain: fwd link broken."); - // LCOV_EXCL_STOP - } if (last->getFieldV256(sfIndexes).empty()) { @@ -347,10 +273,10 @@ ApplyView::dirRemove( // This can never happen for nodes other than the root: if (nextPage == page) - LogicError("Directory chain: fwd link broken"); // LCOV_EXCL_LINE + LogicError("Directory chain: fwd link broken"); if (prevPage == page) - LogicError("Directory chain: rev link broken"); // LCOV_EXCL_LINE + LogicError("Directory chain: rev link broken"); // This node isn't the root, so it can either be in the // middle of the list, or at the end. Unlink it first @@ -358,14 +284,14 @@ ApplyView::dirRemove( // root: auto prev = peek(keylet::page(directory, prevPage)); if (!prev) - LogicError("Directory chain: fwd link broken."); // LCOV_EXCL_LINE + LogicError("Directory chain: fwd link broken."); // Fix previous to point to its new next. prev->setFieldU64(sfIndexNext, nextPage); update(prev); auto next = peek(keylet::page(directory, nextPage)); if (!next) - LogicError("Directory chain: rev link broken."); // LCOV_EXCL_LINE + LogicError("Directory chain: rev link broken."); // Fix next to point to its new previous. next->setFieldU64(sfIndexPrevious, prevPage); update(next); @@ -389,10 +315,7 @@ ApplyView::dirRemove( // And the root points to the last page: auto root = peek(keylet::page(directory, rootPage)); if (!root) - { // LCOV_EXCL_START LogicError("Directory chain: root link broken."); - // LCOV_EXCL_STOP - } root->setFieldU64(sfIndexPrevious, prevPage); update(root); diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index 33c6cf69ec..6356dd1131 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -364,99 +364,6 @@ isLPTokenFrozen( isFrozen(view, account, asset2.currency, asset2.account); } -static SLE::const_pointer -getLineIfUsable( - ReadView const& view, - AccountID const& account, - Currency const& currency, - AccountID const& issuer, - FreezeHandling zeroIfFrozen, - beast::Journal j) -{ - auto const sle = view.read(keylet::line(account, issuer, currency)); - - if (!sle) - { - return nullptr; - } - - if (zeroIfFrozen == fhZERO_IF_FROZEN) - { - if (isFrozen(view, account, currency, issuer) || - isDeepFrozen(view, account, currency, issuer)) - { - return nullptr; - } - - // when fixFrozenLPTokenTransfer is enabled, if currency is lptoken, - // we need to check if the associated assets have been frozen - if (view.rules().enabled(fixFrozenLPTokenTransfer)) - { - auto const sleIssuer = view.read(keylet::account(issuer)); - if (!sleIssuer) - { - return nullptr; // LCOV_EXCL_LINE - } - else if (sleIssuer->isFieldPresent(sfAMMID)) - { - auto const sleAmm = - view.read(keylet::amm((*sleIssuer)[sfAMMID])); - - if (!sleAmm || - isLPTokenFrozen( - view, - account, - (*sleAmm)[sfAsset].get(), - (*sleAmm)[sfAsset2].get())) - { - return nullptr; - } - } - } - } - - return sle; -} - -static STAmount -getTrustLineBalance( - ReadView const& view, - SLE::const_ref sle, - AccountID const& account, - Currency const& currency, - AccountID const& issuer, - bool includeOppositeLimit, - beast::Journal j) -{ - STAmount amount; - if (sle) - { - amount = sle->getFieldAmount(sfBalance); - bool const accountHigh = account > issuer; - auto const& oppositeField = accountHigh ? sfLowLimit : sfHighLimit; - if (accountHigh) - { - // Put balance in account terms. - amount.negate(); - } - if (includeOppositeLimit) - { - amount += sle->getFieldAmount(oppositeField); - } - amount.setIssuer(issuer); - } - else - { - amount.clear(Issue{currency, issuer}); - } - - JLOG(j.trace()) << "getTrustLineBalance:" - << " account=" << to_string(account) - << " amount=" << amount.getFullText(); - - return view.balanceHook(account, issuer, amount); -} - STAmount accountHolds( ReadView const& view, @@ -473,10 +380,71 @@ accountHolds( } // IOU: Return balance on trust line modulo freeze - SLE::const_pointer const sle = - getLineIfUsable(view, account, currency, issuer, zeroIfFrozen, j); + auto const sle = view.read(keylet::line(account, issuer, currency)); + auto const allowBalance = [&]() { + if (!sle) + { + return false; + } - return getTrustLineBalance(view, sle, account, currency, issuer, false, j); + if (zeroIfFrozen == fhZERO_IF_FROZEN) + { + if (isFrozen(view, account, currency, issuer) || + isDeepFrozen(view, account, currency, issuer)) + { + return false; + } + + // when fixFrozenLPTokenTransfer is enabled, if currency is lptoken, + // we need to check if the associated assets have been frozen + if (view.rules().enabled(fixFrozenLPTokenTransfer)) + { + auto const sleIssuer = view.read(keylet::account(issuer)); + if (!sleIssuer) + { + return false; // LCOV_EXCL_LINE + } + else if (sleIssuer->isFieldPresent(sfAMMID)) + { + auto const sleAmm = + view.read(keylet::amm((*sleIssuer)[sfAMMID])); + + if (!sleAmm || + isLPTokenFrozen( + view, + account, + (*sleAmm)[sfAsset].get(), + (*sleAmm)[sfAsset2].get())) + { + return false; + } + } + } + } + + return true; + }(); + + if (allowBalance) + { + amount = sle->getFieldAmount(sfBalance); + if (account > issuer) + { + // Put balance in account terms. + amount.negate(); + } + amount.setIssuer(issuer); + } + else + { + amount.clear(Issue{currency, issuer}); + } + + JLOG(j.trace()) << "accountHolds:" + << " account=" << to_string(account) + << " amount=" << amount.getFullText(); + + return view.balanceHook(account, issuer, amount); } STAmount @@ -563,96 +531,6 @@ accountHolds( asset.value()); } -STAmount -accountSpendable( - ReadView const& view, - AccountID const& account, - Currency const& currency, - AccountID const& issuer, - FreezeHandling zeroIfFrozen, - beast::Journal j) -{ - if (isXRP(currency)) - return accountHolds(view, account, currency, issuer, zeroIfFrozen, j); - - if (account == issuer) - // If the account is the issuer, then their limit is effectively - // infinite - return STAmount{ - Issue{currency, issuer}, STAmount::cMaxValue, STAmount::cMaxOffset}; - - // IOU: Return balance on trust line modulo freeze - SLE::const_pointer const sle = - getLineIfUsable(view, account, currency, issuer, zeroIfFrozen, j); - - return getTrustLineBalance(view, sle, account, currency, issuer, true, j); -} - -STAmount -accountSpendable( - ReadView const& view, - AccountID const& account, - Issue const& issue, - FreezeHandling zeroIfFrozen, - beast::Journal j) -{ - return accountSpendable( - view, account, issue.currency, issue.account, zeroIfFrozen, j); -} - -STAmount -accountSpendable( - ReadView const& view, - AccountID const& account, - MPTIssue const& mptIssue, - FreezeHandling zeroIfFrozen, - AuthHandling zeroIfUnauthorized, - beast::Journal j) -{ - if (account == mptIssue.getIssuer()) - { - // if the account is the issuer, and the issuance exists, their limit is - // the issuance limit minus the outstanding value - auto const issuance = - view.read(keylet::mptIssuance(mptIssue.getMptID())); - - if (!issuance) - { - return STAmount{mptIssue}; - } - return STAmount{ - mptIssue, - issuance->at(~sfMaximumAmount).value_or(maxMPTokenAmount) - - issuance->at(sfOutstandingAmount)}; - } - - return accountHolds( - view, account, mptIssue, zeroIfFrozen, zeroIfUnauthorized, j); -} - -[[nodiscard]] STAmount -accountSpendable( - ReadView const& view, - AccountID const& account, - Asset const& asset, - FreezeHandling zeroIfFrozen, - AuthHandling zeroIfUnauthorized, - beast::Journal j) -{ - return std::visit( - [&](auto const& value) { - if constexpr (std::is_same_v< - std::remove_cvref_t, - Issue>) - { - return accountSpendable(view, account, value, zeroIfFrozen, j); - } - return accountSpendable( - view, account, value, zeroIfFrozen, zeroIfUnauthorized, j); - }, - asset.value()); -} - STAmount accountFunds( ReadView const& view, @@ -1158,17 +1036,13 @@ describeOwnerDir(AccountID const& account) } TER -dirLink( - ApplyView& view, - AccountID const& owner, - std::shared_ptr& object, - SF_UINT64 const& node) +dirLink(ApplyView& view, AccountID const& owner, std::shared_ptr& object) { auto const page = view.dirInsert( keylet::ownerDir(owner), object->key(), describeOwnerDir(owner)); if (!page) return tecDIR_FULL; // LCOV_EXCL_LINE - object->setFieldU64(node, *page); + object->setFieldU64(sfOwnerNode, *page); return tesSUCCESS; } @@ -1204,8 +1078,7 @@ getPseudoAccountFields() { // LCOV_EXCL_START LogicError( - "ripple::getPseudoAccountFields : unable to find account root " - "ledger " + "ripple::isPseudoAccount : unable to find account root ledger " "format"); // LCOV_EXCL_STOP } @@ -1223,9 +1096,7 @@ getPseudoAccountFields() } [[nodiscard]] bool -isPseudoAccount( - std::shared_ptr sleAcct, - std::set const& pseudoFieldFilter) +isPseudoAccount(std::shared_ptr sleAcct) { auto const& fields = getPseudoAccountFields(); @@ -1233,12 +1104,8 @@ isPseudoAccount( // semantics of true return value clean. return sleAcct && sleAcct->getType() == ltACCOUNT_ROOT && std::count_if( - fields.begin(), - fields.end(), - [&sleAcct, &pseudoFieldFilter](SField const* sf) -> bool { - return sleAcct->isFieldPresent(*sf) && - (pseudoFieldFilter.empty() || - pseudoFieldFilter.contains(sf)); + fields.begin(), fields.end(), [&sleAcct](SField const* sf) -> bool { + return sleAcct->isFieldPresent(*sf); }) > 0; } @@ -1248,8 +1115,7 @@ createPseudoAccount( uint256 const& pseudoOwnerKey, SField const& ownerField) { - [[maybe_unused]] - auto const& fields = getPseudoAccountFields(); + [[maybe_unused]] auto const& fields = getPseudoAccountFields(); XRPL_ASSERT( std::count_if( fields.begin(), @@ -1271,10 +1137,9 @@ createPseudoAccount( // Pseudo-accounts can't submit transactions, so set the sequence number // to 0 to make them easier to spot and verify, and add an extra level // of protection. - std::uint32_t const seqno = // - view.rules().enabled(featureSingleAssetVault) || // - view.rules().enabled(featureLendingProtocol) // - ? 0 // + std::uint32_t const seqno = // + view.rules().enabled(featureSingleAssetVault) // + ? 0 // : view.seq(); account->setFieldU32(sfSequence, seqno); // Ignore reserves requirement, disable the master key, allow default @@ -1328,113 +1193,6 @@ canAddHolding(ReadView const& view, Asset const& asset) asset.value()); } -[[nodiscard]] TER -checkDestinationAndTag(SLE::const_ref toSle, bool hasDestinationTag) -{ - if (toSle == nullptr) - return tecNO_DST; - - // The tag is basically account-specific information we don't - // understand, but we can require someone to fill it in. - if (toSle->isFlag(lsfRequireDestTag) && !hasDestinationTag) - return tecDST_TAG_NEEDED; // Cannot send without a tag - - return tesSUCCESS; -} - -[[nodiscard]] TER -canWithdraw( - AccountID const& from, - ReadView const& view, - AccountID const& to, - SLE::const_ref toSle, - bool hasDestinationTag) -{ - if (auto const ret = checkDestinationAndTag(toSle, hasDestinationTag)) - return ret; - - if (from == to) - return tesSUCCESS; - - if (toSle->isFlag(lsfDepositAuth)) - { - if (!view.exists(keylet::depositPreauth(to, from))) - return tecNO_PERMISSION; - } - - return tesSUCCESS; -} - -[[nodiscard]] TER -canWithdraw( - AccountID const& from, - ReadView const& view, - AccountID const& to, - bool hasDestinationTag) -{ - auto const toSle = view.read(keylet::account(to)); - - return canWithdraw(from, view, to, toSle, hasDestinationTag); -} - -[[nodiscard]] TER -canWithdraw(ReadView const& view, STTx const& tx) -{ - auto const from = tx[sfAccount]; - auto const to = tx[~sfDestination].value_or(from); - - return canWithdraw(from, view, to, tx.isFieldPresent(sfDestinationTag)); -} - -TER -doWithdraw( - ApplyView& view, - STTx const& tx, - AccountID const& senderAcct, - AccountID const& dstAcct, - AccountID const& sourceAcct, - XRPAmount priorBalance, - STAmount const& amount, - beast::Journal j) -{ - // Create trust line or MPToken for the receiving account - if (dstAcct == senderAcct) - { - if (auto const ter = addEmptyHolding( - view, senderAcct, priorBalance, amount.asset(), j); - !isTesSuccess(ter) && ter != tecDUPLICATE) - return ter; - } - else - { - auto dstSle = view.peek(keylet::account(dstAcct)); - if (auto err = - verifyDepositPreauth(tx, view, senderAcct, dstAcct, dstSle, j)) - return err; - } - - // Sanity check - if (accountHolds( - view, - sourceAcct, - amount.asset(), - FreezeHandling::fhIGNORE_FREEZE, - AuthHandling::ahIGNORE_AUTH, - j) < amount) - { - // LCOV_EXCL_START - JLOG(j.error()) << "LoanBrokerCoverWithdraw: negative balance of " - "broker cover assets."; - return tefINTERNAL; - // LCOV_EXCL_STOP - } - - // Move the funds directly from the broker's pseudo-account to the - // dstAcct - return accountSend( - view, sourceAcct, dstAcct, amount, j, WaiveTransferFee::Yes); -} - [[nodiscard]] TER addEmptyHolding( ApplyView& view, @@ -1443,8 +1201,8 @@ addEmptyHolding( Issue const& issue, beast::Journal journal) { - // Every account can hold XRP. An issuer can issue directly. - if (issue.native() || accountID == issue.getIssuer()) + // Every account can hold XRP. + if (issue.native()) return tesSUCCESS; auto const& issuerId = issue.getIssuer(); @@ -1505,8 +1263,6 @@ addEmptyHolding( return tefINTERNAL; // LCOV_EXCL_LINE if (view.peek(keylet::mptoken(mptID, accountID))) return tecDUPLICATE; - if (accountID == mptIssue.getIssuer()) - return tesSUCCESS; return authorizeMPToken(view, priorBalance, mptID, accountID, journal); } @@ -1570,18 +1326,6 @@ authorizeMPToken( if (priorBalance < reserveCreate) return tecINSUFFICIENT_RESERVE; - // Defensive check before we attempt to create MPToken for the issuer - auto const mpt = view.read(keylet::mptIssuance(mptIssuanceID)); - if (!mpt || mpt->getAccountID(sfIssuer) == account) - { - // LCOV_EXCL_START - UNREACHABLE( - "ripple::authorizeMPToken : invalid issuance or issuers token"); - if (view.rules().enabled(featureLendingProtocol)) - return tecINTERNAL; - // LCOV_EXCL_STOP - } - auto const mptokenKey = keylet::mptoken(mptIssuanceID, account); auto mptoken = std::make_shared(mptokenKey); if (auto ter = dirLink(view, account, mptoken)) @@ -1657,14 +1401,6 @@ trustCreate( auto const& uLowAccountID = !bSrcHigh ? uSrcAccountID : uDstAccountID; auto const& uHighAccountID = bSrcHigh ? uSrcAccountID : uDstAccountID; - if (uLowAccountID == uHighAccountID) - { - // LCOV_EXCL_START - UNREACHABLE("ripple::trustCreate : trust line to self"); - if (view.rules().enabled(featureLendingProtocol)) - return tecINTERNAL; - // LCOV_EXCL_STOP - } auto const sleRippleState = std::make_shared(ltRIPPLE_STATE, uIndex); view.insert(sleRippleState); @@ -1779,13 +1515,10 @@ removeEmptyHolding( } // `asset` is an IOU. - // If the account is the issuer, then no line should exist. Check anyway. If - // a line does exist, it will get deleted. If not, return success. - bool const accountIsIssuer = accountID == issue.account; auto const line = view.peek(keylet::line(accountID, issue)); if (!line) - return accountIsIssuer ? (TER)tesSUCCESS : (TER)tecOBJECT_NOT_FOUND; - if (!accountIsIssuer && line->at(sfBalance)->iou() != beast::zero) + return tecOBJECT_NOT_FOUND; + if (line->at(sfBalance)->iou() != beast::zero) return tecHAS_OBLIGATIONS; // Adjust the owner count(s) @@ -1834,18 +1567,10 @@ removeEmptyHolding( MPTIssue const& mptIssue, beast::Journal journal) { - // If the account is the issuer, then no token should exist. MPTs do not - // have the legacy ability to create such a situation, but check anyway. If - // a token does exist, it will get deleted. If not, return success. - bool const accountIsIssuer = accountID == mptIssue.getIssuer(); auto const& mptID = mptIssue.getMptID(); auto const mptoken = view.peek(keylet::mptoken(mptID, accountID)); if (!mptoken) - return accountIsIssuer ? (TER)tesSUCCESS : (TER)tecOBJECT_NOT_FOUND; - // Unlike a trust line, if the account is the issuer, and the token has a - // balance, it can not just be deleted, because that will throw the issuance - // accounting out of balance, so fail. Since this should be impossible - // anyway, I'm not going to put any effort into it. + return tecOBJECT_NOT_FOUND; if (mptoken->at(sfMPTAmount) != 0) return tecHAS_OBLIGATIONS; @@ -2124,7 +1849,7 @@ rippleSendIOU( beast::Journal j, WaiveTransferFee waiveFee) { - auto const& issuer = saAmount.getIssuer(); + auto const issuer = saAmount.getIssuer(); XRPL_ASSERT( !isXRP(uSenderID) && !isXRP(uReceiverID), @@ -2166,87 +1891,6 @@ rippleSendIOU( return terResult; } -// Send regardless of limits. -// --> receivers: Amount/currency/issuer to deliver to receivers. -// <-- saActual: Amount actually cost to sender. Sender pays fees. -static TER -rippleSendMultiIOU( - ApplyView& view, - AccountID const& senderID, - Issue const& issue, - MultiplePaymentDestinations const& receivers, - STAmount& actual, - beast::Journal j, - WaiveTransferFee waiveFee) -{ - auto const& issuer = issue.getIssuer(); - - XRPL_ASSERT( - !isXRP(senderID), "ripple::rippleSendMultiIOU : sender is not XRP"); - - // These may diverge - STAmount takeFromSender{issue}; - actual = takeFromSender; - - // Failures return immediately. - for (auto const& r : receivers) - { - auto const& receiverID = r.first; - STAmount amount{issue, r.second}; - - /* If we aren't sending anything or if the sender is the same as the - * receiver then we don't need to do anything. - */ - if (!amount || (senderID == receiverID)) - continue; - - XRPL_ASSERT( - !isXRP(receiverID), - "ripple::rippleSendMultiIOU : receiver is not XRP"); - - if (senderID == issuer || receiverID == issuer || issuer == noAccount()) - { - // Direct send: redeeming IOUs and/or sending own IOUs. - if (auto const ter = rippleCreditIOU( - view, senderID, receiverID, amount, false, j)) - return ter; - actual += amount; - // Do not add amount to takeFromSender, because rippleCreditIOU took - // it. - - continue; - } - - // Sending 3rd party IOUs: transit. - - // Calculate the amount to transfer accounting - // for any transfer fees if the fee is not waived: - STAmount actualSend = (waiveFee == WaiveTransferFee::Yes) - ? amount - : multiply(amount, transferRate(view, issuer)); - actual += actualSend; - takeFromSender += actualSend; - - JLOG(j.debug()) << "rippleSendMultiIOU> " << to_string(senderID) - << " - > " << to_string(receiverID) - << " : deliver=" << amount.getFullText() - << " cost=" << actual.getFullText(); - - if (TER const terResult = - rippleCreditIOU(view, issuer, receiverID, amount, true, j)) - return terResult; - } - - if (senderID != issuer && takeFromSender) - { - if (TER const terResult = rippleCreditIOU( - view, senderID, issuer, takeFromSender, true, j)) - return terResult; - } - - return tesSUCCESS; -} - static TER accountSendIOU( ApplyView& view, @@ -2371,144 +2015,6 @@ accountSendIOU( return terResult; } -static TER -accountSendMultiIOU( - ApplyView& view, - AccountID const& senderID, - Issue const& issue, - MultiplePaymentDestinations const& receivers, - beast::Journal j, - WaiveTransferFee waiveFee) -{ - XRPL_ASSERT_PARTS( - receivers.size() > 1, - "ripple::accountSendMultiIOU", - "multiple recipients provided"); - - if (!issue.native()) - { - STAmount actual; - JLOG(j.trace()) << "accountSendMultiIOU: " << to_string(senderID) - << " sending " << receivers.size() << " IOUs"; - - return rippleSendMultiIOU( - view, senderID, issue, receivers, actual, j, waiveFee); - } - - /* XRP send which does not check reserve and can do pure adjustment. - * Note that sender or receiver may be null and this not a mistake; this - * setup could be used during pathfinding and it is carefully controlled to - * ensure that transfers are balanced. - */ - - SLE::pointer sender = senderID != beast::zero - ? view.peek(keylet::account(senderID)) - : SLE::pointer(); - - if (auto stream = j.trace()) - { - std::string sender_bal("-"); - - if (sender) - sender_bal = sender->getFieldAmount(sfBalance).getFullText(); - - stream << "accountSendMultiIOU> " << to_string(senderID) << " (" - << sender_bal << ") -> " << receivers.size() << " receivers."; - } - - // Failures return immediately. - STAmount takeFromSender{issue}; - for (auto const& r : receivers) - { - auto const& receiverID = r.first; - STAmount amount{issue, r.second}; - - if (amount < beast::zero) - { - return tecINTERNAL; // LCOV_EXCL_LINE - } - - /* If we aren't sending anything or if the sender is the same as the - * receiver then we don't need to do anything. - */ - if (!amount || (senderID == receiverID)) - continue; - - SLE::pointer receiver = receiverID != beast::zero - ? view.peek(keylet::account(receiverID)) - : SLE::pointer(); - - if (auto stream = j.trace()) - { - std::string receiver_bal("-"); - - if (receiver) - receiver_bal = - receiver->getFieldAmount(sfBalance).getFullText(); - - stream << "accountSendMultiIOU> " << to_string(senderID) << " -> " - << to_string(receiverID) << " (" << receiver_bal - << ") : " << amount.getFullText(); - } - - if (receiver) - { - // Increment XRP balance. - auto const rcvBal = receiver->getFieldAmount(sfBalance); - receiver->setFieldAmount(sfBalance, rcvBal + amount); - view.creditHook(xrpAccount(), receiverID, amount, -rcvBal); - - view.update(receiver); - - // Take what is actually sent - takeFromSender += amount; - } - - if (auto stream = j.trace()) - { - std::string receiver_bal("-"); - - if (receiver) - receiver_bal = - receiver->getFieldAmount(sfBalance).getFullText(); - - stream << "accountSendMultiIOU< " << to_string(senderID) << " -> " - << to_string(receiverID) << " (" << receiver_bal - << ") : " << amount.getFullText(); - } - } - - if (sender) - { - if (sender->getFieldAmount(sfBalance) < takeFromSender) - { - return TER{tecFAILED_PROCESSING}; - } - else - { - auto const sndBal = sender->getFieldAmount(sfBalance); - view.creditHook(senderID, xrpAccount(), takeFromSender, sndBal); - - // Decrement XRP balance. - sender->setFieldAmount(sfBalance, sndBal - takeFromSender); - view.update(sender); - } - } - - if (auto stream = j.trace()) - { - std::string sender_bal("-"); - std::string receiver_bal("-"); - - if (sender) - sender_bal = sender->getFieldAmount(sfBalance).getFullText(); - - stream << "accountSendMultiIOU< " << to_string(senderID) << " (" - << sender_bal << ") -> " << receivers.size() << " receivers."; - } - return tesSUCCESS; -} - static TER rippleCreditMPT( ApplyView& view, @@ -2519,7 +2025,7 @@ rippleCreditMPT( { // Do not check MPT authorization here - it must have been checked earlier auto const mptID = keylet::mptIssuance(saAmount.get().getMptID()); - auto const& issuer = saAmount.getIssuer(); + auto const issuer = saAmount.getIssuer(); auto sleIssuance = view.peek(mptID); if (!sleIssuance) return tecOBJECT_NOT_FOUND; @@ -2586,7 +2092,7 @@ rippleSendMPT( "ripple::rippleSendMPT : sender is not receiver"); // Safe to get MPT since rippleSendMPT is only called by accountSendMPT - auto const& issuer = saAmount.getIssuer(); + auto const issuer = saAmount.getIssuer(); auto const sle = view.read(keylet::mptIssuance(saAmount.get().getMptID())); @@ -2637,102 +2143,6 @@ rippleSendMPT( return rippleCreditMPT(view, uSenderID, issuer, saActual, j); } -static TER -rippleSendMultiMPT( - ApplyView& view, - AccountID const& senderID, - MPTIssue const& mptIssue, - MultiplePaymentDestinations const& receivers, - STAmount& actual, - beast::Journal j, - WaiveTransferFee waiveFee) -{ - // Safe to get MPT since rippleSendMultiMPT is only called by - // accountSendMultiMPT - auto const& issuer = mptIssue.getIssuer(); - - auto const sle = view.read(keylet::mptIssuance(mptIssue.getMptID())); - if (!sle) - return tecOBJECT_NOT_FOUND; - - // These may diverge - STAmount takeFromSender{mptIssue}; - actual = takeFromSender; - - for (auto const& r : receivers) - { - auto const& receiverID = r.first; - STAmount amount{mptIssue, r.second}; - - if (amount < beast::zero) - { - return tecINTERNAL; // LCOV_EXCL_LINE - } - - /* If we aren't sending anything or if the sender is the same as the - * receiver then we don't need to do anything. - */ - if (!amount || (senderID == receiverID)) - continue; - - if (senderID == issuer || receiverID == issuer) - { - // if sender is issuer, check that the new OutstandingAmount will - // not exceed MaximumAmount - if (senderID == issuer) - { - XRPL_ASSERT_PARTS( - takeFromSender == beast::zero, - "rippler::rippleSendMultiMPT", - "sender == issuer, takeFromSender == zero"); - auto const sendAmount = amount.mpt().value(); - auto const maximumAmount = - sle->at(~sfMaximumAmount).value_or(maxMPTokenAmount); - if (sendAmount > maximumAmount || - sle->getFieldU64(sfOutstandingAmount) > - maximumAmount - sendAmount) - return tecPATH_DRY; - } - - // Direct send: redeeming MPTs and/or sending own MPTs. - if (auto const ter = - rippleCreditMPT(view, senderID, receiverID, amount, j)) - return ter; - actual += amount; - // Do not add amount to takeFromSender, because rippleCreditMPT took - // it - - continue; - } - - // Sending 3rd party MPTs: transit. - STAmount actualSend = (waiveFee == WaiveTransferFee::Yes) - ? amount - : multiply( - amount, - transferRate(view, amount.get().getMptID())); - actual += actualSend; - takeFromSender += actualSend; - - JLOG(j.debug()) << "rippleSendMultiMPT> " << to_string(senderID) - << " - > " << to_string(receiverID) - << " : deliver=" << amount.getFullText() - << " cost=" << actualSend.getFullText(); - - if (auto const terResult = - rippleCreditMPT(view, issuer, receiverID, amount, j)) - return terResult; - } - if (senderID != issuer && takeFromSender) - { - if (TER const terResult = - rippleCreditMPT(view, senderID, issuer, takeFromSender, j)) - return terResult; - } - - return tesSUCCESS; -} - static TER accountSendMPT( ApplyView& view, @@ -2758,21 +2168,6 @@ accountSendMPT( view, uSenderID, uReceiverID, saAmount, saActual, j, waiveFee); } -static TER -accountSendMultiMPT( - ApplyView& view, - AccountID const& senderID, - MPTIssue const& mptIssue, - MultiplePaymentDestinations const& receivers, - beast::Journal j, - WaiveTransferFee waiveFee) -{ - STAmount actual; - - return rippleSendMultiMPT( - view, senderID, mptIssue, receivers, actual, j, waiveFee); -} - TER accountSend( ApplyView& view, @@ -2794,31 +2189,6 @@ accountSend( saAmount.asset().value()); } -TER -accountSendMulti( - ApplyView& view, - AccountID const& senderID, - Asset const& asset, - MultiplePaymentDestinations const& receivers, - beast::Journal j, - WaiveTransferFee waiveFee) -{ - XRPL_ASSERT_PARTS( - receivers.size() > 1, - "ripple::accountSendMulti", - "multiple recipients provided"); - return std::visit( - [&](TIss const& issue) { - if constexpr (std::is_same_v) - return accountSendMultiIOU( - view, senderID, issue, receivers, j, waiveFee); - else - return accountSendMultiMPT( - view, senderID, issue, receivers, j, waiveFee); - }, - asset.value()); -} - static bool updateTrustLine( ApplyView& view, @@ -3143,10 +2513,7 @@ requireAuth( if (mptIssuer == account) // Issuer won't have MPToken return tesSUCCESS; - bool const featureSAVEnabled = - view.rules().enabled(featureSingleAssetVault); - - if (featureSAVEnabled) + if (view.rules().enabled(featureSingleAssetVault)) { if (depth >= maxAssetCheckDepth) return tecINTERNAL; // LCOV_EXCL_LINE @@ -3205,13 +2572,6 @@ requireAuth( // belong to someone who is explicitly authorized e.g. a vault owner. } - if (featureSAVEnabled) - { - // Implicitly authorize Vault and LoanBroker pseudo-accounts - if (isPseudoAccount(view, account, {&sfVaultID, &sfLoanBrokerID})) - return tesSUCCESS; - } - // mptoken must be authorized if issuance enabled requireAuth if (sleIssuance->isFlag(lsfMPTRequireAuth) && (!sleToken || !sleToken->isFlag(lsfMPTAuthorized))) @@ -3341,42 +2701,6 @@ canTransfer( return tesSUCCESS; } -[[nodiscard]] TER -canTransfer( - ReadView const& view, - Issue const& issue, - AccountID const& from, - AccountID const& to) -{ - if (issue.native()) - return tesSUCCESS; - - auto const& issuerId = issue.getIssuer(); - if (issuerId == from || issuerId == to) - return tesSUCCESS; - auto const sleIssuer = view.read(keylet::account(issuerId)); - if (sleIssuer == nullptr) - return tefINTERNAL; // LCOV_EXCL_LINE - - auto const isRippleDisabled = [&](AccountID account) -> bool { - // Line might not exist, but some transfers can create it. If this - // is the case, just check the default ripple on the issuer account. - auto const line = view.read(keylet::line(account, issue)); - if (line) - { - bool const issuerHigh = issuerId > account; - return line->isFlag(issuerHigh ? lsfHighNoRipple : lsfLowNoRipple); - } - return sleIssuer->isFlag(lsfDefaultRipple) == false; - }; - - // Fail if rippling disabled on both trust lines - if (isRippleDisabled(from) && isRippleDisabled(to)) - return terNO_RIPPLE; - - return tesSUCCESS; -} - TER cleanupOnAccountDelete( ApplyView& view, diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index b80f648c98..fbe591074e 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -77,8 +77,6 @@ enum class LedgerNameSpace : std::uint16_t { PERMISSIONED_DOMAIN = 'm', DELEGATE = 'E', 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. @@ -549,18 +547,6 @@ 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(uint256 const& loanBrokerID, std::uint32_t loanSeq) noexcept -{ - return loan(indexHash(LedgerNameSpace::LOAN, loanBrokerID, loanSeq)); -} - Keylet permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept { diff --git a/src/libxrpl/protocol/InnerObjectFormats.cpp b/src/libxrpl/protocol/InnerObjectFormats.cpp index d720c6c665..60a2f51f44 100644 --- a/src/libxrpl/protocol/InnerObjectFormats.cpp +++ b/src/libxrpl/protocol/InnerObjectFormats.cpp @@ -153,14 +153,6 @@ InnerObjectFormats::InnerObjectFormats() {sfBookDirectory, soeREQUIRED}, {sfBookNode, soeREQUIRED}, }); - - add(sfCounterpartySignature.jsonName, - sfCounterpartySignature.getCode(), - { - {sfSigningPubKey, soeOPTIONAL}, - {sfTxnSignature, soeOPTIONAL}, - {sfSigners, soeOPTIONAL}, - }); } InnerObjectFormats const& diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 8b714193cc..d659ee0d05 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -279,7 +279,7 @@ STAmount::xrp() const IOUAmount STAmount::iou() const { - if (integral()) + if (native() || !holds()) Throw("Cannot return non-IOU STAmount as IOUAmount"); auto mantissa = static_cast(mValue); @@ -830,7 +830,7 @@ STAmount::isDefault() const void STAmount::canonicalize() { - if (integral()) + if (native() || mAsset.holds()) { // native and MPT currency amounts should always have an offset of zero // log(2^64,10) ~ 19.2 @@ -859,10 +859,8 @@ STAmount::canonicalize() }; if (native()) set(XRPAmount{num}); - else if (mAsset.holds()) - set(MPTAmount{num}); else - Throw("Unknown integral asset type"); + set(MPTAmount{num}); mOffset = 0; } else @@ -1463,33 +1461,6 @@ canonicalizeRoundStrict( } } -STAmount -roundToScale( - STAmount const& value, - std::int32_t scale, - Number::rounding_mode rounding) -{ - // Nothing to do for integral types. - if (value.integral()) - return value; - - // If the value's exponent is greater than or equal to the scale, then - // rounding will do nothing, and might even lose precision, so just return - // the value. - if (value.exponent() >= scale) - return value; - - STAmount const referenceValue{ - value.asset(), STAmount::cMinValue, scale, value.negative()}; - - NumberRoundModeGuard mg(rounding); - // With an IOU, the the result of addition will be truncated to the - // precision of the larger value, which in this case is referenceValue. Then - // remove the reference value via subtraction, and we're left with the - // rounded value. - return (value + referenceValue) - referenceValue; -} - namespace { // We need a class that has an interface similar to NumberRoundModeGuard diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index d3b3ab4a88..b89f337288 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -261,12 +261,15 @@ STTx::checkSign(Rules const& rules) const if (auto const ret = checkSign(rules, *this); !ret) return ret; + /* Placeholder for field that will be added by Lending Protocol if (isFieldPresent(sfCounterpartySignature)) { auto const counterSig = getFieldObject(sfCounterpartySignature); - if (auto const ret = checkSign(rules, counterSig); !ret) + if (auto const ret = checkSign(rules, counterSig); + !ret) return Unexpected("Counterparty: " + ret.error()); } + */ return {}; } diff --git a/src/test/app/Batch_test.cpp b/src/test/app/Batch_test.cpp index f2fae58a9b..9823feaa11 100644 --- a/src/test/app/Batch_test.cpp +++ b/src/test/app/Batch_test.cpp @@ -2535,207 +2535,6 @@ class Batch_test : public beast::unit_test::suite } } - void - testLoan(FeatureBitset features) - { - testcase("loan"); - - bool const lendingBatchEnabled = !std::any_of( - Batch::disabledTxTypes.begin(), - Batch::disabledTxTypes.end(), - [](auto const& disabled) { return disabled == ttLOAN_BROKER_SET; }); - - using namespace test::jtx; - - test::jtx::Env env{ - *this, - envconfig(), - features | featureSingleAssetVault | featureLendingProtocol | - featureMPTokensV1}; - - Account const issuer{"issuer"}; - // For simplicity, lender will be the sole actor for the vault & - // brokers. - Account const lender{"lender"}; - // Borrower only wants to borrow - Account const borrower{"borrower"}; - - // Fund the accounts and trust lines with the same amount so that tests - // can use the same values regardless of the asset. - env.fund(XRP(100'000), issuer, noripple(lender, borrower)); - env.close(); - - // Just use an XRP asset - PrettyAsset const asset{xrpIssue(), 1'000'000}; - - Vault vault{env}; - - auto const deposit = asset(50'000); - auto const debtMaximumValue = asset(25'000).value(); - auto const coverDepositValue = asset(1000).value(); - - auto [tx, vaultKeylet] = - vault.create({.owner = lender, .asset = asset}); - env(tx); - env.close(); - BEAST_EXPECT(env.le(vaultKeylet)); - - env(vault.deposit( - {.depositor = lender, .id = vaultKeylet.key, .amount = deposit})); - env.close(); - - auto const brokerKeylet = - keylet::loanbroker(lender.id(), env.seq(lender)); - - { - using namespace loanBroker; - env(set(lender, vaultKeylet.key), - managementFeeRate(TenthBips16(100)), - debtMaximum(debtMaximumValue), - coverRateMinimum(TenthBips32(percentageToTenthBips(10))), - coverRateLiquidation(TenthBips32(percentageToTenthBips(25)))); - - env(coverDeposit(lender, brokerKeylet.key, coverDepositValue)); - - env.close(); - } - - { - using namespace loan; - using namespace std::chrono_literals; - - auto const lenderSeq = env.seq(lender); - auto const batchFee = batch::calcBatchFee(env, 0, 2); - - auto const loanKeylet = keylet::loan(brokerKeylet.key, 1); - { - auto const [txIDs, batchID] = submitBatch( - env, - lendingBatchEnabled ? temBAD_SIGNATURE - : temINVALID_INNER_BATCH, - batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing), - batch::inner( - env.json( - set(lender, brokerKeylet.key, asset(1000).value()), - // Not allowed to include the counterparty signature - sig(sfCounterpartySignature, borrower), - sig(none), - fee(none), - seq(none)), - lenderSeq + 1), - batch::inner( - pay(lender, - loanKeylet.key, - STAmount{asset, asset(500).value()}), - lenderSeq + 2)); - } - { - auto const [txIDs, batchID] = submitBatch( - env, - temINVALID_INNER_BATCH, - batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing), - batch::inner( - env.json( - set(lender, brokerKeylet.key, asset(1000).value()), - // Counterparty must be set - sig(none), - fee(none), - seq(none)), - lenderSeq + 1), - batch::inner( - pay(lender, - loanKeylet.key, - STAmount{asset, asset(500).value()}), - lenderSeq + 2)); - } - { - auto const [txIDs, batchID] = submitBatch( - env, - lendingBatchEnabled ? temBAD_SIGNER - : temINVALID_INNER_BATCH, - batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing), - batch::inner( - env.json( - set(lender, brokerKeylet.key, asset(1000).value()), - // Counterparty must sign the outer transaction - counterparty(borrower.id()), - sig(none), - fee(none), - seq(none)), - lenderSeq + 1), - batch::inner( - pay(lender, - loanKeylet.key, - STAmount{asset, asset(500).value()}), - lenderSeq + 2)); - } - { - // LoanSet normally charges at least 2x base fee, but since the - // signature check is done by the batch, it only charges the - // base fee. - auto const batchFee = batch::calcBatchFee(env, 1, 2); - auto const [txIDs, batchID] = submitBatch( - env, - lendingBatchEnabled ? TER(tesSUCCESS) - : TER(temINVALID_INNER_BATCH), - batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing), - batch::inner( - env.json( - set(lender, brokerKeylet.key, asset(1000).value()), - counterparty(borrower.id()), - sig(none), - fee(none), - seq(none)), - lenderSeq + 1), - batch::inner( - pay( - // However, this inner transaction will fail, - // because the lender is not allowed to draw the - // transaction - lender, - loanKeylet.key, - STAmount{asset, asset(500).value()}), - lenderSeq + 2), - batch::sig(borrower)); - } - env.close(); - BEAST_EXPECT(env.le(brokerKeylet)); - BEAST_EXPECT(!env.le(loanKeylet)); - { - // LoanSet normally charges at least 2x base fee, but since the - // signature check is done by the batch, it only charges the - // base fee. - auto const lenderSeq = env.seq(lender); - auto const batchFee = batch::calcBatchFee(env, 1, 2); - auto const [txIDs, batchID] = submitBatch( - env, - lendingBatchEnabled ? TER(tesSUCCESS) - : TER(temINVALID_INNER_BATCH), - batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing), - batch::inner( - env.json( - set(lender, brokerKeylet.key, asset(1000).value()), - counterparty(borrower.id()), - sig(none), - fee(none), - seq(none)), - lenderSeq + 1), - batch::inner( - manage(lender, loanKeylet.key, tfLoanImpair), - lenderSeq + 2), - batch::sig(borrower)); - } - env.close(); - BEAST_EXPECT(env.le(brokerKeylet)); - if (auto const sleLoan = env.le(loanKeylet); lendingBatchEnabled - ? BEAST_EXPECT(sleLoan) - : !BEAST_EXPECT(!sleLoan)) - { - BEAST_EXPECT(sleLoan->isFlag(lsfLoanImpaired)); - } - } - } - void testObjectCreateSequence(FeatureBitset features) { @@ -4329,7 +4128,6 @@ class Batch_test : public beast::unit_test::suite testAccountActivation(features); testAccountSet(features); testAccountDelete(features); - testLoan(features); testObjectCreateSequence(features); testObjectCreateTicket(features); testObjectCreate3rdParty(features); diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index 4e4632e49b..63682dff0d 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -155,13 +155,9 @@ class Invariants_test : public beast::unit_test::suite {{"an account root was deleted"}}, [](Account const& A1, Account const&, ApplyContext& ac) { // remove an account from the view - auto sle = ac.view().peek(keylet::account(A1.id())); + auto const sle = ac.view().peek(keylet::account(A1.id())); if (!sle) return false; - // Clear the balance so the "account deletion left behind a - // non-zero balance" check doesn't trip earlier than the desired - // check. - sle->at(sfBalance) = beast::zero; ac.view().erase(sle); return true; }); @@ -185,15 +181,10 @@ class Invariants_test : public beast::unit_test::suite {{"account deletion succeeded but deleted multiple accounts"}}, [](Account const& A1, Account const& A2, ApplyContext& ac) { // remove two accounts from the view - auto sleA1 = ac.view().peek(keylet::account(A1.id())); - auto sleA2 = ac.view().peek(keylet::account(A2.id())); + auto const sleA1 = ac.view().peek(keylet::account(A1.id())); + auto const sleA2 = ac.view().peek(keylet::account(A2.id())); if (!sleA1 || !sleA2) return false; - // Clear the balance so the "account deletion left behind a - // non-zero balance" check doesn't trip earlier than the desired - // check. - sleA1->at(sfBalance) = beast::zero; - sleA2->at(sfBalance) = beast::zero; ac.view().erase(sleA1); ac.view().erase(sleA2); return true; @@ -208,46 +199,6 @@ class Invariants_test : public beast::unit_test::suite using namespace test::jtx; testcase << "account root deletion left artifact"; - doInvariantCheck( - {{"account deletion left behind a non-zero balance"}}, - [&](Account const& A1, Account const& A2, ApplyContext& ac) { - // A1 has a balance. Delete A1 - auto const a1 = A1.id(); - auto const sleA1 = ac.view().peek(keylet::account(a1)); - if (!sleA1) - return false; - if (!BEAST_EXPECT(*sleA1->at(sfBalance) != beast::zero)) - return false; - - ac.view().erase(sleA1); - - return true; - }, - XRPAmount{}, - STTx{ttACCOUNT_DELETE, [](STObject& tx) {}}); - - doInvariantCheck( - {{"account deletion left behind a non-zero owner count"}}, - [&](Account const& A1, Account const& A2, ApplyContext& ac) { - // Increment A1's owner count, then delete A1 - auto const a1 = A1.id(); - auto const sleA1 = ac.view().peek(keylet::account(a1)); - if (!sleA1) - return false; - // Clear the balance so the "account deletion left behind a - // non-zero balance" check doesn't trip earlier than the desired - // check. - sleA1->at(sfBalance) = beast::zero; - BEAST_EXPECT(sleA1->at(sfOwnerCount) == 0); - adjustOwnerCount(ac.view(), sleA1, 1, ac.journal); - - ac.view().erase(sleA1); - - return true; - }, - XRPAmount{}, - STTx{ttACCOUNT_DELETE, [](STObject& tx) {}}); - for (auto const& keyletInfo : directAccountKeylets) { // TODO: Use structured binding once LLVM 16 is the minimum @@ -268,38 +219,29 @@ class Invariants_test : public beast::unit_test::suite // Add an object to the ledger for account A1, then delete // A1 auto const a1 = A1.id(); - auto sleA1 = ac.view().peek(keylet::account(a1)); + auto const sleA1 = ac.view().peek(keylet::account(a1)); if (!sleA1) return false; auto const key = std::invoke(keyletfunc, a1); auto const newSLE = std::make_shared(key); ac.view().insert(newSLE); - // Clear the balance so the "account deletion left behind a - // non-zero balance" check doesn't trip earlier than the - // desired check. - sleA1->at(sfBalance) = beast::zero; ac.view().erase(sleA1); return true; }, XRPAmount{}, STTx{ttACCOUNT_DELETE, [](STObject& tx) {}}); - } + }; // NFT special case doInvariantCheck( {{"account deletion left behind a NFTokenPage object"}}, [&](Account const& A1, Account const&, ApplyContext& ac) { // remove an account from the view - auto sle = ac.view().peek(keylet::account(A1.id())); + auto const sle = ac.view().peek(keylet::account(A1.id())); if (!sle) return false; - // Clear the balance so the "account deletion left behind a - // non-zero balance" check doesn't trip earlier than the desired - // check. - sle->at(sfBalance) = beast::zero; - sle->at(sfOwnerCount) = 0; ac.view().erase(sle); return true; }, @@ -323,18 +265,13 @@ class Invariants_test : public beast::unit_test::suite [&](Account const& A1, Account const& A2, ApplyContext& ac) { // Delete the AMM account without cleaning up the directory or // deleting the AMM object - auto sle = ac.view().peek(keylet::account(ammAcctID)); + auto const sle = ac.view().peek(keylet::account(ammAcctID)); if (!sle) return false; BEAST_EXPECT(sle->at(~sfAMMID)); BEAST_EXPECT(sle->at(~sfAMMID) == ammKey); - // Clear the balance so the "account deletion left behind a - // non-zero balance" check doesn't trip earlier than the desired - // check. - sle->at(sfBalance) = beast::zero; - sle->at(sfOwnerCount) = 0; ac.view().erase(sle); return true; @@ -357,7 +294,7 @@ class Invariants_test : public beast::unit_test::suite // Delete all the AMM's trust lines, remove the AMM from the AMM // account's directory (this deletes the directory), and delete // the AMM account. Do not delete the AMM object. - auto sle = ac.view().peek(keylet::account(ammAcctID)); + auto const sle = ac.view().peek(keylet::account(ammAcctID)); if (!sle) return false; @@ -397,11 +334,6 @@ class Invariants_test : public beast::unit_test::suite !ac.view().exists(ownerDirKeylet) || ac.view().emptyDirDelete(ownerDirKeylet)); - // Clear the balance so the "account deletion left behind a - // non-zero balance" check doesn't trip earlier than the desired - // check. - sle->at(sfBalance) = beast::zero; - sle->at(sfOwnerCount) = 0; ac.view().erase(sle); return true; @@ -1790,359 +1722,6 @@ class Invariants_test : public beast::unit_test::suite {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); } - Keylet - createLoanBroker( - jtx::Account const& a, - jtx::Env& env, - jtx::PrettyAsset const& asset) - { - using namespace jtx; - - // Create vault - uint256 vaultID; - Vault vault{env}; - auto [tx, vKeylet] = vault.create({.owner = a, .asset = asset}); - env(tx); - BEAST_EXPECT(env.le(vKeylet)); - - vaultID = vKeylet.key; - - // Create Loan Broker - using namespace loanBroker; - - auto const loanBrokerKeylet = keylet::loanbroker(a.id(), env.seq(a)); - // Create a Loan Broker with all default values. - env(set(a, vaultID), fee(increment)); - - return loanBrokerKeylet; - }; - - void - testNoModifiedUnmodifiableFields() - { - testcase("no modified unmodifiable fields"); - using namespace jtx; - - // Initialize with a placeholder value because there's no default ctor - Keylet loanBrokerKeylet = keylet::amendments(); - Preclose createLoanBroker = - [&, this](Account const& a, Account const& b, Env& env) { - PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; - - loanBrokerKeylet = this->createLoanBroker(a, env, xrpAsset); - return BEAST_EXPECT(env.le(loanBrokerKeylet)); - }; - - { - auto const mods = - std::to_array>({ - [](SLE::pointer& sle) { sle->at(sfSequence) += 1; }, - [](SLE::pointer& sle) { sle->at(sfOwnerNode) += 1; }, - [](SLE::pointer& sle) { sle->at(sfVaultNode) += 1; }, - [](SLE::pointer& sle) { sle->at(sfVaultID) = uint256(1u); }, - [](SLE::pointer& sle) { - sle->at(sfAccount) = sle->at(sfOwner); - }, - [](SLE::pointer& sle) { - sle->at(sfOwner) = sle->at(sfAccount); - }, - [](SLE::pointer& sle) { - sle->at(sfManagementFeeRate) += 1; - }, - [](SLE::pointer& sle) { sle->at(sfCoverRateMinimum) += 1; }, - [](SLE::pointer& sle) { - sle->at(sfCoverRateLiquidation) += 1; - }, - [](SLE::pointer& sle) { sle->at(sfLedgerEntryType) += 1; }, - [](SLE::pointer& sle) { - sle->at(sfLedgerIndex) = sle->at(sfVaultID).value(); - }, - }); - - for (auto const& mod : mods) - { - doInvariantCheck( - {{"changed an unchangable field"}}, - [&](Account const& A1, Account const&, ApplyContext& ac) { - auto sle = ac.view().peek(loanBrokerKeylet); - if (!sle) - return false; - mod(sle); - ac.view().update(sle); - return true; - }, - XRPAmount{}, - STTx{ttACCOUNT_SET, [](STObject& tx) {}}, - {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, - createLoanBroker); - } - } - - // TODO: Loan Object - - { - auto const mods = - std::to_array>({ - [](SLE::pointer& sle) { sle->at(sfLedgerEntryType) += 1; }, - [](SLE::pointer& sle) { - sle->at(sfLedgerIndex) = uint256(1u); - }, - }); - - for (auto const& mod : mods) - { - doInvariantCheck( - {{"changed an unchangable field"}}, - [&](Account const& A1, Account const&, ApplyContext& ac) { - auto sle = ac.view().peek(keylet::account(A1.id())); - if (!sle) - return false; - mod(sle); - ac.view().update(sle); - return true; - }); - } - } - } - - void - testValidLoanBroker() - { - testcase << "valid loan broker"; - - using namespace jtx; - - enum class Asset { XRP, IOU, MPT }; - auto const assetTypes = - std::to_array({Asset::XRP, Asset::IOU, Asset::MPT}); - - for (auto const assetType : assetTypes) - { - // Initialize with a placeholder value because there's no default - // ctor - Keylet loanBrokerKeylet = keylet::amendments(); - Preclose createLoanBroker = [&, this]( - Account const& alice, - Account const& issuer, - Env& env) { - PrettyAsset const asset = [&]() { - switch (assetType) - { - case Asset::IOU: { - PrettyAsset const iouAsset = issuer["IOU"]; - env(trust(alice, iouAsset(1000))); - env(pay(issuer, alice, iouAsset(1000))); - env.close(); - return iouAsset; - } - - case Asset::MPT: { - MPTTester mptt{env, issuer, mptInitNoFund}; - mptt.create( - {.flags = tfMPTCanClawback | tfMPTCanTransfer | - tfMPTCanLock}); - PrettyAsset const mptAsset = mptt.issuanceID(); - mptt.authorize({.account = alice}); - env(pay(issuer, alice, mptAsset(1000))); - env.close(); - return mptAsset; - } - - case Asset::XRP: - default: - return PrettyAsset{xrpIssue(), 1'000'000}; - } - }(); - loanBrokerKeylet = this->createLoanBroker(alice, env, asset); - return BEAST_EXPECT(env.le(loanBrokerKeylet)); - }; - - // Ensure the test scenarios are set up completely. The test cases - // will need to recompute any of these values it needs for itself - // rather than trying to return a bunch of items - auto setupTest = - [&, this](Account const& A1, Account const&, ApplyContext& ac) - -> std::optional> { - if (loanBrokerKeylet.type != ltLOAN_BROKER) - return {}; - auto sleBroker = ac.view().peek(loanBrokerKeylet); - if (!sleBroker) - return {}; - if (!BEAST_EXPECT(sleBroker->at(sfOwnerCount) == 0)) - return {}; - // Need to touch sleBroker so that it is included in the - // modified entries for the invariant to find - ac.view().update(sleBroker); - - // The pseudo-account holds the directory, so get it - auto const pseudoAccountID = sleBroker->at(sfAccount); - auto const pseudoAccountKeylet = - keylet::account(pseudoAccountID); - // Strictly speaking, we don't need to load the - // ACCOUNT_ROOT, but check anyway - auto slePseudo = ac.view().peek(pseudoAccountKeylet); - if (!BEAST_EXPECT(slePseudo)) - return {}; - // Make sure the directory doesn't already exist - auto const dirKeylet = keylet::ownerDir(pseudoAccountID); - auto sleDir = ac.view().peek(dirKeylet); - auto const describe = describeOwnerDir(pseudoAccountID); - if (!sleDir) - { - // Create the directory - BEAST_EXPECT( - ::ripple::directory::createRoot( - ac.view(), - dirKeylet, - loanBrokerKeylet.key, - describe) == 0); - - sleDir = ac.view().peek(dirKeylet); - } - - return std::make_pair(slePseudo, sleDir); - }; - - doInvariantCheck( - {{"Loan Broker with zero OwnerCount has multiple directory " - "pages"}}, - [&setupTest, this]( - Account const& A1, Account const& A2, ApplyContext& ac) { - auto test = setupTest(A1, A2, ac); - if (!test || !test->first || !test->second) - return false; - - auto slePseudo = test->first; - auto sleDir = test->second; - auto const describe = - describeOwnerDir(slePseudo->at(sfAccount)); - - BEAST_EXPECT( - ::ripple::directory::insertPage( - ac.view(), - 0, - sleDir, - 0, - sleDir, - slePseudo->key(), - keylet::page(sleDir->key(), 0), - describe) == 1); - - return true; - }, - XRPAmount{}, - STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}}, - {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, - createLoanBroker); - - doInvariantCheck( - {{"Loan Broker with zero OwnerCount has multiple indexes in " - "the Directory root"}}, - [&setupTest]( - Account const& A1, Account const& A2, ApplyContext& ac) { - auto test = setupTest(A1, A2, ac); - if (!test || !test->first || !test->second) - return false; - - auto slePseudo = test->first; - auto sleDir = test->second; - auto indexes = sleDir->getFieldV256(sfIndexes); - - // Put some extra garbage into the directory - for (auto const& key : {slePseudo->key(), sleDir->key()}) - { - ::ripple::directory::insertKey( - ac.view(), sleDir, 0, false, indexes, key); - } - - return true; - }, - XRPAmount{}, - STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}}, - {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, - createLoanBroker); - - doInvariantCheck( - {{"Loan Broker directory corrupt"}}, - [&setupTest]( - Account const& A1, Account const& A2, ApplyContext& ac) { - auto test = setupTest(A1, A2, ac); - if (!test || !test->first || !test->second) - return false; - - auto slePseudo = test->first; - auto sleDir = test->second; - auto const describe = - describeOwnerDir(slePseudo->at(sfAccount)); - // Empty vector will overwrite the existing entry for the - // holding, if any, avoiding the "has multiple indexes" - // failure. - STVector256 indexes; - - // Put one meaningless key into the directory - auto const key = - keylet::account(Account("random").id()).key; - ::ripple::directory::insertKey( - ac.view(), sleDir, 0, false, indexes, key); - - return true; - }, - XRPAmount{}, - STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}}, - {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, - createLoanBroker); - - doInvariantCheck( - {{"Loan Broker with zero OwnerCount has an unexpected entry in " - "the directory"}}, - [&setupTest]( - Account const& A1, Account const& A2, ApplyContext& ac) { - auto test = setupTest(A1, A2, ac); - if (!test || !test->first || !test->second) - return false; - - auto slePseudo = test->first; - auto sleDir = test->second; - // Empty vector will overwrite the existing entry for the - // holding, if any, avoiding the "has multiple indexes" - // failure. - STVector256 indexes; - - ::ripple::directory::insertKey( - ac.view(), sleDir, 0, false, indexes, slePseudo->key()); - - return true; - }, - XRPAmount{}, - STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}}, - {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, - createLoanBroker); - - doInvariantCheck( - {{"Loan Broker sequence number decreased"}}, - [&](Account const& A1, Account const& A2, ApplyContext& ac) { - if (loanBrokerKeylet.type != ltLOAN_BROKER) - return false; - auto sleBroker = ac.view().peek(loanBrokerKeylet); - if (!sleBroker) - return false; - if (!BEAST_EXPECT(sleBroker->at(sfLoanSequence) > 0)) - return false; - // Need to touch sleBroker so that it is included in the - // modified entries for the invariant to find - ac.view().update(sleBroker); - - sleBroker->at(sfLoanSequence) -= 1; - - return true; - }, - XRPAmount{}, - STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}}, - {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, - createLoanBroker); - } - } - void testVault() { @@ -3906,10 +3485,8 @@ public: testValidNewAccountRoot(); testNFTokenPageInvariants(); testPermissionedDomainInvariants(); - testPermissionedDEX(); - testNoModifiedUnmodifiableFields(); testValidPseudoAccounts(); - testValidLoanBroker(); + testPermissionedDEX(); testVault(); } }; diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp deleted file mode 100644 index 597e2ea75b..0000000000 --- a/src/test/app/LoanBroker_test.cpp +++ /dev/null @@ -1,1462 +0,0 @@ -#include - -#include - -#include - -namespace ripple { -namespace test { - -class LoanBroker_test : public beast::unit_test::suite -{ - // Ensure that all the features needed for Lending Protocol are included, - // even if they are set to unsupported. - FeatureBitset const all{ - jtx::testable_amendments() | featureMPTokensV1 | - featureSingleAssetVault | featureLendingProtocol}; - - void - testDisabled() - { - testcase("Disabled"); - // Lending Protocol depends on Single Asset Vault (SAV). Test - // combinations of the two amendments. - // Single Asset Vault depends on MPTokensV1, but don't test every combo - // of that. - using namespace jtx; - auto failAll = [this](FeatureBitset features, bool goodVault = false) { - Env env(*this, features); - - Account const alice{"alice"}; - env.fund(XRP(10000), alice); - - // Try to create a vault - PrettyAsset const asset{xrpIssue(), 1'000'000}; - Vault vault{env}; - auto const [tx, keylet] = - vault.create({.owner = alice, .asset = asset}); - env(tx, ter(goodVault ? ter(tesSUCCESS) : ter(temDISABLED))); - env.close(); - BEAST_EXPECT(static_cast(env.le(keylet)) == goodVault); - - using namespace loanBroker; - // Can't create a loan broker regardless of whether the vault exists - env(set(alice, keylet.key), ter(temDISABLED)); - auto const brokerKeylet = - keylet::loanbroker(alice.id(), env.seq(alice)); - // Other LoanBroker transactions are disabled, too. - // 1. LoanBrokerCoverDeposit - env(coverDeposit(alice, brokerKeylet.key, asset(1000)), - ter(temDISABLED)); - // 2. LoanBrokerCoverWithdraw - env(coverWithdraw(alice, brokerKeylet.key, asset(1000)), - ter(temDISABLED)); - // 3. LoanBrokerCoverClawback - env(coverClawback(alice), ter(temDISABLED)); - env(coverClawback(alice), - loanBrokerID(brokerKeylet.key), - ter(temDISABLED)); - env(coverClawback(alice), amount(asset(0)), ter(temDISABLED)); - env(coverClawback(alice), - loanBrokerID(brokerKeylet.key), - amount(asset(1000)), - ter(temDISABLED)); - // 4. LoanBrokerDelete - env(del(alice, brokerKeylet.key), ter(temDISABLED)); - }; - failAll(all - featureMPTokensV1); - failAll(all - featureSingleAssetVault - featureLendingProtocol); - failAll(all - featureSingleAssetVault); - failAll(all - featureLendingProtocol, true); - } - - struct VaultInfo - { - jtx::PrettyAsset asset; - uint256 vaultID; - jtx::Account pseudoAccount; - VaultInfo( - jtx::PrettyAsset const& asset_, - uint256 const& vaultID_, - AccountID const& pseudo) - : asset(asset_), vaultID(vaultID_), pseudoAccount("vault", pseudo) - { - } - }; - - void - lifecycle( - char const* label, - jtx::Env& env, - jtx::Account const& issuer, - jtx::Account const& alice, - jtx::Account const& evan, - jtx::Account const& bystander, - VaultInfo const& vault, - VaultInfo const& badVault, - std::function modifyJTx, - std::function checkBroker, - std::function changeBroker, - std::function checkChangedBroker) - { - { - auto const& asset = vault.asset.raw(); - testcase << "Lifecycle: " - << (asset.native() ? "XRP " - : asset.holds() ? "IOU " - : asset.holds() ? "MPT " - : "Unknown ") - << label; - } - - using namespace jtx; - using namespace loanBroker; - - // Bogus assets to use in test cases - static PrettyAsset const badMptAsset = [&]() { - MPTTester badMptt{env, evan, mptInitNoFund}; - badMptt.create( - {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); - env.close(); - return badMptt["BAD"]; - }(); - static PrettyAsset const badIouAsset = evan["BAD"]; - static Account const nonExistent{"NonExistent"}; - static PrettyAsset const ghostIouAsset = nonExistent["GST"]; - PrettyAsset const vaultPseudoIouAsset = vault.pseudoAccount["PSD"]; - - auto const badKeylet = keylet::loanbroker(alice.id(), env.seq(alice)); - env(set(alice, badVault.vaultID)); - env.close(); - auto const badBrokerPseudo = [&]() { - if (auto const le = env.le(badKeylet); BEAST_EXPECT(le)) - { - return Account{"Bad Broker pseudo-account", le->at(sfAccount)}; - } - // Just to make the build work - return vault.pseudoAccount; - }(); - PrettyAsset const badBrokerPseudoIouAsset = badBrokerPseudo["WAT"]; - - auto const keylet = keylet::loanbroker(alice.id(), env.seq(alice)); - { - // Start with default values - auto jtx = env.jt(set(alice, vault.vaultID)); - // Modify as desired - if (modifyJTx) - jtx = modifyJTx(jtx); - // Successfully create a Loan Broker - env(jtx); - } - - env.close(); - if (auto broker = env.le(keylet); BEAST_EXPECT(broker)) - { - // log << "Broker after create: " << to_string(broker->getJson()) - // << std::endl; - BEAST_EXPECT(broker->at(sfVaultID) == vault.vaultID); - BEAST_EXPECT(broker->at(sfAccount) != alice.id()); - BEAST_EXPECT(broker->at(sfOwner) == alice.id()); - BEAST_EXPECT(broker->at(sfFlags) == 0); - BEAST_EXPECT(broker->at(sfSequence) == env.seq(alice) - 1); - BEAST_EXPECT(broker->at(sfOwnerCount) == 0); - BEAST_EXPECT(broker->at(sfLoanSequence) == 1); - BEAST_EXPECT(broker->at(sfDebtTotal) == 0); - BEAST_EXPECT(broker->at(sfCoverAvailable) == 0); - if (checkBroker) - checkBroker(broker); - - // if (auto const vaultSLE = env.le(keylet::vault(vault.vaultID))) - //{ - // log << "Vault: " << to_string(vaultSLE->getJson()) << - // std::endl; - // } - // Load the pseudo-account - Account const pseudoAccount{ - "Broker pseudo-account", broker->at(sfAccount)}; - - auto const pseudoKeylet = keylet::account(pseudoAccount); - if (auto const pseudo = env.le(pseudoKeylet); BEAST_EXPECT(pseudo)) - { - // log << "Pseudo-account after create: " - // << to_string(pseudo->getJson()) << std::endl - // << std::endl; - BEAST_EXPECT( - pseudo->at(sfFlags) == - (lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth)); - BEAST_EXPECT(pseudo->at(sfSequence) == 0); - BEAST_EXPECT(pseudo->at(sfBalance) == beast::zero); - BEAST_EXPECT( - pseudo->at(sfOwnerCount) == - (vault.asset.raw().native() ? 0 : 1)); - BEAST_EXPECT(!pseudo->isFieldPresent(sfAccountTxnID)); - BEAST_EXPECT(!pseudo->isFieldPresent(sfRegularKey)); - BEAST_EXPECT(!pseudo->isFieldPresent(sfEmailHash)); - BEAST_EXPECT(!pseudo->isFieldPresent(sfWalletLocator)); - BEAST_EXPECT(!pseudo->isFieldPresent(sfWalletSize)); - BEAST_EXPECT(!pseudo->isFieldPresent(sfMessageKey)); - BEAST_EXPECT(!pseudo->isFieldPresent(sfTransferRate)); - BEAST_EXPECT(!pseudo->isFieldPresent(sfDomain)); - BEAST_EXPECT(!pseudo->isFieldPresent(sfTickSize)); - BEAST_EXPECT(!pseudo->isFieldPresent(sfTicketCount)); - BEAST_EXPECT(!pseudo->isFieldPresent(sfNFTokenMinter)); - BEAST_EXPECT(!pseudo->isFieldPresent(sfMintedNFTokens)); - BEAST_EXPECT(!pseudo->isFieldPresent(sfBurnedNFTokens)); - BEAST_EXPECT(!pseudo->isFieldPresent(sfFirstNFTokenSequence)); - BEAST_EXPECT(!pseudo->isFieldPresent(sfAMMID)); - BEAST_EXPECT(!pseudo->isFieldPresent(sfVaultID)); - BEAST_EXPECT(pseudo->at(sfLoanBrokerID) == keylet.key); - } - - { - // Get the AccountInfo RPC result for the broker pseudo-account - std::string const pseudoStr = to_string(pseudoAccount.id()); - auto const accountInfo = env.rpc("account_info", pseudoStr); - if (BEAST_EXPECT(accountInfo.isObject())) - { - auto const& accountData = - accountInfo[jss::result][jss::account_data]; - if (BEAST_EXPECT(accountData.isObject())) - { - BEAST_EXPECT(accountData[jss::Account] == pseudoStr); - BEAST_EXPECT( - accountData[sfLoanBrokerID] == - to_string(keylet.key)); - } - auto const& pseudoInfo = - accountInfo[jss::result][jss::pseudo_account]; - if (BEAST_EXPECT(pseudoInfo.isObject())) - { - BEAST_EXPECT(pseudoInfo[jss::type] == "LoanBroker"); - } - } - } - - auto verifyCoverAmount = - [&env, &vault, &pseudoAccount, &broker, &keylet, this](auto n) { - using namespace jtx; - - if (BEAST_EXPECT(broker = env.le(keylet))) - { - auto const amount = vault.asset(n); - BEAST_EXPECT( - broker->at(sfCoverAvailable) == amount.number()); - env.require(balance(pseudoAccount, amount)); - } - }; - - // Test Cover funding before allowing alterations - env(coverDeposit(alice, uint256(0), vault.asset(10)), - ter(temINVALID)); - env(coverDeposit(evan, keylet.key, vault.asset(10)), - ter(tecNO_PERMISSION)); - env(coverDeposit(evan, keylet.key, vault.asset(0)), - ter(temBAD_AMOUNT)); - env(coverDeposit(evan, keylet.key, vault.asset(-10)), - ter(temBAD_AMOUNT)); - env(coverDeposit(alice, vault.vaultID, vault.asset(10)), - ter(tecNO_ENTRY)); - - verifyCoverAmount(0); - - // Test cover clawback failure cases BEFORE depositing any cover - // Need one of brokerID or amount - env(coverClawback(alice), ter(temINVALID)); - env(coverClawback(alice), - loanBrokerID(uint256(0)), - ter(temINVALID)); - env(coverClawback(alice), amount(XRP(1000)), ter(temBAD_AMOUNT)); - env(coverClawback(alice), - amount(vault.asset(-10)), - ter(temBAD_AMOUNT)); - // Clawbacks with an MPT need to specify the broker ID - env(coverClawback(alice), amount(badMptAsset(1)), ter(temINVALID)); - env(coverClawback(evan), - loanBrokerID(vault.vaultID), - ter(tecNO_ENTRY)); - // Only the issuer can clawback - env(coverClawback(alice), - loanBrokerID(keylet.key), - ter(tecNO_PERMISSION)); - if (vault.asset.raw().native()) - { - // Can not clawback XRP under any circumstances - env(coverClawback(issuer), - loanBrokerID(keylet.key), - ter(tecNO_PERMISSION)); - } - else - { - if (vault.asset.raw().holds()) - { - // Clawbacks without a loanBrokerID need to specify an IOU - // with the broker's pseudo-account as the issuer - env(coverClawback(alice), - amount(ghostIouAsset(1)), - ter(tecNO_ENTRY)); - env(coverClawback(alice), - amount(badIouAsset(1)), - ter(tecOBJECT_NOT_FOUND)); - // Pseudo-account is not for a broker - env(coverClawback(alice), - amount(vaultPseudoIouAsset(1)), - ter(tecOBJECT_NOT_FOUND)); - // If we specify a pseudo-account as the IOU amount, it - // needs to match the loan broker - env(coverClawback(issuer), - loanBrokerID(keylet.key), - amount(badBrokerPseudoIouAsset(10)), - ter(tecWRONG_ASSET)); - PrettyAsset const brokerWrongCurrencyAsset = - pseudoAccount["WAT"]; - env(coverClawback(issuer), - loanBrokerID(keylet.key), - amount(brokerWrongCurrencyAsset(10)), - ter(tecWRONG_ASSET)); - } - else - { - // Clawbacks with an MPT need to specify the broker ID, even - // if the asset is valid - BEAST_EXPECT(vault.asset.raw().holds()); - env(coverClawback(alice), - amount(vault.asset(10)), - ter(temINVALID)); - } - // Since no cover has been deposited, there's nothing to claw - // back - env(coverClawback(issuer), - loanBrokerID(keylet.key), - amount(vault.asset(10)), - ter(tecINSUFFICIENT_FUNDS)); - } - env.close(); - - // Fund the cover deposit - env(coverDeposit(alice, keylet.key, vault.asset(10))); - env.close(); - verifyCoverAmount(10); - - // Test withdrawal failure cases - env(coverWithdraw(alice, uint256(0), vault.asset(10)), - ter(temINVALID)); - env(coverWithdraw(evan, keylet.key, vault.asset(10)), - ter(tecNO_PERMISSION)); - env(coverWithdraw(evan, keylet.key, vault.asset(0)), - ter(temBAD_AMOUNT)); - env(coverWithdraw(evan, keylet.key, vault.asset(-10)), - ter(temBAD_AMOUNT)); - env(coverWithdraw(alice, vault.vaultID, vault.asset(10)), - ter(tecNO_ENTRY)); - env(coverWithdraw(alice, keylet.key, vault.asset(900)), - ter(tecINSUFFICIENT_FUNDS)); - - // Skip this test for XRP, because that can always be sent - if (!vault.asset.raw().native()) - { - TER const expected = vault.asset.raw().holds() - ? tecNO_AUTH - : tecNO_LINE; - env(coverWithdraw(alice, keylet.key, vault.asset(1)), - destination(bystander), - ter(expected)); - } - - // Can not withdraw to the zero address - env(coverWithdraw(alice, keylet.key, vault.asset(1)), - destination(AccountID{}), - ter(temMALFORMED)); - - // Withdraw some of the cover amount - env(coverWithdraw(alice, keylet.key, vault.asset(7))); - env.close(); - verifyCoverAmount(3); - - // Add some more cover - env(coverDeposit(alice, keylet.key, vault.asset(5))); - env.close(); - verifyCoverAmount(8); - - // Withdraw some more. Send it to Evan. Very generous, considering - // how much trouble he's been. - env(coverWithdraw(alice, keylet.key, vault.asset(1)), - destination(evan)); - env.close(); - verifyCoverAmount(7); - - // Withdraw some more. Send it to Evan. Very generous, considering - // how much trouble he's been. - env(coverWithdraw(alice, keylet.key, vault.asset(1)), - destination(evan), - dtag(3)); - env.close(); - verifyCoverAmount(6); - - if (!vault.asset.raw().native()) - { - // Issuer claws back some of the cover - env(coverClawback(issuer), - loanBrokerID(keylet.key), - amount(vault.asset(2))); - env.close(); - verifyCoverAmount(4); - - // Deposit some back - env(coverDeposit(alice, keylet.key, vault.asset(5))); - env.close(); - verifyCoverAmount(9); - - // Issuer claws it all back in various different ways - for (auto const& tx : { - // defer autofills until submission time - env.json( - coverClawback(issuer), - loanBrokerID(keylet.key), - fee(none), - seq(none), - sig(none)), - env.json( - coverClawback(issuer), - loanBrokerID(keylet.key), - amount(vault.asset(0)), - fee(none), - seq(none), - sig(none)), - env.json( - coverClawback(issuer), - loanBrokerID(keylet.key), - amount(vault.asset(6)), - fee(none), - seq(none), - sig(none)), - // amount will be truncated to what's available - env.json( - coverClawback(issuer), - loanBrokerID(keylet.key), - amount(vault.asset(100)), - fee(none), - seq(none), - sig(none)), - }) - { - // Issuer claws it all back - env(tx); - env.close(); - verifyCoverAmount(0); - - // Deposit some back - env(coverDeposit(alice, keylet.key, vault.asset(6))); - env.close(); - verifyCoverAmount(6); - } - } - - // no-op - env(set(alice, vault.vaultID), loanBrokerID(keylet.key)); - env.close(); - - // Make modifications to the broker - if (changeBroker) - changeBroker(broker); - - env.close(); - - // Check the results of modifications - if (BEAST_EXPECT(broker = env.le(keylet)) && checkChangedBroker) - checkChangedBroker(broker); - - // Verify that fields get removed when set to default values - // Debt maximum: explicit 0 - // Data: explicit empty - env(set(alice, vault.vaultID), - loanBrokerID(broker->key()), - debtMaximum(Number(0)), - data("")); - env.close(); - - // Check the updated fields - if (BEAST_EXPECT(broker = env.le(keylet))) - { - BEAST_EXPECT(!broker->isFieldPresent(sfDebtMaximum)); - BEAST_EXPECT(!broker->isFieldPresent(sfData)); - } - - ///////////////////////////////////// - // try to delete the wrong broker object - env(del(alice, vault.vaultID), ter(tecNO_ENTRY)); - // evan tries to delete the broker - env(del(evan, keylet.key), ter(tecNO_PERMISSION)); - - // Get the "bad" broker out of the way - env(del(alice, badKeylet.key)); - env.close(); - - // Note alice's balance of the asset and the broker account's cover - // funds - auto const aliceBalance = env.balance(alice, vault.asset); - auto const coverFunds = env.balance(pseudoAccount, vault.asset); - BEAST_EXPECT(coverFunds.number() == broker->at(sfCoverAvailable)); - BEAST_EXPECT(coverFunds != beast::zero); - verifyCoverAmount(6); - - // delete the broker - // log << "Broker before delete: " << to_string(broker->getJson()) - // << std::endl; - // if (auto const pseudo = env.le(pseudoKeylet); - // BEAST_EXPECT(pseudo)) - //{ - // log << "Pseudo-account before delete: " - // << to_string(pseudo->getJson()) << std::endl - // << std::endl; - //} - - env(del(alice, keylet.key)); - env.close(); - { - broker = env.le(keylet); - BEAST_EXPECT(!broker); - auto pseudo = env.le(pseudoKeylet); - BEAST_EXPECT(!pseudo); - } - auto const expectedBalance = aliceBalance + coverFunds - - (aliceBalance.value().native() - ? STAmount(env.current()->fees().base.value()) - : vault.asset(0)); - env.require(balance(alice, expectedBalance)); - env.require(balance(pseudoAccount, vault.asset(none))); - } - } - - void - testLifecycle() - { - testcase("Lifecycle"); - using namespace jtx; - - // Create 3 loan brokers: one for XRP, one for an IOU, and one for an - // MPT. That'll require three corresponding SAVs. - Env env(*this, all); - - Account issuer{"issuer"}; - // For simplicity, alice will be the sole actor for the vault & brokers. - Account alice{"alice"}; - // Evan will attempt to be naughty - Account evan{"evan"}; - // Bystander doesn't have anything to do with the SAV or Broker, or any - // of the relevant tokens - Account bystander{"bystander"}; - Vault vault{env}; - - // Fund the accounts and trust lines with the same amount so that tests - // can use the same values regardless of the asset. - env.fund(XRP(100'000), issuer, noripple(alice, evan, bystander)); - env.close(); - - env(fset(issuer, asfAllowTrustLineClawback)); - env.close(); - - // Create assets - PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; - PrettyAsset const iouAsset = issuer["IOU"]; - env(trust(alice, iouAsset(1'000'000))); - env(trust(evan, iouAsset(1'000'000))); - env.close(); - env(pay(issuer, evan, iouAsset(100'000))); - env(pay(issuer, alice, iouAsset(100'000))); - env.close(); - - MPTTester mptt{env, issuer, mptInitNoFund}; - mptt.create( - {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); - env.close(); - PrettyAsset const mptAsset = mptt["MPT"]; - mptt.authorize({.account = alice}); - mptt.authorize({.account = evan}); - env.close(); - env(pay(issuer, alice, mptAsset(100'000))); - env(pay(issuer, evan, mptAsset(100'000))); - env.close(); - - std::array const assets{xrpAsset, iouAsset, mptAsset}; - - // Create vaults - std::vector vaults; - for (auto const& asset : assets) - { - auto [tx, keylet] = vault.create({.owner = alice, .asset = asset}); - env(tx); - env.close(); - if (auto const le = env.le(keylet); BEAST_EXPECT(env.le(keylet))) - { - vaults.emplace_back(asset, keylet.key, le->at(sfAccount)); - } - - env(vault.deposit( - {.depositor = alice, .id = keylet.key, .amount = asset(50)})); - env.close(); - } - VaultInfo const badVault = [&]() -> VaultInfo { - auto [tx, keylet] = - vault.create({.owner = alice, .asset = iouAsset}); - env(tx); - env.close(); - if (auto const le = env.le(keylet); BEAST_EXPECT(env.le(keylet))) - { - return {iouAsset, keylet.key, le->at(sfAccount)}; - } - // This should never happen - return {iouAsset, keylet.key, evan.id()}; - }(); - - auto const aliceOriginalCount = env.ownerCount(alice); - - // Create and update Loan Brokers - for (auto const& vault : vaults) - { - { - // Get the AccountInfo RPC result for the vault pseudo-account - std::string const pseudoStr = - to_string(vault.pseudoAccount.id()); - auto const accountInfo = env.rpc("account_info", pseudoStr); - if (BEAST_EXPECT(accountInfo.isObject())) - { - auto const& accountData = - accountInfo[jss::result][jss::account_data]; - if (BEAST_EXPECT(accountData.isObject())) - { - BEAST_EXPECT(accountData[jss::Account] == pseudoStr); - BEAST_EXPECT( - accountData[sfVaultID] == to_string(vault.vaultID)); - } - auto const& pseudoInfo = - accountInfo[jss::result][jss::pseudo_account]; - if (BEAST_EXPECT(pseudoInfo.isObject())) - { - BEAST_EXPECT(pseudoInfo[jss::type] == "Vault"); - } - } - } - - using namespace loanBroker; - using namespace ripple::Lending; - - TenthBips32 const tenthBipsZero{0}; - - auto badKeylet = keylet::vault(alice.id(), env.seq(alice)); - // Try some failure cases - // not the vault owner - env(set(evan, vault.vaultID), ter(tecNO_PERMISSION)); - // not a vault - env(set(alice, badKeylet.key), ter(tecNO_ENTRY)); - // flags are checked first - env(set(evan, vault.vaultID, ~tfUniversal), ter(temINVALID_FLAG)); - // field length validation - // sfData: good length, bad account - env(set(evan, vault.vaultID), - data(std::string(maxDataPayloadLength, 'X')), - ter(tecNO_PERMISSION)); - // sfData: too long - env(set(evan, vault.vaultID), - data(std::string(maxDataPayloadLength + 1, 'Y')), - ter(temINVALID)); - // sfManagementFeeRate: good value, bad account - env(set(evan, vault.vaultID), - managementFeeRate(maxManagementFeeRate), - ter(tecNO_PERMISSION)); - // sfManagementFeeRate: too big - env(set(evan, vault.vaultID), - managementFeeRate(maxManagementFeeRate + TenthBips16(10)), - ter(temINVALID)); - // sfCoverRateMinimum and sfCoverRateLiquidation are linked - // Cover: good value, bad account - env(set(evan, vault.vaultID), - coverRateMinimum(maxCoverRate), - coverRateLiquidation(maxCoverRate), - ter(tecNO_PERMISSION)); - // CoverMinimum: too big - env(set(evan, vault.vaultID), - coverRateMinimum(maxCoverRate + 1), - coverRateLiquidation(maxCoverRate + 1), - ter(temINVALID)); - // CoverLiquidation: too big - env(set(evan, vault.vaultID), - coverRateMinimum(maxCoverRate / 2), - coverRateLiquidation(maxCoverRate + 1), - ter(temINVALID)); - // Cover: zero min, non-zero liquidation - implicit and - // explicit zero values. - env(set(evan, vault.vaultID), - coverRateLiquidation(maxCoverRate), - ter(temINVALID)); - env(set(evan, vault.vaultID), - coverRateMinimum(tenthBipsZero), - coverRateLiquidation(maxCoverRate), - ter(temINVALID)); - // Cover: non-zero min, zero liquidation - implicit and - // explicit zero values. - env(set(evan, vault.vaultID), - coverRateMinimum(maxCoverRate), - ter(temINVALID)); - env(set(evan, vault.vaultID), - coverRateMinimum(maxCoverRate), - coverRateLiquidation(tenthBipsZero), - ter(temINVALID)); - // sfDebtMaximum: good value, bad account - env(set(evan, vault.vaultID), - debtMaximum(Number(0)), - ter(tecNO_PERMISSION)); - // sfDebtMaximum: overflow - env(set(evan, vault.vaultID), - debtMaximum(Number(1, 100)), - ter(temINVALID)); - // sfDebtMaximum: negative - env(set(evan, vault.vaultID), - debtMaximum(Number(-1)), - ter(temINVALID)); - - std::string testData; - lifecycle( - "default fields", - env, - issuer, - alice, - evan, - bystander, - vault, - badVault, - // No modifications - {}, - [&](SLE::const_ref broker) { - // Extra checks - BEAST_EXPECT(!broker->isFieldPresent(sfManagementFeeRate)); - BEAST_EXPECT(!broker->isFieldPresent(sfCoverRateMinimum)); - BEAST_EXPECT( - !broker->isFieldPresent(sfCoverRateLiquidation)); - BEAST_EXPECT(!broker->isFieldPresent(sfData)); - BEAST_EXPECT(!broker->isFieldPresent(sfDebtMaximum)); - BEAST_EXPECT(broker->at(sfDebtMaximum) == 0); - BEAST_EXPECT(broker->at(sfCoverRateMinimum) == 0); - BEAST_EXPECT(broker->at(sfCoverRateLiquidation) == 0); - - BEAST_EXPECT( - env.ownerCount(alice) == aliceOriginalCount + 4); - }, - [&](SLE::const_ref broker) { - // Modifications - - // Update the fields - auto const nextKeylet = - keylet::loanbroker(alice.id(), env.seq(alice)); - - // fields that can't be changed - // LoanBrokerID - env(set(alice, vault.vaultID), - loanBrokerID(nextKeylet.key), - ter(tecNO_ENTRY)); - // VaultID - env(set(alice, nextKeylet.key), - loanBrokerID(broker->key()), - ter(tecNO_PERMISSION)); - // Owner - env(set(evan, vault.vaultID), - loanBrokerID(broker->key()), - ter(tecNO_PERMISSION)); - // ManagementFeeRate - env(set(alice, vault.vaultID), - loanBrokerID(broker->key()), - managementFeeRate(maxManagementFeeRate), - ter(temINVALID)); - // CoverRateMinimum - env(set(alice, vault.vaultID), - loanBrokerID(broker->key()), - coverRateMinimum(maxManagementFeeRate), - ter(temINVALID)); - // CoverRateLiquidation - env(set(alice, vault.vaultID), - loanBrokerID(broker->key()), - coverRateLiquidation(maxManagementFeeRate), - ter(temINVALID)); - - // fields that can be changed - testData = "Test Data 1234"; - // Bad data: too long - env(set(alice, vault.vaultID), - loanBrokerID(broker->key()), - data(std::string(maxDataPayloadLength + 1, 'W')), - ter(temINVALID)); - - // Bad debt maximum - env(set(alice, vault.vaultID), - loanBrokerID(broker->key()), - debtMaximum(Number(-175, -1)), - ter(temINVALID)); - // Data & Debt maximum - env(set(alice, vault.vaultID), - loanBrokerID(broker->key()), - data(testData), - debtMaximum(Number(175, -1))); - }, - [&](SLE::const_ref broker) { - // Check the updated fields - BEAST_EXPECT(checkVL(broker->at(sfData), testData)); - BEAST_EXPECT(broker->at(sfDebtMaximum) == Number(175, -1)); - }); - - lifecycle( - "non-default fields", - env, - issuer, - alice, - evan, - bystander, - vault, - badVault, - [&](jtx::JTx const& jv) { - testData = "spam spam spam spam"; - // Finally, create another Loan Broker with none of the - // values at default - return env.jt( - jv, - data(testData), - managementFeeRate(TenthBips16(123)), - debtMaximum(Number(9)), - coverRateMinimum(TenthBips32(100)), - coverRateLiquidation(TenthBips32(200))); - }, - [&](SLE::const_ref broker) { - // Extra checks - BEAST_EXPECT(broker->at(sfManagementFeeRate) == 123); - BEAST_EXPECT(broker->at(sfCoverRateMinimum) == 100); - BEAST_EXPECT(broker->at(sfCoverRateLiquidation) == 200); - BEAST_EXPECT(broker->at(sfDebtMaximum) == Number(9)); - BEAST_EXPECT(checkVL(broker->at(sfData), testData)); - }, - [&](SLE::const_ref broker) { - // Reset Data & Debt maximum to default values - env(set(alice, vault.vaultID), - loanBrokerID(broker->key()), - data(""), - debtMaximum(Number(0))); - }, - [&](SLE::const_ref broker) { - // Check the updated fields - BEAST_EXPECT(!broker->isFieldPresent(sfData)); - BEAST_EXPECT(!broker->isFieldPresent(sfDebtMaximum)); - }); - } - - BEAST_EXPECT(env.ownerCount(alice) == aliceOriginalCount); - } - - enum LoanBrokerTest { - CoverClawback, - CoverDeposit, - CoverWithdraw, - Delete, - Set - }; - - void - testLoanBroker( - std::function getAsset, - LoanBrokerTest brokerTest) - { - using namespace jtx; - using namespace loanBroker; - Account const issuer{"issuer"}; - Account const alice{"alice"}; - Env env(*this); - Vault vault{env}; - - env.fund(XRP(100'000), issuer, alice); - env.close(); - - PrettyAsset const asset = [&]() { - if (getAsset) - return getAsset(env, issuer, alice); - env(trust(alice, issuer["IOU"](1'000'000)), THISLINE); - env.close(); - return PrettyAsset(issuer["IOU"]); - }(); - - env(pay(issuer, alice, asset(100'000)), THISLINE); - env.close(); - - auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = asset}); - env(tx, THISLINE); - env.close(); - auto const le = env.le(vaultKeylet); - VaultInfo vaultInfo = [&]() { - if (BEAST_EXPECT(le)) - return VaultInfo{asset, vaultKeylet.key, le->at(sfAccount)}; - return VaultInfo{asset, {}, {}}; - }(); - if (vaultInfo.vaultID == uint256{}) - return; - - env(vault.deposit( - {.depositor = alice, - .id = vaultKeylet.key, - .amount = asset(50)}), - THISLINE); - env.close(); - - auto const brokerKeylet = - keylet::loanbroker(alice.id(), env.seq(alice)); - env(set(alice, vaultInfo.vaultID), THISLINE); - env.close(); - - auto broker = env.le(brokerKeylet); - if (!BEAST_EXPECT(broker)) - return; - - auto testZeroBrokerID = [&](auto&& getTxJv) { - auto jv = getTxJv(); - // empty broker ID - jv[sfLoanBrokerID] = ""; - env(jv, ter(temINVALID), THISLINE); - // zero broker ID - jv[sfLoanBrokerID] = to_string(uint256{}); - // needs a flag to distinguish the parsed STTx from the prior - // test - env(jv, txflags(tfFullyCanonicalSig), ter(temINVALID), THISLINE); - }; - auto testZeroVaultID = [&](auto&& getTxJv) { - auto jv = getTxJv(); - // empty broker ID - jv[sfVaultID] = ""; - env(jv, ter(temINVALID), THISLINE); - // zero broker ID - jv[sfVaultID] = to_string(uint256{}); - // needs a flag to distinguish the parsed STTx from the prior - // test - env(jv, txflags(tfFullyCanonicalSig), ter(temINVALID), THISLINE); - }; - - if (brokerTest == CoverDeposit) - { - // preflight: temINVALID (empty/zero broker id) - testZeroBrokerID([&]() { - return coverDeposit(alice, brokerKeylet.key, asset(10)); - }); - - // preclaim: tecWRONG_ASSET - env(coverDeposit(alice, brokerKeylet.key, issuer["BAD"](10)), - ter(tecWRONG_ASSET), - THISLINE); - - // preclaim: tecINSUFFICIENT_FUNDS - env(pay(alice, issuer, asset(100'000 - 50)), THISLINE); - env.close(); - env(coverDeposit(alice, brokerKeylet.key, vaultInfo.asset(10)), - ter(tecINSUFFICIENT_FUNDS)); - - // preclaim: tecFROZEN - env(fset(issuer, asfGlobalFreeze), THISLINE); - env.close(); - env(coverDeposit(alice, brokerKeylet.key, vaultInfo.asset(10)), - ter(tecFROZEN), - THISLINE); - } - else - // Fund the cover deposit - env(coverDeposit(alice, brokerKeylet.key, vaultInfo.asset(10)), - THISLINE); - env.close(); - - if (brokerTest == CoverWithdraw) - { - // preflight: temINVALID (empty/zero broker id) - testZeroBrokerID([&]() { - return coverWithdraw(alice, brokerKeylet.key, asset(10)); - }); - - // preclaim: tecWRONG_ASSSET - env(coverWithdraw(alice, brokerKeylet.key, issuer["BAD"](10)), - ter(tecWRONG_ASSET), - THISLINE); - - // preclaim: tecNO_DST - Account const bogus{"bogus"}; - env(coverWithdraw(alice, brokerKeylet.key, asset(10)), - destination(bogus), - ter(tecNO_DST), - THISLINE); - - // preclaim: tecDST_TAG_NEEDED - Account const dest{"dest"}; - env.fund(XRP(1'000), dest); - env(fset(dest, asfRequireDest), THISLINE); - env.close(); - env(coverWithdraw(alice, brokerKeylet.key, asset(10)), - destination(dest), - ter(tecDST_TAG_NEEDED), - THISLINE); - - // preclaim: tecNO_PERMISSION - env(fclear(dest, asfRequireDest), THISLINE); - env(fset(dest, asfDepositAuth), THISLINE); - env.close(); - env(coverWithdraw(alice, brokerKeylet.key, asset(10)), - destination(dest), - ter(tecNO_PERMISSION), - THISLINE); - - // preclaim: tecFROZEN - env(trust(dest, asset(1'000)), THISLINE); - env(fclear(dest, asfDepositAuth), THISLINE); - env(fset(issuer, asfGlobalFreeze), THISLINE); - env.close(); - env(coverWithdraw(alice, brokerKeylet.key, asset(10)), - destination(dest), - ter(tecFROZEN), - THISLINE); - - // preclaim:: tecFROZEN (deep frozen) - env(fclear(issuer, asfGlobalFreeze), THISLINE); - env(trust( - issuer, asset(1'000), dest, tfSetFreeze | tfSetDeepFreeze), - THISLINE); - env(coverWithdraw(alice, brokerKeylet.key, asset(10)), - destination(dest), - ter(tecFROZEN), - THISLINE); - } - - if (brokerTest == CoverClawback) - { - // preflight: temINVALID (empty/zero broker id) - testZeroBrokerID([&]() { - return env.json( - coverClawback(alice), - loanBrokerID(brokerKeylet.key), - amount(vaultInfo.asset(2))); - }); - - if (asset.holds()) - { - // preclaim: AllowTrustLineClaback is not set - env(coverClawback(issuer), - loanBrokerID(brokerKeylet.key), - amount(vaultInfo.asset(2)), - ter(tecNO_PERMISSION), - THISLINE); - - // preclaim: NoFreeze is set - env(fset(issuer, asfAllowTrustLineClawback | asfNoFreeze), - THISLINE); - env.close(); - env(coverClawback(issuer), - loanBrokerID(brokerKeylet.key), - amount(vaultInfo.asset(2)), - ter(tecNO_PERMISSION), - THISLINE); - } - else - { - // preclaim: MPTCanClawback is not set or MPTCanLock is not set - env(coverClawback(issuer), - loanBrokerID(brokerKeylet.key), - amount(vaultInfo.asset(2)), - ter(tecNO_PERMISSION), - THISLINE); - } - env.close(); - } - - if (brokerTest == Delete) - { - Account const borrower{"borrower"}; - env.fund(XRP(1'000), borrower); - env(loan::set(borrower, brokerKeylet.key, asset(50).value()), - sig(sfCounterpartySignature, alice), - fee(env.current()->fees().base * 2), - THISLINE); - - // preflight: temINVALID (empty/zero broker id) - testZeroBrokerID([&]() { return del(alice, brokerKeylet.key); }); - - // preclaim: tecHAS_OBLIGATIONS - env(del(alice, brokerKeylet.key), - ter(tecHAS_OBLIGATIONS), - THISLINE); - - // Repay and delete the loan - auto const loanKeylet = keylet::loan(brokerKeylet.key, 1); - env(loan::pay(borrower, loanKeylet.key, asset(50).value()), - THISLINE); - env(loan::del(alice, loanKeylet.key), THISLINE); - - env(trust(issuer, asset(0), alice, tfSetFreeze | tfSetDeepFreeze), - THISLINE); - // preclaim: tecFROZEN (deep frozen) - env(del(alice, brokerKeylet.key), ter(tecFROZEN), THISLINE); - env(trust( - issuer, asset(0), alice, tfClearFreeze | tfClearDeepFreeze), - THISLINE); - - // successful delete the loan broker object - env(del(alice, brokerKeylet.key), ter(tesSUCCESS), THISLINE); - } - else - env(del(alice, brokerKeylet.key), THISLINE); - - if (brokerTest == Set) - { - // preflight: temINVALID (empty/zero broker id) - testZeroBrokerID([&]() { - return env.json( - set(alice, vaultInfo.vaultID), - loanBrokerID(brokerKeylet.key)); - }); - // preflight: temINVALID (empty/zero vault id) - testZeroVaultID([&]() { - return env.json( - set(alice, vaultInfo.vaultID), - loanBrokerID(brokerKeylet.key)); - }); - - if (asset.holds()) - { - env(fclear(issuer, asfDefaultRipple), THISLINE); - env.close(); - // preclaim: DefaultRipple is not set - env(set(alice, vaultInfo.vaultID), ter(terNO_RIPPLE), THISLINE); - - env(fset(issuer, asfDefaultRipple), THISLINE); - env.close(); - } - - auto const amt = env.balance(alice) - - env.current()->fees().accountReserve(env.ownerCount(alice)); - env(pay(alice, issuer, amt), THISLINE); - - // preclaim:: tecINSUFFICIENT_RESERVE - env(set(alice, vaultInfo.vaultID), - ter(tecINSUFFICIENT_RESERVE), - THISLINE); - } - } - - void - testInvalidLoanBrokerCoverClawback() - { - testcase("Invalid LoanBrokerCoverClawback"); - using namespace jtx; - using namespace loanBroker; - - // preflight - { - Account const alice{"alice"}; - Account const issuer{"issuer"}; - auto const USD = alice["USD"]; - Env env(*this); - env.fund(XRP(100'000), alice); - env.close(); - - auto jtx = env.jt(coverClawback(alice), amount(USD(100))); - - // holder == account - env(jtx, ter(temINVALID), THISLINE); - - // holder == beast::zero - STAmount bad(Issue{USD.currency, beast::zero}, 100); - jtx.jv[sfAmount] = bad.getJson(); - jtx.stx = env.ust(jtx); - Serializer s; - jtx.stx->add(s); - auto const jrr = env.rpc("submit", strHex(s.slice()))[jss::result]; - // fails in doSubmit() on STTx construction - BEAST_EXPECT(jrr[jss::error] == "invalidTransaction"); - BEAST_EXPECT(jrr[jss::error_exception] == "invalid native account"); - } - - // preclaim - - // Issue: - // AllowTrustLineClawback is not set or NoFreeze is set - testLoanBroker({}, CoverClawback); - - // MPTIssue: - // MPTCanClawback is not set - testLoanBroker( - [&](Env& env, Account const& issuer, Account const& alice) -> MPT { - MPTTester mpt( - {.env = env, .issuer = issuer, .holders = {alice}}); - return mpt; - }, - CoverClawback); - } - - void - testInvalidLoanBrokerCoverDeposit() - { - testcase("Invalid LoanBrokerCoverDeposit"); - using namespace jtx; - - // preclaim: - // tecWRONG_ASSET, tecINSUFFICIENT_FUNDS, frozen asset - testLoanBroker({}, CoverDeposit); - } - - void - testInvalidLoanBrokerCoverWithdraw() - { - testcase("Invalid LoanBrokerCoverWithdraw"); - using namespace jtx; - - /* - preflight: illegal net - isLegalNet() check is probably redundant. STAmount parsing - should throw an exception on deserialize - - preclaim: tecWRONG_ASSET, tecNO_DST, tecDST_TAG_NEEDED, - tecNO_PERMISSION, checkFrozen failure, checkDeepFrozenFailure, - second+third tecINSUFFICIENT_FUNDS (can this happen)? - doApply: tecPATH_DRY (can it happen, funds already checked?) - */ - testLoanBroker({}, CoverWithdraw); - } - - void - testInvalidLoanBrokerDelete() - { - using namespace jtx; - testcase("Invalid LoanBrokerDelete"); - /* - preclaim: tecHAS_OBLIGATIONS - doApply: - accountSend failure, removeEmptyHolding failure, - all tecHAS_OBLIGATIONS (can any of these happen?) - */ - testLoanBroker({}, Delete); - } - - void - testInvalidLoanBrokerSet() - { - using namespace jtx; - testcase("Invalid LoanBrokerSet"); - - /*preclaim: canAddHolding failure (can it happen with MPT? - can't create Vault if CanTransfer is not enabled.) - doApply: - first+second dirLink failure, createPseudoAccount failure, - addEmptyHolding failure - can any of these happen? - */ - testLoanBroker({}, Set); - } - - void - testLoanBrokerCoverDepositNullVault() - { - // This test is lifted directly from - // https://bugs.immunefi.com/dashboard/submission/57808 - using namespace jtx; - Env env(*this); - - Account const alice{"alice"}; - env.fund(XRP(10000), alice); - env.close(); - - // Create a Vault owned by alice with an XRP asset - PrettyAsset const asset{xrpIssue(), 1}; - Vault vault{env}; - auto const [createTx, vaultKeylet] = - vault.create({.owner = alice, .asset = asset}); - env(createTx); - env.close(); - - // Predict LoanBroker key using alice's current sequence BEFORE submit - auto const brokerKeylet = - keylet::loanbroker(alice.id(), env.seq(alice)); - - // Create LoanBroker pointing to the vault - env(loanBroker::set(alice, vaultKeylet.key)); - env.close(); - - // Build the CoverDeposit STTx directly - STTx tx{ttLOAN_BROKER_COVER_DEPOSIT, [](STObject&) {}}; - tx.setAccountID(sfAccount, alice.id()); - tx.setFieldH256(sfLoanBrokerID, brokerKeylet.key); - tx.setFieldAmount(sfAmount, asset(1)); - - // Create a writable view cloned from the current ledger and remove the - // vault SLE - OpenView ov{*env.current()}; - test::StreamSink sink{beast::severities::kWarning}; - beast::Journal jlog{sink}; - ApplyContext ac{ - env.app(), - ov, - tx, - tesSUCCESS, - env.current()->fees().base, - tapNONE, - jlog}; - - if (auto sleBroker = - ac.view().peek(keylet::loanbroker(brokerKeylet.key))) - { - auto const vaultID = (*sleBroker)[sfVaultID]; - if (auto sleVault = ac.view().peek(keylet::vault(vaultID))) - { - ac.view().erase(sleVault); - } - } - - // Invoke preclaim against the mutated (ApplyView) view; triggers - // nullptr deref - PreclaimContext pctx{ - env.app(), ac.view(), tesSUCCESS, tx, tapNONE, jlog}; - (void)LoanBrokerCoverDeposit::preclaim(pctx); - } - - void - testRequireAuth() - { - testcase("Require Auth - Implicit Pseudo-account authorization"); - using namespace jtx; - using namespace loanBroker; - - Account const issuer{"issuer"}; - Account const alice{"alice"}; - Env env(*this); - Vault vault{env}; - - env.fund(XRP(100'000), issuer, alice); - env.close(); - - auto asset = MPTTester({ - .env = env, - .issuer = issuer, - .holders = {alice}, - .flags = MPTDEXFlags | tfMPTRequireAuth | tfMPTCanClawback | - tfMPTCanLock, - .authHolder = true, - }); - - env(pay(issuer, alice, asset(100'000))); - env.close(); - - // Alice is not authorized, can still create the vault - asset.authorize( - {.account = issuer, .holder = alice, .flags = tfMPTUnauthorize}); - auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = asset}); - env(tx); - env.close(); - - auto const le = env.le(vaultKeylet); - VaultInfo vaultInfo = [&]() { - if (BEAST_EXPECT(le)) - return VaultInfo{asset, vaultKeylet.key, le->at(sfAccount)}; - return VaultInfo{asset, {}, {}}; - }(); - if (vaultInfo.vaultID == uint256{}) - return; - - // Can't unauthorize Vault pseudo-account - asset.authorize( - {.account = issuer, - .holder = vaultInfo.pseudoAccount, - .flags = tfMPTUnauthorize, - .err = tecNO_PERMISSION}); - - auto forUnauthAuth = [&](auto&& doTx) { - for (auto const flag : {tfMPTUnauthorize, 0u}) - { - asset.authorize( - {.account = issuer, .holder = alice, .flags = flag}); - env.close(); - doTx(flag == 0); - env.close(); - } - }; - - // Can't deposit into Vault if the vault owner is not authorized - forUnauthAuth([&](bool authorized) { - auto const err = !authorized ? ter(tecNO_AUTH) : ter(tesSUCCESS); - env(vault.deposit( - {.depositor = alice, - .id = vaultKeylet.key, - .amount = asset(51)}), - err); - }); - - // Can't withdraw from Vault if the vault owner is not authorized - forUnauthAuth([&](bool authorized) { - auto const err = !authorized ? ter(tecNO_AUTH) : ter(tesSUCCESS); - env(vault.withdraw( - {.depositor = alice, - .id = vaultKeylet.key, - .amount = asset(1)}), - err); - }); - - auto const brokerKeylet = - keylet::loanbroker(alice.id(), env.seq(alice)); - // Can create LoanBroker if the vault owner is not authorized - forUnauthAuth([&](auto) { env(set(alice, vaultInfo.vaultID)); }); - - auto const broker = env.le(brokerKeylet); - if (!BEAST_EXPECT(broker)) - return; - Account brokerPseudo("pseudo", broker->at(sfAccount)); - - // Can't unauthorize LoanBroker pseudo-account - asset.authorize( - {.account = issuer, - .holder = brokerPseudo, - .flags = tfMPTUnauthorize, - .err = tecNO_PERMISSION}); - - // Can't cover deposit into Vault if the vault owner is not authorized - forUnauthAuth([&](bool authorized) { - auto const err = !authorized ? ter(tecNO_AUTH) : ter(tesSUCCESS); - env(coverDeposit(alice, brokerKeylet.key, vaultInfo.asset(10)), - err); - }); - - // Can't cover withdraw from Vault if the vault owner is not authorized - forUnauthAuth([&](bool authorized) { - auto const err = !authorized ? ter(tecNO_AUTH) : ter(tesSUCCESS); - env(coverWithdraw(alice, brokerKeylet.key, vaultInfo.asset(5)), - err); - }); - - // Issuer can always cover clawback. The holder authorization is n/a. - forUnauthAuth([&](bool) { - env(coverClawback(issuer), - loanBrokerID(brokerKeylet.key), - amount(vaultInfo.asset(1))); - }); - } - -public: - void - run() override - { - testLoanBrokerCoverDepositNullVault(); - - testDisabled(); - testLifecycle(); - testInvalidLoanBrokerCoverClawback(); - testInvalidLoanBrokerCoverDeposit(); - testInvalidLoanBrokerCoverWithdraw(); - testInvalidLoanBrokerDelete(); - testInvalidLoanBrokerSet(); - testRequireAuth(); - - // TODO: Write clawback failure tests with an issuer / MPT that doesn't - // have the right flags set. - } -}; - -BEAST_DEFINE_TESTSUITE(LoanBroker, tx, ripple); - -} // namespace test -} // namespace ripple diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp deleted file mode 100644 index b2ad47c2b4..0000000000 --- a/src/test/app/Loan_test.cpp +++ /dev/null @@ -1,7215 +0,0 @@ -#include -// -#include -#include - -#include -#include -#include -#include - -#include -#include - -namespace ripple { -namespace test { - -class Loan_test : public beast::unit_test::suite -{ -protected: - // Ensure that all the features needed for Lending Protocol are included, - // even if they are set to unsupported. - FeatureBitset const all{ - jtx::testable_amendments() | featureMPTokensV1 | - featureSingleAssetVault | featureLendingProtocol}; - - std::string const iouCurrency{"IOU"}; - - void - testDisabled() - { - testcase("Disabled"); - // Lending Protocol depends on Single Asset Vault (SAV). Test - // combinations of the two amendments. - // Single Asset Vault depends on MPTokensV1, but don't test every combo - // of that. - using namespace jtx; - auto failAll = [this](FeatureBitset features) { - Env env(*this, features); - - Account const alice{"alice"}; - Account const bob{"bob"}; - env.fund(XRP(10000), alice, bob); - - auto const keylet = keylet::loanbroker(alice, env.seq(alice)); - - using namespace std::chrono_literals; - using namespace loan; - - // counter party signature is optional on LoanSet. Confirm that by - // sending transaction without one. - auto setTx = - env.jt(set(alice, keylet.key, Number(10000)), ter(temDISABLED)); - env(setTx); - - // All loan transactions are disabled. - // 1. LoanSet - setTx = env.jt( - setTx, sig(sfCounterpartySignature, bob), ter(temDISABLED)); - env(setTx); - // Actual sequence will be based off the loan broker, but we - // obviously don't have one of those if the amendment is disabled - auto const loanKeylet = keylet::loan(keylet.key, env.seq(alice)); - // Other Loan transactions are disabled, too. - // 2. LoanDelete - env(del(alice, loanKeylet.key), ter(temDISABLED)); - // 3. LoanManage - env(manage(alice, loanKeylet.key, tfLoanImpair), ter(temDISABLED)); - // 4. LoanPay - env(pay(alice, loanKeylet.key, XRP(500)), ter(temDISABLED)); - }; - failAll(all - featureMPTokensV1); - failAll(all - featureSingleAssetVault - featureLendingProtocol); - failAll(all - featureSingleAssetVault); - failAll(all - featureLendingProtocol); - } - - struct BrokerParameters - { - Number vaultDeposit = 1'000'000; - Number debtMax = 25'000; - TenthBips32 coverRateMin = percentageToTenthBips(10); - int coverDeposit = 1000; - TenthBips16 managementFeeRate{100}; - TenthBips32 coverRateLiquidation = percentageToTenthBips(25); - std::string data{}; - std::uint32_t flags = 0; - - Number - maxCoveredLoanValue(Number const& currentDebt) const - { - NumberRoundModeGuard mg(Number::downward); - auto debtLimit = - coverDeposit * tenthBipsPerUnity.value() / coverRateMin.value(); - - return debtLimit - currentDebt; - } - - static BrokerParameters const& - defaults() - { - static BrokerParameters const result{}; - return result; - } - - // TODO: create an operator() which returns a transaction similar to - // LoanParameters - }; - - struct BrokerInfo - { - jtx::PrettyAsset asset; - uint256 brokerID; - uint256 vaultID; - BrokerParameters params; - BrokerInfo( - jtx::PrettyAsset const& asset_, - Keylet const& brokerKeylet_, - Keylet const& vaultKeylet_, - BrokerParameters const& p) - : asset(asset_) - , brokerID(brokerKeylet_.key) - , vaultID(vaultKeylet_.key) - , params(p) - { - } - - Keylet - brokerKeylet() const - { - return keylet::loanbroker(brokerID); - } - Keylet - vaultKeylet() const - { - return keylet::vault(vaultID); - } - - int - vaultScale(jtx::Env const& env) const - { - using namespace jtx; - - auto const vaultSle = env.le(keylet::vault(vaultID)); - return getVaultScale(vaultSle); - } - }; - - struct LoanParameters - { - // The account submitting the transaction. May be borrower or broker. - jtx::Account account; - // The counterparty. Should be the other of borrower or broker. - jtx::Account counter; - // Whether the counterparty is specified in the `counterparty` field, or - // only signs. - bool counterpartyExplicit = true; - Number principalRequest; - std::optional setFee{}; - std::optional originationFee{}; - std::optional serviceFee{}; - std::optional lateFee{}; - std::optional closeFee{}; - std::optional overFee{}; - std::optional interest{}; - std::optional lateInterest{}; - std::optional closeInterest{}; - std::optional overpaymentInterest{}; - std::optional payTotal{}; - std::optional payInterval{}; - std::optional gracePd{}; - std::optional flags{}; - - template - jtx::JTx - operator()(jtx::Env& env, BrokerInfo const& broker, FN const&... fN) - const - { - using namespace jtx; - using namespace jtx::loan; - - JTx jt{loan::set( - account, - broker.brokerID, - broker.asset(principalRequest).number(), - flags.value_or(0))}; - - sig(sfCounterpartySignature, counter)(env, jt); - - fee{setFee.value_or(env.current()->fees().base * 2)}(env, jt); - - if (counterpartyExplicit) - counterparty(counter)(env, jt); - if (originationFee) - loanOriginationFee(broker.asset(*originationFee).number())( - env, jt); - if (serviceFee) - loanServiceFee(broker.asset(*serviceFee).number())(env, jt); - if (lateFee) - latePaymentFee(broker.asset(*lateFee).number())(env, jt); - if (closeFee) - closePaymentFee(broker.asset(*closeFee).number())(env, jt); - if (overFee) - overpaymentFee (*overFee)(env, jt); - if (interest) - interestRate (*interest)(env, jt); - if (lateInterest) - lateInterestRate (*lateInterest)(env, jt); - if (closeInterest) - closeInterestRate (*closeInterest)(env, jt); - if (overpaymentInterest) - overpaymentInterestRate (*overpaymentInterest)(env, jt); - if (payTotal) - paymentTotal (*payTotal)(env, jt); - if (payInterval) - paymentInterval (*payInterval)(env, jt); - if (gracePd) - gracePeriod (*gracePd)(env, jt); - - return env.jt(jt, fN...); - } - }; - - struct PaymentParameters - { - Number overpaymentFactor = Number{1}; - std::optional overpaymentExtra = std::nullopt; - std::uint32_t flags = 0; - bool showStepBalances = false; - bool validateBalances = true; - - static PaymentParameters const& - defaults() - { - static PaymentParameters const result{}; - return result; - } - }; - - struct LoanState - { - std::uint32_t previousPaymentDate = 0; - NetClock::time_point startDate = {}; - std::uint32_t nextPaymentDate = 0; - std::uint32_t paymentRemaining = 0; - std::int32_t const loanScale = 0; - Number totalValue = 0; - Number principalOutstanding = 0; - Number managementFeeOutstanding = 0; - Number periodicPayment = 0; - std::uint32_t flags = 0; - std::uint32_t const paymentInterval = 0; - TenthBips32 const interestRate{}; - }; - - /** Helper class to compare the expected state of a loan and loan broker - * against the data in the ledger. - */ - struct VerifyLoanStatus - { - public: - jtx::Env const& env; - BrokerInfo const& broker; - jtx::Account const& pseudoAccount; - Keylet const& loanKeylet; - - VerifyLoanStatus( - jtx::Env const& env_, - BrokerInfo const& broker_, - jtx::Account const& pseudo_, - Keylet const& keylet_) - : env(env_) - , broker(broker_) - , pseudoAccount(pseudo_) - , loanKeylet(keylet_) - { - } - - /** Checks the expected broker state against the ledger - */ - void - checkBroker( - Number const& principalOutstanding, - Number const& interestOwed, - TenthBips32 interestRate, - std::uint32_t paymentInterval, - std::uint32_t paymentsRemaining, - std::uint32_t ownerCount) const - { - using namespace jtx; - if (auto brokerSle = env.le(keylet::loanbroker(broker.brokerID)); - env.test.BEAST_EXPECT(brokerSle)) - { - TenthBips16 const managementFeeRate{ - brokerSle->at(sfManagementFeeRate)}; - auto const brokerDebt = brokerSle->at(sfDebtTotal); - auto const expectedDebt = principalOutstanding + interestOwed; - env.test.BEAST_EXPECT(brokerDebt == expectedDebt); - env.test.BEAST_EXPECT( - env.balance(pseudoAccount, broker.asset).number() == - brokerSle->at(sfCoverAvailable)); - env.test.BEAST_EXPECT( - brokerSle->at(sfOwnerCount) == ownerCount); - - if (auto vaultSle = - env.le(keylet::vault(brokerSle->at(sfVaultID))); - env.test.BEAST_EXPECT(vaultSle)) - { - Account const vaultPseudo{ - "vaultPseudoAccount", vaultSle->at(sfAccount)}; - env.test.BEAST_EXPECT( - vaultSle->at(sfAssetsAvailable) == - env.balance(vaultPseudo, broker.asset).number()); - if (ownerCount == 0) - { - // Allow some slop for rounding IOUs - - // TODO: This needs to be an exact match once all the - // other rounding issues are worked out. - auto const total = vaultSle->at(sfAssetsTotal); - auto const available = vaultSle->at(sfAssetsAvailable); - env.test.BEAST_EXPECT( - total == available || - (!broker.asset.integral() && available != 0 && - ((total - available) / available < - Number(1, -6)))); - env.test.BEAST_EXPECT( - vaultSle->at(sfLossUnrealized) == 0); - } - } - } - } - - void - checkPayment( - std::int32_t loanScale, - jtx::Account const& account, - jtx::PrettyAmount const& balanceBefore, - STAmount const& expectedPayment, - jtx::PrettyAmount const& adjustment) const - { - auto const borrowerScale = - std::max(loanScale, balanceBefore.number().exponent()); - - STAmount const balanceChangeAmount{ - broker.asset, - roundToAsset( - broker.asset, expectedPayment + adjustment, borrowerScale)}; - { - auto const difference = roundToScale( - env.balance(account, broker.asset) - - (balanceBefore - balanceChangeAmount), - borrowerScale); - env.test.BEAST_EXPECT( - roundToScale(difference, loanScale) >= beast::zero); - } - } - - /** Checks both the loan and broker expect states against the ledger */ - void - operator()( - std::uint32_t previousPaymentDate, - std::uint32_t nextPaymentDate, - std::uint32_t paymentRemaining, - Number const& loanScale, - Number const& totalValue, - Number const& principalOutstanding, - Number const& managementFeeOutstanding, - Number const& periodicPayment, - std::uint32_t flags) const - { - using namespace jtx; - if (auto loan = env.le(loanKeylet); env.test.BEAST_EXPECT(loan)) - { - env.test.BEAST_EXPECT( - loan->at(sfPreviousPaymentDate) == previousPaymentDate); - env.test.BEAST_EXPECT( - loan->at(sfPaymentRemaining) == paymentRemaining); - env.test.BEAST_EXPECT( - loan->at(sfNextPaymentDueDate) == nextPaymentDate); - env.test.BEAST_EXPECT(loan->at(sfLoanScale) == loanScale); - env.test.BEAST_EXPECT( - loan->at(sfTotalValueOutstanding) == totalValue); - env.test.BEAST_EXPECT( - loan->at(sfPrincipalOutstanding) == principalOutstanding); - env.test.BEAST_EXPECT( - loan->at(sfManagementFeeOutstanding) == - managementFeeOutstanding); - env.test.BEAST_EXPECT( - loan->at(sfPeriodicPayment) == periodicPayment); - env.test.BEAST_EXPECT(loan->at(sfFlags) == flags); - - auto const ls = constructRoundedLoanState(loan); - - auto const interestRate = TenthBips32{loan->at(sfInterestRate)}; - auto const paymentInterval = loan->at(sfPaymentInterval); - checkBroker( - principalOutstanding, - ls.interestDue, - interestRate, - paymentInterval, - paymentRemaining, - 1); - - if (auto brokerSle = - env.le(keylet::loanbroker(broker.brokerID)); - env.test.BEAST_EXPECT(brokerSle)) - { - if (auto vaultSle = - env.le(keylet::vault(brokerSle->at(sfVaultID))); - env.test.BEAST_EXPECT(vaultSle)) - { - if ((flags & lsfLoanImpaired) && - !(flags & lsfLoanDefault)) - { - env.test.BEAST_EXPECT( - vaultSle->at(sfLossUnrealized) == - totalValue - managementFeeOutstanding); - } - else - { - env.test.BEAST_EXPECT( - vaultSle->at(sfLossUnrealized) == 0); - } - } - } - } - } - - /** Checks both the loan and broker expect states against the ledger */ - void - operator()(LoanState const& state) const - { - operator()( - state.previousPaymentDate, - state.nextPaymentDate, - state.paymentRemaining, - state.loanScale, - state.totalValue, - state.principalOutstanding, - state.managementFeeOutstanding, - state.periodicPayment, - state.flags); - }; - }; - - BrokerInfo - createVaultAndBroker( - jtx::Env& env, - jtx::PrettyAsset const& asset, - jtx::Account const& lender, - BrokerParameters const& params = BrokerParameters::defaults()) - { - using namespace jtx; - - Vault vault{env}; - - auto const deposit = asset(params.vaultDeposit); - auto const debtMaximumValue = asset(params.debtMax).value(); - auto const coverDepositValue = asset(params.coverDeposit).value(); - - auto const coverRateMinValue = params.coverRateMin; - - auto [tx, vaultKeylet] = - vault.create({.owner = lender, .asset = asset}); - env(tx); - env.close(); - BEAST_EXPECT(env.le(vaultKeylet)); - - env(vault.deposit( - {.depositor = lender, .id = vaultKeylet.key, .amount = deposit})); - env.close(); - if (auto const vault = env.le(keylet::vault(vaultKeylet.key)); - BEAST_EXPECT(vault)) - { - BEAST_EXPECT(vault->at(sfAssetsAvailable) == deposit.value()); - } - - auto const keylet = keylet::loanbroker(lender.id(), env.seq(lender)); - - using namespace loanBroker; - env(set(lender, vaultKeylet.key, params.flags), - data(params.data), - managementFeeRate(params.managementFeeRate), - debtMaximum(debtMaximumValue), - coverRateMinimum(coverRateMinValue), - coverRateLiquidation(TenthBips32(params.coverRateLiquidation))); - - if (coverDepositValue != beast::zero) - env(coverDeposit(lender, keylet.key, coverDepositValue)); - - env.close(); - - return {asset, keylet, vaultKeylet, params}; - } - - /// Get the state without checking anything - LoanState - getCurrentState( - jtx::Env const& env, - BrokerInfo const& broker, - Keylet const& loanKeylet) - { - using d = NetClock::duration; - using tp = NetClock::time_point; - - // Lookup the current loan state - if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan)) - { - return LoanState{ - .previousPaymentDate = loan->at(sfPreviousPaymentDate), - .startDate = tp{d{loan->at(sfStartDate)}}, - .nextPaymentDate = loan->at(sfNextPaymentDueDate), - .paymentRemaining = loan->at(sfPaymentRemaining), - .loanScale = loan->at(sfLoanScale), - .totalValue = loan->at(sfTotalValueOutstanding), - .principalOutstanding = loan->at(sfPrincipalOutstanding), - .managementFeeOutstanding = - loan->at(sfManagementFeeOutstanding), - .periodicPayment = loan->at(sfPeriodicPayment), - .flags = loan->at(sfFlags), - .paymentInterval = loan->at(sfPaymentInterval), - .interestRate = TenthBips32{loan->at(sfInterestRate)}, - }; - } - return LoanState{}; - } - - /// Get the state and check the values against the parameters used in - /// `lifecycle` - LoanState - getCurrentState( - jtx::Env const& env, - BrokerInfo const& broker, - Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus) - { - using namespace std::chrono_literals; - using d = NetClock::duration; - using tp = NetClock::time_point; - - auto const state = getCurrentState(env, broker, loanKeylet); - BEAST_EXPECT(state.previousPaymentDate == 0); - BEAST_EXPECT(tp{d{state.nextPaymentDate}} == state.startDate + 600s); - BEAST_EXPECT(state.paymentRemaining == 12); - BEAST_EXPECT(state.principalOutstanding == broker.asset(1000).value()); - BEAST_EXPECT( - state.loanScale >= - (broker.asset.integral() - ? 0 - : std::max( - broker.vaultScale(env), - state.principalOutstanding.exponent()))); - BEAST_EXPECT(state.paymentInterval == 600); - BEAST_EXPECT( - state.totalValue == - roundToAsset( - broker.asset, - state.periodicPayment * state.paymentRemaining, - state.loanScale)); - BEAST_EXPECT( - state.managementFeeOutstanding == - computeManagementFee( - broker.asset, - state.totalValue - state.principalOutstanding, - broker.params.managementFeeRate, - state.loanScale)); - - verifyLoanStatus(state); - - return state; - } - - bool - canImpairLoan( - jtx::Env const& env, - BrokerInfo const& broker, - LoanState const& state) - { - if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); - BEAST_EXPECT(brokerSle)) - { - if (auto const vaultSle = - env.le(keylet::vault(brokerSle->at(sfVaultID))); - BEAST_EXPECT(vaultSle)) - { - // log << vaultSle->getJson() << std::endl; - auto const assetsUnavailable = vaultSle->at(sfAssetsTotal) - - vaultSle->at(sfAssetsAvailable); - auto const unrealizedLoss = vaultSle->at(sfLossUnrealized) + - state.totalValue - state.managementFeeOutstanding; - - if (unrealizedLoss > assetsUnavailable) - { - return false; - } - } - } - return true; - } - - enum class AssetType { XRP = 0, IOU = 1, MPT = 2 }; - - // Specify the accounts as params to allow other accounts to be used - jtx::PrettyAsset - createAsset( - jtx::Env& env, - AssetType assetType, - BrokerParameters const& brokerParams, - jtx::Account const& issuer, - jtx::Account const& lender, - jtx::Account const& borrower) - { - using namespace jtx; - - switch (assetType) - { - case AssetType::XRP: - // TODO: remove the factor, and set up loans in drops - return PrettyAsset{xrpIssue(), 1'000'000}; - - case AssetType::IOU: { - PrettyAsset const asset{issuer[iouCurrency]}; - - auto const limit = asset( - 100 * - (brokerParams.vaultDeposit + brokerParams.coverDeposit)); - if (lender != issuer) - env(trust(lender, limit)); - if (borrower != issuer) - env(trust(borrower, limit)); - - return asset; - } - - case AssetType::MPT: { - // Enough to cover initial fees - if (!env.le(keylet::account(issuer))) - env.fund( - env.current()->fees().accountReserve(10) * 10, issuer); - if (!env.le(keylet::account(lender))) - env.fund( - env.current()->fees().accountReserve(10) * 10, - noripple(lender)); - if (!env.le(keylet::account(borrower))) - env.fund( - env.current()->fees().accountReserve(10) * 10, - noripple(borrower)); - - MPTTester mptt{env, issuer, mptInitNoFund}; - mptt.create( - {.flags = - tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); - // Scale the MPT asset so interest is interesting - PrettyAsset const asset{mptt.issuanceID(), 10'000}; - // Need to do the authorization here because mptt isn't - // accessible outside - if (lender != issuer) - mptt.authorize({.account = lender}); - if (borrower != issuer) - mptt.authorize({.account = borrower}); - - env.close(); - - return asset; - } - - default: - throw std::runtime_error("Unknown asset type"); - } - } - - void - describeLoan( - jtx::Env& env, - BrokerParameters const& brokerParams, - LoanParameters const& loanParams, - AssetType assetType, - jtx::Account const& issuer, - jtx::Account const& lender, - jtx::Account const& borrower) - { - using namespace jtx; - - auto const asset = - createAsset(env, assetType, brokerParams, issuer, lender, borrower); - auto const principal = asset(loanParams.principalRequest).number(); - auto const interest = loanParams.interest.value_or(TenthBips32{}); - auto const interval = - loanParams.payInterval.value_or(LoanSet::defaultPaymentInterval); - auto const total = - loanParams.payTotal.value_or(LoanSet::defaultPaymentTotal); - auto const feeRate = brokerParams.managementFeeRate; - auto const props = computeLoanProperties( - asset, - principal, - interest, - interval, - total, - feeRate, - asset(brokerParams.vaultDeposit).number().exponent()); - log << "Loan properties:\n" - << "\tPrincipal: " << principal << std::endl - << "\tInterest rate: " << interest << std::endl - << "\tPayment interval: " << interval << std::endl - << "\tManagement Fee Rate: " << feeRate << std::endl - << "\tTotal Payments: " << total << std::endl - << "\tPeriodic Payment: " << props.periodicPayment << std::endl - << "\tTotal Value: " << props.totalValueOutstanding << std::endl - << "\tManagement Fee: " << props.managementFeeOwedToBroker - << std::endl - << "\tLoan Scale: " << props.loanScale << std::endl - << "\tFirst payment principal: " << props.firstPaymentPrincipal - << std::endl; - - // checkGuards returns a TER, so success is 0 - BEAST_EXPECT(!checkLoanGuards( - asset, - asset(loanParams.principalRequest).number(), - loanParams.interest.value_or(TenthBips32{}) != beast::zero, - loanParams.payTotal.value_or(LoanSet::defaultPaymentTotal), - props, - env.journal)); - } - - std::optional> - createLoan( - jtx::Env& env, - AssetType assetType, - BrokerParameters const& brokerParams, - LoanParameters const& loanParams, - jtx::Account const& issuer, - jtx::Account const& lender, - jtx::Account const& borrower) - { - using namespace jtx; - - // Enough to cover initial fees - env.fund(env.current()->fees().accountReserve(10) * 10, issuer); - if (lender != issuer) - env.fund( - env.current()->fees().accountReserve(10) * 10, - noripple(lender)); - if (borrower != issuer && borrower != lender) - env.fund( - env.current()->fees().accountReserve(10) * 10, - noripple(borrower)); - - describeLoan( - env, brokerParams, loanParams, assetType, issuer, lender, borrower); - - // Make the asset - auto const asset = - createAsset(env, assetType, brokerParams, issuer, lender, borrower); - - env.close(); - if (asset.native() || lender != issuer) - env(pay( - (asset.native() ? env.master : issuer), - lender, - asset(brokerParams.vaultDeposit + brokerParams.coverDeposit))); - // Fund the borrower later once we know the total loan - // size - - BrokerInfo const broker = - createVaultAndBroker(env, asset, lender, brokerParams); - - auto const pseudoAcctOpt = [&]() -> std::optional { - auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - return std::nullopt; - auto const brokerPseudo = brokerSle->at(sfAccount); - return Account("Broker pseudo-account", brokerPseudo); - }(); - if (!pseudoAcctOpt) - return std::nullopt; - Account const& pseudoAcct = *pseudoAcctOpt; - - auto const loanKeyletOpt = [&]() -> std::optional { - auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - return std::nullopt; - - // Broker has no loans - BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); - - // The loan keylet is based on the LoanSequence of the - // _LOAN_BROKER_ object. - auto const loanSequence = brokerSle->at(sfLoanSequence); - return keylet::loan(broker.brokerID, loanSequence); - }(); - if (!loanKeyletOpt) - return std::nullopt; - Keylet const& loanKeylet = *loanKeyletOpt; - - env(loanParams(env, broker)); - - env.close(); - - return std::make_tuple(broker, loanKeylet, pseudoAcct); - } - - void - topUpBorrower( - jtx::Env& env, - BrokerInfo const& broker, - jtx::Account const& issuer, - jtx::Account const& borrower, - LoanState const& state, - std::optional const& servFee) - { - using namespace jtx; - - STAmount const serviceFee = broker.asset(servFee.value_or(0)); - - // Ensure the borrower has enough funds to make the payments - // (including tx fees, if necessary) - auto const borrowerBalance = env.balance(borrower, broker.asset); - - auto const baseFee = env.current()->fees().base; - - // Add extra for transaction fees and reserves, if appropriate, or a - // tiny amount for the extra paid in each transaction - auto const totalNeeded = state.totalValue + - (serviceFee * state.paymentRemaining) + - (broker.asset.native() ? Number( - baseFee * state.paymentRemaining + - env.current()->fees().accountReserve( - env.ownerCount(borrower))) - : broker.asset(15).number()); - - auto const shortage = totalNeeded - borrowerBalance.number(); - - if (shortage > beast::zero && - (broker.asset.native() || issuer != borrower)) - env( - pay((broker.asset.native() ? env.master : issuer), - borrower, - STAmount{broker.asset, shortage})); - } - - void - makeLoanPayments( - jtx::Env& env, - BrokerInfo const& broker, - LoanParameters const& loanParams, - Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus, - jtx::Account const& issuer, - jtx::Account const& lender, - jtx::Account const& borrower, - PaymentParameters const& paymentParams = PaymentParameters::defaults()) - { - // Make all the individual payments - using namespace jtx; - using namespace jtx::loan; - using namespace std::chrono_literals; - using d = NetClock::duration; - - // Account const evan{"evan"}; - // Account const alice{"alice"}; - - bool const showStepBalances = paymentParams.showStepBalances; - - auto const currencyLabel = getCurrencyLabel(broker.asset); - - auto const baseFee = env.current()->fees().base; - - env.close(); - auto state = getCurrentState(env, broker, loanKeylet); - - verifyLoanStatus(state); - - STAmount const serviceFee = - broker.asset(loanParams.serviceFee.value_or(0)); - - topUpBorrower( - env, broker, issuer, borrower, state, loanParams.serviceFee); - - // Periodic payment amount will consist of - // 1. principal outstanding (1000) - // 2. interest interest rate (at 12%) - // 3. payment interval (600s) - // 4. loan service fee (2) - // Calculate these values without the helper functions - // to verify they're working correctly The numbers in - // the below BEAST_EXPECTs may not hold across assets. - auto const periodicRate = - loanPeriodicRate(state.interestRate, state.paymentInterval); - STAmount const roundedPeriodicPayment{ - broker.asset, - roundPeriodicPayment( - broker.asset, state.periodicPayment, state.loanScale)}; - - if (!showStepBalances) - log << currencyLabel << " Payment components: " - << "Payments remaining, " - << "rawInterest, rawPrincipal, " - "rawMFee, " - << "trackedValueDelta, trackedPrincipalDelta, " - "trackedInterestDelta, trackedMgmtFeeDelta, special" - << std::endl; - - // Include the service fee - STAmount const totalDue = roundToScale( - roundedPeriodicPayment + serviceFee, - state.loanScale, - Number::upward); - - auto currentRoundedState = constructLoanState( - state.totalValue, - state.principalOutstanding, - state.managementFeeOutstanding); - { - auto const raw = computeRawLoanState( - state.periodicPayment, - periodicRate, - state.paymentRemaining, - broker.params.managementFeeRate); - - if (showStepBalances) - { - log << currencyLabel << " Starting loan balances: " - << "\n\tTotal value: " - << currentRoundedState.valueOutstanding << "\n\tPrincipal: " - << currentRoundedState.principalOutstanding - << "\n\tInterest: " << currentRoundedState.interestDue - << "\n\tMgmt fee: " << currentRoundedState.managementFeeDue - << "\n\tPayments remaining " << state.paymentRemaining - << std::endl; - } - else - { - log << currencyLabel - << " Loan starting state: " << state.paymentRemaining - << ", " << raw.interestDue << ", " - << raw.principalOutstanding << ", " << raw.managementFeeDue - << ", " << currentRoundedState.valueOutstanding << ", " - << currentRoundedState.principalOutstanding << ", " - << currentRoundedState.interestDue << ", " - << currentRoundedState.managementFeeDue << std::endl; - } - } - - // Try to pay a little extra to show that it's _not_ - // taken - auto const extraAmount = paymentParams.overpaymentExtra - ? broker.asset(*paymentParams.overpaymentExtra).value() - : std::min( - broker.asset(10).value(), - STAmount{broker.asset, totalDue / 20}); - - STAmount const transactionAmount = - STAmount{broker.asset, totalDue * paymentParams.overpaymentFactor} + - extraAmount; - - auto const borrowerInitialBalance = - env.balance(borrower, broker.asset).number(); - auto const initialState = state; - detail::PaymentComponents totalPaid{ - .trackedValueDelta = 0, - .trackedPrincipalDelta = 0, - .trackedManagementFeeDelta = 0}; - Number totalInterestPaid = 0; - Number totalFeesPaid = 0; - std::size_t totalPaymentsMade = 0; - - ripple::LoanState currentTrueState = computeRawLoanState( - state.periodicPayment, - periodicRate, - state.paymentRemaining, - broker.params.managementFeeRate); - - auto validateBorrowerBalance = [&]() { - if (borrower == issuer || !paymentParams.validateBalances) - return; - auto const totalSpent = - (totalPaid.trackedValueDelta + totalFeesPaid + - (broker.asset.native() ? Number(baseFee) * totalPaymentsMade - : numZero)); - BEAST_EXPECT( - env.balance(borrower, broker.asset).number() == - borrowerInitialBalance - totalSpent); - }; - - auto const defaultRound = broker.asset.integral() ? 3 : 0; - auto truncate = [defaultRound]( - Number const& n, - std::optional places = std::nullopt) { - auto const p = places.value_or(defaultRound); - if (p == 0) - return n; - auto const factor = Number{1, p}; - return (n * factor).truncate() / factor; - }; - while (state.paymentRemaining > 0) - { - validateBorrowerBalance(); - // Compute the expected principal amount - auto const paymentComponents = detail::computePaymentComponents( - broker.asset.raw(), - state.loanScale, - state.totalValue, - state.principalOutstanding, - state.managementFeeOutstanding, - state.periodicPayment, - periodicRate, - state.paymentRemaining, - broker.params.managementFeeRate); - - BEAST_EXPECT( - paymentComponents.trackedValueDelta <= roundedPeriodicPayment || - (paymentComponents.specialCase == - detail::PaymentSpecialCase::final && - paymentComponents.trackedValueDelta >= - roundedPeriodicPayment)); - BEAST_EXPECT( - paymentComponents.trackedValueDelta == - paymentComponents.trackedPrincipalDelta + - paymentComponents.trackedInterestPart() + - paymentComponents.trackedManagementFeeDelta); - - ripple::LoanState const nextTrueState = computeRawLoanState( - state.periodicPayment, - periodicRate, - state.paymentRemaining - 1, - broker.params.managementFeeRate); - detail::LoanStateDeltas const deltas = - currentTrueState - nextTrueState; - BEAST_EXPECT( - deltas.total() == - deltas.principal + deltas.interest + deltas.managementFee); - BEAST_EXPECT( - paymentComponents.specialCase == - detail::PaymentSpecialCase::final || - deltas.total() == state.periodicPayment || - (state.loanScale - - (deltas.total() - state.periodicPayment).exponent()) > 14); - - if (!showStepBalances) - log << currencyLabel - << " Payment components: " << state.paymentRemaining << ", " - - << deltas.interest << ", " << deltas.principal << ", " - << deltas.managementFee << ", " - << paymentComponents.trackedValueDelta << ", " - << paymentComponents.trackedPrincipalDelta << ", " - << paymentComponents.trackedInterestPart() << ", " - << paymentComponents.trackedManagementFeeDelta << ", " - << (paymentComponents.specialCase == - detail::PaymentSpecialCase::final - ? "final" - : paymentComponents.specialCase == - detail::PaymentSpecialCase::extra - ? "extra" - : "none") - << std::endl; - - auto const totalDueAmount = STAmount{ - broker.asset, paymentComponents.trackedValueDelta + serviceFee}; - - if (paymentParams.validateBalances) - { - // Due to the rounding algorithms to keep the interest and - // principal in sync with "true" values, the computed amount - // may be a little less than the rounded fixed payment - // amount. For integral types, the difference should be < 3 - // (1 unit for each of the interest and management fee). For - // IOUs, the difference should be dust. - Number const diff = totalDue - totalDueAmount; - BEAST_EXPECT( - paymentComponents.specialCase == - detail::PaymentSpecialCase::final || - diff == beast::zero || - (diff > beast::zero && - ((broker.asset.integral() && - (static_cast(diff) < 3)) || - (state.loanScale - diff.exponent() > 13)))); - - BEAST_EXPECT( - paymentComponents.trackedPrincipalDelta >= beast::zero && - paymentComponents.trackedPrincipalDelta <= - state.principalOutstanding); - BEAST_EXPECT( - paymentComponents.specialCase != - detail::PaymentSpecialCase::final || - paymentComponents.trackedPrincipalDelta == - state.principalOutstanding); - } - - auto const borrowerBalanceBeforePayment = - env.balance(borrower, broker.asset); - - // Make the payment - env( - pay(borrower, - loanKeylet.key, - transactionAmount, - paymentParams.flags)); - - env.close(d{state.paymentInterval / 2}); - - if (paymentParams.validateBalances) - { - // Need to account for fees if the loan is in XRP - PrettyAmount adjustment = broker.asset(0); - if (broker.asset.native()) - { - adjustment = env.current()->fees().base; - } - - // Check the result - verifyLoanStatus.checkPayment( - state.loanScale, - borrower, - borrowerBalanceBeforePayment, - totalDueAmount, - adjustment); - } - - if (showStepBalances) - { - auto const loanSle = env.le(loanKeylet); - if (!BEAST_EXPECT(loanSle)) - // No reason for this not to exist - return; - auto const current = constructRoundedLoanState(loanSle); - auto const errors = nextTrueState - current; - log << currencyLabel << " Loan balances: " - << "\n\tAmount taken: " - << paymentComponents.trackedValueDelta - << "\n\tTotal value: " << current.valueOutstanding - << " (true: " << truncate(nextTrueState.valueOutstanding) - << ", error: " << truncate(errors.total()) - << ")\n\tPrincipal: " << current.principalOutstanding - << " (true: " - << truncate(nextTrueState.principalOutstanding) - << ", error: " << truncate(errors.principal) - << ")\n\tInterest: " << current.interestDue - << " (true: " << truncate(nextTrueState.interestDue) - << ", error: " << truncate(errors.interest) - << ")\n\tMgmt fee: " << current.managementFeeDue - << " (true: " << truncate(nextTrueState.managementFeeDue) - << ", error: " << truncate(errors.managementFee) - << ")\n\tPayments remaining " - << loanSle->at(sfPaymentRemaining) << std::endl; - - currentRoundedState = current; - } - - --state.paymentRemaining; - state.previousPaymentDate = state.nextPaymentDate; - if (paymentComponents.specialCase == - detail::PaymentSpecialCase::final) - { - state.paymentRemaining = 0; - state.nextPaymentDate = 0; - } - else - { - state.nextPaymentDate += state.paymentInterval; - } - state.principalOutstanding -= - paymentComponents.trackedPrincipalDelta; - state.managementFeeOutstanding -= - paymentComponents.trackedManagementFeeDelta; - state.totalValue -= paymentComponents.trackedValueDelta; - - if (paymentParams.validateBalances) - verifyLoanStatus(state); - - totalPaid.trackedValueDelta += paymentComponents.trackedValueDelta; - totalPaid.trackedPrincipalDelta += - paymentComponents.trackedPrincipalDelta; - totalPaid.trackedManagementFeeDelta += - paymentComponents.trackedManagementFeeDelta; - totalInterestPaid += paymentComponents.trackedInterestPart(); - totalFeesPaid += serviceFee; - ++totalPaymentsMade; - - currentTrueState = nextTrueState; - } - validateBorrowerBalance(); - - // Loan is paid off - BEAST_EXPECT(state.paymentRemaining == 0); - BEAST_EXPECT(state.principalOutstanding == 0); - - auto const initialInterestDue = initialState.totalValue - - (initialState.principalOutstanding + - initialState.managementFeeOutstanding); - if (paymentParams.validateBalances) - { - // Make sure all the payments add up - BEAST_EXPECT( - totalPaid.trackedValueDelta == initialState.totalValue); - BEAST_EXPECT( - totalPaid.trackedPrincipalDelta == - initialState.principalOutstanding); - BEAST_EXPECT( - totalPaid.trackedManagementFeeDelta == - initialState.managementFeeOutstanding); - // This is almost a tautology given the previous checks, but - // check it anyway for completeness. - BEAST_EXPECT(totalInterestPaid == initialInterestDue); - BEAST_EXPECT(totalPaymentsMade == initialState.paymentRemaining); - } - - if (showStepBalances) - { - auto const loanSle = env.le(loanKeylet); - if (!BEAST_EXPECT(loanSle)) - // No reason for this not to exist - return; - log << currencyLabel << " Total amounts paid: " - << "\n\tTotal value: " << totalPaid.trackedValueDelta - << " (initial: " << truncate(initialState.totalValue) - << ", error: " - << truncate( - initialState.totalValue - totalPaid.trackedValueDelta) - << ")\n\tPrincipal: " << totalPaid.trackedPrincipalDelta - << " (initial: " << truncate(initialState.principalOutstanding) - << ", error: " - << truncate( - initialState.principalOutstanding - - totalPaid.trackedPrincipalDelta) - << ")\n\tInterest: " << totalInterestPaid - << " (initial: " << truncate(initialInterestDue) << ", error: " - << truncate(initialInterestDue - totalInterestPaid) - << ")\n\tMgmt fee: " << totalPaid.trackedManagementFeeDelta - << " (initial: " - << truncate(initialState.managementFeeOutstanding) - << ", error: " - << truncate( - initialState.managementFeeOutstanding - - totalPaid.trackedManagementFeeDelta) - << ")\n\tTotal payments made: " << totalPaymentsMade - << std::endl; - } - } - - void - runLoan( - AssetType assetType, - BrokerParameters const& brokerParams, - LoanParameters const& loanParams) - { - using namespace jtx; - - Account const issuer("issuer"); - Account const lender("lender"); - Account const borrower("borrower"); - - Env env(*this, all); - - auto loanResult = createLoan( - env, assetType, brokerParams, loanParams, issuer, lender, borrower); - if (!BEAST_EXPECT(loanResult)) - return; - - auto broker = std::get(*loanResult); - auto loanKeylet = std::get(*loanResult); - auto pseudoAcct = std::get(*loanResult); - - VerifyLoanStatus verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); - - makeLoanPayments( - env, - broker, - loanParams, - loanKeylet, - verifyLoanStatus, - issuer, - lender, - borrower); - } - - /** Runs through the complete lifecycle of a loan - * - * 1. Create a loan. - * 2. Test a bunch of transaction failure conditions. - * 3. Use the `toEndOfLife` callback to take the loan to 0. How that is done - * depends on the callback. e.g. Default, Early payoff, make all the - * normal payments, etc. - * 4. Delete the loan. The loan will alternate between being deleted by the - * lender and the borrower. - */ - void - lifecycle( - std::string const& caseLabel, - char const* label, - jtx::Env& env, - Number const& loanAmount, - int interestExponent, - jtx::Account const& lender, - jtx::Account const& borrower, - jtx::Account const& evan, - BrokerInfo const& broker, - jtx::Account const& pseudoAcct, - std::uint32_t flags, - // The end of life callback is expected to take the loan to 0 payments - // remaining, one way or another - std::function toEndOfLife) - { - auto const [keylet, loanSequence] = [&]() { - auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - // will be invalid - return std::make_pair( - keylet::loan(broker.brokerID), std::uint32_t(0)); - - // Broker has no loans - BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); - - // The loan keylet is based on the LoanSequence of the _LOAN_BROKER_ - // object. - auto const loanSequence = brokerSle->at(sfLoanSequence); - return std::make_pair( - keylet::loan(broker.brokerID, loanSequence), loanSequence); - }(); - - VerifyLoanStatus const verifyLoanStatus( - env, broker, pseudoAcct, keylet); - - // No loans yet - verifyLoanStatus.checkBroker(0, 0, TenthBips32{0}, 1, 0, 0); - - if (!BEAST_EXPECT(loanSequence != 0)) - return; - - testcase << caseLabel << " " << label; - - using namespace jtx; - using namespace loan; - using namespace std::chrono_literals; - - auto applyExponent = [interestExponent, - this](TenthBips32 value) mutable { - BEAST_EXPECT(value > TenthBips32(0)); - while (interestExponent > 0) - { - auto const oldValue = value; - value *= 10; - --interestExponent; - BEAST_EXPECT(value / 10 == oldValue); - } - while (interestExponent < 0) - { - auto const oldValue = value; - value /= 10; - ++interestExponent; - BEAST_EXPECT(value * 10 == oldValue); - } - return value; - }; - - auto const borrowerOwnerCount = env.ownerCount(borrower); - - auto const loanSetFee = env.current()->fees().base * 2; - LoanParameters const loanParams{ - .account = borrower, - .counter = lender, - .counterpartyExplicit = false, - .principalRequest = loanAmount, - .setFee = loanSetFee, - .originationFee = 1, - .serviceFee = 2, - .lateFee = 3, - .closeFee = 4, - .overFee = applyExponent(percentageToTenthBips(5) / 10), - .interest = applyExponent(percentageToTenthBips(12)), - // 2.4% - .lateInterest = applyExponent(percentageToTenthBips(24) / 10), - .closeInterest = applyExponent(percentageToTenthBips(36) / 10), - .overpaymentInterest = - applyExponent(percentageToTenthBips(48) / 10), - .payTotal = 12, - .payInterval = 600, - .gracePd = 60, - .flags = flags, - }; - Number const principalRequestAmount = - broker.asset(loanParams.principalRequest).value(); - auto const originationFeeAmount = - broker.asset(*loanParams.originationFee).value(); - auto const serviceFeeAmount = - broker.asset(*loanParams.serviceFee).value(); - auto const lateFeeAmount = broker.asset(*loanParams.lateFee).value(); - auto const closeFeeAmount = broker.asset(*loanParams.closeFee).value(); - - auto const borrowerStartbalance = env.balance(borrower, broker.asset); - - auto createJtx = loanParams(env, broker); - // Successfully create a Loan - env(createJtx); - - env.close(); - - auto const startDate = - env.current()->info().parentCloseTime.time_since_epoch().count(); - - if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); - BEAST_EXPECT(brokerSle)) - { - BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 1); - } - - { - // Need to account for fees if the loan is in XRP - PrettyAmount adjustment = broker.asset(0); - if (broker.asset.native()) - { - adjustment = 2 * env.current()->fees().base; - } - - BEAST_EXPECT( - env.balance(borrower, broker.asset).value() == - borrowerStartbalance.value() + principalRequestAmount - - originationFeeAmount - adjustment.value()); - } - - auto const loanFlags = createJtx.stx->isFlag(tfLoanOverpayment) - ? lsfLoanOverpayment - : LedgerSpecificFlags(0); - - if (auto loan = env.le(keylet); BEAST_EXPECT(loan)) - { - // log << "loan after create: " << to_string(loan->getJson()) - // << std::endl; - BEAST_EXPECT( - loan->isFlag(lsfLoanOverpayment) == - createJtx.stx->isFlag(tfLoanOverpayment)); - BEAST_EXPECT(loan->at(sfLoanSequence) == loanSequence); - BEAST_EXPECT(loan->at(sfBorrower) == borrower.id()); - BEAST_EXPECT(loan->at(sfLoanBrokerID) == broker.brokerID); - BEAST_EXPECT( - loan->at(sfLoanOriginationFee) == originationFeeAmount); - BEAST_EXPECT(loan->at(sfLoanServiceFee) == serviceFeeAmount); - BEAST_EXPECT(loan->at(sfLatePaymentFee) == lateFeeAmount); - BEAST_EXPECT(loan->at(sfClosePaymentFee) == closeFeeAmount); - BEAST_EXPECT(loan->at(sfOverpaymentFee) == *loanParams.overFee); - BEAST_EXPECT(loan->at(sfInterestRate) == *loanParams.interest); - BEAST_EXPECT( - loan->at(sfLateInterestRate) == *loanParams.lateInterest); - BEAST_EXPECT( - loan->at(sfCloseInterestRate) == *loanParams.closeInterest); - BEAST_EXPECT( - loan->at(sfOverpaymentInterestRate) == - *loanParams.overpaymentInterest); - BEAST_EXPECT(loan->at(sfStartDate) == startDate); - BEAST_EXPECT( - loan->at(sfPaymentInterval) == *loanParams.payInterval); - BEAST_EXPECT(loan->at(sfGracePeriod) == *loanParams.gracePd); - BEAST_EXPECT(loan->at(sfPreviousPaymentDate) == 0); - BEAST_EXPECT( - loan->at(sfNextPaymentDueDate) == - startDate + *loanParams.payInterval); - BEAST_EXPECT(loan->at(sfPaymentRemaining) == *loanParams.payTotal); - BEAST_EXPECT( - loan->at(sfLoanScale) >= - (broker.asset.integral() - ? 0 - : std::max( - broker.vaultScale(env), - principalRequestAmount.exponent()))); - BEAST_EXPECT( - loan->at(sfPrincipalOutstanding) == principalRequestAmount); - } - - auto state = getCurrentState(env, broker, keylet, verifyLoanStatus); - - auto const loanProperties = computeLoanProperties( - broker.asset.raw(), - state.principalOutstanding, - state.interestRate, - state.paymentInterval, - state.paymentRemaining, - broker.params.managementFeeRate, - state.loanScale); - - verifyLoanStatus( - 0, - startDate + *loanParams.payInterval, - *loanParams.payTotal, - state.loanScale, - loanProperties.totalValueOutstanding, - principalRequestAmount, - loanProperties.managementFeeOwedToBroker, - loanProperties.periodicPayment, - loanFlags | 0); - - // Manage the loan - // no-op - env(manage(lender, keylet.key, 0)); - { - // no flags - auto jt = manage(lender, keylet.key, 0); - jt.removeMember(sfFlags.getName()); - env(jt); - } - // Only the lender can manage - env(manage(evan, keylet.key, 0), ter(tecNO_PERMISSION)); - // unknown flags - env(manage(lender, keylet.key, tfLoanManageMask), ter(temINVALID_FLAG)); - // combinations of flags are not allowed - env(manage(lender, keylet.key, tfLoanUnimpair | tfLoanImpair), - ter(temINVALID_FLAG)); - env(manage(lender, keylet.key, tfLoanImpair | tfLoanDefault), - ter(temINVALID_FLAG)); - env(manage(lender, keylet.key, tfLoanUnimpair | tfLoanDefault), - ter(temINVALID_FLAG)); - env(manage( - lender, - keylet.key, - tfLoanUnimpair | tfLoanImpair | tfLoanDefault), - ter(temINVALID_FLAG)); - // invalid loan ID - env(manage(lender, broker.brokerID, tfLoanImpair), ter(tecNO_ENTRY)); - // Loan is unimpaired, can't unimpair it again - env(manage(lender, keylet.key, tfLoanUnimpair), ter(tecNO_PERMISSION)); - // Loan is unimpaired, it can go into default, but only after it's past - // due - env(manage(lender, keylet.key, tfLoanDefault), ter(tecTOO_SOON)); - - // Check the vault - bool const canImpair = canImpairLoan(env, broker, state); - // Impair the loan, if possible - env(manage(lender, keylet.key, tfLoanImpair), - canImpair ? ter(tesSUCCESS) : ter(tecLIMIT_EXCEEDED)); - // Unimpair the loan - env(manage(lender, keylet.key, tfLoanUnimpair), - canImpair ? ter(tesSUCCESS) : ter(tecNO_PERMISSION)); - - auto const nextDueDate = startDate + *loanParams.payInterval; - - env.close(); - - verifyLoanStatus( - 0, - nextDueDate, - *loanParams.payTotal, - loanProperties.loanScale, - loanProperties.totalValueOutstanding, - principalRequestAmount, - loanProperties.managementFeeOwedToBroker, - loanProperties.periodicPayment, - loanFlags | 0); - - // Can't delete the loan yet. It has payments remaining. - env(del(lender, keylet.key), ter(tecHAS_OBLIGATIONS)); - - if (BEAST_EXPECT(toEndOfLife)) - toEndOfLife(keylet, verifyLoanStatus); - env.close(); - - // Verify the loan is at EOL - if (auto loan = env.le(keylet); BEAST_EXPECT(loan)) - { - BEAST_EXPECT(loan->at(sfPaymentRemaining) == 0); - BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == 0); - } - auto const borrowerStartingBalance = - env.balance(borrower, broker.asset); - - // Try to delete the loan broker with an active loan - env(loanBroker::del(lender, broker.brokerID), ter(tecHAS_OBLIGATIONS)); - // Ensure the above tx doesn't get ordered after the LoanDelete and - // delete our broker! - env.close(); - - // Test failure cases - env(del(lender, keylet.key, tfLoanOverpayment), ter(temINVALID_FLAG)); - env(del(evan, keylet.key), ter(tecNO_PERMISSION)); - env(del(lender, broker.brokerID), ter(tecNO_ENTRY)); - - // Delete the loan - // Either the borrower or the lender can delete the loan. Alternate - // between who does it across tests. - static unsigned deleteCounter = 0; - auto const deleter = ++deleteCounter % 2 ? lender : borrower; - env(del(deleter, keylet.key)); - env.close(); - - PrettyAmount adjustment = broker.asset(0); - if (deleter == borrower) - { - // Need to account for fees if the loan is in XRP - if (broker.asset.native()) - { - adjustment = env.current()->fees().base; - } - } - - // No loans left - verifyLoanStatus.checkBroker(0, 0, *loanParams.interest, 1, 0, 0); - - BEAST_EXPECT( - env.balance(borrower, broker.asset).value() == - borrowerStartingBalance.value() - adjustment); - BEAST_EXPECT(env.ownerCount(borrower) == borrowerOwnerCount); - - if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); - BEAST_EXPECT(brokerSle)) - { - BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); - } - } - - std::string - getCurrencyLabel(Asset const& asset) - { - return ( - asset.native() ? "XRP" - : asset.holds() ? "IOU" - : asset.holds() ? "MPT" - : "Unknown"); - } - - /** Wrapper to run a series of lifecycle tests for a given asset and loan - * amount - * - * Will be used in the future to vary the loan parameters. For now, it is - * only called once. - * - * Tests a bunch of LoanSet failure conditions before lifecycle. - */ - template - void - testCaseWrapper( - jtx::Env& env, - jtx::MPTTester& mptt, - std::array const& assets, - BrokerInfo const& broker, - Number const& loanAmount, - int interestExponent) - { - using namespace jtx; - using namespace Lending; - - auto const& asset = broker.asset.raw(); - auto const currencyLabel = getCurrencyLabel(asset); - auto const caseLabel = [&]() { - std::stringstream ss; - ss << "Lifecycle: " << loanAmount << " " << currencyLabel - << " Scale interest to: " << interestExponent << " "; - return ss.str(); - }(); - testcase << caseLabel; - - using namespace loan; - using namespace std::chrono_literals; - using d = NetClock::duration; - using tp = NetClock::time_point; - - Account const issuer{"issuer"}; - // For simplicity, lender will be the sole actor for the vault & - // brokers. - Account const lender{"lender"}; - // Borrower only wants to borrow - Account const borrower{"borrower"}; - // Evan will attempt to be naughty - Account const evan{"evan"}; - // Do not fund alice - Account const alice{"alice"}; - - Number const principalRequest = broker.asset(loanAmount).value(); - Number const maxCoveredLoanValue = broker.params.maxCoveredLoanValue(0); - BEAST_EXPECT(maxCoveredLoanValue == 1000 * 100 / 10); - Number const maxCoveredLoanRequest = - broker.asset(maxCoveredLoanValue).value(); - Number const totalVaultRequest = - broker.asset(broker.params.vaultDeposit).value(); - Number const debtMaximumRequest = - broker.asset(broker.params.debtMax).value(); - - auto const loanSetFee = fee(env.current()->fees().base * 2); - - auto const pseudoAcct = [&]() { - auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - return lender; - auto const brokerPseudo = brokerSle->at(sfAccount); - return Account("Broker pseudo-account", brokerPseudo); - }(); - - auto const baseFee = env.current()->fees().base; - - auto badKeylet = keylet::vault(lender.id(), env.seq(lender)); - // Try some failure cases - // flags are checked first - env(set(evan, broker.brokerID, principalRequest, tfLoanSetMask), - sig(sfCounterpartySignature, lender), - loanSetFee, - ter(temINVALID_FLAG)); - - // field length validation - // sfData: good length, bad account - env(set(evan, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, borrower), - data(std::string(maxDataPayloadLength, 'X')), - loanSetFee, - ter(tefBAD_AUTH)); - // sfData: too long - env(set(evan, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, lender), - data(std::string(maxDataPayloadLength + 1, 'Y')), - loanSetFee, - ter(temINVALID)); - - // field range validation - // sfOverpaymentFee: good value, bad account - env(set(evan, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, borrower), - overpaymentFee(maxOverpaymentFee), - loanSetFee, - ter(tefBAD_AUTH)); - // sfOverpaymentFee: too big - env(set(evan, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, lender), - overpaymentFee(maxOverpaymentFee + 1), - loanSetFee, - ter(temINVALID)); - - // sfInterestRate: good value, bad account - env(set(evan, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, borrower), - interestRate(maxInterestRate), - loanSetFee, - ter(tefBAD_AUTH)); - env(set(evan, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, borrower), - interestRate(TenthBips32(0)), - loanSetFee, - ter(tefBAD_AUTH)); - // sfInterestRate: too big - env(set(evan, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, lender), - interestRate(maxInterestRate + 1), - loanSetFee, - ter(temINVALID)); - // sfInterestRate: too small - env(set(evan, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, lender), - interestRate(TenthBips32(-1)), - loanSetFee, - ter(temINVALID)); - - // sfLateInterestRate: good value, bad account - env(set(evan, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, borrower), - lateInterestRate(maxLateInterestRate), - loanSetFee, - ter(tefBAD_AUTH)); - env(set(evan, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, borrower), - lateInterestRate(TenthBips32(0)), - loanSetFee, - ter(tefBAD_AUTH)); - // sfLateInterestRate: too big - env(set(evan, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, lender), - lateInterestRate(maxLateInterestRate + 1), - loanSetFee, - ter(temINVALID)); - // sfLateInterestRate: too small - env(set(evan, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, lender), - lateInterestRate(TenthBips32(-1)), - loanSetFee, - ter(temINVALID)); - - // sfCloseInterestRate: good value, bad account - env(set(evan, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, borrower), - closeInterestRate(maxCloseInterestRate), - loanSetFee, - ter(tefBAD_AUTH)); - env(set(evan, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, borrower), - closeInterestRate(TenthBips32(0)), - loanSetFee, - ter(tefBAD_AUTH)); - // sfCloseInterestRate: too big - env(set(evan, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, lender), - closeInterestRate(maxCloseInterestRate + 1), - loanSetFee, - ter(temINVALID)); - env(set(evan, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, lender), - closeInterestRate(TenthBips32(-1)), - loanSetFee, - ter(temINVALID)); - - // sfOverpaymentInterestRate: good value, bad account - env(set(evan, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, borrower), - overpaymentInterestRate(maxOverpaymentInterestRate), - loanSetFee, - ter(tefBAD_AUTH)); - env(set(evan, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, borrower), - overpaymentInterestRate(TenthBips32(0)), - loanSetFee, - ter(tefBAD_AUTH)); - // sfOverpaymentInterestRate: too big - env(set(evan, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, lender), - overpaymentInterestRate(maxOverpaymentInterestRate + 1), - loanSetFee, - ter(temINVALID)); - env(set(evan, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, lender), - overpaymentInterestRate(TenthBips32(-1)), - loanSetFee, - ter(temINVALID)); - - // sfPaymentTotal: good value, bad account - env(set(evan, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, borrower), - paymentTotal(LoanSet::minPaymentTotal), - loanSetFee, - ter(tefBAD_AUTH)); - // sfPaymentTotal: too small (there is no max) - env(set(evan, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, lender), - paymentTotal(LoanSet::minPaymentTotal - 1), - loanSetFee, - ter(temINVALID)); - - // sfPaymentInterval: good value, bad account - env(set(evan, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, borrower), - paymentInterval(LoanSet::minPaymentInterval), - loanSetFee, - ter(tefBAD_AUTH)); - // sfPaymentInterval: too small (there is no max) - env(set(evan, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, lender), - paymentInterval(LoanSet::minPaymentInterval - 1), - loanSetFee, - ter(temINVALID)); - - // sfGracePeriod: good value, bad account - env(set(evan, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, borrower), - paymentInterval(LoanSet::minPaymentInterval * 2), - gracePeriod(LoanSet::minPaymentInterval * 2), - loanSetFee, - ter(tefBAD_AUTH)); - // sfGracePeriod: larger than paymentInterval - env(set(evan, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, lender), - paymentInterval(LoanSet::minPaymentInterval * 2), - gracePeriod(LoanSet::minPaymentInterval * 3), - loanSetFee, - ter(temINVALID)); - - // insufficient fee - single sign - env(set(borrower, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, lender), - ter(telINSUF_FEE_P)); - // insufficient fee - multisign - env(signers(lender, 2, {{evan, 1}, {borrower, 1}})); - env(signers(borrower, 2, {{evan, 1}, {lender, 1}})); - env(set(borrower, broker.brokerID, principalRequest), - counterparty(lender), - msig(evan, lender), - msig(sfCounterpartySignature, evan, borrower), - fee(env.current()->fees().base * 5 - 1), - ter(telINSUF_FEE_P)); - // Bad multisign signatures for borrower (Account) - env(set(borrower, broker.brokerID, principalRequest), - counterparty(lender), - msig(alice, issuer), - msig(sfCounterpartySignature, evan, borrower), - fee(env.current()->fees().base * 5), - ter(tefBAD_SIGNATURE)); - // Bad multisign signatures for issuer (Counterparty) - env(set(borrower, broker.brokerID, principalRequest), - counterparty(lender), - msig(evan, lender), - msig(sfCounterpartySignature, alice, issuer), - fee(env.current()->fees().base * 5 - 1), - ter(tefBAD_SIGNATURE)); - env(signers(lender, none)); - env(signers(borrower, none)); - // multisign sufficient fee, but no signers set up - env(set(borrower, broker.brokerID, principalRequest), - counterparty(lender), - msig(evan, lender), - msig(sfCounterpartySignature, evan, borrower), - fee(env.current()->fees().base * 5), - ter(tefNOT_MULTI_SIGNING)); - // not the broker owner, no counterparty, not signed by broker - // owner - env(set(borrower, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, evan), - loanSetFee, - ter(tefBAD_AUTH)); - // not the broker owner, counterparty is borrower - env(set(evan, broker.brokerID, principalRequest), - counterparty(borrower), - sig(sfCounterpartySignature, borrower), - loanSetFee, - ter(tecNO_PERMISSION)); - // not a LoanBroker object, no counterparty - env(set(lender, badKeylet.key, principalRequest), - sig(sfCounterpartySignature, evan), - loanSetFee, - ter(temBAD_SIGNER)); - // not a LoanBroker object, counterparty is valid - env(set(lender, badKeylet.key, principalRequest), - counterparty(borrower), - sig(sfCounterpartySignature, borrower), - loanSetFee, - ter(tecNO_ENTRY)); - // borrower doesn't exist - env(set(lender, broker.brokerID, principalRequest), - counterparty(alice), - sig(sfCounterpartySignature, alice), - loanSetFee, - ter(terNO_ACCOUNT)); - - // Request more funds than the vault has available - env(set(evan, broker.brokerID, totalVaultRequest + 1), - sig(sfCounterpartySignature, lender), - loanSetFee, - ter(tecINSUFFICIENT_FUNDS)); - - // Request more funds than the broker's first-loss capital can - // cover. - env(set(evan, broker.brokerID, maxCoveredLoanRequest + 1), - sig(sfCounterpartySignature, lender), - loanSetFee, - ter(tecINSUFFICIENT_FUNDS)); - - // Frozen trust line / locked MPT issuance - // XRP can not be frozen, but run through the loop anyway to test - // the tecLIMIT_EXCEEDED case - { - auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - return; - - auto const vaultPseudo = [&]() { - auto const vaultSle = - env.le(keylet::vault(brokerSle->at(sfVaultID))); - if (!BEAST_EXPECT(vaultSle)) - // This will be wrong, but the test has failed anyway. - return lender; - auto const vaultPseudo = - Account("Vault pseudo-account", vaultSle->at(sfAccount)); - return vaultPseudo; - }(); - - auto const [freeze, deepfreeze, unfreeze, expectedResult] = - [&]() -> std::tuple< - std::function, - std::function, - std::function, - TER> { - // Freeze / lock the asset - std::function empty; - if (broker.asset.native()) - { - // XRP can't be frozen - return std::make_tuple(empty, empty, empty, tesSUCCESS); - } - else if (broker.asset.holds()) - { - auto freeze = [&](Account const& holder) { - env(trust(issuer, holder[iouCurrency](0), tfSetFreeze)); - }; - auto deepfreeze = [&](Account const& holder) { - env(trust( - issuer, - holder[iouCurrency](0), - tfSetFreeze | tfSetDeepFreeze)); - }; - auto unfreeze = [&](Account const& holder) { - env(trust( - issuer, - holder[iouCurrency](0), - tfClearFreeze | tfClearDeepFreeze)); - }; - return std::make_tuple( - freeze, deepfreeze, unfreeze, tecFROZEN); - } - else - { - auto freeze = [&](Account const& holder) { - mptt.set( - {.account = issuer, - .holder = holder, - .flags = tfMPTLock}); - }; - auto unfreeze = [&](Account const& holder) { - mptt.set( - {.account = issuer, - .holder = holder, - .flags = tfMPTUnlock}); - }; - return std::make_tuple(freeze, empty, unfreeze, tecLOCKED); - } - }(); - - // Try freezing the accounts that can't be frozen - if (freeze) - { - for (auto const& account : {vaultPseudo, evan}) - { - // Freeze the account - freeze(account); - - // Try to create a loan with a frozen line - env(set(evan, broker.brokerID, debtMaximumRequest), - sig(sfCounterpartySignature, lender), - loanSetFee, - ter(expectedResult)); - - // Unfreeze the account - BEAST_EXPECT(unfreeze); - unfreeze(account); - - // Ensure the line is unfrozen with a request that is fine - // except too it requests more principal than the broker can - // carry - env(set(evan, broker.brokerID, debtMaximumRequest + 1), - sig(sfCounterpartySignature, lender), - loanSetFee, - ter(tecLIMIT_EXCEEDED)); - } - } - - // Deep freeze the borrower, which prevents them from receiving - // funds - if (deepfreeze) - { - // Make sure evan has a trust line that so the issuer can - // freeze it. (Don't need to do this for the borrower, - // because LoanSet will create a line to the borrower - // automatically.) - env(trust(evan, issuer[iouCurrency](100'000))); - - for (auto const& account : - {// these accounts can't be frozen, which deep freeze - // implies - vaultPseudo, - evan, - // these accounts can't be deep frozen - lender}) - { - // Freeze evan - deepfreeze(account); - - // Try to create a loan with a deep frozen line - env(set(evan, broker.brokerID, debtMaximumRequest), - sig(sfCounterpartySignature, lender), - loanSetFee, - ter(expectedResult)); - - // Unfreeze evan - BEAST_EXPECT(unfreeze); - unfreeze(account); - - // Ensure the line is unfrozen with a request that is fine - // except too it requests more principal than the broker can - // carry - env(set(evan, broker.brokerID, debtMaximumRequest + 1), - sig(sfCounterpartySignature, lender), - loanSetFee, - ter(tecLIMIT_EXCEEDED)); - } - } - } - - // Finally! Create a loan - std::string testData; - - auto coverAvailable = - [&env, this](uint256 const& brokerID, Number const& expected) { - if (auto const brokerSle = env.le(keylet::loanbroker(brokerID)); - BEAST_EXPECT(brokerSle)) - { - auto const available = brokerSle->at(sfCoverAvailable); - BEAST_EXPECT(available == expected); - return available; - } - return Number{}; - }; - auto getDefaultInfo = [&env, this]( - LoanState const& state, - BrokerInfo const& broker) { - if (auto const brokerSle = - env.le(keylet::loanbroker(broker.brokerID)); - BEAST_EXPECT(brokerSle)) - { - BEAST_EXPECT( - state.loanScale >= - (broker.asset.integral() - ? 0 - : std::max( - broker.vaultScale(env), - state.principalOutstanding.exponent()))); - NumberRoundModeGuard mg(Number::upward); - auto const defaultAmount = roundToAsset( - broker.asset, - std::min( - tenthBipsOfValue( - tenthBipsOfValue( - brokerSle->at(sfDebtTotal), - broker.params.coverRateMin), - broker.params.coverRateLiquidation), - state.totalValue - state.managementFeeOutstanding), - state.loanScale); - return std::make_pair(defaultAmount, brokerSle->at(sfOwner)); - } - return std::make_pair(Number{}, AccountID{}); - }; - auto replenishCover = [&env, &coverAvailable]( - BrokerInfo const& broker, - AccountID const& brokerAcct, - Number const& startingCoverAvailable, - Number const& amountToBeCovered) { - coverAvailable( - broker.brokerID, startingCoverAvailable - amountToBeCovered); - env(loanBroker::coverDeposit( - brokerAcct, - broker.brokerID, - STAmount{broker.asset, amountToBeCovered})); - coverAvailable(broker.brokerID, startingCoverAvailable); - env.close(); - }; - - auto defaultImmediately = [&](std::uint32_t baseFlag, - bool impair = true) { - return [&, impair, baseFlag]( - Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus) { - // toEndOfLife - // - // Default the loan - - // Initialize values with the current state - auto state = - getCurrentState(env, broker, loanKeylet, verifyLoanStatus); - BEAST_EXPECT(state.flags == baseFlag); - - auto const& broker = verifyLoanStatus.broker; - auto const startingCoverAvailable = coverAvailable( - broker.brokerID, - broker.asset(broker.params.coverDeposit).number()); - - if (impair) - { - // Check the vault - bool const canImpair = canImpairLoan(env, broker, state); - // Impair the loan, if possible - env(manage(lender, loanKeylet.key, tfLoanImpair), - canImpair ? ter(tesSUCCESS) : ter(tecLIMIT_EXCEEDED)); - - if (canImpair) - { - state.flags |= tfLoanImpair; - state.nextPaymentDate = - env.now().time_since_epoch().count(); - - // Once the loan is impaired, it can't be impaired again - env(manage(lender, loanKeylet.key, tfLoanImpair), - ter(tecNO_PERMISSION)); - } - verifyLoanStatus(state); - } - - auto const nextDueDate = tp{d{state.nextPaymentDate}}; - - // Can't default the loan yet. The grace period hasn't - // expired - env(manage(lender, loanKeylet.key, tfLoanDefault), - ter(tecTOO_SOON)); - - // Let some time pass so that the loan can be - // defaulted - env.close(nextDueDate + 60s); - - auto const [amountToBeCovered, brokerAcct] = - getDefaultInfo(state, broker); - - // Default the loan - env(manage(lender, loanKeylet.key, tfLoanDefault)); - env.close(); - - // The LoanBroker just lost some of it's first-loss capital. - // Replenish it. - replenishCover( - broker, - brokerAcct, - startingCoverAvailable, - amountToBeCovered); - - state.flags |= tfLoanDefault; - state.paymentRemaining = 0; - state.totalValue = 0; - state.principalOutstanding = 0; - state.managementFeeOutstanding = 0; - state.nextPaymentDate = 0; - verifyLoanStatus(state); - - // Once a loan is defaulted, it can't be managed - env(manage(lender, loanKeylet.key, tfLoanUnimpair), - ter(tecNO_PERMISSION)); - env(manage(lender, loanKeylet.key, tfLoanImpair), - ter(tecNO_PERMISSION)); - // Can't make a payment on it either - env(pay(borrower, loanKeylet.key, broker.asset(300)), - ter(tecKILLED)); - }; - }; - - auto singlePayment = [&](Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus, - LoanState& state, - STAmount const& payoffAmount, - std::uint32_t numPayments, - std::uint32_t baseFlag, - std::uint32_t txFlags) { - // toEndOfLife - // - verifyLoanStatus(state); - - // Send some bogus pay transactions - env(pay(borrower, - keylet::loan(uint256(0)).key, - broker.asset(10), - txFlags), - ter(temINVALID)); - // broker.asset(80) is less than a single payment, but all these - // checks fail before that matters - env(pay(borrower, loanKeylet.key, broker.asset(-80), txFlags), - ter(temBAD_AMOUNT)); - env(pay(borrower, broker.brokerID, broker.asset(80), txFlags), - ter(tecNO_ENTRY)); - env(pay(evan, loanKeylet.key, broker.asset(80), txFlags), - ter(tecNO_PERMISSION)); - - // TODO: Write a general "isFlag" function? See STObject::isFlag. - // Maybe add a static overloaded member? - if (!(state.flags & lsfLoanOverpayment)) - { - // If the loan does not allow overpayments, send a payment that - // tries to make an overpayment. Do not include `txFlags`, so we - // don't end up duplicating the next test transaction. - env(pay(borrower, - loanKeylet.key, - STAmount{ - broker.asset, - state.periodicPayment * Number{15, -1}}, - tfLoanOverpayment), - fee(XRPAmount{ - baseFee * - (Number{15, -1} / loanPaymentsPerFeeIncrement + 1)}), - ter(temINVALID_FLAG)); - } - // Try to send a payment marked as multiple mutually exclusive - // payment types. Do not include `txFlags`, so we don't duplicate - // the prior test transaction. - env(pay(borrower, - loanKeylet.key, - broker.asset(state.periodicPayment * 2), - tfLoanLatePayment | tfLoanFullPayment), - ter(temINVALID_FLAG)); - env(pay(borrower, - loanKeylet.key, - broker.asset(state.periodicPayment * 2), - tfLoanLatePayment | tfLoanOverpayment), - ter(temINVALID_FLAG)); - env(pay(borrower, - loanKeylet.key, - broker.asset(state.periodicPayment * 2), - tfLoanOverpayment | tfLoanFullPayment), - ter(temINVALID_FLAG)); - env(pay(borrower, - loanKeylet.key, - broker.asset(state.periodicPayment * 2), - tfLoanLatePayment | tfLoanOverpayment | tfLoanFullPayment), - ter(temINVALID_FLAG)); - - { - auto const otherAsset = broker.asset.raw() == assets[0].raw() - ? assets[1] - : assets[0]; - env(pay(borrower, loanKeylet.key, otherAsset(100), txFlags), - ter(tecWRONG_ASSET)); - } - - // Amount doesn't cover a single payment - env(pay(borrower, - loanKeylet.key, - STAmount{broker.asset, 1}, - txFlags), - ter(tecINSUFFICIENT_PAYMENT)); - - // Get the balance after these failed transactions take - // fees - auto const borrowerBalanceBeforePayment = - env.balance(borrower, broker.asset); - - BEAST_EXPECT(payoffAmount > state.principalOutstanding); - // Try to pay a little extra to show that it's _not_ - // taken - auto const transactionAmount = payoffAmount + broker.asset(10); - - // Send a transaction that tries to pay more than the borrowers's - // balance - XRPAmount const badFee{ - baseFee * - (borrowerBalanceBeforePayment.number() * 2 / - state.periodicPayment / loanPaymentsPerFeeIncrement + - 1)}; - env(pay(borrower, - loanKeylet.key, - STAmount{ - broker.asset, - borrowerBalanceBeforePayment.number() * 2}, - txFlags), - fee(badFee), - ter(tecINSUFFICIENT_FUNDS)); - - XRPAmount const goodFee{ - baseFee * (numPayments / loanPaymentsPerFeeIncrement + 1)}; - env(pay(borrower, loanKeylet.key, transactionAmount, txFlags), - fee(goodFee)); - - env.close(); - - // log << env.meta()->getJson() << std::endl; - - // Need to account for fees if the loan is in XRP - PrettyAmount adjustment = broker.asset(0); - if (broker.asset.native()) - { - adjustment = badFee + goodFee; - } - - state.paymentRemaining = 0; - state.principalOutstanding = 0; - state.totalValue = 0; - state.managementFeeOutstanding = 0; - state.previousPaymentDate = state.nextPaymentDate + - state.paymentInterval * (numPayments - 1); - state.nextPaymentDate = 0; - verifyLoanStatus(state); - - verifyLoanStatus.checkPayment( - state.loanScale, - borrower, - borrowerBalanceBeforePayment, - payoffAmount, - adjustment); - - // Can't impair or default a paid off loan - env(manage(lender, loanKeylet.key, tfLoanImpair), - ter(tecNO_PERMISSION)); - env(manage(lender, loanKeylet.key, tfLoanDefault), - ter(tecNO_PERMISSION)); - }; - - auto fullPayment = [&](std::uint32_t baseFlag) { - return [&, baseFlag]( - Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus) { - // toEndOfLife - // - auto state = - getCurrentState(env, broker, loanKeylet, verifyLoanStatus); - env.close(state.startDate + 20s); - auto const loanAge = (env.now() - state.startDate).count(); - BEAST_EXPECT(loanAge == 30); - - // Full payoff amount will consist of - // 1. principal outstanding (1000) - // 2. accrued interest (at 12%) - // 3. prepayment penalty (closeInterest at 3.6%) - // 4. close payment fee (4) - // Calculate these values without the helper functions - // to verify they're working correctly The numbers in - // the below BEAST_EXPECTs may not hold across assets. - Number const interval = state.paymentInterval; - auto const periodicRate = - interval * Number(12, -2) / secondsInYear; - BEAST_EXPECT( - periodicRate == - Number(2283105022831050, -21, Number::unchecked{})); - STAmount const principalOutstanding{ - broker.asset, state.principalOutstanding}; - STAmount const accruedInterest{ - broker.asset, - state.principalOutstanding * periodicRate * loanAge / - interval}; - BEAST_EXPECT( - accruedInterest == - broker.asset(Number(1141552511415525, -19))); - STAmount const prepaymentPenalty{ - broker.asset, state.principalOutstanding * Number(36, -3)}; - BEAST_EXPECT(prepaymentPenalty == broker.asset(36)); - STAmount const closePaymentFee = broker.asset(4); - auto const payoffAmount = roundToScale( - principalOutstanding + accruedInterest + prepaymentPenalty + - closePaymentFee, - state.loanScale); - BEAST_EXPECT( - payoffAmount == - roundToAsset( - broker.asset, - broker.asset(Number(1040000114155251, -12)).number(), - state.loanScale)); - - // The terms of this loan actually make the early payoff - // more expensive than just making payments - BEAST_EXPECT( - payoffAmount > state.paymentRemaining * - (state.periodicPayment + broker.asset(2).value())); - - singlePayment( - loanKeylet, - verifyLoanStatus, - state, - payoffAmount, - 1, - baseFlag, - tfLoanFullPayment); - }; - }; - - auto combineAllPayments = [&](std::uint32_t baseFlag) { - return [&, baseFlag]( - Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus) { - // toEndOfLife - // - - auto state = - getCurrentState(env, broker, loanKeylet, verifyLoanStatus); - env.close(); - - // Make all the payments in one transaction - // service fee is 2 - auto const startingPayments = state.paymentRemaining; - auto const rawPayoff = startingPayments * - (state.periodicPayment + broker.asset(2).value()); - STAmount const payoffAmount{broker.asset, rawPayoff}; - BEAST_EXPECT( - payoffAmount == - broker.asset(Number(1024014840139457, -12))); - BEAST_EXPECT(payoffAmount > state.principalOutstanding); - - singlePayment( - loanKeylet, - verifyLoanStatus, - state, - payoffAmount, - state.paymentRemaining, - baseFlag, - 0); - }; - }; - - // There are a lot of fields that can be set on a loan, but most - // of them only affect the "math" when a payment is made. The - // only one that really affects behavior is the - // `tfLoanOverpayment` flag. - lifecycle( - caseLabel, - "Loan overpayment allowed - Impair and Default", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - defaultImmediately(lsfLoanOverpayment)); - - lifecycle( - caseLabel, - "Loan overpayment prohibited - Impair and Default", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - 0, - defaultImmediately(0)); - - lifecycle( - caseLabel, - "Loan overpayment allowed - Default without Impair", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - defaultImmediately(lsfLoanOverpayment, false)); - - lifecycle( - caseLabel, - "Loan overpayment prohibited - Default without Impair", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - 0, - defaultImmediately(0, false)); - - lifecycle( - caseLabel, - "Loan overpayment prohibited - Pay off immediately", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - 0, - fullPayment(0)); - - lifecycle( - caseLabel, - "Loan overpayment allowed - Pay off immediately", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - fullPayment(lsfLoanOverpayment)); - - lifecycle( - caseLabel, - "Loan overpayment prohibited - Combine all payments", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - 0, - combineAllPayments(0)); - - lifecycle( - caseLabel, - "Loan overpayment allowed - Combine all payments", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - combineAllPayments(lsfLoanOverpayment)); - - lifecycle( - caseLabel, - "Loan overpayment prohibited - Make payments", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - 0, - [&](Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus) { - // toEndOfLife - // - // Draw and make multiple payments - auto state = - getCurrentState(env, broker, loanKeylet, verifyLoanStatus); - BEAST_EXPECT(state.flags == 0); - env.close(); - - verifyLoanStatus(state); - - env.close(state.startDate + 20s); - auto const loanAge = (env.now() - state.startDate).count(); - BEAST_EXPECT(loanAge == 30); - - // Periodic payment amount will consist of - // 1. principal outstanding (1000) - // 2. interest interest rate (at 12%) - // 3. payment interval (600s) - // 4. loan service fee (2) - // Calculate these values without the helper functions - // to verify they're working correctly The numbers in - // the below BEAST_EXPECTs may not hold across assets. - Number const interval = state.paymentInterval; - auto const periodicRate = - interval * Number(12, -2) / secondsInYear; - BEAST_EXPECT( - periodicRate == - Number(2283105022831050, -21, Number::unchecked{})); - STAmount const roundedPeriodicPayment{ - broker.asset, - roundPeriodicPayment( - broker.asset, state.periodicPayment, state.loanScale)}; - - testcase - << currencyLabel << " Payment components: " - << "Payments remaining, rawInterest, rawPrincipal, " - "rawMFee, trackedValueDelta, trackedPrincipalDelta, " - "trackedInterestDelta, trackedMgmtFeeDelta, special"; - - auto const serviceFee = broker.asset(2); - - BEAST_EXPECT( - roundedPeriodicPayment == - roundToScale( - broker.asset( - Number(8333457001162141, -14), Number::upward), - state.loanScale, - Number::upward)); - // 83334570.01162141 - // Include the service fee - STAmount const totalDue = roundToScale( - roundedPeriodicPayment + serviceFee, - state.loanScale, - Number::upward); - // Only check the first payment since the rounding - // may drift as payments are made - BEAST_EXPECT( - totalDue == - roundToScale( - broker.asset( - Number(8533457001162141, -14), Number::upward), - state.loanScale, - Number::upward)); - - { - auto const raw = computeRawLoanState( - state.periodicPayment, - periodicRate, - state.paymentRemaining, - broker.params.managementFeeRate); - auto const rounded = constructLoanState( - state.totalValue, - state.principalOutstanding, - state.managementFeeOutstanding); - testcase - << currencyLabel - << " Loan starting state: " << state.paymentRemaining - << ", " << raw.interestDue << ", " - << raw.principalOutstanding << ", " - << raw.managementFeeDue << ", " - << rounded.valueOutstanding << ", " - << rounded.principalOutstanding << ", " - << rounded.interestDue << ", " - << rounded.managementFeeDue; - } - - // Try to pay a little extra to show that it's _not_ - // taken - STAmount const transactionAmount = - STAmount{broker.asset, totalDue} + broker.asset(10); - // Only check the first payment since the rounding - // may drift as payments are made - BEAST_EXPECT( - transactionAmount == - roundToScale( - broker.asset( - Number(9533457001162141, -14), Number::upward), - state.loanScale, - Number::upward)); - - auto const initialState = state; - detail::PaymentComponents totalPaid{ - .trackedValueDelta = 0, - .trackedPrincipalDelta = 0, - .trackedManagementFeeDelta = 0}; - Number totalInterestPaid = 0; - std::size_t totalPaymentsMade = 0; - - ripple::LoanState currentTrueState = computeRawLoanState( - state.periodicPayment, - periodicRate, - state.paymentRemaining, - broker.params.managementFeeRate); - - while (state.paymentRemaining > 0) - { - // Compute the expected principal amount - auto const paymentComponents = - detail::computePaymentComponents( - broker.asset.raw(), - state.loanScale, - state.totalValue, - state.principalOutstanding, - state.managementFeeOutstanding, - state.periodicPayment, - periodicRate, - state.paymentRemaining, - broker.params.managementFeeRate); - - BEAST_EXPECT( - paymentComponents.trackedValueDelta <= - roundedPeriodicPayment); - - ripple::LoanState const nextTrueState = computeRawLoanState( - state.periodicPayment, - periodicRate, - state.paymentRemaining - 1, - broker.params.managementFeeRate); - detail::LoanStateDeltas const deltas = - currentTrueState - nextTrueState; - - testcase - << currencyLabel - << " Payment components: " << state.paymentRemaining - << ", " << deltas.interest << ", " << deltas.principal - << ", " << deltas.managementFee << ", " - << paymentComponents.trackedValueDelta << ", " - << paymentComponents.trackedPrincipalDelta << ", " - << paymentComponents.trackedInterestPart() << ", " - << paymentComponents.trackedManagementFeeDelta << ", " - << (paymentComponents.specialCase == - detail::PaymentSpecialCase::final - ? "final" - : paymentComponents.specialCase == - detail::PaymentSpecialCase::extra - ? "extra" - : "none"); - - auto const totalDueAmount = STAmount{ - broker.asset, - paymentComponents.trackedValueDelta + - serviceFee.number()}; - - // Due to the rounding algorithms to keep the interest and - // principal in sync with "true" values, the computed amount - // may be a little less than the rounded fixed payment - // amount. For integral types, the difference should be < 3 - // (1 unit for each of the interest and management fee). For - // IOUs, the difference should be after the 8th digit. - Number const diff = totalDue - totalDueAmount; - BEAST_EXPECT( - paymentComponents.specialCase == - detail::PaymentSpecialCase::final || - diff == beast::zero || - (diff > beast::zero && - ((broker.asset.integral() && - (static_cast(diff) < 3)) || - (state.loanScale - diff.exponent() > 13)))); - - BEAST_EXPECT( - paymentComponents.trackedValueDelta == - paymentComponents.trackedPrincipalDelta + - paymentComponents.trackedInterestPart() + - paymentComponents.trackedManagementFeeDelta); - BEAST_EXPECT( - paymentComponents.trackedValueDelta <= - roundedPeriodicPayment); - - BEAST_EXPECT( - state.paymentRemaining < 12 || - roundToAsset( - broker.asset, - deltas.principal, - state.loanScale, - Number::upward) == - roundToScale( - broker.asset( - Number(8333228695260180, -14), - Number::upward), - state.loanScale, - Number::upward)); - BEAST_EXPECT( - paymentComponents.trackedPrincipalDelta >= - beast::zero && - paymentComponents.trackedPrincipalDelta <= - state.principalOutstanding); - BEAST_EXPECT( - paymentComponents.specialCase != - detail::PaymentSpecialCase::final || - paymentComponents.trackedPrincipalDelta == - state.principalOutstanding); - BEAST_EXPECT( - paymentComponents.specialCase == - detail::PaymentSpecialCase::final || - (state.periodicPayment.exponent() - - (deltas.principal + deltas.interest + - deltas.managementFee - state.periodicPayment) - .exponent()) > 14); - - auto const borrowerBalanceBeforePayment = - env.balance(borrower, broker.asset); - - if (canImpairLoan(env, broker, state)) - // Making a payment will unimpair the loan - env(manage(lender, loanKeylet.key, tfLoanImpair)); - - env.close(); - - // Make the payment - env(pay(borrower, loanKeylet.key, transactionAmount)); - - env.close(); - - // Need to account for fees if the loan is in XRP - PrettyAmount adjustment = broker.asset(0); - if (broker.asset.native()) - { - adjustment = env.current()->fees().base; - } - - // Check the result - verifyLoanStatus.checkPayment( - state.loanScale, - borrower, - borrowerBalanceBeforePayment, - totalDueAmount, - adjustment); - - --state.paymentRemaining; - state.previousPaymentDate = state.nextPaymentDate; - if (paymentComponents.specialCase == - detail::PaymentSpecialCase::final) - { - state.paymentRemaining = 0; - state.nextPaymentDate = 0; - } - else - { - state.nextPaymentDate += state.paymentInterval; - } - state.principalOutstanding -= - paymentComponents.trackedPrincipalDelta; - state.managementFeeOutstanding -= - paymentComponents.trackedManagementFeeDelta; - state.totalValue -= paymentComponents.trackedValueDelta; - - verifyLoanStatus(state); - - totalPaid.trackedValueDelta += - paymentComponents.trackedValueDelta; - totalPaid.trackedPrincipalDelta += - paymentComponents.trackedPrincipalDelta; - totalPaid.trackedManagementFeeDelta += - paymentComponents.trackedManagementFeeDelta; - totalInterestPaid += - paymentComponents.trackedInterestPart(); - ++totalPaymentsMade; - - currentTrueState = nextTrueState; - } - - // Loan is paid off - BEAST_EXPECT(state.paymentRemaining == 0); - BEAST_EXPECT(state.principalOutstanding == 0); - - // Make sure all the payments add up - BEAST_EXPECT( - totalPaid.trackedValueDelta == initialState.totalValue); - BEAST_EXPECT( - totalPaid.trackedPrincipalDelta == - initialState.principalOutstanding); - BEAST_EXPECT( - totalPaid.trackedManagementFeeDelta == - initialState.managementFeeOutstanding); - // This is almost a tautology given the previous checks, but - // check it anyway for completeness. - BEAST_EXPECT( - totalInterestPaid == - initialState.totalValue - - (initialState.principalOutstanding + - initialState.managementFeeOutstanding)); - BEAST_EXPECT( - totalPaymentsMade == initialState.paymentRemaining); - - // Can't impair or default a paid off loan - env(manage(lender, loanKeylet.key, tfLoanImpair), - ter(tecNO_PERMISSION)); - env(manage(lender, loanKeylet.key, tfLoanDefault), - ter(tecNO_PERMISSION)); - }); - -#if LOANTODO - // TODO - - /* - LoanPay fails with tecINVARIANT_FAILED error when loan_broker(also - borrower) tries to do the payment. Here's the sceanrio: Create a XRP - loan with loan broker as borrower, loan origination fee and loan service - fee. Loan broker makes the first payment with periodic payment and loan - service fee. - */ - - auto time = [&](std::string label, std::function timed) { - if (!BEAST_EXPECT(timed)) - return; - - using clock_type = std::chrono::steady_clock; - using duration_type = std::chrono::milliseconds; - - auto const start = clock_type::now(); - timed(); - auto const duration = std::chrono::duration_cast( - clock_type::now() - start); - - log << label << " took " << duration.count() << "ms" << std::endl; - - return duration; - }; - - lifecycle( - caseLabel, - "timing", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - [&](Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus) { - // Estimate optimal values for loanPaymentsPerFeeIncrement and - // loanMaximumPaymentsPerTransaction. - using namespace loan; - - auto const state = - getCurrentState(env, broker, verifyLoanStatus.keylet); - auto const serviceFee = broker.asset(2).value(); - - STAmount const totalDue{ - broker.asset, - roundPeriodicPayment( - broker.asset, - state.periodicPayment + serviceFee, - state.loanScale)}; - - // Make a single payment - time("single payment", [&]() { - env(pay(borrower, loanKeylet.key, totalDue)); - }); - env.close(); - - // Make all but the final payment - auto const numPayments = (state.paymentRemaining - 2); - STAmount const bigPayment{broker.asset, totalDue * numPayments}; - XRPAmount const bigFee{ - baseFee * (numPayments / loanPaymentsPerFeeIncrement + 1)}; - time("ten payments", [&]() { - env(pay(borrower, loanKeylet.key, bigPayment), fee(bigFee)); - }); - env.close(); - - time("final payment", [&]() { - // Make the final payment - env( - pay(borrower, - loanKeylet.key, - totalDue + STAmount{broker.asset, 1})); - }); - env.close(); - }); - - lifecycle( - caseLabel, - "Loan overpayment allowed - Explicit overpayment", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - [&](Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus) { throw 0; }); - - lifecycle( - caseLabel, - "Loan overpayment prohibited - Late payment", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - [&](Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus) { throw 0; }); - - lifecycle( - caseLabel, - "Loan overpayment allowed - Late payment", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - [&](Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus) { throw 0; }); - - lifecycle( - caseLabel, - "Loan overpayment allowed - Late payment and overpayment", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - [&](Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus) { throw 0; }); - -#endif - } - - void - testLoanSet() - { - using namespace jtx; - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - struct CaseArgs - { - bool requireAuth = false; - bool authorizeBorrower = false; - int initialXRP = 1'000'000; - }; - - auto const testCase = - [&, this]( - std::function - mptTest, - std::function iouTest, - CaseArgs args = {}) { - Env env(*this, all); - env.fund(XRP(args.initialXRP), issuer, lender, borrower); - env.close(); - if (args.requireAuth) - { - env(fset(issuer, asfRequireAuth)); - env.close(); - } - - // We need two different asset types, MPT and IOU. Prepare MPT - // first - MPTTester mptt{env, issuer, mptInitNoFund}; - - auto const none = LedgerSpecificFlags(0); - mptt.create( - {.flags = tfMPTCanTransfer | tfMPTCanLock | - (args.requireAuth ? tfMPTRequireAuth : none)}); - env.close(); - PrettyAsset mptAsset = mptt.issuanceID(); - mptt.authorize({.account = lender}); - mptt.authorize({.account = borrower}); - env.close(); - if (args.requireAuth) - { - mptt.authorize({.account = issuer, .holder = lender}); - if (args.authorizeBorrower) - mptt.authorize({.account = issuer, .holder = borrower}); - env.close(); - } - - env(pay(issuer, lender, mptAsset(10'000'000))); - env.close(); - - // Prepare IOU - PrettyAsset const iouAsset = issuer[iouCurrency]; - env(trust(lender, iouAsset(10'000'000))); - env(trust(borrower, iouAsset(10'000'000))); - env.close(); - if (args.requireAuth) - { - env(trust(issuer, iouAsset(0), lender, tfSetfAuth)); - env(pay(issuer, lender, iouAsset(10'000'000))); - if (args.authorizeBorrower) - { - env(trust(issuer, iouAsset(0), borrower, tfSetfAuth)); - env(pay(issuer, borrower, iouAsset(10'000))); - } - } - else - { - env(pay(issuer, lender, iouAsset(10'000'000))); - env(pay(issuer, borrower, iouAsset(10'000))); - } - env.close(); - - // Create vaults and loan brokers - std::array const assets{mptAsset, iouAsset}; - std::vector brokers; - for (auto const& asset : assets) - { - brokers.emplace_back( - createVaultAndBroker(env, asset, lender)); - } - - if (mptTest) - (mptTest)(env, brokers[0], mptt); - if (iouTest) - (iouTest)(env, brokers[1]); - }; - - testCase( - [&, this](Env& env, BrokerInfo const& broker, auto&) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase("MPT issuer is borrower, issuer submits"); - env(set(issuer, broker.brokerID, principalRequest), - counterparty(lender), - sig(sfCounterpartySignature, lender), - fee(env.current()->fees().base * 5)); - - testcase("MPT issuer is borrower, lender submits"); - env(set(lender, broker.brokerID, principalRequest), - counterparty(issuer), - sig(sfCounterpartySignature, issuer), - fee(env.current()->fees().base * 5)); - }, - [&, this](Env& env, BrokerInfo const& broker) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase("IOU issuer is borrower, issuer submits"); - env(set(issuer, broker.brokerID, principalRequest), - counterparty(lender), - sig(sfCounterpartySignature, lender), - fee(env.current()->fees().base * 5)); - - testcase("IOU issuer is borrower, lender submits"); - env(set(lender, broker.brokerID, principalRequest), - counterparty(issuer), - sig(sfCounterpartySignature, issuer), - fee(env.current()->fees().base * 5)); - }, - CaseArgs{.requireAuth = true}); - - testCase( - [&, this](Env& env, BrokerInfo const& broker, auto&) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase("MPT unauthorized borrower, borrower submits"); - env(set(borrower, broker.brokerID, principalRequest), - counterparty(lender), - sig(sfCounterpartySignature, lender), - fee(env.current()->fees().base * 5), - ter{tecNO_AUTH}); - - testcase("MPT unauthorized borrower, lender submits"); - env(set(lender, broker.brokerID, principalRequest), - counterparty(borrower), - sig(sfCounterpartySignature, borrower), - fee(env.current()->fees().base * 5), - ter{tecNO_AUTH}); - }, - [&, this](Env& env, BrokerInfo const& broker) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase("IOU unauthorized borrower, borrower submits"); - env(set(borrower, broker.brokerID, principalRequest), - counterparty(lender), - sig(sfCounterpartySignature, lender), - fee(env.current()->fees().base * 5), - ter{tecNO_AUTH}); - - testcase("IOU unauthorized borrower, lender submits"); - env(set(lender, broker.brokerID, principalRequest), - counterparty(borrower), - sig(sfCounterpartySignature, borrower), - fee(env.current()->fees().base * 5), - ter{tecNO_AUTH}); - }, - CaseArgs{.requireAuth = true}); - - auto const [acctReserve, incReserve] = [this]() -> std::pair { - Env env{*this, testable_amendments()}; - return { - env.current()->fees().accountReserve(0).drops() / - DROPS_PER_XRP.drops(), - env.current()->fees().increment.drops() / - DROPS_PER_XRP.drops()}; - }(); - - testCase( - [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase( - "MPT authorized borrower, borrower submits, borrower has " - "no reserve"); - mptt.authorize( - {.account = borrower, .flags = tfMPTUnauthorize}); - env.close(); - - auto const mptoken = - keylet::mptoken(mptt.issuanceID(), borrower); - auto const sleMPT1 = env.le(mptoken); - BEAST_EXPECT(sleMPT1 == nullptr); - - // Burn some XRP - env(noop(borrower), fee(XRP(acctReserve * 2 + incReserve * 2))); - env.close(); - - // Cannot create loan, not enough reserve to create MPToken - env(set(borrower, broker.brokerID, principalRequest), - counterparty(lender), - sig(sfCounterpartySignature, lender), - fee(env.current()->fees().base * 5), - ter{tecINSUFFICIENT_RESERVE}); - env.close(); - - // Can create loan now, will implicitly create MPToken - env(pay(issuer, borrower, XRP(incReserve))); - env.close(); - env(set(borrower, broker.brokerID, principalRequest), - counterparty(lender), - sig(sfCounterpartySignature, lender), - fee(env.current()->fees().base * 5)); - env.close(); - - auto const sleMPT2 = env.le(mptoken); - BEAST_EXPECT(sleMPT2 != nullptr); - }, - {}, - CaseArgs{.initialXRP = acctReserve * 2 + incReserve * 8 + 1}); - - testCase( - {}, - [&, this](Env& env, BrokerInfo const& broker) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase( - "IOU authorized borrower, borrower submits, borrower has " - "no reserve"); - // Remove trust line from borrower to issuer - env.trust(broker.asset(0), borrower); - env.close(); - - env(pay(borrower, issuer, broker.asset(10'000))); - env.close(); - auto const trustline = - keylet::line(borrower, broker.asset.raw().get()); - auto const sleLine1 = env.le(trustline); - BEAST_EXPECT(sleLine1 == nullptr); - - // Burn some XRP - env(noop(borrower), fee(XRP(acctReserve * 2 + incReserve * 2))); - env.close(); - - // Cannot create loan, not enough reserve to create trust line - env(set(borrower, broker.brokerID, principalRequest), - counterparty(lender), - sig(sfCounterpartySignature, lender), - fee(env.current()->fees().base * 5), - ter{tecNO_LINE_INSUF_RESERVE}); - env.close(); - - // Can create loan now, will implicitly create trust line - env(pay(issuer, borrower, XRP(incReserve))); - env.close(); - env(set(borrower, broker.brokerID, principalRequest), - counterparty(lender), - sig(sfCounterpartySignature, lender), - fee(env.current()->fees().base * 5)); - env.close(); - - auto const sleLine2 = env.le(trustline); - BEAST_EXPECT(sleLine2 != nullptr); - }, - CaseArgs{.initialXRP = acctReserve * 2 + incReserve * 8 + 1}); - - testCase( - [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase( - "MPT authorized borrower, borrower submits, lender has " - "no reserve"); - auto const mptoken = keylet::mptoken(mptt.issuanceID(), lender); - auto const sleMPT1 = env.le(mptoken); - BEAST_EXPECT(sleMPT1 != nullptr); - - env(pay( - lender, issuer, broker.asset(sleMPT1->at(sfMPTAmount)))); - env.close(); - - mptt.authorize({.account = lender, .flags = tfMPTUnauthorize}); - env.close(); - - auto const sleMPT2 = env.le(mptoken); - BEAST_EXPECT(sleMPT2 == nullptr); - - // Burn some XRP - env(noop(lender), fee(XRP(incReserve))); - env.close(); - - // Cannot create loan, not enough reserve to create MPToken - env(set(borrower, broker.brokerID, principalRequest), - loanOriginationFee(broker.asset(1).value()), - counterparty(lender), - sig(sfCounterpartySignature, lender), - fee(env.current()->fees().base * 5), - ter{tecINSUFFICIENT_RESERVE}); - env.close(); - - // Can create loan now, will implicitly create MPToken - env(pay(issuer, lender, XRP(incReserve))); - env.close(); - env(set(borrower, broker.brokerID, principalRequest), - loanOriginationFee(broker.asset(1).value()), - counterparty(lender), - sig(sfCounterpartySignature, lender), - fee(env.current()->fees().base * 5)); - env.close(); - - auto const sleMPT3 = env.le(mptoken); - BEAST_EXPECT(sleMPT3 != nullptr); - }, - {}, - CaseArgs{.initialXRP = acctReserve * 2 + incReserve * 8 + 1}); - - testCase( - {}, - [&, this](Env& env, BrokerInfo const& broker) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase( - "IOU authorized borrower, borrower submits, lender has no " - "reserve"); - // Remove trust line from lender to issuer - env.trust(broker.asset(0), lender); - env.close(); - - auto const trustline = - keylet::line(lender, broker.asset.raw().get()); - auto const sleLine1 = env.le(trustline); - BEAST_EXPECT(sleLine1 != nullptr); - - env( - pay(lender, - issuer, - broker.asset(abs(sleLine1->at(sfBalance).value())))); - env.close(); - auto const sleLine2 = env.le(trustline); - BEAST_EXPECT(sleLine2 == nullptr); - - // Burn some XRP - env(noop(lender), fee(XRP(incReserve))); - env.close(); - - // Cannot create loan, not enough reserve to create trust line - env(set(borrower, broker.brokerID, principalRequest), - loanOriginationFee(broker.asset(1).value()), - counterparty(lender), - sig(sfCounterpartySignature, lender), - fee(env.current()->fees().base * 5), - ter{tecNO_LINE_INSUF_RESERVE}); - env.close(); - - // Can create loan now, will implicitly create trust line - env(pay(issuer, lender, XRP(incReserve))); - env.close(); - env(set(borrower, broker.brokerID, principalRequest), - loanOriginationFee(broker.asset(1).value()), - counterparty(lender), - sig(sfCounterpartySignature, lender), - fee(env.current()->fees().base * 5)); - env.close(); - - auto const sleLine3 = env.le(trustline); - BEAST_EXPECT(sleLine3 != nullptr); - }, - CaseArgs{.initialXRP = acctReserve * 2 + incReserve * 8 + 1}); - - testCase( - [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase("MPT authorized borrower, unauthorized lender"); - auto const mptoken = keylet::mptoken(mptt.issuanceID(), lender); - auto const sleMPT1 = env.le(mptoken); - BEAST_EXPECT(sleMPT1 != nullptr); - - env(pay( - lender, issuer, broker.asset(sleMPT1->at(sfMPTAmount)))); - env.close(); - - mptt.authorize({.account = lender, .flags = tfMPTUnauthorize}); - env.close(); - - auto const sleMPT2 = env.le(mptoken); - BEAST_EXPECT(sleMPT2 == nullptr); - - // Cannot create loan, lender not authorized to receive fee - env(set(borrower, broker.brokerID, principalRequest), - loanOriginationFee(broker.asset(1).value()), - counterparty(lender), - sig(sfCounterpartySignature, lender), - fee(env.current()->fees().base * 5), - ter{tecNO_AUTH}); - env.close(); - - // Can create loan without origination fee - env(set(borrower, broker.brokerID, principalRequest), - counterparty(lender), - sig(sfCounterpartySignature, lender), - fee(env.current()->fees().base * 5)); - env.close(); - - // No MPToken for lender - no authorization and no payment - auto const sleMPT3 = env.le(mptoken); - BEAST_EXPECT(sleMPT3 == nullptr); - }, - {}, - CaseArgs{.requireAuth = true, .authorizeBorrower = true}); - - testCase( - [&, this](Env& env, BrokerInfo const& broker, auto&) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase("MPT authorized borrower, borrower submits"); - env(set(borrower, broker.brokerID, principalRequest), - counterparty(lender), - sig(sfCounterpartySignature, lender), - fee(env.current()->fees().base * 5)); - }, - [&, this](Env& env, BrokerInfo const& broker) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase("IOU authorized borrower, borrower submits"); - env(set(borrower, broker.brokerID, principalRequest), - counterparty(lender), - sig(sfCounterpartySignature, lender), - fee(env.current()->fees().base * 5)); - }, - CaseArgs{.requireAuth = true, .authorizeBorrower = true}); - - testCase( - [&, this](Env& env, BrokerInfo const& broker, auto&) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase("MPT authorized borrower, lender submits"); - env(set(lender, broker.brokerID, principalRequest), - counterparty(borrower), - sig(sfCounterpartySignature, borrower), - fee(env.current()->fees().base * 5)); - }, - [&, this](Env& env, BrokerInfo const& broker) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase("IOU authorized borrower, lender submits"); - env(set(lender, broker.brokerID, principalRequest), - counterparty(borrower), - sig(sfCounterpartySignature, borrower), - fee(env.current()->fees().base * 5)); - }, - CaseArgs{.requireAuth = true, .authorizeBorrower = true}); - - jtx::Account const alice{"alice"}; - jtx::Account const bella{"bella"}; - auto const msigSetup = [&](Env& env, Account const& account) { - Json::Value tx1 = signers(account, 2, {{alice, 1}, {bella, 1}}); - env(tx1); - env.close(); - }; - - testCase( - [&, this](Env& env, BrokerInfo const& broker, auto&) { - using namespace loan; - msigSetup(env, lender); - Number const principalRequest = broker.asset(1'000).value(); - - testcase( - "MPT authorized borrower, borrower submits, lender " - "multisign"); - env(set(borrower, broker.brokerID, principalRequest), - counterparty(lender), - msig(sfCounterpartySignature, alice, bella), - fee(env.current()->fees().base * 5)); - }, - [&, this](Env& env, BrokerInfo const& broker) { - using namespace loan; - msigSetup(env, lender); - Number const principalRequest = broker.asset(1'000).value(); - - testcase( - "IOU authorized borrower, borrower submits, lender " - "multisign"); - env(set(borrower, broker.brokerID, principalRequest), - counterparty(lender), - msig(sfCounterpartySignature, alice, bella), - fee(env.current()->fees().base * 5)); - }, - CaseArgs{.requireAuth = true, .authorizeBorrower = true}); - - testCase( - [&, this](Env& env, BrokerInfo const& broker, auto&) { - using namespace loan; - msigSetup(env, borrower); - Number const principalRequest = broker.asset(1'000).value(); - - testcase( - "MPT authorized borrower, lender submits, borrower " - "multisign"); - env(set(lender, broker.brokerID, principalRequest), - counterparty(borrower), - msig(sfCounterpartySignature, alice, bella), - fee(env.current()->fees().base * 5)); - }, - [&, this](Env& env, BrokerInfo const& broker) { - using namespace loan; - msigSetup(env, borrower); - Number const principalRequest = broker.asset(1'000).value(); - - testcase( - "IOU authorized borrower, lender submits, borrower " - "multisign"); - env(set(lender, broker.brokerID, principalRequest), - counterparty(borrower), - msig(sfCounterpartySignature, alice, bella), - fee(env.current()->fees().base * 5)); - }, - CaseArgs{.requireAuth = true, .authorizeBorrower = true}); - } - - void - testLifecycle() - { - testcase("Lifecycle"); - using namespace jtx; - - // Create 3 loan brokers: one for XRP, one for an IOU, and one for - // an MPT. That'll require three corresponding SAVs. - Env env(*this, all); - - Account const issuer{"issuer"}; - // For simplicity, lender will be the sole actor for the vault & - // brokers. - Account const lender{"lender"}; - // Borrower only wants to borrow - Account const borrower{"borrower"}; - // Evan will attempt to be naughty - Account const evan{"evan"}; - // Do not fund alice - Account const alice{"alice"}; - - // Fund the accounts and trust lines with the same amount so that - // tests can use the same values regardless of the asset. - env.fund(XRP(100'000'000), issuer, noripple(lender, borrower, evan)); - env.close(); - - // Create assets - PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; - PrettyAsset const iouAsset = issuer[iouCurrency]; - env(trust(lender, iouAsset(10'000'000))); - env(trust(borrower, iouAsset(10'000'000))); - env(trust(evan, iouAsset(10'000'000))); - env(pay(issuer, evan, iouAsset(1'000'000))); - env(pay(issuer, lender, iouAsset(10'000'000))); - // Fund the borrower with enough to cover interest and fees - env(pay(issuer, borrower, iouAsset(10'000))); - env.close(); - - MPTTester mptt{env, issuer, mptInitNoFund}; - mptt.create( - {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); - // Scale the MPT asset a little bit so we can get some interest - PrettyAsset const mptAsset{mptt.issuanceID(), 100}; - mptt.authorize({.account = lender}); - mptt.authorize({.account = borrower}); - mptt.authorize({.account = evan}); - env(pay(issuer, lender, mptAsset(10'000'000))); - env(pay(issuer, evan, mptAsset(1'000'000))); - // Fund the borrower with enough to cover interest and fees - env(pay(issuer, borrower, mptAsset(10'000))); - env.close(); - - std::array const assets{xrpAsset, mptAsset, iouAsset}; - - // Create vaults and loan brokers - std::vector brokers; - for (auto const& asset : assets) - { - brokers.emplace_back(createVaultAndBroker( - env, - asset, - lender, - BrokerParameters{.data = "spam spam spam spam"})); - } - - // Create and update Loans - for (auto const& broker : brokers) - { - for (int amountExponent = 3; amountExponent >= 3; --amountExponent) - { - Number const loanAmount{1, amountExponent}; - for (int interestExponent = 0; interestExponent >= 0; - --interestExponent) - { - testCaseWrapper( - env, - mptt, - assets, - broker, - loanAmount, - interestExponent); - } - } - - if (auto brokerSle = env.le(keylet::loanbroker(broker.brokerID)); - BEAST_EXPECT(brokerSle)) - { - BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); - BEAST_EXPECT(brokerSle->at(sfDebtTotal) == 0); - - auto const coverAvailable = brokerSle->at(sfCoverAvailable); - env(loanBroker::coverWithdraw( - lender, - broker.brokerID, - STAmount(broker.asset, coverAvailable))); - env.close(); - - brokerSle = env.le(keylet::loanbroker(broker.brokerID)); - BEAST_EXPECT(brokerSle && brokerSle->at(sfCoverAvailable) == 0); - } - // Verify we can delete the loan broker - env(loanBroker::del(lender, broker.brokerID)); - env.close(); - } - } - - void - testSelfLoan() - { - testcase << "Self Loan"; - - using namespace jtx; - using namespace std::chrono_literals; - // Create 3 loan brokers: one for XRP, one for an IOU, and one for - // an MPT. That'll require three corresponding SAVs. - Env env(*this, all); - - Account const issuer{"issuer"}; - // For simplicity, lender will be the sole actor for the vault & - // brokers. - Account const lender{"lender"}; - - // Fund the accounts and trust lines with the same amount so that - // tests can use the same values regardless of the asset. - env.fund(XRP(100'000'000), issuer, noripple(lender)); - env.close(); - - // Use an XRP asset for simplicity - PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; - - // Create vaults and loan brokers - BrokerInfo broker{createVaultAndBroker(env, xrpAsset, lender)}; - - using namespace loan; - - auto const loanSetFee = fee(env.current()->fees().base * 2); - Number const principalRequest{1, 3}; - - // The LoanSet json can be created without a counterparty signature, - // but it will not pass preflight - auto createJson = env.json( - set(lender, - broker.brokerID, - broker.asset(principalRequest).value()), - fee(loanSetFee)); - env(createJson, ter(temBAD_SIGNER)); - - // Adding an empty counterparty signature object also fails, but - // at the RPC level. - createJson = env.json( - createJson, json(sfCounterpartySignature, Json::objectValue)); - env(createJson, ter(telENV_RPC_FAILED)); - - if (auto const jt = env.jt(createJson); BEAST_EXPECT(jt.stx)) - { - Serializer s; - jt.stx->add(s); - auto const jr = env.rpc("submit", strHex(s.slice())); - - BEAST_EXPECT(jr.isMember(jss::result)); - auto const jResult = jr[jss::result]; - BEAST_EXPECT(jResult[jss::error] == "invalidTransaction"); - BEAST_EXPECT( - jResult[jss::error_exception] == - "fails local checks: Transaction has bad signature."); - } - - // Copy the transaction signature into the counterparty signature. - Json::Value counterpartyJson{Json::objectValue}; - counterpartyJson[sfTxnSignature] = createJson[sfTxnSignature]; - counterpartyJson[sfSigningPubKey] = createJson[sfSigningPubKey]; - if (!BEAST_EXPECT(!createJson.isMember(jss::Signers))) - counterpartyJson[sfSigners] = createJson[sfSigners]; - - // The duplicated signature works - createJson = env.json( - createJson, json(sfCounterpartySignature, counterpartyJson)); - env(createJson); - - env.close(); - - auto const startDate = env.current()->info().parentCloseTime; - - // Loan is successfully created - { - auto const res = env.rpc("account_objects", lender.human()); - auto const objects = res[jss::result][jss::account_objects]; - - std::map types; - BEAST_EXPECT(objects.size() == 4); - for (auto const& object : objects) - { - ++types[object[sfLedgerEntryType].asString()]; - } - BEAST_EXPECT(types.size() == 4); - for (std::string const type : - {"MPToken", "Vault", "LoanBroker", "Loan"}) - { - BEAST_EXPECT(types[type] == 1); - } - } - auto const loanID = [&]() { - Json::Value params(Json::objectValue); - params[jss::account] = lender.human(); - params[jss::type] = "Loan"; - auto const res = - env.rpc("json", "account_objects", to_string(params)); - auto const objects = res[jss::result][jss::account_objects]; - - BEAST_EXPECT(objects.size() == 1); - - auto const loan = objects[0u]; - BEAST_EXPECT(loan[sfBorrower] == lender.human()); - // soeDEFAULT fields are not returned if they're in the default - // state - BEAST_EXPECT(!loan.isMember(sfCloseInterestRate)); - BEAST_EXPECT(!loan.isMember(sfClosePaymentFee)); - BEAST_EXPECT(loan[sfFlags] == 0); - BEAST_EXPECT(loan[sfGracePeriod] == 60); - BEAST_EXPECT(!loan.isMember(sfInterestRate)); - BEAST_EXPECT(!loan.isMember(sfLateInterestRate)); - BEAST_EXPECT(!loan.isMember(sfLatePaymentFee)); - BEAST_EXPECT(loan[sfLoanBrokerID] == to_string(broker.brokerID)); - BEAST_EXPECT(!loan.isMember(sfLoanOriginationFee)); - BEAST_EXPECT(loan[sfLoanSequence] == 1); - BEAST_EXPECT(!loan.isMember(sfLoanServiceFee)); - BEAST_EXPECT( - loan[sfNextPaymentDueDate] == loan[sfStartDate].asUInt() + 60); - BEAST_EXPECT(!loan.isMember(sfOverpaymentFee)); - BEAST_EXPECT(!loan.isMember(sfOverpaymentInterestRate)); - BEAST_EXPECT(loan[sfPaymentInterval] == 60); - BEAST_EXPECT(loan[sfPeriodicPayment] == "1000000000"); - BEAST_EXPECT(loan[sfPaymentRemaining] == 1); - BEAST_EXPECT(!loan.isMember(sfPreviousPaymentDate)); - BEAST_EXPECT(loan[sfPrincipalOutstanding] == "1000000000"); - BEAST_EXPECT(loan[sfTotalValueOutstanding] == "1000000000"); - BEAST_EXPECT(!loan.isMember(sfLoanScale)); - BEAST_EXPECT( - loan[sfStartDate].asUInt() == - startDate.time_since_epoch().count()); - - return loan["index"].asString(); - }(); - auto const loanKeylet{keylet::loan(uint256{std::string_view(loanID)})}; - - env.close(startDate); - - // Make a payment - env(pay(lender, loanKeylet.key, broker.asset(1000))); - } - - void - testBatchBypassCounterparty() - { - // From FIND-001 - testcase << "Batch Bypass Counterparty"; - - bool const lendingBatchEnabled = !std::any_of( - Batch::disabledTxTypes.begin(), - Batch::disabledTxTypes.end(), - [](auto const& disabled) { return disabled == ttLOAN_BROKER_SET; }); - - using namespace jtx; - using namespace std::chrono_literals; - Env env(*this, all); - - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - BrokerParameters brokerParams; - env.fund(XRP(brokerParams.vaultDeposit * 100), lender, borrower); - env.close(); - - PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; - - BrokerInfo broker{ - createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; - - using namespace loan; - - auto const loanSetFee = fee(env.current()->fees().base * 2); - Number const principalRequest{1, 3}; - - auto forgedLoanSet = - set(borrower, broker.brokerID, principalRequest, 0); - - Json::Value randomData{Json::objectValue}; - randomData[jss::SigningPubKey] = Json::StaticString{"2600"}; - Json::Value sigObject{Json::objectValue}; - sigObject[jss::SigningPubKey] = strHex(lender.pk().slice()); - Serializer ss; - ss.add32(HashPrefix::txSign); - parse(randomData).addWithoutSigningFields(ss); - auto const sig = ripple::sign(borrower.pk(), borrower.sk(), ss.slice()); - sigObject[jss::TxnSignature] = strHex(Slice{sig.data(), sig.size()}); - - forgedLoanSet[Json::StaticString{"CounterpartySignature"}] = sigObject; - - // ? Fails because the lender hasn't signed the tx - env(env.json(forgedLoanSet, fee(loanSetFee)), ter(telENV_RPC_FAILED)); - - auto const seq = env.seq(borrower); - auto const batchFee = batch::calcBatchFee(env, 1, 2); - // ! Should fail because the lender hasn't signed the tx - env(batch::outer(borrower, seq, batchFee, tfAllOrNothing), - batch::inner(forgedLoanSet, seq + 1), - batch::inner(pay(borrower, lender, XRP(1)), seq + 2), - ter(lendingBatchEnabled ? temBAD_SIGNATURE - : temINVALID_INNER_BATCH)); - env.close(); - - // ? Check that the loan was NOT created - { - Json::Value params(Json::objectValue); - params[jss::account] = borrower.human(); - params[jss::type] = "Loan"; - auto const res = - env.rpc("json", "account_objects", to_string(params)); - auto const objects = res[jss::result][jss::account_objects]; - BEAST_EXPECT(objects.size() == 0); - } - } - - void - testWrongMaxDebtBehavior() - { - // From FIND-003 - testcase << "Wrong Max Debt Behavior"; - - using namespace jtx; - using namespace std::chrono_literals; - Env env(*this, all); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - - BrokerParameters brokerParams{.debtMax = 0}; - env.fund( - XRP(brokerParams.vaultDeposit * 100), issuer, noripple(lender)); - env.close(); - - PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; - - BrokerInfo broker{ - createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; - - if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); - BEAST_EXPECT(brokerSle)) - { - BEAST_EXPECT(brokerSle->at(sfDebtMaximum) == 0); - } - - using namespace loan; - - auto const loanSetFee = fee(env.current()->fees().base * 2); - Number const principalRequest{1, 3}; - - auto createJson = env.json( - set(lender, broker.brokerID, principalRequest), fee(loanSetFee)); - - Json::Value counterpartyJson{Json::objectValue}; - counterpartyJson[sfTxnSignature] = createJson[sfTxnSignature]; - counterpartyJson[sfSigningPubKey] = createJson[sfSigningPubKey]; - if (!BEAST_EXPECT(!createJson.isMember(jss::Signers))) - counterpartyJson[sfSigners] = createJson[sfSigners]; - - createJson = env.json( - createJson, json(sfCounterpartySignature, counterpartyJson)); - env(createJson); - - env.close(); - } - - void - testLoanPayComputePeriodicPaymentValidRateInvariant() - { - // From FIND-012 - testcase << "LoanPay ripple::detail::computePeriodicPayment : " - "valid rate"; - - using namespace jtx; - using namespace std::chrono_literals; - Env env(*this, all); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - BrokerParameters brokerParams; - env.fund( - XRP(brokerParams.vaultDeposit * 100), issuer, lender, borrower); - env.close(); - - PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; - BrokerInfo broker{ - createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; - - using namespace loan; - - auto const loanSetFee = fee(env.current()->fees().base * 2); - Number const principalRequest{640562, -5}; - - Number const serviceFee{2462611968}; - std::uint32_t const numPayments{4294967295 / 800}; - - auto createJson = env.json( - set(borrower, broker.brokerID, principalRequest), - fee(loanSetFee), - loanServiceFee(serviceFee), - paymentTotal(numPayments), - json(sfCounterpartySignature, Json::objectValue)); - - createJson["CloseInterestRate"] = 55374; - createJson["ClosePaymentFee"] = "3825205248"; - createJson["GracePeriod"] = 0; - createJson["LatePaymentFee"] = "237"; - createJson["LoanOriginationFee"] = "0"; - createJson["OverpaymentFee"] = 35167; - createJson["OverpaymentInterestRate"] = 1360; - createJson["PaymentInterval"] = 727; - - auto const brokerStateBefore = - env.le(keylet::loanbroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - - createJson = env.json(createJson, sig(sfCounterpartySignature, lender)); - // Fails in preclaim because principal requested can't be - // represented as XRP - env(createJson, ter(tecPRECISION_LOSS)); - env.close(); - - BEAST_EXPECT(!env.le(keylet)); - - Number const actualPrincipal{6}; - - createJson[sfPrincipalRequested] = actualPrincipal; - createJson.removeMember(sfSequence.jsonName); - createJson = env.json(createJson, sig(sfCounterpartySignature, lender)); - // Fails in doApply because the payment is too small to be - // represented as XRP. - env(createJson, ter(tecPRECISION_LOSS)); - env.close(); - } - - void - testRPC() - { - // This will expand as more test cases are added. Some functionality - // is tested in other test functions. - testcase("RPC"); - - using namespace jtx; - - Env env(*this, all); - - auto lowerFee = [&]() { - // Run the local fee back down. - while (env.app().getFeeTrack().lowerLocalFee()) - ; - }; - - auto const baseFee = env.current()->fees().base; - - Account const alice{"alice"}; - std::string const borrowerPass = "borrower"; - std::string const borrowerSeed = "ssBRAsLpH4778sLNYC4ik1JBJsBVf"; - Account borrower{borrowerPass, KeyType::ed25519}; - auto const lenderPass = "lender"; - std::string const lenderSeed = "shPTCZGwTEhJrYT8NbcNkeaa8pzPM"; - Account lender{lenderPass, KeyType::ed25519}; - - env.fund(XRP(1'000'000), alice, lender, borrower); - env.close(); - env(noop(lender)); - env(noop(lender)); - env(noop(lender)); - env(noop(lender)); - env(noop(lender)); - env.close(); - - { - testcase("RPC AccountSet"); - Json::Value txJson{Json::objectValue}; - txJson[sfTransactionType] = "AccountSet"; - txJson[sfAccount] = borrower.human(); - - auto const signParams = [&]() { - Json::Value signParams{Json::objectValue}; - signParams[jss::passphrase] = borrowerPass; - signParams[jss::key_type] = "ed25519"; - signParams[jss::tx_json] = txJson; - return signParams; - }(); - auto const jSign = env.rpc("json", "sign", to_string(signParams)); - BEAST_EXPECT( - jSign.isMember(jss::result) && - jSign[jss::result].isMember(jss::tx_json)); - auto txSignResult = jSign[jss::result][jss::tx_json]; - auto txSignBlob = jSign[jss::result][jss::tx_blob].asString(); - txSignResult.removeMember(jss::hash); - - auto const jtx = env.jt(txJson, sig(borrower)); - BEAST_EXPECT(txSignResult == jtx.jv); - - lowerFee(); - auto const jSubmit = env.rpc("submit", txSignBlob); - BEAST_EXPECT( - jSubmit.isMember(jss::result) && - jSubmit[jss::result].isMember(jss::engine_result) && - jSubmit[jss::result][jss::engine_result].asString() == - "tesSUCCESS"); - - lowerFee(); - env(jtx.jv, sig(none), seq(none), fee(none), ter(tefPAST_SEQ)); - } - - { - testcase("RPC LoanSet - illegal signature_target"); - - Json::Value txJson{Json::objectValue}; - txJson[sfTransactionType] = "AccountSet"; - txJson[sfAccount] = borrower.human(); - - auto const borrowerSignParams = [&]() { - Json::Value params{Json::objectValue}; - params[jss::passphrase] = borrowerPass; - params[jss::key_type] = "ed25519"; - params[jss::signature_target] = "Destination"; - params[jss::tx_json] = txJson; - return params; - }(); - auto const jSignBorrower = - env.rpc("json", "sign", to_string(borrowerSignParams)); - BEAST_EXPECT( - jSignBorrower.isMember(jss::result) && - jSignBorrower[jss::result].isMember(jss::error) && - jSignBorrower[jss::result][jss::error] == "invalidParams" && - jSignBorrower[jss::result].isMember(jss::error_message) && - jSignBorrower[jss::result][jss::error_message] == - "Destination"); - } - { - testcase("RPC LoanSet - sign and submit borrower initiated"); - // 1. Borrower creates the transaction - Json::Value txJson{Json::objectValue}; - txJson[sfTransactionType] = "LoanSet"; - txJson[sfAccount] = borrower.human(); - txJson[sfCounterparty] = lender.human(); - txJson[sfLoanBrokerID] = - "FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FEC" - "F83F" - "5C"; - txJson[sfPrincipalRequested] = "100000000"; - txJson[sfPaymentTotal] = 10000; - txJson[sfPaymentInterval] = 3600; - txJson[sfGracePeriod] = 300; - txJson[sfFlags] = 65536; // tfLoanOverpayment - txJson[sfFee] = to_string(24 * baseFee / 10); - - // 2. Borrower signs the transaction - auto const borrowerSignParams = [&]() { - Json::Value params{Json::objectValue}; - params[jss::passphrase] = borrowerPass; - params[jss::key_type] = "ed25519"; - params[jss::tx_json] = txJson; - return params; - }(); - auto const jSignBorrower = - env.rpc("json", "sign", to_string(borrowerSignParams)); - BEAST_EXPECTS( - jSignBorrower.isMember(jss::result) && - jSignBorrower[jss::result].isMember(jss::tx_json), - to_string(jSignBorrower)); - auto const txBorrowerSignResult = - jSignBorrower[jss::result][jss::tx_json]; - auto const txBorrowerSignBlob = - jSignBorrower[jss::result][jss::tx_blob].asString(); - - // 2a. Borrower attempts to submit the transaction. It doesn't - // work - { - lowerFee(); - auto const jSubmitBlob = env.rpc("submit", txBorrowerSignBlob); - BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); - auto const jSubmitBlobResult = jSubmitBlob[jss::result]; - BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json)); - // Transaction fails because the CounterpartySignature is - // missing - BEAST_EXPECT( - jSubmitBlobResult.isMember(jss::engine_result) && - jSubmitBlobResult[jss::engine_result].asString() == - "temBAD_SIGNER"); - } - - // 3. Borrower sends the signed transaction to the lender - // 4. Lender signs the transaction - auto const lenderSignParams = [&]() { - Json::Value params{Json::objectValue}; - params[jss::passphrase] = lenderPass; - params[jss::key_type] = "ed25519"; - params[jss::signature_target] = "CounterpartySignature"; - params[jss::tx_json] = txBorrowerSignResult; - return params; - }(); - auto const jSignLender = - env.rpc("json", "sign", to_string(lenderSignParams)); - BEAST_EXPECT( - jSignLender.isMember(jss::result) && - jSignLender[jss::result].isMember(jss::tx_json)); - auto const txLenderSignResult = - jSignLender[jss::result][jss::tx_json]; - auto const txLenderSignBlob = - jSignLender[jss::result][jss::tx_blob].asString(); - - // 5. Lender submits the signed transaction blob - lowerFee(); - auto const jSubmitBlob = env.rpc("submit", txLenderSignBlob); - BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); - auto const jSubmitBlobResult = jSubmitBlob[jss::result]; - BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json)); - auto const jSubmitBlobTx = jSubmitBlobResult[jss::tx_json]; - // To get far enough to return tecNO_ENTRY means that the - // signatures all validated. Of course the transaction won't - // succeed because no Vault or Broker were created. - BEAST_EXPECTS( - jSubmitBlobResult.isMember(jss::engine_result) && - jSubmitBlobResult[jss::engine_result].asString() == - "tecNO_ENTRY", - to_string(jSubmitBlobResult)); - - BEAST_EXPECT( - !jSubmitBlob.isMember(jss::error) && - !jSubmitBlobResult.isMember(jss::error)); - - // 4-alt. Lender submits the transaction json originally - // received from the Borrower. It gets signed, but is now a - // duplicate, so fails. Borrower could done this instead of - // steps 4 and 5. - lowerFee(); - auto const jSubmitJson = - env.rpc("json", "submit", to_string(lenderSignParams)); - BEAST_EXPECT(jSubmitJson.isMember(jss::result)); - auto const jSubmitJsonResult = jSubmitJson[jss::result]; - BEAST_EXPECT(jSubmitJsonResult.isMember(jss::tx_json)); - auto const jSubmitJsonTx = jSubmitJsonResult[jss::tx_json]; - // Since the previous tx claimed a fee, this duplicate is not - // going anywhere - BEAST_EXPECTS( - jSubmitJsonResult.isMember(jss::engine_result) && - jSubmitJsonResult[jss::engine_result].asString() == - "tefPAST_SEQ", - to_string(jSubmitJsonResult)); - - BEAST_EXPECT( - !jSubmitJson.isMember(jss::error) && - !jSubmitJsonResult.isMember(jss::error)); - - BEAST_EXPECT(jSubmitBlobTx == jSubmitJsonTx); - } - - { - testcase("RPC LoanSet - sign and submit lender initiated"); - // 1. Lender creates the transaction - Json::Value txJson{Json::objectValue}; - txJson[sfTransactionType] = "LoanSet"; - txJson[sfAccount] = lender.human(); - txJson[sfCounterparty] = borrower.human(); - txJson[sfLoanBrokerID] = - "FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FEC" - "F83F" - "5C"; - txJson[sfPrincipalRequested] = "100000000"; - txJson[sfPaymentTotal] = 10000; - txJson[sfPaymentInterval] = 3600; - txJson[sfGracePeriod] = 300; - txJson[sfFlags] = 65536; // tfLoanOverpayment - txJson[sfFee] = to_string(24 * baseFee / 10); - - // 2. Lender signs the transaction - auto const lenderSignParams = [&]() { - Json::Value params{Json::objectValue}; - params[jss::passphrase] = lenderPass; - params[jss::key_type] = "ed25519"; - params[jss::tx_json] = txJson; - return params; - }(); - auto const jSignLender = - env.rpc("json", "sign", to_string(lenderSignParams)); - BEAST_EXPECT( - jSignLender.isMember(jss::result) && - jSignLender[jss::result].isMember(jss::tx_json)); - auto const txLenderSignResult = - jSignLender[jss::result][jss::tx_json]; - auto const txLenderSignBlob = - jSignLender[jss::result][jss::tx_blob].asString(); - - // 2a. Lender attempts to submit the transaction. It doesn't - // work - { - lowerFee(); - auto const jSubmitBlob = env.rpc("submit", txLenderSignBlob); - BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); - auto const jSubmitBlobResult = jSubmitBlob[jss::result]; - BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json)); - // Transaction fails because the CounterpartySignature is - // missing - BEAST_EXPECT( - jSubmitBlobResult.isMember(jss::engine_result) && - jSubmitBlobResult[jss::engine_result].asString() == - "temBAD_SIGNER"); - } - - // 3. Lender sends the signed transaction to the Borrower - // 4. Borrower signs the transaction - auto const borrowerSignParams = [&]() { - Json::Value params{Json::objectValue}; - params[jss::passphrase] = borrowerPass; - params[jss::key_type] = "ed25519"; - params[jss::signature_target] = "CounterpartySignature"; - params[jss::tx_json] = txLenderSignResult; - return params; - }(); - auto const jSignBorrower = - env.rpc("json", "sign", to_string(borrowerSignParams)); - BEAST_EXPECT( - jSignBorrower.isMember(jss::result) && - jSignBorrower[jss::result].isMember(jss::tx_json)); - auto const txBorrowerSignResult = - jSignBorrower[jss::result][jss::tx_json]; - auto const txBorrowerSignBlob = - jSignBorrower[jss::result][jss::tx_blob].asString(); - - // 5. Borrower submits the signed transaction blob - lowerFee(); - auto const jSubmitBlob = env.rpc("submit", txBorrowerSignBlob); - BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); - auto const jSubmitBlobResult = jSubmitBlob[jss::result]; - BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json)); - auto const jSubmitBlobTx = jSubmitBlobResult[jss::tx_json]; - // To get far enough to return tecNO_ENTRY means that the - // signatures all validated. Of course the transaction won't - // succeed because no Vault or Broker were created. - BEAST_EXPECTS( - jSubmitBlobResult.isMember(jss::engine_result) && - jSubmitBlobResult[jss::engine_result].asString() == - "tecNO_ENTRY", - to_string(jSubmitBlobResult)); - - BEAST_EXPECT( - !jSubmitBlob.isMember(jss::error) && - !jSubmitBlobResult.isMember(jss::error)); - - // 4-alt. Borrower submits the transaction json originally - // received from the Lender. It gets signed, but is now a - // duplicate, so fails. Lender could done this instead of steps - // 4 and 5. - lowerFee(); - auto const jSubmitJson = - env.rpc("json", "submit", to_string(borrowerSignParams)); - BEAST_EXPECT(jSubmitJson.isMember(jss::result)); - auto const jSubmitJsonResult = jSubmitJson[jss::result]; - BEAST_EXPECT(jSubmitJsonResult.isMember(jss::tx_json)); - auto const jSubmitJsonTx = jSubmitJsonResult[jss::tx_json]; - // Since the previous tx claimed a fee, this duplicate is not - // going anywhere - BEAST_EXPECTS( - jSubmitJsonResult.isMember(jss::engine_result) && - jSubmitJsonResult[jss::engine_result].asString() == - "tefPAST_SEQ", - to_string(jSubmitJsonResult)); - - BEAST_EXPECT( - !jSubmitJson.isMember(jss::error) && - !jSubmitJsonResult.isMember(jss::error)); - - BEAST_EXPECT(jSubmitBlobTx == jSubmitJsonTx); - } - } - - void - testServiceFeeOnBrokerDeepFreeze() - { - testcase << "Service Fee On Broker Deep Freeze"; - using namespace jtx; - using namespace loan; - Account const issuer("issuer"); - Account const borrower("borrower"); - Account const broker("broker"); - auto const IOU = issuer["IOU"]; - - for (bool const deepFreeze : {true, false}) - { - Env env(*this); - - auto getCoverBalance = [&](BrokerInfo const& brokerInfo, - auto const& accountField) { - if (auto const le = - env.le(keylet::loanbroker(brokerInfo.brokerID)); - BEAST_EXPECT(le)) - { - auto const account = le->at(accountField); - if (auto const sleLine = env.le(keylet::line(account, IOU)); - BEAST_EXPECT(sleLine)) - { - STAmount balance = sleLine->at(sfBalance); - if (account > issuer.id()) - balance.negate(); - return balance; - } - } - return STAmount{IOU}; - }; - - env.fund(XRP(20'000), issuer, broker, borrower); - env.close(); - - env(trust(broker, IOU(20'000'000))); - env(pay(issuer, broker, IOU(10'000'000))); - env.close(); - - auto const brokerInfo = createVaultAndBroker(env, IOU, broker); - - BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == IOU(1'000)); - - auto const keylet = keylet::loan(brokerInfo.brokerID, 1); - - env(set(borrower, brokerInfo.brokerID, 10'000), - sig(sfCounterpartySignature, broker), - loanServiceFee(IOU(100).value()), - paymentInterval(100), - fee(XRP(100))); - env.close(); - - env(trust(borrower, IOU(20'000'000))); - // The borrower increases their limit and acquires some IOU so - // they can pay interest - env(pay(issuer, borrower, IOU(500))); - env.close(); - - if (auto const le = env.le(keylet::loan(keylet.key)); - BEAST_EXPECT(le)) - { - if (deepFreeze) - { - env(trust( - issuer, - broker["IOU"](0), - tfSetFreeze | tfSetDeepFreeze)); - env.close(); - } - - env(pay(borrower, keylet.key, IOU(10'100)), fee(XRP(100))); - env.close(); - - if (deepFreeze) - { - // The fee goes to the broker pseudo-account - BEAST_EXPECT( - getCoverBalance(brokerInfo, sfAccount) == IOU(1'100)); - BEAST_EXPECT( - getCoverBalance(brokerInfo, sfOwner) == IOU(8'999'000)); - } - else - { - // The fee goes to the broker account - BEAST_EXPECT( - getCoverBalance(brokerInfo, sfOwner) == IOU(8'999'100)); - BEAST_EXPECT( - getCoverBalance(brokerInfo, sfAccount) == IOU(1'000)); - } - } - }; - } - - void - testBasicMath() - { - // Test the functions defined in LendingHelpers.h - testcase("Basic Math"); - - pass(); - } - - void - testIssuerLoan() - { - testcase << "Issuer Loan"; - - using namespace jtx; - using namespace loan; - Account const issuer("issuer"); - Account const borrower = issuer; - Account const lender("lender"); - Env env(*this); - - env.fund(XRP(1'000), issuer, lender); - - std::int64_t constexpr issuerBalance = 10'000'000; - MPTTester asset( - {.env = env, - .issuer = issuer, - .holders = {lender}, - .pay = issuerBalance}); - - BrokerParameters const brokerParams{ - .debtMax = 200, - }; - auto const broker = - createVaultAndBroker(env, asset, lender, brokerParams); - auto const loanSetFee = fee(env.current()->fees().base * 2); - // Create Loan - env(set(borrower, broker.brokerID, 200), - sig(sfCounterpartySignature, lender), - loanSetFee); - env.close(); - // Issuer should not create MPToken - BEAST_EXPECT(!env.le(keylet::mptoken(asset.issuanceID(), issuer))); - // Issuer "borrowed" 200, OutstandingAmount decreased by 200 - BEAST_EXPECT(env.balance(issuer, asset) == asset(-issuerBalance + 200)); - // Pay Loan - auto const loanKeylet = keylet::loan(broker.brokerID, 1); - env(pay(borrower, loanKeylet.key, asset(200))); - env.close(); - // Issuer "re-payed" 200, OutstandingAmount increased by 200 - BEAST_EXPECT(env.balance(issuer, asset) == asset(-issuerBalance)); - } - - void - testInvalidLoanDelete() - { - testcase("Invalid LoanDelete"); - using namespace jtx; - using namespace loan; - - // preflight: temINVALID, LoanID == zero - { - Account const alice{"alice"}; - Env env(*this); - env.fund(XRP(1'000), alice); - env.close(); - env(del(alice, beast::zero), ter(temINVALID)); - } - } - - void - testInvalidLoanManage() - { - testcase("Invalid LoanManage"); - using namespace jtx; - using namespace loan; - - // preflight: temINVALID, LoanID == zero - { - Account const alice{"alice"}; - Env env(*this); - env.fund(XRP(1'000), alice); - env.close(); - env(manage(alice, beast::zero, tfLoanDefault), ter(temINVALID)); - } - } - - void - testInvalidLoanPay() - { - testcase("Invalid LoanPay"); - using namespace jtx; - using namespace loan; - Account const lender{"lender"}; - Account const issuer{"issuer"}; - Account const borrower{"borrower"}; - auto const IOU = issuer["IOU"]; - - // preclaim - Env env(*this); - env.fund(XRP(1'000), lender, issuer, borrower); - env(trust(lender, IOU(10'000'000)), THISLINE); - env(pay(issuer, lender, IOU(5'000'000)), THISLINE); - BrokerInfo brokerInfo{createVaultAndBroker(env, issuer["IOU"], lender)}; - - auto const loanSetFee = fee(env.current()->fees().base * 2); - STAmount const debtMaximumRequest = brokerInfo.asset(1'000).value(); - - env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), - sig(sfCounterpartySignature, lender), - loanSetFee, - THISLINE); - - env.close(); - - std::uint32_t const loanSequence = 1; - auto const loanKeylet = keylet::loan(brokerInfo.brokerID, loanSequence); - - env(fset(issuer, asfGlobalFreeze), THISLINE); - env.close(); - - // preclaim: tecFROZEN - env(pay(borrower, loanKeylet.key, debtMaximumRequest), - ter(tecFROZEN), - THISLINE); - env.close(); - - env(fclear(issuer, asfGlobalFreeze), THISLINE); - env.close(); - - auto const pseudoBroker = [&]() -> std::optional { - if (auto brokerSle = - env.le(keylet::loanbroker(brokerInfo.brokerID)); - BEAST_EXPECT(brokerSle)) - { - return Account{"pseudo", brokerSle->at(sfAccount)}; - } - else - { - return std::nullopt; - } - }(); - if (!pseudoBroker) - return; - - // Lender and pseudoaccount must both be frozen - env(trust( - issuer, - lender["IOU"](1'000), - lender, - tfSetFreeze | tfSetDeepFreeze), - THISLINE); - env(trust( - issuer, - (*pseudoBroker)["IOU"](1'000), - *pseudoBroker, - tfSetFreeze | tfSetDeepFreeze), - THISLINE); - env.close(); - - // preclaim: tecFROZEN due to deep frozen - env(pay(borrower, loanKeylet.key, debtMaximumRequest), - ter(tecFROZEN), - THISLINE); - env.close(); - - // Only one needs to be unfrozen - env(trust( - issuer, - lender["IOU"](1'000), - tfClearFreeze | tfClearDeepFreeze), - THISLINE); - env.close(); - - // The payment is late by this point - env(pay(borrower, loanKeylet.key, debtMaximumRequest), - ter(tecEXPIRED), - THISLINE); - env.close(); - env(pay(borrower, - loanKeylet.key, - debtMaximumRequest, - tfLoanLatePayment), - THISLINE); - env.close(); - - // preclaim: tecKILLED - // note that tecKILLED in loanMakePayment() - // doesn't happen because of the preclaim check. - env(pay(borrower, loanKeylet.key, debtMaximumRequest), - ter(tecKILLED), - THISLINE); - } - - void - testInvalidLoanSet() - { - testcase("Invalid LoanSet"); - using namespace jtx; - using namespace loan; - Account const lender{"lender"}; - Account const issuer{"issuer"}; - Account const borrower{"borrower"}; - auto const IOU = issuer["IOU"]; - - auto testWrapper = [&](auto&& test) { - Env env(*this); - env.fund(XRP(1'000), lender, issuer, borrower); - env(trust(lender, IOU(10'000'000))); - env(pay(issuer, lender, IOU(5'000'000))); - BrokerInfo brokerInfo{ - createVaultAndBroker(env, issuer["IOU"], lender)}; - - auto const loanSetFee = fee(env.current()->fees().base * 2); - Number const debtMaximumRequest = brokerInfo.asset(1'000).value(); - test(env, brokerInfo, loanSetFee, debtMaximumRequest); - }; - - // preflight: - testWrapper([&](Env& env, - BrokerInfo const& brokerInfo, - jtx::fee const& loanSetFee, - Number const& debtMaximumRequest) { - // first temBAD_SIGNER: TODO - - // empty/zero broker ID - { - auto jv = set(borrower, uint256{}, debtMaximumRequest); - - auto testZeroBrokerID = [&](std::string const& id, - std::uint32_t flags = 0) { - // empty broker ID - jv[sfLoanBrokerID] = id; - env(jv, - sig(sfCounterpartySignature, lender), - loanSetFee, - txflags(flags), - ter(temINVALID)); - }; - // empty broker ID - testZeroBrokerID(std::string("")); - // zero broker ID - // needs a flag to distinguish the parsed STTx from the prior - // test - testZeroBrokerID(to_string(uint256{}), tfFullyCanonicalSig); - } - - // preflightCheckSigningKey() failure: - // can it happen? the signature is checked before transactor - // executes - - JTx tx = env.jt( - set(borrower, brokerInfo.brokerID, debtMaximumRequest), - sig(sfCounterpartySignature, lender), - loanSetFee); - STTx local = *(tx.stx); - auto counterpartySig = - local.getFieldObject(sfCounterpartySignature); - auto badPubKey = counterpartySig.getFieldVL(sfSigningPubKey); - badPubKey[20] ^= 0xAA; - counterpartySig.setFieldVL(sfSigningPubKey, badPubKey); - local.setFieldObject(sfCounterpartySignature, counterpartySig); - Json::Value jvResult; - jvResult[jss::tx_blob] = strHex(local.getSerializer().slice()); - auto res = env.rpc("json", "submit", to_string(jvResult))["result"]; - BEAST_EXPECT( - res[jss::error] == "invalidTransaction" && - res[jss::error_exception] == - "fails local checks: Counterparty: Invalid signature."); - }); - - // preclaim: - testWrapper([&](Env& env, - BrokerInfo const& brokerInfo, - jtx::fee const& loanSetFee, - Number const& debtMaximumRequest) { - // canAddHoldingFailure (IOU only, if MPT doesn't have - // MPTCanTransfer set, then can't create Vault/LoanBroker, - // and LoanSet will fail with different error - env(fclear(issuer, asfDefaultRipple)); - env.close(); - env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), - sig(sfCounterpartySignature, lender), - loanSetFee, - ter(terNO_RIPPLE)); - }); - - // doApply: - testWrapper([&](Env& env, - BrokerInfo const& brokerInfo, - jtx::fee const& loanSetFee, - Number const& debtMaximumRequest) { - auto const amt = env.balance(borrower) - - env.current()->fees().accountReserve(env.ownerCount(borrower)); - env(pay(borrower, issuer, amt)); - - // tecINSUFFICIENT_RESERVE - env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), - sig(sfCounterpartySignature, lender), - loanSetFee, - ter(tecINSUFFICIENT_RESERVE)); - - // addEmptyHolding failure - env(pay(issuer, borrower, amt)); - env(fset(issuer, asfGlobalFreeze)); - env.close(); - - env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), - sig(sfCounterpartySignature, lender), - loanSetFee, - ter(tecFROZEN)); - }); - } - - void - testAccountSendMptMinAmountInvariant() - { - // (From FIND-006) - testcase << "LoanSet trigger ripple::accountSendMPT : minimum amount " - "and MPT"; - - using namespace jtx; - using namespace std::chrono_literals; - Env env(*this, all); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - - MPTTester mptt{env, issuer, mptInitNoFund}; - mptt.create( - {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); - PrettyAsset const mptAsset = mptt.issuanceID(); - mptt.authorize({.account = lender}); - mptt.authorize({.account = borrower}); - env(pay(issuer, lender, mptAsset(2'000'000))); - env(pay(issuer, borrower, mptAsset(1'000))); - env.close(); - - BrokerInfo broker{createVaultAndBroker(env, mptAsset, lender)}; - - using namespace loan; - - auto const loanSetFee = fee(env.current()->fees().base * 2); - Number const principalRequest{1, 3}; - - auto createJson = env.json( - set(borrower, broker.brokerID, principalRequest), - fee(loanSetFee), - json(sfCounterpartySignature, Json::objectValue)); - - createJson["CloseInterestRate"] = 76671; - createJson["ClosePaymentFee"] = "2061925410"; - createJson["GracePeriod"] = 434; - createJson["InterestRate"] = 50302; - createJson["LateInterestRate"] = 30322; - createJson["LatePaymentFee"] = "294427911"; - createJson["LoanOriginationFee"] = "3250635102"; - createJson["LoanServiceFee"] = "9557386"; - createJson["OverpaymentFee"] = 51249; - createJson["OverpaymentInterestRate"] = 14304; - createJson["PaymentInterval"] = 434; - createJson["PaymentTotal"] = "2891743748"; - createJson["PrincipalRequested"] = "8516.98"; - - auto const brokerStateBefore = - env.le(keylet::loanbroker(broker.brokerID)); - - createJson = env.json(createJson, sig(sfCounterpartySignature, lender)); - env(createJson, ter(temINVALID)); - env.close(); - } - - void - testLoanPayDebtDecreaseInvariant() - { - // From FIND-007 - testcase << "LoanPay ripple::LoanPay::doApply : debtDecrease " - "rounding good"; - - using namespace jtx; - using namespace std::chrono_literals; - using namespace Lending; - Env env(*this, all); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - - PrettyAsset const iouAsset = issuer[iouCurrency]; - auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000))); - env(trustLenderTx); - auto trustBorrowerTx = - env.json(trust(borrower, iouAsset(1'000'000'000))); - env(trustBorrowerTx); - auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000)); - env(payLenderTx); - auto payIssuerTx = pay(issuer, borrower, iouAsset(1'000'000)); - env(payIssuerTx); - env.close(); - - BrokerInfo broker{createVaultAndBroker(env, iouAsset, lender)}; - - using namespace loan; - - auto const baseFee = env.current()->fees().base; - auto const loanSetFee = fee(baseFee * 2); - Number const principalRequest{1, 3}; - - auto createJson = env.json( - set(borrower, broker.brokerID, principalRequest), - fee(loanSetFee), - json(sfCounterpartySignature, Json::objectValue)); - - createJson["ClosePaymentFee"] = "0"; - createJson["GracePeriod"] = 60; - createJson["InterestRate"] = 24346; - createJson["LateInterestRate"] = 65535; - createJson["LatePaymentFee"] = "0"; - createJson["LoanOriginationFee"] = "218"; - createJson["LoanServiceFee"] = "0"; - createJson["PaymentInterval"] = 60; - createJson["PaymentTotal"] = 5678; - createJson["PrincipalRequested"] = "9924.81"; - - auto const brokerStateBefore = - env.le(keylet::loanbroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - - createJson = env.json(createJson, sig(sfCounterpartySignature, lender)); - env(createJson, ter(tesSUCCESS)); - env.close(); - - auto const pseudoAcct = [&]() { - auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - return lender; - auto const brokerPseudo = brokerSle->at(sfAccount); - return Account("Broker pseudo-account", brokerPseudo); - }(); - - VerifyLoanStatus verifyLoanStatus(env, broker, pseudoAcct, keylet); - auto const originalState = getCurrentState(env, broker, keylet); - verifyLoanStatus(originalState); - - Number const payment{3'269'349'176'470'588, -12}; - XRPAmount const payFee{ - baseFee * - ((payment / originalState.periodicPayment) / - loanPaymentsPerFeeIncrement + - 1)}; - auto loanPayTx = env.json( - pay(borrower, keylet.key, STAmount{broker.asset, payment}), - fee(payFee)); - BEAST_EXPECT(to_string(payment) == "3269.349176470588"); - env(loanPayTx, ter(tesSUCCESS)); - env.close(); - - auto const newState = getCurrentState(env, broker, keylet); - BEAST_EXPECT(isRounded( - broker.asset, - newState.managementFeeOutstanding, - originalState.loanScale)); - BEAST_EXPECT( - newState.managementFeeOutstanding < - originalState.managementFeeOutstanding); - BEAST_EXPECT(isRounded( - broker.asset, newState.totalValue, originalState.loanScale)); - BEAST_EXPECT(isRounded( - broker.asset, - newState.principalOutstanding, - originalState.loanScale)); - } - - void - testLoanPayComputePeriodicPaymentValidTotalInterestInvariant() - { - // From FIND-010 - testcase << "ripple::loanComputePaymentParts : valid total interest"; - - using namespace jtx; - using namespace std::chrono_literals; - Env env(*this, all); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - - PrettyAsset const iouAsset = issuer[iouCurrency]; - auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000))); - env(trustLenderTx); - auto trustBorrowerTx = - env.json(trust(borrower, iouAsset(1'000'000'000))); - env(trustBorrowerTx); - auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000)); - env(payLenderTx); - auto payIssuerTx = pay(issuer, borrower, iouAsset(1'000'000)); - env(payIssuerTx); - env.close(); - - BrokerInfo broker{createVaultAndBroker(env, iouAsset, lender)}; - - using namespace loan; - - auto const loanSetFee = fee(env.current()->fees().base * 2); - Number const principalRequest{1, 3}; - auto const startDate = env.now() + 60s; - - auto createJson = env.json( - set(borrower, broker.brokerID, principalRequest), - fee(loanSetFee), - json(sfCounterpartySignature, Json::objectValue)); - - createJson["CloseInterestRate"] = 47299; - createJson["ClosePaymentFee"] = "3985819770"; - createJson["GracePeriod"] = 0; - createJson["InterestRate"] = 92; - createJson["LatePaymentFee"] = "3866894865"; - createJson["LoanOriginationFee"] = "0"; - createJson["LoanServiceFee"] = "2348810240"; - createJson["OverpaymentFee"] = 58545; - createJson["PaymentInterval"] = 60; - createJson["PaymentTotal"] = 1; - createJson["PrincipalRequested"] = "0.000763058"; - - auto const brokerStateBefore = - env.le(keylet::loanbroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - - createJson = env.json(createJson, sig(sfCounterpartySignature, lender)); - env(createJson, ter(tecPRECISION_LOSS)); - env.close(startDate); - - auto loanPayTx = env.json( - pay(borrower, keylet.key, STAmount{broker.asset, Number{}})); - loanPayTx["Amount"]["value"] = "0.000281284125490196"; - env(loanPayTx, ter(tecNO_ENTRY)); - env.close(); - } - - void - testDosLoanPay() - { - // From FIND-005 - testcase << "DoS LoanPay"; - - using namespace jtx; - using namespace std::chrono_literals; - using namespace Lending; - Env env(*this, all); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - - PrettyAsset const iouAsset = issuer[iouCurrency]; - env(trust(lender, iouAsset(100'000'000))); - env(trust(borrower, iouAsset(100'000'000))); - env(pay(issuer, lender, iouAsset(10'000'000))); - env(pay(issuer, borrower, iouAsset(1'000))); - env.close(); - - BrokerInfo broker{createVaultAndBroker(env, iouAsset, lender)}; - - using namespace loan; - - auto const loanSetFee = fee(env.current()->fees().base * 2); - Number const principalRequest{1, 3}; - auto const baseFee = env.current()->fees().base; - - auto createJson = env.json( - set(borrower, broker.brokerID, principalRequest), - fee(loanSetFee), - json(sfCounterpartySignature, Json::objectValue)); - - createJson["ClosePaymentFee"] = "0"; - createJson["GracePeriod"] = 60; - createJson["InterestRate"] = 20930; - createJson["LateInterestRate"] = 77049; - createJson["LatePaymentFee"] = "0"; - createJson["LoanServiceFee"] = "0"; - createJson["OverpaymentFee"] = 7; - createJson["OverpaymentInterestRate"] = 66653; - createJson["PaymentInterval"] = 60; - createJson["PaymentTotal"] = 3239184; - createJson["PrincipalRequested"] = "3959.37"; - - auto const brokerStateBefore = - env.le(keylet::loanbroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - - createJson = env.json(createJson, sig(sfCounterpartySignature, lender)); - env(createJson, ter(tesSUCCESS)); - env.close(); - - auto const stateBefore = getCurrentState(env, broker, keylet); - BEAST_EXPECT(stateBefore.paymentRemaining == 3239184); - BEAST_EXPECT( - stateBefore.paymentRemaining > loanMaximumPaymentsPerTransaction); - - auto loanPayTx = env.json( - pay(borrower, keylet.key, STAmount{broker.asset, Number{}})); - Number const amount{395937, -2}; - loanPayTx["Amount"]["value"] = to_string(amount); - XRPAmount const payFee{ - baseFee * - std::int64_t( - amount / stateBefore.periodicPayment / - loanPaymentsPerFeeIncrement + - 1)}; - env(loanPayTx, ter(tesSUCCESS), fee(payFee)); - env.close(); - - auto const stateAfter = getCurrentState(env, broker, keylet); - BEAST_EXPECT( - stateAfter.paymentRemaining == - stateBefore.paymentRemaining - loanMaximumPaymentsPerTransaction); - } - - void - testLoanPayComputePeriodicPaymentValidTotalPrincipalPaidInvariant() - { - // From FIND-009 - testcase << "ripple::loanComputePaymentParts : totalPrincipalPaid " - "rounded"; - - using namespace jtx; - using namespace std::chrono_literals; - using namespace Lending; - Env env(*this, all); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - - PrettyAsset const iouAsset = issuer[iouCurrency]; - auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000))); - env(trustLenderTx); - auto trustBorrowerTx = - env.json(trust(borrower, iouAsset(1'000'000'000))); - env(trustBorrowerTx); - auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000)); - env(payLenderTx); - auto payIssuerTx = pay(issuer, borrower, iouAsset(1'000'000)); - env(payIssuerTx); - env.close(); - - BrokerInfo broker{createVaultAndBroker(env, iouAsset, lender)}; - - using namespace loan; - - auto const loanSetFee = fee(env.current()->fees().base * 2); - Number const principalRequest{1, 3}; - - auto createJson = env.json( - set(borrower, broker.brokerID, principalRequest), - fee(loanSetFee), - json(sfCounterpartySignature, Json::objectValue)); - - createJson["ClosePaymentFee"] = "0"; - createJson["GracePeriod"] = 0; - createJson["InterestRate"] = 24346; - createJson["LateInterestRate"] = 65535; - createJson["LatePaymentFee"] = "0"; - createJson["LoanOriginationFee"] = "218"; - createJson["LoanServiceFee"] = "0"; - createJson["PaymentInterval"] = 60; - createJson["PaymentTotal"] = 5678; - createJson["PrincipalRequested"] = "9924.81"; - - auto const brokerStateBefore = - env.le(keylet::loanbroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - - createJson = env.json(createJson, sig(sfCounterpartySignature, lender)); - env(createJson, ter(tesSUCCESS)); - env.close(); - - auto const baseFee = env.current()->fees().base; - - auto const stateBefore = getCurrentState(env, broker, keylet); - - { - auto loanPayTx = env.json( - pay(borrower, keylet.key, STAmount{broker.asset, Number{}})); - Number const amount{3074'745'058'823'529, -12}; - BEAST_EXPECT(to_string(amount) == "3074.745058823529"); - XRPAmount const payFee{ - baseFee * - (amount / stateBefore.periodicPayment / - loanPaymentsPerFeeIncrement + - 1)}; - loanPayTx["Amount"]["value"] = to_string(amount); - env(loanPayTx, fee(payFee), ter(tesSUCCESS)); - env.close(); - } - - { - auto loanPayTx = env.json( - pay(borrower, keylet.key, STAmount{broker.asset, Number{}})); - Number const amount{6732'118'170'944'051, -12}; - BEAST_EXPECT(to_string(amount) == "6732.118170944051"); - XRPAmount const payFee{ - baseFee * - (amount / stateBefore.periodicPayment / - loanPaymentsPerFeeIncrement + - 1)}; - loanPayTx["Amount"]["value"] = to_string(amount); - env(loanPayTx, fee(payFee), ter(tesSUCCESS)); - env.close(); - } - - auto const stateAfter = getCurrentState(env, broker, keylet); - // Total interest outstanding is non-negative - BEAST_EXPECT(stateAfter.totalValue >= stateAfter.principalOutstanding); - // Principal paid is non-negative - BEAST_EXPECT( - stateBefore.principalOutstanding >= - stateAfter.principalOutstanding); - // Total value change is non-negative - BEAST_EXPECT(stateBefore.totalValue >= stateAfter.totalValue); - // Value delta is larger or same as principal delta (meaning - // non-negative interest paid) - BEAST_EXPECT( - (stateBefore.totalValue - stateAfter.totalValue) >= - (stateBefore.principalOutstanding - - stateAfter.principalOutstanding)); - } - - void - testLoanPayComputePeriodicPaymentValidTotalInterestPaidInvariant() - { - // From FIND-008 - testcase << "ripple::loanComputePaymentParts : loanValueChange rounded"; - - using namespace jtx; - using namespace std::chrono_literals; - using namespace Lending; - Env env(*this, all); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - - PrettyAsset const iouAsset = issuer[iouCurrency]; - auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000))); - env(trustLenderTx); - auto trustBorrowerTx = - env.json(trust(borrower, iouAsset(1'000'000'000))); - env(trustBorrowerTx); - auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000)); - env(payLenderTx); - auto payIssuerTx = pay(issuer, borrower, iouAsset(10'000'000)); - env(payIssuerTx); - env.close(); - - BrokerInfo broker{createVaultAndBroker(env, iouAsset, lender)}; - { - auto const coverDepositValue = - broker.asset(broker.params.coverDeposit * 10).value(); - env(loanBroker::coverDeposit( - lender, broker.brokerID, coverDepositValue)); - env.close(); - } - - using namespace loan; - - auto const loanSetFee = fee(env.current()->fees().base * 2); - Number const principalRequest{1, 3}; - - auto createJson = env.json( - set(borrower, broker.brokerID, principalRequest), - fee(loanSetFee), - json(sfCounterpartySignature, Json::objectValue)); - - createJson["ClosePaymentFee"] = "0"; - createJson["GracePeriod"] = 0; - createJson["InterestRate"] = 12833; - createJson["LateInterestRate"] = 77048; - createJson["LatePaymentFee"] = "0"; - createJson["LoanOriginationFee"] = "218"; - createJson["LoanServiceFee"] = "0"; - createJson["PaymentInterval"] = 752; - createJson["PaymentTotal"] = 5678; - createJson["PrincipalRequested"] = "9924.81"; - - auto const brokerStateBefore = - env.le(keylet::loanbroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - - createJson = env.json(createJson, sig(sfCounterpartySignature, lender)); - env(createJson, ter(tesSUCCESS)); - env.close(); - - auto const baseFee = env.current()->fees().base; - - auto const stateBefore = getCurrentState(env, broker, keylet); - BEAST_EXPECT(stateBefore.paymentRemaining == 5678); - BEAST_EXPECT( - stateBefore.paymentRemaining > loanMaximumPaymentsPerTransaction); - - auto loanPayTx = env.json( - pay(borrower, keylet.key, STAmount{broker.asset, Number{}})); - Number const amount{9924'81, -2}; - BEAST_EXPECT(to_string(amount) == "9924.81"); - XRPAmount const payFee{ - baseFee * - (amount / stateBefore.periodicPayment / - loanPaymentsPerFeeIncrement + - 1)}; - loanPayTx["Amount"]["value"] = to_string(amount); - env(loanPayTx, fee(payFee), ter(tesSUCCESS)); - env.close(); - - auto const stateAfter = getCurrentState(env, broker, keylet); - BEAST_EXPECT( - stateAfter.paymentRemaining == - stateBefore.paymentRemaining - loanMaximumPaymentsPerTransaction); - } - - void - testLoanNextPaymentDueDateOverflow() - { - // For FIND-013 - testcase << "Prevent nextPaymentDueDate overflow"; - - using namespace jtx; - using namespace std::chrono_literals; - using namespace Lending; - Env env(*this, all); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - - PrettyAsset const iouAsset = issuer[iouCurrency]; - auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000))); - env(trustLenderTx); - auto trustBorrowerTx = - env.json(trust(borrower, iouAsset(1'000'000'000))); - env(trustBorrowerTx); - auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000)); - env(payLenderTx); - auto payIssuerTx = pay(issuer, borrower, iouAsset(10'000'000)); - env(payIssuerTx); - env.close(); - - BrokerParameters const brokerParams{ - .debtMax = Number{0}, .coverRateMin = TenthBips32{1}}; - BrokerInfo broker{ - createVaultAndBroker(env, iouAsset, lender, brokerParams)}; - - using namespace loan; - - auto const loanSetFee = fee(env.current()->fees().base * 2); - - using timeType = decltype(sfNextPaymentDueDate)::type::value_type; - static_assert(std::is_same_v); - timeType constexpr maxTime = std::numeric_limits::max(); - static_assert(maxTime == 4'294'967'295); - - auto const baseJson = [&]() { - auto createJson = env.json( - set(borrower, broker.brokerID, Number{55524'81, -2}), - fee(loanSetFee), - closePaymentFee(0), - gracePeriod(0), - interestRate(TenthBips32(12833)), - lateInterestRate(TenthBips32(77048)), - latePaymentFee(0), - loanOriginationFee(218), - json(sfCounterpartySignature, Json::objectValue)); - - createJson.removeMember(sfSequence.getJsonName()); - - return createJson; - }(); - - auto const baseFee = env.current()->fees().base; - - auto parentCloseTime = [&]() { - return env.current()->parentCloseTime().time_since_epoch().count(); - }; - auto maxLoanTime = [&]() { - auto const startDate = parentCloseTime(); - - BEAST_EXPECT(startDate >= 50); - - return maxTime - startDate; - }; - - { - // straight-up overflow: interval - auto const interval = maxLoanTime() + 1; - auto const total = 1; - auto createJson = env.json( - baseJson, paymentInterval(interval), paymentTotal(total)); - - env(createJson, - sig(sfCounterpartySignature, lender), - ter(tecKILLED)); - env.close(); - } - { - // straight-up overflow: total - // min interval is 60 - auto const interval = 60; - auto const total = maxLoanTime() + 1; - auto createJson = env.json( - baseJson, paymentInterval(interval), paymentTotal(total)); - - env(createJson, - sig(sfCounterpartySignature, lender), - ter(tecKILLED)); - env.close(); - } - { - // straight-up overflow: grace period - // min interval is 60 - auto const interval = maxLoanTime() + 1; - auto const total = 1; - auto const grace = interval; - auto createJson = env.json( - baseJson, - paymentInterval(interval), - paymentTotal(total), - gracePeriod(grace)); - - // The grace period can't be larger than the interval. - env(createJson, - sig(sfCounterpartySignature, lender), - ter(tecKILLED)); - env.close(); - } - { - // Overflow with multiplication of a few large intervals - auto const interval = 1'000'000'000; - auto const total = 10; - auto createJson = env.json( - baseJson, paymentInterval(interval), paymentTotal(total)); - - env(createJson, - sig(sfCounterpartySignature, lender), - ter(tecKILLED)); - env.close(); - } - { - // Overflow with multiplication of many small payments - // min interval is 60 - auto const interval = 60; - auto const total = 1'000'000'000; - auto createJson = env.json( - baseJson, paymentInterval(interval), paymentTotal(total)); - - env(createJson, - sig(sfCounterpartySignature, lender), - ter(tecKILLED)); - env.close(); - } - { - // Overflow with an absurdly large grace period - // min interval is 60 - auto const total = 60; - auto const interval = (maxLoanTime() - total) / total; - auto const grace = interval; - auto createJson = env.json( - baseJson, - paymentInterval(interval), - paymentTotal(total), - gracePeriod(grace)); - - env(createJson, - sig(sfCounterpartySignature, lender), - ter(tecKILLED)); - env.close(); - } - { - // Start date when the ledger is closed will be larger - auto const brokerStateBefore = - env.le(keylet::loanbroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - - auto const grace = 100; - auto const interval = maxLoanTime() - grace; - auto const total = 1; - auto createJson = env.json( - baseJson, - paymentInterval(interval), - paymentTotal(total), - gracePeriod(grace)); - - env(createJson, - sig(sfCounterpartySignature, lender), - ter(tesSUCCESS)); - env.close(); - - // The transaction is killed in the closed ledger - auto const meta = env.meta(); - if (BEAST_EXPECT(meta)) - { - BEAST_EXPECT(meta->at(sfTransactionResult) == tecKILLED); - } - - // If the transaction had succeeded, the loan would exist - auto const loanSle = env.le(keylet); - // but it doesn't - BEAST_EXPECT(!loanSle); - } - { - // Start date when the ledger is closed will be larger - auto const brokerStateBefore = - env.le(keylet::loanbroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - - auto const closeStartDate = (parentCloseTime() / 10 + 1) * 10; - auto const grace = 5'000; - auto const interval = maxTime - closeStartDate - grace; - auto const total = 1; - auto createJson = env.json( - baseJson, - paymentInterval(interval), - paymentTotal(total), - gracePeriod(grace)); - - env(createJson, - sig(sfCounterpartySignature, lender), - ter(tesSUCCESS)); - env.close(); - - // The transaction succeeds in the closed ledger - auto const meta = env.meta(); - if (BEAST_EXPECT(meta)) - { - BEAST_EXPECT(meta->at(sfTransactionResult) == tesSUCCESS); - } - - // This loan exists - auto const afterState = getCurrentState(env, broker, keylet); - BEAST_EXPECT(afterState.nextPaymentDate == maxTime - grace); - BEAST_EXPECT(afterState.previousPaymentDate == 0); - BEAST_EXPECT(afterState.paymentRemaining == 1); - } - - { - // Ensure the borrower has funds to pay back the loan - env(pay(issuer, borrower, iouAsset(Number{1'055'524'81, -2}))); - - // Start date when the ledger is closed will be larger - auto const closeStartDate = (parentCloseTime() / 10 + 1) * 10; - auto const grace = 5'000; - auto const maxLoanTime = maxTime - closeStartDate - grace; - auto const total = [&]() { - if (maxLoanTime % 5 == 0) - return 5; - if (maxLoanTime % 3 == 0) - return 3; - if (maxLoanTime % 2 == 0) - return 2; - return 0; - }(); - if (!BEAST_EXPECT(total != 0)) - return; - - auto const brokerState = - env.le(keylet::loanbroker(broker.brokerID)); - // Intentionally shadow the outer values - auto const loanSequence = brokerState->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - - auto const interval = maxLoanTime / total; - auto createJson = env.json( - baseJson, - paymentInterval(interval), - paymentTotal(total), - gracePeriod(grace)); - - env(createJson, - sig(sfCounterpartySignature, lender), - ter(tesSUCCESS)); - env.close(); - - // This loan exists - auto const beforeState = getCurrentState(env, broker, keylet); - BEAST_EXPECT( - beforeState.nextPaymentDate == closeStartDate + interval); - BEAST_EXPECT(beforeState.previousPaymentDate == 0); - BEAST_EXPECT(beforeState.paymentRemaining == total); - BEAST_EXPECT(beforeState.periodicPayment > 0); - - // pay all but the last payment - Number const payment = beforeState.periodicPayment * (total - 1); - XRPAmount const payFee{ - baseFee * ((total - 1) / loanPaymentsPerFeeIncrement + 1)}; - auto loanPayTx = env.json( - pay(borrower, keylet.key, STAmount{broker.asset, payment}), - fee(payFee)); - env(loanPayTx, ter(tesSUCCESS)); - env.close(); - - // The loan is on the last payment - auto const afterState = getCurrentState(env, broker, keylet); - BEAST_EXPECT(afterState.nextPaymentDate == maxTime - grace); - BEAST_EXPECT( - afterState.previousPaymentDate == maxTime - grace - interval); - BEAST_EXPECT(afterState.paymentRemaining == 1); - } - } - - void - testRequireAuth() - { - testcase("Require Auth - Implicit Pseudo-account authorization"); - using namespace jtx; - using namespace loan; - Account const lender{"lender"}; - Account const issuer{"issuer"}; - Account const borrower{"borrower"}; - Env env(*this); - - env.fund(XRP(100'000), issuer, lender, borrower); - env.close(); - - auto asset = MPTTester({ - .env = env, - .issuer = issuer, - .holders = {lender, borrower}, - .flags = MPTDEXFlags | tfMPTRequireAuth | tfMPTCanClawback | - tfMPTCanLock, - .authHolder = true, - }); - - env(pay(issuer, lender, asset(5'000'000))); - BrokerInfo brokerInfo{createVaultAndBroker(env, asset, lender)}; - - auto const loanSetFee = fee(env.current()->fees().base * 2); - STAmount const debtMaximumRequest = brokerInfo.asset(1'000).value(); - - auto forUnauthAuth = [&](auto&& doTx) { - for (auto const flag : {tfMPTUnauthorize, 0u}) - { - asset.authorize( - {.account = issuer, .holder = borrower, .flags = flag}); - env.close(); - doTx(flag == 0); - env.close(); - } - }; - - // Can't create a loan if the borrower is not authorized - forUnauthAuth([&](bool authorized) { - auto const err = !authorized ? ter(tecNO_AUTH) : ter(tesSUCCESS); - env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), - sig(sfCounterpartySignature, lender), - loanSetFee, - err); - }); - - std::uint32_t constexpr loanSequence = 1; - auto const loanKeylet = keylet::loan(brokerInfo.brokerID, loanSequence); - - // Can't loan pay if the borrower is not authorized - forUnauthAuth([&](bool authorized) { - auto const err = !authorized ? ter(tecNO_AUTH) : ter(tesSUCCESS); - env(pay(borrower, loanKeylet.key, debtMaximumRequest), err); - }); - } - - void - testCoverDepositWithdrawNonTransferableMPT() - { - testcase( - "CoverDeposit and CoverWithdraw reject MPT without CanTransfer"); - using namespace jtx; - using namespace loanBroker; - - Env env(*this, all); - - Account const issuer{"issuer"}; - Account const alice{"alice"}; - - env.fund(XRP(100'000), issuer, alice); - env.close(); - - MPTTester mpt{env, issuer, mptInitNoFund}; - - mpt.create( - {.flags = tfMPTCanTransfer, - .mutableFlags = tmfMPTCanMutateCanTransfer}); - - env.close(); - - PrettyAsset const asset = mpt["MPT"]; - mpt.authorize({.account = alice}); - env.close(); - - // Issuer can fund the holder even if CanTransfer is not set. - env(pay(issuer, alice, asset(100))); - env.close(); - - Vault vault{env}; - auto const [createTx, vaultKeylet] = - vault.create({.owner = alice, .asset = asset}); - env(createTx); - env.close(); - - auto const brokerKeylet = - keylet::loanbroker(alice.id(), env.seq(alice)); - env(set(alice, vaultKeylet.key)); - env.close(); - - auto const brokerSle = env.le(brokerKeylet); - if (!BEAST_EXPECT(brokerSle)) - return; - - Account const pseudoAccount{ - "Loan Broker pseudo-account", brokerSle->at(sfAccount)}; - - // Remove CanTransfer after the broker is set up. - mpt.set({.mutableFlags = tmfMPTClearCanTransfer}); - env.close(); - - // Standard Payment path should forbid third-party transfers. - env(pay(alice, pseudoAccount, asset(1)), ter(tecNO_AUTH)); - env.close(); - - // Cover cannot be transferred to broker account - auto const depositAmount = asset(1); - env(coverDeposit(alice, brokerKeylet.key, depositAmount), - ter{tecNO_AUTH}); - env.close(); - - if (auto const refreshed = env.le(brokerKeylet); - BEAST_EXPECT(refreshed)) - { - BEAST_EXPECT(refreshed->at(sfCoverAvailable) == 0); - env.require(balance(pseudoAccount, asset(0))); - } - - // Set CanTransfer again and transfer some deposit - mpt.set({.mutableFlags = tmfMPTSetCanTransfer}); - env.close(); - - env(coverDeposit(alice, brokerKeylet.key, depositAmount)); - env.close(); - - if (auto const refreshed = env.le(brokerKeylet); - BEAST_EXPECT(refreshed)) - { - BEAST_EXPECT(refreshed->at(sfCoverAvailable) == 1); - env.require(balance(pseudoAccount, depositAmount)); - } - - // Remove CanTransfer after the deposit - mpt.set({.mutableFlags = tmfMPTClearCanTransfer}); - env.close(); - - // Cover cannot be transferred from broker account - env(coverWithdraw(alice, brokerKeylet.key, depositAmount), - ter{tecNO_AUTH}); - env.close(); - - // Set CanTransfer again and withdraw - mpt.set({.mutableFlags = tmfMPTSetCanTransfer}); - env.close(); - - env(coverWithdraw(alice, brokerKeylet.key, depositAmount)); - env.close(); - - if (auto const refreshed = env.le(brokerKeylet); - BEAST_EXPECT(refreshed)) - { - BEAST_EXPECT(refreshed->at(sfCoverAvailable) == 0); - env.require(balance(pseudoAccount, asset(0))); - } - } - -#if LOANTODO - void - testLoanPayLateFullPaymentBypassesPenalties() - { - testcase("LoanPay full payment skips late penalties"); - using namespace jtx; - using namespace loan; - using namespace std::chrono_literals; - - Env env(*this, all); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - - PrettyAsset const asset = issuer[iouCurrency]; - env(trust(lender, asset(100'000'000))); - env(trust(borrower, asset(100'000'000))); - env(pay(issuer, lender, asset(50'000'000))); - env(pay(issuer, borrower, asset(5'000'000))); - env.close(); - - BrokerInfo broker{createVaultAndBroker(env, asset, lender)}; - - auto const loanSetFee = fee(env.current()->fees().base * 2); - - auto const brokerPreLoan = env.le(keylet::loanbroker(broker.brokerID)); - if (!BEAST_EXPECT(brokerPreLoan)) - return; - - auto const loanSequence = brokerPreLoan->at(sfLoanSequence); - auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); - - Number const principal = asset(1'000).value(); - Number const serviceFee = asset(2).value(); - Number const lateFee = asset(5).value(); - Number const closeFee = asset(4).value(); - - env(set(borrower, broker.brokerID, principal), - sig(sfCounterpartySignature, lender), - loanServiceFee(serviceFee), - latePaymentFee(lateFee), - closePaymentFee(closeFee), - interestRate(percentageToTenthBips(12)), - lateInterestRate(percentageToTenthBips(24) / 10), - closeInterestRate(percentageToTenthBips(5)), - paymentTotal(12), - paymentInterval(600), - gracePeriod(0), - fee(loanSetFee)); - env.close(); - - auto state1 = getCurrentState(env, broker, loanKeylet); - if (!BEAST_EXPECT(state1.paymentRemaining > 1)) - return; - - using d = NetClock::duration; - using tp = NetClock::time_point; - auto const overdueClose = - tp{d{state1.nextPaymentDate + state1.paymentInterval}}; - env.close(overdueClose); - - auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); - auto const loanSle = env.le(loanKeylet); - if (!BEAST_EXPECT(brokerSle && loanSle)) - return; - - auto state = getCurrentState(env, broker, loanKeylet); - - TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; - TenthBips32 const interestRateValue{loanSle->at(sfInterestRate)}; - TenthBips32 const lateInterestRateValue{ - loanSle->at(sfLateInterestRate)}; - TenthBips32 const closeInterestRateValue{ - loanSle->at(sfCloseInterestRate)}; - - Number const closePaymentFeeRounded = roundToAsset( - broker.asset, loanSle->at(sfClosePaymentFee), state.loanScale); - Number const latePaymentFeeRounded = roundToAsset( - broker.asset, loanSle->at(sfLatePaymentFee), state.loanScale); - - auto const roundedLoanState = constructLoanState( - state.totalValue, - state.principalOutstanding, - state.managementFeeOutstanding); - Number const totalInterestOutstanding = roundedLoanState.interestDue; - - auto const periodicRate = - loanPeriodicRate(interestRateValue, state.paymentInterval); - auto const rawLoanState = computeRawLoanState( - state.periodicPayment, - periodicRate, - state.paymentRemaining, - managementFeeRate); - - auto const parentCloseTime = env.current()->parentCloseTime(); - auto const startDateSeconds = static_cast( - state.startDate.time_since_epoch().count()); - - Number const fullPaymentInterest = computeFullPaymentInterest( - rawLoanState.principalOutstanding, - periodicRate, - parentCloseTime, - state.paymentInterval, - state.previousPaymentDate, - startDateSeconds, - closeInterestRateValue); - - Number const roundedFullInterestAmount = - roundToAsset(broker.asset, fullPaymentInterest, state.loanScale); - Number const roundedFullManagementFee = computeManagementFee( - broker.asset, - roundedFullInterestAmount, - managementFeeRate, - state.loanScale); - Number const roundedFullInterest = - roundedFullInterestAmount - roundedFullManagementFee; - - Number const trackedValueDelta = state.principalOutstanding + - totalInterestOutstanding + state.managementFeeOutstanding; - Number const untrackedManagementFee = closePaymentFeeRounded + - roundedFullManagementFee - state.managementFeeOutstanding; - Number const untrackedInterest = - roundedFullInterest - totalInterestOutstanding; - - Number const baseFullDue = - trackedValueDelta + untrackedInterest + untrackedManagementFee; - BEAST_EXPECT( - baseFullDue == - roundToAsset(broker.asset, baseFullDue, state.loanScale)); - - auto const overdueSeconds = - parentCloseTime.time_since_epoch().count() - state.nextPaymentDate; - if (!BEAST_EXPECT(overdueSeconds > 0)) - return; - - Number const overdueRate = - loanPeriodicRate(lateInterestRateValue, overdueSeconds); - Number const lateInterestRaw = state.principalOutstanding * overdueRate; - Number const lateInterestRounded = - roundToAsset(broker.asset, lateInterestRaw, state.loanScale); - Number const lateManagementFeeRounded = computeManagementFee( - broker.asset, - lateInterestRounded, - managementFeeRate, - state.loanScale); - Number const penaltyDue = lateInterestRounded + - lateManagementFeeRounded + latePaymentFeeRounded; - BEAST_EXPECT(penaltyDue > Number{}); - - auto const balanceBefore = env.balance(borrower, broker.asset).number(); - - STAmount const paymentAmount{broker.asset.raw(), baseFullDue}; - env(pay(borrower, loanKeylet.key, paymentAmount, tfLoanFullPayment)); - env.close(); - - if (auto const meta = env.meta(); BEAST_EXPECT(meta)) - BEAST_EXPECT(meta->at(sfTransactionResult) == tesSUCCESS); - - auto const balanceAfter = env.balance(borrower, broker.asset).number(); - Number const actualPaid = balanceBefore - balanceAfter; - BEAST_EXPECT(actualPaid == baseFullDue); - - Number const expectedWithPenalty = baseFullDue + penaltyDue; - BEAST_EXPECT(expectedWithPenalty > actualPaid); - BEAST_EXPECT(expectedWithPenalty - actualPaid == penaltyDue); - } - - void - testLoanCoverMinimumRoundingExploit() - { - auto testLoanCoverMinimumRoundingExploit = - [&, this](Number const& principalRequest) { - testcase << "LoanBrokerCoverClawback drains cover via rounding" - << " principalRequested=" - << to_string(principalRequest); - - using namespace jtx; - using namespace loan; - using namespace loanBroker; - - Env env(*this, all); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000'000), issuer, lender, borrower); - env.close(); - - env(fset(issuer, asfAllowTrustLineClawback)); - env.close(); - - PrettyAsset const asset = issuer[iouCurrency]; - env(trust(lender, asset(2'000'0000))); - env(trust(borrower, asset(2'000'0000))); - env.close(); - - env(pay(issuer, lender, asset(2'000'0000))); - env.close(); - - BrokerParameters brokerParams{ - .debtMax = 0, .coverRateMin = TenthBips32{10'000}}; - BrokerInfo broker{ - createVaultAndBroker(env, asset, lender, brokerParams)}; - - auto const loanSetFee = fee(env.current()->fees().base * 2); - auto createTx = env.jt( - set(borrower, broker.brokerID, principalRequest), - sig(sfCounterpartySignature, lender), - loanSetFee, - paymentInterval(600), - paymentTotal(1), - gracePeriod(60)); - env(createTx); - env.close(); - - auto const brokerBefore = - env.le(keylet::loanbroker(broker.brokerID)); - BEAST_EXPECT(brokerBefore); - if (!brokerBefore) - return; - - Number const debtOutstanding = brokerBefore->at(sfDebtTotal); - Number const coverAvailableBefore = - brokerBefore->at(sfCoverAvailable); - - BEAST_EXPECT(debtOutstanding > Number{}); - BEAST_EXPECT(coverAvailableBefore > Number{}); - - log << "debt=" << to_string(debtOutstanding) - << " cover_available=" << to_string(coverAvailableBefore); - - env(coverClawback(issuer, 0), loanBrokerID(broker.brokerID)); - env.close(); - - auto const brokerAfter = - env.le(keylet::loanbroker(broker.brokerID)); - BEAST_EXPECT(brokerAfter); - if (!brokerAfter) - return; - - Number const debtAfter = brokerAfter->at(sfDebtTotal); - // the debt has not changed - BEAST_EXPECT(debtAfter == debtOutstanding); - - Number const coverAvailableAfter = - brokerAfter->at(sfCoverAvailable); - - // since the cover rate min != 0, the cover available should not - // be zero - BEAST_EXPECT(coverAvailableAfter != Number{}); - }; - - // Call the lambda with different principal values - testLoanCoverMinimumRoundingExploit(Number{1, -30}); // 1e-30 units - testLoanCoverMinimumRoundingExploit(Number{1, -20}); // 1e-20 units - testLoanCoverMinimumRoundingExploit(Number{1, -10}); // 1e-10 units - testLoanCoverMinimumRoundingExploit(Number{1, 1}); // 1e-10 units - } -#endif - - void - testPoC_UnsignedUnderflowOnFullPayAfterEarlyPeriodic() - { - // --- PoC Summary ---------------------------------------------------- - // Scenario: Borrower makes one periodic payment early (before next due) - // so doPayment sets sfPreviousPaymentDate to the (future) - // sfNextPaymentDueDate and advances sfNextPaymentDueDate by one - // interval. Borrower then immediately performs a full-payment - // (tfLoanFullPayment). Why it matters: Full-payment interest accrual - // uses - // delta = now - max(prevPaymentDate, startDate) - // with an unsigned clock representation (uint32). If prevPaymentDate is - // in the future, the subtraction underflows to a very large positive - // number. This inflates roundedFullInterest and total full-close due, - // and LoanPay applies the inflated valueChange to the vault - // (sfAssetsTotal), increasing NAV. - // -------------------------------------------------------------------- - testcase( - "PoC: Unsigned-underflow full-pay accrual after early periodic"); - - using namespace jtx; - using namespace loan; - using namespace std::chrono_literals; - - Env env(*this, all); - - Account const lender{"poc_lender4"}; - Account const borrower{"poc_borrower4"}; - env.fund(XRP(3'000'000), lender, borrower); - env.close(); - - PrettyAsset const asset{xrpIssue(), 1'000'000}; - BrokerParameters brokerParams{}; - auto const broker = - createVaultAndBroker(env, asset, lender, brokerParams); - - // Create a 3-payment loan so full-payment path is enabled after 1 - // periodic payment. - auto const loanSetFee = fee(env.current()->fees().base * 2); - Number const principalRequest = asset(1000).value(); - auto const originationFee = asset(0).value(); - auto const serviceFee = asset(1).value(); - auto const serviceFeePA = asset(1); - auto const lateFee = asset(0).value(); - auto const closeFee = asset(0).value(); - auto const interest = percentageToTenthBips(12); - auto const lateInterest = percentageToTenthBips(12) / 10; - auto const closeInterest = percentageToTenthBips(12) / 10; - auto const overpaymentInterest = percentageToTenthBips(12) / 10; - auto const total = 3u; - auto const interval = 600u; - auto const grace = 60u; - - auto createJtx = env.jt( - set(borrower, broker.brokerID, principalRequest, 0), - sig(sfCounterpartySignature, lender), - loanOriginationFee(originationFee), - loanServiceFee(serviceFee), - latePaymentFee(lateFee), - closePaymentFee(closeFee), - overpaymentFee(percentageToTenthBips(5) / 10), - interestRate(interest), - lateInterestRate(lateInterest), - closeInterestRate(closeInterest), - overpaymentInterestRate(overpaymentInterest), - paymentTotal(total), - paymentInterval(interval), - gracePeriod(grace), - fee(loanSetFee)); - - auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); - BEAST_EXPECT(brokerSle); - auto const loanSequence = brokerSle ? brokerSle->at(sfLoanSequence) : 0; - auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); - - env(createJtx); - env.close(); - - // Compute a regular periodic due and pay it early (before next due). - auto state = getCurrentState(env, broker, loanKeylet); - Number const periodicRate = - loanPeriodicRate(state.interestRate, state.paymentInterval); - auto const components = detail::computePaymentComponents( - asset.raw(), - state.loanScale, - state.totalValue, - state.principalOutstanding, - state.managementFeeOutstanding, - state.periodicPayment, - periodicRate, - state.paymentRemaining, - brokerParams.managementFeeRate); - STAmount const regularDue{ - asset, components.trackedValueDelta + serviceFeePA.number()}; - // now < nextDue immediately after creation, so this is an early pay. - env(pay(borrower, loanKeylet.key, regularDue)); - env.close(); - - // Immediately attempt a full payoff. Compute the exact full-payment - // due to ensure the tx applies. - auto after = getCurrentState(env, broker, loanKeylet); - auto const loanSle = env.le(loanKeylet); - BEAST_EXPECT(loanSle); - auto const brokerSle2 = env.le(keylet::loanbroker(broker.brokerID)); - BEAST_EXPECT(brokerSle2); - - auto const closePaymentFee = - loanSle ? loanSle->at(sfClosePaymentFee) : Number{}; - auto const closeInterestRate = loanSle - ? TenthBips32{loanSle->at(sfCloseInterestRate)} - : TenthBips32{}; - auto const managementFeeRate = brokerSle2 - ? TenthBips16{brokerSle2->at(sfManagementFeeRate)} - : TenthBips16{}; - - Number const periodicRate2 = - loanPeriodicRate(after.interestRate, after.paymentInterval); - // Accrued + prepayment-penalty interest based on current periodic - // schedule - auto const fullPaymentInterest = computeFullPaymentInterest( - after.periodicPayment, - periodicRate2, - after.paymentRemaining, - env.current()->parentCloseTime(), - after.paymentInterval, - after.previousPaymentDate, - static_cast( - after.startDate.time_since_epoch().count()), - closeInterestRate); - // Round to asset scale and split interest/fee parts - auto const roundedInterest = - roundToAsset(asset.raw(), fullPaymentInterest, after.loanScale); - Number const roundedFullMgmtFee = computeManagementFee( - asset.raw(), roundedInterest, managementFeeRate, after.loanScale); - Number const roundedFullInterest = roundedInterest - roundedFullMgmtFee; - - // Show both signed and unsigned deltas to highlight the underflow. - auto const nowSecs = static_cast( - env.current()->parentCloseTime().time_since_epoch().count()); - auto const startSecs = static_cast( - after.startDate.time_since_epoch().count()); - auto const lastPaymentDate = - std::max(after.previousPaymentDate, startSecs); - auto const signedDelta = static_cast(nowSecs) - - static_cast(lastPaymentDate); - auto const unsignedDelta = - static_cast(nowSecs - lastPaymentDate); - log << "PoC window: prev=" << after.previousPaymentDate - << " start=" << startSecs << " now=" << nowSecs - << " signedDelta=" << signedDelta - << " unsignedDelta=" << unsignedDelta << std::endl; - - // Reference (clamped) computation: emulate a non-negative accrual - // window by clamping prevPaymentDate to 'now' for the full-pay path. - auto const prevClamped = std::min(after.previousPaymentDate, nowSecs); - auto const fullPaymentInterestClamped = computeFullPaymentInterest( - after.periodicPayment, - periodicRate2, - after.paymentRemaining, - env.current()->parentCloseTime(), - after.paymentInterval, - prevClamped, - startSecs, - closeInterestRate); - auto const roundedInterestClamped = roundToAsset( - asset.raw(), fullPaymentInterestClamped, after.loanScale); - Number const roundedFullMgmtFeeClamped = computeManagementFee( - asset.raw(), - roundedInterestClamped, - managementFeeRate, - after.loanScale); - Number const roundedFullInterestClamped = - roundedInterestClamped - roundedFullMgmtFeeClamped; - STAmount const fullDueClamped{ - asset, - after.principalOutstanding + roundedFullInterestClamped + - roundedFullMgmtFeeClamped + closePaymentFee}; - - // Collect vault NAV before closing payment - auto const vaultId2 = - brokerSle2 ? brokerSle2->at(sfVaultID) : uint256{}; - auto const vaultKey2 = keylet::vault(vaultId2); - auto const vaultBefore = env.le(vaultKey2); - BEAST_EXPECT(vaultBefore); - Number const assetsTotalBefore = - vaultBefore ? vaultBefore->at(sfAssetsTotal) : Number{}; - - STAmount const fullDue{ - asset, - after.principalOutstanding + roundedFullInterest + - roundedFullMgmtFee + closePaymentFee}; - - log << "PoC payoff: principalOutstanding=" << after.principalOutstanding - << " roundedFullInterest=" << roundedFullInterest - << " roundedFullMgmtFee=" << roundedFullMgmtFee - << " closeFee=" << closePaymentFee - << " fullDue=" << to_string(fullDue.getJson()) << std::endl; - log << "PoC reference (clamped): roundedFullInterestClamped=" - << roundedFullInterestClamped - << " roundedFullMgmtFeeClamped=" << roundedFullMgmtFeeClamped - << " fullDueClamped=" << to_string(fullDueClamped.getJson()) - << std::endl; - - env(pay(borrower, loanKeylet.key, fullDue), txflags(tfLoanFullPayment)); - env.close(); - - // Sanity: underflow present (unsigned delta very large relative to - // interval) - BEAST_EXPECT(unsignedDelta > after.paymentInterval); - - // Compare vault NAV before/after the full close - auto const vaultAfter = env.le(vaultKey2); - BEAST_EXPECT(vaultAfter); - if (vaultAfter) - { - auto const assetsTotalAfter = vaultAfter->at(sfAssetsTotal); - log << "PoC NAV: assetsTotalBefore=" << assetsTotalBefore - << " assetsTotalAfter=" << assetsTotalAfter - << " delta=" << (assetsTotalAfter - assetsTotalBefore) - << std::endl; - - // Value-based proof: underflowed window yields a payoff larger than - // the clamped (non-underflow) reference. - BEAST_EXPECT(fullDue == fullDueClamped); - if (fullDue > fullDueClamped) - log << "PoC delta: overcharge (fullDue > clamped)" << std::endl; - } - - // Loan should be paid off - auto const finalLoan = env.le(loanKeylet); - BEAST_EXPECT(finalLoan); - if (finalLoan) - { - BEAST_EXPECT(finalLoan->at(sfPaymentRemaining) == 0); - BEAST_EXPECT(finalLoan->at(sfPrincipalOutstanding) == 0); - } - } - - void - testDustManipulation() - { - testcase("Dust manipulation"); - - using namespace jtx; - using namespace std::chrono_literals; - Env env(*this, all); - - // Setup: Create accounts - Account issuer{"issuer"}; - Account lender{"lender"}; - Account borrower{"borrower"}; - Account victim{"victim"}; - - env.fund(XRP(1'000'000'00), issuer, lender, borrower, victim); - env.close(); - - // Step 1: Create vault with IOU asset - auto asset = issuer["USD"]; - env(trust(lender, asset(100000))); - env(trust(borrower, asset(100000))); - env(trust(victim, asset(100000))); - env(pay(issuer, lender, asset(50000))); - env(pay(issuer, borrower, asset(50000))); - env(pay(issuer, victim, asset(50000))); - env.close(); - - BrokerParameters brokerParams{ - .vaultDeposit = 10000, - .debtMax = Number{0}, - .coverRateMin = TenthBips32{1000}, - .coverRateLiquidation = TenthBips32{2500}}; - - auto broker = createVaultAndBroker(env, asset, lender, brokerParams); - - auto const loanKeyletOpt = [&]() -> std::optional { - auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - return std::nullopt; - - // Broker has no loans - BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); - - // The loan keylet is based on the LoanSequence of the - // _LOAN_BROKER_ object. - auto const loanSequence = brokerSle->at(sfLoanSequence); - return keylet::loan(broker.brokerID, loanSequence); - }(); - if (!loanKeyletOpt) - return; - - auto const& vaultKeylet = broker.vaultKeylet(); - - { - auto const vaultSle = env.le(vaultKeylet); - Number assetsTotal = vaultSle->at(sfAssetsTotal); - Number assetsAvail = vaultSle->at(sfAssetsAvailable); - - log << "Before loan creation:" << std::endl; - log << " AssetsTotal: " << assetsTotal << std::endl; - log << " AssetsAvailable: " << assetsAvail << std::endl; - log << " Difference: " << (assetsTotal - assetsAvail) << std::endl; - - // before the loan the assets total and available should be equal - BEAST_EXPECT(assetsAvail == assetsTotal); - BEAST_EXPECT( - assetsAvail == - broker.asset(brokerParams.vaultDeposit).number()); - } - - Keylet const& loanKeylet = *loanKeyletOpt; - - LoanParameters const loanParams{ - .account = lender, - .counter = borrower, - .principalRequest = Number{100}, - .interest = TenthBips32{1922}, - .payTotal = 5816, - .payInterval = 86400 * 6, - .gracePd = 86400 * 5, - }; - - env(loanParams(env, broker)); - env.close(); - - // Wait for loan to be late enough to default - env.close(std::chrono::seconds(86400 * 40)); // 40 days - - { - auto const vaultSle = env.le(vaultKeylet); - Number assetsTotal = vaultSle->at(sfAssetsTotal); - Number assetsAvail = vaultSle->at(sfAssetsAvailable); - - log << "After loan creation:" << std::endl; - log << " AssetsTotal: " << assetsTotal << std::endl; - log << " AssetsAvailable: " << assetsAvail << std::endl; - log << " Difference: " << (assetsTotal - assetsAvail) << std::endl; - - auto const loanSle = env.le(loanKeylet); - if (!BEAST_EXPECT(loanSle)) - return; - auto const state = constructRoundedLoanState(loanSle); - - log << "Loan state:" << std::endl; - log << " ValueOutstanding: " << state.valueOutstanding - << std::endl; - log << " PrincipalOutstanding: " << state.principalOutstanding - << std::endl; - log << " InterestOutstanding: " << state.interestOutstanding() - << std::endl; - log << " InterestDue: " << state.interestDue << std::endl; - log << " FeeDue: " << state.managementFeeDue << std::endl; - - // after loan creation the assets total and available should - // reflect the value of the loan - BEAST_EXPECT(assetsAvail < assetsTotal); - BEAST_EXPECT( - assetsAvail == - broker - .asset( - brokerParams.vaultDeposit - loanParams.principalRequest) - .number()); - BEAST_EXPECT( - assetsTotal == - broker.asset(brokerParams.vaultDeposit + state.interestDue) - .number()); - } - - // Step 7: Trigger default (dust adjustment will occur) - env(jtx::loan::manage(lender, loanKeylet.key, tfLoanDefault)); - env.close(); - - // Step 8: Verify phantom assets created - { - auto const vaultSle2 = env.le(vaultKeylet); - Number assetsTotal2 = vaultSle2->at(sfAssetsTotal); - Number assetsAvail2 = vaultSle2->at(sfAssetsAvailable); - - log << "After default:" << std::endl; - log << " AssetsTotal: " << assetsTotal2 << std::endl; - log << " AssetsAvailable: " << assetsAvail2 << std::endl; - log << " Difference: " << (assetsTotal2 - assetsAvail2) - << std::endl; - - // after a default the assets total and available should be equal - BEAST_EXPECT(assetsAvail2 == assetsTotal2); - } - } - - void - testRIPD3831() - { - using namespace jtx; - - testcase("RIPD-3831"); - - Account const issuer("issuer"); - Account const lender("lender"); - Account const borrower("borrower"); - - BrokerParameters const brokerParams{ - .vaultDeposit = 100000, - .debtMax = 0, - .coverRateMin = TenthBips32{0}, - // .managementFeeRate = TenthBips16{5919}, - .coverRateLiquidation = TenthBips32{0}}; - LoanParameters const loanParams{ - .account = lender, - .counter = borrower, - .principalRequest = Number{200'000, -6}, - .lateFee = Number{200, -6}, - .interest = TenthBips32{50'000}, - .payTotal = 10, - .payInterval = 150, - .gracePd = 0}; - - auto const assetType = AssetType::XRP; - - Env env(*this, all); - - auto loanResult = createLoan( - env, assetType, brokerParams, loanParams, issuer, lender, borrower); - - if (!BEAST_EXPECT(loanResult)) - return; - - auto broker = std::get(*loanResult); - auto loanKeylet = std::get(*loanResult); - - using tp = NetClock::time_point; - using d = NetClock::duration; - - auto state = getCurrentState(env, broker, loanKeylet); - if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan)) - { - // log << "loan after create: " << to_string(loan->getJson()) - // << std::endl; - - env.close(tp{d{ - loan->at(sfNextPaymentDueDate) + loan->at(sfGracePeriod) + 1}}); - } - - topUpBorrower( - env, broker, issuer, borrower, state, loanParams.serviceFee); - - using namespace jtx::loan; - - auto jv = - pay(borrower, loanKeylet.key, drops(XRPAmount(state.totalValue))); - - { - auto const submitParam = to_string(jv); - // log << "about to submit: " << submitParam << std::endl; - auto const jr = env.rpc("submit", borrower.name(), submitParam); - - // log << jr << std::endl; - BEAST_EXPECT(jr.isMember(jss::result)); - auto const jResult = jr[jss::result]; - // BEAST_EXPECT(jResult[jss::error] == "invalidTransaction"); - // BEAST_EXPECT( - // jResult[jss::error_exception] == - // "fails local checks: Transaction has bad signature."); - } - - env.close(); - - // Make sure the system keeps responding - env(noop(borrower)); - env.close(); - env(noop(issuer)); - env.close(); - env(noop(lender)); - env.close(); - } - - void - testRIPD3459() - { - testcase("RIPD-3459 - LoanBroker incorrect debt total"); - - using namespace jtx; - - Account const issuer("issuer"); - Account const lender("lender"); - Account const borrower("borrower"); - - BrokerParameters const brokerParams{ - .vaultDeposit = 200'000, - .debtMax = 0, - .coverRateMin = TenthBips32{0}, - .managementFeeRate = TenthBips16{500}, - .coverRateLiquidation = TenthBips32{0}}; - LoanParameters const loanParams{ - .account = lender, - .counter = borrower, - .principalRequest = Number{100'000, -4}, - .interest = TenthBips32{100'000}, - .payTotal = 10, - .gracePd = 0}; - - auto const assetType = AssetType::MPT; - - Env env(*this, all); - - auto loanResult = createLoan( - env, assetType, brokerParams, loanParams, issuer, lender, borrower); - - if (!BEAST_EXPECT(loanResult)) - return; - - auto broker = std::get(*loanResult); - auto loanKeylet = std::get(*loanResult); - auto pseudoAcct = std::get(*loanResult); - - VerifyLoanStatus verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); - - if (auto const brokerSle = env.le(broker.brokerKeylet()); - BEAST_EXPECT(brokerSle)) - { - if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle)) - { - BEAST_EXPECT( - brokerSle->at(sfDebtTotal) == - loanSle->at(sfTotalValueOutstanding)); - } - } - - makeLoanPayments( - env, - broker, - loanParams, - loanKeylet, - verifyLoanStatus, - issuer, - lender, - borrower, - PaymentParameters{.showStepBalances = true}); - - if (auto const brokerSle = env.le(broker.brokerKeylet()); - BEAST_EXPECT(brokerSle)) - { - if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle)) - { - BEAST_EXPECT( - brokerSle->at(sfDebtTotal) == - loanSle->at(sfTotalValueOutstanding)); - BEAST_EXPECT(brokerSle->at(sfDebtTotal) == beast::zero); - } - } - } - - void - testRIPD3901() - { - testcase("Crash with tfLoanOverpayment"); - using namespace jtx; - using namespace loan; - Account const lender{"lender"}; - Account const issuer{"issuer"}; - Account const borrower{"borrower"}; - Account const depositor{"depositor"}; - auto const txfee = fee(XRP(100)); - - Env env(*this); - Vault vault(env); - - env.fund(XRP(10'000), lender, issuer, borrower, depositor); - env.close(); - - auto [tx, vaultKeyLet] = - vault.create({.owner = lender, .asset = xrpIssue()}); - env(tx, txfee); - env.close(); - - env(vault.deposit( - {.depositor = depositor, - .id = vaultKeyLet.key, - .amount = XRP(1'000)}), - txfee); - env.close(); - - auto const brokerKeyLet = - keylet::loanbroker(lender.id(), env.seq(lender)); - - env(loanBroker::set(lender, vaultKeyLet.key), txfee); - env.close(); - - // BrokerInfo brokerInfo{xrpIssue(), keylet, vaultKeyLet, {}}; - - STAmount const debtMaximumRequest = XRPAmount(200'000); - - env(set(borrower, brokerKeyLet.key, debtMaximumRequest), - sig(sfCounterpartySignature, lender), - interestRate(TenthBips32(50'000)), - paymentTotal(2), - paymentInterval(150), - txflags(tfLoanOverpayment), - txfee); - env.close(); - - std::uint32_t const loanSequence = 1; - auto const loanKeylet = keylet::loan(brokerKeyLet.key, loanSequence); - - if (auto loan = env.le(loanKeylet); env.test.BEAST_EXPECT(loan)) - { - env(loan::pay(borrower, loanKeylet.key, XRPAmount(150'001)), - txflags(tfLoanOverpayment), - txfee); - env.close(); - } - } - - void - testRoundingAllowsUndercoverage() - { - testcase("Minimum cover rounding allows undercoverage (XRP)"); - - using namespace jtx; - using namespace loanBroker; - - Env env(*this, all); - - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(200'000), lender, borrower); - env.close(); - - // Vault with XRP asset - Vault vault{env}; - auto [vaultCreate, vaultKeylet] = - vault.create({.owner = lender, .asset = xrpIssue()}); - env(vaultCreate); - env.close(); - BEAST_EXPECT(env.le(vaultKeylet)); - - // Seed the vault with XRP so it can fund the loan principal - PrettyAsset const xrpAsset{xrpIssue(), 1}; - - BrokerParameters const brokerParams{ - .vaultDeposit = 1'000, - .debtMax = Number{0}, - .coverRateMin = TenthBips32{10'000}, - .coverDeposit = 82, - }; - - auto const brokerInfo = - createVaultAndBroker(env, xrpAsset, lender, brokerParams); - // Create a loan with principal 804 XRP and 0% interest (so - // DebtTotal increases by exactly 804) - env(loan::set(borrower, brokerInfo.brokerID, xrpAsset(804).value()), - loan::interestRate(TenthBips32(0)), - sig(sfCounterpartySignature, lender), - fee(env.current()->fees().base * 2)); - BEAST_EXPECT(env.ter() == tesSUCCESS); - env.close(); - - // Verify DebtTotal is exactly 804 - if (auto const brokerSle = - env.le(keylet::loanbroker(brokerInfo.brokerID)); - BEAST_EXPECT(brokerSle)) - { - log << *brokerSle << std::endl; - BEAST_EXPECT(brokerSle->at(sfDebtTotal) == Number(804)); - } - - // Attempt to withdraw 2 XRP to self, leaving 80 XRP CoverAvailable. - // The minimum is 80.4 XRP, which rounds up to 81 XRP, so this fails. - env(coverWithdraw(lender, brokerInfo.brokerID, xrpAsset(2).value()), - ter(tecINSUFFICIENT_FUNDS)); - BEAST_EXPECT(env.ter() == tecINSUFFICIENT_FUNDS); - env.close(); - - // Attempt to withdraw 1 XRP to self, leaving 81 XRP CoverAvailable. - // because that leaves sufficient cover, this succeeds - env(coverWithdraw(lender, brokerInfo.brokerID, xrpAsset(1).value())); - BEAST_EXPECT(env.ter() == tesSUCCESS); - env.close(); - - // Validate CoverAvailable == 80 XRP and DebtTotal remains 804 - if (auto const brokerSle = - env.le(keylet::loanbroker(brokerInfo.brokerID)); - BEAST_EXPECT(brokerSle)) - { - log << *brokerSle << std::endl; - BEAST_EXPECT( - brokerSle->at(sfCoverAvailable) == xrpAsset(81).value()); - BEAST_EXPECT(brokerSle->at(sfDebtTotal) == Number(804)); - - // Also demonstrate that the true minimum (804 * 10%) exceeds 80 - auto const theoreticalMin = - tenthBipsOfValue(Number(804), TenthBips32(10'000)); - log << "Theoretical min cover: " << theoreticalMin << std::endl; - BEAST_EXPECT(Number(804, -1) == theoreticalMin); - } - } - - void - testRIPD3902() - { - testcase("RIPD-3902 - 1 IOU loan payments"); - - using namespace jtx; - - Account const issuer("issuer"); - Account const lender("lender"); - Account const borrower("borrower"); - - BrokerParameters const brokerParams{ - .vaultDeposit = 10, - .debtMax = 0, - .coverRateMin = TenthBips32{0}, - .managementFeeRate = TenthBips16{0}, - .coverRateLiquidation = TenthBips32{0}}; - LoanParameters const loanParams{ - .account = lender, - .counter = borrower, - .principalRequest = Number{1, 0}, - .interest = TenthBips32{100'000}, - .payTotal = 5, - .payInterval = 150, - .gracePd = 60}; - - auto const assetType = AssetType::IOU; - - Env env(*this, all); - - auto loanResult = createLoan( - env, assetType, brokerParams, loanParams, issuer, lender, borrower); - - if (!BEAST_EXPECT(loanResult)) - return; - - auto broker = std::get(*loanResult); - auto loanKeylet = std::get(*loanResult); - auto pseudoAcct = std::get(*loanResult); - - VerifyLoanStatus verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); - - makeLoanPayments( - env, - broker, - loanParams, - loanKeylet, - verifyLoanStatus, - issuer, - lender, - borrower, - PaymentParameters{.showStepBalances = true}); - } - - void - testBorrowerIsBroker() - { - testcase("Test Borrower is Broker"); - using namespace jtx; - using namespace loan; - Account const broker{"broker"}; - Account const issuer{"issuer"}; - Account const borrower_{"borrower"}; - Account const depositor{"depositor"}; - - auto testLoanAsset = [&](auto&& getMaxDebt, auto const& borrower) { - Env env(*this); - Vault vault(env); - - if (borrower == broker) - env.fund(XRP(10'000), broker, issuer, depositor); - else - env.fund(XRP(10'000), broker, borrower, issuer, depositor); - env.close(); - - auto const xrpFee = XRP(100); - auto const txFee = fee(xrpFee); - - STAmount const debtMaximumRequest = getMaxDebt(env); - - auto const& asset = debtMaximumRequest.asset(); - auto const initialVault = asset(debtMaximumRequest * 100); - - auto [tx, vaultKeylet] = - vault.create({.owner = broker, .asset = asset}); - env(tx, txFee); - env.close(); - - env(vault.deposit( - {.depositor = depositor, - .id = vaultKeylet.key, - .amount = initialVault}), - txFee); - env.close(); - - auto const brokerKeylet = - keylet::loanbroker(broker.id(), env.seq(broker)); - - env(loanBroker::set(broker, vaultKeylet.key), txFee); - env.close(); - - auto const serviceFee = 101; - - env(set(broker, brokerKeylet.key, debtMaximumRequest), - counterparty(borrower), - sig(sfCounterpartySignature, borrower), - loanServiceFee(serviceFee), - paymentTotal(10), - txFee); - env.close(); - - std::uint32_t const loanSequence = 1; - auto const loanKeylet = - keylet::loan(brokerKeylet.key, loanSequence); - - auto const brokerBalanceBefore = env.balance(broker, asset); - - if (auto const loanSle = env.le(loanKeylet); - env.test.BEAST_EXPECT(loanSle)) - { - auto const payment = loanSle->at(sfPeriodicPayment); - auto const totalPayment = payment + serviceFee; - env(loan::pay(borrower, loanKeylet.key, asset(totalPayment)), - txFee); - env.close(); - if (auto const vaultSle = env.le(vaultKeylet); - BEAST_EXPECT(vaultSle)) - { - auto const expected = [&]() { - // The service fee is transferred to the broker if - // a borrower is not the broker - if (borrower != broker) - return brokerBalanceBefore.number() + serviceFee; - // Since a borrower is the broker, the payment is - // transferred to the Vault from the broker but not - // the service fee. - // If the asset is XRP then the broker pays the txfee. - if (asset.native()) - return brokerBalanceBefore.number() - payment - - xrpFee.number(); - return brokerBalanceBefore.number() - payment; - }(); - BEAST_EXPECT( - env.balance(broker, asset).value() == - asset(expected).value()); - } - } - }; - // Test when a borrower is the broker and is not to verify correct - // service fee transfer in both cases. - for (auto const& borrowerAcct : {broker, borrower_}) - { - testLoanAsset( - [&](Env&) -> STAmount { return STAmount{XRPAmount{200'000}}; }, - borrowerAcct); - testLoanAsset( - [&](Env& env) -> STAmount { - auto const IOU = issuer["USD"]; - env(trust(broker, IOU(1'000'000'000))); - env(trust(depositor, IOU(1'000'000'000))); - env(pay(issuer, broker, IOU(100'000'000))); - env(pay(issuer, depositor, IOU(100'000'000))); - env.close(); - return IOU(200'000); - }, - borrowerAcct); - testLoanAsset( - [&](Env& env) -> STAmount { - MPTTester mpt( - {.env = env, - .issuer = issuer, - .holders = {broker, depositor}, - .pay = 100'000'000}); - return mpt(200'000); - }, - borrowerAcct); - } - } - - void - testIssuerIsBorrower() - { - testcase("RIPD-4096 - Issuer as borrower"); - - using namespace jtx; - - Account const issuer("issuer"); - Account const lender("lender"); - - BrokerParameters const brokerParams{ - .vaultDeposit = 100'000, - .debtMax = 0, - .coverRateMin = TenthBips32{0}, - .managementFeeRate = TenthBips16{0}, - .coverRateLiquidation = TenthBips32{0}}; - LoanParameters const loanParams{ - .account = lender, - .counter = issuer, - .principalRequest = Number{10000}}; - - auto const assetType = AssetType::IOU; - - Env env(*this, all); - - auto loanResult = createLoan( - env, assetType, brokerParams, loanParams, issuer, lender, issuer); - - if (!BEAST_EXPECT(loanResult)) - return; - - auto broker = std::get(*loanResult); - auto loanKeylet = std::get(*loanResult); - auto pseudoAcct = std::get(*loanResult); - - VerifyLoanStatus verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); - - makeLoanPayments( - env, - broker, - loanParams, - loanKeylet, - verifyLoanStatus, - issuer, - lender, - issuer, - PaymentParameters{.showStepBalances = true}); - } - - void - testLimitExceeded() - { - testcase("RIPD-4125 - overpayment"); - - using namespace jtx; - - Account const issuer("issuer"); - Account const lender("lender"); - Account const borrower("borrower"); - - BrokerParameters const brokerParams{ - .vaultDeposit = 100'000, - .debtMax = 0, - .coverRateMin = TenthBips32{0}, - .managementFeeRate = TenthBips16{0}, - .coverRateLiquidation = TenthBips32{0}}; - LoanParameters const loanParams{ - .account = lender, - .counter = borrower, - .principalRequest = Number{200000, -6}, - .interest = TenthBips32{50000}, - .payTotal = 3, - .payInterval = 200, - .gracePd = 60, - .flags = tfLoanOverpayment, - }; - - auto const assetType = AssetType::XRP; - - Env env( - *this, - makeConfig(), - all, - nullptr, - beast::severities::Severity::kWarning); - - auto loanResult = createLoan( - env, assetType, brokerParams, loanParams, issuer, lender, borrower); - - if (!BEAST_EXPECT(loanResult)) - return; - - auto broker = std::get(*loanResult); - auto loanKeylet = std::get(*loanResult); - auto pseudoAcct = std::get(*loanResult); - - VerifyLoanStatus verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); - - auto const state = getCurrentState(env, broker, loanKeylet); - - env(loan::pay( - borrower, - loanKeylet.key, - STAmount{broker.asset, state.periodicPayment * 3 / 2 + 1}, - tfLoanOverpayment)); - env.close(); - - PaymentParameters paymentParams{ - //.overpaymentFactor = Number{15, -1}, - //.overpaymentExtra = Number{1, -6}, - //.flags = tfLoanOverpayment, - .showStepBalances = true, - //.validateBalances = false, - }; - - makeLoanPayments( - env, - broker, - loanParams, - loanKeylet, - verifyLoanStatus, - issuer, - lender, - borrower, - paymentParams); - } - -public: - void - run() override - { -#if LOANTODO - testLoanPayLateFullPaymentBypassesPenalties(); - testLoanCoverMinimumRoundingExploit(); -#endif - testCoverDepositWithdrawNonTransferableMPT(); - testPoC_UnsignedUnderflowOnFullPayAfterEarlyPeriodic(); - - testDisabled(); - testSelfLoan(); - testIssuerLoan(); - testLoanSet(); - testLifecycle(); - testServiceFeeOnBrokerDeepFreeze(); - - testRPC(); - testBasicMath(); - - testInvalidLoanDelete(); - testInvalidLoanManage(); - testInvalidLoanPay(); - testInvalidLoanSet(); - - testBatchBypassCounterparty(); - testLoanPayComputePeriodicPaymentValidRateInvariant(); - testAccountSendMptMinAmountInvariant(); - testLoanPayDebtDecreaseInvariant(); - testWrongMaxDebtBehavior(); - testLoanPayComputePeriodicPaymentValidTotalInterestInvariant(); - testDosLoanPay(); - testLoanPayComputePeriodicPaymentValidTotalPrincipalPaidInvariant(); - testLoanPayComputePeriodicPaymentValidTotalInterestPaidInvariant(); - testLoanNextPaymentDueDateOverflow(); - - testRequireAuth(); - testDustManipulation(); - - testRIPD3831(); - testRIPD3459(); - testRIPD3901(); - testRIPD3902(); - testRoundingAllowsUndercoverage(); - testBorrowerIsBroker(); - testIssuerIsBorrower(); - testLimitExceeded(); - } -}; - -class LoanBatch_test : public Loan_test -{ -protected: - beast::xor_shift_engine engine_; - - std::uniform_int_distribution<> assetDist{0, 2}; - std::uniform_int_distribution principalDist{ - 100'000, - 1'000'000'000}; - std::uniform_int_distribution interestRateDist{0, 10000}; - std::uniform_int_distribution<> paymentTotalDist{12, 10000}; - std::uniform_int_distribution<> paymentIntervalDist{60, 3600 * 24 * 30}; - std::uniform_int_distribution managementFeeRateDist{ - 0, - 10'000}; - std::uniform_int_distribution<> serviceFeeDist{0, 20}; - /* - # Generate parameters that are more likely to be valid - principal = Decimal(str(rand.randint(100000, - 100'000'000))).quantize(ROUND_TARGET) - - interest_rate = Decimal(rand.randint(1, 10000)) / - Decimal(100000) - - payment_total = rand.randint(12, 10000) - - payment_interval = Decimal(str(rand.randint(60, 2629746))) - - interest_fee = Decimal(rand.randint(0, 100000)) / - Decimal(100000) -*/ - - void - testRandomLoan() - { - using namespace jtx; - - Account const issuer("issuer"); - Account const lender("lender"); - Account const borrower("borrower"); - - // Determine all the random parameters at once - AssetType assetType = static_cast(assetDist(engine_)); - auto const principalRequest = principalDist(engine_); - TenthBips16 managementFeeRate{managementFeeRateDist(engine_)}; - auto const serviceFee = serviceFeeDist(engine_); - TenthBips32 interest{interestRateDist(engine_)}; - auto const payTotal = paymentTotalDist(engine_); - auto const payInterval = paymentIntervalDist(engine_); - - BrokerParameters brokerParams{ - .vaultDeposit = principalRequest * 10, - .debtMax = 0, - .coverRateMin = TenthBips32{0}, - .managementFeeRate = managementFeeRate}; - LoanParameters loanParams{ - .account = lender, - .counter = borrower, - .principalRequest = principalRequest, - .serviceFee = serviceFee, - .interest = interest, - .payTotal = payTotal, - .payInterval = payInterval, - }; - - runLoan(assetType, brokerParams, loanParams); - } - -public: - void - run() override - { - auto const argument = arg(); - auto const numIterations = [s = arg()]() -> int { - int defaultNum = 5; - if (s.empty()) - return defaultNum; - try - { - std::size_t pos; - auto const r = stoi(s, &pos); - if (pos != s.size()) - return defaultNum; - return r; - } - catch (...) - { - return defaultNum; - } - }(); - - using namespace jtx; - - auto const updateInterval = std::min(numIterations / 5, 100); - - for (int i = 0; i < numIterations; ++i) - { - if (i % updateInterval == 0) - testcase << "Random Loan Test iteration " << (i + 1) << "/" - << numIterations; - testRandomLoan(); - } - } -}; - -class LoanArbitrary_test : public LoanBatch_test -{ - void - run() override - { - using namespace jtx; - - BrokerParameters const brokerParams{ - .vaultDeposit = 10000, - .debtMax = 0, - .coverRateMin = TenthBips32{0}, - // .managementFeeRate = TenthBips16{5919}, - .coverRateLiquidation = TenthBips32{0}}; - LoanParameters const loanParams{ - .account = Account("lender"), - .counter = Account("borrower"), - .principalRequest = Number{10000, 0}, - // .interest = TenthBips32{0}, - // .payTotal = 5816, - .payInterval = 150}; - - runLoan(AssetType::XRP, brokerParams, loanParams); - } -}; - -BEAST_DEFINE_TESTSUITE(Loan, tx, ripple); -BEAST_DEFINE_TESTSUITE_MANUAL(LoanBatch, tx, ripple); -BEAST_DEFINE_TESTSUITE_MANUAL(LoanArbitrary, tx, ripple); - -} // namespace test -} // namespace ripple diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index 7d0126bd65..f7c93d3c62 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -1152,7 +1152,7 @@ class MPToken_test : public beast::unit_test::suite env(credentials::accept(bob, credIssuer1, credType)); env.close(); - MPTTester mptAlice(env, alice); + MPTTester mptAlice(env, alice, {}); env.close(); mptAlice.create({ @@ -1194,7 +1194,7 @@ class MPToken_test : public beast::unit_test::suite env(credentials::accept(bob, credIssuer1, credType)); env.close(); - MPTTester mptAlice(env, alice); + MPTTester mptAlice(env, alice, {}); env.close(); mptAlice.create({ @@ -1274,7 +1274,7 @@ class MPToken_test : public beast::unit_test::suite env(credentials::accept(carol, credIssuer2, credType)); env.close(); - MPTTester mptAlice(env, alice); + MPTTester mptAlice(env, alice, {}); env.close(); mptAlice.create({ diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index ce76e06912..4097b93786 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -2,8 +2,6 @@ #include #include #include -#include -#include #include #include @@ -71,14 +69,14 @@ class Vault_test : public beast::unit_test::suite this]() -> std::tuple { auto const vault = env.le(keylet); BEAST_EXPECT(vault != nullptr); - if (!asset.integral()) + if (asset.raw().holds() && !asset.raw().native()) BEAST_EXPECT(vault->at(sfScale) == 6); else BEAST_EXPECT(vault->at(sfScale) == 0); auto const shares = env.le(keylet::mptIssuance(vault->at(sfShareMPTID))); BEAST_EXPECT(shares != nullptr); - if (!asset.integral()) + if (asset.raw().holds() && !asset.raw().native()) BEAST_EXPECT(shares->at(sfAssetScale) == 6); else BEAST_EXPECT(shares->at(sfAssetScale) == 0); @@ -504,7 +502,7 @@ class Vault_test : public beast::unit_test::suite } } - if (!asset.integral()) + if (!asset.raw().native() && asset.raw().holds()) { testcase(prefix + " temporary authorization for 3rd party"); env(trust(erin, asset(1000))); @@ -672,13 +670,12 @@ class Vault_test : public beast::unit_test::suite test(env, issuer, owner, asset, vault); }; - auto testDisabled = [&](TER resultAfterCreate = temDISABLED) { - return [&, resultAfterCreate]( - Env& env, - Account const& issuer, - Account const& owner, - Asset const& asset, - Vault& vault) { + testCase( + [&](Env& env, + Account const& issuer, + Account const& owner, + Asset const& asset, + Vault& vault) { testcase("disabled single asset vault"); auto [tx, keylet] = @@ -687,7 +684,7 @@ class Vault_test : public beast::unit_test::suite { auto tx = vault.set({.owner = owner, .id = keylet.key}); - env(tx, data("test"), ter{resultAfterCreate}); + env(tx, ter{temDISABLED}); } { @@ -695,7 +692,7 @@ class Vault_test : public beast::unit_test::suite {.depositor = owner, .id = keylet.key, .amount = asset(10)}); - env(tx, ter{resultAfterCreate}); + env(tx, ter{temDISABLED}); } { @@ -703,7 +700,7 @@ class Vault_test : public beast::unit_test::suite {.depositor = owner, .id = keylet.key, .amount = asset(10)}); - env(tx, ter{resultAfterCreate}); + env(tx, ter{temDISABLED}); } { @@ -712,49 +709,15 @@ class Vault_test : public beast::unit_test::suite .id = keylet.key, .holder = owner, .amount = asset(10)}); - env(tx, ter{resultAfterCreate}); + env(tx, ter{temDISABLED}); } { auto tx = vault.del({.owner = owner, .id = keylet.key}); - env(tx, ter{resultAfterCreate}); - } - }; - }; - - testCase( - testDisabled(), - {.features = testable_amendments() - featureSingleAssetVault}); - - testCase( - testDisabled(tecNO_ENTRY), - {.features = testable_amendments() - featureMPTokensV1}); - - testCase( - [&](Env& env, - Account const& issuer, - Account const& owner, - Asset const& asset, - Vault& vault) { - testcase("disabled permissioned domains"); - - auto [tx, keylet] = - vault.create({.owner = owner, .asset = asset}); - env(tx); - - tx[sfFlags] = tx[sfFlags].asUInt() | tfVaultPrivate; - tx[sfDomainID] = to_string(base_uint<256>(42ul)); - env(tx, ter{temDISABLED}); - - { - auto tx = vault.set({.owner = owner, .id = keylet.key}); - env(tx, data("Test")); - - tx[sfDomainID] = to_string(base_uint<256>(13ul)); env(tx, ter{temDISABLED}); } }, - {.features = testable_amendments() - featurePermissionedDomains}); + {.features = testable_amendments() - featureSingleAssetVault}); testCase([&](Env& env, Account const& issuer, @@ -1767,8 +1730,7 @@ class Vault_test : public beast::unit_test::suite mptt.create( {.flags = tfMPTCanTransfer | tfMPTCanLock | (args.enableClawback ? tfMPTCanClawback : none) | - (args.requireAuth ? tfMPTRequireAuth : none), - .mutableFlags = tmfMPTCanMutateCanTransfer}); + (args.requireAuth ? tfMPTRequireAuth : none)}); PrettyAsset asset = mptt.issuanceID(); mptt.authorize({.account = owner}); mptt.authorize({.account = depositor}); @@ -2486,53 +2448,6 @@ class Vault_test : public beast::unit_test::suite env(tx2, ter{tecWRONG_ASSET}); env.close(); } - - testCase([this]( - Env& env, - Account const&, - Account const& owner, - Account const& depositor, - PrettyAsset const& asset, - Vault& vault, - MPTTester& mptt) { - testcase("MPT non-transferable"); - - auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); - env(tx); - env.close(); - - tx = vault.deposit( - {.depositor = depositor, - .id = keylet.key, - .amount = asset(100)}); - env(tx); - env.close(); - - // Remove CanTransfer - mptt.set({.mutableFlags = tmfMPTClearCanTransfer}); - env.close(); - - env(tx, ter{tecNO_AUTH}); - env.close(); - - tx = vault.withdraw( - {.depositor = depositor, - .id = keylet.key, - .amount = asset(100)}); - - env(tx, ter{tecNO_AUTH}); - env.close(); - - // Restore CanTransfer - mptt.set({.mutableFlags = tmfMPTSetCanTransfer}); - env.close(); - - env(tx); - env.close(); - - // Delete vault with zero balance - env(vault.del({.owner = owner, .id = keylet.key})); - }); } void @@ -2545,7 +2460,6 @@ class Vault_test : public beast::unit_test::suite int initialXRP = 1000; Number initialIOU = 200; double transferRate = 1.0; - bool charlieRipple = true; }; auto testCase = @@ -2571,21 +2485,8 @@ class Vault_test : public beast::unit_test::suite PrettyAsset const asset = issuer["IOU"]; env.trust(asset(1000), owner); + env.trust(asset(1000), charlie); env(pay(issuer, owner, asset(args.initialIOU))); - env.close(); - if (!args.charlieRipple) - { - env(fset(issuer, 0, asfDefaultRipple)); - env.close(); - env.trust(asset(1000), charlie); - env.close(); - env(pay(issuer, charlie, asset(args.initialIOU))); - env.close(); - env(fset(issuer, asfDefaultRipple)); - } - else - env.trust(asset(1000), charlie); - env.close(); env(rate(issuer, args.transferRate)); env.close(); @@ -2963,94 +2864,6 @@ class Vault_test : public beast::unit_test::suite env(tx1); }); - testCase( - [&, this]( - Env& env, - Account const& owner, - Account const& issuer, - Account const& charlie, - auto vaultAccount, - Vault& vault, - PrettyAsset const& asset, - std::function issuanceId) { - testcase("IOU non-transferable"); - - auto [tx, keylet] = - vault.create({.owner = owner, .asset = asset}); - tx[sfScale] = 0; - env(tx); - env.close(); - - // Turn on noripple on the pseudo account's trust line. - // Charlie's is already set. - env(trust(issuer, vaultAccount(keylet)["IOU"], tfSetNoRipple), - THISLINE); - - { - // Charlie cannot deposit - auto tx = vault.deposit( - {.depositor = charlie, - .id = keylet.key, - .amount = asset(100)}); - env(tx, ter{terNO_RIPPLE}, THISLINE); - env.close(); - } - - { - PrettyAsset shares = issuanceId(keylet); - auto tx1 = vault.deposit( - {.depositor = owner, - .id = keylet.key, - .amount = asset(100)}); - env(tx1, THISLINE); - env.close(); - - // Charlie cannot receive funds - auto tx2 = vault.withdraw( - {.depositor = owner, - .id = keylet.key, - .amount = shares(100)}); - tx2[sfDestination] = charlie.human(); - env(tx2, ter{terNO_RIPPLE}, THISLINE); - env.close(); - - { - // Create MPToken for shares held by Charlie - Json::Value tx{Json::objectValue}; - tx[sfAccount] = charlie.human(); - tx[sfMPTokenIssuanceID] = - to_string(shares.raw().get().getMptID()); - tx[sfTransactionType] = jss::MPTokenAuthorize; - env(tx); - env.close(); - } - env(pay(owner, charlie, shares(100)), THISLINE); - env.close(); - - // Charlie cannot withdraw - auto tx3 = vault.withdraw( - {.depositor = charlie, - .id = keylet.key, - .amount = shares(100)}); - env(tx3, ter{terNO_RIPPLE}); - env.close(); - - env(pay(charlie, owner, shares(100)), THISLINE); - env.close(); - } - - tx = vault.withdraw( - {.depositor = owner, - .id = keylet.key, - .amount = asset(100)}); - env(tx, THISLINE); - env.close(); - - // Delete vault with zero balance - env(vault.del({.owner = owner, .id = keylet.key}), THISLINE); - }, - {.charlieRipple = false}); - testCase( [&, this]( Env& env, @@ -4712,7 +4525,7 @@ class Vault_test : public beast::unit_test::suite BEAST_EXPECT(checkString(vault, sfAssetsAvailable, "50")); BEAST_EXPECT(checkString(vault, sfAssetsMaximum, "1000")); BEAST_EXPECT(checkString(vault, sfAssetsTotal, "50")); - BEAST_EXPECT(!vault.isMember(sfLossUnrealized.getJsonName())); + BEAST_EXPECT(checkString(vault, sfLossUnrealized, "0")); auto const strShareID = strHex(sle->at(sfShareMPTID)); BEAST_EXPECT(checkString(vault, sfShareMPTID, strShareID)); diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index bb5262d028..06203a4c2a 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -725,115 +725,6 @@ public: BEAST_EXPECT(Number(-100, -30000).truncate() == Number(0, 0)); } - void - testRounding() - { - // Test that rounding works as expected. - testcase("Rounding"); - - using NumberRoundings = std::map; - - std::map const expected{ - // Positive numbers - {Number{13, -1}, - {{Number::to_nearest, 1}, - {Number::towards_zero, 1}, - {Number::downward, 1}, - {Number::upward, 2}}}, - {Number{23, -1}, - {{Number::to_nearest, 2}, - {Number::towards_zero, 2}, - {Number::downward, 2}, - {Number::upward, 3}}}, - {Number{15, -1}, - {{Number::to_nearest, 2}, - {Number::towards_zero, 1}, - {Number::downward, 1}, - {Number::upward, 2}}}, - {Number{25, -1}, - {{Number::to_nearest, 2}, - {Number::towards_zero, 2}, - {Number::downward, 2}, - {Number::upward, 3}}}, - {Number{152, -2}, - {{Number::to_nearest, 2}, - {Number::towards_zero, 1}, - {Number::downward, 1}, - {Number::upward, 2}}}, - {Number{252, -2}, - {{Number::to_nearest, 3}, - {Number::towards_zero, 2}, - {Number::downward, 2}, - {Number::upward, 3}}}, - {Number{17, -1}, - {{Number::to_nearest, 2}, - {Number::towards_zero, 1}, - {Number::downward, 1}, - {Number::upward, 2}}}, - {Number{27, -1}, - {{Number::to_nearest, 3}, - {Number::towards_zero, 2}, - {Number::downward, 2}, - {Number::upward, 3}}}, - - // Negative numbers - {Number{-13, -1}, - {{Number::to_nearest, -1}, - {Number::towards_zero, -1}, - {Number::downward, -2}, - {Number::upward, -1}}}, - {Number{-23, -1}, - {{Number::to_nearest, -2}, - {Number::towards_zero, -2}, - {Number::downward, -3}, - {Number::upward, -2}}}, - {Number{-15, -1}, - {{Number::to_nearest, -2}, - {Number::towards_zero, -1}, - {Number::downward, -2}, - {Number::upward, -1}}}, - {Number{-25, -1}, - {{Number::to_nearest, -2}, - {Number::towards_zero, -2}, - {Number::downward, -3}, - {Number::upward, -2}}}, - {Number{-152, -2}, - {{Number::to_nearest, -2}, - {Number::towards_zero, -1}, - {Number::downward, -2}, - {Number::upward, -1}}}, - {Number{-252, -2}, - {{Number::to_nearest, -3}, - {Number::towards_zero, -2}, - {Number::downward, -3}, - {Number::upward, -2}}}, - {Number{-17, -1}, - {{Number::to_nearest, -2}, - {Number::towards_zero, -1}, - {Number::downward, -2}, - {Number::upward, -1}}}, - {Number{-27, -1}, - {{Number::to_nearest, -3}, - {Number::towards_zero, -2}, - {Number::downward, -3}, - {Number::upward, -2}}}, - }; - - for (auto const& [num, roundings] : expected) - { - for (auto const& [mode, val] : roundings) - { - NumberRoundModeGuard g{mode}; - auto const res = static_cast(num); - BEAST_EXPECTS( - res == val, - to_string(num) + " with mode " + std::to_string(mode) + - " expected " + std::to_string(val) + " got " + - std::to_string(res)); - } - } - } - void run() override { @@ -855,7 +746,6 @@ public: test_inc_dec(); test_toSTAmount(); test_truncate(); - testRounding(); } }; diff --git a/src/test/jtx.h b/src/test/jtx.h index 1d7f38ff54..4c33495ad8 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -52,7 +52,6 @@ #include #include #include -#include #include #include #include diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index 9858e17078..8bf579cdda 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -232,9 +232,7 @@ public: * * @param suite_ the current unit_test::suite */ - Env(beast::unit_test::suite& suite_, - beast::severities::Severity thresh = beast::severities::kError) - : Env(suite_, envconfig(), nullptr, thresh) + Env(beast::unit_test::suite& suite_) : Env(suite_, envconfig()) { } diff --git a/src/test/jtx/JTx.h b/src/test/jtx/JTx.h index 2baaf8cd10..d1160b0f34 100644 --- a/src/test/jtx/JTx.h +++ b/src/test/jtx/JTx.h @@ -40,9 +40,6 @@ struct JTx // Functions that sign something else after the mainSigners, such as // sfCounterpartySignature std::vector> postSigners; - // Metadata about the unit test itself - // The line where the JTx was constructed - std::optional testLine = std::nullopt; JTx() = default; JTx(JTx const&) = default; diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index 5ae44acaac..82a01d2e59 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -160,29 +160,6 @@ public: } }; -struct stAmountField : public JTxField -{ - using SF = SF_AMOUNT; - using SV = STAmount; - using OV = Json::Value; - using base = JTxField; - -protected: - using base::value_; - -public: - explicit stAmountField(SF const& sfield, SV const& value) - : JTxField(sfield, value) - { - } - - OV - value() const override - { - return value_.getJson(JsonOptions::none); - } -}; - struct blobField : public JTxField { using SF = SF_VL; @@ -298,8 +275,6 @@ using simpleField = JTxFieldWrapper>; */ auto const data = JTxFieldWrapper(sfData); -auto const amount = JTxFieldWrapper(sfAmount); - // TODO We only need this long "requires" clause as polyfill, for C++20 // implementations which are missing header. Replace with // `std::ranges::range`, and accordingly use std::ranges::begin/end @@ -722,110 +697,6 @@ checkMetrics( line); } -/* LoanBroker */ -/******************************************************************************/ - -namespace loanBroker { - -Json::Value -set(AccountID const& account, uint256 const& vaultId, std::uint32_t flags = 0); - -// Use "del" because "delete" is a reserved word in C++. -Json::Value -del(AccountID const& account, uint256 const& brokerID, std::uint32_t flags = 0); - -Json::Value -coverDeposit( - AccountID const& account, - uint256 const& brokerID, - STAmount const& amount, - std::uint32_t flags = 0); - -Json::Value -coverWithdraw( - AccountID const& account, - uint256 const& brokerID, - STAmount const& amount, - std::uint32_t flags = 0); - -// Must specify at least one of loanBrokerID or amount. -Json::Value -coverClawback(AccountID const& account, std::uint32_t flags = 0); - -auto const loanBrokerID = JTxFieldWrapper(sfLoanBrokerID); - -auto const managementFeeRate = - valueUnitWrapper(sfManagementFeeRate); - -auto const debtMaximum = simpleField(sfDebtMaximum); - -auto const coverRateMinimum = - valueUnitWrapper(sfCoverRateMinimum); - -auto const coverRateLiquidation = - valueUnitWrapper(sfCoverRateLiquidation); - -auto const destination = JTxFieldWrapper(sfDestination); - -} // namespace loanBroker - -/* Loan */ -/******************************************************************************/ -namespace loan { - -Json::Value -set(AccountID const& account, - uint256 const& loanBrokerID, - Number principalRequested, - std::uint32_t flags = 0); - -auto const counterparty = JTxFieldWrapper(sfCounterparty); - -// For `CounterPartySignature`, use `sig(sfCounterpartySignature, ...)` - -auto const loanOriginationFee = simpleField(sfLoanOriginationFee); - -auto const loanServiceFee = simpleField(sfLoanServiceFee); - -auto const latePaymentFee = simpleField(sfLatePaymentFee); - -auto const closePaymentFee = simpleField(sfClosePaymentFee); - -auto const overpaymentFee = - valueUnitWrapper(sfOverpaymentFee); - -auto const interestRate = - valueUnitWrapper(sfInterestRate); - -auto const lateInterestRate = - valueUnitWrapper(sfLateInterestRate); - -auto const closeInterestRate = - valueUnitWrapper(sfCloseInterestRate); - -auto const overpaymentInterestRate = - valueUnitWrapper(sfOverpaymentInterestRate); - -auto const paymentTotal = simpleField(sfPaymentTotal); - -auto const paymentInterval = simpleField(sfPaymentInterval); - -auto const gracePeriod = simpleField(sfGracePeriod); - -Json::Value -manage(AccountID const& account, uint256 const& loanID, std::uint32_t flags); - -Json::Value -del(AccountID const& account, uint256 const& loanID, std::uint32_t flags = 0); - -Json::Value -pay(AccountID const& account, - uint256 const& loanID, - STAmount const& amount, - std::uint32_t flags = 0); - -} // namespace loan - } // namespace jtx } // namespace test } // namespace ripple diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h index ed0fa57cbf..a5a73c59e8 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -213,25 +213,6 @@ public: { return {asset_}; } - - bool - integral() const - { - return asset_.integral(); - } - - bool - native() const - { - return asset_.native(); - } - - template - bool - holds() const - { - return asset_.holds(); - } }; //------------------------------------------------------------------------------ @@ -276,21 +257,6 @@ struct XRP_t return {TOut{v} * dropsPerXRP}; } - /** Returns an amount of XRP as PrettyAmount, - which is trivially convertable to STAmount - - @param v The Number of XRP (not drops). May be fractional. - */ - PrettyAmount - operator()(Number v) const - { - auto const c = dropsPerXRP.drops(); - auto const d = std::int64_t(v * c); - if (Number(d) / c != v) - Throw("unrepresentable"); - return {d}; - } - PrettyAmount operator()(double v) const { diff --git a/src/test/jtx/batch.h b/src/test/jtx/batch.h index 14122e3c01..4c90c28a50 100644 --- a/src/test/jtx/batch.h +++ b/src/test/jtx/batch.h @@ -49,7 +49,8 @@ public: inner( Json::Value const& txn, std::uint32_t const& sequence, - std::optional const& ticket = std::nullopt) + std::optional const& ticket = std::nullopt, + std::optional const& fee = std::nullopt) : txn_(txn), seq_(sequence), ticket_(ticket) { txn_[jss::SigningPubKey] = ""; diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index 9ce76d01c9..2ef7879c97 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -427,16 +427,14 @@ Env::postconditions( ParsedResult const& parsed, Json::Value const& jr) { - auto const line = jt.testLine ? " (" + to_string(*jt.testLine) + ")" : ""; - bool bad = !test.expect(parsed.ter, "apply: No ter result!" + line); + bool bad = !test.expect(parsed.ter, "apply: No ter result!"); bad = (jt.ter && parsed.ter && !test.expect( *parsed.ter == *jt.ter, "apply: Got " + transToken(*parsed.ter) + " (" + transHuman(*parsed.ter) + "); Expected " + - transToken(*jt.ter) + " (" + transHuman(*jt.ter) + ")" + - line)); + transToken(*jt.ter) + " (" + transHuman(*jt.ter) + ")")); using namespace std::string_literals; bad = (jt.rpcCode && !test.expect( @@ -448,21 +446,21 @@ Env::postconditions( : "NO RESULT") + " (" + parsed.rpcMessage + "); Expected " + RPC::get_error_info(jt.rpcCode->first).token.c_str() + " (" + - jt.rpcCode->second + ")" + line)) || + jt.rpcCode->second + ")")) || bad; // If we have an rpcCode (just checked), then the rpcException check is // optional - the 'error' field may not be defined, but if it is, it must // match rpcError. - bad = (jt.rpcException && - !test.expect( - (jt.rpcCode && parsed.rpcError.empty()) || - (parsed.rpcError == jt.rpcException->first && - (!jt.rpcException->second || - parsed.rpcException == *jt.rpcException->second)), - "apply: Got RPC result "s + parsed.rpcError + " (" + - parsed.rpcException + "); Expected " + - jt.rpcException->first + " (" + - jt.rpcException->second.value_or("n/a") + ")" + line)) || + bad = + (jt.rpcException && + !test.expect( + (jt.rpcCode && parsed.rpcError.empty()) || + (parsed.rpcError == jt.rpcException->first && + (!jt.rpcException->second || + parsed.rpcException == *jt.rpcException->second)), + "apply: Got RPC result "s + parsed.rpcError + " (" + + parsed.rpcException + "); Expected " + jt.rpcException->first + + " (" + jt.rpcException->second.value_or("n/a") + ")")) || bad; if (bad) { diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index 4dfc5fb7b1..9ea3e8fb03 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -356,134 +356,6 @@ allpe(AccountID const& a, Issue const& iss) iss.account); }; -/* LoanBroker */ -/******************************************************************************/ - -namespace loanBroker { - -Json::Value -set(AccountID const& account, uint256 const& vaultId, uint32_t flags) -{ - Json::Value jv; - jv[sfTransactionType] = jss::LoanBrokerSet; - jv[sfAccount] = to_string(account); - jv[sfVaultID] = to_string(vaultId); - jv[sfFlags] = flags; - return jv; -} - -Json::Value -del(AccountID const& account, uint256 const& brokerID, uint32_t flags) -{ - Json::Value jv; - jv[sfTransactionType] = jss::LoanBrokerDelete; - jv[sfAccount] = to_string(account); - jv[sfLoanBrokerID] = to_string(brokerID); - jv[sfFlags] = flags; - return jv; -} - -Json::Value -coverDeposit( - AccountID const& account, - uint256 const& brokerID, - STAmount const& amount, - uint32_t flags) -{ - Json::Value jv; - jv[sfTransactionType] = jss::LoanBrokerCoverDeposit; - jv[sfAccount] = to_string(account); - jv[sfLoanBrokerID] = to_string(brokerID); - jv[sfAmount] = amount.getJson(JsonOptions::none); - jv[sfFlags] = flags; - return jv; -} - -Json::Value -coverWithdraw( - AccountID const& account, - uint256 const& brokerID, - STAmount const& amount, - uint32_t flags) -{ - Json::Value jv; - jv[sfTransactionType] = jss::LoanBrokerCoverWithdraw; - jv[sfAccount] = to_string(account); - jv[sfLoanBrokerID] = to_string(brokerID); - jv[sfAmount] = amount.getJson(JsonOptions::none); - jv[sfFlags] = flags; - return jv; -} - -Json::Value -coverClawback(AccountID const& account, std::uint32_t flags) -{ - Json::Value jv; - jv[sfTransactionType] = jss::LoanBrokerCoverClawback; - jv[sfAccount] = to_string(account); - jv[sfFlags] = flags; - return jv; -} - -} // namespace loanBroker - -/* Loan */ -/******************************************************************************/ -namespace loan { - -Json::Value -set(AccountID const& account, - uint256 const& loanBrokerID, - Number principalRequested, - std::uint32_t flags) -{ - Json::Value jv; - jv[sfTransactionType] = jss::LoanSet; - jv[sfAccount] = to_string(account); - jv[sfLoanBrokerID] = to_string(loanBrokerID); - jv[sfPrincipalRequested] = to_string(principalRequested); - jv[sfFlags] = flags; - return jv; -} - -Json::Value -manage(AccountID const& account, uint256 const& loanID, std::uint32_t flags) -{ - Json::Value jv; - jv[sfTransactionType] = jss::LoanManage; - jv[sfAccount] = to_string(account); - jv[sfLoanID] = to_string(loanID); - jv[sfFlags] = flags; - return jv; -} - -Json::Value -del(AccountID const& account, uint256 const& loanID, std::uint32_t flags) -{ - Json::Value jv; - jv[sfTransactionType] = jss::LoanDelete; - jv[sfAccount] = to_string(account); - jv[sfLoanID] = to_string(loanID); - jv[sfFlags] = flags; - return jv; -} - -Json::Value -pay(AccountID const& account, - uint256 const& loanID, - STAmount const& amount, - std::uint32_t flags) -{ - Json::Value jv; - jv[sfTransactionType] = jss::LoanPay; - jv[sfAccount] = to_string(account); - jv[sfLoanID] = to_string(loanID); - jv[sfAmount] = amount.getJson(); - jv[sfFlags] = flags; - return jv; -} - -} // namespace loan } // namespace jtx } // namespace test } // namespace ripple diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index fc831790f1..ca6c4c6cd5 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -62,67 +62,17 @@ MPTTester::MPTTester(Env& env, Account const& issuer, MPTInit const& arg) env_.require(owners(it.second, 0)); } } - if (arg.create) - create(*arg.create); } -MPTTester::MPTTester( - Env& env, - Account const& issuer, - MPTID const& id, - std::vector const& holders, - bool close) - : env_(env) - , issuer_(issuer) - , holders_(makeHolders(holders)) - , id_(id) - , close_(close) +void +MPTTester::create(MPTCreate const& arg) { -} - -static MPTCreate -makeMPTCreate(MPTInitDef const& arg) -{ - if (arg.pay) - return { - .maxAmt = arg.maxAmt, - .transferFee = arg.transferFee, - .pay = {{arg.holders, *arg.pay}}, - .flags = arg.flags, - .authHolder = arg.authHolder}; - return { - .maxAmt = arg.maxAmt, - .transferFee = arg.transferFee, - .authorize = arg.holders, - .flags = arg.flags, - .authHolder = arg.authHolder}; -} - -MPTTester::MPTTester(MPTInitDef const& arg) - : MPTTester{ - arg.env, - arg.issuer, - MPTInit{ - .fund = arg.fund, - .close = arg.close, - .create = makeMPTCreate(arg)}} -{ -} - -MPTTester::operator MPT() const -{ - if (!id_) - Throw("MPT has not been created"); - return MPT("", *id_); -} - -Json::Value -MPTTester::createjv(MPTCreate const& arg) -{ - if (!arg.issuer) - Throw("MPTTester::createjv: issuer is not set"); + if (id_) + Throw("MPT can't be reused"); + id_ = makeMptID(env_.seq(issuer_), issuer_); Json::Value jv; - jv[sfAccount] = arg.issuer->human(); + jv[sfAccount] = issuer_.human(); + jv[sfTransactionType] = jss::MPTokenIssuanceCreate; if (arg.assetScale) jv[sfAssetScale] = *arg.assetScale; if (arg.transferFee) @@ -135,25 +85,6 @@ MPTTester::createjv(MPTCreate const& arg) jv[sfDomainID] = to_string(*arg.domainID); if (arg.mutableFlags) jv[sfMutableFlags] = *arg.mutableFlags; - jv[sfTransactionType] = jss::MPTokenIssuanceCreate; - - return jv; -} - -void -MPTTester::create(MPTCreate const& arg) -{ - if (id_) - Throw("MPT can't be reused"); - id_ = makeMptID(env_.seq(issuer_), issuer_); - Json::Value jv = createjv( - {.issuer = issuer_, - .maxAmt = arg.maxAmt, - .assetScale = arg.assetScale, - .transferFee = arg.transferFee, - .metadata = arg.metadata, - .mutableFlags = arg.mutableFlags, - .domainID = arg.domainID}); if (submit(arg, jv) != tesSUCCESS) { // Verify issuance doesn't exist @@ -164,62 +95,26 @@ MPTTester::create(MPTCreate const& arg) id_.reset(); } else - { env_.require(mptflags(*this, arg.flags.value_or(0))); - auto authAndPay = [&](auto const& accts, auto const&& getAcct) { - for (auto const& it : accts) - { - authorize({.account = getAcct(it)}); - if ((arg.flags.value_or(0) & tfMPTRequireAuth) && - arg.authHolder) - authorize({.account = issuer_, .holder = getAcct(it)}); - if (arg.pay && arg.pay->first.empty()) - pay(issuer_, getAcct(it), arg.pay->second); - } - if (arg.pay) - { - for (auto const& p : arg.pay->first) - pay(issuer_, p, arg.pay->second); - } - }; - if (arg.authorize) - { - if (arg.authorize->empty()) - authAndPay(holders_, [](auto const& it) { return it.second; }); - else - authAndPay(*arg.authorize, [](auto const& it) { return it; }); - } - else if (arg.pay) - { - if (arg.pay->first.empty()) - authAndPay(holders_, [](auto const& it) { return it.second; }); - else - authAndPay(arg.pay->first, [](auto const& it) { return it; }); - } - } -} - -Json::Value -MPTTester::destroyjv(MPTDestroy const& arg) -{ - Json::Value jv; - if (!arg.issuer || !arg.id) - Throw("MPTTester::destroyjv: issuer/id is not set"); - jv[sfAccount] = arg.issuer->human(); - jv[sfMPTokenIssuanceID] = to_string(*arg.id); - jv[sfTransactionType] = jss::MPTokenIssuanceDestroy; - - return jv; } void MPTTester::destroy(MPTDestroy const& arg) { - if (!arg.id && !id_) - Throw("MPT has not been created"); - Json::Value jv = destroyjv( - {.issuer = arg.issuer ? arg.issuer : issuer_, - .id = arg.id ? arg.id : id_}); + Json::Value jv; + if (arg.issuer) + jv[sfAccount] = arg.issuer->human(); + else + jv[sfAccount] = issuer_.human(); + if (arg.id) + jv[sfMPTokenIssuanceID] = to_string(*arg.id); + else + { + if (!id_) + Throw("MPT has not been created"); + jv[sfMPTokenIssuanceID] = to_string(*id_); + } + jv[sfTransactionType] = jss::MPTokenIssuanceDestroy; submit(arg, jv); } @@ -232,32 +127,25 @@ MPTTester::holder(std::string const& holder_) const return it->second; } -Json::Value -MPTTester::authorizejv(MPTAuthorize const& arg) -{ - Json::Value jv; - if (!arg.account || !arg.id) - Throw( - "MPTTester::authorizejv: issuer/id is not set"); - jv[sfAccount] = arg.account->human(); - jv[sfMPTokenIssuanceID] = to_string(*arg.id); - if (arg.holder) - jv[sfHolder] = arg.holder->human(); - jv[sfTransactionType] = jss::MPTokenAuthorize; - - return jv; -} - void MPTTester::authorize(MPTAuthorize const& arg) { - if (!arg.id && !id_) - Throw("MPT has not been created"); - Json::Value jv = authorizejv({ - .account = arg.account ? arg.account : issuer_, - .holder = arg.holder, - .id = arg.id ? arg.id : id_, - }); + Json::Value jv; + if (arg.account) + jv[sfAccount] = arg.account->human(); + else + jv[sfAccount] = issuer_.human(); + jv[sfTransactionType] = jss::MPTokenAuthorize; + if (arg.id) + jv[sfMPTokenIssuanceID] = to_string(*arg.id); + else + { + if (!id_) + Throw("MPT has not been created"); + jv[sfMPTokenIssuanceID] = to_string(*id_); + } + if (arg.holder) + jv[sfHolder] = arg.holder->human(); if (auto const result = submit(arg, jv); result == tesSUCCESS) { // Issuer authorizes @@ -313,34 +201,24 @@ MPTTester::authorize(MPTAuthorize const& arg) } void -MPTTester::authorizeHolders(Holders const& holders) -{ - for (auto const& holder : holders) - { - authorize({.account = holder}); - } -} - -Json::Value -MPTTester::setjv(MPTSet const& arg) +MPTTester::set(MPTSet const& arg) { Json::Value jv; - if (!arg.account || !arg.id) - Throw("MPTTester::setjv: issuer/id is not set"); - jv[sfAccount] = arg.account->human(); - jv[sfMPTokenIssuanceID] = to_string(*arg.id); - if (arg.holder) + if (arg.account) + jv[sfAccount] = arg.account->human(); + else + jv[sfAccount] = issuer_.human(); + jv[sfTransactionType] = jss::MPTokenIssuanceSet; + if (arg.id) + jv[sfMPTokenIssuanceID] = to_string(*arg.id); + else { - std::visit( - [&jv](T const& holder) { - if constexpr (std::is_same_v) - jv[sfHolder] = holder.human(); - else if constexpr (std::is_same_v) - jv[sfHolder] = toBase58(holder); - }, - *arg.holder); + if (!id_) + Throw("MPT has not been created"); + jv[sfMPTokenIssuanceID] = to_string(*id_); } - + if (arg.holder) + jv[sfHolder] = arg.holder->human(); if (arg.delegate) jv[sfDelegate] = arg.delegate->human(); if (arg.domainID) @@ -351,27 +229,7 @@ MPTTester::setjv(MPTSet const& arg) jv[sfTransferFee] = *arg.transferFee; if (arg.metadata) jv[sfMPTokenMetadata] = strHex(*arg.metadata); - jv[sfTransactionType] = jss::MPTokenIssuanceSet; - - return jv; -} - -void -MPTTester::set(MPTSet const& arg) -{ - if (!arg.id && !id_) - Throw("MPT has not been created"); - Json::Value jv = setjv( - {.account = arg.account ? arg.account : issuer_, - .holder = arg.holder, - .id = arg.id ? arg.id : id_, - .mutableFlags = arg.mutableFlags, - .transferFee = arg.transferFee, - .metadata = arg.metadata, - .delegate = arg.delegate, - .domainID = arg.domainID}); - if (submit(arg, jv) == tesSUCCESS && - (arg.flags.value_or(0) || arg.mutableFlags)) + if (submit(arg, jv) == tesSUCCESS && (arg.flags || arg.mutableFlags)) { auto require = [&](std::optional const& holder, bool unchanged) { @@ -423,9 +281,8 @@ MPTTester::set(MPTSet const& arg) }; if (arg.account) require(std::nullopt, arg.holder.has_value()); - if (auto const account = - (arg.holder ? std::get_if(&(*arg.holder)) : nullptr)) - require(*account, false); + if (arg.holder) + require(*arg.holder, false); } } @@ -598,13 +455,6 @@ MPTTester::mpt(std::int64_t amount) const return ripple::test::jtx::MPT(issuer_.name(), *id_)(amount); } -MPTTester::operator Asset() const -{ - if (!id_) - Throw("MPT has not been created"); - return Asset(*id_); -} - std::int64_t MPTTester::getBalance(Account const& account) const { @@ -643,12 +493,6 @@ MPTTester::operator[](std::string const& name) const return MPT(name, issuanceID()); } -PrettyAmount -MPTTester::operator()(std::uint64_t amount) const -{ - return MPT("", issuanceID())(amount); -} - } // namespace jtx } // namespace test } // namespace ripple diff --git a/src/test/jtx/impl/testline.cpp b/src/test/jtx/impl/testline.cpp deleted file mode 100644 index 722dc33bff..0000000000 --- a/src/test/jtx/impl/testline.cpp +++ /dev/null @@ -1,15 +0,0 @@ -#include - -namespace ripple { -namespace test { -namespace jtx { - -void -testline::operator()(Env&, JTx& jt) const -{ - jt.testLine = line_; -} - -} // namespace jtx -} // namespace test -} // namespace ripple diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index f9c58ebc9e..85c842737c 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -14,8 +14,6 @@ namespace jtx { class MPTTester; -auto const MPTDEXFlags = tfMPTCanTrade | tfMPTCanTransfer; - // Check flags settings on MPT create class mptflags { @@ -69,59 +67,31 @@ public: operator()(Env& env) const; }; -using Holders = std::vector; +struct MPTInit +{ + std::vector holders = {}; + PrettyAmount const xrp = XRP(10'000); + PrettyAmount const xrpHolders = XRP(10'000); + bool fund = true; + bool close = true; +}; +static MPTInit const mptInitNoFund{.fund = false}; struct MPTCreate { - static inline std::vector AllHolders = {}; - std::optional issuer = std::nullopt; std::optional maxAmt = std::nullopt; std::optional assetScale = std::nullopt; std::optional transferFee = std::nullopt; std::optional metadata = std::nullopt; std::optional ownerCount = std::nullopt; std::optional holderCount = std::nullopt; - // authorize if seated. - // if empty vector then authorize all holders - std::optional> authorize = std::nullopt; - // pay if seated. if authorize is not seated then authorize. - // if empty vector then pay to either authorize or all holders. - std::optional, std::uint64_t>> pay = - std::nullopt; + bool fund = true; std::optional flags = {0}; std::optional mutableFlags = std::nullopt; - bool authHolder = false; std::optional domainID = std::nullopt; std::optional err = std::nullopt; }; -struct MPTInit -{ - Holders holders = {}; - PrettyAmount const xrp = XRP(10'000); - PrettyAmount const xrpHolders = XRP(10'000); - bool fund = true; - bool close = true; - // create MPTIssuanceID if seated and follow rules for MPTCreate args - std::optional create = std::nullopt; -}; -static MPTInit const mptInitNoFund{.fund = false}; - -struct MPTInitDef -{ - Env& env; - Account issuer; - Holders holders = {}; - std::uint16_t transferFee = 0; - std::optional pay = std::nullopt; - std::uint32_t flags = MPTDEXFlags; - bool authHolder = false; - bool fund = false; - bool close = true; - std::optional maxAmt = std::nullopt; - std::optional err = std::nullopt; -}; - struct MPTDestroy { std::optional issuer = std::nullopt; @@ -146,7 +116,7 @@ struct MPTAuthorize struct MPTSet { std::optional account = std::nullopt; - std::optional> holder = std::nullopt; + std::optional holder = std::nullopt; std::optional id = std::nullopt; std::optional ownerCount = std::nullopt; std::optional holderCount = std::nullopt; @@ -162,49 +132,26 @@ struct MPTSet class MPTTester { Env& env_; - Account const issuer_; + Account const& issuer_; std::unordered_map const holders_; std::optional id_; bool close_; public: MPTTester(Env& env, Account const& issuer, MPTInit const& constr = {}); - MPTTester(MPTInitDef const& constr); - MPTTester( - Env& env, - Account const& issuer, - MPTID const& id, - std::vector const& holders = {}, - bool close = true); - operator MPT() const; void create(MPTCreate const& arg = MPTCreate{}); - static Json::Value - createjv(MPTCreate const& arg = MPTCreate{}); - void destroy(MPTDestroy const& arg = MPTDestroy{}); - static Json::Value - destroyjv(MPTDestroy const& arg = MPTDestroy{}); - void authorize(MPTAuthorize const& arg = MPTAuthorize{}); - static Json::Value - authorizejv(MPTAuthorize const& arg = MPTAuthorize{}); - - void - authorizeHolders(Holders const& holders); - void set(MPTSet const& set = {}); - static Json::Value - setjv(MPTSet const& set = {}); - [[nodiscard]] bool checkDomainID(std::optional expected) const; @@ -271,13 +218,8 @@ public: MPT operator[](std::string const& name) const; - PrettyAmount - operator()(std::uint64_t amount) const; - - operator Asset() const; - private: - using SLEP = SLE::const_pointer; + using SLEP = std::shared_ptr; bool forObject( std::function const& cb, diff --git a/src/test/jtx/testline.h b/src/test/jtx/testline.h deleted file mode 100644 index 65dd3b7d92..0000000000 --- a/src/test/jtx/testline.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef XRPL_TEST_JTX_TESTLINE_H_INCLUDED -#define XRPL_TEST_JTX_TESTLINE_H_INCLUDED - -#include - -namespace ripple { -namespace test { -namespace jtx { - -/** Store the line number of the current test in a JTx. - - Intended to help debug failing transaction submission tests. -*/ -class testline -{ -private: - int line_; - -public: - explicit testline(int line) : line_(line) - { - } - - void - operator()(Env&, JTx& jt) const; -}; - -#define THISLINE testline(__LINE__) - -} // namespace jtx -} // namespace test -} // namespace ripple - -#endif diff --git a/src/test/protocol/STParsedJSON_test.cpp b/src/test/protocol/STParsedJSON_test.cpp index 13983444da..7ad7e36097 100644 --- a/src/test/protocol/STParsedJSON_test.cpp +++ b/src/test/protocol/STParsedJSON_test.cpp @@ -724,63 +724,63 @@ class STParsedJSON_test : public beast::unit_test::suite { Json::Value j; int const minInt32 = -2147483648; - j[sfLoanScale] = minInt32; + j[sfDummyInt32] = minInt32; STParsedJSONObject obj("Test", j); BEAST_EXPECT(obj.object.has_value()); - if (BEAST_EXPECT(obj.object->isFieldPresent(sfLoanScale))) - BEAST_EXPECT(obj.object->getFieldI32(sfLoanScale) == minInt32); + if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) + BEAST_EXPECT(obj.object->getFieldI32(sfDummyInt32) == minInt32); } // max value { Json::Value j; int const maxInt32 = 2147483647; - j[sfLoanScale] = maxInt32; + j[sfDummyInt32] = maxInt32; STParsedJSONObject obj("Test", j); BEAST_EXPECT(obj.object.has_value()); - if (BEAST_EXPECT(obj.object->isFieldPresent(sfLoanScale))) - BEAST_EXPECT(obj.object->getFieldI32(sfLoanScale) == maxInt32); + if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) + BEAST_EXPECT(obj.object->getFieldI32(sfDummyInt32) == maxInt32); } // max uint value { Json::Value j; unsigned int const maxUInt32 = 2147483647u; - j[sfLoanScale] = maxUInt32; + j[sfDummyInt32] = maxUInt32; STParsedJSONObject obj("Test", j); BEAST_EXPECT(obj.object.has_value()); - if (BEAST_EXPECT(obj.object->isFieldPresent(sfLoanScale))) + if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) BEAST_EXPECT( - obj.object->getFieldI32(sfLoanScale) == + obj.object->getFieldI32(sfDummyInt32) == static_cast(maxUInt32)); } // Test with string value { Json::Value j; - j[sfLoanScale] = "2147483647"; + j[sfDummyInt32] = "2147483647"; STParsedJSONObject obj("Test", j); BEAST_EXPECT(obj.object.has_value()); - if (BEAST_EXPECT(obj.object->isFieldPresent(sfLoanScale))) + if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) BEAST_EXPECT( - obj.object->getFieldI32(sfLoanScale) == 2147483647u); + obj.object->getFieldI32(sfDummyInt32) == 2147483647u); } // Test with string negative value { Json::Value j; int value = -2147483648; - j[sfLoanScale] = std::to_string(value); + j[sfDummyInt32] = std::to_string(value); STParsedJSONObject obj("Test", j); BEAST_EXPECT(obj.object.has_value()); - if (BEAST_EXPECT(obj.object->isFieldPresent(sfLoanScale))) - BEAST_EXPECT(obj.object->getFieldI32(sfLoanScale) == value); + if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) + BEAST_EXPECT(obj.object->getFieldI32(sfDummyInt32) == value); } // Test out of range value for int32 (negative) { Json::Value j; - j[sfLoanScale] = "-2147483649"; + j[sfDummyInt32] = "-2147483649"; STParsedJSONObject obj("Test", j); BEAST_EXPECT(!obj.object.has_value()); } @@ -788,7 +788,7 @@ class STParsedJSON_test : public beast::unit_test::suite // Test out of range value for int32 (positive) { Json::Value j; - j[sfLoanScale] = 2147483648u; + j[sfDummyInt32] = 2147483648u; STParsedJSONObject obj("Test", j); BEAST_EXPECT(!obj.object.has_value()); } @@ -796,7 +796,7 @@ class STParsedJSON_test : public beast::unit_test::suite // Test string value out of range { Json::Value j; - j[sfLoanScale] = "2147483648"; + j[sfDummyInt32] = "2147483648"; STParsedJSONObject obj("Test", j); BEAST_EXPECT(!obj.object.has_value()); } @@ -804,7 +804,7 @@ class STParsedJSON_test : public beast::unit_test::suite // Test bad_type (arrayValue) { Json::Value j; - j[sfLoanScale] = Json::Value(Json::arrayValue); + j[sfDummyInt32] = Json::Value(Json::arrayValue); STParsedJSONObject obj("Test", j); BEAST_EXPECT(!obj.object.has_value()); } @@ -812,7 +812,7 @@ class STParsedJSON_test : public beast::unit_test::suite // Test bad_type (objectValue) { Json::Value j; - j[sfLoanScale] = Json::Value(Json::objectValue); + j[sfDummyInt32] = Json::Value(Json::objectValue); STParsedJSONObject obj("Test", j); BEAST_EXPECT(!obj.object.has_value()); } diff --git a/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp b/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp index 4b068882c9..bee90346b4 100644 --- a/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp +++ b/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp @@ -280,7 +280,7 @@ LedgerReplayTask::addDelta(std::shared_ptr const& delta) deltas_.empty() || deltas_.back()->ledgerSeq_ + 1 == delta->ledgerSeq_, "ripple::LedgerReplayTask::addDelta : no deltas or consecutive " - "sequence"); + "sequence", ); deltas_.push_back(delta); } } diff --git a/src/xrpld/app/ledger/detail/OpenLedger.cpp b/src/xrpld/app/ledger/detail/OpenLedger.cpp index 14db13aad4..1a009c91c7 100644 --- a/src/xrpld/app/ledger/detail/OpenLedger.cpp +++ b/src/xrpld/app/ledger/detail/OpenLedger.cpp @@ -105,11 +105,8 @@ OpenLedger::accept( auto const txId = tx->getTransactionID(); // skip batch txns - // The flag should only be settable if Batch feature is enabled. If - // Batch is not enabled, the flag is always invalid, so don't relay it - // regardless. // LCOV_EXCL_START - if (tx->isFlag(tfInnerBatchTxn)) + if (tx->isFlag(tfInnerBatchTxn) && rules.enabled(featureBatch)) { XRPL_ASSERT( txpair.second && txpair.second->isFieldPresent(sfParentBatchID), diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h deleted file mode 100644 index 559af28a47..0000000000 --- a/src/xrpld/app/misc/LendingHelpers.h +++ /dev/null @@ -1,444 +0,0 @@ -#ifndef XRPL_APP_MISC_LENDINGHELPERS_H_INCLUDED -#define XRPL_APP_MISC_LENDINGHELPERS_H_INCLUDED - -#include -#include - -namespace ripple { - -struct PreflightContext; - -// Lending protocol has dependencies, so capture them here. -bool -checkLendingProtocolDependencies(PreflightContext const& ctx); - -static constexpr std::uint32_t secondsInYear = 365 * 24 * 60 * 60; - -Number -loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval); - -/// Ensure the periodic payment is always rounded consistently -inline Number -roundPeriodicPayment( - Asset const& asset, - Number const& periodicPayment, - std::int32_t scale) -{ - return roundToAsset(asset, periodicPayment, scale, Number::upward); -} - -/* Represents the breakdown of amounts to be paid and changes applied to the - * Loan object while processing a loan payment. - * - * This structure is returned after processing a loan payment transaction and - * captures the amounts that need to be paid. The actual ledger entry changes - * are made in LoanPay based on this structure values. - * - * The sum of principalPaid, interestPaid, and feePaid represents the total - * amount to be deducted from the borrower's account. The valueChange field - * tracks whether the loan's total value increased or decreased beyond normal - * amortization. - * - * This structure is explained in the XLS-66 spec, section 3.2.4.2 (Payment - * Processing). - */ -struct LoanPaymentParts -{ - // The amount of principal paid that reduces the loan balance. - // This amount is subtracted from sfPrincipalOutstanding in the Loan object - // and paid to the Vault - Number principalPaid = numZero; - - // The total amount of interest paid to the Vault. - // This includes: - // - Tracked interest from the amortization schedule - // - Untracked interest (e.g., late payment penalty interest) - // This value is always non-negative. - Number interestPaid = numZero; - - // The change in the loan's total value outstanding. - // - If valueChange < 0: Loan value decreased - // - If valueChange > 0: Loan value increased - // - If valueChange = 0: No value adjustment - // - // For regular on-time payments, this is always 0. Non-zero values occur - // when: - // - Overpayments reduce the loan balance beyond the scheduled amount - // - Late payments add penalty interest to the loan value - // - Early full payment may increase or decrease the loan value based on - // terms - Number valueChange = numZero; - - /* The total amount of fees paid to the Broker. - * This includes: - * - Tracked management fees from the amortization schedule - * - Untracked fees (e.g., late payment fees, service fees, origination - * fees) This value is always non-negative. - */ - Number feePaid = numZero; - - LoanPaymentParts& - operator+=(LoanPaymentParts const& other); - - bool - operator==(LoanPaymentParts const& other) const; -}; - -/* Describes the initial computed properties of a loan. - * - * This structure contains the fundamental calculated values that define a - * loan's payment structure and amortization schedule. These properties are - * computed: - * - At loan creation (LoanSet transaction) - * - When loan terms change (e.g., after an overpayment that reduces the loan - * balance) - */ -struct LoanProperties -{ - // The unrounded amount to be paid at each regular payment period. - // Calculated using the standard amortization formula based on principal, - // interest rate, and number of payments. - // The actual amount paid in the LoanPay transaction must be rounded up to - // the precision of the asset and loan. - Number periodicPayment; - - // The total amount the borrower will pay over the life of the loan. - // Equal to periodicPayment * paymentsRemaining. - // This includes principal, interest, and management fees. - Number totalValueOutstanding; - - // The total management fee that will be paid to the broker over the - // loan's lifetime. This is a percentage of the total interest (gross) - // as specified by the broker's management fee rate. - Number managementFeeOwedToBroker; - - // The scale (decimal places) used for rounding all loan amounts. - // This is the maximum of: - // - The asset's native scale - // - A minimum scale required to represent the periodic payment accurately - // All loan state values (principal, interest, fees) are rounded to this - // scale. - std::int32_t loanScale; - - // The principal portion of the first payment. - Number firstPaymentPrincipal; -}; - -/** This structure captures the parts of a loan state. - * - * Whether the values are raw (unrounded) or rounded will depend on how it was - * computed. - * - * Many of the fields can be derived from each other, but they're all provided - * here to reduce code duplication and possible mistakes. - * e.g. - * * interestOutstanding = valueOutstanding - principalOutstanding - * * interestDue = interestOutstanding - managementFeeDue - */ -struct LoanState -{ - // Total value still due to be paid by the borrower. - Number valueOutstanding; - // Principal still due to be paid by the borrower. - Number principalOutstanding; - // Interest still due to be paid to the Vault. - // This is a portion of interestOutstanding - Number interestDue; - // Management fee still due to be paid to the broker. - // This is a portion of interestOutstanding - Number managementFeeDue; - - // Interest still due to be paid by the borrower. - Number - interestOutstanding() const - { - XRPL_ASSERT_PARTS( - interestDue + managementFeeDue == - valueOutstanding - principalOutstanding, - "ripple::LoanState::interestOutstanding", - "other values add up correctly"); - return interestDue + managementFeeDue; - } -}; - -// Some values get re-rounded to the vault scale any time they are adjusted. In -// addition, they are prevented from ever going below zero. This helps avoid -// accumulated rounding errors and leftover dust amounts. -template -void -adjustImpreciseNumber( - NumberProxy value, - Number const& adjustment, - Asset const& asset, - int vaultScale) -{ - value = roundToAsset(asset, value + adjustment, vaultScale); - - if (*value < beast::zero) - value = 0; -} - -inline int -getVaultScale(SLE::const_ref vaultSle) -{ - if (!vaultSle) - return Number::minExponent - 1; // LCOV_EXCL_LINE - return vaultSle->at(sfAssetsTotal).exponent(); -} - -TER -checkLoanGuards( - Asset const& vaultAsset, - Number const& principalRequested, - bool expectInterest, - std::uint32_t paymentTotal, - LoanProperties const& properties, - beast::Journal j); - -LoanState -computeRawLoanState( - Number const& periodicPayment, - Number const& periodicRate, - std::uint32_t const paymentRemaining, - TenthBips32 const managementFeeRate); - -LoanState -computeRawLoanState( - Number const& periodicPayment, - TenthBips32 interestRate, - std::uint32_t paymentInterval, - std::uint32_t const paymentRemaining, - TenthBips32 const managementFeeRate); - -// Constructs a valid LoanState object from arbitrary inputs -LoanState -constructLoanState( - Number const& totalValueOutstanding, - Number const& principalOutstanding, - Number const& managementFeeOutstanding); - -// Constructs a valid LoanState object from a Loan object, which always has -// rounded values -LoanState -constructRoundedLoanState(SLE::const_ref loan); - -Number -computeManagementFee( - Asset const& asset, - Number const& interest, - TenthBips32 managementFeeRate, - std::int32_t scale); - -Number -computeFullPaymentInterest( - Number const& rawPrincipalOutstanding, - Number const& periodicRate, - NetClock::time_point parentCloseTime, - std::uint32_t paymentInterval, - std::uint32_t prevPaymentDate, - std::uint32_t startDate, - TenthBips32 closeInterestRate); - -Number -computeFullPaymentInterest( - Number const& periodicPayment, - Number const& periodicRate, - std::uint32_t paymentRemaining, - NetClock::time_point parentCloseTime, - std::uint32_t paymentInterval, - std::uint32_t prevPaymentDate, - std::uint32_t startDate, - TenthBips32 closeInterestRate); - -namespace detail { -// These classes and functions should only be accessed by LendingHelper -// functions and unit tests - -enum class PaymentSpecialCase { none, final, extra }; - -/* Represents a single loan payment component parts. - -* This structure captures the "delta" (change) values that will be applied to -* the tracked fields in the Loan ledger object when a payment is processed. -* -* These are called "deltas" because they represent the amount by which each -* corresponding field in the Loan object will be reduced. -* They are "tracked" as they change tracked loan values. -*/ -struct PaymentComponents -{ - // The change in total value outstanding for this payment. - // This amount will be subtracted from sfTotalValueOutstanding in the Loan - // object. Equal to the sum of trackedPrincipalDelta, - // trackedInterestPart(), and trackedManagementFeeDelta. - Number trackedValueDelta; - - // The change in principal outstanding for this payment. - // This amount will be subtracted from sfPrincipalOutstanding in the Loan - // object, representing the portion of the payment that reduces the - // original loan amount. - Number trackedPrincipalDelta; - - // The change in management fee outstanding for this payment. - // This amount will be subtracted from sfManagementFeeOutstanding in the - // Loan object. This represents only the tracked management fees from the - // amortization schedule and does not include additional untracked fees - // (such as late payment fees) that go directly to the broker. - Number trackedManagementFeeDelta; - - // Indicates if this payment has special handling requirements. - // - none: Regular scheduled payment - // - final: The last payment that closes out the loan - // - extra: An additional payment beyond the regular schedule (overpayment) - PaymentSpecialCase specialCase = PaymentSpecialCase::none; - - // Calculates the tracked interest portion of this payment. - // This is derived from the other components as: - // trackedValueDelta - trackedPrincipalDelta - trackedManagementFeeDelta - // - // @return The amount of tracked interest included in this payment that - // will be paid to the vault. - Number - trackedInterestPart() const; -}; - -/* Extends PaymentComponents with untracked payment amounts. - * - * This structure adds untracked fees and interest to the base - * PaymentComponents, representing amounts that don't affect the Loan object's - * tracked state but are still part of the total payment due from the borrower. - * - * Untracked amounts include: - * - Late payment fees that go directly to the Broker - * - Late payment penalty interest that goes directly to the Vault - * - Service fees - * - * The key distinction is that tracked amounts reduce the Loan object's state - * (sfTotalValueOutstanding, sfPrincipalOutstanding, - * sfManagementFeeOutstanding), while untracked amounts are paid directly to the - * recipient without affecting the loan's amortization schedule. - */ -struct ExtendedPaymentComponents : public PaymentComponents -{ - // Additional management fees that go directly to the Broker. - // This includes fees not part of the standard amortization schedule - // (e.g., late fees, service fees, origination fees). - // This value may be negative, though the final value returned in - // LoanPaymentParts.feePaid will never be negative. - Number untrackedManagementFee; - - // Additional interest that goes directly to the Vault. - // This includes interest not part of the standard amortization schedule - // (e.g., late payment penalty interest). - // This value may be negative, though the final value returned in - // LoanPaymentParts.interestPaid will never be negative. - Number untrackedInterest; - - // The complete amount due from the borrower for this payment. - // Calculated as: trackedValueDelta + untrackedInterest + - // untrackedManagementFee - // - // This value is used to validate that the payment amount provided by the - // borrower is sufficient to cover all components of the payment. - Number totalDue; - - ExtendedPaymentComponents( - PaymentComponents const& p, - Number fee, - Number interest = numZero) - : PaymentComponents(p) - , untrackedManagementFee(fee) - , untrackedInterest(interest) - , totalDue( - trackedValueDelta + untrackedInterest + untrackedManagementFee) - { - } -}; - -/* Represents the differences between two loan states. - * - * This structure is used to capture the change in each component of a loan's - * state, typically when computing the difference between two LoanState objects - * (e.g., before and after a payment). It is a convenient way to capture changes - * in each component. How that difference is used depends on the context. - */ -struct LoanStateDeltas -{ - // The difference in principal outstanding between two loan states. - Number principal; - - // The difference in interest due between two loan states. - Number interest; - - // The difference in management fee outstanding between two loan states. - Number managementFee; - - /* Calculates the total change across all components. - * @return The sum of principal, interest, and management fee deltas. - */ - Number - total() const - { - return principal + interest + managementFee; - } - - // Ensures all delta values are non-negative. - void - nonNegative(); -}; - -PaymentComponents -computePaymentComponents( - Asset const& asset, - std::int32_t scale, - Number const& totalValueOutstanding, - Number const& principalOutstanding, - Number const& managementFeeOutstanding, - Number const& periodicPayment, - Number const& periodicRate, - std::uint32_t paymentRemaining, - TenthBips16 managementFeeRate); - -} // namespace detail - -detail::LoanStateDeltas -operator-(LoanState const& lhs, LoanState const& rhs); - -LoanState -operator-(LoanState const& lhs, detail::LoanStateDeltas const& rhs); - -LoanState -operator+(LoanState const& lhs, detail::LoanStateDeltas const& rhs); - -LoanProperties -computeLoanProperties( - Asset const& asset, - Number principalOutstanding, - TenthBips32 interestRate, - std::uint32_t paymentInterval, - std::uint32_t paymentsRemaining, - TenthBips32 managementFeeRate, - std::int32_t minimumScale); - -bool -isRounded(Asset const& asset, Number const& value, std::int32_t scale); - -// Indicates what type of payment is being made. -// regular, late, and full are mutually exclusive. -// overpayment is an "add on" to a regular payment, and follows that path with -// potential extra work at the end. -enum class LoanPaymentType { regular = 0, late, full, overpayment }; - -Expected -loanMakePayment( - Asset const& asset, - ApplyView& view, - SLE::ref loan, - SLE::const_ref brokerSle, - STAmount const& amount, - LoanPaymentType const paymentType, - beast::Journal j); - -} // namespace ripple - -#endif // XRPL_APP_MISC_LENDINGHELPERS_H_INCLUDED diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index 157de028e5..963f3dc3ea 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -1677,11 +1677,10 @@ NetworkOPsImp::apply(std::unique_lock& batchLock) app_.getHashRouter().shouldRelay(e.transaction->getID()); if (auto const sttx = *(e.transaction->getSTransaction()); toSkip && - // Skip relaying if it's an inner batch txn. The flag should - // only be set if the Batch feature is enabled. If Batch is - // not enabled, the flag is always invalid, so don't relay - // it regardless. - !sttx.isFlag(tfInnerBatchTxn)) + // Skip relaying if it's an inner batch txn and batch + // feature is enabled + !(sttx.isFlag(tfInnerBatchTxn) && + newOL->rules().enabled(featureBatch))) { protocol::TMTransaction tx; Serializer s; @@ -3041,11 +3040,9 @@ NetworkOPsImp::pubProposedTransaction( std::shared_ptr const& transaction, TER result) { - // never publish an inner txn inside a batch txn. The flag should - // only be set if the Batch feature is enabled. If Batch is not - // enabled, the flag is always invalid, so don't publish it - // regardless. - if (transaction->isFlag(tfInnerBatchTxn)) + // never publish an inner txn inside a batch txn + if (transaction->isFlag(tfInnerBatchTxn) && + ledger->rules().enabled(featureBatch)) return; MultiApiJson jvObj = diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp deleted file mode 100644 index 8020b47ba9..0000000000 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ /dev/null @@ -1,2058 +0,0 @@ -#include -// DO NOT REMOVE forces header file include to sort first -#include - -namespace ripple { - -bool -checkLendingProtocolDependencies(PreflightContext const& ctx) -{ - return ctx.rules.enabled(featureSingleAssetVault) && - VaultCreate::checkExtraFeatures(ctx); -} - -LoanPaymentParts& -LoanPaymentParts::operator+=(LoanPaymentParts const& other) -{ - XRPL_ASSERT( - - other.principalPaid >= beast::zero, - "ripple::LoanPaymentParts::operator+= : other principal " - "non-negative"); - XRPL_ASSERT( - other.interestPaid >= beast::zero, - "ripple::LoanPaymentParts::operator+= : other interest paid " - "non-negative"); - XRPL_ASSERT( - other.feePaid >= beast::zero, - "ripple::LoanPaymentParts::operator+= : other fee paid " - "non-negative"); - - principalPaid += other.principalPaid; - interestPaid += other.interestPaid; - valueChange += other.valueChange; - feePaid += other.feePaid; - return *this; -} - -bool -LoanPaymentParts::operator==(LoanPaymentParts const& other) const -{ - return principalPaid == other.principalPaid && - interestPaid == other.interestPaid && - valueChange == other.valueChange && feePaid == other.feePaid; -} - -/* Converts annualized interest rate to per-payment-period rate. - * The rate is prorated based on the payment interval in seconds. - * - * Equation (1) from XLS-66 spec, Section A-2 Equation Glossary - */ -Number -loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval) -{ - // Need floating point math, since we're dividing by a large number - return tenthBipsOfValue(Number(paymentInterval), interestRate) / - secondsInYear; -} - -/* Checks if a value is already rounded to the specified scale. - * Returns true if rounding down and rounding up produce the same result, - * indicating no further precision exists beyond the scale. - */ -bool -isRounded(Asset const& asset, Number const& value, std::int32_t scale) -{ - return roundToAsset(asset, value, scale, Number::downward) == - roundToAsset(asset, value, scale, Number::upward); -} - -namespace detail { - -void -LoanStateDeltas::nonNegative() -{ - if (principal < beast::zero) - principal = numZero; - if (interest < beast::zero) - interest = numZero; - if (managementFee < beast::zero) - managementFee = numZero; -} - -/* Computes (1 + periodicRate)^paymentsRemaining for amortization calculations. - * - * Equation (5) from XLS-66 spec, Section A-2 Equation Glossary - */ -Number -computeRaisedRate(Number const& periodicRate, std::uint32_t paymentsRemaining) -{ - return power(1 + periodicRate, paymentsRemaining); -} - -/* Computes the payment factor used in standard amortization formulas. - * This factor converts principal to periodic payment amount. - * - * Equation (6) from XLS-66 spec, Section A-2 Equation Glossary - */ -Number -computePaymentFactor( - Number const& periodicRate, - std::uint32_t paymentsRemaining) -{ - // For zero interest, payment factor is simply 1/paymentsRemaining - if (periodicRate == beast::zero) - return Number{1} / paymentsRemaining; - - Number const raisedRate = - computeRaisedRate(periodicRate, paymentsRemaining); - - return (periodicRate * raisedRate) / (raisedRate - 1); -} - -/* Calculates the periodic payment amount using standard amortization formula. - * For interest-free loans, returns principal divided equally across payments. - * - * Equation (7) from XLS-66 spec, Section A-2 Equation Glossary - */ -Number -loanPeriodicPayment( - Number const& principalOutstanding, - Number const& periodicRate, - std::uint32_t paymentsRemaining) -{ - if (principalOutstanding == 0 || paymentsRemaining == 0) - return 0; - - // Interest-free loans: equal principal payments - if (periodicRate == beast::zero) - return principalOutstanding / paymentsRemaining; - - return principalOutstanding * - computePaymentFactor(periodicRate, paymentsRemaining); -} - -/* Calculates the periodic payment amount from annualized interest rate. - * Converts the annual rate to periodic rate before computing payment. - * - * Equation (7) from XLS-66 spec, Section A-2 Equation Glossary - */ -Number -loanPeriodicPayment( - Number const& principalOutstanding, - TenthBips32 interestRate, - std::uint32_t paymentInterval, - std::uint32_t paymentsRemaining) -{ - if (principalOutstanding == 0 || paymentsRemaining == 0) - return 0; - - Number const periodicRate = loanPeriodicRate(interestRate, paymentInterval); - - return loanPeriodicPayment( - principalOutstanding, periodicRate, paymentsRemaining); -} - -/* Reverse-calculates principal from periodic payment amount. - * Used to determine theoretical principal at any point in the schedule. - * - * Equation (10) from XLS-66 spec, Section A-2 Equation Glossary - */ -Number -loanPrincipalFromPeriodicPayment( - Number const& periodicPayment, - Number const& periodicRate, - std::uint32_t paymentsRemaining) -{ - if (periodicRate == 0) - return periodicPayment * paymentsRemaining; - - return periodicPayment / - computePaymentFactor(periodicRate, paymentsRemaining); -} - -/* Splits gross interest into net interest (to vault) and management fee (to - * broker). Returns pair of (net interest, management fee). - * - * Equation (33) from XLS-66 spec, Section A-2 Equation Glossary - */ -std::pair -computeInterestAndFeeParts( - Number const& interest, - TenthBips16 managementFeeRate) -{ - auto const fee = tenthBipsOfValue(interest, managementFeeRate); - - return std::make_pair(interest - fee, fee); -} - -/* - * Computes the interest and management fee parts from interest amount. - * - * Equation (33) from XLS-66 spec, Section A-2 Equation Glossary - */ -std::pair -computeInterestAndFeeParts( - Asset const& asset, - Number const& interest, - TenthBips16 managementFeeRate, - std::int32_t loanScale) -{ - auto const fee = - computeManagementFee(asset, interest, managementFeeRate, loanScale); - - return std::make_pair(interest - fee, fee); -} - -/* Calculates penalty interest accrued on overdue payments. - * Returns 0 if payment is not late. - * - * Equation (16) from XLS-66 spec, Section A-2 Equation Glossary - */ -Number -loanLatePaymentInterest( - Number const& principalOutstanding, - TenthBips32 lateInterestRate, - NetClock::time_point parentCloseTime, - std::uint32_t nextPaymentDueDate) -{ - auto const now = parentCloseTime.time_since_epoch().count(); - - // If the payment is not late by any amount of time, then there's no late - // interest - if (now <= nextPaymentDueDate) - return 0; - - // Equation (3) from XLS-66 spec, Section A-2 Equation Glossary - auto const secondsOverdue = now - nextPaymentDueDate; - - auto const rate = loanPeriodicRate(lateInterestRate, secondsOverdue); - - return principalOutstanding * rate; -} - -/* Calculates interest accrued since the last payment based on time elapsed. - * Returns 0 if loan is paid ahead of schedule. - * - * Equation (27) from XLS-66 spec, Section A-2 Equation Glossary - */ -Number -loanAccruedInterest( - Number const& principalOutstanding, - Number const& periodicRate, - NetClock::time_point parentCloseTime, - std::uint32_t startDate, - std::uint32_t prevPaymentDate, - std::uint32_t paymentInterval) -{ - if (periodicRate == beast::zero) - return numZero; - - auto const lastPaymentDate = std::max(prevPaymentDate, startDate); - auto const now = parentCloseTime.time_since_epoch().count(); - - // If the loan has been paid ahead, then "lastPaymentDate" is in the future, - // and no interest has accrued. - if (now <= lastPaymentDate) - return numZero; - - // Equation (4) from XLS-66 spec, Section A-2 Equation Glossary - auto const secondsSinceLastPayment = now - lastPaymentDate; - - // Division is more likely to introduce rounding errors, which will then get - // amplified by multiplication. Therefore, we first multiply, and only then - // divide. - return principalOutstanding * periodicRate * secondsSinceLastPayment / - paymentInterval; -} - -/* Applies a payment to the loan state and returns the breakdown of amounts - * paid. - * - * This is the core function that updates the Loan ledger object fields based on - * a computed payment. - - * The function is templated to work with both direct Number/uint32_t values - * (for testing/simulation) and ValueProxy types (for actual ledger updates). - */ -template -LoanPaymentParts -doPayment( - ExtendedPaymentComponents const& payment, - NumberProxy& totalValueOutstandingProxy, - NumberProxy& principalOutstandingProxy, - NumberProxy& managementFeeOutstandingProxy, - UInt32Proxy& paymentRemainingProxy, - UInt32Proxy& prevPaymentDateProxy, - UInt32OptionalProxy& nextDueDateProxy, - std::uint32_t paymentInterval) -{ - XRPL_ASSERT_PARTS( - nextDueDateProxy, - "ripple::detail::doPayment", - "Next due date proxy set"); - - if (payment.specialCase == PaymentSpecialCase::final) - { - XRPL_ASSERT_PARTS( - principalOutstandingProxy == payment.trackedPrincipalDelta, - "ripple::detail::doPayment", - "Full principal payment"); - XRPL_ASSERT_PARTS( - totalValueOutstandingProxy == payment.trackedValueDelta, - "ripple::detail::doPayment", - "Full value payment"); - XRPL_ASSERT_PARTS( - managementFeeOutstandingProxy == payment.trackedManagementFeeDelta, - "ripple::detail::doPayment", - "Full management fee payment"); - - // Mark the loan as complete - paymentRemainingProxy = 0; - - // Record when the final payment was made - prevPaymentDateProxy = *nextDueDateProxy; - - // Clear the next due date. Setting it to 0 causes - // it to be removed from the Loan ledger object, saving space. - nextDueDateProxy = 0; - - // Zero out all tracked loan balances to mark the loan as paid off. - // These will be removed from the Loan object since they're default - // values. - principalOutstandingProxy = 0; - totalValueOutstandingProxy = 0; - managementFeeOutstandingProxy = 0; - } - else - { - // For regular payments (not overpayments), advance the payment schedule - if (payment.specialCase != PaymentSpecialCase::extra) - { - paymentRemainingProxy -= 1; - - prevPaymentDateProxy = nextDueDateProxy; - nextDueDateProxy += paymentInterval; - } - XRPL_ASSERT_PARTS( - principalOutstandingProxy > payment.trackedPrincipalDelta, - "ripple::detail::doPayment", - "Partial principal payment"); - XRPL_ASSERT_PARTS( - totalValueOutstandingProxy > payment.trackedValueDelta, - "ripple::detail::doPayment", - "Partial value payment"); - // Management fees are expected to be relatively small, and could get to - // zero before the loan is paid off - XRPL_ASSERT_PARTS( - managementFeeOutstandingProxy >= payment.trackedManagementFeeDelta, - "ripple::detail::doPayment", - "Valid management fee"); - - // Apply the payment deltas to reduce the outstanding balances - principalOutstandingProxy -= payment.trackedPrincipalDelta; - totalValueOutstandingProxy -= payment.trackedValueDelta; - managementFeeOutstandingProxy -= payment.trackedManagementFeeDelta; - } - - // Principal can never exceed total value (principal is part of total value) - XRPL_ASSERT_PARTS( - // Use an explicit cast because the template parameter can be - // ValueProxy or Number - static_cast(principalOutstandingProxy) <= - static_cast(totalValueOutstandingProxy), - "ripple::detail::doPayment", - "principal does not exceed total"); - - XRPL_ASSERT_PARTS( - // Use an explicit cast because the template parameter can be - // ValueProxy or Number - static_cast(managementFeeOutstandingProxy) >= beast::zero, - "ripple::detail::doPayment", - "fee outstanding stays valid"); - - return LoanPaymentParts{ - // Principal paid is straightforward - it's the tracked delta - .principalPaid = payment.trackedPrincipalDelta, - - // Interest paid combines: - // 1. Tracked interest from the amortization schedule - // (derived from the tracked deltas) - // 2. Untracked interest (e.g., late payment penalties) - .interestPaid = - payment.trackedInterestPart() + payment.untrackedInterest, - - // Value change represents how the loan's total value changed beyond - // normal amortization. - .valueChange = payment.untrackedInterest, - - // Fee paid combines: - // 1. Tracked management fees from the amortization schedule - // 2. Untracked fees (e.g., late payment fees, service fees) - .feePaid = - payment.trackedManagementFeeDelta + payment.untrackedManagementFee}; -} - -/* Simulates an overpayment to validate it won't break the loan's amortization. - * - * When a borrower pays more than the scheduled amount, the loan needs to be - * re-amortized with a lower principal. This function performs that calculation - * in a "sandbox" using temporary variables, allowing the caller to validate - * the result before committing changes to the actual ledger. - * - * The function preserves accumulated rounding errors across the re-amortization - * to ensure the loan state remains consistent with its payment history. - */ -Expected -tryOverpayment( - Asset const& asset, - std::int32_t loanScale, - ExtendedPaymentComponents const& overpaymentComponents, - Number& totalValueOutstanding, - Number& principalOutstanding, - Number& managementFeeOutstanding, - Number& periodicPayment, - TenthBips32 interestRate, - std::uint32_t paymentInterval, - Number const& periodicRate, - std::uint32_t paymentRemaining, - std::uint32_t prevPaymentDate, - std::optional nextDueDate, - TenthBips16 const managementFeeRate, - beast::Journal j) -{ - // Calculate what the loan state SHOULD be theoretically (at full precision) - auto const raw = computeRawLoanState( - periodicPayment, periodicRate, paymentRemaining, managementFeeRate); - - // Get the actual loan state (with accumulated rounding from past payments) - auto const rounded = constructLoanState( - totalValueOutstanding, principalOutstanding, managementFeeOutstanding); - - // Calculate the accumulated rounding errors. These need to be preserved - // across the re-amortization to maintain consistency with the loan's - // payment history. Without preserving these errors, the loan could end - // up with a different total value than what the borrower has actually paid. - auto const errors = rounded - raw; - - // Compute the new principal by applying the overpayment to the raw - // (theoretical) principal. Use max with 0 to ensure we never go negative. - auto const newRawPrincipal = std::max( - raw.principalOutstanding - overpaymentComponents.trackedPrincipalDelta, - Number{0}); - - // Compute new loan properties based on the reduced principal. This - // recalculates the periodic payment, total value, and management fees - // for the remaining payment schedule. - auto newLoanProperties = computeLoanProperties( - asset, - newRawPrincipal, - interestRate, - paymentInterval, - paymentRemaining, - managementFeeRate, - loanScale); - - JLOG(j.debug()) << "new periodic payment: " - << newLoanProperties.periodicPayment - << ", new total value: " - << newLoanProperties.totalValueOutstanding - << ", first payment principal: " - << newLoanProperties.firstPaymentPrincipal; - - // Calculate what the new loan state should be with the new periodic payment - auto const newRaw = computeRawLoanState( - newLoanProperties.periodicPayment, - periodicRate, - paymentRemaining, - managementFeeRate) + - errors; - - JLOG(j.debug()) << "new raw value: " << newRaw.valueOutstanding - << ", principal: " << newRaw.principalOutstanding - << ", interest gross: " << newRaw.interestOutstanding(); - // Update the loan state variables with the new values PLUS the preserved - // rounding errors. This ensures the loan's tracked state remains - // consistent with its payment history. - - principalOutstanding = std::clamp( - roundToAsset( - asset, newRaw.principalOutstanding, loanScale, Number::upward), - numZero, - rounded.principalOutstanding); - totalValueOutstanding = std::clamp( - roundToAsset( - asset, - principalOutstanding + newRaw.interestOutstanding(), - loanScale, - Number::upward), - numZero, - rounded.valueOutstanding); - managementFeeOutstanding = std::clamp( - roundToAsset(asset, newRaw.managementFeeDue, loanScale), - numZero, - rounded.managementFeeDue); - - auto const newRounded = constructLoanState( - totalValueOutstanding, principalOutstanding, managementFeeOutstanding); - - // Update newLoanProperties so that checkLoanGuards can make an accurate - // evaluation. - newLoanProperties.totalValueOutstanding = newRounded.valueOutstanding; - - JLOG(j.debug()) << "new rounded value: " << newRounded.valueOutstanding - << ", principal: " << newRounded.principalOutstanding - << ", interest gross: " << newRounded.interestOutstanding(); - - // Update the periodic payment to reflect the re-amortized schedule - periodicPayment = newLoanProperties.periodicPayment; - - // check that the loan is still valid - if (auto const ter = checkLoanGuards( - asset, - principalOutstanding, - // The loan may have been created with interest, but for - // small interest amounts, that may have already been paid - // off. Check what's still outstanding. This should - // guarantee that the interest checks pass. - newRounded.interestOutstanding() != beast::zero, - paymentRemaining, - newLoanProperties, - j)) - { - JLOG(j.warn()) << "Principal overpayment would cause the loan to be in " - "an invalid state. Ignore the overpayment"; - - return Unexpected(tesSUCCESS); - } - - // Validate that all computed properties are reasonable. These checks should - // never fail under normal circumstances, but we validate defensively. - if (newLoanProperties.periodicPayment <= 0 || - newLoanProperties.totalValueOutstanding <= 0 || - newLoanProperties.managementFeeOwedToBroker < 0) - { - // LCOV_EXCL_START - JLOG(j.warn()) << "Overpayment not allowed: Computed loan " - "properties are invalid. Does " - "not compute. TotalValueOutstanding: " - << newLoanProperties.totalValueOutstanding - << ", PeriodicPayment : " - << newLoanProperties.periodicPayment - << ", ManagementFeeOwedToBroker: " - << newLoanProperties.managementFeeOwedToBroker; - return Unexpected(tesSUCCESS); - // LCOV_EXCL_STOP - } - - auto const deltas = rounded - newRounded; - - auto const hypotheticalValueOutstanding = - rounded.valueOutstanding - deltas.principal; - - // Calculate how the loan's value changed due to the overpayment. - // This should be negative (value decreased) or zero. A principal - // overpayment should never increase the loan's value. - auto const valueChange = - newRounded.valueOutstanding - hypotheticalValueOutstanding; - if (valueChange > 0) - { - JLOG(j.warn()) << "Principal overpayment would increase the value of " - "the loan. Ignore the overpayment"; - return Unexpected(tesSUCCESS); - } - - return LoanPaymentParts{ - // Principal paid is the reduction in principal outstanding - .principalPaid = deltas.principal, - // Interest paid is the reduction in interest due - .interestPaid = - deltas.interest + overpaymentComponents.untrackedInterest, - // Value change includes both the reduction from paying down principal - // (negative) and any untracked interest penalties (positive, e.g., if - // the overpayment itself incurs a fee) - .valueChange = - valueChange + overpaymentComponents.trackedInterestPart(), - // Fee paid includes both the reduction in tracked management fees and - // any untracked fees on the overpayment itself - .feePaid = deltas.managementFee + - overpaymentComponents.untrackedManagementFee}; -} - -/* Validates and applies an overpayment to the loan state. - * - * This function acts as a wrapper around tryOverpayment(), performing the - * re-amortization calculation in a sandbox (using temporary copies of the - * loan state), then validating the results before committing them to the - * actual ledger via the proxy objects. - * - * The two-step process (try in sandbox, then commit) ensures that if the - * overpayment would leave the loan in an invalid state, we can reject it - * gracefully without corrupting the ledger data. - */ -template -Expected -doOverpayment( - Asset const& asset, - std::int32_t loanScale, - ExtendedPaymentComponents const& overpaymentComponents, - NumberProxy& totalValueOutstandingProxy, - NumberProxy& principalOutstandingProxy, - NumberProxy& managementFeeOutstandingProxy, - NumberProxy& periodicPaymentProxy, - TenthBips32 const interestRate, - std::uint32_t const paymentInterval, - Number const& periodicRate, - std::uint32_t const paymentRemaining, - std::uint32_t const prevPaymentDate, - std::optional const nextDueDate, - TenthBips16 const managementFeeRate, - beast::Journal j) -{ - // Create temporary copies of the loan state that can be safely modified - // and discarded if the overpayment doesn't work out. This prevents - // corrupting the actual ledger data if validation fails. - Number totalValueOutstanding = totalValueOutstandingProxy; - Number principalOutstanding = principalOutstandingProxy; - Number managementFeeOutstanding = managementFeeOutstandingProxy; - Number periodicPayment = periodicPaymentProxy; - - JLOG(j.debug()) - << "overpayment components:" - << ", totalValue before: " << *totalValueOutstandingProxy - << ", valueDelta: " << overpaymentComponents.trackedValueDelta - << ", principalDelta: " << overpaymentComponents.trackedPrincipalDelta - << ", managementFeeDelta: " - << overpaymentComponents.trackedManagementFeeDelta - << ", interestPart: " << overpaymentComponents.trackedInterestPart() - << ", untrackedInterest: " << overpaymentComponents.untrackedInterest - << ", totalDue: " << overpaymentComponents.totalDue - << ", payments remaining :" << paymentRemaining; - - // Attempt to re-amortize the loan with the overpayment applied. - // This modifies the temporary copies, leaving the proxies unchanged. - auto const ret = tryOverpayment( - asset, - loanScale, - overpaymentComponents, - totalValueOutstanding, - principalOutstanding, - managementFeeOutstanding, - periodicPayment, - interestRate, - paymentInterval, - periodicRate, - paymentRemaining, - prevPaymentDate, - nextDueDate, - managementFeeRate, - j); - if (!ret) - return Unexpected(ret.error()); - - auto const& loanPaymentParts = *ret; - - // Safety check: the principal must have decreased. If it didn't (or - // increased!), something went wrong in the calculation and we should - // reject the overpayment. - if (principalOutstandingProxy <= principalOutstanding) - { - // LCOV_EXCL_START - JLOG(j.warn()) << "Overpayment not allowed: principal " - << "outstanding did not decrease. Before: " - << *principalOutstandingProxy - << ". After: " << principalOutstanding; - return Unexpected(tesSUCCESS); - // LCOV_EXCL_STOP - } - - // The proxies still hold the original (pre-overpayment) values, which - // allows us to compute deltas and verify they match what we expect - // from the overpaymentComponents and loanPaymentParts. - - XRPL_ASSERT_PARTS( - overpaymentComponents.trackedPrincipalDelta == - principalOutstandingProxy - principalOutstanding, - "ripple::detail::doOverpayment", - "principal change agrees"); - - XRPL_ASSERT_PARTS( - overpaymentComponents.trackedManagementFeeDelta == - managementFeeOutstandingProxy - managementFeeOutstanding, - "ripple::detail::doOverpayment", - "no fee change"); - - // I'm not 100% sure the following asserts are correct. If in doubt, and - // everything else works, remove any that cause trouble. - - JLOG(j.debug()) << "valueChange: " << loanPaymentParts.valueChange - << ", totalValue before: " << *totalValueOutstandingProxy - << ", totalValue after: " << totalValueOutstanding - << ", totalValue delta: " - << (totalValueOutstandingProxy - totalValueOutstanding) - << ", principalDelta: " - << overpaymentComponents.trackedPrincipalDelta - << ", principalPaid: " << loanPaymentParts.principalPaid - << ", Computed difference: " - << overpaymentComponents.trackedPrincipalDelta - - (totalValueOutstandingProxy - totalValueOutstanding); - - XRPL_ASSERT_PARTS( - loanPaymentParts.valueChange == - totalValueOutstanding - - (totalValueOutstandingProxy - - overpaymentComponents.trackedPrincipalDelta) + - overpaymentComponents.trackedInterestPart(), - "ripple::detail::doOverpayment", - "interest paid agrees"); - - XRPL_ASSERT_PARTS( - overpaymentComponents.trackedPrincipalDelta == - loanPaymentParts.principalPaid, - "ripple::detail::doOverpayment", - "principal payment matches"); - - XRPL_ASSERT_PARTS( - loanPaymentParts.feePaid == - overpaymentComponents.untrackedManagementFee + - overpaymentComponents.trackedManagementFeeDelta, - "ripple::detail::doOverpayment", - "fee payment matches"); - - // All validations passed, so update the proxy objects (which will - // modify the actual Loan ledger object) - totalValueOutstandingProxy = totalValueOutstanding; - principalOutstandingProxy = principalOutstanding; - managementFeeOutstandingProxy = managementFeeOutstanding; - periodicPaymentProxy = periodicPayment; - - return loanPaymentParts; -} - -/* Computes the payment components for a late payment. - * - * A late payment is made after the grace period has expired and includes: - * 1. All components of a regular periodic payment - * 2. Late payment penalty interest (accrued since the due date) - * 3. Late payment fee charged by the broker - * - * The late penalty interest increases the loan's total value (the borrower - * owes more than scheduled), while the regular payment components follow - * the normal amortization schedule. - * - * Implements equation (15) from XLS-66 spec, Section A-2 Equation Glossary - */ -Expected -computeLatePayment( - Asset const& asset, - ApplyView const& view, - Number const& principalOutstanding, - std::int32_t nextDueDate, - ExtendedPaymentComponents const& periodic, - TenthBips32 lateInterestRate, - std::int32_t loanScale, - Number const& latePaymentFee, - STAmount const& amount, - TenthBips16 managementFeeRate, - beast::Journal j) -{ - // Check if the due date has passed. If not, reject the payment as - // being too soon - if (!hasExpired(view, nextDueDate)) - return Unexpected(tecTOO_SOON); - - // Calculate the penalty interest based on how long the payment is overdue. - auto const latePaymentInterest = loanLatePaymentInterest( - principalOutstanding, - lateInterestRate, - view.parentCloseTime(), - nextDueDate); - - // Round the late interest and split it between the vault (net interest) - // and the broker (management fee portion). This lambda ensures we - // round before splitting to maintain precision. - auto const [roundedLateInterest, roundedLateManagementFee] = [&]() { - auto const interest = - roundToAsset(asset, latePaymentInterest, loanScale); - return computeInterestAndFeeParts( - asset, interest, managementFeeRate, loanScale); - }(); - - XRPL_ASSERT( - roundedLateInterest >= 0, - "ripple::detail::computeLatePayment : valid late interest"); - XRPL_ASSERT_PARTS( - periodic.specialCase != PaymentSpecialCase::extra, - "ripple::detail::computeLatePayment", - "no extra parts to this payment"); - - // Create the late payment components by copying the regular periodic - // payment and adding the late penalties. We use a lambda to construct - // this to keep the logic clear. This preserves all the other fields without - // having to enumerate them. - - ExtendedPaymentComponents const late = [&]() { - auto inner = periodic; - - return ExtendedPaymentComponents{ - inner, - // Untracked management fee includes: - // 1. Regular service fee (from periodic.untrackedManagementFee) - // 2. Late payment fee (fixed penalty) - // 3. Management fee portion of late interest - periodic.untrackedManagementFee + latePaymentFee + - roundedLateManagementFee, - - // Untracked interest includes: - // 1. Any untracked interest from the regular payment (usually 0) - // 2. Late penalty interest (increases loan value) - // This positive value indicates the loan's value increased due - // to the late payment. - periodic.untrackedInterest + roundedLateInterest}; - }(); - - XRPL_ASSERT_PARTS( - isRounded(asset, late.totalDue, loanScale), - "ripple::detail::computeLatePayment", - "total due is rounded"); - - // Check that the borrower provided enough funds to cover the late payment. - // The late payment is more expensive than a regular payment due to the - // penalties. - if (amount < late.totalDue) - { - JLOG(j.warn()) << "Late loan payment amount is insufficient. Due: " - << late.totalDue << ", paid: " << amount; - return Unexpected(tecINSUFFICIENT_PAYMENT); - } - - return late; -} - -/* Computes payment components for paying off a loan early (before final - * payment). - * - * A full payment closes the loan immediately, paying off all outstanding - * balances plus a prepayment penalty and any accrued interest since the last - * payment. This is different from the final scheduled payment, which has no - * prepayment penalty. - * - * The function calculates: - * - Accrued interest since last payment (time-based) - * - Prepayment penalty (percentage of remaining principal) - * - Close payment fee (fixed fee for early closure) - * - All remaining principal and outstanding fees - * - * The loan's value may increase or decrease depending on whether the prepayment - * penalty exceeds the scheduled interest that would have been paid. - * - * Implements equation (26) from XLS-66 spec, Section A-2 Equation Glossary - */ -Expected -computeFullPayment( - Asset const& asset, - ApplyView& view, - Number const& principalOutstanding, - Number const& managementFeeOutstanding, - Number const& periodicPayment, - std::uint32_t paymentRemaining, - std::uint32_t prevPaymentDate, - std::uint32_t const startDate, - std::uint32_t const paymentInterval, - TenthBips32 const closeInterestRate, - std::int32_t loanScale, - Number const& totalInterestOutstanding, - Number const& periodicRate, - Number const& closePaymentFee, - STAmount const& amount, - TenthBips16 managementFeeRate, - beast::Journal j) -{ - // Full payment must be made before the final scheduled payment. - if (paymentRemaining <= 1) - { - // If this is the last payment, it has to be a regular payment - JLOG(j.warn()) << "Last payment cannot be a full payment."; - return Unexpected(tecKILLED); - } - - // Calculate the theoretical principal based on the payment schedule. - // This raw (unrounded) value is used to compute interest and penalties - // accurately. - Number const rawPrincipalOutstanding = loanPrincipalFromPeriodicPayment( - periodicPayment, periodicRate, paymentRemaining); - - // Full payment interest includes both accrued interest (time since last - // payment) and prepayment penalty (for closing early). - auto const fullPaymentInterest = computeFullPaymentInterest( - rawPrincipalOutstanding, - periodicRate, - view.parentCloseTime(), - paymentInterval, - prevPaymentDate, - startDate, - closeInterestRate); - - // Split the full payment interest into net interest (to vault) and - // management fee (to broker), applying proper rounding. - auto const [roundedFullInterest, roundedFullManagementFee] = [&]() { - auto const interest = roundToAsset( - asset, fullPaymentInterest, loanScale, Number::downward); - auto const parts = computeInterestAndFeeParts( - asset, interest, managementFeeRate, loanScale); - return std::make_tuple(parts.first, parts.second); - }(); - - ExtendedPaymentComponents const full{ - PaymentComponents{ - // Pay off all tracked outstanding balances: principal, interest, - // and fees. - // This marks the loan as complete (final payment). - .trackedValueDelta = principalOutstanding + - totalInterestOutstanding + managementFeeOutstanding, - .trackedPrincipalDelta = principalOutstanding, - - // All outstanding management fees are paid. This zeroes out the - // tracked fee balance. - .trackedManagementFeeDelta = managementFeeOutstanding, - .specialCase = PaymentSpecialCase::final, - }, - - // Untracked management fee includes: - // 1. Close payment fee (fixed fee for early closure) - // 2. Management fee on the full payment interest - // 3. Minus the outstanding tracked fee (already accounted for above) - // This can be negative because the outstanding fee is subtracted, but - // it gets combined with trackedManagementFeeDelta in the final - // accounting. - closePaymentFee + roundedFullManagementFee - managementFeeOutstanding, - - // Value change represents the difference between what the loan was - // expected to earn (totalInterestOutstanding) and what it actually - // earns (roundedFullInterest with prepayment penalty). - // - Positive: Prepayment penalty exceeds scheduled interest (loan value - // increases) - // - Negative: Prepayment penalty is less than scheduled interest (loan - // value decreases) - roundedFullInterest - totalInterestOutstanding, - }; - - XRPL_ASSERT_PARTS( - isRounded(asset, full.totalDue, loanScale), - "ripple::detail::computeFullPayment", - "total due is rounded"); - - JLOG(j.trace()) << "computeFullPayment result: periodicPayment: " - << periodicPayment << ", periodicRate: " << periodicRate - << ", paymentRemaining: " << paymentRemaining - << ", rawPrincipalOutstanding: " << rawPrincipalOutstanding - << ", fullPaymentInterest: " << fullPaymentInterest - << ", roundedFullInterest: " << roundedFullInterest - << ", roundedFullManagementFee: " - << roundedFullManagementFee - << ", untrackedInterest: " << full.untrackedInterest; - - if (amount < full.totalDue) - // If the payment is less than the full payment amount, it's not - // sufficient to be a full payment. - return Unexpected(tecINSUFFICIENT_PAYMENT); - - return full; -} - -Number -PaymentComponents::trackedInterestPart() const -{ - return trackedValueDelta - - (trackedPrincipalDelta + trackedManagementFeeDelta); -} - -/* Computes the breakdown of a regular periodic payment into principal, - * interest, and management fee components. - * - * This function determines how a single scheduled payment should be split among - * the three tracked loan components. The calculation accounts for accumulated - * rounding errors. - * - * The algorithm: - * 1. Calculate what the loan state SHOULD be after this payment (target) - * 2. Compare current state to target to get deltas - * 3. Adjust deltas to handle rounding artifacts and edge cases - * 4. Ensure deltas don't exceed available balances or payment amount - * - * Special handling for the final payment: all remaining balances are paid off - * regardless of the periodic payment amount. - */ -PaymentComponents -computePaymentComponents( - Asset const& asset, - std::int32_t scale, - Number const& totalValueOutstanding, - Number const& principalOutstanding, - Number const& managementFeeOutstanding, - Number const& periodicPayment, - Number const& periodicRate, - std::uint32_t paymentRemaining, - TenthBips16 managementFeeRate) -{ - XRPL_ASSERT_PARTS( - isRounded(asset, totalValueOutstanding, scale) && - isRounded(asset, principalOutstanding, scale) && - isRounded(asset, managementFeeOutstanding, scale), - "ripple::detail::computePaymentComponents", - "Outstanding values are rounded"); - XRPL_ASSERT_PARTS( - paymentRemaining > 0, - "ripple::detail::computePaymentComponents", - "some payments remaining"); - - auto const roundedPeriodicPayment = - roundPeriodicPayment(asset, periodicPayment, scale); - - // Final payment: pay off everything remaining, ignoring the normal - // periodic payment amount. This ensures the loan completes cleanly. - if (paymentRemaining == 1 || - totalValueOutstanding <= roundedPeriodicPayment) - { - // If there's only one payment left, we need to pay off each of the loan - // parts. - return PaymentComponents{ - .trackedValueDelta = totalValueOutstanding, - .trackedPrincipalDelta = principalOutstanding, - .trackedManagementFeeDelta = managementFeeOutstanding, - .specialCase = PaymentSpecialCase::final}; - } - - // Calculate what the loan state SHOULD be after this payment (the target). - // This is computed at full precision using the theoretical amortization. - LoanState const trueTarget = computeRawLoanState( - periodicPayment, periodicRate, paymentRemaining - 1, managementFeeRate); - - // Round the target to the loan's scale to match how actual loan values - // are stored. - LoanState const roundedTarget = LoanState{ - .valueOutstanding = - roundToAsset(asset, trueTarget.valueOutstanding, scale), - .principalOutstanding = - roundToAsset(asset, trueTarget.principalOutstanding, scale), - .interestDue = roundToAsset(asset, trueTarget.interestDue, scale), - .managementFeeDue = - roundToAsset(asset, trueTarget.managementFeeDue, scale)}; - - // Get the current actual loan state from the ledger values - LoanState const currentLedgerState = constructLoanState( - totalValueOutstanding, principalOutstanding, managementFeeOutstanding); - - // The difference between current and target states gives us the payment - // components. Any discrepancies from accumulated rounding are captured - // here. - - LoanStateDeltas deltas = currentLedgerState - roundedTarget; - - // Rounding can occasionally produce negative deltas. Zero them out. - deltas.nonNegative(); - - XRPL_ASSERT_PARTS( - deltas.principal <= currentLedgerState.principalOutstanding, - "ripple::detail::computePaymentComponents", - "principal delta not greater than outstanding"); - - // Cap each component to never exceed what's actually outstanding - deltas.principal = - std::min(deltas.principal, currentLedgerState.principalOutstanding); - - XRPL_ASSERT_PARTS( - deltas.interest <= currentLedgerState.interestDue, - "ripple::detail::computePaymentComponents", - "interest due delta not greater than outstanding"); - - // Cap interest to both the outstanding amount AND what's left of the - // periodic payment after principal is paid - deltas.interest = std::min( - {deltas.interest, - std::max(numZero, roundedPeriodicPayment - deltas.principal), - currentLedgerState.interestDue}); - - XRPL_ASSERT_PARTS( - deltas.managementFee <= currentLedgerState.managementFeeDue, - "ripple::detail::computePaymentComponents", - "management fee due delta not greater than outstanding"); - - // Cap management fee to both the outstanding amount AND what's left of the - // periodic payment after principal and interest are paid - deltas.managementFee = std::min( - {deltas.managementFee, - roundedPeriodicPayment - (deltas.principal + deltas.interest), - currentLedgerState.managementFeeDue}); - - // The shortage must never be negative, which indicates that the parts are - // trying to take more than the whole payment. The excess can be positive, - // which indicates that we're not going to take the whole payment amount, - // but if so, it must be small. - auto takeFrom = [](Number& component, Number& excess) { - if (excess > beast::zero) - { - auto part = std::min(component, excess); - component -= part; - excess -= part; - } - XRPL_ASSERT_PARTS( - excess >= beast::zero, - "ripple::detail::computePaymentComponents", - "excess non-negative"); - }; - // Helper to reduce deltas when they collectively exceed a limit. - // Order matters: we prefer to reduce interest first (most flexible), - // then management fee, then principal (least flexible). - auto addressExcess = [&takeFrom](LoanStateDeltas& deltas, Number& excess) { - // This order is based on where errors are the least problematic - takeFrom(deltas.interest, excess); - takeFrom(deltas.managementFee, excess); - takeFrom(deltas.principal, excess); - }; - - // Check if deltas exceed the total outstanding value. This should never - // happen due to earlier caps, but handle it defensively. - Number totalOverpayment = - deltas.total() - currentLedgerState.valueOutstanding; - - if (totalOverpayment > beast::zero) - { - // LCOV_EXCL_START - UNREACHABLE( - "ripple::detail::computePaymentComponents : payment exceeded loan " - "state"); - addressExcess(deltas, totalOverpayment); - // LCOV_EXCL_STOP - } - - // Check if deltas exceed the periodic payment amount. Reduce if needed. - Number shortage = roundedPeriodicPayment - deltas.total(); - - XRPL_ASSERT_PARTS( - isRounded(asset, shortage, scale), - "ripple::detail::computePaymentComponents", - "shortage is rounded"); - - if (shortage < beast::zero) - { - // Deltas exceed payment amount - reduce them proportionally - Number excess = -shortage; - addressExcess(deltas, excess); - shortage = -excess; - } - - // At this point, shortage >= 0 means we're paying less than the full - // periodic payment (due to rounding or component caps). - // shortage < 0 would mean we're trying to pay more than allowed (bug). - XRPL_ASSERT_PARTS( - shortage >= beast::zero, - "ripple::detail::computePaymentComponents", - "no shortage or excess"); - - // Final validation that all components are valid - XRPL_ASSERT_PARTS( - deltas.total() == - deltas.principal + deltas.interest + deltas.managementFee, - "ripple::detail::computePaymentComponents", - "total value adds up"); - - XRPL_ASSERT_PARTS( - deltas.principal >= beast::zero && - deltas.principal <= currentLedgerState.principalOutstanding, - "ripple::detail::computePaymentComponents", - "valid principal result"); - XRPL_ASSERT_PARTS( - deltas.interest >= beast::zero && - deltas.interest <= currentLedgerState.interestDue, - "ripple::detail::computePaymentComponents", - "valid interest result"); - XRPL_ASSERT_PARTS( - deltas.managementFee >= beast::zero && - deltas.managementFee <= currentLedgerState.managementFeeDue, - "ripple::detail::computePaymentComponents", - "valid fee result"); - - XRPL_ASSERT_PARTS( - deltas.principal + deltas.interest + deltas.managementFee > beast::zero, - "ripple::detail::computePaymentComponents", - "payment parts add to payment"); - - // Final safety clamp to ensure no value exceeds its outstanding balance - return PaymentComponents{ - .trackedValueDelta = std::clamp( - deltas.total(), numZero, currentLedgerState.valueOutstanding), - .trackedPrincipalDelta = std::clamp( - deltas.principal, numZero, currentLedgerState.principalOutstanding), - .trackedManagementFeeDelta = std::clamp( - deltas.managementFee, numZero, currentLedgerState.managementFeeDue), - }; -} - -/* Computes payment components for an overpayment scenario. - * - * An overpayment occurs when a borrower pays more than the scheduled periodic - * payment amount. The overpayment is treated as extra principal reduction, - * but incurs a fee and potentially a penalty interest charge. - * - * The calculation (Section 3.2.4.2.3 from XLS-66 spec): - * 1. Calculate gross penalty interest on the overpayment amount - * 2. Split the gross interest into net interest and management fee - * 3. Calculate the penalty fee - * 4. Determine the principal portion by subtracting the interest (gross) and - * management fee from the overpayment amount - * - * Unlike regular payments which follow the amortization schedule, overpayments - * apply to principal, reducing the loan balance and future interest costs. - * - * Equations (20), (21) and (22) from XLS-66 spec, Section A-2 Equation Glossary - */ -ExtendedPaymentComponents -computeOverpaymentComponents( - Asset const& asset, - int32_t const loanScale, - Number const& overpayment, - TenthBips32 const overpaymentInterestRate, - TenthBips32 const overpaymentFeeRate, - TenthBips16 const managementFeeRate) -{ - XRPL_ASSERT( - overpayment > 0 && isRounded(asset, overpayment, loanScale), - "ripple::detail::computeOverpaymentComponents : valid overpayment " - "amount"); - - // First, deduct the fixed overpayment fee from the total amount. - // This reduces the effective payment that will be applied to the loan. - // Equation (22) from XLS-66 spec, Section A-2 Equation Glossary - Number const overpaymentFee = roundToAsset( - asset, tenthBipsOfValue(overpayment, overpaymentFeeRate), loanScale); - - // Calculate the penalty interest on the effective payment amount. - // This interest doesn't follow the normal amortization schedule - it's - // a one-time charge for paying early. - // Equation (20) and (21) from XLS-66 spec, Section A-2 Equation Glossary - auto const [rawOverpaymentInterest, _] = [&]() { - Number const interest = - tenthBipsOfValue(overpayment, overpaymentInterestRate); - return detail::computeInterestAndFeeParts(interest, managementFeeRate); - }(); - - // Round the penalty interest components to the loan scale - auto const [roundedOverpaymentInterest, roundedOverpaymentManagementFee] = - [&]() { - Number const interest = - roundToAsset(asset, rawOverpaymentInterest, loanScale); - return detail::computeInterestAndFeeParts( - asset, interest, managementFeeRate, loanScale); - }(); - - auto const result = detail::ExtendedPaymentComponents{ - // Build the payment components, after fees and penalty - // interest are deducted, the remainder goes entirely to principal - // reduction. - detail::PaymentComponents{ - .trackedValueDelta = overpayment - overpaymentFee, - .trackedPrincipalDelta = overpayment - roundedOverpaymentInterest - - roundedOverpaymentManagementFee - overpaymentFee, - .trackedManagementFeeDelta = roundedOverpaymentManagementFee, - .specialCase = detail::PaymentSpecialCase::extra}, - // Untracked management fee is the fixed overpayment fee - overpaymentFee, - // Untracked interest is the penalty interest charged for - // overpaying. - // This is positive, representing a one-time cost, but it's - // typically - // much smaller than the interest savings from reducing - // principal. - roundedOverpaymentInterest}; - XRPL_ASSERT_PARTS( - result.trackedInterestPart() == roundedOverpaymentInterest, - "ripple::detail::computeOverpaymentComponents", - "valid interest computation"); - return result; -} - -} // namespace detail - -detail::LoanStateDeltas -operator-(LoanState const& lhs, LoanState const& rhs) -{ - detail::LoanStateDeltas result{ - .principal = lhs.principalOutstanding - rhs.principalOutstanding, - .interest = lhs.interestDue - rhs.interestDue, - .managementFee = lhs.managementFeeDue - rhs.managementFeeDue, - }; - - return result; -} - -LoanState -operator-(LoanState const& lhs, detail::LoanStateDeltas const& rhs) -{ - LoanState result{ - .valueOutstanding = lhs.valueOutstanding - rhs.total(), - .principalOutstanding = lhs.principalOutstanding - rhs.principal, - .interestDue = lhs.interestDue - rhs.interest, - .managementFeeDue = lhs.managementFeeDue - rhs.managementFee, - }; - - return result; -} - -LoanState -operator+(LoanState const& lhs, detail::LoanStateDeltas const& rhs) -{ - LoanState result{ - .valueOutstanding = lhs.valueOutstanding + rhs.total(), - .principalOutstanding = lhs.principalOutstanding + rhs.principal, - .interestDue = lhs.interestDue + rhs.interest, - .managementFeeDue = lhs.managementFeeDue + rhs.managementFee, - }; - - return result; -} - -TER -checkLoanGuards( - Asset const& vaultAsset, - Number const& principalRequested, - bool expectInterest, - std::uint32_t paymentTotal, - LoanProperties const& properties, - beast::Journal j) -{ - auto const totalInterestOutstanding = - properties.totalValueOutstanding - principalRequested; - // Guard 1: if there is no computed total interest over the life of the - // loan for a non-zero interest rate, we cannot properly amortize the - // loan - if (expectInterest && totalInterestOutstanding <= 0) - { - // Unless this is a zero-interest loan, there must be some interest - // due on the loan, even if it's (measurable) dust - JLOG(j.warn()) << "Loan for " << principalRequested - << " with interest has no interest due"; - return tecPRECISION_LOSS; - } - // Guard 1a: If there is any interest computed over the life of the - // loan, for a zero interest rate, something went sideways. - if (!expectInterest && totalInterestOutstanding > 0) - { - // LCOV_EXCL_START - JLOG(j.warn()) << "Loan for " << principalRequested - << " with no interest has interest due"; - return tecINTERNAL; - // LCOV_EXCL_STOP - } - - // Guard 2: if the principal portion of the first periodic payment is - // too small to be accurately represented with the given rounding mode, - // raise an error - if (properties.firstPaymentPrincipal <= 0) - { - // Check that some true (unrounded) principal is paid each period. - // Since the first payment pays the least principal, if it's good, - // they'll all be good. Note that the outstanding principal is - // rounded, and may not change right away. - JLOG(j.warn()) << "Loan is unable to pay principal."; - return tecPRECISION_LOSS; - } - - // Guard 3: If the periodic payment is so small that it can't even be - // rounded to a representable value, then the loan can't be paid. Also, - // avoids dividing by 0. - auto const roundedPayment = roundPeriodicPayment( - vaultAsset, properties.periodicPayment, properties.loanScale); - if (roundedPayment == beast::zero) - { - JLOG(j.warn()) << "Loan Periodic payment (" - << properties.periodicPayment << ") rounds to 0. "; - return tecPRECISION_LOSS; - } - - // Guard 4: if the rounded periodic payment is large enough that the - // loan can't be amortized in the specified number of payments, raise an - // error - { - NumberRoundModeGuard mg(Number::upward); - - if (std::int64_t const computedPayments{ - properties.totalValueOutstanding / roundedPayment}; - computedPayments != paymentTotal) - { - JLOG(j.warn()) << "Loan Periodic payment (" - << properties.periodicPayment << ") rounding (" - << roundedPayment << ") on a total value of " - << properties.totalValueOutstanding - << " can not complete the loan in the specified " - "number of payments (" - << computedPayments << " != " << paymentTotal << ")"; - return tecPRECISION_LOSS; - } - } - return tesSUCCESS; -} - -/* - * This function calculates the full payment interest accrued since the last - * payment, plus any prepayment penalty. - * - * Equations (27) and (28) from XLS-66 spec, Section A-2 Equation Glossary - */ -Number -computeFullPaymentInterest( - Number const& rawPrincipalOutstanding, - Number const& periodicRate, - NetClock::time_point parentCloseTime, - std::uint32_t paymentInterval, - std::uint32_t prevPaymentDate, - std::uint32_t startDate, - TenthBips32 closeInterestRate) -{ - auto const accruedInterest = detail::loanAccruedInterest( - rawPrincipalOutstanding, - periodicRate, - parentCloseTime, - startDate, - prevPaymentDate, - paymentInterval); - XRPL_ASSERT( - accruedInterest >= 0, - "ripple::detail::computeFullPaymentInterest : valid accrued " - "interest"); - - // Equation (28) from XLS-66 spec, Section A-2 Equation Glossary - auto const prepaymentPenalty = closeInterestRate == beast::zero - ? Number{} - : tenthBipsOfValue(rawPrincipalOutstanding, closeInterestRate); - - XRPL_ASSERT( - prepaymentPenalty >= 0, - "ripple::detail::computeFullPaymentInterest : valid prepayment " - "interest"); - - // Part of equation (27) from XLS-66 spec, Section A-2 Equation Glossary - return accruedInterest + prepaymentPenalty; -} - -Number -computeFullPaymentInterest( - Number const& periodicPayment, - Number const& periodicRate, - std::uint32_t paymentRemaining, - NetClock::time_point parentCloseTime, - std::uint32_t paymentInterval, - std::uint32_t prevPaymentDate, - std::uint32_t startDate, - TenthBips32 closeInterestRate) -{ - Number const rawPrincipalOutstanding = - detail::loanPrincipalFromPeriodicPayment( - periodicPayment, periodicRate, paymentRemaining); - - return computeFullPaymentInterest( - rawPrincipalOutstanding, - periodicRate, - parentCloseTime, - paymentInterval, - prevPaymentDate, - startDate, - closeInterestRate); -} - -/* Calculates the theoretical loan state at maximum precision for a given point - * in the amortization schedule. - * - * This function computes what the loan's outstanding balances should be based - * on the periodic payment amount and number of payments remaining, - * without considering any rounding that may have been applied to the actual - * Loan object's state. This "raw" (unrounded) state is used as a target for - * computing payment components and validating that the loan's tracked state - * hasn't drifted too far from the theoretical values. - * - * The raw state serves several purposes: - * 1. Computing the expected payment breakdown (principal, interest, fees) - * 2. Detecting and correcting rounding errors that accumulate over time - * 3. Validating that overpayments are calculated correctly - * 4. Ensuring the loan will be fully paid off at the end of its term - * - * If paymentRemaining is 0, returns a fully zeroed-out LoanState, - * representing a completely paid-off loan. - */ -LoanState -computeRawLoanState( - Number const& periodicPayment, - Number const& periodicRate, - std::uint32_t const paymentRemaining, - TenthBips32 const managementFeeRate) -{ - if (paymentRemaining == 0) - { - return LoanState{ - .valueOutstanding = 0, - .principalOutstanding = 0, - .interestDue = 0, - .managementFeeDue = 0}; - } - - // Equation (30) from XLS-66 spec, Section A-2 Equation Glossary - Number const rawTotalValueOutstanding = periodicPayment * paymentRemaining; - - Number const rawPrincipalOutstanding = - detail::loanPrincipalFromPeriodicPayment( - periodicPayment, periodicRate, paymentRemaining); - - // Equation (31) from XLS-66 spec, Section A-2 Equation Glossary - Number const rawInterestOutstandingGross = - rawTotalValueOutstanding - rawPrincipalOutstanding; - - // Equation (32) from XLS-66 spec, Section A-2 Equation Glossary - Number const rawManagementFeeOutstanding = - tenthBipsOfValue(rawInterestOutstandingGross, managementFeeRate); - - // Equation (33) from XLS-66 spec, Section A-2 Equation Glossary - Number const rawInterestOutstandingNet = - rawInterestOutstandingGross - rawManagementFeeOutstanding; - - return LoanState{ - .valueOutstanding = rawTotalValueOutstanding, - .principalOutstanding = rawPrincipalOutstanding, - .interestDue = rawInterestOutstandingNet, - .managementFeeDue = rawManagementFeeOutstanding}; -}; - -LoanState -computeRawLoanState( - Number const& periodicPayment, - TenthBips32 interestRate, - std::uint32_t paymentInterval, - std::uint32_t const paymentRemaining, - TenthBips32 const managementFeeRate) -{ - return computeRawLoanState( - periodicPayment, - loanPeriodicRate(interestRate, paymentInterval), - paymentRemaining, - managementFeeRate); -} - -/* Constructs a LoanState from rounded Loan ledger object values. - * - * This function creates a LoanState structure from the three tracked values - * stored in a Loan ledger object. Unlike calculateRawLoanState(), which - * computes theoretical unrounded values, this function works with values - * that have already been rounded to the loan's scale. - * - * The key difference from calculateRawLoanState(): - * - calculateRawLoanState: Computes theoretical values at full precision - * - constructRoundedLoanState: Builds state from actual rounded ledger values - * - * The interestDue field is derived from the other three values rather than - * stored directly, since it can be calculated as: - * interestDue = totalValueOutstanding - principalOutstanding - - * managementFeeOutstanding - * - * This ensures consistency across the codebase and prevents copy-paste errors - * when creating LoanState objects from Loan ledger data. - */ -LoanState -constructLoanState( - Number const& totalValueOutstanding, - Number const& principalOutstanding, - Number const& managementFeeOutstanding) -{ - // This implementation is pretty trivial, but ensures the calculations - // are consistent everywhere, and reduces copy/paste errors. - return LoanState{ - .valueOutstanding = totalValueOutstanding, - .principalOutstanding = principalOutstanding, - .interestDue = totalValueOutstanding - principalOutstanding - - managementFeeOutstanding, - .managementFeeDue = managementFeeOutstanding}; -} - -LoanState -constructRoundedLoanState(SLE::const_ref loan) -{ - return constructLoanState( - loan->at(sfTotalValueOutstanding), - loan->at(sfPrincipalOutstanding), - loan->at(sfManagementFeeOutstanding)); -} - -/* - * This function calculates the fee owed to the broker based on the asset, - * value, and management fee rate. - * - * Equation (32) from XLS-66 spec, Section A-2 Equation Glossary - */ -Number -computeManagementFee( - Asset const& asset, - Number const& value, - TenthBips32 managementFeeRate, - std::int32_t scale) -{ - return roundToAsset( - asset, - tenthBipsOfValue(value, managementFeeRate), - scale, - Number::downward); -} - -/* - * Given the loan parameters, compute the derived properties of the loan. - */ -LoanProperties -computeLoanProperties( - Asset const& asset, - Number principalOutstanding, - TenthBips32 interestRate, - std::uint32_t paymentInterval, - std::uint32_t paymentsRemaining, - TenthBips32 managementFeeRate, - std::int32_t minimumScale) -{ - auto const periodicRate = loanPeriodicRate(interestRate, paymentInterval); - XRPL_ASSERT( - interestRate == 0 || periodicRate > 0, - "ripple::computeLoanProperties : valid rate"); - - auto const periodicPayment = detail::loanPeriodicPayment( - principalOutstanding, periodicRate, paymentsRemaining); - - auto const [totalValueOutstanding, loanScale] = [&]() { - NumberRoundModeGuard mg(Number::to_nearest); - // Use STAmount's internal rounding instead of roundToAsset, because - // we're going to use this result to determine the scale for all the - // other rounding. - - // Equation (30) from XLS-66 spec, Section A-2 Equation Glossary - STAmount amount{asset, periodicPayment * paymentsRemaining}; - - // Base the loan scale on the total value, since that's going to be - // the biggest number involved (barring unusual parameters for late, - // full, or over payments) - auto const loanScale = std::max(minimumScale, amount.exponent()); - XRPL_ASSERT_PARTS( - (amount.integral() && loanScale == 0) || - (!amount.integral() && - loanScale >= static_cast(amount).exponent()), - "ripple::computeLoanProperties", - "loanScale value fits expectations"); - - // We may need to truncate the total value because of the minimum - // scale - amount = roundToAsset(asset, amount, loanScale, Number::to_nearest); - - return std::make_pair(amount, loanScale); - }(); - - // Since we just figured out the loan scale, we haven't been able to - // validate that the principal fits in it, so to allow this function to - // succeed, round it here, and let the caller do the validation. - principalOutstanding = roundToAsset( - asset, principalOutstanding, loanScale, Number::to_nearest); - - // E -loanMakePayment( - Asset const& asset, - ApplyView& view, - SLE::ref loan, - SLE::const_ref brokerSle, - STAmount const& amount, - LoanPaymentType const paymentType, - beast::Journal j) -{ - using namespace Lending; - - auto principalOutstandingProxy = loan->at(sfPrincipalOutstanding); - auto paymentRemainingProxy = loan->at(sfPaymentRemaining); - - if (paymentRemainingProxy == 0 || principalOutstandingProxy == 0) - { - // Loan complete this is already checked in LoanPay::preclaim() - // LCOV_EXCL_START - JLOG(j.warn()) << "Loan is already paid off."; - return Unexpected(tecKILLED); - // LCOV_EXCL_STOP - } - - auto totalValueOutstandingProxy = loan->at(sfTotalValueOutstanding); - auto managementFeeOutstandingProxy = loan->at(sfManagementFeeOutstanding); - - // Next payment due date must be set unless the loan is complete - auto nextDueDateProxy = loan->at(sfNextPaymentDueDate); - if (*nextDueDateProxy == 0) - { - JLOG(j.warn()) << "Loan next payment due date is not set."; - return Unexpected(tecINTERNAL); - } - - std::int32_t const loanScale = loan->at(sfLoanScale); - - TenthBips32 const interestRate{loan->at(sfInterestRate)}; - - Number const serviceFee = loan->at(sfLoanServiceFee); - TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; - - Number const periodicPayment = loan->at(sfPeriodicPayment); - - auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDate); - std::uint32_t const startDate = loan->at(sfStartDate); - - std::uint32_t const paymentInterval = loan->at(sfPaymentInterval); - - // Compute the periodic rate that will be used for calculations - // throughout - Number const periodicRate = loanPeriodicRate(interestRate, paymentInterval); - XRPL_ASSERT( - interestRate == 0 || periodicRate > 0, - "ripple::loanMakePayment : valid rate"); - - XRPL_ASSERT( - *totalValueOutstandingProxy > 0, - "ripple::loanMakePayment : valid total value"); - - view.update(loan); - - // ------------------------------------------------------------- - // A late payment not flagged as late overrides all other options. - if (paymentType != LoanPaymentType::late && - hasExpired(view, nextDueDateProxy)) - { - // If the payment is late, and the late flag was not set, it's not - // valid - JLOG(j.warn()) << "Loan payment is overdue. Use the tfLoanLatePayment " - "transaction " - "flag to make a late payment. Loan was created on " - << startDate << ", prev payment due date is " - << prevPaymentDateProxy << ", next payment due date is " - << nextDueDateProxy << ", ledger time is " - << view.parentCloseTime().time_since_epoch().count(); - return Unexpected(tecEXPIRED); - } - - // ------------------------------------------------------------- - // full payment handling - if (paymentType == LoanPaymentType::full) - { - TenthBips32 const closeInterestRate{loan->at(sfCloseInterestRate)}; - Number const closePaymentFee = - roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale); - - LoanState const roundedLoanState = constructLoanState( - totalValueOutstandingProxy, - principalOutstandingProxy, - managementFeeOutstandingProxy); - - if (auto const fullPaymentComponents = detail::computeFullPayment( - asset, - view, - principalOutstandingProxy, - managementFeeOutstandingProxy, - periodicPayment, - paymentRemainingProxy, - prevPaymentDateProxy, - startDate, - paymentInterval, - closeInterestRate, - loanScale, - roundedLoanState.interestDue, - periodicRate, - closePaymentFee, - amount, - managementFeeRate, - j)) - { - return doPayment( - *fullPaymentComponents, - totalValueOutstandingProxy, - principalOutstandingProxy, - managementFeeOutstandingProxy, - paymentRemainingProxy, - prevPaymentDateProxy, - nextDueDateProxy, - paymentInterval); - } - else if (fullPaymentComponents.error()) - // error() will be the TER returned if a payment is not made. It - // will only evaluate to true if it's unsuccessful. Otherwise, - // tesSUCCESS means nothing was done, so continue. - return Unexpected(fullPaymentComponents.error()); - - // LCOV_EXCL_START - UNREACHABLE("ripple::loanMakePayment : invalid full payment result"); - JLOG(j.error()) << "Full payment computation failed unexpectedly."; - return Unexpected(tecINTERNAL); - // LCOV_EXCL_STOP - } - - // ------------------------------------------------------------- - // compute the periodic payment info that will be needed whether the - // payment is late or regular - detail::ExtendedPaymentComponents periodic{ - detail::computePaymentComponents( - asset, - loanScale, - totalValueOutstandingProxy, - principalOutstandingProxy, - managementFeeOutstandingProxy, - periodicPayment, - periodicRate, - paymentRemainingProxy, - managementFeeRate), - serviceFee}; - XRPL_ASSERT_PARTS( - periodic.trackedPrincipalDelta >= 0, - "ripple::loanMakePayment", - "regular payment valid principal"); - - // ------------------------------------------------------------- - // late payment handling - if (paymentType == LoanPaymentType::late) - { - TenthBips32 const lateInterestRate{loan->at(sfLateInterestRate)}; - Number const latePaymentFee = loan->at(sfLatePaymentFee); - - if (auto const latePaymentComponents = detail::computeLatePayment( - asset, - view, - principalOutstandingProxy, - nextDueDateProxy, - periodic, - lateInterestRate, - loanScale, - latePaymentFee, - amount, - managementFeeRate, - j)) - { - return doPayment( - *latePaymentComponents, - totalValueOutstandingProxy, - principalOutstandingProxy, - managementFeeOutstandingProxy, - paymentRemainingProxy, - prevPaymentDateProxy, - nextDueDateProxy, - paymentInterval); - } - else if (latePaymentComponents.error()) - { - // error() will be the TER returned if a payment is not made. It - // will only evaluate to true if it's unsuccessful. - return Unexpected(latePaymentComponents.error()); - } - - // LCOV_EXCL_START - UNREACHABLE("ripple::loanMakePayment : invalid late payment result"); - JLOG(j.error()) << "Late payment computation failed unexpectedly."; - return Unexpected(tecINTERNAL); - // LCOV_EXCL_STOP - } - - // ------------------------------------------------------------- - // regular periodic payment handling - - XRPL_ASSERT_PARTS( - paymentType == LoanPaymentType::regular || - paymentType == LoanPaymentType::overpayment, - "ripple::loanMakePayment", - "regular payment type"); - - // Keep a running total of the actual parts paid - LoanPaymentParts totalParts; - Number totalPaid; - std::size_t numPayments = 0; - - while ((amount >= (totalPaid + periodic.totalDue)) && - paymentRemainingProxy > 0 && - numPayments < loanMaximumPaymentsPerTransaction) - { - // Try to make more payments - XRPL_ASSERT_PARTS( - periodic.trackedPrincipalDelta >= 0, - "ripple::loanMakePayment", - "payment pays non-negative principal"); - - totalPaid += periodic.totalDue; - totalParts += detail::doPayment( - periodic, - totalValueOutstandingProxy, - principalOutstandingProxy, - managementFeeOutstandingProxy, - paymentRemainingProxy, - prevPaymentDateProxy, - nextDueDateProxy, - paymentInterval); - ++numPayments; - - XRPL_ASSERT_PARTS( - (periodic.specialCase == detail::PaymentSpecialCase::final) == - (paymentRemainingProxy == 0), - "ripple::loanMakePayment", - "final payment is the final payment"); - - // Don't compute the next payment if this was the last payment - if (periodic.specialCase == detail::PaymentSpecialCase::final) - break; - - periodic = detail::ExtendedPaymentComponents{ - detail::computePaymentComponents( - asset, - loanScale, - totalValueOutstandingProxy, - principalOutstandingProxy, - managementFeeOutstandingProxy, - periodicPayment, - periodicRate, - paymentRemainingProxy, - managementFeeRate), - serviceFee}; - } - - if (numPayments == 0) - { - JLOG(j.warn()) << "Regular loan payment amount is insufficient. Due: " - << periodic.totalDue << ", paid: " << amount; - return Unexpected(tecINSUFFICIENT_PAYMENT); - } - - XRPL_ASSERT_PARTS( - totalParts.principalPaid + totalParts.interestPaid + - totalParts.feePaid == - totalPaid, - "ripple::loanMakePayment", - "payment parts add up"); - XRPL_ASSERT_PARTS( - totalParts.valueChange == 0, - "ripple::loanMakePayment", - "no value change"); - - // ------------------------------------------------------------- - // overpayment handling - if (paymentType == LoanPaymentType::overpayment && - loan->isFlag(lsfLoanOverpayment) && paymentRemainingProxy > 0 && - totalPaid < amount && numPayments < loanMaximumPaymentsPerTransaction) - { - TenthBips32 const overpaymentInterestRate{ - loan->at(sfOverpaymentInterestRate)}; - TenthBips32 const overpaymentFeeRate{loan->at(sfOverpaymentFee)}; - - // It shouldn't be possible for the overpayment to be greater than - // totalValueOutstanding, because that would have been processed as - // another normal payment. But cap it just in case. - Number const overpayment = - std::min(amount - totalPaid, *totalValueOutstandingProxy); - - detail::ExtendedPaymentComponents const overpaymentComponents = - detail::computeOverpaymentComponents( - asset, - loanScale, - overpayment, - overpaymentInterestRate, - overpaymentFeeRate, - managementFeeRate); - - // Don't process an overpayment if the whole amount (or more!) - // gets eaten by fees and interest. - if (overpaymentComponents.trackedPrincipalDelta > 0) - { - XRPL_ASSERT_PARTS( - overpaymentComponents.untrackedInterest >= beast::zero, - "ripple::loanMakePayment", - "overpayment penalty did not reduce value of loan"); - // Can't just use `periodicPayment` here, because it might - // change - auto periodicPaymentProxy = loan->at(sfPeriodicPayment); - if (auto const overResult = detail::doOverpayment( - asset, - loanScale, - overpaymentComponents, - totalValueOutstandingProxy, - principalOutstandingProxy, - managementFeeOutstandingProxy, - periodicPaymentProxy, - interestRate, - paymentInterval, - periodicRate, - paymentRemainingProxy, - prevPaymentDateProxy, - nextDueDateProxy, - managementFeeRate, - j)) - totalParts += *overResult; - else if (overResult.error()) - // error() will be the TER returned if a payment is not - // made. It will only evaluate to true if it's unsuccessful. - // Otherwise, tesSUCCESS means nothing was done, so - // continue. - return Unexpected(overResult.error()); - } - } - - // Check the final results are rounded, to double-check that the - // intermediate steps were rounded. - XRPL_ASSERT( - isRounded(asset, totalParts.principalPaid, loanScale) && - totalParts.principalPaid >= beast::zero, - "ripple::loanMakePayment : total principal paid is valid"); - XRPL_ASSERT( - isRounded(asset, totalParts.interestPaid, loanScale) && - totalParts.interestPaid >= beast::zero, - "ripple::loanMakePayment : total interest paid is valid"); - XRPL_ASSERT( - isRounded(asset, totalParts.valueChange, loanScale), - "ripple::loanMakePayment : loan value change is valid"); - XRPL_ASSERT( - isRounded(asset, totalParts.feePaid, loanScale) && - totalParts.feePaid >= beast::zero, - "ripple::loanMakePayment : fee paid is valid"); - return totalParts; -} -} // namespace ripple diff --git a/src/xrpld/app/tx/detail/Batch.cpp b/src/xrpld/app/tx/detail/Batch.cpp index 8bf0c7fe02..b233249170 100644 --- a/src/xrpld/app/tx/detail/Batch.cpp +++ b/src/xrpld/app/tx/detail/Batch.cpp @@ -263,8 +263,7 @@ Batch::preflight(PreflightContext const& ctx) return temREDUNDANT; } - auto const txType = stx.getFieldU16(sfTransactionType); - if (txType == ttBATCH) + if (stx.getFieldU16(sfTransactionType) == ttBATCH) { JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " << "batch cannot have an inner batch txn. " @@ -272,14 +271,6 @@ Batch::preflight(PreflightContext const& ctx) return temINVALID; } - if (std::any_of( - disabledTxTypes.begin(), - disabledTxTypes.end(), - [txType](auto const& disabled) { return txType == disabled; })) - { - return temINVALID_INNER_BATCH; - } - if (!(stx.getFlags() & tfInnerBatchTxn)) { JLOG(ctx.j.debug()) @@ -292,6 +283,7 @@ Batch::preflight(PreflightContext const& ctx) if (auto const ret = checkSignatureFields(stx, hash)) return ret; + /* Placeholder for field that will be added by Lending Protocol // Note that the CounterpartySignature is optional, and should not be // included, but if it is, ensure it doesn't contain a signature. if (stx.isFieldPresent(sfCounterpartySignature)) @@ -304,6 +296,7 @@ Batch::preflight(PreflightContext const& ctx) return ret; } } + */ auto const innerAccount = stx.getAccountID(sfAccount); if (auto const preflightResult = ripple::preflight( @@ -400,11 +393,13 @@ Batch::preflightSigValidated(PreflightContext const& ctx) // inner account to the required signers set. if (innerAccount != outerAccount) requiredSigners.insert(innerAccount); + /* Placeholder for field that will be added by Lending Protocol // Some transactions have a Counterparty, who must also sign the // transaction if they are not the outer account if (auto const counterparty = rb.at(~sfCounterparty); counterparty && counterparty != outerAccount) requiredSigners.insert(*counterparty); + */ } // Validation Batch Signers diff --git a/src/xrpld/app/tx/detail/Batch.h b/src/xrpld/app/tx/detail/Batch.h index 7889e91bdc..1b1d7614d5 100644 --- a/src/xrpld/app/tx/detail/Batch.h +++ b/src/xrpld/app/tx/detail/Batch.h @@ -35,24 +35,6 @@ public: TER doApply() override; - - static constexpr auto disabledTxTypes = std::to_array({ - ttVAULT_CREATE, - ttVAULT_SET, - ttVAULT_DELETE, - ttVAULT_DEPOSIT, - ttVAULT_WITHDRAW, - ttVAULT_CLAWBACK, - ttLOAN_BROKER_SET, - ttLOAN_BROKER_DELETE, - ttLOAN_BROKER_COVER_DEPOSIT, - ttLOAN_BROKER_COVER_WITHDRAW, - ttLOAN_BROKER_COVER_CLAWBACK, - ttLOAN_SET, - ttLOAN_DELETE, - ttLOAN_MANAGE, - ttLOAN_PAY, - }); }; } // namespace ripple diff --git a/src/xrpld/app/tx/detail/Change.cpp b/src/xrpld/app/tx/detail/Change.cpp index 43c6f7c619..43db9ae13c 100644 --- a/src/xrpld/app/tx/detail/Change.cpp +++ b/src/xrpld/app/tx/detail/Change.cpp @@ -19,11 +19,7 @@ NotTEC Transactor::invokePreflight(PreflightContext const& ctx) { // 0 means "Allow any flags" - // The check for tfChangeMask is gated by LendingProtocol because that - // feature introduced this parameter, and it's not worth adding another - // amendment just for this. - if (auto const ret = preflight0( - ctx, ctx.rules.enabled(featureLendingProtocol) ? tfChangeMask : 0)) + if (auto const ret = preflight0(ctx, 0)) return ret; auto account = ctx.tx.getAccountID(sfAccount); diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 27c87e074c..80c41f4d2b 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -70,8 +70,6 @@ enum Privilege { 0x0400, // The transaction MAY delete an MPT object. May not create. mustModifyVault = 0x0800, // The transaction must modify, delete or create, a vault - mayModifyVault = - 0x1000, // The transaction MAY modify, delete or create, a vault }; constexpr Privilege operator|(Privilege lhs, Privilege rhs) @@ -481,10 +479,10 @@ void AccountRootsDeletedClean::visitEntry( bool isDelete, std::shared_ptr const& before, - std::shared_ptr const& after) + std::shared_ptr const&) { if (isDelete && before && before->getType() == ltACCOUNT_ROOT) - accountsDeleted_.emplace_back(before, after); + accountsDeleted_.emplace_back(before); } bool @@ -501,8 +499,7 @@ AccountRootsDeletedClean::finalize( // be logged [[maybe_unused]] bool const enforce = view.rules().enabled(featureInvariantsV1_1) || - view.rules().enabled(featureSingleAssetVault) || - view.rules().enabled(featureLendingProtocol); + view.rules().enabled(featureSingleAssetVault); auto const objectExists = [&view, enforce, &j](auto const& keylet) { (void)enforce; @@ -532,33 +529,9 @@ AccountRootsDeletedClean::finalize( return false; }; - for (auto const& [before, after] : accountsDeleted_) + for (auto const& accountSLE : accountsDeleted_) { - auto const accountID = before->getAccountID(sfAccount); - // An account should not be deleted with a balance - if (after->at(sfBalance) != beast::zero) - { - JLOG(j.fatal()) << "Invariant failed: account deletion left " - "behind a non-zero balance"; - XRPL_ASSERT( - enforce, - "ripple::AccountRootsDeletedClean::finalize : " - "deleted account has zero balance"); - if (enforce) - return false; - } - // An account should not be deleted with a non-zero owner count - if (after->at(sfOwnerCount) != 0) - { - JLOG(j.fatal()) << "Invariant failed: account deletion left " - "behind a non-zero owner count"; - XRPL_ASSERT( - enforce, - "ripple::AccountRootsDeletedClean::finalize : " - "deleted account has zero owner count"); - if (enforce) - return false; - } + auto const accountID = accountSLE->getAccountID(sfAccount); // Simple types for (auto const& [keyletfunc, _, __] : directAccountKeylets) { @@ -585,9 +558,9 @@ AccountRootsDeletedClean::finalize( // also be deleted. e.g. AMM, Vault, etc. for (auto const& field : getPseudoAccountFields()) { - if (before->isFieldPresent(*field)) + if (accountSLE->isFieldPresent(*field)) { - auto const key = before->getFieldH256(*field); + auto const key = accountSLE->getFieldH256(*field); if (objectExists(keylet::unchecked(key)) && enforce) return false; } @@ -1046,9 +1019,7 @@ ValidNewAccountRoot::finalize( if (hasPrivilege(tx, createAcct | createPseudoAcct) && result == tesSUCCESS) { bool const pseudoAccount = - (pseudoAccount_ && - (view.rules().enabled(featureSingleAssetVault) || - view.rules().enabled(featureLendingProtocol))); + (pseudoAccount_ && view.rules().enabled(featureSingleAssetVault)); if (pseudoAccount && !hasPrivilege(tx, createPseudoAcct)) { @@ -1455,12 +1426,7 @@ ValidMPTIssuance::visitEntry( if (isDelete) mptokensDeleted_++; else if (!before) - { mptokensCreated_++; - MPTIssue const mptIssue{after->at(sfMPTokenIssuanceID)}; - if (mptIssue.getIssuer() == after->at(sfAccount)) - mptCreatedByIssuer_ = true; - } } } @@ -1474,25 +1440,6 @@ ValidMPTIssuance::finalize( { if (result == tesSUCCESS) { - auto const& rules = view.rules(); - [[maybe_unused]] - bool enforceCreatedByIssuer = rules.enabled(featureSingleAssetVault) || - rules.enabled(featureLendingProtocol); - if (mptCreatedByIssuer_) - { - JLOG(j.fatal()) - << "Invariant failed: MPToken created for the MPT issuer"; - // The comment above starting with "assert(enforce)" explains this - // assert. - XRPL_ASSERT_PARTS( - enforceCreatedByIssuer, - "ripple::ValidMPTIssuance::finalize", - "no issuer MPToken"); - if (enforceCreatedByIssuer) - return false; - } - - auto const txnType = tx.getTxnType(); if (hasPrivilege(tx, createMPTIssuance)) { if (mptIssuancesCreated_ == 0) @@ -1535,14 +1482,15 @@ ValidMPTIssuance::finalize( return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 1; } - bool const lendingProtocolEnabled = - view.rules().enabled(featureLendingProtocol); // ttESCROW_FINISH may authorize an MPT, but it can't have the // mayAuthorizeMPT privilege, because that may cause // non-amendment-gated side effects. - bool const enforceEscrowFinish = (txnType == ttESCROW_FINISH) && - (view.rules().enabled(featureSingleAssetVault) || - lendingProtocolEnabled); + bool const enforceEscrowFinish = (tx.getTxnType() == ttESCROW_FINISH) && + (view.rules().enabled(featureSingleAssetVault) + /* + TODO: Uncomment when LendingProtocol is defined + || view.rules().enabled(featureLendingProtocol)*/ + ); if (hasPrivilege(tx, mustAuthorizeMPT | mayAuthorizeMPT) || enforceEscrowFinish) { @@ -1560,14 +1508,6 @@ ValidMPTIssuance::finalize( "succeeded but deleted issuances"; return false; } - else if ( - lendingProtocolEnabled && - mptokensCreated_ + mptokensDeleted_ > 1) - { - JLOG(j.fatal()) << "Invariant failed: MPT authorize succeeded " - "but created/deleted bad number mptokens"; - return false; - } else if ( submittedByIssuer && (mptokensCreated_ > 0 || mptokensDeleted_ > 0)) @@ -1591,7 +1531,7 @@ ValidMPTIssuance::finalize( return true; } - if (txnType == ttESCROW_FINISH) + if (tx.getTxnType() == ttESCROW_FINISH) { // ttESCROW_FINISH may authorize an MPT, but it can't have the // mayAuthorizeMPT privilege, because that may cause @@ -1809,6 +1749,8 @@ ValidPseudoAccounts::finalize( beast::Journal const& j) { bool const enforce = view.rules().enabled(featureSingleAssetVault); + + // The comment above starting with "assert(enforce)" explains this assert. XRPL_ASSERT( errors_.empty() || enforce, "ripple::ValidPseudoAccounts::finalize : no bad " @@ -2217,401 +2159,6 @@ ValidAMM::finalize( //------------------------------------------------------------------------------ -void -NoModifiedUnmodifiableFields::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - if (isDelete || !before) - // Creation and deletion are ignored - return; - - 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) { - bool const beforeField = before->isFieldPresent(field); - bool const afterField = after->isFieldPresent(field); - return beforeField != afterField || - (afterField && 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, sfLedgerEntryType) || - fieldChanged(before, after, sfLedgerIndex) || - fieldChanged(before, after, sfSequence) || - fieldChanged(before, after, sfOwnerNode) || - fieldChanged(before, after, sfVaultNode) || - 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, sfLedgerEntryType) || - fieldChanged(before, after, sfLedgerIndex) || - fieldChanged(before, after, sfSequence) || - fieldChanged(before, after, sfOwnerNode) || - fieldChanged(before, after, sfLoanBrokerNode) || - 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, sfStartDate) || - fieldChanged(before, after, sfPaymentInterval) || - fieldChanged(before, after, sfGracePeriod) || - fieldChanged(before, after, sfLoanScale); - break; - default: - /* - * We check this invariant regardless of lending protocol - * amendment status, allowing for detection and logging of - * potential issues even when the amendment is disabled. - * - * We use the lending protocol as a gate, even though - * all transactions are affected because that's when it - * was added. - */ - enforce = view.rules().enabled(featureLendingProtocol); - bad = fieldChanged(before, after, sfLedgerEntryType) || - fieldChanged(before, after, sfLedgerIndex); - } - 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; -} - -//------------------------------------------------------------------------------ - -void -ValidLoanBroker::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - if (after) - { - if (after->getType() == ltLOAN_BROKER) - { - auto& broker = brokers_[after->key()]; - broker.brokerBefore = before; - broker.brokerAfter = after; - } - else if ( - after->getType() == ltACCOUNT_ROOT && - after->isFieldPresent(sfLoanBrokerID)) - { - auto const& loanBrokerID = after->at(sfLoanBrokerID); - // create an entry if one doesn't already exist - brokers_.emplace(loanBrokerID, BrokerInfo{}); - } - else if (after->getType() == ltRIPPLE_STATE) - { - lines_.emplace_back(after); - } - else if (after->getType() == ltMPTOKEN) - { - mpts_.emplace_back(after); - } - } -} - -bool -ValidLoanBroker::goodZeroDirectory( - ReadView const& view, - SLE::const_ref dir, - beast::Journal const& j) const -{ - auto const next = dir->at(~sfIndexNext); - auto const prev = dir->at(~sfIndexPrevious); - if ((prev && *prev) || (next && *next)) - { - JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero " - "OwnerCount has multiple directory pages"; - return false; - } - auto indexes = dir->getFieldV256(sfIndexes); - if (indexes.size() > 1) - { - JLOG(j.fatal()) - << "Invariant failed: Loan Broker with zero " - "OwnerCount has multiple indexes in the Directory root"; - return false; - } - if (indexes.size() == 1) - { - auto const index = indexes.value().front(); - auto const sle = view.read(keylet::unchecked(index)); - if (!sle) - { - JLOG(j.fatal()) - << "Invariant failed: Loan Broker directory corrupt"; - return false; - } - if (sle->getType() != ltRIPPLE_STATE && sle->getType() != ltMPTOKEN) - { - JLOG(j.fatal()) - << "Invariant failed: Loan Broker with zero " - "OwnerCount has an unexpected entry in the directory"; - return false; - } - } - - return true; -} - -bool -ValidLoanBroker::finalize( - STTx const& tx, - TER const, - XRPAmount const, - ReadView const& view, - beast::Journal const& j) -{ - // Loan Brokers will not exist on ledger if the Lending Protocol amendment - // is not enabled, so there's no need to check it. - - for (auto const& line : lines_) - { - for (auto const& field : {&sfLowLimit, &sfHighLimit}) - { - auto const account = - view.read(keylet::account(line->at(*field).getIssuer())); - // This Invariant doesn't know about the rules for Trust Lines, so - // if the account is missing, don't treat it as an error. This - // loop is only concerned with finding Broker pseudo-accounts - if (account && account->isFieldPresent(sfLoanBrokerID)) - { - auto const& loanBrokerID = account->at(sfLoanBrokerID); - // create an entry if one doesn't already exist - brokers_.emplace(loanBrokerID, BrokerInfo{}); - } - } - } - for (auto const& mpt : mpts_) - { - auto const account = view.read(keylet::account(mpt->at(sfAccount))); - // This Invariant doesn't know about the rules for MPTokens, so - // if the account is missing, don't treat is as an error. This - // loop is only concerned with finding Broker pseudo-accounts - if (account && account->isFieldPresent(sfLoanBrokerID)) - { - auto const& loanBrokerID = account->at(sfLoanBrokerID); - // create an entry if one doesn't already exist - brokers_.emplace(loanBrokerID, BrokerInfo{}); - } - } - - for (auto const& [brokerID, broker] : brokers_) - { - auto const& after = broker.brokerAfter - ? broker.brokerAfter - : view.read(keylet::loanbroker(brokerID)); - - if (!after) - { - JLOG(j.fatal()) << "Invariant failed: Loan Broker missing"; - return false; - } - - auto const& before = broker.brokerBefore; - - // https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3123-invariants - // If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most - // one node (the root), which will only hold entries for `RippleState` - // or `MPToken` objects. - if (after->at(sfOwnerCount) == 0) - { - auto const dir = view.read(keylet::ownerDir(after->at(sfAccount))); - if (dir) - { - if (!goodZeroDirectory(view, dir, j)) - { - return false; - } - } - } - if (before && before->at(sfLoanSequence) > after->at(sfLoanSequence)) - { - JLOG(j.fatal()) << "Invariant failed: Loan Broker sequence number " - "decreased"; - return false; - } - if (after->at(sfDebtTotal) < 0) - { - JLOG(j.fatal()) - << "Invariant failed: Loan Broker debt total is negative"; - return false; - } - if (after->at(sfCoverAvailable) < 0) - { - JLOG(j.fatal()) - << "Invariant failed: Loan Broker cover available is negative"; - return false; - } - auto const vault = view.read(keylet::vault(after->at(sfVaultID))); - if (!vault) - { - JLOG(j.fatal()) - << "Invariant failed: Loan Broker vault ID is invalid"; - return false; - } - auto const& vaultAsset = vault->at(sfAsset); - if (after->at(sfCoverAvailable) < accountHolds( - view, - after->at(sfAccount), - vaultAsset, - FreezeHandling::fhIGNORE_FREEZE, - AuthHandling::ahIGNORE_AUTH, - j)) - { - JLOG(j.fatal()) << "Invariant failed: Loan Broker cover available " - "is less than pseudo-account asset balance"; - return false; - } - } - return true; -} - -//------------------------------------------------------------------------------ - -void -ValidLoan::visitEntry( - bool isDelete, - std::shared_ptr const& before, - std::shared_ptr const& after) -{ - if (after && after->getType() == ltLOAN) - { - loans_.emplace_back(before, after); - } -} - -bool -ValidLoan::finalize( - STTx const& tx, - TER const, - XRPAmount const, - ReadView const& view, - beast::Journal const& j) -{ - // Loans will not exist on ledger if the Lending Protocol amendment - // is not enabled, so there's no need to check it. - - for (auto const& [before, after] : loans_) - { - // https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3223-invariants - // If `Loan.PaymentRemaining = 0` then the loan MUST be fully paid off - if (after->at(sfPaymentRemaining) == 0 && - (after->at(sfTotalValueOutstanding) != beast::zero || - after->at(sfPrincipalOutstanding) != beast::zero || - after->at(sfManagementFeeOutstanding) != beast::zero)) - { - JLOG(j.fatal()) << "Invariant failed: Loan with zero payments " - "remaining has not been paid off"; - return false; - } - // If `Loan.PaymentRemaining != 0` then the loan MUST NOT be fully paid - // off - if (after->at(sfPaymentRemaining) != 0 && - after->at(sfTotalValueOutstanding) == beast::zero && - after->at(sfPrincipalOutstanding) == beast::zero && - after->at(sfManagementFeeOutstanding) == beast::zero) - { - JLOG(j.fatal()) << "Invariant failed: Loan with zero payments " - "remaining has not been paid off"; - return false; - } - if (before && - (before->isFlag(lsfLoanOverpayment) != - after->isFlag(lsfLoanOverpayment))) - { - JLOG(j.fatal()) - << "Invariant failed: Loan Overpayment flag changed"; - return false; - } - // Must not be negative - STNumber - for (auto const field : - {&sfLoanServiceFee, - &sfLatePaymentFee, - &sfClosePaymentFee, - &sfPrincipalOutstanding, - &sfTotalValueOutstanding, - &sfManagementFeeOutstanding}) - { - if (after->at(*field) < 0) - { - JLOG(j.fatal()) << "Invariant failed: " << field->getName() - << " is negative "; - return false; - } - } - // Must be positive - STNumber - for (auto const field : { - &sfPeriodicPayment, - }) - { - if (after->at(*field) <= 0) - { - JLOG(j.fatal()) << "Invariant failed: " << field->getName() - << " is zero or negative "; - return false; - } - } - } - return true; -} - ValidVault::Vault ValidVault::Vault::make(SLE const& from) { @@ -2761,8 +2308,7 @@ ValidVault::finalize( return true; // Not a vault operation } - else if (!(hasPrivilege(tx, mustModifyVault) || - hasPrivilege(tx, mayModifyVault))) + else if (!hasPrivilege(tx, mustModifyVault)) // TODO: mayModifyVault { JLOG(j.fatal()) << // "Invariant failed: vault updated by a wrong transaction type"; @@ -2977,8 +2523,7 @@ ValidVault::finalize( } if (!beforeVault_.empty() && - afterVault.lossUnrealized != beforeVault_[0].lossUnrealized && - txnType != ttLOAN_MANAGE && txnType != ttLOAN_PAY) + afterVault.lossUnrealized != beforeVault_[0].lossUnrealized) { JLOG(j.fatal()) << // "Invariant failed: vault transaction must not change loss " @@ -3526,13 +3071,6 @@ ValidVault::finalize( return result; } - case ttLOAN_SET: - case ttLOAN_MANAGE: - case ttLOAN_PAY: { - // TBD - return true; - } - default: // LCOV_EXCL_START UNREACHABLE( diff --git a/src/xrpld/app/tx/detail/InvariantCheck.h b/src/xrpld/app/tx/detail/InvariantCheck.h index 09a84b1ab4..f9b37f03ed 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.h +++ b/src/xrpld/app/tx/detail/InvariantCheck.h @@ -158,14 +158,7 @@ public: */ class AccountRootsDeletedClean { - // Pair is . Before is used for most of the checks, so that - // if, for example, an object ID field is cleared, but the object is not - // deleted, it can still be found. After is used specifically for any checks - // that are expected as part of the deletion, such as zeroing out the - // balance. - std::vector< - std::pair, std::shared_ptr>> - accountsDeleted_; + std::vector> accountsDeleted_; public: void @@ -557,9 +550,6 @@ class ValidMPTIssuance std::uint32_t mptokensCreated_ = 0; std::uint32_t mptokensDeleted_ = 0; - // non-MPT transactions may attempt to create - // MPToken by an issuer - bool mptCreatedByIssuer_ = false; public: void @@ -726,114 +716,6 @@ private: }; /** - * @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 -{ - // Pair is . - 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&); -}; - -/** - * @brief Invariants: Loan brokers are internally consistent - * - * 1. If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most one - * node (the root), which will only hold entries for `RippleState` or - * `MPToken` objects. - * - */ -class ValidLoanBroker -{ - // Not all of these elements will necessarily be populated. Remaining items - // will be looked up as needed. - struct BrokerInfo - { - SLE::const_pointer brokerBefore = nullptr; - // After is used for most of the checks, except - // those that check changed values. - SLE::const_pointer brokerAfter = nullptr; - }; - // Collect all the LoanBrokers found directly or indirectly through - // pseudo-accounts. Key is the brokerID / index. It will be used to find the - // LoanBroker object if brokerBefore and brokerAfter are nullptr - std::map brokers_; - // Collect all the modified trust lines. Their high and low accounts will be - // loaded to look for LoanBroker pseudo-accounts. - std::vector lines_; - // Collect all the modified MPTokens. Their accounts will be loaded to look - // for LoanBroker pseudo-accounts. - std::vector mpts_; - - bool - goodZeroDirectory( - ReadView const& view, - SLE::const_ref dir, - beast::Journal const& j) const; - -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&); -}; - -/** - * @brief Invariants: Loans are internally consistent - * - * 1. If `Loan.PaymentRemaining = 0` then `Loan.PrincipalOutstanding = 0` - * - */ -class ValidLoan -{ - // Pair is . After is used for most of the checks, except - // those that check changed values. - std::vector> loans_; - -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&); -}; - -/* * @brief Invariants: Vault object and MPTokenIssuance for vault shares * * - vault deleted and vault created is empty @@ -923,10 +805,7 @@ using InvariantChecks = std::tuple< ValidPermissionedDomain, ValidPermissionedDEX, ValidAMM, - NoModifiedUnmodifiableFields, ValidPseudoAccounts, - ValidLoanBroker, - ValidLoan, ValidVault>; /** diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp deleted file mode 100644 index 26e978697c..0000000000 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp +++ /dev/null @@ -1,340 +0,0 @@ -#include -// -#include - -namespace ripple { - -bool -LoanBrokerCoverClawback::checkExtraFeatures(PreflightContext const& ctx) -{ - return checkLendingProtocolDependencies(ctx); -} - -NotTEC -LoanBrokerCoverClawback::preflight(PreflightContext const& ctx) -{ - auto const brokerID = ctx.tx[~sfLoanBrokerID]; - auto const amount = ctx.tx[~sfAmount]; - - if (!brokerID && !amount) - return temINVALID; - - if (brokerID && *brokerID == beast::zero) - return temINVALID; - - if (amount) - { - // XRP has no counterparty, and thus nobody can claw it back - if (amount->native()) - return temBAD_AMOUNT; - - // Zero is OK, and indicates "take it all" (down to the minimum cover) - if (*amount < beast::zero) - return temBAD_AMOUNT; - - // This should be redundant - if (!isLegalNet(*amount)) - return temBAD_AMOUNT; // LCOV_EXCL_LINE - - if (!brokerID) - { - if (amount->holds()) - return temINVALID; - - auto const account = ctx.tx[sfAccount]; - // Since we don't have a LoanBrokerID, holder _should_ be the loan - // broker's pseudo-account, but we don't know yet whether it is, so - // use a generic placeholder name. - auto const holder = amount->getIssuer(); - if (holder == account || holder == beast::zero) - return temINVALID; - } - } - - return tesSUCCESS; -} - -Expected -determineBrokerID(ReadView const& view, STTx const& tx) -{ - // If the broker ID was provided in the transaction, that's all we - // need. - if (auto const brokerID = tx[~sfLoanBrokerID]) - return *brokerID; - - // If the broker ID was not provided, and the amount is either - // absent or holds a non-IOU - including MPT, something went wrong, - // because that should have been rejected in preflight(). - auto const dstAmount = tx[~sfAmount]; - if (!dstAmount || !dstAmount->holds()) - return Unexpected{tecINTERNAL}; // LCOV_EXCL_LINE - - // Every trust line is bidirectional. Both sides are simultaneously - // issuer and holder. For this transaction, the Account is acting as - // a holder, and clawing back funds from the LoanBroker - // Pseudo-account acting as holder. If the Amount is an IOU, and the - // `issuer` field specified in that Amount is a LoanBroker - // Pseudo-account, we can get the LoanBrokerID from there. - // - // Thus, Amount.issuer _should_ be the loan broker's - // pseudo-account, but we don't know yet whether it is. - auto const maybePseudo = dstAmount->getIssuer(); - auto const sle = view.read(keylet::account(maybePseudo)); - - // If the account was not found, the transaction can't go further. - if (!sle) - return Unexpected{tecNO_ENTRY}; - - // If the account was found, and has a LoanBrokerID (and therefore - // is a pseudo-account), that's the - // answer we need. - if (auto const brokerID = sle->at(~sfLoanBrokerID)) - return *brokerID; - - // If the account does not have a LoanBrokerID, the transaction - // can't go further, even if it's a different type of Pseudo-account. - return Unexpected{tecOBJECT_NOT_FOUND}; - // Or tecWRONG_ASSET? -} - -Expected -determineAsset( - ReadView const& view, - AccountID const& account, - AccountID const& brokerPseudoAccountID, - STAmount const& amount) -{ - if (amount.holds()) - return amount.asset(); - - // An IOU has an issue, which could be either end of the trust line. - // This check only applies to IOUs - auto const holder = amount.getIssuer(); - - // holder can be the submitting account (the issuer of the asset) if a - // LoanBrokerID was provided in the transaction. - if (holder == account) - { - return amount.asset(); - } - else if (holder == brokerPseudoAccountID) - { - // We want the asset to match the vault asset, so use the account as the - // issuer - return Issue{amount.getCurrency(), account}; - } - else - return Unexpected(tecWRONG_ASSET); -} - -Expected -determineClawAmount( - SLE const& sleBroker, - Asset const& vaultAsset, - std::optional const& amount) -{ - auto const maxClawAmount = [&]() { - // Always round the minimum required up - NumberRoundModeGuard mg1(Number::upward); - auto const minRequiredCover = tenthBipsOfValue( - sleBroker[sfDebtTotal], TenthBips32(sleBroker[sfCoverRateMinimum])); - // The subtraction probably won't round, but round down if it does. - NumberRoundModeGuard mg2(Number::downward); - return sleBroker[sfCoverAvailable] - minRequiredCover; - }(); - if (maxClawAmount <= beast::zero) - return Unexpected(tecINSUFFICIENT_FUNDS); - - // Use the vaultAsset here, because it will be the right type in all - // circumstances. The amount may be an IOU indicating the pseudo-account's - // asset, which is correct, but not what is needed here. - if (!amount || *amount == beast::zero) - return STAmount{vaultAsset, maxClawAmount}; - Number const magnitude{*amount}; - if (magnitude > maxClawAmount) - return STAmount{vaultAsset, maxClawAmount}; - return STAmount{vaultAsset, magnitude}; -} - -template -static TER -preclaimHelper( - PreclaimContext const& ctx, - SLE const& sleIssuer, - STAmount const& clawAmount); - -template <> -TER -preclaimHelper( - PreclaimContext const& ctx, - SLE const& sleIssuer, - STAmount const& clawAmount) -{ - // If AllowTrustLineClawback is not set or NoFreeze is set, return no - // permission - if (!(sleIssuer.isFlag(lsfAllowTrustLineClawback)) || - (sleIssuer.isFlag(lsfNoFreeze))) - return tecNO_PERMISSION; - - return tesSUCCESS; -} - -template <> -TER -preclaimHelper( - PreclaimContext const& ctx, - SLE const& sleIssuer, - STAmount const& clawAmount) -{ - auto const issuanceKey = - keylet::mptIssuance(clawAmount.get().getMptID()); - auto const sleIssuance = ctx.view.read(issuanceKey); - if (!sleIssuance) - return tecOBJECT_NOT_FOUND; - - if (!sleIssuance->isFlag(lsfMPTCanClawback)) - return tecNO_PERMISSION; - - // With all the checking already done, this should be impossible - if (sleIssuance->at(sfIssuer) != sleIssuer[sfAccount]) - return tecINTERNAL; // LCOV_EXCL_LINE - - return tesSUCCESS; -} - -TER -LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx) -{ - auto const& tx = ctx.tx; - - auto const account = tx[sfAccount]; - auto const findBrokerID = determineBrokerID(ctx.view, tx); - if (!findBrokerID) - return findBrokerID.error(); - auto const brokerID = *findBrokerID; - auto const amount = tx[~sfAmount]; - - auto const sleBroker = ctx.view.read(keylet::loanbroker(brokerID)); - if (!sleBroker) - { - JLOG(ctx.j.warn()) << "LoanBroker does not exist."; - return tecNO_ENTRY; - } - - auto const brokerPseudoAccountID = sleBroker->at(sfAccount); - - auto const vault = ctx.view.read(keylet::vault(sleBroker->at(sfVaultID))); - if (!vault) - { - // LCOV_EXCL_START - JLOG(ctx.j.fatal()) << "Vault is missing for Broker " << brokerID; - return tefBAD_LEDGER; - // LCOV_EXCL_STOP - } - - auto const vaultAsset = vault->at(sfAsset); - - if (vaultAsset.native()) - { - JLOG(ctx.j.warn()) << "Cannot clawback native asset."; - return tecNO_PERMISSION; - } - - // Only the issuer of the vault asset can claw it back from the broker's - // cover funds. - if (vaultAsset.getIssuer() != account) - { - JLOG(ctx.j.warn()) << "Account is not the issuer of the vault asset."; - return tecNO_PERMISSION; - } - - if (amount) - { - auto const findAsset = - determineAsset(ctx.view, account, brokerPseudoAccountID, *amount); - if (!findAsset) - return findAsset.error(); - auto const txAsset = *findAsset; - if (txAsset != vaultAsset) - { - JLOG(ctx.j.warn()) << "Account is the correct issuer, but trying " - "to clawback the wrong asset from LoanBroker"; - return tecWRONG_ASSET; - } - } - - auto const findClawAmount = - determineClawAmount(*sleBroker, vaultAsset, amount); - if (!findClawAmount) - { - JLOG(ctx.j.warn()) << "LoanBroker cover is already at minimum."; - return findClawAmount.error(); - } - STAmount const clawAmount = *findClawAmount; - - // Explicitly check the balance of the trust line / MPT to make sure the - // balance is actually there. It should always match `sfCoverAvailable`, so - // if there isn't, this is an internal error. - if (accountHolds( - ctx.view, - brokerPseudoAccountID, - vaultAsset, - fhIGNORE_FREEZE, - ahIGNORE_AUTH, - ctx.j) < clawAmount) - return tecINTERNAL; // tecINSUFFICIENT_FUNDS; LCOV_EXCL_LINE - - // Check if the vault asset issuer has the correct flags - auto const sleIssuer = - ctx.view.read(keylet::account(vaultAsset.getIssuer())); - return std::visit( - [&](T const&) { - return preclaimHelper(ctx, *sleIssuer, clawAmount); - }, - vaultAsset.value()); -} - -TER -LoanBrokerCoverClawback::doApply() -{ - auto const& tx = ctx_.tx; - auto const account = tx[sfAccount]; - auto const findBrokerID = determineBrokerID(view(), tx); - if (!findBrokerID) - return tecINTERNAL; // LCOV_EXCL_LINE - auto const brokerID = *findBrokerID; - auto const amount = tx[~sfAmount]; - - auto sleBroker = view().peek(keylet::loanbroker(brokerID)); - if (!sleBroker) - return tecINTERNAL; // LCOV_EXCL_LINE - - auto const brokerPseudoID = *sleBroker->at(sfAccount); - - auto const vault = view().read(keylet::vault(sleBroker->at(sfVaultID))); - if (!vault) - return tecINTERNAL; // LCOV_EXCL_LINE - - auto const vaultAsset = vault->at(sfAsset); - - auto const findClawAmount = - determineClawAmount(*sleBroker, vaultAsset, amount); - if (!findClawAmount) - return tecINTERNAL; // LCOV_EXCL_LINE - STAmount const clawAmount = *findClawAmount; - // Just for paranoia's sake - if (clawAmount.native()) - return tecINTERNAL; // LCOV_EXCL_LINE - - // Decrease the LoanBroker's CoverAvailable by Amount - sleBroker->at(sfCoverAvailable) -= clawAmount; - view().update(sleBroker); - - // Transfer assets from pseudo-account to depositor. - return accountSend( - view(), brokerPseudoID, account, clawAmount, j_, WaiveTransferFee::Yes); -} - -//------------------------------------------------------------------------------ - -} // namespace ripple diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.h b/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.h deleted file mode 100644 index 183d3c4479..0000000000 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverClawback.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef XRPL_TX_LOANBROKERCOVERCLAWBACK_H_INCLUDED -#define XRPL_TX_LOANBROKERCOVERCLAWBACK_H_INCLUDED - -#include - -namespace ripple { - -class LoanBrokerCoverClawback : public Transactor -{ -public: - static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; - - explicit LoanBrokerCoverClawback(ApplyContext& ctx) : Transactor(ctx) - { - } - - static bool - checkExtraFeatures(PreflightContext const& ctx); - - static NotTEC - preflight(PreflightContext const& ctx); - - static TER - preclaim(PreclaimContext const& ctx); - - TER - doApply() override; -}; - -//------------------------------------------------------------------------------ - -} // namespace ripple - -#endif diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp deleted file mode 100644 index 4e9e0e9c05..0000000000 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp +++ /dev/null @@ -1,123 +0,0 @@ -#include -// -#include - -namespace ripple { - -bool -LoanBrokerCoverDeposit::checkExtraFeatures(PreflightContext const& ctx) -{ - return checkLendingProtocolDependencies(ctx); -} - -NotTEC -LoanBrokerCoverDeposit::preflight(PreflightContext const& ctx) -{ - if (ctx.tx[sfLoanBrokerID] == beast::zero) - return temINVALID; - - auto const dstAmount = ctx.tx[sfAmount]; - if (dstAmount <= beast::zero) - return temBAD_AMOUNT; - - if (!isLegalNet(dstAmount)) - return temBAD_AMOUNT; - - return tesSUCCESS; -} - -TER -LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx) -{ - auto const& tx = ctx.tx; - - auto const account = tx[sfAccount]; - auto const brokerID = tx[sfLoanBrokerID]; - auto const amount = tx[sfAmount]; - - auto const sleBroker = ctx.view.read(keylet::loanbroker(brokerID)); - if (!sleBroker) - { - JLOG(ctx.j.warn()) << "LoanBroker does not exist."; - return tecNO_ENTRY; - } - if (account != sleBroker->at(sfOwner)) - { - JLOG(ctx.j.warn()) << "Account is not the owner of the LoanBroker."; - return tecNO_PERMISSION; - } - auto const vault = ctx.view.read(keylet::vault(sleBroker->at(sfVaultID))); - if (!vault) - { - // LCOV_EXCL_START - JLOG(ctx.j.fatal()) << "Vault is missing for Broker " << brokerID; - return tefBAD_LEDGER; - // LCOV_EXCL_STOP - } - - auto const vaultAsset = vault->at(sfAsset); - if (amount.asset() != vaultAsset) - return tecWRONG_ASSET; - - auto const pseudoAccountID = sleBroker->at(sfAccount); - // Cannot transfer a non-transferable Asset - if (auto const ret = - canTransfer(ctx.view, vaultAsset, account, pseudoAccountID)) - return ret; - // Cannot transfer a frozen Asset - if (auto const ret = checkFrozen(ctx.view, account, vaultAsset)) - return ret; - // Pseudo-account cannot receive if asset is deep frozen - if (auto const ret = checkDeepFrozen(ctx.view, pseudoAccountID, vaultAsset)) - return ret; - // Cannot transfer unauthorized asset - if (auto const ret = - requireAuth(ctx.view, vaultAsset, account, AuthType::StrongAuth)) - return ret; - - if (accountHolds( - ctx.view, - account, - vaultAsset, - FreezeHandling::fhZERO_IF_FROZEN, - AuthHandling::ahZERO_IF_UNAUTHORIZED, - ctx.j) < amount) - return tecINSUFFICIENT_FUNDS; - - return tesSUCCESS; -} - -TER -LoanBrokerCoverDeposit::doApply() -{ - auto const& tx = ctx_.tx; - - auto const brokerID = tx[sfLoanBrokerID]; - auto const amount = tx[sfAmount]; - - auto broker = view().peek(keylet::loanbroker(brokerID)); - if (!broker) - return tecINTERNAL; // LCOV_EXCL_LINE - - auto const brokerPseudoID = broker->at(sfAccount); - - // Transfer assets from depositor to pseudo-account. - if (auto ter = accountSend( - view(), - account_, - brokerPseudoID, - amount, - j_, - WaiveTransferFee::Yes)) - return ter; - - // Increase the LoanBroker's CoverAvailable by Amount - broker->at(sfCoverAvailable) += amount; - view().update(broker); - - return tesSUCCESS; -} - -//------------------------------------------------------------------------------ - -} // namespace ripple diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.h b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.h deleted file mode 100644 index 23863b479c..0000000000 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef XRPL_TX_LOANBROKERCOVERDEPOSIT_H_INCLUDED -#define XRPL_TX_LOANBROKERCOVERDEPOSIT_H_INCLUDED - -#include - -namespace ripple { - -class LoanBrokerCoverDeposit : public Transactor -{ -public: - static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; - - explicit LoanBrokerCoverDeposit(ApplyContext& ctx) : Transactor(ctx) - { - } - - static bool - checkExtraFeatures(PreflightContext const& ctx); - - static NotTEC - preflight(PreflightContext const& ctx); - - static TER - preclaim(PreclaimContext const& ctx); - - TER - doApply() override; -}; - -//------------------------------------------------------------------------------ - -} // namespace ripple - -#endif diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp deleted file mode 100644 index 1fd5a1a471..0000000000 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp +++ /dev/null @@ -1,173 +0,0 @@ -#include -// -#include -#include - -#include - -namespace ripple { - -bool -LoanBrokerCoverWithdraw::checkExtraFeatures(PreflightContext const& ctx) -{ - return checkLendingProtocolDependencies(ctx); -} - -NotTEC -LoanBrokerCoverWithdraw::preflight(PreflightContext const& ctx) -{ - if (ctx.tx[sfLoanBrokerID] == beast::zero) - return temINVALID; - - auto const dstAmount = ctx.tx[sfAmount]; - if (dstAmount <= beast::zero) - return temBAD_AMOUNT; - - if (!isLegalNet(dstAmount)) - return temBAD_AMOUNT; - - if (auto const destination = ctx.tx[~sfDestination]) - { - if (*destination == beast::zero) - { - return temMALFORMED; - } - } - - return tesSUCCESS; -} - -TER -LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) -{ - auto const& tx = ctx.tx; - - auto const account = tx[sfAccount]; - auto const brokerID = tx[sfLoanBrokerID]; - auto const amount = tx[sfAmount]; - - auto const dstAcct = tx[~sfDestination].value_or(account); - - auto const sleBroker = ctx.view.read(keylet::loanbroker(brokerID)); - if (!sleBroker) - { - JLOG(ctx.j.warn()) << "LoanBroker does not exist."; - return tecNO_ENTRY; - } - if (account != sleBroker->at(sfOwner)) - { - JLOG(ctx.j.warn()) << "Account is not the owner of the LoanBroker."; - return tecNO_PERMISSION; - } - auto const vault = ctx.view.read(keylet::vault(sleBroker->at(sfVaultID))); - if (!vault) - { - // LCOV_EXCL_START - JLOG(ctx.j.fatal()) << "Vault is missing for Broker " << brokerID; - return tefBAD_LEDGER; - // LCOV_EXCL_STOP - } - - auto const vaultAsset = vault->at(sfAsset); - if (amount.asset() != vaultAsset) - return tecWRONG_ASSET; - - // The broker's pseudo-account is the source of funds. - auto const pseudoAccountID = sleBroker->at(sfAccount); - // Cannot transfer a non-transferable Asset - if (auto const ret = - canTransfer(ctx.view, vaultAsset, pseudoAccountID, dstAcct)) - return ret; - - // Withdrawal to a 3rd party destination account is essentially a transfer. - // Enforce all the usual asset transfer checks. - AuthType authType = AuthType::WeakAuth; - if (account != dstAcct) - { - if (auto const ret = canWithdraw(ctx.view, tx)) - return ret; - - // The destination account must have consented to receive the asset by - // creating a RippleState or MPToken - authType = AuthType::StrongAuth; - } - - // Destination MPToken must exist (if asset is an MPT) - if (auto const ter = requireAuth(ctx.view, vaultAsset, dstAcct, authType)) - return ter; - - // Check for freezes, unless sending directly to the issuer - if (dstAcct != vaultAsset.getIssuer()) - { - // Cannot send a frozen Asset - if (auto const ret = checkFrozen(ctx.view, pseudoAccountID, vaultAsset)) - return ret; - // Destination account cannot receive if asset is deep frozen - if (auto const ret = checkDeepFrozen(ctx.view, dstAcct, vaultAsset)) - return ret; - } - - auto const coverAvail = sleBroker->at(sfCoverAvailable); - // Cover Rate is in 1/10 bips units - auto const currentDebtTotal = sleBroker->at(sfDebtTotal); - auto const minimumCover = [&]() { - // Always round the minimum required up. - // Applies to `tenthBipsOfValue` as well as `roundToAsset`. - NumberRoundModeGuard mg(Number::upward); - return roundToAsset( - vaultAsset, - tenthBipsOfValue( - currentDebtTotal, - TenthBips32(sleBroker->at(sfCoverRateMinimum))), - currentDebtTotal.exponent()); - }(); - if (coverAvail < amount) - return tecINSUFFICIENT_FUNDS; - if ((coverAvail - amount) < minimumCover) - return tecINSUFFICIENT_FUNDS; - - if (accountHolds( - ctx.view, - pseudoAccountID, - vaultAsset, - FreezeHandling::fhZERO_IF_FROZEN, - AuthHandling::ahZERO_IF_UNAUTHORIZED, - ctx.j) < amount) - return tecINSUFFICIENT_FUNDS; - - return tesSUCCESS; -} - -TER -LoanBrokerCoverWithdraw::doApply() -{ - auto const& tx = ctx_.tx; - - auto const brokerID = tx[sfLoanBrokerID]; - auto const amount = tx[sfAmount]; - auto const dstAcct = tx[~sfDestination].value_or(account_); - - auto broker = view().peek(keylet::loanbroker(brokerID)); - if (!broker) - return tecINTERNAL; // LCOV_EXCL_LINE - - auto const brokerPseudoID = *broker->at(sfAccount); - - // Decrease the LoanBroker's CoverAvailable by Amount - broker->at(sfCoverAvailable) -= amount; - view().update(broker); - - return doWithdraw( - view(), - tx, - account_, - dstAcct, - brokerPseudoID, - mPriorBalance, - amount, - j_); -} - -//------------------------------------------------------------------------------ - -} // namespace ripple diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h deleted file mode 100644 index eab2c9e60f..0000000000 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef XRPL_TX_LOANBROKERCOVERWITHDRAW_H_INCLUDED -#define XRPL_TX_LOANBROKERCOVERWITHDRAW_H_INCLUDED - -#include - -namespace ripple { - -class LoanBrokerCoverWithdraw : public Transactor -{ -public: - static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; - - explicit LoanBrokerCoverWithdraw(ApplyContext& ctx) : Transactor(ctx) - { - } - - static bool - checkExtraFeatures(PreflightContext const& ctx); - - static NotTEC - preflight(PreflightContext const& ctx); - - static TER - preclaim(PreclaimContext const& ctx); - - TER - doApply() override; -}; - -//------------------------------------------------------------------------------ - -} // namespace ripple - -#endif diff --git a/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp b/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp deleted file mode 100644 index f3dd781bb5..0000000000 --- a/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp +++ /dev/null @@ -1,197 +0,0 @@ -#include -// -#include - -namespace ripple { - -bool -LoanBrokerDelete::checkExtraFeatures(PreflightContext const& ctx) -{ - return checkLendingProtocolDependencies(ctx); -} - -NotTEC -LoanBrokerDelete::preflight(PreflightContext const& ctx) -{ - if (ctx.tx[sfLoanBrokerID] == beast::zero) - return temINVALID; - - return tesSUCCESS; -} - -TER -LoanBrokerDelete::preclaim(PreclaimContext const& ctx) -{ - auto const& tx = ctx.tx; - - auto const account = tx[sfAccount]; - auto const brokerID = tx[sfLoanBrokerID]; - - auto const sleBroker = ctx.view.read(keylet::loanbroker(brokerID)); - if (!sleBroker) - { - JLOG(ctx.j.warn()) << "LoanBroker does not exist."; - return tecNO_ENTRY; - } - - auto const brokerOwner = sleBroker->at(sfOwner); - - if (account != brokerOwner) - { - JLOG(ctx.j.warn()) << "Account is not the owner of the LoanBroker."; - return tecNO_PERMISSION; - } - if (auto const ownerCount = sleBroker->at(sfOwnerCount); ownerCount != 0) - { - JLOG(ctx.j.warn()) << "LoanBrokerDelete: Owner count is " << ownerCount; - return tecHAS_OBLIGATIONS; - } - if (auto const debtTotal = sleBroker->at(sfDebtTotal); - debtTotal != beast::zero) - { - // Any remaining debt should have been wiped out by the last Loan - // Delete. This check is purely defensive. - auto const vault = - ctx.view.read(keylet::vault(sleBroker->at(sfVaultID))); - if (!vault) - return tefINTERNAL; // LCOV_EXCL_LINE - auto const asset = vault->at(sfAsset); - auto const scale = getVaultScale(vault); - - auto const rounded = - roundToAsset(asset, debtTotal, scale, Number::towards_zero); - - if (rounded != beast::zero) - { - // LCOV_EXCL_START - JLOG(ctx.j.warn()) << "LoanBrokerDelete: Debt total is " - << debtTotal << ", which rounds to " << rounded; - return tecHAS_OBLIGATIONS; - // LCOV_EXCL_START - } - } - - auto const vault = ctx.view.read(keylet::vault(sleBroker->at(sfVaultID))); - if (!vault) - { - // LCOV_EXCL_START - JLOG(ctx.j.fatal()) << "Vault is missing for Broker " << brokerID; - return tefBAD_LEDGER; - // LCOV_EXCL_STOP - } - - Asset const asset = vault->at(sfAsset); - - auto const coverAvailable = - STAmount{asset, sleBroker->at(sfCoverAvailable)}; - // If there are assets in the cover, broker will receive them on deletion. - // So we need to check if the broker owner is deep frozen for that asset. - if (coverAvailable > beast::zero) - { - if (auto const ret = checkDeepFrozen(ctx.view, brokerOwner, asset)) - { - JLOG(ctx.j.warn()) << "Broker owner account is frozen."; - return ret; - } - } - - return tesSUCCESS; -} - -TER -LoanBrokerDelete::doApply() -{ - auto const& tx = ctx_.tx; - - auto const brokerID = tx[sfLoanBrokerID]; - - // Delete the loan broker - auto broker = view().peek(keylet::loanbroker(brokerID)); - if (!broker) - return tefBAD_LEDGER; // LCOV_EXCL_LINE - auto const vaultID = broker->at(sfVaultID); - auto const sleVault = view().read(keylet::vault(vaultID)); - if (!sleVault) - return tefBAD_LEDGER; // LCOV_EXCL_LINE - auto const vaultPseudoID = sleVault->at(sfAccount); - auto const vaultAsset = sleVault->at(sfAsset); - - auto const brokerPseudoID = broker->at(sfAccount); - - if (!view().dirRemove( - keylet::ownerDir(account_), - broker->at(sfOwnerNode), - broker->key(), - false)) - { - return tefBAD_LEDGER; // LCOV_EXCL_LINE - } - if (!view().dirRemove( - keylet::ownerDir(vaultPseudoID), - broker->at(sfVaultNode), - broker->key(), - false)) - { - return tefBAD_LEDGER; // LCOV_EXCL_LINE - } - - { - auto const coverAvailable = - STAmount{vaultAsset, broker->at(sfCoverAvailable)}; - if (auto const ter = accountSend( - view(), - brokerPseudoID, - account_, - coverAvailable, - j_, - WaiveTransferFee::Yes)) - return ter; - } - - if (auto ter = removeEmptyHolding(view(), brokerPseudoID, vaultAsset, j_)) - return ter; - - auto brokerPseudoSLE = view().peek(keylet::account(brokerPseudoID)); - if (!brokerPseudoSLE) - return tefBAD_LEDGER; // LCOV_EXCL_LINE - - // Making the payment and removing the empty holding should have deleted any - // obligations associated with the broker or broker pseudo-account. - if (*brokerPseudoSLE->at(sfBalance)) - { - JLOG(j_.warn()) << "LoanBrokerDelete: Pseudo-account has a balance"; - return tecHAS_OBLIGATIONS; // LCOV_EXCL_LINE - } - if (brokerPseudoSLE->at(sfOwnerCount) != 0) - { - JLOG(j_.warn()) - << "LoanBrokerDelete: Pseudo-account still owns objects"; - return tecHAS_OBLIGATIONS; // LCOV_EXCL_LINE - } - if (auto const directory = keylet::ownerDir(brokerPseudoID); - view().read(directory)) - { - JLOG(j_.warn()) << "LoanBrokerDelete: Pseudo-account has a directory"; - return tecHAS_OBLIGATIONS; // LCOV_EXCL_LINE - } - - view().erase(brokerPseudoSLE); - - view().erase(broker); - - { - auto owner = view().peek(keylet::account(account_)); - if (!owner) - return tefBAD_LEDGER; // LCOV_EXCL_LINE - - // Decreases the owner count by two: one for the LoanBroker object, and - // one for the pseudo-account. - adjustOwnerCount(view(), owner, -2, j_); - } - - return tesSUCCESS; -} - -//------------------------------------------------------------------------------ - -} // namespace ripple diff --git a/src/xrpld/app/tx/detail/LoanBrokerDelete.h b/src/xrpld/app/tx/detail/LoanBrokerDelete.h deleted file mode 100644 index 8466fe4f95..0000000000 --- a/src/xrpld/app/tx/detail/LoanBrokerDelete.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef XRPL_TX_LOANBROKERDELETE_H_INCLUDED -#define XRPL_TX_LOANBROKERDELETE_H_INCLUDED - -#include - -namespace ripple { - -class LoanBrokerDelete : public Transactor -{ -public: - static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; - - explicit LoanBrokerDelete(ApplyContext& ctx) : Transactor(ctx) - { - } - - static bool - checkExtraFeatures(PreflightContext const& ctx); - - static NotTEC - preflight(PreflightContext const& ctx); - - static TER - preclaim(PreclaimContext const& ctx); - - TER - doApply() override; -}; - -//------------------------------------------------------------------------------ - -} // namespace ripple - -#endif diff --git a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp deleted file mode 100644 index c2e6effd7a..0000000000 --- a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp +++ /dev/null @@ -1,215 +0,0 @@ -#include -// -#include - -namespace ripple { - -bool -LoanBrokerSet::checkExtraFeatures(PreflightContext const& ctx) -{ - return checkLendingProtocolDependencies(ctx); -} - -NotTEC -LoanBrokerSet::preflight(PreflightContext const& ctx) -{ - using namespace Lending; - - auto const& tx = ctx.tx; - if (auto const data = tx[~sfData]; data && !data->empty() && - !validDataLength(tx[~sfData], maxDataPayloadLength)) - return temINVALID; - if (!validNumericRange(tx[~sfManagementFeeRate], maxManagementFeeRate)) - return temINVALID; - if (!validNumericRange(tx[~sfCoverRateMinimum], maxCoverRate)) - return temINVALID; - if (!validNumericRange(tx[~sfCoverRateLiquidation], maxCoverRate)) - return temINVALID; - if (!validNumericRange( - tx[~sfDebtMaximum], Number(maxMPTokenAmount), Number(0))) - return temINVALID; - - if (tx.isFieldPresent(sfLoanBrokerID)) - { - // Fixed fields can not be specified if we're modifying an existing - // LoanBroker Object - if (tx.isFieldPresent(sfManagementFeeRate) || - tx.isFieldPresent(sfCoverRateMinimum) || - tx.isFieldPresent(sfCoverRateLiquidation)) - return temINVALID; - - if (tx[sfLoanBrokerID] == beast::zero) - return temINVALID; - } - - if (auto const vaultID = tx.at(~sfVaultID)) - { - if (*vaultID == beast::zero) - return temINVALID; - } - - { - auto const minimumZero = tx[~sfCoverRateMinimum].value_or(0) == 0; - auto const liquidationZero = - tx[~sfCoverRateLiquidation].value_or(0) == 0; - // Both must be zero or non-zero. - if (minimumZero != liquidationZero) - { - return temINVALID; - } - } - - return tesSUCCESS; -} - -TER -LoanBrokerSet::preclaim(PreclaimContext const& ctx) -{ - auto const& tx = ctx.tx; - - auto const account = tx[sfAccount]; - auto const vaultID = tx[sfVaultID]; - - if (auto const brokerID = tx[~sfLoanBrokerID]) - { - auto const sleBroker = ctx.view.read(keylet::loanbroker(*brokerID)); - if (!sleBroker) - { - JLOG(ctx.j.warn()) << "LoanBroker does not exist."; - return tecNO_ENTRY; - } - if (vaultID != sleBroker->at(sfVaultID)) - { - JLOG(ctx.j.warn()) - << "Can not change VaultID on an existing LoanBroker."; - return tecNO_PERMISSION; - } - if (account != sleBroker->at(sfOwner)) - { - JLOG(ctx.j.warn()) << "Account is not the owner of the LoanBroker."; - return tecNO_PERMISSION; - } - } - else - { - auto const sleVault = ctx.view.read(keylet::vault(vaultID)); - if (!sleVault) - { - JLOG(ctx.j.warn()) << "Vault does not exist."; - return tecNO_ENTRY; - } - if (account != sleVault->at(sfOwner)) - { - JLOG(ctx.j.warn()) << "Account is not the owner of the Vault."; - return tecNO_PERMISSION; - } - if (auto const ter = canAddHolding(ctx.view, sleVault->at(sfAsset))) - return ter; - } - return tesSUCCESS; -} - -TER -LoanBrokerSet::doApply() -{ - auto const& tx = ctx_.tx; - auto& view = ctx_.view(); - - if (auto const brokerID = tx[~sfLoanBrokerID]) - { - // Modify an existing LoanBroker - auto broker = view.peek(keylet::loanbroker(*brokerID)); - if (!broker) - { - // This should be impossible - // LCOV_EXCL_START - JLOG(j_.fatal()) << "LoanBroker does not exist."; - return tefBAD_LEDGER; - // LCOV_EXCL_STOP - } - - if (auto const data = tx[~sfData]) - broker->at(sfData) = *data; - if (auto const debtMax = tx[~sfDebtMaximum]) - broker->at(sfDebtMaximum) = *debtMax; - - view.update(broker); - } - else - { - // Create a new LoanBroker pointing back to the given Vault - auto const vaultID = tx[sfVaultID]; - auto const sleVault = view.read(keylet::vault(vaultID)); - if (!sleVault) - { - // This should be impossible - // LCOV_EXCL_START - JLOG(j_.fatal()) << "Vault does not exist."; - return tefBAD_LEDGER; - // LCOV_EXCL_STOP - } - auto const vaultPseudoID = sleVault->at(sfAccount); - auto const sequence = tx.getSeqValue(); - - auto owner = view.peek(keylet::account(account_)); - if (!owner) - { - // This should be impossible - // LCOV_EXCL_START - JLOG(j_.fatal()) << "Account does not exist."; - return tefBAD_LEDGER; - // LCOV_EXCL_STOP - } - auto broker = - std::make_shared(keylet::loanbroker(account_, sequence)); - - if (auto const ter = dirLink(view, account_, broker)) - return ter; // LCOV_EXCL_LINE - if (auto const ter = dirLink(view, vaultPseudoID, broker, sfVaultNode)) - return ter; // LCOV_EXCL_LINE - - // Increases the owner count by two: one for the LoanBroker object, and - // one for the pseudo-account. - adjustOwnerCount(view, owner, 2, j_); - auto const ownerCount = owner->at(sfOwnerCount); - if (mPriorBalance < view.fees().accountReserve(ownerCount)) - return tecINSUFFICIENT_RESERVE; - - auto maybePseudo = - createPseudoAccount(view, broker->key(), sfLoanBrokerID); - if (!maybePseudo) - return maybePseudo.error(); // LCOV_EXCL_LINE - auto& pseudo = *maybePseudo; - auto pseudoId = pseudo->at(sfAccount); - - if (auto ter = addEmptyHolding( - view, pseudoId, mPriorBalance, sleVault->at(sfAsset), j_)) - return ter; - - // Initialize data fields: - broker->at(sfSequence) = sequence; - broker->at(sfVaultID) = vaultID; - broker->at(sfOwner) = account_; - broker->at(sfAccount) = pseudoId; - // The LoanSequence indexes loans created by this broker, starting at 1 - broker->at(sfLoanSequence) = 1; - if (auto const data = tx[~sfData]) - broker->at(sfData) = *data; - if (auto const rate = tx[~sfManagementFeeRate]) - broker->at(sfManagementFeeRate) = *rate; - if (auto const debtMax = tx[~sfDebtMaximum]) - broker->at(sfDebtMaximum) = *debtMax; - if (auto const coverMin = tx[~sfCoverRateMinimum]) - broker->at(sfCoverRateMinimum) = *coverMin; - if (auto const coverLiq = tx[~sfCoverRateLiquidation]) - broker->at(sfCoverRateLiquidation) = *coverLiq; - - view.insert(broker); - } - - return tesSUCCESS; -} - -//------------------------------------------------------------------------------ - -} // namespace ripple diff --git a/src/xrpld/app/tx/detail/LoanBrokerSet.h b/src/xrpld/app/tx/detail/LoanBrokerSet.h deleted file mode 100644 index 39ed9bcd61..0000000000 --- a/src/xrpld/app/tx/detail/LoanBrokerSet.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef XRPL_TX_LOANBROKERSET_H_INCLUDED -#define XRPL_TX_LOANBROKERSET_H_INCLUDED - -#include - -namespace ripple { - -class LoanBrokerSet : public Transactor -{ -public: - static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; - - explicit LoanBrokerSet(ApplyContext& ctx) : Transactor(ctx) - { - } - - static bool - checkExtraFeatures(PreflightContext const& ctx); - - static NotTEC - preflight(PreflightContext const& ctx); - - static TER - preclaim(PreclaimContext const& ctx); - - TER - doApply() override; -}; - -//------------------------------------------------------------------------------ - -} // namespace ripple - -#endif diff --git a/src/xrpld/app/tx/detail/LoanDelete.cpp b/src/xrpld/app/tx/detail/LoanDelete.cpp deleted file mode 100644 index 87ff4d594b..0000000000 --- a/src/xrpld/app/tx/detail/LoanDelete.cpp +++ /dev/null @@ -1,133 +0,0 @@ -#include -// -#include - -namespace ripple { - -bool -LoanDelete::checkExtraFeatures(PreflightContext const& ctx) -{ - return checkLendingProtocolDependencies(ctx); -} - -NotTEC -LoanDelete::preflight(PreflightContext const& ctx) -{ - if (ctx.tx[sfLoanID] == beast::zero) - return temINVALID; - - return tesSUCCESS; -} - -TER -LoanDelete::preclaim(PreclaimContext const& ctx) -{ - auto const& tx = ctx.tx; - - auto const account = tx[sfAccount]; - auto const loanID = tx[sfLoanID]; - - auto const loanSle = ctx.view.read(keylet::loan(loanID)); - if (!loanSle) - { - JLOG(ctx.j.warn()) << "Loan does not exist."; - return tecNO_ENTRY; - } - if (loanSle->at(sfPaymentRemaining) > 0) - { - JLOG(ctx.j.warn()) << "Active loan can not be deleted."; - return tecHAS_OBLIGATIONS; - } - - auto const loanBrokerID = loanSle->at(sfLoanBrokerID); - auto const loanBrokerSle = ctx.view.read(keylet::loanbroker(loanBrokerID)); - if (!loanBrokerSle) - { - // should be impossible - return tecINTERNAL; // LCOV_EXCL_LINE - } - if (loanBrokerSle->at(sfOwner) != account && - loanSle->at(sfBorrower) != account) - { - JLOG(ctx.j.warn()) - << "Account is not Loan Broker Owner or Loan Borrower."; - return tecNO_PERMISSION; - } - - return tesSUCCESS; -} - -TER -LoanDelete::doApply() -{ - auto const& tx = ctx_.tx; - auto& view = ctx_.view(); - - auto const loanID = tx[sfLoanID]; - auto const loanSle = view.peek(keylet::loan(loanID)); - if (!loanSle) - return tefBAD_LEDGER; // LCOV_EXCL_LINE - auto const borrower = loanSle->at(sfBorrower); - auto const borrowerSle = view.peek(keylet::account(borrower)); - if (!borrowerSle) - return tefBAD_LEDGER; // LCOV_EXCL_LINE - - auto const brokerID = loanSle->at(sfLoanBrokerID); - auto const brokerSle = view.peek(keylet::loanbroker(brokerID)); - if (!brokerSle) - return tefBAD_LEDGER; // LCOV_EXCL_LINE - auto const brokerPseudoAccount = brokerSle->at(sfAccount); - - auto const vaultSle = view.peek(keylet ::vault(brokerSle->at(sfVaultID))); - if (!vaultSle) - return tefBAD_LEDGER; // LCOV_EXCL_LINE - - // Remove LoanID from Directory of the LoanBroker pseudo-account. - if (!view.dirRemove( - keylet::ownerDir(brokerPseudoAccount), - loanSle->at(sfLoanBrokerNode), - loanID, - false)) - return tefBAD_LEDGER; // LCOV_EXCL_LINE - // Remove LoanID from Directory of the Borrower. - if (!view.dirRemove( - keylet::ownerDir(borrower), - loanSle->at(sfOwnerNode), - loanID, - false)) - return tefBAD_LEDGER; // LCOV_EXCL_LINE - - // Delete the Loan object - view.erase(loanSle); - - // Decrement the LoanBroker's owner count. - // The broker's owner count is solely for the number of outstanding loans, - // and is distinct from the broker's pseudo-account's owner count - adjustOwnerCount(view, brokerSle, -1, j_); - // If there are no loans left, then any remaining debt must be forgiven, - // because there is no other way to pay it back. - if (brokerSle->at(sfOwnerCount) == 0) - { - auto debtTotalProxy = brokerSle->at(sfDebtTotal); - if (*debtTotalProxy != beast::zero) - { - XRPL_ASSERT_PARTS( - roundToAsset( - vaultSle->at(sfAsset), - debtTotalProxy, - getVaultScale(vaultSle), - Number::towards_zero) == beast::zero, - "ripple::LoanDelete::doApply", - "last loan, remaining debt rounds to zero"); - debtTotalProxy = 0; - } - } - // Decrement the borrower's owner count - adjustOwnerCount(view, borrowerSle, -1, j_); - - return tesSUCCESS; -} - -//------------------------------------------------------------------------------ - -} // namespace ripple diff --git a/src/xrpld/app/tx/detail/LoanDelete.h b/src/xrpld/app/tx/detail/LoanDelete.h deleted file mode 100644 index cbc37dec14..0000000000 --- a/src/xrpld/app/tx/detail/LoanDelete.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef XRPL_TX_LOANDELETE_H_INCLUDED -#define XRPL_TX_LOANDELETE_H_INCLUDED - -#include - -namespace ripple { - -class LoanDelete : public Transactor -{ -public: - static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; - - explicit LoanDelete(ApplyContext& ctx) : Transactor(ctx) - { - } - - static bool - checkExtraFeatures(PreflightContext const& ctx); - - static NotTEC - preflight(PreflightContext const& ctx); - - static TER - preclaim(PreclaimContext const& ctx); - - TER - doApply() override; -}; - -//------------------------------------------------------------------------------ - -} // namespace ripple - -#endif diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp deleted file mode 100644 index adf08d71bf..0000000000 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ /dev/null @@ -1,420 +0,0 @@ -#include -// -#include - -#include - -namespace ripple { - -bool -LoanManage::checkExtraFeatures(PreflightContext const& ctx) -{ - return checkLendingProtocolDependencies(ctx); -} - -std::uint32_t -LoanManage::getFlagsMask(PreflightContext const& ctx) -{ - return tfLoanManageMask; -} - -NotTEC -LoanManage::preflight(PreflightContext const& ctx) -{ - if (ctx.tx[sfLoanID] == beast::zero) - return temINVALID; - - // Flags are mutually exclusive - if (auto const flagField = ctx.tx[~sfFlags]; flagField && *flagField) - { - auto const flags = *flagField & tfUniversalMask; - if ((flags & (flags - 1)) != 0) - { - JLOG(ctx.j.warn()) - << "LoanManage: Only one of tfLoanDefault, tfLoanImpair, or " - "tfLoanUnimpair can be set."; - return temINVALID_FLAG; - } - } - - return tesSUCCESS; -} - -TER -LoanManage::preclaim(PreclaimContext const& ctx) -{ - auto const& tx = ctx.tx; - - auto const account = tx[sfAccount]; - auto const loanID = tx[sfLoanID]; - - auto const loanSle = ctx.view.read(keylet::loan(loanID)); - if (!loanSle) - { - JLOG(ctx.j.warn()) << "Loan does not exist."; - return tecNO_ENTRY; - } - // Impairment only allows certain transitions. - // 1. Once it's in default, it can't be changed. - // 2. It can get worse: unimpaired -> impaired -> default - // or unimpaired -> default - // 3. It can get better: impaired -> unimpaired - // 4. If it's in a state, it can't be put in that state again. - if (loanSle->isFlag(lsfLoanDefault)) - { - JLOG(ctx.j.warn()) - << "Loan is in default. A defaulted loan can not be modified."; - return tecNO_PERMISSION; - } - if (loanSle->isFlag(lsfLoanImpaired) && tx.isFlag(tfLoanImpair)) - { - JLOG(ctx.j.warn()) - << "Loan is impaired. A loan can not be impaired twice."; - return tecNO_PERMISSION; - } - if (!(loanSle->isFlag(lsfLoanImpaired) || - loanSle->isFlag(lsfLoanDefault)) && - (tx.isFlag(tfLoanUnimpair))) - { - JLOG(ctx.j.warn()) - << "Loan is unimpaired. Can not be unimpaired again."; - return tecNO_PERMISSION; - } - if (loanSle->at(sfPaymentRemaining) == 0) - { - JLOG(ctx.j.warn()) << "Loan is fully paid. A loan can not be modified " - "after it is fully paid."; - return tecNO_PERMISSION; - } - if (tx.isFlag(tfLoanDefault) && - !hasExpired( - ctx.view, - loanSle->at(sfNextPaymentDueDate) + loanSle->at(sfGracePeriod))) - { - JLOG(ctx.j.warn()) - << "A loan can not be defaulted before the next payment due date."; - return tecTOO_SOON; - } - - auto const loanBrokerID = loanSle->at(sfLoanBrokerID); - auto const loanBrokerSle = ctx.view.read(keylet::loanbroker(loanBrokerID)); - if (!loanBrokerSle) - { - // should be impossible - return tecINTERNAL; // LCOV_EXCL_LINE - } - if (loanBrokerSle->at(sfOwner) != account) - { - JLOG(ctx.j.warn()) - << "LoanBroker for Loan does not belong to the account. LoanModify " - "can only be submitted by the Loan Broker."; - return tecNO_PERMISSION; - } - - return tesSUCCESS; -} - -static Number -owedToVault(SLE::ref loanSle) -{ - // Spec section 3.2.3.2, defines the default amount as - // - // DefaultAmount = (Loan.PrincipalOutstanding + Loan.InterestOutstanding) - // - // Loan.InterestOutstanding is not stored directly on ledger. - // It is computed as - // - // Loan.TotalValueOutstanding - Loan.PrincipalOutstanding - - // Loan.ManagementFeeOutstanding - // - // Add that to the original formula, and you get this: - return loanSle->at(sfTotalValueOutstanding) - - loanSle->at(sfManagementFeeOutstanding); -} - -TER -LoanManage::defaultLoan( - ApplyView& view, - SLE::ref loanSle, - SLE::ref brokerSle, - SLE::ref vaultSle, - Asset const& vaultAsset, - beast::Journal j) -{ - // Calculate the amount of the Default that First-Loss Capital covers: - - std::int32_t const loanScale = loanSle->at(sfLoanScale); - auto brokerDebtTotalProxy = brokerSle->at(sfDebtTotal); - - Number const totalDefaultAmount = owedToVault(loanSle); - - // Apply the First-Loss Capital to the Default Amount - TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)}; - TenthBips32 const coverRateLiquidation{ - brokerSle->at(sfCoverRateLiquidation)}; - auto const defaultCovered = [&]() { - // Always round the minimum required up. - NumberRoundModeGuard mg(Number::upward); - auto const minimumCover = - tenthBipsOfValue(brokerDebtTotalProxy.value(), coverRateMinimum); - // Round the liquidation amount up, too - return roundToAsset( - vaultAsset, - /* - * This formula is from the XLS-66 spec, section 3.2.3.2 (State - * Changes), specifically "if the `tfLoanDefault` flag is set" / - * "Apply the First-Loss Capital to the Default Amount" - */ - std::min( - tenthBipsOfValue(minimumCover, coverRateLiquidation), - totalDefaultAmount), - loanScale); - }(); - - auto const vaultDefaultAmount = totalDefaultAmount - defaultCovered; - - // Update the Vault object: - - // The vault may be at a different scale than the loan. Reduce rounding - // errors during the accounting by rounding some of the values to that - // scale. - auto const vaultScale = getVaultScale(vaultSle); - - { - // Decrease the Total Value of the Vault: - auto vaultTotalProxy = vaultSle->at(sfAssetsTotal); - auto vaultAvailableProxy = vaultSle->at(sfAssetsAvailable); - - if (vaultTotalProxy < vaultDefaultAmount) - { - // LCOV_EXCL_START - JLOG(j.warn()) - << "Vault total assets is less than the vault default amount"; - return tefBAD_LEDGER; - // LCOV_EXCL_STOP - } - - auto const vaultDefaultRounded = roundToAsset( - vaultAsset, vaultDefaultAmount, vaultScale, Number::downward); - vaultTotalProxy -= vaultDefaultRounded; - // Increase the Asset Available of the Vault by liquidated First-Loss - // Capital and any unclaimed funds amount: - vaultAvailableProxy += defaultCovered; - if (*vaultAvailableProxy > *vaultTotalProxy && !vaultAsset.integral()) - { - auto const difference = vaultAvailableProxy - vaultTotalProxy; - JLOG(j.debug()) - << "Vault assets available: " << *vaultAvailableProxy << "(" - << vaultAvailableProxy.value().exponent() - << "), Total: " << *vaultTotalProxy << "(" - << vaultTotalProxy.value().exponent() - << "), Difference: " << difference << "(" - << difference.exponent() << ")"; - if (vaultAvailableProxy.value().exponent() - difference.exponent() > - 13) - { - // If the difference is dust, bring the total up to match - // the available - JLOG(j.debug()) - << "Difference between vault assets available and total is " - "dust. Set both to the larger value."; - vaultTotalProxy = vaultAvailableProxy; - } - } - if (*vaultAvailableProxy > *vaultTotalProxy) - { - JLOG(j.warn()) << "Vault assets available must not be greater " - "than assets outstanding. Available: " - << *vaultAvailableProxy - << ", Total: " << *vaultTotalProxy; - return tecLIMIT_EXCEEDED; - } - - // The loss has been realized - if (loanSle->isFlag(lsfLoanImpaired)) - { - auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized); - if (vaultLossUnrealizedProxy < totalDefaultAmount) - { - // LCOV_EXCL_START - JLOG(j.warn()) - << "Vault unrealized loss is less than the default amount"; - return tefBAD_LEDGER; - // LCOV_EXCL_STOP - } - vaultLossUnrealizedProxy -= totalDefaultAmount; - } - view.update(vaultSle); - } - - // Update the LoanBroker object: - - { - auto const asset = *vaultSle->at(sfAsset); - - // Decrease the Debt of the LoanBroker: - adjustImpreciseNumber( - brokerDebtTotalProxy, -totalDefaultAmount, asset, vaultScale); - // Decrease the First-Loss Capital Cover Available: - auto coverAvailableProxy = brokerSle->at(sfCoverAvailable); - if (coverAvailableProxy < defaultCovered) - { - // LCOV_EXCL_START - JLOG(j.warn()) - << "LoanBroker cover available is less than amount covered"; - return tefBAD_LEDGER; - // LCOV_EXCL_STOP - } - coverAvailableProxy -= defaultCovered; - view.update(brokerSle); - } - - // Update the Loan object: - loanSle->setFlag(lsfLoanDefault); - - loanSle->at(sfTotalValueOutstanding) = 0; - loanSle->at(sfPaymentRemaining) = 0; - loanSle->at(sfPrincipalOutstanding) = 0; - loanSle->at(sfManagementFeeOutstanding) = 0; - // Zero out the next due date. Since it's default, it'll be removed from - // the object. - loanSle->at(sfNextPaymentDueDate) = 0; - view.update(loanSle); - - // Return funds from the LoanBroker pseudo-account to the - // Vault pseudo-account: - return accountSend( - view, - brokerSle->at(sfAccount), - vaultSle->at(sfAccount), - STAmount{vaultAsset, defaultCovered}, - j, - WaiveTransferFee::Yes); -} - -TER -LoanManage::impairLoan( - ApplyView& view, - SLE::ref loanSle, - SLE::ref vaultSle, - beast::Journal j) -{ - Number const lossUnrealized = owedToVault(loanSle); - - // Update the Vault object(set "paper loss") - auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized); - vaultLossUnrealizedProxy += lossUnrealized; - if (vaultLossUnrealizedProxy > - vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable)) - { - // Having a loss greater than the vault's unavailable assets - // will leave the vault in an invalid / inconsistent state. - JLOG(j.warn()) << "Vault unrealized loss is too large, and will " - "corrupt the vault."; - return tecLIMIT_EXCEEDED; - } - view.update(vaultSle); - - // Update the Loan object - loanSle->setFlag(lsfLoanImpaired); - auto loanNextDueProxy = loanSle->at(sfNextPaymentDueDate); - if (!hasExpired(view, loanNextDueProxy)) - { - // loan payment is not yet late - - // move the next payment due date to now - loanNextDueProxy = view.parentCloseTime().time_since_epoch().count(); - } - view.update(loanSle); - - return tesSUCCESS; -} - -TER -LoanManage::unimpairLoan( - ApplyView& view, - SLE::ref loanSle, - SLE::ref vaultSle, - beast::Journal j) -{ - // Update the Vault object(clear "paper loss") - auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized); - Number const lossReversed = owedToVault(loanSle); - if (vaultLossUnrealizedProxy < lossReversed) - { - // LCOV_EXCL_START - JLOG(j.warn()) - << "Vault unrealized loss is less than the amount to be cleared"; - return tefBAD_LEDGER; - // LCOV_EXCL_STOP - } - vaultLossUnrealizedProxy -= lossReversed; - view.update(vaultSle); - - // Update the Loan object - loanSle->clearFlag(lsfLoanImpaired); - auto const paymentInterval = loanSle->at(sfPaymentInterval); - auto const normalPaymentDueDate = - std::max(loanSle->at(sfPreviousPaymentDate), loanSle->at(sfStartDate)) + - paymentInterval; - if (!hasExpired(view, normalPaymentDueDate)) - { - // loan was unimpaired within the payment interval - loanSle->at(sfNextPaymentDueDate) = normalPaymentDueDate; - } - else - { - // loan was unimpaired after the original payment due date - loanSle->at(sfNextPaymentDueDate) = - view.parentCloseTime().time_since_epoch().count() + paymentInterval; - } - view.update(loanSle); - - return tesSUCCESS; -} - -TER -LoanManage::doApply() -{ - auto const& tx = ctx_.tx; - auto& view = ctx_.view(); - - auto const loanID = tx[sfLoanID]; - auto const loanSle = view.peek(keylet::loan(loanID)); - if (!loanSle) - return tefBAD_LEDGER; // LCOV_EXCL_LINE - - auto const brokerID = loanSle->at(sfLoanBrokerID); - auto const brokerSle = view.peek(keylet::loanbroker(brokerID)); - if (!brokerSle) - return tefBAD_LEDGER; // LCOV_EXCL_LINE - - auto const vaultSle = view.peek(keylet ::vault(brokerSle->at(sfVaultID))); - if (!vaultSle) - return tefBAD_LEDGER; // LCOV_EXCL_LINE - auto const vaultAsset = vaultSle->at(sfAsset); - - // Valid flag combinations are checked in preflight. No flags is valid - - // just a noop. - if (tx.isFlag(tfLoanDefault)) - { - if (auto const ter = - defaultLoan(view, loanSle, brokerSle, vaultSle, vaultAsset, j_)) - return ter; - } - else if (tx.isFlag(tfLoanImpair)) - { - if (auto const ter = impairLoan(view, loanSle, vaultSle, j_)) - return ter; - } - else if (tx.isFlag(tfLoanUnimpair)) - { - if (auto const ter = unimpairLoan(view, loanSle, vaultSle, j_)) - return ter; - } - - return tesSUCCESS; -} - -//------------------------------------------------------------------------------ - -} // namespace ripple diff --git a/src/xrpld/app/tx/detail/LoanManage.h b/src/xrpld/app/tx/detail/LoanManage.h deleted file mode 100644 index dde1023cad..0000000000 --- a/src/xrpld/app/tx/detail/LoanManage.h +++ /dev/null @@ -1,66 +0,0 @@ -#ifndef XRPL_TX_LOANMANAGE_H_INCLUDED -#define XRPL_TX_LOANMANAGE_H_INCLUDED - -#include - -namespace ripple { - -class LoanManage : public Transactor -{ -public: - static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; - - explicit LoanManage(ApplyContext& ctx) : Transactor(ctx) - { - } - - static bool - checkExtraFeatures(PreflightContext const& ctx); - - static std::uint32_t - getFlagsMask(PreflightContext const& ctx); - - static NotTEC - preflight(PreflightContext const& ctx); - - static TER - preclaim(PreclaimContext const& ctx); - - /** Helper function that might be needed by other transactors - */ - static TER - defaultLoan( - ApplyView& view, - SLE::ref loanSle, - SLE::ref brokerSle, - SLE::ref vaultSle, - Asset const& vaultAsset, - beast::Journal j); - - /** Helper function that might be needed by other transactors - */ - static TER - impairLoan( - ApplyView& view, - SLE::ref loanSle, - SLE::ref vaultSle, - beast::Journal j); - - /** Helper function that might be needed by other transactors - */ - static TER - unimpairLoan( - ApplyView& view, - SLE::ref loanSle, - SLE::ref vaultSle, - beast::Journal j); - - TER - doApply() override; -}; - -//------------------------------------------------------------------------------ - -} // namespace ripple - -#endif diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp deleted file mode 100644 index 43f19743a7..0000000000 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ /dev/null @@ -1,602 +0,0 @@ -#include -// -#include -#include - -#include -#include -#include - -#include - -namespace ripple { - -bool -LoanPay::checkExtraFeatures(PreflightContext const& ctx) -{ - return checkLendingProtocolDependencies(ctx); -} - -std::uint32_t -LoanPay::getFlagsMask(PreflightContext const& ctx) -{ - return tfLoanPayMask; -} - -NotTEC -LoanPay::preflight(PreflightContext const& ctx) -{ - if (ctx.tx[sfLoanID] == beast::zero) - return temINVALID; - - if (ctx.tx[sfAmount] <= beast::zero) - return temBAD_AMOUNT; - - // The loan payment flags are all mutually exclusive. If more than one is - // set, the tx is malformed. - static_assert( - (tfLoanLatePayment | tfLoanFullPayment | tfLoanOverpayment) == - ~(tfLoanPayMask | tfUniversal)); - auto const flagsSet = ctx.tx.getFlags() & ~(tfLoanPayMask | tfUniversal); - if (std::popcount(flagsSet) > 1) - { - JLOG(ctx.j.warn()) << "Only one LoanPay flag can be set per tx. " - << flagsSet << " is too many."; - return temINVALID_FLAG; - } - - return tesSUCCESS; -} - -XRPAmount -LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx) -{ - using namespace Lending; - - auto const normalCost = Transactor::calculateBaseFee(view, tx); - - if (tx.isFlag(tfLoanFullPayment) || tx.isFlag(tfLoanLatePayment)) - // The loan will be making one set of calculations for one full or late - // payment - return normalCost; - - // The fee is based on the potential number of payments, unless the loan is - // being fully paid off. - auto const amount = tx[sfAmount]; - auto const loanID = tx[sfLoanID]; - - auto const loanSle = view.read(keylet::loan(loanID)); - if (!loanSle) - // Let preclaim worry about the error for this - return normalCost; - - if (loanSle->at(sfPaymentRemaining) <= loanPaymentsPerFeeIncrement) - { - // If there are fewer than loanPaymentsPerFeeIncrement payments left to - // pay, we can skip the computations. - return normalCost; - } - - if (hasExpired(view, loanSle->at(sfNextPaymentDueDate))) - // If the payment is late, and the late payment flag is not set, it'll - // fail - return normalCost; - - auto const brokerSle = - view.read(keylet::loanbroker(loanSle->at(sfLoanBrokerID))); - if (!brokerSle) - // Let preclaim worry about the error for this - return normalCost; - auto const vaultSle = view.read(keylet::vault(brokerSle->at(sfVaultID))); - if (!vaultSle) - // Let preclaim worry about the error for this - return normalCost; - - auto const asset = vaultSle->at(sfAsset); - - if (asset != amount.asset()) - // Let preclaim worry about the error for this - return normalCost; - - auto const scale = loanSle->at(sfLoanScale); - - auto const regularPayment = - roundPeriodicPayment(asset, loanSle->at(sfPeriodicPayment), scale) + - loanSle->at(sfLoanServiceFee); - - // If making an overpayment, count it as a full payment because it will do - // about the same amount of work, if not more. - NumberRoundModeGuard mg( - tx.isFlag(tfLoanOverpayment) ? Number::upward : Number::downward); - // Estimate how many payments will be made - Number const numPaymentEstimate = - static_cast(amount / regularPayment); - - // Charge one base fee per paymentsPerFeeIncrement payments, rounding up. - Number::setround(Number::upward); - auto const feeIncrements = std::max( - std::int64_t(1), - static_cast( - numPaymentEstimate / loanPaymentsPerFeeIncrement)); - - return feeIncrements * normalCost; -} - -TER -LoanPay::preclaim(PreclaimContext const& ctx) -{ - auto const& tx = ctx.tx; - - auto const account = tx[sfAccount]; - auto const loanID = tx[sfLoanID]; - auto const amount = tx[sfAmount]; - - auto const loanSle = ctx.view.read(keylet::loan(loanID)); - if (!loanSle) - { - JLOG(ctx.j.warn()) << "Loan does not exist."; - return tecNO_ENTRY; - } - - if (loanSle->at(sfBorrower) != account) - { - JLOG(ctx.j.warn()) << "Loan does not belong to the account."; - return tecNO_PERMISSION; - } - - if (tx.isFlag(tfLoanOverpayment) && !loanSle->isFlag(lsfLoanOverpayment)) - { - JLOG(ctx.j.warn()) - << "Requested overpayment on a loan that doesn't allow it"; - return temINVALID_FLAG; - } - - auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding); - TenthBips32 const interestRate{loanSle->at(sfInterestRate)}; - auto const paymentRemaining = loanSle->at(sfPaymentRemaining); - TenthBips32 const lateInterestRate{loanSle->at(sfLateInterestRate)}; - - if (paymentRemaining == 0 || principalOutstanding == 0) - { - JLOG(ctx.j.warn()) << "Loan is already paid off."; - return tecKILLED; - } - - auto const loanBrokerID = loanSle->at(sfLoanBrokerID); - auto const loanBrokerSle = ctx.view.read(keylet::loanbroker(loanBrokerID)); - if (!loanBrokerSle) - { - // This should be impossible - // LCOV_EXCL_START - JLOG(ctx.j.fatal()) << "LoanBroker does not exist."; - return tefBAD_LEDGER; - // LCOV_EXCL_STOP - } - auto const vaultID = loanBrokerSle->at(sfVaultID); - auto const vaultSle = ctx.view.read(keylet::vault(vaultID)); - if (!vaultSle) - { - // This should be impossible - // LCOV_EXCL_START - JLOG(ctx.j.fatal()) << "Vault does not exist."; - return tefBAD_LEDGER; - // LCOV_EXCL_STOP - } - auto const asset = vaultSle->at(sfAsset); - auto const vaultPseudoAccount = vaultSle->at(sfAccount); - - if (amount.asset() != asset) - { - JLOG(ctx.j.warn()) << "Loan amount does not match the Vault asset."; - return tecWRONG_ASSET; - } - - if (auto const ret = checkFrozen(ctx.view, account, asset)) - { - JLOG(ctx.j.warn()) << "Borrower account is frozen."; - return ret; - } - if (auto const ret = checkDeepFrozen(ctx.view, vaultPseudoAccount, asset)) - { - JLOG(ctx.j.warn()) - << "Vault pseudo-account can not receive funds (deep frozen)."; - return ret; - } - if (auto const ret = requireAuth(ctx.view, asset, account)) - { - JLOG(ctx.j.warn()) << "Borrower account is not authorized."; - return ret; - } - // Make sure the borrower has enough funds to make the payment! - // Do not support "partial payments" - if the transaction says to pay X, - // then the account must have X available, even if the loan payment takes - // less. - if (auto const balance = accountSpendable( - ctx.view, - account, - asset, - fhZERO_IF_FROZEN, - ahZERO_IF_UNAUTHORIZED, - ctx.j); - balance < amount) - { - JLOG(ctx.j.warn()) << "Payment amount too large. Amount: " - << to_string(amount.getJson()) - << ". Balance: " << to_string(balance.getJson()); - return tecINSUFFICIENT_FUNDS; - } - - return tesSUCCESS; -} - -TER -LoanPay::doApply() -{ - auto const& tx = ctx_.tx; - auto& view = ctx_.view(); - - auto const amount = tx[sfAmount]; - - auto const loanID = tx[sfLoanID]; - auto const loanSle = view.peek(keylet::loan(loanID)); - if (!loanSle) - return tefBAD_LEDGER; // LCOV_EXCL_LINE - std::int32_t const loanScale = loanSle->at(sfLoanScale); - - auto const brokerID = loanSle->at(sfLoanBrokerID); - auto const brokerSle = view.peek(keylet::loanbroker(brokerID)); - if (!brokerSle) - return tefBAD_LEDGER; // LCOV_EXCL_LINE - auto const brokerOwner = brokerSle->at(sfOwner); - auto const brokerPseudoAccount = brokerSle->at(sfAccount); - auto const vaultID = brokerSle->at(sfVaultID); - auto const vaultSle = view.peek(keylet::vault(vaultID)); - if (!vaultSle) - return tefBAD_LEDGER; // LCOV_EXCL_LINE - auto const vaultPseudoAccount = vaultSle->at(sfAccount); - auto const asset = *vaultSle->at(sfAsset); - - // Determine where to send the broker's fee - auto coverAvailableProxy = brokerSle->at(sfCoverAvailable); - TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)}; - auto debtTotalProxy = brokerSle->at(sfDebtTotal); - - // Send the broker fee to the owner if they have sufficient cover available, - // _and_ if the owner can receive funds. If not, so as not to block the - // payment, add it to the cover balance (send it to the broker pseudo - // account). - // - // Normally freeze status is checked in preflight, but we do it here to - // avoid duplicating the check. It'll claim a fee either way. - bool const sendBrokerFeeToOwner = [&]() { - // Round the minimum required cover up to be conservative. This ensures - // CoverAvailable never drops below the theoretical minimum, protecting - // the broker's solvency. - NumberRoundModeGuard mg(Number::upward); - return coverAvailableProxy >= - roundToAsset( - asset, - tenthBipsOfValue(debtTotalProxy.value(), coverRateMinimum), - loanScale) && - !isDeepFrozen(view, brokerOwner, asset); - }(); - - auto const brokerPayee = - sendBrokerFeeToOwner ? brokerOwner : brokerPseudoAccount; - auto const brokerPayeeSle = view.peek(keylet::account(brokerPayee)); - if (!sendBrokerFeeToOwner) - { - // If we can't send the fee to the owner, and the pseudo-account is - // frozen, then we have to fail the payment. - if (auto const ret = checkDeepFrozen(view, brokerPayee, asset)) - { - JLOG(j_.warn()) - << "Both Loan Broker and Loan Broker pseudo-account " - "can not receive funds (deep frozen)."; - return ret; - } - } - - //------------------------------------------------------ - // Loan object state changes - - // Unimpair the loan if it was impaired. Do this before the payment is - // attempted, so the original values can be used. If the payment fails, this - // change will be discarded. - if (loanSle->isFlag(lsfLoanImpaired)) - { - LoanManage::unimpairLoan(view, loanSle, vaultSle, j_); - } - - LoanPaymentType const paymentType = [&tx]() { - // preflight already checked that at most one flag is set. - if (tx.isFlag(tfLoanLatePayment)) - return LoanPaymentType::late; - if (tx.isFlag(tfLoanFullPayment)) - return LoanPaymentType::full; - if (tx.isFlag(tfLoanOverpayment)) - return LoanPaymentType::overpayment; - return LoanPaymentType::regular; - }(); - - Expected const paymentParts = loanMakePayment( - asset, view, loanSle, brokerSle, amount, paymentType, j_); - - if (!paymentParts) - { - XRPL_ASSERT_PARTS( - paymentParts.error(), - "ripple::LoanPay::doApply", - "payment error is an error"); - return paymentParts.error(); - } - - // If the payment computation completed without error, the loanSle object - // has been modified. - view.update(loanSle); - - XRPL_ASSERT_PARTS( - // It is possible to pay 0 principal - paymentParts->principalPaid >= 0, - "ripple::LoanPay::doApply", - "valid principal paid"); - XRPL_ASSERT_PARTS( - // It is possible to pay 0 interest - paymentParts->interestPaid >= 0, - "ripple::LoanPay::doApply", - "valid interest paid"); - XRPL_ASSERT_PARTS( - // It should not be possible to pay 0 total - paymentParts->principalPaid + paymentParts->interestPaid > 0, - "ripple::LoanPay::doApply", - "valid total paid"); - XRPL_ASSERT_PARTS( - paymentParts->feePaid >= 0, - "ripple::LoanPay::doApply", - "valid fee paid"); - - if (paymentParts->principalPaid < 0 || paymentParts->interestPaid < 0 || - paymentParts->feePaid < 0) - { - // LCOV_EXCL_START - JLOG(j_.fatal()) << "Loan payment computation returned invalid values."; - return tecLIMIT_EXCEEDED; - // LCOV_EXCL_STOP - } - - JLOG(j_.debug()) << "Loan Pay: principal paid: " - << paymentParts->principalPaid - << ", interest paid: " << paymentParts->interestPaid - << ", fee paid: " << paymentParts->feePaid - << ", value change: " << paymentParts->valueChange; - - //------------------------------------------------------ - // LoanBroker object state changes - view.update(brokerSle); - - auto assetsAvailableProxy = vaultSle->at(sfAssetsAvailable); - auto assetsTotalProxy = vaultSle->at(sfAssetsTotal); - - // The vault may be at a different scale than the loan. Reduce rounding - // errors during the payment by rounding some of the values to that scale. - auto const vaultScale = assetsTotalProxy.value().exponent(); - - auto const totalPaidToVaultRaw = - paymentParts->principalPaid + paymentParts->interestPaid; - auto const totalPaidToVaultRounded = - roundToAsset(asset, totalPaidToVaultRaw, vaultScale, Number::downward); - XRPL_ASSERT_PARTS( - !asset.integral() || totalPaidToVaultRaw == totalPaidToVaultRounded, - "ripple::LoanPay::doApply", - "rounding does nothing for integral asset"); - // Account for value changes when reducing the broker's debt: - // - Positive value change (from full/late/overpayments): Subtract from the - // amount credited toward debt to avoid over-reducing the debt. - // - Negative value change (from full/overpayments): Add to the amount - // credited toward debt,effectively increasing the debt reduction. - auto const totalPaidToVaultForDebt = - totalPaidToVaultRaw - paymentParts->valueChange; - - auto const totalPaidToBroker = paymentParts->feePaid; - - XRPL_ASSERT_PARTS( - (totalPaidToVaultRaw + totalPaidToBroker) == - (paymentParts->principalPaid + paymentParts->interestPaid + - paymentParts->feePaid), - "ripple::LoanPay::doApply", - "payments add up"); - - // Decrease LoanBroker Debt by the amount paid, add the Loan value change - // (which might be negative). totalPaidToVaultForDebt may be negative, - // increasing the debt - XRPL_ASSERT_PARTS( - isRounded(asset, totalPaidToVaultForDebt, loanScale), - "ripple::LoanPay::doApply", - "totalPaidToVaultForDebt rounding good"); - // Despite our best efforts, it's possible for rounding errors to accumulate - // in the loan broker's debt total. This is because the broker may have more - // than one loan with significantly different scales. - adjustImpreciseNumber( - debtTotalProxy, -totalPaidToVaultForDebt, asset, vaultScale); - - //------------------------------------------------------ - // Vault object state changes - view.update(vaultSle); - - Number const assetsAvailableBefore = *assetsAvailableProxy; - Number const pseudoAccountBalanceBefore = accountHolds( - view, - vaultPseudoAccount, - asset, - FreezeHandling::fhIGNORE_FREEZE, - AuthHandling::ahIGNORE_AUTH, - j_); - - { - XRPL_ASSERT_PARTS( - assetsAvailableBefore == pseudoAccountBalanceBefore, - "ripple::LoanPay::doApply", - "vault pseudo balance agrees before"); - - assetsAvailableProxy += totalPaidToVaultRounded; - assetsTotalProxy += paymentParts->valueChange; - - XRPL_ASSERT_PARTS( - *assetsAvailableProxy <= *assetsTotalProxy, - "ripple::LoanPay::doApply", - "assets available must not be greater than assets outstanding"); - - if (*assetsAvailableProxy > *assetsTotalProxy) - { - // LCOV_EXCL_START - return tecINTERNAL; - // LCOV_EXCL_STOP - } - } - - JLOG(j_.debug()) << "total paid to vault raw: " << totalPaidToVaultRaw - << ", total paid to vault rounded: " - << totalPaidToVaultRounded - << ", total paid to broker: " << totalPaidToBroker - << ", amount from transaction: " << amount; - - // Move funds - XRPL_ASSERT_PARTS( - totalPaidToVaultRounded + totalPaidToBroker <= amount, - "ripple::LoanPay::doApply", - "amount is sufficient"); - - if (!sendBrokerFeeToOwner) - { - // If there is not enough first-loss capital, add the fee to First Loss - // Cover Pool. Note that this moves the entire fee - it does not attempt - // to split it. The broker can Withdraw it later if they want, or leave - // it for future needs. - coverAvailableProxy += totalPaidToBroker; - } - -#if !NDEBUG - auto const accountBalanceBefore = accountSpendable( - view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); - auto const vaultBalanceBefore = account_ == vaultPseudoAccount - ? STAmount{asset, 0} - : accountSpendable( - view, - vaultPseudoAccount, - asset, - fhIGNORE_FREEZE, - ahIGNORE_AUTH, - j_); - auto const brokerBalanceBefore = account_ == brokerPayee - ? STAmount{asset, 0} - : accountSpendable( - view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); -#endif - - if (totalPaidToVaultRounded != beast::zero) - { - if (auto const ter = requireAuth( - view, asset, vaultPseudoAccount, AuthType::StrongAuth)) - return ter; - } - - if (totalPaidToBroker != beast::zero) - { - if (brokerPayee == account_) - { - // The broker may have deleted their holding. Recreate it if needed - if (auto const ter = addEmptyHolding( - view, - brokerPayee, - brokerPayeeSle->at(sfBalance).value().xrp(), - asset, - j_); - ter && ter != tecDUPLICATE) - // ignore tecDUPLICATE. That means the holding already exists, - // and is fine here - return ter; - } - if (auto const ter = - requireAuth(view, asset, brokerPayee, AuthType::StrongAuth)) - return ter; - } - - if (auto const ter = accountSendMulti( - view, - account_, - asset, - {{vaultPseudoAccount, totalPaidToVaultRounded}, - {brokerPayee, totalPaidToBroker}}, - j_, - WaiveTransferFee::Yes)) - return ter; - - Number const assetsAvailableAfter = *assetsAvailableProxy; - Number const pseudoAccountBalanceAfter = accountHolds( - view, - vaultPseudoAccount, - asset, - FreezeHandling::fhIGNORE_FREEZE, - AuthHandling::ahIGNORE_AUTH, - j_); - XRPL_ASSERT_PARTS( - assetsAvailableAfter == pseudoAccountBalanceAfter, - "ripple::LoanPay::doApply", - "vault pseudo balance agrees after"); - -#if !NDEBUG - auto const accountBalanceAfter = accountSpendable( - view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); - auto const vaultBalanceAfter = account_ == vaultPseudoAccount - ? STAmount{asset, 0} - : accountSpendable( - view, - vaultPseudoAccount, - asset, - fhIGNORE_FREEZE, - ahIGNORE_AUTH, - j_); - auto const brokerBalanceAfter = account_ == brokerPayee - ? STAmount{asset, 0} - : accountSpendable( - view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); - - XRPL_ASSERT_PARTS( - accountBalanceBefore + vaultBalanceBefore + brokerBalanceBefore == - accountBalanceAfter + vaultBalanceAfter + brokerBalanceAfter, - "ripple::LoanPay::doApply", - "funds are conserved (with rounding)"); - XRPL_ASSERT_PARTS( - accountBalanceAfter >= beast::zero, - "ripple::LoanPay::doApply", - "positive account balance"); - XRPL_ASSERT_PARTS( - accountBalanceAfter < accountBalanceBefore || - account_ == asset.getIssuer(), - "ripple::LoanPay::doApply", - "account balance decreased"); - XRPL_ASSERT_PARTS( - vaultBalanceAfter >= beast::zero && brokerBalanceAfter >= beast::zero, - "ripple::LoanPay::doApply", - "positive vault and broker balances"); - XRPL_ASSERT_PARTS( - vaultBalanceAfter >= vaultBalanceBefore, - "ripple::LoanPay::doApply", - "vault balance did not decrease"); - XRPL_ASSERT_PARTS( - brokerBalanceAfter >= brokerBalanceBefore, - "ripple::LoanPay::doApply", - "broker balance did not decrease"); - XRPL_ASSERT_PARTS( - vaultBalanceAfter > vaultBalanceBefore || - brokerBalanceAfter > brokerBalanceBefore, - "ripple::LoanPay::doApply", - "vault and/or broker balance increased"); -#endif - - return tesSUCCESS; -} - -//------------------------------------------------------------------------------ - -} // namespace ripple diff --git a/src/xrpld/app/tx/detail/LoanPay.h b/src/xrpld/app/tx/detail/LoanPay.h deleted file mode 100644 index 3f8eb16d04..0000000000 --- a/src/xrpld/app/tx/detail/LoanPay.h +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef XRPL_TX_LOANPAY_H_INCLUDED -#define XRPL_TX_LOANPAY_H_INCLUDED - -#include - -namespace ripple { - -class LoanPay : public Transactor -{ -public: - static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; - - explicit LoanPay(ApplyContext& ctx) : Transactor(ctx) - { - } - - static bool - checkExtraFeatures(PreflightContext const& ctx); - - static std::uint32_t - getFlagsMask(PreflightContext const& ctx); - - static NotTEC - preflight(PreflightContext const& ctx); - - static XRPAmount - calculateBaseFee(ReadView const& view, STTx const& tx); - - static TER - preclaim(PreclaimContext const& ctx); - - TER - doApply() override; -}; - -//------------------------------------------------------------------------------ - -} // namespace ripple - -#endif diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp deleted file mode 100644 index 838e774cae..0000000000 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ /dev/null @@ -1,632 +0,0 @@ -#include -// -#include - -#include - -namespace ripple { - -bool -LoanSet::checkExtraFeatures(PreflightContext const& ctx) -{ - return checkLendingProtocolDependencies(ctx); -} - -std::uint32_t -LoanSet::getFlagsMask(PreflightContext const& ctx) -{ - return tfLoanSetMask; -} - -NotTEC -LoanSet::preflight(PreflightContext const& ctx) -{ - using namespace Lending; - - auto const& tx = ctx.tx; - - // Special case for Batch inner transactions - if (tx.isFlag(tfInnerBatchTxn) && ctx.rules.enabled(featureBatch) && - !tx.isFieldPresent(sfCounterparty)) - { - auto const parentBatchId = ctx.parentBatchId.value_or(uint256{0}); - JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " - << "no Counterparty for inner LoanSet transaction."; - return temBAD_SIGNER; - } - - // These extra hoops are because STObjects cannot be Proxy'd from STObject. - auto const counterPartySig = [&tx]() -> std::optional { - if (tx.isFieldPresent(sfCounterpartySignature)) - return tx.getFieldObject(sfCounterpartySignature); - return std::nullopt; - }(); - if (!tx.isFlag(tfInnerBatchTxn) && !counterPartySig) - { - JLOG(ctx.j.warn()) - << "LoanSet transaction must have a CounterpartySignature."; - return temBAD_SIGNER; - } - - if (counterPartySig) - { - if (auto const ret = ripple::detail::preflightCheckSigningKey( - *counterPartySig, ctx.j)) - return ret; - } - - if (auto const data = tx[~sfData]; data && !data->empty() && - !validDataLength(tx[~sfData], maxDataPayloadLength)) - return temINVALID; - for (auto const& field : - {&sfLoanServiceFee, &sfLatePaymentFee, &sfClosePaymentFee}) - { - if (!validNumericMinimum(tx[~*field])) - return temINVALID; - } - // Principal Requested is required - if (auto const p = tx[sfPrincipalRequested]; p <= 0) - return temINVALID; - else if (!validNumericRange(tx[~sfLoanOriginationFee], p)) - return temINVALID; - if (!validNumericRange(tx[~sfInterestRate], maxInterestRate)) - return temINVALID; - if (!validNumericRange(tx[~sfOverpaymentFee], maxOverpaymentFee)) - return temINVALID; - if (!validNumericRange(tx[~sfLateInterestRate], maxLateInterestRate)) - return temINVALID; - if (!validNumericRange(tx[~sfCloseInterestRate], maxCloseInterestRate)) - return temINVALID; - if (!validNumericRange( - tx[~sfOverpaymentInterestRate], maxOverpaymentInterestRate)) - return temINVALID; - - if (auto const paymentTotal = tx[~sfPaymentTotal]; - paymentTotal && *paymentTotal <= 0) - return temINVALID; - - if (auto const paymentInterval = tx[~sfPaymentInterval]; - !validNumericMinimum(paymentInterval, LoanSet::minPaymentInterval)) - return temINVALID; - - else if (!validNumericRange( - tx[~sfGracePeriod], - paymentInterval.value_or(LoanSet::defaultPaymentInterval))) - return temINVALID; - - // Copied from preflight2 - if (counterPartySig) - { - if (auto const ret = ripple::detail::preflightCheckSimulateKeys( - ctx.flags, *counterPartySig, ctx.j)) - return *ret; - } - - if (auto const brokerID = ctx.tx[~sfLoanBrokerID]; - brokerID && *brokerID == beast::zero) - return temINVALID; - - return tesSUCCESS; -} - -NotTEC -LoanSet::checkSign(PreclaimContext const& ctx) -{ - if (auto ret = Transactor::checkSign(ctx)) - return ret; - - // Counter signer is optional. If it's not specified, it's assumed to be - // `LoanBroker.Owner`. Note that we have not checked whether the - // loanbroker exists at this point. - auto const counterSigner = [&]() -> std::optional { - if (auto const c = ctx.tx.at(~sfCounterparty)) - return c; - - if (auto const broker = - ctx.view.read(keylet::loanbroker(ctx.tx[sfLoanBrokerID]))) - return broker->at(sfOwner); - return std::nullopt; - }(); - if (!counterSigner) - return temBAD_SIGNER; - - // Counterparty signature is optional. Presence is checked in preflight. - if (!ctx.tx.isFieldPresent(sfCounterpartySignature)) - return tesSUCCESS; - auto const counterSig = ctx.tx.getFieldObject(sfCounterpartySignature); - return Transactor::checkSign( - ctx.view, - ctx.flags, - ctx.parentBatchId, - *counterSigner, - counterSig, - ctx.j); -} - -XRPAmount -LoanSet::calculateBaseFee(ReadView const& view, STTx const& tx) -{ - auto const normalCost = Transactor::calculateBaseFee(view, tx); - - // Compute the additional cost of each signature in the - // CounterpartySignature, whether a single signature or a multisignature - XRPAmount const baseFee = view.fees().base; - - // Counterparty signature is optional, but getFieldObject will return an - // empty object if it's not present. - auto const counterSig = tx.getFieldObject(sfCounterpartySignature); - // Each signer adds one more baseFee to the minimum required fee - // for the transaction. Note that unlike the base class, the single signer - // is counted if present. It will only be absent in a batch inner - // transaction. - std::size_t const signerCount = [&counterSig]() { - // Compute defensively. Assure that "tx" cannot be accessed and cause - // confusion or miscalculations. - return counterSig.isFieldPresent(sfSigners) - ? counterSig.getFieldArray(sfSigners).size() - : (counterSig.isFieldPresent(sfTxnSignature) ? 1 : 0); - }(); - - return normalCost + (signerCount * baseFee); -} - -std::vector> const& -LoanSet::getValueFields() -{ - static std::vector> const valueFields{ - ~sfPrincipalRequested, - ~sfLoanOriginationFee, - ~sfLoanServiceFee, - ~sfLatePaymentFee, - ~sfClosePaymentFee - // Overpayment fee is really a rate. Don't check it here. - }; - - return valueFields; -} - -static std::uint32_t -getStartDate(ReadView const& view) -{ - return view.info().closeTime.time_since_epoch().count(); -} - -TER -LoanSet::preclaim(PreclaimContext const& ctx) -{ - auto const& tx = ctx.tx; - - { - // Check for numeric overflow of the schedule before we load any - // objects. The Grace Period for the last payment ends at: - // startDate + (paymentInterval * paymentTotal) + gracePeriod. - // If that value is larger than "maxTime", the value - // overflows, and we kill the transaction. - using timeType = decltype(sfNextPaymentDueDate)::type::value_type; - static_assert(std::is_same_v); - timeType constexpr maxTime = std::numeric_limits::max(); - static_assert(maxTime == 4'294'967'295); - - auto const timeAvailable = maxTime - getStartDate(ctx.view); - - auto const interval = - ctx.tx.at(~sfPaymentInterval).value_or(defaultPaymentInterval); - auto const total = - ctx.tx.at(~sfPaymentTotal).value_or(defaultPaymentTotal); - auto const grace = - ctx.tx.at(~sfGracePeriod).value_or(defaultGracePeriod); - - // The grace period can't be larger than the interval. Check it first, - // mostly so that unit tests can test that specific case. - if (grace > timeAvailable) - { - JLOG(ctx.j.warn()) << "Grace period exceeds protocol time limit."; - return tecKILLED; - } - - if (interval > timeAvailable) - { - JLOG(ctx.j.warn()) - << "Payment interval exceeds protocol time limit."; - return tecKILLED; - } - - if (total > timeAvailable) - { - JLOG(ctx.j.warn()) << "Payment total exceeds protocol time limit."; - return tecKILLED; - } - - auto const timeLastPayment = timeAvailable - grace; - - if (timeLastPayment / interval < total) - { - JLOG(ctx.j.warn()) << "Last payment due date, or grace period for " - "last payment exceeds protocol time limit."; - return tecKILLED; - } - } - - auto const account = tx[sfAccount]; - auto const brokerID = tx[sfLoanBrokerID]; - - auto const brokerSle = ctx.view.read(keylet::loanbroker(brokerID)); - if (!brokerSle) - { - // This can only be hit if there's a counterparty specified, otherwise - // it'll fail in the signature check - JLOG(ctx.j.warn()) << "LoanBroker does not exist."; - return tecNO_ENTRY; - } - auto const brokerOwner = brokerSle->at(sfOwner); - auto const counterparty = tx[~sfCounterparty].value_or(brokerOwner); - if (account != brokerOwner && counterparty != brokerOwner) - { - JLOG(ctx.j.warn()) << "Neither Account nor Counterparty are the owner " - "of the LoanBroker."; - return tecNO_PERMISSION; - } - auto const brokerPseudo = brokerSle->at(sfAccount); - - auto const borrower = counterparty == brokerOwner ? account : counterparty; - if (auto const borrowerSle = ctx.view.read(keylet::account(borrower)); - !borrowerSle) - { - // It may not be possible to hit this case, because it'll fail the - // signature check with terNO_ACCOUNT. - JLOG(ctx.j.warn()) << "Borrower does not exist."; - return terNO_ACCOUNT; - } - - auto const vault = ctx.view.read(keylet::vault(brokerSle->at(sfVaultID))); - if (!vault) - // Should be impossible - return tefBAD_LEDGER; // LCOV_EXCL_LINE - Asset const asset = vault->at(sfAsset); - - auto const vaultPseudo = vault->at(sfAccount); - - // Check that relevant values can be represented as the vault asset type. - // This check is almost duplicated in doApply, but that check is done after - // the overall loan scale is known. This is mostly only relevant for - // integral (non-IOU) types - { - for (auto const& field : getValueFields()) - { - if (auto const value = tx[field]; - value && STAmount{asset, *value} != *value) - { - JLOG(ctx.j.warn()) << field.f->getName() << " (" << *value - << ") can not be represented as a(n) " - << to_string(asset) << "."; - return tecPRECISION_LOSS; - } - } - } - - if (auto const ter = canAddHolding(ctx.view, asset)) - return ter; - - // vaultPseudo is going to send funds, so it can't be frozen. - if (auto const ret = checkFrozen(ctx.view, vaultPseudo, asset)) - { - JLOG(ctx.j.warn()) << "Vault pseudo-account is frozen."; - return ret; - } - - // brokerPseudo is the fallback account to receive LoanPay fees, even if the - // broker owner is unable to accept them. Don't create the loan if it is - // deep frozen. - if (auto const ret = checkDeepFrozen(ctx.view, brokerPseudo, asset)) - { - JLOG(ctx.j.warn()) << "Broker pseudo-account is frozen."; - return ret; - } - - // borrower is eventually going to have to pay back the loan, so it can't be - // frozen now. It is also going to receive funds, so it can't be deep - // frozen, but being frozen is a prerequisite for being deep frozen, so - // checking the one is sufficient. - if (auto const ret = checkFrozen(ctx.view, borrower, asset)) - { - JLOG(ctx.j.warn()) << "Borrower account is frozen."; - return ret; - } - // brokerOwner is going to receive funds if there's an origination fee, so - // it can't be deep frozen - if (auto const ret = checkDeepFrozen(ctx.view, brokerOwner, asset)) - { - JLOG(ctx.j.warn()) << "Broker owner account is frozen."; - return ret; - } - - return tesSUCCESS; -} - -TER -LoanSet::doApply() -{ - auto const& tx = ctx_.tx; - auto& view = ctx_.view(); - - auto const brokerID = tx[sfLoanBrokerID]; - - auto const brokerSle = view.peek(keylet::loanbroker(brokerID)); - if (!brokerSle) - return tefBAD_LEDGER; // LCOV_EXCL_LINE - auto const brokerOwner = brokerSle->at(sfOwner); - auto const brokerOwnerSle = view.peek(keylet::account(brokerOwner)); - if (!brokerOwnerSle) - return tefBAD_LEDGER; // LCOV_EXCL_LINE - - auto const vaultSle = view.peek(keylet ::vault(brokerSle->at(sfVaultID))); - if (!vaultSle) - return tefBAD_LEDGER; // LCOV_EXCL_LINE - auto const vaultPseudo = vaultSle->at(sfAccount); - Asset const vaultAsset = vaultSle->at(sfAsset); - - auto const counterparty = tx[~sfCounterparty].value_or(brokerOwner); - auto const borrower = counterparty == brokerOwner ? account_ : counterparty; - auto const borrowerSle = view.peek(keylet::account(borrower)); - if (!borrowerSle) - { - return tefBAD_LEDGER; // LCOV_EXCL_LINE - } - - auto const brokerPseudo = brokerSle->at(sfAccount); - auto const brokerPseudoSle = view.peek(keylet::account(brokerPseudo)); - if (!brokerPseudoSle) - { - return tefBAD_LEDGER; // LCOV_EXCL_LINE - } - auto const principalRequested = tx[sfPrincipalRequested]; - - auto vaultAvailableProxy = vaultSle->at(sfAssetsAvailable); - auto vaultTotalProxy = vaultSle->at(sfAssetsTotal); - auto const vaultScale = getVaultScale(vaultSle); - if (vaultAvailableProxy < principalRequested) - { - JLOG(j_.warn()) - << "Insufficient assets available in the Vault to fund the loan."; - return tecINSUFFICIENT_FUNDS; - } - - TenthBips32 const interestRate{tx[~sfInterestRate].value_or(0)}; - - auto const paymentInterval = - tx[~sfPaymentInterval].value_or(defaultPaymentInterval); - auto const paymentTotal = tx[~sfPaymentTotal].value_or(defaultPaymentTotal); - - auto const properties = computeLoanProperties( - vaultAsset, - principalRequested, - interestRate, - paymentInterval, - paymentTotal, - TenthBips16{brokerSle->at(sfManagementFeeRate)}, - vaultScale); - - // Check that relevant values won't lose precision. This is mostly only - // relevant for IOU assets. - { - for (auto const& field : getValueFields()) - { - if (auto const value = tx[field]; - value && !isRounded(vaultAsset, *value, properties.loanScale)) - { - JLOG(j_.warn()) - << field.f->getName() << " (" << *value - << ") has too much precision. Total loan value is " - << properties.totalValueOutstanding << " with a scale of " - << properties.loanScale; - return tecPRECISION_LOSS; - } - } - } - - if (auto const ret = checkLoanGuards( - vaultAsset, - principalRequested, - interestRate != beast::zero, - paymentTotal, - properties, - j_)) - return ret; - - // Check that the other computed values are valid - if (properties.managementFeeOwedToBroker < 0 || - properties.totalValueOutstanding <= 0 || - properties.periodicPayment <= 0) - { - // LCOV_EXCL_START - JLOG(j_.warn()) - << "Computed loan properties are invalid. Does not compute."; - return tecINTERNAL; - // LCOV_EXCL_STOP - } - - LoanState const state = constructLoanState( - properties.totalValueOutstanding, - principalRequested, - properties.managementFeeOwedToBroker); - - auto const originationFee = tx[~sfLoanOriginationFee].value_or(Number{}); - - auto const loanAssetsToBorrower = principalRequested - originationFee; - - auto const newDebtDelta = principalRequested + state.interestDue; - auto const newDebtTotal = brokerSle->at(sfDebtTotal) + newDebtDelta; - if (auto const debtMaximum = brokerSle->at(sfDebtMaximum); - debtMaximum != 0 && debtMaximum < newDebtTotal) - { - JLOG(j_.warn()) - << "Loan would exceed the maximum debt limit of the LoanBroker."; - return tecLIMIT_EXCEEDED; - } - TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)}; - { - // Round the minimum required cover up to be conservative. This ensures - // CoverAvailable never drops below the theoretical minimum, protecting - // the broker's solvency. - NumberRoundModeGuard mg(Number::upward); - if (brokerSle->at(sfCoverAvailable) < - tenthBipsOfValue(newDebtTotal, coverRateMinimum)) - { - JLOG(j_.warn()) - << "Insufficient first-loss capital to cover the loan."; - return tecINSUFFICIENT_FUNDS; - } - } - - adjustOwnerCount(view, borrowerSle, 1, j_); - { - auto const ownerCount = borrowerSle->at(sfOwnerCount); - auto const balance = account_ == borrower - ? mPriorBalance - : borrowerSle->at(sfBalance).value().xrp(); - if (balance < view.fees().accountReserve(ownerCount)) - return tecINSUFFICIENT_RESERVE; - } - - // Account for the origination fee using two payments - // - // 1. Transfer loanAssetsAvailable (principalRequested - originationFee) - // from vault pseudo-account to the borrower. - // Create a holding for the borrower if one does not already exist. - - XRPL_ASSERT_PARTS( - borrower == account_ || borrower == counterparty, - "ripple::LoanSet::doApply", - "borrower signed transaction"); - if (auto const ter = addEmptyHolding( - view, - borrower, - borrowerSle->at(sfBalance).value().xrp(), - vaultAsset, - j_); - ter && ter != tecDUPLICATE) - // ignore tecDUPLICATE. That means the holding already exists, and - // is fine here - return ter; - - if (auto const ter = - requireAuth(view, vaultAsset, borrower, AuthType::StrongAuth)) - return ter; - - // 2. Transfer originationFee, if any, from vault pseudo-account to - // LoanBroker owner. - if (originationFee != beast::zero) - { - // Create the holding if it doesn't already exist (necessary for MPTs). - // The owner may have deleted their MPT / line at some point. - XRPL_ASSERT_PARTS( - brokerOwner == account_ || brokerOwner == counterparty, - "ripple::LoanSet::doApply", - "broker owner signed transaction"); - - if (auto const ter = addEmptyHolding( - view, - brokerOwner, - brokerOwnerSle->at(sfBalance).value().xrp(), - vaultAsset, - j_); - ter && ter != tecDUPLICATE) - // ignore tecDUPLICATE. That means the holding already exists, - // and is fine here - return ter; - - if (auto const ter = requireAuth( - view, vaultAsset, brokerOwner, AuthType::StrongAuth)) - return ter; - } - - if (auto const ter = accountSendMulti( - view, - vaultPseudo, - vaultAsset, - {{borrower, loanAssetsToBorrower}, {brokerOwner, originationFee}}, - j_, - WaiveTransferFee::Yes)) - return ter; - - // Get shortcuts to the loan property values - auto const startDate = getStartDate(view); - auto loanSequenceProxy = brokerSle->at(sfLoanSequence); - - // Create the loan - auto loan = - std::make_shared(keylet::loan(brokerID, *loanSequenceProxy)); - - // Prevent copy/paste errors - auto setLoanField = - [&loan, &tx](auto const& field, std::uint32_t const defValue = 0) { - // at() is smart enough to unseat a default field set to the default - // value - loan->at(field) = tx[field].value_or(defValue); - }; - - // Set required and fixed tx fields - loan->at(sfLoanScale) = properties.loanScale; - loan->at(sfStartDate) = startDate; - loan->at(sfPaymentInterval) = paymentInterval; - loan->at(sfLoanSequence) = *loanSequenceProxy; - loan->at(sfLoanBrokerID) = brokerID; - loan->at(sfBorrower) = borrower; - // Set all other transaction fields directly from the transaction - if (tx.isFlag(tfLoanOverpayment)) - loan->setFlag(lsfLoanOverpayment); - setLoanField(~sfLoanOriginationFee); - setLoanField(~sfLoanServiceFee); - setLoanField(~sfLatePaymentFee); - setLoanField(~sfClosePaymentFee); - setLoanField(~sfOverpaymentFee); - setLoanField(~sfInterestRate); - setLoanField(~sfLateInterestRate); - setLoanField(~sfCloseInterestRate); - setLoanField(~sfOverpaymentInterestRate); - setLoanField(~sfGracePeriod, defaultGracePeriod); - // Set dynamic / computed fields to their initial values - loan->at(sfPrincipalOutstanding) = principalRequested; - loan->at(sfPeriodicPayment) = properties.periodicPayment; - loan->at(sfTotalValueOutstanding) = properties.totalValueOutstanding; - loan->at(sfManagementFeeOutstanding) = properties.managementFeeOwedToBroker; - loan->at(sfPreviousPaymentDate) = 0; - loan->at(sfNextPaymentDueDate) = startDate + paymentInterval; - loan->at(sfPaymentRemaining) = paymentTotal; - view.insert(loan); - - // Update the balances in the vault - vaultAvailableProxy -= principalRequested; - vaultTotalProxy += state.interestDue; - XRPL_ASSERT_PARTS( - *vaultAvailableProxy <= *vaultTotalProxy, - "ripple::LoanSet::doApply", - "assets available must not be greater than assets outstanding"); - view.update(vaultSle); - - // Update the balances in the loan broker - adjustImpreciseNumber( - brokerSle->at(sfDebtTotal), newDebtDelta, vaultAsset, vaultScale); - // The broker's owner count is solely for the number of outstanding loans, - // and is distinct from the broker's pseudo-account's owner count - adjustOwnerCount(view, brokerSle, 1, j_); - loanSequenceProxy += 1; - // The sequence should be extremely unlikely to roll over, but fail if it - // does - if (loanSequenceProxy == 0) - return tecMAX_SEQUENCE_REACHED; - view.update(brokerSle); - - // Put the loan into the pseudo-account's directory - if (auto const ter = dirLink(view, brokerPseudo, loan, sfLoanBrokerNode)) - return ter; - // Borrower is the owner of the loan - if (auto const ter = dirLink(view, borrower, loan, sfOwnerNode)) - return ter; - - return tesSUCCESS; -} - -//------------------------------------------------------------------------------ - -} // namespace ripple diff --git a/src/xrpld/app/tx/detail/LoanSet.h b/src/xrpld/app/tx/detail/LoanSet.h deleted file mode 100644 index 91f3960891..0000000000 --- a/src/xrpld/app/tx/detail/LoanSet.h +++ /dev/null @@ -1,59 +0,0 @@ -#ifndef XRPL_TX_LOANSET_H_INCLUDED -#define XRPL_TX_LOANSET_H_INCLUDED - -#include -#include - -namespace ripple { - -class LoanSet : public Transactor -{ -public: - static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; - - explicit LoanSet(ApplyContext& ctx) : Transactor(ctx) - { - } - - static bool - checkExtraFeatures(PreflightContext const& ctx); - - static std::uint32_t - getFlagsMask(PreflightContext const& ctx); - - static NotTEC - preflight(PreflightContext const& ctx); - - static NotTEC - checkSign(PreclaimContext const& ctx); - - static XRPAmount - calculateBaseFee(ReadView const& view, STTx const& tx); - - static std::vector> const& - getValueFields(); - - static TER - preclaim(PreclaimContext const& ctx); - - TER - doApply() override; - -public: - static std::uint32_t constexpr minPaymentTotal = 1; - static std::uint32_t constexpr defaultPaymentTotal = 1; - static_assert(defaultPaymentTotal >= minPaymentTotal); - - static std::uint32_t constexpr minPaymentInterval = 60; - static std::uint32_t constexpr defaultPaymentInterval = 60; - static_assert(defaultPaymentInterval >= minPaymentInterval); - - static std::uint32_t constexpr defaultGracePeriod = 60; - static_assert(defaultGracePeriod >= minPaymentInterval); -}; - -//------------------------------------------------------------------------------ - -} // namespace ripple - -#endif diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp index b6eaacb372..858fd6d0d6 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp @@ -94,8 +94,7 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx) return tesSUCCESS; } - auto const sleHolder = ctx.view.read(keylet::account(*holderID)); - if (!sleHolder) + if (!ctx.view.exists(keylet::account(*holderID))) return tecNO_DST; auto const sleMptIssuance = @@ -125,12 +124,6 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx) keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], *holderID))) return tecOBJECT_NOT_FOUND; - // Can't unauthorize the pseudo-accounts because they are implicitly - // always authorized. No need to amendment gate since Vault and LoanBroker - // can only be created if the Vault amendment is enabled. - if (isPseudoAccount(ctx.view, *holderID, {&sfVaultID, &sfLoanBrokerID})) - return tecNO_PERMISSION; - return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/SetTrust.cpp b/src/xrpld/app/tx/detail/SetTrust.cpp index 4e8f20f3ae..37ecdf2386 100644 --- a/src/xrpld/app/tx/detail/SetTrust.cpp +++ b/src/xrpld/app/tx/detail/SetTrust.cpp @@ -251,9 +251,7 @@ SetTrust::preclaim(PreclaimContext const& ctx) else return tecINTERNAL; // LCOV_EXCL_LINE } - else if ( - sleDst->isFieldPresent(sfVaultID) || - sleDst->isFieldPresent(sfLoanBrokerID)) + else if (sleDst->isFieldPresent(sfVaultID)) { if (!ctx.view.exists(keylet::line(id, uDstAccountID, currency))) return tecNO_PERMISSION; diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 2ddef72c39..9d84abe12e 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -204,24 +204,13 @@ Transactor::preflight2(PreflightContext const& ctx) // regardless of success or failure return *ret; - // Skip signature check on batch inner transactions - if (ctx.tx.isFlag(tfInnerBatchTxn) && !ctx.rules.enabled(featureBatch)) - return tesSUCCESS; - // Do not add any checks after this point that are relevant for - // batch inner transactions. They will be skipped. - auto const sigValid = checkValidity( ctx.app.getHashRouter(), ctx.tx, ctx.rules, ctx.app.config()); if (sigValid.first == Validity::SigBad) - { // LCOV_EXCL_START + { JLOG(ctx.j.debug()) << "preflight2: bad signature. " << sigValid.second; - return temINVALID; - // LCOV_EXCL_STOP + return temINVALID; // LCOV_EXCL_LINE } - - // Do not add any checks after this point that are relevant for - // batch inner transactions. They will be skipped. - return tesSUCCESS; } @@ -653,25 +642,13 @@ NotTEC Transactor::checkSign( ReadView const& view, ApplyFlags flags, - std::optional const& parentBatchId, AccountID const& idAccount, STObject const& sigObject, beast::Journal const j) { - { - auto const sle = view.read(keylet::account(idAccount)); - - if (view.rules().enabled(featureLendingProtocol) && - isPseudoAccount(sle)) - // Pseudo-accounts can't sign transactions. This check is gated on - // the Lending Protocol amendment because that's the project it was - // added under, and it doesn't justify another amendment - return tefBAD_AUTH; - } - auto const pkSigner = sigObject.getFieldVL(sfSigningPubKey); // Ignore signature check on batch inner transactions - if (parentBatchId && view.rules().enabled(featureBatch)) + if (sigObject.isFlag(tfInnerBatchTxn) && view.rules().enabled(featureBatch)) { // Defensive Check: These values are also checked in Batch::preflight if (sigObject.isFieldPresent(sfTxnSignature) || !pkSigner.empty() || @@ -724,8 +701,7 @@ Transactor::checkSign(PreclaimContext const& ctx) auto const idAccount = ctx.tx.isFieldPresent(sfDelegate) ? ctx.tx.getAccountID(sfDelegate) : ctx.tx.getAccountID(sfAccount); - return checkSign( - ctx.view, ctx.flags, ctx.parentBatchId, idAccount, ctx.tx, ctx.j); + return checkSign(ctx.view, ctx.flags, idAccount, ctx.tx, ctx.j); } NotTEC diff --git a/src/xrpld/app/tx/detail/Transactor.h b/src/xrpld/app/tx/detail/Transactor.h index ea3479ee6f..2195ef0a85 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -266,7 +266,6 @@ protected: checkSign( ReadView const& view, ApplyFlags flags, - std::optional const& parentBatchId, AccountID const& idAccount, STObject const& sigObject, beast::Journal const j); @@ -288,26 +287,14 @@ protected: template static bool - validNumericRange(std::optional value, T max, T min = T{}); + validNumericRange(std::optional value, T max, T min = {}); template static bool validNumericRange( std::optional value, unit::ValueUnit max, - unit::ValueUnit min = unit::ValueUnit{}); - - /// Minimum will usually be zero. - template - static bool - validNumericMinimum(std::optional value, T min = T{}); - - /// Minimum will usually be zero. - template - static bool - validNumericMinimum( - std::optional value, - unit::ValueUnit min = unit::ValueUnit{}); + unit::ValueUnit min = {}); private: std::pair @@ -434,24 +421,6 @@ Transactor::validNumericRange( return validNumericRange(value, max.value(), min.value()); } -template -bool -Transactor::validNumericMinimum(std::optional value, T min) -{ - if (!value) - return true; - return value >= min; -} - -template -bool -Transactor::validNumericMinimum( - std::optional value, - unit::ValueUnit min) -{ - return validNumericMinimum(value, min.value()); -} - } // namespace ripple #endif diff --git a/src/xrpld/app/tx/detail/VaultCreate.cpp b/src/xrpld/app/tx/detail/VaultCreate.cpp index d8a2e7bc3b..393faa35f8 100644 --- a/src/xrpld/app/tx/detail/VaultCreate.cpp +++ b/src/xrpld/app/tx/detail/VaultCreate.cpp @@ -22,11 +22,8 @@ VaultCreate::checkExtraFeatures(PreflightContext const& ctx) if (!ctx.rules.enabled(featureMPTokensV1)) return false; - if (ctx.tx.isFieldPresent(sfDomainID) && - !ctx.rules.enabled(featurePermissionedDomains)) - return false; - - return true; + return !ctx.tx.isFieldPresent(sfDomainID) || + ctx.rules.enabled(featurePermissionedDomains); } std::uint32_t diff --git a/src/xrpld/app/tx/detail/VaultDeposit.cpp b/src/xrpld/app/tx/detail/VaultDeposit.cpp index aeaf890126..3e5ae741e3 100644 --- a/src/xrpld/app/tx/detail/VaultDeposit.cpp +++ b/src/xrpld/app/tx/detail/VaultDeposit.cpp @@ -36,19 +36,41 @@ VaultDeposit::preclaim(PreclaimContext const& ctx) if (!vault) return tecNO_ENTRY; - auto const& account = ctx.tx[sfAccount]; + auto const account = ctx.tx[sfAccount]; auto const assets = ctx.tx[sfAmount]; auto const vaultAsset = vault->at(sfAsset); if (assets.asset() != vaultAsset) return tecWRONG_ASSET; - auto const& vaultAccount = vault->at(sfAccount); - if (auto ter = canTransfer(ctx.view, vaultAsset, account, vaultAccount); - !isTesSuccess(ter)) + if (vaultAsset.native()) + ; // No special checks for XRP + else if (vaultAsset.holds()) { - JLOG(ctx.j.debug()) - << "VaultDeposit: vault assets are non-transferable."; - return ter; + auto mptID = vaultAsset.get().getMptID(); + auto issuance = ctx.view.read(keylet::mptIssuance(mptID)); + if (!issuance) + return tecOBJECT_NOT_FOUND; + if (!issuance->isFlag(lsfMPTCanTransfer)) + { + // LCOV_EXCL_START + JLOG(ctx.j.error()) + << "VaultDeposit: vault assets are non-transferable."; + return tecNO_AUTH; + // LCOV_EXCL_STOP + } + } + else if (vaultAsset.holds()) + { + auto const issuer = + ctx.view.read(keylet::account(vaultAsset.getIssuer())); + if (!issuer) + { + // LCOV_EXCL_START + JLOG(ctx.j.error()) + << "VaultDeposit: missing issuer of vault assets."; + return tefINTERNAL; + // LCOV_EXCL_STOP + } } auto const mptIssuanceID = vault->at(sfShareMPTID); diff --git a/src/xrpld/app/tx/detail/VaultSet.cpp b/src/xrpld/app/tx/detail/VaultSet.cpp index 97522dad12..38ab6296ef 100644 --- a/src/xrpld/app/tx/detail/VaultSet.cpp +++ b/src/xrpld/app/tx/detail/VaultSet.cpp @@ -14,11 +14,8 @@ namespace ripple { bool VaultSet::checkExtraFeatures(PreflightContext const& ctx) { - if (ctx.tx.isFieldPresent(sfDomainID) && - !ctx.rules.enabled(featurePermissionedDomains)) - return false; - - return true; + return !ctx.tx.isFieldPresent(sfDomainID) || + ctx.rules.enabled(featurePermissionedDomains); } NotTEC diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.cpp b/src/xrpld/app/tx/detail/VaultWithdraw.cpp index a3bef88d49..7777f2257f 100644 --- a/src/xrpld/app/tx/detail/VaultWithdraw.cpp +++ b/src/xrpld/app/tx/detail/VaultWithdraw.cpp @@ -23,10 +23,13 @@ VaultWithdraw::preflight(PreflightContext const& ctx) if (ctx.tx[sfAmount] <= beast::zero) return temBAD_AMOUNT; - if (auto const destination = ctx.tx[~sfDestination]) + if (auto const destination = ctx.tx[~sfDestination]; + destination.has_value()) { if (*destination == beast::zero) { + JLOG(ctx.j.debug()) + << "VaultWithdraw: zero/empty destination account."; return temMALFORMED; } } @@ -47,15 +50,35 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx) if (assets.asset() != vaultAsset && assets.asset() != vaultShare) return tecWRONG_ASSET; - auto const& vaultAccount = vault->at(sfAccount); - auto const& account = ctx.tx[sfAccount]; - auto const& dstAcct = ctx.tx[~sfDestination].value_or(account); - if (auto ter = canTransfer(ctx.view, vaultAsset, vaultAccount, dstAcct); - !isTesSuccess(ter)) + if (vaultAsset.native()) + ; // No special checks for XRP + else if (vaultAsset.holds()) { - JLOG(ctx.j.debug()) - << "VaultWithdraw: vault assets are non-transferable."; - return ter; + auto mptID = vaultAsset.get().getMptID(); + auto issuance = ctx.view.read(keylet::mptIssuance(mptID)); + if (!issuance) + return tecOBJECT_NOT_FOUND; + if (!issuance->isFlag(lsfMPTCanTransfer)) + { + // LCOV_EXCL_START + JLOG(ctx.j.error()) + << "VaultWithdraw: vault assets are non-transferable."; + return tecNO_AUTH; + // LCOV_EXCL_STOP + } + } + else if (vaultAsset.holds()) + { + auto const issuer = + ctx.view.read(keylet::account(vaultAsset.getIssuer())); + if (!issuer) + { + // LCOV_EXCL_START + JLOG(ctx.j.error()) + << "VaultWithdraw: missing issuer of vault assets."; + return tefINTERNAL; + // LCOV_EXCL_STOP + } } // Enforce valid withdrawal policy @@ -67,8 +90,23 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx) // LCOV_EXCL_STOP } - if (auto const ret = canWithdraw(ctx.view, ctx.tx)) - return ret; + auto const account = ctx.tx[sfAccount]; + auto const dstAcct = ctx.tx[~sfDestination].value_or(account); + auto const sleDst = ctx.view.read(keylet::account(dstAcct)); + if (sleDst == nullptr) + return account == dstAcct ? tecINTERNAL : tecNO_DST; + + if (sleDst->isFlag(lsfRequireDestTag) && + !ctx.tx.isFieldPresent(sfDestinationTag)) + return tecDST_TAG_NEEDED; // Cannot send without a tag + + // Withdrawal to a 3rd party destination account is essentially a transfer, + // via shares in the vault. Enforce all the usual asset transfer checks. + if (account != dstAcct && sleDst->isFlag(lsfDepositAuth)) + { + if (!ctx.view.exists(keylet::depositPreauth(dstAcct, account))) + return tecNO_PERMISSION; + } // If sending to Account (i.e. not a transfer), we will also create (only // if authorized) a trust line or MPToken as needed, in doApply(). @@ -238,16 +276,43 @@ VaultWithdraw::doApply() } auto const dstAcct = ctx_.tx[~sfDestination].value_or(account_); + if (!vaultAsset.native() && // + dstAcct != vaultAsset.getIssuer() && // + dstAcct == account_) + { + if (auto const ter = addEmptyHolding( + view(), account_, mPriorBalance, vaultAsset, j_); + !isTesSuccess(ter) && ter != tecDUPLICATE) + return ter; + } - return doWithdraw( - view(), - ctx_.tx, - account_, - dstAcct, - vaultAccount, - mPriorBalance, - assetsWithdrawn, - j_); + // Transfer assets from vault to depositor or destination account. + if (auto const ter = accountSend( + view(), + vaultAccount, + dstAcct, + assetsWithdrawn, + j_, + WaiveTransferFee::Yes); + !isTesSuccess(ter)) + return ter; + + // Sanity check + if (accountHolds( + view(), + vaultAccount, + assetsWithdrawn.asset(), + FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, + j_) < beast::zero) + { + // LCOV_EXCL_START + JLOG(j_.error()) << "VaultWithdraw: negative balance of vault assets."; + return tefINTERNAL; + // LCOV_EXCL_STOP + } + + return tesSUCCESS; } } // namespace ripple diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index 0a973a9218..b85536c6f1 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -1365,23 +1365,8 @@ PeerImp::handleTransaction( // Charge strongly for attempting to relay a txn with tfInnerBatchTxn // LCOV_EXCL_START - /* - There is no need to check whether the featureBatch amendment is - enabled. - - * If the `tfInnerBatchTxn` flag is set, and the amendment is - enabled, then it's an invalid transaction because inner batch - transactions should not be relayed. - * If the `tfInnerBatchTxn` flag is set, and the amendment is *not* - enabled, then the transaction is malformed because it's using an - "unknown" flag. There's no need to waste the resources to send it - to the transaction engine. - - We don't normally check transaction validity at this level, but - since we _need_ to check it when the amendment is enabled, we may as - well drop it if the flag is set regardless. - */ - if (stx->isFlag(tfInnerBatchTxn)) + if (stx->isFlag(tfInnerBatchTxn) && + getCurrentTransactionRules()->enabled(featureBatch)) { JLOG(p_journal_.warn()) << "Ignoring Network relayed Tx containing " "tfInnerBatchTxn (handleTransaction)."; @@ -2939,23 +2924,8 @@ PeerImp::checkTransaction( { // charge strongly for relaying batch txns // LCOV_EXCL_START - /* - There is no need to check whether the featureBatch amendment is - enabled. - - * If the `tfInnerBatchTxn` flag is set, and the amendment is - enabled, then it's an invalid transaction because inner batch - transactions should not be relayed. - * If the `tfInnerBatchTxn` flag is set, and the amendment is *not* - enabled, then the transaction is malformed because it's using an - "unknown" flag. There's no need to waste the resources to send it - to the transaction engine. - - We don't normally check transaction validity at this level, but - since we _need_ to check it when the amendment is enabled, we may as - well drop it if the flag is set regardless. - */ - if (stx->isFlag(tfInnerBatchTxn)) + if (stx->isFlag(tfInnerBatchTxn) && + getCurrentTransactionRules()->enabled(featureBatch)) { JLOG(p_journal_.warn()) << "Ignoring Network relayed Tx containing " "tfInnerBatchTxn (checkSignature)."; diff --git a/src/xrpld/rpc/detail/TransactionSign.cpp b/src/xrpld/rpc/detail/TransactionSign.cpp index 8ca58d671e..5e9e2d9cc4 100644 --- a/src/xrpld/rpc/detail/TransactionSign.cpp +++ b/src/xrpld/rpc/detail/TransactionSign.cpp @@ -1078,11 +1078,7 @@ checkMultiSignFields(Json::Value const& jvRequest) if (!tx_json.isMember(sfSigningPubKey.getJsonName())) return RPC::missing_field_error("tx_json.SigningPubKey"); - // Multi-signing into a signature_target object field is fine, - // because it means the signature is not for the transaction - // Account. - if (!jvRequest.isMember(jss::signature_target) && - !tx_json[sfSigningPubKey.getJsonName()].asString().empty()) + if (!tx_json[sfSigningPubKey.getJsonName()].asString().empty()) return RPC::make_error( rpcINVALID_PARAMS, "When multi-signing 'tx_json.SigningPubKey' must be empty."); diff --git a/src/xrpld/rpc/handlers/AccountInfo.cpp b/src/xrpld/rpc/handlers/AccountInfo.cpp index e9f95d450b..7365ccc6ef 100644 --- a/src/xrpld/rpc/handlers/AccountInfo.cpp +++ b/src/xrpld/rpc/handlers/AccountInfo.cpp @@ -129,28 +129,6 @@ doAccountInfo(RPC::JsonContext& context) result[jss::account_flags] = std::move(acctFlags); - auto const pseudoFields = getPseudoAccountFields(); - for (auto const& pseudoField : pseudoFields) - { - if (sleAccepted->isFieldPresent(*pseudoField)) - { - std::string name = pseudoField->fieldName; - if (name.ends_with("ID")) - { - // Remove the ID suffix from the field name. - name = name.substr(0, name.size() - 2); - XRPL_ASSERT_PARTS( - !name.empty(), - "ripple::doAccountInfo", - "name is not empty"); - } - // ValidPseudoAccounts invariant guarantees that only one field - // can be set - result[jss::pseudo_account][jss::type] = name; - break; - } - } - // The document[https://xrpl.org/account_info.html#account_info] states // that signer_lists is a bool, however assigning any string value // works. Do not allow this. This check is for api Version 2 onwards diff --git a/src/xrpld/rpc/handlers/LedgerEntry.cpp b/src/xrpld/rpc/handlers/LedgerEntry.cpp index 5a1dfb4ace..bf8a4a5b80 100644 --- a/src/xrpld/rpc/handlers/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/LedgerEntry.cpp @@ -395,46 +395,6 @@ parseLedgerHashes(Json::Value const& params, Json::StaticString const fieldName) return parseObjectID(params, fieldName, "hex string"); } -static Expected -parseLoanBroker(Json::Value const& params, Json::StaticString const fieldName) -{ - if (!params.isObject()) - { - return parseObjectID(params, fieldName, "hex string"); - } - - auto const id = LedgerEntryHelpers::requiredAccountID( - params, jss::owner, "malformedOwner"); - if (!id) - return Unexpected(id.error()); - auto const seq = - LedgerEntryHelpers::requiredUInt32(params, jss::seq, "malformedSeq"); - if (!seq) - return Unexpected(seq.error()); - - return keylet::loanbroker(*id, *seq).key; -} - -static Expected -parseLoan(Json::Value const& params, Json::StaticString const fieldName) -{ - if (!params.isObject()) - { - return parseObjectID(params, fieldName, "hex string"); - } - - auto const id = LedgerEntryHelpers::requiredUInt256( - params, jss::loan_broker_id, "malformedOwner"); - if (!id) - return Unexpected(id.error()); - auto const seq = LedgerEntryHelpers::requiredUInt32( - params, jss::loan_seq, "malformedSeq"); - if (!seq) - return Unexpected(seq.error()); - - return keylet::loan(*id, *seq).key; -} - static Expected parseMPToken(Json::Value const& params, Json::StaticString const fieldName) { From ad190a8d6fa804352d1673559f59e4d20b5cf6a4 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Mon, 1 Dec 2025 16:18:12 -0500 Subject: [PATCH 287/291] Review feedback from @dangell7: early return & coverage - Exclude LogicError lines in ApplyView.cpp (specifically directory operations) from code coverage. - Replace the ability to set the next page on a new directory page with an assert, because nothing uses it right now. - Early return with success for batch inner transactions in preflight2. --- src/libxrpl/ledger/ApplyView.cpp | 41 +++++++++++++++++++++----- src/xrpld/app/tx/detail/Transactor.cpp | 26 +++++++++------- 2 files changed, 49 insertions(+), 18 deletions(-) diff --git a/src/libxrpl/ledger/ApplyView.cpp b/src/libxrpl/ledger/ApplyView.cpp index 5e8d03b506..911bec1722 100644 --- a/src/libxrpl/ledger/ApplyView.cpp +++ b/src/libxrpl/ledger/ApplyView.cpp @@ -40,7 +40,10 @@ findPreviousPage(ApplyView& view, Keylet const& directory, SLE::ref start) { node = view.peek(keylet::page(directory, page)); if (!node) + { // LCOV_EXCL_START LogicError("Directory chain: root back-pointer broken."); + // LCOV_EXCL_STOP + } } auto indexes = node->getFieldV256(sfIndexes); @@ -59,7 +62,7 @@ insertKey( if (preserveOrder) { if (std::find(indexes.begin(), indexes.end(), key) != indexes.end()) - LogicError("dirInsert: double insertion"); + LogicError("dirInsert: double insertion"); // LCOV_EXCL_LINE indexes.push_back(key); } @@ -73,7 +76,7 @@ insertKey( auto pos = std::lower_bound(indexes.begin(), indexes.end(), key); if (pos != indexes.end() && key == *pos) - LogicError("dirInsert: double insertion"); + LogicError("dirInsert: double insertion"); // LCOV_EXCL_LINE indexes.insert(pos, key); } @@ -131,8 +134,15 @@ insertPage( // it's the default. if (page != 1) node->setFieldU64(sfIndexPrevious, page - 1); + XRPL_ASSERT_PARTS( + !nextPage, + "ripple::directory::insertPage", + "nextPage has default value"); + /* Reserved for future use when directory pages may be inserted in + * between two other pages instead of only at the end of the chain. if (nextPage) node->setFieldU64(sfIndexNext, nextPage); + */ describe(node); view.insert(node); @@ -197,10 +207,10 @@ ApplyView::emptyDirDelete(Keylet const& directory) auto nextPage = node->getFieldU64(sfIndexNext); if (nextPage == rootPage && prevPage != rootPage) - LogicError("Directory chain: fwd link broken"); + LogicError("Directory chain: fwd link broken"); // LCOV_EXCL_LINE if (prevPage == rootPage && nextPage != rootPage) - LogicError("Directory chain: rev link broken"); + LogicError("Directory chain: rev link broken"); // LCOV_EXCL_LINE // Older versions of the code would, in some cases, allow the last // page to be empty. Remove such pages: @@ -209,7 +219,10 @@ ApplyView::emptyDirDelete(Keylet const& directory) auto last = peek(keylet::page(directory, nextPage)); if (!last) + { // LCOV_EXCL_START LogicError("Directory chain: fwd link broken."); + // LCOV_EXCL_STOP + } if (!last->getFieldV256(sfIndexes).empty()) return false; @@ -281,10 +294,16 @@ ApplyView::dirRemove( if (page == rootPage) { if (nextPage == page && prevPage != page) + { // LCOV_EXCL_START LogicError("Directory chain: fwd link broken"); + // LCOV_EXCL_STOP + } if (prevPage == page && nextPage != page) + { // LCOV_EXCL_START LogicError("Directory chain: rev link broken"); + // LCOV_EXCL_STOP + } // Older versions of the code would, in some cases, // allow the last page to be empty. Remove such @@ -293,7 +312,10 @@ ApplyView::dirRemove( { auto last = peek(keylet::page(directory, nextPage)); if (!last) + { // LCOV_EXCL_START LogicError("Directory chain: fwd link broken."); + // LCOV_EXCL_STOP + } if (last->getFieldV256(sfIndexes).empty()) { @@ -325,10 +347,10 @@ ApplyView::dirRemove( // This can never happen for nodes other than the root: if (nextPage == page) - LogicError("Directory chain: fwd link broken"); + LogicError("Directory chain: fwd link broken"); // LCOV_EXCL_LINE if (prevPage == page) - LogicError("Directory chain: rev link broken"); + LogicError("Directory chain: rev link broken"); // LCOV_EXCL_LINE // This node isn't the root, so it can either be in the // middle of the list, or at the end. Unlink it first @@ -336,14 +358,14 @@ ApplyView::dirRemove( // root: auto prev = peek(keylet::page(directory, prevPage)); if (!prev) - LogicError("Directory chain: fwd link broken."); + LogicError("Directory chain: fwd link broken."); // LCOV_EXCL_LINE // Fix previous to point to its new next. prev->setFieldU64(sfIndexNext, nextPage); update(prev); auto next = peek(keylet::page(directory, nextPage)); if (!next) - LogicError("Directory chain: rev link broken."); + LogicError("Directory chain: rev link broken."); // LCOV_EXCL_LINE // Fix next to point to its new previous. next->setFieldU64(sfIndexPrevious, prevPage); update(next); @@ -367,7 +389,10 @@ ApplyView::dirRemove( // And the root points to the last page: auto root = peek(keylet::page(directory, rootPage)); if (!root) + { // LCOV_EXCL_START LogicError("Directory chain: root link broken."); + // LCOV_EXCL_STOP + } root->setFieldU64(sfIndexPrevious, prevPage); update(root); diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 5da62f2cde..2ddef72c39 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -205,17 +205,23 @@ Transactor::preflight2(PreflightContext const& ctx) return *ret; // Skip signature check on batch inner transactions - if (!ctx.tx.isFlag(tfInnerBatchTxn) || !ctx.rules.enabled(featureBatch)) - { - auto const sigValid = checkValidity( - ctx.app.getHashRouter(), ctx.tx, ctx.rules, ctx.app.config()); - if (sigValid.first == Validity::SigBad) - { // LCOV_EXCL_START - JLOG(ctx.j.debug()) - << "preflight2: bad signature. " << sigValid.second; - return temINVALID; - } // LCOV_EXCL_STOP + if (ctx.tx.isFlag(tfInnerBatchTxn) && !ctx.rules.enabled(featureBatch)) + return tesSUCCESS; + // Do not add any checks after this point that are relevant for + // batch inner transactions. They will be skipped. + + auto const sigValid = checkValidity( + ctx.app.getHashRouter(), ctx.tx, ctx.rules, ctx.app.config()); + if (sigValid.first == Validity::SigBad) + { // LCOV_EXCL_START + JLOG(ctx.j.debug()) << "preflight2: bad signature. " << sigValid.second; + return temINVALID; + // LCOV_EXCL_STOP } + + // Do not add any checks after this point that are relevant for + // batch inner transactions. They will be skipped. + return tesSUCCESS; } From 803380c53cb6932f02caaccf6839c0ac2d5ef73b Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Mon, 1 Dec 2025 20:59:12 +0100 Subject: [PATCH 288/291] Fix overpayment asserts (#6084) --- src/xrpld/app/misc/detail/LendingHelpers.cpp | 22 +++++++------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index 8020b47ba9..1361b0679a 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -547,6 +547,14 @@ tryOverpayment( auto const deltas = rounded - newRounded; + // The change in loan management fee is equal to the change between the old + // and the new outstanding management fees + XRPL_ASSERT_PARTS( + deltas.managementFee == + rounded.managementFeeDue - managementFeeOutstanding, + "ripple::detail::tryOverpayment", + "no fee change"); + auto const hypotheticalValueOutstanding = rounded.valueOutstanding - deltas.principal; @@ -561,7 +569,6 @@ tryOverpayment( "the loan. Ignore the overpayment"; return Unexpected(tesSUCCESS); } - return LoanPaymentParts{ // Principal paid is the reduction in principal outstanding .principalPaid = deltas.principal, @@ -676,12 +683,6 @@ doOverpayment( "ripple::detail::doOverpayment", "principal change agrees"); - XRPL_ASSERT_PARTS( - overpaymentComponents.trackedManagementFeeDelta == - managementFeeOutstandingProxy - managementFeeOutstanding, - "ripple::detail::doOverpayment", - "no fee change"); - // I'm not 100% sure the following asserts are correct. If in doubt, and // everything else works, remove any that cause trouble. @@ -712,13 +713,6 @@ doOverpayment( "ripple::detail::doOverpayment", "principal payment matches"); - XRPL_ASSERT_PARTS( - loanPaymentParts.feePaid == - overpaymentComponents.untrackedManagementFee + - overpaymentComponents.trackedManagementFeeDelta, - "ripple::detail::doOverpayment", - "fee payment matches"); - // All validations passed, so update the proxy objects (which will // modify the actual Loan ledger object) totalValueOutstandingProxy = totalValueOutstanding; From e2e9582ff102fbc69cf4372eee8d2dc12856b70c Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 2 Dec 2025 10:50:57 -0500 Subject: [PATCH 289/291] Test updates - show balances in runLoan() --- src/test/app/Loan_test.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index b2ad47c2b4..aa196d0e65 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -1271,7 +1271,8 @@ protected: verifyLoanStatus, issuer, lender, - borrower); + borrower, + PaymentParameters{.showStepBalances = true}); } /** Runs through the complete lifecycle of a loan @@ -7193,15 +7194,15 @@ class LoanArbitrary_test : public LoanBatch_test .vaultDeposit = 10000, .debtMax = 0, .coverRateMin = TenthBips32{0}, - // .managementFeeRate = TenthBips16{5919}, + .managementFeeRate = TenthBips16{0}, .coverRateLiquidation = TenthBips32{0}}; LoanParameters const loanParams{ .account = Account("lender"), .counter = Account("borrower"), - .principalRequest = Number{10000, 0}, - // .interest = TenthBips32{0}, - // .payTotal = 5816, - .payInterval = 150}; + .principalRequest = Number{200000, -6}, + .interest = TenthBips32{50000}, + .payTotal = 2, + .payInterval = 200}; runLoan(AssetType::XRP, brokerParams, loanParams); } From 43a6f10050bc6c6680ffe2f753bcb80f89efa2e7 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 2 Dec 2025 22:56:19 -0500 Subject: [PATCH 290/291] MPTTester::operator() parameter should be std::int64_t - Originally defined as uint64_t, but the testIssuerLoan() test called it with a negative number, causing an overflow to a very large number that in some circumstances could be silently cast back to an int64_t, but might not be. I believe this is UB, and we don't want to rely on that. --- src/test/jtx/impl/mpt.cpp | 2 +- src/test/jtx/mpt.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index fc831790f1..d7b4a1570e 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -644,7 +644,7 @@ MPTTester::operator[](std::string const& name) const } PrettyAmount -MPTTester::operator()(std::uint64_t amount) const +MPTTester::operator()(std::int64_t amount) const { return MPT("", issuanceID())(amount); } diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index f9c58ebc9e..e3097b28b0 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -272,7 +272,7 @@ public: operator[](std::string const& name) const; PrettyAmount - operator()(std::uint64_t amount) const; + operator()(std::int64_t amount) const; operator Asset() const; From 0650e6e89dbd0e3371773983bc7fc1b888d70b65 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 3 Dec 2025 19:49:47 -0500 Subject: [PATCH 291/291] Fix LCOV exclusion --- src/xrpld/app/tx/detail/LoanBrokerDelete.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp b/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp index f3dd781bb5..ea98141ea6 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerDelete.cpp @@ -67,7 +67,7 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx) JLOG(ctx.j.warn()) << "LoanBrokerDelete: Debt total is " << debtTotal << ", which rounds to " << rounded; return tecHAS_OBLIGATIONS; - // LCOV_EXCL_START + // LCOV_EXCL_STOP } }