mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-20 02:28:07 +00:00
test: Cover YieldUnrealized net of management fee on LoanSet
Check YieldUnrealized against the loans' TotalValueOutstanding - PrincipalOutstanding - ManagementFeeOutstanding in the shared FixedPrecision Vault helper, and add LoanSet cases with a nonzero management fee on IOU, XRP and MPT FixedPrecision Vaults.
This commit is contained in:
@@ -79,6 +79,7 @@ protected:
|
||||
|
||||
// Cover rates default to 0%, so no cover deposit is needed; pass non-zero
|
||||
// values to exercise First-Loss Capital. scale overrides the default sfScale.
|
||||
// managementFeeRate defaults to 0%, so interest due equals gross interest.
|
||||
static LendingFixture
|
||||
setupLendingVault(
|
||||
test::jtx::Env& env,
|
||||
@@ -89,7 +90,8 @@ protected:
|
||||
std::chrono::seconds investmentWindow = std::chrono::seconds{1'000'000},
|
||||
std::optional<std::uint8_t> scale = std::nullopt,
|
||||
TenthBips32 coverRateMinimum = TenthBips32(0),
|
||||
TenthBips32 coverRateLiquidation = TenthBips32(0))
|
||||
TenthBips32 coverRateLiquidation = TenthBips32(0),
|
||||
TenthBips16 managementFeeRate = TenthBips16(0))
|
||||
{
|
||||
using namespace test::jtx;
|
||||
using namespace loan_broker;
|
||||
@@ -115,7 +117,8 @@ protected:
|
||||
keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
|
||||
env(set(owner, vaultKeylet.key),
|
||||
kCoverRateMinimum(coverRateMinimum),
|
||||
kCoverRateLiquidation(coverRateLiquidation));
|
||||
kCoverRateLiquidation(coverRateLiquidation),
|
||||
kManagementFeeRate(managementFeeRate));
|
||||
env.close();
|
||||
|
||||
return {
|
||||
|
||||
@@ -95,6 +95,102 @@ class LoanSetFixedPrecision_test : public LoanFixedPrecisionBase
|
||||
}
|
||||
}
|
||||
|
||||
// YieldUnrealized books only the Vault's share of the interest: gross
|
||||
// interest less the broker's management fee. Covers IOU, XRP and MPT
|
||||
// Vaults, since XRP and MPT use base scale 0.
|
||||
void
|
||||
testOriginationBooksInterestDueNetOfManagementFee()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
enum class AssetKind { Iou, Xrp, Mpt };
|
||||
struct Row
|
||||
{
|
||||
char const* name = nullptr;
|
||||
AssetKind kind = AssetKind::Iou;
|
||||
Number deposit;
|
||||
Number principal;
|
||||
};
|
||||
Row const rows[] = {
|
||||
{.name =
|
||||
"LoanSet on an IOU FixedPrecision vault books interest net of the management fee",
|
||||
.kind = AssetKind::Iou,
|
||||
.deposit = Number{1'000},
|
||||
.principal = Number{300}},
|
||||
{.name =
|
||||
"LoanSet on an XRP FixedPrecision vault books interest net of the management fee",
|
||||
.kind = AssetKind::Xrp,
|
||||
.deposit = Number{10'000'000},
|
||||
.principal = Number{3'000'000}},
|
||||
{.name =
|
||||
"LoanSet on an MPT FixedPrecision vault books interest net of the management fee",
|
||||
.kind = AssetKind::Mpt,
|
||||
.deposit = Number{10'000'000},
|
||||
.principal = Number{3'000'000}},
|
||||
};
|
||||
for (auto const& row : rows)
|
||||
{
|
||||
testcase(row.name);
|
||||
|
||||
constexpr std::uint32_t paymentTotal = 2;
|
||||
constexpr std::uint32_t paymentInterval = 30 * 24 * 60 * 60;
|
||||
TenthBips32 const interestRate{12'000};
|
||||
TenthBips16 const managementFeeRate{10'000};
|
||||
|
||||
Env env(*this, features());
|
||||
AssetAccounts accounts;
|
||||
switch (row.kind)
|
||||
{
|
||||
case AssetKind::Iou:
|
||||
accounts = setupIou(env);
|
||||
break;
|
||||
case AssetKind::Xrp:
|
||||
env.fund(XRP(1'000'000), accounts.owner, accounts.depositor);
|
||||
env.close();
|
||||
accounts.asset = xrpIssue();
|
||||
break;
|
||||
case AssetKind::Mpt:
|
||||
accounts = setupMpt(
|
||||
env,
|
||||
{.maxAmt = 100'000'000, .withDepositor = true, .holderFunds = 10'000'000});
|
||||
break;
|
||||
}
|
||||
|
||||
auto const fixture = setupLendingVault(
|
||||
env,
|
||||
accounts.owner,
|
||||
accounts.depositor,
|
||||
accounts.asset,
|
||||
row.deposit,
|
||||
std::chrono::seconds{10'000'000},
|
||||
std::nullopt,
|
||||
TenthBips32(0),
|
||||
TenthBips32(0),
|
||||
managementFeeRate);
|
||||
|
||||
auto const loanKeylet = openLoan(
|
||||
env, fixture, row.principal, paymentTotal, interestRate, 0, paymentInterval);
|
||||
|
||||
auto const loanSle = env.le(loanKeylet);
|
||||
auto const vaultSle = env.le(fixture.vaultKeylet);
|
||||
if (!BEAST_EXPECT(loanSle && vaultSle))
|
||||
return;
|
||||
|
||||
Number const principal = loanSle->at(sfPrincipalOutstanding);
|
||||
Number const grossInterest = loanSle->at(sfTotalValueOutstanding) - principal;
|
||||
Number const managementFee = loanSle->at(sfManagementFeeOutstanding);
|
||||
BEAST_EXPECT(principal == row.principal);
|
||||
BEAST_EXPECT(managementFee > beast::kZero);
|
||||
BEAST_EXPECT(grossInterest > managementFee);
|
||||
|
||||
BEAST_EXPECT(vaultSle->at(sfAssetsDeployed) == row.principal);
|
||||
BEAST_EXPECT(Number(vaultSle->at(sfYieldUnrealized)) == grossInterest - managementFee);
|
||||
// Origination books principal as debt only; AssetsTotal is unchanged.
|
||||
BEAST_EXPECT(vaultSle->at(sfAssetsTotal) == row.deposit);
|
||||
checkVaultLoanSums(env, fixture, {loanKeylet}, row.name);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testLendingClawbackAndWithdrawWhileLoanOpen()
|
||||
{
|
||||
@@ -649,6 +745,7 @@ public:
|
||||
run() override
|
||||
{
|
||||
testAssetsDeployedOrigination();
|
||||
testOriginationBooksInterestDueNetOfManagementFee();
|
||||
testLendingClawbackAndWithdrawWhileLoanOpen();
|
||||
testLendingFinalWithdrawalWhileLoanOpen();
|
||||
testLendingFullClawbackClampsInsteadOfHasObligations();
|
||||
|
||||
@@ -72,6 +72,8 @@ namespace xrpl::test {
|
||||
// - AssetsDeployed exactly equals the sum of the given loans'
|
||||
// PrincipalOutstanding, and (when brokerKeylets is non-empty) also
|
||||
// exactly equals the sum of the given brokers' DebtTotal;
|
||||
// - YieldUnrealized exactly equals the sum of the given loans'
|
||||
// TotalValueOutstanding - PrincipalOutstanding - ManagementFeeOutstanding;
|
||||
// - stored AssetsTotal <= derived getAssetsTotal, with the gap under one
|
||||
// live unit (equal below coarsening), since the stored field is a
|
||||
// Downward-rounded cache;
|
||||
@@ -106,15 +108,24 @@ checkFixedPrecisionVaultAssetsDeployed(
|
||||
Number const vaultAssetsDeployed = vaultSle->at(sfAssetsDeployed);
|
||||
|
||||
Number principalSum{0};
|
||||
Number scheduledInterestSum{0};
|
||||
for (auto const& loanKeylet : loanKeylets)
|
||||
{
|
||||
if (auto const loanSle = env.le(loanKeylet))
|
||||
{
|
||||
principalSum += loanSle->at(sfPrincipalOutstanding);
|
||||
scheduledInterestSum += loanSle->at(sfTotalValueOutstanding) -
|
||||
loanSle->at(sfPrincipalOutstanding) - loanSle->at(sfManagementFeeOutstanding);
|
||||
}
|
||||
}
|
||||
suite.expect(
|
||||
vaultAssetsDeployed == principalSum,
|
||||
(label.empty() ? "" : label + ": ") +
|
||||
std::string{"AssetsDeployed vs PrincipalOutstanding sum mismatch"});
|
||||
suite.expect(
|
||||
Number(vaultSle->at(sfYieldUnrealized)) == scheduledInterestSum,
|
||||
(label.empty() ? "" : label + ": ") +
|
||||
std::string{"YieldUnrealized vs scheduled interest sum mismatch"});
|
||||
|
||||
if (!brokerKeylets.empty())
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user