Compare commits

..

13 Commits

13 changed files with 238 additions and 833 deletions

View File

@@ -1,642 +0,0 @@
#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

View File

@@ -141,7 +141,7 @@ protected:
using namespace jtx; using namespace jtx;
auto const vaultSle = env.le(keylet::vault(vaultID)); auto const vaultSle = env.le(keylet::vault(vaultID));
return getAssetsTotalScale(vaultSle); return getVaultScale(vaultSle);
} }
}; };
@@ -551,15 +551,12 @@ protected:
broker.vaultScale(env), broker.vaultScale(env),
state.principalOutstanding.exponent()))); state.principalOutstanding.exponent())));
BEAST_EXPECT(state.paymentInterval == 600); BEAST_EXPECT(state.paymentInterval == 600);
{
NumberRoundModeGuard mg(Number::upward);
BEAST_EXPECT( BEAST_EXPECT(
state.totalValue == state.totalValue ==
roundToAsset( roundToAsset(
broker.asset, broker.asset,
state.periodicPayment * state.paymentRemaining, state.periodicPayment * state.paymentRemaining,
state.loanScale)); state.loanScale));
}
BEAST_EXPECT( BEAST_EXPECT(
state.managementFeeOutstanding == state.managementFeeOutstanding ==
computeManagementFee( computeManagementFee(
@@ -700,8 +697,7 @@ protected:
interval, interval,
total, total,
feeRate, feeRate,
asset(brokerParams.vaultDeposit).number().exponent(), asset(brokerParams.vaultDeposit).number().exponent());
env.journal);
log << "Loan properties:\n" log << "Loan properties:\n"
<< "\tPrincipal: " << principal << std::endl << "\tPrincipal: " << principal << std::endl
<< "\tInterest rate: " << interest << std::endl << "\tInterest rate: " << interest << std::endl
@@ -1275,8 +1271,7 @@ protected:
verifyLoanStatus, verifyLoanStatus,
issuer, issuer,
lender, lender,
borrower, borrower);
PaymentParameters{.showStepBalances = true});
} }
/** Runs through the complete lifecycle of a loan /** Runs through the complete lifecycle of a loan
@@ -1482,8 +1477,7 @@ protected:
state.paymentInterval, state.paymentInterval,
state.paymentRemaining, state.paymentRemaining,
broker.params.managementFeeRate, broker.params.managementFeeRate,
state.loanScale, state.loanScale);
env.journal);
verifyLoanStatus( verifyLoanStatus(
0, 0,
@@ -2454,18 +2448,13 @@ protected:
// Make all the payments in one transaction // Make all the payments in one transaction
// service fee is 2 // service fee is 2
auto const startingPayments = state.paymentRemaining; auto const startingPayments = state.paymentRemaining;
STAmount const payoffAmount = [&]() {
NumberRoundModeGuard mg(Number::upward);
auto const rawPayoff = startingPayments * auto const rawPayoff = startingPayments *
(state.periodicPayment + broker.asset(2).value()); (state.periodicPayment + broker.asset(2).value());
STAmount const payoffAmount{broker.asset, rawPayoff}; STAmount const payoffAmount{broker.asset, rawPayoff};
BEAST_EXPECTS( BEAST_EXPECT(
payoffAmount == payoffAmount ==
broker.asset(Number(1024014840139457, -12)), broker.asset(Number(1024014840139457, -12)));
to_string(payoffAmount));
BEAST_EXPECT(payoffAmount > state.principalOutstanding); BEAST_EXPECT(payoffAmount > state.principalOutstanding);
return payoffAmount;
}();
singlePayment( singlePayment(
loanKeylet, loanKeylet,
@@ -4020,7 +4009,7 @@ protected:
createJson = env.json(createJson, sig(sfCounterpartySignature, lender)); createJson = env.json(createJson, sig(sfCounterpartySignature, lender));
// Fails in preclaim because principal requested can't be // Fails in preclaim because principal requested can't be
// represented as XRP // represented as XRP
env(createJson, ter(tecPRECISION_LOSS), THISLINE); env(createJson, ter(tecPRECISION_LOSS));
env.close(); env.close();
BEAST_EXPECT(!env.le(keylet)); BEAST_EXPECT(!env.le(keylet));
@@ -4032,7 +4021,7 @@ protected:
createJson = env.json(createJson, sig(sfCounterpartySignature, lender)); createJson = env.json(createJson, sig(sfCounterpartySignature, lender));
// Fails in doApply because the payment is too small to be // Fails in doApply because the payment is too small to be
// represented as XRP. // represented as XRP.
env(createJson, ter(tecPRECISION_LOSS), THISLINE); env(createJson, ter(tecPRECISION_LOSS));
env.close(); env.close();
} }
@@ -5007,7 +4996,7 @@ protected:
auto const keylet = keylet::loan(broker.brokerID, loanSequence); auto const keylet = keylet::loan(broker.brokerID, loanSequence);
createJson = env.json(createJson, sig(sfCounterpartySignature, lender)); createJson = env.json(createJson, sig(sfCounterpartySignature, lender));
env(createJson, ter(tecPRECISION_LOSS), THISLINE); env(createJson, ter(tecPRECISION_LOSS));
env.close(startDate); env.close(startDate);
auto loanPayTx = env.json( auto loanPayTx = env.json(
@@ -6155,16 +6144,15 @@ protected:
// Accrued + prepayment-penalty interest based on current periodic // Accrued + prepayment-penalty interest based on current periodic
// schedule // schedule
auto const fullPaymentInterest = computeFullPaymentInterest( auto const fullPaymentInterest = computeFullPaymentInterest(
detail::loanPrincipalFromPeriodicPayment( after.periodicPayment,
after.periodicPayment, periodicRate2, after.paymentRemaining),
periodicRate2, periodicRate2,
after.paymentRemaining,
env.current()->parentCloseTime(), env.current()->parentCloseTime(),
after.paymentInterval, after.paymentInterval,
after.previousPaymentDate, after.previousPaymentDate,
static_cast<std::uint32_t>( static_cast<std::uint32_t>(
after.startDate.time_since_epoch().count()), after.startDate.time_since_epoch().count()),
closeInterestRate); closeInterestRate);
// Round to asset scale and split interest/fee parts // Round to asset scale and split interest/fee parts
auto const roundedInterest = auto const roundedInterest =
roundToAsset(asset.raw(), fullPaymentInterest, after.loanScale); roundToAsset(asset.raw(), fullPaymentInterest, after.loanScale);
@@ -6192,9 +6180,9 @@ protected:
// window by clamping prevPaymentDate to 'now' for the full-pay path. // window by clamping prevPaymentDate to 'now' for the full-pay path.
auto const prevClamped = std::min(after.previousPaymentDate, nowSecs); auto const prevClamped = std::min(after.previousPaymentDate, nowSecs);
auto const fullPaymentInterestClamped = computeFullPaymentInterest( auto const fullPaymentInterestClamped = computeFullPaymentInterest(
detail::loanPrincipalFromPeriodicPayment( after.periodicPayment,
after.periodicPayment, periodicRate2, after.paymentRemaining),
periodicRate2, periodicRate2,
after.paymentRemaining,
env.current()->parentCloseTime(), env.current()->parentCloseTime(),
after.paymentInterval, after.paymentInterval,
prevClamped, prevClamped,
@@ -7205,15 +7193,15 @@ class LoanArbitrary_test : public LoanBatch_test
.vaultDeposit = 10000, .vaultDeposit = 10000,
.debtMax = 0, .debtMax = 0,
.coverRateMin = TenthBips32{0}, .coverRateMin = TenthBips32{0},
.managementFeeRate = TenthBips16{0}, // .managementFeeRate = TenthBips16{5919},
.coverRateLiquidation = TenthBips32{0}}; .coverRateLiquidation = TenthBips32{0}};
LoanParameters const loanParams{ LoanParameters const loanParams{
.account = Account("lender"), .account = Account("lender"),
.counter = Account("borrower"), .counter = Account("borrower"),
.principalRequest = Number{200000, -6}, .principalRequest = Number{10000, 0},
.interest = TenthBips32{50000}, // .interest = TenthBips32{0},
.payTotal = 2, // .payTotal = 5816,
.payInterval = 200}; .payInterval = 150};
runLoan(AssetType::XRP, brokerParams, loanParams); runLoan(AssetType::XRP, brokerParams, loanParams);
} }

View File

@@ -644,7 +644,7 @@ MPTTester::operator[](std::string const& name) const
} }
PrettyAmount PrettyAmount
MPTTester::operator()(std::int64_t amount) const MPTTester::operator()(std::uint64_t amount) const
{ {
return MPT("", issuanceID())(amount); return MPT("", issuanceID())(amount);
} }

View File

@@ -272,7 +272,7 @@ public:
operator[](std::string const& name) const; operator[](std::string const& name) const;
PrettyAmount PrettyAmount
operator()(std::int64_t amount) const; operator()(std::uint64_t amount) const;
operator Asset() const; operator Asset() const;

View File

@@ -10,6 +10,7 @@
#include <doctest/doctest.h> #include <doctest/doctest.h>
#include <atomic> #include <atomic>
#include <iostream>
#include <map> #include <map>
#include <thread> #include <thread>
@@ -17,6 +18,40 @@ using namespace ripple;
namespace { namespace {
struct logger
{
std::string name;
logger const* const parent = nullptr;
static std::size_t depth;
logger(std::string n) : name(n)
{
std::clog << indent() << name << " begin\n";
++depth;
}
logger(logger const& p, std::string n) : parent(&p), name(n)
{
std::clog << indent() << parent->name << " : " << name << " begin\n";
++depth;
}
~logger()
{
--depth;
if (parent)
std::clog << indent() << parent->name << " : " << name << " end\n";
else
std::clog << indent() << name << " end\n";
}
std::string
indent()
{
return std::string(depth, ' ');
}
};
std::size_t logger::depth = 0;
// Simple HTTP server using Beast for testing // Simple HTTP server using Beast for testing
class TestHTTPServer class TestHTTPServer
{ {
@@ -35,6 +70,7 @@ private:
public: public:
TestHTTPServer() : acceptor_(ioc_), port_(0) TestHTTPServer() : acceptor_(ioc_), port_(0)
{ {
logger l("TestHTTPServer()");
// Bind to any available port // Bind to any available port
endpoint_ = {boost::asio::ip::tcp::v4(), 0}; endpoint_ = {boost::asio::ip::tcp::v4(), 0};
acceptor_.open(endpoint_.protocol()); acceptor_.open(endpoint_.protocol());
@@ -50,6 +86,7 @@ public:
~TestHTTPServer() ~TestHTTPServer()
{ {
logger l("~TestHTTPServer()");
stop(); stop();
} }
@@ -87,6 +124,7 @@ private:
void void
stop() stop()
{ {
logger l("TestHTTPServer::stop");
running_ = false; running_ = false;
acceptor_.close(); acceptor_.close();
} }
@@ -94,6 +132,7 @@ private:
void void
accept() accept()
{ {
logger l("TestHTTPServer::accept");
if (!running_) if (!running_)
return; return;
@@ -115,31 +154,37 @@ private:
void void
handleConnection(boost::asio::ip::tcp::socket socket) handleConnection(boost::asio::ip::tcp::socket socket)
{ {
logger l("TestHTTPServer::handleConnection");
try try
{ {
std::optional<logger> r(std::in_place, l, "read the http request");
// Read the HTTP request // Read the HTTP request
boost::beast::flat_buffer buffer; boost::beast::flat_buffer buffer;
boost::beast::http::request<boost::beast::http::string_body> req; boost::beast::http::request<boost::beast::http::string_body> req;
boost::beast::http::read(socket, buffer, req); boost::beast::http::read(socket, buffer, req);
// Create response // Create response
r.emplace(l, "create response");
boost::beast::http::response<boost::beast::http::string_body> res; boost::beast::http::response<boost::beast::http::string_body> res;
res.version(req.version()); res.version(req.version());
res.result(status_code_); res.result(status_code_);
res.set(boost::beast::http::field::server, "TestServer"); res.set(boost::beast::http::field::server, "TestServer");
// Add custom headers // Add custom headers
r.emplace(l, "add custom headers");
for (auto const& [name, value] : custom_headers_) for (auto const& [name, value] : custom_headers_)
{ {
res.set(name, value); res.set(name, value);
} }
// Set body and prepare payload first // Set body and prepare payload first
r.emplace(l, "set body and prepare payload");
res.body() = response_body_; res.body() = response_body_;
res.prepare_payload(); res.prepare_payload();
// Override Content-Length with custom headers after prepare_payload // Override Content-Length with custom headers after prepare_payload
// This allows us to test case-insensitive header parsing // This allows us to test case-insensitive header parsing
r.emplace(l, "override content-length");
for (auto const& [name, value] : custom_headers_) for (auto const& [name, value] : custom_headers_)
{ {
if (boost::iequals(name, "Content-Length")) if (boost::iequals(name, "Content-Length"))
@@ -150,20 +195,26 @@ private:
} }
// Send response // Send response
r.emplace(l, "send response");
boost::beast::http::write(socket, res); boost::beast::http::write(socket, res);
// Shutdown socket gracefully // Shutdown socket gracefully
r.emplace(l, "shutdown socket");
boost::system::error_code ec; boost::system::error_code ec;
socket.shutdown(boost::asio::ip::tcp::socket::shutdown_send, ec); socket.shutdown(boost::asio::ip::tcp::socket::shutdown_send, ec);
} }
catch (std::exception const&) catch (std::exception const&)
{ {
// Connection handling errors are expected // Connection handling errors are expected
logger c(l, "catch");
} }
if (running_) if (running_)
{
logger r(l, "accept");
accept(); accept();
} }
}
}; };
// Helper function to run HTTP client test // Helper function to run HTTP client test
@@ -176,12 +227,16 @@ runHTTPTest(
std::string& result_data, std::string& result_data,
boost::system::error_code& result_error) boost::system::error_code& result_error)
{ {
logger l("runHTTPTest");
// Create a null journal for testing // Create a null journal for testing
beast::Journal j{beast::Journal::getNullSink()}; beast::Journal j{beast::Journal::getNullSink()};
std::optional<logger> r(std::in_place, l, "initializeSSLContext");
// Initialize HTTPClient SSL context // Initialize HTTPClient SSL context
HTTPClient::initializeSSLContext("", "", false, j); HTTPClient::initializeSSLContext("", "", false, j);
r.emplace(l, "HTTPClient::get");
HTTPClient::get( HTTPClient::get(
false, // no SSL false, // no SSL
server.ioc(), server.ioc(),
@@ -206,6 +261,7 @@ runHTTPTest(
while (!completed && while (!completed &&
std::chrono::steady_clock::now() - start < std::chrono::seconds(10)) std::chrono::steady_clock::now() - start < std::chrono::seconds(10))
{ {
r.emplace(l, "ioc.run_one");
if (server.ioc().run_one() == 0) if (server.ioc().run_one() == 0)
{ {
break; break;
@@ -219,6 +275,8 @@ runHTTPTest(
TEST_CASE("HTTPClient case insensitive Content-Length") TEST_CASE("HTTPClient case insensitive Content-Length")
{ {
logger l("HTTPClient case insensitive Content-Length");
// Test different cases of Content-Length header // Test different cases of Content-Length header
std::vector<std::string> header_cases = { std::vector<std::string> header_cases = {
"Content-Length", // Standard case "Content-Length", // Standard case
@@ -230,6 +288,7 @@ TEST_CASE("HTTPClient case insensitive Content-Length")
for (auto const& header_name : header_cases) for (auto const& header_name : header_cases)
{ {
logger h(l, header_name);
TestHTTPServer server; TestHTTPServer server;
std::string test_body = "Hello World!"; std::string test_body = "Hello World!";
server.setResponseBody(test_body); server.setResponseBody(test_body);
@@ -258,6 +317,7 @@ TEST_CASE("HTTPClient case insensitive Content-Length")
TEST_CASE("HTTPClient basic HTTP request") TEST_CASE("HTTPClient basic HTTP request")
{ {
logger l("HTTPClient basic HTTP request");
TestHTTPServer server; TestHTTPServer server;
std::string test_body = "Test response body"; std::string test_body = "Test response body";
server.setResponseBody(test_body); server.setResponseBody(test_body);
@@ -279,6 +339,7 @@ TEST_CASE("HTTPClient basic HTTP request")
TEST_CASE("HTTPClient empty response") TEST_CASE("HTTPClient empty response")
{ {
logger l("HTTPClient empty response");
TestHTTPServer server; TestHTTPServer server;
server.setResponseBody(""); // Empty body server.setResponseBody(""); // Empty body
server.setHeader("Content-Length", "0"); server.setHeader("Content-Length", "0");
@@ -299,6 +360,7 @@ TEST_CASE("HTTPClient empty response")
TEST_CASE("HTTPClient different status codes") TEST_CASE("HTTPClient different status codes")
{ {
logger l("HTTPClient different status codes");
std::vector<unsigned int> status_codes = {200, 404, 500}; std::vector<unsigned int> status_codes = {200, 404, 500};
for (auto status : status_codes) for (auto status : status_codes)

View File

@@ -179,12 +179,11 @@ adjustImpreciseNumber(
} }
inline int inline int
getAssetsTotalScale(SLE::const_ref vaultSle) getVaultScale(SLE::const_ref vaultSle)
{ {
if (!vaultSle) if (!vaultSle)
return Number::minExponent - 1; // LCOV_EXCL_LINE return Number::minExponent - 1; // LCOV_EXCL_LINE
return STAmount{vaultSle->at(sfAsset), vaultSle->at(sfAssetsTotal)} return vaultSle->at(sfAssetsTotal).exponent();
.exponent();
} }
TER TER
@@ -203,6 +202,14 @@ computeRawLoanState(
std::uint32_t const paymentRemaining, std::uint32_t const paymentRemaining,
TenthBips32 const managementFeeRate); 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 // Constructs a valid LoanState object from arbitrary inputs
LoanState LoanState
constructLoanState( constructLoanState(
@@ -232,6 +239,17 @@ computeFullPaymentInterest(
std::uint32_t startDate, std::uint32_t startDate,
TenthBips32 closeInterestRate); 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 { namespace detail {
// These classes and functions should only be accessed by LendingHelper // These classes and functions should only be accessed by LendingHelper
// functions and unit tests // functions and unit tests
@@ -369,58 +387,6 @@ struct LoanStateDeltas
nonNegative(); 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 PaymentComponents
computePaymentComponents( computePaymentComponents(
Asset const& asset, Asset const& asset,
@@ -452,8 +418,7 @@ computeLoanProperties(
std::uint32_t paymentInterval, std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining, std::uint32_t paymentsRemaining,
TenthBips32 managementFeeRate, TenthBips32 managementFeeRate,
std::int32_t minimumScale, std::int32_t minimumScale);
beast::Journal j);
bool bool
isRounded(Asset const& asset, Number const& value, std::int32_t scale); isRounded(Asset const& asset, Number const& value, std::int32_t scale);

View File

@@ -100,9 +100,6 @@ computePaymentFactor(
Number const& periodicRate, Number const& periodicRate,
std::uint32_t paymentsRemaining) std::uint32_t paymentsRemaining)
{ {
if (paymentsRemaining == 0)
return numZero;
// For zero interest, payment factor is simply 1/paymentsRemaining // For zero interest, payment factor is simply 1/paymentsRemaining
if (periodicRate == beast::zero) if (periodicRate == beast::zero)
return Number{1} / paymentsRemaining; return Number{1} / paymentsRemaining;
@@ -135,6 +132,27 @@ loanPeriodicPayment(
computePaymentFactor(periodicRate, paymentsRemaining); 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. /* Reverse-calculates principal from periodic payment amount.
* Used to determine theoretical principal at any point in the schedule. * Used to determine theoretical principal at any point in the schedule.
* *
@@ -146,9 +164,6 @@ loanPrincipalFromPeriodicPayment(
Number const& periodicRate, Number const& periodicRate,
std::uint32_t paymentsRemaining) std::uint32_t paymentsRemaining)
{ {
if (paymentsRemaining == 0)
return numZero;
if (periodicRate == 0) if (periodicRate == 0)
return periodicPayment * paymentsRemaining; return periodicPayment * paymentsRemaining;
@@ -156,6 +171,21 @@ loanPrincipalFromPeriodicPayment(
computePaymentFactor(periodicRate, paymentsRemaining); 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. * Computes the interest and management fee parts from interest amount.
* *
@@ -186,12 +216,6 @@ loanLatePaymentInterest(
NetClock::time_point parentCloseTime, NetClock::time_point parentCloseTime,
std::uint32_t nextPaymentDueDate) std::uint32_t nextPaymentDueDate)
{ {
if (principalOutstanding == beast::zero)
return numZero;
if (lateInterestRate == TenthBips32{0})
return numZero;
auto const now = parentCloseTime.time_since_epoch().count(); auto const now = parentCloseTime.time_since_epoch().count();
// If the payment is not late by any amount of time, then there's no late // If the payment is not late by any amount of time, then there's no late
@@ -224,9 +248,6 @@ loanAccruedInterest(
if (periodicRate == beast::zero) if (periodicRate == beast::zero)
return numZero; return numZero;
if (paymentInterval == 0)
return numZero;
auto const lastPaymentDate = std::max(prevPaymentDate, startDate); auto const lastPaymentDate = std::max(prevPaymentDate, startDate);
auto const now = parentCloseTime.time_since_epoch().count(); auto const now = parentCloseTime.time_since_epoch().count();
@@ -430,8 +451,7 @@ tryOverpayment(
paymentInterval, paymentInterval,
paymentRemaining, paymentRemaining,
managementFeeRate, managementFeeRate,
loanScale, loanScale);
j);
JLOG(j.debug()) << "new periodic payment: " JLOG(j.debug()) << "new periodic payment: "
<< newLoanProperties.periodicPayment << newLoanProperties.periodicPayment
@@ -527,28 +547,21 @@ tryOverpayment(
auto const deltas = rounded - newRounded; auto const deltas = rounded - newRounded;
// The change in loan management fee is equal to the change between the old
// and the new outstanding management fees
XRPL_ASSERT_PARTS(
deltas.managementFee ==
rounded.managementFeeDue - managementFeeOutstanding,
"ripple::detail::tryOverpayment",
"no fee change");
auto const hypotheticalValueOutstanding = auto const hypotheticalValueOutstanding =
rounded.valueOutstanding - deltas.principal; rounded.valueOutstanding - deltas.principal;
// Calculate how the loan's value changed due to the overpayment. // Calculate how the loan's value changed due to the overpayment.
// This should be negative (value decreased) or zero. A principal // This should be negative (value decreased) or zero. A principal
// overpayment should never increase the loan's value. // overpayment should never increase the loan's value.
auto const valueChange = newRounded.valueOutstanding - auto const valueChange =
hypotheticalValueOutstanding - deltas.managementFee; newRounded.valueOutstanding - hypotheticalValueOutstanding;
if (valueChange > 0) if (valueChange > 0)
{ {
JLOG(j.warn()) << "Principal overpayment would increase the value of " JLOG(j.warn()) << "Principal overpayment would increase the value of "
"the loan. Ignore the overpayment"; "the loan. Ignore the overpayment";
return Unexpected(tesSUCCESS); return Unexpected(tesSUCCESS);
} }
return LoanPaymentParts{ return LoanPaymentParts{
// Principal paid is the reduction in principal outstanding // Principal paid is the reduction in principal outstanding
.principalPaid = deltas.principal, .principalPaid = deltas.principal,
@@ -663,6 +676,12 @@ doOverpayment(
"ripple::detail::doOverpayment", "ripple::detail::doOverpayment",
"principal change agrees"); "principal change agrees");
XRPL_ASSERT_PARTS(
overpaymentComponents.trackedManagementFeeDelta ==
managementFeeOutstandingProxy - managementFeeOutstanding,
"ripple::detail::doOverpayment",
"no fee change");
// I'm not 100% sure the following asserts are correct. If in doubt, and // I'm not 100% sure the following asserts are correct. If in doubt, and
// everything else works, remove any that cause trouble. // everything else works, remove any that cause trouble.
@@ -693,6 +712,13 @@ doOverpayment(
"ripple::detail::doOverpayment", "ripple::detail::doOverpayment",
"principal payment matches"); "principal payment matches");
XRPL_ASSERT_PARTS(
loanPaymentParts.feePaid ==
overpaymentComponents.untrackedManagementFee +
overpaymentComponents.trackedManagementFeeDelta,
"ripple::detail::doOverpayment",
"fee payment matches");
// All validations passed, so update the proxy objects (which will // All validations passed, so update the proxy objects (which will
// modify the actual Loan ledger object) // modify the actual Loan ledger object)
totalValueOutstandingProxy = totalValueOutstanding; totalValueOutstandingProxy = totalValueOutstanding;
@@ -1205,12 +1231,17 @@ computeOverpaymentComponents(
// This interest doesn't follow the normal amortization schedule - it's // This interest doesn't follow the normal amortization schedule - it's
// a one-time charge for paying early. // a one-time charge for paying early.
// Equation (20) and (21) from XLS-66 spec, Section A-2 Equation Glossary // 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] = auto const [roundedOverpaymentInterest, roundedOverpaymentManagementFee] =
[&]() { [&]() {
auto const interest = roundToAsset( Number const interest =
asset, roundToAsset(asset, rawOverpaymentInterest, loanScale);
tenthBipsOfValue(overpayment, overpaymentInterestRate),
loanScale);
return detail::computeInterestAndFeeParts( return detail::computeInterestAndFeeParts(
asset, interest, managementFeeRate, loanScale); asset, interest, managementFeeRate, loanScale);
}(); }();
@@ -1404,6 +1435,31 @@ computeFullPaymentInterest(
return accruedInterest + prepaymentPenalty; 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 /* Calculates the theoretical loan state at maximum precision for a given point
* in the amortization schedule. * in the amortization schedule.
* *
@@ -1465,6 +1521,21 @@ computeRawLoanState(
.managementFeeDue = rawManagementFeeOutstanding}; .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. /* Constructs a LoanState from rounded Loan ledger object values.
* *
* This function creates a LoanState structure from the three tracked values * This function creates a LoanState structure from the three tracked values
@@ -1540,8 +1611,7 @@ computeLoanProperties(
std::uint32_t paymentInterval, std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining, std::uint32_t paymentsRemaining,
TenthBips32 managementFeeRate, TenthBips32 managementFeeRate,
std::int32_t minimumScale, std::int32_t minimumScale)
beast::Journal j)
{ {
auto const periodicRate = loanPeriodicRate(interestRate, paymentInterval); auto const periodicRate = loanPeriodicRate(interestRate, paymentInterval);
XRPL_ASSERT( XRPL_ASSERT(
@@ -1552,22 +1622,13 @@ computeLoanProperties(
principalOutstanding, periodicRate, paymentsRemaining); principalOutstanding, periodicRate, paymentsRemaining);
auto const [totalValueOutstanding, loanScale] = [&]() { auto const [totalValueOutstanding, loanScale] = [&]() {
// only round up if there should be interest NumberRoundModeGuard mg(Number::to_nearest);
NumberRoundModeGuard mg(
periodicRate == 0 ? Number::to_nearest : Number::upward);
// Use STAmount's internal rounding instead of roundToAsset, because // Use STAmount's internal rounding instead of roundToAsset, because
// we're going to use this result to determine the scale for all the // we're going to use this result to determine the scale for all the
// other rounding. // other rounding.
// Equation (30) from XLS-66 spec, Section A-2 Equation Glossary // Equation (30) from XLS-66 spec, Section A-2 Equation Glossary
STAmount amount{asset, periodicPayment * paymentsRemaining}; 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 // Base the loan scale on the total value, since that's going to be
// the biggest number involved (barring unusual parameters for late, // the biggest number involved (barring unusual parameters for late,
@@ -1582,10 +1643,7 @@ computeLoanProperties(
// We may need to truncate the total value because of the minimum // We may need to truncate the total value because of the minimum
// scale // scale
amount = roundToAsset(asset, amount, loanScale); amount = roundToAsset(asset, amount, loanScale, Number::to_nearest);
JLOG(j.debug()) << "computeLoanProperties: Loan scale:" << loanScale
<< ". Actual total value: " << amount << std::endl;
return std::make_pair(amount, loanScale); return std::make_pair(amount, loanScale);
}(); }();

View File

@@ -56,7 +56,7 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx)
if (!vault) if (!vault)
return tefINTERNAL; // LCOV_EXCL_LINE return tefINTERNAL; // LCOV_EXCL_LINE
auto const asset = vault->at(sfAsset); auto const asset = vault->at(sfAsset);
auto const scale = getAssetsTotalScale(vault); auto const scale = getVaultScale(vault);
auto const rounded = auto const rounded =
roundToAsset(asset, debtTotal, scale, Number::towards_zero); roundToAsset(asset, debtTotal, scale, Number::towards_zero);
@@ -67,7 +67,7 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx)
JLOG(ctx.j.warn()) << "LoanBrokerDelete: Debt total is " JLOG(ctx.j.warn()) << "LoanBrokerDelete: Debt total is "
<< debtTotal << ", which rounds to " << rounded; << debtTotal << ", which rounds to " << rounded;
return tecHAS_OBLIGATIONS; return tecHAS_OBLIGATIONS;
// LCOV_EXCL_STOP // LCOV_EXCL_START
} }
} }

View File

@@ -115,7 +115,7 @@ LoanDelete::doApply()
roundToAsset( roundToAsset(
vaultSle->at(sfAsset), vaultSle->at(sfAsset),
debtTotalProxy, debtTotalProxy,
getAssetsTotalScale(vaultSle), getVaultScale(vaultSle),
Number::towards_zero) == beast::zero, Number::towards_zero) == beast::zero,
"ripple::LoanDelete::doApply", "ripple::LoanDelete::doApply",
"last loan, remaining debt rounds to zero"); "last loan, remaining debt rounds to zero");

View File

@@ -106,7 +106,7 @@ LoanManage::preclaim(PreclaimContext const& ctx)
if (loanBrokerSle->at(sfOwner) != account) if (loanBrokerSle->at(sfOwner) != account)
{ {
JLOG(ctx.j.warn()) JLOG(ctx.j.warn())
<< "LoanBroker for Loan does not belong to the account. LoanManage " << "LoanBroker for Loan does not belong to the account. LoanModify "
"can only be submitted by the Loan Broker."; "can only be submitted by the Loan Broker.";
return tecNO_PERMISSION; return tecNO_PERMISSION;
} }
@@ -178,7 +178,7 @@ LoanManage::defaultLoan(
// The vault may be at a different scale than the loan. Reduce rounding // 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 // errors during the accounting by rounding some of the values to that
// scale. // scale.
auto const vaultScale = getAssetsTotalScale(vaultSle); auto const vaultScale = getVaultScale(vaultSle);
{ {
// Decrease the Total Value of the Vault: // Decrease the Total Value of the Vault:
@@ -242,11 +242,7 @@ LoanManage::defaultLoan(
return tefBAD_LEDGER; return tefBAD_LEDGER;
// LCOV_EXCL_STOP // LCOV_EXCL_STOP
} }
adjustImpreciseNumber( vaultLossUnrealizedProxy -= totalDefaultAmount;
vaultLossUnrealizedProxy,
-totalDefaultAmount,
vaultAsset,
vaultScale);
} }
view.update(vaultSle); view.update(vaultSle);
} }
@@ -254,9 +250,11 @@ LoanManage::defaultLoan(
// Update the LoanBroker object: // Update the LoanBroker object:
{ {
auto const asset = *vaultSle->at(sfAsset);
// Decrease the Debt of the LoanBroker: // Decrease the Debt of the LoanBroker:
adjustImpreciseNumber( adjustImpreciseNumber(
brokerDebtTotalProxy, -totalDefaultAmount, vaultAsset, vaultScale); brokerDebtTotalProxy, -totalDefaultAmount, asset, vaultScale);
// Decrease the First-Loss Capital Cover Available: // Decrease the First-Loss Capital Cover Available:
auto coverAvailableProxy = brokerSle->at(sfCoverAvailable); auto coverAvailableProxy = brokerSle->at(sfCoverAvailable);
if (coverAvailableProxy < defaultCovered) if (coverAvailableProxy < defaultCovered)
@@ -299,20 +297,13 @@ LoanManage::impairLoan(
ApplyView& view, ApplyView& view,
SLE::ref loanSle, SLE::ref loanSle,
SLE::ref vaultSle, SLE::ref vaultSle,
Asset const& vaultAsset,
beast::Journal j) beast::Journal j)
{ {
Number const lossUnrealized = owedToVault(loanSle); 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") // Update the Vault object(set "paper loss")
auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized); auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
adjustImpreciseNumber( vaultLossUnrealizedProxy += lossUnrealized;
vaultLossUnrealizedProxy, lossUnrealized, vaultAsset, vaultScale);
if (vaultLossUnrealizedProxy > if (vaultLossUnrealizedProxy >
vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable)) vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable))
{ {
@@ -343,14 +334,8 @@ LoanManage::unimpairLoan(
ApplyView& view, ApplyView& view,
SLE::ref loanSle, SLE::ref loanSle,
SLE::ref vaultSle, SLE::ref vaultSle,
Asset const& vaultAsset,
beast::Journal j) 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") // Update the Vault object(clear "paper loss")
auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized); auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
Number const lossReversed = owedToVault(loanSle); Number const lossReversed = owedToVault(loanSle);
@@ -362,10 +347,7 @@ LoanManage::unimpairLoan(
return tefBAD_LEDGER; return tefBAD_LEDGER;
// LCOV_EXCL_STOP // LCOV_EXCL_STOP
} }
// Reverse the "paper loss" vaultLossUnrealizedProxy -= lossReversed;
adjustImpreciseNumber(
vaultLossUnrealizedProxy, -lossReversed, vaultAsset, vaultScale);
view.update(vaultSle); view.update(vaultSle);
// Update the Loan object // Update the Loan object
@@ -421,14 +403,12 @@ LoanManage::doApply()
} }
else if (tx.isFlag(tfLoanImpair)) else if (tx.isFlag(tfLoanImpair))
{ {
if (auto const ter = if (auto const ter = impairLoan(view, loanSle, vaultSle, j_))
impairLoan(view, loanSle, vaultSle, vaultAsset, j_))
return ter; return ter;
} }
else if (tx.isFlag(tfLoanUnimpair)) else if (tx.isFlag(tfLoanUnimpair))
{ {
if (auto const ter = if (auto const ter = unimpairLoan(view, loanSle, vaultSle, j_))
unimpairLoan(view, loanSle, vaultSle, vaultAsset, j_))
return ter; return ter;
} }

View File

@@ -44,7 +44,6 @@ public:
ApplyView& view, ApplyView& view,
SLE::ref loanSle, SLE::ref loanSle,
SLE::ref vaultSle, SLE::ref vaultSle,
Asset const& vaultAsset,
beast::Journal j); beast::Journal j);
/** Helper function that might be needed by other transactors /** Helper function that might be needed by other transactors
@@ -54,7 +53,6 @@ public:
ApplyView& view, ApplyView& view,
SLE::ref loanSle, SLE::ref loanSle,
SLE::ref vaultSle, SLE::ref vaultSle,
Asset const& vaultAsset,
beast::Journal j); beast::Journal j);
TER TER

View File

@@ -305,7 +305,7 @@ LoanPay::doApply()
// change will be discarded. // change will be discarded.
if (loanSle->isFlag(lsfLoanImpaired)) if (loanSle->isFlag(lsfLoanImpaired))
{ {
LoanManage::unimpairLoan(view, loanSle, vaultSle, asset, j_); LoanManage::unimpairLoan(view, loanSle, vaultSle, j_);
} }
LoanPaymentType const paymentType = [&tx]() { LoanPaymentType const paymentType = [&tx]() {
@@ -379,7 +379,7 @@ LoanPay::doApply()
// The vault may be at a different scale than the loan. Reduce rounding // 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. // errors during the payment by rounding some of the values to that scale.
auto const vaultScale = getAssetsTotalScale(vaultSle); auto const vaultScale = assetsTotalProxy.value().exponent();
auto const totalPaidToVaultRaw = auto const totalPaidToVaultRaw =
paymentParts->principalPaid + paymentParts->interestPaid; paymentParts->principalPaid + paymentParts->interestPaid;

View File

@@ -383,7 +383,7 @@ LoanSet::doApply()
auto vaultAvailableProxy = vaultSle->at(sfAssetsAvailable); auto vaultAvailableProxy = vaultSle->at(sfAssetsAvailable);
auto vaultTotalProxy = vaultSle->at(sfAssetsTotal); auto vaultTotalProxy = vaultSle->at(sfAssetsTotal);
auto const vaultScale = getAssetsTotalScale(vaultSle); auto const vaultScale = getVaultScale(vaultSle);
if (vaultAvailableProxy < principalRequested) if (vaultAvailableProxy < principalRequested)
{ {
JLOG(j_.warn()) JLOG(j_.warn())
@@ -404,8 +404,7 @@ LoanSet::doApply()
paymentInterval, paymentInterval,
paymentTotal, paymentTotal,
TenthBips16{brokerSle->at(sfManagementFeeRate)}, TenthBips16{brokerSle->at(sfManagementFeeRate)},
vaultScale, vaultScale);
j_);
// Check that relevant values won't lose precision. This is mostly only // Check that relevant values won't lose precision. This is mostly only
// relevant for IOU assets. // relevant for IOU assets.
@@ -441,10 +440,7 @@ LoanSet::doApply()
{ {
// LCOV_EXCL_START // LCOV_EXCL_START
JLOG(j_.warn()) 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; return tecINTERNAL;
// LCOV_EXCL_STOP // LCOV_EXCL_STOP
} }