mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-29 07:25:51 +00:00
Finish LoanManage functionality and tests, modulo LoanDraw/Pay
This commit is contained in:
@@ -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
|
||||
lifecycle(
|
||||
const char* label,
|
||||
@@ -121,10 +162,11 @@ class Loan_test : public beast::unit_test::suite
|
||||
jtx::Account const& evan,
|
||||
BrokerInfo const& broker,
|
||||
std::uint32_t flags,
|
||||
std::function<jtx::JTx(jtx::JTx const&)> modifyJTx,
|
||||
std::function<void(SLE::const_ref)> checkLoan,
|
||||
std::function<void(SLE::const_ref)> changeLoan,
|
||||
std::function<void(SLE::const_ref)> checkChangedLoan)
|
||||
// The end of life callback is expected to take the loan to 0 payments
|
||||
// remaining, one way or another
|
||||
std::function<
|
||||
void(Keylet const& loanKeylet, VerifyStatus const& verifyStatus)>
|
||||
toEndOfLife)
|
||||
{
|
||||
auto const [keylet, loanSequence] = [&]() {
|
||||
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(
|
||||
keylet::loan(broker.brokerID, loanSequence), loanSequence);
|
||||
}();
|
||||
|
||||
VerifyStatus const verifyStatus(env, broker, keylet);
|
||||
|
||||
if (!BEAST_EXPECT(loanSequence != 0))
|
||||
return;
|
||||
{
|
||||
@@ -189,14 +234,14 @@ class Loan_test : public beast::unit_test::suite
|
||||
paymentInterval(interval),
|
||||
gracePeriod(grace),
|
||||
fee(loanSetFee));
|
||||
// Modify as desired
|
||||
if (modifyJTx)
|
||||
createJtx = modifyJTx(createJtx);
|
||||
// Successfully create a Loan
|
||||
env(createJtx);
|
||||
|
||||
env.close();
|
||||
|
||||
auto const loanFlags =
|
||||
createJtx.stx->isFlag(tfLoanOverpayment) ? lsfLoanOverpayment : 0;
|
||||
|
||||
if (auto loan = env.le(keylet); BEAST_EXPECT(loan))
|
||||
{
|
||||
// log << "loan after create: " << to_string(loan->getJson())
|
||||
@@ -230,155 +275,59 @@ class Loan_test : public beast::unit_test::suite
|
||||
loan->at(sfAssetsAvailable) ==
|
||||
principalRequest - originationFee);
|
||||
BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == principalRequest);
|
||||
if (checkLoan)
|
||||
checkLoan(loan);
|
||||
}
|
||||
|
||||
#if 0
|
||||
auto verifyStatus = [&env, &broker, &loan, this](
|
||||
auto previousPaymentDate,
|
||||
auto nextPaymentDate,
|
||||
auto paymentRemaining,
|
||||
auto assetsAvailable,
|
||||
auto principalOutstanding) {
|
||||
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());
|
||||
};
|
||||
verifyStatus(
|
||||
0,
|
||||
startDate.time_since_epoch().count() + interval,
|
||||
total,
|
||||
principalRequest - originationFee,
|
||||
principalRequest,
|
||||
loanFlags | 0);
|
||||
|
||||
// Test Cover funding before allowing alterations
|
||||
env(coverDeposit(alice, uint256(0), vault.asset(10)),
|
||||
ter(temINVALID));
|
||||
env(coverDeposit(evan, keylet.key, vault.asset(10)),
|
||||
ter(tecNO_PERMISSION));
|
||||
env(coverDeposit(evan, keylet.key, vault.asset(0)),
|
||||
ter(temBAD_AMOUNT));
|
||||
env(coverDeposit(evan, keylet.key, vault.asset(-10)),
|
||||
ter(temBAD_AMOUNT));
|
||||
env(coverDeposit(alice, vault.vaultID, vault.asset(10)),
|
||||
ter(tecNO_ENTRY));
|
||||
// Manage the loan
|
||||
// no-op
|
||||
env(manage(lender, keylet.key, 0));
|
||||
// Only the lender can manage
|
||||
env(manage(evan, keylet.key, 0), ter(tecNO_PERMISSION));
|
||||
// unknown flags
|
||||
env(manage(lender, keylet.key, tfLoanManageMask), ter(temINVALID_FLAG));
|
||||
// combinations of flags are not allowed
|
||||
env(manage(lender, keylet.key, tfLoanUnimpair | tfLoanImpair),
|
||||
ter(temINVALID_FLAG));
|
||||
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(coverDeposit(alice, keylet.key, vault.asset(10)));
|
||||
if (BEAST_EXPECT(broker = env.le(keylet)))
|
||||
{
|
||||
verifyCoverAmount(10);
|
||||
}
|
||||
env.close();
|
||||
|
||||
// Test withdrawal failure cases
|
||||
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));
|
||||
// TODO: Draw and make some payments
|
||||
|
||||
// Withdraw some of the cover amount
|
||||
env(coverWithdraw(alice, keylet.key, vault.asset(7)));
|
||||
if (BEAST_EXPECT(broker = env.le(keylet)))
|
||||
{
|
||||
verifyCoverAmount(3);
|
||||
}
|
||||
if (BEAST_EXPECT(toEndOfLife))
|
||||
toEndOfLife(keylet, verifyStatus);
|
||||
|
||||
// Add some more cover
|
||||
env(coverDeposit(alice, keylet.key, vault.asset(5)));
|
||||
if (BEAST_EXPECT(broker = env.le(keylet)))
|
||||
{
|
||||
verifyCoverAmount(8);
|
||||
}
|
||||
|
||||
// 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
|
||||
// Verify the loan is at EOL
|
||||
if (auto loan = env.le(keylet); BEAST_EXPECT(loan))
|
||||
{
|
||||
BEAST_EXPECT(loan->at(sfPaymentRemaining) == 0);
|
||||
BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -833,66 +782,125 @@ class Loan_test : public beast::unit_test::suite
|
||||
|
||||
// Finally! Create a loan
|
||||
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
|
||||
// them only affect the "math" when a payment is made. The only one
|
||||
// that really affects behavior is the `tfLoanOverpayment` flag.
|
||||
lifecycle(
|
||||
"Loan overpayment allowed",
|
||||
"Loan overpayment allowed - Default before start date",
|
||||
env,
|
||||
lender,
|
||||
borrower,
|
||||
evan,
|
||||
broker,
|
||||
tfLoanOverpayment,
|
||||
{},
|
||||
[&](SLE::const_ref broker) {
|
||||
// Extra checks
|
||||
},
|
||||
[&](SLE::const_ref broker) {
|
||||
// Modifications
|
||||
},
|
||||
[&](SLE::const_ref broker) {
|
||||
// Check the updated fields
|
||||
defaultBeforeStartDate(lsfLoanOverpayment));
|
||||
|
||||
#if 0
|
||||
lifecycle(
|
||||
"Loan overpayment allowed - Pay off",
|
||||
env,
|
||||
lender,
|
||||
borrower,
|
||||
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
|
||||
lifecycle(
|
||||
"non-default fields",
|
||||
"Loan overpayment prohibited - Pay off",
|
||||
env,
|
||||
lender,
|
||||
borrower,
|
||||
evan,
|
||||
vault,
|
||||
[&](jtx::JTx const& jv) {
|
||||
testData = "spam spam spam spam";
|
||||
// Finally, create another Loan Broker with none of the
|
||||
// values at default
|
||||
return env.jt(
|
||||
jv,
|
||||
data(testData),
|
||||
managementFeeRate(TenthBips16(123)),
|
||||
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));
|
||||
broker,
|
||||
0,
|
||||
[&](Keylet const& loanKeylet,
|
||||
VerifyStatus const& verifyStatus) {
|
||||
// toEndOfLife
|
||||
//
|
||||
// Make payments down to 0
|
||||
|
||||
// TODO: Try to impair a paid off loan
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -662,25 +662,27 @@ create(
|
||||
namespace loanBroker {
|
||||
|
||||
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++.
|
||||
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
|
||||
coverDeposit(
|
||||
AccountID const& account,
|
||||
uint256 const& loanBrokerID,
|
||||
STAmount const& amount,
|
||||
uint32_t flags = 0);
|
||||
std::uint32_t flags = 0);
|
||||
|
||||
Json::Value
|
||||
coverWithdraw(
|
||||
AccountID const& account,
|
||||
uint256 const& loanBrokerID,
|
||||
STAmount const& amount,
|
||||
uint32_t flags = 0);
|
||||
std::uint32_t flags = 0);
|
||||
|
||||
auto const loanBrokerID = JTxFieldWrapper<uint256Field>(sfLoanBrokerID);
|
||||
|
||||
@@ -703,10 +705,10 @@ namespace loan {
|
||||
|
||||
Json::Value
|
||||
set(AccountID const& account,
|
||||
uint256 loanBrokerID,
|
||||
uint256 const& loanBrokerID,
|
||||
Number principalRequested,
|
||||
NetClock::time_point const& startDate,
|
||||
uint32_t flags = 0);
|
||||
std::uint32_t flags = 0);
|
||||
|
||||
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 gracePeriod = simpleField<SF_UINT32>(sfGracePeriod);
|
||||
|
||||
Json::Value
|
||||
manage(AccountID const& account, uint256 const& loanID, std::uint32_t flags);
|
||||
|
||||
} // namespace loan
|
||||
|
||||
} // namespace jtx
|
||||
|
||||
@@ -476,10 +476,10 @@ namespace loan {
|
||||
|
||||
Json::Value
|
||||
set(AccountID const& account,
|
||||
uint256 loanBrokerID,
|
||||
uint256 const& loanBrokerID,
|
||||
Number principalRequested,
|
||||
NetClock::time_point const& startDate,
|
||||
uint32_t flags)
|
||||
std::uint32_t flags)
|
||||
{
|
||||
Json::Value jv;
|
||||
jv[sfTransactionType] = jss::LoanSet;
|
||||
@@ -491,6 +491,17 @@ set(AccountID const& account,
|
||||
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 jtx
|
||||
} // namespace test
|
||||
|
||||
@@ -1914,7 +1914,8 @@ ValidLoanBroker::finalize(
|
||||
ReadView const& view,
|
||||
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_)
|
||||
{
|
||||
@@ -1929,12 +1930,7 @@ ValidLoanBroker::finalize(
|
||||
{
|
||||
if (!goodZeroDirectory(view, dir, j))
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
enforce,
|
||||
"ripple::ValidLoanBroker::finalize : Enforcing "
|
||||
"invariant: directory");
|
||||
if (enforce)
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1942,12 +1938,7 @@ ValidLoanBroker::finalize(
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: Loan Broker sequence number "
|
||||
"decreased";
|
||||
XRPL_ASSERT(
|
||||
enforce,
|
||||
"ripple::ValidLoanBroker::finalize : Enforcing "
|
||||
"invariant: loan sequence");
|
||||
if (enforce)
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -1975,7 +1966,8 @@ ValidLoan::finalize(
|
||||
ReadView const& view,
|
||||
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_)
|
||||
{
|
||||
@@ -1984,12 +1976,15 @@ ValidLoan::finalize(
|
||||
if (after->at(sfPaymentRemaining) == 0 &&
|
||||
after->at(sfPrincipalOutstanding) != 0)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
enforce,
|
||||
"ripple::ValidLoan::finalize : Enforcing "
|
||||
"invariant: zero payments remaining");
|
||||
if (enforce)
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
if (before &&
|
||||
(before->isFlag(lsfLoanOverpayment) !=
|
||||
after->isFlag(lsfLoanOverpayment)))
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: Loan Overpayment flag changed";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -100,7 +100,6 @@ LoanManage::preclaim(PreclaimContext const& ctx)
|
||||
// 2. It can get worse: unimpaired -> impaired -> default
|
||||
// 3. It can get better: impaired -> unimpaired
|
||||
// 4. If it's in a state, it can't be put in that state again.
|
||||
// TODO: implement this.
|
||||
if (loanSle->isFlag(lsfLoanDefault))
|
||||
{
|
||||
JLOG(ctx.j.warn())
|
||||
@@ -113,6 +112,14 @@ LoanManage::preclaim(PreclaimContext const& ctx)
|
||||
<< "Loan is impaired. A loan can not be impaired twice.";
|
||||
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)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Loan is fully paid. A loan can not be modified "
|
||||
@@ -155,9 +162,63 @@ defaultLoan(
|
||||
SLE::ref vaultSle,
|
||||
Number const& principalOutstanding,
|
||||
Number const& interestOutstanding,
|
||||
Asset const& vaultAsset,
|
||||
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
|
||||
@@ -168,6 +229,7 @@ impairLoan(
|
||||
SLE::ref vaultSle,
|
||||
Number const& principalOutstanding,
|
||||
Number const& interestOutstanding,
|
||||
Asset const& vaultAsset,
|
||||
beast::Journal j)
|
||||
{
|
||||
// Update the Vault object(set "paper loss")
|
||||
@@ -176,7 +238,7 @@ impairLoan(
|
||||
view.update(vaultSle);
|
||||
|
||||
// Update the Loan object
|
||||
loanSle->at(sfFlags) = lsfLoanImpaired;
|
||||
loanSle->setFlag(lsfLoanImpaired);
|
||||
auto nextDueProxy = loanSle->at(sfNextPaymentDueDate);
|
||||
if (!hasExpired(view, nextDueProxy))
|
||||
{
|
||||
@@ -197,6 +259,7 @@ unimpairLoan(
|
||||
SLE::ref vaultSle,
|
||||
Number const& principalOutstanding,
|
||||
Number const& interestOutstanding,
|
||||
Asset const& vaultAsset,
|
||||
beast::Journal j)
|
||||
{
|
||||
// Update the Vault object(clear "paper loss")
|
||||
@@ -205,7 +268,7 @@ unimpairLoan(
|
||||
view.update(vaultSle);
|
||||
|
||||
// Update the Loan object
|
||||
loanSle->at(sfFlags) = 0;
|
||||
loanSle->clearFlag(lsfLoanImpaired);
|
||||
auto const paymentInterval = loanSle->at(sfPaymentInterval);
|
||||
auto const normalPaymentDueDate =
|
||||
loanSle->at(sfPreviousPaymentDate) + paymentInterval;
|
||||
@@ -250,9 +313,9 @@ LoanManage::doApply()
|
||||
TenthBips32 const interestRate{loanSle->at(sfInterestRate)};
|
||||
auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding);
|
||||
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.
|
||||
if (tx.isFlag(tfLoanDefault))
|
||||
{
|
||||
@@ -263,6 +326,7 @@ LoanManage::doApply()
|
||||
vaultSle,
|
||||
principalOutstanding,
|
||||
interestOutstanding,
|
||||
vaultAsset,
|
||||
j_))
|
||||
return ter;
|
||||
}
|
||||
@@ -275,6 +339,7 @@ LoanManage::doApply()
|
||||
vaultSle,
|
||||
principalOutstanding,
|
||||
interestOutstanding,
|
||||
vaultAsset,
|
||||
j_))
|
||||
return ter;
|
||||
}
|
||||
@@ -287,23 +352,10 @@ LoanManage::doApply()
|
||||
vaultSle,
|
||||
principalOutstanding,
|
||||
interestOutstanding,
|
||||
vaultAsset,
|
||||
j_))
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user