Compare commits

...

34 Commits

Author SHA1 Message Date
Vito
5fff81a722 Merge remote-tracking branch 'origin/develop' into tapanito/transactor-invariant-pt2
# Conflicts:
#	src/libxrpl/tx/transactors/nft/NFTokenMint.cpp
#	src/libxrpl/tx/transactors/vault/VaultClawback.cpp
#	src/libxrpl/tx/transactors/vault/VaultCreate.cpp
#	src/libxrpl/tx/transactors/vault/VaultDelete.cpp
#	src/libxrpl/tx/transactors/vault/VaultDeposit.cpp
#	src/libxrpl/tx/transactors/vault/VaultSet.cpp
#	src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp
2026-04-24 18:06:40 +02:00
Vito
a444d80b45 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.
2026-04-24 17:46:03 +02:00
Vito
6167c70043 Merge remote-tracking branch 'origin/tapanito/transaction-invariant' into tapanito/transactor-invariant-pt2
# Conflicts:
#	include/xrpl/tx/invariants/VaultInvariant.h
#	src/libxrpl/tx/invariants/VaultInvariant.cpp
#	src/libxrpl/tx/transactors/vault/VaultCreate.cpp
2026-04-22 17:16:13 +02:00
Vito
dd654f649e fix: Remove unreachable tefINVARIANT_FAILED check on tx invariants
checkTransactionInvariants only returns tecINVARIANT_FAILED or the
input result, never tefINVARIANT_FAILED. Only protocol invariants can
escalate to tef, so the txResult comparison was dead code.
2026-04-22 15:25:38 +02:00
Vito
0365449598 chore: Mark conditionally-used includes with IWYU pragma keep
Serializer.h, STAmount.h, Number.h, and <iterator> are only used inside
XRPL_ASSERT or #ifdef DEBUG blocks that compile to nothing in release
builds. Without the pragma, include-cleaner flags them as unused in
release CI while flagging them as missing in debug builds.
2026-04-22 13:43:39 +02:00
Vito
3f569f4cca chore: Add more direct includes flagged by include-cleaner
Add direct includes for std::exception, std::distance, beast::Journal,
xrpl::ReadView, xrpl::Serializer, xrpl::SerialIter, xrpl::Number, and
xrpl::roundToAsset across files that use them. Also suppress
modernize-use-ranges on a std::is_sorted call where the ranges version
does not compile because SignerEntry is not std::totally_ordered.
2026-04-22 12:43:12 +02:00
Vito
cdef4f3f2c chore: Apply readability fixes flagged by clang-tidy
Use container::empty() and container::contains() instead of size() == 0
and find() != end(), matching the readability-container-size-empty and
readability-container-contains checks.
2026-04-22 12:43:11 +02:00
Vito Tumas
4b9e3997f8 Merge branch 'develop' into tapanito/transaction-invariant 2026-04-22 11:21:39 +02:00
Vito
273f8a13cd fix: Log transaction JSON on invariant failure
The fatal log emitted when finalizeInvariants returns false previously
lacked identifying information, making postmortems difficult. Include
the full transaction JSON, matching the global invariant checker.
2026-04-22 10:00:13 +02:00
Vito
50aa2603ec docs: Clarify visitInvariantEntry after parameter semantics
Callers must use isDelete rather than after == nullptr to detect
deletions; after is non-null for erased SLEs as supplied by the apply
logic.
2026-04-22 10:00:01 +02:00
Vito
c84cab12ae Merge remote-tracking branch 'origin/develop' into tapanito/transaction-invariant
# Conflicts:
#	src/test/app/Invariants_test.cpp
2026-04-21 19:02:58 +02:00
Vito
96f60d6def chore: Add direct includes flagged by include-cleaner
Add direct includes for std::shared_ptr, SLE, STTx, and XRPAmount in
transactor translation units that override visitInvariantEntry or
finalizeInvariants. Previously these types were pulled in transitively,
which misc-include-cleaner flags as errors under the project's
clang-tidy configuration.
2026-04-21 18:58:20 +02:00
Vito
687d9489d7 test: Guard against null transactor in Invariants_test
Addresses a review comment: the soft BEAST_EXPECT assertion would not
abort on failure, risking a null pointer dereference in the subsequent
checkInvariants call. Use an early-return guard instead.
2026-04-21 18:58:20 +02:00
Vito Tumas
99d745d4a2 Merge branch 'develop' into tapanito/transaction-invariant 2026-04-21 16:56:01 +02:00
Vito
e28d5e5b50 formatting issues 2026-04-21 16:50:27 +02:00
Vito
da44fdb5d3 Merge remote-tracking branch 'origin/develop' into tapanito/transaction-invariant 2026-04-21 14:41:20 +02:00
Vito
9e9fdb40a8 Merge remote-tracking branch 'origin/develop' into tapanito/transaction-invariant 2026-04-21 13:47:31 +02:00
Vito Tumas
1664669bcc Merge branch 'develop' into tapanito/transaction-invariant 2026-03-31 13:46:04 +02:00
Vito Tumas
e873e17d2b Merge branch 'tapanito/transaction-invariant' into tapanito/transactor-invariant-pt2 2026-03-31 12:18:57 +02:00
Vito
a62ac8b32f addresses review comments 2026-03-25 15:17:47 +01:00
Vito
05738afbb9 Merge remote-tracking branch 'origin/tapanito/transaction-invariant' into tapanito/transactor-invariant-pt2 2026-03-24 16:10:33 +01:00
Vito
b5b97bc3e6 addresses review comments 2026-03-24 16:10:07 +01:00
Vito
1bf045d2bd removes bad files 2026-03-23 16:57:42 +01:00
Vito
009e05a463 restores invariant tests 2026-03-23 16:49:03 +01:00
Vito
ce92da9161 fix: Clean up VaultInvariantData and deduplicate ValidVault
- Remove unused zero member, clear() method, and MPT accessors
- Fix std::move from const ref in resolveBeforeShares
- Fix assertion label to use VaultInvariantData class name
- Simplify dead ternary in VaultSet::finalizeInvariants
- Add [[nodiscard]] to afterVault()/beforeVault() accessors
- Add isTesSuccess early return guard to all transactor invariants
- Deduplicate ValidVault by delegating to VaultInvariantData
- Remove ReadView/STTx/Journal dependencies from VaultInvariantData
2026-03-22 13:23:06 +01:00
Vito
c59683d07c refactor: Move vault invariants from global to per-transactor
Move transaction-specific invariant checks from the global ValidVault
invariant into each vault transactor's finalizeInvariants method. Run
both transaction and protocol invariants unconditionally, returning
failure if either check fails (tef takes priority over tec).
2026-03-22 12:13:53 +01:00
Vito
e3d9e06345 fix: Run both transaction and protocol invariants unconditionally
Always run both invariant checks instead of short-circuiting on
transaction invariant failure. Return the most severe failure code
(tef > tec). Also switch logger from j_ to ctx_.journal.
2026-03-22 12:11:00 +01:00
Vito
038e50abed fix: Align invariant method class names with transactor class names
Rename class qualifiers in visitInvariantEntry and finalizeInvariants
definitions to match their actual transactor classes (e.g.,
DeleteAccount → AccountDelete, CancelCheck → CheckCancel).
2026-03-21 17:56:12 +01:00
Vito
34804eb53a Merge remote-tracking branch 'origin/tapanito/transaction-invariant' into tapanito/transaction-invariant 2026-03-21 17:55:00 +01:00
Vito
08f70c85d4 Merge remote-tracking branch 'origin/develop' into tapanito/transaction-invariant 2026-03-20 17:51:35 +01:00
Vito Tumas
b6e792cede Merge branch 'develop' into tapanito/transaction-invariant 2026-03-17 17:56:19 +01:00
Vito
23e117bde7 Merge remote-tracking branch 'origin/develop' into tapanito/transaction-invariant 2026-03-16 19:09:43 +01:00
Vito
40ee1e1ff3 feat: Add no-op transaction invariant overrides to all transactors 2026-03-16 19:05:11 +01:00
Vito
10cb46c3f0 feat: Add transaction-specific invariant checking
Introduce a two-phase visitor pattern (visitInvariantEntry /
finalizeInvariants) on Transactor so individual transaction types
can define their own post-condition checks.  These run before the
existing protocol-wide invariants and short-circuit on failure to
avoid misleading secondary errors.

