fix: Re-apply vault invariant rounding to per-transactor checks

Restore the DeltaInfo + roundToAsset rounding logic in VaultDeposit,
VaultWithdraw, and VaultClawback finalizeInvariants methods. These
changes were lost when the previous parent-branch merge was committed
without staging the transactor files.
This commit is contained in:
Vito
2026-04-24 17:46:03 +02:00
parent 6167c70043
commit a444d80b45
3 changed files with 122 additions and 49 deletions

View File

@@ -500,9 +500,10 @@ VaultClawback::finalizeInvariants(
auto const& beforeVault = invariantData_.beforeVault()[0];
auto const& afterVault = invariantData_.afterVault()[0];
auto const& vaultAsset = afterVault.asset;
auto const beforeShares = invariantData_.resolveBeforeShares(beforeVault);
if (afterVault.asset.native() || afterVault.asset.getIssuer() != tx[sfAccount])
if (vaultAsset.native() || vaultAsset.getIssuer() != tx[sfAccount])
{
// The owner can use clawback to force-burn shares when the
// vault is empty but there are outstanding shares
@@ -518,22 +519,35 @@ VaultClawback::finalizeInvariants(
auto result = true;
auto const vaultDeltaAssets = invariantData_.deltaAssets(afterVault.asset, afterVault.pseudoId);
if (vaultDeltaAssets)
auto const maybeVaultDeltaAssets = invariantData_.deltaAssets(vaultAsset, afterVault.pseudoId);
if (maybeVaultDeltaAssets)
{
if (*vaultDeltaAssets >= beast::zero)
auto const totalDelta = VaultInvariantData::DeltaInfo::makeDelta(
beforeVault.assetsTotal, afterVault.assetsTotal, vaultAsset);
auto const availableDelta = VaultInvariantData::DeltaInfo::makeDelta(
beforeVault.assetsAvailable, afterVault.assetsAvailable, vaultAsset);
auto const minScale = VaultInvariantData::computeCoarsestScale(
{*maybeVaultDeltaAssets, totalDelta, availableDelta});
auto const vaultDeltaAssets =
roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale);
if (vaultDeltaAssets >= beast::zero)
{
JLOG(j.fatal()) << "Invariant failed: clawback must decrease vault balance";
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 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: clawback and assets available must add up";
result = false;
@@ -545,15 +559,16 @@ VaultClawback::finalizeInvariants(
return false;
}
auto const accountDeltaShares =
// We don't need to round shares, they are integral MPT.
auto const maybeAccountDeltaShares =
invariantData_.deltaShares(afterVault.pseudoId, afterVault.shareMPTID, tx[sfHolder]);
if (!accountDeltaShares)
if (!maybeAccountDeltaShares)
{
JLOG(j.fatal()) << "Invariant failed: clawback must change holder shares";
return false;
}
if (*accountDeltaShares >= beast::zero)
if (maybeAccountDeltaShares->delta >= beast::zero)
{
JLOG(j.fatal()) << "Invariant failed: clawback must decrease holder shares";
result = false;
@@ -561,13 +576,13 @@ VaultClawback::finalizeInvariants(
auto const vaultDeltaShares =
invariantData_.deltaShares(afterVault.pseudoId, afterVault.shareMPTID, afterVault.pseudoId);
if (!vaultDeltaShares || *vaultDeltaShares == beast::zero)
if (!vaultDeltaShares || vaultDeltaShares->delta == beast::zero)
{
JLOG(j.fatal()) << "Invariant failed: clawback must change vault shares";
return false;
}
if (*vaultDeltaShares * -1 != *accountDeltaShares)
if (vaultDeltaShares->delta * -1 != maybeAccountDeltaShares->delta)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback must change holder and vault shares by equal amount";

View File

@@ -12,6 +12,7 @@
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
#include <xrpl/protocol/STTakesAsset.h>
@@ -20,6 +21,7 @@
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/Transactor.h>
#include <algorithm>
#include <memory>
#include <stdexcept>
@@ -310,23 +312,35 @@ VaultDeposit::finalizeInvariants(
auto result = true;
auto const& beforeVault = invariantData_.beforeVault()[0];
auto const& afterVault = invariantData_.afterVault()[0];
auto const& vaultAsset = afterVault.asset;
auto const vaultDeltaAssets = invariantData_.deltaAssets(afterVault.asset, afterVault.pseudoId);
auto const maybeVaultDeltaAssets = invariantData_.deltaAssets(vaultAsset, afterVault.pseudoId);
if (!vaultDeltaAssets)
if (!maybeVaultDeltaAssets)
{
JLOG(j.fatal()) << "Invariant failed: deposit must change vault balance";
return false;
}
if (*vaultDeltaAssets > tx[sfAmount])
// Get the coarsest scale to round calculations to.
auto const totalDelta = VaultInvariantData::DeltaInfo::makeDelta(
beforeVault.assetsTotal, afterVault.assetsTotal, vaultAsset);
auto const availableDelta = VaultInvariantData::DeltaInfo::makeDelta(
beforeVault.assetsAvailable, afterVault.assetsAvailable, vaultAsset);
auto const minScale = VaultInvariantData::computeCoarsestScale(
{*maybeVaultDeltaAssets, totalDelta, availableDelta});
auto const vaultDeltaAssets = roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale);
auto const txAmount = roundToAsset(vaultAsset, tx[sfAmount], minScale);
if (vaultDeltaAssets > txAmount)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must not change vault balance by more than deposited amount";
result = false;
}
if (*vaultDeltaAssets <= beast::zero)
if (vaultDeltaAssets <= beast::zero)
{
JLOG(j.fatal()) << "Invariant failed: deposit must increase vault balance";
result = false;
@@ -335,28 +349,35 @@ VaultDeposit::finalizeInvariants(
// Any payments (including deposits) made by the issuer
// do not change their balance, but create funds instead.
bool const issuerDeposit = [&]() -> bool {
if (afterVault.asset.native())
if (vaultAsset.native())
return false;
return tx[sfAccount] == afterVault.asset.getIssuer();
return tx[sfAccount] == vaultAsset.getIssuer();
}();
if (!issuerDeposit)
{
auto const accountDeltaAssets = invariantData_.deltaAssetsTxAccount(
tx[sfAccount], tx[~sfDelegate], afterVault.asset, fee);
if (!accountDeltaAssets)
auto const maybeAccDeltaAssets =
invariantData_.deltaAssetsTxAccount(tx[sfAccount], tx[~sfDelegate], vaultAsset, fee);
if (!maybeAccDeltaAssets)
{
JLOG(j.fatal()) << "Invariant failed: deposit must change depositor balance";
return false;
}
auto const localMinScale =
std::max(minScale, VaultInvariantData::computeCoarsestScale({*maybeAccDeltaAssets}));
if (*accountDeltaAssets >= beast::zero)
auto const accountDeltaAssets =
roundToAsset(vaultAsset, maybeAccDeltaAssets->delta, localMinScale);
auto const localVaultDeltaAssets =
roundToAsset(vaultAsset, vaultDeltaAssets, localMinScale);
if (accountDeltaAssets >= beast::zero)
{
JLOG(j.fatal()) << "Invariant failed: deposit must decrease depositor balance";
result = false;
}
if (*accountDeltaAssets * -1 != *vaultDeltaAssets)
if (localVaultDeltaAssets * -1 != accountDeltaAssets)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must change vault and depositor balance by equal amount";
@@ -371,41 +392,49 @@ VaultDeposit::finalizeInvariants(
result = false;
}
auto const accountDeltaShares =
// We don't need to round shares, they are integral MPT.
auto const maybeAccDeltaShares =
invariantData_.deltaShares(afterVault.pseudoId, afterVault.shareMPTID, tx[sfAccount]);
if (!accountDeltaShares)
if (!maybeAccDeltaShares)
{
JLOG(j.fatal()) << "Invariant failed: deposit must change depositor shares";
return false;
}
auto const& accountDeltaShares = *maybeAccDeltaShares;
if (*accountDeltaShares <= beast::zero)
if (accountDeltaShares.delta <= beast::zero)
{
JLOG(j.fatal()) << "Invariant failed: deposit must increase depositor shares";
result = false;
}
auto const vaultDeltaShares =
auto const maybeVaultDeltaShares =
invariantData_.deltaShares(afterVault.pseudoId, afterVault.shareMPTID, afterVault.pseudoId);
if (!vaultDeltaShares || *vaultDeltaShares == beast::zero)
if (!maybeVaultDeltaShares || maybeVaultDeltaShares->delta == beast::zero)
{
JLOG(j.fatal()) << "Invariant failed: deposit must change vault shares";
return false;
}
auto const& vaultDeltaShares = *maybeVaultDeltaShares;
if (*vaultDeltaShares * -1 != *accountDeltaShares)
if (vaultDeltaShares.delta * -1 != accountDeltaShares.delta)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must change depositor and vault shares by equal amount";
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";
result = false;

View File

@@ -13,6 +13,7 @@
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
#include <xrpl/protocol/STTakesAsset.h>
@@ -21,6 +22,7 @@
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/Transactor.h>
#include <algorithm>
#include <memory>
#include <stdexcept>
@@ -327,16 +329,28 @@ VaultWithdraw::finalizeInvariants(
auto result = true;
auto const& beforeVault = invariantData_.beforeVault()[0];
auto const& afterVault = invariantData_.afterVault()[0];
auto const& vaultAsset = afterVault.asset;
auto const vaultDeltaAssets = invariantData_.deltaAssets(afterVault.asset, afterVault.pseudoId);
auto const maybeVaultDeltaAssets = invariantData_.deltaAssets(vaultAsset, afterVault.pseudoId);
if (!vaultDeltaAssets)
if (!maybeVaultDeltaAssets)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal must change vault balance";
return false;
}
if (*vaultDeltaAssets >= beast::zero)
// Get the most coarse scale to round calculations to.
auto const totalDelta = VaultInvariantData::DeltaInfo::makeDelta(
beforeVault.assetsTotal, afterVault.assetsTotal, vaultAsset);
auto const availableDelta = VaultInvariantData::DeltaInfo::makeDelta(
beforeVault.assetsAvailable, afterVault.assetsAvailable, vaultAsset);
auto const minScale = VaultInvariantData::computeCoarsestScale(
{*maybeVaultDeltaAssets, totalDelta, availableDelta});
auto const vaultPseudoDeltaAssets =
roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale);
if (vaultPseudoDeltaAssets >= beast::zero)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal must decrease vault balance";
result = false;
@@ -345,39 +359,49 @@ VaultWithdraw::finalizeInvariants(
// Any payments (including withdrawal) going to the issuer
// do not change their balance, but destroy funds instead.
bool const issuerWithdrawal = [&]() -> bool {
if (afterVault.asset.native())
if (vaultAsset.native())
return false;
auto const destination = tx[~sfDestination].value_or(tx[sfAccount]);
return destination == afterVault.asset.getIssuer();
return destination == vaultAsset.getIssuer();
}();
if (!issuerWithdrawal)
{
auto const accountDeltaAssets = invariantData_.deltaAssetsTxAccount(
tx[sfAccount], tx[~sfDelegate], afterVault.asset, fee);
auto const otherAccountDelta = [&]() -> std::optional<Number> {
auto const maybeAccDelta =
invariantData_.deltaAssetsTxAccount(tx[sfAccount], tx[~sfDelegate], vaultAsset, fee);
auto const maybeOtherAccDelta = [&]() -> std::optional<VaultInvariantData::DeltaInfo> {
if (auto const destination = tx[~sfDestination];
destination && *destination != tx[sfAccount])
return invariantData_.deltaAssets(afterVault.asset, *destination);
return invariantData_.deltaAssets(vaultAsset, *destination);
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 destination balance";
return false;
}
auto const destinationDelta = accountDeltaAssets ? *accountDeltaAssets : *otherAccountDelta;
auto const destinationDelta = maybeAccDelta ? *maybeAccDelta : *maybeOtherAccDelta;
if (destinationDelta <= beast::zero)
// The scale of destinationDelta can be coarser than minScale, so we
// take that into account when rounding.
auto const localMinScale =
std::max(minScale, VaultInvariantData::computeCoarsestScale({destinationDelta}));
auto const roundedDestinationDelta =
roundToAsset(vaultAsset, destinationDelta.delta, localMinScale);
if (roundedDestinationDelta <= beast::zero)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal must increase destination balance";
result = false;
}
if (*vaultDeltaAssets * -1 != destinationDelta)
auto const localPseudoDeltaAssets =
roundToAsset(vaultAsset, vaultPseudoDeltaAssets, localMinScale);
if (localPseudoDeltaAssets * -1 != roundedDestinationDelta)
{
JLOG(j.fatal()) << //
"Invariant failed: withdrawal must change vault and destination balance by equal "
@@ -386,6 +410,7 @@ VaultWithdraw::finalizeInvariants(
}
}
// We don't need to round shares, they are integral MPT.
auto const accountDeltaShares =
invariantData_.deltaShares(afterVault.pseudoId, afterVault.shareMPTID, tx[sfAccount]);
if (!accountDeltaShares)
@@ -394,7 +419,7 @@ VaultWithdraw::finalizeInvariants(
return false;
}
if (*accountDeltaShares >= beast::zero)
if (accountDeltaShares->delta >= beast::zero)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal must decrease depositor shares";
result = false;
@@ -402,27 +427,31 @@ VaultWithdraw::finalizeInvariants(
auto const vaultDeltaShares =
invariantData_.deltaShares(afterVault.pseudoId, afterVault.shareMPTID, afterVault.pseudoId);
if (!vaultDeltaShares || *vaultDeltaShares == beast::zero)
if (!vaultDeltaShares || vaultDeltaShares->delta == beast::zero)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal must change vault shares";
return false;
}
if (*vaultDeltaShares * -1 != *accountDeltaShares)
if (vaultDeltaShares->delta * -1 != accountDeltaShares->delta)
{
JLOG(j.fatal()) << //
"Invariant failed: withdrawal must change depositor and vault shares by equal amount";
result = false;
}
// Note, vaultBalance is negative (see check above)
if (beforeVault.assetsTotal + *vaultDeltaAssets != afterVault.assetsTotal)
auto const assetTotalDelta =
roundToAsset(vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
// Note, vaultBalance is negative (see check above).
if (assetTotalDelta != vaultPseudoDeltaAssets)
{
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, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
if (assetAvailableDelta != vaultPseudoDeltaAssets)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal and assets available must add up";
result = false;