From 4fe50c2d3132141fdbac15d30af9ce466636a6ee Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Wed, 14 Jan 2026 20:58:04 +0100 Subject: [PATCH 1/9] attempt to fix rounding issues --- src/xrpld/app/tx/detail/InvariantCheck.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 2e0b3cbfab1..4d1a2512237 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -3382,7 +3382,10 @@ ValidVault::finalize( result = false; } - if (*vaultDeltaAssets * -1 != destinationDelta) + if (!withinRelativeDistance( + *vaultDeltaAssets * -1, + destinationDelta, + Number{1, -11})) { JLOG(j.fatal()) << // "Invariant failed: withdrawal must change vault " @@ -3425,8 +3428,10 @@ ValidVault::finalize( } // Note, vaultBalance is negative (see check above) - if (beforeVault.assetsTotal + *vaultDeltaAssets != - afterVault.assetsTotal) + if (!withinRelativeDistance( + beforeVault.assetsTotal + *vaultDeltaAssets, + afterVault.assetsTotal, + Number{1, -11})) { JLOG(j.fatal()) << "Invariant failed: withdrawal and " "assets outstanding must add up"; From 6223ebe05e1e51f907b6fb9b0e97427de7060d6b Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Thu, 15 Jan 2026 16:09:13 +0100 Subject: [PATCH 2/9] improves VaultWithdraw invariant rounding --- src/xrpld/app/tx/detail/InvariantCheck.cpp | 91 ++++++++++++++++------ 1 file changed, 68 insertions(+), 23 deletions(-) diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 4d1a2512237..2917f0c98cc 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -3068,6 +3068,26 @@ ValidVault::finalize( : std::nullopt; }; + // Helper to get the minimal scale of an STAmount by removing trailing + // zeros from its mantissa + auto const getNatScale = [](Asset const& asset, + Number const& value) -> std::int32_t { + if (value == beast::zero || asset.integral()) + return 0; + + auto mantissa = std::abs(value.mantissa()); + auto scale = value.exponent(); + + // Remove trailing zeros from mantissa, adjusting scale accordingly + while (mantissa % 10 == 0) + { + mantissa /= 10; + ++scale; + } + + return scale; + }; + auto const vaultHoldsNoAssets = [&](Vault const& vault) { return vault.assetsAvailable == 0 && vault.assetsTotal == 0; }; @@ -3324,22 +3344,37 @@ ValidVault::finalize( "vault"); auto const& beforeVault = beforeVault_[0]; - auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId); + auto const maybeVaultDeltaAssets = + deltaAssets(afterVault.pseudoId); - if (!vaultDeltaAssets) + if (!maybeVaultDeltaAssets) { JLOG(j.fatal()) << "Invariant failed: withdrawal must " "change vault balance"; return false; // That's all we can do } - if (*vaultDeltaAssets >= zero) + // Get the most coarse scale to round calculations to + auto const minScale = // + std::max( + {getNatScale(vaultAsset, *maybeVaultDeltaAssets), + getNatScale( + vaultAsset, + afterVault.assetsTotal - beforeVault.assetsTotal), + getNatScale( + vaultAsset, + afterVault.assetsAvailable - + beforeVault.assetsAvailable)}); + + auto const vaultPseudoDeltaAssets = + roundToAsset(vaultAsset, *maybeVaultDeltaAssets, minScale); + + if (vaultPseudoDeltaAssets >= zero) { JLOG(j.fatal()) << "Invariant failed: withdrawal must " "decrease vault balance"; result = false; } - // Any payments (including withdrawal) going to the issuer // do not change their balance, but destroy funds instead. bool const issuerWithdrawal = [&]() -> bool { @@ -3352,8 +3387,8 @@ ValidVault::finalize( if (!issuerWithdrawal) { - auto const accountDeltaAssets = deltaAssetsTxAccount(); - auto const otherAccountDelta = + auto const maybeAccDelta = deltaAssetsTxAccount(); + auto const maybeOtherAccDelta = [&]() -> std::optional { if (auto const destination = tx[~sfDestination]; destination && *destination != tx[sfAccount]) @@ -3361,8 +3396,8 @@ ValidVault::finalize( return std::nullopt; }(); - if (accountDeltaAssets.has_value() == - otherAccountDelta.has_value()) + if (maybeAccDelta.has_value() == + maybeOtherAccDelta.has_value()) { JLOG(j.fatal()) << // "Invariant failed: withdrawal must change one " @@ -3371,10 +3406,16 @@ ValidVault::finalize( } auto const destinationDelta = // - accountDeltaAssets ? *accountDeltaAssets - : *otherAccountDelta; + maybeAccDelta ? *maybeAccDelta : *maybeOtherAccDelta; - if (destinationDelta <= zero) + // the scale of destinationDelta can be more corse, so we take the max + // with minScale + auto const localMinScale = std::max( + minScale, getNatScale(vaultAsset, destinationDelta)); + auto const roundedDestinationDelta = roundToAsset( + vaultAsset, destinationDelta, localMinScale); + + if (roundedDestinationDelta <= zero) { JLOG(j.fatal()) << // "Invariant failed: withdrawal must increase " @@ -3382,10 +3423,9 @@ ValidVault::finalize( result = false; } - if (!withinRelativeDistance( - *vaultDeltaAssets * -1, - destinationDelta, - Number{1, -11})) + auto const localPseudoDeltaAssets = + roundToAsset(vaultAsset, vaultPseudoDeltaAssets, localMinScale); + if (localPseudoDeltaAssets * -1 != roundedDestinationDelta) { JLOG(j.fatal()) << // "Invariant failed: withdrawal must change vault " @@ -3393,7 +3433,7 @@ ValidVault::finalize( result = false; } } - + // We don't need to round here, as shares are always integral auto const accountDeltaShares = deltaShares(tx[sfAccount]); if (!accountDeltaShares) { @@ -3410,7 +3450,7 @@ ValidVault::finalize( "shares"; result = false; } - + // We don't need to round here, as shares are always integral auto const vaultDeltaShares = deltaShares(afterVault.pseudoId); if (!vaultDeltaShares || *vaultDeltaShares == zero) { @@ -3427,19 +3467,24 @@ ValidVault::finalize( result = false; } + auto const assetTotalDelta = roundToAsset( + vaultAsset, + beforeVault.assetsTotal - afterVault.assetsTotal, + minScale); // Note, vaultBalance is negative (see check above) - if (!withinRelativeDistance( - beforeVault.assetsTotal + *vaultDeltaAssets, - afterVault.assetsTotal, - Number{1, -11})) + if (assetTotalDelta != vaultPseudoDeltaAssets * -1) { JLOG(j.fatal()) << "Invariant failed: withdrawal and " "assets outstanding must add up"; result = false; } - if (beforeVault.assetsAvailable + *vaultDeltaAssets != - afterVault.assetsAvailable) + auto const assetAvailableDelta = roundToAsset( + vaultAsset, + beforeVault.assetsAvailable - afterVault.assetsAvailable, + minScale); + + if (assetAvailableDelta != vaultPseudoDeltaAssets * -1) { JLOG(j.fatal()) << "Invariant failed: withdrawal and " "assets available must add up"; From add9071b2069fc2f7f0f576e378a031324156c42 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Fri, 16 Jan 2026 11:25:53 +0100 Subject: [PATCH 3/9] fixes formatting --- src/xrpld/app/tx/detail/InvariantCheck.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 2917f0c98cc..15b3737df2e 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -3408,8 +3408,8 @@ ValidVault::finalize( auto const destinationDelta = // maybeAccDelta ? *maybeAccDelta : *maybeOtherAccDelta; - // the scale of destinationDelta can be more corse, so we take the max - // with minScale + // the scale of destinationDelta can be more corse, so we + // take the max with minScale auto const localMinScale = std::max( minScale, getNatScale(vaultAsset, destinationDelta)); auto const roundedDestinationDelta = roundToAsset( @@ -3423,8 +3423,8 @@ ValidVault::finalize( result = false; } - auto const localPseudoDeltaAssets = - roundToAsset(vaultAsset, vaultPseudoDeltaAssets, localMinScale); + auto const localPseudoDeltaAssets = roundToAsset( + vaultAsset, vaultPseudoDeltaAssets, localMinScale); if (localPseudoDeltaAssets * -1 != roundedDestinationDelta) { JLOG(j.fatal()) << // From 9235ec483a1adf86fa7fcc7630482a7bb6a82df9 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Tue, 20 Jan 2026 13:39:29 +0100 Subject: [PATCH 4/9] adds missing incldues --- src/test/app/Invariants_test.cpp | 53 ++++++ src/xrpld/app/tx/detail/InvariantCheck.cpp | 208 ++++++++++++++------- src/xrpld/app/tx/detail/InvariantCheck.h | 6 + 3 files changed, 195 insertions(+), 72 deletions(-) diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index 9f70538778d..8051ab1cbe9 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -20,6 +20,11 @@ #include +#include "xrpld/app/tx/detail/InvariantCheck.h" + +#include +#include + namespace xrpl { namespace test { @@ -3888,6 +3893,53 @@ class Invariants_test : public beast::unit_test::suite precloseMpt); } + void + testVaultComputeMinScale() + { + using namespace jtx; + + Account const issuer{"issuer"}; + PrettyAsset const vaultAsset = issuer["IOU"]; + + struct TestCase + { + std::string name; + std::int32_t expectedMinScale; + std::initializer_list values; + }; + + auto const testCases = std::vector{ + { + .name = "No values", + .expectedMinScale = 0, + .values = {}, + }, + { + .name = "Mixed integer and Number values", + .expectedMinScale = 0, + .values = {1, -1, Number{10, -1}}, + }, + { + .name = "Mixed scales", + .expectedMinScale = -2, + .values = {Number{1, -2}, Number{5, -3}, Number{3, -2}}, + }, + }; + + for (auto const& tc : testCases) + { + testcase("vault computeMinScale: " + tc.name); + + auto const actualScale = + ValidVault::computeMinScale(vaultAsset, tc.values); + + BEAST_EXPECTS( + actualScale == tc.expectedMinScale, + "expected: " + std::to_string(tc.expectedMinScale) + + ", actual: " + std::to_string(actualScale)); + } + } + public: void run() override @@ -3911,6 +3963,7 @@ class Invariants_test : public beast::unit_test::suite testValidPseudoAccounts(); testValidLoanBroker(); testVault(); + testVaultComputeMinScale(); } }; diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 15b3737df2e..5e4e1d6baeb 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -22,7 +22,11 @@ #include #include +#include +#include #include +#include +#include #include namespace xrpl { @@ -3068,26 +3072,6 @@ ValidVault::finalize( : std::nullopt; }; - // Helper to get the minimal scale of an STAmount by removing trailing - // zeros from its mantissa - auto const getNatScale = [](Asset const& asset, - Number const& value) -> std::int32_t { - if (value == beast::zero || asset.integral()) - return 0; - - auto mantissa = std::abs(value.mantissa()); - auto scale = value.exponent(); - - // Remove trailing zeros from mantissa, adjusting scale accordingly - while (mantissa % 10 == 0) - { - mantissa /= 10; - ++scale; - } - - return scale; - }; - auto const vaultHoldsNoAssets = [&](Vault const& vault) { return vault.assetsAvailable == 0 && vault.assetsTotal == 0; }; @@ -3216,16 +3200,29 @@ ValidVault::finalize( "xrpl::ValidVault::finalize : deposit updated a vault"); auto const& beforeVault = beforeVault_[0]; - auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId); - - if (!vaultDeltaAssets) + auto const maybeVaultDeltaAssets = + deltaAssets(afterVault.pseudoId); + if (!maybeVaultDeltaAssets) { JLOG(j.fatal()) << // "Invariant failed: deposit must change vault balance"; return false; // That's all we can do } - if (*vaultDeltaAssets > tx[sfAmount]) + // Get the coarsest scale to round calculations to + auto const minScale = computeMinScale( + vaultAsset, + { + *maybeVaultDeltaAssets, + afterVault.assetsTotal - beforeVault.assetsTotal, + afterVault.assetsAvailable - + beforeVault.assetsAvailable, + }); + + auto const vaultDeltaAssets = + roundToAsset(vaultAsset, *maybeVaultDeltaAssets, minScale); + + if (vaultDeltaAssets > tx[sfAmount]) { JLOG(j.fatal()) << // "Invariant failed: deposit must not change vault " @@ -3233,7 +3230,7 @@ ValidVault::finalize( result = false; } - if (*vaultDeltaAssets <= zero) + if (vaultDeltaAssets <= zero) { JLOG(j.fatal()) << // "Invariant failed: deposit must increase vault balance"; @@ -3250,16 +3247,23 @@ ValidVault::finalize( if (!issuerDeposit) { - auto const accountDeltaAssets = deltaAssetsTxAccount(); - if (!accountDeltaAssets) + auto const maybeAccDeltaAssets = deltaAssetsTxAccount(); + if (!maybeAccDeltaAssets) { JLOG(j.fatal()) << // "Invariant failed: deposit must change depositor " "balance"; return false; } + auto const localMinScale = computeMinScale( + vaultAsset, {minScale, *maybeAccDeltaAssets}); + + auto const accountDeltaAssets = roundToAsset( + vaultAsset, *maybeAccDeltaAssets, localMinScale); + auto const localVaultDeltaAssets = roundToAsset( + vaultAsset, vaultDeltaAssets, localMinScale); - if (*accountDeltaAssets >= zero) + if (accountDeltaAssets >= zero) { JLOG(j.fatal()) << // "Invariant failed: deposit must decrease depositor " @@ -3267,7 +3271,7 @@ ValidVault::finalize( result = false; } - if (*accountDeltaAssets * -1 != *vaultDeltaAssets) + if (localVaultDeltaAssets * -1 != accountDeltaAssets) { JLOG(j.fatal()) << // "Invariant failed: deposit must change vault and " @@ -3285,16 +3289,17 @@ ValidVault::finalize( result = false; } - auto const accountDeltaShares = deltaShares(tx[sfAccount]); - if (!accountDeltaShares) + auto const maybeAccDeltaShares = deltaShares(tx[sfAccount]); + if (!maybeAccDeltaShares) { JLOG(j.fatal()) << // "Invariant failed: deposit must change depositor " "shares"; return false; // That's all we can do } - - if (*accountDeltaShares <= zero) + // We don't need to round shares, they are integral MPT + auto const accountDeltaShares = *maybeAccDeltaShares; + if (accountDeltaShares <= zero) { JLOG(j.fatal()) << // "Invariant failed: deposit must increase depositor " @@ -3302,15 +3307,18 @@ ValidVault::finalize( result = false; } - auto const vaultDeltaShares = deltaShares(afterVault.pseudoId); - if (!vaultDeltaShares || *vaultDeltaShares == zero) + auto const maybeVaultDeltaShares = + deltaShares(afterVault.pseudoId); + if (!maybeVaultDeltaShares || *maybeVaultDeltaShares == zero) { JLOG(j.fatal()) << // "Invariant failed: deposit must change vault shares"; return false; // That's all we can do } - if (*vaultDeltaShares * -1 != *accountDeltaShares) + // We don't need to round shares, they are integral MPT + auto const vaultDeltaShares = *maybeVaultDeltaShares; + if (vaultDeltaShares * -1 != accountDeltaShares) { JLOG(j.fatal()) << // "Invariant failed: deposit must change depositor and " @@ -3318,15 +3326,22 @@ ValidVault::finalize( result = false; } - if (beforeVault.assetsTotal + *vaultDeltaAssets != - afterVault.assetsTotal) + auto const assetTotalDelta = roundToAsset( + vaultAsset, + afterVault.assetsTotal - beforeVault.assetsTotal, + minScale); + if (assetTotalDelta != vaultDeltaAssets) { JLOG(j.fatal()) << "Invariant failed: deposit and assets " "outstanding must add up"; result = false; } - if (beforeVault.assetsAvailable + *vaultDeltaAssets != - afterVault.assetsAvailable) + + auto const assetAvailableDelta = roundToAsset( + vaultAsset, + afterVault.assetsAvailable - beforeVault.assetsAvailable, + minScale); + if (assetAvailableDelta != vaultDeltaAssets) { JLOG(j.fatal()) << "Invariant failed: deposit and assets " "available must add up"; @@ -3355,16 +3370,11 @@ ValidVault::finalize( } // Get the most coarse scale to round calculations to - auto const minScale = // - std::max( - {getNatScale(vaultAsset, *maybeVaultDeltaAssets), - getNatScale( - vaultAsset, - afterVault.assetsTotal - beforeVault.assetsTotal), - getNatScale( - vaultAsset, - afterVault.assetsAvailable - - beforeVault.assetsAvailable)}); + auto const minScale = computeMinScale( + vaultAsset, + {*maybeVaultDeltaAssets, + afterVault.assetsTotal - beforeVault.assetsTotal, + afterVault.assetsAvailable - beforeVault.assetsAvailable}); auto const vaultPseudoDeltaAssets = roundToAsset(vaultAsset, *maybeVaultDeltaAssets, minScale); @@ -3408,10 +3418,11 @@ ValidVault::finalize( auto const destinationDelta = // maybeAccDelta ? *maybeAccDelta : *maybeOtherAccDelta; - // the scale of destinationDelta can be more corse, so we - // take the max with minScale - auto const localMinScale = std::max( - minScale, getNatScale(vaultAsset, destinationDelta)); + // the scale of destinationDelta can be coarser than + // minScale, so we take that into account when rounding + auto const localMinScale = computeMinScale( + vaultAsset, {minScale, destinationDelta}); + auto const roundedDestinationDelta = roundToAsset( vaultAsset, destinationDelta, localMinScale); @@ -3433,7 +3444,7 @@ ValidVault::finalize( result = false; } } - // We don't need to round here, as shares are always integral + // We don't need to round shares, they are integral MPT auto const accountDeltaShares = deltaShares(tx[sfAccount]); if (!accountDeltaShares) { @@ -3450,7 +3461,7 @@ ValidVault::finalize( "shares"; result = false; } - // We don't need to round here, as shares are always integral + // We don't need to round shares, they are integral MPT auto const vaultDeltaShares = deltaShares(afterVault.pseudoId); if (!vaultDeltaShares || *vaultDeltaShares == zero) { @@ -3469,10 +3480,10 @@ ValidVault::finalize( auto const assetTotalDelta = roundToAsset( vaultAsset, - beforeVault.assetsTotal - afterVault.assetsTotal, + afterVault.assetsTotal - beforeVault.assetsTotal, minScale); // Note, vaultBalance is negative (see check above) - if (assetTotalDelta != vaultPseudoDeltaAssets * -1) + if (assetTotalDelta != vaultPseudoDeltaAssets) { JLOG(j.fatal()) << "Invariant failed: withdrawal and " "assets outstanding must add up"; @@ -3481,10 +3492,10 @@ ValidVault::finalize( auto const assetAvailableDelta = roundToAsset( vaultAsset, - beforeVault.assetsAvailable - afterVault.assetsAvailable, + afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale); - if (assetAvailableDelta != vaultPseudoDeltaAssets * -1) + if (assetAvailableDelta != vaultPseudoDeltaAssets) { JLOG(j.fatal()) << "Invariant failed: withdrawal and " "assets available must add up"; @@ -3518,10 +3529,19 @@ ValidVault::finalize( } } - auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId); - if (vaultDeltaAssets) + auto const maybeVaultDeltaAssets = + deltaAssets(afterVault.pseudoId); + if (maybeVaultDeltaAssets) { - if (*vaultDeltaAssets >= zero) + auto const minScale = computeMinScale( + vaultAsset, + {*maybeVaultDeltaAssets, + afterVault.assetsTotal - beforeVault.assetsTotal, + afterVault.assetsAvailable - + beforeVault.assetsAvailable}); + auto const vaultDeltaAssets = roundToAsset( + vaultAsset, *maybeVaultDeltaAssets, minScale); + if (vaultDeltaAssets >= zero) { JLOG(j.fatal()) << // "Invariant failed: clawback must decrease vault " @@ -3529,8 +3549,11 @@ ValidVault::finalize( result = false; } - if (beforeVault.assetsTotal + *vaultDeltaAssets != - afterVault.assetsTotal) + auto const assetsTotalDelta = roundToAsset( + vaultAsset, + afterVault.assetsTotal - beforeVault.assetsTotal, + minScale); + if (assetsTotalDelta != vaultDeltaAssets) { JLOG(j.fatal()) << // "Invariant failed: clawback and assets outstanding " @@ -3538,8 +3561,12 @@ ValidVault::finalize( result = false; } - if (beforeVault.assetsAvailable + *vaultDeltaAssets != - afterVault.assetsAvailable) + auto const assetAvailableDelta = roundToAsset( + vaultAsset, + afterVault.assetsAvailable - + beforeVault.assetsAvailable, + minScale); + if (assetAvailableDelta != vaultDeltaAssets) { JLOG(j.fatal()) << // "Invariant failed: clawback and assets available " @@ -3554,15 +3581,15 @@ ValidVault::finalize( return false; // That's all we can do } - auto const accountDeltaShares = deltaShares(tx[sfHolder]); - if (!accountDeltaShares) + // We don't need to round shares, they are integral MPT + auto const maybeAccountDeltaShares = deltaShares(tx[sfHolder]); + if (!maybeAccountDeltaShares) { JLOG(j.fatal()) << // "Invariant failed: clawback must change holder shares"; return false; // That's all we can do } - - if (*accountDeltaShares >= zero) + if (*maybeAccountDeltaShares >= zero) { JLOG(j.fatal()) << // "Invariant failed: clawback must decrease holder " @@ -3570,6 +3597,7 @@ ValidVault::finalize( result = false; } + // We don't need to round shares, they are integral MPT auto const vaultDeltaShares = deltaShares(afterVault.pseudoId); if (!vaultDeltaShares || *vaultDeltaShares == zero) { @@ -3578,7 +3606,7 @@ ValidVault::finalize( return false; // That's all we can do } - if (*vaultDeltaShares * -1 != *accountDeltaShares) + if (*vaultDeltaShares * -1 != *maybeAccountDeltaShares) { JLOG(j.fatal()) << // "Invariant failed: clawback must change holder and " @@ -3616,4 +3644,40 @@ ValidVault::finalize( return true; } +[[nodiscard]] std::int32_t +ValidVault::computeMinScale( + Asset const& asset, + std::initializer_list numbers) +{ + if (numbers.size() == 0) + return 0; + // Helper to get the minimal scale of an STAmount by removing trailing + // zeros from its mantissa + auto const getNatScale = [](Asset const& asset, + Number const& value) -> std::int32_t { + if (value == beast::zero || asset.integral()) + return 0; + + auto mantissa = std::abs(value.mantissa()); + auto scale = value.exponent(); + + // Remove trailing zeros from mantissa, adjusting scale accordingly + while (mantissa % 10 == 0) + { + mantissa /= 10; + ++scale; + } + + return scale; + }; + + std::vector natScales; + std::transform( + numbers.begin(), + numbers.end(), + std::back_inserter(natScales), + [&](Number const& n) { return getNatScale(asset, n); }); + + return *std::max_element(natScales.begin(), natScales.end()); +} } // namespace xrpl diff --git a/src/xrpld/app/tx/detail/InvariantCheck.h b/src/xrpld/app/tx/detail/InvariantCheck.h index 87a1afb623c..3f61588cacd 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.h +++ b/src/xrpld/app/tx/detail/InvariantCheck.h @@ -900,6 +900,12 @@ class ValidVault XRPAmount const, ReadView const&, beast::Journal const&); + + // Compute the coarsest scale required to represent all numbers + [[nodiscard]] static std::int32_t + computeMinScale( + Asset const& asset, + std::initializer_list numbers); }; // additional invariant checks can be declared above and then added to this From aa12210fcd504f501388266d9a9e030fb4fd7f0d Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Tue, 20 Jan 2026 18:01:14 +0100 Subject: [PATCH 5/9] fixes a minor min bug --- src/xrpld/app/tx/detail/InvariantCheck.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 5e4e1d6baeb..d40c58152d8 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -3255,8 +3256,9 @@ ValidVault::finalize( "balance"; return false; } - auto const localMinScale = computeMinScale( - vaultAsset, {minScale, *maybeAccDeltaAssets}); + auto const localMinScale = std::min( + minScale, + computeMinScale(vaultAsset, {*maybeAccDeltaAssets})); auto const accountDeltaAssets = roundToAsset( vaultAsset, *maybeAccDeltaAssets, localMinScale); @@ -3420,8 +3422,9 @@ ValidVault::finalize( // the scale of destinationDelta can be coarser than // minScale, so we take that into account when rounding - auto const localMinScale = computeMinScale( - vaultAsset, {minScale, destinationDelta}); + auto const localMinScale = std::min( + minScale, + computeMinScale(vaultAsset, {destinationDelta})); auto const roundedDestinationDelta = roundToAsset( vaultAsset, destinationDelta, localMinScale); From c6821ab84251c0502473f5e390a9de0eec7051a0 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Wed, 21 Jan 2026 11:29:12 +0100 Subject: [PATCH 6/9] adds invariant test --- src/test/app/Loan_test.cpp | 144 +++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index e9780211de3..44af0b61668 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -7641,6 +7641,149 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT(afterSecondCoverAvailable == 0); } + // Tests that vault withdrawals work correctly when the vault has unrealized + // loss from an impaired loan, ensuring the invariant check properly + // accounts for the loss. + void + testWithdrawReflectsUnrealizedLoss() + { + using namespace jtx; + using namespace loan; + using namespace std::chrono_literals; + + testcase("Vault withdraw reflects sfLossUnrealized"); + + // Test constants + static constexpr std::int64_t INITIAL_FUNDING = 1'000'000; + static constexpr std::int64_t LENDER_INITIAL_IOU = 5'000'000; + static constexpr std::int64_t DEPOSITOR_INITIAL_IOU = 1'000'000; + static constexpr std::int64_t BORROWER_INITIAL_IOU = 100'000; + static constexpr std::int64_t DEPOSIT_AMOUNT = 5'000; + static constexpr std::int64_t PRINCIPAL_AMOUNT = 99; + static constexpr std::uint64_t EXPECTED_SHARES_PER_DEPOSITOR = + 5'000'000'000; + static constexpr std::uint32_t PAYMENT_INTERVAL = 600; + static constexpr std::uint32_t PAYMENT_TOTAL = 2; + + Env env(*this, all); + + // Setup accounts + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const depositorA{"lpA"}; + Account const depositorB{"lpB"}; + Account const borrower{"borrowerA"}; + + env.fund( + XRP(INITIAL_FUNDING), + issuer, + lender, + depositorA, + depositorB, + borrower); + env.close(); + + // Setup trust lines + PrettyAsset const iouAsset = issuer[iouCurrency]; + env(trust(lender, iouAsset(10'000'000))); + env(trust(depositorA, iouAsset(10'000'000))); + env(trust(depositorB, iouAsset(10'000'000))); + env(trust(borrower, iouAsset(10'000'000))); + env.close(); + + // Fund accounts with IOUs + env(pay(issuer, lender, iouAsset(LENDER_INITIAL_IOU))); + env(pay(issuer, depositorA, iouAsset(DEPOSITOR_INITIAL_IOU))); + env(pay(issuer, depositorB, iouAsset(DEPOSITOR_INITIAL_IOU))); + env(pay(issuer, borrower, iouAsset(BORROWER_INITIAL_IOU))); + env.close(); + + // Create vault and broker, then add deposits from two depositors + auto const broker = createVaultAndBroker(env, iouAsset, lender); + Vault v{env}; + + env(v.deposit({ + .depositor = depositorA, + .id = broker.vaultKeylet().key, + .amount = iouAsset(DEPOSIT_AMOUNT), + }), + ter(tesSUCCESS)); + env(v.deposit({ + .depositor = depositorB, + .id = broker.vaultKeylet().key, + .amount = iouAsset(DEPOSIT_AMOUNT), + }), + ter(tesSUCCESS)); + env.close(); + + // Create a loan + auto const sleBroker = env.le(keylet::loanbroker(broker.brokerID)); + if (!BEAST_EXPECT(sleBroker)) + return; + + auto const loanKeylet = + keylet::loan(broker.brokerID, sleBroker->at(sfLoanSequence)); + + env(set(borrower, broker.brokerID, PRINCIPAL_AMOUNT), + sig(sfCounterpartySignature, lender), + paymentTotal(PAYMENT_TOTAL), + paymentInterval(PAYMENT_INTERVAL), + fee(env.current()->fees().base * 2), + ter(tesSUCCESS)); + env.close(); + + // Impair the loan to create unrealized loss + env(manage(lender, loanKeylet.key, tfLoanImpair), ter(tesSUCCESS)); + env.close(); + + // Verify unrealized loss is recorded in the vault + auto const vaultAfterImpair = env.le(broker.vaultKeylet()); + if (!BEAST_EXPECT(vaultAfterImpair)) + return; + + BEAST_EXPECT( + vaultAfterImpair->at(sfLossUnrealized) == + broker.asset(PRINCIPAL_AMOUNT).value()); + + // Helper to get share balance for a depositor + auto const shareAsset = vaultAfterImpair->at(sfShareMPTID); + auto const getShareBalance = + [&](Account const& depositor) -> std::uint64_t { + auto const token = + env.le(keylet::mptoken(shareAsset, depositor.id())); + return token ? token->getFieldU64(sfMPTAmount) : 0; + }; + + // Verify both depositors have equal shares + auto const sharesLpA = getShareBalance(depositorA); + auto const sharesLpB = getShareBalance(depositorB); + BEAST_EXPECT(sharesLpA == EXPECTED_SHARES_PER_DEPOSITOR); + BEAST_EXPECT(sharesLpB == EXPECTED_SHARES_PER_DEPOSITOR); + BEAST_EXPECT(sharesLpA == sharesLpB); + + // Helper to attempt withdrawal + auto const attemptWithdrawShares = [&](Account const& depositor, + std::uint64_t shareAmount, + TER expected) { + STAmount const shareAmt{MPTIssue{shareAsset}, Number(shareAmount)}; + env(v.withdraw( + {.depositor = depositor, + .id = broker.vaultKeylet().key, + .amount = shareAmt}), + ter(expected)); + env.close(); + }; + + // Regression test: Both depositors should successfully withdraw despite + // unrealized loss. Previously failed with invariant violation: + // "withdrawal must change vault and destination balance by equal + // amount". This was caused by sharesToAssetsWithdraw rounding down, + // creating a mismatch where vaultDeltaAssets * -1 != destinationDelta + // when unrealized loss exists. + attemptWithdrawShares(depositorA, sharesLpA, tesSUCCESS); + attemptWithdrawShares(depositorB, sharesLpB, tesSUCCESS); + } + public: void run() override @@ -7694,6 +7837,7 @@ class Loan_test : public beast::unit_test::suite testLoanPayBrokerOwnerNoPermissionedDomainMPT(); testLoanSetBrokerOwnerNoPermissionedDomainMPT(); testSequentialFLCDepletion(); + testWithdrawReflectsUnrealizedLoss(); } }; From 1af0f4bd43e65cf524278c8f506d18916e595b38 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Wed, 21 Jan 2026 11:50:18 +0100 Subject: [PATCH 7/9] addreses review comments --- src/test/app/Invariants_test.cpp | 9 ++++++--- src/xrpld/app/tx/detail/InvariantCheck.cpp | 8 ++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index 8051ab1cbe9..b94aa9bbb14 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -20,8 +21,6 @@ #include -#include "xrpld/app/tx/detail/InvariantCheck.h" - #include #include @@ -3924,7 +3923,11 @@ class Invariants_test : public beast::unit_test::suite .expectedMinScale = -2, .values = {Number{1, -2}, Number{5, -3}, Number{3, -2}}, }, - }; + { + .name = "Equal scales", + .expectedMinScale = -1, + .values = {Number{1, -1}, Number{5, -1}, Number{1, -1}}, + }}; for (auto const& tc : testCases) { diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index d40c58152d8..caff5f127ab 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -3256,7 +3256,7 @@ ValidVault::finalize( "balance"; return false; } - auto const localMinScale = std::min( + auto const localMinScale = std::max( minScale, computeMinScale(vaultAsset, {*maybeAccDeltaAssets})); @@ -3300,7 +3300,7 @@ ValidVault::finalize( return false; // That's all we can do } // We don't need to round shares, they are integral MPT - auto const accountDeltaShares = *maybeAccDeltaShares; + auto const& accountDeltaShares = *maybeAccDeltaShares; if (accountDeltaShares <= zero) { JLOG(j.fatal()) << // @@ -3319,7 +3319,7 @@ ValidVault::finalize( } // We don't need to round shares, they are integral MPT - auto const vaultDeltaShares = *maybeVaultDeltaShares; + auto const& vaultDeltaShares = *maybeVaultDeltaShares; if (vaultDeltaShares * -1 != accountDeltaShares) { JLOG(j.fatal()) << // @@ -3422,7 +3422,7 @@ ValidVault::finalize( // the scale of destinationDelta can be coarser than // minScale, so we take that into account when rounding - auto const localMinScale = std::min( + auto const localMinScale = std::max( minScale, computeMinScale(vaultAsset, {destinationDelta})); From f76bf5340c728ca93df8ca08ee763142dfb736b3 Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Wed, 21 Jan 2026 11:50:38 +0100 Subject: [PATCH 8/9] flyby change removing unused includes --- src/test/app/LendingHelpers_test.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/test/app/LendingHelpers_test.cpp b/src/test/app/LendingHelpers_test.cpp index 55fffad6b0e..53659c65262 100644 --- a/src/test/app/LendingHelpers_test.cpp +++ b/src/test/app/LendingHelpers_test.cpp @@ -3,16 +3,11 @@ #include #include #include -#include #include -#include #include #include -#include -#include - #include #include From 0a9436def4caa82e8fa44afa44a6f67e8fa5634b Mon Sep 17 00:00:00 2001 From: Vito <5780819+Tapanito@users.noreply.github.com> Date: Wed, 21 Jan 2026 18:57:33 +0100 Subject: [PATCH 9/9] refactors vault invariant to use relative distance --- src/test/app/Invariants_test.cpp | 53 ------ src/test/app/Loan_test.cpp | 2 +- src/xrpld/app/tx/detail/InvariantCheck.cpp | 180 ++++++--------------- src/xrpld/app/tx/detail/InvariantCheck.h | 7 - 4 files changed, 49 insertions(+), 193 deletions(-) diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index b94aa9bbb14..1e81959ae65 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -4,7 +4,6 @@ #include #include -#include #include #include @@ -3892,57 +3891,6 @@ class Invariants_test : public beast::unit_test::suite precloseMpt); } - void - testVaultComputeMinScale() - { - using namespace jtx; - - Account const issuer{"issuer"}; - PrettyAsset const vaultAsset = issuer["IOU"]; - - struct TestCase - { - std::string name; - std::int32_t expectedMinScale; - std::initializer_list values; - }; - - auto const testCases = std::vector{ - { - .name = "No values", - .expectedMinScale = 0, - .values = {}, - }, - { - .name = "Mixed integer and Number values", - .expectedMinScale = 0, - .values = {1, -1, Number{10, -1}}, - }, - { - .name = "Mixed scales", - .expectedMinScale = -2, - .values = {Number{1, -2}, Number{5, -3}, Number{3, -2}}, - }, - { - .name = "Equal scales", - .expectedMinScale = -1, - .values = {Number{1, -1}, Number{5, -1}, Number{1, -1}}, - }}; - - for (auto const& tc : testCases) - { - testcase("vault computeMinScale: " + tc.name); - - auto const actualScale = - ValidVault::computeMinScale(vaultAsset, tc.values); - - BEAST_EXPECTS( - actualScale == tc.expectedMinScale, - "expected: " + std::to_string(tc.expectedMinScale) + - ", actual: " + std::to_string(actualScale)); - } - } - public: void run() override @@ -3966,7 +3914,6 @@ class Invariants_test : public beast::unit_test::suite testValidPseudoAccounts(); testValidLoanBroker(); testVault(); - testVaultComputeMinScale(); } }; diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 44af0b61668..34407c4b94a 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -7792,6 +7792,7 @@ class Loan_test : public beast::unit_test::suite testLoanPayLateFullPaymentBypassesPenalties(); testLoanCoverMinimumRoundingExploit(); #endif + testWithdrawReflectsUnrealizedLoss(); testInvalidLoanSet(); testCoverDepositWithdrawNonTransferableMPT(); @@ -7837,7 +7838,6 @@ class Loan_test : public beast::unit_test::suite testLoanPayBrokerOwnerNoPermissionedDomainMPT(); testLoanSetBrokerOwnerNoPermissionedDomainMPT(); testSequentialFLCDepletion(); - testWithdrawReflectsUnrealizedLoss(); } }; diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index caff5f127ab..6fb832a05ea 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -19,15 +19,11 @@ #include #include #include -#include #include #include #include -#include #include -#include -#include #include namespace xrpl { @@ -2712,8 +2708,9 @@ ValidVault::visitEntry( // At this moment we have no way of telling if this object holds // vault shares or something else. Save it for finalize. afterMPTs_.push_back(Shares::make(*after)); - balanceDelta -= Number(static_cast( - after->getFieldU64(sfOutstandingAmount))); + balanceDelta -= Number( + static_cast( + after->getFieldU64(sfOutstandingAmount))); sign = 1; break; case ltMPTOKEN: @@ -3077,6 +3074,15 @@ ValidVault::finalize( return vault.assetsAvailable == 0 && vault.assetsTotal == 0; }; + auto const withinDistance = [&](Number const& num1, + Number const& num2) -> bool { + if (vaultAsset.integral()) + return num1 == num2; + // The exponent was set experimentally, based on whether + // Loan_test::testWithdrawReflectsUnrealizedLoss unit test fail + return withinRelativeDistance(num1, num2, Number{1, -13}); + }; + // Technically this does not need to be a lambda, but it's more // convenient thanks to early "return false"; the not-so-nice // alternatives are several layers of nested if/else or more complex @@ -3210,19 +3216,7 @@ ValidVault::finalize( return false; // That's all we can do } - // Get the coarsest scale to round calculations to - auto const minScale = computeMinScale( - vaultAsset, - { - *maybeVaultDeltaAssets, - afterVault.assetsTotal - beforeVault.assetsTotal, - afterVault.assetsAvailable - - beforeVault.assetsAvailable, - }); - - auto const vaultDeltaAssets = - roundToAsset(vaultAsset, *maybeVaultDeltaAssets, minScale); - + auto const vaultDeltaAssets = *maybeVaultDeltaAssets; if (vaultDeltaAssets > tx[sfAmount]) { JLOG(j.fatal()) << // @@ -3256,14 +3250,7 @@ ValidVault::finalize( "balance"; return false; } - auto const localMinScale = std::max( - minScale, - computeMinScale(vaultAsset, {*maybeAccDeltaAssets})); - - auto const accountDeltaAssets = roundToAsset( - vaultAsset, *maybeAccDeltaAssets, localMinScale); - auto const localVaultDeltaAssets = roundToAsset( - vaultAsset, vaultDeltaAssets, localMinScale); + auto const accountDeltaAssets = *maybeAccDeltaAssets; if (accountDeltaAssets >= zero) { @@ -3273,7 +3260,8 @@ ValidVault::finalize( result = false; } - if (localVaultDeltaAssets * -1 != accountDeltaAssets) + if (!withinDistance( + vaultDeltaAssets * -1, accountDeltaAssets)) { JLOG(j.fatal()) << // "Invariant failed: deposit must change vault and " @@ -3328,22 +3316,18 @@ ValidVault::finalize( result = false; } - auto const assetTotalDelta = roundToAsset( - vaultAsset, - afterVault.assetsTotal - beforeVault.assetsTotal, - minScale); - if (assetTotalDelta != vaultDeltaAssets) + auto const assetTotalDelta = + afterVault.assetsTotal - beforeVault.assetsTotal; + if (!withinDistance(assetTotalDelta, vaultDeltaAssets)) { JLOG(j.fatal()) << "Invariant failed: deposit and assets " "outstanding must add up"; result = false; } - auto const assetAvailableDelta = roundToAsset( - vaultAsset, - afterVault.assetsAvailable - beforeVault.assetsAvailable, - minScale); - if (assetAvailableDelta != vaultDeltaAssets) + auto const assetAvailableDelta = + afterVault.assetsAvailable - beforeVault.assetsAvailable; + if (!withinDistance(assetAvailableDelta, vaultDeltaAssets)) { JLOG(j.fatal()) << "Invariant failed: deposit and assets " "available must add up"; @@ -3371,15 +3355,7 @@ ValidVault::finalize( return false; // That's all we can do } - // Get the most coarse scale to round calculations to - auto const minScale = computeMinScale( - vaultAsset, - {*maybeVaultDeltaAssets, - afterVault.assetsTotal - beforeVault.assetsTotal, - afterVault.assetsAvailable - beforeVault.assetsAvailable}); - - auto const vaultPseudoDeltaAssets = - roundToAsset(vaultAsset, *maybeVaultDeltaAssets, minScale); + auto const vaultPseudoDeltaAssets = *maybeVaultDeltaAssets; if (vaultPseudoDeltaAssets >= zero) { @@ -3420,16 +3396,7 @@ ValidVault::finalize( auto const destinationDelta = // maybeAccDelta ? *maybeAccDelta : *maybeOtherAccDelta; - // the scale of destinationDelta can be coarser than - // minScale, so we take that into account when rounding - auto const localMinScale = std::max( - minScale, - computeMinScale(vaultAsset, {destinationDelta})); - - auto const roundedDestinationDelta = roundToAsset( - vaultAsset, destinationDelta, localMinScale); - - if (roundedDestinationDelta <= zero) + if (destinationDelta <= zero) { JLOG(j.fatal()) << // "Invariant failed: withdrawal must increase " @@ -3437,9 +3404,8 @@ ValidVault::finalize( result = false; } - auto const localPseudoDeltaAssets = roundToAsset( - vaultAsset, vaultPseudoDeltaAssets, localMinScale); - if (localPseudoDeltaAssets * -1 != roundedDestinationDelta) + if (!withinDistance( + vaultPseudoDeltaAssets * -1, destinationDelta)) { JLOG(j.fatal()) << // "Invariant failed: withdrawal must change vault " @@ -3481,24 +3447,20 @@ ValidVault::finalize( result = false; } - auto const assetTotalDelta = roundToAsset( - vaultAsset, - afterVault.assetsTotal - beforeVault.assetsTotal, - minScale); + auto const assetTotalDelta = + afterVault.assetsTotal - beforeVault.assetsTotal; // Note, vaultBalance is negative (see check above) - if (assetTotalDelta != vaultPseudoDeltaAssets) + if (!withinDistance(assetTotalDelta, vaultPseudoDeltaAssets)) { JLOG(j.fatal()) << "Invariant failed: withdrawal and " "assets outstanding must add up"; result = false; } - auto const assetAvailableDelta = roundToAsset( - vaultAsset, - afterVault.assetsAvailable - beforeVault.assetsAvailable, - minScale); - - if (assetAvailableDelta != vaultPseudoDeltaAssets) + auto const assetAvailableDelta = + afterVault.assetsAvailable - beforeVault.assetsAvailable; + if (!withinDistance( + assetAvailableDelta, vaultPseudoDeltaAssets)) { JLOG(j.fatal()) << "Invariant failed: withdrawal and " "assets available must add up"; @@ -3536,14 +3498,7 @@ ValidVault::finalize( deltaAssets(afterVault.pseudoId); if (maybeVaultDeltaAssets) { - auto const minScale = computeMinScale( - vaultAsset, - {*maybeVaultDeltaAssets, - afterVault.assetsTotal - beforeVault.assetsTotal, - afterVault.assetsAvailable - - beforeVault.assetsAvailable}); - auto const vaultDeltaAssets = roundToAsset( - vaultAsset, *maybeVaultDeltaAssets, minScale); + auto const vaultDeltaAssets = *maybeVaultDeltaAssets; if (vaultDeltaAssets >= zero) { JLOG(j.fatal()) << // @@ -3552,24 +3507,22 @@ ValidVault::finalize( result = false; } - auto const assetsTotalDelta = roundToAsset( - vaultAsset, - afterVault.assetsTotal - beforeVault.assetsTotal, - minScale); + auto const assetsTotalDelta = + afterVault.assetsTotal - beforeVault.assetsTotal; if (assetsTotalDelta != vaultDeltaAssets) - { - JLOG(j.fatal()) << // - "Invariant failed: clawback and assets outstanding " - "must add up"; - result = false; - } - - auto const assetAvailableDelta = roundToAsset( - vaultAsset, + if (!withinDistance(assetsTotalDelta, vaultDeltaAssets)) + { + JLOG(j.fatal()) << // + "Invariant failed: clawback and assets " + "outstanding " + "must add up"; + result = false; + } + + auto const assetAvailableDelta = afterVault.assetsAvailable - - beforeVault.assetsAvailable, - minScale); - if (assetAvailableDelta != vaultDeltaAssets) + beforeVault.assetsAvailable; + if (!withinDistance(assetAvailableDelta, vaultDeltaAssets)) { JLOG(j.fatal()) << // "Invariant failed: clawback and assets available " @@ -3646,41 +3599,4 @@ ValidVault::finalize( return true; } - -[[nodiscard]] std::int32_t -ValidVault::computeMinScale( - Asset const& asset, - std::initializer_list numbers) -{ - if (numbers.size() == 0) - return 0; - // Helper to get the minimal scale of an STAmount by removing trailing - // zeros from its mantissa - auto const getNatScale = [](Asset const& asset, - Number const& value) -> std::int32_t { - if (value == beast::zero || asset.integral()) - return 0; - - auto mantissa = std::abs(value.mantissa()); - auto scale = value.exponent(); - - // Remove trailing zeros from mantissa, adjusting scale accordingly - while (mantissa % 10 == 0) - { - mantissa /= 10; - ++scale; - } - - return scale; - }; - - std::vector natScales; - std::transform( - numbers.begin(), - numbers.end(), - std::back_inserter(natScales), - [&](Number const& n) { return getNatScale(asset, n); }); - - return *std::max_element(natScales.begin(), natScales.end()); -} } // namespace xrpl diff --git a/src/xrpld/app/tx/detail/InvariantCheck.h b/src/xrpld/app/tx/detail/InvariantCheck.h index 3f61588cacd..83152f74859 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.h +++ b/src/xrpld/app/tx/detail/InvariantCheck.h @@ -9,7 +9,6 @@ #include #include -#include #include #include @@ -900,12 +899,6 @@ class ValidVault XRPAmount const, ReadView const&, beast::Journal const&); - - // Compute the coarsest scale required to represent all numbers - [[nodiscard]] static std::int32_t - computeMinScale( - Asset const& asset, - std::initializer_list numbers); }; // additional invariant checks can be declared above and then added to this