- Add pure virtual visitInvariantEntry and finalizeInvariants to
  Transactor
- Implement checkTransactionInvariants to drive the visitor loop
- Extract checkInvariants to orchestrate transaction-specific then
  protocol-wide checks with reset-and-retry on failure
- Move failInvariantCheck from private to public in ApplyContext
2026-03-16 19:05:03 +01:00
18 changed files with 1096 additions and 1011 deletions

View File

@@ -1,94 +1,41 @@
#pragma once
#include <xrpl/basics/Number.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <optional>
#include <unordered_map>
#include <vector>
#include <xrpl/tx/transactors/vault/VaultInvariantData.h>
namespace xrpl {
/*
* @brief Invariants: Vault object and MPTokenIssuance for vault shares
*
* - vault deleted and vault created is empty
* - vault created must be linked to pseudo-account for shares and assets
* - vault must have MPTokenIssuance for shares
* - vault without shares outstanding must have no shares
* - loss unrealized does not exceed the difference between assets total and
* assets available
* - assets available do not exceed assets total
* - vault deposit increases assets and share issuance, and adds to:
* total assets, assets available, shares outstanding
* - vault withdrawal and clawback reduce assets and share issuance, and
* subtracts from: total assets, assets available, shares outstanding
* - vault set must not alter the vault assets or shares balance
* - no vault transaction can change loss unrealized (it's updated by loan
* transactions)
* @brief Protocol-level vault invariants.
*
* These checks apply to every transaction that touches a vault, regardless of
* transaction type. Transaction-specific invariants live in each transactor's
* finalizeInvariants method.
*/
class ValidVault
{
Number static constexpr zero{};
struct Vault final
{
uint256 key = beast::zero;
Asset asset;
AccountID pseudoId;
AccountID owner;
uint192 shareMPTID = beast::zero;
Number assetsTotal = 0;
Number assetsAvailable = 0;
Number assetsMaximum = 0;
Number lossUnrealized = 0;
Vault static make(SLE const&);
};
struct Shares final
{
MPTIssue share;
std::uint64_t sharesTotal = 0;
std::uint64_t sharesMaximum = 0;
Shares static make(SLE const&);
};
VaultInvariantData data_;
public:
struct DeltaInfo final
{
Number delta = numZero;
std::optional<int> scale;
using DeltaInfo = VaultInvariantData::DeltaInfo;
// Compute the delta between two Numbers, taking the coarsest scale
[[nodiscard]] static DeltaInfo
makeDelta(Number const& before, Number const& after, Asset const& asset);
};
private:
std::vector<Vault> afterVault_;
std::vector<Shares> afterMPTs_;
std::vector<Vault> beforeVault_;
std::vector<Shares> beforeMPTs_;
std::unordered_map<uint256, DeltaInfo> deltas_;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
// Compute the coarsest scale required to represent all numbers
[[nodiscard]] static std::int32_t
computeCoarsestScale(std::vector<DeltaInfo> const& numbers);
computeCoarsestScale(std::vector<DeltaInfo> const& numbers)
{
return VaultInvariantData::computeCoarsestScale(numbers);
}
};
} // namespace xrpl

