[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.
This commit is contained in:
Ed Hennis
2025-04-18 17:45:30 -04:00
parent a01ce0ad00
commit 5bed1d94e6
10 changed files with 386 additions and 48 deletions

View File

@@ -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%.
/** 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::uint16_t constexpr maxFeeRate = 10'000;
std::uint32_t constexpr bipsPerUnity = 100 * 100;
std::uint32_t constexpr tenthBipsPerUnity = bipsPerUnity * 10;
/** The maximum coverage rate allowed in lending.
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 <typename T>
constexpr T
bipsOfValue(T value, std::uint32_t bips)
{
return value * bips / bipsPerUnity;
}
template <typename T>
constexpr T
tenthBipsOfValue(T value, std::uint32_t bips)
{
return value * bips / tenthBipsPerUnity;
}
TODO: Is this a good name?
/** The maximum management fee rate allowed by a loan broker in 1/10 bips.
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%.
Valid values are between 0 and 10% inclusive.
*/
std::uint32_t constexpr maxCoverRate = 100'000;
std::uint16_t constexpr maxManagementFeeRate = percentageToTenthBips(10);
static_assert(maxManagementFeeRate == 10'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
/** 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 bpsPerOne = 10'000;
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;

View File

@@ -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

View File

@@ -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

View File

@@ -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 <xrpld/app/tx/detail/LoanSet.h>
@@ -750,6 +749,7 @@ TRANSACTION(ttLOAN_SET, 78, LoanSet, noPriv, ({
{sfGracePeriod, soeOPTIONAL},
}))
#if 0
/** This transaction deletes an existing Loan */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/LoanDelete.h>

View File

@@ -28,12 +28,6 @@ InnerObjectFormats::InnerObjectFormats()
// inner objects with the default fields have to be
// constructed with STObject::makeInnerObject()
std::initializer_list<SOElement> 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&

View File

@@ -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

View File

@@ -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)

View File

@@ -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;

View File

@@ -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 <xrpld/app/tx/detail/LoanSet.h>
//
#include <xrpld/app/tx/detail/LoanBrokerSet.h>
#include <xrpld/app/tx/detail/SignerEntries.h>
#include <xrpld/app/tx/detail/VaultCreate.h>
#include <xrpld/ledger/ApplyView.h>
#include <xrpld/ledger/View.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STXChainBridge.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
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<SLE>(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

View File

@@ -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 <xrpld/app/tx/detail/Transactor.h>
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