diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index aacb356439..b8f7cb52be 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -188,9 +188,14 @@ public: * Checks if this amount evaluates to zero when constrained to a specific * accounting scale. * + * For XRP and MPT `roundToScale` is a no-op, returns true only when the amount itself is zero. + * The `scale` argument is ignored in that case. + * For IOU, the amount is rounded to the given scale (using the current rounding mode) + * and the result is checked for zero; if `scale <= exponent()`, `roundToScale` short-circuits + * and returns the value unchanged, so this returns false for any non-zero amount. + * * @param scale The target accounting scale to evaluate against. - * @return `true` if this amount rounds to zero at the given scale, - * `false` otherwise. + * @return `true` if this amount rounds to zero at the given scale, `false` otherwise. * * @see roundToScale */ diff --git a/src/libxrpl/ledger/helpers/LendingHelpers.cpp b/src/libxrpl/ledger/helpers/LendingHelpers.cpp index 8048092b88..aa5b3cd539 100644 --- a/src/libxrpl/ledger/helpers/LendingHelpers.cpp +++ b/src/libxrpl/ledger/helpers/LendingHelpers.cpp @@ -43,7 +43,7 @@ canApplyToBrokerCover( sleBroker && sleBroker->getType() == ltLOAN_BROKER, "xrpl::canApplyToBrokerCover : valid LoanBroker sle"); XRPL_ASSERT( - vaultAsset.getIssuer() == amount.getIssuer() && amount > beast::kZero, + vaultAsset == amount.asset() && amount > beast::kZero, "xrpl::canApplyToBrokerCover : valid amount for asset"); if (!view.rules().enabled(fixCleanup3_2_0)) diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp index 6a3cea0725..d39e08ad9c 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index cd9a67a0eb..db6e5d51cb 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -1876,7 +1876,7 @@ class LoanBroker_test : public beast::unit_test::Suite features[fixCleanup3_2_0] ? TER{tecPRECISION_LOSS} : TER{tesSUCCESS}; { - testcase("Cover precision guard: Deposit"); + testcase("Cover precision guard: Deposit zero-at-scale"); Env env{*this, features}; auto const [brokerKeylet, iou] = setup(env); PrettyAmount const subUlpAmt = iou(Number{1, -16}); @@ -1891,7 +1891,7 @@ class LoanBroker_test : public beast::unit_test::Suite } { - testcase("Cover precision guard: Deposit"); + testcase("Cover precision guard: Deposit rounds down"); // Both cases succeed; post-fix the amount is rounded DOWN to // cover scale first, so the delta differs from pre-fix // Input: 1.8e-14 IOU (sub-scale at cover scale -14) diff --git a/src/test/protocol/STAmount_test.cpp b/src/test/protocol/STAmount_test.cpp index 8efc2d8794..5d4b68adde 100644 --- a/src/test/protocol/STAmount_test.cpp +++ b/src/test/protocol/STAmount_test.cpp @@ -1212,10 +1212,10 @@ public: Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)}; - // IOU: 10 IOU — mantissa = kMIN_VALUE (10^15), exponent = -14. + // IOU: 10 IOU — mantissa = kMinValue (10^15), exponent = -14. // One ULP at this scale is 10^-14; half-ULP is 5*10^-15. { - STAmount const ref{usd, STAmount::kMIN_VALUE, -14}; + STAmount const ref{usd, STAmount::kMinValue, -14}; int const refScale = ref.exponent(); // -14 BEAST_EXPECT(refScale == -14); @@ -1223,14 +1223,14 @@ public: STAmount const iouZero{usd, 0}; BEAST_EXPECT(iouZero.isZeroAtScale(refScale)); - // Sub-ULP: 1e-16 IOU (mantissa = kMIN_VALUE, exponent = -31). + // Sub-ULP: 1e-16 IOU (mantissa = kMinValue, exponent = -31). // Far below half-ULP → rounds to zero. - STAmount const subUlp{usd, STAmount::kMIN_VALUE, -31}; + STAmount const subUlp{usd, STAmount::kMinValue, -31}; BEAST_EXPECT(subUlp.isZeroAtScale(refScale)); - // One ULP: 1e-14 IOU (mantissa = kMIN_VALUE, exponent = -29). + // One ULP: 1e-14 IOU (mantissa = kMinValue, exponent = -29). // Exactly the smallest representable unit at refScale → not zero. - STAmount const oneUlp{usd, STAmount::kMIN_VALUE, -29}; + STAmount const oneUlp{usd, STAmount::kMinValue, -29}; BEAST_EXPECT(!oneUlp.isZeroAtScale(refScale)); // The reference value itself: exponent == scale → returned @@ -1238,13 +1238,41 @@ public: BEAST_EXPECT(!ref.isZeroAtScale(refScale)); // A much larger value: certainly not zero at this scale. - STAmount const large{usd, STAmount::kMIN_VALUE, 0}; // 1e15 IOU + STAmount const large{usd, STAmount::kMinValue, 0}; // 1e15 IOU BEAST_EXPECT(!large.isZeroAtScale(refScale)); // When scale equals the value's own exponent, roundToScale // short-circuits and returns the value unchanged. BEAST_EXPECT(!subUlp.isZeroAtScale(subUlp.exponent())); BEAST_EXPECT(!oneUlp.isZeroAtScale(oneUlp.exponent())); + + // Half-ULP boundary. roundToScale forms (value + ref) - ref + // where ref = 10 IOU has mantissa 1e15 (LSB 0, even). + // Number's default rounding is to-nearest-even, so an exact + // half-ULP tie rounds toward the even-LSB neighbour — the + // reference itself — and the round-trip result is zero. + // Just below half-ULP rounds the same way; just above + // clears half-ULP and bumps the mantissa to 1e15 + 1. + STAmount const justBelowHalf{usd, STAmount::kMinValue * 4, -30}; + BEAST_EXPECT(justBelowHalf.isZeroAtScale(refScale)); + + STAmount const halfUlp{usd, STAmount::kMinValue * 5, -30}; + BEAST_EXPECT(halfUlp.isZeroAtScale(refScale)); + + STAmount const justAboveHalf{usd, STAmount::kMinValue * 6, -30}; + BEAST_EXPECT(!justAboveHalf.isZeroAtScale(refScale)); + + // Large magnitude gap: dust value far below an enormous scale. + // 1e-80 with scale +15 — the value vanishes utterly. + STAmount const dust{usd, STAmount::kMinValue, -95}; + BEAST_EXPECT(dust.isZeroAtScale(15)); + + // Negative values mirror positive behaviour. + STAmount const negSubUlp{usd, STAmount::kMinValue, -31, true}; + BEAST_EXPECT(negSubUlp.isZeroAtScale(refScale)); + + STAmount const negOneUlp{usd, STAmount::kMinValue, -29, true}; + BEAST_EXPECT(!negOneUlp.isZeroAtScale(refScale)); } // XRP is integral — roundToScale short-circuits, value is preserved.