View File

@@ -1,11 +1,14 @@
#pragma once
#include <xrpl/tx/Transactor.h>
#include <xrpl/tx/transactors/vault/VaultInvariantData.h>
namespace xrpl {
class VaultClawback : public Transactor
{
VaultInvariantData invariantData_;
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};

View File

@@ -1,11 +1,14 @@
#pragma once
#include <xrpl/tx/Transactor.h>
#include <xrpl/tx/transactors/vault/VaultInvariantData.h>
namespace xrpl {
class VaultCreate : public Transactor
{
VaultInvariantData invariantData_;
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};

View File

@@ -1,11 +1,14 @@
#pragma once
#include <xrpl/tx/Transactor.h>
#include <xrpl/tx/transactors/vault/VaultInvariantData.h>
namespace xrpl {
class VaultDelete : public Transactor
{
VaultInvariantData invariantData_;
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};

View File

@@ -1,11 +1,14 @@
#pragma once
#include <xrpl/tx/Transactor.h>
#include <xrpl/tx/transactors/vault/VaultInvariantData.h>
namespace xrpl {
class VaultDeposit : public Transactor
{
VaultInvariantData invariantData_;
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};

View File

@@ -0,0 +1,101 @@
#pragma once
#include <xrpl/basics/Number.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/XRPAmount.h>
#include <optional>
#include <unordered_map>
#include <vector>
namespace xrpl {
class VaultInvariantData
{
public:
struct Vault final
{
uint256 key = beast::zero;
Asset asset;
AccountID pseudoId;
AccountID owner;
uint192 shareMPTID = beast::zero;
Number assetsTotal = 0;
Number assetsAvailable = 0;
Number assetsMaximum = 0;
Number lossUnrealized = 0;
Vault static make(SLE const&);
};
struct Shares final
{
MPTIssue share;
std::uint64_t sharesTotal = 0;
std::uint64_t sharesMaximum = 0;
Shares static make(SLE const&);
};
struct DeltaInfo final
{
Number delta = numZero;
std::optional<int> scale;
// Compute the delta between two Numbers, taking the coarsest scale
[[nodiscard]] static DeltaInfo
makeDelta(Number const& before, Number const& after, Asset const& asset);
};
void
visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after);
[[nodiscard]] std::optional<DeltaInfo>
deltaAssets(Asset const& vaultAsset, AccountID const& id) const;
[[nodiscard]] std::optional<DeltaInfo>
deltaAssetsTxAccount(
AccountID const& account,
std::optional<AccountID> const& delegate,
Asset const& vaultAsset,
XRPAmount fee) const;
[[nodiscard]] std::optional<DeltaInfo>
deltaShares(AccountID const& pseudoId, uint192 const& shareMPTID, AccountID const& id) const;
[[nodiscard]] std::optional<Shares>
resolveUpdatedShares(Vault const& afterVault) const;
[[nodiscard]] std::optional<Shares>
resolveBeforeShares(Vault const& beforeVault) const;
[[nodiscard]] static bool
vaultHoldsNoAssets(Vault const& vault);
// Compute the coarsest scale required to represent all numbers
[[nodiscard]] static std::int32_t
computeCoarsestScale(std::vector<DeltaInfo> const& numbers);
[[nodiscard]] std::vector<Vault> const&
afterVault() const
{
return afterVault_;
}
[[nodiscard]] std::vector<Vault> const&
beforeVault() const
{
return beforeVault_;
}
private:
std::vector<Vault> afterVault_;
std::vector<Shares> afterMPTs_;
std::vector<Vault> beforeVault_;
std::vector<Shares> beforeMPTs_;
std::unordered_map<uint256, DeltaInfo> deltas_;
};
} // namespace xrpl

View File

@@ -1,11 +1,14 @@
#pragma once
#include <xrpl/tx/Transactor.h>
#include <xrpl/tx/transactors/vault/VaultInvariantData.h>
namespace xrpl {
class VaultSet : public Transactor
{
VaultInvariantData invariantData_;
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};

View File

@@ -1,11 +1,14 @@
#pragma once
#include <xrpl/tx/Transactor.h>
#include <xrpl/tx/transactors/vault/VaultInvariantData.h>
namespace xrpl {
class VaultWithdraw : public Transactor
{
VaultInvariantData invariantData_;
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};

File diff suppressed because it is too large Load Diff

View File

@@ -257,6 +257,9 @@ SignerListSet::validateQuorumAndSignerEntries(
}
// Make sure there are no duplicate signers.
// SignerEntry only defines operator< and operator==, not the full
// std::totally_ordered set required by std::ranges::less, so the
// ranges version does not compile. NOLINTNEXTLINE(modernize-use-ranges)
XRPL_ASSERT(
std::ranges::is_sorted(signers),
"xrpl::SignerListSet::validateQuorumAndSignerEntries : sorted "

View File

@@ -472,21 +472,124 @@ VaultClawback::doApply()
void
VaultClawback::visitInvariantEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&)
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
invariantData_.visitEntry(isDelete, before, after);
}
bool
VaultClawback::finalizeInvariants(
STTx const&,
TER,
XRPAmount,
ReadView const&,
beast::Journal const&)
STTx const& tx,
TER txResult,
XRPAmount fee,
ReadView const& view,
beast::Journal const& j)
{
return true;
// TODO: Invariants should run for failed transactions too, but skipping
// here preserves the behaviour from before the refactoring.
if (!isTesSuccess(txResult))
return true;
if (invariantData_.beforeVault().empty() || invariantData_.afterVault().empty())
{
JLOG(j.fatal()) << "Invariant failed: vault operation succeeded without modifying a vault";
return false;
}
auto const& beforeVault = invariantData_.beforeVault()[0];
auto const& afterVault = invariantData_.afterVault()[0];
auto const& vaultAsset = afterVault.asset;
auto const beforeShares = invariantData_.resolveBeforeShares(beforeVault);
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
if (!(beforeShares && beforeShares->sharesTotal > 0 &&
VaultInvariantData::vaultHoldsNoAssets(beforeVault) &&
beforeVault.owner == tx[sfAccount]))
{
JLOG(j.fatal()) << "Invariant failed: clawback may only be performed by the asset "
"issuer, or by the vault owner of an empty vault";
return false;
}
}
auto result = true;
auto const maybeVaultDeltaAssets = invariantData_.deltaAssets(vaultAsset, afterVault.pseudoId);
if (maybeVaultDeltaAssets)
{
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;
}
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;
}
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;
}
}
else if (!VaultInvariantData::vaultHoldsNoAssets(beforeVault))
{
JLOG(j.fatal()) << "Invariant failed: clawback must change vault balance";
return false;
}
// We don't need to round shares, they are integral MPT.
auto const maybeAccountDeltaShares =
invariantData_.deltaShares(afterVault.pseudoId, afterVault.shareMPTID, tx[sfHolder]);
if (!maybeAccountDeltaShares)
{
JLOG(j.fatal()) << "Invariant failed: clawback must change holder shares";
return false;
}
if (maybeAccountDeltaShares->delta >= beast::zero)
{
JLOG(j.fatal()) << "Invariant failed: clawback must decrease holder shares";
result = false;
}
auto const vaultDeltaShares =
invariantData_.deltaShares(afterVault.pseudoId, afterVault.shareMPTID, afterVault.pseudoId);
if (!vaultDeltaShares || vaultDeltaShares->delta == beast::zero)
{
JLOG(j.fatal()) << "Invariant failed: clawback must change vault shares";
return false;
}
if (vaultDeltaShares->delta * -1 != maybeAccountDeltaShares->delta)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback must change holder and vault shares by equal amount";
result = false;
}
return result;
}
} // namespace xrpl

