Add deep freeze feature (XLS-77d) (#5187)

- spec: XRPLF/XRPL-Standards#220
- amendment: "DeepFreeze"
- implemented deep freeze spec to allow token issuers to prevent currency holders from being able to acquire more of these tokens.
- in combination with normal freeze, deep freeze effectively prevents any balance trust line balance change of a currency holder (except direct issuer <-> holder payments).
- added 2 new invariant checks to verify that deep freeze cannot be enacted without normal freeze and transfer is not frozen.
- made some fixes to existing freeze handling.

Co-authored-by: Ed Hennis <ed@ripple.com>
Co-authored-by: Howard Hinnant <howard.hinnant@gmail.com>
This commit is contained in:
Vlad
2025-01-31 18:40:33 +00:00
committed by Qi Zhao
parent 0613024aac
commit ffc88d703b
21 changed files with 2479 additions and 49 deletions

View File

@@ -80,7 +80,7 @@ namespace detail {
// Feature.cpp. Because it's only used to reserve storage, and determine how
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
// the actual number of amendments. A LogicError on startup will verify this.
static constexpr std::size_t numFeatures = 85;
static constexpr std::size_t numFeatures = 86;
/** Amendments that this server supports and the default voting behavior.
Whether they are enabled depends on the Rules defined in the validated

View File

@@ -160,10 +160,12 @@ enum LedgerSpecificFlags {
lsfHighAuth = 0x00080000,
lsfLowNoRipple = 0x00100000,
lsfHighNoRipple = 0x00200000,
lsfLowFreeze = 0x00400000, // True, low side has set freeze flag
lsfHighFreeze = 0x00800000, // True, high side has set freeze flag
lsfAMMNode = 0x01000000, // True, trust line to AMM. Used by client
// apps to identify payments via AMM.
lsfLowFreeze = 0x00400000, // True, low side has set freeze flag
lsfHighFreeze = 0x00800000, // True, high side has set freeze flag
lsfLowDeepFreeze = 0x02000000, // True, low side has set deep freeze flag
lsfHighDeepFreeze = 0x04000000, // True, high side has set deep freeze flag
lsfAMMNode = 0x01000000, // True, trust line to AMM. Used by client
// apps to identify payments via AMM.
// ltSIGNER_LIST
lsfOneOwnerCount = 0x00010000, // True, uses only one OwnerCount

View File

@@ -114,9 +114,11 @@ constexpr std::uint32_t tfSetNoRipple = 0x00020000;
constexpr std::uint32_t tfClearNoRipple = 0x00040000;
constexpr std::uint32_t tfSetFreeze = 0x00100000;
constexpr std::uint32_t tfClearFreeze = 0x00200000;
constexpr std::uint32_t tfSetDeepFreeze = 0x00400000;
constexpr std::uint32_t tfClearDeepFreeze = 0x00800000;
constexpr std::uint32_t tfTrustSetMask =
~(tfUniversal | tfSetfAuth | tfSetNoRipple | tfClearNoRipple | tfSetFreeze |
tfClearFreeze);
tfClearFreeze | tfSetDeepFreeze | tfClearDeepFreeze);
// EnableAmendment flags:
constexpr std::uint32_t tfGotMajority = 0x00010000;

View File

@@ -29,6 +29,7 @@
// If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h.
XRPL_FEATURE(DeepFreeze, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(PermissionedDomains, Supported::no, VoteBehavior::DefaultNo)
XRPL_FEATURE(DynamicNFT, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(Credentials, Supported::yes, VoteBehavior::DefaultNo)
@@ -116,3 +117,4 @@ XRPL_FIX (NFTokenNegOffer, Supported::yes, VoteBehavior::Obsolete)
XRPL_FIX (NFTokenDirV1, Supported::yes, VoteBehavior::Obsolete)
XRPL_FEATURE(NonFungibleTokensV1, Supported::yes, VoteBehavior::Obsolete)
XRPL_FEATURE(CryptoConditionsSuite, Supported::yes, VoteBehavior::Obsolete)

View File

@@ -284,6 +284,8 @@ JSS(flags); // out: AccountOffers,
JSS(forward); // in: AccountTx
JSS(freeze); // out: AccountLines
JSS(freeze_peer); // out: AccountLines
JSS(deep_freeze); // out: AccountLines
JSS(deep_freeze_peer); // out: AccountLines
JSS(frozen_balances); // out: GatewayBalances
JSS(full); // in: LedgerClearer, handlers/Ledger
JSS(full_reply); // out: PathFind

File diff suppressed because it is too large Load Diff

View File

@@ -408,6 +408,183 @@ class Invariants_test : public beast::unit_test::suite
});
}
void
testNoDeepFreezeTrustLinesWithoutFreeze()
{
using namespace test::jtx;
testcase << "trust lines with deep freeze flag without freeze "
"not allowed";
doInvariantCheck(
{{"a trust line with deep freeze flag without normal freeze was "
"created"}},
[](Account const& A1, Account const& A2, ApplyContext& ac) {
auto const sleNew = std::make_shared<SLE>(
keylet::line(A1, A2, A1["USD"].currency));
sleNew->setFieldAmount(sfLowLimit, A1["USD"](0));
sleNew->setFieldAmount(sfHighLimit, A1["USD"](0));
std::uint32_t uFlags = 0u;
uFlags |= lsfLowDeepFreeze;
sleNew->setFieldU32(sfFlags, uFlags);
ac.view().insert(sleNew);
return true;
});
doInvariantCheck(
{{"a trust line with deep freeze flag without normal freeze was "
"created"}},
[](Account const& A1, Account const& A2, ApplyContext& ac) {
auto const sleNew = std::make_shared<SLE>(
keylet::line(A1, A2, A1["USD"].currency));
sleNew->setFieldAmount(sfLowLimit, A1["USD"](0));
sleNew->setFieldAmount(sfHighLimit, A1["USD"](0));
std::uint32_t uFlags = 0u;
uFlags |= lsfHighDeepFreeze;
sleNew->setFieldU32(sfFlags, uFlags);
ac.view().insert(sleNew);
return true;
});
doInvariantCheck(
{{"a trust line with deep freeze flag without normal freeze was "
"created"}},
[](Account const& A1, Account const& A2, ApplyContext& ac) {
auto const sleNew = std::make_shared<SLE>(
keylet::line(A1, A2, A1["USD"].currency));
sleNew->setFieldAmount(sfLowLimit, A1["USD"](0));
sleNew->setFieldAmount(sfHighLimit, A1["USD"](0));
std::uint32_t uFlags = 0u;
uFlags |= lsfLowDeepFreeze | lsfHighDeepFreeze;
sleNew->setFieldU32(sfFlags, uFlags);
ac.view().insert(sleNew);
return true;
});
doInvariantCheck(
{{"a trust line with deep freeze flag without normal freeze was "
"created"}},
[](Account const& A1, Account const& A2, ApplyContext& ac) {
auto const sleNew = std::make_shared<SLE>(
keylet::line(A1, A2, A1["USD"].currency));
sleNew->setFieldAmount(sfLowLimit, A1["USD"](0));
sleNew->setFieldAmount(sfHighLimit, A1["USD"](0));
std::uint32_t uFlags = 0u;
uFlags |= lsfLowDeepFreeze | lsfHighFreeze;
sleNew->setFieldU32(sfFlags, uFlags);
ac.view().insert(sleNew);
return true;
});
doInvariantCheck(
{{"a trust line with deep freeze flag without normal freeze was "
"created"}},
[](Account const& A1, Account const& A2, ApplyContext& ac) {
auto const sleNew = std::make_shared<SLE>(
keylet::line(A1, A2, A1["USD"].currency));
sleNew->setFieldAmount(sfLowLimit, A1["USD"](0));
sleNew->setFieldAmount(sfHighLimit, A1["USD"](0));
std::uint32_t uFlags = 0u;
uFlags |= lsfLowFreeze | lsfHighDeepFreeze;
sleNew->setFieldU32(sfFlags, uFlags);
ac.view().insert(sleNew);
return true;
});
}
void
testTransfersNotFrozen()
{
using namespace test::jtx;
testcase << "transfers when frozen";
Account G1{"G1"};
// Helper function to establish the trustlines
auto const createTrustlines =
[&](Account const& A1, Account const& A2, Env& env) {
// Preclose callback to establish trust lines with gateway
env.fund(XRP(1000), G1);
env.trust(G1["USD"](10000), A1);
env.trust(G1["USD"](10000), A2);
env.close();
env(pay(G1, A1, G1["USD"](1000)));
env(pay(G1, A2, G1["USD"](1000)));
env.close();
return true;
};
auto const A1FrozenByIssuer =
[&](Account const& A1, Account const& A2, Env& env) {
createTrustlines(A1, A2, env);
env(trust(G1, A1["USD"](10000), tfSetFreeze));
env.close();
return true;
};
auto const A1DeepFrozenByIssuer =
[&](Account const& A1, Account const& A2, Env& env) {
A1FrozenByIssuer(A1, A2, env);
env(trust(G1, A1["USD"](10000), tfSetDeepFreeze));
env.close();
return true;
};
auto const changeBalances = [&](Account const& A1,
Account const& A2,
ApplyContext& ac,
int A1Balance,
int A2Balance) {
auto const sleA1 = ac.view().peek(keylet::line(A1, G1["USD"]));
auto const sleA2 = ac.view().peek(keylet::line(A2, G1["USD"]));
sleA1->setFieldAmount(sfBalance, G1["USD"](A1Balance));
sleA2->setFieldAmount(sfBalance, G1["USD"](A2Balance));
ac.view().update(sleA1);
ac.view().update(sleA2);
};
// test: imitating frozen A1 making a payment to A2.
doInvariantCheck(
{{"Attempting to move frozen funds"}},
[&](Account const& A1, Account const& A2, ApplyContext& ac) {
changeBalances(A1, A2, ac, -900, -1100);
return true;
},
XRPAmount{},
STTx{ttPAYMENT, [](STObject& tx) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
A1FrozenByIssuer);
// test: imitating deep frozen A1 making a payment to A2.
doInvariantCheck(
{{"Attempting to move frozen funds"}},
[&](Account const& A1, Account const& A2, ApplyContext& ac) {
changeBalances(A1, A2, ac, -900, -1100);
return true;
},
XRPAmount{},
STTx{ttPAYMENT, [](STObject& tx) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
A1DeepFrozenByIssuer);
// test: imitating A2 making a payment to deep frozen A1.
doInvariantCheck(
{{"Attempting to move frozen funds"}},
[&](Account const& A1, Account const& A2, ApplyContext& ac) {
changeBalances(A1, A2, ac, -1100, -900);
return true;
},
XRPAmount{},
STTx{ttPAYMENT, [](STObject& tx) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
A1DeepFrozenByIssuer);
}
void
testXRPBalanceCheck()
{
@@ -1061,6 +1238,8 @@ public:
testAccountRootsDeletedClean();
testTypesMatch();
testNoXRPTrustLine();
testNoDeepFreezeTrustLinesWithoutFreeze();
testTransfersNotFrozen();
testXRPBalanceCheck();
testTransactionFeeCheck();
testNoBadOffers();

View File

@@ -167,7 +167,11 @@ public:
env.close();
// Set flags on gw2 trust lines so we can look for them.
env(trust(alice, gw2Currency(0), gw2, tfSetNoRipple | tfSetFreeze));
env(trust(
alice,
gw2Currency(0),
gw2,
tfSetNoRipple | tfSetFreeze | tfSetDeepFreeze));
}
env.close();
LedgerInfo const ledger58Info = env.closed()->info();
@@ -344,6 +348,7 @@ public:
gw2.human() + R"("})");
auto const& line = lines[jss::result][jss::lines][0u];
BEAST_EXPECT(line[jss::freeze].asBool() == true);
BEAST_EXPECT(line[jss::deep_freeze].asBool() == true);
BEAST_EXPECT(line[jss::no_ripple].asBool() == true);
BEAST_EXPECT(line[jss::peer_authorized].asBool() == true);
}
@@ -359,6 +364,7 @@ public:
alice.human() + R"("})");
auto const& lineA = linesA[jss::result][jss::lines][0u];
BEAST_EXPECT(lineA[jss::freeze_peer].asBool() == true);
BEAST_EXPECT(lineA[jss::deep_freeze_peer].asBool() == true);
BEAST_EXPECT(lineA[jss::no_ripple_peer].asBool() == true);
BEAST_EXPECT(lineA[jss::authorized].asBool() == true);
@@ -981,7 +987,11 @@ public:
env.close();
// Set flags on gw2 trust lines so we can look for them.
env(trust(alice, gw2Currency(0), gw2, tfSetNoRipple | tfSetFreeze));
env(trust(
alice,
gw2Currency(0),
gw2,
tfSetNoRipple | tfSetFreeze | tfSetDeepFreeze));
}
env.close();
LedgerInfo const ledger58Info = env.closed()->info();
@@ -1311,6 +1321,7 @@ public:
gw2.human() + R"("}})");
auto const& line = lines[jss::result][jss::lines][0u];
BEAST_EXPECT(line[jss::freeze].asBool() == true);
BEAST_EXPECT(line[jss::deep_freeze].asBool() == true);
BEAST_EXPECT(line[jss::no_ripple].asBool() == true);
BEAST_EXPECT(line[jss::peer_authorized].asBool() == true);
BEAST_EXPECT(
@@ -1338,6 +1349,7 @@ public:
alice.human() + R"("}})");
auto const& lineA = linesA[jss::result][jss::lines][0u];
BEAST_EXPECT(lineA[jss::freeze_peer].asBool() == true);
BEAST_EXPECT(lineA[jss::deep_freeze_peer].asBool() == true);
BEAST_EXPECT(lineA[jss::no_ripple_peer].asBool() == true);
BEAST_EXPECT(lineA[jss::authorized].asBool() == true);
BEAST_EXPECT(

View File

@@ -139,6 +139,13 @@ public:
return mFlags & (mViewLowest ? lsfLowFreeze : lsfHighFreeze);
}
/** Have we set the deep freeze flag on our peer */
bool
getDeepFreeze() const
{
return mFlags & (mViewLowest ? lsfLowDeepFreeze : lsfHighDeepFreeze);
}
/** Has the peer set the freeze flag on us */
bool
getFreezePeer() const
@@ -146,6 +153,13 @@ public:
return mFlags & (!mViewLowest ? lsfLowFreeze : lsfHighFreeze);
}
/** Has the peer set the deep freeze flag on us */
bool
getDeepFreezePeer() const
{
return mFlags & (!mViewLowest ? lsfLowDeepFreeze : lsfHighDeepFreeze);
}
STAmount const&
getBalance() const
{

View File

@@ -52,6 +52,12 @@ checkFreeze(
{
return terNO_LINE;
}
// Unlike normal freeze, a deep frozen trust line acts the same
// regardless of which side froze it
if (sle->isFlag(lsfHighDeepFreeze) || sle->isFlag(lsfLowDeepFreeze))
{
return terNO_LINE;
}
}
return tesSUCCESS;

View File

@@ -392,6 +392,7 @@ CashCheck::doApply()
false, // authorize account
(sleDst->getFlags() & lsfDefaultRipple) == 0,
false, // freeze trust line
false, // deep freeze trust line
initialBalance, // zero initial balance
Issue(currency, account_), // limit of zero
0, // quality in

View File

@@ -259,6 +259,32 @@ CreateOffer::checkAcceptAsset(
}
}
// An account can not create a trustline to itself, so no line can exist
// to be frozen. Additionally, an issuer can always accept its own
// issuance.
if (issue.account == id)
{
return tesSUCCESS;
}
auto const trustLine =
view.read(keylet::line(id, issue.account, issue.currency));
if (!trustLine)
{
return tesSUCCESS;
}
// There's no difference which side enacted deep freeze, accepting
// tokens shouldn't be possible.
bool const deepFrozen =
(*trustLine)[sfFlags] & (lsfLowDeepFreeze | lsfHighDeepFreeze);
if (deepFrozen)
{
return tecFROZEN;
}
return tesSUCCESS;
}

