mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
Add vault invariants (#5518)
This change adds invariants for SingleAssetVault #5224 (XLS-065), which had been intentionally skipped earlier to keep the SAV PR size manageable.
This commit is contained in:
@@ -851,7 +851,7 @@ TRANSACTION(ttDELEGATE_SET, 64, DelegateSet,
|
|||||||
TRANSACTION(ttVAULT_CREATE, 65, VaultCreate,
|
TRANSACTION(ttVAULT_CREATE, 65, VaultCreate,
|
||||||
Delegation::delegatable,
|
Delegation::delegatable,
|
||||||
featureSingleAssetVault,
|
featureSingleAssetVault,
|
||||||
createPseudoAcct | createMPTIssuance,
|
createPseudoAcct | createMPTIssuance | mustModifyVault,
|
||||||
({
|
({
|
||||||
{sfAsset, soeREQUIRED, soeMPTSupported},
|
{sfAsset, soeREQUIRED, soeMPTSupported},
|
||||||
{sfAssetsMaximum, soeOPTIONAL},
|
{sfAssetsMaximum, soeOPTIONAL},
|
||||||
@@ -869,7 +869,7 @@ TRANSACTION(ttVAULT_CREATE, 65, VaultCreate,
|
|||||||
TRANSACTION(ttVAULT_SET, 66, VaultSet,
|
TRANSACTION(ttVAULT_SET, 66, VaultSet,
|
||||||
Delegation::delegatable,
|
Delegation::delegatable,
|
||||||
featureSingleAssetVault,
|
featureSingleAssetVault,
|
||||||
noPriv,
|
mustModifyVault,
|
||||||
({
|
({
|
||||||
{sfVaultID, soeREQUIRED},
|
{sfVaultID, soeREQUIRED},
|
||||||
{sfAssetsMaximum, soeOPTIONAL},
|
{sfAssetsMaximum, soeOPTIONAL},
|
||||||
@@ -884,7 +884,7 @@ TRANSACTION(ttVAULT_SET, 66, VaultSet,
|
|||||||
TRANSACTION(ttVAULT_DELETE, 67, VaultDelete,
|
TRANSACTION(ttVAULT_DELETE, 67, VaultDelete,
|
||||||
Delegation::delegatable,
|
Delegation::delegatable,
|
||||||
featureSingleAssetVault,
|
featureSingleAssetVault,
|
||||||
mustDeleteAcct | destroyMPTIssuance,
|
mustDeleteAcct | destroyMPTIssuance | mustModifyVault,
|
||||||
({
|
({
|
||||||
{sfVaultID, soeREQUIRED},
|
{sfVaultID, soeREQUIRED},
|
||||||
}))
|
}))
|
||||||
@@ -896,7 +896,7 @@ TRANSACTION(ttVAULT_DELETE, 67, VaultDelete,
|
|||||||
TRANSACTION(ttVAULT_DEPOSIT, 68, VaultDeposit,
|
TRANSACTION(ttVAULT_DEPOSIT, 68, VaultDeposit,
|
||||||
Delegation::delegatable,
|
Delegation::delegatable,
|
||||||
featureSingleAssetVault,
|
featureSingleAssetVault,
|
||||||
mayAuthorizeMPT,
|
mayAuthorizeMPT | mustModifyVault,
|
||||||
({
|
({
|
||||||
{sfVaultID, soeREQUIRED},
|
{sfVaultID, soeREQUIRED},
|
||||||
{sfAmount, soeREQUIRED, soeMPTSupported},
|
{sfAmount, soeREQUIRED, soeMPTSupported},
|
||||||
@@ -909,7 +909,7 @@ TRANSACTION(ttVAULT_DEPOSIT, 68, VaultDeposit,
|
|||||||
TRANSACTION(ttVAULT_WITHDRAW, 69, VaultWithdraw,
|
TRANSACTION(ttVAULT_WITHDRAW, 69, VaultWithdraw,
|
||||||
Delegation::delegatable,
|
Delegation::delegatable,
|
||||||
featureSingleAssetVault,
|
featureSingleAssetVault,
|
||||||
mayDeleteMPT,
|
mayDeleteMPT | mustModifyVault,
|
||||||
({
|
({
|
||||||
{sfVaultID, soeREQUIRED},
|
{sfVaultID, soeREQUIRED},
|
||||||
{sfAmount, soeREQUIRED, soeMPTSupported},
|
{sfAmount, soeREQUIRED, soeMPTSupported},
|
||||||
@@ -924,7 +924,7 @@ TRANSACTION(ttVAULT_WITHDRAW, 69, VaultWithdraw,
|
|||||||
TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback,
|
TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback,
|
||||||
Delegation::delegatable,
|
Delegation::delegatable,
|
||||||
featureSingleAssetVault,
|
featureSingleAssetVault,
|
||||||
mayDeleteMPT,
|
mayDeleteMPT | mustModifyVault,
|
||||||
({
|
({
|
||||||
{sfVaultID, soeREQUIRED},
|
{sfVaultID, soeREQUIRED},
|
||||||
{sfHolder, soeREQUIRED},
|
{sfHolder, soeREQUIRED},
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -177,6 +177,14 @@ class Vault_test : public beast::unit_test::suite
|
|||||||
env.close();
|
env.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
testcase(prefix + " set maximum is idempotent, set it again");
|
||||||
|
auto tx = vault.set({.owner = owner, .id = keylet.key});
|
||||||
|
tx[sfAssetsMaximum] = asset(150).number();
|
||||||
|
env(tx);
|
||||||
|
env.close();
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
testcase(prefix + " set data");
|
testcase(prefix + " set data");
|
||||||
auto tx = vault.set({.owner = owner, .id = keylet.key});
|
auto tx = vault.set({.owner = owner, .id = keylet.key});
|
||||||
@@ -218,6 +226,7 @@ class Vault_test : public beast::unit_test::suite
|
|||||||
.id = keylet.key,
|
.id = keylet.key,
|
||||||
.amount = asset(1000)});
|
.amount = asset(1000)});
|
||||||
env(tx, ter(tecINSUFFICIENT_FUNDS));
|
env(tx, ter(tecINSUFFICIENT_FUNDS));
|
||||||
|
env.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -385,6 +394,27 @@ class Vault_test : public beast::unit_test::suite
|
|||||||
env.balance(depositor, shares) == share(50 * scale));
|
env.balance(depositor, shares) == share(50 * scale));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!asset.raw().native())
|
||||||
|
{
|
||||||
|
testcase(prefix + " issuer deposits");
|
||||||
|
auto tx = vault.deposit(
|
||||||
|
{.depositor = issuer,
|
||||||
|
.id = keylet.key,
|
||||||
|
.amount = asset(10)});
|
||||||
|
env(tx);
|
||||||
|
env.close();
|
||||||
|
BEAST_EXPECT(env.balance(issuer, shares) == share(10 * scale));
|
||||||
|
|
||||||
|
testcase(prefix + " issuer withdraws");
|
||||||
|
tx = vault.withdraw(
|
||||||
|
{.depositor = issuer,
|
||||||
|
.id = keylet.key,
|
||||||
|
.amount = share(10 * scale)});
|
||||||
|
env(tx);
|
||||||
|
env.close();
|
||||||
|
BEAST_EXPECT(env.balance(issuer, shares) == share(0 * scale));
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
testcase(prefix + " withdraw remaining assets");
|
testcase(prefix + " withdraw remaining assets");
|
||||||
auto tx = vault.withdraw(
|
auto tx = vault.withdraw(
|
||||||
@@ -454,6 +484,8 @@ class Vault_test : public beast::unit_test::suite
|
|||||||
.amount = asset(10)});
|
.amount = asset(10)});
|
||||||
tx[sfDestination] = erin.human();
|
tx[sfDestination] = erin.human();
|
||||||
env(tx);
|
env(tx);
|
||||||
|
env.close();
|
||||||
|
|
||||||
// Erin returns assets to issuer
|
// Erin returns assets to issuer
|
||||||
env(pay(erin, issuer, asset(10)));
|
env(pay(erin, issuer, asset(10)));
|
||||||
env.close();
|
env.close();
|
||||||
@@ -479,12 +511,14 @@ class Vault_test : public beast::unit_test::suite
|
|||||||
testcase(prefix + " fail to delete because wrong owner");
|
testcase(prefix + " fail to delete because wrong owner");
|
||||||
auto tx = vault.del({.owner = issuer, .id = keylet.key});
|
auto tx = vault.del({.owner = issuer, .id = keylet.key});
|
||||||
env(tx, ter(tecNO_PERMISSION));
|
env(tx, ter(tecNO_PERMISSION));
|
||||||
|
env.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
testcase(prefix + " delete empty vault");
|
testcase(prefix + " delete empty vault");
|
||||||
auto tx = vault.del({.owner = owner, .id = keylet.key});
|
auto tx = vault.del({.owner = owner, .id = keylet.key});
|
||||||
env(tx);
|
env(tx);
|
||||||
|
env.close();
|
||||||
BEAST_EXPECT(!env.le(keylet));
|
BEAST_EXPECT(!env.le(keylet));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1328,6 +1362,26 @@ class Vault_test : public beast::unit_test::suite
|
|||||||
{
|
{
|
||||||
using namespace test::jtx;
|
using namespace test::jtx;
|
||||||
{
|
{
|
||||||
|
{
|
||||||
|
testcase("IOU fail because MPT is disabled");
|
||||||
|
Env env{
|
||||||
|
*this,
|
||||||
|
(testable_amendments() - featureMPTokensV1) |
|
||||||
|
featureSingleAssetVault};
|
||||||
|
Account issuer{"issuer"};
|
||||||
|
Account owner{"owner"};
|
||||||
|
env.fund(XRP(1000), issuer, owner);
|
||||||
|
env.close();
|
||||||
|
|
||||||
|
Vault vault{env};
|
||||||
|
Asset asset = issuer["IOU"].asset();
|
||||||
|
auto [tx, keylet] =
|
||||||
|
vault.create({.owner = owner, .asset = asset});
|
||||||
|
|
||||||
|
env(tx, ter(temDISABLED));
|
||||||
|
env.close();
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
testcase("IOU fail create frozen");
|
testcase("IOU fail create frozen");
|
||||||
Env env{*this, testable_amendments() | featureSingleAssetVault};
|
Env env{*this, testable_amendments() | featureSingleAssetVault};
|
||||||
@@ -2878,6 +2932,12 @@ class Vault_test : public beast::unit_test::suite
|
|||||||
tx[sfDomainID] = to_string(domainId);
|
tx[sfDomainID] = to_string(domainId);
|
||||||
env(tx);
|
env(tx);
|
||||||
env.close();
|
env.close();
|
||||||
|
|
||||||
|
// Should be idempotent
|
||||||
|
tx = vault.set({.owner = owner, .id = keylet.key});
|
||||||
|
tx[sfDomainID] = to_string(domainId);
|
||||||
|
env(tx);
|
||||||
|
env.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3033,6 +3093,7 @@ class Vault_test : public beast::unit_test::suite
|
|||||||
.id = keylet.key,
|
.id = keylet.key,
|
||||||
.amount = asset(50)});
|
.amount = asset(50)});
|
||||||
env(tx);
|
env(tx);
|
||||||
|
env.close();
|
||||||
|
|
||||||
tx = vault.clawback(
|
tx = vault.clawback(
|
||||||
{.issuer = issuer,
|
{.issuer = issuer,
|
||||||
@@ -3047,6 +3108,7 @@ class Vault_test : public beast::unit_test::suite
|
|||||||
.holder = owner,
|
.holder = owner,
|
||||||
.amount = asset(0)});
|
.amount = asset(0)});
|
||||||
env(tx);
|
env(tx);
|
||||||
|
env.close();
|
||||||
|
|
||||||
tx = vault.del({
|
tx = vault.del({
|
||||||
.owner = owner,
|
.owner = owner,
|
||||||
@@ -3093,6 +3155,7 @@ class Vault_test : public beast::unit_test::suite
|
|||||||
auto tx = vault.deposit(
|
auto tx = vault.deposit(
|
||||||
{.depositor = owner, .id = keylet.key, .amount = asset(50)});
|
{.depositor = owner, .id = keylet.key, .amount = asset(50)});
|
||||||
env(tx);
|
env(tx);
|
||||||
|
env.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,17 +24,26 @@
|
|||||||
#include <xrpld/app/tx/detail/PermissionedDomainSet.h>
|
#include <xrpld/app/tx/detail/PermissionedDomainSet.h>
|
||||||
|
|
||||||
#include <xrpl/basics/Log.h>
|
#include <xrpl/basics/Log.h>
|
||||||
|
#include <xrpl/beast/utility/instrumentation.h>
|
||||||
#include <xrpl/ledger/CredentialHelpers.h>
|
#include <xrpl/ledger/CredentialHelpers.h>
|
||||||
#include <xrpl/ledger/ReadView.h>
|
#include <xrpl/ledger/ReadView.h>
|
||||||
#include <xrpl/ledger/View.h>
|
#include <xrpl/ledger/View.h>
|
||||||
#include <xrpl/protocol/Feature.h>
|
#include <xrpl/protocol/Feature.h>
|
||||||
|
#include <xrpl/protocol/Indexes.h>
|
||||||
|
#include <xrpl/protocol/LedgerFormats.h>
|
||||||
|
#include <xrpl/protocol/MPTIssue.h>
|
||||||
|
#include <xrpl/protocol/SField.h>
|
||||||
#include <xrpl/protocol/STArray.h>
|
#include <xrpl/protocol/STArray.h>
|
||||||
#include <xrpl/protocol/STNumber.h>
|
#include <xrpl/protocol/STNumber.h>
|
||||||
#include <xrpl/protocol/SystemParameters.h>
|
#include <xrpl/protocol/SystemParameters.h>
|
||||||
|
#include <xrpl/protocol/TER.h>
|
||||||
#include <xrpl/protocol/TxFormats.h>
|
#include <xrpl/protocol/TxFormats.h>
|
||||||
#include <xrpl/protocol/Units.h>
|
#include <xrpl/protocol/Units.h>
|
||||||
#include <xrpl/protocol/nftPageMask.h>
|
#include <xrpl/protocol/nftPageMask.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
namespace ripple {
|
namespace ripple {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -78,6 +87,8 @@ enum Privilege {
|
|||||||
// object (except by issuer)
|
// object (except by issuer)
|
||||||
mayDeleteMPT =
|
mayDeleteMPT =
|
||||||
0x0400, // The transaction MAY delete an MPT object. May not create.
|
0x0400, // The transaction MAY delete an MPT object. May not create.
|
||||||
|
mustModifyVault =
|
||||||
|
0x0800, // The transaction must modify, delete or create, a vault
|
||||||
};
|
};
|
||||||
constexpr Privilege
|
constexpr Privilege
|
||||||
operator|(Privilege lhs, Privilege rhs)
|
operator|(Privilege lhs, Privilege rhs)
|
||||||
@@ -2170,4 +2181,943 @@ ValidAMM::finalize(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
ValidVault::Vault
|
||||||
|
ValidVault::Vault::make(SLE const& from)
|
||||||
|
{
|
||||||
|
XRPL_ASSERT(
|
||||||
|
from.getType() == ltVAULT,
|
||||||
|
"ValidVault::Vault::make : from Vault object");
|
||||||
|
|
||||||
|
ValidVault::Vault self;
|
||||||
|
self.key = from.key();
|
||||||
|
self.asset = from.at(sfAsset);
|
||||||
|
self.pseudoId = from.getAccountID(sfAccount);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidVault::Shares
|
||||||
|
ValidVault::Shares::make(SLE const& from)
|
||||||
|
{
|
||||||
|
XRPL_ASSERT(
|
||||||
|
from.getType() == ltMPTOKEN_ISSUANCE,
|
||||||
|
"ValidVault::Shares::make : from MPTokenIssuance object");
|
||||||
|
|
||||||
|
ValidVault::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;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
ValidVault::visitEntry(
|
||||||
|
bool isDelete,
|
||||||
|
std::shared_ptr<SLE const> const& before,
|
||||||
|
std::shared_ptr<SLE const> const& 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),
|
||||||
|
"ripple::ValidVault::visitEntry : some object is available");
|
||||||
|
|
||||||
|
// `Number balance` will capture the difference (delta) between "before"
|
||||||
|
// state (zero if created) and "after" state (zero if destroyed), so the
|
||||||
|
// invariants can validate that the change in account balances matches the
|
||||||
|
// change in vault balances, stored to deltas_ at the end of this function.
|
||||||
|
Number balance{};
|
||||||
|
|
||||||
|
// By default do not add anything to deltas
|
||||||
|
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));
|
||||||
|
balance = static_cast<std::int64_t>(
|
||||||
|
before->getFieldU64(sfOutstandingAmount));
|
||||||
|
sign = 1;
|
||||||
|
break;
|
||||||
|
case ltMPTOKEN:
|
||||||
|
balance =
|
||||||
|
static_cast<std::int64_t>(before->getFieldU64(sfMPTAmount));
|
||||||
|
sign = -1;
|
||||||
|
break;
|
||||||
|
case ltACCOUNT_ROOT:
|
||||||
|
case ltRIPPLE_STATE:
|
||||||
|
balance = before->getFieldAmount(sfBalance);
|
||||||
|
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));
|
||||||
|
balance -= Number(static_cast<std::int64_t>(
|
||||||
|
after->getFieldU64(sfOutstandingAmount)));
|
||||||
|
sign = 1;
|
||||||
|
break;
|
||||||
|
case ltMPTOKEN:
|
||||||
|
balance -= Number(
|
||||||
|
static_cast<std::int64_t>(after->getFieldU64(sfMPTAmount)));
|
||||||
|
sign = -1;
|
||||||
|
break;
|
||||||
|
case ltACCOUNT_ROOT:
|
||||||
|
case ltRIPPLE_STATE:
|
||||||
|
balance -= Number(after->getFieldAmount(sfBalance));
|
||||||
|
sign = -1;
|
||||||
|
break;
|
||||||
|
default:;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint256 const key = (before ? before->key() : after->key());
|
||||||
|
if (sign && balance != zero)
|
||||||
|
deltas_[key] = balance * sign;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
ValidVault::finalize(
|
||||||
|
STTx const& tx,
|
||||||
|
TER const ret,
|
||||||
|
XRPAmount const fee,
|
||||||
|
ReadView const& view,
|
||||||
|
beast::Journal const& j)
|
||||||
|
{
|
||||||
|
bool const enforce = view.rules().enabled(featureSingleAssetVault);
|
||||||
|
|
||||||
|
if (!isTesSuccess(ret))
|
||||||
|
return true; // Do not perform checks
|
||||||
|
|
||||||
|
if (afterVault_.empty() && beforeVault_.empty())
|
||||||
|
{
|
||||||
|
if (hasPrivilege(tx, mustModifyVault))
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: vault operation succeeded without modifying "
|
||||||
|
"a vault";
|
||||||
|
XRPL_ASSERT(
|
||||||
|
enforce, "ripple::ValidVault::finalize : vault noop invariant");
|
||||||
|
return !enforce;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true; // Not a vault operation
|
||||||
|
}
|
||||||
|
else if (!hasPrivilege(tx, mustModifyVault)) // TODO: mayModifyVault
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: vault updated by a wrong transaction type";
|
||||||
|
XRPL_ASSERT(
|
||||||
|
enforce,
|
||||||
|
"ripple::ValidVault::finalize : illegal vault transaction "
|
||||||
|
"invariant");
|
||||||
|
return !enforce; // Also not a vault operation
|
||||||
|
}
|
||||||
|
|
||||||
|
if (beforeVault_.size() > 1 || afterVault_.size() > 1)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: vault operation updated more than single vault";
|
||||||
|
XRPL_ASSERT(
|
||||||
|
enforce, "ripple::ValidVault::finalize : single vault invariant");
|
||||||
|
return !enforce; // That's all we can do here
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const txnType = tx.getTxnType();
|
||||||
|
|
||||||
|
// We do special handling for ttVAULT_DELETE first, because it's the only
|
||||||
|
// vault-modifying transaction without an "after" state of the vault
|
||||||
|
if (afterVault_.empty())
|
||||||
|
{
|
||||||
|
if (txnType != ttVAULT_DELETE)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: vault deleted by a wrong transaction type";
|
||||||
|
XRPL_ASSERT(
|
||||||
|
enforce,
|
||||||
|
"ripple::ValidVault::finalize : illegal vault deletion "
|
||||||
|
"invariant");
|
||||||
|
return !enforce; // That's all we can do here
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note, if afterVault_ is empty then we know that beforeVault_ is not
|
||||||
|
// empty, as enforced at the top of this function
|
||||||
|
auto const& beforeVault = beforeVault_[0];
|
||||||
|
|
||||||
|
// At this moment we only know a vault is being deleted and there
|
||||||
|
// might be some MPTokenIssuance objects which are deleted in the
|
||||||
|
// same transaction. Find the one matching this vault.
|
||||||
|
auto const deletedShares = [&]() -> std::optional<Shares> {
|
||||||
|
for (auto const& e : beforeMPTs_)
|
||||||
|
{
|
||||||
|
if (e.share.getMptID() == beforeVault.shareMPTID)
|
||||||
|
return std::move(e);
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}();
|
||||||
|
|
||||||
|
if (!deletedShares)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << "Invariant failed: deleted vault must also "
|
||||||
|
"delete shares";
|
||||||
|
XRPL_ASSERT(
|
||||||
|
enforce,
|
||||||
|
"ripple::ValidVault::finalize : shares deletion invariant");
|
||||||
|
return !enforce; // That's all we can do here
|
||||||
|
}
|
||||||
|
|
||||||
|
bool result = true;
|
||||||
|
if (deletedShares->sharesTotal != 0)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << "Invariant failed: deleted vault must have no "
|
||||||
|
"shares outstanding";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
if (beforeVault.assetsTotal != zero)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << "Invariant failed: deleted vault must have no "
|
||||||
|
"assets outstanding";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
if (beforeVault.assetsAvailable != zero)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << "Invariant failed: deleted vault must have no "
|
||||||
|
"assets available";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
else if (txnType == ttVAULT_DELETE)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << "Invariant failed: vault deletion succeeded without "
|
||||||
|
"deleting a vault";
|
||||||
|
XRPL_ASSERT(
|
||||||
|
enforce, "ripple::ValidVault::finalize : vault deletion invariant");
|
||||||
|
return !enforce; // That's all we can do here
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note, `afterVault_.empty()` is handled above
|
||||||
|
auto const& afterVault = afterVault_[0];
|
||||||
|
XRPL_ASSERT(
|
||||||
|
beforeVault_.empty() || beforeVault_[0].key == afterVault.key,
|
||||||
|
"ripple::ValidVault::finalize : single vault operation");
|
||||||
|
|
||||||
|
auto const updatedShares = [&]() -> std::optional<Shares> {
|
||||||
|
// At this moment we only know that a vault is being updated and there
|
||||||
|
// might be some MPTokenIssuance objects which are also updated in the
|
||||||
|
// same transaction. Find the one matching the shares to this vault.
|
||||||
|
// Note, we expect updatedMPTs collection 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const sleShares =
|
||||||
|
view.read(keylet::mptIssuance(afterVault.shareMPTID));
|
||||||
|
|
||||||
|
return sleShares ? std::optional<Shares>(Shares::make(*sleShares))
|
||||||
|
: std::nullopt;
|
||||||
|
}();
|
||||||
|
|
||||||
|
bool result = true;
|
||||||
|
|
||||||
|
// Universal transaction checks
|
||||||
|
if (!beforeVault_.empty())
|
||||||
|
{
|
||||||
|
auto const& beforeVault = beforeVault_[0];
|
||||||
|
if (afterVault.asset != beforeVault.asset ||
|
||||||
|
afterVault.pseudoId != beforeVault.pseudoId ||
|
||||||
|
afterVault.shareMPTID != beforeVault.shareMPTID)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal())
|
||||||
|
<< "Invariant failed: violation of vault immutable data";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!updatedShares)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << "Invariant failed: updated vault must have shares";
|
||||||
|
XRPL_ASSERT(
|
||||||
|
enforce,
|
||||||
|
"ripple::ValidVault::finalize : vault has shares invariant");
|
||||||
|
return !enforce; // That's all we can do here
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedShares->sharesTotal == 0)
|
||||||
|
{
|
||||||
|
if (afterVault.assetsTotal != zero)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << "Invariant failed: updated zero sized "
|
||||||
|
"vault must have no assets outstanding";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
if (afterVault.assetsAvailable != zero)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << "Invariant failed: updated zero sized "
|
||||||
|
"vault must have no assets available";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (updatedShares->sharesTotal > updatedShares->sharesMaximum)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) //
|
||||||
|
<< "Invariant failed: updated shares must not exceed maximum "
|
||||||
|
<< updatedShares->sharesMaximum;
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (afterVault.assetsAvailable < zero)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal())
|
||||||
|
<< "Invariant failed: assets available must be positive";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (afterVault.assetsAvailable > afterVault.assetsTotal)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << "Invariant failed: assets available must "
|
||||||
|
"not be greater than assets outstanding";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
else if (
|
||||||
|
afterVault.lossUnrealized >
|
||||||
|
afterVault.assetsTotal - afterVault.assetsAvailable)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) //
|
||||||
|
<< "Invariant failed: loss unrealized must not exceed "
|
||||||
|
"the difference between assets outstanding and available";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (afterVault.assetsTotal < zero)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal())
|
||||||
|
<< "Invariant failed: assets outstanding must be positive";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (afterVault.assetsMaximum < zero)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << "Invariant failed: assets maximum must be positive";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thanks to this check we can simply do `assert(!beforeVault_.empty()` when
|
||||||
|
// enforcing invariants on transaction types other than ttVAULT_CREATE
|
||||||
|
if (beforeVault_.empty() && txnType != ttVAULT_CREATE)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: vault created by a wrong transaction type";
|
||||||
|
XRPL_ASSERT(
|
||||||
|
enforce, "ripple::ValidVault::finalize : vault creation invariant");
|
||||||
|
return !enforce; // That's all we can do here
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!beforeVault_.empty() &&
|
||||||
|
afterVault.lossUnrealized != beforeVault_[0].lossUnrealized)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: vault transaction must not change loss "
|
||||||
|
"unrealized";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const beforeShares = [&]() -> std::optional<Shares> {
|
||||||
|
if (beforeVault_.empty())
|
||||||
|
return std::nullopt;
|
||||||
|
auto const& beforeVault = beforeVault_[0];
|
||||||
|
|
||||||
|
for (auto const& e : beforeMPTs_)
|
||||||
|
{
|
||||||
|
if (e.share.getMptID() == beforeVault.shareMPTID)
|
||||||
|
return std::move(e);
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}();
|
||||||
|
|
||||||
|
if (!beforeShares &&
|
||||||
|
(tx.getTxnType() == ttVAULT_DEPOSIT || //
|
||||||
|
tx.getTxnType() == ttVAULT_WITHDRAW || //
|
||||||
|
tx.getTxnType() == ttVAULT_CLAWBACK))
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << "Invariant failed: vault operation succeeded "
|
||||||
|
"without updating shares";
|
||||||
|
XRPL_ASSERT(
|
||||||
|
enforce, "ripple::ValidVault::finalize : shares noop invariant");
|
||||||
|
return !enforce; // That's all we can do here
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const& vaultAsset = afterVault.asset;
|
||||||
|
auto const deltaAssets = [&](AccountID const& id) -> std::optional<Number> {
|
||||||
|
auto const get = //
|
||||||
|
[&](auto const& it, std::int8_t sign = 1) -> std::optional<Number> {
|
||||||
|
if (it == deltas_.end())
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
return it->second * sign;
|
||||||
|
};
|
||||||
|
|
||||||
|
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());
|
||||||
|
};
|
||||||
|
auto const deltaShares = [&](AccountID const& id) -> std::optional<Number> {
|
||||||
|
auto const it = [&]() {
|
||||||
|
if (id == afterVault.pseudoId)
|
||||||
|
return deltas_.find(
|
||||||
|
keylet::mptIssuance(afterVault.shareMPTID).key);
|
||||||
|
return deltas_.find(keylet::mptoken(afterVault.shareMPTID, id).key);
|
||||||
|
}();
|
||||||
|
|
||||||
|
return it != deltas_.end() ? std::optional<Number>(it->second)
|
||||||
|
: std::nullopt;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Technically this does not need to be a lambda, but it's more
|
||||||
|
// convenient thanks to early "return false"; the not-so-nice
|
||||||
|
// alternatives are several layers of nested if/else or more complex
|
||||||
|
// (i.e. brittle) if statements.
|
||||||
|
result &= [&]() {
|
||||||
|
switch (txnType)
|
||||||
|
{
|
||||||
|
case ttVAULT_CREATE: {
|
||||||
|
bool result = true;
|
||||||
|
|
||||||
|
if (!beforeVault_.empty())
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) //
|
||||||
|
<< "Invariant failed: create operation must not have "
|
||||||
|
"updated a vault";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (afterVault.assetsAvailable != zero ||
|
||||||
|
afterVault.assetsTotal != zero ||
|
||||||
|
afterVault.lossUnrealized != 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;
|
||||||
|
}
|
||||||
|
case ttVAULT_SET: {
|
||||||
|
bool result = true;
|
||||||
|
|
||||||
|
XRPL_ASSERT(
|
||||||
|
!beforeVault_.empty(),
|
||||||
|
"ripple::ValidVault::finalize : set updated a vault");
|
||||||
|
auto const& beforeVault = beforeVault_[0];
|
||||||
|
|
||||||
|
auto const vaultDeltaAssets = deltaAssets(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 (afterVault.assetsMaximum > zero &&
|
||||||
|
afterVault.assetsTotal > afterVault.assetsMaximum)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: set assets outstanding must not "
|
||||||
|
"exceed assets maximum";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (beforeVault.assetsAvailable != afterVault.assetsAvailable)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: set must not change assets "
|
||||||
|
"available";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (beforeShares && updatedShares &&
|
||||||
|
beforeShares->sharesTotal != updatedShares->sharesTotal)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: set must not change shares "
|
||||||
|
"outstanding";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
case ttVAULT_DEPOSIT: {
|
||||||
|
bool result = true;
|
||||||
|
|
||||||
|
XRPL_ASSERT(
|
||||||
|
!beforeVault_.empty(),
|
||||||
|
"ripple::ValidVault::finalize : deposit updated a vault");
|
||||||
|
auto const& beforeVault = beforeVault_[0];
|
||||||
|
|
||||||
|
auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
|
||||||
|
|
||||||
|
if (!vaultDeltaAssets)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: deposit must change vault balance";
|
||||||
|
return false; // That's all we can do
|
||||||
|
}
|
||||||
|
|
||||||
|
if (*vaultDeltaAssets > tx[sfAmount])
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: deposit must not change vault "
|
||||||
|
"balance by more than deposited amount";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (*vaultDeltaAssets <= 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 accountDeltaAssets =
|
||||||
|
[&]() -> std::optional<Number> {
|
||||||
|
if (auto ret = deltaAssets(tx[sfAccount]); ret)
|
||||||
|
{
|
||||||
|
// Compensate for transaction fee deduced from
|
||||||
|
// sfAccount
|
||||||
|
if (vaultAsset.native())
|
||||||
|
*ret += fee.drops();
|
||||||
|
if (*ret != zero)
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}();
|
||||||
|
|
||||||
|
if (!accountDeltaAssets)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: deposit must change depositor "
|
||||||
|
"balance";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (*accountDeltaAssets >= zero)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: deposit must decrease depositor "
|
||||||
|
"balance";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (*accountDeltaAssets * -1 != *vaultDeltaAssets)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: deposit must change vault and "
|
||||||
|
"depositor balance by equal amount";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (afterVault.assetsMaximum > zero &&
|
||||||
|
afterVault.assetsTotal > afterVault.assetsMaximum)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: deposit assets outstanding must not "
|
||||||
|
"exceed assets maximum";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const accountDeltaShares = deltaShares(tx[sfAccount]);
|
||||||
|
if (!accountDeltaShares)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: deposit must change depositor "
|
||||||
|
"shares";
|
||||||
|
return false; // That's all we can do
|
||||||
|
}
|
||||||
|
|
||||||
|
if (*accountDeltaShares <= zero)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: deposit must increase depositor "
|
||||||
|
"shares";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
|
||||||
|
if (!vaultDeltaShares)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: deposit must change vault shares";
|
||||||
|
return false; // That's all we can do
|
||||||
|
}
|
||||||
|
|
||||||
|
if (*vaultDeltaShares * -1 != *accountDeltaShares)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: deposit must change depositor and "
|
||||||
|
"vault shares by equal amount";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (beforeVault.assetsTotal + *vaultDeltaAssets !=
|
||||||
|
afterVault.assetsTotal)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << "Invariant failed: deposit and assets "
|
||||||
|
"outstanding must add up";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
if (beforeVault.assetsAvailable + *vaultDeltaAssets !=
|
||||||
|
afterVault.assetsAvailable)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << "Invariant failed: deposit and assets "
|
||||||
|
"available must add up";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
case ttVAULT_WITHDRAW: {
|
||||||
|
bool result = true;
|
||||||
|
|
||||||
|
XRPL_ASSERT(
|
||||||
|
!beforeVault_.empty(),
|
||||||
|
"ripple::ValidVault::finalize : withdrawal updated a "
|
||||||
|
"vault");
|
||||||
|
auto const& beforeVault = beforeVault_[0];
|
||||||
|
|
||||||
|
auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
|
||||||
|
|
||||||
|
if (!vaultDeltaAssets)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << "Invariant failed: withdrawal must "
|
||||||
|
"change vault balance";
|
||||||
|
return false; // That's all we can do
|
||||||
|
}
|
||||||
|
|
||||||
|
if (*vaultDeltaAssets >= 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 accountDeltaAssets =
|
||||||
|
[&]() -> std::optional<Number> {
|
||||||
|
if (auto ret = deltaAssets(tx[sfAccount]); ret)
|
||||||
|
{
|
||||||
|
// Compensate for transaction fee deduced from
|
||||||
|
// sfAccount
|
||||||
|
if (vaultAsset.native())
|
||||||
|
*ret += fee.drops();
|
||||||
|
if (*ret != zero)
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}();
|
||||||
|
|
||||||
|
auto const otherAccountDelta =
|
||||||
|
[&]() -> std::optional<Number> {
|
||||||
|
if (auto const destination = tx[~sfDestination];
|
||||||
|
destination && *destination != tx[sfAccount])
|
||||||
|
return deltaAssets(*destination);
|
||||||
|
return std::nullopt;
|
||||||
|
}();
|
||||||
|
|
||||||
|
if (accountDeltaAssets.has_value() ==
|
||||||
|
otherAccountDelta.has_value())
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: withdrawal must change one "
|
||||||
|
"destination balance";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const destinationDelta = //
|
||||||
|
accountDeltaAssets ? *accountDeltaAssets
|
||||||
|
: *otherAccountDelta;
|
||||||
|
|
||||||
|
if (destinationDelta <= zero)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: withdrawal must increase "
|
||||||
|
"destination balance";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (*vaultDeltaAssets * -1 != destinationDelta)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: withdrawal must change vault "
|
||||||
|
"and destination balance by equal amount";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const accountDeltaShares = deltaShares(tx[sfAccount]);
|
||||||
|
if (!accountDeltaShares)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: withdrawal must change depositor "
|
||||||
|
"shares";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (*accountDeltaShares >= zero)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: withdrawal must decrease depositor "
|
||||||
|
"shares";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
|
||||||
|
if (!vaultDeltaShares)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: withdrawal must change vault shares";
|
||||||
|
return false; // That's all we can do
|
||||||
|
}
|
||||||
|
|
||||||
|
if (*vaultDeltaShares * -1 != *accountDeltaShares)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: withdrawal must change depositor "
|
||||||
|
"and vault shares by equal amount";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note, vaultBalance is negative (see check above)
|
||||||
|
if (beforeVault.assetsTotal + *vaultDeltaAssets !=
|
||||||
|
afterVault.assetsTotal)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << "Invariant failed: withdrawal and "
|
||||||
|
"assets outstanding must add up";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (beforeVault.assetsAvailable + *vaultDeltaAssets !=
|
||||||
|
afterVault.assetsAvailable)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << "Invariant failed: withdrawal and "
|
||||||
|
"assets available must add up";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
case ttVAULT_CLAWBACK: {
|
||||||
|
bool result = true;
|
||||||
|
|
||||||
|
XRPL_ASSERT(
|
||||||
|
!beforeVault_.empty(),
|
||||||
|
"ripple::ValidVault::finalize : clawback updated a vault");
|
||||||
|
auto const& beforeVault = beforeVault_[0];
|
||||||
|
|
||||||
|
if (vaultAsset.native() ||
|
||||||
|
vaultAsset.getIssuer() != tx[sfAccount])
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: clawback may only be performed by "
|
||||||
|
"the asset issuer";
|
||||||
|
return false; // That's all we can do
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
|
||||||
|
|
||||||
|
if (!vaultDeltaAssets)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: clawback must change vault balance";
|
||||||
|
return false; // That's all we can do
|
||||||
|
}
|
||||||
|
|
||||||
|
if (*vaultDeltaAssets >= zero)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: clawback must decrease vault "
|
||||||
|
"balance";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const accountDeltaShares = deltaShares(tx[sfHolder]);
|
||||||
|
if (!accountDeltaShares)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: clawback must change holder shares";
|
||||||
|
return false; // That's all we can do
|
||||||
|
}
|
||||||
|
|
||||||
|
if (*accountDeltaShares >= zero)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: clawback must decrease holder "
|
||||||
|
"shares";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
|
||||||
|
if (!vaultDeltaShares)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: clawback must change vault shares";
|
||||||
|
return false; // That's all we can do
|
||||||
|
}
|
||||||
|
|
||||||
|
if (*vaultDeltaShares * -1 != *accountDeltaShares)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: clawback must change holder and "
|
||||||
|
"vault shares by equal amount";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (beforeVault.assetsTotal + *vaultDeltaAssets !=
|
||||||
|
afterVault.assetsTotal)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: clawback and assets outstanding "
|
||||||
|
"must add up";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (beforeVault.assetsAvailable + *vaultDeltaAssets !=
|
||||||
|
afterVault.assetsAvailable)
|
||||||
|
{
|
||||||
|
JLOG(j.fatal()) << //
|
||||||
|
"Invariant failed: clawback and assets available must "
|
||||||
|
"add up";
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// LCOV_EXCL_START
|
||||||
|
UNREACHABLE(
|
||||||
|
"ripple::ValidVault::finalize : unknown transaction type");
|
||||||
|
return false;
|
||||||
|
// LCOV_EXCL_STOP
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
// The comment at the top of this file starting with "assert(enforce)"
|
||||||
|
// explains this assert.
|
||||||
|
XRPL_ASSERT(enforce, "ripple::ValidVault::finalize : vault invariants");
|
||||||
|
return !enforce;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace ripple
|
} // namespace ripple
|
||||||
|
|||||||
@@ -20,8 +20,10 @@
|
|||||||
#ifndef RIPPLE_APP_TX_INVARIANTCHECK_H_INCLUDED
|
#ifndef RIPPLE_APP_TX_INVARIANTCHECK_H_INCLUDED
|
||||||
#define RIPPLE_APP_TX_INVARIANTCHECK_H_INCLUDED
|
#define RIPPLE_APP_TX_INVARIANTCHECK_H_INCLUDED
|
||||||
|
|
||||||
|
#include <xrpl/basics/Number.h>
|
||||||
#include <xrpl/basics/base_uint.h>
|
#include <xrpl/basics/base_uint.h>
|
||||||
#include <xrpl/beast/utility/Journal.h>
|
#include <xrpl/beast/utility/Journal.h>
|
||||||
|
#include <xrpl/protocol/MPTIssue.h>
|
||||||
#include <xrpl/protocol/STLedgerEntry.h>
|
#include <xrpl/protocol/STLedgerEntry.h>
|
||||||
#include <xrpl/protocol/STTx.h>
|
#include <xrpl/protocol/STTx.h>
|
||||||
#include <xrpl/protocol/TER.h>
|
#include <xrpl/protocol/TER.h>
|
||||||
@@ -732,6 +734,74 @@ private:
|
|||||||
beast::Journal const&) const;
|
beast::Journal const&) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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)
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class ValidVault
|
||||||
|
{
|
||||||
|
Number static constexpr zero{};
|
||||||
|
|
||||||
|
struct Vault final
|
||||||
|
{
|
||||||
|
uint256 key = beast::zero;
|
||||||
|
Asset asset = {};
|
||||||
|
AccountID pseudoId = {};
|
||||||
|
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&);
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<Vault> afterVault_ = {};
|
||||||
|
std::vector<Shares> afterMPTs_ = {};
|
||||||
|
std::vector<Vault> beforeVault_ = {};
|
||||||
|
std::vector<Shares> beforeMPTs_ = {};
|
||||||
|
std::unordered_map<uint256, Number> deltas_ = {};
|
||||||
|
|
||||||
|
public:
|
||||||
|
void
|
||||||
|
visitEntry(
|
||||||
|
bool,
|
||||||
|
std::shared_ptr<SLE const> const&,
|
||||||
|
std::shared_ptr<SLE const> const&);
|
||||||
|
|
||||||
|
bool
|
||||||
|
finalize(
|
||||||
|
STTx const&,
|
||||||
|
TER const,
|
||||||
|
XRPAmount const,
|
||||||
|
ReadView const&,
|
||||||
|
beast::Journal const&);
|
||||||
|
};
|
||||||
|
|
||||||
// additional invariant checks can be declared above and then added to this
|
// additional invariant checks can be declared above and then added to this
|
||||||
// tuple
|
// tuple
|
||||||
using InvariantChecks = std::tuple<
|
using InvariantChecks = std::tuple<
|
||||||
@@ -754,7 +824,8 @@ using InvariantChecks = std::tuple<
|
|||||||
ValidPermissionedDomain,
|
ValidPermissionedDomain,
|
||||||
ValidPermissionedDEX,
|
ValidPermissionedDEX,
|
||||||
ValidAMM,
|
ValidAMM,
|
||||||
ValidPseudoAccounts>;
|
ValidPseudoAccounts,
|
||||||
|
ValidVault>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief get a tuple of all invariant checks
|
* @brief get a tuple of all invariant checks
|
||||||
|
|||||||
@@ -156,7 +156,10 @@ VaultDeposit::preclaim(PreclaimContext const& ctx)
|
|||||||
!isTesSuccess(ter))
|
!isTesSuccess(ter))
|
||||||
return ter;
|
return ter;
|
||||||
|
|
||||||
if (accountHolds(
|
// Asset issuer does not have any balance, they can just create funds by
|
||||||
|
// depositing in the vault.
|
||||||
|
if ((vaultAsset.native() || vaultAsset.getIssuer() != account) &&
|
||||||
|
accountHolds(
|
||||||
ctx.view,
|
ctx.view,
|
||||||
account,
|
account,
|
||||||
vaultAsset,
|
vaultAsset,
|
||||||
|
|||||||
@@ -183,6 +183,9 @@ VaultSet::doApply()
|
|||||||
view().update(sleIssuance);
|
view().update(sleIssuance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note, we must update Vault object even if only DomainID is being updated
|
||||||
|
// in Issuance object. Otherwise it's really difficult for Vault invariants
|
||||||
|
// to verify the operation.
|
||||||
view().update(vault);
|
view().update(vault);
|
||||||
|
|
||||||
return tesSUCCESS;
|
return tesSUCCESS;
|
||||||
|
|||||||
Reference in New Issue
Block a user