Skip to content

Commit 33027ef

Browse files
Tapanitoximinez
authored andcommitted
Fix bugs: frozen pseudo-account, and FLC cutoff (#6170)
- Fixes LoanManage tfBAD_LEDGER case by capping the amount of FLC to use to cover a loss at the amount of cover available. - Check if the Vault pseudo-account is frozen in LoanBrokerSet
1 parent e5a92fb commit 33027ef

File tree

4 files changed

+160
-1
lines changed

4 files changed

+160
-1
lines changed

src/test/app/LoanBroker_test.cpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1595,6 +1595,32 @@ class LoanBroker_test : public beast::unit_test::suite
15951595
}
15961596
}
15971597

1598+
void
1599+
testAMB06_VaultFreezeCheckMissing()
1600+
{
1601+
testcase << "RIPD-4466 - LoanBrokerSet disallows frozen vaults";
1602+
using namespace jtx;
1603+
Env env(*this);
1604+
1605+
Account const issuer{"issuer"}, lender{"lender"}, borrower{"borrower"};
1606+
env.fund(XRP(20'000), issuer, lender, borrower);
1607+
auto const IOU = issuer["IOU"];
1608+
1609+
Vault vault{env};
1610+
auto [tx, vaultKeylet] =
1611+
vault.create({.owner = lender, .asset = IOU.asset()});
1612+
env(tx);
1613+
env.close();
1614+
1615+
// Get vault pseudo-account and FREEZE it
1616+
auto const vaultSle = env.le(vaultKeylet);
1617+
auto const vaultPseudo = vaultSle->at(sfAccount);
1618+
auto const vaultPseudoAcct = Account("VaultPseudo", vaultPseudo);
1619+
env(trust(issuer, vaultPseudoAcct["IOU"](0), tfSetFreeze));
1620+
1621+
env(loanBroker::set(lender, vaultKeylet.key), ter(tecFROZEN));
1622+
}
1623+
15981624
public:
15991625
void
16001626
run() override
@@ -1612,6 +1638,7 @@ class LoanBroker_test : public beast::unit_test::suite
16121638
testRequireAuth();
16131639

16141640
testRIPD4323();
1641+
testAMB06_VaultFreezeCheckMissing();
16151642

16161643
// TODO: Write clawback failure tests with an issuer / MPT that doesn't
16171644
// have the right flags set.

src/test/app/Loan_test.cpp

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
#include <xrpl/beast/xor_shift_engine.h>
1212
#include <xrpl/protocol/SField.h>
1313

14+
#include <chrono>
15+
1416
namespace ripple {
1517
namespace test {
1618

@@ -7486,6 +7488,125 @@ class Loan_test : public beast::unit_test::suite
74867488
env.close();
74877489
}
74887490

7491+
void
7492+
testSequentialFLCDepletion()
7493+
{
7494+
testcase << "First-Loss Capital Depletion on Sequential Defaults";
7495+
7496+
using namespace jtx;
7497+
using namespace loan;
7498+
using namespace loanBroker;
7499+
7500+
Env env(*this, all);
7501+
7502+
Account const issuer{"issuer"};
7503+
Account const lender{"lender"};
7504+
Account const borrowerA{"borrowerA"};
7505+
Account const borrowerB{"borrowerB"};
7506+
7507+
env.fund(XRP(1'000'000), issuer, lender, borrowerA, borrowerB);
7508+
env.close();
7509+
7510+
PrettyAsset const asset = xrpIssue();
7511+
auto const vaultDepositAmount =
7512+
asset(200'000); // Enough for 2 x 50k loans plus interest/fees
7513+
7514+
auto const brokerInfo = createVaultAndBroker(
7515+
env,
7516+
asset,
7517+
lender,
7518+
{
7519+
.vaultDeposit = vaultDepositAmount.value(),
7520+
.debtMax = 0,
7521+
.coverRateMin = TenthBips32(20000), // 20%
7522+
.coverDeposit = 21'000,
7523+
.managementFeeRate = TenthBips16(100), // 0.1%
7524+
.coverRateLiquidation = TenthBips32(100000),
7525+
});
7526+
auto const brokerKeylet = brokerInfo.brokerKeylet();
7527+
7528+
// Create two identical loans: each 50,000 XRP principal (scaled down to
7529+
// avoid funding issues) Total DebtTotal will be ~100,000 XRP (principal
7530+
// + interest) Formula will calculate cover as: 100% × (20% × 100,000) =
7531+
// 20,000 XRP So we need FLC = 20,000 XRP to be fully consumed by first
7532+
// default
7533+
auto const principalAmount = Number(50'000);
7534+
auto const loanPaymentInterval = 2592000; // 30 days
7535+
auto const loanGracePeriod = 604800; // 7 days
7536+
7537+
// Create Loan A
7538+
auto loanATx = env.jt(
7539+
set(borrowerA, brokerKeylet.key, principalAmount),
7540+
sig(sfCounterpartySignature, lender),
7541+
interestRate(TenthBips32(500)), // 5%
7542+
paymentTotal(12),
7543+
loan::paymentInterval(loanPaymentInterval),
7544+
loan::gracePeriod(loanGracePeriod),
7545+
fee(XRP(10))); // Sufficient fee for multi-sig transaction
7546+
env(loanATx);
7547+
env.close();
7548+
7549+
auto const loanAKeylet = keylet::loan(brokerKeylet.key, 1);
7550+
7551+
// Create Loan B
7552+
auto loanBTx = env.jt(
7553+
set(borrowerB, brokerKeylet.key, principalAmount),
7554+
sig(sfCounterpartySignature, lender),
7555+
interestRate(TenthBips32(500)), // 5%
7556+
paymentTotal(12),
7557+
loan::paymentInterval(loanPaymentInterval),
7558+
loan::gracePeriod(loanGracePeriod),
7559+
fee(XRP(10))); // Sufficient fee for multi-sig transaction
7560+
env(loanBTx);
7561+
env.close();
7562+
7563+
auto const loanBKeylet = keylet::loan(brokerKeylet.key, 2);
7564+
7565+
auto loanASle = env.le(loanAKeylet);
7566+
if (!BEAST_EXPECT(loanASle))
7567+
return;
7568+
7569+
// Advance time past grace period for both loans to be defaultable
7570+
auto const loanANextDue = loanASle->at(sfNextPaymentDueDate);
7571+
auto const loanAGrace = loanASle->at(sfGracePeriod);
7572+
env.close(std::chrono::seconds{loanANextDue + loanAGrace + 60});
7573+
7574+
env(manage(lender, loanAKeylet.key, tfLoanDefault), ter(tesSUCCESS));
7575+
env.close();
7576+
7577+
// Verify Loan A is defaulted
7578+
loanASle = env.le(loanAKeylet);
7579+
if (!BEAST_EXPECT(loanASle))
7580+
return;
7581+
BEAST_EXPECT(loanASle->isFlag(lsfLoanDefault));
7582+
BEAST_EXPECT(loanASle->at(sfPaymentRemaining) == 0);
7583+
7584+
// Check broker state after first default (from committed ledger)
7585+
auto brokerSle = env.le(brokerKeylet);
7586+
if (!BEAST_EXPECT(brokerSle))
7587+
return;
7588+
auto const afterFirstDebtTotal = brokerSle->at(sfDebtTotal);
7589+
auto const afterFirstCoverAvailable = brokerSle->at(sfCoverAvailable);
7590+
7591+
// DebtTotal should have decreased by Loan A's debt
7592+
BEAST_EXPECT(afterFirstDebtTotal == 50'134);
7593+
7594+
// CoverAvailable should have decreased significantly
7595+
BEAST_EXPECT(afterFirstCoverAvailable == 946);
7596+
7597+
env(manage(lender, loanBKeylet.key, tfLoanDefault), ter(tesSUCCESS));
7598+
7599+
brokerSle = env.le(brokerKeylet);
7600+
if (!BEAST_EXPECT(brokerSle))
7601+
return;
7602+
auto const afterSecondDebtTotal = brokerSle->at(sfDebtTotal);
7603+
auto const afterSecondCoverAvailable = brokerSle->at(sfCoverAvailable);
7604+
7605+
BEAST_EXPECT(afterSecondDebtTotal == 0);
7606+
7607+
BEAST_EXPECT(afterSecondCoverAvailable == 0);
7608+
}
7609+
74897610
public:
74907611
void
74917612
run() override
@@ -7538,6 +7659,7 @@ class Loan_test : public beast::unit_test::suite
75387659
testLoanPayBrokerOwnerUnauthorizedMPT();
75397660
testLoanPayBrokerOwnerNoPermissionedDomainMPT();
75407661
testLoanSetBrokerOwnerNoPermissionedDomainMPT();
7662+
testSequentialFLCDepletion();
75417663
}
75427664
};
75437665

src/xrpld/app/tx/detail/LoanBrokerSet.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,13 @@ LoanBrokerSet::preclaim(PreclaimContext const& ctx)
117117
}
118118
if (auto const ter = canAddHolding(ctx.view, sleVault->at(sfAsset)))
119119
return ter;
120+
121+
if (auto const ter = checkFrozen(
122+
ctx.view, sleVault->at(sfAccount), sleVault->at(sfAsset)))
123+
{
124+
JLOG(ctx.j.warn()) << "Vault pseudo-account is frozen.";
125+
return ter;
126+
}
120127
}
121128
return tesSUCCESS;
122129
}

src/xrpld/app/tx/detail/LoanManage.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ LoanManage::defaultLoan(
158158
auto const minimumCover =
159159
tenthBipsOfValue(brokerDebtTotalProxy.value(), coverRateMinimum);
160160
// Round the liquidation amount up, too
161-
return roundToAsset(
161+
auto const covered = roundToAsset(
162162
vaultAsset,
163163
/*
164164
* This formula is from the XLS-66 spec, section 3.2.3.2 (State
@@ -169,6 +169,9 @@ LoanManage::defaultLoan(
169169
tenthBipsOfValue(minimumCover, coverRateLiquidation),
170170
totalDefaultAmount),
171171
loanScale);
172+
auto const coverAvailable = *brokerSle->at(sfCoverAvailable);
173+
174+
return std::min(covered, coverAvailable);
172175
}();
173176

174177
auto const vaultDefaultAmount = totalDefaultAmount - defaultCovered;

0 commit comments

Comments
 (0)