mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-05 01:37:00 +00:00
fix: Reject sub-ULP cover amounts with tecPRECISION_LOSS (fixCleanup3_2_0)
Add STAmount::isZeroAtScale() and canApplyToBrokerCover() to detect amounts that round to zero at sfCoverAvailable's precision scale, then call the guard in LoanBrokerCoverDeposit, LoanBrokerCoverWithdraw, and LoanBrokerCoverClawback preclaim. Without the guard a sub-ULP deposit, withdrawal, or clawback would silently succeed while moving no funds.
This commit is contained in:
@@ -4,8 +4,38 @@
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/st.h>
|
||||
|
||||
#include <string_view>
|
||||
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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -28,6 +28,31 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
[[nodiscard]] TER
|
||||
canApplyToBrokerCover(
|
||||
ReadView const& view,
|
||||
std::shared_ptr<SLE const> 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)
|
||||
{
|
||||
|
||||
@@ -1739,4 +1739,9 @@ divRoundStrict(STAmount const& num, STAmount const& den, Asset const& asset, boo
|
||||
return divRoundImpl<NumberRoundModeGuard>(num, den, asset, roundUp);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
STAmount::isZeroAtScale(int scale) const
|
||||
{
|
||||
return roundToScale(*this, scale).signum() == 0;
|
||||
}
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user