mirror of
https://github.com/XRPLF/rippled.git
synced 2026-04-29 15:37:57 +00:00
Implement LoanBrokerCoverClawback and many test cases
- Not all tests are passing yet
This commit is contained in:
@@ -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},
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
343
src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp
Normal file
343
src/xrpld/app/tx/detail/LoanBrokerCoverClawback.cpp
Normal 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
|
||||
53
src/xrpld/app/tx/detail/LoanBrokerCoverClawback.h
Normal file
53
src/xrpld/app/tx/detail/LoanBrokerCoverClawback.h
Normal 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
|
||||
Reference in New Issue
Block a user