xrpld
Loading...
Searching...
No Matches
LoanManage.cpp
1#include <xrpl/tx/transactors/lending/LoanManage.h>
2
3#include <xrpl/basics/Log.h>
4#include <xrpl/basics/Number.h>
5#include <xrpl/beast/utility/Zero.h>
6#include <xrpl/core/ServiceRegistry.h>
7#include <xrpl/ledger/ApplyView.h>
8#include <xrpl/ledger/View.h>
9#include <xrpl/ledger/helpers/LendingHelpers.h>
10#include <xrpl/ledger/helpers/TokenHelpers.h>
11#include <xrpl/protocol/Asset.h>
12#include <xrpl/protocol/Feature.h>
13#include <xrpl/protocol/Indexes.h>
14#include <xrpl/protocol/LedgerFormats.h>
15#include <xrpl/protocol/Protocol.h>
16#include <xrpl/protocol/SField.h>
17#include <xrpl/protocol/STAmount.h>
18#include <xrpl/protocol/STLedgerEntry.h>
19#include <xrpl/protocol/STTakesAsset.h>
20#include <xrpl/protocol/STTx.h>
21#include <xrpl/protocol/TER.h>
22#include <xrpl/protocol/TxFlags.h>
23#include <xrpl/protocol/Units.h>
24#include <xrpl/protocol/XRPAmount.h>
25#include <xrpl/tx/Transactor.h>
26
27#include <algorithm>
28#include <cstdint>
29namespace xrpl {
30
31bool
36
39{
40 return tfLoanManageMask;
41}
42
45{
46 if (ctx.tx[sfLoanID] == beast::kZero)
47 return temINVALID;
48
49 // Flags are mutually exclusive
50 if (auto const flagField = ctx.tx[~sfFlags]; flagField && (*flagField != 0u))
51 {
52 auto const flags = *flagField & tfUniversalMask;
53 if ((flags & (flags - 1)) != 0)
54 {
55 JLOG(ctx.j.warn()) << "LoanManage: Only one of tfLoanDefault, tfLoanImpair, or "
56 "tfLoanUnimpair can be set.";
57 return temINVALID_FLAG;
58 }
59 }
60
61 return tesSUCCESS;
62}
63
64TER
66{
67 auto const& tx = ctx.tx;
68
69 auto const account = tx[sfAccount];
70 auto const loanID = tx[sfLoanID];
71
72 auto const loanSle = ctx.view.read(keylet::loan(loanID));
73 if (!loanSle)
74 {
75 JLOG(ctx.j.warn()) << "Loan does not exist.";
76 return tecNO_ENTRY;
77 }
78 // Impairment only allows certain transitions.
79 // 1. Once it's in default, it can't be changed.
80 // 2. It can get worse: unimpaired -> impaired -> default
81 // or unimpaired -> default
82 // 3. It can get better: impaired -> unimpaired
83 // 4. If it's in a state, it can't be put in that state again.
84 if (loanSle->isFlag(lsfLoanDefault))
85 {
86 JLOG(ctx.j.warn()) << "Loan is in default. A defaulted loan can not be modified.";
87 return tecNO_PERMISSION;
88 }
89 if (loanSle->isFlag(lsfLoanImpaired) && tx.isFlag(tfLoanImpair))
90 {
91 JLOG(ctx.j.warn()) << "Loan is impaired. A loan can not be impaired twice.";
92 return tecNO_PERMISSION;
93 }
94 if (!(loanSle->isFlag(lsfLoanImpaired) || loanSle->isFlag(lsfLoanDefault)) &&
95 (tx.isFlag(tfLoanUnimpair)))
96 {
97 JLOG(ctx.j.warn()) << "Loan is unimpaired. Can not be unimpaired again.";
98 return tecNO_PERMISSION;
99 }
100 if (loanSle->at(sfPaymentRemaining) == 0)
101 {
102 JLOG(ctx.j.warn()) << "Loan is fully paid. A loan can not be modified "
103 "after it is fully paid.";
104 return tecNO_PERMISSION;
105 }
106 if (tx.isFlag(tfLoanDefault) &&
107 !hasExpired(
108 ctx.view,
109 loanSle->at(sfNextPaymentDueDate) + loanSle->at(sfGracePeriod),
110 ctx.view.rules().enabled(fixCleanup3_4_0) ? ExpiryComparison::Exclusive
112 {
113 JLOG(ctx.j.warn()) << "A loan can not be defaulted before the next payment due date.";
114 return tecTOO_SOON;
115 }
116
117 auto const loanBrokerID = loanSle->at(sfLoanBrokerID);
118 auto const loanBrokerSle = ctx.view.read(keylet::loanBroker(loanBrokerID));
119 if (!loanBrokerSle)
120 {
121 // should be impossible
122 return tecINTERNAL; // LCOV_EXCL_LINE
123 }
124 if (loanBrokerSle->at(sfOwner) != account)
125 {
126 JLOG(ctx.j.warn()) << "LoanBroker for Loan does not belong to the account. LoanManage "
127 "can only be submitted by the Loan Broker.";
128 return tecNO_PERMISSION;
129 }
130
131 return tesSUCCESS;
132}
133
134TER
137 SLE::Ref loanSle,
138 SLE::Ref brokerSle,
139 SLE::Ref vaultSle,
140 Asset const& vaultAsset,
142{
143 // Calculate the amount of the Default that First-Loss Capital covers:
144
145 std::int32_t const loanScale = loanSle->at(sfLoanScale);
146 auto brokerDebtTotalProxy = brokerSle->at(sfDebtTotal);
147
148 Number const totalDefaultAmount = loanVaultExposure(vaultSle, loanSle);
149
150 // Apply the First-Loss Capital to the Default Amount
151 TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
152 TenthBips32 const coverRateLiquidation{brokerSle->at(sfCoverRateLiquidation)};
153 auto const defaultCovered = [&]() {
154 // Always round the minimum required up.
156 auto const minimumCover = tenthBipsOfValue(brokerDebtTotalProxy.value(), coverRateMinimum);
157 // Round the liquidation amount up, too
158 auto const covered = roundToAsset(
159 vaultAsset,
160 /*
161 * This formula is from the XLS-66 spec, section 3.2.3.2 (State
162 * Changes), specifically "if the `tfLoanDefault` flag is set" /
163 * "Apply the First-Loss Capital to the Default Amount"
164 */
165 std::min(tenthBipsOfValue(minimumCover, coverRateLiquidation), totalDefaultAmount),
166 loanScale);
167 auto const coverAvailable = *brokerSle->at(sfCoverAvailable);
168
169 return std::min(covered, coverAvailable);
170 }();
171
172 auto const vaultDefaultAmount = totalDefaultAmount - defaultCovered;
173
174 // Update the Vault object:
175
176 // The vault may be at a different scale than the loan. Reduce rounding
177 // errors during the accounting by rounding some of the values to that
178 // scale.
179 auto const vaultScale = getAssetsTotalScale(vaultSle);
180
181 {
182 // Decrease the Total Value of the Vault:
183 auto vaultTotalProxy = vaultSle->at(sfAssetsTotal);
184 auto vaultAvailableProxy = vaultSle->at(sfAssetsAvailable);
185
186 if (vaultTotalProxy < vaultDefaultAmount)
187 {
188 // LCOV_EXCL_START
189 JLOG(j.warn()) << "Vault total assets is less than the vault default amount";
190 return tefBAD_LEDGER;
191 // LCOV_EXCL_STOP
192 }
193
194 auto const vaultDefaultRounded = roundToAsset(
195 vaultAsset, vaultDefaultAmount, vaultScale, Number::RoundingMode::Downward);
196 vaultTotalProxy -= vaultDefaultRounded;
197 // Increase the Asset Available of the Vault by liquidated First-Loss
198 // Capital and any unclaimed funds amount:
199 vaultAvailableProxy += defaultCovered;
200 if (*vaultAvailableProxy > *vaultTotalProxy && !vaultAsset.integral())
201 {
202 auto const difference = vaultAvailableProxy - vaultTotalProxy;
203 JLOG(j.debug()) << "Vault assets available: " << *vaultAvailableProxy << "("
204 << vaultAvailableProxy.value().exponent()
205 << "), Total: " << *vaultTotalProxy << "("
206 << vaultTotalProxy.value().exponent() << "), Difference: " << difference
207 << "(" << difference.exponent() << ")";
208 if (vaultAvailableProxy.value().exponent() - difference.exponent() > 13)
209 {
210 // If the difference is dust, bring the total up to match
211 // the available
212 JLOG(j.debug()) << "Difference between vault assets available and total is "
213 "dust. Set both to the larger value.";
214 vaultTotalProxy = vaultAvailableProxy;
215 }
216 }
217 if (*vaultAvailableProxy > *vaultTotalProxy)
218 {
219 // LCOV_EXCL_START
220 JLOG(j.fatal()) << "Vault assets available must not be greater "
221 "than assets outstanding. Available: "
222 << *vaultAvailableProxy << ", Total: " << *vaultTotalProxy;
223 return tecINTERNAL;
224 // LCOV_EXCL_STOP
225 }
226
227 // The loss has been realized
228 if (loanSle->isFlag(lsfLoanImpaired))
229 {
230 auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
231 if (vaultLossUnrealizedProxy < totalDefaultAmount)
232 {
233 // LCOV_EXCL_START
234 JLOG(j.warn()) << "Vault unrealized loss is less than the default amount";
235 return tefBAD_LEDGER;
236 // LCOV_EXCL_STOP
237 }
239 vaultLossUnrealizedProxy, -totalDefaultAmount, vaultAsset, vaultScale);
240 }
241 view.update(vaultSle);
242 }
243
244 // Update the LoanBroker object:
245
246 {
247 // Decrease the Debt of the LoanBroker:
248 adjustImpreciseNumber(brokerDebtTotalProxy, -totalDefaultAmount, vaultAsset, vaultScale);
249 // Decrease the First-Loss Capital Cover Available:
250 auto coverAvailableProxy = brokerSle->at(sfCoverAvailable);
251 if (coverAvailableProxy < defaultCovered)
252 {
253 // LCOV_EXCL_START
254 JLOG(j.warn()) << "LoanBroker cover available is less than amount covered";
255 return tefBAD_LEDGER;
256 // LCOV_EXCL_STOP
257 }
258 coverAvailableProxy -= defaultCovered;
259 view.update(brokerSle);
260 }
261
262 // Update the Loan object:
263 loanSle->setFlag(lsfLoanDefault);
264
265 loanSle->at(sfTotalValueOutstanding) = 0;
266 loanSle->at(sfPaymentRemaining) = 0;
267 loanSle->at(sfPrincipalOutstanding) = 0;
268 loanSle->at(sfManagementFeeOutstanding) = 0;
269 // Zero out the next due date. Since it's default, it'll be removed from
270 // the object.
271 loanSle->at(sfNextPaymentDueDate) = 0;
272 view.update(loanSle);
273
274 // Return funds from the LoanBroker pseudo-account to the
275 // Vault pseudo-account:
276 return accountSend(
277 view,
278 brokerSle->at(sfAccount),
279 vaultSle->at(sfAccount),
280 STAmount{vaultAsset, defaultCovered},
281 j,
282 {},
284}
285
286TER
289 SLE::Ref loanSle,
290 SLE::Ref vaultSle,
291 Asset const& vaultAsset,
293{
294 bool const fixEnabled340 = view.rules().enabled(fixCleanup3_4_0);
295
296 if (fixEnabled340 && !isPaymentLate(view, loanSle))
297 {
298 JLOG(j.warn()) << "Cannot impair a loan that is not late";
299 return tecTOO_SOON;
300 }
301
302 Number const lossUnrealized = loanVaultExposure(vaultSle, loanSle);
303
304 // The vault may be at a different scale than the loan. Reduce rounding
305 // errors during the accounting by rounding some of the values to that
306 // scale.
307 auto const vaultScale = getAssetsTotalScale(vaultSle);
308
309 // Update the Vault object(set "paper loss")
310 auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
311 adjustImpreciseNumber(vaultLossUnrealizedProxy, lossUnrealized, vaultAsset, vaultScale);
312 if (vaultLossUnrealizedProxy > vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable))
313 {
314 // Having a loss greater than the vault's unavailable assets
315 // will leave the vault in an invalid / inconsistent state.
316 JLOG(j.warn()) << "Vault unrealized loss is too large, and will corrupt the vault.";
317 return tecLIMIT_EXCEEDED;
318 }
319 view.update(vaultSle);
320
321 // Update the Loan object
322 loanSle->setFlag(lsfLoanImpaired);
323
324 if (!fixEnabled340)
325 {
326 auto loanNextDueProxy = loanSle->at(sfNextPaymentDueDate);
327 if (!isPaymentLate(view, loanSle))
328 {
329 // loan payment is not yet late move the next payment due date to now
330 loanNextDueProxy = view.parentCloseTime().time_since_epoch().count();
331 }
332 }
333 view.update(loanSle);
334
335 return tesSUCCESS;
336}
337
338[[nodiscard]] TER
341 SLE::Ref loanSle,
342 SLE::Ref vaultSle,
343 Asset const& vaultAsset,
345{
346 // The vault may be at a different scale than the loan. Reduce rounding
347 // errors during the accounting by rounding some of the values to that
348 // scale.
349 auto const vaultScale = getAssetsTotalScale(vaultSle);
350
351 // Update the Vault object(clear "paper loss")
352 auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
353 Number const lossReversed = loanVaultExposure(vaultSle, loanSle);
354 if (vaultLossUnrealizedProxy < lossReversed)
355 {
356 // LCOV_EXCL_START
357 JLOG(j.warn()) << "Vault unrealized loss is less than the amount to be cleared";
358 return tefBAD_LEDGER;
359 // LCOV_EXCL_STOP
360 }
361 // Reverse the "paper loss"
362 adjustImpreciseNumber(vaultLossUnrealizedProxy, -lossReversed, vaultAsset, vaultScale);
363
364 view.update(vaultSle);
365
366 // Update the Loan object
367 loanSle->clearFlag(lsfLoanImpaired);
368 if (!view.rules().enabled(fixCleanup3_4_0))
369 {
370 auto const paymentInterval = loanSle->at(sfPaymentInterval);
371 auto const normalPaymentDueDate =
372 std::max(loanSle->at(sfPreviousPaymentDueDate), loanSle->at(sfStartDate)) +
373 paymentInterval;
374
375 if (!hasExpired(view, normalPaymentDueDate))
376 {
377 // loan was unimpaired within the payment interval
378 loanSle->at(sfNextPaymentDueDate) = normalPaymentDueDate;
379 }
380 else
381 {
382 // loan was unimpaired after the original payment due date
383 loanSle->at(sfNextPaymentDueDate) =
384 view.parentCloseTime().time_since_epoch().count() + paymentInterval;
385 }
386 }
387 view.update(loanSle);
388
389 return tesSUCCESS;
390}
391
392TER
394{
395 auto const& tx = ctx_.tx;
396 auto& view = ctx_.view();
397
398 auto const loanID = tx[sfLoanID];
399 auto const loanSle = view.peek(keylet::loan(loanID));
400 if (!loanSle)
401 return tefBAD_LEDGER; // LCOV_EXCL_LINE
402
403 auto const brokerID = loanSle->at(sfLoanBrokerID);
404 auto const brokerSle = view.peek(keylet::loanBroker(brokerID));
405 if (!brokerSle)
406 return tefBAD_LEDGER; // LCOV_EXCL_LINE
407
408 auto const vaultSle = view.peek(keylet::vault(brokerSle->at(sfVaultID)));
409 if (!vaultSle)
410 return tefBAD_LEDGER; // LCOV_EXCL_LINE
411 auto const vaultAsset = vaultSle->at(sfAsset);
412
413 auto const result = [&]() -> TER {
414 // Valid flag combinations are checked in preflight. No flags is valid -
415 // just a noop.
416 if (tx.isFlag(tfLoanDefault))
417 return defaultLoan(view, loanSle, brokerSle, vaultSle, vaultAsset, j_);
418 if (tx.isFlag(tfLoanImpair))
419 return impairLoan(view, loanSle, vaultSle, vaultAsset, j_);
420 if (tx.isFlag(tfLoanUnimpair))
421 return unimpairLoan(view, loanSle, vaultSle, vaultAsset, j_);
422 // NoOp, as described above.
423 return tesSUCCESS;
424 }();
425
426 // Pre-amendment, associateAsset was only called on the noop (no flags)
427 // path. Post-amendment, we call associateAsset on all successful paths.
428 if (view.rules().enabled(fixCleanup3_1_3) && isTesSuccess(result))
429 {
430 associateAsset(*loanSle, vaultAsset);
431 associateAsset(*brokerSle, vaultAsset);
432 associateAsset(*vaultSle, vaultAsset);
433 }
434
435 return result;
436}
437
438void
440{
441 // No transaction-specific invariants yet (future work).
442}
443
444bool
446{
447 // No transaction-specific invariants yet (future work).
448 return true;
449}
450
451//------------------------------------------------------------------------------
452
453} // namespace xrpl
A generic endpoint for log messages.
Definition Journal.h:44
Stream fatal() const
Definition Journal.h:368
Stream debug() const
Definition Journal.h:344
Stream warn() const
Definition Journal.h:356
Writeable view to a ledger, for applying a transaction.
Definition ApplyView.h:141
bool integral() const
Definition Asset.h:133
static TER impairLoan(ApplyView &view, SLE::Ref loanSle, SLE::Ref vaultSle, Asset const &vaultAsset, beast::Journal j)
Helper function that might be needed by other transactors.
static TER preclaim(PreclaimContext const &ctx)
bool finalizeInvariants(STTx const &tx, TER result, XRPAmount fee, ReadView const &view, beast::Journal const &j) override
Check transaction-specific post-conditions after all entries have been visited.
static std::uint32_t getFlagsMask(PreflightContext const &ctx)
static TER unimpairLoan(ApplyView &view, SLE::Ref loanSle, SLE::Ref vaultSle, Asset const &vaultAsset, beast::Journal j)
Helper function that might be needed by other transactors.
void visitInvariantEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after) override
Inspect a single ledger entry modified by this transaction.
TER doApply() override
static TER defaultLoan(ApplyView &view, SLE::Ref loanSle, SLE::Ref brokerSle, SLE::Ref vaultSle, Asset const &vaultAsset, beast::Journal j)
Helper function that might be needed by other transactors.
static NotTEC preflight(PreflightContext const &ctx)
static bool checkExtraFeatures(PreflightContext const &ctx)
Number is a floating point type that can represent a wide range of values.
Definition Number.h:351
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
std::shared_ptr< STLedgerEntry > const & Ref
beast::Journal const j_
Definition Transactor.h:164
ApplyView & view()
Definition Transactor.h:184
ApplyContext & ctx_
Definition Transactor.h:162
T max(T... args)
T min(T... args)
constexpr Zero kZero
Definition Zero.h:30
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 loan(UInt256 const &loanBrokerID, SeqProxy const &loanSeq) noexcept
Definition Indexes.cpp:603
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
bool hasExpired(ReadView const &view, std::optional< std::uint32_t > const &exp, ExpiryComparison comparison=ExpiryComparison::Inclusive)
Determines whether the given expiration time has passed.
Definition View.cpp:51
constexpr T tenthBipsOfValue(T value, TenthBips< TBips > bips)
Definition Protocol.h:139
void adjustImpreciseNumber(NumberProxy value, Number const &adjustment, Asset const &asset, int vaultScale)
@ tefBAD_LEDGER
Definition TER.h:165
int getAssetsTotalScale(SLE::ConstRef vaultSle)
TenthBips< std::uint32_t > TenthBips32
Definition Units.h:454
TERSubset< CanCvtToNotTEC > NotTEC
Definition TER.h:614
TER accountSend(ApplyView &view, AccountID const &from, AccountID const &to, STAmount const &saAmount, beast::Journal j, SLE::Ref sponsorSle={}, WaiveTransferFee waiveFee=WaiveTransferFee::No, AllowMPTOverflow allowOverflow=AllowMPTOverflow::No)
Calls static accountSendIOU if saAmount represents Issue.
bool isPaymentLate(ReadView const &view, SLE::ConstRef loanSle)
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
@ temINVALID
Definition TER.h:98
@ temINVALID_FLAG
Definition TER.h:99
bool isTesSuccess(TER x) noexcept
Definition TER.h:683
TERSubset< CanCvtToTER > TER
Definition TER.h:654
Number loanVaultExposure(SLE::ConstRef vaultSle, SLE::ConstRef loanSle)
@ tecNO_ENTRY
Definition TER.h:314
@ tecINTERNAL
Definition TER.h:318
@ tecTOO_SOON
Definition TER.h:326
@ tecLIMIT_EXCEEDED
Definition TER.h:369
@ tecNO_PERMISSION
Definition TER.h:313
void associateAsset(STLedgerEntry &sle, Asset const &asset)
Associate an Asset with all sMD_NeedsAsset fields in a ledger entry.
constexpr FlagValue tfUniversalMask
Definition TxFlags.h:46
@ tesSUCCESS
Definition TER.h:250
bool checkLendingProtocolDependencies(Rules const &rules, STTx const &tx)
State information when determining if a tx is likely to claim a fee.
Definition Transactor.h:92
ReadView const & view
Definition Transactor.h:95
beast::Journal const j
Definition Transactor.h:100
State information when preflighting a tx.
Definition Transactor.h:39
beast::Journal const j
Definition Transactor.h:46