View File

@@ -1,8 +1,5 @@
#include <xrpl/tx/transactors/vault/VaultCreate.h>
#include <xrpl/basics/Number.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/core/ServiceRegistry.h>
//
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
@@ -21,14 +18,10 @@
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/Transactor.h>
#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
#include <xrpl/tx/transactors/token/MPTokenAuthorize.h>
#include <xrpl/tx/transactors/token/MPTokenIssuanceCreate.h>
#include <cstdint>
#include <memory>
#include <optional>
namespace xrpl {
bool
@@ -251,16 +244,88 @@ VaultCreate::doApply()
void
VaultCreate::visitInvariantEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&)
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
invariantData_.visitEntry(isDelete, before, after);
}
bool
VaultCreate::finalizeInvariants(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&)
VaultCreate::finalizeInvariants(
STTx const&,
TER txResult,
XRPAmount,
ReadView const& view,
beast::Journal const& j)
{
return true;
// TODO: Invariants should run for failed transactions too, but skipping
// here preserves the behaviour from before the refactoring.
if (!isTesSuccess(txResult))
return true;
auto result = true;
if (!invariantData_.beforeVault().empty())
{
JLOG(j.fatal()) << //
"Invariant failed: create operation must not have updated a vault";
result = false;
}
if (invariantData_.afterVault().empty())
{
JLOG(j.fatal()) << //
"Invariant failed: vault operation succeeded without modifying a vault";
return false;
}
auto const& afterVault = invariantData_.afterVault()[0];
auto const updatedShares = [&]() -> std::optional<VaultInvariantData::Shares> {
if (auto s = invariantData_.resolveUpdatedShares(afterVault))
return s;
auto const sle = view.read(keylet::mptIssuance(afterVault.shareMPTID));
return sle ? std::optional(VaultInvariantData::Shares::make(*sle)) : std::nullopt;
}();
if (!updatedShares)
{
JLOG(j.fatal()) << "Invariant failed: updated vault must have shares";
return false;
}
if (afterVault.assetsAvailable != beast::zero || afterVault.assetsTotal != beast::zero ||
afterVault.lossUnrealized != beast::zero || updatedShares->sharesTotal != 0)
{
JLOG(j.fatal()) << "Invariant failed: created vault must be empty";
result = false;
}
if (afterVault.pseudoId != updatedShares->share.getIssuer())
{
JLOG(j.fatal()) << //
"Invariant failed: shares issuer and vault pseudo-account must be the same";
result = false;
}
auto const sleSharesIssuer = view.read(keylet::account(updatedShares->share.getIssuer()));
if (!sleSharesIssuer)
{
JLOG(j.fatal()) << "Invariant failed: shares issuer must exist";
return false;
}
if (!isPseudoAccount(sleSharesIssuer))
{
JLOG(j.fatal()) << "Invariant failed: shares issuer must be a pseudo-account";
result = false;
}
if (auto const vaultId = (*sleSharesIssuer)[~sfVaultID]; !vaultId || *vaultId != afterVault.key)
{
JLOG(j.fatal()) << //
"Invariant failed: shares issuer pseudo-account must point back to the vault";
result = false;
}
return result;
}
} // namespace xrpl

