From 5bed1d94e62abf33325e2be7a29fb589bc91381c Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 18 Apr 2025 17:45:30 -0400 Subject: [PATCH] [WIP] 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. --- include/xrpl/protocol/Protocol.h | 102 +++++++-- include/xrpl/protocol/TxFlags.h | 6 +- include/xrpl/protocol/detail/sfields.macro | 12 +- .../xrpl/protocol/detail/transactions.macro | 2 +- src/libxrpl/protocol/InnerObjectFormats.cpp | 20 +- src/test/app/LoanBroker_test.cpp | 10 +- .../app/tx/detail/LoanBrokerCoverWithdraw.cpp | 6 +- src/xrpld/app/tx/detail/LoanBrokerSet.cpp | 2 +- src/xrpld/app/tx/detail/LoanSet.cpp | 213 ++++++++++++++++++ src/xrpld/app/tx/detail/LoanSet.h | 61 +++++ 10 files changed, 386 insertions(+), 48 deletions(-) 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/Protocol.h b/include/xrpl/protocol/Protocol.h index b677df01b3..9544fe37bb 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -82,30 +82,86 @@ std::size_t constexpr maxDeletableTokenOfferEntries = 500; */ std::uint16_t constexpr maxTransferFee = 50000; -/** The maximum management fee rate allowed in lending. - - TODO: Is this a good name? - - Valid values for the the management fee charged by the Lending Protocol are - between 0 and 10000 inclusive. A value of 1 is equivalent to 1/10 basis - point fee or 0.001%. -*/ -std::uint16_t constexpr maxFeeRate = 10'000; - -/** The maximum coverage rate allowed in lending. - - TODO: Is this a good name? - - Valid values for the coverage rate charged by the Lending Protocol for first - loss capital operations are between 0 and 100000 inclusive. A value of 1 is - equivalent to 1/10 bps or 0.001%. -*/ -std::uint32_t constexpr maxCoverRate = 100'000; - -/** Basis points (bps) represent 0.01% of a thing. Given a value X, to find the - * amount for B bps, use X * B / bpsPerOne +/** 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. */ -std::uint32_t constexpr bpsPerOne = 10'000; +std::uint32_t constexpr bipsPerUnity = 100 * 100; +std::uint32_t constexpr tenthBipsPerUnity = bipsPerUnity * 10; + +constexpr std::uint32_t +percentageToBips(std::uint32_t percentage) +{ + return percentage * bipsPerUnity / 100; +} +constexpr std::uint32_t +percentageToTenthBips(std::uint32_t percentage) +{ + return percentage * tenthBipsPerUnity / 100; +} +template +constexpr T +bipsOfValue(T value, std::uint32_t bips) +{ + return value * bips / bipsPerUnity; +} +template +constexpr T +tenthBipsOfValue(T value, std::uint32_t bips) +{ + return value * bips / tenthBipsPerUnity; +} + +/** The maximum management fee rate allowed by a loan broker in 1/10 bips. + + Valid values are between 0 and 10% inclusive. +*/ +std::uint16_t constexpr maxManagementFeeRate = percentageToTenthBips(10); +static_assert(maxManagementFeeRate == 10'000); + +/** The maximum coverage rate required of a loan broker in 1/10 bips. + + Valid values are between 0 and 100% inclusive. +*/ +std::uint32_t constexpr maxCoverRate = percentageToTenthBips(100); +static_assert(maxCoverRate == 100'000); + +/** The maximum overpayment fee on a loan in 1/10 bips. +* + Valid values are between 0 and 100% inclusive. +*/ +std::uint32_t 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. + */ +std::uint32_t constexpr maxLateInterestRate = percentageToTenthBips(100); + +/** The maximum interest rate charged for repaying a loan early in 1/10 bips. + * + * Valid values are between 0 and 100% inclusive. + */ +std::uint32_t constexpr maxCloseInterestRate = percentageToTenthBips(100); + +/** The maximum interest rate charged on loan overpayments in 1/10 bips. + * + * Valid values are between 0 and 100% inclusive. + */ +std::uint32_t 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/TxFlags.h b/include/xrpl/protocol/TxFlags.h index d6aa7eb26e..6fe212394d 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -234,14 +234,14 @@ constexpr std::uint32_t const tfVaultShareNonTransferable = 0x00020000; constexpr std::uint32_t const tfVaultCreateMask = ~(tfUniversal | tfVaultPrivate | tfVaultShareNonTransferable); // LoanSet flags: -// True, indicates the load supports overpayments +// 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 = 0x00010000; -constexpr std::uint32_t const tfLoanUnimpair = 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 diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index a509c85c50..2c089a81de 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) @@ -60,10 +62,6 @@ TYPED_SFIELD(sfHookExecutionIndex, UINT16, 19) TYPED_SFIELD(sfHookApiVersion, UINT16, 20) TYPED_SFIELD(sfLedgerFixType, UINT16, 21) TYPED_SFIELD(sfManagementFeeRate, UINT16, 22) -TYPED_SFIELD(sfInterestRate, UINT16, 25) -TYPED_SFIELD(sfLateInterestRate, UINT16, 26) -TYPED_SFIELD(sfCloseInterestRate, UINT16, 27) -TYPED_SFIELD(sfOverpaymentInterestRate, UINT16, 28) // 32-bit integers (common) TYPED_SFIELD(sfNetworkID, UINT32, 1) @@ -127,6 +125,10 @@ TYPED_SFIELD(sfPaymentRemaining, UINT32, 57) TYPED_SFIELD(sfPaymentTotal, UINT32, 58) TYPED_SFIELD(sfCoverRateMinimum, UINT32, 59) TYPED_SFIELD(sfCoverRateLiquidation, UINT32, 60) +TYPED_SFIELD(sfInterestRate, UINT32, 61) +TYPED_SFIELD(sfLateInterestRate, UINT32, 62) +TYPED_SFIELD(sfCloseInterestRate, UINT32, 63) +TYPED_SFIELD(sfOverpaymentInterestRate, UINT32, 64) // 64-bit integers (common) TYPED_SFIELD(sfIndexNext, UINT64, 1) @@ -418,3 +420,5 @@ UNTYPED_SFIELD(sfAuthAccounts, ARRAY, 25) UNTYPED_SFIELD(sfAuthorizeCredentials, ARRAY, 26) UNTYPED_SFIELD(sfUnauthorizeCredentials, ARRAY, 27) UNTYPED_SFIELD(sfAcceptedCredentials, ARRAY, 28) + +// clang-format on \ No newline at end of file diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 8c5070892c..2c16657a87 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -726,7 +726,6 @@ TRANSACTION(ttLOAN_BROKER_COVER_WITHDRAW, 77, LoanBrokerCoverWithdraw, noPriv, ( {sfAmount, soeREQUIRED, soeMPTSupported}, })) -#if 0 /** This transaction creates a Loan */ #if TRANSACTION_INCLUDE # include @@ -750,6 +749,7 @@ TRANSACTION(ttLOAN_SET, 78, LoanSet, noPriv, ({ {sfGracePeriod, soeOPTIONAL}, })) +#if 0 /** This transaction deletes an existing Loan */ #if TRANSACTION_INCLUDE # include diff --git a/src/libxrpl/protocol/InnerObjectFormats.cpp b/src/libxrpl/protocol/InnerObjectFormats.cpp index 02b7591c32..4119643546 100644 --- a/src/libxrpl/protocol/InnerObjectFormats.cpp +++ b/src/libxrpl/protocol/InnerObjectFormats.cpp @@ -28,12 +28,6 @@ InnerObjectFormats::InnerObjectFormats() // inner objects with the default fields have to be // constructed with STObject::makeInnerObject() - std::initializer_list const signingFields = { - {sfAccount, soeREQUIRED}, - {sfSigningPubKey, soeREQUIRED}, - {sfTxnSignature, soeREQUIRED}, - }; - add(sfSignerEntry.jsonName, sfSignerEntry.getCode(), { @@ -42,7 +36,13 @@ InnerObjectFormats::InnerObjectFormats() {sfWalletLocator, soeOPTIONAL}, }); - add(sfSigner.jsonName, sfSigner.getCode(), signingFields); + add(sfSigner.jsonName, + sfSigner.getCode(), + { + {sfAccount, soeREQUIRED}, + {sfSigningPubKey, soeREQUIRED}, + {sfTxnSignature, soeREQUIRED}, + }); add(sfMajority.jsonName, sfMajority.getCode(), @@ -157,7 +157,11 @@ InnerObjectFormats::InnerObjectFormats() add(sfCounterpartySignature.jsonName, sfCounterpartySignature.getCode(), - signingFields); + { + {sfSigningPubKey, soeREQUIRED}, + {sfTxnSignature, soeOPTIONAL}, + {sfSigners, soeOPTIONAL}, + }); } InnerObjectFormats const& diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index f566cece90..a680037880 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -416,12 +416,12 @@ class LoanBroker_test : public beast::unit_test::suite ter(temINVALID)); // sfManagementFeeRate: good value, bad account env(set(evan, vault.vaultID), - managementFeeRate(maxFeeRate), + managementFeeRate(maxManagementFeeRate), fee(increment), ter(tecNO_PERMISSION)); // sfManagementFeeRate: too big env(set(evan, vault.vaultID), - managementFeeRate(maxFeeRate + 1), + managementFeeRate(maxManagementFeeRate + 1), fee(increment), ter(temINVALID)); // sfCoverRateMinimum: good value, bad account @@ -504,17 +504,17 @@ class LoanBroker_test : public beast::unit_test::suite // ManagementFeeRate env(set(alice, vault.vaultID), loanBrokerID(broker->key()), - managementFeeRate(maxFeeRate), + managementFeeRate(maxManagementFeeRate), ter(temINVALID)); // CoverRateMinimum env(set(alice, vault.vaultID), loanBrokerID(broker->key()), - coverRateMinimum(maxFeeRate), + coverRateMinimum(maxManagementFeeRate), ter(temINVALID)); // CoverRateLiquidation env(set(alice, vault.vaultID), loanBrokerID(broker->key()), - coverRateLiquidation(maxFeeRate), + coverRateLiquidation(maxManagementFeeRate), ter(temINVALID)); // fields that can be changed diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp index 03687caaa1..4b3102eef0 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp @@ -109,9 +109,9 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) } auto const coverAvail = sleBroker->at(sfCoverAvailable); - // Cover Rate is in 1/10 bps units - auto const minimumCover = sleBroker->at(sfDebtTotal) * - sleBroker->at(sfCoverRateMinimum) / bpsPerOne / 10; + // Cover Rate is in 1/10 bips units + auto const minimumCover = tenthBipsOfValue( + sleBroker->at(sfDebtTotal), sleBroker->at(sfCoverRateMinimum)); if (coverAvail < amount) return tecINSUFFICIENT_FUNDS; if ((coverAvail - amount) < minimumCover) diff --git a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp index 97858a4137..857abc601b 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerSet.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerSet.cpp @@ -69,7 +69,7 @@ LoanBrokerSet::doPreflight(PreflightContext const& ctx) if (auto const data = tx[~sfData]; data && !data->empty() && !validDataLength(tx[~sfData], maxDataPayloadLength)) return temINVALID; - if (!validNumericRange(tx[~sfManagementFeeRate], maxFeeRate)) + if (!validNumericRange(tx[~sfManagementFeeRate], maxManagementFeeRate)) return temINVALID; if (!validNumericRange(tx[~sfCoverRateMinimum], maxCoverRate)) return temINVALID; diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp new file mode 100644 index 0000000000..2415172bb7 --- /dev/null +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -0,0 +1,213 @@ +//------------------------------------------------------------------------------ +/* + 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 +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; + if (auto const data = tx[~sfData]; data && !data->empty() && + !validDataLength(tx[~sfData], maxDataPayloadLength)) + 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::defaultPaymentInterval) + return temINVALID; + else if (auto const gracePeriod = + tx[~sfGracePeriod].value_or(LoanSet::defaultGracePeriod); + gracePeriod > paymentInterval) + return temINVALID; + + return tesSUCCESS; +} + +TER +LoanSet::preclaim(PreclaimContext const& ctx) +{ + return temDISABLED; + + auto const& tx = ctx.tx; + + auto const account = tx[sfAccount]; + if (auto const ID = tx[~sfLoanID]) + { + auto const sle = ctx.view.read(keylet::loan(*ID)); + if (!sle) + { + JLOG(ctx.j.warn()) << "Loan does not exist."; + return tecNO_ENTRY; + } + if (tx[sfVaultID] != sle->at(sfVaultID)) + { + JLOG(ctx.j.warn()) << "Can not change VaultID on an existing Loan."; + return tecNO_PERMISSION; + } + if (account != sle->at(sfOwner)) + { + JLOG(ctx.j.warn()) << "Account is not the owner of the Loan."; + 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; + } + } + return tesSUCCESS; +} + +TER +LoanSet::doApply() +{ + return temDISABLED; + + auto const& tx = ctx_.tx; + auto& view = ctx_.view(); + +#if 0 + if (auto const ID = tx[~sfLoanID]) + { + // Modify an existing Loan + auto loan = view.peek(keylet::loan(*ID)); + + if (auto const data = tx[~sfData]) + loan->at(sfData) = *data; + if (auto const debtMax = tx[~sfDebtMaximum]) + loan->at(sfDebtMaximum) = *debtMax; + + view.update(); + } + else + { + // Create a new Loan 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_)); + auto loan = std::make_shared(keylet::loan(account_, sequence)); + + if (auto const ter = dirLink(view, account_, )) + return ter; + if (auto const ter = dirLink(view, vaultPseudoID, , sfVaultNode)) + return ter; + + adjustOwnerCount(view, owner, 1, j_); + auto ownerCount = owner->at(sfOwnerCount); + if (mPriorBalance < view.fees().accountReserve(ownerCount)) + return tecINSUFFICIENT_RESERVE; + + auto maybePseudo = + createPseudoAccount(view, loan->key(), PseudoAccountOwnerType::Loan); + 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: + loan->at(sfSequence) = sequence; + loan->at(sfVaultID) = vaultID; + loan->at(sfOwner) = account_; + loan->at(sfAccount) = pseudoId; + if (auto const data = tx[~sfData]) + loan->at(sfData) = *data; + if (auto const rate = tx[~sfManagementFeeRate]) + loan->at(sfManagementFeeRate) = *rate; + if (auto const debtMax = tx[~sfDebtMaximum]) + loan->at(sfDebtMaximum) = *debtMax; + if (auto const coverMin = tx[~sfCoverRateMinimum]) + loan->at(sfCoverRateMinimum) = *coverMin; + if (auto const coverLiq = tx[~sfCoverRateLiquidation]) + loan->at(sfCoverRateLiquidation) = *coverLiq; + + view.insert(loan); + } +#endif + + 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..09f677805e --- /dev/null +++ b/src/xrpld/app/tx/detail/LoanSet.h @@ -0,0 +1,61 @@ +//------------------------------------------------------------------------------ +/* + 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 TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; + +private: + static std::uint32_t constexpr defaultPaymentTotal = 1; + static std::uint32_t constexpr defaultPaymentInterval = 60; + static std::uint32_t constexpr defaultGracePeriod = 60; +}; + +//------------------------------------------------------------------------------ + +} // namespace ripple + +#endif