Implement LoanBrokerCoverClawback and many test cases

- Not all tests are passing yet
This commit is contained in:
Ed Hennis
2025-07-17 20:01:20 -04:00
parent e4480569f7
commit c46d894192
8 changed files with 648 additions and 36 deletions

View File

@@ -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 <xrpld/app/tx/detail/LoanBrokerCoverClawback.h>
#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 <xrpld/app/tx/detail/LoanSet.h>
#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 <xrpld/app/tx/detail/LoanDelete.h>
#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 <xrpld/app/tx/detail/LoanManage.h>
#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 <xrpld/app/tx/detail/LoanDraw.h>
#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 <xrpld/app/tx/detail/LoanPay.h>
#endif
TRANSACTION(ttLOAN_PAY, 82, LoanPay,
TRANSACTION(ttLOAN_PAY, 84, LoanPay,
Delegation::delegatable,
noPriv, ({
{sfLoanID, soeREQUIRED},

View File

@@ -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<jtx::JTx(jtx::JTx const&)> modifyJTx,
std::function<void(SLE::const_ref)> checkBroker,
std::function<void(SLE::const_ref)> changeBroker,
std::function<void(SLE::const_ref)> 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<Issue>())
{
// 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<MPTIssue>());
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.
}
};

View File

@@ -177,6 +177,29 @@ public:
}
};
struct stAmountField : public JTxField<SF_AMOUNT, STAmount, Json::Value>
{
using SF = SF_AMOUNT;
using SV = STAmount;
using OV = Json::Value;
using base = JTxField<SF, SV, OV>;
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<SF_VL, std::string>
{
using SF = SF_VL;
@@ -292,6 +315,8 @@ using simpleField = JTxFieldWrapper<JTxField<SField, StoredValue>>;
*/
auto const data = JTxFieldWrapper<blobField>(sfData);
auto const amount = JTxFieldWrapper<stAmountField>(sfAmount);
// TODO We only need this long "requires" clause as polyfill, for C++20
// implementations which are missing <ranges> header. Replace with
// `std::ranges::range<Input>`, 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<uint256Field>(sfLoanBrokerID);
auto const managementFeeRate =

View File

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

View File

@@ -413,7 +413,7 @@ MPTTester::getFlags(std::optional<Account> const& holder) const
}
MPT
MPTTester::operator[](std::string const& name)
MPTTester::operator[](std::string const& name) const
{
return MPT(name, issuanceID());
}

View File

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

View File

@@ -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 <xrpld/app/tx/detail/LoanBrokerCoverClawback.h>
//
#include <xrpld/app/misc/LendingHelpers.h>
#include <xrpld/app/tx/detail/Payment.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/Protocol.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
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<MPTIssue>())
return temINVALID;
auto const account = ctx.tx[sfAccount];
auto const holder = amount->getIssuer();
if (holder == account || holder == beast::zero)
return temINVALID;
}
}
return tesSUCCESS;
}
Expected<uint256, TER>
determineBrokerID(ReadView const& view, STTx const& tx)
{
if (auto const brokerID = tx[~sfLoanBrokerID])
return *brokerID;
auto const dstAmount = tx[~sfAmount];
if (!dstAmount || !dstAmount->holds<Issue>())
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<Asset, TER>
determineAsset(
ReadView const& view,
AccountID const& account,
AccountID const& brokerPseudoAccountID,
STAmount const& amount)
{
if (amount.holds<MPTIssue>())
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<STAmount, TER>
determineClawAmount(
SLE const& sleBroker,
Asset const& vaultAsset,
std::optional<STAmount> 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 <ValidIssueType T>
static TER
preclaimHelper(
PreclaimContext const& ctx,
SLE const& sleIssuer,
STAmount const& clawAmount);
template <>
TER
preclaimHelper<Issue>(
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<MPTIssue>(
PreclaimContext const& ctx,
SLE const& sleIssuer,
STAmount const& clawAmount)
{
auto const issuanceKey =
keylet::mptIssuance(clawAmount.get<MPTIssue>().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(
[&]<typename T>(T const&) {
return preclaimHelper<T>(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

View File

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