View File

@@ -214,16 +214,72 @@ VaultDelete::doApply()
void
VaultDelete::visitInvariantEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&)
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
invariantData_.visitEntry(isDelete, before, after);
}
bool
VaultDelete::finalizeInvariants(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&)
VaultDelete::finalizeInvariants(
STTx const&,
TER txResult,
XRPAmount,
ReadView const&,
beast::Journal const& j)
{
return true;
// TODO: Invariants should run for failed transactions too, but skipping
// here preserves the behaviour from before the refactoring.
if (!isTesSuccess(txResult))
return true;
// VaultDelete must have a before state and no after state
if (invariantData_.beforeVault().empty())
{
JLOG(j.fatal()) << //
"Invariant failed: vault deletion succeeded without modifying a vault";
return false;
}
if (!invariantData_.afterVault().empty())
{
JLOG(j.fatal()) << //
"Invariant failed: vault deletion succeeded without deleting a vault";
return false;
}
auto const& beforeVault = invariantData_.beforeVault()[0];
// Find the deleted shares matching this vault
auto const deletedShares = invariantData_.resolveBeforeShares(beforeVault);
if (!deletedShares)
{
JLOG(j.fatal()) << "Invariant failed: deleted vault must also delete shares";
return false;
}
auto result = true;
if (deletedShares->sharesTotal != 0)
{
JLOG(j.fatal()) << //
"Invariant failed: deleted vault must have no shares outstanding";
result = false;
}
if (beforeVault.assetsTotal != beast::zero)
{
JLOG(j.fatal()) << //
"Invariant failed: deleted vault must have no assets outstanding";
result = false;
}
if (beforeVault.assetsAvailable != beast::zero)
{
JLOG(j.fatal()) << //
"Invariant failed: deleted vault must have no assets available";
result = false;
}
return result;
}
} // namespace xrpl

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>
@@ -280,21 +282,165 @@ VaultDeposit::doApply()
void
VaultDeposit::visitInvariantEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&)
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
invariantData_.visitEntry(isDelete, before, after);
}
bool
VaultDeposit::finalizeInvariants(
STTx const&,
TER,
XRPAmount,
ReadView const&,
beast::Journal const&)
STTx const& tx,
TER txResult,
XRPAmount fee,
ReadView const& view,
beast::Journal const& j)
{
return true;
// TODO: Invariants should run for failed transactions too, but skipping
// here preserves the behaviour from before the refactoring.
if (!isTesSuccess(txResult))
return true;
if (invariantData_.beforeVault().empty() || invariantData_.afterVault().empty())
{
JLOG(j.fatal()) << //
"Invariant failed: vault operation succeeded without modifying a vault";
return false;
}
auto result = true;
auto const& beforeVault = invariantData_.beforeVault()[0];
auto const& afterVault = invariantData_.afterVault()[0];
auto const& vaultAsset = afterVault.asset;
auto const maybeVaultDeltaAssets = invariantData_.deltaAssets(vaultAsset, afterVault.pseudoId);
if (!maybeVaultDeltaAssets)
{
JLOG(j.fatal()) << "Invariant failed: deposit must change vault balance";
return false;
}
// 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)
{
JLOG(j.fatal()) << "Invariant failed: deposit must increase vault balance";
result = false;
}
// Any payments (including deposits) made by the issuer
// do not change their balance, but create funds instead.
bool const issuerDeposit = [&]() -> bool {
if (vaultAsset.native())
return false;
return tx[sfAccount] == vaultAsset.getIssuer();
}();
if (!issuerDeposit)
{
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}));
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 (localVaultDeltaAssets * -1 != accountDeltaAssets)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must change vault and depositor balance by equal amount";
result = false;
}
}
if (afterVault.assetsMaximum > beast::zero && afterVault.assetsTotal > afterVault.assetsMaximum)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit assets outstanding must not exceed assets maximum";
result = false;
}
// We don't need to round shares, they are integral MPT.
auto const maybeAccDeltaShares =
invariantData_.deltaShares(afterVault.pseudoId, afterVault.shareMPTID, tx[sfAccount]);
if (!maybeAccDeltaShares)
{
JLOG(j.fatal()) << "Invariant failed: deposit must change depositor shares";
return false;
}
auto const& accountDeltaShares = *maybeAccDeltaShares;
if (accountDeltaShares.delta <= beast::zero)
{
JLOG(j.fatal()) << "Invariant failed: deposit must increase depositor shares";
result = false;
}
auto const maybeVaultDeltaShares =
invariantData_.deltaShares(afterVault.pseudoId, afterVault.shareMPTID, afterVault.pseudoId);
if (!maybeVaultDeltaShares || maybeVaultDeltaShares->delta == beast::zero)
{
JLOG(j.fatal()) << "Invariant failed: deposit must change vault shares";
return false;
}
auto const& vaultDeltaShares = *maybeVaultDeltaShares;
if (vaultDeltaShares.delta * -1 != accountDeltaShares.delta)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must change depositor and vault shares by equal amount";
result = false;
}
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;
}
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;
}
return result;
}
} // namespace xrpl

