[wip] added test for edge case balance

This commit is contained in:
Vladislav Vysokikh
2026-01-20 15:47:34 +00:00
parent fef83c296e
commit 1bb39b9b4f
2 changed files with 128 additions and 28 deletions

View File

@@ -2069,8 +2069,18 @@ rippleCreditIOU(
&& !(uFlags & (bSenderHigh ? lsfLowReserve : lsfHighReserve)))
// Receiver reserve is not set.
{
adjustOwnerCount(
view, view.peek(keylet::account(uReceiverID)), 1, j);
auto const sleReceiver = view.peek(keylet::account(uReceiverID));
auto const ownerCount = sleReceiver->getFieldU32(sfOwnerCount);
// Trust lines use "owner count < 2" reserve exception
XRPAmount const reserve = (ownerCount < 2)
? XRPAmount(beast::zero)
: view.fees().accountReserve(ownerCount + 1);
if (sleReceiver->getFieldAmount(sfBalance) < reserve)
return tecINSUFFICIENT_RESERVE;
adjustOwnerCount(view, sleReceiver, 1, j);
sleRippleState->setFieldU32(
sfFlags,

View File

@@ -453,7 +453,6 @@ class TrustAndBalance_test : public beast::unit_test::suite
auto const USD = gw["USD"];
// Helper lambda to check alice's owner count and reserve flag
auto checkAlice = [&](std::uint32_t expectedOwnerCount,
bool expectedReserveSet) {
BEAST_EXPECT(
@@ -468,24 +467,20 @@ class TrustAndBalance_test : public beast::unit_test::suite
env.fund(XRP(10000), gw, alice, market);
env.close();
// create trust lines
env(trust(alice, USD(1000)));
env(trust(market, USD(1000)));
env.close();
checkAlice(1, true);
// gw issues USD to alice and market
env(pay(gw, alice, USD(100)));
env(pay(gw, market, USD(1000)));
env.close();
checkAlice(1, true);
// gw clears asfDefaultRipple
env(fclear(gw, asfDefaultRipple));
env.close();
// alice clears trustline limit. This should trigger the check for
// default ripple state and charge gw for non-default state
// alice clears trustline limit, triggering default ripple state check
env(trust(alice, USD(0)));
env.close();
checkAlice(1, true);
@@ -495,13 +490,11 @@ class TrustAndBalance_test : public beast::unit_test::suite
env.close();
checkAlice(0, false);
// market offers USD for XRP
env(offer(market, XRP(100), USD(100)));
env.close();
// Now alice acquires balance again by placing an offer (direct payment
// would fail because alice set limit to 0). This offer will cross
// market's. Balance goes from 0 to positive - this triggers the new
// increment logic
// alice acquires balance via offer crossing (direct payment would fail
// because limit is 0). Balance 0 -> positive triggers increment logic
env(offer(alice, USD(50), XRP(50)));
env.close();
@@ -512,31 +505,128 @@ class TrustAndBalance_test : public beast::unit_test::suite
checkAlice(0, false);
}
void
testInsufficientReserveOnOwnerCountChange(FeatureBitset features)
{
testcase("Insufficient Reserve on Owner Count Change");
using namespace test::jtx;
Env env{*this, features};
Account gw{"gateway"};
Account alice{"alice"};
Account market{"market"};
bool const aliceHigh = alice.id() > gw.id();
auto const USD = gw["USD"];
auto const fee = env.current()->fees().base;
auto const acctReserve = env.current()->fees().accountReserve(0);
auto const incReserve = env.current()->fees().increment;
auto checkAliceUSD = [&](std::uint32_t expectedOwnerCount,
bool expectedReserveSet) {
BEAST_EXPECT(
env.le(alice)->getFieldU32(sfOwnerCount) == expectedOwnerCount);
auto const line =
env.le(keylet::line(alice, gw, to_currency("USD")));
BEAST_EXPECT(
line->isFlag(aliceHigh ? lsfHighReserve : lsfLowReserve) ==
expectedReserveSet);
};
env.fund(XRP(10000), gw, market);
// Fund alice with enough for reserve + increment + 50 XRP + fees
// alice performs 6 transactions: 2x trust, 2x pay, 1x pay excess, 1x
// offer
env.fund(acctReserve + incReserve + XRP(50) + fee * 6, alice);
env.close();
// Create trust lines - alice needs owner count >= 2 for reserve check
env(trust(alice, USD(1000)));
env(trust(market, USD(1000)));
env.close();
checkAliceUSD(1, true);
env(pay(gw, alice, USD(100)));
env(pay(gw, market, USD(1000)));
env.close();
env(fclear(gw, asfDefaultRipple));
env.close();
env(trust(alice, USD(0)));
env.close();
checkAliceUSD(1, true);
// alice clears the USD balance causing decrement in owners count
env(pay(alice, gw, USD(100)));
env.close();
checkAliceUSD(0, false);
// Reduce alice's balance to 50 XRP + fee
// Owner count is 0
auto aliceBalance = env.balance(alice);
auto targetBalance = acctReserve + XRP(50) + fee;
if (aliceBalance > targetBalance)
{
auto excess = aliceBalance - targetBalance - fee;
if (excess > drops(0))
env(pay(alice, gw, excess));
env.close();
}
aliceBalance = env.balance(alice);
BEAST_EXPECT(aliceBalance == acctReserve + XRP(50) + fee);
env(offer(market, XRP(100), USD(100)));
env.close();
// alice's offer would cross, causing USD balance 0 ->
// positive. With fix: owner count increment requires reserve, so fails.
// Without fix: no owner count change, so succeeds.
if (features[fixTrustLineOwnerCount])
{
env(offer(alice, USD(50), XRP(50)));
env.close();
env.require(balance(alice, USD(0)), offers(alice, 1));
checkAliceUSD(1, false);
}
else
{
env(offer(alice, USD(50), XRP(50)));
env.close();
env.require(balance(alice, USD(50)), offers(alice, 0));
checkAliceUSD(0, false);
}
}
public:
void
run() override
{
testTrustNonexistent();
testCreditLimit();
// testTrustNonexistent();
// testCreditLimit();
auto testWithFeatures = [this](FeatureBitset features) {
testPayNonexistent(features);
testDirectRipple(features);
testWithTransferFee(false, false, features);
testWithTransferFee(false, true, features);
testWithTransferFee(true, false, features);
testWithTransferFee(true, true, features);
testWithPath(features);
testIndirect(features);
testIndirectMultiPath(true, features);
testIndirectMultiPath(false, features);
testInvoiceID(features);
testOwnerCountOnBalanceChange(features);
// testPayNonexistent(features);
// testDirectRipple(features);
// testWithTransferFee(false, false, features);
// testWithTransferFee(false, true, features);
// testWithTransferFee(true, false, features);
// testWithTransferFee(true, true, features);
// testWithPath(features);
// testIndirect(features);
// testIndirectMultiPath(true, features);
// testIndirectMultiPath(false, features);
// testInvoiceID(features);
// testOwnerCountOnBalanceChange(features);
testInsufficientReserveOnOwnerCountChange(features);
};
using namespace test::jtx;
auto const sa = testable_amendments();
testWithFeatures(sa - featurePermissionedDEX);
// testWithFeatures(sa - featurePermissionedDEX);
testWithFeatures(sa - fixTrustLineOwnerCount);
testWithFeatures(sa);
}