Add the testPoC_UnsignedUnderflowOnFullPayAfterEarlyPeriodic

- Updated to reflect that the issue in RIPD-3971 has been resolved by
  commit 4396b77c4b
This commit is contained in:
Ed Hennis
2025-11-14 12:52:27 -05:00
parent c65a311ad1
commit b9fdf9e564

View File

@@ -5834,6 +5834,100 @@ protected:
BEAST_EXPECT(expectedWithPenalty - actualPaid == penaltyDue);
}
void
testLoanCoverMinimumRoundingExploit()
{
auto testLoanCoverMinimumRoundingExploit =
[&, this](Number const& principalRequest) {
testcase << "LoanBrokerCoverClawback drains cover via rounding"
<< " principalRequested="
<< to_string(principalRequest);
using namespace jtx;
using namespace loan;
using namespace loanBroker;
Env env(*this, all);
Account const issuer{"issuer"};
Account const lender{"lender"};
Account const borrower{"borrower"};
env.fund(XRP(1'000'000'000), issuer, lender, borrower);
env.close();
env(fset(issuer, asfAllowTrustLineClawback));
env.close();
PrettyAsset const asset = issuer[iouCurrency];
env(trust(lender, asset(2'000'0000)));
env(trust(borrower, asset(2'000'0000)));
env.close();
env(pay(issuer, lender, asset(2'000'0000)));
env.close();
BrokerParameters brokerParams{
.debtMax = 0, .coverRateMin = TenthBips32{10'000}};
BrokerInfo broker{
createVaultAndBroker(env, asset, lender, brokerParams)};
auto const loanSetFee = fee(env.current()->fees().base * 2);
auto createTx = env.jt(
set(borrower, broker.brokerID, principalRequest),
sig(sfCounterpartySignature, lender),
loanSetFee,
paymentInterval(600),
paymentTotal(1),
gracePeriod(60));
env(createTx);
env.close();
auto const brokerBefore =
env.le(keylet::loanbroker(broker.brokerID));
BEAST_EXPECT(brokerBefore);
if (!brokerBefore)
return;
Number const debtOutstanding = brokerBefore->at(sfDebtTotal);
Number const coverAvailableBefore =
brokerBefore->at(sfCoverAvailable);
BEAST_EXPECT(debtOutstanding > Number{});
BEAST_EXPECT(coverAvailableBefore > Number{});
log << "debt=" << to_string(debtOutstanding)
<< " cover_available=" << to_string(coverAvailableBefore);
env(coverClawback(issuer, 0), loanBrokerID(broker.brokerID));
env.close();
auto const brokerAfter =
env.le(keylet::loanbroker(broker.brokerID));
BEAST_EXPECT(brokerAfter);
if (!brokerAfter)
return;
Number const debtAfter = brokerAfter->at(sfDebtTotal);
// the debt has not changed
BEAST_EXPECT(debtAfter == debtOutstanding);
Number const coverAvailableAfter =
brokerAfter->at(sfCoverAvailable);
// since the cover rate min != 0, the cover available should not
// be zero
BEAST_EXPECT(coverAvailableAfter != Number{});
};
// Call the lambda with different principal values
testLoanCoverMinimumRoundingExploit(Number{1, -30}); // 1e-30 units
testLoanCoverMinimumRoundingExploit(Number{1, -20}); // 1e-20 units
testLoanCoverMinimumRoundingExploit(Number{1, -10}); // 1e-10 units
testLoanCoverMinimumRoundingExploit(Number{1, 1}); // 1e-10 units
}
#endif
void
testPoC_UnsignedUnderflowOnFullPayAfterEarlyPeriodic()
{
@@ -6057,8 +6151,7 @@ protected:
// Value-based proof: underflowed window yields a payoff larger than
// the clamped (non-underflow) reference.
BEAST_EXPECT(fullDue != fullDueClamped);
BEAST_EXPECT(fullDue > fullDueClamped);
BEAST_EXPECT(fullDue == fullDueClamped);
if (fullDue > fullDueClamped)
log << "PoC delta: overcharge (fullDue > clamped)" << std::endl;
}
@@ -6073,100 +6166,6 @@ protected:
}
}
void
testLoanCoverMinimumRoundingExploit()
{
auto testLoanCoverMinimumRoundingExploit =
[&, this](Number const& principalRequest) {
testcase << "LoanBrokerCoverClawback drains cover via rounding"
<< " principalRequested="
<< to_string(principalRequest);
using namespace jtx;
using namespace loan;
using namespace loanBroker;
Env env(*this, all);
Account const issuer{"issuer"};
Account const lender{"lender"};
Account const borrower{"borrower"};
env.fund(XRP(1'000'000'000), issuer, lender, borrower);
env.close();
env(fset(issuer, asfAllowTrustLineClawback));
env.close();
PrettyAsset const asset = issuer[iouCurrency];
env(trust(lender, asset(2'000'0000)));
env(trust(borrower, asset(2'000'0000)));
env.close();
env(pay(issuer, lender, asset(2'000'0000)));
env.close();
BrokerParameters brokerParams{
.debtMax = 0, .coverRateMin = TenthBips32{10'000}};
BrokerInfo broker{
createVaultAndBroker(env, asset, lender, brokerParams)};
auto const loanSetFee = fee(env.current()->fees().base * 2);
auto createTx = env.jt(
set(borrower, broker.brokerID, principalRequest),
sig(sfCounterpartySignature, lender),
loanSetFee,
paymentInterval(600),
paymentTotal(1),
gracePeriod(60));
env(createTx);
env.close();
auto const brokerBefore =
env.le(keylet::loanbroker(broker.brokerID));
BEAST_EXPECT(brokerBefore);
if (!brokerBefore)
return;
Number const debtOutstanding = brokerBefore->at(sfDebtTotal);
Number const coverAvailableBefore =
brokerBefore->at(sfCoverAvailable);
BEAST_EXPECT(debtOutstanding > Number{});
BEAST_EXPECT(coverAvailableBefore > Number{});
log << "debt=" << to_string(debtOutstanding)
<< " cover_available=" << to_string(coverAvailableBefore);
env(coverClawback(issuer, 0), loanBrokerID(broker.brokerID));
env.close();
auto const brokerAfter =
env.le(keylet::loanbroker(broker.brokerID));
BEAST_EXPECT(brokerAfter);
if (!brokerAfter)
return;
Number const debtAfter = brokerAfter->at(sfDebtTotal);
// the debt has not changed
BEAST_EXPECT(debtAfter == debtOutstanding);
Number const coverAvailableAfter =
brokerAfter->at(sfCoverAvailable);
// since the cover rate min != 0, the cover available should not
// be zero
BEAST_EXPECT(coverAvailableAfter != Number{});
};
// Call the lambda with different principal values
testLoanCoverMinimumRoundingExploit(Number{1, -30}); // 1e-30 units
testLoanCoverMinimumRoundingExploit(Number{1, -20}); // 1e-20 units
testLoanCoverMinimumRoundingExploit(Number{1, -10}); // 1e-10 units
testLoanCoverMinimumRoundingExploit(Number{1, 1}); // 1e-10 units
}
#endif
void
testDustManipulation()
{
@@ -6861,9 +6860,9 @@ public:
#if LOANTODO
testCoverDepositAllowsNonTransferableMPT();
testLoanPayLateFullPaymentBypassesPenalties();
testPoC_UnsignedUnderflowOnFullPayAfterEarlyPeriodic();
testLoanCoverMinimumRoundingExploit();
#endif
testPoC_UnsignedUnderflowOnFullPayAfterEarlyPeriodic();
testDisabled();
testSelfLoan();