#include #include #include 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::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(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 modifyJTx, std::function checkBroker, std::function changeBroker, std::function checkChangedBroker) { { auto const& asset = vault.asset.raw(); testcase << "Lifecycle: " << (asset.native() ? "XRP " : asset.holds() ? "IOU " : asset.holds() ? "MPT " : "Unknown ") << 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()) { // 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()); 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() ? 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 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; 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)); // Cover: too big env(set(evan, vault.vaultID), coverRateMinimum(maxCoverRate + 1), 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_PERMISSION)); // 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)); // 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, 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 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; if (brokerTest == CoverDeposit) { // preclaim: tecWRONG_ASET 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) { // preclaim: tecWRONG_ASSSET 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)); } if (brokerTest == CoverClawback) { if (asset.holds()) { // preclaim: AllowTrustLineClaback 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 broker id) { auto jv = del(alice, brokerKeylet.key); jv[sfLoanBrokerID] = ""; env(jv, ter(temINVALID)); } // preflight: temINVALID (zero broker id) { // needs a flag to distinguish the parsed STTx from the prior // test auto jv = del(alice, uint256{}, tfFullyCanonicalSig); BEAST_EXPECT( jv[sfLoanBrokerID] == "0000000000000000000000000000000000000000000000000000000000" "000000"); env(jv, ter(temINVALID)); } // preclaim: tecHAS_OBLIGATIONS env(del(alice, brokerKeylet.key), ter(tecHAS_OBLIGATIONS)); } else env(del(alice, brokerKeylet.key)); if (brokerTest == Set) { if (asset.holds()) { 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); // MPTCanLock is not set testLoanBroker( [&](Env& env, Account const& issuer, Account const& alice) -> MPT { MPTTester mpt( {.env = env, .issuer = issuer, .holders = {alice}, .flags = MPTDEXFlags | tfMPTCanClawback}); 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); } public: void run() override { testLoanBrokerCoverDepositNullVault(); testDisabled(); testLifecycle(); testInvalidLoanBrokerCoverClawback(); testInvalidLoanBrokerCoverDeposit(); testInvalidLoanBrokerCoverWithdraw(); testInvalidLoanBrokerDelete(); testInvalidLoanBrokerSet(); // TODO: Write clawback failure tests with an issuer / MPT that doesn't // have the right flags set. } }; BEAST_DEFINE_TESTSUITE(LoanBroker, tx, ripple); } // namespace test } // namespace ripple