View File

@@ -556,6 +556,322 @@ NoXRPTrustLines::finalize(
//------------------------------------------------------------------------------
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)
{
XRPL_ASSERT(
enforce,
"ripple::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, "ripple::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,
"ripple::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) && tx.getTxnType() == ttAMM_CLAWBACK)
{
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();
XRPL_ASSERT(
enforce,
"ripple::TransfersNotFrozen::validateFrozenState : enforce "
"invariant.");
if (enforce)
{
return false;
}
return true;
}
//------------------------------------------------------------------------------
void
ValidNewAccountRoot::visitEntry(
bool,

View File

@@ -270,6 +270,114 @@ public:
beast::Journal const&);
};
/**
* @brief Invariant: Trust lines with deep freeze flag are not allowed if normal
* freeze flag is not set.
*
* We iterate all the trust lines created by this transaction and ensure
* that they don't have deep freeze flag set without normal freeze flag set.
*/
class NoDeepFreezeTrustLinesWithoutFreeze
{
bool deepFreezeWithoutFreeze_ = false;
public:
void
visitEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&);
bool
finalize(
STTx const&,
TER const,
XRPAmount const,
ReadView const&,
beast::Journal const&);
};
/**
* @brief Invariant: frozen trust line balance change is not allowed.
*
* We iterate all affected trust lines and ensure that they don't have
* unexpected change of balance if they're frozen.
*/
class TransfersNotFrozen
{
struct BalanceChange
{
std::shared_ptr<SLE const> const line;
int const balanceChangeSign;
};
struct IssuerChanges
{
std::vector<BalanceChange> senders;
std::vector<BalanceChange> receivers;
};
using ByIssuer = std::map<Issue, IssuerChanges>;
ByIssuer balanceChanges_;
std::map<AccountID, std::shared_ptr<SLE const> const> possibleIssuers_;
public:
void
visitEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&);
bool
finalize(
STTx const&,
TER const,
XRPAmount const,
ReadView const&,
beast::Journal const&);
private:
bool
isValidEntry(
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after);
STAmount
calculateBalanceChange(
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after,
bool isDelete);
void
recordBalance(Issue const& issue, BalanceChange change);
void
recordBalanceChanges(
std::shared_ptr<SLE const> const& after,
STAmount const& balanceChange);
std::shared_ptr<SLE const>
findIssuer(AccountID const& issuerID, ReadView const& view);
bool
validateIssuerChanges(
std::shared_ptr<SLE const> const& issuer,
IssuerChanges const& changes,
STTx const& tx,
beast::Journal const& j,
bool enforce);
bool
validateFrozenState(
BalanceChange const& change,
bool high,
STTx const& tx,
beast::Journal const& j,
bool enforce,
bool globalFreeze);
};
/**
* @brief Invariant: offers should be for non-negative amounts and must not
* be XRP to XRP.
@@ -518,6 +626,8 @@ using InvariantChecks = std::tuple<
XRPBalanceChecks,
XRPNotCreated,
NoXRPTrustLines,
NoDeepFreezeTrustLinesWithoutFreeze,
TransfersNotFrozen,
NoBadOffers,
NoZeroEscrow,
ValidNewAccountRoot,

View File

@@ -268,6 +268,20 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx)
ctx.j) < needed)
return tecINSUFFICIENT_FUNDS;
}
// Make sure that we are allowed to hold what the taker will pay us.
// This is a similar approach taken by usual offers.
if (!needed.native())
{
auto const result = checkAcceptAsset(
ctx.view,
ctx.flags,
(*so)[sfOwner],
ctx.j,
needed.asset().get<Issue>());
if (result != tesSUCCESS)
return result;
}
}
// Fix a bug where the transfer of an NFToken with a transfer fee could
@@ -510,4 +524,62 @@ NFTokenAcceptOffer::doApply()
return tecINTERNAL;
}
TER
NFTokenAcceptOffer::checkAcceptAsset(
ReadView const& view,
ApplyFlags const flags,
AccountID const id,
beast::Journal const j,
Issue const& issue)
{
// Only valid for custom currencies
if (!view.rules().enabled(featureDeepFreeze))
{
return tesSUCCESS;
}
XRPL_ASSERT(
!isXRP(issue.currency),
"NFTokenAcceptOffer::checkAcceptAsset : valid to check.");
auto const issuerAccount = view.read(keylet::account(issue.account));
if (!issuerAccount)
{
JLOG(j.debug())
<< "delay: can't receive IOUs from non-existent issuer: "
<< to_string(issue.account);
return tecNO_ISSUER;
}
// An account can not create a trustline to itself, so no line can exist
// to be frozen. Additionally, an issuer can always accept its own
// issuance.
if (issue.account == id)
{
return tesSUCCESS;
}
auto const trustLine =
view.read(keylet::line(id, issue.account, issue.currency));
if (!trustLine)
{
return tesSUCCESS;
}
// There's no difference which side enacted deep freeze, accepting
// tokens shouldn't be possible.
bool const deepFrozen =
(*trustLine)[sfFlags] & (lsfLowDeepFreeze | lsfHighDeepFreeze);
if (deepFrozen)
{
return tecFROZEN;
}
return tesSUCCESS;
}
} // namespace ripple

View File

@@ -44,6 +44,14 @@ private:
AccountID const& seller,
uint256 const& nfTokenID);
static TER
checkAcceptAsset(
ReadView const& view,
ApplyFlags const flags,
AccountID const id,
beast::Journal const j,
Issue const& issue);
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};

View File

@@ -273,6 +273,20 @@ TOfferStreamBase<TIn, TOut>::step()
continue;
}
bool const deepFrozen = isDeepFrozen(
view_,
offer_.owner(),
offer_.issueIn().currency,
offer_.issueIn().account);
if (deepFrozen)
{
JLOG(j_.trace())
<< "Removing deep frozen unfunded offer " << entry->key();
permRmOffer(entry->key());
offer_ = TOffer<TIn, TOut>{};
continue;
}
// Calculate owner funds
ownerFunds_ = accountFundsHelper(
view_,

View File

@@ -26,6 +26,42 @@
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/st.h>
namespace {
uint32_t
computeFreezeFlags(
uint32_t uFlags,
bool bHigh,
bool bNoFreeze,
bool bSetFreeze,
bool bClearFreeze,
bool bSetDeepFreeze,
bool bClearDeepFreeze)
{
if (bSetFreeze && !bClearFreeze && !bNoFreeze)
{
uFlags |= (bHigh ? ripple::lsfHighFreeze : ripple::lsfLowFreeze);
}
else if (bClearFreeze && !bSetFreeze)
{
uFlags &= ~(bHigh ? ripple::lsfHighFreeze : ripple::lsfLowFreeze);
}
if (bSetDeepFreeze && !bClearDeepFreeze && !bNoFreeze)
{
uFlags |=
(bHigh ? ripple::lsfHighDeepFreeze : ripple::lsfLowDeepFreeze);
}
else if (bClearDeepFreeze && !bSetDeepFreeze)
{
uFlags &=
~(bHigh ? ripple::lsfHighDeepFreeze : ripple::lsfLowDeepFreeze);
}
return uFlags;
}
} // namespace
namespace ripple {
NotTEC
@@ -45,6 +81,16 @@ SetTrust::preflight(PreflightContext const& ctx)
return temINVALID_FLAG;
}
if (!ctx.rules.enabled(featureDeepFreeze))
{
// Even though the deep freeze flags are included in the
// `tfTrustSetMask`, they are not valid if the amendment is not enabled.
if (uTxFlags & (tfSetDeepFreeze | tfClearDeepFreeze))
{
return temINVALID_FLAG;
}
}
STAmount const saLimitAmount(tx.getFieldAmount(sfLimitAmount));
if (!isLegalNet(saLimitAmount))
@@ -182,6 +228,58 @@ SetTrust::preclaim(PreclaimContext const& ctx)
}
}
// Checking all freeze/deep freeze flag invariants.
if (ctx.view.rules().enabled(featureDeepFreeze))
{
bool const bNoFreeze = sle->isFlag(lsfNoFreeze);
bool const bSetFreeze = (uTxFlags & tfSetFreeze);
bool const bSetDeepFreeze = (uTxFlags & tfSetDeepFreeze);
if (bNoFreeze && (bSetFreeze || bSetDeepFreeze))
{
// Cannot freeze the trust line if NoFreeze is set
return tecNO_PERMISSION;
}
bool const bClearFreeze = (uTxFlags & tfClearFreeze);
bool const bClearDeepFreeze = (uTxFlags & tfClearDeepFreeze);
if ((bSetFreeze || bSetDeepFreeze) &&
(bClearFreeze || bClearDeepFreeze))
{
// Freezing and unfreezing in the same transaction should be
// illegal
return tecNO_PERMISSION;
}
bool const bHigh = id > uDstAccountID;
// Fetching current state of trust line
auto const sleRippleState =
ctx.view.read(keylet::line(id, uDstAccountID, currency));
std::uint32_t uFlags =
sleRippleState ? sleRippleState->getFieldU32(sfFlags) : 0u;
// Computing expected trust line state
uFlags = computeFreezeFlags(
uFlags,
bHigh,
bNoFreeze,
bSetFreeze,
bClearFreeze,
bSetDeepFreeze,
bClearDeepFreeze);
auto const frozen = uFlags & (bHigh ? lsfHighFreeze : lsfLowFreeze);
auto const deepFrozen =
uFlags & (bHigh ? lsfHighDeepFreeze : lsfLowDeepFreeze);
// Trying to set deep freeze on not already frozen trust line must
// fail. This also checks that clearing normal freeze while deep
// frozen must not work
if (deepFrozen && !frozen)
{
return tecNO_PERMISSION;
}
}
return tesSUCCESS;
}
@@ -197,7 +295,7 @@ SetTrust::doApply()
Currency const currency(saLimitAmount.getCurrency());
AccountID uDstAccountID(saLimitAmount.getIssuer());
// true, iff current is high account.
// true, if current is high account.
bool const bHigh = account_ > uDstAccountID;
auto const sle = view().peek(keylet::account(account_));
@@ -242,13 +340,15 @@ SetTrust::doApply()
bool const bClearNoRipple = (uTxFlags & tfClearNoRipple);
bool const bSetFreeze = (uTxFlags & tfSetFreeze);
bool const bClearFreeze = (uTxFlags & tfClearFreeze);
bool const bSetDeepFreeze = (uTxFlags & tfSetDeepFreeze);
bool const bClearDeepFreeze = (uTxFlags & tfClearDeepFreeze);
auto viewJ = ctx_.app.journal("View");
// Trust lines to self are impossible but because of the old bug there are
// two on 19-02-2022. This code was here to allow those trust lines to be
// deleted. The fixTrustLinesToSelf fix amendment will remove them when it
// enables so this code will no longer be needed.
// Trust lines to self are impossible but because of the old bug there
// are two on 19-02-2022. This code was here to allow those trust lines
// to be deleted. The fixTrustLinesToSelf fix amendment will remove them
// when it enables so this code will no longer be needed.
if (!view().rules().enabled(fixTrustLinesToSelf) &&
account_ == uDstAccountID)
{
@@ -408,14 +508,16 @@ SetTrust::doApply()
uFlagsOut &= ~(bHigh ? lsfHighNoRipple : lsfLowNoRipple);
}
if (bSetFreeze && !bClearFreeze && !sle->isFlag(lsfNoFreeze))
{
uFlagsOut |= (bHigh ? lsfHighFreeze : lsfLowFreeze);
}
else if (bClearFreeze && !bSetFreeze)
{
uFlagsOut &= ~(bHigh ? lsfHighFreeze : lsfLowFreeze);
}
// Have to use lsfNoFreeze to maintain pre-deep freeze behavior
bool const bNoFreeze = sle->isFlag(lsfNoFreeze);
uFlagsOut = computeFreezeFlags(
uFlagsOut,
bHigh,
bNoFreeze,
bSetFreeze,
bClearFreeze,
bSetDeepFreeze,
bClearDeepFreeze);
if (QUALITY_ONE == uLowQualityOut)
uLowQualityOut = 0;
@@ -498,8 +600,8 @@ SetTrust::doApply()
// Reserve is not scaled by load.
else if (bReserveIncrease && mPriorBalance < reserveCreate)
{
JLOG(j_.trace())
<< "Delay transaction: Insufficent reserve to add trust line.";
JLOG(j_.trace()) << "Delay transaction: Insufficent reserve to "
"add trust line.";
// Another transaction could provide XRP to the account and then
// this transaction would succeed.
@@ -515,17 +617,18 @@ SetTrust::doApply()
// Line does not exist.
else if (
!saLimitAmount && // Setting default limit.
(!bQualityIn || !uQualityIn) && // Not setting quality in or setting
// default quality in.
(!bQualityOut || !uQualityOut) && // Not setting quality out or setting
// default quality out.
(!bQualityIn || !uQualityIn) && // Not setting quality in or
// setting default quality in.
(!bQualityOut || !uQualityOut) && // Not setting quality out or
// setting default quality out.
(!bSetAuth))
{
JLOG(j_.trace())
<< "Redundant: Setting non-existent ripple line to defaults.";
return tecNO_LINE_REDUNDANT;
}
else if (mPriorBalance < reserveCreate) // Reserve is not scaled by load.
else if (mPriorBalance < reserveCreate) // Reserve is not scaled by
// load.
{
JLOG(j_.trace()) << "Delay transaction: Line does not exist. "
"Insufficent reserve to create line.";
@@ -555,6 +658,7 @@ SetTrust::doApply()
bSetAuth,
bSetNoRipple && !bClearNoRipple,
bSetFreeze && !bClearFreeze,
bSetDeepFreeze,
saBalance,
saLimitAllow, // Limit for who is being charged.
uQualityIn,

View File

@@ -153,6 +153,13 @@ isFrozen(ReadView const& view, AccountID const& account, Asset const& asset)
asset.value());
}
[[nodiscard]] bool
isDeepFrozen(
ReadView const& view,
AccountID const& account,
Currency const& currency,
AccountID const& issuer);
// Returns the amount an account can spend without going into debt.
//
// <-- saAmount: amount of currency held by account. May be negative.
@@ -438,6 +445,7 @@ trustCreate(
const bool bAuth, // --> authorize account.
const bool bNoRipple, // --> others cannot ripple through
const bool bFreeze, // --> funds cannot leave
bool bDeepFreeze, // --> can neither receive nor send funds
STAmount const& saBalance, // --> balance of account being set.
// Issuer should be noAccount()
STAmount const& saLimit, // --> limit for account being set.

View File

@@ -267,6 +267,32 @@ isFrozen(
isIndividualFrozen(view, account, mptIssue);
}
bool
isDeepFrozen(
ReadView const& view,
AccountID const& account,
Currency const& currency,
AccountID const& issuer)
{
if (isXRP(currency))
{
return false;
}
if (issuer == account)
{
return false;
}
auto const sle = view.read(keylet::line(account, issuer, currency));
if (!sle)
{
return false;
}
return sle->isFlag(lsfHighDeepFreeze) || sle->isFlag(lsfLowDeepFreeze);
}
STAmount
accountHolds(
ReadView const& view,
@@ -284,17 +310,25 @@ accountHolds(
// IOU: Return balance on trust line modulo freeze
auto const sle = view.read(keylet::line(account, issuer, currency));
if (!sle)
{
amount.clear(Issue{currency, issuer});
}
else if (
(zeroIfFrozen == fhZERO_IF_FROZEN) &&
isFrozen(view, account, currency, issuer))
{
amount.clear(Issue{currency, issuer});
}
else
auto const allowBalance = [&]() {
if (!sle)
{
return false;
}
if (zeroIfFrozen == fhZERO_IF_FROZEN)
{
if (isFrozen(view, account, currency, issuer) ||
isDeepFrozen(view, account, currency, issuer))
{
return false;
}
}
return true;
}();
if (allowBalance)
{
amount = sle->getFieldAmount(sfBalance);
if (account > issuer)
@@ -304,6 +338,11 @@ accountHolds(
}
amount.setIssuer(issuer);
}
else
{
amount.clear(Issue{currency, issuer});
}
JLOG(j.trace()) << "accountHolds:" << " account=" << to_string(account)
<< " amount=" << amount.getFullText();
@@ -863,6 +902,7 @@ trustCreate(
const bool bAuth, // --> authorize account.
const bool bNoRipple, // --> others cannot ripple through
const bool bFreeze, // --> funds cannot leave
bool bDeepFreeze, // --> can neither receive nor send funds
STAmount const& saBalance, // --> balance of account being set.
// Issuer should be noAccount()
STAmount const& saLimit, // --> limit for account being set.
@@ -944,7 +984,11 @@ trustCreate(
}
if (bFreeze)
{
uFlags |= (!bSetHigh ? lsfLowFreeze : lsfHighFreeze);
uFlags |= (bSetHigh ? lsfHighFreeze : lsfLowFreeze);
}
if (bDeepFreeze)
{
uFlags |= (bSetHigh ? lsfHighDeepFreeze : lsfLowDeepFreeze);
}
if ((slePeer->getFlags() & lsfDefaultRipple) == 0)
@@ -1189,6 +1233,7 @@ rippleCreditIOU(
false,
noRipple,
false,
false,
saBalance,
saReceiverLimit,
0,
@@ -1688,6 +1733,7 @@ issueIOU(
false,
noRipple,
false,
false,
final_balance,
limit,
0,

View File

@@ -62,6 +62,10 @@ addLine(Json::Value& jsonLines, RPCTrustLine const& line)
jPeer[jss::freeze] = true;
if (line.getFreezePeer())
jPeer[jss::freeze_peer] = true;
if (line.getDeepFreeze())
jPeer[jss::deep_freeze] = true;
if (line.getDeepFreezePeer())
jPeer[jss::deep_freeze_peer] = true;
}
// {