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