From 5fe00e32a34153b71c5c4812a0f8e57077fc9f47 Mon Sep 17 00:00:00 2001 From: Valentin Balaschenko <13349202+vlntb@users.noreply.github.com> Date: Thu, 4 Dec 2025 19:38:07 +0000 Subject: [PATCH 1/8] fix --- src/test/app/Vault_test.cpp | 41 +++++++++++++++++++++++ src/xrpld/app/tx/detail/VaultWithdraw.cpp | 32 ++++++++++++++---- 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index ce76e069126..11b405e082f 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -5243,6 +5243,46 @@ class Vault_test : public beast::unit_test::suite }); } + void + testFrozenWithdrawToIssuer() + { + using namespace test::jtx; + + testcase("frozen asset cannot withdraw to issuer (spec deviation)"); + + Env env{*this, testable_amendments() | featureSingleAssetVault}; + Account issuer{"issuer"}; + Account owner{"owner"}; + Account depositor{"depositor"}; + env.fund(XRP(1000), issuer, owner, depositor); + env.close(); + + PrettyAsset asset = issuer["IOU"]; + env.trust(asset(1000), owner); + env.trust(asset(1000), depositor); + env(pay(issuer, owner, asset(100))); + env(pay(issuer, depositor, asset(200))); + env.close(); + + Vault vault{env}; + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + + env(vault.deposit( + {.depositor = depositor, .id = keylet.key, .amount = asset(50)})); + env.close(); + + env(fset(issuer, asfGlobalFreeze)); + env.close(); + + auto withdraw = vault.withdraw( + {.depositor = depositor, .id = keylet.key, .amount = asset(10)}); + withdraw[sfDestination] = issuer.human(); + env(withdraw, ter{tesSUCCESS}); + env.close(); + } + public: void run() override @@ -5261,6 +5301,7 @@ class Vault_test : public beast::unit_test::suite testScaleIOU(); testRPC(); testDelegate(); + testFrozenWithdrawToIssuer(); } }; diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.cpp b/src/xrpld/app/tx/detail/VaultWithdraw.cpp index a3bef88d49b..000cd84e0d4 100644 --- a/src/xrpld/app/tx/detail/VaultWithdraw.cpp +++ b/src/xrpld/app/tx/detail/VaultWithdraw.cpp @@ -80,13 +80,23 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx) return ter; // Cannot withdraw from a Vault an Asset frozen for the destination account - if (auto const ret = checkFrozen(ctx.view, dstAcct, vaultAsset)) - return ret; + if (!vaultAsset.holds() || + (dstAcct != vaultAsset.getIssuer() && + account != vaultAsset.getIssuer())) + { + if (auto const ret = checkFrozen(ctx.view, dstAcct, vaultAsset)) + return ret; + } // Cannot return shares to the vault, if the underlying asset was frozen for // the submitter - if (auto const ret = checkFrozen(ctx.view, account, vaultShare)) - return ret; + if (!vaultAsset.holds() || + (dstAcct != vaultAsset.getIssuer() && + account != vaultAsset.getIssuer())) + { + if (auto const ret = checkFrozen(ctx.view, account, vaultShare)) + return ret; + } return tesSUCCESS; } @@ -115,6 +125,7 @@ VaultWithdraw::doApply() auto const amount = ctx_.tx[sfAmount]; Asset const vaultAsset = vault->at(sfAsset); + auto const dstAcct = ctx_.tx[~sfDestination].value_or(account_); MPTIssue const share{mptIssuanceID}; STAmount sharesRedeemed = {share}; STAmount assetsWithdrawn; @@ -165,11 +176,20 @@ VaultWithdraw::doApply() return tecPATH_DRY; } + // When withdrawing IOU to the issuer, ignore freeze since spec allows + // returning frozen IOU assets to their issuer (MPTs don't have this + // concept) + FreezeHandling const freezeHandling = (vaultAsset.holds() && + (dstAcct == vaultAsset.getIssuer() || + account_ == vaultAsset.getIssuer())) + ? FreezeHandling::fhIGNORE_FREEZE + : FreezeHandling::fhZERO_IF_FROZEN; + if (accountHolds( view(), account_, share, - FreezeHandling::fhZERO_IF_FROZEN, + freezeHandling, AuthHandling::ahIGNORE_AUTH, j_) < sharesRedeemed) { @@ -237,8 +257,6 @@ VaultWithdraw::doApply() // else quietly ignore, account balance is not zero } - auto const dstAcct = ctx_.tx[~sfDestination].value_or(account_); - return doWithdraw( view(), ctx_.tx, From 933447ce48a3896746cb77012339cc0d0aa2e574 Mon Sep 17 00:00:00 2001 From: Valentin Balaschenko <13349202+vlntb@users.noreply.github.com> Date: Fri, 5 Dec 2025 10:52:42 +0000 Subject: [PATCH 2/8] test case description update --- src/test/app/Vault_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 11b405e082f..1876616f681 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -5248,7 +5248,7 @@ class Vault_test : public beast::unit_test::suite { using namespace test::jtx; - testcase("frozen asset cannot withdraw to issuer (spec deviation)"); + testcase("frozen IOU can be withdrawn to issuer"); Env env{*this, testable_amendments() | featureSingleAssetVault}; Account issuer{"issuer"}; From a913a791aa38ba068ee395e41ff7493d2572b185 Mon Sep 17 00:00:00 2001 From: Valentin Balaschenko <13349202+vlntb@users.noreply.github.com> Date: Fri, 5 Dec 2025 12:54:45 +0000 Subject: [PATCH 3/8] clarifying comment --- src/xrpld/app/tx/detail/VaultWithdraw.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/xrpld/app/tx/detail/VaultWithdraw.cpp b/src/xrpld/app/tx/detail/VaultWithdraw.cpp index 000cd84e0d4..f1b1ff18b75 100644 --- a/src/xrpld/app/tx/detail/VaultWithdraw.cpp +++ b/src/xrpld/app/tx/detail/VaultWithdraw.cpp @@ -177,8 +177,9 @@ VaultWithdraw::doApply() } // When withdrawing IOU to the issuer, ignore freeze since spec allows - // returning frozen IOU assets to their issuer (MPTs don't have this - // concept) + // returning frozen IOU assets to their issuer. MPTs don't have this + // exemption - MPT locks function like "deep freeze" with no issuer + // exception. FreezeHandling const freezeHandling = (vaultAsset.holds() && (dstAcct == vaultAsset.getIssuer() || account_ == vaultAsset.getIssuer())) From 6d7e851809a4667e9553fc3b1d7e237d921c5d52 Mon Sep 17 00:00:00 2001 From: Valentin Balaschenko <13349202+vlntb@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:58:19 +0000 Subject: [PATCH 4/8] Global freeze + additional unit-test --- src/libxrpl/ledger/View.cpp | 2 +- src/test/app/Freeze_test.cpp | 53 ++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index 33c6cf69ece..3d43a1541e8 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -234,7 +234,7 @@ isFrozen( if (isXRP(currency)) return false; auto sle = view.read(keylet::account(issuer)); - if (sle && sle->isFlag(lsfGlobalFreeze)) + if (sle && sle->isFlag(lsfGlobalFreeze) && issuer != account) return true; if (issuer != account) { diff --git a/src/test/app/Freeze_test.cpp b/src/test/app/Freeze_test.cpp index 43d7c6d1e4e..e9a714de21f 100644 --- a/src/test/app/Freeze_test.cpp +++ b/src/test/app/Freeze_test.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -2002,6 +2003,57 @@ class Freeze_test : public beast::unit_test::suite } } + void + testIsFrozenDirectly(FeatureBitset features) + { + testcase("isFrozen function issuer exemption"); + + using namespace test::jtx; + Env env(*this, features); + + Account issuer{"issuer"}; + Account holder{"holder"}; + + env.fund(XRP(10000), issuer, holder); + env.close(); + + auto const USD = issuer["USD"]; + env.trust(USD(1000), holder); + env.close(); + + env(pay(issuer, holder, USD(100))); + env.close(); + + // Before global freeze, neither account is frozen + BEAST_EXPECT( + !isFrozen(*env.current(), issuer.id(), USD.currency, issuer.id())); + BEAST_EXPECT( + !isFrozen(*env.current(), holder.id(), USD.currency, issuer.id())); + + // Enable global freeze + env(fset(issuer, asfGlobalFreeze)); + env.close(); + + // After global freeze, issuer is NOT frozen for their own currency + BEAST_EXPECT( + !isFrozen(*env.current(), issuer.id(), USD.currency, issuer.id())); + + // After global freeze, holder IS frozen + BEAST_EXPECT( + isFrozen(*env.current(), holder.id(), USD.currency, issuer.id())); + + // Verify issuer can still receive payments (uses isFrozen internally) + env(pay(holder, issuer, USD(10))); + env.close(); + + // Verify holder cannot send to other accounts + Account other{"other"}; + env.fund(XRP(10000), other); + env.trust(USD(1000), other); + env.close(); + env(pay(holder, other, USD(10)), ter(tecPATH_DRY)); + } + // Helper function to extract trustline flags from open ledger uint32_t getTrustlineFlags( @@ -2065,6 +2117,7 @@ class Freeze_test : public beast::unit_test::suite testCreateFrozenTrustline(features); testSetAndClear(features); testGlobalFreeze(features); + testIsFrozenDirectly(features); testNoFreeze(features); testOffersWhenFrozen(features); testOffersWhenDeepFrozen(features); From 4e4e6264025fda546e34cbf77b94adbe8d6aeed7 Mon Sep 17 00:00:00 2001 From: Valentin Balaschenko <13349202+vlntb@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:50:40 +0000 Subject: [PATCH 5/8] fix LoanBrokerCoverDeposit --- src/test/app/LoanBroker_test.cpp | 58 +++++++++++++++++++ .../app/tx/detail/LoanBrokerCoverDeposit.cpp | 9 ++- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 597e2ea75b7..4e22f82d9fc 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -1436,6 +1436,63 @@ class LoanBroker_test : public beast::unit_test::suite }); } + void + testIssuerCoverDepositDuringGlobalFreeze() + { + testcase("Issuer can deposit to broker during global freeze"); + using namespace jtx; + using namespace loanBroker; + + Account const issuer{"issuer"}; + Account const holder{"holder"}; + Env env(*this); + Vault vault{env}; + + env.fund(XRP(100'000), issuer, holder); + env.close(); + + PrettyAsset const asset = issuer["IOU"]; + env.trust(asset(1'000'000), holder); + env.close(); + env(pay(issuer, holder, asset(100'000))); + env.close(); + + auto [tx, vaultKeylet] = + vault.create({.owner = issuer, .asset = asset}); + env(tx); + env.close(); + + env(vault.deposit( + {.depositor = holder, .id = vaultKeylet.key, .amount = asset(50)})); + env.close(); + + auto const brokerKeylet = + keylet::loanbroker(issuer.id(), env.seq(issuer)); + env(set(issuer, vaultKeylet.key)); + env.close(); + + auto broker = env.le(brokerKeylet); + if (!BEAST_EXPECT(broker)) + return; + + env(fset(issuer, asfGlobalFreeze)); + env.close(); + + // Issuer CAN deposit to their own broker during global freeze + // This is the issuer exemption - issuer can always send (issue) their + // own tokens Per spec: "Counterparties of the frozen issuer can still + // send and receive payments directly to and from the issuing address." + env(coverDeposit(issuer, brokerKeylet.key, asset(10))); + env.close(); + + // Verify the deposit succeeded + broker = env.le(brokerKeylet); + if (BEAST_EXPECT(broker)) + { + BEAST_EXPECT(broker->at(sfCoverAvailable) == asset(10).number()); + } + } + public: void run() override @@ -1450,6 +1507,7 @@ class LoanBroker_test : public beast::unit_test::suite testInvalidLoanBrokerDelete(); testInvalidLoanBrokerSet(); testRequireAuth(); + testIssuerCoverDepositDuringGlobalFreeze(); // TODO: Write clawback failure tests with an issuer / MPT that doesn't // have the right flags set. diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp index 4e9e0e9c054..52228d0c853 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverDeposit.cpp @@ -75,7 +75,14 @@ LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx) requireAuth(ctx.view, vaultAsset, account, AuthType::StrongAuth)) return ret; - if (accountHolds( + // IOU issuers have infinite issuance ability and don't have a "balance" + // of their own tokens (accountHolds returns 0 for them). Skip the balance + // check for issuers. Note: issuer freeze exemption is handled by the + // isFrozen() function. This exemption does not apply to MPTs. + bool const isIssuer = + vaultAsset.holds() && account == vaultAsset.getIssuer(); + if (!isIssuer && + accountHolds( ctx.view, account, vaultAsset, From cb744e76a404da8f447cb3b940ead8384ed4fc6b Mon Sep 17 00:00:00 2001 From: Valentin Balaschenko <13349202+vlntb@users.noreply.github.com> Date: Wed, 10 Dec 2025 16:58:30 +0000 Subject: [PATCH 6/8] GlobalFreezeIssuer amendment --- include/xrpl/protocol/detail/features.macro | 1 + src/libxrpl/ledger/View.cpp | 7 ++- src/test/app/Freeze_test.cpp | 68 +++++++++++++++++++++ 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 5c8d2aa1985..521e625a855 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -16,6 +16,7 @@ // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. +XRPL_FIX (GlobalFreezeIssuer, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(LendingProtocol, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(PermissionDelegationV1_1, Supported::no, VoteBehavior::DefaultNo) XRPL_FIX (DirectoryLimit, Supported::yes, VoteBehavior::DefaultNo) diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index 3d43a1541e8..3c97b104ac5 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -234,8 +234,11 @@ isFrozen( if (isXRP(currency)) return false; auto sle = view.read(keylet::account(issuer)); - if (sle && sle->isFlag(lsfGlobalFreeze) && issuer != account) - return true; + if (sle && sle->isFlag(lsfGlobalFreeze)) + { + if (!view.rules().enabled(fixGlobalFreezeIssuer) || issuer != account) + return true; + } if (issuer != account) { // Check if the issuer froze the line diff --git a/src/test/app/Freeze_test.cpp b/src/test/app/Freeze_test.cpp index e9a714de21f..b96cbfd73ab 100644 --- a/src/test/app/Freeze_test.cpp +++ b/src/test/app/Freeze_test.cpp @@ -2054,6 +2054,73 @@ class Freeze_test : public beast::unit_test::suite env(pay(holder, other, USD(10)), ter(tecPATH_DRY)); } + void + testGlobalFreezeIssuerAmendment(FeatureBitset features) + { + testcase("Global Freeze Issuer Amendment"); + + using namespace test::jtx; + + Account issuer{"issuer"}; + Account holder{"holder"}; + + // Test pre-amendment behavior (issuer IS frozen under global freeze) + { + Env env_pre(*this, features - fixGlobalFreezeIssuer); + + env_pre.fund(XRP(10000), issuer, holder); + env_pre.close(); + + auto const USD = issuer["USD"]; + env_pre.trust(USD(1000), holder); + env_pre.close(); + + env_pre(pay(issuer, holder, USD(100))); + env_pre.close(); + + env_pre(fset(issuer, asfGlobalFreeze)); + env_pre.close(); + + BEAST_EXPECT(isFrozen( + *env_pre.current(), issuer.id(), USD.currency, issuer.id())); + + BEAST_EXPECT(isFrozen( + *env_pre.current(), holder.id(), USD.currency, issuer.id())); + } + + // Test post-amendment behavior (issuer is NOT frozen under global + // freeze) + if (features[fixGlobalFreezeIssuer]) + { + Env env_post(*this, features); + + env_post.fund(XRP(10000), issuer, holder); + env_post.close(); + + auto const USD = issuer["USD"]; + env_post.trust(USD(1000), holder); + env_post.close(); + + env_post(pay(issuer, holder, USD(100))); + env_post.close(); + + env_post(fset(issuer, asfGlobalFreeze)); + env_post.close(); + + BEAST_EXPECT(!isFrozen( + *env_post.current(), issuer.id(), USD.currency, issuer.id())); + + BEAST_EXPECT(isFrozen( + *env_post.current(), holder.id(), USD.currency, issuer.id())); + + env_post(pay(issuer, holder, USD(10))); + env_post.close(); + + env_post(pay(holder, issuer, USD(10))); + env_post.close(); + } + } + // Helper function to extract trustline flags from open ledger uint32_t getTrustlineFlags( @@ -2118,6 +2185,7 @@ class Freeze_test : public beast::unit_test::suite testSetAndClear(features); testGlobalFreeze(features); testIsFrozenDirectly(features); + testGlobalFreezeIssuerAmendment(features); testNoFreeze(features); testOffersWhenFrozen(features); testOffersWhenDeepFrozen(features); From ee33367031104801363c9d31fedebe694ac61508 Mon Sep 17 00:00:00 2001 From: Valentin Balaschenko <13349202+vlntb@users.noreply.github.com> Date: Mon, 15 Dec 2025 15:54:22 +0000 Subject: [PATCH 7/8] amendment fix --- include/xrpl/protocol/detail/features.macro | 1 - src/libxrpl/ledger/View.cpp | 2 +- src/test/app/Freeze_test.cpp | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 521e625a855..5c8d2aa1985 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -16,7 +16,6 @@ // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. -XRPL_FIX (GlobalFreezeIssuer, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(LendingProtocol, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(PermissionDelegationV1_1, Supported::no, VoteBehavior::DefaultNo) XRPL_FIX (DirectoryLimit, Supported::yes, VoteBehavior::DefaultNo) diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index fa7b730a4cf..7d47e0b3780 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -236,7 +236,7 @@ isFrozen( auto sle = view.read(keylet::account(issuer)); if (sle && sle->isFlag(lsfGlobalFreeze)) { - if (!view.rules().enabled(fixGlobalFreezeIssuer) || issuer != account) + if (!view.rules().enabled(featureLendingProtocol) || issuer != account) return true; } if (issuer != account) diff --git a/src/test/app/Freeze_test.cpp b/src/test/app/Freeze_test.cpp index c59aaa08aa1..d17b17bf15c 100644 --- a/src/test/app/Freeze_test.cpp +++ b/src/test/app/Freeze_test.cpp @@ -2066,7 +2066,7 @@ class Freeze_test : public beast::unit_test::suite // Test pre-amendment behavior (issuer IS frozen under global freeze) { - Env env_pre(*this, features - fixGlobalFreezeIssuer); + Env env_pre(*this, features - featureLendingProtocol); env_pre.fund(XRP(10000), issuer, holder); env_pre.close(); @@ -2090,7 +2090,7 @@ class Freeze_test : public beast::unit_test::suite // Test post-amendment behavior (issuer is NOT frozen under global // freeze) - if (features[fixGlobalFreezeIssuer]) + if (features[featureLendingProtocol]) { Env env_post(*this, features); From 8523f426a6c65337f2450f985052f5838ab218f4 Mon Sep 17 00:00:00 2001 From: Valentin Balaschenko <13349202+vlntb@users.noreply.github.com> Date: Wed, 17 Dec 2025 14:05:57 +0000 Subject: [PATCH 8/8] minor test fixes --- src/test/app/LoanBroker_test.cpp | 163 ++++++++++++++++--------------- 1 file changed, 83 insertions(+), 80 deletions(-) diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 7a107a196f7..5ab082c65e2 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -1491,100 +1491,103 @@ class LoanBroker_test : public beast::unit_test::suite { BEAST_EXPECT(broker->at(sfCoverAvailable) == asset(10).number()); } - 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(); + void + testLoanBrokerSetDebtMaximum() + { + testcase("testLoanBrokerSetDebtMaximum"); + using namespace jtx; + using namespace loanBroker; + Account const issuer{"issuer"}; + Account const alice{"alice"}; + Env env(*this); + Vault vault{env}; - PrettyAsset const asset = [&]() { - env(trust(alice, issuer["IOU"](1'000'000)), THISLINE); - env.close(); - return PrettyAsset(issuer["IOU"]); - }(); + env.fund(XRP(100'000), issuer, alice); + env.close(); - env(pay(issuer, alice, asset(100'000)), THISLINE); + PrettyAsset const asset = [&]() { + env(trust(alice, issuer["IOU"](1'000'000)), THISLINE); env.close(); + return PrettyAsset(issuer["IOU"]); + }(); - auto [tx, vaultKeylet] = - vault.create({.owner = alice, .asset = asset}); - env(tx, THISLINE); - 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(pay(issuer, alice, asset(100'000)), THISLINE); + env.close(); - env(vault.deposit( - {.depositor = alice, - .id = vaultKeylet.key, - .amount = asset(50)}), - THISLINE); - env.close(); + auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = asset}); + env(tx, THISLINE); + 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; - auto const brokerKeylet = - keylet::loanbroker(alice.id(), env.seq(alice)); - env(set(alice, vaultInfo.vaultID), THISLINE); - env.close(); + env(vault.deposit( + {.depositor = alice, + .id = vaultKeylet.key, + .amount = asset(50)}), + THISLINE); + 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), - THISLINE); - auto const broker = env.le(brokerKeylet); - if (!BEAST_EXPECT(broker)) - return; + auto const brokerKeylet = + keylet::loanbroker(alice.id(), env.seq(alice)); + env(set(alice, vaultInfo.vaultID), THISLINE); + 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), + THISLINE); + auto const broker = env.le(brokerKeylet); + if (!BEAST_EXPECT(broker)) + return; - BEAST_EXPECT(broker->at(sfDebtTotal) == 50); - auto debtTotal = broker->at(sfDebtTotal); + 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), THISLINE); + auto tx2 = set(alice, vaultInfo.vaultID); + tx2[sfLoanBrokerID] = to_string(brokerKeylet.key); + tx2[sfDebtMaximum] = debtTotal - 1; + env(tx2, ter(tecLIMIT_EXCEEDED), THISLINE); - tx2[sfDebtMaximum] = debtTotal + 1; - env(tx2, ter(tesSUCCESS), THISLINE); + tx2[sfDebtMaximum] = debtTotal + 1; + env(tx2, ter(tesSUCCESS), THISLINE); - tx2[sfDebtMaximum] = 0; - env(tx2, ter(tesSUCCESS), THISLINE); - } + tx2[sfDebtMaximum] = 0; + env(tx2, ter(tesSUCCESS), THISLINE); + } - public: - void run() override - { - testLoanBrokerSetDebtMaximum(); - testLoanBrokerCoverDepositNullVault(); - - testDisabled(); - testLifecycle(); - testInvalidLoanBrokerCoverClawback(); - testInvalidLoanBrokerCoverDeposit(); - testInvalidLoanBrokerCoverWithdraw(); - testInvalidLoanBrokerDelete(); - testInvalidLoanBrokerSet(); - testRequireAuth(); - testIssuerCoverDepositDuringGlobalFreeze(); - - // TODO: Write clawback failure tests with an issuer / MPT that - // doesn't have the right flags set. - } - }; +public: + void + run() override + { + testLoanBrokerSetDebtMaximum(); + testLoanBrokerCoverDepositNullVault(); + + testDisabled(); + testLifecycle(); + testInvalidLoanBrokerCoverClawback(); + testInvalidLoanBrokerCoverDeposit(); + testInvalidLoanBrokerCoverWithdraw(); + testInvalidLoanBrokerDelete(); + testInvalidLoanBrokerSet(); + testRequireAuth(); + testIssuerCoverDepositDuringGlobalFreeze(); + + // TODO: Write clawback failure tests with an issuer / MPT that + // doesn't have the right flags set. + } +}; - BEAST_DEFINE_TESTSUITE(LoanBroker, tx, xrpl); +BEAST_DEFINE_TESTSUITE(LoanBroker, tx, xrpl); } // namespace test } // namespace xrpl