Finish LoanManage functionality and tests, modulo LoanDraw/Pay

This commit is contained in:
Ed Hennis
2025-05-04 21:53:07 -04:00
parent ef2a0edc67
commit 5295f47999
5 changed files with 312 additions and 240 deletions

View File

@@ -112,6 +112,47 @@ class Loan_test : public beast::unit_test::suite
} }
}; };
struct VerifyStatus
{
public:
jtx::Env const& env;
BrokerInfo const& broker;
Keylet const& keylet;
VerifyStatus(
jtx::Env const& env_,
BrokerInfo const& broker_,
Keylet const& keylet_)
: env(env_), broker(broker_), keylet(keylet_)
{
}
void
operator()(
std::uint32_t previousPaymentDate,
std::uint32_t nextPaymentDate,
std::uint32_t paymentRemaining,
Number const& assetsAvailable,
Number const& principalOutstanding,
std::uint32_t flags) const
{
if (auto loan = env.le(keylet); env.test.BEAST_EXPECT(loan))
{
env.test.BEAST_EXPECT(
loan->at(sfPreviousPaymentDate) == previousPaymentDate);
env.test.BEAST_EXPECT(
loan->at(sfNextPaymentDueDate) == nextPaymentDate);
env.test.BEAST_EXPECT(
loan->at(sfPaymentRemaining) == paymentRemaining);
env.test.BEAST_EXPECT(
loan->at(sfAssetsAvailable) == assetsAvailable);
env.test.BEAST_EXPECT(
loan->at(sfPrincipalOutstanding) == principalOutstanding);
env.test.BEAST_EXPECT(loan->at(sfFlags) == flags);
}
}
};
void void
lifecycle( lifecycle(
const char* label, const char* label,
@@ -121,10 +162,11 @@ class Loan_test : public beast::unit_test::suite
jtx::Account const& evan, jtx::Account const& evan,
BrokerInfo const& broker, BrokerInfo const& broker,
std::uint32_t flags, std::uint32_t flags,
std::function<jtx::JTx(jtx::JTx const&)> modifyJTx, // The end of life callback is expected to take the loan to 0 payments
std::function<void(SLE::const_ref)> checkLoan, // remaining, one way or another
std::function<void(SLE::const_ref)> changeLoan, std::function<
std::function<void(SLE::const_ref)> checkChangedLoan) void(Keylet const& loanKeylet, VerifyStatus const& verifyStatus)>
toEndOfLife)
{ {
auto const [keylet, loanSequence] = [&]() { auto const [keylet, loanSequence] = [&]() {
auto const brokerSLE = env.le(keylet::loanbroker(broker.brokerID)); auto const brokerSLE = env.le(keylet::loanbroker(broker.brokerID));
@@ -139,6 +181,9 @@ class Loan_test : public beast::unit_test::suite
return std::make_pair( return std::make_pair(
keylet::loan(broker.brokerID, loanSequence), loanSequence); keylet::loan(broker.brokerID, loanSequence), loanSequence);
}(); }();
VerifyStatus const verifyStatus(env, broker, keylet);
if (!BEAST_EXPECT(loanSequence != 0)) if (!BEAST_EXPECT(loanSequence != 0))
return; return;
{ {
@@ -189,14 +234,14 @@ class Loan_test : public beast::unit_test::suite
paymentInterval(interval), paymentInterval(interval),
gracePeriod(grace), gracePeriod(grace),
fee(loanSetFee)); fee(loanSetFee));
// Modify as desired
if (modifyJTx)
createJtx = modifyJTx(createJtx);
// Successfully create a Loan // Successfully create a Loan
env(createJtx); env(createJtx);
env.close(); env.close();
auto const loanFlags =
createJtx.stx->isFlag(tfLoanOverpayment) ? lsfLoanOverpayment : 0;
if (auto loan = env.le(keylet); BEAST_EXPECT(loan)) if (auto loan = env.le(keylet); BEAST_EXPECT(loan))
{ {
// log << "loan after create: " << to_string(loan->getJson()) // log << "loan after create: " << to_string(loan->getJson())
@@ -230,155 +275,59 @@ class Loan_test : public beast::unit_test::suite
loan->at(sfAssetsAvailable) == loan->at(sfAssetsAvailable) ==
principalRequest - originationFee); principalRequest - originationFee);
BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == principalRequest); BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == principalRequest);
if (checkLoan) }
checkLoan(loan);
#if 0 verifyStatus(
auto verifyStatus = [&env, &broker, &loan, this]( 0,
auto previousPaymentDate, startDate.time_since_epoch().count() + interval,
auto nextPaymentDate, total,
auto paymentRemaining, principalRequest - originationFee,
auto assetsAvailable, principalRequest,
auto principalOutstanding) { loanFlags | 0);
auto const available = broker.asset(assetsAvailable);
auto const outstanding = broker.asset(principalOutstanding);
BEAST_EXPECT(
loan->at(sfPreviousPaymentDate) == previousPaymentDate);
BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == nextPaymentDate);
BEAST_EXPECT(loan->at(sfPaymentRemaining) == paymentRemaining);
BEAST_EXPECT(loan->at(sfAssetsAvailable) == available.number());
BEAST_EXPECT(
loan->at(sfPrincipalOutstanding) == outstanding.number());
};
// Test Cover funding before allowing alterations // Manage the loan
env(coverDeposit(alice, uint256(0), vault.asset(10)), // no-op
ter(temINVALID)); env(manage(lender, keylet.key, 0));
env(coverDeposit(evan, keylet.key, vault.asset(10)), // Only the lender can manage
ter(tecNO_PERMISSION)); env(manage(evan, keylet.key, 0), ter(tecNO_PERMISSION));
env(coverDeposit(evan, keylet.key, vault.asset(0)), // unknown flags
ter(temBAD_AMOUNT)); env(manage(lender, keylet.key, tfLoanManageMask), ter(temINVALID_FLAG));
env(coverDeposit(evan, keylet.key, vault.asset(-10)), // combinations of flags are not allowed
ter(temBAD_AMOUNT)); env(manage(lender, keylet.key, tfLoanUnimpair | tfLoanImpair),
env(coverDeposit(alice, vault.vaultID, vault.asset(10)), ter(temINVALID_FLAG));
ter(tecNO_ENTRY)); env(manage(lender, keylet.key, tfLoanImpair | tfLoanDefault),
ter(temINVALID_FLAG));
env(manage(lender, keylet.key, tfLoanUnimpair | tfLoanDefault),
ter(temINVALID_FLAG));
env(manage(
lender,
keylet.key,
tfLoanUnimpair | tfLoanImpair | tfLoanDefault),
ter(temINVALID_FLAG));
// invalid loan ID
env(manage(lender, broker.brokerID, tfLoanImpair), ter(tecNO_ENTRY));
// Loan is unimpaired, can't unimpair it again
env(manage(lender, keylet.key, tfLoanUnimpair), ter(tecNO_PERMISSION));
// Loan is unimpaired, can't jump straight to default
env(manage(lender, keylet.key, tfLoanDefault), ter(tecNO_PERMISSION));
verifyCoverAmount(0); // Impair the loan
env(manage(lender, keylet.key, tfLoanImpair));
// Unimpair the loan
env(manage(lender, keylet.key, tfLoanUnimpair));
// Fund the cover deposit env.close();
env(coverDeposit(alice, keylet.key, vault.asset(10)));
if (BEAST_EXPECT(broker = env.le(keylet)))
{
verifyCoverAmount(10);
}
// Test withdrawal failure cases // TODO: Draw and make some payments
env(coverWithdraw(alice, uint256(0), vault.asset(10)),
ter(temINVALID));
env(coverWithdraw(evan, keylet.key, vault.asset(10)),
ter(tecNO_PERMISSION));
env(coverWithdraw(evan, keylet.key, vault.asset(0)),
ter(temBAD_AMOUNT));
env(coverWithdraw(evan, keylet.key, vault.asset(-10)),
ter(temBAD_AMOUNT));
env(coverWithdraw(alice, vault.vaultID, vault.asset(10)),
ter(tecNO_ENTRY));
env(coverWithdraw(alice, keylet.key, vault.asset(900)),
ter(tecINSUFFICIENT_FUNDS));
// Withdraw some of the cover amount if (BEAST_EXPECT(toEndOfLife))
env(coverWithdraw(alice, keylet.key, vault.asset(7))); toEndOfLife(keylet, verifyStatus);
if (BEAST_EXPECT(broker = env.le(keylet)))
{
verifyCoverAmount(3);
}
// Add some more cover // Verify the loan is at EOL
env(coverDeposit(alice, keylet.key, vault.asset(5))); if (auto loan = env.le(keylet); BEAST_EXPECT(loan))
if (BEAST_EXPECT(broker = env.le(keylet))) {
{ BEAST_EXPECT(loan->at(sfPaymentRemaining) == 0);
verifyCoverAmount(8); BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == 0);
}
// Withdraw some more
env(coverWithdraw(alice, keylet.key, vault.asset(2)));
if (BEAST_EXPECT(broker = env.le(keylet)))
{
verifyCoverAmount(6);
}
env.close();
// no-op
env(set(alice, vault.vaultID), loanBrokerID(keylet.key));
// Make modifications to the broker
if (changeBroker)
changeBroker(broker);
env.close();
// Check the results of modifications
if (BEAST_EXPECT(broker = env.le(keylet)) && checkChangedBroker)
checkChangedBroker(broker);
// Verify that fields get removed when set to default values
// Debt maximum: explicit 0
// Data: explicit empty
env(set(alice, vault.vaultID),
loanBrokerID(broker->key()),
debtMaximum(Number(0)),
data(""));
// Check the updated fields
if (BEAST_EXPECT(broker = env.le(keylet)))
{
BEAST_EXPECT(!broker->isFieldPresent(sfDebtMaximum));
BEAST_EXPECT(!broker->isFieldPresent(sfData));
}
/////////////////////////////////////
// try to delete the wrong broker object
env(del(alice, vault.vaultID), ter(tecNO_ENTRY));
// evan tries to delete the broker
env(del(evan, keylet.key), ter(tecNO_PERMISSION));
// TODO: test deletion with an active loan
// Note alice's balance of the asset and the broker account's cover
// funds
auto const aliceBalance = env.balance(alice, vault.asset);
auto const coverFunds = env.balance(pseudoAccount, vault.asset);
BEAST_EXPECT(coverFunds.number() == broker->at(sfCoverAvailable));
BEAST_EXPECT(coverFunds != beast::zero);
verifyCoverAmount(6);
// delete the broker
// log << "Broker before delete: " << to_string(broker->getJson())
// << std::endl;
// if (auto const pseudo = env.le(pseudoKeylet);
// BEAST_EXPECT(pseudo))
//{
// log << "Pseudo-account before delete: "
// << to_string(pseudo->getJson()) << std::endl
// << std::endl;
//}
env(del(alice, keylet.key));
env.close();
{
broker = env.le(keylet);
BEAST_EXPECT(!broker);
auto pseudo = env.le(pseudoKeylet);
BEAST_EXPECT(!pseudo);
}
auto const expectedBalance = aliceBalance + coverFunds -
(aliceBalance.value().native()
? STAmount(env.current()->fees().base.value())
: vault.asset(0));
env.require(balance(alice, expectedBalance));
env.require(balance(pseudoAccount, None(vault.asset.raw())));
#endif
} }
} }
@@ -833,66 +782,125 @@ class Loan_test : public beast::unit_test::suite
// Finally! Create a loan // Finally! Create a loan
std::string testData; std::string testData;
auto defaultBeforeStartDate = [&](std::uint32_t baseFlag) {
return [&, baseFlag](
Keylet const& loanKeylet,
VerifyStatus const& verifyStatus) {
// toEndOfLife
//
// Default the loan
// Impair the loan
env(manage(lender, loanKeylet.key, tfLoanImpair));
// Once the loan is impaired, it can't be impaired again
env(manage(lender, loanKeylet.key, tfLoanImpair),
ter(tecNO_PERMISSION));
using d = NetClock::duration;
using tp = NetClock::time_point;
// start time = 3600s
// interval = 600s
// grace period = 60s
auto const nextDueDate = [&]() {
if (auto const loan = env.le(loanKeylet);
BEAST_EXPECT(loan))
{
auto const dueDate =
tp{d{loan->at(sfNextPaymentDueDate)}};
BEAST_EXPECT(dueDate == env.now());
return dueDate;
}
return tp{d{0}};
}();
// Can't default the loan yet. The grace period hasn't
// expired
env(manage(lender, loanKeylet.key, tfLoanDefault),
ter(tecTOO_SOON));
// Let some time pass so that the loan can be
// defaulted
env.close(nextDueDate + 60s);
// Default the loan
env(manage(lender, loanKeylet.key, tfLoanDefault));
// Once a loan is defaulted, it can't be managed
env(manage(lender, loanKeylet.key, tfLoanUnimpair),
ter(tecNO_PERMISSION));
env(manage(lender, loanKeylet.key, tfLoanImpair),
ter(tecNO_PERMISSION));
verifyStatus(
0,
nextDueDate.time_since_epoch().count(),
0,
0,
0,
baseFlag | tfLoanDefault);
};
};
// There are a lot of fields that can be set on a loan, but most of // There are a lot of fields that can be set on a loan, but most of
// them only affect the "math" when a payment is made. The only one // them only affect the "math" when a payment is made. The only one
// that really affects behavior is the `tfLoanOverpayment` flag. // that really affects behavior is the `tfLoanOverpayment` flag.
lifecycle( lifecycle(
"Loan overpayment allowed", "Loan overpayment allowed - Default before start date",
env, env,
lender, lender,
borrower, borrower,
evan, evan,
broker, broker,
tfLoanOverpayment, tfLoanOverpayment,
{}, defaultBeforeStartDate(lsfLoanOverpayment));
[&](SLE::const_ref broker) {
// Extra checks #if 0
}, lifecycle(
[&](SLE::const_ref broker) { "Loan overpayment allowed - Pay off",
// Modifications env,
}, lender,
[&](SLE::const_ref broker) { borrower,
// Check the updated fields evan,
broker,
tfLoanOverpayment,
[&](Keylet const& loanKeylet,
VerifyStatus const& verifyStatus) {
// toEndOfLife
//
// Make payments down to 0
// TODO: Try to impair a paid off loan
}); });
#endif
lifecycle(
"Loan overpayment prohibited - Default before start date",
env,
lender,
borrower,
evan,
broker,
0,
defaultBeforeStartDate(0));
#if 0 #if 0
lifecycle( lifecycle(
"non-default fields", "Loan overpayment prohibited - Pay off",
env, env,
lender, lender,
borrower,
evan, evan,
vault, broker,
[&](jtx::JTx const& jv) { 0,
testData = "spam spam spam spam"; [&](Keylet const& loanKeylet,
// Finally, create another Loan Broker with none of the VerifyStatus const& verifyStatus) {
// values at default // toEndOfLife
return env.jt( //
jv, // Make payments down to 0
data(testData),
managementFeeRate(TenthBips16(123)), // TODO: Try to impair a paid off loan
debtMaximum(Number(9)),
coverRateMinimum(TenthBips32(100)),
coverRateLiquidation(TenthBips32(200)));
},
[&](SLE::const_ref broker) {
// Extra checks
BEAST_EXPECT(broker->at(sfManagementFeeRate) == 123);
BEAST_EXPECT(broker->at(sfCoverRateMinimum) == 100);
BEAST_EXPECT(broker->at(sfCoverRateLiquidation) == 200);
BEAST_EXPECT(broker->at(sfDebtMaximum) == Number(9));
BEAST_EXPECT(checkVL(broker->at(sfData), testData));
},
[&](SLE::const_ref broker) {
// Reset Data & Debt maximum to default values
env(set(lender, broker.brokerID),
loanBrokerID(broker->key()),
data(""),
debtMaximum(Number(0)));
},
[&](SLE::const_ref broker) {
// Check the updated fields
BEAST_EXPECT(!broker->isFieldPresent(sfData));
BEAST_EXPECT(!broker->isFieldPresent(sfDebtMaximum));
}); });
#endif #endif
} }

