xrpld
Loading...
Searching...
No Matches
LoanInvariant.cpp
1#include <xrpl/tx/invariants/LoanInvariant.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/VaultHelpers.h>
9#include <xrpl/protocol/Asset.h>
10#include <xrpl/protocol/Feature.h>
11#include <xrpl/protocol/Indexes.h>
12#include <xrpl/protocol/LedgerFormats.h>
13#include <xrpl/protocol/Protocol.h>
14#include <xrpl/protocol/SField.h>
15#include <xrpl/protocol/STLedgerEntry.h>
16#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
17#include <xrpl/protocol/STTx.h>
18#include <xrpl/protocol/TER.h>
19#include <xrpl/protocol/TxFormats.h>
20#include <xrpl/protocol/XRPAmount.h>
21
22#include <cstdint>
23
24namespace xrpl {
25
26void
28{
29 // Classify here, but leave the decision about which checks apply to
30 // finalize(), which is the only place that can see the Rules.
31 if (isDelete)
32 {
33 if (before && before->getType() == ltLOAN)
34 deletedLoans_.emplace_back(before, after);
35 }
36 else if (after && after->getType() == ltLOAN)
37 {
38 loans_.emplace_back(before, after);
39 }
40}
41
42bool
44 STTx const& tx,
45 TER const result,
46 XRPAmount const,
47 ReadView const& view,
48 beast::Journal const& j)
49{
50 // Loans will not exist on ledger if the Lending Protocol amendment
51 // is not enabled, so there's no need to check it.
52
53 auto const txType = tx.getTxnType();
54 bool const lpV11Enabled = view.rules().enabled(featureLendingProtocolV1_1);
55
56 // Without featureLendingProtocolV1_1 an erased Loan is subject to the same
57 // per-entry checks as any modified Loan. From V1_1 onward it is only subject
58 // to the ttLOAN_DELETE check below.
59 if (!lpV11Enabled)
60 loans_.insert(loans_.end(), deletedLoans_.begin(), deletedLoans_.end());
61
62 // Ledger entry validation checks.
63 for (auto const& [before, after] : loans_)
64 {
65 // A closed-ended vault must not accept a loan whose final scheduled payment falls fewer
66 // than kLoanRedemptionBuffer seconds before the vault's RedemptionDate. This mirrors the
67 // LoanSet::preclaim gate and only fires on loan creation; once the loan exists, its
68 // StartDate / PaymentInterval are immutable and PaymentRemaining only decreases, so the
69 // bound is preserved.
70 if (!before && isTesSuccess(result))
71 {
72 auto const broker = view.read(keylet::loanBroker(after->at(sfLoanBrokerID)));
73 if (broker)
74 {
75 auto const vault = view.read(keylet::vault(broker->at(sfVaultID)));
76 // We don't check for LendingProtocolV1_1 amendment because a ClosedEnded Vault will
77 // not exist without the amendment enabled
78 if (vault && getVaultKind(vault) == VaultKind::ClosedEnded)
79 {
80 std::uint32_t const startDate = after->at(sfStartDate);
81 std::uint32_t const interval = after->at(sfPaymentInterval);
82 std::uint32_t const remaining = after->at(sfPaymentRemaining);
83 std::uint32_t const redemption = vault->at(sfRedemptionDate);
84 if (std::uint64_t{startDate} + (std::uint64_t{interval} * remaining) +
86 redemption)
87 {
88 JLOG(j.fatal()) << "Invariant failed: closed-ended loan final payment "
89 "must precede RedemptionDate by at least "
90 "kLoanRedemptionBuffer";
91 return false;
92 }
93 }
94 }
95 }
96
97 // https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3223-invariants
98 // If `Loan.PaymentRemaining = 0` then the loan MUST be fully paid off
99 if (after->at(sfPaymentRemaining) == 0 &&
100 (after->at(sfTotalValueOutstanding) != beast::kZero ||
101 after->at(sfPrincipalOutstanding) != beast::kZero ||
102 after->at(sfManagementFeeOutstanding) != beast::kZero))
103 {
104 JLOG(j.fatal()) << "Invariant failed: Loan with zero payments "
105 "remaining has not been paid off";
106 return false;
107 }
108 // If `Loan.PaymentRemaining != 0` then the loan MUST NOT be fully paid
109 // off
110 if (after->at(sfPaymentRemaining) != 0 &&
111 after->at(sfTotalValueOutstanding) == beast::kZero &&
112 after->at(sfPrincipalOutstanding) == beast::kZero &&
113 after->at(sfManagementFeeOutstanding) == beast::kZero)
114 {
115 JLOG(j.fatal()) << "Invariant failed: Fully paid off Loan still has payments remaining";
116 return false;
117 }
118
119 // From featureLendingProtocolV1_1 onwards this flag is immutable by way of
120 // NoModifiedUnmodifiableFields.
121 if (!lpV11Enabled && before &&
122 (before->isFlag(lsfLoanOverpayment) != after->isFlag(lsfLoanOverpayment)))
123 {
124 JLOG(j.fatal()) << "Invariant failed: Loan Overpayment flag changed";
125 return false;
126 }
127 // Must not be negative - STNumber
128 for (auto const field :
129 {&sfLoanServiceFee,
130 &sfLatePaymentFee,
131 &sfClosePaymentFee,
132 &sfPrincipalOutstanding,
133 &sfTotalValueOutstanding,
134 &sfManagementFeeOutstanding})
135 {
136 if (after->at(*field) < 0)
137 {
138 JLOG(j.fatal()) << "Invariant failed: " << field->getName() << " is negative ";
139 return false;
140 }
141 }
142 // Must be positive - STNumber
143 for (auto const field : {
144 &sfPeriodicPayment,
145 })
146 {
147 if (after->at(*field) <= 0)
148 {
149 JLOG(j.fatal()) << "Invariant failed: " << field->getName()
150 << " is zero or negative ";
151 return false;
152 }
153 }
154 if (lpV11Enabled)
155 {
156 // Only LoanSet may create a loan.
157 if (!before && txType != ttLOAN_SET)
158 {
159 JLOG(j.fatal()) << "Invariant failed: Loan created by a transaction "
160 "other than LoanSet";
161 return false;
162 }
163
164 if (after->at(sfPaymentRemaining) == 0 &&
165 after->at(~sfNextPaymentDueDate).value_or(0) != 0)
166 {
167 JLOG(j.fatal()) << "Invariant failed: Loan with zero payments must have zero next "
168 "payment due date";
169 return false;
170 }
171
172 if (before)
173 {
174 bool const wasImpaired = before->isFlag(lsfLoanImpaired);
175 bool const isImpaired = after->isFlag(lsfLoanImpaired);
176 bool const wasDefaulted = before->isFlag(lsfLoanDefault);
177 bool const isDefaulted = after->isFlag(lsfLoanDefault);
178
179 if (wasImpaired != isImpaired && txType != ttLOAN_MANAGE && txType != ttLOAN_PAY)
180 {
181 JLOG(j.fatal()) << "Invariant failed: lsfLoanImpaired changed "
182 "outside LoanManage or LoanPay";
183 return false;
184 }
185 if (wasDefaulted != isDefaulted && txType != ttLOAN_MANAGE)
186 {
187 JLOG(j.fatal()) << "Invariant failed: lsfLoanDefault changed "
188 "outside LoanManage";
189 return false;
190 }
191 }
192
193 // A loan must reference a live loan broker, and that broker must
194 // reference a live vault; otherwise the loan is orphaned and its
195 // balances have no counterparty on the ledger.
196 auto const brokerSle = view.read(keylet::loanBroker(after->at(sfLoanBrokerID)));
197 if (!brokerSle)
198 {
199 JLOG(j.fatal()) << "Invariant failed: Loan broker does not exist";
200 return false;
201 }
202 auto const vaultSle = view.read(keylet::vault(brokerSle->at(sfVaultID)));
203 if (!vaultSle)
204 {
205 JLOG(j.fatal()) << "Invariant failed: Loan broker vault does not exist";
206 return false;
207 }
208
209 // Interest due (the total value owed less principal and management fee)
210 // must never be negative. TotalValueOutstanding, PrincipalOutstanding and
211 // ManagementFeeOutstanding are each independently rounded to sfLoanScale
212 // by the accounting code, so their difference can carry one unit of
213 // quantization noise even when the underlying flow is correct. Absorb
214 // one unit at that scale, matching the pattern used in ValidVault.
215 auto const interestDue = after->at(sfTotalValueOutstanding) -
216 after->at(sfPrincipalOutstanding) - after->at(sfManagementFeeOutstanding);
217
218 // Only IOU amounts can accumulate STAmount quantization noise. For integral-domain
219 // assets (XRP/MPT) enforce the boundary strictly.
220 bool const integral = Asset{vaultSle->at(sfAsset)}.integral();
221
222 Number const tolerance = integral ? Number{} : Number{-1, after->at(sfLoanScale)};
223 if (interestDue < tolerance)
224 {
225 JLOG(j.fatal()) << "Invariant failed: Loan interest due is negative";
226 return false;
227 }
228
229 // Transaction success post-conditions. A successful loan pay makes at least
230 // one scheduled payment, so a loan left with payments still outstanding
231 // must show that payment in its balance and schedule. A payment that clears
232 // the loan outright instead drives PaymentRemaining to zero, which the
233 // fully-paid-off and zero due-date checks above pin.
234 //
235 // PrincipalOutstanding may stay put on a non-final pay: at integer
236 // scale, fixCleanup3_2_0 rounds principal up so a fractional
237 // amortization step does not reduce it. Interest (TVO) still falls.
238 // Neither balance may grow: a payment never adds to what is owed,
239 // since late-payment penalties are charged in the same transaction
240 // rather than tracked in TotalValueOutstanding.
241 if (isTesSuccess(result) && txType == ttLOAN_PAY)
242 {
243 if (before && after->at(sfPaymentRemaining) != 0)
244 {
245 if (after->at(sfPrincipalOutstanding) > before->at(sfPrincipalOutstanding))
246 {
247 JLOG(j.fatal()) << "Invariant failed: loan pay must not increase "
248 "PrincipalOutstanding on a non-full-repayment";
249 return false;
250 }
251 if (after->at(sfTotalValueOutstanding) > before->at(sfTotalValueOutstanding))
252 {
253 JLOG(j.fatal()) << "Invariant failed: loan pay must not increase "
254 "TotalValueOutstanding on a non-full-repayment";
255 return false;
256 }
257 if (after->at(sfPrincipalOutstanding) == before->at(sfPrincipalOutstanding) &&
258 after->at(sfTotalValueOutstanding) == before->at(sfTotalValueOutstanding))
259 {
260 JLOG(j.fatal()) << "Invariant failed: loan pay must decrease "
261 "PrincipalOutstanding or TotalValueOutstanding "
262 "on a non-full-repayment";
263 return false;
264 }
265 if (after->at(sfPaymentRemaining) >= before->at(sfPaymentRemaining))
266 {
267 JLOG(j.fatal()) << "Invariant failed: loan pay must decrease "
268 "PaymentRemaining on a non-full-repayment";
269 return false;
270 }
271
272 std::uint32_t const beforeDue = before->at(~sfNextPaymentDueDate).value_or(0);
273 std::uint32_t const afterDue = after->at(~sfNextPaymentDueDate).value_or(0);
274 std::uint32_t const interval = after->at(sfPaymentInterval);
275 if (afterDue <= beforeDue || interval == 0 ||
276 (afterDue - beforeDue) % interval != 0)
277 {
278 JLOG(j.fatal()) << "Invariant failed: loan pay must advance "
279 "NextPaymentDueDate by a positive multiple of "
280 "PaymentInterval on a non-full-repayment";
281 return false;
282 }
283 }
284 }
285 }
286 }
287
288 // Only LoanDelete may delete a loan.
289 if (lpV11Enabled && txType != ttLOAN_DELETE && !deletedLoans_.empty())
290 {
291 JLOG(j.fatal()) << "Invariant failed: Loan deleted by a transaction "
292 "other than LoanDelete";
293 return false;
294 }
295 return true;
296}
297
298} // namespace xrpl
A generic endpoint for log messages.
Definition Journal.h:44
Stream fatal() const
Definition Journal.h:368
bool integral() const
Definition Asset.h:133
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
TxType getTxnType() const
Definition STTx.h:250
void visitEntry(bool, SLE::ConstRef, SLE::ConstRef)
std::vector< std::pair< SLE::const_pointer, SLE::const_pointer > > deletedLoans_
std::vector< std::pair< SLE::const_pointer, SLE::const_pointer > > loans_
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
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
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
bool after(NetClock::time_point now, std::uint32_t mark)
Has the specified time passed?
Definition View.cpp:644
VaultKind getVaultKind(SLE::ConstRef vault)
Resolves the VaultKind of a vault SLE.
bool isTesSuccess(TER x) noexcept
Definition TER.h:683
TERSubset< CanCvtToTER > TER
Definition TER.h:654
constexpr std::uint32_t kLoanRedemptionBuffer
Minimum gap between a closed-ended loan's final scheduled payment and the vault's RedemptionDate.
Definition Protocol.h:357