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 // 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 // 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. // 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. /** Amendments that this server supports and the default voting behavior.
Whether they are enabled depends on the Rules defined in the validated Whether they are enabled depends on the Rules defined in the validated

View File

@@ -162,6 +162,8 @@ enum LedgerSpecificFlags {
lsfHighNoRipple = 0x00200000, lsfHighNoRipple = 0x00200000,
lsfLowFreeze = 0x00400000, // True, low side has set freeze flag lsfLowFreeze = 0x00400000, // True, low side has set freeze flag
lsfHighFreeze = 0x00800000, // True, high 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 lsfAMMNode = 0x01000000, // True, trust line to AMM. Used by client
// apps to identify payments via AMM. // apps to identify payments via AMM.

View File

@@ -114,9 +114,11 @@ constexpr std::uint32_t tfSetNoRipple = 0x00020000;
constexpr std::uint32_t tfClearNoRipple = 0x00040000; constexpr std::uint32_t tfClearNoRipple = 0x00040000;
constexpr std::uint32_t tfSetFreeze = 0x00100000; constexpr std::uint32_t tfSetFreeze = 0x00100000;
constexpr std::uint32_t tfClearFreeze = 0x00200000; constexpr std::uint32_t tfClearFreeze = 0x00200000;
constexpr std::uint32_t tfSetDeepFreeze = 0x00400000;
constexpr std::uint32_t tfClearDeepFreeze = 0x00800000;
constexpr std::uint32_t tfTrustSetMask = constexpr std::uint32_t tfTrustSetMask =
~(tfUniversal | tfSetfAuth | tfSetNoRipple | tfClearNoRipple | tfSetFreeze | ~(tfUniversal | tfSetfAuth | tfSetNoRipple | tfClearNoRipple | tfSetFreeze |
tfClearFreeze); tfClearFreeze | tfSetDeepFreeze | tfClearDeepFreeze);
// EnableAmendment flags: // EnableAmendment flags:
constexpr std::uint32_t tfGotMajority = 0x00010000; 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` // If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h. // in include/xrpl/protocol/Feature.h.
XRPL_FEATURE(DeepFreeze, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(PermissionedDomains, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(PermissionedDomains, Supported::no, VoteBehavior::DefaultNo)
XRPL_FEATURE(DynamicNFT, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(DynamicNFT, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(Credentials, 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_FIX (NFTokenDirV1, Supported::yes, VoteBehavior::Obsolete)
XRPL_FEATURE(NonFungibleTokensV1, Supported::yes, VoteBehavior::Obsolete) XRPL_FEATURE(NonFungibleTokensV1, Supported::yes, VoteBehavior::Obsolete)
XRPL_FEATURE(CryptoConditionsSuite, 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(forward); // in: AccountTx
JSS(freeze); // out: AccountLines JSS(freeze); // out: AccountLines
JSS(freeze_peer); // out: AccountLines JSS(freeze_peer); // out: AccountLines
JSS(deep_freeze); // out: AccountLines
JSS(deep_freeze_peer); // out: AccountLines
JSS(frozen_balances); // out: GatewayBalances JSS(frozen_balances); // out: GatewayBalances
JSS(full); // in: LedgerClearer, handlers/Ledger JSS(full); // in: LedgerClearer, handlers/Ledger
JSS(full_reply); // out: PathFind 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 void
testXRPBalanceCheck() testXRPBalanceCheck()
{ {
@@ -1061,6 +1238,8 @@ public:
testAccountRootsDeletedClean(); testAccountRootsDeletedClean();
testTypesMatch(); testTypesMatch();
testNoXRPTrustLine(); testNoXRPTrustLine();
testNoDeepFreezeTrustLinesWithoutFreeze();
testTransfersNotFrozen();
testXRPBalanceCheck(); testXRPBalanceCheck();
testTransactionFeeCheck(); testTransactionFeeCheck();
testNoBadOffers(); testNoBadOffers();

View File

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

View File

@@ -139,6 +139,13 @@ public:
return mFlags & (mViewLowest ? lsfLowFreeze : lsfHighFreeze); 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 */ /** Has the peer set the freeze flag on us */
bool bool
getFreezePeer() const getFreezePeer() const
@@ -146,6 +153,13 @@ public:
return mFlags & (!mViewLowest ? lsfLowFreeze : lsfHighFreeze); 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& STAmount const&
getBalance() const getBalance() const
{ {

View File

@@ -52,6 +52,12 @@ checkFreeze(
{ {
return terNO_LINE; 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; return tesSUCCESS;

View File

@@ -392,6 +392,7 @@ CashCheck::doApply()
false, // authorize account false, // authorize account
(sleDst->getFlags() & lsfDefaultRipple) == 0, (sleDst->getFlags() & lsfDefaultRipple) == 0,
false, // freeze trust line false, // freeze trust line
false, // deep freeze trust line
initialBalance, // zero initial balance initialBalance, // zero initial balance
Issue(currency, account_), // limit of zero Issue(currency, account_), // limit of zero
0, // quality in 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; 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 void
ValidNewAccountRoot::visitEntry( ValidNewAccountRoot::visitEntry(
bool, bool,

View File

@@ -270,6 +270,114 @@ public:
beast::Journal const&); 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 * @brief Invariant: offers should be for non-negative amounts and must not
* be XRP to XRP. * be XRP to XRP.
@@ -518,6 +626,8 @@ using InvariantChecks = std::tuple<
XRPBalanceChecks, XRPBalanceChecks,
XRPNotCreated, XRPNotCreated,
NoXRPTrustLines, NoXRPTrustLines,
NoDeepFreezeTrustLinesWithoutFreeze,
TransfersNotFrozen,
NoBadOffers, NoBadOffers,
NoZeroEscrow, NoZeroEscrow,
ValidNewAccountRoot, ValidNewAccountRoot,

View File

@@ -268,6 +268,20 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx)
ctx.j) < needed) ctx.j) < needed)
return tecINSUFFICIENT_FUNDS; 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 // Fix a bug where the transfer of an NFToken with a transfer fee could
@@ -510,4 +524,62 @@ NFTokenAcceptOffer::doApply()
return tecINTERNAL; 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 } // namespace ripple

View File

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

View File

@@ -273,6 +273,20 @@ TOfferStreamBase<TIn, TOut>::step()
continue; 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 // Calculate owner funds
ownerFunds_ = accountFundsHelper( ownerFunds_ = accountFundsHelper(
view_, view_,

View File

@@ -26,6 +26,42 @@
#include <xrpl/protocol/Quality.h> #include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/st.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 { namespace ripple {
NotTEC NotTEC
@@ -45,6 +81,16 @@ SetTrust::preflight(PreflightContext const& ctx)
return temINVALID_FLAG; 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)); STAmount const saLimitAmount(tx.getFieldAmount(sfLimitAmount));
if (!isLegalNet(saLimitAmount)) 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; return tesSUCCESS;
} }
@@ -197,7 +295,7 @@ SetTrust::doApply()
Currency const currency(saLimitAmount.getCurrency()); Currency const currency(saLimitAmount.getCurrency());
AccountID uDstAccountID(saLimitAmount.getIssuer()); AccountID uDstAccountID(saLimitAmount.getIssuer());
// true, iff current is high account. // true, if current is high account.
bool const bHigh = account_ > uDstAccountID; bool const bHigh = account_ > uDstAccountID;
auto const sle = view().peek(keylet::account(account_)); auto const sle = view().peek(keylet::account(account_));
@@ -242,13 +340,15 @@ SetTrust::doApply()
bool const bClearNoRipple = (uTxFlags & tfClearNoRipple); bool const bClearNoRipple = (uTxFlags & tfClearNoRipple);
bool const bSetFreeze = (uTxFlags & tfSetFreeze); bool const bSetFreeze = (uTxFlags & tfSetFreeze);
bool const bClearFreeze = (uTxFlags & tfClearFreeze); bool const bClearFreeze = (uTxFlags & tfClearFreeze);
bool const bSetDeepFreeze = (uTxFlags & tfSetDeepFreeze);
bool const bClearDeepFreeze = (uTxFlags & tfClearDeepFreeze);
auto viewJ = ctx_.app.journal("View"); auto viewJ = ctx_.app.journal("View");
// Trust lines to self are impossible but because of the old bug there are // Trust lines to self are impossible but because of the old bug there
// two on 19-02-2022. This code was here to allow those trust lines to be // are two on 19-02-2022. This code was here to allow those trust lines
// deleted. The fixTrustLinesToSelf fix amendment will remove them when it // to be deleted. The fixTrustLinesToSelf fix amendment will remove them
// enables so this code will no longer be needed. // when it enables so this code will no longer be needed.
if (!view().rules().enabled(fixTrustLinesToSelf) && if (!view().rules().enabled(fixTrustLinesToSelf) &&
account_ == uDstAccountID) account_ == uDstAccountID)
{ {
@@ -408,14 +508,16 @@ SetTrust::doApply()
uFlagsOut &= ~(bHigh ? lsfHighNoRipple : lsfLowNoRipple); uFlagsOut &= ~(bHigh ? lsfHighNoRipple : lsfLowNoRipple);
} }
if (bSetFreeze && !bClearFreeze && !sle->isFlag(lsfNoFreeze)) // Have to use lsfNoFreeze to maintain pre-deep freeze behavior
{ bool const bNoFreeze = sle->isFlag(lsfNoFreeze);
uFlagsOut |= (bHigh ? lsfHighFreeze : lsfLowFreeze); uFlagsOut = computeFreezeFlags(
} uFlagsOut,
else if (bClearFreeze && !bSetFreeze) bHigh,
{ bNoFreeze,
uFlagsOut &= ~(bHigh ? lsfHighFreeze : lsfLowFreeze); bSetFreeze,
} bClearFreeze,
bSetDeepFreeze,
bClearDeepFreeze);
if (QUALITY_ONE == uLowQualityOut) if (QUALITY_ONE == uLowQualityOut)
uLowQualityOut = 0; uLowQualityOut = 0;
@@ -498,8 +600,8 @@ SetTrust::doApply()
// Reserve is not scaled by load. // Reserve is not scaled by load.
else if (bReserveIncrease && mPriorBalance < reserveCreate) else if (bReserveIncrease && mPriorBalance < reserveCreate)
{ {
JLOG(j_.trace()) JLOG(j_.trace()) << "Delay transaction: Insufficent reserve to "
<< "Delay transaction: Insufficent reserve to add trust line."; "add trust line.";
// Another transaction could provide XRP to the account and then // Another transaction could provide XRP to the account and then
// this transaction would succeed. // this transaction would succeed.
@@ -515,17 +617,18 @@ SetTrust::doApply()
// Line does not exist. // Line does not exist.
else if ( else if (
!saLimitAmount && // Setting default limit. !saLimitAmount && // Setting default limit.
(!bQualityIn || !uQualityIn) && // Not setting quality in or setting (!bQualityIn || !uQualityIn) && // Not setting quality in or
// default quality in. // setting default quality in.
(!bQualityOut || !uQualityOut) && // Not setting quality out or setting (!bQualityOut || !uQualityOut) && // Not setting quality out or
// default quality out. // setting default quality out.
(!bSetAuth)) (!bSetAuth))
{ {
JLOG(j_.trace()) JLOG(j_.trace())
<< "Redundant: Setting non-existent ripple line to defaults."; << "Redundant: Setting non-existent ripple line to defaults.";
return tecNO_LINE_REDUNDANT; 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. " JLOG(j_.trace()) << "Delay transaction: Line does not exist. "
"Insufficent reserve to create line."; "Insufficent reserve to create line.";
@@ -555,6 +658,7 @@ SetTrust::doApply()
bSetAuth, bSetAuth,
bSetNoRipple && !bClearNoRipple, bSetNoRipple && !bClearNoRipple,
bSetFreeze && !bClearFreeze, bSetFreeze && !bClearFreeze,
bSetDeepFreeze,
saBalance, saBalance,
saLimitAllow, // Limit for who is being charged. saLimitAllow, // Limit for who is being charged.
uQualityIn, uQualityIn,

View File

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

View File

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

View File

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