mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-02 17:06:00 +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
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user