diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 29f44c5f45..562d058413 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -82,6 +82,26 @@ 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::uint16_t constexpr maxCoverRate = 100'000; + /** 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 ba3cf2e850..7e6a6a152a 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -237,6 +237,13 @@ constexpr std::uint32_t const tfVaultCreateMask = ~(tfUniversal | tfVaultPrivate // True, indicates the load 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 tfLoanManageMask = ~(tfUniversal | tfLoanDefault | tfLoanImpair | tfLoanUnimpair); + // clang-format on } // namespace ripple diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 6ca6f5d446..df380668ac 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -501,12 +501,12 @@ LEDGER_ENTRY(ltLOAN_BROKER, 0x0084, LoanBroker, loan_broker, ({ {sfOwner, soeREQUIRED}, {sfData, soeDEFAULT}, {sfManagementFeeRate, soeDEFAULT}, - {sfOwnerCount, soeREQUIRED}, - {sfDebtTotal, soeREQUIRED}, - {sfDebtMaximum, soeREQUIRED}, - {sfCoverAvailable, soeREQUIRED}, - {sfCoverRateMinimum, soeREQUIRED}, - {sfCoverRateLiquidation, soeREQUIRED}, + {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 diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index db63b39404..f6555cba58 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -211,6 +211,7 @@ TYPED_SFIELD(sfHookSetTxnID, UINT256, 33) TYPED_SFIELD(sfDomainID, UINT256, 34) TYPED_SFIELD(sfVaultID, UINT256, 35) TYPED_SFIELD(sfLoanBrokerID, UINT256, 36) +TYPED_SFIELD(sfLoanID, UINT256, 37) // number (common) TYPED_SFIELD(sfNumber, NUMBER, 1) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index c25eff0de3..e9381640dc 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -508,7 +508,6 @@ TRANSACTION(ttVAULT_CLAWBACK, 69, VaultClawback, ({ {sfAmount, soeOPTIONAL, soeMPTSupported}, })) -#if 0 /** This transaction creates and updates a Loan Broker */ TRANSACTION(ttLOAN_BROKER_SET, 70, LoanBrokerSet, ({ {sfVaultID, soeREQUIRED}, @@ -520,6 +519,7 @@ TRANSACTION(ttLOAN_BROKER_SET, 70, LoanBrokerSet, ({ {sfCoverRateLiquidation, soeDEFAULT}, })) +#if 0 /** This transaction deletes a Loan Broker */ TRANSACTION(ttLOAN_BROKER_DELETE, 71, LoanBrokerDelete, ({ {sfLoanBrokerID, soeREQUIRED}, @@ -556,6 +556,28 @@ TRANSACTION(ttLOAN_SET, 74, LoanSet, ({ {sfPaymentInterval, soeOPTIONAL}, {sfGracePeriod, soeOPTIONAL}, })) + +/** This transaction deletes an existing Loan */ +TRANSACTION(ttLOAN_DELETE, 75, LoanDelete, ({ + {sfLoanID, soeREQUIRED}, +})) + +/** This transaction is used to change the delinquency status of an existing Loan */ +TRANSACTION(ttLOAN_MANAGE, 76, LoanManage, ({ + {sfLoanID, soeREQUIRED}, +})) + +/** The Borrower uses this transaction to draws funds from the Loan. */ +TRANSACTION(ttLOAN_DRAW, 77, LoanDraw, ({ + {sfLoanID, soeREQUIRED}, + {sfAmount, soeREQUIRED}, +})) + +/** The Borrower uses this transaction to make a Payment on the Loan. */ +TRANSACTION(ttLOAN_PAY, 77, LoanPay, ({ + {sfLoanID, soeREQUIRED}, + {sfAmount, soeREQUIRED}, +})) #endif /** This system-generated transaction type is used to update the status of the various amendments. diff --git a/src/xrpld/app/tx/detail/LoanBroker.cpp b/src/xrpld/app/tx/detail/LoanBroker.cpp new file mode 100644 index 0000000000..19aeb8bb28 --- /dev/null +++ b/src/xrpld/app/tx/detail/LoanBroker.cpp @@ -0,0 +1,205 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 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 +lendingProtocolEnabled(PreflightContext const& ctx) +{ + return ctx.rules.enabled(featureLendingProtocol) && + VaultCreate::isEnabled(ctx); +} + +bool +LoanBrokerSet::isEnabled(PreflightContext const& ctx) +{ + return lendingProtocolEnabled(ctx); +} + +std::uint32_t +LoanBrokerSet::getFlagsMask(PreflightContext const& ctx) +{ + return tfUniversalMask; +} + +NotTEC +LoanBrokerSet::doPreflight(PreflightContext const& ctx) +{ + auto const& tx = ctx.tx; + if (!validDataLength(tx[~sfData], maxDataPayloadLength)) + return temINVALID; + if (!validNumericRange(tx[~sfManagementFeeRate], 0, maxFeeRate)) + return temINVALID; + if (!validNumericRange(tx[~sfCoverRateMinimum], 0, maxCoverRate)) + return temINVALID; + if (!validNumericRange(tx[~sfCoverRateLiquidation], 0, maxCoverRate)) + 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; + } + } + 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 const sleBroker = view.read(keylet::loanbroker(*brokerID)); + + assert(0); + } + 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_)); + 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)) + 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, broker->key(), PseudoAccountOwnerType::LoanBroker); + 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; + 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]; debtMax && *debtMax) + 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 temDISABLED; +} + +//------------------------------------------------------------------------------ + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/LoanBroker.h b/src/xrpld/app/tx/detail/LoanBroker.h new file mode 100644 index 0000000000..b8f08ce684 --- /dev/null +++ b/src/xrpld/app/tx/detail/LoanBroker.h @@ -0,0 +1,60 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 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_LOANBROKER_H_INCLUDED +#define RIPPLE_TX_LOANBROKER_H_INCLUDED + +#include + +namespace ripple { + +// Lending protocol has dependencies, so capture them here. +bool +lendingProtocolEnabled(PreflightContext const& ctx); + +class LoanBrokerSet : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit LoanBrokerSet(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/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index e52cbca69a..bce695c990 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -205,6 +205,16 @@ 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; +} + XRPAmount Transactor::calculateBaseFee(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 5a8e50fa1e..ecabf9fdfe 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -146,10 +146,6 @@ public: static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); - // Base class always returns true - static bool - isEnabled(PreflightContext const& ctx); - /* Do NOT define a preflight function in a derived class. Instead, define @@ -212,6 +208,17 @@ protected: Fees const& fees, ApplyFlags flags); + // Base class always returns true + static bool + isEnabled(PreflightContext const& ctx); + + static bool + validDataLength(std::optional const& slice, std::size_t maxLength); + + template + static bool + validNumericRange(std::optional value, T min, T max); + private: std::pair reset(XRPAmount fee); @@ -272,6 +279,15 @@ Transactor::preflight(PreflightContext const& ctx) return detail::preflight2(ctx); } +template +bool +Transactor::validNumericRange(std::optional value, T min, T max) +{ + if (!value) + return true; + return value >= min && value <= max; +} + } // namespace ripple #endif diff --git a/src/xrpld/app/tx/detail/VaultCreate.cpp b/src/xrpld/app/tx/detail/VaultCreate.cpp index 93166e19c0..05bab34d5c 100644 --- a/src/xrpld/app/tx/detail/VaultCreate.cpp +++ b/src/xrpld/app/tx/detail/VaultCreate.cpp @@ -53,11 +53,8 @@ VaultCreate::getFlagsMask(PreflightContext const& ctx) NotTEC VaultCreate::doPreflight(PreflightContext const& ctx) { - if (auto const data = ctx.tx[~sfData]) - { - if (data->empty() || data->length() > maxDataPayloadLength) - return temMALFORMED; - } + if (!validDataLength(ctx.tx[~sfData], maxDataPayloadLength)) + return temMALFORMED; if (auto const withdrawalPolicy = ctx.tx[~sfWithdrawalPolicy]) { diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index 0e5ed07bc2..d222220d9b 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #include