mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
Merge branch 'ximinez/lending-XLS-66-ongoing' into ximinez/lending-number-simple
This commit is contained in:
642
src/test/app/LendingHelpers_test.cpp
Normal file
642
src/test/app/LendingHelpers_test.cpp
Normal file
@@ -0,0 +1,642 @@
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
// DO NOT REMOVE
|
||||
#include <test/jtx.h>
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/mpt.h>
|
||||
|
||||
#include <xrpld/app/misc/LendingHelpers.h>
|
||||
#include <xrpld/app/misc/LoadFeeTrack.h>
|
||||
#include <xrpld/app/tx/detail/Batch.h>
|
||||
#include <xrpld/app/tx/detail/LoanSet.h>
|
||||
|
||||
#include <xrpl/beast/xor_shift_engine.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class LendingHelpers_test : public beast::unit_test::suite
|
||||
{
|
||||
void
|
||||
testComputeRaisedRate()
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace ripple::detail;
|
||||
struct TestCase
|
||||
{
|
||||
std::string name;
|
||||
Number periodicRate;
|
||||
std::uint32_t paymentsRemaining;
|
||||
Number expectedRaisedRate;
|
||||
};
|
||||
|
||||
auto const testCases = std::vector<TestCase>{
|
||||
{
|
||||
.name = "Zero payments remaining",
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 0,
|
||||
.expectedRaisedRate = Number{1}, // (1 + r)^0 = 1
|
||||
},
|
||||
{
|
||||
.name = "One payment remaining",
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 1,
|
||||
.expectedRaisedRate = Number{105, -2},
|
||||
}, // 1.05^1
|
||||
{
|
||||
.name = "Multiple payments remaining",
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 3,
|
||||
.expectedRaisedRate = Number{1157625, -6},
|
||||
}, // 1.05^3
|
||||
{
|
||||
.name = "Zero periodic rate",
|
||||
.periodicRate = Number{0},
|
||||
.paymentsRemaining = 5,
|
||||
.expectedRaisedRate = Number{1}, // (1 + 0)^5 = 1
|
||||
}};
|
||||
|
||||
for (auto const& tc : testCases)
|
||||
{
|
||||
testcase("computeRaisedRate: " + tc.name);
|
||||
|
||||
auto const computedRaisedRate =
|
||||
computeRaisedRate(tc.periodicRate, tc.paymentsRemaining);
|
||||
BEAST_EXPECTS(
|
||||
computedRaisedRate == tc.expectedRaisedRate,
|
||||
"Raised rate mismatch: expected " +
|
||||
to_string(tc.expectedRaisedRate) + ", got " +
|
||||
to_string(computedRaisedRate));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testComputePaymentFactor()
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace ripple::detail;
|
||||
struct TestCase
|
||||
{
|
||||
std::string name;
|
||||
Number periodicRate;
|
||||
std::uint32_t paymentsRemaining;
|
||||
Number expectedPaymentFactor;
|
||||
};
|
||||
|
||||
auto const testCases = std::vector<TestCase>{
|
||||
{
|
||||
.name = "Zero periodic rate",
|
||||
.periodicRate = Number{0},
|
||||
.paymentsRemaining = 4,
|
||||
.expectedPaymentFactor = Number{25, -2},
|
||||
}, // 1/4 = 0.25
|
||||
{
|
||||
.name = "One payment remaining",
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 1,
|
||||
.expectedPaymentFactor = Number{105, -2},
|
||||
}, // 0.05/1 = 1.05
|
||||
{
|
||||
.name = "Multiple payments remaining",
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 3,
|
||||
.expectedPaymentFactor = Number{367208564631245, -15},
|
||||
}, // from calc
|
||||
{
|
||||
.name = "Zero payments remaining",
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 0,
|
||||
.expectedPaymentFactor = Number{0},
|
||||
} // edge case
|
||||
};
|
||||
|
||||
for (auto const& tc : testCases)
|
||||
{
|
||||
testcase("computePaymentFactor: " + tc.name);
|
||||
|
||||
auto const computedPaymentFactor =
|
||||
computePaymentFactor(tc.periodicRate, tc.paymentsRemaining);
|
||||
BEAST_EXPECTS(
|
||||
computedPaymentFactor == tc.expectedPaymentFactor,
|
||||
"Payment factor mismatch: expected " +
|
||||
to_string(tc.expectedPaymentFactor) + ", got " +
|
||||
to_string(computedPaymentFactor));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testLoanPeriodicPayment()
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace ripple::detail;
|
||||
|
||||
struct TestCase
|
||||
{
|
||||
std::string name;
|
||||
Number principalOutstanding;
|
||||
Number periodicRate;
|
||||
std::uint32_t paymentsRemaining;
|
||||
Number expectedPeriodicPayment;
|
||||
};
|
||||
|
||||
auto const testCases = std::vector<TestCase>{
|
||||
{
|
||||
.name = "Zero principal outstanding",
|
||||
.principalOutstanding = Number{0},
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 5,
|
||||
.expectedPeriodicPayment = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Zero payments remaining",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 0,
|
||||
.expectedPeriodicPayment = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Zero periodic rate",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.periodicRate = Number{0},
|
||||
.paymentsRemaining = 4,
|
||||
.expectedPeriodicPayment = Number{250},
|
||||
},
|
||||
{
|
||||
.name = "Standard case",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.periodicRate =
|
||||
loanPeriodicRate(TenthBips32(100'000), 30 * 24 * 60 * 60),
|
||||
.paymentsRemaining = 3,
|
||||
.expectedPeriodicPayment =
|
||||
Number{3895690663961231, -13}, // from calc
|
||||
},
|
||||
};
|
||||
|
||||
for (auto const& tc : testCases)
|
||||
{
|
||||
testcase("loanPeriodicPayment: " + tc.name);
|
||||
|
||||
auto const computedPeriodicPayment = loanPeriodicPayment(
|
||||
tc.principalOutstanding, tc.periodicRate, tc.paymentsRemaining);
|
||||
BEAST_EXPECTS(
|
||||
computedPeriodicPayment == tc.expectedPeriodicPayment,
|
||||
"Periodic payment mismatch: expected " +
|
||||
to_string(tc.expectedPeriodicPayment) + ", got " +
|
||||
to_string(computedPeriodicPayment));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testLoanPrincipalFromPeriodicPayment()
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace ripple::detail;
|
||||
|
||||
struct TestCase
|
||||
{
|
||||
std::string name;
|
||||
Number periodicPayment;
|
||||
Number periodicRate;
|
||||
std::uint32_t paymentsRemaining;
|
||||
Number expectedPrincipalOutstanding;
|
||||
};
|
||||
|
||||
auto const testCases = std::vector<TestCase>{
|
||||
{
|
||||
.name = "Zero periodic payment",
|
||||
.periodicPayment = Number{0},
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 5,
|
||||
.expectedPrincipalOutstanding = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Zero payments remaining",
|
||||
.periodicPayment = Number{1'000},
|
||||
.periodicRate = Number{5, -2},
|
||||
.paymentsRemaining = 0,
|
||||
.expectedPrincipalOutstanding = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Zero periodic rate",
|
||||
.periodicPayment = Number{250},
|
||||
.periodicRate = Number{0},
|
||||
.paymentsRemaining = 4,
|
||||
.expectedPrincipalOutstanding = Number{1'000},
|
||||
},
|
||||
{
|
||||
.name = "Standard case",
|
||||
.periodicPayment = Number{3895690663961231, -13}, // from calc
|
||||
.periodicRate =
|
||||
loanPeriodicRate(TenthBips32(100'000), 30 * 24 * 60 * 60),
|
||||
.paymentsRemaining = 3,
|
||||
.expectedPrincipalOutstanding = Number{1'000},
|
||||
},
|
||||
};
|
||||
|
||||
for (auto const& tc : testCases)
|
||||
{
|
||||
testcase("loanPrincipalFromPeriodicPayment: " + tc.name);
|
||||
|
||||
auto const computedPrincipalOutstanding =
|
||||
loanPrincipalFromPeriodicPayment(
|
||||
tc.periodicPayment, tc.periodicRate, tc.paymentsRemaining);
|
||||
BEAST_EXPECTS(
|
||||
computedPrincipalOutstanding == tc.expectedPrincipalOutstanding,
|
||||
"Principal outstanding mismatch: expected " +
|
||||
to_string(tc.expectedPrincipalOutstanding) + ", got " +
|
||||
to_string(computedPrincipalOutstanding));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testComputeOverpaymentComponents()
|
||||
{
|
||||
testcase("computeOverpaymentComponents");
|
||||
using namespace jtx;
|
||||
using namespace ripple::detail;
|
||||
|
||||
Account const issuer{"issuer"};
|
||||
PrettyAsset const IOU = issuer["IOU"];
|
||||
int32_t const loanScale = 1;
|
||||
auto const overpayment = Number{1'000};
|
||||
auto const overpaymentInterestRate = TenthBips32{10'000}; // 10%
|
||||
auto const overpaymentFeeRate = TenthBips32{50'000}; // 50%
|
||||
auto const managementFeeRate = TenthBips16{10'000}; // 10%
|
||||
|
||||
auto const expectedOverpaymentFee = Number{500}; // 50% of 1,000
|
||||
auto const expectedOverpaymentInterestGross =
|
||||
Number{100}; // 10% of 1,000
|
||||
auto const expectedOverpaymentInterestNet =
|
||||
Number{90}; // 100 - 10% of 100
|
||||
auto const expectedOverpaymentManagementFee = Number{10}; // 10% of 100
|
||||
auto const expectedPrincipalPortion = Number{400}; // 1,000 - 100 - 500
|
||||
|
||||
auto const components = detail::computeOverpaymentComponents(
|
||||
IOU,
|
||||
loanScale,
|
||||
overpayment,
|
||||
overpaymentInterestRate,
|
||||
overpaymentFeeRate,
|
||||
managementFeeRate);
|
||||
|
||||
BEAST_EXPECT(
|
||||
components.untrackedManagementFee == expectedOverpaymentFee);
|
||||
|
||||
BEAST_EXPECT(
|
||||
components.untrackedInterest == expectedOverpaymentInterestNet);
|
||||
BEAST_EXPECT(
|
||||
components.trackedManagementFeeDelta ==
|
||||
expectedOverpaymentManagementFee);
|
||||
BEAST_EXPECT(
|
||||
components.trackedPrincipalDelta == expectedPrincipalPortion);
|
||||
BEAST_EXPECT(
|
||||
components.trackedManagementFeeDelta +
|
||||
components.untrackedInterest ==
|
||||
expectedOverpaymentInterestGross);
|
||||
|
||||
BEAST_EXPECT(
|
||||
components.trackedManagementFeeDelta +
|
||||
components.untrackedInterest +
|
||||
components.trackedPrincipalDelta +
|
||||
components.untrackedManagementFee ==
|
||||
overpayment);
|
||||
}
|
||||
|
||||
void
|
||||
testComputeInterestAndFeeParts()
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace ripple::detail;
|
||||
|
||||
struct TestCase
|
||||
{
|
||||
std::string name;
|
||||
Number interest;
|
||||
TenthBips16 managementFeeRate;
|
||||
Number expectedInterestPart;
|
||||
Number expectedFeePart;
|
||||
};
|
||||
|
||||
Account const issuer{"issuer"};
|
||||
PrettyAsset const IOU = issuer["IOU"];
|
||||
std::int32_t const loanScale = 1;
|
||||
|
||||
auto const testCases = std::vector<TestCase>{
|
||||
{.name = "Zero interest",
|
||||
.interest = Number{0},
|
||||
.managementFeeRate = TenthBips16{10'000},
|
||||
.expectedInterestPart = Number{0},
|
||||
.expectedFeePart = Number{0}},
|
||||
{.name = "Zero fee rate",
|
||||
.interest = Number{1'000},
|
||||
.managementFeeRate = TenthBips16{0},
|
||||
.expectedInterestPart = Number{1'000},
|
||||
.expectedFeePart = Number{0}},
|
||||
{.name = "10% fee rate",
|
||||
.interest = Number{1'000},
|
||||
.managementFeeRate = TenthBips16{10'000},
|
||||
.expectedInterestPart = Number{900},
|
||||
.expectedFeePart = Number{100}},
|
||||
};
|
||||
|
||||
for (auto const& tc : testCases)
|
||||
{
|
||||
testcase("computeInterestAndFeeParts: " + tc.name);
|
||||
|
||||
auto const [computedInterestPart, computedFeePart] =
|
||||
computeInterestAndFeeParts(
|
||||
IOU, tc.interest, tc.managementFeeRate, loanScale);
|
||||
BEAST_EXPECTS(
|
||||
computedInterestPart == tc.expectedInterestPart,
|
||||
"Interest part mismatch: expected " +
|
||||
to_string(tc.expectedInterestPart) + ", got " +
|
||||
to_string(computedInterestPart));
|
||||
BEAST_EXPECTS(
|
||||
computedFeePart == tc.expectedFeePart,
|
||||
"Fee part mismatch: expected " + to_string(tc.expectedFeePart) +
|
||||
", got " + to_string(computedFeePart));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testLoanLatePaymentInterest()
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace ripple::detail;
|
||||
struct TestCase
|
||||
{
|
||||
std::string name;
|
||||
Number principalOutstanding;
|
||||
TenthBips32 lateInterestRate;
|
||||
NetClock::time_point parentCloseTime;
|
||||
std::uint32_t nextPaymentDueDate;
|
||||
Number expectedLateInterest;
|
||||
};
|
||||
|
||||
auto const testCases = std::vector<TestCase>{
|
||||
{
|
||||
.name = "On-time payment",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.lateInterestRate = TenthBips32{10'000}, // 10%
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.nextPaymentDueDate = 3'000,
|
||||
.expectedLateInterest = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Early payment",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.lateInterestRate = TenthBips32{10'000}, // 10%
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.nextPaymentDueDate = 4'000,
|
||||
.expectedLateInterest = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "No principal outstanding",
|
||||
.principalOutstanding = Number{0},
|
||||
.lateInterestRate = TenthBips32{10'000}, // 10%
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.nextPaymentDueDate = 2'000,
|
||||
.expectedLateInterest = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "No late interest rate",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.lateInterestRate = TenthBips32{0}, // 0%
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.nextPaymentDueDate = 2'000,
|
||||
.expectedLateInterest = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Late payment",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.lateInterestRate = TenthBips32{100'000}, // 100%
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.nextPaymentDueDate = 2'000,
|
||||
.expectedLateInterest =
|
||||
Number{3170979198376459, -17}, // from calc
|
||||
},
|
||||
};
|
||||
|
||||
for (auto const& tc : testCases)
|
||||
{
|
||||
testcase("loanLatePaymentInterest: " + tc.name);
|
||||
|
||||
auto const computedLateInterest = loanLatePaymentInterest(
|
||||
tc.principalOutstanding,
|
||||
tc.lateInterestRate,
|
||||
tc.parentCloseTime,
|
||||
tc.nextPaymentDueDate);
|
||||
BEAST_EXPECTS(
|
||||
computedLateInterest == tc.expectedLateInterest,
|
||||
"Late interest mismatch: expected " +
|
||||
to_string(tc.expectedLateInterest) + ", got " +
|
||||
to_string(computedLateInterest));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testLoanAccruedInterest()
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace ripple::detail;
|
||||
struct TestCase
|
||||
{
|
||||
std::string name;
|
||||
Number principalOutstanding;
|
||||
Number periodicRate;
|
||||
NetClock::time_point parentCloseTime;
|
||||
std::uint32_t startDate;
|
||||
std::uint32_t prevPaymentDate;
|
||||
std::uint32_t paymentInterval;
|
||||
Number expectedAccruedInterest;
|
||||
};
|
||||
|
||||
auto const testCases = std::vector<TestCase>{
|
||||
{
|
||||
.name = "Zero principal outstanding",
|
||||
.principalOutstanding = Number{0},
|
||||
.periodicRate = Number{5, -2},
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.startDate = 2'000,
|
||||
.prevPaymentDate = 2'500,
|
||||
.paymentInterval = 30 * 24 * 60 * 60,
|
||||
.expectedAccruedInterest = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Before start date",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.periodicRate = Number{5, -2},
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{1'000}},
|
||||
.startDate = 2'000,
|
||||
.prevPaymentDate = 1'500,
|
||||
.paymentInterval = 30 * 24 * 60 * 60,
|
||||
.expectedAccruedInterest = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Zero periodic rate",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.periodicRate = Number{0},
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.startDate = 2'000,
|
||||
.prevPaymentDate = 2'500,
|
||||
.paymentInterval = 30 * 24 * 60 * 60,
|
||||
.expectedAccruedInterest = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Zero payment interval",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.periodicRate = Number{5, -2},
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.startDate = 2'000,
|
||||
.prevPaymentDate = 2'500,
|
||||
.paymentInterval = 0,
|
||||
.expectedAccruedInterest = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Standard case",
|
||||
.principalOutstanding = Number{1'000},
|
||||
.periodicRate = Number{5, -2},
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.startDate = 1'000,
|
||||
.prevPaymentDate = 2'000,
|
||||
.paymentInterval = 30 * 24 * 60 * 60,
|
||||
.expectedAccruedInterest =
|
||||
Number{1929012345679012, -17}, // from calc
|
||||
},
|
||||
};
|
||||
|
||||
for (auto const& tc : testCases)
|
||||
{
|
||||
testcase("loanAccruedInterest: " + tc.name);
|
||||
|
||||
auto const computedAccruedInterest = loanAccruedInterest(
|
||||
tc.principalOutstanding,
|
||||
tc.periodicRate,
|
||||
tc.parentCloseTime,
|
||||
tc.startDate,
|
||||
tc.prevPaymentDate,
|
||||
tc.paymentInterval);
|
||||
BEAST_EXPECTS(
|
||||
computedAccruedInterest == tc.expectedAccruedInterest,
|
||||
"Accrued interest mismatch: expected " +
|
||||
to_string(tc.expectedAccruedInterest) + ", got " +
|
||||
to_string(computedAccruedInterest));
|
||||
}
|
||||
}
|
||||
|
||||
// This test overlaps with testLoanAccruedInterest, the test cases only
|
||||
// exercise the computeFullPaymentInterest parts unique to it.
|
||||
void
|
||||
testComputeFullPaymentInterest()
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace ripple::detail;
|
||||
|
||||
struct TestCase
|
||||
{
|
||||
std::string name;
|
||||
Number rawPrincipalOutstanding;
|
||||
Number periodicRate;
|
||||
NetClock::time_point parentCloseTime;
|
||||
std::uint32_t paymentInterval;
|
||||
std::uint32_t prevPaymentDate;
|
||||
std::uint32_t startDate;
|
||||
TenthBips32 closeInterestRate;
|
||||
Number expectedFullPaymentInterest;
|
||||
};
|
||||
|
||||
auto const testCases = std::vector<TestCase>{
|
||||
{
|
||||
.name = "Zero principal outstanding",
|
||||
.rawPrincipalOutstanding = Number{0},
|
||||
.periodicRate = Number{5, -2},
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.paymentInterval = 30 * 24 * 60 * 60,
|
||||
.prevPaymentDate = 2'000,
|
||||
.startDate = 1'000,
|
||||
.closeInterestRate = TenthBips32{10'000},
|
||||
.expectedFullPaymentInterest = Number{0},
|
||||
},
|
||||
{
|
||||
.name = "Zero close interest rate",
|
||||
.rawPrincipalOutstanding = Number{1'000},
|
||||
.periodicRate = Number{5, -2},
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.paymentInterval = 30 * 24 * 60 * 60,
|
||||
.prevPaymentDate = 2'000,
|
||||
.startDate = 1'000,
|
||||
.closeInterestRate = TenthBips32{0},
|
||||
.expectedFullPaymentInterest =
|
||||
Number{1929012345679012, -17}, // from calc
|
||||
},
|
||||
{
|
||||
.name = "Standard case",
|
||||
.rawPrincipalOutstanding = Number{1'000},
|
||||
.periodicRate = Number{5, -2},
|
||||
.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{3'000}},
|
||||
.paymentInterval = 30 * 24 * 60 * 60,
|
||||
.prevPaymentDate = 2'000,
|
||||
.startDate = 1'000,
|
||||
.closeInterestRate = TenthBips32{10'000},
|
||||
.expectedFullPaymentInterest =
|
||||
Number{1000192901234568, -13}, // from calc
|
||||
},
|
||||
};
|
||||
|
||||
for (auto const& tc : testCases)
|
||||
{
|
||||
testcase("computeFullPaymentInterest: " + tc.name);
|
||||
|
||||
auto const computedFullPaymentInterest = computeFullPaymentInterest(
|
||||
tc.rawPrincipalOutstanding,
|
||||
tc.periodicRate,
|
||||
tc.parentCloseTime,
|
||||
tc.paymentInterval,
|
||||
tc.prevPaymentDate,
|
||||
tc.startDate,
|
||||
tc.closeInterestRate);
|
||||
BEAST_EXPECTS(
|
||||
computedFullPaymentInterest == tc.expectedFullPaymentInterest,
|
||||
"Full payment interest mismatch: expected " +
|
||||
to_string(tc.expectedFullPaymentInterest) + ", got " +
|
||||
to_string(computedFullPaymentInterest));
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testComputeFullPaymentInterest();
|
||||
testLoanAccruedInterest();
|
||||
testLoanLatePaymentInterest();
|
||||
testLoanPeriodicPayment();
|
||||
testLoanPrincipalFromPeriodicPayment();
|
||||
testComputeRaisedRate();
|
||||
testComputePaymentFactor();
|
||||
testComputeOverpaymentComponents();
|
||||
testComputeInterestAndFeeParts();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(LendingHelpers, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -141,7 +141,7 @@ protected:
|
||||
using namespace jtx;
|
||||
|
||||
auto const vaultSle = env.le(keylet::vault(vaultID));
|
||||
return getVaultScale(vaultSle);
|
||||
return getAssetsTotalScale(vaultSle);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -551,12 +551,15 @@ protected:
|
||||
broker.vaultScale(env),
|
||||
state.principalOutstanding.exponent())));
|
||||
BEAST_EXPECT(state.paymentInterval == 600);
|
||||
BEAST_EXPECT(
|
||||
state.totalValue ==
|
||||
roundToAsset(
|
||||
broker.asset,
|
||||
state.periodicPayment * state.paymentRemaining,
|
||||
state.loanScale));
|
||||
{
|
||||
NumberRoundModeGuard mg(Number::upward);
|
||||
BEAST_EXPECT(
|
||||
state.totalValue ==
|
||||
roundToAsset(
|
||||
broker.asset,
|
||||
state.periodicPayment * state.paymentRemaining,
|
||||
state.loanScale));
|
||||
}
|
||||
BEAST_EXPECT(
|
||||
state.managementFeeOutstanding ==
|
||||
computeManagementFee(
|
||||
@@ -697,7 +700,8 @@ protected:
|
||||
interval,
|
||||
total,
|
||||
feeRate,
|
||||
asset(brokerParams.vaultDeposit).number().exponent());
|
||||
asset(brokerParams.vaultDeposit).number().exponent(),
|
||||
env.journal);
|
||||
log << "Loan properties:\n"
|
||||
<< "\tPrincipal: " << principal << std::endl
|
||||
<< "\tInterest rate: " << interest << std::endl
|
||||
@@ -1478,7 +1482,8 @@ protected:
|
||||
state.paymentInterval,
|
||||
state.paymentRemaining,
|
||||
broker.params.managementFeeRate,
|
||||
state.loanScale);
|
||||
state.loanScale,
|
||||
env.journal);
|
||||
|
||||
verifyLoanStatus(
|
||||
0,
|
||||
@@ -2449,13 +2454,18 @@ protected:
|
||||
// Make all the payments in one transaction
|
||||
// service fee is 2
|
||||
auto const startingPayments = state.paymentRemaining;
|
||||
auto const rawPayoff = startingPayments *
|
||||
(state.periodicPayment + broker.asset(2).value());
|
||||
STAmount const payoffAmount{broker.asset, rawPayoff};
|
||||
BEAST_EXPECT(
|
||||
payoffAmount ==
|
||||
broker.asset(Number(1024014840139457, -12)));
|
||||
BEAST_EXPECT(payoffAmount > state.principalOutstanding);
|
||||
STAmount const payoffAmount = [&]() {
|
||||
NumberRoundModeGuard mg(Number::upward);
|
||||
auto const rawPayoff = startingPayments *
|
||||
(state.periodicPayment + broker.asset(2).value());
|
||||
STAmount const payoffAmount{broker.asset, rawPayoff};
|
||||
BEAST_EXPECTS(
|
||||
payoffAmount ==
|
||||
broker.asset(Number(1024014840139457, -12)),
|
||||
to_string(payoffAmount));
|
||||
BEAST_EXPECT(payoffAmount > state.principalOutstanding);
|
||||
return payoffAmount;
|
||||
}();
|
||||
|
||||
singlePayment(
|
||||
loanKeylet,
|
||||
@@ -4010,7 +4020,7 @@ protected:
|
||||
createJson = env.json(createJson, sig(sfCounterpartySignature, lender));
|
||||
// Fails in preclaim because principal requested can't be
|
||||
// represented as XRP
|
||||
env(createJson, ter(tecPRECISION_LOSS));
|
||||
env(createJson, ter(tecPRECISION_LOSS), THISLINE);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!env.le(keylet));
|
||||
@@ -4022,7 +4032,7 @@ protected:
|
||||
createJson = env.json(createJson, sig(sfCounterpartySignature, lender));
|
||||
// Fails in doApply because the payment is too small to be
|
||||
// represented as XRP.
|
||||
env(createJson, ter(tecPRECISION_LOSS));
|
||||
env(createJson, ter(tecPRECISION_LOSS), THISLINE);
|
||||
env.close();
|
||||
}
|
||||
|
||||
@@ -4997,7 +5007,7 @@ protected:
|
||||
auto const keylet = keylet::loan(broker.brokerID, loanSequence);
|
||||
|
||||
createJson = env.json(createJson, sig(sfCounterpartySignature, lender));
|
||||
env(createJson, ter(tecPRECISION_LOSS));
|
||||
env(createJson, ter(tecPRECISION_LOSS), THISLINE);
|
||||
env.close(startDate);
|
||||
|
||||
auto loanPayTx = env.json(
|
||||
@@ -6145,15 +6155,16 @@ protected:
|
||||
// Accrued + prepayment-penalty interest based on current periodic
|
||||
// schedule
|
||||
auto const fullPaymentInterest = computeFullPaymentInterest(
|
||||
after.periodicPayment,
|
||||
detail::loanPrincipalFromPeriodicPayment(
|
||||
after.periodicPayment, periodicRate2, after.paymentRemaining),
|
||||
periodicRate2,
|
||||
after.paymentRemaining,
|
||||
env.current()->parentCloseTime(),
|
||||
after.paymentInterval,
|
||||
after.previousPaymentDate,
|
||||
static_cast<std::uint32_t>(
|
||||
after.startDate.time_since_epoch().count()),
|
||||
closeInterestRate);
|
||||
|
||||
// Round to asset scale and split interest/fee parts
|
||||
auto const roundedInterest =
|
||||
roundToAsset(asset.raw(), fullPaymentInterest, after.loanScale);
|
||||
@@ -6181,9 +6192,9 @@ protected:
|
||||
// window by clamping prevPaymentDate to 'now' for the full-pay path.
|
||||
auto const prevClamped = std::min(after.previousPaymentDate, nowSecs);
|
||||
auto const fullPaymentInterestClamped = computeFullPaymentInterest(
|
||||
after.periodicPayment,
|
||||
detail::loanPrincipalFromPeriodicPayment(
|
||||
after.periodicPayment, periodicRate2, after.paymentRemaining),
|
||||
periodicRate2,
|
||||
after.paymentRemaining,
|
||||
env.current()->parentCloseTime(),
|
||||
after.paymentInterval,
|
||||
prevClamped,
|
||||
|
||||
@@ -179,11 +179,12 @@ adjustImpreciseNumber(
|
||||
}
|
||||
|
||||
inline int
|
||||
getVaultScale(SLE::const_ref vaultSle)
|
||||
getAssetsTotalScale(SLE::const_ref vaultSle)
|
||||
{
|
||||
if (!vaultSle)
|
||||
return Number::minExponent - 1; // LCOV_EXCL_LINE
|
||||
return vaultSle->at(sfAssetsTotal).exponent();
|
||||
return STAmount{vaultSle->at(sfAsset), vaultSle->at(sfAssetsTotal)}
|
||||
.exponent();
|
||||
}
|
||||
|
||||
TER
|
||||
@@ -202,14 +203,6 @@ computeRawLoanState(
|
||||
std::uint32_t const paymentRemaining,
|
||||
TenthBips32 const managementFeeRate);
|
||||
|
||||
LoanState
|
||||
computeRawLoanState(
|
||||
Number const& periodicPayment,
|
||||
TenthBips32 interestRate,
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t const paymentRemaining,
|
||||
TenthBips32 const managementFeeRate);
|
||||
|
||||
// Constructs a valid LoanState object from arbitrary inputs
|
||||
LoanState
|
||||
constructLoanState(
|
||||
@@ -239,17 +232,6 @@ computeFullPaymentInterest(
|
||||
std::uint32_t startDate,
|
||||
TenthBips32 closeInterestRate);
|
||||
|
||||
Number
|
||||
computeFullPaymentInterest(
|
||||
Number const& periodicPayment,
|
||||
Number const& periodicRate,
|
||||
std::uint32_t paymentRemaining,
|
||||
NetClock::time_point parentCloseTime,
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t prevPaymentDate,
|
||||
std::uint32_t startDate,
|
||||
TenthBips32 closeInterestRate);
|
||||
|
||||
namespace detail {
|
||||
// These classes and functions should only be accessed by LendingHelper
|
||||
// functions and unit tests
|
||||
@@ -387,6 +369,58 @@ struct LoanStateDeltas
|
||||
nonNegative();
|
||||
};
|
||||
|
||||
Number
|
||||
computeRaisedRate(Number const& periodicRate, std::uint32_t paymentsRemaining);
|
||||
|
||||
Number
|
||||
computePaymentFactor(
|
||||
Number const& periodicRate,
|
||||
std::uint32_t paymentsRemaining);
|
||||
|
||||
std::pair<Number, Number>
|
||||
computeInterestAndFeeParts(
|
||||
Asset const& asset,
|
||||
Number const& interest,
|
||||
TenthBips16 managementFeeRate,
|
||||
std::int32_t loanScale);
|
||||
|
||||
Number
|
||||
loanPeriodicPayment(
|
||||
Number const& principalOutstanding,
|
||||
Number const& periodicRate,
|
||||
std::uint32_t paymentsRemaining);
|
||||
|
||||
Number
|
||||
loanPrincipalFromPeriodicPayment(
|
||||
Number const& periodicPayment,
|
||||
Number const& periodicRate,
|
||||
std::uint32_t paymentsRemaining);
|
||||
|
||||
Number
|
||||
loanLatePaymentInterest(
|
||||
Number const& principalOutstanding,
|
||||
TenthBips32 lateInterestRate,
|
||||
NetClock::time_point parentCloseTime,
|
||||
std::uint32_t nextPaymentDueDate);
|
||||
|
||||
Number
|
||||
loanAccruedInterest(
|
||||
Number const& principalOutstanding,
|
||||
Number const& periodicRate,
|
||||
NetClock::time_point parentCloseTime,
|
||||
std::uint32_t startDate,
|
||||
std::uint32_t prevPaymentDate,
|
||||
std::uint32_t paymentInterval);
|
||||
|
||||
ExtendedPaymentComponents
|
||||
computeOverpaymentComponents(
|
||||
Asset const& asset,
|
||||
int32_t const loanScale,
|
||||
Number const& overpayment,
|
||||
TenthBips32 const overpaymentInterestRate,
|
||||
TenthBips32 const overpaymentFeeRate,
|
||||
TenthBips16 const managementFeeRate);
|
||||
|
||||
PaymentComponents
|
||||
computePaymentComponents(
|
||||
Asset const& asset,
|
||||
@@ -418,7 +452,8 @@ computeLoanProperties(
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t paymentsRemaining,
|
||||
TenthBips32 managementFeeRate,
|
||||
std::int32_t minimumScale);
|
||||
std::int32_t minimumScale,
|
||||
beast::Journal j);
|
||||
|
||||
bool
|
||||
isRounded(Asset const& asset, Number const& value, std::int32_t scale);
|
||||
|
||||
@@ -100,6 +100,9 @@ computePaymentFactor(
|
||||
Number const& periodicRate,
|
||||
std::uint32_t paymentsRemaining)
|
||||
{
|
||||
if (paymentsRemaining == 0)
|
||||
return numZero;
|
||||
|
||||
// For zero interest, payment factor is simply 1/paymentsRemaining
|
||||
if (periodicRate == beast::zero)
|
||||
return Number{1} / paymentsRemaining;
|
||||
@@ -132,27 +135,6 @@ loanPeriodicPayment(
|
||||
computePaymentFactor(periodicRate, paymentsRemaining);
|
||||
}
|
||||
|
||||
/* Calculates the periodic payment amount from annualized interest rate.
|
||||
* Converts the annual rate to periodic rate before computing payment.
|
||||
*
|
||||
* Equation (7) from XLS-66 spec, Section A-2 Equation Glossary
|
||||
*/
|
||||
Number
|
||||
loanPeriodicPayment(
|
||||
Number const& principalOutstanding,
|
||||
TenthBips32 interestRate,
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t paymentsRemaining)
|
||||
{
|
||||
if (principalOutstanding == 0 || paymentsRemaining == 0)
|
||||
return 0;
|
||||
|
||||
Number const periodicRate = loanPeriodicRate(interestRate, paymentInterval);
|
||||
|
||||
return loanPeriodicPayment(
|
||||
principalOutstanding, periodicRate, paymentsRemaining);
|
||||
}
|
||||
|
||||
/* Reverse-calculates principal from periodic payment amount.
|
||||
* Used to determine theoretical principal at any point in the schedule.
|
||||
*
|
||||
@@ -164,6 +146,9 @@ loanPrincipalFromPeriodicPayment(
|
||||
Number const& periodicRate,
|
||||
std::uint32_t paymentsRemaining)
|
||||
{
|
||||
if (paymentsRemaining == 0)
|
||||
return numZero;
|
||||
|
||||
if (periodicRate == 0)
|
||||
return periodicPayment * paymentsRemaining;
|
||||
|
||||
@@ -171,21 +156,6 @@ loanPrincipalFromPeriodicPayment(
|
||||
computePaymentFactor(periodicRate, paymentsRemaining);
|
||||
}
|
||||
|
||||
/* Splits gross interest into net interest (to vault) and management fee (to
|
||||
* broker). Returns pair of (net interest, management fee).
|
||||
*
|
||||
* Equation (33) from XLS-66 spec, Section A-2 Equation Glossary
|
||||
*/
|
||||
std::pair<Number, Number>
|
||||
computeInterestAndFeeParts(
|
||||
Number const& interest,
|
||||
TenthBips16 managementFeeRate)
|
||||
{
|
||||
auto const fee = tenthBipsOfValue(interest, managementFeeRate);
|
||||
|
||||
return std::make_pair(interest - fee, fee);
|
||||
}
|
||||
|
||||
/*
|
||||
* Computes the interest and management fee parts from interest amount.
|
||||
*
|
||||
@@ -216,6 +186,12 @@ loanLatePaymentInterest(
|
||||
NetClock::time_point parentCloseTime,
|
||||
std::uint32_t nextPaymentDueDate)
|
||||
{
|
||||
if (principalOutstanding == beast::zero)
|
||||
return numZero;
|
||||
|
||||
if (lateInterestRate == TenthBips32{0})
|
||||
return numZero;
|
||||
|
||||
auto const now = parentCloseTime.time_since_epoch().count();
|
||||
|
||||
// If the payment is not late by any amount of time, then there's no late
|
||||
@@ -248,6 +224,9 @@ loanAccruedInterest(
|
||||
if (periodicRate == beast::zero)
|
||||
return numZero;
|
||||
|
||||
if (paymentInterval == 0)
|
||||
return numZero;
|
||||
|
||||
auto const lastPaymentDate = std::max(prevPaymentDate, startDate);
|
||||
auto const now = parentCloseTime.time_since_epoch().count();
|
||||
|
||||
@@ -451,7 +430,8 @@ tryOverpayment(
|
||||
paymentInterval,
|
||||
paymentRemaining,
|
||||
managementFeeRate,
|
||||
loanScale);
|
||||
loanScale,
|
||||
j);
|
||||
|
||||
JLOG(j.debug()) << "new periodic payment: "
|
||||
<< newLoanProperties.periodicPayment
|
||||
@@ -561,8 +541,8 @@ tryOverpayment(
|
||||
// Calculate how the loan's value changed due to the overpayment.
|
||||
// This should be negative (value decreased) or zero. A principal
|
||||
// overpayment should never increase the loan's value.
|
||||
auto const valueChange =
|
||||
newRounded.valueOutstanding - hypotheticalValueOutstanding;
|
||||
auto const valueChange = newRounded.valueOutstanding -
|
||||
hypotheticalValueOutstanding - deltas.managementFee;
|
||||
if (valueChange > 0)
|
||||
{
|
||||
JLOG(j.warn()) << "Principal overpayment would increase the value of "
|
||||
@@ -1225,17 +1205,12 @@ computeOverpaymentComponents(
|
||||
// This interest doesn't follow the normal amortization schedule - it's
|
||||
// a one-time charge for paying early.
|
||||
// Equation (20) and (21) from XLS-66 spec, Section A-2 Equation Glossary
|
||||
auto const [rawOverpaymentInterest, _] = [&]() {
|
||||
Number const interest =
|
||||
tenthBipsOfValue(overpayment, overpaymentInterestRate);
|
||||
return detail::computeInterestAndFeeParts(interest, managementFeeRate);
|
||||
}();
|
||||
|
||||
// Round the penalty interest components to the loan scale
|
||||
auto const [roundedOverpaymentInterest, roundedOverpaymentManagementFee] =
|
||||
[&]() {
|
||||
Number const interest =
|
||||
roundToAsset(asset, rawOverpaymentInterest, loanScale);
|
||||
auto const interest = roundToAsset(
|
||||
asset,
|
||||
tenthBipsOfValue(overpayment, overpaymentInterestRate),
|
||||
loanScale);
|
||||
return detail::computeInterestAndFeeParts(
|
||||
asset, interest, managementFeeRate, loanScale);
|
||||
}();
|
||||
@@ -1429,31 +1404,6 @@ computeFullPaymentInterest(
|
||||
return accruedInterest + prepaymentPenalty;
|
||||
}
|
||||
|
||||
Number
|
||||
computeFullPaymentInterest(
|
||||
Number const& periodicPayment,
|
||||
Number const& periodicRate,
|
||||
std::uint32_t paymentRemaining,
|
||||
NetClock::time_point parentCloseTime,
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t prevPaymentDate,
|
||||
std::uint32_t startDate,
|
||||
TenthBips32 closeInterestRate)
|
||||
{
|
||||
Number const rawPrincipalOutstanding =
|
||||
detail::loanPrincipalFromPeriodicPayment(
|
||||
periodicPayment, periodicRate, paymentRemaining);
|
||||
|
||||
return computeFullPaymentInterest(
|
||||
rawPrincipalOutstanding,
|
||||
periodicRate,
|
||||
parentCloseTime,
|
||||
paymentInterval,
|
||||
prevPaymentDate,
|
||||
startDate,
|
||||
closeInterestRate);
|
||||
}
|
||||
|
||||
/* Calculates the theoretical loan state at maximum precision for a given point
|
||||
* in the amortization schedule.
|
||||
*
|
||||
@@ -1515,21 +1465,6 @@ computeRawLoanState(
|
||||
.managementFeeDue = rawManagementFeeOutstanding};
|
||||
};
|
||||
|
||||
LoanState
|
||||
computeRawLoanState(
|
||||
Number const& periodicPayment,
|
||||
TenthBips32 interestRate,
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t const paymentRemaining,
|
||||
TenthBips32 const managementFeeRate)
|
||||
{
|
||||
return computeRawLoanState(
|
||||
periodicPayment,
|
||||
loanPeriodicRate(interestRate, paymentInterval),
|
||||
paymentRemaining,
|
||||
managementFeeRate);
|
||||
}
|
||||
|
||||
/* Constructs a LoanState from rounded Loan ledger object values.
|
||||
*
|
||||
* This function creates a LoanState structure from the three tracked values
|
||||
@@ -1605,7 +1540,8 @@ computeLoanProperties(
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t paymentsRemaining,
|
||||
TenthBips32 managementFeeRate,
|
||||
std::int32_t minimumScale)
|
||||
std::int32_t minimumScale,
|
||||
beast::Journal j)
|
||||
{
|
||||
auto const periodicRate = loanPeriodicRate(interestRate, paymentInterval);
|
||||
XRPL_ASSERT(
|
||||
@@ -1616,13 +1552,22 @@ computeLoanProperties(
|
||||
principalOutstanding, periodicRate, paymentsRemaining);
|
||||
|
||||
auto const [totalValueOutstanding, loanScale] = [&]() {
|
||||
NumberRoundModeGuard mg(Number::to_nearest);
|
||||
// only round up if there should be interest
|
||||
NumberRoundModeGuard mg(
|
||||
periodicRate == 0 ? Number::to_nearest : Number::upward);
|
||||
// Use STAmount's internal rounding instead of roundToAsset, because
|
||||
// we're going to use this result to determine the scale for all the
|
||||
// other rounding.
|
||||
|
||||
// Equation (30) from XLS-66 spec, Section A-2 Equation Glossary
|
||||
STAmount amount{asset, periodicPayment * paymentsRemaining};
|
||||
JLOG(j.debug()) << "computeLoanProperties:" << " Principal requested: "
|
||||
<< principalOutstanding
|
||||
<< ". Periodic payment: " << periodicPayment
|
||||
<< ". Payments remaining: " << paymentsRemaining
|
||||
<< ". Raw total value: "
|
||||
<< periodicPayment * paymentsRemaining
|
||||
<< ". Candidate total value: " << amount << std::endl;
|
||||
|
||||
// Base the loan scale on the total value, since that's going to be
|
||||
// the biggest number involved (barring unusual parameters for late,
|
||||
@@ -1637,7 +1582,10 @@ computeLoanProperties(
|
||||
|
||||
// We may need to truncate the total value because of the minimum
|
||||
// scale
|
||||
amount = roundToAsset(asset, amount, loanScale, Number::to_nearest);
|
||||
amount = roundToAsset(asset, amount, loanScale);
|
||||
|
||||
JLOG(j.debug()) << "computeLoanProperties: Loan scale:" << loanScale
|
||||
<< ". Actual total value: " << amount << std::endl;
|
||||
|
||||
return std::make_pair(amount, loanScale);
|
||||
}();
|
||||
|
||||
@@ -56,7 +56,7 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx)
|
||||
if (!vault)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
auto const asset = vault->at(sfAsset);
|
||||
auto const scale = getVaultScale(vault);
|
||||
auto const scale = getAssetsTotalScale(vault);
|
||||
|
||||
auto const rounded =
|
||||
roundToAsset(asset, debtTotal, scale, Number::towards_zero);
|
||||
|
||||
@@ -115,7 +115,7 @@ LoanDelete::doApply()
|
||||
roundToAsset(
|
||||
vaultSle->at(sfAsset),
|
||||
debtTotalProxy,
|
||||
getVaultScale(vaultSle),
|
||||
getAssetsTotalScale(vaultSle),
|
||||
Number::towards_zero) == beast::zero,
|
||||
"ripple::LoanDelete::doApply",
|
||||
"last loan, remaining debt rounds to zero");
|
||||
|
||||
@@ -106,7 +106,7 @@ LoanManage::preclaim(PreclaimContext const& ctx)
|
||||
if (loanBrokerSle->at(sfOwner) != account)
|
||||
{
|
||||
JLOG(ctx.j.warn())
|
||||
<< "LoanBroker for Loan does not belong to the account. LoanModify "
|
||||
<< "LoanBroker for Loan does not belong to the account. LoanManage "
|
||||
"can only be submitted by the Loan Broker.";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
@@ -178,7 +178,7 @@ LoanManage::defaultLoan(
|
||||
// The vault may be at a different scale than the loan. Reduce rounding
|
||||
// errors during the accounting by rounding some of the values to that
|
||||
// scale.
|
||||
auto const vaultScale = getVaultScale(vaultSle);
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
|
||||
{
|
||||
// Decrease the Total Value of the Vault:
|
||||
@@ -242,7 +242,11 @@ LoanManage::defaultLoan(
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
vaultLossUnrealizedProxy -= totalDefaultAmount;
|
||||
adjustImpreciseNumber(
|
||||
vaultLossUnrealizedProxy,
|
||||
-totalDefaultAmount,
|
||||
vaultAsset,
|
||||
vaultScale);
|
||||
}
|
||||
view.update(vaultSle);
|
||||
}
|
||||
@@ -250,11 +254,9 @@ LoanManage::defaultLoan(
|
||||
// Update the LoanBroker object:
|
||||
|
||||
{
|
||||
auto const asset = *vaultSle->at(sfAsset);
|
||||
|
||||
// Decrease the Debt of the LoanBroker:
|
||||
adjustImpreciseNumber(
|
||||
brokerDebtTotalProxy, -totalDefaultAmount, asset, vaultScale);
|
||||
brokerDebtTotalProxy, -totalDefaultAmount, vaultAsset, vaultScale);
|
||||
// Decrease the First-Loss Capital Cover Available:
|
||||
auto coverAvailableProxy = brokerSle->at(sfCoverAvailable);
|
||||
if (coverAvailableProxy < defaultCovered)
|
||||
@@ -297,13 +299,20 @@ LoanManage::impairLoan(
|
||||
ApplyView& view,
|
||||
SLE::ref loanSle,
|
||||
SLE::ref vaultSle,
|
||||
Asset const& vaultAsset,
|
||||
beast::Journal j)
|
||||
{
|
||||
Number const lossUnrealized = owedToVault(loanSle);
|
||||
|
||||
// The vault may be at a different scale than the loan. Reduce rounding
|
||||
// errors during the accounting by rounding some of the values to that
|
||||
// scale.
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
|
||||
// Update the Vault object(set "paper loss")
|
||||
auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
|
||||
vaultLossUnrealizedProxy += lossUnrealized;
|
||||
adjustImpreciseNumber(
|
||||
vaultLossUnrealizedProxy, lossUnrealized, vaultAsset, vaultScale);
|
||||
if (vaultLossUnrealizedProxy >
|
||||
vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable))
|
||||
{
|
||||
@@ -334,8 +343,14 @@ LoanManage::unimpairLoan(
|
||||
ApplyView& view,
|
||||
SLE::ref loanSle,
|
||||
SLE::ref vaultSle,
|
||||
Asset const& vaultAsset,
|
||||
beast::Journal j)
|
||||
{
|
||||
// The vault may be at a different scale than the loan. Reduce rounding
|
||||
// errors during the accounting by rounding some of the values to that
|
||||
// scale.
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
|
||||
// Update the Vault object(clear "paper loss")
|
||||
auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
|
||||
Number const lossReversed = owedToVault(loanSle);
|
||||
@@ -347,7 +362,10 @@ LoanManage::unimpairLoan(
|
||||
return tefBAD_LEDGER;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
vaultLossUnrealizedProxy -= lossReversed;
|
||||
// Reverse the "paper loss"
|
||||
adjustImpreciseNumber(
|
||||
vaultLossUnrealizedProxy, -lossReversed, vaultAsset, vaultScale);
|
||||
|
||||
view.update(vaultSle);
|
||||
|
||||
// Update the Loan object
|
||||
@@ -403,12 +421,14 @@ LoanManage::doApply()
|
||||
}
|
||||
else if (tx.isFlag(tfLoanImpair))
|
||||
{
|
||||
if (auto const ter = impairLoan(view, loanSle, vaultSle, j_))
|
||||
if (auto const ter =
|
||||
impairLoan(view, loanSle, vaultSle, vaultAsset, j_))
|
||||
return ter;
|
||||
}
|
||||
else if (tx.isFlag(tfLoanUnimpair))
|
||||
{
|
||||
if (auto const ter = unimpairLoan(view, loanSle, vaultSle, j_))
|
||||
if (auto const ter =
|
||||
unimpairLoan(view, loanSle, vaultSle, vaultAsset, j_))
|
||||
return ter;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ public:
|
||||
ApplyView& view,
|
||||
SLE::ref loanSle,
|
||||
SLE::ref vaultSle,
|
||||
Asset const& vaultAsset,
|
||||
beast::Journal j);
|
||||
|
||||
/** Helper function that might be needed by other transactors
|
||||
@@ -53,6 +54,7 @@ public:
|
||||
ApplyView& view,
|
||||
SLE::ref loanSle,
|
||||
SLE::ref vaultSle,
|
||||
Asset const& vaultAsset,
|
||||
beast::Journal j);
|
||||
|
||||
TER
|
||||
|
||||
@@ -305,7 +305,7 @@ LoanPay::doApply()
|
||||
// change will be discarded.
|
||||
if (loanSle->isFlag(lsfLoanImpaired))
|
||||
{
|
||||
LoanManage::unimpairLoan(view, loanSle, vaultSle, j_);
|
||||
LoanManage::unimpairLoan(view, loanSle, vaultSle, asset, j_);
|
||||
}
|
||||
|
||||
LoanPaymentType const paymentType = [&tx]() {
|
||||
@@ -379,7 +379,7 @@ LoanPay::doApply()
|
||||
|
||||
// The vault may be at a different scale than the loan. Reduce rounding
|
||||
// errors during the payment by rounding some of the values to that scale.
|
||||
auto const vaultScale = assetsTotalProxy.value().exponent();
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
|
||||
auto const totalPaidToVaultRaw =
|
||||
paymentParts->principalPaid + paymentParts->interestPaid;
|
||||
|
||||
@@ -383,7 +383,7 @@ LoanSet::doApply()
|
||||
|
||||
auto vaultAvailableProxy = vaultSle->at(sfAssetsAvailable);
|
||||
auto vaultTotalProxy = vaultSle->at(sfAssetsTotal);
|
||||
auto const vaultScale = getVaultScale(vaultSle);
|
||||
auto const vaultScale = getAssetsTotalScale(vaultSle);
|
||||
if (vaultAvailableProxy < principalRequested)
|
||||
{
|
||||
JLOG(j_.warn())
|
||||
@@ -404,7 +404,8 @@ LoanSet::doApply()
|
||||
paymentInterval,
|
||||
paymentTotal,
|
||||
TenthBips16{brokerSle->at(sfManagementFeeRate)},
|
||||
vaultScale);
|
||||
vaultScale,
|
||||
j_);
|
||||
|
||||
// Check that relevant values won't lose precision. This is mostly only
|
||||
// relevant for IOU assets.
|
||||
@@ -440,7 +441,10 @@ LoanSet::doApply()
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j_.warn())
|
||||
<< "Computed loan properties are invalid. Does not compute.";
|
||||
<< "Computed loan properties are invalid. Does not compute."
|
||||
<< " Management fee: " << properties.managementFeeOwedToBroker
|
||||
<< ". Total Value: " << properties.totalValueOutstanding
|
||||
<< ". PeriodicPayment: " << properties.periodicPayment;
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user