xrpld
Loading...
Searching...
No Matches
LoanBrokerInvariant.cpp
1#include <xrpl/tx/invariants/LoanBrokerInvariant.h>
2
3#include <xrpl/basics/Log.h>
4#include <xrpl/basics/Number.h>
5#include <xrpl/beast/utility/Journal.h>
6#include <xrpl/beast/utility/Zero.h>
7#include <xrpl/ledger/ReadView.h>
8#include <xrpl/ledger/helpers/LendingHelpers.h>
9#include <xrpl/ledger/helpers/TokenHelpers.h>
10#include <xrpl/protocol/Asset.h>
11#include <xrpl/protocol/Feature.h>
12#include <xrpl/protocol/Indexes.h>
13#include <xrpl/protocol/LedgerFormats.h>
14#include <xrpl/protocol/SField.h>
15#include <xrpl/protocol/STAmount.h>
16#include <xrpl/protocol/STLedgerEntry.h>
17#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
18#include <xrpl/protocol/STTx.h>
19#include <xrpl/protocol/TER.h>
20#include <xrpl/protocol/TxFormats.h>
21#include <xrpl/protocol/XRPAmount.h>
22
23#include <algorithm>
24
25namespace xrpl {
26
27void
29{
30 // Track LoanBroker deletions so finalize() can enforce:
31 // (a) only ttLOAN_BROKER_DELETE removes a broker
32 // (b) at most one broker is removed per transaction
33 // (c) DebtTotal and OwnerCount were zero before deletion
34 // `before` is the pre-transaction state, which is what
35 // LoanBrokerDelete::preclaim reads. Erased trust lines and MPTokens need no
36 // special handling here: the `if (after)` branch below already records them.
37 if (isDelete && before && before->getType() == ltLOAN_BROKER)
38 {
40 {
42 }
43 else
44 {
45 deletedBroker_ = before;
46 }
47 }
48 if (after)
49 {
50 if (after->getType() == ltLOAN_BROKER)
51 {
52 auto& broker = brokers_[after->key()];
53 broker.brokerBefore = before;
54 broker.brokerAfter = after;
55 }
56 else if (after->getType() == ltACCOUNT_ROOT && after->isFieldPresent(sfLoanBrokerID))
57 {
58 auto const& loanBrokerID = after->at(sfLoanBrokerID);
59 // create an entry if one doesn't already exist
60 brokers_.emplace(loanBrokerID, BrokerInfo{});
61 }
62 else if (after->getType() == ltRIPPLE_STATE)
63 {
64 lines_.emplace_back(after);
65 }
66 else if (after->getType() == ltMPTOKEN)
67 {
68 mpts_.emplace_back(after);
69 }
70 }
71}
72
73bool
75{
76 auto const next = dir->at(~sfIndexNext);
77 auto const prev = dir->at(~sfIndexPrevious);
78 if ((prev && (*prev != 0u)) || (next && (*next != 0u)))
79 {
80 JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero "
81 "OwnerCount has multiple directory pages";
82 return false;
83 }
84 auto indexes = dir->getFieldV256(sfIndexes);
85 if (indexes.size() > 1)
86 {
87 JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero "
88 "OwnerCount has multiple indexes in the Directory root";
89 return false;
90 }
91 if (indexes.size() == 1)
92 {
93 auto const index = indexes.value().front();
94 auto const sle = view.read(keylet::unchecked(index));
95 if (!sle)
96 {
97 JLOG(j.fatal()) << "Invariant failed: Loan Broker directory corrupt";
98 return false;
99 }
100 if (sle->getType() != ltRIPPLE_STATE && sle->getType() != ltMPTOKEN)
101 {
102 JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero "
103 "OwnerCount has an unexpected entry in the directory";
104 return false;
105 }
106 }
107
108 return true;
109}
110
111bool
113 STTx const& tx,
114 TER const,
115 XRPAmount const,
116 ReadView const& view,
117 beast::Journal const& j)
118{
119 // Loan Brokers will not exist on ledger if the Lending Protocol amendment
120 // is not enabled, so there's no need to check it.
121
122 // Deletion invariants (featureLendingProtocolV1_1). At most one
123 // LoanBroker may be removed per transaction, and only by
124 // ttLOAN_BROKER_DELETE, and only when its pre-state OwnerCount is zero and
125 // its pre-state DebtTotal is zero to the precision of the vault asset. The
126 // DebtTotal check complements ValidLoan's
127 // LoanBrokerDelete-must-not-touch-any-loan rule: even a broker that has
128 // finished paying off every loan may still hold non-zero exposure until
129 // its LoanBrokerCoverWithdraw settles, and neither state is safe to
130 // delete.
131 if (view.rules().enabled(featureLendingProtocolV1_1))
132 {
134 {
135 JLOG(j.fatal())
136 << "Invariant failed: more than one Loan Broker deleted in a single transaction";
137 return false;
138 }
139 if (deletedBroker_)
140 {
141 if (tx.getTxnType() != ttLOAN_BROKER_DELETE)
142 {
143 JLOG(j.fatal()) << "Invariant failed: " << //
144 "Loan Broker deleted by a transaction other than LoanBrokerDelete";
145 return false;
146 }
147 // Mirror LoanBrokerDelete::preclaim, which accepts a DebtTotal
148 // that rounds to zero at the vault's AssetsTotal scale rather than
149 // requiring an exact zero. Requiring more here would turn a
150 // transaction the transactor deliberately permits into an
151 // invariant failure.
152 if (auto const debtTotal = deletedBroker_->at(sfDebtTotal); debtTotal != beast::kZero)
153 {
154 // The erased broker is also collected in brokers_, and that
155 // loop reports a missing vault, so no separate diagnostic is
156 // needed here. Without a vault there is no scale to round at,
157 // so the residue cannot be excused as dust.
158 auto const vault = view.read(keylet::vault(deletedBroker_->at(sfVaultID)));
159 if (!vault ||
161 Asset{vault->at(sfAsset)},
162 debtTotal,
163 getAssetsTotalScale(vault),
165 {
166 JLOG(j.fatal())
167 << "Invariant failed: Loan Broker deleted with non-zero debt total";
168 return false;
169 }
170 }
171 if (deletedBroker_->at(sfOwnerCount) != 0)
172 {
173 JLOG(j.fatal())
174 << "Invariant failed: Loan Broker deleted with non-zero owner count";
175 return false;
176 }
177 }
178 }
179
180 for (auto const& line : lines_)
181 {
182 for (auto const& field : {&sfLowLimit, &sfHighLimit})
183 {
184 auto const account = view.read(keylet::account(line->at(*field).getIssuer()));
185 // This Invariant doesn't know about the rules for Trust Lines, so
186 // if the account is missing, don't treat it as an error. This
187 // loop is only concerned with finding Broker pseudo-accounts
188 if (account && account->isFieldPresent(sfLoanBrokerID))
189 {
190 auto const& loanBrokerID = account->at(sfLoanBrokerID);
191 // create an entry if one doesn't already exist
192 brokers_.emplace(loanBrokerID, BrokerInfo{});
193 }
194 }
195 }
196 for (auto const& mpt : mpts_)
197 {
198 auto const account = view.read(keylet::account(mpt->at(sfAccount)));
199 // This Invariant doesn't know about the rules for MPTokens, so
200 // if the account is missing, don't treat is as an error. This
201 // loop is only concerned with finding Broker pseudo-accounts
202 if (account && account->isFieldPresent(sfLoanBrokerID))
203 {
204 auto const& loanBrokerID = account->at(sfLoanBrokerID);
205 // create an entry if one doesn't already exist
206 brokers_.emplace(loanBrokerID, BrokerInfo{});
207 }
208 }
209
210 return std::ranges::all_of(brokers_, [&](auto const& entry) {
211 auto const& [brokerID, broker] = entry;
212 auto const& after =
213 broker.brokerAfter ? broker.brokerAfter : view.read(keylet::loanBroker(brokerID));
214
215 if (!after)
216 {
217 JLOG(j.fatal()) << "Invariant failed: Loan Broker missing";
218 return false;
219 }
220
221 auto const& before = broker.brokerBefore;
222
223 // If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most
224 // one node (the root), which will only hold entries for `RippleState`
225 // or `MPToken` objects.
226 if (after->at(sfOwnerCount) == 0)
227 {
228 auto const dir = view.read(keylet::ownerDir(after->at(sfAccount)));
229 if (dir)
230 {
231 if (!goodZeroDirectory(view, dir, j))
232 {
233 return false;
234 }
235 }
236 }
237 if (before && before->at(sfLoanSequence) > after->at(sfLoanSequence))
238 {
239 JLOG(j.fatal()) << "Invariant failed: Loan Broker sequence number "
240 "decreased";
241 return false;
242 }
243 if (after->at(sfDebtTotal) < 0)
244 {
245 JLOG(j.fatal()) << "Invariant failed: Loan Broker debt total is negative";
246 return false;
247 }
248 if (after->at(sfCoverAvailable) < 0)
249 {
250 JLOG(j.fatal()) << "Invariant failed: Loan Broker cover available is negative";
251 return false;
252 }
253 auto const vault = view.read(keylet::vault(after->at(sfVaultID)));
254 if (!vault)
255 {
256 JLOG(j.fatal()) << "Invariant failed: Loan Broker vault ID is invalid";
257 return false;
258 }
259 auto const& vaultAsset = vault->at(sfAsset);
260 auto const pseudoBalance = accountHolds(
261 view,
262 after->at(sfAccount),
263 vaultAsset,
266 j);
267 if (after->at(sfCoverAvailable) < pseudoBalance)
268 {
269 JLOG(j.fatal()) << "Invariant failed: Loan Broker cover available "
270 "is less than pseudo-account asset balance";
271 return false;
272 }
273
274 if (view.rules().enabled(fixCleanup3_1_3))
275 {
276 // Don't check the balance when LoanBroker is deleted,
277 // sfCoverAvailable is not zeroed
278 if (tx.getTxnType() != ttLOAN_BROKER_DELETE &&
279 after->at(sfCoverAvailable) > pseudoBalance)
280 {
281 JLOG(j.fatal()) << "Invariant failed: Loan Broker cover available is greater "
282 "than pseudo-account asset balance";
283 return false;
284 }
285 }
286 return true;
287 });
288}
289
290} // namespace xrpl
T all_of(T... args)
A generic endpoint for log messages.
Definition Journal.h:44
Stream fatal() const
Definition Journal.h:368
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
std::shared_ptr< STLedgerEntry const > const & ConstRef
TxType getTxnType() const
Definition STTx.h:250
static bool goodZeroDirectory(ReadView const &view, SLE::ConstRef dir, beast::Journal const &j)
std::map< UInt256, BrokerInfo > brokers_
void visitEntry(bool, SLE::ConstRef, SLE::ConstRef)
SLE::const_pointer deletedBroker_
std::vector< SLE::const_pointer > mpts_
std::vector< SLE::const_pointer > lines_
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
constexpr Zero kZero
Definition Zero.h:30
Keylet ownerDir(AccountID const &id) noexcept
The root page of an account's directory.
Definition Indexes.cpp:403
Keylet unchecked(UInt256 const &key) noexcept
Any ledger entry.
Definition Indexes.cpp:397
Keylet vault(AccountID const &owner, SeqProxy const &seq) noexcept
Definition Indexes.cpp:591
Keylet loanBroker(AccountID const &owner, SeqProxy const &seq) noexcept
Definition Indexes.cpp:597
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
int getAssetsTotalScale(SLE::ConstRef vaultSle)
bool after(NetClock::time_point now, std::uint32_t mark)
Has the specified time passed?
Definition View.cpp:644
void roundToAsset(A const &asset, Number &value)
Round an arbitrary precision Number IN PLACE to the precision of a given Asset.
Definition STAmount.h:735
TERSubset< CanCvtToTER > TER
Definition TER.h:654
STAmount accountHolds(ReadView const &view, AccountID const &account, Currency const &currency, AccountID const &issuer, FreezeHandling zeroIfFrozen, beast::Journal j, SpendableHandling includeFullBalance=SpendableHandling::SimpleBalance)