View File

@@ -662,25 +662,27 @@ create(
namespace loanBroker { namespace loanBroker {
Json::Value Json::Value
set(AccountID const& account, uint256 const& vaultId, uint32_t flags = 0); set(AccountID const& account, uint256 const& vaultId, std::uint32_t flags = 0);
// Use "del" because "delete" is a reserved word in C++. // Use "del" because "delete" is a reserved word in C++.
Json::Value Json::Value
del(AccountID const& account, uint256 const& loanBrokerID, uint32_t flags = 0); del(AccountID const& account,
uint256 const& loanBrokerID,
std::uint32_t flags = 0);
Json::Value Json::Value
coverDeposit( coverDeposit(
AccountID const& account, AccountID const& account,
uint256 const& loanBrokerID, uint256 const& loanBrokerID,
STAmount const& amount, STAmount const& amount,
uint32_t flags = 0); std::uint32_t flags = 0);
Json::Value Json::Value
coverWithdraw( coverWithdraw(
AccountID const& account, AccountID const& account,
uint256 const& loanBrokerID, uint256 const& loanBrokerID,
STAmount const& amount, STAmount const& amount,
uint32_t flags = 0); std::uint32_t flags = 0);
auto const loanBrokerID = JTxFieldWrapper<uint256Field>(sfLoanBrokerID); auto const loanBrokerID = JTxFieldWrapper<uint256Field>(sfLoanBrokerID);
@@ -703,10 +705,10 @@ namespace loan {
Json::Value Json::Value
set(AccountID const& account, set(AccountID const& account,
uint256 loanBrokerID, uint256 const& loanBrokerID,
Number principalRequested, Number principalRequested,
NetClock::time_point const& startDate, NetClock::time_point const& startDate,
uint32_t flags = 0); std::uint32_t flags = 0);
auto const counterparty = JTxFieldWrapper<accountIDField>(sfCounterparty); auto const counterparty = JTxFieldWrapper<accountIDField>(sfCounterparty);
@@ -740,6 +742,10 @@ auto const paymentTotal = simpleField<SF_UINT32>(sfPaymentTotal);
auto const paymentInterval = simpleField<SF_UINT32>(sfPaymentInterval); auto const paymentInterval = simpleField<SF_UINT32>(sfPaymentInterval);
auto const gracePeriod = simpleField<SF_UINT32>(sfGracePeriod); auto const gracePeriod = simpleField<SF_UINT32>(sfGracePeriod);
Json::Value
manage(AccountID const& account, uint256 const& loanID, std::uint32_t flags);
} // namespace loan } // namespace loan
} // namespace jtx } // namespace jtx

View File

@@ -476,10 +476,10 @@ namespace loan {
Json::Value Json::Value
set(AccountID const& account, set(AccountID const& account,
uint256 loanBrokerID, uint256 const& loanBrokerID,
Number principalRequested, Number principalRequested,
NetClock::time_point const& startDate, NetClock::time_point const& startDate,
uint32_t flags) std::uint32_t flags)
{ {
Json::Value jv; Json::Value jv;
jv[sfTransactionType] = jss::LoanSet; jv[sfTransactionType] = jss::LoanSet;
@@ -491,6 +491,17 @@ set(AccountID const& account,
return jv; return jv;
} }
Json::Value
manage(AccountID const& account, uint256 const& loanID, std::uint32_t flags)
{
Json::Value jv;
jv[sfTransactionType] = jss::LoanManage;
jv[sfAccount] = to_string(account);
jv[sfLoanID] = to_string(loanID);
jv[sfFlags] = flags;
return jv;
}
} // namespace loan } // namespace loan
} // namespace jtx } // namespace jtx
} // namespace test } // namespace test

View File

@@ -1914,7 +1914,8 @@ ValidLoanBroker::finalize(
ReadView const& view, ReadView const& view,
beast::Journal const& j) beast::Journal const& j)
{ {
bool const enforce = view.rules().enabled(featureLendingProtocol); // Loan Brokers will not exist on ledger if the Lending Protocol amendment
// is not enabled, so there's no need to check it.
for (auto const& [before, after] : brokers_) for (auto const& [before, after] : brokers_)
{ {
@@ -1929,12 +1930,7 @@ ValidLoanBroker::finalize(
{ {
if (!goodZeroDirectory(view, dir, j)) if (!goodZeroDirectory(view, dir, j))
{ {
XRPL_ASSERT( return false;
enforce,
"ripple::ValidLoanBroker::finalize : Enforcing "
"invariant: directory");
if (enforce)
return false;
} }
} }
} }
@@ -1942,12 +1938,7 @@ ValidLoanBroker::finalize(
{ {
JLOG(j.fatal()) << "Invariant failed: Loan Broker sequence number " JLOG(j.fatal()) << "Invariant failed: Loan Broker sequence number "
"decreased"; "decreased";
XRPL_ASSERT( return false;
enforce,
"ripple::ValidLoanBroker::finalize : Enforcing "
"invariant: loan sequence");
if (enforce)
return false;
} }
} }
return true; return true;
@@ -1975,7 +1966,8 @@ ValidLoan::finalize(
ReadView const& view, ReadView const& view,
beast::Journal const& j) beast::Journal const& j)
{ {
bool const enforce = view.rules().enabled(featureLendingProtocol); // Loan Brokers will not exist on ledger if the Lending Protocol amendment
// is not enabled, so there's no need to check it.
for (auto const& [before, after] : loans_) for (auto const& [before, after] : loans_)
{ {
@@ -1984,12 +1976,15 @@ ValidLoan::finalize(
if (after->at(sfPaymentRemaining) == 0 && if (after->at(sfPaymentRemaining) == 0 &&
after->at(sfPrincipalOutstanding) != 0) after->at(sfPrincipalOutstanding) != 0)
{ {
XRPL_ASSERT( return false;
enforce, }
"ripple::ValidLoan::finalize : Enforcing " if (before &&
"invariant: zero payments remaining"); (before->isFlag(lsfLoanOverpayment) !=
if (enforce) after->isFlag(lsfLoanOverpayment)))
return false; {
JLOG(j.fatal())
<< "Invariant failed: Loan Overpayment flag changed";
return false;
} }
} }
return true; return true;

View File

@@ -100,7 +100,6 @@ LoanManage::preclaim(PreclaimContext const& ctx)
// 2. It can get worse: unimpaired -> impaired -> default // 2. It can get worse: unimpaired -> impaired -> default
// 3. It can get better: impaired -> unimpaired // 3. It can get better: impaired -> unimpaired
// 4. If it's in a state, it can't be put in that state again. // 4. If it's in a state, it can't be put in that state again.
// TODO: implement this.
if (loanSle->isFlag(lsfLoanDefault)) if (loanSle->isFlag(lsfLoanDefault))
{ {
JLOG(ctx.j.warn()) JLOG(ctx.j.warn())
@@ -113,6 +112,14 @@ LoanManage::preclaim(PreclaimContext const& ctx)
<< "Loan is impaired. A loan can not be impaired twice."; << "Loan is impaired. A loan can not be impaired twice.";
return tecNO_PERMISSION; return tecNO_PERMISSION;
} }
if (!(loanSle->isFlag(lsfLoanImpaired) ||
loanSle->isFlag(lsfLoanDefault)) &&
(tx.isFlag(tfLoanDefault) || tx.isFlag(tfLoanUnimpair)))
{
JLOG(ctx.j.warn())
<< "Loan is unimpaired. Only valid modification is to impair";
return tecNO_PERMISSION;
}
if (loanSle->at(sfPaymentRemaining) == 0) if (loanSle->at(sfPaymentRemaining) == 0)
{ {
JLOG(ctx.j.warn()) << "Loan is fully paid. A loan can not be modified " JLOG(ctx.j.warn()) << "Loan is fully paid. A loan can not be modified "
@@ -155,9 +162,63 @@ defaultLoan(
SLE::ref vaultSle, SLE::ref vaultSle,
Number const& principalOutstanding, Number const& principalOutstanding,
Number const& interestOutstanding, Number const& interestOutstanding,
Asset const& vaultAsset,
beast::Journal j) beast::Journal j)
{ {
return temDISABLED; // Calculate the amount of the Default that First-Loss Capital covers:
// The default Amount equals the outstanding principal and interest,
// excluding any funds unclaimed by the Borrower.
auto assetsAvailableProxy = loanSle->at(sfAssetsAvailable);
auto defaultAmount =
(principalOutstanding + interestOutstanding) - assetsAvailableProxy;
// Apply the First-Loss Capital to the Default Amount
auto debtTotalProxy = brokerSle->at(sfDebtTotal);
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
TenthBips32 const coverRateLiquidation{
brokerSle->at(sfCoverRateLiquidation)};
auto const defaultCovered = std::min(
tenthBipsOfValue(
tenthBipsOfValue(debtTotalProxy.value(), coverRateMinimum),
coverRateLiquidation),
defaultAmount);
defaultAmount -= defaultCovered;
// Update the Vault object:
// Decrease the Total Value of the Vault:
vaultSle->at(sfAssetsTotal) -= defaultAmount;
// Increase the Asset Available of the Vault by liquidated First-Loss
// Capital and any unclaimed funds amount:
vaultSle->at(sfAssetsAvailable) += defaultCovered + assetsAvailableProxy;
view.update(vaultSle);
// Update the LoanBroker object:
// Decrease the Debt of the LoanBroker:
debtTotalProxy -=
principalOutstanding + interestOutstanding + assetsAvailableProxy;
// Decrease the First-Loss Capital Cover Available:
brokerSle->at(sfCoverAvailable) -= defaultCovered;
view.update(brokerSle);
// Update the Loan object:
loanSle->setFlag(lsfLoanDefault);
loanSle->clearFlag(lsfLoanImpaired);
loanSle->at(sfPaymentRemaining) = 0;
assetsAvailableProxy = 0;
loanSle->at(sfPrincipalOutstanding) = 0;
view.update(loanSle);
// Move the First-Loss Capital from the LoanBroker pseudo-account to the
// Vault pseudo-account:
return accountSend(
view,
brokerSle->at(sfAccount),
vaultSle->at(sfAccount),
STAmount{vaultAsset, defaultCovered},
j,
WaiveTransferFee::Yes);
} }
TER TER
@@ -168,6 +229,7 @@ impairLoan(
SLE::ref vaultSle, SLE::ref vaultSle,
Number const& principalOutstanding, Number const& principalOutstanding,
Number const& interestOutstanding, Number const& interestOutstanding,
Asset const& vaultAsset,
beast::Journal j) beast::Journal j)
{ {
// Update the Vault object(set "paper loss") // Update the Vault object(set "paper loss")
@@ -176,7 +238,7 @@ impairLoan(
view.update(vaultSle); view.update(vaultSle);
// Update the Loan object // Update the Loan object
loanSle->at(sfFlags) = lsfLoanImpaired; loanSle->setFlag(lsfLoanImpaired);
auto nextDueProxy = loanSle->at(sfNextPaymentDueDate); auto nextDueProxy = loanSle->at(sfNextPaymentDueDate);
if (!hasExpired(view, nextDueProxy)) if (!hasExpired(view, nextDueProxy))
{ {
@@ -197,6 +259,7 @@ unimpairLoan(
SLE::ref vaultSle, SLE::ref vaultSle,
Number const& principalOutstanding, Number const& principalOutstanding,
Number const& interestOutstanding, Number const& interestOutstanding,
Asset const& vaultAsset,
beast::Journal j) beast::Journal j)
{ {
// Update the Vault object(clear "paper loss") // Update the Vault object(clear "paper loss")
@@ -205,7 +268,7 @@ unimpairLoan(
view.update(vaultSle); view.update(vaultSle);
// Update the Loan object // Update the Loan object
loanSle->at(sfFlags) = 0; loanSle->clearFlag(lsfLoanImpaired);
auto const paymentInterval = loanSle->at(sfPaymentInterval); auto const paymentInterval = loanSle->at(sfPaymentInterval);
auto const normalPaymentDueDate = auto const normalPaymentDueDate =
loanSle->at(sfPreviousPaymentDate) + paymentInterval; loanSle->at(sfPreviousPaymentDate) + paymentInterval;
@@ -250,9 +313,9 @@ LoanManage::doApply()
TenthBips32 const interestRate{loanSle->at(sfInterestRate)}; TenthBips32 const interestRate{loanSle->at(sfInterestRate)};
auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding); auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding);
auto const interestOutstanding = auto const interestOutstanding =
tenthBipsOfValue(principalOutstanding, interestRate); tenthBipsOfValue(principalOutstanding.value(), interestRate);
// Valid flag combinations are checked in preflight. Not flags is valid - // Valid flag combinations are checked in preflight. No flags is valid -
// just a noop. // just a noop.
if (tx.isFlag(tfLoanDefault)) if (tx.isFlag(tfLoanDefault))
{ {
@@ -263,6 +326,7 @@ LoanManage::doApply()
vaultSle, vaultSle,
principalOutstanding, principalOutstanding,
interestOutstanding, interestOutstanding,
vaultAsset,
j_)) j_))
return ter; return ter;
} }
@@ -275,6 +339,7 @@ LoanManage::doApply()
vaultSle, vaultSle,
principalOutstanding, principalOutstanding,
interestOutstanding, interestOutstanding,
vaultAsset,
j_)) j_))
return ter; return ter;
} }
@@ -287,23 +352,10 @@ LoanManage::doApply()
vaultSle, vaultSle,
principalOutstanding, principalOutstanding,
interestOutstanding, interestOutstanding,
vaultAsset,
j_)) j_))
return ter; return ter;
} }
/*
auto const brokerOwner = brokerSle->at(sfOwner);
auto const brokerOwnerSle = view.peek(keylet::account(brokerOwner));
auto const vaultPseudo = vaultSle->at(sfAccount);
auto const brokerPseudo = brokerSle->at(sfAccount);
auto const brokerPseudoSle = view.peek(keylet::account(brokerPseudo));
auto const principalRequested = tx[sfPrincipalRequested];
TenthBips32 const interestRate{tx[~sfInterestRate].value_or(0)};
auto const originationFee = tx[~sfLoanOriginationFee];
auto const loanAssetsAvailable =
principalRequested - originationFee.value_or(Number{});
*/
return tesSUCCESS; return tesSUCCESS;
} }