Files
rippled/src/test/app/LoanBroker_test.cpp
Alex Kremer 57e4cbbcd9 refactor: Add simple clang-tidy readability checks (#6556)
This change enables the following clang-tidy checks:
-  readability-avoid-nested-conditional-operator,
-  readability-avoid-return-with-void-value,
-  readability-braces-around-statements,
-  readability-const-return-type,
-  readability-container-contains,
-  readability-container-size-empty,
-  readability-else-after-return,
-  readability-make-member-function-const,
-  readability-redundant-casting,
-  readability-redundant-inline-specifier,
-  readability-redundant-member-init,
-  readability-redundant-string-init,
-  readability-reference-to-constructed-temporary,
-  readability-static-definition
2026-03-18 16:41:49 +00:00

1803 lines
68 KiB
C++

#include <test/jtx.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/tx/transactors/lending/LoanBrokerCoverDeposit.h>
namespace xrpl {
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::testable_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), ter(temDISABLED));
auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
// Other LoanBroker transactions are disabled, too.
// 1. LoanBrokerCoverDeposit
env(coverDeposit(alice, brokerKeylet.key, asset(1000)), ter(temDISABLED));
// 2. LoanBrokerCoverWithdraw
env(coverWithdraw(alice, brokerKeylet.key, asset(1000)), ter(temDISABLED));
// 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);
failAll(all - featureSingleAssetVault - featureLendingProtocol);
failAll(all - featureSingleAssetVault);
failAll(all - featureLendingProtocol, true);
}
struct VaultInfo
{
jtx::PrettyAsset asset;
uint256 vaultID;
jtx::Account pseudoAccount;
VaultInfo(jtx::PrettyAsset const& asset_, uint256 const& vaultID_, AccountID const& pseudo)
: asset(asset_), vaultID(vaultID_), pseudoAccount("vault", pseudo)
{
}
};
void
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& asset = vault.asset.raw();
std::string_view assetLabel;
if (asset.native())
{
assetLabel = "XRP ";
}
else if (asset.holds<Issue>())
{
assetLabel = "IOU ";
}
else if (asset.holds<MPTIssue>())
{
assetLabel = "MPT ";
}
else
{
assetLabel = "Unknown ";
}
testcase << "Lifecycle: " << assetLabel << label;
}
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});
env.close();
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));
env.close();
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));
// 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(sfLoanSequence) == 1);
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
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))
{
// 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);
}
{
// Get the AccountInfo RPC result for the broker pseudo-account
std::string const pseudoStr = to_string(pseudoAccount.id());
auto const accountInfo = env.rpc("account_info", pseudoStr);
if (BEAST_EXPECT(accountInfo.isObject()))
{
auto const& accountData = accountInfo[jss::result][jss::account_data];
if (BEAST_EXPECT(accountData.isObject()))
{
BEAST_EXPECT(accountData[jss::Account] == pseudoStr);
BEAST_EXPECT(accountData[sfLoanBrokerID] == to_string(keylet.key));
}
auto const& pseudoInfo = accountInfo[jss::result][jss::pseudo_account];
if (BEAST_EXPECT(pseudoInfo.isObject()))
{
BEAST_EXPECT(pseudoInfo[jss::type] == "LoanBroker");
}
}
}
auto verifyCoverAmount =
[&env, &vault, &pseudoAccount, &broker, &keylet, this](auto n) {
using namespace jtx;
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
env(coverDeposit(alice, uint256(0), vault.asset(10)), ter(temINVALID));
env(coverDeposit(evan, keylet.key, vault.asset(10)), ter(tecNO_PERMISSION));
env(coverDeposit(evan, keylet.key, vault.asset(0)), ter(temBAD_AMOUNT));
env(coverDeposit(evan, keylet.key, vault.asset(-10)), ter(temBAD_AMOUNT));
env(coverDeposit(alice, vault.vaultID, vault.asset(10)), ter(tecNO_ENTRY));
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));
// Pseudo-account is not for a broker
env(coverClawback(alice),
amount(vaultPseudoIouAsset(1)),
ter(tecOBJECT_NOT_FOUND));
// If we specify a pseudo-account as the IOU amount, it
// needs to match the loan broker
env(coverClawback(issuer),
loanBrokerID(keylet.key),
amount(badBrokerPseudoIouAsset(10)),
ter(tecWRONG_ASSET));
PrettyAsset const brokerWrongCurrencyAsset = pseudoAccount["WAT"];
env(coverClawback(issuer),
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));
}
env.close();
// Fund the cover deposit
env(coverDeposit(alice, keylet.key, vault.asset(10)));
env.close();
verifyCoverAmount(10);
// Test withdrawal failure cases
env(coverWithdraw(alice, uint256(0), vault.asset(10)), ter(temINVALID));
env(coverWithdraw(evan, keylet.key, vault.asset(10)), ter(tecNO_PERMISSION));
env(coverWithdraw(evan, keylet.key, vault.asset(0)), ter(temBAD_AMOUNT));
env(coverWithdraw(evan, keylet.key, vault.asset(-10)), ter(temBAD_AMOUNT));
env(coverWithdraw(alice, vault.vaultID, vault.asset(10)), ter(tecNO_ENTRY));
env(coverWithdraw(alice, keylet.key, vault.asset(900)), ter(tecINSUFFICIENT_FUNDS));
// Skip this test for XRP, because that can always be sent
if (!vault.asset.raw().native())
{
TER const expected = vault.asset.raw().holds<MPTIssue>() ? tecNO_AUTH : tecNO_LINE;
env(coverWithdraw(alice, keylet.key, vault.asset(1)),
destination(bystander),
ter(expected));
}
// Can not withdraw to the zero address
env(coverWithdraw(alice, keylet.key, vault.asset(1)),
destination(AccountID{}),
ter(temMALFORMED));
// Withdraw some of the cover amount
env(coverWithdraw(alice, keylet.key, vault.asset(7)));
env.close();
verifyCoverAmount(3);
// Add some more cover
env(coverDeposit(alice, keylet.key, vault.asset(5)));
env.close();
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(1)), destination(evan));
env.close();
verifyCoverAmount(7);
// Withdraw some more. Send it to Evan. Very generous, considering
// how much trouble he's been.
env(coverWithdraw(alice, keylet.key, vault.asset(1)), destination(evan), dtag(3));
env.close();
verifyCoverAmount(6);
if (!vault.asset.raw().native())
{
// Issuer claws back some of the cover
env(coverClawback(issuer), loanBrokerID(keylet.key), amount(vault.asset(2)));
env.close();
verifyCoverAmount(4);
// Deposit some back
env(coverDeposit(alice, keylet.key, vault.asset(5)));
env.close();
verifyCoverAmount(9);
// Issuer claws it all back in various different ways
for (auto const& tx : {
// defer autofills until submission time
env.json(
coverClawback(issuer),
loanBrokerID(keylet.key),
fee(none),
seq(none),
sig(none)),
env.json(
coverClawback(issuer),
loanBrokerID(keylet.key),
amount(vault.asset(0)),
fee(none),
seq(none),
sig(none)),
env.json(
coverClawback(issuer),
loanBrokerID(keylet.key),
amount(vault.asset(6)),
fee(none),
seq(none),
sig(none)),
// amount will be truncated to what's available
env.json(
coverClawback(issuer),
loanBrokerID(keylet.key),
amount(vault.asset(100)),
fee(none),
seq(none),
sig(none)),
})
{
// Issuer claws it all back
env(tx);
env.close();
verifyCoverAmount(0);
// Deposit some back
env(coverDeposit(alice, keylet.key, vault.asset(6)));
env.close();
verifyCoverAmount(6);
}
}
// no-op
env(set(alice, vault.vaultID), loanBrokerID(keylet.key));
env.close();
// Make modifications to the broker
if (changeBroker)
changeBroker(broker);
env.close();
// Check the results of modifications
if (BEAST_EXPECT(broker = env.le(keylet)) && 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(""));
env.close();
// Check the updated fields
if (BEAST_EXPECT(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));
// Get the "bad" broker out of the way
env(del(alice, badKeylet.key));
env.close();
// Note alice's balance of the asset and the broker account's cover
// funds
auto const aliceBalance = env.balance(alice, vault.asset);
auto const coverFunds = env.balance(pseudoAccount, vault.asset);
BEAST_EXPECT(coverFunds.number() == broker->at(sfCoverAvailable));
BEAST_EXPECT(coverFunds != beast::zero);
verifyCoverAmount(6);
// 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);
}
auto const expectedBalance = aliceBalance + coverFunds -
(aliceBalance.value().native() ? STAmount(env.current()->fees().base.value())
: vault.asset(0));
env.require(balance(alice, expectedBalance));
env.require(balance(pseudoAccount, vault.asset(none)));
}
}
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"};
// Bystander doesn't have anything to do with the SAV or Broker, or any
// of the relevant tokens
Account bystander{"bystander"};
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, bystander));
env.close();
env(fset(issuer, asfAllowTrustLineClawback));
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.close();
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});
env.close();
PrettyAsset const mptAsset = mptt["MPT"];
mptt.authorize({.account = alice});
mptt.authorize({.account = evan});
env.close();
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();
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);
// Create and update Loan Brokers
for (auto const& vault : vaults)
{
{
// Get the AccountInfo RPC result for the vault pseudo-account
std::string const pseudoStr = to_string(vault.pseudoAccount.id());
auto const accountInfo = env.rpc("account_info", pseudoStr);
if (BEAST_EXPECT(accountInfo.isObject()))
{
auto const& accountData = accountInfo[jss::result][jss::account_data];
if (BEAST_EXPECT(accountData.isObject()))
{
BEAST_EXPECT(accountData[jss::Account] == pseudoStr);
BEAST_EXPECT(accountData[sfVaultID] == to_string(vault.vaultID));
}
auto const& pseudoInfo = accountInfo[jss::result][jss::pseudo_account];
if (BEAST_EXPECT(pseudoInfo.isObject()))
{
BEAST_EXPECT(pseudoInfo[jss::type] == "Vault");
}
}
}
using namespace loanBroker;
using namespace xrpl::Lending;
TenthBips32 const tenthBipsZero{0};
auto badKeylet = keylet::vault(alice.id(), env.seq(alice));
// Try some failure cases
// not the vault owner
env(set(evan, vault.vaultID), ter(tecNO_PERMISSION));
// not a vault
env(set(alice, badKeylet.key), ter(tecNO_ENTRY));
// flags are checked first
env(set(evan, vault.vaultID, ~tfUniversal), ter(temINVALID_FLAG));
// field length validation
// sfData: good length, bad account
env(set(evan, vault.vaultID),
data(std::string(maxDataPayloadLength, 'X')),
ter(tecNO_PERMISSION));
// sfData: too long
env(set(evan, vault.vaultID),
data(std::string(maxDataPayloadLength + 1, 'Y')),
ter(temINVALID));
// sfManagementFeeRate: good value, bad account
env(set(evan, vault.vaultID),
managementFeeRate(maxManagementFeeRate),
ter(tecNO_PERMISSION));
// sfManagementFeeRate: too big
env(set(evan, vault.vaultID),
managementFeeRate(maxManagementFeeRate + TenthBips16(10)),
ter(temINVALID));
// sfCoverRateMinimum and sfCoverRateLiquidation are linked
// Cover: good value, bad account
env(set(evan, vault.vaultID),
coverRateMinimum(maxCoverRate),
coverRateLiquidation(maxCoverRate),
ter(tecNO_PERMISSION));
// CoverMinimum: too big
env(set(evan, vault.vaultID),
coverRateMinimum(maxCoverRate + 1),
coverRateLiquidation(maxCoverRate + 1),
ter(temINVALID));
// CoverLiquidation: too big
env(set(evan, vault.vaultID),
coverRateMinimum(maxCoverRate / 2),
coverRateLiquidation(maxCoverRate + 1),
ter(temINVALID));
// Cover: zero min, non-zero liquidation - implicit and
// explicit zero values.
env(set(evan, vault.vaultID), coverRateLiquidation(maxCoverRate), ter(temINVALID));
env(set(evan, vault.vaultID),
coverRateMinimum(tenthBipsZero),
coverRateLiquidation(maxCoverRate),
ter(temINVALID));
// Cover: non-zero min, zero liquidation - implicit and
// explicit zero values.
env(set(evan, vault.vaultID), coverRateMinimum(maxCoverRate), ter(temINVALID));
env(set(evan, vault.vaultID),
coverRateMinimum(maxCoverRate),
coverRateLiquidation(tenthBipsZero),
ter(temINVALID));
// sfDebtMaximum: good value, bad account
env(set(evan, vault.vaultID), debtMaximum(Number(0)), ter(tecNO_PERMISSION));
// sfDebtMaximum: overflow
env(set(evan, vault.vaultID), debtMaximum(Number(1, 100)), ter(temINVALID));
// sfDebtMaximum: negative
env(set(evan, vault.vaultID), debtMaximum(Number(-1)), ter(temINVALID));
std::string testData;
lifecycle(
"default fields",
env,
issuer,
alice,
evan,
bystander,
vault,
badVault,
// 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);
BEAST_EXPECT(env.ownerCount(alice) == aliceOriginalCount + 4);
},
[&](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_ENTRY));
// Owner
env(set(evan, vault.vaultID),
loanBrokerID(broker->key()),
ter(tecNO_PERMISSION));
// ManagementFeeRate
env(set(alice, vault.vaultID),
loanBrokerID(broker->key()),
managementFeeRate(maxManagementFeeRate),
ter(temINVALID));
// CoverRateMinimum
env(set(alice, vault.vaultID),
loanBrokerID(broker->key()),
coverRateMinimum(maxManagementFeeRate),
ter(temINVALID));
// CoverRateLiquidation
env(set(alice, vault.vaultID),
loanBrokerID(broker->key()),
coverRateLiquidation(maxManagementFeeRate),
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));
Number debtMax{175, -1};
if (vault.asset.integral())
{
env(set(alice, vault.vaultID),
loanBrokerID(broker->key()),
data(testData),
debtMaximum(debtMax),
ter(tecPRECISION_LOSS));
roundToAsset(vault.asset, debtMax);
}
// Data & Debt maximum
env(set(alice, vault.vaultID),
loanBrokerID(broker->key()),
data(testData),
debtMaximum(debtMax));
},
[&](SLE::const_ref broker) {
// Check the updated fields
BEAST_EXPECT(checkVL(broker->at(sfData), testData));
Number const expected = STAmount{vault.asset, Number(175, -1)};
auto const actual = broker->at(sfDebtMaximum);
BEAST_EXPECTS(
actual == expected,
"Expected: " + to_string(expected) + ", Actual: " + to_string(actual));
});
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
// values at default
return env.jt(
jv,
data(testData),
managementFeeRate(TenthBips16(123)),
debtMaximum(Number(9)),
coverRateMinimum(TenthBips32(100)),
coverRateLiquidation(TenthBips32(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));
});
}
BEAST_EXPECT(env.ownerCount(alice) == aliceOriginalCount);
}
enum LoanBrokerTest { CoverClawback, CoverDeposit, CoverWithdraw, Delete, Set };
void
testLoanBroker(
std::function<jtx::PrettyAsset(jtx::Env&, jtx::Account const&, jtx::Account const&)>
getAsset,
LoanBrokerTest brokerTest)
{
using namespace jtx;
using namespace loanBroker;
Account const issuer{"issuer"};
Account const alice{"alice"};
Env env(*this);
Vault vault{env};
env.fund(XRP(100'000), issuer, alice);
env.close();
PrettyAsset const asset = [&]() {
if (getAsset)
return getAsset(env, issuer, alice);
env(trust(alice, issuer["IOU"](1'000'000)));
env.close();
return PrettyAsset(issuer["IOU"]);
}();
env(pay(issuer, alice, asset(100'000)));
env.close();
auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = asset});
env(tx);
env.close();
auto const le = env.le(vaultKeylet);
VaultInfo vaultInfo = [&]() {
if (BEAST_EXPECT(le))
return VaultInfo{asset, vaultKeylet.key, le->at(sfAccount)};
return VaultInfo{asset, {}, {}};
}();
if (vaultInfo.vaultID == uint256{})
return;
env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(50)}));
env.close();
auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
env(set(alice, vaultInfo.vaultID));
env.close();
auto broker = env.le(brokerKeylet);
if (!BEAST_EXPECT(broker))
return;
auto testZeroBrokerID = [&](auto&& getTxJv) {
auto jv = getTxJv();
// empty broker ID
jv[sfLoanBrokerID] = "";
env(jv, ter(temINVALID));
// zero broker ID
jv[sfLoanBrokerID] = to_string(uint256{});
// needs a flag to distinguish the parsed STTx from the prior
// test
env(jv, txflags(tfFullyCanonicalSig), ter(temINVALID));
};
auto testZeroVaultID = [&](auto&& getTxJv) {
auto jv = getTxJv();
// empty broker ID
jv[sfVaultID] = "";
env(jv, ter(temINVALID));
// zero broker ID
jv[sfVaultID] = to_string(uint256{});
// needs a flag to distinguish the parsed STTx from the prior
// test
env(jv, txflags(tfFullyCanonicalSig), ter(temINVALID));
};
if (brokerTest == CoverDeposit)
{
// preflight: temINVALID (empty/zero broker id)
testZeroBrokerID([&]() { return coverDeposit(alice, brokerKeylet.key, asset(10)); });
// preclaim: tecWRONG_ASSET
env(coverDeposit(alice, brokerKeylet.key, issuer["BAD"](10)), ter(tecWRONG_ASSET));
// preclaim: tecINSUFFICIENT_FUNDS
env(pay(alice, issuer, asset(100'000 - 50)));
env.close();
env(coverDeposit(alice, brokerKeylet.key, vaultInfo.asset(10)),
ter(tecINSUFFICIENT_FUNDS));
// preclaim: tecFROZEN
env(fset(issuer, asfGlobalFreeze));
env.close();
env(coverDeposit(alice, brokerKeylet.key, vaultInfo.asset(10)), ter(tecFROZEN));
}
else
{
// Fund the cover deposit
env(coverDeposit(alice, brokerKeylet.key, vaultInfo.asset(10)));
}
env.close();
if (brokerTest == CoverWithdraw)
{
// preflight: temINVALID (empty/zero broker id)
testZeroBrokerID([&]() { return coverWithdraw(alice, brokerKeylet.key, asset(10)); });
// preclaim: tecWRONG_ASSET
env(coverWithdraw(alice, brokerKeylet.key, issuer["BAD"](10)), ter(tecWRONG_ASSET));
// preclaim: tecNO_DST
Account const bogus{"bogus"};
env(coverWithdraw(alice, brokerKeylet.key, asset(10)),
destination(bogus),
ter(tecNO_DST));
// preclaim: tecDST_TAG_NEEDED
Account const dest{"dest"};
env.fund(XRP(1'000), dest);
env(fset(dest, asfRequireDest));
env.close();
env(coverWithdraw(alice, brokerKeylet.key, asset(10)),
destination(dest),
ter(tecDST_TAG_NEEDED));
// preclaim: tecNO_PERMISSION
env(fclear(dest, asfRequireDest));
env(fset(dest, asfDepositAuth));
env.close();
env(coverWithdraw(alice, brokerKeylet.key, asset(10)),
destination(dest),
ter(tecNO_PERMISSION));
// preclaim: tecFROZEN
env(trust(dest, asset(1'000)));
env(fclear(dest, asfDepositAuth));
env(fset(issuer, asfGlobalFreeze));
env.close();
env(coverWithdraw(alice, brokerKeylet.key, asset(10)),
destination(dest),
ter(tecFROZEN));
// preclaim:: tecFROZEN (deep frozen)
env(fclear(issuer, asfGlobalFreeze));
env(trust(issuer, asset(1'000), dest, tfSetFreeze | tfSetDeepFreeze));
env(coverWithdraw(alice, brokerKeylet.key, asset(10)),
destination(dest),
ter(tecFROZEN));
// preclaim: tecPSEUDO_ACCOUNT
env(coverWithdraw(alice, brokerKeylet.key, asset(10)),
destination(vaultInfo.pseudoAccount),
ter(tecPSEUDO_ACCOUNT));
}
if (brokerTest == CoverClawback)
{
// preflight: temINVALID (empty/zero broker id)
testZeroBrokerID([&]() {
return env.json(
coverClawback(alice),
loanBrokerID(brokerKeylet.key),
amount(vaultInfo.asset(2)));
});
if (asset.holds<Issue>())
{
// preclaim: AllowTrustLineClawback is not set
env(coverClawback(issuer),
loanBrokerID(brokerKeylet.key),
amount(vaultInfo.asset(2)),
ter(tecNO_PERMISSION));
// preclaim: NoFreeze is set
env(fset(issuer, asfAllowTrustLineClawback | asfNoFreeze));
env.close();
env(coverClawback(issuer),
loanBrokerID(brokerKeylet.key),
amount(vaultInfo.asset(2)),
ter(tecNO_PERMISSION));
}
else
{
// preclaim: MPTCanClawback is not set or MPTCanLock is not set
env(coverClawback(issuer),
loanBrokerID(brokerKeylet.key),
amount(vaultInfo.asset(2)),
ter(tecNO_PERMISSION));
}
env.close();
}
if (brokerTest == Delete)
{
Account const borrower{"borrower"};
env.fund(XRP(1'000), borrower);
env(loan::set(borrower, brokerKeylet.key, asset(50).value()),
sig(sfCounterpartySignature, alice),
fee(env.current()->fees().base * 2));
// preflight: temINVALID (empty/zero broker id)
testZeroBrokerID([&]() { return del(alice, brokerKeylet.key); });
// preclaim: tecHAS_OBLIGATIONS
env(del(alice, brokerKeylet.key), ter(tecHAS_OBLIGATIONS));
// Repay and delete the loan
auto const loanKeylet = keylet::loan(brokerKeylet.key, 1);
env(loan::pay(borrower, loanKeylet.key, asset(50).value()));
env(loan::del(alice, loanKeylet.key));
env(trust(issuer, asset(0), alice, tfSetFreeze | tfSetDeepFreeze));
// preclaim: tecFROZEN (deep frozen)
env(del(alice, brokerKeylet.key), ter(tecFROZEN));
env(trust(issuer, asset(0), alice, tfClearFreeze | tfClearDeepFreeze));
// successful delete the loan broker object
env(del(alice, brokerKeylet.key), ter(tesSUCCESS));
}
else
{
env(del(alice, brokerKeylet.key));
}
if (brokerTest == Set)
{
// preflight: temINVALID (empty/zero broker id)
testZeroBrokerID([&]() {
return env.json(set(alice, vaultInfo.vaultID), loanBrokerID(brokerKeylet.key));
});
// preflight: temINVALID (empty/zero vault id)
testZeroVaultID([&]() {
return env.json(set(alice, vaultInfo.vaultID), loanBrokerID(brokerKeylet.key));
});
if (asset.holds<Issue>())
{
env(fclear(issuer, asfDefaultRipple));
env.close();
// preclaim: DefaultRipple is not set
env(set(alice, vaultInfo.vaultID), ter(terNO_RIPPLE));
env(fset(issuer, asfDefaultRipple));
env.close();
}
auto const amt =
env.balance(alice) - env.current()->fees().accountReserve(env.ownerCount(alice));
env(pay(alice, issuer, amt));
// preclaim:: tecINSUFFICIENT_RESERVE
env(set(alice, vaultInfo.vaultID), ter(tecINSUFFICIENT_RESERVE));
}
}
void
testInvalidLoanBrokerCoverClawback()
{
testcase("Invalid LoanBrokerCoverClawback");
using namespace jtx;
using namespace loanBroker;
// preflight
{
Account const alice{"alice"};
Account const issuer{"issuer"};
auto const USD = alice["USD"];
Env env(*this);
env.fund(XRP(100'000), alice);
env.close();
auto jtx = env.jt(coverClawback(alice), amount(USD(100)));
// holder == account
env(jtx, ter(temINVALID));
// holder == beast::zero
STAmount bad(Issue{USD.currency, beast::zero}, 100);
jtx.jv[sfAmount] = bad.getJson();
jtx.stx = env.ust(jtx);
Serializer s;
jtx.stx->add(s);
auto const jrr = env.rpc("submit", strHex(s.slice()))[jss::result];
// fails in doSubmit() on STTx construction
BEAST_EXPECT(jrr[jss::error] == "invalidTransaction");
BEAST_EXPECT(jrr[jss::error_exception] == "invalid native account");
}
// preclaim
// Issue:
// AllowTrustLineClawback is not set or NoFreeze is set
testLoanBroker({}, CoverClawback);
// MPTIssue:
// MPTCanClawback is not set
testLoanBroker(
[&](Env& env, Account const& issuer, Account const& alice) -> MPT {
MPTTester mpt({.env = env, .issuer = issuer, .holders = {alice}});
return mpt;
},
CoverClawback);
}
void
testInvalidLoanBrokerCoverDeposit()
{
testcase("Invalid LoanBrokerCoverDeposit");
using namespace jtx;
// preclaim:
// tecWRONG_ASSET, tecINSUFFICIENT_FUNDS, frozen asset
testLoanBroker({}, CoverDeposit);
}
void
testInvalidLoanBrokerCoverWithdraw()
{
testcase("Invalid LoanBrokerCoverWithdraw");
using namespace jtx;
/*
preflight: illegal net
isLegalNet() check is probably redundant. STAmount parsing
should throw an exception on deserialize
preclaim: tecWRONG_ASSET, tecNO_DST, tecDST_TAG_NEEDED,
tecNO_PERMISSION, checkFrozen failure, checkDeepFrozenFailure,
second+third tecINSUFFICIENT_FUNDS (can this happen)?
doApply: tecPATH_DRY (can it happen, funds already checked?)
*/
testLoanBroker({}, CoverWithdraw);
}
void
testInvalidLoanBrokerDelete()
{
using namespace jtx;
testcase("Invalid LoanBrokerDelete");
/*
preclaim: tecHAS_OBLIGATIONS
doApply:
accountSend failure, removeEmptyHolding failure,
all tecHAS_OBLIGATIONS (can any of these happen?)
*/
testLoanBroker({}, Delete);
}
void
testInvalidLoanBrokerSet()
{
using namespace jtx;
testcase("Invalid LoanBrokerSet");
/*preclaim: canAddHolding failure (can it happen with MPT?
can't create Vault if CanTransfer is not enabled.)
doApply:
first+second dirLink failure, createPseudoAccount failure,
addEmptyHolding failure
can any of these happen?
*/
testLoanBroker({}, Set);
}
void
testLoanBrokerCoverDepositNullVault()
{
// This test is lifted directly from
// https://bugs.immunefi.com/dashboard/submission/57808
using namespace jtx;
Env env(*this);
Account const alice{"alice"};
env.fund(XRP(10000), alice);
env.close();
// Create a Vault owned by alice with an XRP asset
PrettyAsset const asset{xrpIssue(), 1};
Vault vault{env};
auto const [createTx, vaultKeylet] = vault.create({.owner = alice, .asset = asset});
env(createTx);
env.close();
// Predict LoanBroker key using alice's current sequence BEFORE submit
auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
// Create LoanBroker pointing to the vault
env(loanBroker::set(alice, vaultKeylet.key));
env.close();
// Build the CoverDeposit STTx directly
STTx tx{ttLOAN_BROKER_COVER_DEPOSIT, [](STObject&) {}};
tx.setAccountID(sfAccount, alice.id());
tx.setFieldH256(sfLoanBrokerID, brokerKeylet.key);
tx.setFieldAmount(sfAmount, asset(1));
// Create a writable view cloned from the current ledger and remove the
// vault SLE
OpenView ov{*env.current()};
test::StreamSink sink{beast::severities::kWarning};
beast::Journal jlog{sink};
ApplyContext ac{env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, tapNONE, jlog};
if (auto sleBroker = ac.view().peek(keylet::loanbroker(brokerKeylet.key)))
{
auto const vaultID = (*sleBroker)[sfVaultID];
if (auto sleVault = ac.view().peek(keylet::vault(vaultID)))
{
ac.view().erase(sleVault);
}
}
// Invoke preclaim against the mutated (ApplyView) view; triggers
// nullptr deref
PreclaimContext pctx{env.app(), ac.view(), tesSUCCESS, tx, tapNONE, jlog};
(void)LoanBrokerCoverDeposit::preclaim(pctx);
}
void
testRequireAuth()
{
testcase("Require Auth - Implicit Pseudo-account authorization");
using namespace jtx;
using namespace loanBroker;
Account const issuer{"issuer"};
Account const alice{"alice"};
Env env(*this);
Vault vault{env};
env.fund(XRP(100'000), issuer, alice);
env.close();
auto asset = MPTTester({
.env = env,
.issuer = issuer,
.holders = {alice},
.flags = MPTDEXFlags | tfMPTRequireAuth | tfMPTCanClawback | tfMPTCanLock,
.authHolder = true,
});
env(pay(issuer, alice, asset(100'000)));
env.close();
// Alice is not authorized, can still create the vault
asset.authorize({.account = issuer, .holder = alice, .flags = tfMPTUnauthorize});
auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = asset});
env(tx);
env.close();
auto const le = env.le(vaultKeylet);
VaultInfo vaultInfo = [&]() {
if (BEAST_EXPECT(le))
return VaultInfo{asset, vaultKeylet.key, le->at(sfAccount)};
return VaultInfo{asset, {}, {}};
}();
if (vaultInfo.vaultID == uint256{})
return;
// Can't unauthorize Vault pseudo-account
asset.authorize(
{.account = issuer,
.holder = vaultInfo.pseudoAccount,
.flags = tfMPTUnauthorize,
.err = tecNO_PERMISSION});
auto forUnauthAuth = [&](auto&& doTx) {
for (auto const flag : {tfMPTUnauthorize, 0u})
{
asset.authorize({.account = issuer, .holder = alice, .flags = flag});
env.close();
doTx(flag == 0);
env.close();
}
};
// Can't deposit into Vault if the vault owner is not authorized
forUnauthAuth([&](bool authorized) {
auto const err = !authorized ? ter(tecNO_AUTH) : ter(tesSUCCESS);
env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(51)}),
err);
});
// Can't withdraw from Vault if the vault owner is not authorized
forUnauthAuth([&](bool authorized) {
auto const err = !authorized ? ter(tecNO_AUTH) : ter(tesSUCCESS);
env(vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = asset(1)}),
err);
});
auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
// Can create LoanBroker if the vault owner is not authorized
forUnauthAuth([&](auto) { env(set(alice, vaultInfo.vaultID)); });
auto const broker = env.le(brokerKeylet);
if (!BEAST_EXPECT(broker))
return;
Account brokerPseudo("pseudo", broker->at(sfAccount));
// Can't unauthorize LoanBroker pseudo-account
asset.authorize(
{.account = issuer,
.holder = brokerPseudo,
.flags = tfMPTUnauthorize,
.err = tecNO_PERMISSION});
// Can't cover deposit into Vault if the vault owner is not authorized
forUnauthAuth([&](bool authorized) {
auto const err = !authorized ? ter(tecNO_AUTH) : ter(tesSUCCESS);
env(coverDeposit(alice, brokerKeylet.key, vaultInfo.asset(10)), err);
});
// Can't cover withdraw from Vault if the vault owner is not authorized
forUnauthAuth([&](bool authorized) {
auto const err = !authorized ? ter(tecNO_AUTH) : ter(tesSUCCESS);
env(coverWithdraw(alice, brokerKeylet.key, vaultInfo.asset(5)), err);
});
// Issuer can always cover clawback. The holder authorization is n/a.
forUnauthAuth([&](bool) {
env(coverClawback(issuer), loanBrokerID(brokerKeylet.key), amount(vaultInfo.asset(1)));
});
}
void
testLoanBrokerSetDebtMaximum()
{
testcase("testLoanBrokerSetDebtMaximum");
using namespace jtx;
using namespace loanBroker;
Account const issuer{"issuer"};
Account const alice{"alice"};
Env env(*this);
Vault vault{env};
env.fund(XRP(100'000), issuer, alice);
env.close();
PrettyAsset const asset = [&]() {
MPTTester mptt{env, issuer, mptInitNoFund};
mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
env.close();
PrettyAsset const mptAsset = mptt["MPT"];
mptt.authorize({.account = alice});
env.close();
return mptAsset;
}();
env(pay(issuer, alice, asset(100'000)));
env.close();
auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = asset});
env(tx);
env.close();
auto const le = env.le(vaultKeylet);
VaultInfo vaultInfo = [&]() {
if (BEAST_EXPECT(le))
return VaultInfo{asset, vaultKeylet.key, le->at(sfAccount)};
return VaultInfo{asset, {}, {}};
}();
if (vaultInfo.vaultID == uint256{})
return;
env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(50)}));
env.close();
auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
env(set(alice, vaultInfo.vaultID));
env.close();
Account const borrower{"borrower"};
env.fund(XRP(1'000), borrower);
env(loan::set(borrower, brokerKeylet.key, asset(50).value()),
sig(sfCounterpartySignature, alice),
fee(env.current()->fees().base * 2));
auto const broker = env.le(brokerKeylet);
if (!BEAST_EXPECT(broker))
return;
BEAST_EXPECT(broker->at(sfDebtTotal) == 50);
auto debtTotal = broker->at(sfDebtTotal);
auto tx2 = set(alice, vaultInfo.vaultID);
tx2[sfLoanBrokerID] = to_string(brokerKeylet.key);
tx2[sfDebtMaximum] = debtTotal - 1;
env(tx2, ter(tecLIMIT_EXCEEDED));
tx2[sfDebtMaximum] = debtTotal + 1;
env(tx2, ter(tesSUCCESS));
tx2[sfDebtMaximum] = 0;
env(tx2, ter(tesSUCCESS));
tx2[sfDebtMaximum] = Json::Value::maxInt;
env(tx2, ter(tesSUCCESS));
{
auto const dm = power(2, 64) - 1;
BEAST_EXPECT(dm > maxMPTokenAmount);
tx2[sfDebtMaximum] = dm;
env(tx2, ter(temINVALID));
}
{
auto const dm = power(2, 63) - 1;
BEAST_EXPECTS(dm > maxMPTokenAmount, to_string(dm));
tx2[sfDebtMaximum] = dm;
env(tx2, ter(temINVALID));
}
{
auto const dm = power(2, 63) - 3;
BEAST_EXPECTS(dm == maxMPTokenAmount, to_string(dm));
tx2[sfDebtMaximum] = dm;
env(tx2, ter(tesSUCCESS));
}
{
auto const dm = 2 * (power(2, 62) - 1) + 1;
BEAST_EXPECTS(dm == maxMPTokenAmount, to_string(dm));
tx2[sfDebtMaximum] = dm;
env(tx2, ter(tesSUCCESS));
}
tx2[sfDebtMaximum] = Number{9223372036854775807, 0};
env(tx2, ter(tesSUCCESS));
}
void
testRIPD4323()
{
testcase << "RIPD-4323";
using namespace jtx;
Account const issuer("issuer");
Account const holder("holder");
Account const& broker = issuer;
auto test = [&](auto&& getToken) {
Env env(*this);
env.fund(XRP(1'000), issuer, holder);
env.close();
auto const [token, deposit, err] = getToken(env);
Vault vault(env);
auto const [tx, keylet] = vault.create({.owner = broker, .asset = token.asset()});
env(tx);
env.close();
env(vault.deposit({.depositor = broker, .id = keylet.key, .amount = deposit}),
ter(err));
env.close();
auto const brokerKeylet = keylet::loanbroker(broker, env.seq(broker));
env(loanBroker::set(broker, keylet.key));
env.close();
env(loanBroker::coverDeposit(broker, brokerKeylet.key, deposit), ter(err));
env.close();
};
test([&](Env&) {
// issuer can issue any amount
auto const token = issuer["IOU"];
return std::make_tuple(token, token(1'000), tesSUCCESS);
});
std::vector<std::tuple<
std::uint64_t, // pay to holder
std::optional<std::uint64_t>, // max amount
std::uint64_t, // deposit amount
TER>> // expected error
mptTests = {
// issuer can issue up to 2'000 tokens
{2'000, 4'000, 1'000, tesSUCCESS},
// issuer can issue 500 tokens (250 VaultDeposit +
// 250 LoanBrokerCoverDeposit)
{2'000, 2'500, 250, tesSUCCESS},
// issuer can issue 500 tokens (250 VaultDeposit +
// 250 LoanBrokerCoverDeposit). MaximumAmount is default.
{maxMPTokenAmount - 500, std::nullopt, 250, tesSUCCESS},
// issuer can issue 500, and fails on depositing 1'000
{2'000, 2'500, 1'000, tecINSUFFICIENT_FUNDS},
// issuer has already issued MaximumAmount
{2'000, 2'000, 1'000, tecINSUFFICIENT_FUNDS},
// issuer has already issued MaximumAmount. MaximumAmount is
// default.
{maxMPTokenAmount, std::nullopt, 250, tecINSUFFICIENT_FUNDS},
};
for (auto const& [pay, max, deposit, err] : mptTests)
{
test([&](Env& env) -> std::tuple<MPT, PrettyAmount, TER> {
MPT const token = MPTTester(
{.env = env,
.issuer = issuer,
.holders = {holder},
.pay = pay,
.flags = MPTDEXFlags,
.maxAmt = max});
return std::make_tuple(token, token(deposit), err);
});
}
}
void
testAMB06_VaultFreezeCheckMissing()
{
testcase << "RIPD-4466 - LoanBrokerSet disallows frozen vaults";
using namespace jtx;
Env env(*this);
Account const issuer{"issuer"}, lender{"lender"}, borrower{"borrower"};
env.fund(XRP(20'000), issuer, lender, borrower);
auto const IOU = issuer["IOU"];
Vault vault{env};
auto [tx, vaultKeylet] = vault.create({.owner = lender, .asset = IOU.asset()});
env(tx);
env.close();
// Get vault pseudo-account and FREEZE it
auto const vaultSle = env.le(vaultKeylet);
auto const vaultPseudo = vaultSle->at(sfAccount);
auto const vaultPseudoAcct = Account("VaultPseudo", vaultPseudo);
env(trust(issuer, vaultPseudoAcct["IOU"](0), tfSetFreeze));
env(loanBroker::set(lender, vaultKeylet.key), ter(tecFROZEN));
}
void
testRIPD4274IOU()
{
using namespace jtx;
Account issuer("broker");
Account broker("issuer");
Account dest("destination");
auto const token = issuer["IOU"];
enum TrustState {
RequireAuth,
ZeroLimit,
ReachedLimit,
NearLimit,
NoTrustLine,
};
auto test = [&](TrustState trustState) {
Env env(*this);
testcase << "RIPD-4274 IOU with state: " << static_cast<int>(trustState);
auto setTrustLine = [&](Account const& acct, TrustState state) {
switch (state)
{
case RequireAuth:
env(trust(issuer, token(0), acct, tfSetfAuth));
break;
case ZeroLimit: {
auto jv = trust(acct, token(0));
// set QualityIn so that the trustline is not
// auto-deleted
jv[sfQualityIn] = 10'000'000;
env(jv);
}
break;
case ReachedLimit: {
env(trust(acct, token(1'000)));
env(pay(issuer, acct, token(1'000)));
env.close();
}
break;
case NearLimit: {
env(trust(acct, token(1'000)));
env(pay(issuer, acct, token(950)));
env.close();
}
break;
case NoTrustLine:
// don't create a trustline
break;
default:
BEAST_EXPECT(false);
}
env.close();
};
env.fund(XRP(1'000), issuer, broker, dest);
env.close();
if (trustState == RequireAuth)
{
env(fset(issuer, asfRequireAuth));
env.close();
setTrustLine(broker, RequireAuth);
}
setTrustLine(dest, trustState);
env(trust(broker, token(2'000), 0));
env(pay(issuer, broker, token(2'000)));
env.close();
Vault vault(env);
auto const [tx, keylet] = vault.create({.owner = broker, .asset = token.asset()});
env(tx);
env.close();
// Test Vault withdraw
env(vault.deposit({.depositor = broker, .id = keylet.key, .amount = token(1'000)}));
env.close();
env(vault.withdraw({.depositor = broker, .id = keylet.key, .amount = token(1'000)}),
loanBroker::destination(dest),
ter(std::ignore));
BEAST_EXPECT(env.ter() == tecNO_LINE);
env.close();
env(vault.withdraw({.depositor = broker, .id = keylet.key, .amount = token(1'000)}));
// Test LoanBroker withdraw
auto const brokerKeylet = keylet::loanbroker(broker, env.seq(broker));
env(loanBroker::set(broker, keylet.key));
env.close();
env(loanBroker::coverDeposit(broker, brokerKeylet.key, token(1'000)));
env.close();
env(loanBroker::coverWithdraw(broker, brokerKeylet.key, token(100)),
loanBroker::destination(dest),
ter(std::ignore));
BEAST_EXPECT(env.ter() == tecNO_LINE);
env.close();
// Clearing RequireAuth shouldn't change the result
if (trustState == RequireAuth)
{
env(fclear(issuer, asfRequireAuth));
env.close();
env(loanBroker::coverWithdraw(broker, brokerKeylet.key, token(100)),
loanBroker::destination(dest),
ter(std::ignore));
BEAST_EXPECT(env.ter() == tecNO_LINE);
env.close();
}
};
test(RequireAuth);
test(ZeroLimit);
test(ReachedLimit);
test(NearLimit);
test(NoTrustLine);
}
void
testRIPD4274MPT()
{
using namespace jtx;
Account issuer("broker");
Account broker("issuer");
Account dest("destination");
enum MPTState {
RequireAuth,
ReachedMAX,
NoMPT,
};
auto test = [&](MPTState MPTState) {
Env env(*this);
testcase << "RIPD-4274 MPT with state: " << static_cast<int>(MPTState);
env.fund(XRP(1'000), issuer, broker, dest);
env.close();
auto const maybeToken = [&]() -> std::optional<MPT> {
switch (MPTState)
{
case RequireAuth: {
auto tester = MPTTester(
{.env = env,
.issuer = issuer,
.holders = {broker, dest},
.pay = 2'000,
.flags = MPTDEXFlags | tfMPTRequireAuth,
.authHolder = true,
.maxAmt = 5'000});
// unauthorize dest
tester.authorize(
{.account = issuer, .holder = dest, .flags = tfMPTUnauthorize});
return tester;
}
case ReachedMAX: {
auto tester = MPTTester(
{.env = env,
.issuer = issuer,
.holders = {broker, dest},
.pay = 2'000,
.flags = MPTDEXFlags,
.maxAmt = 4'000});
BEAST_EXPECT(env.balance(issuer, tester) == tester(-4'000));
return tester;
}
case NoMPT: {
return MPTTester(
{.env = env,
.issuer = issuer,
.holders = {broker},
.pay = 2'000,
.flags = MPTDEXFlags,
.maxAmt = 4'000});
}
default:
return std::nullopt;
}
}();
if (!BEAST_EXPECT(maybeToken))
return;
auto const& token = *maybeToken;
Vault vault(env);
auto const [tx, keylet] = vault.create({.owner = broker, .asset = token.asset()});
env(tx);
env.close();
// Test Vault withdraw
env(vault.deposit({.depositor = broker, .id = keylet.key, .amount = token(1'000)}));
env.close();
env(vault.withdraw({.depositor = broker, .id = keylet.key, .amount = token(1'000)}),
loanBroker::destination(dest),
ter(std::ignore));
// Shouldn't fail if at MaximumAmount since no new tokens are issued
TER const err = MPTState == ReachedMAX ? TER(tesSUCCESS) : tecNO_AUTH;
BEAST_EXPECT(env.ter() == err);
env.close();
if (!isTesSuccess(err))
{
env(vault.withdraw(
{.depositor = broker, .id = keylet.key, .amount = token(1'000)}));
}
// Test LoanBroker withdraw
auto const brokerKeylet = keylet::loanbroker(broker, env.seq(broker));
env(loanBroker::set(broker, keylet.key));
env.close();
env(loanBroker::coverDeposit(broker, brokerKeylet.key, token(1'000)));
env.close();
env(loanBroker::coverWithdraw(broker, brokerKeylet.key, token(100)),
loanBroker::destination(dest),
ter(std::ignore));
BEAST_EXPECT(env.ter() == err);
env.close();
};
test(RequireAuth);
test(ReachedMAX);
test(NoMPT);
}
void
testRIPD4274()
{
testRIPD4274IOU();
testRIPD4274MPT();
}
public:
void
run() override
{
testLoanBrokerSetDebtMaximum();
testLoanBrokerCoverDepositNullVault();
testDisabled();
testLifecycle();
testInvalidLoanBrokerCoverClawback();
testInvalidLoanBrokerCoverDeposit();
testInvalidLoanBrokerCoverWithdraw();
testInvalidLoanBrokerDelete();
testInvalidLoanBrokerSet();
testRequireAuth();
testRIPD4323();
testAMB06_VaultFreezeCheckMissing();
testRIPD4274();
// TODO: Write clawback failure tests with an issuer / MPT that doesn't
// have the right flags set.
}
};
BEAST_DEFINE_TESTSUITE(LoanBroker, tx, xrpl);
} // namespace test
} // namespace xrpl