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] 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 597e2ea75b..4e22f82d9f 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 @@ public: 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 4e9e0e9c05..52228d0c85 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,