mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-27 09:00:32 +00:00
Add loan and vault invariant checks and invariant tests
This commit is contained in:
@@ -17,12 +17,20 @@ namespace xrpl {
|
||||
*
|
||||
* 1. If `Loan.PaymentRemaining = 0` then `Loan.PrincipalOutstanding = 0`
|
||||
*
|
||||
* A loan may only be deleted once it is fully paid off (no payments
|
||||
* remaining):
|
||||
*
|
||||
* 2. A loan may only be deleted by a `LoanDelete` transaction.
|
||||
* 3. A loan that is not fully paid off must not be deleted.
|
||||
*
|
||||
*/
|
||||
class ValidLoan
|
||||
{
|
||||
// Pair is <before, after>. After is used for most of the checks, except
|
||||
// those that check changed values.
|
||||
std::vector<std::pair<SLE::const_pointer, SLE::const_pointer>> loans_;
|
||||
// Loans removed from the ledger (final state captured at deletion).
|
||||
std::vector<SLE::const_pointer> deletedLoans_;
|
||||
|
||||
public:
|
||||
void
|
||||
|
||||
@@ -43,7 +43,9 @@ namespace xrpl {
|
||||
* - loan manage never removes assets from the vault: assets available may only
|
||||
* grow (and the vault balance grows with it, by the returned first-loss
|
||||
* capital on a default), assets outstanding may only shrink (realized loss),
|
||||
* loss unrealized stays non-negative, and shares do not change
|
||||
* loss unrealized stays non-negative, and shares do not change; a loan
|
||||
* manage with none of the sub-operation flags (impair, unimpair, default)
|
||||
* is a no-op and must not modify the vault
|
||||
* - loan pay adds the paid principal and interest to the vault: assets
|
||||
* available (and the vault balance) increase by the same amount, which is at
|
||||
* most the amount paid, loss unrealized stays non-negative, and shares do not
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -17,7 +18,13 @@ namespace xrpl {
|
||||
void
|
||||
ValidLoan::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
|
||||
{
|
||||
if (after && after->getType() == ltLOAN)
|
||||
if (isDelete)
|
||||
{
|
||||
// On deletion `after` holds the loan's final state.
|
||||
if (after && after->getType() == ltLOAN)
|
||||
deletedLoans_.emplace_back(after);
|
||||
}
|
||||
else if (after && after->getType() == ltLOAN)
|
||||
{
|
||||
loans_.emplace_back(before, after);
|
||||
}
|
||||
@@ -34,6 +41,8 @@ ValidLoan::finalize(
|
||||
// Loans will not exist on ledger if the Lending Protocol amendment
|
||||
// is not enabled, so there's no need to check it.
|
||||
|
||||
auto const txType = tx.getTxnType();
|
||||
|
||||
for (auto const& [before, after] : loans_)
|
||||
{
|
||||
// https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3223-invariants
|
||||
@@ -90,6 +99,27 @@ ValidLoan::finalize(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A loan may only be deleted by a LoanDelete transaction, and only once it
|
||||
// is fully paid off (no payments remaining). Deleting a loan with
|
||||
// outstanding obligations is a violation.
|
||||
for (auto const& loan : deletedLoans_)
|
||||
{
|
||||
if (txType != ttLOAN_DELETE)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: Loan deleted by a transaction "
|
||||
"other than LoanDelete";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (loan->at(sfPaymentRemaining) != 0)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: Loan deleted while not fully "
|
||||
"paid off";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1277,6 +1277,16 @@ ValidVault::finalize(
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// A loan manage with none of the sub-operation flags
|
||||
// (impair, unimpair, default) is a no-op and must not
|
||||
// modify the vault.
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: loan manage without a sub-operation "
|
||||
"must not modify the vault";
|
||||
result = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -3396,6 +3396,34 @@ class Invariants_test : public beast::unit_test::Suite
|
||||
|
||||
testcase << "Vault loan operations";
|
||||
|
||||
// ttLOAN_MANAGE (impair): assets outstanding must not change. Only
|
||||
// assets outstanding is bumped, so the common checks (vault balance,
|
||||
// assets available, shares) all pass and only the impair/unimpair
|
||||
// sub-check fires.
|
||||
doInvariantCheck(
|
||||
{"loan impair/unimpair must not change assets outstanding"},
|
||||
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
|
||||
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
|
||||
return kAdjust(ac.view(), keylet, Adjustments{.assetsTotal = 100});
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttLOAN_MANAGE, [](STObject& tx) { tx.setFieldU32(sfFlags, tfLoanImpair); }},
|
||||
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
|
||||
precloseXrp);
|
||||
|
||||
// ttLOAN_MANAGE with none of the sub-operation flags (impair,
|
||||
// unimpair, default) is a no-op and must not modify the vault.
|
||||
doInvariantCheck(
|
||||
{"loan manage without a sub-operation must not modify the vault"},
|
||||
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
|
||||
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
|
||||
return kAdjust(ac.view(), keylet, Adjustments{.assetsTotal = 100});
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttLOAN_MANAGE, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
|
||||
precloseXrp);
|
||||
|
||||
// ttLOAN_SET: the vault (pseudo-account) balance must change
|
||||
doInvariantCheck(
|
||||
{"loan set must change vault balance"},
|
||||
|
||||
Reference in New Issue
Block a user