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/protocol/AccountID.h>
8#include <xrpl/protocol/Feature.h>
9#include <xrpl/protocol/Indexes.h>
10#include <xrpl/protocol/Issue.h>
11#include <xrpl/protocol/LedgerFormats.h>
12#include <xrpl/protocol/SField.h>
13#include <xrpl/protocol/STLedgerEntry.h>
14#include <xrpl/protocol/STTx.h>
15#include <xrpl/protocol/TER.h>
16#include <xrpl/protocol/XRPAmount.h>
17#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
18
19#include <algorithm>
20#include <utility>
21
22namespace xrpl {
23
24void
26{
27 /*
28 * A trust line freeze state alone doesn't determine if a transfer is
29 * frozen. The transfer must be examined "end-to-end" because both sides of
30 * the transfer may have different freeze states and freeze impact depends
31 * on the transfer direction. This is why first we need to track the
32 * transfers using IssuerChanges senders/receivers.
33 *
34 * Only in validateIssuerChanges, after we collected all changes can we
35 * determine if the transfer is valid.
36 */
37 if (!isValidEntry(before, after))
38 {
39 return;
40 }
41
42 auto const balanceChange = calculateBalanceChange(before, after, isDelete);
43 if (balanceChange.signum() == 0)
44 {
45 return;
46 }
47
48 recordBalanceChanges(after, balanceChange);
49}
50
51bool
53 STTx const& tx,
54 TER const ter,
55 XRPAmount const fee,
56 ReadView const& view,
57 beast::Journal const& j)
58{
59 /*
60 * We check this invariant regardless of deep freeze amendment status,
61 * allowing for detection and logging of potential issues even when the
62 * amendment is disabled.
63 *
64 * If an exploit that allows moving frozen assets is discovered,
65 * we can alert operators who monitor fatal messages and trigger assert in
66 * debug builds for an early warning.
67 *
68 * In an unlikely event that an exploit is found, this early detection
69 * enables encouraging the UNL to expedite deep freeze amendment activation
70 * or deploy hotfixes via new amendments. In case of a new amendment, we'd
71 * only have to change this line setting 'enforce' variable.
72 * enforce = view.rules().enabled(featureDeepFreeze) ||
73 * view.rules().enabled(fixFreezeExploit);
74 */
75 [[maybe_unused]] bool const enforce = view.rules().enabled(featureDeepFreeze);
76 bool const fixOverrideFreeze = view.rules().enabled(fixCleanup3_4_0);
77
78 return std::ranges::all_of(balanceChanges_, [&](auto const& entry) {
79 auto const& [issue, changes] = entry;
80 auto const issuerSle = findIssuer(issue.account, view);
81 // It should be impossible for the issuer to not be found, but check
82 // just in case so xrpld doesn't crash in release.
83 if (!issuerSle)
84 {
85 // The comment above starting with "assert(enforce)" explains this
86 // assert.
87 XRPL_ASSERT(
88 enforce,
89 "xrpl::TransfersNotFrozen::finalize : enforce "
90 "invariant.");
91 return !enforce;
92 }
93
94 return validateIssuerChanges(issuerSle, changes, tx, j, enforce, fixOverrideFreeze);
95 });
96}
97
98bool
100{
101 // `after` can never be null, even if the trust line is deleted.
102 XRPL_ASSERT(after, "xrpl::TransfersNotFrozen::isValidEntry : valid after.");
103 if (!after)
104 {
105 return false;
106 }
107
108 if (after->getType() == ltACCOUNT_ROOT)
109 {
110 possibleIssuers_.emplace(after->at(sfAccount), after);
111 return false;
112 }
113
114 /* While LedgerEntryTypesMatch invariant also checks types, all invariants
115 * are processed regardless of previous failures.
116 *
117 * This type check is still necessary here because it prevents potential
118 * issues in subsequent processing.
119 */
120 return after->getType() == ltRIPPLE_STATE && (!before || before->getType() == ltRIPPLE_STATE);
121}
122
125 SLE::const_ref before,
127 bool isDelete)
128{
129 auto const getBalance = [](auto const& line, auto const& other, bool zero) {
130 STAmount const amt = line ? line->at(sfBalance) : other->at(sfBalance).zeroed();
131 return zero ? amt.zeroed() : amt;
132 };
133
134 /* Trust lines can be created dynamically by other transactions such as
135 * Payment and OfferCreate that cross offers. Such trust line won't be
136 * created frozen, but the sender might be, so the starting balance must be
137 * treated as zero.
138 */
139 auto const balanceBefore = getBalance(before, after, false);
140
141 /* Same as above, trust lines can be dynamically deleted, and for frozen
142 * trust lines, payments not involving the issuer must be blocked. This is
143 * achieved by treating the final balance as zero when isDelete=true to
144 * ensure frozen line restrictions are enforced even during deletion.
145 */
146 auto const balanceAfter = getBalance(after, before, isDelete);
147
148 return balanceAfter - balanceBefore;
149}
150
151void
153{
154 XRPL_ASSERT(
155 change.balanceChangeSign,
156 "xrpl::TransfersNotFrozen::recordBalance : valid trustline "
157 "balance sign.");
158 auto& changes = balanceChanges_[issue];
159 if (change.balanceChangeSign < 0)
160 {
161 changes.senders.emplace_back(std::move(change));
162 }
163 else
164 {
165 changes.receivers.emplace_back(std::move(change));
166 }
167}
168
169void
171{
172 auto const balanceChangeSign = balanceChange.signum();
173 auto const currency = after->at(sfBalance).get<Issue>().currency;
174
175 // Change from low account's perspective, which is trust line default
177 {currency, after->at(sfHighLimit).getIssuer()},
178 {.line = after, .balanceChangeSign = balanceChangeSign});
179
180 // Change from high account's perspective, which reverses the sign.
182 {currency, after->at(sfLowLimit).getIssuer()},
183 {.line = after, .balanceChangeSign = -balanceChangeSign});
184}
185
188{
189 if (auto it = possibleIssuers_.find(issuerID); it != possibleIssuers_.end())
190 {
191 return it->second;
192 }
193
194 return view.read(keylet::account(issuerID));
195}
196
197bool
199 SLE::const_ref issuer,
200 IssuerChanges const& changes,
201 STTx const& tx,
202 beast::Journal const& j,
203 bool enforce,
204 bool fixOverrideFreeze)
205{
206 if (!issuer)
207 {
208 return false;
209 }
210
211 bool const globalFreeze = issuer->isFlag(lsfGlobalFreeze);
212 if (changes.receivers.empty() || changes.senders.empty())
213 {
214 /* If there are no receivers, then the holder(s) are returning
215 * their tokens to the issuer. Likewise, if there are no
216 * senders, then the issuer is issuing tokens to the holder(s).
217 * This is allowed regardless of the issuer's freeze flags. (The
218 * holder may have contradicting freeze flags, but that will be
219 * checked when the holder is treated as issuer.)
220 */
221 return true;
222 }
223
224 for (auto const& actors : {changes.senders, changes.receivers})
225 {
226 for (auto const& change : actors)
227 {
228 bool const high = change.line->at(sfLowLimit).getIssuer() == issuer->at(sfAccount);
229
230 if (!validateFrozenState(change, high, tx, j, enforce, globalFreeze, fixOverrideFreeze))
231 {
232 return false;
233 }
234 }
235 }
236 return true;
237}
238
239bool
241 BalanceChange const& change,
242 bool high,
243 STTx const& tx,
244 beast::Journal const& j,
245 bool enforce,
246 bool globalFreeze,
247 bool fixOverrideFreeze)
248{
249 bool const freeze =
250 change.balanceChangeSign < 0 && change.line->isFlag(high ? lsfLowFreeze : lsfHighFreeze);
251 bool const deepFreeze = change.line->isFlag(high ? lsfLowDeepFreeze : lsfHighDeepFreeze);
252 bool const frozen = globalFreeze || deepFreeze || freeze;
253
254 if (!frozen)
255 {
256 return true;
257 }
258
259 // Pre-fixCleanup3_4_0: the isAMMLine check incorrectly blocked clawback on
260 // individually-frozen or deep-frozen AMM trust lines.
261 // Post-fixCleanup3_4_0: AMMClawbacks are allowed to override all freeze types.
262 bool const isAMMLine = change.line->isFlag(lsfAMMNode);
263 if ((fixOverrideFreeze || !isAMMLine || globalFreeze) && hasPrivilege(tx, OverrideFreeze))
264 {
265 JLOG(j.debug()) << "Invariant check allowing funds to be moved "
266 << (change.balanceChangeSign > 0 ? "to" : "from")
267 << " a frozen trustline for a freeze privileged transaction "
268 << tx.getTransactionID();
269 return true;
270 }
271
272 JLOG(j.fatal()) << "Invariant failed: Attempting to move frozen funds for "
273 << tx.getTransactionID();
274 // The comment above starting with "assert(enforce)" explains this assert.
275 XRPL_ASSERT(
276 enforce,
277 "xrpl::TransfersNotFrozen::validateFrozenState : enforce "
278 "invariant.");
279
280 return !enforce;
281}
282
283} // 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
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:180
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 & const_ref
std::shared_ptr< STLedgerEntry const > const_pointer
uint256 getTransactionID() const
Definition STTx.h:238
void recordBalance(Issue const &issue, BalanceChange change)
std::map< AccountID, SLE::const_pointer const > possibleIssuers_
static bool validateFrozenState(BalanceChange const &change, bool high, STTx const &tx, beast::Journal const &j, bool enforce, bool globalFreeze, bool fixOverrideFreeze)
static STAmount calculateBalanceChange(SLE::const_ref before, SLE::const_ref after, bool isDelete)
void recordBalanceChanges(SLE::const_ref after, STAmount const &balanceChange)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
bool isValidEntry(SLE::const_ref before, SLE::const_ref after)
static bool validateIssuerChanges(SLE::const_ref issuer, IssuerChanges const &changes, STTx const &tx, beast::Journal const &j, bool enforce, bool fixOverrideFreeze)
void visitEntry(bool, SLE::const_ref, SLE::const_ref)
SLE::const_pointer findIssuer(AccountID const &issuerID, ReadView const &view)
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:198
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)
bool after(NetClock::time_point now, std::uint32_t mark)
Has the specified time passed?
Definition View.cpp:572
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:34
TERSubset< CanCvtToTER > TER
Definition TER.h:647
std::vector< BalanceChange > senders
std::vector< BalanceChange > receivers