View File

@@ -0,0 +1,293 @@
#include <xrpl/tx/transactors/vault/VaultInvariantData.h>
//
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STNumber.h>
#include <algorithm>
namespace xrpl {
VaultInvariantData::Vault
VaultInvariantData::Vault::make(SLE const& from)
{
XRPL_ASSERT(from.getType() == ltVAULT, "VaultInvariantData::Vault::make : from Vault object");
VaultInvariantData::Vault self;
self.key = from.key();
self.asset = from.at(sfAsset);
self.pseudoId = from.getAccountID(sfAccount);
self.owner = from.at(sfOwner);
self.shareMPTID = from.getFieldH192(sfShareMPTID);
self.assetsTotal = from.at(sfAssetsTotal);
self.assetsAvailable = from.at(sfAssetsAvailable);
self.assetsMaximum = from.at(sfAssetsMaximum);
self.lossUnrealized = from.at(sfLossUnrealized);
return self;
}
VaultInvariantData::Shares
VaultInvariantData::Shares::make(SLE const& from)
{
XRPL_ASSERT(
from.getType() == ltMPTOKEN_ISSUANCE,
"VaultInvariantData::Shares::make : from MPTokenIssuance object");
VaultInvariantData::Shares self;
self.share = MPTIssue(makeMptID(from.getFieldU32(sfSequence), from.getAccountID(sfIssuer)));
self.sharesTotal = from.at(sfOutstandingAmount);
self.sharesMaximum = from[~sfMaximumAmount].value_or(maxMPTokenAmount);
return self;
}
[[nodiscard]] VaultInvariantData::DeltaInfo
VaultInvariantData::DeltaInfo::makeDelta(
Number const& before,
Number const& after,
Asset const& asset)
{
return {
.delta = after - before,
.scale = std::max(xrpl::scale(after, asset), xrpl::scale(before, asset))};
}
[[nodiscard]] std::int32_t
VaultInvariantData::computeCoarsestScale(std::vector<DeltaInfo> const& numbers)
{
if (numbers.empty())
return 0;
auto const max = std::ranges::max_element(
numbers, [](auto const& a, auto const& b) -> bool { return a.scale < b.scale; });
XRPL_ASSERT_PARTS(
max->scale,
"xrpl::VaultInvariantData::computeCoarsestScale",
"scale set for destinationDelta");
return max->scale.value_or(STAmount::cMaxOffset);
}
void
VaultInvariantData::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
{
// If `before` is empty, this means an object is being created, in which
// case `isDelete` must be false. Otherwise `before` and `after` are set and
// `isDelete` indicates whether an object is being deleted or modified.
XRPL_ASSERT(
after != nullptr && (before != nullptr || !isDelete),
"xrpl::VaultInvariantData::visitEntry : some object is available");
// Number balanceDelta will capture the difference (delta) between "before"
// state (zero if created) and "after" state (zero if destroyed), and
// preserves value scale (exponent) to round values to the same scale during
// validation. It is used to validate that the change in account balances
// matches the change in vault balances, stored to deltas_ at the end of
// this function.
DeltaInfo balanceDelta{.delta = numZero, .scale = std::nullopt};
std::int8_t sign = 0;
if (before)
{
switch (before->getType())
{
case ltVAULT:
beforeVault_.push_back(Vault::make(*before));
break;
case ltMPTOKEN_ISSUANCE:
// At this moment we have no way of telling if this object holds
// vault shares or something else. Save it for finalize.
beforeMPTs_.push_back(Shares::make(*before));
balanceDelta.delta =
static_cast<std::int64_t>(before->getFieldU64(sfOutstandingAmount));
// MPTs are ints, so the scale is always 0.
balanceDelta.scale = 0;
sign = 1;
break;
case ltMPTOKEN:
balanceDelta.delta = static_cast<std::int64_t>(before->getFieldU64(sfMPTAmount));
// MPTs are ints, so the scale is always 0.
balanceDelta.scale = 0;
sign = -1;
break;
case ltACCOUNT_ROOT:
balanceDelta.delta = before->getFieldAmount(sfBalance);
// Account balance is XRP, which is an int, so the scale is
// always 0.
balanceDelta.scale = 0;
sign = -1;
break;
case ltRIPPLE_STATE: {
auto const amount = before->getFieldAmount(sfBalance);
balanceDelta.delta = amount;
// Trust Line balances are STAmounts, so we can use the exponent
// directly to get the scale.
balanceDelta.scale = amount.exponent();
sign = -1;
break;
}
default:;
}
}
if (!isDelete && after)
{
switch (after->getType())
{
case ltVAULT:
afterVault_.push_back(Vault::make(*after));
break;
case ltMPTOKEN_ISSUANCE:
// 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.delta -=
Number(static_cast<std::int64_t>(after->getFieldU64(sfOutstandingAmount)));
// MPTs are ints, so the scale is always 0.
balanceDelta.scale = 0;
sign = 1;
break;
case ltMPTOKEN:
balanceDelta.delta -=
Number(static_cast<std::int64_t>(after->getFieldU64(sfMPTAmount)));
// MPTs are ints, so the scale is always 0.
balanceDelta.scale = 0;
sign = -1;
break;
case ltACCOUNT_ROOT:
balanceDelta.delta -= Number(after->getFieldAmount(sfBalance));
// Account balance is XRP, which is an int, so the scale is
// always 0.
balanceDelta.scale = 0;
sign = -1;
break;
case ltRIPPLE_STATE: {
auto const amount = after->getFieldAmount(sfBalance);
balanceDelta.delta -= Number(amount);
// Trust Line balances are STAmounts, so we can use the exponent
// directly to get the scale.
if (amount.exponent() > balanceDelta.scale)
balanceDelta.scale = amount.exponent();
sign = -1;
break;
}
default:;
}
}
uint256 const key = (before ? before->key() : after->key());
// Append to deltas if sign is non-zero, i.e. an object of an interesting
// type has been updated. A transaction may update an object even when
// its balance has not changed, e.g. transaction fee equals the amount
// transferred to the account. We intentionally do not compare balanceDelta
// against zero, to avoid missing such updates.
if (sign != 0)
{
XRPL_ASSERT_PARTS(
balanceDelta.scale, "xrpl::VaultInvariantData::visitEntry", "scale initialized");
balanceDelta.delta *= sign;
deltas_[key] = balanceDelta;
}
}
std::optional<VaultInvariantData::DeltaInfo>
VaultInvariantData::deltaAssets(Asset const& vaultAsset, AccountID const& id) const
{
auto const get = //
[&](auto const& it, std::int8_t sign = 1) -> std::optional<DeltaInfo> {
if (it == deltas_.end())
return std::nullopt;
return DeltaInfo{it->second.delta * sign, it->second.scale};
};
return std::visit(
[&]<typename TIss>(TIss const& issue) {
if constexpr (std::is_same_v<TIss, Issue>)
{
if (isXRP(issue))
return get(deltas_.find(keylet::account(id).key));
return get(
deltas_.find(keylet::line(id, issue).key), id > issue.getIssuer() ? -1 : 1);
}
else if constexpr (std::is_same_v<TIss, MPTIssue>)
{
return get(deltas_.find(keylet::mptoken(issue.getMptID(), id).key));
}
},
vaultAsset.value());
}
std::optional<VaultInvariantData::DeltaInfo>
VaultInvariantData::deltaAssetsTxAccount(
AccountID const& account,
std::optional<AccountID> const& delegate,
Asset const& vaultAsset,
XRPAmount fee) const
{
auto ret = deltaAssets(vaultAsset, account);
// Nothing returned or not XRP transaction
if (!ret.has_value() || !vaultAsset.native())
return ret;
// Delegated transaction; no need to compensate for fees
if (delegate.has_value() && *delegate != account)
return ret;
ret->delta += fee.drops();
if (ret->delta == beast::zero)
return std::nullopt;
return ret;
}
std::optional<VaultInvariantData::DeltaInfo>
VaultInvariantData::deltaShares(
AccountID const& pseudoId,
uint192 const& shareMPTID,
AccountID const& id) const
{
auto const it = [&]() {
if (id == pseudoId)
return deltas_.find(keylet::mptIssuance(shareMPTID).key);
return deltas_.find(keylet::mptoken(shareMPTID, id).key);
}();
return it != deltas_.end() ? std::optional<DeltaInfo>(it->second) : std::nullopt;
}
std::optional<VaultInvariantData::Shares>
VaultInvariantData::resolveUpdatedShares(Vault const& afterVault) const
{
// Find the shares MPTokenIssuance that was modified in the same
// transaction. Note, we expect afterMPTs_ to be extremely small.
// For such collections linear search is faster than lookup.
for (auto const& e : afterMPTs_)
{
if (e.share.getMptID() == afterVault.shareMPTID)
return e;
}
return std::nullopt;
}
std::optional<VaultInvariantData::Shares>
VaultInvariantData::resolveBeforeShares(Vault const& beforeVault) const
{
for (auto const& e : beforeMPTs_)
{
if (e.share.getMptID() == beforeVault.shareMPTID)
return e;
}
return std::nullopt;
}
bool
VaultInvariantData::vaultHoldsNoAssets(Vault const& vault)
{
return vault.assetsAvailable == 0 && vault.assetsTotal == 0;
}
} // namespace xrpl

