Files
rippled/src/test/app/LoanBroker_test.cpp
2025-05-07 12:11:53 -04:00

523 lines
21 KiB
C++

//------------------------------------------------------------------------------
/*
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 <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/mpt.h>
#include <test/jtx/vault.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/json/json_forwards.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
namespace ripple {
namespace test {
class LoanBroker_test : public beast::unit_test::suite
{
// Ensure that all the features needed for Lending Protocol are included,
// even if they are set to unsupported.
FeatureBitset const all{
jtx::supported_amendments() | featureMPTokensV1 |
featureSingleAssetVault | featureLendingProtocol};
void
testDisabled()
{
testcase("Disabled");
// Lending Protocol depends on Single Asset Vault (SAV). Test
// combinations of the two amendments.
// Single Asset Vault depends on MPTokensV1, but don't test every combo
// of that.
using namespace jtx;
auto failAll = [this](FeatureBitset features, bool goodVault = false) {
Env env(*this, features);
Account const alice{"alice"};
env.fund(XRP(10000), alice);
// Try to create a vault
PrettyAsset const asset{xrpIssue(), 1'000'000};
Vault vault{env};
auto const [tx, keylet] =
vault.create({.owner = alice, .asset = asset});
env(tx, ter(goodVault ? ter(tesSUCCESS) : ter(temDISABLED)));
env.close();
BEAST_EXPECT(static_cast<bool>(env.le(keylet)) == goodVault);
using namespace loanBroker;
// Can't create a loan broker regardless of whether the vault exists
env(set(alice, keylet.key), fee(increment), ter(temDISABLED));
auto const brokerKeylet =
keylet::loanbroker(alice.id(), env.seq(alice));
// LoanBrokerDelete is disabled, too.
env(del(alice, brokerKeylet.key), ter(temDISABLED));
};
failAll(all - featureMPTokensV1);
failAll(all - featureSingleAssetVault - featureLendingProtocol);
failAll(all - featureSingleAssetVault);
failAll(all - featureLendingProtocol, true);
}
struct VaultInfo
{
jtx::PrettyAsset asset;
uint256 vaultID;
VaultInfo(jtx::PrettyAsset const& asset_, uint256 const& vaultID_)
: asset(asset_), vaultID(vaultID_)
{
}
};
void
lifecycle(
const char* label,
jtx::Env& env,
jtx::Account const& alice,
jtx::Account const& evan,
VaultInfo const& vault,
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: "
<< (asset.native() ? "XRP "
: asset.holds<Issue>() ? "IOU "
: asset.holds<MPTIssue>() ? "MPT "
: "Unknown ")
<< label;
}
using namespace jtx;
using namespace loanBroker;
{
// Start with default values
auto jtx = env.jt(set(alice, vault.vaultID), fee(increment));
// Modify as desired
if (modifyJTx)
jtx = modifyJTx(jtx);
// Successfully create a Loan Broker
env(jtx);
}
env.close();
if (auto broker = env.le(keylet); BEAST_EXPECT(broker))
{
// log << "Broker after create: " << to_string(broker->getJson())
// << std::endl;
BEAST_EXPECT(broker->at(sfVaultID) == vault.vaultID);
BEAST_EXPECT(broker->at(sfAccount) != alice.id());
BEAST_EXPECT(broker->at(sfOwner) == alice.id());
BEAST_EXPECT(broker->at(sfFlags) == 0);
BEAST_EXPECT(broker->at(sfSequence) == env.seq(alice) - 1);
BEAST_EXPECT(broker->at(sfOwnerCount) == 0);
BEAST_EXPECT(broker->at(sfDebtTotal) == 0);
BEAST_EXPECT(broker->at(sfCoverAvailable) == 0);
if (checkBroker)
checkBroker(broker);
// if (auto const vaultSLE = env.le(keylet::vault(vault.vaultID)))
//{
// log << "Vault: " << to_string(vaultSLE->getJson()) <<
// std::endl;
// }
// Load the pseudo-account
auto const pseudoKeylet = keylet::account(broker->at(sfAccount));
if (auto const pseudo = env.le(pseudoKeylet); BEAST_EXPECT(pseudo))
{
// log << "Pseudo-account after create: "
// << to_string(pseudo->getJson()) << std::endl
// << std::endl;
BEAST_EXPECT(
pseudo->at(sfFlags) ==
(lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth));
BEAST_EXPECT(pseudo->at(sfSequence) == 0);
BEAST_EXPECT(pseudo->at(sfBalance) == beast::zero);
BEAST_EXPECT(
pseudo->at(sfOwnerCount) ==
(vault.asset.raw().native() ? 0 : 1));
BEAST_EXPECT(!pseudo->isFieldPresent(sfAccountTxnID));
BEAST_EXPECT(!pseudo->isFieldPresent(sfRegularKey));
BEAST_EXPECT(!pseudo->isFieldPresent(sfEmailHash));
BEAST_EXPECT(!pseudo->isFieldPresent(sfWalletLocator));
BEAST_EXPECT(!pseudo->isFieldPresent(sfWalletSize));
BEAST_EXPECT(!pseudo->isFieldPresent(sfMessageKey));
BEAST_EXPECT(!pseudo->isFieldPresent(sfTransferRate));
BEAST_EXPECT(!pseudo->isFieldPresent(sfDomain));
BEAST_EXPECT(!pseudo->isFieldPresent(sfTickSize));
BEAST_EXPECT(!pseudo->isFieldPresent(sfTicketCount));
BEAST_EXPECT(!pseudo->isFieldPresent(sfNFTokenMinter));
BEAST_EXPECT(!pseudo->isFieldPresent(sfMintedNFTokens));
BEAST_EXPECT(!pseudo->isFieldPresent(sfBurnedNFTokens));
BEAST_EXPECT(!pseudo->isFieldPresent(sfFirstNFTokenSequence));
BEAST_EXPECT(!pseudo->isFieldPresent(sfAMMID));
BEAST_EXPECT(!pseudo->isFieldPresent(sfVaultID));
BEAST_EXPECT(pseudo->at(sfLoanBrokerID) == keylet.key);
}
// Test Cover funding before allowing alterations
env(coverDeposit(alice, uint256(0), vault.asset(10)),
ter(temINVALID));
env(coverDeposit(alice, uint256(0), vault.asset(-10)),
ter(temBAD_AMOUNT));
env(coverDeposit(evan, keylet.key, vault.asset(10)),
ter(tecNO_PERMISSION));
env(coverDeposit(alice, vault.vaultID, vault.asset(10)),
ter(tecNO_ENTRY));
// no-op
env(set(alice, vault.vaultID), loanBrokerID(keylet.key));
// Make modifications to the broker
if (changeBroker)
changeBroker(broker);
env.close();
broker = env.le(keylet);
// Check the results of modifications
if (checkChangedBroker)
checkChangedBroker(broker);
// Verify that fields get removed when set to default values
// Debt maximum: explicit 0
// Data: explicit empty
env(set(alice, vault.vaultID),
loanBrokerID(broker->key()),
debtMaximum(Number(0)),
data(""));
// Check the updated fields
broker = env.le(keylet);
BEAST_EXPECT(!broker->isFieldPresent(sfDebtMaximum));
BEAST_EXPECT(!broker->isFieldPresent(sfData));
/////////////////////////////////////
// try to delete the wrong broker object
env(del(alice, vault.vaultID), ter(tecNO_ENTRY));
// evan tries to delete the broker
env(del(evan, keylet.key), ter(tecNO_PERMISSION));
// TODO: test deletion with an active loan
// delete the broker
// log << "Broker before delete: " << to_string(broker->getJson())
// << std::endl;
// if (auto const pseudo = env.le(pseudoKeylet);
// BEAST_EXPECT(pseudo))
//{
// log << "Pseudo-account before delete: "
// << to_string(pseudo->getJson()) << std::endl
// << std::endl;
//}
env(del(alice, keylet.key));
env.close();
{
broker = env.le(keylet);
BEAST_EXPECT(!broker);
auto pseudo = env.le(pseudoKeylet);
BEAST_EXPECT(!pseudo);
}
}
}
void
testLifecycle()
{
testcase("Lifecycle");
using namespace jtx;
// Create 3 loan brokers: one for XRP, one for an IOU, and one for an
// MPT. That'll require three corresponding SAVs.
Env env(*this, all);
Account issuer{"issuer"};
// For simplicity, alice will be the sole actor for the vault & brokers.
Account alice{"alice"};
// Evan will attempt to be naughty
Account evan{"evan"};
Vault vault{env};
// Fund the accounts and trust lines with the same amount so that tests
// can use the same values regardless of the asset.
env.fund(XRP(100'000), issuer, noripple(alice, evan));
env.close();
// Create assets
PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
PrettyAsset const iouAsset = issuer["IOU"];
env(trust(alice, iouAsset(1'000'000)));
env(trust(evan, iouAsset(1'000'000)));
env(pay(issuer, evan, iouAsset(100'000)));
env(pay(issuer, alice, iouAsset(100'000)));
env.close();
MPTTester mptt{env, issuer, mptInitNoFund};
mptt.create(
{.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
PrettyAsset const mptAsset = mptt.issuanceID();
mptt.authorize({.account = alice});
mptt.authorize({.account = evan});
env(pay(issuer, alice, mptAsset(100'000)));
env(pay(issuer, evan, mptAsset(100'000)));
env.close();
std::array const assets{xrpAsset, iouAsset, mptAsset};
// Create vaults
std::vector<VaultInfo> vaults;
for (auto const& asset : assets)
{
auto [tx, keylet] = vault.create({.owner = alice, .asset = asset});
env(tx);
env.close();
BEAST_EXPECT(env.le(keylet));
vaults.emplace_back(asset, keylet.key);
env(vault.deposit(
{.depositor = alice, .id = keylet.key, .amount = asset(50)}));
env.close();
}
// Create and update Loan Brokers
for (auto const& vault : vaults)
{
using namespace loanBroker;
auto badKeylet = keylet::vault(alice.id(), env.seq(alice));
// Try some failure cases
// insufficient fee
env(set(evan, vault.vaultID), ter(telINSUF_FEE_P));
// not the vault owner
env(set(evan, vault.vaultID),
fee(increment),
ter(tecNO_PERMISSION));
// not a vault
env(set(alice, badKeylet.key), fee(increment), ter(tecNO_ENTRY));
// flags are checked first
env(set(evan, vault.vaultID, ~tfUniversal),
fee(increment),
ter(temINVALID_FLAG));
// field length validation
// sfData: good length, bad account
env(set(evan, vault.vaultID),
fee(increment),
data(std::string(maxDataPayloadLength, 'X')),
ter(tecNO_PERMISSION));
// sfData: too long
env(set(evan, vault.vaultID),
fee(increment),
data(std::string(maxDataPayloadLength + 1, 'Y')),
ter(temINVALID));
// sfManagementFeeRate: good value, bad account
env(set(evan, vault.vaultID),
managementFeeRate(maxFeeRate),
fee(increment),
ter(tecNO_PERMISSION));
// sfManagementFeeRate: too big
env(set(evan, vault.vaultID),
managementFeeRate(maxFeeRate + 1),
fee(increment),
ter(temINVALID));
// sfCoverRateMinimum: good value, bad account
env(set(evan, vault.vaultID),
coverRateMinimum(maxCoverRate),
fee(increment),
ter(tecNO_PERMISSION));
// sfCoverRateMinimum: too big
env(set(evan, vault.vaultID),
coverRateMinimum(maxCoverRate + 1),
fee(increment),
ter(temINVALID));
// sfCoverRateLiquidation: good value, bad account
env(set(evan, vault.vaultID),
coverRateLiquidation(maxCoverRate),
fee(increment),
ter(tecNO_PERMISSION));
// sfCoverRateLiquidation: too big
env(set(evan, vault.vaultID),
coverRateLiquidation(maxCoverRate + 1),
fee(increment),
ter(temINVALID));
// sfDebtMaximum: good value, bad account
env(set(evan, vault.vaultID),
debtMaximum(Number(0)),
fee(increment),
ter(tecNO_PERMISSION));
// sfDebtMaximum: overflow
env(set(evan, vault.vaultID),
debtMaximum(Number(1, 100)),
fee(increment),
ter(temINVALID));
// sfDebtMaximum: negative
env(set(evan, vault.vaultID),
debtMaximum(Number(-1)),
fee(increment),
ter(temINVALID));
std::string testData;
lifecycle(
"default fields",
env,
alice,
evan,
vault,
// No modifications
{},
[&](SLE::const_ref broker) {
// Extra checks
BEAST_EXPECT(!broker->isFieldPresent(sfManagementFeeRate));
BEAST_EXPECT(!broker->isFieldPresent(sfCoverRateMinimum));
BEAST_EXPECT(
!broker->isFieldPresent(sfCoverRateLiquidation));
BEAST_EXPECT(!broker->isFieldPresent(sfData));
BEAST_EXPECT(!broker->isFieldPresent(sfDebtMaximum));
BEAST_EXPECT(broker->at(sfDebtMaximum) == 0);
BEAST_EXPECT(broker->at(sfCoverRateMinimum) == 0);
BEAST_EXPECT(broker->at(sfCoverRateLiquidation) == 0);
},
[&](SLE::const_ref broker) {
// Modifications
// Update the fields
auto const nextKeylet =
keylet::loanbroker(alice.id(), env.seq(alice));
// fields that can't be changed
// LoanBrokerID
env(set(alice, vault.vaultID),
loanBrokerID(nextKeylet.key),
ter(tecNO_ENTRY));
// VaultID
env(set(alice, nextKeylet.key),
loanBrokerID(broker->key()),
ter(tecNO_PERMISSION));
// Owner
env(set(evan, vault.vaultID),
loanBrokerID(broker->key()),
ter(tecNO_PERMISSION));
// ManagementFeeRate
env(set(alice, vault.vaultID),
loanBrokerID(broker->key()),
managementFeeRate(maxFeeRate),
ter(temINVALID));
// CoverRateMinimum
env(set(alice, vault.vaultID),
loanBrokerID(broker->key()),
coverRateMinimum(maxFeeRate),
ter(temINVALID));
// CoverRateLiquidation
env(set(alice, vault.vaultID),
loanBrokerID(broker->key()),
coverRateLiquidation(maxFeeRate),
ter(temINVALID));
// fields that can be changed
testData = "Test Data 1234";
// Bad data: too long
env(set(alice, vault.vaultID),
loanBrokerID(broker->key()),
data(std::string(maxDataPayloadLength + 1, 'W')),
ter(temINVALID));
// Bad debt maximum
env(set(alice, vault.vaultID),
loanBrokerID(broker->key()),
debtMaximum(Number(-175, -1)),
ter(temINVALID));
// Data & Debt maximum
env(set(alice, vault.vaultID),
loanBrokerID(broker->key()),
data(testData),
debtMaximum(Number(175, -1)));
},
[&](SLE::const_ref broker) {
// Check the updated fields
BEAST_EXPECT(checkVL(broker->at(sfData), testData));
BEAST_EXPECT(broker->at(sfDebtMaximum) == Number(175, -1));
});
lifecycle(
"non-default fields",
env,
alice,
evan,
vault,
[&](jtx::JTx const& jv) {
testData = "spam spam spam spam";
// Finally, create another Loan Broker with none of the
// values at default
return env.jt(
jv,
data(testData),
managementFeeRate(123),
debtMaximum(Number(9)),
coverRateMinimum(100),
coverRateLiquidation(200));
},
[&](SLE::const_ref broker) {
// Extra checks
BEAST_EXPECT(broker->at(sfManagementFeeRate) == 123);
BEAST_EXPECT(broker->at(sfCoverRateMinimum) == 100);
BEAST_EXPECT(broker->at(sfCoverRateLiquidation) == 200);
BEAST_EXPECT(broker->at(sfDebtMaximum) == Number(9));
BEAST_EXPECT(checkVL(broker->at(sfData), testData));
},
[&](SLE::const_ref broker) {
// Reset Data & Debt maximum to default values
env(set(alice, vault.vaultID),
loanBrokerID(broker->key()),
data(""),
debtMaximum(Number(0)));
},
[&](SLE::const_ref broker) {
// Check the updated fields
BEAST_EXPECT(!broker->isFieldPresent(sfData));
BEAST_EXPECT(!broker->isFieldPresent(sfDebtMaximum));
});
}
}
public:
void
run() override
{
testDisabled();
testLifecycle();
}
};
BEAST_DEFINE_TESTSUITE(LoanBroker, tx, ripple);
} // namespace test
} // namespace ripple