mirror of
https://github.com/XRPLF/rippled.git
synced 2026-04-29 15:37:57 +00:00
Update InvariantCheck, LoanManage, and LoanPay to make tests work
- Primarily updating tests, and fixing stuff that didn't work. - Tests still not expected to pass. - Add Json::Value::isMember(StaticString) so SFields can be used. - Validate more fields in ValidLoan Invariant
This commit is contained in:
@@ -395,6 +395,9 @@ public:
|
||||
/// Return true if the object has a member named key.
|
||||
bool
|
||||
isMember(std::string const& key) const;
|
||||
/// Return true if the object has a member named key.
|
||||
bool
|
||||
isMember(StaticString const& key) const;
|
||||
|
||||
/// \brief Return a list of the member names.
|
||||
///
|
||||
|
||||
@@ -573,12 +573,13 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({
|
||||
// represents interest owed to the vault.
|
||||
// There are two additional values that can be computed from these:
|
||||
// - InterestOutstanding: TotalValueOutstanding - PrincipalOutstanding
|
||||
// This is the total amount of interest still pending on the loan,
|
||||
// The total amount of interest still pending on the loan,
|
||||
// independent of management fees.
|
||||
// - ManagementFeeOwed: InterestOutstanding - InterestOwed
|
||||
// This is the amount of the total interest that will be sent to the
|
||||
// The amount of the total interest that will be sent to the
|
||||
// broker as management fees.
|
||||
{sfPrincipalOutstanding, soeDEFAULT},
|
||||
{sfReferencePrincipal, soeDEFAULT},
|
||||
{sfTotalValueOutstanding, soeDEFAULT},
|
||||
{sfInterestOwed, soeDEFAULT},
|
||||
// Based on the original principal borrowed, used for
|
||||
|
||||
@@ -1001,6 +1001,12 @@ Value::isMember(std::string const& key) const
|
||||
return isMember(key.c_str());
|
||||
}
|
||||
|
||||
bool
|
||||
Value::isMember(StaticString const& key) const
|
||||
{
|
||||
return isMember(key.c_str());
|
||||
}
|
||||
|
||||
Value::Members
|
||||
Value::getMemberNames() const
|
||||
{
|
||||
|
||||
@@ -44,6 +44,7 @@ class Loan_test : public beast::unit_test::suite
|
||||
static constexpr auto const maxCoveredLoanValue = 1000 * 100 / 10;
|
||||
static constexpr auto const vaultDeposit = 1'000'000;
|
||||
static constexpr auto const debtMaximumParameter = 25'000;
|
||||
static constexpr TenthBips16 const managementFeeRateParameter{100};
|
||||
std::string const iouCurrency{"IOU"};
|
||||
|
||||
void
|
||||
@@ -112,7 +113,11 @@ class Loan_test : public beast::unit_test::suite
|
||||
std::uint32_t nextPaymentDate = 0;
|
||||
std::uint32_t paymentRemaining = 0;
|
||||
std::int32_t const loanScale = 0;
|
||||
Number totalValue = 0;
|
||||
Number principalOutstanding = 0;
|
||||
Number referencePrincipal = 0;
|
||||
Number interestOwed = 0;
|
||||
Number periodicPayment = 0;
|
||||
std::uint32_t flags = 0;
|
||||
std::uint32_t paymentInterval = 0;
|
||||
TenthBips32 const interestRate{};
|
||||
@@ -148,36 +153,22 @@ class Loan_test : public beast::unit_test::suite
|
||||
*/
|
||||
void
|
||||
checkBroker(
|
||||
Number const& principalRequested,
|
||||
Number const& principalOutstanding,
|
||||
Number const& interestOwed,
|
||||
TenthBips32 interestRate,
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t paymentsRemaining,
|
||||
std::uint32_t ownerCount) const
|
||||
{
|
||||
#if LOANCOMPLETE
|
||||
using namespace jtx;
|
||||
if (auto brokerSle = env.le(keylet::loanbroker(broker.brokerID));
|
||||
env.test.BEAST_EXPECT(brokerSle))
|
||||
{
|
||||
TenthBips16 const managementFeeRate{
|
||||
brokerSle->at(sfManagementFeeRate)};
|
||||
auto const loanInterest = loanInterestOutstandingMinusFee(
|
||||
broker.asset,
|
||||
principalRequested,
|
||||
principalOutstanding,
|
||||
interestRate,
|
||||
paymentInterval,
|
||||
paymentsRemaining,
|
||||
managementFeeRate);
|
||||
auto const brokerDebt = brokerSle->at(sfDebtTotal);
|
||||
auto const expectedDebt = principalOutstanding + loanInterest;
|
||||
env.test.BEAST_EXPECT(
|
||||
// Allow some slop for rounding
|
||||
brokerDebt == expectedDebt ||
|
||||
(expectedDebt != Number(0) &&
|
||||
((brokerDebt - expectedDebt) / expectedDebt <
|
||||
Number(1, -8))));
|
||||
auto const expectedDebt = principalOutstanding + interestOwed;
|
||||
env.test.BEAST_EXPECT(brokerDebt == expectedDebt);
|
||||
env.test.BEAST_EXPECT(
|
||||
env.balance(pseudoAccount, broker.asset).number() ==
|
||||
brokerSle->at(sfCoverAvailable));
|
||||
@@ -213,7 +204,6 @@ class Loan_test : public beast::unit_test::suite
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/** Checks both the loan and broker expect states against the ledger */
|
||||
@@ -223,7 +213,11 @@ class Loan_test : public beast::unit_test::suite
|
||||
std::uint32_t nextPaymentDate,
|
||||
std::uint32_t paymentRemaining,
|
||||
Number const& loanScale,
|
||||
Number const& totalValue,
|
||||
Number const& principalOutstanding,
|
||||
Number const& referencePrincipal,
|
||||
Number const& interestOwed,
|
||||
Number const& periodicPayment,
|
||||
std::uint32_t flags) const
|
||||
{
|
||||
using namespace jtx;
|
||||
@@ -236,18 +230,22 @@ class Loan_test : public beast::unit_test::suite
|
||||
env.test.BEAST_EXPECT(
|
||||
loan->at(sfPaymentRemaining) == paymentRemaining);
|
||||
env.test.BEAST_EXPECT(loan->at(sfLoanScale) == loanScale);
|
||||
env.test.BEAST_EXPECT(
|
||||
loan->at(sfTotalValueOutstanding) == totalValue);
|
||||
env.test.BEAST_EXPECT(
|
||||
loan->at(sfPrincipalOutstanding) == principalOutstanding);
|
||||
env.test.BEAST_EXPECT(
|
||||
loan->at(sfLoanScale) ==
|
||||
broker.asset(loanAmount).value().exponent());
|
||||
loan->at(sfReferencePrincipal) == referencePrincipal);
|
||||
env.test.BEAST_EXPECT(loan->at(sfInterestOwed) == interestOwed);
|
||||
env.test.BEAST_EXPECT(
|
||||
loan->at(sfPeriodicPayment) == periodicPayment);
|
||||
env.test.BEAST_EXPECT(loan->at(sfFlags) == flags);
|
||||
|
||||
auto const interestRate = TenthBips32{loan->at(sfInterestRate)};
|
||||
auto const paymentInterval = loan->at(sfPaymentInterval);
|
||||
checkBroker(
|
||||
loanScale,
|
||||
principalOutstanding,
|
||||
interestOwed,
|
||||
interestRate,
|
||||
paymentInterval,
|
||||
paymentRemaining,
|
||||
@@ -261,30 +259,18 @@ class Loan_test : public beast::unit_test::suite
|
||||
env.le(keylet::vault(brokerSle->at(sfVaultID)));
|
||||
env.test.BEAST_EXPECT(vaultSle))
|
||||
{
|
||||
#if LOANCOMPLETE
|
||||
if ((flags & lsfLoanImpaired) &&
|
||||
!(flags & lsfLoanDefault))
|
||||
{
|
||||
TenthBips32 const managementFeeRate{
|
||||
brokerSle->at(sfManagementFeeRate)};
|
||||
env.test.BEAST_EXPECT(
|
||||
vaultSle->at(sfLossUnrealized) ==
|
||||
principalOutstanding +
|
||||
loanInterestOutstandingMinusFee(
|
||||
broker.asset,
|
||||
loanScale,
|
||||
principalOutstanding,
|
||||
interestRate,
|
||||
paymentInterval,
|
||||
paymentRemaining,
|
||||
managementFeeRate));
|
||||
principalOutstanding + interestOwed);
|
||||
}
|
||||
else
|
||||
{
|
||||
env.test.BEAST_EXPECT(
|
||||
vaultSle->at(sfLossUnrealized) == 0);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -299,7 +285,11 @@ class Loan_test : public beast::unit_test::suite
|
||||
state.nextPaymentDate,
|
||||
state.paymentRemaining,
|
||||
state.loanScale,
|
||||
state.totalValue,
|
||||
state.principalOutstanding,
|
||||
state.referencePrincipal,
|
||||
state.interestOwed,
|
||||
state.periodicPayment,
|
||||
state.flags);
|
||||
};
|
||||
};
|
||||
@@ -342,7 +332,7 @@ class Loan_test : public beast::unit_test::suite
|
||||
using namespace loanBroker;
|
||||
env(set(lender, vaultKeylet.key),
|
||||
data(testData),
|
||||
managementFeeRate(TenthBips16(100)),
|
||||
managementFeeRate(managementFeeRateParameter),
|
||||
debtMaximum(debtMaximumValue),
|
||||
coverRateMinimum(TenthBips32(coverRateMinParameter)),
|
||||
coverRateLiquidation(TenthBips32(coverRateLiquidationParameter)));
|
||||
@@ -373,7 +363,11 @@ class Loan_test : public beast::unit_test::suite
|
||||
.nextPaymentDate = loan->at(sfNextPaymentDueDate),
|
||||
.paymentRemaining = loan->at(sfPaymentRemaining),
|
||||
.loanScale = loan->at(sfLoanScale),
|
||||
.totalValue = loan->at(sfTotalValueOutstanding),
|
||||
.principalOutstanding = loan->at(sfPrincipalOutstanding),
|
||||
.referencePrincipal = loan->at(sfReferencePrincipal),
|
||||
.interestOwed = loan->at(sfInterestOwed),
|
||||
.periodicPayment = loan->at(sfPeriodicPayment),
|
||||
.flags = loan->at(sfFlags),
|
||||
.paymentInterval = loan->at(sfPaymentInterval),
|
||||
.interestRate = TenthBips32{loan->at(sfInterestRate)},
|
||||
@@ -402,7 +396,6 @@ class Loan_test : public beast::unit_test::suite
|
||||
BrokerInfo const& broker,
|
||||
LoanState const& state)
|
||||
{
|
||||
#if LOANCOMPLETE
|
||||
if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
|
||||
BEAST_EXPECT(brokerSle))
|
||||
{
|
||||
@@ -413,17 +406,8 @@ class Loan_test : public beast::unit_test::suite
|
||||
// log << vaultSle->getJson() << std::endl;
|
||||
auto const assetsUnavailable = vaultSle->at(sfAssetsTotal) -
|
||||
vaultSle->at(sfAssetsAvailable);
|
||||
auto const interestOutstanding =
|
||||
loanInterestOutstandingMinusFee(
|
||||
broker.asset,
|
||||
state.loanScale,
|
||||
state.principalOutstanding,
|
||||
state.interestRate,
|
||||
state.paymentInterval,
|
||||
state.paymentRemaining,
|
||||
TenthBips32{brokerSle->at(sfManagementFeeRate)});
|
||||
auto const unrealizedLoss = vaultSle->at(sfLossUnrealized) +
|
||||
state.principalOutstanding + interestOutstanding;
|
||||
state.principalOutstanding + state.interestOwed;
|
||||
|
||||
if (unrealizedLoss > assetsUnavailable)
|
||||
{
|
||||
@@ -431,7 +415,6 @@ class Loan_test : public beast::unit_test::suite
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -485,8 +468,7 @@ class Loan_test : public beast::unit_test::suite
|
||||
env, broker, loanAmount, pseudoAcct, keylet);
|
||||
|
||||
// No loans yet
|
||||
verifyLoanStatus.checkBroker(
|
||||
broker.asset(loanAmount).value(), 0, TenthBips32{0}, 1, 0, 0);
|
||||
verifyLoanStatus.checkBroker(0, 0, TenthBips32{0}, 1, 0, 0);
|
||||
|
||||
if (!BEAST_EXPECT(loanSequence != 0))
|
||||
return;
|
||||
@@ -622,12 +604,25 @@ class Loan_test : public beast::unit_test::suite
|
||||
|
||||
auto state = getCurrentState(env, broker, keylet, verifyLoanStatus);
|
||||
|
||||
auto const loanProperties = computeLoanProperties(
|
||||
broker.asset,
|
||||
state.principalOutstanding,
|
||||
state.referencePrincipal,
|
||||
state.interestRate,
|
||||
state.paymentInterval,
|
||||
state.paymentRemaining,
|
||||
managementFeeRateParameter);
|
||||
|
||||
verifyLoanStatus(
|
||||
0,
|
||||
startDate + interval,
|
||||
total,
|
||||
principalRequest.exponent(),
|
||||
loanProperties.totalValueOutstanding,
|
||||
principalRequest,
|
||||
principalRequest,
|
||||
loanProperties.interestOwedToVault,
|
||||
loanProperties.periodicPayment,
|
||||
loanFlags | 0);
|
||||
|
||||
// Manage the loan
|
||||
@@ -680,8 +675,12 @@ class Loan_test : public beast::unit_test::suite
|
||||
0,
|
||||
nextDueDate,
|
||||
total,
|
||||
principalRequest.exponent(),
|
||||
loanProperties.totalValueOutstanding,
|
||||
principalRequest,
|
||||
principalRequest,
|
||||
loanProperties.interestOwedToVault,
|
||||
loanProperties.periodicPayment,
|
||||
loanFlags | 0);
|
||||
|
||||
// Can't delete the loan yet. It has payments remaining.
|
||||
@@ -730,8 +729,7 @@ class Loan_test : public beast::unit_test::suite
|
||||
}
|
||||
|
||||
// No loans left
|
||||
verifyLoanStatus.checkBroker(
|
||||
broker.asset(1000).value(), 0, interest, 1, 0, 0);
|
||||
verifyLoanStatus.checkBroker(0, 0, interest, 1, 0, 0);
|
||||
|
||||
BEAST_EXPECT(
|
||||
env.balance(borrower, broker.asset).value() ==
|
||||
@@ -763,7 +761,6 @@ class Loan_test : public beast::unit_test::suite
|
||||
Number const& loanAmount,
|
||||
int interestExponent)
|
||||
{
|
||||
#if LOANCOMPLETE
|
||||
using namespace jtx;
|
||||
|
||||
auto const& asset = broker.asset.raw();
|
||||
@@ -1165,15 +1162,6 @@ class Loan_test : public beast::unit_test::suite
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
state.loanScale == state.principalOutstanding.exponent());
|
||||
auto const interestOutstanding =
|
||||
loanInterestOutstandingMinusFee(
|
||||
broker.asset,
|
||||
state.loanScale,
|
||||
state.principalOutstanding,
|
||||
state.interestRate,
|
||||
state.paymentInterval,
|
||||
state.paymentRemaining,
|
||||
TenthBips32{brokerSle->at(sfManagementFeeRate)});
|
||||
auto const defaultAmount = roundToAsset(
|
||||
broker.asset,
|
||||
std::min(
|
||||
@@ -1182,8 +1170,8 @@ class Loan_test : public beast::unit_test::suite
|
||||
brokerSle->at(sfDebtTotal),
|
||||
coverRateMinParameter),
|
||||
coverRateLiquidationParameter),
|
||||
state.principalOutstanding + interestOutstanding),
|
||||
state.principalRequested);
|
||||
state.principalOutstanding + state.interestOwed),
|
||||
state.loanScale);
|
||||
return std::make_pair(defaultAmount, brokerSle->at(sfOwner));
|
||||
}
|
||||
return std::make_pair(Number{}, AccountID{});
|
||||
@@ -1271,7 +1259,10 @@ class Loan_test : public beast::unit_test::suite
|
||||
|
||||
state.flags |= tfLoanDefault;
|
||||
state.paymentRemaining = 0;
|
||||
state.totalValue = 0;
|
||||
state.principalOutstanding = 0;
|
||||
state.referencePrincipal = 0;
|
||||
state.interestOwed = 0;
|
||||
verifyLoanStatus(state);
|
||||
|
||||
// Once a loan is defaulted, it can't be managed
|
||||
@@ -1279,6 +1270,10 @@ class Loan_test : public beast::unit_test::suite
|
||||
ter(tecNO_PERMISSION));
|
||||
env(manage(lender, loanKeylet.key, tfLoanImpair),
|
||||
ter(tecNO_PERMISSION));
|
||||
// Can't make a payment on it either
|
||||
env(pay(borrower, loanKeylet.key, broker.asset(300)),
|
||||
ter(tecKILLED));
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1291,8 +1286,6 @@ class Loan_test : public beast::unit_test::suite
|
||||
auto state =
|
||||
getCurrentState(env, broker, loanKeylet, verifyLoanStatus);
|
||||
BEAST_EXPECT(state.flags == baseFlag);
|
||||
STAmount const drawAmount =
|
||||
STAmount(broker.asset, state.principalRequested - 1);
|
||||
env.close(state.startDate + 20s);
|
||||
auto const loanAge = (env.now() - state.startDate).count();
|
||||
BEAST_EXPECT(loanAge == 30);
|
||||
@@ -1360,7 +1353,7 @@ class Loan_test : public beast::unit_test::suite
|
||||
BEAST_EXPECT(
|
||||
payoffAmount ==
|
||||
broker.asset(Number(1040000114155251, -12)));
|
||||
BEAST_EXPECT(payoffAmount > drawAmount);
|
||||
BEAST_EXPECT(payoffAmount > state.principalOutstanding);
|
||||
// Try to pay a little extra to show that it's _not_
|
||||
// taken
|
||||
auto const transactionAmount = payoffAmount + broker.asset(10);
|
||||
@@ -1384,18 +1377,12 @@ class Loan_test : public beast::unit_test::suite
|
||||
|
||||
STAmount const balanceChangeAmount{
|
||||
broker.asset,
|
||||
roundToAsset(
|
||||
broker.asset,
|
||||
payoffAmount,
|
||||
borrowerBalanceBeforePayment.number())};
|
||||
roundToAsset(broker.asset, payoffAmount, state.loanScale)};
|
||||
{
|
||||
auto const difference = roundToReference(
|
||||
auto const difference =
|
||||
env.balance(borrower, broker.asset) -
|
||||
(borrowerBalanceBeforePayment -
|
||||
balanceChangeAmount - adjustment),
|
||||
STAmount{
|
||||
broker.asset,
|
||||
borrowerBalanceBeforePayment.value() * 10});
|
||||
(borrowerBalanceBeforePayment - balanceChangeAmount -
|
||||
adjustment);
|
||||
BEAST_EXPECT(difference == beast::zero);
|
||||
}
|
||||
|
||||
@@ -1467,63 +1454,6 @@ class Loan_test : public beast::unit_test::suite
|
||||
0,
|
||||
defaultImmediately(0, false));
|
||||
|
||||
lifecycle(
|
||||
caseLabel,
|
||||
"Loan overpayment allowed - Draw then default",
|
||||
env,
|
||||
loanAmount,
|
||||
interestExponent,
|
||||
lender,
|
||||
borrower,
|
||||
evan,
|
||||
broker,
|
||||
pseudoAcct,
|
||||
tfLoanOverpayment,
|
||||
[&](Keylet const& loanKeylet,
|
||||
VerifyLoanStatus const& verifyLoanStatus) {
|
||||
// toEndOfLife
|
||||
//
|
||||
// Initialize values with the current state
|
||||
auto state =
|
||||
getCurrentState(env, broker, loanKeylet, verifyLoanStatus);
|
||||
BEAST_EXPECT(state.flags == lsfLoanOverpayment);
|
||||
|
||||
auto const& broker = verifyLoanStatus.broker;
|
||||
auto const startingCoverAvailable = coverAvailable(
|
||||
broker.brokerID,
|
||||
broker.asset(coverDepositParameter).number());
|
||||
|
||||
// move past the due date + grace period (60s)
|
||||
env.close(tp{d{state.nextPaymentDate}} + 60s + 20s);
|
||||
|
||||
auto const [amountToBeCovered, brokerAcct] =
|
||||
getDefaultInfo(state, broker);
|
||||
|
||||
// default the loan
|
||||
env(manage(lender, loanKeylet.key, tfLoanDefault));
|
||||
env.close();
|
||||
|
||||
// The LoanBroker just lost some of it's first-loss capital.
|
||||
// Replenish it.
|
||||
replenishCover(
|
||||
broker,
|
||||
brokerAcct,
|
||||
startingCoverAvailable,
|
||||
amountToBeCovered);
|
||||
|
||||
state.paymentRemaining = 0;
|
||||
state.principalOutstanding = 0;
|
||||
state.flags |= tfLoanDefault;
|
||||
|
||||
verifyLoanStatus(state);
|
||||
|
||||
// Can't make a payment on it either
|
||||
env(pay(borrower, loanKeylet.key, broker.asset(300)),
|
||||
ter(tecKILLED));
|
||||
|
||||
// Default
|
||||
});
|
||||
|
||||
lifecycle(
|
||||
caseLabel,
|
||||
"Loan overpayment prohibited - Pay off immediately",
|
||||
@@ -1594,52 +1524,36 @@ class Loan_test : public beast::unit_test::suite
|
||||
BEAST_EXPECT(
|
||||
periodicRate ==
|
||||
Number(2283105022831050, -21, Number::unchecked{}));
|
||||
STAmount const roundedPeriodicPayment{
|
||||
broker.asset,
|
||||
roundPeriodicPayment(
|
||||
broker.asset, state.periodicPayment, state.loanScale)};
|
||||
|
||||
while (state.paymentRemaining > 0)
|
||||
{
|
||||
STAmount const principalRequestedAmount{
|
||||
broker.asset, state.principalRequested};
|
||||
// Compute the payment based on the number of
|
||||
// payments remaining
|
||||
auto const rateFactor =
|
||||
power(1 + periodicRate, state.paymentRemaining);
|
||||
Number const rawPeriodicPayment =
|
||||
state.principalOutstanding * periodicRate * rateFactor /
|
||||
(rateFactor - 1);
|
||||
STAmount const periodicPayment{
|
||||
broker.asset,
|
||||
roundToAsset(
|
||||
broker.asset,
|
||||
rawPeriodicPayment,
|
||||
principalRequestedAmount,
|
||||
Number::upward)};
|
||||
|
||||
testcase
|
||||
<< "Payments remaining: " << state.paymentRemaining
|
||||
<< ", computed payment amount: " << periodicPayment;
|
||||
testcase << "Payments remaining: " << state.paymentRemaining
|
||||
<< ", computed payment amount: "
|
||||
<< state.periodicPayment;
|
||||
|
||||
// Only check the first payment since the rounding
|
||||
// may drift as payments are made
|
||||
BEAST_EXPECT(
|
||||
state.paymentRemaining < 12 ||
|
||||
STAmount(broker.asset, rawPeriodicPayment) ==
|
||||
broker.asset(Number(8333457001162141, -14)));
|
||||
roundedPeriodicPayment ==
|
||||
broker.asset(Number(8333457001162141, -14)));
|
||||
// Include the service fee
|
||||
STAmount const totalDue = roundToReference(
|
||||
periodicPayment + broker.asset(2),
|
||||
principalRequestedAmount,
|
||||
STAmount const totalDue = roundToScale(
|
||||
roundedPeriodicPayment + broker.asset(2),
|
||||
state.loanScale,
|
||||
Number::upward);
|
||||
// Only check the first payment since the rounding
|
||||
// may drift as payments are made
|
||||
BEAST_EXPECT(
|
||||
state.paymentRemaining < 12 ||
|
||||
totalDue ==
|
||||
roundToReference(
|
||||
broker.asset(
|
||||
Number(8533457001162141, -14),
|
||||
Number::upward),
|
||||
principalRequestedAmount,
|
||||
Number::upward));
|
||||
roundToScale(
|
||||
broker.asset(
|
||||
Number(8533457001162141, -14), Number::upward),
|
||||
state.loanScale,
|
||||
Number::upward));
|
||||
|
||||
// Try to pay a little extra to show that it's _not_
|
||||
// taken
|
||||
@@ -1648,53 +1562,52 @@ class Loan_test : public beast::unit_test::suite
|
||||
// Only check the first payment since the rounding
|
||||
// may drift as payments are made
|
||||
BEAST_EXPECT(
|
||||
state.paymentRemaining < 12 ||
|
||||
transactionAmount ==
|
||||
roundToReference(
|
||||
broker.asset(
|
||||
Number(9533457001162141, -14),
|
||||
Number::upward),
|
||||
principalRequestedAmount,
|
||||
Number::upward));
|
||||
roundToScale(
|
||||
broker.asset(
|
||||
Number(9533457001162141, -14), Number::upward),
|
||||
state.loanScale,
|
||||
Number::upward));
|
||||
|
||||
auto const totalDueAmount =
|
||||
STAmount{broker.asset, totalDue};
|
||||
|
||||
// Compute the expected principal amount
|
||||
Number const rawInterest = state.paymentRemaining == 1
|
||||
? rawPeriodicPayment - state.principalOutstanding
|
||||
: state.principalOutstanding * periodicRate;
|
||||
? state.periodicPayment - state.referencePrincipal
|
||||
: state.referencePrincipal * periodicRate;
|
||||
STAmount const interest{
|
||||
broker.asset,
|
||||
roundToAsset(
|
||||
broker.asset,
|
||||
rawInterest,
|
||||
principalRequestedAmount,
|
||||
state.loanScale,
|
||||
Number::upward)};
|
||||
BEAST_EXPECT(
|
||||
state.paymentRemaining < 12 ||
|
||||
interest ==
|
||||
roundToReference(
|
||||
roundToScale(
|
||||
broker.asset(
|
||||
Number(2283105022831050, -18),
|
||||
Number::upward),
|
||||
principalRequestedAmount,
|
||||
state.loanScale,
|
||||
Number::upward));
|
||||
BEAST_EXPECT(interest >= Number(0));
|
||||
|
||||
auto const rawPrincipal = rawPeriodicPayment - rawInterest;
|
||||
auto const rawPrincipal =
|
||||
state.periodicPayment - rawInterest;
|
||||
BEAST_EXPECT(
|
||||
state.paymentRemaining < 12 ||
|
||||
roundToAsset(
|
||||
broker.asset,
|
||||
rawPrincipal,
|
||||
principalRequestedAmount,
|
||||
state.loanScale,
|
||||
Number::upward) ==
|
||||
roundToReference(
|
||||
roundToScale(
|
||||
broker.asset(
|
||||
Number(8333228690659858, -14),
|
||||
Number::upward),
|
||||
principalRequestedAmount,
|
||||
state.loanScale,
|
||||
Number::upward));
|
||||
BEAST_EXPECT(
|
||||
state.paymentRemaining > 1 ||
|
||||
@@ -1703,8 +1616,8 @@ class Loan_test : public beast::unit_test::suite
|
||||
broker.asset,
|
||||
roundToAsset(
|
||||
broker.asset,
|
||||
periodicPayment - interest,
|
||||
principalRequestedAmount,
|
||||
roundedPeriodicPayment - interest,
|
||||
state.loanScale,
|
||||
Number::downward)};
|
||||
BEAST_EXPECT(
|
||||
principal > Number(0) &&
|
||||
@@ -1713,8 +1626,9 @@ class Loan_test : public beast::unit_test::suite
|
||||
state.paymentRemaining > 1 ||
|
||||
principal == state.principalOutstanding);
|
||||
BEAST_EXPECT(
|
||||
rawPrincipal + rawInterest == rawPeriodicPayment);
|
||||
BEAST_EXPECT(principal + interest == periodicPayment);
|
||||
rawPrincipal + rawInterest == state.periodicPayment);
|
||||
BEAST_EXPECT(
|
||||
principal + interest == roundedPeriodicPayment);
|
||||
|
||||
auto const borrowerBalanceBeforePayment =
|
||||
env.balance(borrower, broker.asset);
|
||||
@@ -1768,7 +1682,6 @@ class Loan_test : public beast::unit_test::suite
|
||||
env(manage(lender, loanKeylet.key, tfLoanDefault),
|
||||
ter(tecNO_PERMISSION));
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1979,26 +1892,31 @@ class Loan_test : public beast::unit_test::suite
|
||||
|
||||
auto const loan = objects[0u];
|
||||
BEAST_EXPECT(loan[sfBorrower] == lender.human());
|
||||
BEAST_EXPECT(loan[sfCloseInterestRate] == 0);
|
||||
BEAST_EXPECT(loan[sfClosePaymentFee] == "0");
|
||||
// soeDEFAULT fields are not returned if they're in the default
|
||||
// state
|
||||
BEAST_EXPECT(!loan.isMember(sfCloseInterestRate));
|
||||
BEAST_EXPECT(!loan.isMember(sfClosePaymentFee));
|
||||
BEAST_EXPECT(loan[sfFlags] == 0);
|
||||
BEAST_EXPECT(loan[sfGracePeriod] == 60);
|
||||
BEAST_EXPECT(loan[sfInterestRate] == 0);
|
||||
BEAST_EXPECT(loan[sfLateInterestRate] == 0);
|
||||
BEAST_EXPECT(loan[sfLatePaymentFee] == "0");
|
||||
BEAST_EXPECT(!loan.isMember(sfInterestRate));
|
||||
BEAST_EXPECT(!loan.isMember(sfLateInterestRate));
|
||||
BEAST_EXPECT(!loan.isMember(sfLatePaymentFee));
|
||||
BEAST_EXPECT(loan[sfLoanBrokerID] == to_string(broker.brokerID));
|
||||
BEAST_EXPECT(loan[sfLoanOriginationFee] == "0");
|
||||
BEAST_EXPECT(!loan.isMember(sfLoanOriginationFee));
|
||||
BEAST_EXPECT(loan[sfLoanSequence] == 1);
|
||||
BEAST_EXPECT(loan[sfLoanServiceFee] == "0");
|
||||
BEAST_EXPECT(!loan.isMember(sfLoanServiceFee));
|
||||
BEAST_EXPECT(
|
||||
loan[sfNextPaymentDueDate] == loan[sfStartDate].asUInt() + 60);
|
||||
BEAST_EXPECT(loan[sfOverpaymentFee] == 0);
|
||||
BEAST_EXPECT(loan[sfOverpaymentInterestRate] == 0);
|
||||
BEAST_EXPECT(!loan.isMember(sfOverpaymentFee));
|
||||
BEAST_EXPECT(!loan.isMember(sfOverpaymentInterestRate));
|
||||
BEAST_EXPECT(loan[sfPaymentInterval] == 60);
|
||||
BEAST_EXPECT(loan[sfPeriodicPayment] == "1000000000");
|
||||
BEAST_EXPECT(loan[sfPaymentRemaining] == 1);
|
||||
BEAST_EXPECT(loan[sfPreviousPaymentDate] == 0);
|
||||
BEAST_EXPECT(!loan.isMember(sfPreviousPaymentDate));
|
||||
BEAST_EXPECT(loan[sfPrincipalOutstanding] == "1000000000");
|
||||
BEAST_EXPECT(loan[sfLoanScale] == 0);
|
||||
BEAST_EXPECT(loan[sfReferencePrincipal] == "1000000000");
|
||||
BEAST_EXPECT(loan[sfTotalValueOutstanding] == "1000000000");
|
||||
BEAST_EXPECT(loan[sfLoanScale] == -6);
|
||||
BEAST_EXPECT(
|
||||
loan[sfStartDate].asUInt() ==
|
||||
startDate.time_since_epoch().count());
|
||||
|
||||
@@ -2479,17 +2479,54 @@ ValidLoan::finalize(
|
||||
<< "Invariant failed: Loan Overpayment flag changed";
|
||||
return false;
|
||||
}
|
||||
if (after->at(sfAssetsAvailable) < 0)
|
||||
// Must not be negative - STNumber
|
||||
for (auto const field :
|
||||
{&sfLoanServiceFee,
|
||||
&sfLatePaymentFee,
|
||||
&sfClosePaymentFee,
|
||||
&sfPrincipalOutstanding,
|
||||
&sfReferencePrincipal,
|
||||
&sfTotalValueOutstanding,
|
||||
&sfInterestOwed})
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: Loan assets available is negative";
|
||||
return false;
|
||||
if (after->at(*field) < 0)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: " << field->getName()
|
||||
<< " is negative ";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (after->at(sfPrincipalOutstanding) < 0)
|
||||
// Must not be negative - STUInt32
|
||||
for (auto const field :
|
||||
{&sfOverpaymentFee,
|
||||
&sfInterestRate,
|
||||
&sfLateInterestRate,
|
||||
&sfCloseInterestRate,
|
||||
&sfOverpaymentInterestRate,
|
||||
&sfStartDate,
|
||||
&sfPaymentInterval,
|
||||
&sfGracePeriod,
|
||||
&sfPreviousPaymentDate,
|
||||
&sfPaymentRemaining})
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: Loan principal outstanding is negative";
|
||||
return false;
|
||||
if (after->at(*field) < 0)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: " << field->getName()
|
||||
<< " is negative ";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Must be positive
|
||||
for (auto const field : {
|
||||
&sfPeriodicPayment,
|
||||
})
|
||||
{
|
||||
if (after->at(*field) <= 0)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: " << field->getName()
|
||||
<< " is zero or negative ";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -145,10 +145,13 @@ LoanManage::defaultLoan(
|
||||
// Calculate the amount of the Default that First-Loss Capital covers:
|
||||
|
||||
std::int32_t const loanScale = loanSle->at(sfLoanScale);
|
||||
TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
|
||||
auto brokerDebtTotalProxy = brokerSle->at(sfDebtTotal);
|
||||
|
||||
Number const totalDefaultAmount = loanSle->at(sfTotalValueOutstanding);
|
||||
auto principalOutstandingProxy = loanSle->at(sfPrincipalOutstanding);
|
||||
auto interestOwedProxy = loanSle->at(sfInterestOwed);
|
||||
|
||||
Number const totalDefaultAmount =
|
||||
principalOutstandingProxy + interestOwedProxy;
|
||||
|
||||
// Apply the First-Loss Capital to the Default Amount
|
||||
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
|
||||
@@ -225,16 +228,14 @@ LoanManage::defaultLoan(
|
||||
// The loss has been realized
|
||||
if (loanSle->isFlag(lsfLoanImpaired))
|
||||
{
|
||||
Number const lossRealized = loanSle->at(sfPrincipalOutstanding) +
|
||||
loanSle->at(sfInterestOwed);
|
||||
auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
|
||||
if (vaultLossUnrealizedProxy < lossRealized)
|
||||
if (vaultLossUnrealizedProxy < totalDefaultAmount)
|
||||
{
|
||||
JLOG(j.warn())
|
||||
<< "Vault unrealized loss is less than the default amount";
|
||||
return tefBAD_LEDGER;
|
||||
}
|
||||
vaultLossUnrealizedProxy -= lossRealized;
|
||||
vaultLossUnrealizedProxy -= totalDefaultAmount;
|
||||
}
|
||||
view.update(vaultSle);
|
||||
}
|
||||
@@ -268,8 +269,11 @@ LoanManage::defaultLoan(
|
||||
|
||||
// Update the Loan object:
|
||||
loanSle->setFlag(lsfLoanDefault);
|
||||
loanSle->at(sfTotalValueOutstanding) = 0;
|
||||
loanSle->at(sfPaymentRemaining) = 0;
|
||||
loanSle->at(sfPrincipalOutstanding) = 0;
|
||||
loanSle->at(sfReferencePrincipal) = 0;
|
||||
principalOutstandingProxy = 0;
|
||||
interestOwedProxy = 0;
|
||||
view.update(loanSle);
|
||||
|
||||
// Return funds from the LoanBroker pseudo-account to the
|
||||
|
||||
@@ -373,10 +373,19 @@ LoanPay::doApply()
|
||||
#if !NDEBUG
|
||||
auto const accountBalanceBefore =
|
||||
accountHolds(view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
|
||||
auto const vaultBalanceBefore = accountHolds(
|
||||
view, vaultPseudoAccount, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
|
||||
auto const brokerBalanceBefore = accountHolds(
|
||||
view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
|
||||
auto const vaultBalanceBefore = account_ == vaultPseudoAccount
|
||||
? STAmount{asset, 0}
|
||||
: accountHolds(
|
||||
view,
|
||||
vaultPseudoAccount,
|
||||
asset,
|
||||
fhIGNORE_FREEZE,
|
||||
ahIGNORE_AUTH,
|
||||
j_);
|
||||
auto const brokerBalanceBefore = account_ == brokerPayee
|
||||
? STAmount{asset, 0}
|
||||
: accountHolds(
|
||||
view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
|
||||
#endif
|
||||
|
||||
if (auto const ter = accountSend(
|
||||
@@ -399,10 +408,19 @@ LoanPay::doApply()
|
||||
#if !NDEBUG
|
||||
auto const accountBalanceAfter =
|
||||
accountHolds(view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
|
||||
auto const vaultBalanceAfter = accountHolds(
|
||||
view, vaultPseudoAccount, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
|
||||
auto const brokerBalanceAfter = accountHolds(
|
||||
view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
|
||||
auto const vaultBalanceAfter = account_ == vaultPseudoAccount
|
||||
? STAmount{asset, 0}
|
||||
: accountHolds(
|
||||
view,
|
||||
vaultPseudoAccount,
|
||||
asset,
|
||||
fhIGNORE_FREEZE,
|
||||
ahIGNORE_AUTH,
|
||||
j_);
|
||||
auto const brokerBalanceAfter = account_ == brokerPayee
|
||||
? STAmount{asset, 0}
|
||||
: accountHolds(
|
||||
view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
|
||||
|
||||
auto const balanceScale = std::max(
|
||||
{accountBalanceBefore.exponent(),
|
||||
|
||||
Reference in New Issue
Block a user