From f78c5f65bc3c31e49d80870299d842f772c1838d Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 23 Oct 2025 01:46:43 -0400 Subject: [PATCH] Allow issuers to send LoanPay - Implement a new helper accountCanSend, which is like accountHolds, but returns a meaningful value for issuers, and will include the available credit on the other side of a trust line. (The sfHighLimit or sfLowLimit as appropriate.) - Use this new helper when checking the available balance in LoanPay. --- include/xrpl/ledger/View.h | 43 +++++ src/libxrpl/ledger/View.cpp | 250 +++++++++++++++++++++------- src/xrpld/app/tx/detail/LoanPay.cpp | 21 +-- 3 files changed, 238 insertions(+), 76 deletions(-) diff --git a/include/xrpl/ledger/View.h b/include/xrpl/ledger/View.h index 56c762a5a4..4343c29b59 100644 --- a/include/xrpl/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -361,6 +361,49 @@ accountHolds( AuthHandling zeroIfUnauthorized, beast::Journal j); +// Returns the amount an account can spend total. +// +// These functions use accountHolds, but unlike accountHolds: +// * The account can go into debt. +// * If the account is the asset issuer the only limit is defined by the asset / +// issuance. +// +// <-- saAmount: amount of currency held by account. May be negative. +[[nodiscard]] STAmount +accountCanSend( + ReadView const& view, + AccountID const& account, + Currency const& currency, + AccountID const& issuer, + FreezeHandling zeroIfFrozen, + beast::Journal j); + +[[nodiscard]] STAmount +accountCanSend( + ReadView const& view, + AccountID const& account, + Issue const& issue, + FreezeHandling zeroIfFrozen, + beast::Journal j); + +[[nodiscard]] STAmount +accountCanSend( + ReadView const& view, + AccountID const& account, + MPTIssue const& mptIssue, + FreezeHandling zeroIfFrozen, + AuthHandling zeroIfUnauthorized, + beast::Journal j); + +[[nodiscard]] STAmount +accountCanSend( + ReadView const& view, + AccountID const& account, + Asset const& asset, + FreezeHandling zeroIfFrozen, + AuthHandling zeroIfUnauthorized, + beast::Journal j); + // Returns the amount an account can spend of the currency type saDefault, or // returns saDefault if this account is the issuer of the currency in // question. Should be used in favor of accountHolds when questioning how much diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index ea0d4b54fd..eeb704bc3b 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -383,6 +383,99 @@ isLPTokenFrozen( isFrozen(view, account, asset2.currency, asset2.account); } +static SLE::const_pointer +getLineIfUsable( + ReadView const& view, + AccountID const& account, + Currency const& currency, + AccountID const& issuer, + FreezeHandling zeroIfFrozen, + beast::Journal j) +{ + auto const sle = view.read(keylet::line(account, issuer, currency)); + + if (!sle) + { + return nullptr; + } + + if (zeroIfFrozen == fhZERO_IF_FROZEN) + { + if (isFrozen(view, account, currency, issuer) || + isDeepFrozen(view, account, currency, issuer)) + { + return nullptr; + } + + // when fixFrozenLPTokenTransfer is enabled, if currency is lptoken, + // we need to check if the associated assets have been frozen + if (view.rules().enabled(fixFrozenLPTokenTransfer)) + { + auto const sleIssuer = view.read(keylet::account(issuer)); + if (!sleIssuer) + { + return nullptr; // LCOV_EXCL_LINE + } + else if (sleIssuer->isFieldPresent(sfAMMID)) + { + auto const sleAmm = + view.read(keylet::amm((*sleIssuer)[sfAMMID])); + + if (!sleAmm || + isLPTokenFrozen( + view, + account, + (*sleAmm)[sfAsset].get(), + (*sleAmm)[sfAsset2].get())) + { + return nullptr; + } + } + } + } + + return sle; +} + +static STAmount +getTrustLineBalance( + ReadView const& view, + SLE::const_ref sle, + AccountID const& account, + Currency const& currency, + AccountID const& issuer, + bool includeOppositeLimit, + beast::Journal j) +{ + STAmount amount; + if (sle) + { + amount = sle->getFieldAmount(sfBalance); + bool const accountHigh = account > issuer; + auto const& oppositeField = accountHigh ? sfLowLimit : sfHighLimit; + if (accountHigh) + { + // Put balance in account terms. + amount.negate(); + } + if (includeOppositeLimit) + { + amount += sle->getFieldAmount(oppositeField); + } + amount.setIssuer(issuer); + } + else + { + amount.clear(Issue{currency, issuer}); + } + + JLOG(j.trace()) << "getTrustLineBalance:" + << " account=" << to_string(account) + << " amount=" << amount.getFullText(); + + return view.balanceHook(account, issuer, amount); +} + STAmount accountHolds( ReadView const& view, @@ -399,71 +492,10 @@ accountHolds( } // IOU: Return balance on trust line modulo freeze - auto const sle = view.read(keylet::line(account, issuer, currency)); - auto const allowBalance = [&]() { - if (!sle) - { - return false; - } + SLE::const_pointer const sle = + getLineIfUsable(view, account, currency, issuer, zeroIfFrozen, j); - if (zeroIfFrozen == fhZERO_IF_FROZEN) - { - if (isFrozen(view, account, currency, issuer) || - isDeepFrozen(view, account, currency, issuer)) - { - return false; - } - - // when fixFrozenLPTokenTransfer is enabled, if currency is lptoken, - // we need to check if the associated assets have been frozen - if (view.rules().enabled(fixFrozenLPTokenTransfer)) - { - auto const sleIssuer = view.read(keylet::account(issuer)); - if (!sleIssuer) - { - return false; // LCOV_EXCL_LINE - } - else if (sleIssuer->isFieldPresent(sfAMMID)) - { - auto const sleAmm = - view.read(keylet::amm((*sleIssuer)[sfAMMID])); - - if (!sleAmm || - isLPTokenFrozen( - view, - account, - (*sleAmm)[sfAsset].get(), - (*sleAmm)[sfAsset2].get())) - { - return false; - } - } - } - } - - return true; - }(); - - if (allowBalance) - { - amount = sle->getFieldAmount(sfBalance); - if (account > issuer) - { - // Put balance in account terms. - amount.negate(); - } - amount.setIssuer(issuer); - } - else - { - amount.clear(Issue{currency, issuer}); - } - - JLOG(j.trace()) << "accountHolds:" - << " account=" << to_string(account) - << " amount=" << amount.getFullText(); - - return view.balanceHook(account, issuer, amount); + return getTrustLineBalance(view, sle, account, currency, issuer, false, j); } STAmount @@ -550,6 +582,96 @@ accountHolds( asset.value()); } +STAmount +accountCanSend( + ReadView const& view, + AccountID const& account, + Currency const& currency, + AccountID const& issuer, + FreezeHandling zeroIfFrozen, + beast::Journal j) +{ + if (isXRP(currency)) + return accountHolds(view, account, currency, issuer, zeroIfFrozen, j); + + if (account == issuer) + // If the account is the issuer, then their limit is effectively + // infinite + return STAmount{ + Issue{currency, issuer}, STAmount::cMaxValue, STAmount::cMaxOffset}; + + // IOU: Return balance on trust line modulo freeze + SLE::const_pointer const sle = + getLineIfUsable(view, account, currency, issuer, zeroIfFrozen, j); + + return getTrustLineBalance(view, sle, account, currency, issuer, true, j); +} + +STAmount +accountCanSend( + ReadView const& view, + AccountID const& account, + Issue const& issue, + FreezeHandling zeroIfFrozen, + beast::Journal j) +{ + return accountCanSend( + view, account, issue.currency, issue.account, zeroIfFrozen, j); +} + +STAmount +accountCanSend( + ReadView const& view, + AccountID const& account, + MPTIssue const& mptIssue, + FreezeHandling zeroIfFrozen, + AuthHandling zeroIfUnauthorized, + beast::Journal j) +{ + if (account == mptIssue.getIssuer()) + { + // if the account is the issuer, and the issuance exists, their limit is + // the issuance limit minus the outstanding value + auto const issuance = + view.read(keylet::mptIssuance(mptIssue.getMptID())); + + if (!issuance) + { + return STAmount{mptIssue}; + } + return STAmount{ + mptIssue, + issuance->at(~sfMaximumAmount).value_or(maxMPTokenAmount) - + issuance->at(sfOutstandingAmount)}; + } + + return accountHolds( + view, account, mptIssue, zeroIfFrozen, zeroIfUnauthorized, j); +} + +[[nodiscard]] STAmount +accountCanSend( + ReadView const& view, + AccountID const& account, + Asset const& asset, + FreezeHandling zeroIfFrozen, + AuthHandling zeroIfUnauthorized, + beast::Journal j) +{ + return std::visit( + [&](auto const& value) { + if constexpr (std::is_same_v< + std::remove_cvref_t, + Issue>) + { + return accountCanSend(view, account, value, zeroIfFrozen, j); + } + return accountCanSend( + view, account, value, zeroIfFrozen, zeroIfUnauthorized, j); + }, + asset.value()); +} + STAmount accountFunds( ReadView const& view, diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index f87346f299..a5df496fcf 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -201,10 +201,7 @@ LoanPay::preclaim(PreclaimContext const& ctx) // Do not support "partial payments" - if the transaction says to pay X, // then the account must have X available, even if the loan payment takes // less. - // Also assume that anybody taking loans is not using "community credit", - // which would let an IOU balance go negative up to the other side's limit. - // This may change in a later version. - if (auto const balance = accountHolds( + if (auto const balance = accountCanSend( ctx.view, account, asset, @@ -427,11 +424,11 @@ LoanPay::doApply() } #if !NDEBUG - auto const accountBalanceBefore = - accountHolds(view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); + auto const accountBalanceBefore = accountCanSend( + view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); auto const vaultBalanceBefore = account_ == vaultPseudoAccount ? STAmount{asset, 0} - : accountHolds( + : accountCanSend( view, vaultPseudoAccount, asset, @@ -440,7 +437,7 @@ LoanPay::doApply() j_); auto const brokerBalanceBefore = account_ == brokerPayee ? STAmount{asset, 0} - : accountHolds( + : accountCanSend( view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); #endif @@ -483,11 +480,11 @@ LoanPay::doApply() return ter; #if !NDEBUG - auto const accountBalanceAfter = - accountHolds(view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); + auto const accountBalanceAfter = accountCanSend( + view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); auto const vaultBalanceAfter = account_ == vaultPseudoAccount ? STAmount{asset, 0} - : accountHolds( + : accountCanSend( view, vaultPseudoAccount, asset, @@ -496,7 +493,7 @@ LoanPay::doApply() j_); auto const brokerBalanceAfter = account_ == brokerPayee ? STAmount{asset, 0} - : accountHolds( + : accountCanSend( view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); XRPL_ASSERT_PARTS(