mirror of
https://github.com/XRPLF/rippled.git
synced 2026-04-29 15:37:57 +00:00
Compare commits
34 Commits
copilot/re
...
tapanito/t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fff81a722 | ||
|
|
a444d80b45 | ||
|
|
6167c70043 | ||
|
|
dd654f649e | ||
|
|
0365449598 | ||
|
|
3f569f4cca | ||
|
|
cdef4f3f2c | ||
|
|
4b9e3997f8 | ||
|
|
273f8a13cd | ||
|
|
50aa2603ec | ||
|
|
c84cab12ae | ||
|
|
96f60d6def | ||
|
|
687d9489d7 | ||
|
|
99d745d4a2 | ||
|
|
e28d5e5b50 | ||
|
|
da44fdb5d3 | ||
|
|
9e9fdb40a8 | ||
|
|
1664669bcc | ||
|
|
e873e17d2b | ||
|
|
a62ac8b32f | ||
|
|
05738afbb9 | ||
|
|
b5b97bc3e6 | ||
|
|
1bf045d2bd | ||
|
|
009e05a463 | ||
|
|
ce92da9161 | ||
|
|
c59683d07c | ||
|
|
e3d9e06345 | ||
|
|
038e50abed | ||
|
|
34804eb53a | ||
|
|
08f70c85d4 | ||
|
|
b6e792cede | ||
|
|
23e117bde7 | ||
|
|
40ee1e1ff3 | ||
|
|
10cb46c3f0 |
@@ -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
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
101
include/xrpl/tx/transactors/vault/VaultInvariantData.h
Normal file
101
include/xrpl/tx/transactors/vault/VaultInvariantData.h
Normal 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
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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
@@ -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 "
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
293
src/libxrpl/tx/transactors/vault/VaultInvariantData.cpp
Normal file
293
src/libxrpl/tx/transactors/vault/VaultInvariantData.cpp
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3123,7 +3123,6 @@ class Invariants_test : public beast::unit_test::suite
|
||||
env(tx);
|
||||
return true;
|
||||
});
|
||||
|
||||
doInvariantCheck(
|
||||
{
|
||||
"created vault must be empty",
|
||||
|
||||
Reference in New Issue
Block a user