rippled
Loading...
Searching...
No Matches
LoanManage.cpp
1#include <xrpld/app/tx/detail/LoanManage.h>
2//
3#include <xrpld/app/misc/LendingHelpers.h>
4
5#include <xrpl/protocol/TxFlags.h>
6
7namespace xrpl {
8
9bool
14
20
23{
24 if (ctx.tx[sfLoanID] == beast::zero)
25 return temINVALID;
26
27 // Flags are mutually exclusive
28 if (auto const flagField = ctx.tx[~sfFlags]; flagField && *flagField)
29 {
30 auto const flags = *flagField & tfUniversalMask;
31 if ((flags & (flags - 1)) != 0)
32 {
33 JLOG(ctx.j.warn())
34 << "LoanManage: Only one of tfLoanDefault, tfLoanImpair, or "
35 "tfLoanUnimpair can be set.";
36 return temINVALID_FLAG;
37 }
38 }
39
40 return tesSUCCESS;
41}
42
43TER
45{
46 auto const& tx = ctx.tx;
47
48 auto const account = tx[sfAccount];
49 auto const loanID = tx[sfLoanID];
50
51 auto const loanSle = ctx.view.read(keylet::loan(loanID));
52 if (!loanSle)
53 {
54 JLOG(ctx.j.warn()) << "Loan does not exist.";
55 return tecNO_ENTRY;
56 }
57 // Impairment only allows certain transitions.
58 // 1. Once it's in default, it can't be changed.
59 // 2. It can get worse: unimpaired -> impaired -> default
60 // or unimpaired -> default
61 // 3. It can get better: impaired -> unimpaired
62 // 4. If it's in a state, it can't be put in that state again.
63 if (loanSle->isFlag(lsfLoanDefault))
64 {
65 JLOG(ctx.j.warn())
66 << "Loan is in default. A defaulted loan can not be modified.";
67 return tecNO_PERMISSION;
68 }
69 if (loanSle->isFlag(lsfLoanImpaired) && tx.isFlag(tfLoanImpair))
70 {
71 JLOG(ctx.j.warn())
72 << "Loan is impaired. A loan can not be impaired twice.";
73 return tecNO_PERMISSION;
74 }
75 if (!(loanSle->isFlag(lsfLoanImpaired) ||
76 loanSle->isFlag(lsfLoanDefault)) &&
77 (tx.isFlag(tfLoanUnimpair)))
78 {
79 JLOG(ctx.j.warn())
80 << "Loan is unimpaired. Can not be unimpaired again.";
81 return tecNO_PERMISSION;
82 }
83 if (loanSle->at(sfPaymentRemaining) == 0)
84 {
85 JLOG(ctx.j.warn()) << "Loan is fully paid. A loan can not be modified "
86 "after it is fully paid.";
87 return tecNO_PERMISSION;
88 }
89 if (tx.isFlag(tfLoanDefault) &&
91 ctx.view,
92 loanSle->at(sfNextPaymentDueDate) + loanSle->at(sfGracePeriod)))
93 {
94 JLOG(ctx.j.warn())
95 << "A loan can not be defaulted before the next payment due date.";
96 return tecTOO_SOON;
97 }
98
99 auto const loanBrokerID = loanSle->at(sfLoanBrokerID);
100 auto const loanBrokerSle = ctx.view.read(keylet::loanbroker(loanBrokerID));
101 if (!loanBrokerSle)
102 {
103 // should be impossible
104 return tecINTERNAL; // LCOV_EXCL_LINE
105 }
106 if (loanBrokerSle->at(sfOwner) != account)
107 {
108 JLOG(ctx.j.warn())
109 << "LoanBroker for Loan does not belong to the account. LoanModify "
110 "can only be submitted by the Loan Broker.";
111 return tecNO_PERMISSION;
112 }
113
114 return tesSUCCESS;
115}
116
117static Number
119{
120 // Spec section 3.2.3.2, defines the default amount as
121 //
122 // DefaultAmount = (Loan.PrincipalOutstanding + Loan.InterestOutstanding)
123 //
124 // Loan.InterestOutstanding is not stored directly on ledger.
125 // It is computed as
126 //
127 // Loan.TotalValueOutstanding - Loan.PrincipalOutstanding -
128 // Loan.ManagementFeeOutstanding
129 //
130 // Add that to the original formula, and you get this:
131 return loanSle->at(sfTotalValueOutstanding) -
132 loanSle->at(sfManagementFeeOutstanding);
133}
134
135TER
137 ApplyView& view,
138 SLE::ref loanSle,
139 SLE::ref brokerSle,
140 SLE::ref vaultSle,
141 Asset const& vaultAsset,
143{
144 // Calculate the amount of the Default that First-Loss Capital covers:
145
146 std::int32_t const loanScale = loanSle->at(sfLoanScale);
147 auto brokerDebtTotalProxy = brokerSle->at(sfDebtTotal);
148
149 Number const totalDefaultAmount = owedToVault(loanSle);
150
151 // Apply the First-Loss Capital to the Default Amount
152 TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
153 TenthBips32 const coverRateLiquidation{
154 brokerSle->at(sfCoverRateLiquidation)};
155 auto const defaultCovered = [&]() {
156 // Always round the minimum required up.
158 auto const minimumCover =
159 tenthBipsOfValue(brokerDebtTotalProxy.value(), coverRateMinimum);
160 // Round the liquidation amount up, too
161 return roundToAsset(
162 vaultAsset,
163 /*
164 * This formula is from the XLS-66 spec, section 3.2.3.2 (State
165 * Changes), specifically "if the `tfLoanDefault` flag is set" /
166 * "Apply the First-Loss Capital to the Default Amount"
167 */
168 std::min(
169 tenthBipsOfValue(minimumCover, coverRateLiquidation),
170 totalDefaultAmount),
171 loanScale);
172 }();
173
174 auto const vaultDefaultAmount = totalDefaultAmount - defaultCovered;
175
176 // Update the Vault object:
177
178 // The vault may be at a different scale than the loan. Reduce rounding
179 // errors during the accounting by rounding some of the values to that
180 // scale.
181 auto const vaultScale = getVaultScale(vaultSle);
182
183 {
184 // Decrease the Total Value of the Vault:
185 auto vaultTotalProxy = vaultSle->at(sfAssetsTotal);
186 auto vaultAvailableProxy = vaultSle->at(sfAssetsAvailable);
187
188 if (vaultTotalProxy < vaultDefaultAmount)
189 {
190 // LCOV_EXCL_START
191 JLOG(j.warn())
192 << "Vault total assets is less than the vault default amount";
193 return tefBAD_LEDGER;
194 // LCOV_EXCL_STOP
195 }
196
197 auto const vaultDefaultRounded = roundToAsset(
198 vaultAsset, vaultDefaultAmount, vaultScale, Number::downward);
199 vaultTotalProxy -= vaultDefaultRounded;
200 // Increase the Asset Available of the Vault by liquidated First-Loss
201 // Capital and any unclaimed funds amount:
202 vaultAvailableProxy += defaultCovered;
203 if (*vaultAvailableProxy > *vaultTotalProxy && !vaultAsset.integral())
204 {
205 auto const difference = vaultAvailableProxy - vaultTotalProxy;
206 JLOG(j.debug())
207 << "Vault assets available: " << *vaultAvailableProxy << "("
208 << vaultAvailableProxy.value().exponent()
209 << "), Total: " << *vaultTotalProxy << "("
210 << vaultTotalProxy.value().exponent()
211 << "), Difference: " << difference << "("
212 << difference.exponent() << ")";
213 if (vaultAvailableProxy.value().exponent() - difference.exponent() >
214 13)
215 {
216 // If the difference is dust, bring the total up to match
217 // the available
218 JLOG(j.debug())
219 << "Difference between vault assets available and total is "
220 "dust. Set both to the larger value.";
221 vaultTotalProxy = vaultAvailableProxy;
222 }
223 }
224 if (*vaultAvailableProxy > *vaultTotalProxy)
225 {
226 JLOG(j.warn()) << "Vault assets available must not be greater "
227 "than assets outstanding. Available: "
228 << *vaultAvailableProxy
229 << ", Total: " << *vaultTotalProxy;
230 return tecLIMIT_EXCEEDED;
231 }
232
233 // The loss has been realized
234 if (loanSle->isFlag(lsfLoanImpaired))
235 {
236 auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
237 if (vaultLossUnrealizedProxy < totalDefaultAmount)
238 {
239 // LCOV_EXCL_START
240 JLOG(j.warn())
241 << "Vault unrealized loss is less than the default amount";
242 return tefBAD_LEDGER;
243 // LCOV_EXCL_STOP
244 }
245 vaultLossUnrealizedProxy -= totalDefaultAmount;
246 }
247 view.update(vaultSle);
248 }
249
250 // Update the LoanBroker object:
251
252 {
253 auto const asset = *vaultSle->at(sfAsset);
254
255 // Decrease the Debt of the LoanBroker:
257 brokerDebtTotalProxy, -totalDefaultAmount, asset, vaultScale);
258 // Decrease the First-Loss Capital Cover Available:
259 auto coverAvailableProxy = brokerSle->at(sfCoverAvailable);
260 if (coverAvailableProxy < defaultCovered)
261 {
262 // LCOV_EXCL_START
263 JLOG(j.warn())
264 << "LoanBroker cover available is less than amount covered";
265 return tefBAD_LEDGER;
266 // LCOV_EXCL_STOP
267 }
268 coverAvailableProxy -= defaultCovered;
269 view.update(brokerSle);
270 }
271
272 // Update the Loan object:
273 loanSle->setFlag(lsfLoanDefault);
274
275 loanSle->at(sfTotalValueOutstanding) = 0;
276 loanSle->at(sfPaymentRemaining) = 0;
277 loanSle->at(sfPrincipalOutstanding) = 0;
278 loanSle->at(sfManagementFeeOutstanding) = 0;
279 // Zero out the next due date. Since it's default, it'll be removed from
280 // the object.
281 loanSle->at(sfNextPaymentDueDate) = 0;
282 view.update(loanSle);
283
284 // Return funds from the LoanBroker pseudo-account to the
285 // Vault pseudo-account:
286 return accountSend(
287 view,
288 brokerSle->at(sfAccount),
289 vaultSle->at(sfAccount),
290 STAmount{vaultAsset, defaultCovered},
291 j,
293}
294
295TER
297 ApplyView& view,
298 SLE::ref loanSle,
299 SLE::ref vaultSle,
301{
302 Number const lossUnrealized = owedToVault(loanSle);
303
304 // Update the Vault object(set "paper loss")
305 auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
306 vaultLossUnrealizedProxy += lossUnrealized;
307 if (vaultLossUnrealizedProxy >
308 vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable))
309 {
310 // Having a loss greater than the vault's unavailable assets
311 // will leave the vault in an invalid / inconsistent state.
312 JLOG(j.warn()) << "Vault unrealized loss is too large, and will "
313 "corrupt the vault.";
314 return tecLIMIT_EXCEEDED;
315 }
316 view.update(vaultSle);
317
318 // Update the Loan object
319 loanSle->setFlag(lsfLoanImpaired);
320 auto loanNextDueProxy = loanSle->at(sfNextPaymentDueDate);
321 if (!hasExpired(view, loanNextDueProxy))
322 {
323 // loan payment is not yet late -
324 // move the next payment due date to now
325 loanNextDueProxy = view.parentCloseTime().time_since_epoch().count();
326 }
327 view.update(loanSle);
328
329 return tesSUCCESS;
330}
331
332TER
334 ApplyView& view,
335 SLE::ref loanSle,
336 SLE::ref vaultSle,
338{
339 // Update the Vault object(clear "paper loss")
340 auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
341 Number const lossReversed = owedToVault(loanSle);
342 if (vaultLossUnrealizedProxy < lossReversed)
343 {
344 // LCOV_EXCL_START
345 JLOG(j.warn())
346 << "Vault unrealized loss is less than the amount to be cleared";
347 return tefBAD_LEDGER;
348 // LCOV_EXCL_STOP
349 }
350 vaultLossUnrealizedProxy -= lossReversed;
351 view.update(vaultSle);
352
353 // Update the Loan object
354 loanSle->clearFlag(lsfLoanImpaired);
355 auto const paymentInterval = loanSle->at(sfPaymentInterval);
356 auto const normalPaymentDueDate =
357 std::max(loanSle->at(sfPreviousPaymentDate), loanSle->at(sfStartDate)) +
358 paymentInterval;
359 if (!hasExpired(view, normalPaymentDueDate))
360 {
361 // loan was unimpaired within the payment interval
362 loanSle->at(sfNextPaymentDueDate) = normalPaymentDueDate;
363 }
364 else
365 {
366 // loan was unimpaired after the original payment due date
367 loanSle->at(sfNextPaymentDueDate) =
368 view.parentCloseTime().time_since_epoch().count() + paymentInterval;
369 }
370 view.update(loanSle);
371
372 return tesSUCCESS;
373}
374
375TER
377{
378 auto const& tx = ctx_.tx;
379 auto& view = ctx_.view();
380
381 auto const loanID = tx[sfLoanID];
382 auto const loanSle = view.peek(keylet::loan(loanID));
383 if (!loanSle)
384 return tefBAD_LEDGER; // LCOV_EXCL_LINE
385
386 auto const brokerID = loanSle->at(sfLoanBrokerID);
387 auto const brokerSle = view.peek(keylet::loanbroker(brokerID));
388 if (!brokerSle)
389 return tefBAD_LEDGER; // LCOV_EXCL_LINE
390
391 auto const vaultSle = view.peek(keylet ::vault(brokerSle->at(sfVaultID)));
392 if (!vaultSle)
393 return tefBAD_LEDGER; // LCOV_EXCL_LINE
394 auto const vaultAsset = vaultSle->at(sfAsset);
395
396 // Valid flag combinations are checked in preflight. No flags is valid -
397 // just a noop.
398 if (tx.isFlag(tfLoanDefault))
399 {
400 if (auto const ter =
401 defaultLoan(view, loanSle, brokerSle, vaultSle, vaultAsset, j_))
402 return ter;
403 }
404 else if (tx.isFlag(tfLoanImpair))
405 {
406 if (auto const ter = impairLoan(view, loanSle, vaultSle, j_))
407 return ter;
408 }
409 else if (tx.isFlag(tfLoanUnimpair))
410 {
411 if (auto const ter = unimpairLoan(view, loanSle, vaultSle, j_))
412 return ter;
413 }
414
415 return tesSUCCESS;
416}
417
418//------------------------------------------------------------------------------
419
420} // namespace xrpl
A generic endpoint for log messages.
Definition Journal.h:41
Stream debug() const
Definition Journal.h:309
Stream warn() const
Definition Journal.h:321
STTx const & tx
ApplyView & view()
Writeable view to a ledger, for applying a transaction.
Definition ApplyView.h:124
virtual void update(std::shared_ptr< SLE > const &sle)=0
Indicate changes to a peeked SLE.
virtual std::shared_ptr< SLE > peek(Keylet const &k)=0
Prepare to modify the SLE associated with key.
bool integral() const
Definition Asset.h:95
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 TER preclaim(PreclaimContext const &ctx)
static TER unimpairLoan(ApplyView &view, SLE::ref loanSle, SLE::ref vaultSle, beast::Journal j)
Helper function that might be needed by other transactors.
static std::uint32_t getFlagsMask(PreflightContext const &ctx)
static TER impairLoan(ApplyView &view, SLE::ref loanSle, SLE::ref vaultSle, beast::Journal j)
Helper function that might be needed by other transactors.
TER doApply() override
static NotTEC preflight(PreflightContext const &ctx)
static bool checkExtraFeatures(PreflightContext const &ctx)
NetClock::time_point parentCloseTime() const
Returns the close time of the previous ledger.
Definition ReadView.h:92
virtual std::shared_ptr< SLE const > read(Keylet const &k) const =0
Return the state item associated with a key.
beast::Journal const j_
Definition Transactor.h:126
ApplyView & view()
Definition Transactor.h:144
ApplyContext & ctx_
Definition Transactor.h:124
T max(T... args)
T min(T... args)
Keylet loanbroker(AccountID const &owner, std::uint32_t seq) noexcept
Definition Indexes.cpp:552
Keylet loan(uint256 const &loanBrokerID, std::uint32_t loanSeq) noexcept
Definition Indexes.cpp:558
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:6
constexpr std::uint32_t const tfLoanImpair
Definition TxFlags.h:291
Number roundToAsset(A const &asset, Number const &value, std::int32_t scale, Number::rounding_mode rounding=Number::getround())
Round an arbitrary precision Number to the precision of a given Asset.
Definition STAmount.h:722
constexpr T tenthBipsOfValue(T value, TenthBips< TBips > bips)
Definition Protocol.h:108
bool hasExpired(ReadView const &view, std::optional< std::uint32_t > const &exp)
Determines whether the given expiration time has passed.
Definition View.cpp:154
void adjustImpreciseNumber(NumberProxy value, Number const &adjustment, Asset const &asset, int vaultScale)
@ tefBAD_LEDGER
Definition TER.h:151
bool checkLendingProtocolDependencies(PreflightContext const &ctx)
TER accountSend(ApplyView &view, AccountID const &from, AccountID const &to, STAmount const &saAmount, beast::Journal j, WaiveTransferFee waiveFee=WaiveTransferFee::No)
Calls static accountSendIOU if saAmount represents Issue.
Definition View.cpp:2777
TERSubset< CanCvtToTER > TER
Definition TER.h:630
static Number owedToVault(SLE::ref loanSle)
constexpr std::uint32_t const tfLoanDefault
Definition TxFlags.h:290
int getVaultScale(SLE::const_ref vaultSle)
@ temINVALID
Definition TER.h:91
@ temINVALID_FLAG
Definition TER.h:92
constexpr std::uint32_t const tfLoanUnimpair
Definition TxFlags.h:292
@ tecNO_ENTRY
Definition TER.h:288
@ tecINTERNAL
Definition TER.h:292
@ tecTOO_SOON
Definition TER.h:300
@ tecLIMIT_EXCEEDED
Definition TER.h:343
@ tecNO_PERMISSION
Definition TER.h:287
@ lsfLoanImpaired
@ lsfLoanDefault
@ tesSUCCESS
Definition TER.h:226
constexpr std::uint32_t tfUniversalMask
Definition TxFlags.h:44
constexpr std::uint32_t const tfLoanManageMask
Definition TxFlags.h:293
State information when determining if a tx is likely to claim a fee.
Definition Transactor.h:61
ReadView const & view
Definition Transactor.h:64
beast::Journal const j
Definition Transactor.h:69
State information when preflighting a tx.
Definition Transactor.h:16
beast::Journal const j
Definition Transactor.h:23
T time_since_epoch(T... args)