View File

@@ -181,16 +181,77 @@ VaultSet::doApply()
void
VaultSet::visitInvariantEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&)
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
invariantData_.visitEntry(isDelete, before, after);
}
bool
VaultSet::finalizeInvariants(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&)
VaultSet::finalizeInvariants(
STTx const&,
TER txResult,
XRPAmount,
ReadView const& view,
beast::Journal const& j)
{
return true;
// TODO: Invariants should run for failed transactions too, but skipping
// here preserves the behaviour from before the refactoring.
if (!isTesSuccess(txResult))
return true;
auto result = true;
if (invariantData_.beforeVault().empty() || invariantData_.afterVault().empty())
{
JLOG(j.fatal()) << //
"Invariant failed: vault operation succeeded without modifying a vault";
return false;
}
auto const& afterVault = invariantData_.afterVault()[0];
auto const& beforeVault = invariantData_.beforeVault()[0];
auto const vaultDeltaAssets = invariantData_.deltaAssets(afterVault.asset, afterVault.pseudoId);
if (vaultDeltaAssets)
{
JLOG(j.fatal()) << "Invariant failed: set must not change vault balance";
result = false;
}
if (beforeVault.assetsTotal != afterVault.assetsTotal)
{
JLOG(j.fatal()) << "Invariant failed: set must not change assets outstanding";
result = false;
}
if (beforeVault.assetsAvailable != afterVault.assetsAvailable)
{
JLOG(j.fatal()) << "Invariant failed: set must not change assets available";
result = false;
}
if (afterVault.assetsMaximum > beast::zero && afterVault.assetsTotal > afterVault.assetsMaximum)
{
JLOG(j.fatal()) << //
"Invariant failed: set assets outstanding must not exceed assets maximum";
result = false;
}
auto const beforeShares = invariantData_.resolveBeforeShares(invariantData_.beforeVault()[0]);
auto const updatedShares = [&]() -> std::optional<VaultInvariantData::Shares> {
if (auto s = invariantData_.resolveUpdatedShares(afterVault))
return s;
auto const sle = view.read(keylet::mptIssuance(afterVault.shareMPTID));
return sle ? std::optional(VaultInvariantData::Shares::make(*sle)) : std::nullopt;
}();
if (beforeShares && updatedShares && beforeShares->sharesTotal != updatedShares->sharesTotal)
{
JLOG(j.fatal()) << "Invariant failed: set must not change shares outstanding";
result = false;
}
return result;
}
} // namespace xrpl

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>
@@ -297,21 +299,165 @@ VaultWithdraw::doApply()
void
VaultWithdraw::visitInvariantEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&)
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
invariantData_.visitEntry(isDelete, before, after);
}
bool
VaultWithdraw::finalizeInvariants(
STTx const&,
TER,
XRPAmount,
ReadView const&,
beast::Journal const&)
STTx const& tx,
TER txResult,
XRPAmount fee,
ReadView const& view,
beast::Journal const& j)
{
return true;
// TODO: Invariants should run for failed transactions too, but skipping
// here preserves the behaviour from before the refactoring.
if (!isTesSuccess(txResult))
return true;
if (invariantData_.beforeVault().empty() || invariantData_.afterVault().empty())
{
JLOG(j.fatal()) << //
"Invariant failed: vault operation succeeded without modifying a vault";
return false;
}
auto result = true;
auto const& beforeVault = invariantData_.beforeVault()[0];
auto const& afterVault = invariantData_.afterVault()[0];
auto const& vaultAsset = afterVault.asset;
auto const maybeVaultDeltaAssets = invariantData_.deltaAssets(vaultAsset, afterVault.pseudoId);
if (!maybeVaultDeltaAssets)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal must change vault balance";
return false;
}
// 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;
}
// Any payments (including withdrawal) going to the issuer
// do not change their balance, but destroy funds instead.
bool const issuerWithdrawal = [&]() -> bool {
if (vaultAsset.native())
return false;
auto const destination = tx[~sfDestination].value_or(tx[sfAccount]);
return destination == vaultAsset.getIssuer();
}();
if (!issuerWithdrawal)
{
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(vaultAsset, *destination);
return std::nullopt;
}();
if (maybeAccDelta.has_value() == maybeOtherAccDelta.has_value())
{
JLOG(j.fatal()) << //
"Invariant failed: withdrawal must change one destination balance";
return false;
}
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, 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;
}
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 "
"amount";
result = false;
}
}
// We don't need to round shares, they are integral MPT.
auto const accountDeltaShares =
invariantData_.deltaShares(afterVault.pseudoId, afterVault.shareMPTID, tx[sfAccount]);
if (!accountDeltaShares)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal must change depositor shares";
return false;
}
if (accountDeltaShares->delta >= beast::zero)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal must decrease depositor shares";
result = false;
}
auto const vaultDeltaShares =
invariantData_.deltaShares(afterVault.pseudoId, afterVault.shareMPTID, afterVault.pseudoId);
if (!vaultDeltaShares || vaultDeltaShares->delta == beast::zero)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal must change vault shares";
return false;
}
if (vaultDeltaShares->delta * -1 != accountDeltaShares->delta)
{
JLOG(j.fatal()) << //
"Invariant failed: withdrawal must change depositor and vault shares by equal amount";
result = false;
}
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;
}
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;
}
return result;
}
} // namespace xrpl

View File

@@ -3123,7 +3123,6 @@ class Invariants_test : public beast::unit_test::suite
env(tx);
return true;
});
doInvariantCheck(
{
"created vault must be empty",