diff --git a/include/xrpl/ledger/helpers/LendingHelpers.h b/include/xrpl/ledger/helpers/LendingHelpers.h index b9711c4053..6196b90745 100644 --- a/include/xrpl/ledger/helpers/LendingHelpers.h +++ b/include/xrpl/ledger/helpers/LendingHelpers.h @@ -4,8 +4,38 @@ #include #include +#include namespace xrpl { +/** + * Broker cover preclaim precision guard (fixCleanup3_2_0). + * + * Prevents a "silent sub-ULP no-op" where a deposit, withdrawal, or clawback + * amount is so small that it rounds to zero at `sfCoverAvailable`'s scale. + * Without this guard, both the pseudo trust-line and `sfCoverAvailable` would + * identically absorb the rounded zero, resulting in a successful transaction + * (tesSUCCESS) where no funds actually moved. + * + * @param view Apply view (rules used for amendment gating). + * @param sleBroker The loan broker SLE (read-only). + * @param vaultAsset The underlying vault asset (the broker's cover asset). + * @param amount The effective subtraction/addition amount. + * @param j Journal for logging. + * @param logPrefix Transactor name for log diagnostics. + * + * @return `tecPRECISION_LOSS` if the request rounds to zero at cover scale. + * `tesSUCCESS` if the amendment is disabled, the amount is true zero, + * or the request is safely supra-ULP. + */ +[[nodiscard]] TER +canApplyToBrokerCover( + ReadView const& view, + SLE::const_ref sleBroker, + Asset const& vaultAsset, + STAmount const& amount, + beast::Journal j, + std::string_view logPrefix); + // Lending protocol has dependencies, so capture them here. bool checkLendingProtocolDependencies(Rules const& rules, STTx const& tx); diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index f05d44441d..01247dd34d 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -184,6 +184,19 @@ public: [[nodiscard]] STAmount const& value() const noexcept; + /** + * Checks if this amount evaluates to zero when constrained to a specific + * accounting scale. + * + * @param scale The target accounting scale to evaluate against. + * @return `true` if this amount rounds to zero at the given scale, + * `false` otherwise. + * + * @see roundToScale + */ + [[nodiscard]] bool + isZeroAtScale(int scale) const; + //-------------------------------------------------------------------------- // // Operators diff --git a/src/libxrpl/ledger/helpers/LendingHelpers.cpp b/src/libxrpl/ledger/helpers/LendingHelpers.cpp index 3bfa18e3e8..2e50c935b6 100644 --- a/src/libxrpl/ledger/helpers/LendingHelpers.cpp +++ b/src/libxrpl/ledger/helpers/LendingHelpers.cpp @@ -28,6 +28,31 @@ namespace xrpl { +[[nodiscard]] TER +canApplyToBrokerCover( + ReadView const& view, + std::shared_ptr const& sleBroker, + Asset const& vaultAsset, + STAmount const& amount, + beast::Journal j, + std::string_view logPrefix) +{ + if (!view.rules().enabled(fixCleanup3_2_0)) + return tesSUCCESS; + if (amount == beast::kZERO) + return tesSUCCESS; + + int const coverScale = scale(sleBroker->at(sfCoverAvailable), vaultAsset); + if (amount.isZeroAtScale(coverScale)) + { + JLOG(j.warn()) << logPrefix << ": amount " << amount.getFullText() + << " rounds to zero at cover scale " << coverScale; + return tecPRECISION_LOSS; + } + + return tesSUCCESS; +} + bool checkLendingProtocolDependencies(Rules const& rules, STTx const& tx) { diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 20d3db45c0..d7338ffbe4 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -1739,4 +1739,9 @@ divRoundStrict(STAmount const& num, STAmount const& den, Asset const& asset, boo return divRoundImpl(num, den, asset, roundUp); } +[[nodiscard]] bool +STAmount::isZeroAtScale(int scale) const +{ + return roundToScale(*this, scale).signum() == 0; +} } // namespace xrpl diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp index c59a02d4c2..95fa9dacf5 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp @@ -291,6 +291,10 @@ LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx) } STAmount const& clawAmount = *findClawAmount; + if (auto const ret = canApplyToBrokerCover( + ctx.view, sleBroker, vaultAsset, clawAmount, ctx.j, "LoanBrokerCoverClawback")) + return ret; + // Explicitly check the balance of the trust line / MPT to make sure the // balance is actually there. It should always match `sfCoverAvailable`, so // if there isn't, this is an internal error. diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp index a35878f5c6..7e93675e22 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp @@ -73,6 +73,11 @@ LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx) if (amount.asset() != vaultAsset) return tecWRONG_ASSET; + // helper handles both IOU and MPT correctly without explicit branching. + if (auto const ret = canApplyToBrokerCover( + ctx.view, sleBroker, vaultAsset, amount, ctx.j, "LoanBrokerCoverDeposit")) + return ret; + auto const pseudoAccountID = sleBroker->at(sfAccount); // Cannot transfer a non-transferable Asset if (auto const ret = canTransfer(ctx.view, vaultAsset, account, pseudoAccountID)) diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp index 127d23a270..059e865ebf 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp @@ -93,6 +93,11 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) if (amount.asset() != vaultAsset) return tecWRONG_ASSET; + // helper handles both IOU and MPT correctly without explicit branching. + if (auto const ret = canApplyToBrokerCover( + ctx.view, sleBroker, vaultAsset, amount, ctx.j, "LoanBrokerCoverWithdraw")) + return ret; + // The broker's pseudo-account is the source of funds. auto const pseudoAccountID = sleBroker->at(sfAccount); // Cannot transfer a non-transferable Asset