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