xrpld
Loading...
Searching...
No Matches
FreezeInvariant.cpp
1#include <xrpl/tx/invariants/FreezeInvariant.h>
2
3#include <xrpl/basics/Log.h>
4#include <xrpl/beast/utility/Journal.h>
5#include <xrpl/beast/utility/instrumentation.h>
6#include <xrpl/ledger/ReadView.h>
7#include <xrpl/ledger/helpers/LendingHelpers.h>
8#include <xrpl/protocol/AccountID.h>
9#include <xrpl/protocol/Asset.h>
10#include <xrpl/protocol/Feature.h>
11#include <xrpl/protocol/Indexes.h>
12#include <xrpl/protocol/Issue.h>
13#include <xrpl/protocol/LedgerFormats.h>
14#include <xrpl/protocol/SField.h>
15#include <xrpl/protocol/STLedgerEntry.h>
16#include <xrpl/protocol/STTx.h>
17#include <xrpl/protocol/TER.h>
18#include <xrpl/protocol/XRPAmount.h>
19#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
20
21#include <algorithm>
22#include <optional>
23#include <utility>
24
25namespace xrpl {
26
27void
29{
30 /*
31 * A trust line freeze state alone doesn't determine if a transfer is
32 * frozen. The transfer must be examined "end-to-end" because both sides of
33 * the transfer may have different freeze states and freeze impact depends
34 * on the transfer direction. This is why first we need to track the
35 * transfers using IssuerChanges senders/receivers.
36 *
37 * Only in validateIssuerChanges, after we collected all changes can we
38 * determine if the transfer is valid.
39 */
40 if (!isValidEntry(before, after))
41 {
42 return;
43 }
44
45 auto const balanceChange = calculateBalanceChange(before, after, isDelete);
46 if (balanceChange.signum() == 0)
47 {
48 return;
49 }
50
51 recordBalanceChanges(after, balanceChange);
52}
53
54bool
56 STTx const& tx,
57 TER const ter,
58 XRPAmount const fee,
59 ReadView const& view,
60 beast::Journal const& j)
61{
62 /*
63 * We check this invariant regardless of deep freeze amendment status,
64 * allowing for detection and logging of potential issues even when the
65 * amendment is disabled.
66 *
67 * If an exploit that allows moving frozen assets is discovered,
68 * we can alert operators who monitor fatal messages and trigger assert in
69 * debug builds for an early warning.
70 *
71 * In an unlikely event that an exploit is found, this early detection
72 * enables encouraging the UNL to expedite deep freeze amendment activation
73 * or deploy hotfixes via new amendments. In case of a new amendment, we'd
74 * only have to change this line setting 'enforce' variable.
75 * enforce = view.rules().enabled(featureDeepFreeze) ||
76 * view.rules().enabled(fixFreezeExploit);
77 */
78 [[maybe_unused]] bool const enforce = view.rules().enabled(featureDeepFreeze);
79 bool const fixOverrideFreeze = view.rules().enabled(fixCleanup3_4_0);
80
81 /*
82 * XLS-0066: a broker must be able to default an already-late loan
83 * regardless of the vault asset's freeze state. LoanManage::defaultLoan
84 * moves First-Loss Capital from the broker to the vault pseudo-account via
85 * accountSend, which transits through the issuer in two hops (see
86 * getLoanDefaultFreezeExemptAccounts), so a frozen issuer would otherwise
87 * trip this invariant on either hop. Gated behind fixCleanup3_4_0, and
88 * scoped to exactly the issuer/broker and issuer/vault lines involved for
89 * the vault's own currency, so ledgers without the amendment (or an
90 * unrelated frozen currency/line touched by the same transaction) keep
91 * the current (blocking) behavior.
92 */
93 auto const loanDefaultAccounts = getLoanDefaultFreezeExemptAccounts(view, tx);
94
95 return std::ranges::all_of(balanceChanges_, [&](auto const& entry) {
96 auto const& [issue, changes] = entry;
97 auto const issuerSle = findIssuer(issue.account, view);
98 // It should be impossible for the issuer to not be found, but check
99 // just in case so xrpld doesn't crash in release.
100 if (!issuerSle)
101 {
102 // The comment above starting with "assert(enforce)" explains this
103 // assert.
104 XRPL_ASSERT(
105 enforce,
106 "xrpl::TransfersNotFrozen::finalize : enforce "
107 "invariant.");
108 return !enforce;
109 }
110
112 issuerSle, changes, tx, j, enforce, fixOverrideFreeze, loanDefaultAccounts);
113 });
114}
115
116bool
118{
119 // `after` can never be null, even if the trust line is deleted.
120 XRPL_ASSERT(after, "xrpl::TransfersNotFrozen::isValidEntry : valid after.");
121 if (!after)
122 {
123 return false;
124 }
125
126 if (after->getType() == ltACCOUNT_ROOT)
127 {
128 possibleIssuers_.emplace(after->at(sfAccount), after);
129 return false;
130 }
131
132 /* While LedgerEntryTypesMatch invariant also checks types, all invariants
133 * are processed regardless of previous failures.
134 *
135 * This type check is still necessary here because it prevents potential
136 * issues in subsequent processing.
137 */
138 return after->getType() == ltRIPPLE_STATE && (!before || before->getType() == ltRIPPLE_STATE);
139}
140
143{
144 auto const getBalance = [](auto const& line, auto const& other, bool zero) {
145 STAmount const amt = line ? line->at(sfBalance) : other->at(sfBalance).zeroed();
146 return zero ? amt.zeroed() : amt;
147 };
148
149 /* Trust lines can be created dynamically by other transactions such as
150 * Payment and OfferCreate that cross offers. Such trust line won't be
151 * created frozen, but the sender might be, so the starting balance must be
152 * treated as zero.
153 */
154 auto const balanceBefore = getBalance(before, after, false);
155
156 /* Same as above, trust lines can be dynamically deleted, and for frozen
157 * trust lines, payments not involving the issuer must be blocked. This is
158 * achieved by treating the final balance as zero when isDelete=true to
159 * ensure frozen line restrictions are enforced even during deletion.
160 */
161 auto const balanceAfter = getBalance(after, before, isDelete);
162
163 return balanceAfter - balanceBefore;
164}
165
166void
168{
169 XRPL_ASSERT(
170 change.balanceChangeSign,
171 "xrpl::TransfersNotFrozen::recordBalance : valid trustline "
172 "balance sign.");
173 auto& changes = balanceChanges_[issue];
174 if (change.balanceChangeSign < 0)
175 {
176 changes.senders.emplace_back(std::move(change));
177 }
178 else
179 {
180 changes.receivers.emplace_back(std::move(change));
181 }
182}
183
184void
186{
187 auto const balanceChangeSign = balanceChange.signum();
188 auto const currency = after->at(sfBalance).get<Issue>().currency;
189
190 // Change from low account's perspective, which is trust line default
192 {currency, after->at(sfHighLimit).getIssuer()},
193 {.line = after, .balanceChangeSign = balanceChangeSign});
194
195 // Change from high account's perspective, which reverses the sign.
197 {currency, after->at(sfLowLimit).getIssuer()},
198 {.line = after, .balanceChangeSign = -balanceChangeSign});
199}
200
203{
204 if (auto it = possibleIssuers_.find(issuerID); it != possibleIssuers_.end())
205 {
206 return it->second;
207 }
208
209 return view.read(keylet::account(issuerID));
210}
211
212bool
214 SLE::ConstRef issuer,
215 IssuerChanges const& changes,
216 STTx const& tx,
217 beast::Journal const& j,
218 bool enforce,
219 bool fixOverrideFreeze,
220 std::optional<LoanDefaultFreezeExemptAccounts> const& loanDefaultAccounts)
221{
222 if (!issuer)
223 {
224 return false;
225 }
226
227 bool const globalFreeze = issuer->isFlag(lsfGlobalFreeze);
228 if (changes.receivers.empty() || changes.senders.empty())
229 {
230 /* If there are no receivers, then the holder(s) are returning
231 * their tokens to the issuer. Likewise, if there are no
232 * senders, then the issuer is issuing tokens to the holder(s).
233 * This is allowed regardless of the issuer's freeze flags. (The
234 * holder may have contradicting freeze flags, but that will be
235 * checked when the holder is treated as issuer.)
236 */
237 return true;
238 }
239
240 for (auto const& actors : {changes.senders, changes.receivers})
241 {
242 for (auto const& change : actors)
243 {
244 bool const high = change.line->at(sfLowLimit).getIssuer() == issuer->at(sfAccount);
245
247 change,
248 high,
249 tx,
250 j,
251 enforce,
252 globalFreeze,
253 fixOverrideFreeze,
254 loanDefaultAccounts))
255 {
256 return false;
257 }
258 }
259 }
260 return true;
261}
262
263bool
265 BalanceChange const& change,
266 bool high,
267 STTx const& tx,
268 beast::Journal const& j,
269 bool enforce,
270 bool globalFreeze,
271 bool fixOverrideFreeze,
272 std::optional<LoanDefaultFreezeExemptAccounts> const& loanDefaultAccounts)
273{
274 bool const freeze =
275 change.balanceChangeSign < 0 && change.line->isFlag(high ? lsfLowFreeze : lsfHighFreeze);
276 bool const deepFreeze = change.line->isFlag(high ? lsfLowDeepFreeze : lsfHighDeepFreeze);
277 bool const frozen = globalFreeze || deepFreeze || freeze;
278
279 if (!frozen)
280 {
281 return true;
282 }
283
284 // Pre-fixCleanup3_4_0: the isAMMLine check incorrectly blocked clawback on
285 // individually-frozen or deep-frozen AMM trust lines.
286 // Post-fixCleanup3_4_0: AMMClawbacks are allowed to override all freeze types.
287 bool const isAMMLine = change.line->isFlag(lsfAMMNode);
288 if ((fixOverrideFreeze || !isAMMLine || globalFreeze) &&
290 {
291 JLOG(j.debug()) << "Invariant check allowing funds to be moved "
292 << (change.balanceChangeSign > 0 ? "to" : "from")
293 << " a frozen trustline for a freeze privileged transaction "
294 << tx.getTransactionID();
295 return true;
296 }
297
298 // XLS-0066: LoanManage::defaultLoan's transfer is exempt from freeze (see
299 // finalize()). Since neither the broker nor vault pseudo-account is the
300 // asset's issuer, accountSend routes it as two hops through the issuer
301 // (broker -> issuer, issuer -> vault), so both the issuer/broker and
302 // issuer/vault lines are exempt -- but only for the vault's own currency,
303 // so an unrelated frozen line (a different currency, or one touched by
304 // the same transaction for some other reason) is still caught.
305 if (loanDefaultAccounts && loanDefaultAccounts->asset.holds<Issue>() &&
306 loanDefaultAccounts->asset.get<Issue>().currency ==
307 change.line->at(sfBalance).get<Issue>().currency)
308 {
309 AccountID const lowAcct = change.line->at(sfLowLimit).getIssuer();
310 AccountID const highAcct = change.line->at(sfHighLimit).getIssuer();
311 auto const& accts = *loanDefaultAccounts;
312 auto const isPair = [&](AccountID const& a, AccountID const& b) {
313 return (lowAcct == a && highAcct == b) || (lowAcct == b && highAcct == a);
314 };
315 if (isPair(accts.issuer, accts.broker) || isPair(accts.issuer, accts.vault))
316 {
317 JLOG(j.debug()) << "Invariant check allowing funds to be moved "
318 << (change.balanceChangeSign > 0 ? "to" : "from")
319 << " a frozen trustline for LoanManage default "
320 << tx.getTransactionID();
321 return true;
322 }
323 }
324
325 JLOG(j.fatal()) << "Invariant failed: Attempting to move frozen funds for "
326 << tx.getTransactionID();
327 // The comment above starting with "assert(enforce)" explains this assert.
328 XRPL_ASSERT(
329 enforce,
330 "xrpl::TransfersNotFrozen::validateFrozenState : enforce "
331 "invariant.");
332
333 return !enforce;
334}
335
336} // namespace xrpl
T all_of(T... args)
A generic endpoint for log messages.
Definition Journal.h:44
Stream fatal() const
Definition Journal.h:368
Stream debug() const
Definition Journal.h:344
A currency issued by an account.
Definition Issue.h:18
Currency currency
Definition Issue.h:20
A view into a ledger.
Definition ReadView.h:41
virtual Rules const & rules() const =0
Returns the tx processing rules.
virtual SLE::const_pointer read(Keylet const &k) const =0
Return the state item associated with a key.
bool enabled(UInt256 const &feature) const
Returns true if a feature is enabled.
Definition Rules.cpp:182
int signum() const noexcept
Definition STAmount.h:522
STAmount zeroed() const
Returns a zero value with the same issuer and currency.
Definition STAmount.h:530
std::shared_ptr< STLedgerEntry const > const & ConstRef
std::shared_ptr< STLedgerEntry const > const_pointer
UInt256 getTransactionID() const
Definition STTx.h:262
void recordBalance(Issue const &issue, BalanceChange change)
void recordBalanceChanges(SLE::ConstRef after, STAmount const &balanceChange)
std::map< AccountID, SLE::const_pointer const > possibleIssuers_
void visitEntry(bool, SLE::ConstRef, SLE::ConstRef)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
static bool validateIssuerChanges(SLE::ConstRef issuer, IssuerChanges const &changes, STTx const &tx, beast::Journal const &j, bool enforce, bool fixOverrideFreeze, std::optional< LoanDefaultFreezeExemptAccounts > const &loanDefaultAccounts)
bool isValidEntry(SLE::ConstRef before, SLE::ConstRef after)
SLE::const_pointer findIssuer(AccountID const &issuerID, ReadView const &view)
static STAmount calculateBalanceChange(SLE::ConstRef before, SLE::ConstRef after, bool isDelete)
static bool validateFrozenState(BalanceChange const &change, bool high, STTx const &tx, beast::Journal const &j, bool enforce, bool globalFreeze, bool fixOverrideFreeze, std::optional< LoanDefaultFreezeExemptAccounts > const &loanDefaultAccounts)
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:220
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
bool hasPrivilege(STTx const &tx, Privilege priv)
std::optional< LoanDefaultFreezeExemptAccounts > getLoanDefaultFreezeExemptAccounts(ReadView const &view, STTx const &tx)
Resolves the accounts and asset a LoanManage default transaction is exempt from freeze/lock for.
bool after(NetClock::time_point now, std::uint32_t mark)
Has the specified time passed?
Definition View.cpp:644
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:34
TERSubset< CanCvtToTER > TER
Definition TER.h:654
std::vector< BalanceChange > senders
std::vector< BalanceChange > receivers