mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-03 16:56:48 +00:00
3484 lines
115 KiB
C++
3484 lines
115 KiB
C++
#include <xrpl/basics/Log.h>
|
|
#include <xrpl/beast/utility/instrumentation.h>
|
|
#include <xrpl/ledger/CredentialHelpers.h>
|
|
#include <xrpl/ledger/ReadView.h>
|
|
#include <xrpl/ledger/View.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/STNumber.h>
|
|
#include <xrpl/protocol/SystemParameters.h>
|
|
#include <xrpl/protocol/TER.h>
|
|
#include <xrpl/protocol/TxFormats.h>
|
|
#include <xrpl/protocol/Units.h>
|
|
#include <xrpl/protocol/nftPageMask.h>
|
|
#include <xrpl/tx/InvariantCheck.h>
|
|
#include <xrpl/tx/transactors/AMM/AMMHelpers.h>
|
|
#include <xrpl/tx/transactors/AMM/AMMUtils.h>
|
|
#include <xrpl/tx/transactors/NFT/NFTokenUtils.h>
|
|
#include <xrpl/tx/transactors/PermissionedDomain/PermissionedDomainSet.h>
|
|
|
|
#include <cstdint>
|
|
#include <optional>
|
|
|
|
namespace xrpl {
|
|
|
|
/*
|
|
assert(enforce)
|
|
|
|
There are several asserts (or XRPL_ASSERTs) in this file that check a variable
|
|
named `enforce` when an invariant fails. At first glance, those asserts may look
|
|
incorrect, but they are not.
|
|
|
|
Those asserts take advantage of two facts:
|
|
1. `asserts` are not (normally) executed in release builds.
|
|
2. Invariants should *never* fail, except in tests that specifically modify
|
|
the open ledger to break them.
|
|
|
|
This makes `assert(enforce)` sort of a second-layer of invariant enforcement
|
|
aimed at _developers_. It's designed to fire if a developer writes code that
|
|
violates an invariant, and runs it in unit tests or a develop build that _does
|
|
not have the relevant amendments enabled_. It's intentionally a pain in the neck
|
|
so that bad code gets caught and fixed as early as possible.
|
|
*/
|
|
|
|
enum Privilege {
|
|
noPriv = 0x0000, // The transaction can not do any of the enumerated operations
|
|
createAcct = 0x0001, // The transaction can create a new ACCOUNT_ROOT object.
|
|
createPseudoAcct = 0x0002, // The transaction can create a pseudo account,
|
|
// which implies createAcct
|
|
mustDeleteAcct = 0x0004, // The transaction must delete an ACCOUNT_ROOT object
|
|
mayDeleteAcct = 0x0008, // The transaction may delete an ACCOUNT_ROOT
|
|
// object, but does not have to
|
|
overrideFreeze = 0x0010, // The transaction can override some freeze rules
|
|
changeNFTCounts = 0x0020, // The transaction can mint or burn an NFT
|
|
createMPTIssuance = 0x0040, // The transaction can create a new MPT issuance
|
|
destroyMPTIssuance = 0x0080, // The transaction can destroy an MPT issuance
|
|
mustAuthorizeMPT = 0x0100, // The transaction MUST create or delete an MPT
|
|
// object (except by issuer)
|
|
mayAuthorizeMPT = 0x0200, // The transaction MAY create or delete an MPT
|
|
// object (except by issuer)
|
|
mayDeleteMPT = 0x0400, // The transaction MAY delete an MPT object. May not create.
|
|
mustModifyVault = 0x0800, // The transaction must modify, delete or create, a vault
|
|
mayModifyVault = 0x1000, // The transaction MAY modify, delete or create, a vault
|
|
};
|
|
constexpr Privilege
|
|
operator|(Privilege lhs, Privilege rhs)
|
|
{
|
|
return safe_cast<Privilege>(
|
|
safe_cast<std::underlying_type_t<Privilege>>(lhs) |
|
|
safe_cast<std::underlying_type_t<Privilege>>(rhs));
|
|
}
|
|
|
|
#pragma push_macro("TRANSACTION")
|
|
#undef TRANSACTION
|
|
|
|
#define TRANSACTION(tag, value, name, delegable, amendment, privileges, ...) \
|
|
case tag: { \
|
|
return (privileges) & priv; \
|
|
}
|
|
|
|
bool
|
|
hasPrivilege(STTx const& tx, Privilege priv)
|
|
{
|
|
switch (tx.getTxnType())
|
|
{
|
|
#include <xrpl/protocol/detail/transactions.macro>
|
|
|
|
// Deprecated types
|
|
default:
|
|
return false;
|
|
}
|
|
};
|
|
|
|
#undef TRANSACTION
|
|
#pragma pop_macro("TRANSACTION")
|
|
|
|
void
|
|
TransactionFeeCheck::visitEntry(
|
|
bool,
|
|
std::shared_ptr<SLE const> const&,
|
|
std::shared_ptr<SLE const> const&)
|
|
{
|
|
// nothing to do
|
|
}
|
|
|
|
bool
|
|
TransactionFeeCheck::finalize(
|
|
STTx const& tx,
|
|
TER const,
|
|
XRPAmount const fee,
|
|
ReadView const&,
|
|
beast::Journal const& j)
|
|
{
|
|
// We should never charge a negative fee
|
|
if (fee.drops() < 0)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: fee paid was negative: " << fee.drops();
|
|
return false;
|
|
}
|
|
|
|
// We should never charge a fee that's greater than or equal to the
|
|
// entire XRP supply.
|
|
if (fee >= INITIAL_XRP)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: fee paid exceeds system limit: " << fee.drops();
|
|
return false;
|
|
}
|
|
|
|
// We should never charge more for a transaction than the transaction
|
|
// authorizes. It's possible to charge less in some circumstances.
|
|
if (fee > tx.getFieldAmount(sfFee).xrp())
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: fee paid is " << fee.drops()
|
|
<< " exceeds fee specified in transaction.";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
void
|
|
XRPNotCreated::visitEntry(
|
|
bool isDelete,
|
|
std::shared_ptr<SLE const> const& before,
|
|
std::shared_ptr<SLE const> const& after)
|
|
{
|
|
/* We go through all modified ledger entries, looking only at account roots,
|
|
* escrow payments, and payment channels. We remove from the total any
|
|
* previous XRP values and add to the total any new XRP values. The net
|
|
* balance of a payment channel is computed from two fields (amount and
|
|
* balance) and deletions are ignored for paychan and escrow because the
|
|
* amount fields have not been adjusted for those in the case of deletion.
|
|
*/
|
|
if (before)
|
|
{
|
|
switch (before->getType())
|
|
{
|
|
case ltACCOUNT_ROOT:
|
|
drops_ -= (*before)[sfBalance].xrp().drops();
|
|
break;
|
|
case ltPAYCHAN:
|
|
drops_ -= ((*before)[sfAmount] - (*before)[sfBalance]).xrp().drops();
|
|
break;
|
|
case ltESCROW:
|
|
if (isXRP((*before)[sfAmount]))
|
|
drops_ -= (*before)[sfAmount].xrp().drops();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (after)
|
|
{
|
|
switch (after->getType())
|
|
{
|
|
case ltACCOUNT_ROOT:
|
|
drops_ += (*after)[sfBalance].xrp().drops();
|
|
break;
|
|
case ltPAYCHAN:
|
|
if (!isDelete)
|
|
drops_ += ((*after)[sfAmount] - (*after)[sfBalance]).xrp().drops();
|
|
break;
|
|
case ltESCROW:
|
|
if (!isDelete && isXRP((*after)[sfAmount]))
|
|
drops_ += (*after)[sfAmount].xrp().drops();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool
|
|
XRPNotCreated::finalize(
|
|
STTx const& tx,
|
|
TER const,
|
|
XRPAmount const fee,
|
|
ReadView const&,
|
|
beast::Journal const& j)
|
|
{
|
|
// The net change should never be positive, as this would mean that the
|
|
// transaction created XRP out of thin air. That's not possible.
|
|
if (drops_ > 0)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: XRP net change was positive: " << drops_;
|
|
return false;
|
|
}
|
|
|
|
// The negative of the net change should be equal to actual fee charged.
|
|
if (-drops_ != fee.drops())
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: XRP net change of " << drops_ << " doesn't match fee "
|
|
<< fee.drops();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
void
|
|
XRPBalanceChecks::visitEntry(
|
|
bool,
|
|
std::shared_ptr<SLE const> const& before,
|
|
std::shared_ptr<SLE const> const& after)
|
|
{
|
|
auto isBad = [](STAmount const& balance) {
|
|
if (!balance.native())
|
|
return true;
|
|
|
|
auto const drops = balance.xrp();
|
|
|
|
// Can't have more than the number of drops instantiated
|
|
// in the genesis ledger.
|
|
if (drops > INITIAL_XRP)
|
|
return true;
|
|
|
|
// Can't have a negative balance (0 is OK)
|
|
if (drops < XRPAmount{0})
|
|
return true;
|
|
|
|
return false;
|
|
};
|
|
|
|
if (before && before->getType() == ltACCOUNT_ROOT)
|
|
bad_ |= isBad((*before)[sfBalance]);
|
|
|
|
if (after && after->getType() == ltACCOUNT_ROOT)
|
|
bad_ |= isBad((*after)[sfBalance]);
|
|
}
|
|
|
|
bool
|
|
XRPBalanceChecks::finalize(
|
|
STTx const&,
|
|
TER const,
|
|
XRPAmount const,
|
|
ReadView const&,
|
|
beast::Journal const& j)
|
|
{
|
|
if (bad_)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: incorrect account XRP balance";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
void
|
|
NoBadOffers::visitEntry(
|
|
bool isDelete,
|
|
std::shared_ptr<SLE const> const& before,
|
|
std::shared_ptr<SLE const> const& after)
|
|
{
|
|
auto isBad = [](STAmount const& pays, STAmount const& gets) {
|
|
// An offer should never be negative
|
|
if (pays < beast::zero)
|
|
return true;
|
|
|
|
if (gets < beast::zero)
|
|
return true;
|
|
|
|
// Can't have an XRP to XRP offer:
|
|
return pays.native() && gets.native();
|
|
};
|
|
|
|
if (before && before->getType() == ltOFFER)
|
|
bad_ |= isBad((*before)[sfTakerPays], (*before)[sfTakerGets]);
|
|
|
|
if (after && after->getType() == ltOFFER)
|
|
bad_ |= isBad((*after)[sfTakerPays], (*after)[sfTakerGets]);
|
|
}
|
|
|
|
bool
|
|
NoBadOffers::finalize(
|
|
STTx const&,
|
|
TER const,
|
|
XRPAmount const,
|
|
ReadView const&,
|
|
beast::Journal const& j)
|
|
{
|
|
if (bad_)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: offer with a bad amount";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
void
|
|
NoZeroEscrow::visitEntry(
|
|
bool isDelete,
|
|
std::shared_ptr<SLE const> const& before,
|
|
std::shared_ptr<SLE const> const& after)
|
|
{
|
|
auto isBad = [](STAmount const& amount) {
|
|
// XRP case
|
|
if (amount.native())
|
|
{
|
|
if (amount.xrp() <= XRPAmount{0})
|
|
return true;
|
|
|
|
if (amount.xrp() >= INITIAL_XRP)
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
// IOU case
|
|
if (amount.holds<Issue>())
|
|
{
|
|
if (amount <= beast::zero)
|
|
return true;
|
|
|
|
if (badCurrency() == amount.getCurrency())
|
|
return true;
|
|
}
|
|
|
|
// MPT case
|
|
if (amount.holds<MPTIssue>())
|
|
{
|
|
if (amount <= beast::zero)
|
|
return true;
|
|
|
|
if (amount.mpt() > MPTAmount{maxMPTokenAmount})
|
|
return true; // LCOV_EXCL_LINE
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
if (before && before->getType() == ltESCROW)
|
|
bad_ |= isBad((*before)[sfAmount]);
|
|
|
|
if (after && after->getType() == ltESCROW)
|
|
bad_ |= isBad((*after)[sfAmount]);
|
|
|
|
auto checkAmount = [this](std::int64_t amount) {
|
|
if (amount > maxMPTokenAmount || amount < 0)
|
|
bad_ = true;
|
|
};
|
|
|
|
if (after && after->getType() == ltMPTOKEN_ISSUANCE)
|
|
{
|
|
auto const outstanding = (*after)[sfOutstandingAmount];
|
|
checkAmount(outstanding);
|
|
if (auto const locked = (*after)[~sfLockedAmount])
|
|
{
|
|
checkAmount(*locked);
|
|
bad_ = outstanding < *locked;
|
|
}
|
|
}
|
|
|
|
if (after && after->getType() == ltMPTOKEN)
|
|
{
|
|
auto const mptAmount = (*after)[sfMPTAmount];
|
|
checkAmount(mptAmount);
|
|
if (auto const locked = (*after)[~sfLockedAmount])
|
|
{
|
|
checkAmount(*locked);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool
|
|
NoZeroEscrow::finalize(
|
|
STTx const& txn,
|
|
TER const,
|
|
XRPAmount const,
|
|
ReadView const& rv,
|
|
beast::Journal const& j)
|
|
{
|
|
if (bad_)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: escrow specifies invalid amount";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
void
|
|
AccountRootsNotDeleted::visitEntry(
|
|
bool isDelete,
|
|
std::shared_ptr<SLE const> const& before,
|
|
std::shared_ptr<SLE const> const&)
|
|
{
|
|
if (isDelete && before && before->getType() == ltACCOUNT_ROOT)
|
|
accountsDeleted_++;
|
|
}
|
|
|
|
bool
|
|
AccountRootsNotDeleted::finalize(
|
|
STTx const& tx,
|
|
TER const result,
|
|
XRPAmount const,
|
|
ReadView const&,
|
|
beast::Journal const& j)
|
|
{
|
|
// AMM account root can be deleted as the result of AMM withdraw/delete
|
|
// transaction when the total AMM LP Tokens balance goes to 0.
|
|
// A successful AccountDelete or AMMDelete MUST delete exactly
|
|
// one account root.
|
|
if (hasPrivilege(tx, mustDeleteAcct) && result == tesSUCCESS)
|
|
{
|
|
if (accountsDeleted_ == 1)
|
|
return true;
|
|
|
|
if (accountsDeleted_ == 0)
|
|
JLOG(j.fatal()) << "Invariant failed: account deletion "
|
|
"succeeded without deleting an account";
|
|
else
|
|
JLOG(j.fatal()) << "Invariant failed: account deletion "
|
|
"succeeded but deleted multiple accounts!";
|
|
return false;
|
|
}
|
|
|
|
// A successful AMMWithdraw/AMMClawback MAY delete one account root
|
|
// when the total AMM LP Tokens balance goes to 0. Not every AMM withdraw
|
|
// deletes the AMM account, accountsDeleted_ is set if it is deleted.
|
|
if (hasPrivilege(tx, mayDeleteAcct) && result == tesSUCCESS && accountsDeleted_ == 1)
|
|
return true;
|
|
|
|
if (accountsDeleted_ == 0)
|
|
return true;
|
|
|
|
JLOG(j.fatal()) << "Invariant failed: an account root was deleted";
|
|
return false;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
void
|
|
AccountRootsDeletedClean::visitEntry(
|
|
bool isDelete,
|
|
std::shared_ptr<SLE const> const& before,
|
|
std::shared_ptr<SLE const> const& after)
|
|
{
|
|
if (isDelete && before && before->getType() == ltACCOUNT_ROOT)
|
|
accountsDeleted_.emplace_back(before, after);
|
|
}
|
|
|
|
bool
|
|
AccountRootsDeletedClean::finalize(
|
|
STTx const& tx,
|
|
TER const result,
|
|
XRPAmount const,
|
|
ReadView const& view,
|
|
beast::Journal const& j)
|
|
{
|
|
// Always check for objects in the ledger, but to prevent differing
|
|
// transaction processing results, however unlikely, only fail if the
|
|
// feature is enabled. Enabled, or not, though, a fatal-level message will
|
|
// be logged
|
|
[[maybe_unused]] bool const enforce = view.rules().enabled(featureInvariantsV1_1) ||
|
|
view.rules().enabled(featureSingleAssetVault) ||
|
|
view.rules().enabled(featureLendingProtocol);
|
|
|
|
auto const objectExists = [&view, enforce, &j](auto const& keylet) {
|
|
(void)enforce;
|
|
if (auto const sle = view.read(keylet))
|
|
{
|
|
// Finding the object is bad
|
|
auto const typeName = [&sle]() {
|
|
auto item = LedgerFormats::getInstance().findByType(sle->getType());
|
|
|
|
if (item != nullptr)
|
|
return item->getName();
|
|
return std::to_string(sle->getType());
|
|
}();
|
|
|
|
JLOG(j.fatal()) << "Invariant failed: account deletion left behind a " << typeName
|
|
<< " object";
|
|
// The comment above starting with "assert(enforce)" explains this
|
|
// assert.
|
|
XRPL_ASSERT(
|
|
enforce,
|
|
"xrpl::AccountRootsDeletedClean::finalize::objectExists : "
|
|
"account deletion left no objects behind");
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
for (auto const& [before, after] : accountsDeleted_)
|
|
{
|
|
auto const accountID = before->getAccountID(sfAccount);
|
|
// An account should not be deleted with a balance
|
|
if (after->at(sfBalance) != beast::zero)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: account deletion left "
|
|
"behind a non-zero balance";
|
|
XRPL_ASSERT(
|
|
enforce,
|
|
"xrpl::AccountRootsDeletedClean::finalize : "
|
|
"deleted account has zero balance");
|
|
if (enforce)
|
|
return false;
|
|
}
|
|
// An account should not be deleted with a non-zero owner count
|
|
if (after->at(sfOwnerCount) != 0)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: account deletion left "
|
|
"behind a non-zero owner count";
|
|
XRPL_ASSERT(
|
|
enforce,
|
|
"xrpl::AccountRootsDeletedClean::finalize : "
|
|
"deleted account has zero owner count");
|
|
if (enforce)
|
|
return false;
|
|
}
|
|
// Simple types
|
|
for (auto const& [keyletfunc, _, __] : directAccountKeylets)
|
|
{
|
|
if (objectExists(std::invoke(keyletfunc, accountID)) && enforce)
|
|
return false;
|
|
}
|
|
|
|
{
|
|
// NFT pages. nftpage_min and nftpage_max were already explicitly
|
|
// checked above as entries in directAccountKeylets. This uses
|
|
// view.succ() to check for any NFT pages in between the two
|
|
// endpoints.
|
|
Keylet const first = keylet::nftpage_min(accountID);
|
|
Keylet const last = keylet::nftpage_max(accountID);
|
|
|
|
std::optional<uint256> key = view.succ(first.key, last.key.next());
|
|
|
|
// current page
|
|
if (key && objectExists(Keylet{ltNFTOKEN_PAGE, *key}) && enforce)
|
|
return false;
|
|
}
|
|
|
|
// If the account is a pseudo account, then the linked object must
|
|
// also be deleted. e.g. AMM, Vault, etc.
|
|
for (auto const& field : getPseudoAccountFields())
|
|
{
|
|
if (before->isFieldPresent(*field))
|
|
{
|
|
auto const key = before->getFieldH256(*field);
|
|
if (objectExists(keylet::unchecked(key)) && enforce)
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
void
|
|
LedgerEntryTypesMatch::visitEntry(
|
|
bool,
|
|
std::shared_ptr<SLE const> const& before,
|
|
std::shared_ptr<SLE const> const& after)
|
|
{
|
|
if (before && after && before->getType() != after->getType())
|
|
typeMismatch_ = true;
|
|
|
|
if (after)
|
|
{
|
|
#pragma push_macro("LEDGER_ENTRY")
|
|
#undef LEDGER_ENTRY
|
|
|
|
#define LEDGER_ENTRY(tag, ...) case tag:
|
|
|
|
switch (after->getType())
|
|
{
|
|
#include <xrpl/protocol/detail/ledger_entries.macro>
|
|
|
|
break;
|
|
default:
|
|
invalidTypeAdded_ = true;
|
|
break;
|
|
}
|
|
|
|
#undef LEDGER_ENTRY
|
|
#pragma pop_macro("LEDGER_ENTRY")
|
|
}
|
|
}
|
|
|
|
bool
|
|
LedgerEntryTypesMatch::finalize(
|
|
STTx const&,
|
|
TER const,
|
|
XRPAmount const,
|
|
ReadView const&,
|
|
beast::Journal const& j)
|
|
{
|
|
if ((!typeMismatch_) && (!invalidTypeAdded_))
|
|
return true;
|
|
|
|
if (typeMismatch_)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: ledger entry type mismatch";
|
|
}
|
|
|
|
if (invalidTypeAdded_)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: invalid ledger entry type added";
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
void
|
|
NoXRPTrustLines::visitEntry(
|
|
bool,
|
|
std::shared_ptr<SLE const> const&,
|
|
std::shared_ptr<SLE const> const& after)
|
|
{
|
|
if (after && after->getType() == ltRIPPLE_STATE)
|
|
{
|
|
// checking the issue directly here instead of
|
|
// relying on .native() just in case native somehow
|
|
// were systematically incorrect
|
|
xrpTrustLine_ = after->getFieldAmount(sfLowLimit).issue() == xrpIssue() ||
|
|
after->getFieldAmount(sfHighLimit).issue() == xrpIssue();
|
|
}
|
|
}
|
|
|
|
bool
|
|
NoXRPTrustLines::finalize(
|
|
STTx const&,
|
|
TER const,
|
|
XRPAmount const,
|
|
ReadView const&,
|
|
beast::Journal const& j)
|
|
{
|
|
if (!xrpTrustLine_)
|
|
return true;
|
|
|
|
JLOG(j.fatal()) << "Invariant failed: an XRP trust line was created";
|
|
return false;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
void
|
|
NoDeepFreezeTrustLinesWithoutFreeze::visitEntry(
|
|
bool,
|
|
std::shared_ptr<SLE const> const&,
|
|
std::shared_ptr<SLE const> const& after)
|
|
{
|
|
if (after && after->getType() == ltRIPPLE_STATE)
|
|
{
|
|
std::uint32_t const uFlags = after->getFieldU32(sfFlags);
|
|
bool const lowFreeze = uFlags & lsfLowFreeze;
|
|
bool const lowDeepFreeze = uFlags & lsfLowDeepFreeze;
|
|
|
|
bool const highFreeze = uFlags & lsfHighFreeze;
|
|
bool const highDeepFreeze = uFlags & lsfHighDeepFreeze;
|
|
|
|
deepFreezeWithoutFreeze_ = (lowDeepFreeze && !lowFreeze) || (highDeepFreeze && !highFreeze);
|
|
}
|
|
}
|
|
|
|
bool
|
|
NoDeepFreezeTrustLinesWithoutFreeze::finalize(
|
|
STTx const&,
|
|
TER const,
|
|
XRPAmount const,
|
|
ReadView const&,
|
|
beast::Journal const& j)
|
|
{
|
|
if (!deepFreezeWithoutFreeze_)
|
|
return true;
|
|
|
|
JLOG(j.fatal()) << "Invariant failed: a trust line with deep freeze flag "
|
|
"without normal freeze was created";
|
|
return false;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
void
|
|
TransfersNotFrozen::visitEntry(
|
|
bool isDelete,
|
|
std::shared_ptr<SLE const> const& before,
|
|
std::shared_ptr<SLE const> const& after)
|
|
{
|
|
/*
|
|
* A trust line freeze state alone doesn't determine if a transfer is
|
|
* frozen. The transfer must be examined "end-to-end" because both sides of
|
|
* the transfer may have different freeze states and freeze impact depends
|
|
* on the transfer direction. This is why first we need to track the
|
|
* transfers using IssuerChanges senders/receivers.
|
|
*
|
|
* Only in validateIssuerChanges, after we collected all changes can we
|
|
* determine if the transfer is valid.
|
|
*/
|
|
if (!isValidEntry(before, after))
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto const balanceChange = calculateBalanceChange(before, after, isDelete);
|
|
if (balanceChange.signum() == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
recordBalanceChanges(after, balanceChange);
|
|
}
|
|
|
|
bool
|
|
TransfersNotFrozen::finalize(
|
|
STTx const& tx,
|
|
TER const ter,
|
|
XRPAmount const fee,
|
|
ReadView const& view,
|
|
beast::Journal const& j)
|
|
{
|
|
/*
|
|
* We check this invariant regardless of deep freeze amendment status,
|
|
* allowing for detection and logging of potential issues even when the
|
|
* amendment is disabled.
|
|
*
|
|
* If an exploit that allows moving frozen assets is discovered,
|
|
* we can alert operators who monitor fatal messages and trigger assert in
|
|
* debug builds for an early warning.
|
|
*
|
|
* In an unlikely event that an exploit is found, this early detection
|
|
* enables encouraging the UNL to expedite deep freeze amendment activation
|
|
* or deploy hotfixes via new amendments. In case of a new amendment, we'd
|
|
* only have to change this line setting 'enforce' variable.
|
|
* enforce = view.rules().enabled(featureDeepFreeze) ||
|
|
* view.rules().enabled(fixFreezeExploit);
|
|
*/
|
|
[[maybe_unused]] bool const enforce = view.rules().enabled(featureDeepFreeze);
|
|
|
|
for (auto const& [issue, changes] : balanceChanges_)
|
|
{
|
|
auto const issuerSle = findIssuer(issue.account, view);
|
|
// It should be impossible for the issuer to not be found, but check
|
|
// just in case so rippled doesn't crash in release.
|
|
if (!issuerSle)
|
|
{
|
|
// The comment above starting with "assert(enforce)" explains this
|
|
// assert.
|
|
XRPL_ASSERT(
|
|
enforce,
|
|
"xrpl::TransfersNotFrozen::finalize : enforce "
|
|
"invariant.");
|
|
if (enforce)
|
|
{
|
|
return false;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (!validateIssuerChanges(issuerSle, changes, tx, j, enforce))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
TransfersNotFrozen::isValidEntry(
|
|
std::shared_ptr<SLE const> const& before,
|
|
std::shared_ptr<SLE const> const& after)
|
|
{
|
|
// `after` can never be null, even if the trust line is deleted.
|
|
XRPL_ASSERT(after, "xrpl::TransfersNotFrozen::isValidEntry : valid after.");
|
|
if (!after)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (after->getType() == ltACCOUNT_ROOT)
|
|
{
|
|
possibleIssuers_.emplace(after->at(sfAccount), after);
|
|
return false;
|
|
}
|
|
|
|
/* While LedgerEntryTypesMatch invariant also checks types, all invariants
|
|
* are processed regardless of previous failures.
|
|
*
|
|
* This type check is still necessary here because it prevents potential
|
|
* issues in subsequent processing.
|
|
*/
|
|
return after->getType() == ltRIPPLE_STATE && (!before || before->getType() == ltRIPPLE_STATE);
|
|
}
|
|
|
|
STAmount
|
|
TransfersNotFrozen::calculateBalanceChange(
|
|
std::shared_ptr<SLE const> const& before,
|
|
std::shared_ptr<SLE const> const& after,
|
|
bool isDelete)
|
|
{
|
|
auto const getBalance = [](auto const& line, auto const& other, bool zero) {
|
|
STAmount amt = line ? line->at(sfBalance) : other->at(sfBalance).zeroed();
|
|
return zero ? amt.zeroed() : amt;
|
|
};
|
|
|
|
/* Trust lines can be created dynamically by other transactions such as
|
|
* Payment and OfferCreate that cross offers. Such trust line won't be
|
|
* created frozen, but the sender might be, so the starting balance must be
|
|
* treated as zero.
|
|
*/
|
|
auto const balanceBefore = getBalance(before, after, false);
|
|
|
|
/* Same as above, trust lines can be dynamically deleted, and for frozen
|
|
* trust lines, payments not involving the issuer must be blocked. This is
|
|
* achieved by treating the final balance as zero when isDelete=true to
|
|
* ensure frozen line restrictions are enforced even during deletion.
|
|
*/
|
|
auto const balanceAfter = getBalance(after, before, isDelete);
|
|
|
|
return balanceAfter - balanceBefore;
|
|
}
|
|
|
|
void
|
|
TransfersNotFrozen::recordBalance(Issue const& issue, BalanceChange change)
|
|
{
|
|
XRPL_ASSERT(
|
|
change.balanceChangeSign,
|
|
"xrpl::TransfersNotFrozen::recordBalance : valid trustline "
|
|
"balance sign.");
|
|
auto& changes = balanceChanges_[issue];
|
|
if (change.balanceChangeSign < 0)
|
|
changes.senders.emplace_back(std::move(change));
|
|
else
|
|
changes.receivers.emplace_back(std::move(change));
|
|
}
|
|
|
|
void
|
|
TransfersNotFrozen::recordBalanceChanges(
|
|
std::shared_ptr<SLE const> const& after,
|
|
STAmount const& balanceChange)
|
|
{
|
|
auto const balanceChangeSign = balanceChange.signum();
|
|
auto const currency = after->at(sfBalance).getCurrency();
|
|
|
|
// Change from low account's perspective, which is trust line default
|
|
recordBalance({currency, after->at(sfHighLimit).getIssuer()}, {after, balanceChangeSign});
|
|
|
|
// Change from high account's perspective, which reverses the sign.
|
|
recordBalance({currency, after->at(sfLowLimit).getIssuer()}, {after, -balanceChangeSign});
|
|
}
|
|
|
|
std::shared_ptr<SLE const>
|
|
TransfersNotFrozen::findIssuer(AccountID const& issuerID, ReadView const& view)
|
|
{
|
|
if (auto it = possibleIssuers_.find(issuerID); it != possibleIssuers_.end())
|
|
{
|
|
return it->second;
|
|
}
|
|
|
|
return view.read(keylet::account(issuerID));
|
|
}
|
|
|
|
bool
|
|
TransfersNotFrozen::validateIssuerChanges(
|
|
std::shared_ptr<SLE const> const& issuer,
|
|
IssuerChanges const& changes,
|
|
STTx const& tx,
|
|
beast::Journal const& j,
|
|
bool enforce)
|
|
{
|
|
if (!issuer)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool const globalFreeze = issuer->isFlag(lsfGlobalFreeze);
|
|
if (changes.receivers.empty() || changes.senders.empty())
|
|
{
|
|
/* If there are no receivers, then the holder(s) are returning
|
|
* their tokens to the issuer. Likewise, if there are no
|
|
* senders, then the issuer is issuing tokens to the holder(s).
|
|
* This is allowed regardless of the issuer's freeze flags. (The
|
|
* holder may have contradicting freeze flags, but that will be
|
|
* checked when the holder is treated as issuer.)
|
|
*/
|
|
return true;
|
|
}
|
|
|
|
for (auto const& actors : {changes.senders, changes.receivers})
|
|
{
|
|
for (auto const& change : actors)
|
|
{
|
|
bool const high = change.line->at(sfLowLimit).getIssuer() == issuer->at(sfAccount);
|
|
|
|
if (!validateFrozenState(change, high, tx, j, enforce, globalFreeze))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
TransfersNotFrozen::validateFrozenState(
|
|
BalanceChange const& change,
|
|
bool high,
|
|
STTx const& tx,
|
|
beast::Journal const& j,
|
|
bool enforce,
|
|
bool globalFreeze)
|
|
{
|
|
bool const freeze =
|
|
change.balanceChangeSign < 0 && change.line->isFlag(high ? lsfLowFreeze : lsfHighFreeze);
|
|
bool const deepFreeze = change.line->isFlag(high ? lsfLowDeepFreeze : lsfHighDeepFreeze);
|
|
bool const frozen = globalFreeze || deepFreeze || freeze;
|
|
|
|
bool const isAMMLine = change.line->isFlag(lsfAMMNode);
|
|
|
|
if (!frozen)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// AMMClawbacks are allowed to override some freeze rules
|
|
if ((!isAMMLine || globalFreeze) && hasPrivilege(tx, overrideFreeze))
|
|
{
|
|
JLOG(j.debug()) << "Invariant check allowing funds to be moved "
|
|
<< (change.balanceChangeSign > 0 ? "to" : "from")
|
|
<< " a frozen trustline for AMMClawback " << tx.getTransactionID();
|
|
return true;
|
|
}
|
|
|
|
JLOG(j.fatal()) << "Invariant failed: Attempting to move frozen funds for "
|
|
<< tx.getTransactionID();
|
|
// The comment above starting with "assert(enforce)" explains this assert.
|
|
XRPL_ASSERT(
|
|
enforce,
|
|
"xrpl::TransfersNotFrozen::validateFrozenState : enforce "
|
|
"invariant.");
|
|
|
|
if (enforce)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
void
|
|
ValidNewAccountRoot::visitEntry(
|
|
bool,
|
|
std::shared_ptr<SLE const> const& before,
|
|
std::shared_ptr<SLE const> const& after)
|
|
{
|
|
if (!before && after->getType() == ltACCOUNT_ROOT)
|
|
{
|
|
accountsCreated_++;
|
|
accountSeq_ = (*after)[sfSequence];
|
|
pseudoAccount_ = isPseudoAccount(after);
|
|
flags_ = after->getFlags();
|
|
}
|
|
}
|
|
|
|
bool
|
|
ValidNewAccountRoot::finalize(
|
|
STTx const& tx,
|
|
TER const result,
|
|
XRPAmount const,
|
|
ReadView const& view,
|
|
beast::Journal const& j)
|
|
{
|
|
if (accountsCreated_ == 0)
|
|
return true;
|
|
|
|
if (accountsCreated_ > 1)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: multiple accounts "
|
|
"created in a single transaction";
|
|
return false;
|
|
}
|
|
|
|
// From this point on we know exactly one account was created.
|
|
if (hasPrivilege(tx, createAcct | createPseudoAcct) && result == tesSUCCESS)
|
|
{
|
|
bool const pseudoAccount =
|
|
(pseudoAccount_ &&
|
|
(view.rules().enabled(featureSingleAssetVault) ||
|
|
view.rules().enabled(featureLendingProtocol)));
|
|
|
|
if (pseudoAccount && !hasPrivilege(tx, createPseudoAcct))
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: pseudo-account created by a "
|
|
"wrong transaction type";
|
|
return false;
|
|
}
|
|
|
|
std::uint32_t const startingSeq = pseudoAccount ? 0 : view.seq();
|
|
|
|
if (accountSeq_ != startingSeq)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: account created with "
|
|
"wrong starting sequence number";
|
|
return false;
|
|
}
|
|
|
|
if (pseudoAccount)
|
|
{
|
|
std::uint32_t const expected = (lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth);
|
|
if (flags_ != expected)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: pseudo-account created with "
|
|
"wrong flags";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
JLOG(j.fatal()) << "Invariant failed: account root created illegally";
|
|
return false;
|
|
} // namespace xrpl
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
void
|
|
ValidNFTokenPage::visitEntry(
|
|
bool isDelete,
|
|
std::shared_ptr<SLE const> const& before,
|
|
std::shared_ptr<SLE const> const& after)
|
|
{
|
|
static constexpr uint256 const& pageBits = nft::pageMask;
|
|
static constexpr uint256 const accountBits = ~pageBits;
|
|
|
|
if ((before && before->getType() != ltNFTOKEN_PAGE) ||
|
|
(after && after->getType() != ltNFTOKEN_PAGE))
|
|
return;
|
|
|
|
auto check = [this, isDelete](std::shared_ptr<SLE const> const& sle) {
|
|
uint256 const account = sle->key() & accountBits;
|
|
uint256 const hiLimit = sle->key() & pageBits;
|
|
std::optional<uint256> const prev = (*sle)[~sfPreviousPageMin];
|
|
|
|
// Make sure that any page links...
|
|
// 1. Are properly associated with the owning account and
|
|
// 2. The page is correctly ordered between links.
|
|
if (prev)
|
|
{
|
|
if (account != (*prev & accountBits))
|
|
badLink_ = true;
|
|
|
|
if (hiLimit <= (*prev & pageBits))
|
|
badLink_ = true;
|
|
}
|
|
|
|
if (auto const next = (*sle)[~sfNextPageMin])
|
|
{
|
|
if (account != (*next & accountBits))
|
|
badLink_ = true;
|
|
|
|
if (hiLimit >= (*next & pageBits))
|
|
badLink_ = true;
|
|
}
|
|
|
|
{
|
|
auto const& nftokens = sle->getFieldArray(sfNFTokens);
|
|
|
|
// An NFTokenPage should never contain too many tokens or be empty.
|
|
if (std::size_t const nftokenCount = nftokens.size();
|
|
(!isDelete && nftokenCount == 0) || nftokenCount > dirMaxTokensPerPage)
|
|
invalidSize_ = true;
|
|
|
|
// If prev is valid, use it to establish a lower bound for
|
|
// page entries. If prev is not valid the lower bound is zero.
|
|
uint256 const loLimit = prev ? *prev & pageBits : uint256(beast::zero);
|
|
|
|
// Also verify that all NFTokenIDs in the page are sorted.
|
|
uint256 loCmp = loLimit;
|
|
for (auto const& obj : nftokens)
|
|
{
|
|
uint256 const tokenID = obj[sfNFTokenID];
|
|
if (!nft::compareTokens(loCmp, tokenID))
|
|
badSort_ = true;
|
|
loCmp = tokenID;
|
|
|
|
// None of the NFTs on this page should belong on lower or
|
|
// higher pages.
|
|
if (uint256 const tokenPageBits = tokenID & pageBits;
|
|
tokenPageBits < loLimit || tokenPageBits >= hiLimit)
|
|
badEntry_ = true;
|
|
|
|
if (auto uri = obj[~sfURI]; uri && uri->empty())
|
|
badURI_ = true;
|
|
}
|
|
}
|
|
};
|
|
|
|
if (before)
|
|
{
|
|
check(before);
|
|
|
|
// While an account's NFToken directory contains any NFTokens, the last
|
|
// NFTokenPage (with 96 bits of 1 in the low part of the index) should
|
|
// never be deleted.
|
|
if (isDelete && (before->key() & nft::pageMask) == nft::pageMask &&
|
|
before->isFieldPresent(sfPreviousPageMin))
|
|
{
|
|
deletedFinalPage_ = true;
|
|
}
|
|
}
|
|
|
|
if (after)
|
|
check(after);
|
|
|
|
if (!isDelete && before && after)
|
|
{
|
|
// If the NFTokenPage
|
|
// 1. Has a NextMinPage field in before, but loses it in after, and
|
|
// 2. This is not the last page in the directory
|
|
// Then we have identified a corruption in the links between the
|
|
// NFToken pages in the NFToken directory.
|
|
if ((before->key() & nft::pageMask) != nft::pageMask &&
|
|
before->isFieldPresent(sfNextPageMin) && !after->isFieldPresent(sfNextPageMin))
|
|
{
|
|
deletedLink_ = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool
|
|
ValidNFTokenPage::finalize(
|
|
STTx const& tx,
|
|
TER const result,
|
|
XRPAmount const,
|
|
ReadView const& view,
|
|
beast::Journal const& j)
|
|
{
|
|
if (badLink_)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: NFT page is improperly linked.";
|
|
return false;
|
|
}
|
|
|
|
if (badEntry_)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: NFT found in incorrect page.";
|
|
return false;
|
|
}
|
|
|
|
if (badSort_)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: NFTs on page are not sorted.";
|
|
return false;
|
|
}
|
|
|
|
if (badURI_)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: NFT contains empty URI.";
|
|
return false;
|
|
}
|
|
|
|
if (invalidSize_)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: NFT page has invalid size.";
|
|
return false;
|
|
}
|
|
|
|
if (view.rules().enabled(fixNFTokenPageLinks))
|
|
{
|
|
if (deletedFinalPage_)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: Last NFT page deleted with "
|
|
"non-empty directory.";
|
|
return false;
|
|
}
|
|
if (deletedLink_)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: Lost NextMinPage link.";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
void
|
|
NFTokenCountTracking::visitEntry(
|
|
bool,
|
|
std::shared_ptr<SLE const> const& before,
|
|
std::shared_ptr<SLE const> const& after)
|
|
{
|
|
if (before && before->getType() == ltACCOUNT_ROOT)
|
|
{
|
|
beforeMintedTotal += (*before)[~sfMintedNFTokens].value_or(0);
|
|
beforeBurnedTotal += (*before)[~sfBurnedNFTokens].value_or(0);
|
|
}
|
|
|
|
if (after && after->getType() == ltACCOUNT_ROOT)
|
|
{
|
|
afterMintedTotal += (*after)[~sfMintedNFTokens].value_or(0);
|
|
afterBurnedTotal += (*after)[~sfBurnedNFTokens].value_or(0);
|
|
}
|
|
}
|
|
|
|
bool
|
|
NFTokenCountTracking::finalize(
|
|
STTx const& tx,
|
|
TER const result,
|
|
XRPAmount const,
|
|
ReadView const& view,
|
|
beast::Journal const& j)
|
|
{
|
|
if (!hasPrivilege(tx, changeNFTCounts))
|
|
{
|
|
if (beforeMintedTotal != afterMintedTotal)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: the number of minted tokens "
|
|
"changed without a mint transaction!";
|
|
return false;
|
|
}
|
|
|
|
if (beforeBurnedTotal != afterBurnedTotal)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: the number of burned tokens "
|
|
"changed without a burn transaction!";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
if (tx.getTxnType() == ttNFTOKEN_MINT)
|
|
{
|
|
if (result == tesSUCCESS && beforeMintedTotal >= afterMintedTotal)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: successful minting didn't increase "
|
|
"the number of minted tokens.";
|
|
return false;
|
|
}
|
|
|
|
if (result != tesSUCCESS && beforeMintedTotal != afterMintedTotal)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: failed minting changed the "
|
|
"number of minted tokens.";
|
|
return false;
|
|
}
|
|
|
|
if (beforeBurnedTotal != afterBurnedTotal)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: minting changed the number of "
|
|
"burned tokens.";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (tx.getTxnType() == ttNFTOKEN_BURN)
|
|
{
|
|
if (result == tesSUCCESS)
|
|
{
|
|
if (beforeBurnedTotal >= afterBurnedTotal)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: successful burning didn't increase "
|
|
"the number of burned tokens.";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (result != tesSUCCESS && beforeBurnedTotal != afterBurnedTotal)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: failed burning changed the "
|
|
"number of burned tokens.";
|
|
return false;
|
|
}
|
|
|
|
if (beforeMintedTotal != afterMintedTotal)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: burning changed the number of "
|
|
"minted tokens.";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
void
|
|
ValidClawback::visitEntry(
|
|
bool,
|
|
std::shared_ptr<SLE const> const& before,
|
|
std::shared_ptr<SLE const> const&)
|
|
{
|
|
if (before && before->getType() == ltRIPPLE_STATE)
|
|
trustlinesChanged++;
|
|
|
|
if (before && before->getType() == ltMPTOKEN)
|
|
mptokensChanged++;
|
|
}
|
|
|
|
bool
|
|
ValidClawback::finalize(
|
|
STTx const& tx,
|
|
TER const result,
|
|
XRPAmount const,
|
|
ReadView const& view,
|
|
beast::Journal const& j)
|
|
{
|
|
if (tx.getTxnType() != ttCLAWBACK)
|
|
return true;
|
|
|
|
if (result == tesSUCCESS)
|
|
{
|
|
if (trustlinesChanged > 1)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: more than one trustline changed.";
|
|
return false;
|
|
}
|
|
|
|
if (mptokensChanged > 1)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: more than one mptokens changed.";
|
|
return false;
|
|
}
|
|
|
|
if (trustlinesChanged == 1)
|
|
{
|
|
AccountID const issuer = tx.getAccountID(sfAccount);
|
|
STAmount const& amount = tx.getFieldAmount(sfAmount);
|
|
AccountID const& holder = amount.getIssuer();
|
|
STAmount const holderBalance =
|
|
accountHolds(view, holder, amount.getCurrency(), issuer, fhIGNORE_FREEZE, j);
|
|
|
|
if (holderBalance.signum() < 0)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: trustline balance is negative";
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (trustlinesChanged != 0)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: some trustlines were changed "
|
|
"despite failure of the transaction.";
|
|
return false;
|
|
}
|
|
|
|
if (mptokensChanged != 0)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: some mptokens were changed "
|
|
"despite failure of the transaction.";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
void
|
|
ValidMPTIssuance::visitEntry(
|
|
bool isDelete,
|
|
std::shared_ptr<SLE const> const& before,
|
|
std::shared_ptr<SLE const> const& after)
|
|
{
|
|
if (after && after->getType() == ltMPTOKEN_ISSUANCE)
|
|
{
|
|
if (isDelete)
|
|
mptIssuancesDeleted_++;
|
|
else if (!before)
|
|
mptIssuancesCreated_++;
|
|
}
|
|
|
|
if (after && after->getType() == ltMPTOKEN)
|
|
{
|
|
if (isDelete)
|
|
mptokensDeleted_++;
|
|
else if (!before)
|
|
{
|
|
mptokensCreated_++;
|
|
MPTIssue const mptIssue{after->at(sfMPTokenIssuanceID)};
|
|
if (mptIssue.getIssuer() == after->at(sfAccount))
|
|
mptCreatedByIssuer_ = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool
|
|
ValidMPTIssuance::finalize(
|
|
STTx const& tx,
|
|
TER const result,
|
|
XRPAmount const _fee,
|
|
ReadView const& view,
|
|
beast::Journal const& j)
|
|
{
|
|
if (result == tesSUCCESS)
|
|
{
|
|
auto const& rules = view.rules();
|
|
[[maybe_unused]]
|
|
bool enforceCreatedByIssuer =
|
|
rules.enabled(featureSingleAssetVault) || rules.enabled(featureLendingProtocol);
|
|
if (mptCreatedByIssuer_)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: MPToken created for the MPT issuer";
|
|
// The comment above starting with "assert(enforce)" explains this
|
|
// assert.
|
|
XRPL_ASSERT_PARTS(
|
|
enforceCreatedByIssuer, "xrpl::ValidMPTIssuance::finalize", "no issuer MPToken");
|
|
if (enforceCreatedByIssuer)
|
|
return false;
|
|
}
|
|
|
|
auto const txnType = tx.getTxnType();
|
|
if (hasPrivilege(tx, createMPTIssuance))
|
|
{
|
|
if (mptIssuancesCreated_ == 0)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: transaction "
|
|
"succeeded without creating a MPT issuance";
|
|
}
|
|
else if (mptIssuancesDeleted_ != 0)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: transaction "
|
|
"succeeded while removing MPT issuances";
|
|
}
|
|
else if (mptIssuancesCreated_ > 1)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: transaction "
|
|
"succeeded but created multiple issuances";
|
|
}
|
|
|
|
return mptIssuancesCreated_ == 1 && mptIssuancesDeleted_ == 0;
|
|
}
|
|
|
|
if (hasPrivilege(tx, destroyMPTIssuance))
|
|
{
|
|
if (mptIssuancesDeleted_ == 0)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion "
|
|
"succeeded without removing a MPT issuance";
|
|
}
|
|
else if (mptIssuancesCreated_ > 0)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion "
|
|
"succeeded while creating MPT issuances";
|
|
}
|
|
else if (mptIssuancesDeleted_ > 1)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion "
|
|
"succeeded but deleted multiple issuances";
|
|
}
|
|
|
|
return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 1;
|
|
}
|
|
|
|
bool const lendingProtocolEnabled = view.rules().enabled(featureLendingProtocol);
|
|
// ttESCROW_FINISH may authorize an MPT, but it can't have the
|
|
// mayAuthorizeMPT privilege, because that may cause
|
|
// non-amendment-gated side effects.
|
|
bool const enforceEscrowFinish = (txnType == ttESCROW_FINISH) &&
|
|
(view.rules().enabled(featureSingleAssetVault) || lendingProtocolEnabled);
|
|
if (hasPrivilege(tx, mustAuthorizeMPT | mayAuthorizeMPT) || enforceEscrowFinish)
|
|
{
|
|
bool const submittedByIssuer = tx.isFieldPresent(sfHolder);
|
|
|
|
if (mptIssuancesCreated_ > 0)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: MPT authorize "
|
|
"succeeded but created MPT issuances";
|
|
return false;
|
|
}
|
|
else if (mptIssuancesDeleted_ > 0)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: MPT authorize "
|
|
"succeeded but deleted issuances";
|
|
return false;
|
|
}
|
|
else if (lendingProtocolEnabled && mptokensCreated_ + mptokensDeleted_ > 1)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: MPT authorize succeeded "
|
|
"but created/deleted bad number mptokens";
|
|
return false;
|
|
}
|
|
else if (submittedByIssuer && (mptokensCreated_ > 0 || mptokensDeleted_ > 0))
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: MPT authorize submitted by issuer "
|
|
"succeeded but created/deleted mptokens";
|
|
return false;
|
|
}
|
|
else if (
|
|
!submittedByIssuer && hasPrivilege(tx, mustAuthorizeMPT) &&
|
|
(mptokensCreated_ + mptokensDeleted_ != 1))
|
|
{
|
|
// if the holder submitted this tx, then a mptoken must be
|
|
// either created or deleted.
|
|
JLOG(j.fatal()) << "Invariant failed: MPT authorize submitted by holder "
|
|
"succeeded but created/deleted bad number of mptokens";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
if (txnType == ttESCROW_FINISH)
|
|
{
|
|
// ttESCROW_FINISH may authorize an MPT, but it can't have the
|
|
// mayAuthorizeMPT privilege, because that may cause
|
|
// non-amendment-gated side effects.
|
|
XRPL_ASSERT_PARTS(
|
|
!enforceEscrowFinish, "xrpl::ValidMPTIssuance::finalize", "not escrow finish tx");
|
|
return true;
|
|
}
|
|
|
|
if (hasPrivilege(tx, mayDeleteMPT) && mptokensDeleted_ == 1 && mptokensCreated_ == 0 &&
|
|
mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0)
|
|
return true;
|
|
}
|
|
|
|
if (mptIssuancesCreated_ != 0)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: a MPT issuance was created";
|
|
}
|
|
else if (mptIssuancesDeleted_ != 0)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: a MPT issuance was deleted";
|
|
}
|
|
else if (mptokensCreated_ != 0)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: a MPToken was created";
|
|
}
|
|
else if (mptokensDeleted_ != 0)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: a MPToken was deleted";
|
|
}
|
|
|
|
return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 && mptokensCreated_ == 0 &&
|
|
mptokensDeleted_ == 0;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
void
|
|
ValidPermissionedDomain::visitEntry(
|
|
bool isDel,
|
|
std::shared_ptr<SLE const> const& before,
|
|
std::shared_ptr<SLE const> const& after)
|
|
{
|
|
if (before && before->getType() != ltPERMISSIONED_DOMAIN)
|
|
return;
|
|
if (after && after->getType() != ltPERMISSIONED_DOMAIN)
|
|
return;
|
|
|
|
auto check = [isDel](std::vector<SleStatus>& sleStatus, std::shared_ptr<SLE const> const& sle) {
|
|
auto const& credentials = sle->getFieldArray(sfAcceptedCredentials);
|
|
auto const sorted = credentials::makeSorted(credentials);
|
|
|
|
SleStatus ss{credentials.size(), false, !sorted.empty(), isDel};
|
|
|
|
// If array have duplicates then all the other checks are invalid
|
|
if (ss.isUnique_)
|
|
{
|
|
unsigned i = 0;
|
|
for (auto const& cred : sorted)
|
|
{
|
|
auto const& credTx = credentials[i++];
|
|
ss.isSorted_ =
|
|
(cred.first == credTx[sfIssuer]) && (cred.second == credTx[sfCredentialType]);
|
|
if (!ss.isSorted_)
|
|
break;
|
|
}
|
|
}
|
|
sleStatus.emplace_back(std::move(ss));
|
|
};
|
|
|
|
if (after)
|
|
check(sleStatus_, after);
|
|
}
|
|
|
|
bool
|
|
ValidPermissionedDomain::finalize(
|
|
STTx const& tx,
|
|
TER const result,
|
|
XRPAmount const,
|
|
ReadView const& view,
|
|
beast::Journal const& j)
|
|
{
|
|
auto check = [](SleStatus const& sleStatus, beast::Journal const& j) {
|
|
if (!sleStatus.credentialsSize_)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: permissioned domain with "
|
|
"no rules.";
|
|
return false;
|
|
}
|
|
|
|
if (sleStatus.credentialsSize_ > maxPermissionedDomainCredentialsArraySize)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: permissioned domain bad "
|
|
"credentials size "
|
|
<< sleStatus.credentialsSize_;
|
|
return false;
|
|
}
|
|
|
|
if (!sleStatus.isUnique_)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: permissioned domain credentials "
|
|
"aren't unique";
|
|
return false;
|
|
}
|
|
|
|
if (!sleStatus.isSorted_)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: permissioned domain credentials "
|
|
"aren't sorted";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
if (view.rules().enabled(fixPermissionedDomainInvariant))
|
|
{
|
|
// No permissioned domains should be affected if the transaction failed
|
|
if (result != tesSUCCESS)
|
|
// If nothing changed, all is good. If there were changes, that's
|
|
// bad.
|
|
return sleStatus_.empty();
|
|
|
|
if (sleStatus_.size() > 1)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: transaction affected more "
|
|
"than 1 permissioned domain entry.";
|
|
return false;
|
|
}
|
|
|
|
switch (tx.getTxnType())
|
|
{
|
|
case ttPERMISSIONED_DOMAIN_SET: {
|
|
if (sleStatus_.empty())
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: no domain objects affected by "
|
|
"PermissionedDomainSet";
|
|
return false;
|
|
}
|
|
|
|
auto const& sleStatus = sleStatus_[0];
|
|
if (sleStatus.isDelete_)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: domain object "
|
|
"deleted by PermissionedDomainSet";
|
|
return false;
|
|
}
|
|
return check(sleStatus, j);
|
|
}
|
|
case ttPERMISSIONED_DOMAIN_DELETE: {
|
|
if (sleStatus_.empty())
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: no domain objects affected by "
|
|
"PermissionedDomainDelete";
|
|
return false;
|
|
}
|
|
|
|
if (!sleStatus_[0].isDelete_)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: domain object "
|
|
"modified, but not deleted by "
|
|
"PermissionedDomainDelete";
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
default: {
|
|
if (!sleStatus_.empty())
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: " << sleStatus_.size()
|
|
<< " domain object(s) affected by an "
|
|
"unauthorized transaction. "
|
|
<< tx.getTxnType();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (tx.getTxnType() != ttPERMISSIONED_DOMAIN_SET || result != tesSUCCESS ||
|
|
sleStatus_.empty())
|
|
return true;
|
|
return check(sleStatus_[0], j);
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
void
|
|
ValidPseudoAccounts::visitEntry(
|
|
bool isDelete,
|
|
std::shared_ptr<SLE const> const& before,
|
|
std::shared_ptr<SLE const> const& after)
|
|
{
|
|
if (isDelete)
|
|
// Deletion is ignored
|
|
return;
|
|
|
|
if (after && after->getType() == ltACCOUNT_ROOT)
|
|
{
|
|
bool const isPseudo = [&]() {
|
|
// isPseudoAccount checks that any of the pseudo-account fields are
|
|
// set.
|
|
if (isPseudoAccount(after))
|
|
return true;
|
|
// Not all pseudo-accounts have a zero sequence, but all accounts
|
|
// with a zero sequence had better be pseudo-accounts.
|
|
if (after->at(sfSequence) == 0)
|
|
return true;
|
|
|
|
return false;
|
|
}();
|
|
if (isPseudo)
|
|
{
|
|
// Pseudo accounts must have the following properties:
|
|
// 1. Exactly one of the pseudo-account fields is set.
|
|
// 2. The sequence number is not changed.
|
|
// 3. The lsfDisableMaster, lsfDefaultRipple, and lsfDepositAuth
|
|
// flags are set.
|
|
// 4. The RegularKey is not set.
|
|
{
|
|
std::vector<SField const*> const& fields = getPseudoAccountFields();
|
|
|
|
auto const numFields =
|
|
std::count_if(fields.begin(), fields.end(), [&after](SField const* sf) -> bool {
|
|
return after->isFieldPresent(*sf);
|
|
});
|
|
if (numFields != 1)
|
|
{
|
|
std::stringstream error;
|
|
error << "pseudo-account has " << numFields << " pseudo-account fields set";
|
|
errors_.emplace_back(error.str());
|
|
}
|
|
}
|
|
if (before && before->at(sfSequence) != after->at(sfSequence))
|
|
{
|
|
errors_.emplace_back("pseudo-account sequence changed");
|
|
}
|
|
if (!after->isFlag(lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth))
|
|
{
|
|
errors_.emplace_back("pseudo-account flags are not set");
|
|
}
|
|
if (after->isFieldPresent(sfRegularKey))
|
|
{
|
|
errors_.emplace_back("pseudo-account has a regular key");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool
|
|
ValidPseudoAccounts::finalize(
|
|
STTx const& tx,
|
|
TER const,
|
|
XRPAmount const,
|
|
ReadView const& view,
|
|
beast::Journal const& j)
|
|
{
|
|
bool const enforce = view.rules().enabled(featureSingleAssetVault);
|
|
XRPL_ASSERT(
|
|
errors_.empty() || enforce,
|
|
"xrpl::ValidPseudoAccounts::finalize : no bad "
|
|
"changes or enforce invariant");
|
|
if (!errors_.empty())
|
|
{
|
|
for (auto const& error : errors_)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: " << error;
|
|
}
|
|
if (enforce)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
void
|
|
ValidPermissionedDEX::visitEntry(
|
|
bool,
|
|
std::shared_ptr<SLE const> const& before,
|
|
std::shared_ptr<SLE const> const& after)
|
|
{
|
|
if (after && after->getType() == ltDIR_NODE)
|
|
{
|
|
if (after->isFieldPresent(sfDomainID))
|
|
domains_.insert(after->getFieldH256(sfDomainID));
|
|
}
|
|
|
|
if (after && after->getType() == ltOFFER)
|
|
{
|
|
if (after->isFieldPresent(sfDomainID))
|
|
domains_.insert(after->getFieldH256(sfDomainID));
|
|
else
|
|
regularOffers_ = true;
|
|
|
|
// if a hybrid offer is missing domain or additional book, there's
|
|
// something wrong
|
|
if (after->isFlag(lsfHybrid) &&
|
|
(!after->isFieldPresent(sfDomainID) || !after->isFieldPresent(sfAdditionalBooks) ||
|
|
after->getFieldArray(sfAdditionalBooks).size() > 1))
|
|
badHybrids_ = true;
|
|
}
|
|
}
|
|
|
|
bool
|
|
ValidPermissionedDEX::finalize(
|
|
STTx const& tx,
|
|
TER const result,
|
|
XRPAmount const,
|
|
ReadView const& view,
|
|
beast::Journal const& j)
|
|
{
|
|
auto const txType = tx.getTxnType();
|
|
if ((txType != ttPAYMENT && txType != ttOFFER_CREATE) || result != tesSUCCESS)
|
|
return true;
|
|
|
|
// For each offercreate transaction, check if
|
|
// permissioned offers are valid
|
|
if (txType == ttOFFER_CREATE && badHybrids_)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: hybrid offer is malformed";
|
|
return false;
|
|
}
|
|
|
|
if (!tx.isFieldPresent(sfDomainID))
|
|
return true;
|
|
|
|
auto const domain = tx.getFieldH256(sfDomainID);
|
|
|
|
if (!view.exists(keylet::permissionedDomain(domain)))
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: domain doesn't exist";
|
|
return false;
|
|
}
|
|
|
|
// for both payment and offercreate, there shouldn't be another domain
|
|
// that's different from the domain specified
|
|
for (auto const& d : domains_)
|
|
{
|
|
if (d != domain)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: transaction"
|
|
" consumed wrong domains";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (regularOffers_)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: domain transaction"
|
|
" affected regular offers";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void
|
|
ValidAMM::visitEntry(
|
|
bool isDelete,
|
|
std::shared_ptr<SLE const> const& before,
|
|
std::shared_ptr<SLE const> const& after)
|
|
{
|
|
if (isDelete)
|
|
return;
|
|
|
|
if (after)
|
|
{
|
|
auto const type = after->getType();
|
|
// AMM object changed
|
|
if (type == ltAMM)
|
|
{
|
|
ammAccount_ = after->getAccountID(sfAccount);
|
|
lptAMMBalanceAfter_ = after->getFieldAmount(sfLPTokenBalance);
|
|
}
|
|
// AMM pool changed
|
|
else if (
|
|
(type == ltRIPPLE_STATE && after->getFlags() & lsfAMMNode) ||
|
|
(type == ltACCOUNT_ROOT && after->isFieldPresent(sfAMMID)))
|
|
{
|
|
ammPoolChanged_ = true;
|
|
}
|
|
}
|
|
|
|
if (before)
|
|
{
|
|
// AMM object changed
|
|
if (before->getType() == ltAMM)
|
|
{
|
|
lptAMMBalanceBefore_ = before->getFieldAmount(sfLPTokenBalance);
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool
|
|
validBalances(
|
|
STAmount const& amount,
|
|
STAmount const& amount2,
|
|
STAmount const& lptAMMBalance,
|
|
ValidAMM::ZeroAllowed zeroAllowed)
|
|
{
|
|
bool const positive =
|
|
amount > beast::zero && amount2 > beast::zero && lptAMMBalance > beast::zero;
|
|
if (zeroAllowed == ValidAMM::ZeroAllowed::Yes)
|
|
return positive ||
|
|
(amount == beast::zero && amount2 == beast::zero && lptAMMBalance == beast::zero);
|
|
return positive;
|
|
}
|
|
|
|
bool
|
|
ValidAMM::finalizeVote(bool enforce, beast::Journal const& j) const
|
|
{
|
|
if (lptAMMBalanceAfter_ != lptAMMBalanceBefore_ || ammPoolChanged_)
|
|
{
|
|
// LPTokens and the pool can not change on vote
|
|
// LCOV_EXCL_START
|
|
JLOG(j.error()) << "AMMVote invariant failed: " << lptAMMBalanceBefore_.value_or(STAmount{})
|
|
<< " " << lptAMMBalanceAfter_.value_or(STAmount{}) << " "
|
|
<< ammPoolChanged_;
|
|
if (enforce)
|
|
return false;
|
|
// LCOV_EXCL_STOP
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
ValidAMM::finalizeBid(bool enforce, beast::Journal const& j) const
|
|
{
|
|
if (ammPoolChanged_)
|
|
{
|
|
// The pool can not change on bid
|
|
// LCOV_EXCL_START
|
|
JLOG(j.error()) << "AMMBid invariant failed: pool changed";
|
|
if (enforce)
|
|
return false;
|
|
// LCOV_EXCL_STOP
|
|
}
|
|
// LPTokens are burnt, therefore there should be fewer LPTokens
|
|
else if (
|
|
lptAMMBalanceBefore_ && lptAMMBalanceAfter_ &&
|
|
(*lptAMMBalanceAfter_ > *lptAMMBalanceBefore_ || *lptAMMBalanceAfter_ <= beast::zero))
|
|
{
|
|
// LCOV_EXCL_START
|
|
JLOG(j.error()) << "AMMBid invariant failed: " << *lptAMMBalanceBefore_ << " "
|
|
<< *lptAMMBalanceAfter_;
|
|
if (enforce)
|
|
return false;
|
|
// LCOV_EXCL_STOP
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
ValidAMM::finalizeCreate(
|
|
STTx const& tx,
|
|
ReadView const& view,
|
|
bool enforce,
|
|
beast::Journal const& j) const
|
|
{
|
|
if (!ammAccount_)
|
|
{
|
|
// LCOV_EXCL_START
|
|
JLOG(j.error()) << "AMMCreate invariant failed: AMM object is not created";
|
|
if (enforce)
|
|
return false;
|
|
// LCOV_EXCL_STOP
|
|
}
|
|
else
|
|
{
|
|
auto const [amount, amount2] = ammPoolHolds(
|
|
view,
|
|
*ammAccount_,
|
|
tx[sfAmount].get<Issue>(),
|
|
tx[sfAmount2].get<Issue>(),
|
|
fhIGNORE_FREEZE,
|
|
j);
|
|
// Create invariant:
|
|
// sqrt(amount * amount2) == LPTokens
|
|
// all balances are greater than zero
|
|
if (!validBalances(amount, amount2, *lptAMMBalanceAfter_, ZeroAllowed::No) ||
|
|
ammLPTokens(amount, amount2, lptAMMBalanceAfter_->issue()) != *lptAMMBalanceAfter_)
|
|
{
|
|
JLOG(j.error()) << "AMMCreate invariant failed: " << amount << " " << amount2 << " "
|
|
<< *lptAMMBalanceAfter_;
|
|
if (enforce)
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
ValidAMM::finalizeDelete(bool enforce, TER res, beast::Journal const& j) const
|
|
{
|
|
if (ammAccount_)
|
|
{
|
|
// LCOV_EXCL_START
|
|
std::string const msg = (res == tesSUCCESS) ? "AMM object is not deleted on tesSUCCESS"
|
|
: "AMM object is changed on tecINCOMPLETE";
|
|
JLOG(j.error()) << "AMMDelete invariant failed: " << msg;
|
|
if (enforce)
|
|
return false;
|
|
// LCOV_EXCL_STOP
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
ValidAMM::finalizeDEX(bool enforce, beast::Journal const& j) const
|
|
{
|
|
if (ammAccount_)
|
|
{
|
|
// LCOV_EXCL_START
|
|
JLOG(j.error()) << "AMM swap invariant failed: AMM object changed";
|
|
if (enforce)
|
|
return false;
|
|
// LCOV_EXCL_STOP
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
ValidAMM::generalInvariant(
|
|
xrpl::STTx const& tx,
|
|
xrpl::ReadView const& view,
|
|
ZeroAllowed zeroAllowed,
|
|
beast::Journal const& j) const
|
|
{
|
|
auto const [amount, amount2] = ammPoolHolds(
|
|
view,
|
|
*ammAccount_,
|
|
tx[sfAsset].get<Issue>(),
|
|
tx[sfAsset2].get<Issue>(),
|
|
fhIGNORE_FREEZE,
|
|
j);
|
|
// Deposit and Withdrawal invariant:
|
|
// sqrt(amount * amount2) >= LPTokens
|
|
// all balances are greater than zero
|
|
// unless on last withdrawal
|
|
auto const poolProductMean = root2(amount * amount2);
|
|
bool const nonNegativeBalances =
|
|
validBalances(amount, amount2, *lptAMMBalanceAfter_, zeroAllowed);
|
|
bool const strongInvariantCheck = poolProductMean >= *lptAMMBalanceAfter_;
|
|
// Allow for a small relative error if strongInvariantCheck fails
|
|
auto weakInvariantCheck = [&]() {
|
|
return *lptAMMBalanceAfter_ != beast::zero &&
|
|
withinRelativeDistance(poolProductMean, Number{*lptAMMBalanceAfter_}, Number{1, -11});
|
|
};
|
|
if (!nonNegativeBalances || (!strongInvariantCheck && !weakInvariantCheck()))
|
|
{
|
|
JLOG(j.error()) << "AMM " << tx.getTxnType()
|
|
<< " invariant failed: " << tx.getHash(HashPrefix::transactionID) << " "
|
|
<< ammPoolChanged_ << " " << amount << " " << amount2 << " "
|
|
<< poolProductMean << " " << lptAMMBalanceAfter_->getText() << " "
|
|
<< ((*lptAMMBalanceAfter_ == beast::zero)
|
|
? Number{1}
|
|
: ((*lptAMMBalanceAfter_ - poolProductMean) / poolProductMean));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
ValidAMM::finalizeDeposit(
|
|
xrpl::STTx const& tx,
|
|
xrpl::ReadView const& view,
|
|
bool enforce,
|
|
beast::Journal const& j) const
|
|
{
|
|
if (!ammAccount_)
|
|
{
|
|
// LCOV_EXCL_START
|
|
JLOG(j.error()) << "AMMDeposit invariant failed: AMM object is deleted";
|
|
if (enforce)
|
|
return false;
|
|
// LCOV_EXCL_STOP
|
|
}
|
|
else if (!generalInvariant(tx, view, ZeroAllowed::No, j) && enforce)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
ValidAMM::finalizeWithdraw(
|
|
xrpl::STTx const& tx,
|
|
xrpl::ReadView const& view,
|
|
bool enforce,
|
|
beast::Journal const& j) const
|
|
{
|
|
if (!ammAccount_)
|
|
{
|
|
// Last Withdraw or Clawback deleted AMM
|
|
}
|
|
else if (!generalInvariant(tx, view, ZeroAllowed::Yes, j))
|
|
{
|
|
if (enforce)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
ValidAMM::finalize(
|
|
STTx const& tx,
|
|
TER const result,
|
|
XRPAmount const,
|
|
ReadView const& view,
|
|
beast::Journal const& j)
|
|
{
|
|
// Delete may return tecINCOMPLETE if there are too many
|
|
// trustlines to delete.
|
|
if (result != tesSUCCESS && result != tecINCOMPLETE)
|
|
return true;
|
|
|
|
bool const enforce = view.rules().enabled(fixAMMv1_3);
|
|
|
|
switch (tx.getTxnType())
|
|
{
|
|
case ttAMM_CREATE:
|
|
return finalizeCreate(tx, view, enforce, j);
|
|
case ttAMM_DEPOSIT:
|
|
return finalizeDeposit(tx, view, enforce, j);
|
|
case ttAMM_CLAWBACK:
|
|
case ttAMM_WITHDRAW:
|
|
return finalizeWithdraw(tx, view, enforce, j);
|
|
case ttAMM_BID:
|
|
return finalizeBid(enforce, j);
|
|
case ttAMM_VOTE:
|
|
return finalizeVote(enforce, j);
|
|
case ttAMM_DELETE:
|
|
return finalizeDelete(enforce, result, j);
|
|
case ttCHECK_CASH:
|
|
case ttOFFER_CREATE:
|
|
case ttPAYMENT:
|
|
return finalizeDEX(enforce, j);
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
void
|
|
NoModifiedUnmodifiableFields::visitEntry(
|
|
bool isDelete,
|
|
std::shared_ptr<SLE const> const& before,
|
|
std::shared_ptr<SLE const> const& after)
|
|
{
|
|
if (isDelete || !before)
|
|
// Creation and deletion are ignored
|
|
return;
|
|
|
|
changedEntries_.emplace(before, after);
|
|
}
|
|
|
|
bool
|
|
NoModifiedUnmodifiableFields::finalize(
|
|
STTx const& tx,
|
|
TER const,
|
|
XRPAmount const,
|
|
ReadView const& view,
|
|
beast::Journal const& j)
|
|
{
|
|
static auto const fieldChanged = [](auto const& before, auto const& after, auto const& field) {
|
|
bool const beforeField = before->isFieldPresent(field);
|
|
bool const afterField = after->isFieldPresent(field);
|
|
return beforeField != afterField || (afterField && before->at(field) != after->at(field));
|
|
};
|
|
for (auto const& slePair : changedEntries_)
|
|
{
|
|
auto const& before = slePair.first;
|
|
auto const& after = slePair.second;
|
|
auto const type = after->getType();
|
|
bool bad = false;
|
|
[[maybe_unused]] bool enforce = false;
|
|
switch (type)
|
|
{
|
|
case ltLOAN_BROKER:
|
|
/*
|
|
* We check this invariant regardless of lending protocol
|
|
* amendment status, allowing for detection and logging of
|
|
* potential issues even when the amendment is disabled.
|
|
*/
|
|
enforce = view.rules().enabled(featureLendingProtocol);
|
|
bad = fieldChanged(before, after, sfLedgerEntryType) ||
|
|
fieldChanged(before, after, sfLedgerIndex) ||
|
|
fieldChanged(before, after, sfSequence) ||
|
|
fieldChanged(before, after, sfOwnerNode) ||
|
|
fieldChanged(before, after, sfVaultNode) ||
|
|
fieldChanged(before, after, sfVaultID) ||
|
|
fieldChanged(before, after, sfAccount) ||
|
|
fieldChanged(before, after, sfOwner) ||
|
|
fieldChanged(before, after, sfManagementFeeRate) ||
|
|
fieldChanged(before, after, sfCoverRateMinimum) ||
|
|
fieldChanged(before, after, sfCoverRateLiquidation);
|
|
break;
|
|
case ltLOAN:
|
|
/*
|
|
* We check this invariant regardless of lending protocol
|
|
* amendment status, allowing for detection and logging of
|
|
* potential issues even when the amendment is disabled.
|
|
*/
|
|
enforce = view.rules().enabled(featureLendingProtocol);
|
|
bad = fieldChanged(before, after, sfLedgerEntryType) ||
|
|
fieldChanged(before, after, sfLedgerIndex) ||
|
|
fieldChanged(before, after, sfSequence) ||
|
|
fieldChanged(before, after, sfOwnerNode) ||
|
|
fieldChanged(before, after, sfLoanBrokerNode) ||
|
|
fieldChanged(before, after, sfLoanBrokerID) ||
|
|
fieldChanged(before, after, sfBorrower) ||
|
|
fieldChanged(before, after, sfLoanOriginationFee) ||
|
|
fieldChanged(before, after, sfLoanServiceFee) ||
|
|
fieldChanged(before, after, sfLatePaymentFee) ||
|
|
fieldChanged(before, after, sfClosePaymentFee) ||
|
|
fieldChanged(before, after, sfOverpaymentFee) ||
|
|
fieldChanged(before, after, sfInterestRate) ||
|
|
fieldChanged(before, after, sfLateInterestRate) ||
|
|
fieldChanged(before, after, sfCloseInterestRate) ||
|
|
fieldChanged(before, after, sfOverpaymentInterestRate) ||
|
|
fieldChanged(before, after, sfStartDate) ||
|
|
fieldChanged(before, after, sfPaymentInterval) ||
|
|
fieldChanged(before, after, sfGracePeriod) ||
|
|
fieldChanged(before, after, sfLoanScale);
|
|
break;
|
|
default:
|
|
/*
|
|
* We check this invariant regardless of lending protocol
|
|
* amendment status, allowing for detection and logging of
|
|
* potential issues even when the amendment is disabled.
|
|
*
|
|
* We use the lending protocol as a gate, even though
|
|
* all transactions are affected because that's when it
|
|
* was added.
|
|
*/
|
|
enforce = view.rules().enabled(featureLendingProtocol);
|
|
bad = fieldChanged(before, after, sfLedgerEntryType) ||
|
|
fieldChanged(before, after, sfLedgerIndex);
|
|
}
|
|
XRPL_ASSERT(
|
|
!bad || enforce,
|
|
"xrpl::NoModifiedUnmodifiableFields::finalize : no bad "
|
|
"changes or enforce invariant");
|
|
if (bad)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: changed an unchangeable field for "
|
|
<< tx.getTransactionID();
|
|
if (enforce)
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
void
|
|
ValidLoanBroker::visitEntry(
|
|
bool isDelete,
|
|
std::shared_ptr<SLE const> const& before,
|
|
std::shared_ptr<SLE const> const& after)
|
|
{
|
|
if (after)
|
|
{
|
|
if (after->getType() == ltLOAN_BROKER)
|
|
{
|
|
auto& broker = brokers_[after->key()];
|
|
broker.brokerBefore = before;
|
|
broker.brokerAfter = after;
|
|
}
|
|
else if (after->getType() == ltACCOUNT_ROOT && after->isFieldPresent(sfLoanBrokerID))
|
|
{
|
|
auto const& loanBrokerID = after->at(sfLoanBrokerID);
|
|
// create an entry if one doesn't already exist
|
|
brokers_.emplace(loanBrokerID, BrokerInfo{});
|
|
}
|
|
else if (after->getType() == ltRIPPLE_STATE)
|
|
{
|
|
lines_.emplace_back(after);
|
|
}
|
|
else if (after->getType() == ltMPTOKEN)
|
|
{
|
|
mpts_.emplace_back(after);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool
|
|
ValidLoanBroker::goodZeroDirectory(
|
|
ReadView const& view,
|
|
SLE::const_ref dir,
|
|
beast::Journal const& j) const
|
|
{
|
|
auto const next = dir->at(~sfIndexNext);
|
|
auto const prev = dir->at(~sfIndexPrevious);
|
|
if ((prev && *prev) || (next && *next))
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero "
|
|
"OwnerCount has multiple directory pages";
|
|
return false;
|
|
}
|
|
auto indexes = dir->getFieldV256(sfIndexes);
|
|
if (indexes.size() > 1)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero "
|
|
"OwnerCount has multiple indexes in the Directory root";
|
|
return false;
|
|
}
|
|
if (indexes.size() == 1)
|
|
{
|
|
auto const index = indexes.value().front();
|
|
auto const sle = view.read(keylet::unchecked(index));
|
|
if (!sle)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: Loan Broker directory corrupt";
|
|
return false;
|
|
}
|
|
if (sle->getType() != ltRIPPLE_STATE && sle->getType() != ltMPTOKEN)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero "
|
|
"OwnerCount has an unexpected entry in the directory";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
ValidLoanBroker::finalize(
|
|
STTx const& tx,
|
|
TER const,
|
|
XRPAmount const,
|
|
ReadView const& view,
|
|
beast::Journal const& j)
|
|
{
|
|
// Loan Brokers will not exist on ledger if the Lending Protocol amendment
|
|
// is not enabled, so there's no need to check it.
|
|
|
|
for (auto const& line : lines_)
|
|
{
|
|
for (auto const& field : {&sfLowLimit, &sfHighLimit})
|
|
{
|
|
auto const account = view.read(keylet::account(line->at(*field).getIssuer()));
|
|
// This Invariant doesn't know about the rules for Trust Lines, so
|
|
// if the account is missing, don't treat it as an error. This
|
|
// loop is only concerned with finding Broker pseudo-accounts
|
|
if (account && account->isFieldPresent(sfLoanBrokerID))
|
|
{
|
|
auto const& loanBrokerID = account->at(sfLoanBrokerID);
|
|
// create an entry if one doesn't already exist
|
|
brokers_.emplace(loanBrokerID, BrokerInfo{});
|
|
}
|
|
}
|
|
}
|
|
for (auto const& mpt : mpts_)
|
|
{
|
|
auto const account = view.read(keylet::account(mpt->at(sfAccount)));
|
|
// This Invariant doesn't know about the rules for MPTokens, so
|
|
// if the account is missing, don't treat is as an error. This
|
|
// loop is only concerned with finding Broker pseudo-accounts
|
|
if (account && account->isFieldPresent(sfLoanBrokerID))
|
|
{
|
|
auto const& loanBrokerID = account->at(sfLoanBrokerID);
|
|
// create an entry if one doesn't already exist
|
|
brokers_.emplace(loanBrokerID, BrokerInfo{});
|
|
}
|
|
}
|
|
|
|
for (auto const& [brokerID, broker] : brokers_)
|
|
{
|
|
auto const& after =
|
|
broker.brokerAfter ? broker.brokerAfter : view.read(keylet::loanbroker(brokerID));
|
|
|
|
if (!after)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: Loan Broker missing";
|
|
return false;
|
|
}
|
|
|
|
auto const& before = broker.brokerBefore;
|
|
|
|
// https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3123-invariants
|
|
// If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most
|
|
// one node (the root), which will only hold entries for `RippleState`
|
|
// or `MPToken` objects.
|
|
if (after->at(sfOwnerCount) == 0)
|
|
{
|
|
auto const dir = view.read(keylet::ownerDir(after->at(sfAccount)));
|
|
if (dir)
|
|
{
|
|
if (!goodZeroDirectory(view, dir, j))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
if (before && before->at(sfLoanSequence) > after->at(sfLoanSequence))
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: Loan Broker sequence number "
|
|
"decreased";
|
|
return false;
|
|
}
|
|
if (after->at(sfDebtTotal) < 0)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: Loan Broker debt total is negative";
|
|
return false;
|
|
}
|
|
if (after->at(sfCoverAvailable) < 0)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: Loan Broker cover available is negative";
|
|
return false;
|
|
}
|
|
auto const vault = view.read(keylet::vault(after->at(sfVaultID)));
|
|
if (!vault)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: Loan Broker vault ID is invalid";
|
|
return false;
|
|
}
|
|
auto const& vaultAsset = vault->at(sfAsset);
|
|
if (after->at(sfCoverAvailable) < accountHolds(
|
|
view,
|
|
after->at(sfAccount),
|
|
vaultAsset,
|
|
FreezeHandling::fhIGNORE_FREEZE,
|
|
AuthHandling::ahIGNORE_AUTH,
|
|
j))
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: Loan Broker cover available "
|
|
"is less than pseudo-account asset balance";
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
void
|
|
ValidLoan::visitEntry(
|
|
bool isDelete,
|
|
std::shared_ptr<SLE const> const& before,
|
|
std::shared_ptr<SLE const> const& after)
|
|
{
|
|
if (after && after->getType() == ltLOAN)
|
|
{
|
|
loans_.emplace_back(before, after);
|
|
}
|
|
}
|
|
|
|
bool
|
|
ValidLoan::finalize(
|
|
STTx const& tx,
|
|
TER const,
|
|
XRPAmount const,
|
|
ReadView const& view,
|
|
beast::Journal const& j)
|
|
{
|
|
// Loans will not exist on ledger if the Lending Protocol amendment
|
|
// is not enabled, so there's no need to check it.
|
|
|
|
for (auto const& [before, after] : loans_)
|
|
{
|
|
// https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3223-invariants
|
|
// If `Loan.PaymentRemaining = 0` then the loan MUST be fully paid off
|
|
if (after->at(sfPaymentRemaining) == 0 &&
|
|
(after->at(sfTotalValueOutstanding) != beast::zero ||
|
|
after->at(sfPrincipalOutstanding) != beast::zero ||
|
|
after->at(sfManagementFeeOutstanding) != beast::zero))
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: Loan with zero payments "
|
|
"remaining has not been paid off";
|
|
return false;
|
|
}
|
|
// If `Loan.PaymentRemaining != 0` then the loan MUST NOT be fully paid
|
|
// off
|
|
if (after->at(sfPaymentRemaining) != 0 &&
|
|
after->at(sfTotalValueOutstanding) == beast::zero &&
|
|
after->at(sfPrincipalOutstanding) == beast::zero &&
|
|
after->at(sfManagementFeeOutstanding) == beast::zero)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: Loan with zero payments "
|
|
"remaining has not been paid off";
|
|
return false;
|
|
}
|
|
if (before && (before->isFlag(lsfLoanOverpayment) != after->isFlag(lsfLoanOverpayment)))
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: Loan Overpayment flag changed";
|
|
return false;
|
|
}
|
|
// Must not be negative - STNumber
|
|
for (auto const field :
|
|
{&sfLoanServiceFee,
|
|
&sfLatePaymentFee,
|
|
&sfClosePaymentFee,
|
|
&sfPrincipalOutstanding,
|
|
&sfTotalValueOutstanding,
|
|
&sfManagementFeeOutstanding})
|
|
{
|
|
if (after->at(*field) < 0)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: " << field->getName() << " is negative ";
|
|
return false;
|
|
}
|
|
}
|
|
// Must be positive - STNumber
|
|
for (auto const field : {
|
|
&sfPeriodicPayment,
|
|
})
|
|
{
|
|
if (after->at(*field) <= 0)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: " << field->getName()
|
|
<< " is zero or negative ";
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
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.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;
|
|
}
|
|
|
|
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),
|
|
"xrpl::ValidVault::visitEntry : some object is available");
|
|
|
|
// Number balanceDelta 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 balanceDelta{};
|
|
|
|
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 = static_cast<std::int64_t>(before->getFieldU64(sfOutstandingAmount));
|
|
sign = 1;
|
|
break;
|
|
case ltMPTOKEN:
|
|
balanceDelta = static_cast<std::int64_t>(before->getFieldU64(sfMPTAmount));
|
|
sign = -1;
|
|
break;
|
|
case ltACCOUNT_ROOT:
|
|
case ltRIPPLE_STATE:
|
|
balanceDelta = 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));
|
|
balanceDelta -=
|
|
Number(static_cast<std::int64_t>(after->getFieldU64(sfOutstandingAmount)));
|
|
sign = 1;
|
|
break;
|
|
case ltMPTOKEN:
|
|
balanceDelta -= Number(static_cast<std::int64_t>(after->getFieldU64(sfMPTAmount)));
|
|
sign = -1;
|
|
break;
|
|
case ltACCOUNT_ROOT:
|
|
case ltRIPPLE_STATE:
|
|
balanceDelta -= Number(after->getFieldAmount(sfBalance));
|
|
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)
|
|
deltas_[key] = balanceDelta * 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, "xrpl::ValidVault::finalize : vault noop invariant");
|
|
return !enforce;
|
|
}
|
|
|
|
return true; // Not a vault operation
|
|
}
|
|
else if (!(hasPrivilege(tx, mustModifyVault) || hasPrivilege(tx, mayModifyVault)))
|
|
{
|
|
JLOG(j.fatal()) << //
|
|
"Invariant failed: vault updated by a wrong transaction type";
|
|
XRPL_ASSERT(
|
|
enforce,
|
|
"xrpl::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, "xrpl::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,
|
|
"xrpl::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, "xrpl::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, "xrpl::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,
|
|
"xrpl::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, "xrpl::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, "xrpl::ValidVault::finalize : vault creation invariant");
|
|
return !enforce; // That's all we can do here
|
|
}
|
|
|
|
if (!beforeVault_.empty() && afterVault.lossUnrealized != beforeVault_[0].lossUnrealized &&
|
|
txnType != ttLOAN_MANAGE && txnType != ttLOAN_PAY)
|
|
{
|
|
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, "xrpl::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 deltaAssetsTxAccount = [&]() -> std::optional<Number> {
|
|
auto ret = deltaAssets(tx[sfAccount]);
|
|
// Nothing returned or not XRP transaction
|
|
if (!ret.has_value() || !vaultAsset.native())
|
|
return ret;
|
|
|
|
// Delegated transaction; no need to compensate for fees
|
|
if (auto const delegate = tx[~sfDelegate];
|
|
delegate.has_value() && *delegate != tx[sfAccount])
|
|
return ret;
|
|
|
|
*ret += fee.drops();
|
|
if (*ret == zero)
|
|
return std::nullopt;
|
|
|
|
return ret;
|
|
};
|
|
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;
|
|
};
|
|
|
|
auto const vaultHoldsNoAssets = [&](Vault const& vault) {
|
|
return vault.assetsAvailable == 0 && vault.assetsTotal == 0;
|
|
};
|
|
|
|
// 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(), "xrpl::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(), "xrpl::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 = deltaAssetsTxAccount();
|
|
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 || *vaultDeltaShares == zero)
|
|
{
|
|
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(),
|
|
"xrpl::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 = deltaAssetsTxAccount();
|
|
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 || *vaultDeltaShares == zero)
|
|
{
|
|
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(), "xrpl::ValidVault::finalize : clawback updated a vault");
|
|
auto const& beforeVault = beforeVault_[0];
|
|
|
|
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 &&
|
|
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; // That's all we can do
|
|
}
|
|
}
|
|
|
|
auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
|
|
if (vaultDeltaAssets)
|
|
{
|
|
if (*vaultDeltaAssets >= zero)
|
|
{
|
|
JLOG(j.fatal()) << //
|
|
"Invariant failed: clawback must decrease vault "
|
|
"balance";
|
|
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;
|
|
}
|
|
}
|
|
else if (!vaultHoldsNoAssets(beforeVault))
|
|
{
|
|
JLOG(j.fatal()) << //
|
|
"Invariant failed: clawback must change vault balance";
|
|
return false; // That's all we can do
|
|
}
|
|
|
|
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 || *vaultDeltaShares == zero)
|
|
{
|
|
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;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
case ttLOAN_SET:
|
|
case ttLOAN_MANAGE:
|
|
case ttLOAN_PAY: {
|
|
// TBD
|
|
return true;
|
|
}
|
|
|
|
default:
|
|
// LCOV_EXCL_START
|
|
UNREACHABLE("xrpl::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, "xrpl::ValidVault::finalize : vault invariants");
|
|
return !enforce;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
} // namespace xrpl
|