Add LoanBrokerCoverWithdraw transaction

- Untested
This commit is contained in:
Ed Hennis
2025-04-15 18:29:51 -04:00
parent ffbc068dc8
commit 1bb1992cf0
7 changed files with 242 additions and 9 deletions

View File

@@ -102,6 +102,11 @@ std::uint16_t constexpr maxFeeRate = 10'000;
*/ */
std::uint32_t constexpr maxCoverRate = 100'000; 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
*/
std::uint32_t constexpr bpsPerOne = 10'000;
/** The maximum length of a URI inside an NFT */ /** The maximum length of a URI inside an NFT */
std::size_t constexpr maxTokenURILength = 256; std::size_t constexpr maxTokenURILength = 256;

View File

@@ -22,12 +22,12 @@
#endif #endif
/** /**
* TRANSACTION(tag, value, name, fields) * TRANSACTION(tag, value, name, privileges, fields)
* *
* You must define a transactor class in the `ripple` namespace named `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 in `src/xrpld/app/tx/detail/applySteps.cpp`.
* *
* The fourth parameter of the TRANSACTION macro is a bitfield * The `privileges` parameter of the TRANSACTION macro is a bitfield
* defining which operations the transaction can perform. * defining which operations the transaction can perform.
* *
* The values are only used in InvariantCheck.cpp * The values are only used in InvariantCheck.cpp
@@ -557,16 +557,16 @@ TRANSACTION(ttLOAN_BROKER_DELETE, 75, LoanBrokerDelete,
/** This transaction deposits First Loss Capital into a Loan Broker */ /** This transaction deposits First Loss Capital into a Loan Broker */
TRANSACTION(ttLOAN_BROKER_COVER_DEPOSIT, 76, LoanBrokerCoverDeposit, noPriv, ({ TRANSACTION(ttLOAN_BROKER_COVER_DEPOSIT, 76, LoanBrokerCoverDeposit, noPriv, ({
{sfLoanBrokerID, soeREQUIRED}, {sfLoanBrokerID, soeREQUIRED},
{sfAmount, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported},
})) }))
#if 0
/** This transaction withdraws First Loss Capital from a Loan Broker */ /** This transaction withdraws First Loss Capital from a Loan Broker */
TRANSACTION(ttLOAN_BROKER_COVER_WITHDRAW, 77, LoanBrokerCoverWithdraw, noPriv, ({ TRANSACTION(ttLOAN_BROKER_COVER_WITHDRAW, 77, LoanBrokerCoverWithdraw, noPriv, ({
{sfLoanBrokerID, soeREQUIRED}, {sfLoanBrokerID, soeREQUIRED},
{sfAmount, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported},
})) }))
#if 0
/** This transaction creates a Loan */ /** This transaction creates a Loan */
TRANSACTION(ttLOAN_SET, 78, LoanSet, noPriv, ({ TRANSACTION(ttLOAN_SET, 78, LoanSet, noPriv, ({
{sfLoanBrokerID, soeREQUIRED}, {sfLoanBrokerID, soeREQUIRED},
@@ -600,13 +600,13 @@ TRANSACTION(ttLOAN_MANAGE, 80, LoanManage, noPriv, ({
/** The Borrower uses this transaction to draws funds from the Loan. */ /** The Borrower uses this transaction to draws funds from the Loan. */
TRANSACTION(ttLOAN_DRAW, 81, LoanDraw, noPriv, ({ TRANSACTION(ttLOAN_DRAW, 81, LoanDraw, noPriv, ({
{sfLoanID, soeREQUIRED}, {sfLoanID, soeREQUIRED},
{sfAmount, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported},
})) }))
/** The Borrower uses this transaction to make a Payment on the Loan. */ /** The Borrower uses this transaction to make a Payment on the Loan. */
TRANSACTION(ttLOAN_PAY, 82, LoanPay, noPriv, ({ TRANSACTION(ttLOAN_PAY, 82, LoanPay, noPriv, ({
{sfLoanID, soeREQUIRED}, {sfLoanID, soeREQUIRED},
{sfAmount, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported},
})) }))
#endif #endif

View File

@@ -91,9 +91,18 @@ LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx)
if (amount.asset() != vaultAsset) if (amount.asset() != vaultAsset)
return tecWRONG_ASSET; return tecWRONG_ASSET;
auto const pseudoAccountID = sleBroker->at(sfAccount);
// Cannot transfer a frozen Asset // Cannot transfer a frozen Asset
if (isFrozen(ctx.view, account, vaultAsset)) if (isFrozen(ctx.view, account, vaultAsset))
return vaultAsset.holds<Issue>() ? tecFROZEN : tecLOCKED; return vaultAsset.holds<Issue>() ? tecFROZEN : tecLOCKED;
if (vaultAsset.holds<Issue>())
{
auto const issue = vaultAsset.get<Issue>();
if (isDeepFrozen(
ctx.view, pseudoAccountID, issue.currency, issue.account))
return tecFROZEN;
}
if (accountHolds( if (accountHolds(
ctx.view, ctx.view,

View File

@@ -0,0 +1,162 @@
//------------------------------------------------------------------------------
/*
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/LoanBrokerCoverWithdraw.h>
#include <xrpld/app/tx/detail/LoanBrokerSet.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/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
namespace ripple {
bool
LoanBrokerCoverWithdraw::isEnabled(PreflightContext const& ctx)
{
return lendingProtocolEnabled(ctx);
}
std::uint32_t
LoanBrokerCoverWithdraw::getFlagsMask(PreflightContext const& ctx)
{
return tfUniversalMask;
}
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<Issue>() ? tecFROZEN : tecLOCKED;
*/
if (isFrozen(ctx.view, pseudoAccountID, vaultAsset))
return vaultAsset.holds<Issue>() ? tecFROZEN : tecLOCKED;
if (vaultAsset.holds<Issue>())
{
auto const issue = vaultAsset.get<Issue>();
if (isDeepFrozen(ctx.view, account, issue.currency, issue.account))
return tecFROZEN;
}
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;
if (coverAvail < amount)
return tecINSUFFICIENT_FUNDS;
if ((coverAvail - amount) < minimumCover)
return tecINSUFFICIENT_FUNDS;
if (accountHolds(
ctx.view,
account,
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));
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;
return tesSUCCESS;
}
//------------------------------------------------------------------------------
} // namespace ripple

View File

@@ -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_LOANBROKERCOVERWITHDRAW_H_INCLUDED
#define RIPPLE_TX_LOANBROKERCOVERWITHDRAW_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
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 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

View File

@@ -42,6 +42,7 @@
#include <xrpld/app/tx/detail/Escrow.h> #include <xrpld/app/tx/detail/Escrow.h>
#include <xrpld/app/tx/detail/LedgerStateFix.h> #include <xrpld/app/tx/detail/LedgerStateFix.h>
#include <xrpld/app/tx/detail/LoanBrokerCoverDeposit.h> #include <xrpld/app/tx/detail/LoanBrokerCoverDeposit.h>
#include <xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h>
#include <xrpld/app/tx/detail/LoanBrokerDelete.h> #include <xrpld/app/tx/detail/LoanBrokerDelete.h>
#include <xrpld/app/tx/detail/LoanBrokerSet.h> #include <xrpld/app/tx/detail/LoanBrokerSet.h>
#include <xrpld/app/tx/detail/MPTokenAuthorize.h> #include <xrpld/app/tx/detail/MPTokenAuthorize.h>

View File

@@ -618,8 +618,8 @@ xrpLiquid(
std::uint32_t const ownerCount = confineOwnerCount( std::uint32_t const ownerCount = confineOwnerCount(
view.ownerCountHook(id, sle->getFieldU32(sfOwnerCount)), ownerCountAdj); view.ownerCountHook(id, sle->getFieldU32(sfOwnerCount)), ownerCountAdj);
// AMMs have no reserve requirement // Pseudo-accounts have no reserve requirement
auto const reserve = sle->isFieldPresent(sfAMMID) auto const reserve = isPseudoAccount(sle)
? XRPAmount{0} ? XRPAmount{0}
: view.fees().accountReserve(ownerCount); : view.fees().accountReserve(ownerCount);