mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-04 10:48:15 +00:00
A bridge connects two blockchains: a locking chain and an issuing chain (also called a mainchain and a sidechain). Both are independent ledgers, with their own validators and potentially their own custom transactions. Importantly, there is a way to move assets from the locking chain to the issuing chain and a way to return those assets from the issuing chain back to the locking chain: the bridge. This key operation is called a cross-chain transfer. A cross-chain transfer is not a single transaction. It happens on two chains, requires multiple transactions, and involves an additional server type called a "witness". A bridge does not exchange assets between two ledgers. Instead, it locks assets on one ledger (the "locking chain") and represents those assets with wrapped assets on another chain (the "issuing chain"). A good model to keep in mind is a box with an infinite supply of wrapped assets. Putting an asset from the locking chain into the box will release a wrapped asset onto the issuing chain. Putting a wrapped asset from the issuing chain back into the box will release one of the existing locking chain assets back onto the locking chain. There is no other way to get assets into or out of the box. Note that there is no way for the box to "run out of" wrapped assets - it has an infinite supply. Co-authored-by: Gregory Popovitch <greg7mdp@gmail.com>
802 lines
22 KiB
C++
802 lines
22 KiB
C++
//------------------------------------------------------------------------------
|
|
/*
|
|
This file is part of rippled: https://github.com/ripple/rippled
|
|
Copyright (c) 2012-2016 Ripple Labs Inc.
|
|
|
|
Permission to use, copy, modify, and/or distribute this software for any
|
|
purpose with or without fee is hereby granted, provided that the above
|
|
copyright notice and this permission notice appear in all copies.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
//==============================================================================
|
|
|
|
#include <ripple/app/tx/impl/InvariantCheck.h>
|
|
|
|
#include <ripple/app/tx/impl/details/NFTokenUtils.h>
|
|
#include <ripple/basics/FeeUnits.h>
|
|
#include <ripple/basics/Log.h>
|
|
#include <ripple/ledger/ReadView.h>
|
|
#include <ripple/ledger/View.h>
|
|
#include <ripple/protocol/Feature.h>
|
|
#include <ripple/protocol/STArray.h>
|
|
#include <ripple/protocol/SystemParameters.h>
|
|
#include <ripple/protocol/TxFormats.h>
|
|
#include <ripple/protocol/nftPageMask.h>
|
|
|
|
namespace ripple {
|
|
|
|
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:
|
|
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)
|
|
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) {
|
|
if (!amount.native())
|
|
return true;
|
|
|
|
if (amount.xrp() <= XRPAmount{0})
|
|
return true;
|
|
|
|
if (amount.xrp() >= INITIAL_XRP)
|
|
return true;
|
|
|
|
return false;
|
|
};
|
|
|
|
if (before && before->getType() == ltESCROW)
|
|
bad_ |= isBad((*before)[sfAmount]);
|
|
|
|
if (after && after->getType() == ltESCROW)
|
|
bad_ |= isBad((*after)[sfAmount]);
|
|
}
|
|
|
|
bool
|
|
NoZeroEscrow::finalize(
|
|
STTx const&,
|
|
TER const,
|
|
XRPAmount const,
|
|
ReadView const&,
|
|
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 ((tx.getTxnType() == ttACCOUNT_DELETE ||
|
|
tx.getTxnType() == ttAMM_DELETE) &&
|
|
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 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 (tx.getTxnType() == ttAMM_WITHDRAW && result == tesSUCCESS &&
|
|
accountsDeleted_ == 1)
|
|
return true;
|
|
|
|
if (accountsDeleted_ == 0)
|
|
return true;
|
|
|
|
JLOG(j.fatal()) << "Invariant failed: an account root was deleted";
|
|
return false;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
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)
|
|
{
|
|
switch (after->getType())
|
|
{
|
|
case ltACCOUNT_ROOT:
|
|
case ltDIR_NODE:
|
|
case ltRIPPLE_STATE:
|
|
case ltTICKET:
|
|
case ltSIGNER_LIST:
|
|
case ltOFFER:
|
|
case ltLEDGER_HASHES:
|
|
case ltAMENDMENTS:
|
|
case ltFEE_SETTINGS:
|
|
case ltESCROW:
|
|
case ltPAYCHAN:
|
|
case ltCHECK:
|
|
case ltDEPOSIT_PREAUTH:
|
|
case ltNEGATIVE_UNL:
|
|
case ltNFTOKEN_PAGE:
|
|
case ltNFTOKEN_OFFER:
|
|
case ltAMM:
|
|
case ltBRIDGE:
|
|
case ltXCHAIN_OWNED_CLAIM_ID:
|
|
case ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID:
|
|
break;
|
|
default:
|
|
invalidTypeAdded_ = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
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];
|
|
}
|
|
}
|
|
|
|
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 ((tx.getTxnType() == ttPAYMENT || tx.getTxnType() == ttAMM_CREATE ||
|
|
tx.getTxnType() == ttXCHAIN_ADD_CLAIM_ATTESTATION ||
|
|
tx.getTxnType() == ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION) &&
|
|
result == tesSUCCESS)
|
|
{
|
|
std::uint32_t const startingSeq{
|
|
view.rules().enabled(featureDeletableAccounts) ? view.seq() : 1};
|
|
|
|
if (accountSeq_ != startingSeq)
|
|
{
|
|
JLOG(j.fatal()) << "Invariant failed: account created with "
|
|
"wrong starting sequence number";
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
JLOG(j.fatal()) << "Invariant failed: account root created "
|
|
"by a non-Payment, by an unsuccessful transaction, "
|
|
"or by AMM";
|
|
return false;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
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;
|
|
|
|
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 && before->getType() == ltNFTOKEN_PAGE)
|
|
check(before);
|
|
|
|
if (after && after->getType() == ltNFTOKEN_PAGE)
|
|
check(after);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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 (TxType const txType = tx.getTxnType();
|
|
txType != ttNFTOKEN_MINT && txType != ttNFTOKEN_BURN)
|
|
{
|
|
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++;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
} // namespace ripple
|