From 65837f49e1378ab9714e6a9799094117deb3b7e4 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Mon, 2 Jun 2025 09:52:10 -0400 Subject: [PATCH 01/17] fix: Add AMMv1_3 amendment (#5203) * Add AMM bid/create/deposit/swap/withdraw/vote invariants: - Deposit, Withdrawal invariants: `sqrt(asset1Balance * asset2Balance) >= LPTokens`. - Bid: `sqrt(asset1Balance * asset2Balance) > LPTokens` and the pool balances don't change. - Create: `sqrt(asset1Balance * assetBalance2) == LPTokens`. - Swap: `asset1BalanceAfter * asset2BalanceAfter >= asset1BalanceBefore * asset2BalanceBefore` and `LPTokens` don't change. - Vote: `LPTokens` and pool balances don't change. - All AMM and swap transactions: amounts and tokens are greater than zero, except on withdrawal if all tokens are withdrawn. * Add AMM deposit and withdraw rounding to ensure AMM invariant: - On deposit, tokens out are rounded downward and deposit amount is rounded upward. - On withdrawal, tokens in are rounded upward and withdrawal amount is rounded downward. * Add Order Book Offer invariant to verify consumed amounts. Consumed amounts are less than the offer. * Fix Bid validation. `AuthAccount` can't have duplicate accounts or the submitter account. --- include/xrpl/protocol/Feature.h | 2 +- include/xrpl/protocol/IOUAmount.h | 6 + include/xrpl/protocol/Rules.h | 3 + include/xrpl/protocol/detail/features.macro | 1 + src/libxrpl/protocol/Rules.cpp | 8 + src/test/app/AMMClawback_test.cpp | 553 ++++++-- src/test/app/AMMExtended_test.cpp | 7 + src/test/app/AMM_test.cpp | 1354 +++++++++++++++---- src/test/jtx/AMM.h | 11 +- src/test/jtx/AMMTest.h | 14 + src/test/jtx/Env.h | 6 + src/test/jtx/impl/AMM.cpp | 19 +- src/test/jtx/impl/AMMTest.cpp | 34 +- src/test/rpc/AMMInfo_test.cpp | 186 +-- src/xrpld/app/misc/AMMHelpers.h | 151 ++- src/xrpld/app/misc/detail/AMMHelpers.cpp | 204 ++- src/xrpld/app/tx/detail/AMMBid.cpp | 19 +- src/xrpld/app/tx/detail/AMMDeposit.cpp | 156 ++- src/xrpld/app/tx/detail/AMMWithdraw.cpp | 143 +- src/xrpld/app/tx/detail/AMMWithdraw.h | 2 +- src/xrpld/app/tx/detail/InvariantCheck.cpp | 308 +++++ src/xrpld/app/tx/detail/InvariantCheck.h | 66 +- src/xrpld/app/tx/detail/Offer.h | 20 +- 23 files changed, 2667 insertions(+), 606 deletions(-) diff --git a/include/xrpl/protocol/Feature.h b/include/xrpl/protocol/Feature.h index ed07329307..3f7a39778c 100644 --- a/include/xrpl/protocol/Feature.h +++ b/include/xrpl/protocol/Feature.h @@ -80,7 +80,7 @@ namespace detail { // Feature.cpp. Because it's only used to reserve storage, and determine how // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than // the actual number of amendments. A LogicError on startup will verify this. -static constexpr std::size_t numFeatures = 113; +static constexpr std::size_t numFeatures = 114; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated diff --git a/include/xrpl/protocol/IOUAmount.h b/include/xrpl/protocol/IOUAmount.h index e89feb123d..d057b2b742 100644 --- a/include/xrpl/protocol/IOUAmount.h +++ b/include/xrpl/protocol/IOUAmount.h @@ -97,6 +97,12 @@ class IOUAmount : private boost::totally_ordered, static IOUAmount minPositiveAmount(); + + friend std::ostream& + operator<<(std::ostream& os, IOUAmount const& x) + { + return os << to_string(x); + } }; inline IOUAmount::IOUAmount(beast::Zero) diff --git a/include/xrpl/protocol/Rules.h b/include/xrpl/protocol/Rules.h index 7d6d9b0a9c..a6632c23ac 100644 --- a/include/xrpl/protocol/Rules.h +++ b/include/xrpl/protocol/Rules.h @@ -28,6 +28,9 @@ namespace ripple { +bool +isFeatureEnabled(uint256 const& feature); + class DigestAwareReadView; /** Rules controlling protocol behavior. */ diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 3f48ac7145..c04e40094c 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -31,6 +31,7 @@ // If you add an amendment here, then do not forget to increment `numFeatures` // in include/xrpl/protocol/Feature.h. +XRPL_FIX (AMMv1_3, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(HookAPISerializedType240, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(PermissionedDomains, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(DynamicNFT, Supported::no, VoteBehavior::DefaultNo) diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp index 7043acff96..89cb91a0a7 100644 --- a/src/libxrpl/protocol/Rules.cpp +++ b/src/libxrpl/protocol/Rules.cpp @@ -153,4 +153,12 @@ Rules::operator!=(Rules const& other) const { return !(*this == other); } + +bool +isFeatureEnabled(uint256 const& feature) +{ + auto const& rules = getCurrentTransactionRules(); + return rules && rules->enabled(feature); +} + } // namespace ripple diff --git a/src/test/app/AMMClawback_test.cpp b/src/test/app/AMMClawback_test.cpp index c547a537bf..9ccd59b580 100644 --- a/src/test/app/AMMClawback_test.cpp +++ b/src/test/app/AMMClawback_test.cpp @@ -581,8 +581,12 @@ class AMMClawback_test : public jtx::AMMTest AMM amm(env, alice, EUR(5000), USD(4000), ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - USD(4000), EUR(5000), IOUAmount{4472135954999580, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(4000), EUR(5000), IOUAmount{4472135954999580, -12})); + else + BEAST_EXPECT(amm.expectBalances( + USD(4000), EUR(5000), IOUAmount{4472135954999579, -12})); // gw clawback 1000 USD from the AMM pool env(amm::ammClawback(gw, alice, USD, EUR, USD(1000)), @@ -601,12 +605,20 @@ class AMMClawback_test : public jtx::AMMTest // 1000 USD and 1250 EUR was withdrawn from the AMM pool, so the // current balance is 3000 USD and 3750 EUR. - BEAST_EXPECT(amm.expectBalances( - USD(3000), EUR(3750), IOUAmount{3354101966249685, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(3000), EUR(3750), IOUAmount{3354101966249685, -12})); + else + BEAST_EXPECT(amm.expectBalances( + USD(3000), EUR(3750), IOUAmount{3354101966249684, -12})); // Alice has 3/4 of its initial lptokens Left. - BEAST_EXPECT( - amm.expectLPTokens(alice, IOUAmount{3354101966249685, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{3354101966249685, -12})); + else + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{3354101966249684, -12})); // gw clawback another 500 USD from the AMM pool. env(amm::ammClawback(gw, alice, USD, EUR, USD(500)), @@ -617,14 +629,21 @@ class AMMClawback_test : public jtx::AMMTest // AMM pool. env.require(balance(alice, gw["USD"](2000))); - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(2500000000000001), -12}, - STAmount{EUR, UINT64_C(3125000000000001), -12}, - IOUAmount{2795084971874738, -12})); - - BEAST_EXPECT( - env.balance(alice, EUR) == - STAmount(EUR, UINT64_C(2874999999999999), -12)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(2500000000000001), -12}, + STAmount{EUR, UINT64_C(3125000000000001), -12}, + IOUAmount{2795084971874738, -12})); + else + BEAST_EXPECT(amm.expectBalances( + USD(2500), EUR(3125), IOUAmount{2795084971874737, -12})); + + if (!features[fixAMMv1_3]) + BEAST_EXPECT( + env.balance(alice, EUR) == + STAmount(EUR, UINT64_C(2874999999999999), -12)); + else + BEAST_EXPECT(env.balance(alice, EUR) == EUR(2875)); // gw clawback small amount, 1 USD. env(amm::ammClawback(gw, alice, USD, EUR, USD(1)), ter(tesSUCCESS)); @@ -633,14 +652,21 @@ class AMMClawback_test : public jtx::AMMTest // Another 1 USD / 1.25 EUR was withdrawn. env.require(balance(alice, gw["USD"](2000))); - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(2499000000000002), -12}, - STAmount{EUR, UINT64_C(3123750000000002), -12}, - IOUAmount{2793966937885989, -12})); - - BEAST_EXPECT( - env.balance(alice, EUR) == - STAmount(EUR, UINT64_C(2876249999999998), -12)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(2499000000000002), -12}, + STAmount{EUR, UINT64_C(3123750000000002), -12}, + IOUAmount{2793966937885989, -12})); + else + BEAST_EXPECT(amm.expectBalances( + USD(2499), EUR(3123.75), IOUAmount{2793966937885987, -12})); + + if (!features[fixAMMv1_3]) + BEAST_EXPECT( + env.balance(alice, EUR) == + STAmount(EUR, UINT64_C(2'876'249999999998), -12)); + else + BEAST_EXPECT(env.balance(alice, EUR) == EUR(2876.25)); // gw clawback 4000 USD, exceeding the current balance. We // will clawback all. @@ -713,14 +739,26 @@ class AMMClawback_test : public jtx::AMMTest // gw2 creates AMM pool of XRP/EUR, alice and bob deposit XRP/EUR. AMM amm2(env, gw2, XRP(3000), EUR(1000), ter(tesSUCCESS)); - BEAST_EXPECT(amm2.expectBalances( - EUR(1000), XRP(3000), IOUAmount{1732050807568878, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm2.expectBalances( + EUR(1000), XRP(3000), IOUAmount{1732050807568878, -9})); + else + BEAST_EXPECT(amm2.expectBalances( + EUR(1000), XRP(3000), IOUAmount{1732050807568877, -9})); amm2.deposit(alice, EUR(1000), XRP(3000)); - BEAST_EXPECT(amm2.expectBalances( - EUR(2000), XRP(6000), IOUAmount{3464101615137756, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm2.expectBalances( + EUR(2000), XRP(6000), IOUAmount{3464101615137756, -9})); + else + BEAST_EXPECT(amm2.expectBalances( + EUR(2000), XRP(6000), IOUAmount{3464101615137754, -9})); amm2.deposit(bob, EUR(1000), XRP(3000)); - BEAST_EXPECT(amm2.expectBalances( - EUR(3000), XRP(9000), IOUAmount{5196152422706634, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm2.expectBalances( + EUR(3000), XRP(9000), IOUAmount{5196152422706634, -9})); + else + BEAST_EXPECT(amm2.expectBalances( + EUR(3000), XRP(9000), IOUAmount{5196152422706631, -9})); env.close(); auto aliceXrpBalance = env.balance(alice, XRP); @@ -743,10 +781,18 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT( expectLedgerEntryRoot(env, alice, aliceXrpBalance + XRP(1000))); - BEAST_EXPECT(amm.expectBalances( - USD(2500), XRP(5000), IOUAmount{3535533905932738, -9})); - BEAST_EXPECT( - amm.expectLPTokens(alice, IOUAmount{7071067811865480, -10})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(2500), XRP(5000), IOUAmount{3535533905932738, -9})); + else + BEAST_EXPECT(amm.expectBalances( + USD(2500), XRP(5000), IOUAmount{3535533905932737, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{7071067811865480, -10})); + else + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{7071067811865474, -10})); BEAST_EXPECT( amm.expectLPTokens(bob, IOUAmount{1414213562373095, -9})); @@ -760,14 +806,26 @@ class AMMClawback_test : public jtx::AMMTest // Bob gets 20 XRP back. BEAST_EXPECT( expectLedgerEntryRoot(env, bob, bobXrpBalance + XRP(20))); - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(2490000000000001), -12}, - XRP(4980), - IOUAmount{3521391770309008, -9})); - BEAST_EXPECT( - amm.expectLPTokens(alice, IOUAmount{7071067811865480, -10})); - BEAST_EXPECT( - amm.expectLPTokens(bob, IOUAmount{1400071426749365, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(2490000000000001), -12}, + XRP(4980), + IOUAmount{3521391770309008, -9})); + else + BEAST_EXPECT(amm.expectBalances( + USD(2'490), XRP(4980), IOUAmount{3521391770309006, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{7071067811865480, -10})); + else + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{7071067811865474, -10})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{1400071426749365, -9})); + else + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{1400071426749364, -9})); // gw2 clawback 200 EUR from amm2. env(amm::ammClawback(gw2, alice, EUR, XRP, EUR(200)), @@ -780,12 +838,24 @@ class AMMClawback_test : public jtx::AMMTest // Alice gets 600 XRP back. BEAST_EXPECT(expectLedgerEntryRoot( env, alice, aliceXrpBalance + XRP(1000) + XRP(600))); - BEAST_EXPECT(amm2.expectBalances( - EUR(2800), XRP(8400), IOUAmount{4849742261192859, -9})); - BEAST_EXPECT( - amm2.expectLPTokens(alice, IOUAmount{1385640646055103, -9})); - BEAST_EXPECT( - amm2.expectLPTokens(bob, IOUAmount{1732050807568878, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm2.expectBalances( + EUR(2800), XRP(8400), IOUAmount{4849742261192859, -9})); + else + BEAST_EXPECT(amm2.expectBalances( + EUR(2800), XRP(8400), IOUAmount{4849742261192856, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm2.expectLPTokens( + alice, IOUAmount{1385640646055103, -9})); + else + BEAST_EXPECT(amm2.expectLPTokens( + alice, IOUAmount{1385640646055102, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT( + amm2.expectLPTokens(bob, IOUAmount{1732050807568878, -9})); + else + BEAST_EXPECT( + amm2.expectLPTokens(bob, IOUAmount{1732050807568877, -9})); // gw claw back 1000 USD from alice in amm, which exceeds alice's // balance. This will clawback all the remaining LP tokens of alice @@ -798,17 +868,34 @@ class AMMClawback_test : public jtx::AMMTest env.require(balance(bob, gw["USD"](4000))); // Alice gets 1000 XRP back. - BEAST_EXPECT(expectLedgerEntryRoot( - env, - alice, - aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000))); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(expectLedgerEntryRoot( + env, + alice, + aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000))); + else + BEAST_EXPECT(expectLedgerEntryRoot( + env, + alice, + aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) - + XRPAmount{1})); BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); - BEAST_EXPECT( - amm.expectLPTokens(bob, IOUAmount{1400071426749365, -9})); - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(1990000000000001), -12}, - XRP(3980), - IOUAmount{2814284989122460, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{1400071426749365, -9})); + else + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{1400071426749364, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(1990000000000001), -12}, + XRP(3980), + IOUAmount{2814284989122460, -9})); + else + BEAST_EXPECT(amm.expectBalances( + USD(1'990), + XRPAmount{3'980'000'001}, + IOUAmount{2814284989122459, -9})); // gw clawback 1000 USD from bob in amm, which also exceeds bob's // balance in amm. All bob's lptoken in amm will be consumed, which @@ -820,10 +907,17 @@ class AMMClawback_test : public jtx::AMMTest env.require(balance(alice, gw["USD"](5000))); env.require(balance(bob, gw["USD"](4000))); - BEAST_EXPECT(expectLedgerEntryRoot( - env, - alice, - aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000))); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(expectLedgerEntryRoot( + env, + alice, + aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000))); + else + BEAST_EXPECT(expectLedgerEntryRoot( + env, + alice, + aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) - + XRPAmount{1})); BEAST_EXPECT(expectLedgerEntryRoot( env, bob, bobXrpBalance + XRP(20) + XRP(1980))); @@ -843,21 +937,32 @@ class AMMClawback_test : public jtx::AMMTest // Alice gets another 2400 XRP back, bob's XRP balance remains the // same. - BEAST_EXPECT(expectLedgerEntryRoot( - env, - alice, - aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) + - XRP(2400))); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(expectLedgerEntryRoot( + env, + alice, + aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) + + XRP(2400))); + else + BEAST_EXPECT(expectLedgerEntryRoot( + env, + alice, + aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) + + XRP(2400) - XRPAmount{1})); BEAST_EXPECT(expectLedgerEntryRoot( env, bob, bobXrpBalance + XRP(20) + XRP(1980))); // Alice now does not have any lptoken in amm2 BEAST_EXPECT(amm2.expectLPTokens(alice, IOUAmount(0))); - BEAST_EXPECT(amm2.expectBalances( - EUR(2000), XRP(6000), IOUAmount{3464101615137756, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm2.expectBalances( + EUR(2000), XRP(6000), IOUAmount{3464101615137756, -9})); + else + BEAST_EXPECT(amm2.expectBalances( + EUR(2000), XRP(6000), IOUAmount{3464101615137754, -9})); - // gw2 claw back 2000 EUR from bib in amm2, which exceeds bob's + // gw2 claw back 2000 EUR from bob in amm2, which exceeds bob's // balance. All bob's lptokens will be consumed, which corresponds // to 1000EUR / 3000 XRP. env(amm::ammClawback(gw2, bob, EUR, XRP, EUR(2000)), @@ -869,11 +974,18 @@ class AMMClawback_test : public jtx::AMMTest // Bob gets another 3000 XRP back. Alice's XRP balance remains the // same. - BEAST_EXPECT(expectLedgerEntryRoot( - env, - alice, - aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) + - XRP(2400))); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(expectLedgerEntryRoot( + env, + alice, + aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) + + XRP(2400))); + else + BEAST_EXPECT(expectLedgerEntryRoot( + env, + alice, + aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) + + XRP(2400) - XRPAmount{1})); BEAST_EXPECT(expectLedgerEntryRoot( env, bob, bobXrpBalance + XRP(20) + XRP(1980) + XRP(3000))); @@ -881,8 +993,12 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(amm2.expectLPTokens(alice, IOUAmount(0))); BEAST_EXPECT(amm2.expectLPTokens(bob, IOUAmount(0))); - BEAST_EXPECT(amm2.expectBalances( - EUR(1000), XRP(3000), IOUAmount{1732050807568878, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm2.expectBalances( + EUR(1000), XRP(3000), IOUAmount{1732050807568878, -9})); + else + BEAST_EXPECT(amm2.expectBalances( + EUR(1000), XRP(3000), IOUAmount{1732050807568877, -9})); } } @@ -940,21 +1056,45 @@ class AMMClawback_test : public jtx::AMMTest AMM amm(env, alice, EUR(5000), USD(4000), ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - USD(4000), EUR(5000), IOUAmount{4472135954999580, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(4000), EUR(5000), IOUAmount{4472135954999580, -12})); + else + BEAST_EXPECT(amm.expectBalances( + USD(4000), EUR(5000), IOUAmount{4472135954999579, -12})); amm.deposit(bob, USD(2000), EUR(2500)); - BEAST_EXPECT(amm.expectBalances( - USD(6000), EUR(7500), IOUAmount{6708203932499370, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(6000), EUR(7500), IOUAmount{6708203932499370, -12})); + else + BEAST_EXPECT(amm.expectBalances( + USD(6000), EUR(7500), IOUAmount{6708203932499368, -12})); amm.deposit(carol, USD(1000), EUR(1250)); - BEAST_EXPECT(amm.expectBalances( - USD(7000), EUR(8750), IOUAmount{7826237921249265, -12})); - - BEAST_EXPECT( - amm.expectLPTokens(alice, IOUAmount{4472135954999580, -12})); - BEAST_EXPECT( - amm.expectLPTokens(bob, IOUAmount{2236067977499790, -12})); - BEAST_EXPECT( - amm.expectLPTokens(carol, IOUAmount{1118033988749895, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(7000), EUR(8750), IOUAmount{7826237921249265, -12})); + else + BEAST_EXPECT(amm.expectBalances( + USD(7000), EUR(8750), IOUAmount{7826237921249262, -12})); + + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{4472135954999580, -12})); + else + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{4472135954999579, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{2236067977499790, -12})); + else + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{2236067977499789, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectLPTokens( + carol, IOUAmount{1118033988749895, -12})); + else + BEAST_EXPECT(amm.expectLPTokens( + carol, IOUAmount{1118033988749894, -12})); env.require(balance(alice, gw["USD"](2000))); env.require(balance(alice, gw2["EUR"](1000))); @@ -968,16 +1108,30 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(4999999999999999), -12}, - STAmount{EUR, UINT64_C(6249999999999999), -12}, - IOUAmount{5590169943749475, -12})); - - BEAST_EXPECT( - amm.expectLPTokens(alice, IOUAmount{4472135954999580, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(4999999999999999), -12}, + STAmount{EUR, UINT64_C(6249999999999999), -12}, + IOUAmount{5590169943749475, -12})); + else + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(5000000000000001), -12}, + STAmount{EUR, UINT64_C(6250000000000001), -12}, + IOUAmount{5590169943749473, -12})); + + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{4472135954999580, -12})); + else + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{4472135954999579, -12})); BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount(0))); - BEAST_EXPECT( - amm.expectLPTokens(carol, IOUAmount{1118033988749895, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectLPTokens( + carol, IOUAmount{1118033988749895, -12})); + else + BEAST_EXPECT(amm.expectLPTokens( + carol, IOUAmount{1118033988749894, -12})); // Bob will get 2500 EUR back. env.require(balance(alice, gw["USD"](2000))); @@ -986,9 +1140,14 @@ class AMMClawback_test : public jtx::AMMTest env.balance(bob, USD) == STAmount(USD, UINT64_C(3000000000000000), -12)); - BEAST_EXPECT( - env.balance(bob, EUR) == - STAmount(EUR, UINT64_C(5000000000000001), -12)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT( + env.balance(bob, EUR) == + STAmount(EUR, UINT64_C(5000000000000001), -12)); + else + BEAST_EXPECT( + env.balance(bob, EUR) == + STAmount(EUR, UINT64_C(4999999999999999), -12)); env.require(balance(carol, gw["USD"](3000))); env.require(balance(carol, gw2["EUR"](2750))); @@ -996,13 +1155,23 @@ class AMMClawback_test : public jtx::AMMTest env(amm::ammClawback(gw2, carol, EUR, USD, std::nullopt), ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(3999999999999999), -12}, - STAmount{EUR, UINT64_C(4999999999999999), -12}, - IOUAmount{4472135954999580, -12})); - - BEAST_EXPECT( - amm.expectLPTokens(alice, IOUAmount{4472135954999580, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(3999999999999999), -12}, + STAmount{EUR, UINT64_C(4999999999999999), -12}, + IOUAmount{4472135954999580, -12})); + else + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(4000000000000001), -12}, + STAmount{EUR, UINT64_C(5000000000000002), -12}, + IOUAmount{4472135954999579, -12})); + + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{4472135954999580, -12})); + else + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{4472135954999579, -12})); BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount(0))); BEAST_EXPECT(amm.expectLPTokens(carol, IOUAmount(0))); @@ -1041,14 +1210,26 @@ class AMMClawback_test : public jtx::AMMTest // gw creates AMM pool of XRP/USD, alice and bob deposit XRP/USD. AMM amm(env, gw, XRP(2000), USD(10000), ter(tesSUCCESS)); - BEAST_EXPECT(amm.expectBalances( - USD(10000), XRP(2000), IOUAmount{4472135954999580, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(10000), XRP(2000), IOUAmount{4472135954999580, -9})); + else + BEAST_EXPECT(amm.expectBalances( + USD(10000), XRP(2000), IOUAmount{4472135954999579, -9})); amm.deposit(alice, USD(1000), XRP(200)); - BEAST_EXPECT(amm.expectBalances( - USD(11000), XRP(2200), IOUAmount{4919349550499538, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(11000), XRP(2200), IOUAmount{4919349550499538, -9})); + else + BEAST_EXPECT(amm.expectBalances( + USD(11000), XRP(2200), IOUAmount{4919349550499536, -9})); amm.deposit(bob, USD(2000), XRP(400)); - BEAST_EXPECT(amm.expectBalances( - USD(13000), XRP(2600), IOUAmount{5813776741499453, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(13000), XRP(2600), IOUAmount{5813776741499453, -9})); + else + BEAST_EXPECT(amm.expectBalances( + USD(13000), XRP(2600), IOUAmount{5813776741499451, -9})); env.close(); auto aliceXrpBalance = env.balance(alice, XRP); @@ -1058,18 +1239,34 @@ class AMMClawback_test : public jtx::AMMTest env(amm::ammClawback(gw, alice, USD, XRP, std::nullopt), ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - USD(12000), XRP(2400), IOUAmount{5366563145999495, -9})); - BEAST_EXPECT( - expectLedgerEntryRoot(env, alice, aliceXrpBalance + XRP(200))); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(12000), XRP(2400), IOUAmount{5366563145999495, -9})); + else + BEAST_EXPECT(amm.expectBalances( + USD(12000), + XRPAmount(2400000001), + IOUAmount{5366563145999494, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(200))); + else + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(200) - XRPAmount{1})); BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); // gw clawback all bob's USD in amm. (2000 USD / 400 XRP) env(amm::ammClawback(gw, bob, USD, XRP, std::nullopt), ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - USD(10000), XRP(2000), IOUAmount{4472135954999580, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(10000), XRP(2000), IOUAmount{4472135954999580, -9})); + else + BEAST_EXPECT(amm.expectBalances( + USD(10000), + XRPAmount(2000000001), + IOUAmount{4472135954999579, -9})); BEAST_EXPECT( expectLedgerEntryRoot(env, bob, bobXrpBalance + XRP(400))); BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); @@ -1125,10 +1322,12 @@ class AMMClawback_test : public jtx::AMMTest amm.deposit(bob, USD(4000), EUR(1000)); BEAST_EXPECT( amm.expectBalances(USD(12000), EUR(3000), IOUAmount(6000))); - amm.deposit(carol, USD(2000), EUR(500)); + if (!features[fixAMMv1_3]) + amm.deposit(carol, USD(2000), EUR(500)); + else + amm.deposit(carol, USD(2000.25), EUR(500)); BEAST_EXPECT( amm.expectBalances(USD(14000), EUR(3500), IOUAmount(7000))); - // gw clawback 1000 USD from carol. env(amm::ammClawback(gw, carol, USD, EUR, USD(1000)), ter(tesSUCCESS)); env.close(); @@ -1142,7 +1341,12 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(alice, EUR) == EUR(8000)); BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + else + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); // 250 EUR goes back to carol. BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); @@ -1164,7 +1368,12 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); // 250 EUR did not go back to bob because tfClawTwoAssets is set. BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + else + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); // gw clawback all USD from alice and set tfClawTwoAssets. @@ -1181,7 +1390,12 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(alice, EUR) == EUR(8000)); BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + else + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); } @@ -1366,12 +1580,21 @@ class AMMClawback_test : public jtx::AMMTest // gw2 claws back 1000 EUR from gw. env(amm::ammClawback(gw2, gw, EUR, USD, EUR(1000)), ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - USD(4500), - STAmount(EUR, UINT64_C(9000000000000001), -12), - IOUAmount{6363961030678928, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(4500), + STAmount(EUR, UINT64_C(9000000000000001), -12), + IOUAmount{6363961030678928, -12})); + else + BEAST_EXPECT(amm.expectBalances( + USD(4500), EUR(9000), IOUAmount{6363961030678928, -12})); - BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{7071067811865480, -13})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT( + amm.expectLPTokens(gw, IOUAmount{7071067811865480, -13})); + else + BEAST_EXPECT( + amm.expectLPTokens(gw, IOUAmount{7071067811865475, -13})); BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{1414213562373095, -12})); BEAST_EXPECT( amm.expectLPTokens(alice, IOUAmount{4242640687119285, -12})); @@ -1384,12 +1607,21 @@ class AMMClawback_test : public jtx::AMMTest // gw2 claws back 4000 EUR from alice. env(amm::ammClawback(gw2, alice, EUR, USD, EUR(4000)), ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - USD(2500), - STAmount(EUR, UINT64_C(5000000000000001), -12), - IOUAmount{3535533905932738, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(2500), + STAmount(EUR, UINT64_C(5000000000000001), -12), + IOUAmount{3535533905932738, -12})); + else + BEAST_EXPECT(amm.expectBalances( + USD(2500), EUR(5000), IOUAmount{3535533905932738, -12})); - BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{7071067811865480, -13})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT( + amm.expectLPTokens(gw, IOUAmount{7071067811865480, -13})); + else + BEAST_EXPECT( + amm.expectLPTokens(gw, IOUAmount{7071067811865475, -13})); BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{1414213562373095, -12})); BEAST_EXPECT( amm.expectLPTokens(alice, IOUAmount{1414213562373095, -12})); @@ -1653,7 +1885,10 @@ class AMMClawback_test : public jtx::AMMTest amm.deposit(bob, USD(4000), EUR(1000)); BEAST_EXPECT( amm.expectBalances(USD(12000), EUR(3000), IOUAmount(6000))); - amm.deposit(carol, USD(2000), EUR(500)); + if (!features[fixAMMv1_3]) + amm.deposit(carol, USD(2000), EUR(500)); + else + amm.deposit(carol, USD(2000.25), EUR(500)); BEAST_EXPECT( amm.expectBalances(USD(14000), EUR(3500), IOUAmount(7000))); @@ -1675,7 +1910,12 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(alice, EUR) == EUR(8000)); BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + else + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); // 250 EUR goes back to carol. BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); @@ -1697,7 +1937,12 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); // 250 EUR did not go back to bob because tfClawTwoAssets is set. BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + else + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); // gw clawback all USD from alice and set tfClawTwoAssets. @@ -1715,7 +1960,12 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(alice, EUR) == EUR(8000)); BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + else + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); } } @@ -1763,13 +2013,23 @@ class AMMClawback_test : public jtx::AMMTest env(amm::ammClawback(gw, alice, USD, XRP, USD(400)), ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - STAmount(USD, UINT64_C(5656854249492380), -13), - XRP(70.710678), - IOUAmount(200000))); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + STAmount(USD, UINT64_C(5656854249492380), -13), + XRP(70.710678), + IOUAmount(200000))); + else + BEAST_EXPECT(amm.expectBalances( + STAmount(USD, UINT64_C(565'685424949238), -12), + XRP(70.710679), + IOUAmount(200000))); BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); - BEAST_EXPECT(expectLedgerEntryRoot( - env, alice, aliceXrpBalance + XRP(29.289322))); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(29.289322))); + else + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(29.289321))); } void @@ -1780,13 +2040,18 @@ class AMMClawback_test : public jtx::AMMTest testFeatureDisabled(all - featureAMMClawback); testAMMClawbackSpecificAmount(all); testAMMClawbackExceedBalance(all); + testAMMClawbackExceedBalance(all - fixAMMv1_3); testAMMClawbackAll(all); + testAMMClawbackAll(all - fixAMMv1_3); testAMMClawbackSameIssuerAssets(all); + testAMMClawbackSameIssuerAssets(all - fixAMMv1_3); testAMMClawbackSameCurrency(all); testAMMClawbackIssuesEachOther(all); testNotHoldingLptoken(all); testAssetFrozen(all); + testAssetFrozen(all - fixAMMv1_3); testSingleDepositAndClawback(all); + testSingleDepositAndClawback(all - fixAMMv1_3); } }; BEAST_DEFINE_TESTSUITE(AMMClawback, app, ripple); diff --git a/src/test/app/AMMExtended_test.cpp b/src/test/app/AMMExtended_test.cpp index 16e66d803c..d6142d7d3e 100644 --- a/src/test/app/AMMExtended_test.cpp +++ b/src/test/app/AMMExtended_test.cpp @@ -1405,6 +1405,7 @@ struct AMMExtended_test : public jtx::AMMTest using namespace jtx; FeatureBitset const all{supported_amendments()}; testRmFundedOffer(all); + testRmFundedOffer(all - fixAMMv1_3); testEnforceNoRipple(all); testFillModes(all); testOfferCrossWithXRP(all); @@ -1418,6 +1419,7 @@ struct AMMExtended_test : public jtx::AMMTest testOfferCreateThenCross(all); testSellFlagExceedLimit(all); testGatewayCrossCurrency(all); + testGatewayCrossCurrency(all - fixAMMv1_3); testBridgedCross(all); testSellWithFillOrKill(all); testTransferRateOffer(all); @@ -1425,6 +1427,7 @@ struct AMMExtended_test : public jtx::AMMTest testBadPathAssert(all); testSellFlagBasic(all); testDirectToDirectPath(all); + testDirectToDirectPath(all - fixAMMv1_3); testRequireAuth(all); testMissingAuth(all); } @@ -3835,7 +3838,9 @@ struct AMMExtended_test : public jtx::AMMTest testBookStep(all); testBookStep(all | ownerPaysFee); testTransferRate(all | ownerPaysFee); + testTransferRate((all - fixAMMv1_3) | ownerPaysFee); testTransferRateNoOwnerFee(all); + testTransferRateNoOwnerFee(all - fixAMMv1_3); testLimitQuality(); testXRPPathLoop(); } @@ -3846,6 +3851,7 @@ struct AMMExtended_test : public jtx::AMMTest using namespace jtx; FeatureBitset const all{supported_amendments()}; testStepLimit(all); + testStepLimit(all - fixAMMv1_3); } void @@ -3854,6 +3860,7 @@ struct AMMExtended_test : public jtx::AMMTest using namespace jtx; FeatureBitset const all{supported_amendments()}; test_convert_all_of_an_asset(all); + test_convert_all_of_an_asset(all - fixAMMv1_3); } void diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index ad1e761d98..8d5595940a 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -24,7 +24,6 @@ #include #include #include -#include #include #include #include @@ -821,21 +820,6 @@ struct AMM_test : public jtx::AMMTest std::nullopt, ter(tecAMM_FAILED)); - // Tiny deposit - ammAlice.deposit( - carol, - IOUAmount{1, -4}, - std::nullopt, - std::nullopt, - ter(temBAD_AMOUNT)); - ammAlice.deposit( - carol, - STAmount{USD, 1, -12}, - std::nullopt, - std::nullopt, - std::nullopt, - ter(tecAMM_INVALID_TOKENS)); - // Deposit non-empty AMM ammAlice.deposit( carol, @@ -846,6 +830,34 @@ struct AMM_test : public jtx::AMMTest ter(tecAMM_NOT_EMPTY)); }); + // Tiny deposit + testAMM( + [&](AMM& ammAlice, Env& env) { + auto const enabledv1_3 = + env.current()->rules().enabled(fixAMMv1_3); + auto const err = + !enabledv1_3 ? ter(temBAD_AMOUNT) : ter(tesSUCCESS); + // Pre-amendment XRP deposit side is rounded to 0 + // and deposit fails. + // Post-amendment XRP deposit side is rounded to 1 + // and deposit succeeds. + ammAlice.deposit( + carol, IOUAmount{1, -4}, std::nullopt, std::nullopt, err); + // Pre/post-amendment LPTokens is rounded to 0 and deposit + // fails with tecAMM_INVALID_TOKENS. + ammAlice.deposit( + carol, + STAmount{USD, 1, -12}, + std::nullopt, + std::nullopt, + std::nullopt, + ter(tecAMM_INVALID_TOKENS)); + }, + std::nullopt, + 0, + std::nullopt, + {features, features - fixAMMv1_3}); + // Invalid AMM testAMM([&](AMM& ammAlice, Env& env) { ammAlice.withdrawAll(alice); @@ -1301,6 +1313,53 @@ struct AMM_test : public jtx::AMMTest std::nullopt, ter(tecAMM_FAILED)); }); + + // Equal deposit, tokens rounded to 0 + testAMM([&](AMM& amm, Env& env) { + amm.deposit(DepositArg{ + .tokens = IOUAmount{1, -12}, + .err = ter(tecAMM_INVALID_TOKENS)}); + }); + + // Equal deposit limit, tokens rounded to 0 + testAMM( + [&](AMM& amm, Env& env) { + amm.deposit(DepositArg{ + .asset1In = STAmount{USD, 1, -15}, + .asset2In = XRPAmount{1}, + .err = ter(tecAMM_INVALID_TOKENS)}); + }, + {.pool = {{USD(1'000'000), XRP(1'000'000)}}, + .features = {features - fixAMMv1_3}}); + testAMM([&](AMM& amm, Env& env) { + amm.deposit(DepositArg{ + .asset1In = STAmount{USD, 1, -15}, + .asset2In = XRPAmount{1}, + .err = ter(tecAMM_INVALID_TOKENS)}); + }); + + // Single deposit by asset, tokens rounded to 0 + testAMM([&](AMM& amm, Env& env) { + amm.deposit(DepositArg{ + .asset1In = STAmount{USD, 1, -15}, + .err = ter(tecAMM_INVALID_TOKENS)}); + }); + + // Single deposit by tokens, tokens rounded to 0 + testAMM([&](AMM& amm, Env& env) { + amm.deposit(DepositArg{ + .tokens = IOUAmount{1, -10}, + .asset1In = STAmount{USD, 1, -15}, + .err = ter(tecAMM_INVALID_TOKENS)}); + }); + + // Single deposit with eprice, tokens rounded to 0 + testAMM([&](AMM& amm, Env& env) { + amm.deposit(DepositArg{ + .asset1In = STAmount{USD, 1, -15}, + .maxEP = STAmount{USD, 1, -1}, + .err = ter(tecAMM_INVALID_TOKENS)}); + }); } void @@ -1309,6 +1368,7 @@ struct AMM_test : public jtx::AMMTest testcase("Deposit"); using namespace jtx; + auto const all = supported_amendments(); // Equal deposit: 1000000 tokens, 10% of the current pool testAMM([&](AMM& ammAlice, Env& env) { @@ -1513,8 +1573,9 @@ struct AMM_test : public jtx::AMMTest }); // Issuer create/deposit + for (auto const& feat : {all, all - fixAMMv1_3}) { - Env env(*this); + Env env(*this, feat); env.fund(XRP(30000), gw); AMM ammGw(env, gw, XRP(10'000), USD(10'000)); BEAST_EXPECT( @@ -1608,6 +1669,7 @@ struct AMM_test : public jtx::AMMTest testcase("Invalid Withdraw"); using namespace jtx; + auto const all = supported_amendments(); testAMM( [&](AMM& ammAlice, Env& env) { @@ -1901,16 +1963,6 @@ struct AMM_test : public jtx::AMMTest ammAlice.withdraw( carol, 10'000, std::nullopt, std::nullopt, ter(tecAMM_BALANCE)); - // Withdraw entire one side of the pool. - // Equal withdraw but due to XRP precision limit, - // this results in full withdraw of XRP pool only, - // while leaving a tiny amount in USD pool. - ammAlice.withdraw( - alice, - IOUAmount{9'999'999'9999, -4}, - std::nullopt, - std::nullopt, - ter(tecAMM_BALANCE)); // Withdrawing from one side. // XRP by tokens ammAlice.withdraw( @@ -1942,6 +1994,57 @@ struct AMM_test : public jtx::AMMTest ter(tecAMM_BALANCE)); }); + testAMM( + [&](AMM& ammAlice, Env& env) { + // Withdraw entire one side of the pool. + // Pre-amendment: + // Equal withdraw but due to XRP rounding + // this results in full withdraw of XRP pool only, + // while leaving a tiny amount in USD pool. + // Post-amendment: + // Most of the pool is withdrawn with remaining tiny amounts + auto err = env.enabled(fixAMMv1_3) ? ter(tesSUCCESS) + : ter(tecAMM_BALANCE); + ammAlice.withdraw( + alice, + IOUAmount{9'999'999'9999, -4}, + std::nullopt, + std::nullopt, + err); + if (env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(1), STAmount{USD, 1, -7}, IOUAmount{1, -4})); + }, + std::nullopt, + 0, + std::nullopt, + {all, all - fixAMMv1_3}); + + testAMM( + [&](AMM& ammAlice, Env& env) { + // Similar to above with even smaller remaining amount + // is it ok that the pool is unbalanced? + // Withdraw entire one side of the pool. + // Equal withdraw but due to XRP precision limit, + // this results in full withdraw of XRP pool only, + // while leaving a tiny amount in USD pool. + auto err = env.enabled(fixAMMv1_3) ? ter(tesSUCCESS) + : ter(tecAMM_BALANCE); + ammAlice.withdraw( + alice, + IOUAmount{9'999'999'999999999, -9}, + std::nullopt, + std::nullopt, + err); + if (env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(1), STAmount{USD, 1, -11}, IOUAmount{1, -8})); + }, + std::nullopt, + 0, + std::nullopt, + {all, all - fixAMMv1_3}); + // Invalid AMM testAMM([&](AMM& ammAlice, Env& env) { ammAlice.withdrawAll(alice); @@ -2005,15 +2108,19 @@ struct AMM_test : public jtx::AMMTest // Withdraw with EPrice limit. Fails to withdraw, calculated tokens // to withdraw are 0. - testAMM([&](AMM& ammAlice, Env&) { - ammAlice.deposit(carol, 1'000'000); - ammAlice.withdraw( - carol, - USD(100), - std::nullopt, - IOUAmount{500, 0}, - ter(tecAMM_FAILED)); - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.deposit(carol, 1'000'000); + auto const err = env.enabled(fixAMMv1_3) + ? ter(tecAMM_INVALID_TOKENS) + : ter(tecAMM_FAILED); + ammAlice.withdraw( + carol, USD(100), std::nullopt, IOUAmount{500, 0}, err); + }, + std::nullopt, + 0, + std::nullopt, + {all, all - fixAMMv1_3}); // Withdraw with EPrice limit. Fails to withdraw, calculated tokens // to withdraw are greater than the LP shares. @@ -2078,14 +2185,19 @@ struct AMM_test : public jtx::AMMTest // Withdraw close to one side of the pool. Account's LP tokens // are rounded to all LP tokens. - testAMM([&](AMM& ammAlice, Env&) { - ammAlice.withdraw( - alice, - STAmount{USD, UINT64_C(9'999'999999999999), -12}, - std::nullopt, - std::nullopt, - ter(tecAMM_BALANCE)); - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + auto const err = env.enabled(fixAMMv1_3) + ? ter(tecINVARIANT_FAILED) + : ter(tecAMM_BALANCE); + ammAlice.withdraw( + alice, + STAmount{USD, UINT64_C(9'999'999999999999), -12}, + std::nullopt, + std::nullopt, + err); + }, + {.features = {all, all - fixAMMv1_3}, .noLog = true}); // Tiny withdraw testAMM([&](AMM& ammAlice, Env&) { @@ -2116,6 +2228,17 @@ struct AMM_test : public jtx::AMMTest XRPAmount{1}, std::nullopt, ter(tecAMM_INVALID_TOKENS)); + ammAlice.withdraw(WithdrawArg{ + .tokens = IOUAmount{1, -10}, + .err = ter(tecAMM_INVALID_TOKENS)}); + ammAlice.withdraw(WithdrawArg{ + .asset1Out = STAmount{USD, 1, -15}, + .asset2Out = XRPAmount{1}, + .err = ter(tecAMM_INVALID_TOKENS)}); + ammAlice.withdraw(WithdrawArg{ + .tokens = IOUAmount{1, -10}, + .asset1Out = STAmount{USD, 1, -15}, + .err = ter(tecAMM_INVALID_TOKENS)}); }); } @@ -2125,6 +2248,7 @@ struct AMM_test : public jtx::AMMTest testcase("Withdraw"); using namespace jtx; + auto const all = supported_amendments(); // Equal withdrawal by Carol: 1000000 of tokens, 10% of the current // pool @@ -2178,11 +2302,24 @@ struct AMM_test : public jtx::AMMTest }); // Single withdrawal by amount XRP1000 - testAMM([&](AMM& ammAlice, Env&) { - ammAlice.withdraw(alice, XRP(1'000)); - BEAST_EXPECT(ammAlice.expectBalances( - XRP(9'000), USD(10'000), IOUAmount{9'486'832'98050514, -8})); - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.withdraw(alice, XRP(1'000)); + if (!env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(9'000), + USD(10'000), + IOUAmount{9'486'832'98050514, -8})); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{9'000'000'001}, + USD(10'000), + IOUAmount{9'486'832'98050514, -8})); + }, + std::nullopt, + 0, + std::nullopt, + {all, all - fixAMMv1_3}); // Single withdrawal by tokens 10000. testAMM([&](AMM& ammAlice, Env&) { @@ -2233,20 +2370,31 @@ struct AMM_test : public jtx::AMMTest }); // Single deposit/withdraw by the same account - testAMM([&](AMM& ammAlice, Env&) { - // Since a smaller amount might be deposited due to - // the lp tokens adjustment, withdrawing by tokens - // is generally preferred to withdrawing by amount. - auto lpTokens = ammAlice.deposit(carol, USD(1'000)); - ammAlice.withdraw(carol, lpTokens, USD(0)); - lpTokens = ammAlice.deposit(carol, STAmount(USD, 1, -6)); - ammAlice.withdraw(carol, lpTokens, USD(0)); - lpTokens = ammAlice.deposit(carol, XRPAmount(1)); - ammAlice.withdraw(carol, lpTokens, XRPAmount(0)); - BEAST_EXPECT(ammAlice.expectBalances( - XRP(10'000), USD(10'000), ammAlice.tokens())); - BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0})); - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + // Since a smaller amount might be deposited due to + // the lp tokens adjustment, withdrawing by tokens + // is generally preferred to withdrawing by amount. + auto lpTokens = ammAlice.deposit(carol, USD(1'000)); + ammAlice.withdraw(carol, lpTokens, USD(0)); + lpTokens = ammAlice.deposit(carol, STAmount(USD, 1, -6)); + ammAlice.withdraw(carol, lpTokens, USD(0)); + lpTokens = ammAlice.deposit(carol, XRPAmount(1)); + ammAlice.withdraw(carol, lpTokens, XRPAmount(0)); + if (!env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10'000), USD(10'000), ammAlice.tokens())); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(10'000'000'001), + USD(10'000), + ammAlice.tokens())); + BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0})); + }, + std::nullopt, + 0, + std::nullopt, + {all, all - fixAMMv1_3}); // Single deposit by different accounts and then withdraw // in reverse. @@ -2289,27 +2437,28 @@ struct AMM_test : public jtx::AMMTest IOUAmount{10'000'000, 0})); }); - auto const all = supported_amendments(); // Withdraw with EPrice limit. testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit(carol, 1'000'000); ammAlice.withdraw( carol, USD(100), std::nullopt, IOUAmount{520, 0}); - BEAST_EXPECT( - ammAlice.expectBalances( + BEAST_EXPECT(ammAlice.expectLPTokens( + carol, IOUAmount{153'846'15384616, -8})); + if (!env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( XRPAmount(11'000'000'000), STAmount{USD, UINT64_C(9'372'781065088769), -12}, - IOUAmount{10'153'846'15384616, -8}) && - ammAlice.expectLPTokens( - carol, IOUAmount{153'846'15384616, -8})); + IOUAmount{10'153'846'15384616, -8})); + else if (env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(11'000'000'000), + STAmount{USD, UINT64_C(9'372'78106508877), -11}, + IOUAmount{10'153'846'15384616, -8})); ammAlice.withdrawAll(carol); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0})); }, - std::nullopt, - 0, - std::nullopt, - {all}); + {.features = {all, all - fixAMMv1_3}, .noLog = true}); // Withdraw with EPrice limit. AssetOut is 0. testAMM( @@ -2317,18 +2466,23 @@ struct AMM_test : public jtx::AMMTest ammAlice.deposit(carol, 1'000'000); ammAlice.withdraw( carol, USD(0), std::nullopt, IOUAmount{520, 0}); - BEAST_EXPECT( - ammAlice.expectBalances( - XRPAmount(11'000'000'000), + BEAST_EXPECT(ammAlice.expectLPTokens( + carol, IOUAmount{153'846'15384616, -8})); + if (!env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(11'000), STAmount{USD, UINT64_C(9'372'781065088769), -12}, - IOUAmount{10'153'846'15384616, -8}) && - ammAlice.expectLPTokens( - carol, IOUAmount{153'846'15384616, -8})); + IOUAmount{10'153'846'15384616, -8})); + else if (env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(11'000), + STAmount{USD, UINT64_C(9'372'78106508877), -11}, + IOUAmount{10'153'846'15384616, -8})); }, std::nullopt, 0, std::nullopt, - {all}); + {all, all - fixAMMv1_3}); // IOU to IOU + transfer fee { @@ -2367,14 +2521,25 @@ struct AMM_test : public jtx::AMMTest STAmount{USD, UINT64_C(9'999'999999), -6}, IOUAmount{9'999'999'999, -3})); }); - testAMM([&](AMM& ammAlice, Env&) { - // Single XRP pool - ammAlice.withdraw(alice, std::nullopt, XRPAmount{1}); - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount{9'999'999'999}, - USD(10'000), - IOUAmount{9'999'999'9995, -4})); - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + // Single XRP pool + ammAlice.withdraw(alice, std::nullopt, XRPAmount{1}); + if (!env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{9'999'999'999}, + USD(10'000), + IOUAmount{9'999'999'9995, -4})); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10'000), + USD(10'000), + IOUAmount{9'999'999'9995, -4})); + }, + std::nullopt, + 0, + std::nullopt, + {all, all - fixAMMv1_3}); testAMM([&](AMM& ammAlice, Env&) { // Single USD pool ammAlice.withdraw(alice, std::nullopt, STAmount{USD, 1, -10}); @@ -2492,6 +2657,7 @@ struct AMM_test : public jtx::AMMTest { testcase("Fee Vote"); using namespace jtx; + auto const all = supported_amendments(); // One vote sets fee to 1%. testAMM([&](AMM& ammAlice, Env& env) { @@ -2509,6 +2675,12 @@ struct AMM_test : public jtx::AMMTest std::uint32_t tokens = 10'000'000, std::vector* accounts = nullptr) { Account a(std::to_string(i)); + // post-amendment the amount to deposit is slightly higher + // in order to ensure AMM invariant sqrt(asset1 * asset2) >= tokens + // fund just one USD higher in this case, which is enough for + // deposit to succeed + if (env.enabled(fixAMMv1_3)) + ++fundUSD; fund(env, gw, {a}, {USD(fundUSD)}, Fund::Acct); ammAlice.deposit(a, tokens); ammAlice.vote(a, 50 * (i + 1)); @@ -2517,11 +2689,16 @@ struct AMM_test : public jtx::AMMTest }; // Eight votes fill all voting slots, set fee 0.175%. - testAMM([&](AMM& ammAlice, Env& env) { - for (int i = 0; i < 7; ++i) - vote(ammAlice, env, i, 10'000); - BEAST_EXPECT(ammAlice.expectTradingFee(175)); - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + for (int i = 0; i < 7; ++i) + vote(ammAlice, env, i, 10'000); + BEAST_EXPECT(ammAlice.expectTradingFee(175)); + }, + std::nullopt, + 0, + std::nullopt, + {all}); // Eight votes fill all voting slots, set fee 0.175%. // New vote, same account, sets fee 0.225% @@ -2915,8 +3092,14 @@ struct AMM_test : public jtx::AMMTest fund(env, gw, {bob}, {USD(10'000)}, Fund::Acct); ammAlice.deposit(bob, 1'000'000); - BEAST_EXPECT(ammAlice.expectBalances( - XRP(12'000), USD(12'000), IOUAmount{12'000'000, 0})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(12'000), USD(12'000), IOUAmount{12'000'000, 0})); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{12'000'000'001}, + USD(12'000), + IOUAmount{12'000'000, 0})); // Initial state. Pay bidMin. env(ammAlice.bid({.account = carol, .bidMin = 110})).close(); @@ -2948,8 +3131,16 @@ struct AMM_test : public jtx::AMMTest BEAST_EXPECT(ammAlice.expectAuctionSlot( 0, std::nullopt, IOUAmount{110})); // ~321.09 tokens burnt on bidding fees. - BEAST_EXPECT(ammAlice.expectBalances( - XRP(12'000), USD(12'000), IOUAmount{11'999'678'91, -2})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(12'000), + USD(12'000), + IOUAmount{11'999'678'91, -2})); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{12'000'000'001}, + USD(12'000), + IOUAmount{11'999'678'91, -2})); }, std::nullopt, 0, @@ -2978,8 +3169,12 @@ struct AMM_test : public jtx::AMMTest auto const slotPrice = IOUAmount{5'200}; ammTokens -= slotPrice; BEAST_EXPECT(ammAlice.expectAuctionSlot(100, 0, slotPrice)); - BEAST_EXPECT(ammAlice.expectBalances( - XRP(13'000), USD(13'000), ammTokens)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(13'000), USD(13'000), ammTokens)); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'000'000'003}, USD(13'000), ammTokens)); // Discounted trade for (int i = 0; i < 10; ++i) { @@ -3001,11 +3196,16 @@ struct AMM_test : public jtx::AMMTest env.balance(ed, USD) == STAmount(USD, UINT64_C(18'999'0057261184), -10)); // USD pool is slightly higher because of the fees. - BEAST_EXPECT(ammAlice.expectBalances( - XRP(13'000), - STAmount(USD, UINT64_C(13'002'98282151422), -11), - ammTokens)); - + if (!features[fixAMMv1_3]) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(13'000), + STAmount(USD, UINT64_C(13'002'98282151422), -11), + ammTokens)); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'000'000'003}, + STAmount(USD, UINT64_C(13'002'98282151422), -11), + ammTokens)); ammTokens = ammAlice.getLPTokensBalance(); // Trade with the fee for (int i = 0; i < 10; ++i) @@ -3016,48 +3216,81 @@ struct AMM_test : public jtx::AMMTest // dan pays ~9.94USD, which is ~10 times more in fees than // carol, bob, ed. the discounted fee is 10 times less // than the trading fee. - BEAST_EXPECT( - env.balance(dan, USD) == - STAmount(USD, UINT64_C(19'490'05672274399), -11)); + + if (!features[fixAMMv1_3]) + BEAST_EXPECT( + env.balance(dan, USD) == + STAmount(USD, UINT64_C(19'490'05672274399), -11)); + else + BEAST_EXPECT( + env.balance(dan, USD) == + STAmount(USD, UINT64_C(19'490'05672274398), -11)); // USD pool gains more in dan's fees. - BEAST_EXPECT(ammAlice.expectBalances( - XRP(13'000), - STAmount{USD, UINT64_C(13'012'92609877023), -11}, - ammTokens)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(13'000), + STAmount{USD, UINT64_C(13'012'92609877023), -11}, + ammTokens)); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'000'000'003}, + STAmount{USD, UINT64_C(13'012'92609877024), -11}, + ammTokens)); // Discounted fee payment ammAlice.deposit(carol, USD(100)); ammTokens = ammAlice.getLPTokensBalance(); - BEAST_EXPECT(ammAlice.expectBalances( - XRP(13'000), - STAmount{USD, UINT64_C(13'112'92609877023), -11}, - ammTokens)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(13'000), + STAmount{USD, UINT64_C(13'112'92609877023), -11}, + ammTokens)); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'000'000'003}, + STAmount{USD, UINT64_C(13'112'92609877024), -11}, + ammTokens)); env(pay(carol, bob, USD(100)), path(~USD), sendmax(XRP(110))); env.close(); // carol pays 100000 drops in fees // 99900668XRP swapped in for 100USD - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount{13'100'000'668}, - STAmount{USD, UINT64_C(13'012'92609877023), -11}, - ammTokens)); - + if (!features[fixAMMv1_3]) + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'100'000'668}, + STAmount{USD, UINT64_C(13'012'92609877023), -11}, + ammTokens)); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'100'000'671}, + STAmount{USD, UINT64_C(13'012'92609877024), -11}, + ammTokens)); // Payment with the trading fee env(pay(alice, carol, XRP(100)), path(~XRP), sendmax(USD(110))); env.close(); // alice pays ~1.011USD in fees, which is ~10 times more // than carol's fee // 100.099431529USD swapped in for 100XRP - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount{13'000'000'668}, - STAmount{USD, UINT64_C(13'114'03663047269), -11}, - ammTokens)); - + if (!features[fixAMMv1_3]) + { + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'000'000'668}, + STAmount{USD, UINT64_C(13'114'03663047269), -11}, + ammTokens)); + } + else + { + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'000'000'671}, + STAmount{USD, UINT64_C(13'114'03663044937), -11}, + ammTokens)); + } // Auction slot expired, no discounted fee env.close(seconds(TOTAL_TIME_SLOT_SECS + 1)); // clock is parent's based env.close(); - BEAST_EXPECT( - env.balance(carol, USD) == - STAmount(USD, UINT64_C(29'399'00572620544), -11)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(29'399'00572620544), -11)); ammTokens = ammAlice.getLPTokensBalance(); for (int i = 0; i < 10; ++i) { @@ -3066,23 +3299,45 @@ struct AMM_test : public jtx::AMMTest } // carol pays ~9.94USD in fees, which is ~10 times more in // trading fees vs discounted fee. - BEAST_EXPECT( - env.balance(carol, USD) == - STAmount(USD, UINT64_C(29'389'06197177124), -11)); - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount{13'000'000'668}, - STAmount{USD, UINT64_C(13'123'98038490689), -11}, - ammTokens)); - + if (!features[fixAMMv1_3]) + { + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(29'389'06197177124), -11)); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'000'000'668}, + STAmount{USD, UINT64_C(13'123'98038490689), -11}, + ammTokens)); + } + else + { + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(29'389'06197177129), -11)); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'000'000'671}, + STAmount{USD, UINT64_C(13'123'98038488352), -11}, + ammTokens)); + } env(pay(carol, bob, USD(100)), path(~USD), sendmax(XRP(110))); env.close(); // carol pays ~1.008XRP in trading fee, which is // ~10 times more than the discounted fee. // 99.815876XRP is swapped in for 100USD - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount(13'100'824'790), - STAmount{USD, UINT64_C(13'023'98038490689), -11}, - ammTokens)); + if (!features[fixAMMv1_3]) + { + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(13'100'824'790), + STAmount{USD, UINT64_C(13'023'98038490689), -11}, + ammTokens)); + } + else + { + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(13'100'824'793), + STAmount{USD, UINT64_C(13'023'98038488352), -11}, + ammTokens)); + } }, std::nullopt, 1'000, @@ -4187,8 +4442,9 @@ struct AMM_test : public jtx::AMMTest // Offer crossing with AMM LPTokens and XRP. testAMM([&](AMM& ammAlice, Env& env) { + auto const baseFee = env.current()->fees().base.drops(); auto const token1 = ammAlice.lptIssue(); - auto priceXRP = withdrawByTokens( + auto priceXRP = ammAssetOut( STAmount{XRPAmount{10'000'000'000}}, STAmount{token1, 10'000'000}, STAmount{token1, 5'000'000}, @@ -4212,8 +4468,10 @@ struct AMM_test : public jtx::AMMTest env(ammAlice.bid({.account = carol, .bidMin = 100})); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{4'999'900})); BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{100})); - BEAST_EXPECT(accountBalance(env, carol) == "22499999960"); - priceXRP = withdrawByTokens( + BEAST_EXPECT( + accountBalance(env, carol) == + std::to_string(22500000000 - 4 * baseFee)); + priceXRP = ammAssetOut( STAmount{XRPAmount{10'000'000'000}}, STAmount{token1, 9'999'900}, STAmount{token1, 4'999'900}, @@ -4561,7 +4819,10 @@ struct AMM_test : public jtx::AMMTest carol, USD(100), std::nullopt, IOUAmount{520, 0}); // carol withdraws ~1,443.44USD auto const balanceAfterWithdraw = [&]() { - return STAmount(USD, UINT64_C(30'443'43891402714), -11); + if (!features[fixAMMv1_3]) + return STAmount(USD, UINT64_C(30'443'43891402714), -11); + else + return STAmount(USD, UINT64_C(30'443'43891402713), -11); }(); BEAST_EXPECT(env.balance(carol, USD) == balanceAfterWithdraw); // Set to original pool size @@ -4571,12 +4832,22 @@ struct AMM_test : public jtx::AMMTest ammAlice.vote(alice, 0); BEAST_EXPECT(ammAlice.expectTradingFee(0)); auto const tokensNoFee = ammAlice.withdraw(carol, deposit); - BEAST_EXPECT( - env.balance(carol, USD) == - STAmount(USD, UINT64_C(30'443'43891402716), -11)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(30'443'43891402716), -11)); + else + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(30'443'43891402713), -11)); // carol pays ~4008 LPTokens in fees or ~0.5% of the no-fee // LPTokens - BEAST_EXPECT(tokensNoFee == IOUAmount(746'579'80779912, -8)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT( + tokensNoFee == IOUAmount(746'579'80779912, -8)); + else + BEAST_EXPECT( + tokensNoFee == IOUAmount(746'579'80779911, -8)); BEAST_EXPECT(tokensFee == IOUAmount(750'588'23529411, -8)); }, std::nullopt, @@ -4838,22 +5109,40 @@ struct AMM_test : public jtx::AMMTest // Due to round off some accounts have a tiny gain, while // other have a tiny loss. The last account to withdraw // gets everything in the pool. - BEAST_EXPECT(ammAlice.expectBalances( - XRP(10'000), USD(10'000), IOUAmount{10'000'000})); + if (features[fixAMMv1_3]) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10'000), + STAmount{USD, UINT64_C(10'000'0000000003), -10}, + IOUAmount{10'000'000})); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10'000), USD(10'000), IOUAmount{10'000'000})); BEAST_EXPECT(expectLine(env, ben, USD(1'500'000))); BEAST_EXPECT(expectLine(env, simon, USD(1'500'000))); BEAST_EXPECT(expectLine(env, chris, USD(1'500'000))); BEAST_EXPECT(expectLine(env, dan, USD(1'500'000))); - BEAST_EXPECT(expectLine(env, carol, USD(30'000))); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(expectLine(env, carol, USD(30'000))); + else + BEAST_EXPECT(expectLine(env, carol, USD(30'000))); BEAST_EXPECT(expectLine(env, ed, USD(1'500'000))); BEAST_EXPECT(expectLine(env, paul, USD(1'500'000))); - BEAST_EXPECT(expectLine( - env, - nataly, - STAmount{USD, UINT64_C(1'500'000'000000005), -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(expectLine( + env, + nataly, + STAmount{USD, UINT64_C(1'500'000'000000005), -9})); + else + BEAST_EXPECT(expectLine(env, nataly, USD(1'500'000))); ammAlice.withdrawAll(alice); BEAST_EXPECT(!ammAlice.ammExists()); - BEAST_EXPECT(expectLine(env, alice, USD(30'000))); + if (features[fixAMMv1_3]) + BEAST_EXPECT(expectLine( + env, + alice, + STAmount{USD, UINT64_C(30'000'0000000003), -10})); + else + BEAST_EXPECT(expectLine(env, alice, USD(30'000))); // alice XRP balance is 30,000initial - 50 ammcreate fee - // 10drops fee BEAST_EXPECT(accountBalance(env, alice) == "29949999990"); @@ -4864,62 +5153,110 @@ struct AMM_test : public jtx::AMMTest {features}); // Same as above but deposit/withdraw in XRP - testAMM([&](AMM& ammAlice, Env& env) { - Account const bob("bob"); - Account const ed("ed"); - Account const paul("paul"); - Account const dan("dan"); - Account const chris("chris"); - Account const simon("simon"); - Account const ben("ben"); - Account const nataly("nataly"); - fund( - env, - gw, - {bob, ed, paul, dan, chris, simon, ben, nataly}, - XRP(2'000'000), - {}, - Fund::Acct); - for (int i = 0; i < 10; ++i) - { - ammAlice.deposit(ben, XRPAmount{1}); - ammAlice.withdrawAll(ben, XRP(0)); - ammAlice.deposit(simon, XRPAmount(1'000)); - ammAlice.withdrawAll(simon, XRP(0)); - ammAlice.deposit(chris, XRP(1)); - ammAlice.withdrawAll(chris, XRP(0)); - ammAlice.deposit(dan, XRP(10)); - ammAlice.withdrawAll(dan, XRP(0)); - ammAlice.deposit(bob, XRP(100)); - ammAlice.withdrawAll(bob, XRP(0)); - ammAlice.deposit(carol, XRP(1'000)); - ammAlice.withdrawAll(carol, XRP(0)); - ammAlice.deposit(ed, XRP(10'000)); - ammAlice.withdrawAll(ed, XRP(0)); - ammAlice.deposit(paul, XRP(100'000)); - ammAlice.withdrawAll(paul, XRP(0)); - ammAlice.deposit(nataly, XRP(1'000'000)); - ammAlice.withdrawAll(nataly, XRP(0)); - } - // No round off with XRP in this test - BEAST_EXPECT(ammAlice.expectBalances( - XRP(10'000), USD(10'000), IOUAmount{10'000'000})); - ammAlice.withdrawAll(alice); - BEAST_EXPECT(!ammAlice.ammExists()); - // 20,000 initial - (deposit+withdraw) * 10 - auto const xrpBalance = (XRP(2'000'000) - txfee(env, 20)).getText(); - BEAST_EXPECT(accountBalance(env, ben) == xrpBalance); - BEAST_EXPECT(accountBalance(env, simon) == xrpBalance); - BEAST_EXPECT(accountBalance(env, chris) == xrpBalance); - BEAST_EXPECT(accountBalance(env, dan) == xrpBalance); - // 30,000 initial - (deposit+withdraw) * 10 - BEAST_EXPECT(accountBalance(env, carol) == "29999999800"); - BEAST_EXPECT(accountBalance(env, ed) == xrpBalance); - BEAST_EXPECT(accountBalance(env, paul) == xrpBalance); - BEAST_EXPECT(accountBalance(env, nataly) == xrpBalance); - // 30,000 initial - 50 ammcreate fee - 10drops withdraw fee - BEAST_EXPECT(accountBalance(env, alice) == "29949999990"); - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + Account const bob("bob"); + Account const ed("ed"); + Account const paul("paul"); + Account const dan("dan"); + Account const chris("chris"); + Account const simon("simon"); + Account const ben("ben"); + Account const nataly("nataly"); + fund( + env, + gw, + {bob, ed, paul, dan, chris, simon, ben, nataly}, + XRP(2'000'000), + {}, + Fund::Acct); + for (int i = 0; i < 10; ++i) + { + ammAlice.deposit(ben, XRPAmount{1}); + ammAlice.withdrawAll(ben, XRP(0)); + ammAlice.deposit(simon, XRPAmount(1'000)); + ammAlice.withdrawAll(simon, XRP(0)); + ammAlice.deposit(chris, XRP(1)); + ammAlice.withdrawAll(chris, XRP(0)); + ammAlice.deposit(dan, XRP(10)); + ammAlice.withdrawAll(dan, XRP(0)); + ammAlice.deposit(bob, XRP(100)); + ammAlice.withdrawAll(bob, XRP(0)); + ammAlice.deposit(carol, XRP(1'000)); + ammAlice.withdrawAll(carol, XRP(0)); + ammAlice.deposit(ed, XRP(10'000)); + ammAlice.withdrawAll(ed, XRP(0)); + ammAlice.deposit(paul, XRP(100'000)); + ammAlice.withdrawAll(paul, XRP(0)); + ammAlice.deposit(nataly, XRP(1'000'000)); + ammAlice.withdrawAll(nataly, XRP(0)); + } + auto const baseFee = env.current()->fees().base.drops(); + if (!features[fixAMMv1_3]) + { + // No round off with XRP in this test + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10'000), USD(10'000), IOUAmount{10'000'000})); + ammAlice.withdrawAll(alice); + BEAST_EXPECT(!ammAlice.ammExists()); + // 20,000 initial - (deposit+withdraw) * 10 + auto const xrpBalance = + (XRP(2'000'000) - txfee(env, 20)).getText(); + BEAST_EXPECT(accountBalance(env, ben) == xrpBalance); + BEAST_EXPECT(accountBalance(env, simon) == xrpBalance); + BEAST_EXPECT(accountBalance(env, chris) == xrpBalance); + BEAST_EXPECT(accountBalance(env, dan) == xrpBalance); + + // 30,000 initial - (deposit+withdraw) * 10 + BEAST_EXPECT( + accountBalance(env, carol) == + std::to_string(30'000'000'000 - 20 * baseFee)); + BEAST_EXPECT(accountBalance(env, ed) == xrpBalance); + BEAST_EXPECT(accountBalance(env, paul) == xrpBalance); + BEAST_EXPECT(accountBalance(env, nataly) == xrpBalance); + // 30,000 initial - 50 ammcreate fee - 10drops withdraw fee + BEAST_EXPECT( + accountBalance(env, alice) == + std::to_string(29'950'000'000 - baseFee)); + } + else + { + // post-amendment the rounding takes place to ensure + // AMM invariant + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(10'000'000'080), + USD(10'000), + IOUAmount{10'000'000})); + ammAlice.withdrawAll(alice); + BEAST_EXPECT(!ammAlice.ammExists()); + auto const xrpBalance = + XRP(2'000'000) - txfee(env, 20) - drops(10); + auto const xrpBalanceText = xrpBalance.getText(); + BEAST_EXPECT(accountBalance(env, ben) == xrpBalanceText); + BEAST_EXPECT(accountBalance(env, simon) == xrpBalanceText); + BEAST_EXPECT(accountBalance(env, chris) == xrpBalanceText); + BEAST_EXPECT(accountBalance(env, dan) == xrpBalanceText); + BEAST_EXPECT( + accountBalance(env, carol) == + std::to_string(30'000'000'000 - 20 * baseFee - 10)); + BEAST_EXPECT( + accountBalance(env, ed) == + (xrpBalance + drops(2)).getText()); + BEAST_EXPECT( + accountBalance(env, paul) == + (xrpBalance + drops(3)).getText()); + BEAST_EXPECT( + accountBalance(env, nataly) == + (xrpBalance + drops(5)).getText()); + BEAST_EXPECT( + accountBalance(env, alice) == + std::to_string(29'950'000'000 - baseFee + 80)); + } + }, + std::nullopt, + 0, + std::nullopt, + {features}); } void @@ -5804,11 +6141,11 @@ struct AMM_test : public jtx::AMMTest } void - testFixOverflowOffer(FeatureBitset features) + testFixOverflowOffer(FeatureBitset featuresInitial) { using namespace jtx; using namespace std::chrono; - FeatureBitset const all{features}; + FeatureBitset const all{featuresInitial}; Account const gatehub{"gatehub"}; Account const bitstamp{"bitstamp"}; @@ -5833,6 +6170,7 @@ struct AMM_test : public jtx::AMMTest STAmount const goodUsdBIT; STAmount const goodUsdBITr; IOUAmount const lpTokenBalance; + std::optional const lpTokenBalanceAlt = {}; double const offer1BtcGH = 0.1; double const offer2BtcGH = 0.1; double const offer2UsdGH = 1; @@ -5858,6 +6196,7 @@ struct AMM_test : public jtx::AMMTest .goodUsdBIT{usdBIT, uint64_t(8'464739069120721), -15}, // .goodUsdBITr{usdBIT, uint64_t(8'464739069098152), -15}, // .lpTokenBalance = {28'61817604250837, -14}, // + .lpTokenBalanceAlt = IOUAmount{28'61817604250836, -14}, // .offer1BtcGH = 0.1, // .offer2BtcGH = 0.1, // .offer2UsdGH = 1, // @@ -6035,72 +6374,91 @@ struct AMM_test : public jtx::AMMTest }) { testcase(input.testCase); + for (auto const& features : {all - fixAMMv1_3, all}) + { + // Env env(*this, features, + // std::make_unique(&logs)); + Env env( + *this, + envconfig(), + features, + nullptr, + beast::severities::kDisabled); + + env.fund(XRP(5'000), gatehub, bitstamp, trader); + env.close(); - Env env( - *this, envconfig(), all, nullptr, beast::severities::kDisabled); - - env.fund(XRP(5'000), gatehub, bitstamp, trader); - env.close(); + if (input.rateGH != 0.0) + env(rate(gatehub, input.rateGH)); + if (input.rateBIT != 0.0) + env(rate(bitstamp, input.rateBIT)); - if (input.rateGH != 0.0) - env(rate(gatehub, input.rateGH)); - if (input.rateBIT != 0.0) - env(rate(bitstamp, input.rateBIT)); + env(trust(trader, usdGH(10'000'000))); + env(trust(trader, usdBIT(10'000'000))); + env(trust(trader, btcGH(10'000'000))); + env.close(); - env(trust(trader, usdGH(10'000'000))); - env(trust(trader, usdBIT(10'000'000))); - env(trust(trader, btcGH(10'000'000))); - env.close(); + env(pay(gatehub, trader, usdGH(100'000))); + env(pay(gatehub, trader, btcGH(100'000))); + env(pay(bitstamp, trader, usdBIT(100'000))); + env.close(); - env(pay(gatehub, trader, usdGH(100'000))); - env(pay(gatehub, trader, btcGH(100'000))); - env(pay(bitstamp, trader, usdBIT(100'000))); - env.close(); + AMM amm{ + env, + trader, + usdGH(input.poolUsdGH), + usdBIT(input.poolUsdBIT)}; + env.close(); - AMM amm{ - env, trader, usdGH(input.poolUsdGH), usdBIT(input.poolUsdBIT)}; - env.close(); + IOUAmount const preSwapLPTokenBalance = + amm.getLPTokensBalance(); - IOUAmount const preSwapLPTokenBalance = amm.getLPTokensBalance(); + env(offer(trader, usdBIT(1), btcGH(input.offer1BtcGH))); + env(offer( + trader, + btcGH(input.offer2BtcGH), + usdGH(input.offer2UsdGH))); + env.close(); - env(offer(trader, usdBIT(1), btcGH(input.offer1BtcGH))); - env(offer( - trader, btcGH(input.offer2BtcGH), usdGH(input.offer2UsdGH))); - env.close(); + env(pay(trader, trader, input.sendUsdGH), + path(~usdGH), + path(~btcGH, ~usdGH), + sendmax(input.sendMaxUsdBIT), + txflags(tfPartialPayment)); + env.close(); - env(pay(trader, trader, input.sendUsdGH), - path(~usdGH), - path(~btcGH, ~usdGH), - sendmax(input.sendMaxUsdBIT), - txflags(tfPartialPayment)); - env.close(); + auto const failUsdGH = input.failUsdGHr; + auto const failUsdBIT = input.failUsdBITr; + auto const goodUsdGH = input.goodUsdGHr; + auto const goodUsdBIT = input.goodUsdBITr; - auto const failUsdGH = input.failUsdGHr; - auto const failUsdBIT = input.failUsdBITr; - auto const goodUsdGH = input.goodUsdGHr; - auto const goodUsdBIT = input.goodUsdBITr; + auto const lpTokenBalance = + env.enabled(fixAMMv1_3) && input.lpTokenBalanceAlt + ? *input.lpTokenBalanceAlt + : input.lpTokenBalance; - BEAST_EXPECT(amm.expectBalances( - goodUsdGH, goodUsdBIT, input.lpTokenBalance)); - - // Invariant: LPToken balance must not change in a - // payment or a swap transaction - BEAST_EXPECT(amm.getLPTokensBalance() == preSwapLPTokenBalance); - - // Invariant: The square root of (product of the pool - // balances) must be at least the LPTokenBalance - Number const sqrtPoolProduct = root2(goodUsdGH * goodUsdBIT); - - // Include a tiny tolerance for the test cases using - // .goodUsdGH{usdGH, uint64_t(35'44113971506987), - // -14}, .goodUsdBIT{usdBIT, - // uint64_t(2'821579689703915), -15}, - // These two values multiply - // to 99.99999999999994227040383754105 which gets - // internally rounded to 100, due to representation - // error. - BEAST_EXPECT( - (sqrtPoolProduct + Number{1, -14} >= input.lpTokenBalance)); + BEAST_EXPECT( + amm.expectBalances(goodUsdGH, goodUsdBIT, lpTokenBalance)); + + // Invariant: LPToken balance must not change in a + // payment or a swap transaction + BEAST_EXPECT(amm.getLPTokensBalance() == preSwapLPTokenBalance); + + // Invariant: The square root of (product of the pool + // balances) must be at least the LPTokenBalance + Number const sqrtPoolProduct = root2(goodUsdGH * goodUsdBIT); + + // Include a tiny tolerance for the test cases using + // .goodUsdGH{usdGH, uint64_t(35'44113971506987), + // -14}, .goodUsdBIT{usdBIT, + // uint64_t(2'821579689703915), -15}, + // These two values multiply + // to 99.99999999999994227040383754105 which gets + // internally rounded to 100, due to representation + // error. + BEAST_EXPECT( + (sqrtPoolProduct + Number{1, -14} >= input.lpTokenBalance)); + } } } @@ -6246,11 +6604,19 @@ struct AMM_test : public jtx::AMMTest void testLPTokenBalance(FeatureBitset features) { + testcase("LPToken Balance"); using namespace jtx; // Last Liquidity Provider is the issuer of one token { - Env env(*this, features); + std::string logs; + // Env env(*this, features, std::make_unique(&logs)); + Env env( + *this, + envconfig(), + features, + nullptr, + beast::severities::kDisabled); fund( env, gw, @@ -6261,7 +6627,9 @@ struct AMM_test : public jtx::AMMTest amm.deposit(alice, IOUAmount{1'876123487565916, -15}); amm.deposit(carol, IOUAmount{1'000'000}); amm.withdrawAll(alice); + BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{0})); amm.withdrawAll(carol); + BEAST_EXPECT(amm.expectLPTokens(carol, IOUAmount{0})); auto const lpToken = getAccountLines( env, gw, amm.lptIssue())[jss::lines][0u][jss::balance]; auto const lpTokenBalance = @@ -6522,6 +6890,430 @@ struct AMM_test : public jtx::AMMTest }); } + // void + // testFailedPseudoAccount() + // { + // using namespace test::jtx; + + // auto const testCase = [&](std::string suffix, + // FeatureBitset features) { + // testcase("Failed pseudo-account allocation " + suffix); + // std::string logs; + // Env env{ + // *this, features, std::make_unique(&logs)}; + // env.fund(XRP(30'000), gw, alice); + // env.close(); + // env(trust(alice, gw["USD"](30'000), 0)); + // env(pay(gw, alice, USD(10'000))); + // env.close(); + + // STAmount amount = XRP(10'000); + // STAmount amount2 = USD(10'000); + // auto const keylet = + // keylet::amm(amount.issue(), amount2.issue()); + // for (int i = 0; i < 256; ++i) + // { + // AccountID const accountId = + // ripple::pseudoAccountAddress( + // *env.current(), keylet.key); + + // env(pay(env.master.id(), accountId, XRP(1000)), + // seq(autofill), + // fee(autofill), + // sig(autofill)); + // } + + // AMM ammAlice( + // env, + // alice, + // amount, + // amount2, + // features[featureSingleAssetVault] + // ? ter{terADDRESS_COLLISION} + // : ter{tecDUPLICATE}); + // }; + + // testCase( + // "tecDUPLICATE", + // supported_amendments() - featureSingleAssetVault); + // testCase( + // "terADDRESS_COLLISION", + // supported_amendments() | featureSingleAssetVault); + // } + + void + testDepositAndWithdrawRounding(FeatureBitset features) + { + testcase("Deposit and Withdraw Rounding V2"); + using namespace jtx; + + auto const XPM = gw["XPM"]; + STAmount xrpBalance{XRPAmount(692'614'492'126)}; + STAmount xpmBalance{XPM, UINT64_C(18'610'359'80246901), -8}; + STAmount amount{XPM, UINT64_C(6'566'496939465400), -12}; + std::uint16_t tfee = 941; + + auto test = [&](auto&& cb, std::uint16_t tfee_) { + Env env(*this, features); + env.fund(XRP(1'000'000), gw); + env.fund(XRP(1'000), alice); + env(trust(alice, XPM(7'000))); + env(pay(gw, alice, amount)); + + AMM amm(env, gw, xrpBalance, xpmBalance, CreateArg{.tfee = tfee_}); + // AMM LPToken balance required to replicate single deposit + // failure + STAmount lptAMMBalance{ + amm.lptIssue(), UINT64_C(3'234'987'266'485968), -6}; + auto const burn = + IOUAmount{amm.getLPTokensBalance() - lptAMMBalance}; + // burn tokens to get to the required AMM state + env(amm.bid(BidArg{.account = gw, .bidMin = burn, .bidMax = burn})); + cb(amm, env); + }; + test( + [&](AMM& amm, Env& env) { + auto const err = env.enabled(fixAMMv1_3) ? ter(tesSUCCESS) + : ter(tecUNFUNDED_AMM); + amm.deposit(DepositArg{ + .account = alice, .asset1In = amount, .err = err}); + }, + tfee); + test( + [&](AMM& amm, Env& env) { + auto const [amount, amount2, lptAMM] = amm.balances(XRP, XPM); + auto const withdraw = STAmount{XPM, 1, -5}; + amm.withdraw(WithdrawArg{.asset1Out = STAmount{XPM, 1, -5}}); + auto const [amount_, amount2_, lptAMM_] = + amm.balances(XRP, XPM); + if (!env.enabled(fixAMMv1_3)) + BEAST_EXPECT((amount2 - amount2_) > withdraw); + else + BEAST_EXPECT((amount2 - amount2_) <= withdraw); + }, + 0); + } + + void + invariant( + jtx::AMM& amm, + jtx::Env& env, + std::string const& msg, + bool shouldFail) + { + auto const [amount, amount2, lptBalance] = amm.balances(GBP, EUR); + + NumberRoundModeGuard g( + env.enabled(fixAMMv1_3) ? Number::upward : Number::getround()); + auto const res = root2(amount * amount2); + + if (shouldFail) + BEAST_EXPECT(res < lptBalance); + else + BEAST_EXPECT(res >= lptBalance); + } + + void + testDepositRounding(FeatureBitset all) + { + testcase("Deposit Rounding"); + using namespace jtx; + + // Single asset deposit + for (auto const& deposit : + {STAmount(EUR, 1, 1), + STAmount(EUR, 1, 2), + STAmount(EUR, 1, 5), + STAmount(EUR, 1, -3), // fail + STAmount(EUR, 1, -6), + STAmount(EUR, 1, -9)}) + { + testAMM( + [&](AMM& ammAlice, Env& env) { + fund( + env, + gw, + {bob}, + XRP(10'000'000), + {GBP(100'000), EUR(100'000)}, + Fund::Acct); + env.close(); + + ammAlice.deposit( + DepositArg{.account = bob, .asset1In = deposit}); + invariant( + ammAlice, + env, + "dep1", + deposit == STAmount{EUR, 1, -3} && + !env.enabled(fixAMMv1_3)); + }, + {{GBP(30'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + } + + // Two-asset proportional deposit (1:1 pool ratio) + testAMM( + [&](AMM& ammAlice, Env& env) { + fund( + env, + gw, + {bob}, + XRP(10'000'000), + {GBP(100'000), EUR(100'000)}, + Fund::Acct); + env.close(); + + STAmount const depositEuro{ + EUR, UINT64_C(10'1234567890123456), -16}; + STAmount const depositGBP{ + GBP, UINT64_C(10'1234567890123456), -16}; + + ammAlice.deposit(DepositArg{ + .account = bob, + .asset1In = depositEuro, + .asset2In = depositGBP}); + invariant(ammAlice, env, "dep2", false); + }, + {{GBP(30'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // Two-asset proportional deposit (1:3 pool ratio) + for (auto const& exponent : {1, 2, 3, 4, -3 /*fail*/, -6, -9}) + { + testAMM( + [&](AMM& ammAlice, Env& env) { + fund( + env, + gw, + {bob}, + XRP(10'000'000), + {GBP(100'000), EUR(100'000)}, + Fund::Acct); + env.close(); + + STAmount const depositEuro{EUR, 1, exponent}; + STAmount const depositGBP{GBP, 1, exponent}; + + ammAlice.deposit(DepositArg{ + .account = bob, + .asset1In = depositEuro, + .asset2In = depositGBP}); + invariant( + ammAlice, + env, + "dep3", + exponent != -3 && !env.enabled(fixAMMv1_3)); + }, + {{GBP(10'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + } + + // tfLPToken deposit + testAMM( + [&](AMM& ammAlice, Env& env) { + fund( + env, + gw, + {bob}, + XRP(10'000'000), + {GBP(100'000), EUR(100'000)}, + Fund::Acct); + env.close(); + + ammAlice.deposit(DepositArg{ + .account = bob, + .tokens = IOUAmount{10'1234567890123456, -16}}); + invariant(ammAlice, env, "dep4", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // tfOneAssetLPToken deposit + for (auto const& tokens : + {IOUAmount{1, -3}, + IOUAmount{1, -2}, + IOUAmount{1, -1}, + IOUAmount{1}, + IOUAmount{10}, + IOUAmount{100}, + IOUAmount{1'000}, + IOUAmount{10'000}}) + { + testAMM( + [&](AMM& ammAlice, Env& env) { + fund( + env, + gw, + {bob}, + XRP(10'000'000), + {GBP(100'000), EUR(1'000'000)}, + Fund::Acct); + env.close(); + + ammAlice.deposit(DepositArg{ + .account = bob, + .tokens = tokens, + .asset1In = STAmount{EUR, 1, 6}}); + invariant(ammAlice, env, "dep5", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + } + + // Single deposit with EP not exceeding specified: + // 1'000 GBP with EP not to exceed 5 (GBP/TokensOut) + testAMM( + [&](AMM& ammAlice, Env& env) { + fund( + env, + gw, + {bob}, + XRP(10'000'000), + {GBP(100'000), EUR(100'000)}, + Fund::Acct); + env.close(); + + ammAlice.deposit( + bob, GBP(1'000), std::nullopt, STAmount{GBP, 5}); + invariant(ammAlice, env, "dep6", false); + }, + {{GBP(30'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + } + + void + testWithdrawRounding(FeatureBitset all) + { + testcase("Withdraw Rounding"); + + using namespace jtx; + + // tfLPToken mode + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.withdraw(alice, 1'000); + invariant(ammAlice, env, "with1", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // tfWithdrawAll mode + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.withdraw( + WithdrawArg{.account = alice, .flags = tfWithdrawAll}); + invariant(ammAlice, env, "with2", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // tfTwoAsset withdraw mode + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.withdraw(WithdrawArg{ + .account = alice, + .asset1Out = STAmount{GBP, 3'500}, + .asset2Out = STAmount{EUR, 15'000}, + .flags = tfTwoAsset}); + invariant(ammAlice, env, "with3", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // tfSingleAsset withdraw mode + // Note: This test fails with 0 trading fees, but doesn't fail + // if trading fees is set to 1'000 -- I suspect the compound + // operations in AMMHelpers.cpp:withdrawByTokens compensate for + // the rounding errors + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.withdraw(WithdrawArg{ + .account = alice, + .asset1Out = STAmount{GBP, 1'234}, + .flags = tfSingleAsset}); + invariant(ammAlice, env, "with4", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // tfOneAssetWithdrawAll mode + testAMM( + [&](AMM& ammAlice, Env& env) { + fund( + env, + gw, + {bob}, + XRP(10'000'000), + {GBP(100'000), EUR(100'000)}, + Fund::Acct); + env.close(); + + ammAlice.deposit(DepositArg{ + .account = bob, .asset1In = STAmount{GBP, 3'456}}); + + ammAlice.withdraw(WithdrawArg{ + .account = bob, + .asset1Out = STAmount{GBP, 1'000}, + .flags = tfOneAssetWithdrawAll}); + invariant(ammAlice, env, "with5", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // tfOneAssetLPToken mode + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.withdraw(WithdrawArg{ + .account = alice, + .tokens = 1'000, + .asset1Out = STAmount{GBP, 100}, + .flags = tfOneAssetLPToken}); + invariant(ammAlice, env, "with6", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // tfLimitLPToken mode + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.withdraw(WithdrawArg{ + .account = alice, + .asset1Out = STAmount{GBP, 100}, + .maxEP = IOUAmount{2}, + .flags = tfLimitLPToken}); + invariant(ammAlice, env, "with7", true); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + } + void run() override { @@ -6537,32 +7329,52 @@ struct AMM_test : public jtx::AMMTest testFeeVote(); testInvalidBid(); testBid(all); + testBid(all - fixAMMv1_3); testInvalidAMMPayment(); testBasicPaymentEngine(all); + testBasicPaymentEngine(all - fixAMMv1_3); testBasicPaymentEngine(all - fixReducedOffersV2); + testBasicPaymentEngine(all - fixAMMv1_3 - fixReducedOffersV2); testAMMTokens(); testAmendment(); testFlags(); testRippling(); testAMMAndCLOB(all); + testAMMAndCLOB(all - fixAMMv1_3); testTradingFee(all); + testTradingFee(all - fixAMMv1_3); testAdjustedTokens(all); + testAdjustedTokens(all - fixAMMv1_3); testAutoDelete(); testClawback(); testAMMID(); testSelection(all); + testSelection(all - fixAMMv1_3); testFixDefaultInnerObj(); testMalformed(); testFixOverflowOffer(all); + testFixOverflowOffer(all - fixAMMv1_3); testSwapRounding(); testFixChangeSpotPriceQuality(all); + testFixChangeSpotPriceQuality(all - fixAMMv1_3); testFixAMMOfferBlockedByLOB(all); + testFixAMMOfferBlockedByLOB(all - fixAMMv1_3); testLPTokenBalance(all); + testLPTokenBalance(all - fixAMMv1_3); testAMMClawback(all); testAMMClawback(all - featureAMMClawback); + testAMMClawback(all - fixAMMv1_3 - featureAMMClawback); testAMMDepositWithFrozenAssets(all); testAMMDepositWithFrozenAssets(all - featureAMMClawback); + testAMMDepositWithFrozenAssets(all - fixAMMv1_3 - featureAMMClawback); testFixReserveCheckOnWithdrawal(all); + testDepositAndWithdrawRounding(all); + testDepositAndWithdrawRounding(all - fixAMMv1_3); + testDepositRounding(all); + testDepositRounding(all - fixAMMv1_3); + testWithdrawRounding(all); + testWithdrawRounding(all - fixAMMv1_3); + // testFailedPseudoAccount(); } }; diff --git a/src/test/jtx/AMM.h b/src/test/jtx/AMM.h index 52039f74ae..9f3a07e8b6 100644 --- a/src/test/jtx/AMM.h +++ b/src/test/jtx/AMM.h @@ -125,7 +125,6 @@ class AMM STAmount const asset1_; STAmount const asset2_; uint256 const ammID_; - IOUAmount const initialLPTokens_; bool log_; bool doClose_; // Predict next purchase price @@ -138,6 +137,7 @@ class AMM std::uint32_t const fee_; AccountID const ammAccount_; Issue const lptIssue_; + IOUAmount const initialLPTokens_; public: AMM(Env& env, @@ -194,6 +194,12 @@ class AMM Issue const& issue2, std::optional const& account = std::nullopt) const; + std::tuple + balances(std::optional const& account = std::nullopt) const + { + return balances(asset1_.get(), asset2_.get(), account); + } + [[nodiscard]] bool expectLPTokens(AccountID const& account, IOUAmount const& tokens) const; @@ -428,6 +434,9 @@ class AMM [[nodiscard]] bool expectAuctionSlot(auto&& cb) const; + + IOUAmount + initialTokens(); }; namespace amm { diff --git a/src/test/jtx/AMMTest.h b/src/test/jtx/AMMTest.h index 0481dc98a4..4902e5f499 100644 --- a/src/test/jtx/AMMTest.h +++ b/src/test/jtx/AMMTest.h @@ -33,6 +33,15 @@ class AMM; enum class Fund { All, Acct, Gw, IOUOnly }; +struct TestAMMArg +{ + std::optional> pool = std::nullopt; + std::uint16_t tfee = 0; + std::optional ter = std::nullopt; + std::vector features = {supported_amendments()}; + bool noLog = false; +}; + void fund( jtx::Env& env, @@ -85,6 +94,11 @@ class AMMTestBase : public beast::unit_test::suite std::uint16_t tfee = 0, std::optional const& ter = std::nullopt, std::vector const& features = {supported_amendments()}); + + void + testAMM( + std::function&& cb, + TestAMMArg const& arg); }; class AMMTest : public jtx::AMMTestBase diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index dc5b4be48f..34056d3b08 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -646,6 +646,12 @@ class Env void disableFeature(uint256 const feature); + bool + enabled(uint256 feature) const + { + return current()->rules().enabled(feature); + } + private: void fund(bool setDefaultRipple, STAmount const& amount, Account const& account); diff --git a/src/test/jtx/impl/AMM.cpp b/src/test/jtx/impl/AMM.cpp index 089d3508d7..cbb815994d 100644 --- a/src/test/jtx/impl/AMM.cpp +++ b/src/test/jtx/impl/AMM.cpp @@ -18,8 +18,9 @@ //============================================================================== #include - #include + +#include #include #include #include @@ -38,12 +39,16 @@ number(STAmount const& a) return a; } -static IOUAmount -initialTokens(STAmount const& asset1, STAmount const& asset2) +IOUAmount +AMM::initialTokens() { - auto const product = number(asset1) * number(asset2); - return (IOUAmount)(product.mantissa() >= 0 ? root2(product) - : root2(-product)); + if (!env_.enabled(fixAMMv1_3)) + { + auto const product = number(asset1_) * number(asset2_); + return (IOUAmount)(product.mantissa() >= 0 ? root2(product) + : root2(-product)); + } + return getLPTokensBalance(); } AMM::AMM( @@ -64,7 +69,6 @@ AMM::AMM( , asset1_(asset1) , asset2_(asset2) , ammID_(keylet::amm(asset1_.issue(), asset2_.issue()).key) - , initialLPTokens_(initialTokens(asset1, asset2)) , log_(log) , doClose_(close) , lastPurchasePrice_(0) @@ -77,6 +81,7 @@ AMM::AMM( asset1_.issue().currency, asset2_.issue().currency, ammAccount_)) + , initialLPTokens_(initialTokens()) { } diff --git a/src/test/jtx/impl/AMMTest.cpp b/src/test/jtx/impl/AMMTest.cpp index 7ba567a37d..eb80d9ce73 100644 --- a/src/test/jtx/impl/AMMTest.cpp +++ b/src/test/jtx/impl/AMMTest.cpp @@ -17,9 +17,9 @@ */ //============================================================================== -#include - #include +#include +#include #include #include #include @@ -104,15 +104,37 @@ AMMTestBase::testAMM( std::uint16_t tfee, std::optional const& ter, std::vector const& vfeatures) +{ + testAMM( + std::move(cb), + TestAMMArg{ + .pool = pool, .tfee = tfee, .ter = ter, .features = vfeatures}); +} + +void +AMMTestBase::testAMM( + std::function&& cb, + TestAMMArg const& arg) { using namespace jtx; - for (auto const& features : vfeatures) + std::string logs; + + for (auto const& features : arg.features) { - Env env{*this, features}; + // Env env{ + // *this, + // features, + // arg.noLog ? std::make_unique(&logs) : nullptr}; + Env env( + *this, + envconfig(), + features, + nullptr, + beast::severities::kDisabled); auto const [asset1, asset2] = - pool ? *pool : std::make_pair(XRP(10000), USD(10000)); + arg.pool ? *arg.pool : std::make_pair(XRP(10000), USD(10000)); auto tofund = [&](STAmount const& a) -> STAmount { if (a.native()) { @@ -142,7 +164,7 @@ AMMTestBase::testAMM( alice, asset1, asset2, - CreateArg{.log = false, .tfee = tfee, .err = ter}); + CreateArg{.log = false, .tfee = arg.tfee, .err = arg.ter}); if (BEAST_EXPECT( ammAlice.expectBalances(asset1, asset2, ammAlice.tokens()))) cb(ammAlice, env); diff --git a/src/test/rpc/AMMInfo_test.cpp b/src/test/rpc/AMMInfo_test.cpp index c1e059a3ea..98e62d8929 100644 --- a/src/test/rpc/AMMInfo_test.cpp +++ b/src/test/rpc/AMMInfo_test.cpp @@ -203,98 +203,119 @@ class AMMInfo_test : public jtx::AMMTestBase } void - testVoteAndBid() + testVoteAndBid(FeatureBitset features) { testcase("Vote and Bid"); using namespace jtx; - testAMM([&](AMM& ammAlice, Env& env) { - BEAST_EXPECT(ammAlice.expectAmmRpcInfo( - XRP(10000), USD(10000), IOUAmount{10000000, 0})); - std::unordered_map votes; - votes.insert({alice.human(), 0}); - for (int i = 0; i < 7; ++i) - { - Account a(std::to_string(i)); - votes.insert({a.human(), 50 * (i + 1)}); - fund(env, gw, {a}, {USD(10000)}, Fund::Acct); - ammAlice.deposit(a, 10000000); - ammAlice.vote(a, 50 * (i + 1)); - } - BEAST_EXPECT(ammAlice.expectTradingFee(175)); - Account ed("ed"); - Account bill("bill"); - env.fund(XRP(1000), bob, ed, bill); - env(ammAlice.bid( - {.bidMin = 100, .authAccounts = {carol, bob, ed, bill}})); - BEAST_EXPECT(ammAlice.expectAmmRpcInfo( - XRP(80000), - USD(80000), - IOUAmount{79994400}, - std::nullopt, - std::nullopt, - ammAlice.ammAccount())); - for (auto i = 0; i < 2; ++i) - { - std::unordered_set authAccounts = { - carol.human(), bob.human(), ed.human(), bill.human()}; - auto const ammInfo = i ? ammAlice.ammRpcInfo() - : ammAlice.ammRpcInfo( - std::nullopt, - std::nullopt, - std::nullopt, - std::nullopt, - ammAlice.ammAccount()); - auto const& amm = ammInfo[jss::amm]; - try + testAMM( + [&](AMM& ammAlice, Env& env) { + BEAST_EXPECT(ammAlice.expectAmmRpcInfo( + XRP(10000), USD(10000), IOUAmount{10000000, 0})); + std::unordered_map votes; + votes.insert({alice.human(), 0}); + for (int i = 0; i < 7; ++i) { - // votes - auto const voteSlots = amm[jss::vote_slots]; - auto votesCopy = votes; - for (std::uint8_t i = 0; i < 8; ++i) + Account a(std::to_string(i)); + votes.insert({a.human(), 50 * (i + 1)}); + if (!features[fixAMMv1_3]) + fund(env, gw, {a}, {USD(10000)}, Fund::Acct); + else + fund(env, gw, {a}, {USD(10001)}, Fund::Acct); + ammAlice.deposit(a, 10000000); + ammAlice.vote(a, 50 * (i + 1)); + } + BEAST_EXPECT(ammAlice.expectTradingFee(175)); + Account ed("ed"); + Account bill("bill"); + env.fund(XRP(1000), bob, ed, bill); + env(ammAlice.bid( + {.bidMin = 100, .authAccounts = {carol, bob, ed, bill}})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(ammAlice.expectAmmRpcInfo( + XRP(80000), + USD(80000), + IOUAmount{79994400}, + std::nullopt, + std::nullopt, + ammAlice.ammAccount())); + else + BEAST_EXPECT(ammAlice.expectAmmRpcInfo( + XRPAmount(80000000005), + STAmount{USD, UINT64_C(80'000'00000000005), -11}, + IOUAmount{79994400}, + std::nullopt, + std::nullopt, + ammAlice.ammAccount())); + for (auto i = 0; i < 2; ++i) + { + std::unordered_set authAccounts = { + carol.human(), bob.human(), ed.human(), bill.human()}; + auto const ammInfo = i ? ammAlice.ammRpcInfo() + : ammAlice.ammRpcInfo( + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + ammAlice.ammAccount()); + auto const& amm = ammInfo[jss::amm]; + try { - if (!BEAST_EXPECT( - votes[voteSlots[i][jss::account].asString()] == - voteSlots[i][jss::trading_fee].asUInt() && - voteSlots[i][jss::vote_weight].asUInt() == - 12500)) + // votes + auto const voteSlots = amm[jss::vote_slots]; + auto votesCopy = votes; + for (std::uint8_t i = 0; i < 8; ++i) + { + if (!BEAST_EXPECT( + votes[voteSlots[i][jss::account] + .asString()] == + voteSlots[i][jss::trading_fee] + .asUInt() && + voteSlots[i][jss::vote_weight].asUInt() == + 12500)) + return; + votes.erase(voteSlots[i][jss::account].asString()); + } + if (!BEAST_EXPECT(votes.empty())) return; - votes.erase(voteSlots[i][jss::account].asString()); - } - if (!BEAST_EXPECT(votes.empty())) - return; - votes = votesCopy; + votes = votesCopy; - // bid - auto const auctionSlot = amm[jss::auction_slot]; - for (std::uint8_t i = 0; i < 4; ++i) - { - if (!BEAST_EXPECT(authAccounts.contains( + // bid + auto const auctionSlot = amm[jss::auction_slot]; + for (std::uint8_t i = 0; i < 4; ++i) + { + if (!BEAST_EXPECT(authAccounts.contains( + auctionSlot[jss::auth_accounts][i] + [jss::account] + .asString()))) + return; + authAccounts.erase( auctionSlot[jss::auth_accounts][i][jss::account] - .asString()))) + .asString()); + } + if (!BEAST_EXPECT(authAccounts.empty())) return; - authAccounts.erase( - auctionSlot[jss::auth_accounts][i][jss::account] - .asString()); + BEAST_EXPECT( + auctionSlot[jss::account].asString() == + alice.human() && + auctionSlot[jss::discounted_fee].asUInt() == 17 && + auctionSlot[jss::price][jss::value].asString() == + "5600" && + auctionSlot[jss::price][jss::currency].asString() == + to_string(ammAlice.lptIssue().currency) && + auctionSlot[jss::price][jss::issuer].asString() == + to_string(ammAlice.lptIssue().account)); + } + catch (std::exception const& e) + { + fail(e.what(), __FILE__, __LINE__); } - if (!BEAST_EXPECT(authAccounts.empty())) - return; - BEAST_EXPECT( - auctionSlot[jss::account].asString() == alice.human() && - auctionSlot[jss::discounted_fee].asUInt() == 17 && - auctionSlot[jss::price][jss::value].asString() == - "5600" && - auctionSlot[jss::price][jss::currency].asString() == - to_string(ammAlice.lptIssue().currency) && - auctionSlot[jss::price][jss::issuer].asString() == - to_string(ammAlice.lptIssue().account)); - } - catch (std::exception const& e) - { - fail(e.what(), __FILE__, __LINE__); } - } - }); + }, + std::nullopt, + 0, + std::nullopt, + {features}); } void @@ -337,9 +358,12 @@ class AMMInfo_test : public jtx::AMMTestBase void run() override { + using namespace jtx; + auto const all = supported_amendments(); testErrors(); testSimpleRpc(); - testVoteAndBid(); + testVoteAndBid(all); + testVoteAndBid(all - fixAMMv1_3); testFreeze(); testInvalidAmmField(); } diff --git a/src/xrpld/app/misc/AMMHelpers.h b/src/xrpld/app/misc/AMMHelpers.h index be75abce07..5825d6ad95 100644 --- a/src/xrpld/app/misc/AMMHelpers.h +++ b/src/xrpld/app/misc/AMMHelpers.h @@ -51,6 +51,8 @@ reduceOffer(auto const& amount) } // namespace detail +enum class IsDeposit : bool { No = false, Yes = true }; + /** Calculate LP Tokens given AMM pool reserves. * @param asset1 AMM one side of the pool reserve * @param asset2 AMM another side of the pool reserve @@ -70,7 +72,7 @@ ammLPTokens( * @return tokens */ STAmount -lpTokensIn( +lpTokensOut( STAmount const& asset1Balance, STAmount const& asset1Deposit, STAmount const& lptAMMBalance, @@ -99,7 +101,7 @@ ammAssetIn( * @return tokens out amount */ STAmount -lpTokensOut( +lpTokensIn( STAmount const& asset1Balance, STAmount const& asset1Withdraw, STAmount const& lptAMMBalance, @@ -113,7 +115,7 @@ lpTokensOut( * @return calculated asset amount */ STAmount -withdrawByTokens( +ammAssetOut( STAmount const& assetBalance, STAmount const& lptAMMBalance, STAmount const& lpTokens, @@ -517,13 +519,13 @@ square(Number const& n); * withdraw to cancel out the precision loss. * @param lptAMMBalance LPT AMM Balance * @param lpTokens LP tokens to deposit or withdraw - * @param isDeposit true if deposit, false if withdraw + * @param isDeposit Yes if deposit, No if withdraw */ STAmount adjustLPTokens( STAmount const& lptAMMBalance, STAmount const& lpTokens, - bool isDeposit); + IsDeposit isDeposit); /** Calls adjustLPTokens() and adjusts deposit or withdraw amounts if * the adjusted LP tokens are less than the provided LP tokens. @@ -533,7 +535,7 @@ adjustLPTokens( * @param lptAMMBalance LPT AMM Balance * @param lpTokens LP tokens to deposit or withdraw * @param tfee trading fee in basis points - * @param isDeposit true if deposit, false if withdraw + * @param isDeposit Yes if deposit, No if withdraw * @return */ std::tuple, STAmount> @@ -544,7 +546,7 @@ adjustAmountsByLPTokens( STAmount const& lptAMMBalance, STAmount const& lpTokens, std::uint16_t tfee, - bool isDeposit); + IsDeposit isDeposit); /** Positive solution for quadratic equation: * x = (-b + sqrt(b**2 + 4*a*c))/(2*a) @@ -552,6 +554,141 @@ adjustAmountsByLPTokens( Number solveQuadraticEq(Number const& a, Number const& b, Number const& c); +STAmount +multiply(STAmount const& amount, Number const& frac, Number::rounding_mode rm); + +namespace detail { + +inline Number::rounding_mode +getLPTokenRounding(IsDeposit isDeposit) +{ + // Minimize on deposit, maximize on withdraw to ensure + // AMM invariant sqrt(poolAsset1 * poolAsset2) >= LPTokensBalance + return isDeposit == IsDeposit::Yes ? Number::downward : Number::upward; +} + +inline Number::rounding_mode +getAssetRounding(IsDeposit isDeposit) +{ + // Maximize on deposit, minimize on withdraw to ensure + // AMM invariant sqrt(poolAsset1 * poolAsset2) >= LPTokensBalance + return isDeposit == IsDeposit::Yes ? Number::upward : Number::downward; +} + +} // namespace detail + +/** Round AMM equal deposit/withdrawal amount. Deposit/withdrawal formulas + * calculate the amount as a fractional value of the pool balance. The rounding + * takes place on the last step of multiplying the balance by the fraction if + * AMMv1_3 is enabled. + */ +template +STAmount +getRoundedAsset( + Rules const& rules, + STAmount const& balance, + A const& frac, + IsDeposit isDeposit) +{ + if (!rules.enabled(fixAMMv1_3)) + { + if constexpr (std::is_same_v) + return multiply(balance, frac, balance.issue()); + else + return toSTAmount(balance.issue(), balance * frac); + } + auto const rm = detail::getAssetRounding(isDeposit); + return multiply(balance, frac, rm); +} + +/** Round AMM single deposit/withdrawal amount. + * The lambda's are used to delay evaluation until the function + * is executed so that the calculation is not done twice. noRoundCb() is + * called if AMMv1_3 is disabled. Otherwise, the rounding is set and + * the amount is: + * isDeposit is Yes - the balance multiplied by productCb() + * isDeposit is No - the result of productCb(). The rounding is + * the same for all calculations in productCb() + */ +STAmount +getRoundedAsset( + Rules const& rules, + std::function&& noRoundCb, + STAmount const& balance, + std::function&& productCb, + IsDeposit isDeposit); + +/** Round AMM deposit/withdrawal LPToken amount. Deposit/withdrawal formulas + * calculate the lptokens as a fractional value of the AMM total lptokens. + * The rounding takes place on the last step of multiplying the balance by + * the fraction if AMMv1_3 is enabled. The tokens are then + * adjusted to factor in the loss in precision (we only keep 16 significant + * digits) when adding the lptokens to the balance. + */ +STAmount +getRoundedLPTokens( + Rules const& rules, + STAmount const& balance, + Number const& frac, + IsDeposit isDeposit); + +/** Round AMM single deposit/withdrawal LPToken amount. + * The lambda's are used to delay evaluation until the function is executed + * so that the calculations are not done twice. + * noRoundCb() is called if AMMv1_3 is disabled. Otherwise, the rounding is set + * and the lptokens are: + * if isDeposit is Yes - the result of productCb(). The rounding is + * the same for all calculations in productCb() + * if isDeposit is No - the balance multiplied by productCb() + * The lptokens are then adjusted to factor in the loss in precision + * (we only keep 16 significant digits) when adding the lptokens to the balance. + */ +STAmount +getRoundedLPTokens( + Rules const& rules, + std::function&& noRoundCb, + STAmount const& lptAMMBalance, + std::function&& productCb, + IsDeposit isDeposit); + +/* Next two functions adjust asset in/out amount to factor in the adjusted + * lptokens. The lptokens are calculated from the asset in/out. The lptokens are + * then adjusted to factor in the loss in precision. The adjusted lptokens might + * be less than the initially calculated tokens. Therefore, the asset in/out + * must be adjusted. The rounding might result in the adjusted amount being + * greater than the original asset in/out amount. If this happens, + * then the original amount is reduced by the difference in the adjusted amount + * and the original amount. The actual tokens and the actual adjusted amount + * are then recalculated. The minimum of the original and the actual + * adjusted amount is returned. + */ +std::pair +adjustAssetInByTokens( + Rules const& rules, + STAmount const& balance, + STAmount const& amount, + STAmount const& lptAMMBalance, + STAmount const& tokens, + std::uint16_t tfee); +std::pair +adjustAssetOutByTokens( + Rules const& rules, + STAmount const& balance, + STAmount const& amount, + STAmount const& lptAMMBalance, + STAmount const& tokens, + std::uint16_t tfee); + +/** Find a fraction of tokens after the tokens are adjusted. The fraction + * is used to adjust equal deposit/withdraw amount. + */ +Number +adjustFracByTokens( + Rules const& rules, + STAmount const& lptAMMBalance, + STAmount const& tokens, + Number const& frac); + } // namespace ripple #endif // RIPPLE_APP_MISC_AMMHELPERS_H_INCLUDED diff --git a/src/xrpld/app/misc/detail/AMMHelpers.cpp b/src/xrpld/app/misc/detail/AMMHelpers.cpp index 76b977e9a9..97c355b1ff 100644 --- a/src/xrpld/app/misc/detail/AMMHelpers.cpp +++ b/src/xrpld/app/misc/detail/AMMHelpers.cpp @@ -27,6 +27,10 @@ ammLPTokens( STAmount const& asset2, Issue const& lptIssue) { + // AMM invariant: sqrt(asset1 * asset2) >= LPTokensBalance + auto const rounding = + isFeatureEnabled(fixAMMv1_3) ? Number::downward : Number::getround(); + NumberRoundModeGuard g(rounding); auto const tokens = root2(asset1 * asset2); return toSTAmount(lptIssue, tokens); } @@ -38,7 +42,7 @@ ammLPTokens( * where f1 = 1 - tfee, f2 = (1 - tfee/2)/f1 */ STAmount -lpTokensIn( +lpTokensOut( STAmount const& asset1Balance, STAmount const& asset1Deposit, STAmount const& lptAMMBalance, @@ -48,8 +52,17 @@ lpTokensIn( auto const f2 = feeMultHalf(tfee) / f1; Number const r = asset1Deposit / asset1Balance; auto const c = root2(f2 * f2 + r / f1) - f2; - auto const t = lptAMMBalance * (r - c) / (1 + c); - return toSTAmount(lptAMMBalance.issue(), t); + if (!isFeatureEnabled(fixAMMv1_3)) + { + auto const t = lptAMMBalance * (r - c) / (1 + c); + return toSTAmount(lptAMMBalance.issue(), t); + } + else + { + // minimize tokens out + auto const frac = (r - c) / (1 + c); + return multiply(lptAMMBalance, frac, Number::downward); + } } /* Equation 4 solves equation 3 for b: @@ -78,8 +91,17 @@ ammAssetIn( auto const a = 1 / (t2 * t2); auto const b = 2 * d / t2 - 1 / f1; auto const c = d * d - f2 * f2; - return toSTAmount( - asset1Balance.issue(), asset1Balance * solveQuadraticEq(a, b, c)); + if (!isFeatureEnabled(fixAMMv1_3)) + { + return toSTAmount( + asset1Balance.issue(), asset1Balance * solveQuadraticEq(a, b, c)); + } + else + { + // maximize deposit + auto const frac = solveQuadraticEq(a, b, c); + return multiply(asset1Balance, frac, Number::upward); + } } /* Equation 7: @@ -87,7 +109,7 @@ ammAssetIn( * where R = b/B, c = R*fee + 2 - fee */ STAmount -lpTokensOut( +lpTokensIn( STAmount const& asset1Balance, STAmount const& asset1Withdraw, STAmount const& lptAMMBalance, @@ -96,8 +118,17 @@ lpTokensOut( Number const fr = asset1Withdraw / asset1Balance; auto const f1 = getFee(tfee); auto const c = fr * f1 + 2 - f1; - auto const t = lptAMMBalance * (c - root2(c * c - 4 * fr)) / 2; - return toSTAmount(lptAMMBalance.issue(), t); + if (!isFeatureEnabled(fixAMMv1_3)) + { + auto const t = lptAMMBalance * (c - root2(c * c - 4 * fr)) / 2; + return toSTAmount(lptAMMBalance.issue(), t); + } + else + { + // maximize tokens in + auto const frac = (c - root2(c * c - 4 * fr)) / 2; + return multiply(lptAMMBalance, frac, Number::upward); + } } /* Equation 8 solves equation 7 for b: @@ -111,7 +142,7 @@ lpTokensOut( * R = (t1**2 + t1*(f - 2)) / (t1*f - 1) */ STAmount -withdrawByTokens( +ammAssetOut( STAmount const& assetBalance, STAmount const& lptAMMBalance, STAmount const& lpTokens, @@ -119,8 +150,17 @@ withdrawByTokens( { auto const f = getFee(tfee); Number const t1 = lpTokens / lptAMMBalance; - auto const b = assetBalance * (t1 * t1 - t1 * (2 - f)) / (t1 * f - 1); - return toSTAmount(assetBalance.issue(), b); + if (!isFeatureEnabled(fixAMMv1_3)) + { + auto const b = assetBalance * (t1 * t1 - t1 * (2 - f)) / (t1 * f - 1); + return toSTAmount(assetBalance.issue(), b); + } + else + { + // minimize withdraw + auto const frac = (t1 * t1 - t1 * (2 - f)) / (t1 * f - 1); + return multiply(assetBalance, frac, Number::downward); + } } Number @@ -133,12 +173,12 @@ STAmount adjustLPTokens( STAmount const& lptAMMBalance, STAmount const& lpTokens, - bool isDeposit) + IsDeposit isDeposit) { // Force rounding downward to ensure adjusted tokens are less or equal // to requested tokens. saveNumberRoundMode rm(Number::setround(Number::rounding_mode::downward)); - if (isDeposit) + if (isDeposit == IsDeposit::Yes) return (lptAMMBalance + lpTokens) - lptAMMBalance; return (lpTokens - lptAMMBalance) + lptAMMBalance; } @@ -151,8 +191,12 @@ adjustAmountsByLPTokens( STAmount const& lptAMMBalance, STAmount const& lpTokens, std::uint16_t tfee, - bool isDeposit) + IsDeposit isDeposit) { + // AMMv1_3 amendment adjusts tokens and amounts in deposit/withdraw + if (isFeatureEnabled(fixAMMv1_3)) + return std::make_tuple(amount, amount2, lpTokens); + auto const lpTokensActual = adjustLPTokens(lptAMMBalance, lpTokens, isDeposit); @@ -177,10 +221,10 @@ adjustAmountsByLPTokens( // Single trade auto const amountActual = [&]() { - if (isDeposit) + if (isDeposit == IsDeposit::Yes) return ammAssetIn( amountBalance, lptAMMBalance, lpTokensActual, tfee); - return withdrawByTokens( + return ammAssetOut( amountBalance, lptAMMBalance, lpTokensActual, tfee); }(); @@ -215,4 +259,132 @@ solveQuadraticEqSmallest(Number const& a, Number const& b, Number const& c) return (2 * c) / (-b + root2(d)); } +STAmount +multiply(STAmount const& amount, Number const& frac, Number::rounding_mode rm) +{ + NumberRoundModeGuard g(rm); + auto const t = amount * frac; + return toSTAmount(amount.issue(), t, rm); +} + +STAmount +getRoundedAsset( + Rules const& rules, + std::function&& noRoundCb, + STAmount const& balance, + std::function&& productCb, + IsDeposit isDeposit) +{ + if (!rules.enabled(fixAMMv1_3)) + return toSTAmount(balance.issue(), noRoundCb()); + + auto const rm = detail::getAssetRounding(isDeposit); + if (isDeposit == IsDeposit::Yes) + return multiply(balance, productCb(), rm); + NumberRoundModeGuard g(rm); + return toSTAmount(balance.issue(), productCb(), rm); +} + +STAmount +getRoundedLPTokens( + Rules const& rules, + STAmount const& balance, + Number const& frac, + IsDeposit isDeposit) +{ + if (!rules.enabled(fixAMMv1_3)) + return toSTAmount(balance.issue(), balance * frac); + + auto const rm = detail::getLPTokenRounding(isDeposit); + auto const tokens = multiply(balance, frac, rm); + return adjustLPTokens(balance, tokens, isDeposit); +} + +STAmount +getRoundedLPTokens( + Rules const& rules, + std::function&& noRoundCb, + STAmount const& lptAMMBalance, + std::function&& productCb, + IsDeposit isDeposit) +{ + if (!rules.enabled(fixAMMv1_3)) + return toSTAmount(lptAMMBalance.issue(), noRoundCb()); + + auto const tokens = [&] { + auto const rm = detail::getLPTokenRounding(isDeposit); + if (isDeposit == IsDeposit::Yes) + { + NumberRoundModeGuard g(rm); + return toSTAmount(lptAMMBalance.issue(), productCb(), rm); + } + return multiply(lptAMMBalance, productCb(), rm); + }(); + return adjustLPTokens(lptAMMBalance, tokens, isDeposit); +} + +std::pair +adjustAssetInByTokens( + Rules const& rules, + STAmount const& balance, + STAmount const& amount, + STAmount const& lptAMMBalance, + STAmount const& tokens, + std::uint16_t tfee) +{ + if (!rules.enabled(fixAMMv1_3)) + return {tokens, amount}; + auto assetAdj = ammAssetIn(balance, lptAMMBalance, tokens, tfee); + auto tokensAdj = tokens; + // Rounding didn't work the right way. + // Try to adjust the original deposit amount by difference + // in adjust and original amount. Then adjust tokens and deposit amount. + if (assetAdj > amount) + { + auto const adjAmount = amount - (assetAdj - amount); + auto const t = lpTokensOut(balance, adjAmount, lptAMMBalance, tfee); + tokensAdj = adjustLPTokens(lptAMMBalance, t, IsDeposit::Yes); + assetAdj = ammAssetIn(balance, lptAMMBalance, tokensAdj, tfee); + } + return {tokensAdj, std::min(amount, assetAdj)}; +} + +std::pair +adjustAssetOutByTokens( + Rules const& rules, + STAmount const& balance, + STAmount const& amount, + STAmount const& lptAMMBalance, + STAmount const& tokens, + std::uint16_t tfee) +{ + if (!rules.enabled(fixAMMv1_3)) + return {tokens, amount}; + auto assetAdj = ammAssetOut(balance, lptAMMBalance, tokens, tfee); + auto tokensAdj = tokens; + // Rounding didn't work the right way. + // Try to adjust the original deposit amount by difference + // in adjust and original amount. Then adjust tokens and deposit amount. + if (assetAdj > amount) + { + auto const adjAmount = amount - (assetAdj - amount); + auto const t = lpTokensIn(balance, adjAmount, lptAMMBalance, tfee); + tokensAdj = adjustLPTokens(lptAMMBalance, t, IsDeposit::No); + assetAdj = ammAssetOut(balance, lptAMMBalance, tokensAdj, tfee); + } + return {tokensAdj, std::min(amount, assetAdj)}; +} + +Number +adjustFracByTokens( + Rules const& rules, + STAmount const& lptAMMBalance, + STAmount const& tokens, + Number const& frac) +{ + if (!rules.enabled(fixAMMv1_3)) + return frac; + return tokens / lptAMMBalance; +} + } // namespace ripple diff --git a/src/xrpld/app/tx/detail/AMMBid.cpp b/src/xrpld/app/tx/detail/AMMBid.cpp index e8a14c1492..9a9730228d 100644 --- a/src/xrpld/app/tx/detail/AMMBid.cpp +++ b/src/xrpld/app/tx/detail/AMMBid.cpp @@ -79,6 +79,21 @@ AMMBid::preflight(PreflightContext const& ctx) JLOG(ctx.j.debug()) << "AMM Bid: Invalid number of AuthAccounts."; return temMALFORMED; } + else if (ctx.rules.enabled(fixAMMv1_3)) + { + AccountID account = ctx.tx[sfAccount]; + std::set unique; + for (auto const& obj : authAccounts) + { + auto authAccount = obj[sfAccount]; + if (authAccount == account || unique.contains(authAccount)) + { + JLOG(ctx.j.debug()) << "AMM Bid: Invalid auth.account."; + return temMALFORMED; + } + unique.insert(authAccount); + } + } } return preflight2(ctx); @@ -233,7 +248,9 @@ applyBid( auctionSlot.makeFieldAbsent(sfAuthAccounts); // Burn the remaining bid amount auto const saBurn = adjustLPTokens( - lptAMMBalance, toSTAmount(lptAMMBalance.issue(), burn), false); + lptAMMBalance, + toSTAmount(lptAMMBalance.issue(), burn), + IsDeposit::No); if (saBurn >= lptAMMBalance) { // This error case should never occur. diff --git a/src/xrpld/app/tx/detail/AMMDeposit.cpp b/src/xrpld/app/tx/detail/AMMDeposit.cpp index 675f560098..50152e4014 100644 --- a/src/xrpld/app/tx/detail/AMMDeposit.cpp +++ b/src/xrpld/app/tx/detail/AMMDeposit.cpp @@ -545,7 +545,7 @@ AMMDeposit::deposit( lptAMMBalance, lpTokensDeposit, tfee, - true); + IsDeposit::Yes); if (lpTokensDepositActual <= beast::zero) { @@ -628,6 +628,17 @@ AMMDeposit::deposit( return {tesSUCCESS, lptAMMBalance + lpTokensDepositActual}; } +static STAmount +adjustLPTokensOut( + Rules const& rules, + STAmount const& lptAMMBalance, + STAmount const& lpTokensDeposit) +{ + if (!rules.enabled(fixAMMv1_3)) + return lpTokensDeposit; + return adjustLPTokens(lptAMMBalance, lpTokensDeposit, IsDeposit::Yes); +} + /** Proportional deposit of pools assets in exchange for the specified * amount of LPTokens. */ @@ -645,16 +656,25 @@ AMMDeposit::equalDepositTokens( { try { + auto const tokensAdj = + adjustLPTokensOut(view.rules(), lptAMMBalance, lpTokensDeposit); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; auto const frac = - divide(lpTokensDeposit, lptAMMBalance, lptAMMBalance.issue()); + divide(tokensAdj, lptAMMBalance, lptAMMBalance.issue()); + // amounts factor in the adjusted tokens + auto const amountDeposit = + getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::Yes); + auto const amount2Deposit = + getRoundedAsset(view.rules(), amount2Balance, frac, IsDeposit::Yes); return deposit( view, ammAccount, amountBalance, - multiply(amountBalance, frac, amountBalance.issue()), - multiply(amount2Balance, frac, amount2Balance.issue()), + amountDeposit, + amount2Deposit, lptAMMBalance, - lpTokensDeposit, + tokensAdj, depositMin, deposit2Min, std::nullopt, @@ -711,37 +731,55 @@ AMMDeposit::equalDepositLimit( std::uint16_t tfee) { auto frac = Number{amount} / amountBalance; - auto tokens = toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac); - if (tokens == beast::zero) - return {tecAMM_FAILED, STAmount{}}; - auto const amount2Deposit = amount2Balance * frac; + auto tokensAdj = + getRoundedLPTokens(view.rules(), lptAMMBalance, frac, IsDeposit::Yes); + if (tokensAdj == beast::zero) + { + if (!view.rules().enabled(fixAMMv1_3)) + return {tecAMM_FAILED, STAmount{}}; // LCOV_EXCL_LINE + else + return {tecAMM_INVALID_TOKENS, STAmount{}}; + } + // factor in the adjusted tokens + frac = adjustFracByTokens(view.rules(), lptAMMBalance, tokensAdj, frac); + auto const amount2Deposit = + getRoundedAsset(view.rules(), amount2Balance, frac, IsDeposit::Yes); if (amount2Deposit <= amount2) return deposit( view, ammAccount, amountBalance, amount, - toSTAmount(amount2Balance.issue(), amount2Deposit), + amount2Deposit, lptAMMBalance, - tokens, + tokensAdj, std::nullopt, std::nullopt, lpTokensDepositMin, tfee); frac = Number{amount2} / amount2Balance; - tokens = toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac); - if (tokens == beast::zero) - return {tecAMM_FAILED, STAmount{}}; - auto const amountDeposit = amountBalance * frac; + tokensAdj = + getRoundedLPTokens(view.rules(), lptAMMBalance, frac, IsDeposit::Yes); + if (tokensAdj == beast::zero) + { + if (!view.rules().enabled(fixAMMv1_3)) + return {tecAMM_FAILED, STAmount{}}; // LCOV_EXCL_LINE + else + return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE + } + // factor in the adjusted tokens + frac = adjustFracByTokens(view.rules(), lptAMMBalance, tokensAdj, frac); + auto const amountDeposit = + getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::Yes); if (amountDeposit <= amount) return deposit( view, ammAccount, amountBalance, - toSTAmount(amountBalance.issue(), amountDeposit), + amountDeposit, amount2, lptAMMBalance, - tokens, + tokensAdj, std::nullopt, std::nullopt, lpTokensDepositMin, @@ -767,17 +805,30 @@ AMMDeposit::singleDeposit( std::optional const& lpTokensDepositMin, std::uint16_t tfee) { - auto const tokens = lpTokensIn(amountBalance, amount, lptAMMBalance, tfee); + auto const tokens = adjustLPTokensOut( + view.rules(), + lptAMMBalance, + lpTokensOut(amountBalance, amount, lptAMMBalance, tfee)); if (tokens == beast::zero) - return {tecAMM_FAILED, STAmount{}}; + { + if (!view.rules().enabled(fixAMMv1_3)) + return {tecAMM_FAILED, STAmount{}}; // LCOV_EXCL_LINE + else + return {tecAMM_INVALID_TOKENS, STAmount{}}; + } + // factor in the adjusted tokens + auto const [tokensAdj, amountDepositAdj] = adjustAssetInByTokens( + view.rules(), amountBalance, amount, lptAMMBalance, tokens, tfee); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE return deposit( view, ammAccount, amountBalance, - amount, + amountDepositAdj, std::nullopt, lptAMMBalance, - tokens, + tokensAdj, std::nullopt, std::nullopt, lpTokensDepositMin, @@ -801,8 +852,13 @@ AMMDeposit::singleDepositTokens( STAmount const& lpTokensDeposit, std::uint16_t tfee) { + auto const tokensAdj = + adjustLPTokensOut(view.rules(), lptAMMBalance, lpTokensDeposit); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; + // the adjusted tokens are factored in auto const amountDeposit = - ammAssetIn(amountBalance, lptAMMBalance, lpTokensDeposit, tfee); + ammAssetIn(amountBalance, lptAMMBalance, tokensAdj, tfee); if (amountDeposit > amount) return {tecAMM_FAILED, STAmount{}}; return deposit( @@ -812,7 +868,7 @@ AMMDeposit::singleDepositTokens( amountDeposit, std::nullopt, lptAMMBalance, - lpTokensDeposit, + tokensAdj, std::nullopt, std::nullopt, std::nullopt, @@ -856,20 +912,32 @@ AMMDeposit::singleDepositEPrice( { if (amount != beast::zero) { - auto const tokens = - lpTokensIn(amountBalance, amount, lptAMMBalance, tfee); + auto const tokens = adjustLPTokensOut( + view.rules(), + lptAMMBalance, + lpTokensOut(amountBalance, amount, lptAMMBalance, tfee)); if (tokens <= beast::zero) - return {tecAMM_FAILED, STAmount{}}; - auto const ep = Number{amount} / tokens; + { + if (!view.rules().enabled(fixAMMv1_3)) + return {tecAMM_FAILED, STAmount{}}; // LCOV_EXCL_LINE + else + return {tecAMM_INVALID_TOKENS, STAmount{}}; + } + // factor in the adjusted tokens + auto const [tokensAdj, amountDepositAdj] = adjustAssetInByTokens( + view.rules(), amountBalance, amount, lptAMMBalance, tokens, tfee); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE + auto const ep = Number{amountDepositAdj} / tokensAdj; if (ep <= ePrice) return deposit( view, ammAccount, amountBalance, - amount, + amountDepositAdj, std::nullopt, lptAMMBalance, - tokens, + tokensAdj, std::nullopt, std::nullopt, std::nullopt, @@ -900,21 +968,37 @@ AMMDeposit::singleDepositEPrice( auto const a1 = c * c; auto const b1 = c * c * f2 * f2 + 2 * c - d * d; auto const c1 = 2 * c * f2 * f2 + 1 - 2 * d * f2; - auto const amountDeposit = toSTAmount( - amountBalance.issue(), - f1 * amountBalance * solveQuadraticEq(a1, b1, c1)); + auto amtNoRoundCb = [&] { + return f1 * amountBalance * solveQuadraticEq(a1, b1, c1); + }; + auto amtProdCb = [&] { return f1 * solveQuadraticEq(a1, b1, c1); }; + auto const amountDeposit = getRoundedAsset( + view.rules(), amtNoRoundCb, amountBalance, amtProdCb, IsDeposit::Yes); if (amountDeposit <= beast::zero) return {tecAMM_FAILED, STAmount{}}; - auto const tokens = - toSTAmount(lptAMMBalance.issue(), amountDeposit / ePrice); + auto tokNoRoundCb = [&] { return amountDeposit / ePrice; }; + auto tokProdCb = [&] { return amountDeposit / ePrice; }; + auto const tokens = getRoundedLPTokens( + view.rules(), tokNoRoundCb, lptAMMBalance, tokProdCb, IsDeposit::Yes); + // factor in the adjusted tokens + auto const [tokensAdj, amountDepositAdj] = adjustAssetInByTokens( + view.rules(), + amountBalance, + amountDeposit, + lptAMMBalance, + tokens, + tfee); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE + return deposit( view, ammAccount, amountBalance, - amountDeposit, + amountDepositAdj, std::nullopt, lptAMMBalance, - tokens, + tokensAdj, std::nullopt, std::nullopt, std::nullopt, diff --git a/src/xrpld/app/tx/detail/AMMWithdraw.cpp b/src/xrpld/app/tx/detail/AMMWithdraw.cpp index 4fccec937b..233ebb4578 100644 --- a/src/xrpld/app/tx/detail/AMMWithdraw.cpp +++ b/src/xrpld/app/tx/detail/AMMWithdraw.cpp @@ -523,7 +523,7 @@ AMMWithdraw::withdraw( lpTokensAMMBalance, lpTokensWithdraw, tfee, - false); + IsDeposit::No); return std::make_tuple( amountWithdraw, amount2Withdraw, lpTokensWithdraw); }(); @@ -682,6 +682,20 @@ AMMWithdraw::withdraw( amount2WithdrawActual); } +static STAmount +adjustLPTokensIn( + Rules const& rules, + STAmount const& lptAMMBalance, + STAmount const& lpTokensWithdraw, + WithdrawAll withdrawAll) +{ + if (!rules.enabled(fixAMMv1_3) || withdrawAll == WithdrawAll::Yes) + return lpTokensWithdraw; + return adjustLPTokens(lptAMMBalance, lpTokensWithdraw, IsDeposit::No); +} + +/** Proportional withdrawal of pool assets for the amount of LPTokens. + */ std::pair AMMWithdraw::equalWithdrawTokens( Sandbox& view, @@ -785,16 +799,22 @@ AMMWithdraw::equalWithdrawTokens( journal); } - auto const frac = divide(lpTokensWithdraw, lptAMMBalance, noIssue()); - auto const withdrawAmount = - multiply(amountBalance, frac, amountBalance.issue()); - auto const withdraw2Amount = - multiply(amount2Balance, frac, amount2Balance.issue()); + auto const tokensAdj = adjustLPTokensIn( + view.rules(), lptAMMBalance, lpTokensWithdraw, withdrawAll); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return { + tecAMM_INVALID_TOKENS, STAmount{}, STAmount{}, std::nullopt}; + // the adjusted tokens are factored in + auto const frac = divide(tokensAdj, lptAMMBalance, noIssue()); + auto const amountWithdraw = + getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::No); + auto const amount2Withdraw = + getRoundedAsset(view.rules(), amount2Balance, frac, IsDeposit::No); // LP is making equal withdrawal by tokens but the requested amount // of LP tokens is likely too small and results in one-sided pool // withdrawal due to round off. Fail so the user withdraws // more tokens. - if (withdrawAmount == beast::zero || withdraw2Amount == beast::zero) + if (amountWithdraw == beast::zero || amount2Withdraw == beast::zero) return {tecAMM_FAILED, STAmount{}, STAmount{}, STAmount{}}; return withdraw( @@ -803,10 +823,10 @@ AMMWithdraw::equalWithdrawTokens( ammAccount, account, amountBalance, - withdrawAmount, - withdraw2Amount, + amountWithdraw, + amount2Withdraw, lptAMMBalance, - lpTokensWithdraw, + tokensAdj, tfee, freezeHanding, withdrawAll, @@ -861,7 +881,16 @@ AMMWithdraw::equalWithdrawLimit( std::uint16_t tfee) { auto frac = Number{amount} / amountBalance; - auto const amount2Withdraw = amount2Balance * frac; + auto amount2Withdraw = + getRoundedAsset(view.rules(), amount2Balance, frac, IsDeposit::No); + auto tokensAdj = + getRoundedLPTokens(view.rules(), lptAMMBalance, frac, IsDeposit::No); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; + // factor in the adjusted tokens + frac = adjustFracByTokens(view.rules(), lptAMMBalance, tokensAdj, frac); + amount2Withdraw = + getRoundedAsset(view.rules(), amount2Balance, frac, IsDeposit::No); if (amount2Withdraw <= amount2) { return withdraw( @@ -870,26 +899,42 @@ AMMWithdraw::equalWithdrawLimit( ammAccount, amountBalance, amount, - toSTAmount(amount2.issue(), amount2Withdraw), + amount2Withdraw, lptAMMBalance, - toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac), + tokensAdj, tfee); } frac = Number{amount2} / amount2Balance; - auto const amountWithdraw = amountBalance * frac; - XRPL_ASSERT( - amountWithdraw <= amount, - "ripple::AMMWithdraw::equalWithdrawLimit : maximum amountWithdraw"); + auto amountWithdraw = + getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::No); + tokensAdj = + getRoundedLPTokens(view.rules(), lptAMMBalance, frac, IsDeposit::No); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE + // factor in the adjusted tokens + frac = adjustFracByTokens(view.rules(), lptAMMBalance, tokensAdj, frac); + amountWithdraw = + getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::No); + if (!view.rules().enabled(fixAMMv1_3)) + { + // LCOV_EXCL_START + XRPL_ASSERT( + amountWithdraw <= amount, + "ripple::AMMWithdraw::equalWithdrawLimit : maximum amountWithdraw"); + // LCOV_EXCL_STOP + } + else if (amountWithdraw > amount) + return {tecAMM_FAILED, STAmount{}}; // LCOV_EXCL_LINE return withdraw( view, ammSle, ammAccount, amountBalance, - toSTAmount(amount.issue(), amountWithdraw), + amountWithdraw, amount2, lptAMMBalance, - toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac), + tokensAdj, tfee); } @@ -908,19 +953,32 @@ AMMWithdraw::singleWithdraw( STAmount const& amount, std::uint16_t tfee) { - auto const tokens = lpTokensOut(amountBalance, amount, lptAMMBalance, tfee); + auto const tokens = adjustLPTokensIn( + view.rules(), + lptAMMBalance, + lpTokensIn(amountBalance, amount, lptAMMBalance, tfee), + isWithdrawAll(ctx_.tx)); if (tokens == beast::zero) - return {tecAMM_FAILED, STAmount{}}; - + { + if (!view.rules().enabled(fixAMMv1_3)) + return {tecAMM_FAILED, STAmount{}}; // LCOV_EXCL_LINE + else + return {tecAMM_INVALID_TOKENS, STAmount{}}; + } + // factor in the adjusted tokens + auto const [tokensAdj, amountWithdrawAdj] = adjustAssetOutByTokens( + view.rules(), amountBalance, amount, lptAMMBalance, tokens, tfee); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE return withdraw( view, ammSle, ammAccount, amountBalance, - amount, + amountWithdrawAdj, std::nullopt, lptAMMBalance, - tokens, + tokensAdj, tfee); } @@ -945,8 +1003,13 @@ AMMWithdraw::singleWithdrawTokens( STAmount const& lpTokensWithdraw, std::uint16_t tfee) { + auto const tokensAdj = adjustLPTokensIn( + view.rules(), lptAMMBalance, lpTokensWithdraw, isWithdrawAll(ctx_.tx)); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; + // the adjusted tokens are factored in auto const amountWithdraw = - withdrawByTokens(amountBalance, lptAMMBalance, lpTokensWithdraw, tfee); + ammAssetOut(amountBalance, lptAMMBalance, tokensAdj, tfee); if (amount == beast::zero || amountWithdraw >= amount) { return withdraw( @@ -957,7 +1020,7 @@ AMMWithdraw::singleWithdrawTokens( amountWithdraw, std::nullopt, lptAMMBalance, - lpTokensWithdraw, + tokensAdj, tfee); } @@ -1006,11 +1069,27 @@ AMMWithdraw::singleWithdrawEPrice( // t = T*(T + A*E*(f - 2))/(T*f - A*E) Number const ae = amountBalance * ePrice; auto const f = getFee(tfee); - auto const tokens = lptAMMBalance * (lptAMMBalance + ae * (f - 2)) / - (lptAMMBalance * f - ae); - if (tokens <= 0) - return {tecAMM_FAILED, STAmount{}}; - auto const amountWithdraw = toSTAmount(amount.issue(), tokens / ePrice); + auto tokNoRoundCb = [&] { + return lptAMMBalance * (lptAMMBalance + ae * (f - 2)) / + (lptAMMBalance * f - ae); + }; + auto tokProdCb = [&] { + return (lptAMMBalance + ae * (f - 2)) / (lptAMMBalance * f - ae); + }; + auto const tokensAdj = getRoundedLPTokens( + view.rules(), tokNoRoundCb, lptAMMBalance, tokProdCb, IsDeposit::No); + if (tokensAdj <= beast::zero) + { + if (!view.rules().enabled(fixAMMv1_3)) + return {tecAMM_FAILED, STAmount{}}; + else + return {tecAMM_INVALID_TOKENS, STAmount{}}; + } + auto amtNoRoundCb = [&] { return tokensAdj / ePrice; }; + auto amtProdCb = [&] { return tokensAdj / ePrice; }; + // the adjusted tokens are factored in + auto const amountWithdraw = getRoundedAsset( + view.rules(), amtNoRoundCb, amount, amtProdCb, IsDeposit::No); if (amount == beast::zero || amountWithdraw >= amount) { return withdraw( @@ -1021,7 +1100,7 @@ AMMWithdraw::singleWithdrawEPrice( amountWithdraw, std::nullopt, lptAMMBalance, - toSTAmount(lptAMMBalance.issue(), tokens), + tokensAdj, tfee); } diff --git a/src/xrpld/app/tx/detail/AMMWithdraw.h b/src/xrpld/app/tx/detail/AMMWithdraw.h index ae9328cb05..1de91fd787 100644 --- a/src/xrpld/app/tx/detail/AMMWithdraw.h +++ b/src/xrpld/app/tx/detail/AMMWithdraw.h @@ -301,7 +301,7 @@ class AMMWithdraw : public Transactor std::uint16_t tfee); /** Check from the flags if it's withdraw all */ - WithdrawAll + static WithdrawAll isWithdrawAll(STTx const& tx); }; diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index aca956dad6..9484eefd76 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -17,6 +17,9 @@ */ //============================================================================== +#include +#include +#include #include #include @@ -1674,4 +1677,309 @@ ValidPermissionedDomain::finalize( (sleStatus_[1] ? check(*sleStatus_[1], j) : true); } +void +ValidAMM::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (isDelete) + return; + + if (after) + { + auto const type = after->getType(); + // AMM object changed + if (type == ltAMM) + { + ammAccount_ = after->getAccountID(sfAccount); + lptAMMBalanceAfter_ = after->getFieldAmount(sfLPTokenBalance); + } + // AMM pool changed + else if ( + (type == ltRIPPLE_STATE && after->getFlags() & lsfAMMNode) || + (type == ltACCOUNT_ROOT && after->isFieldPresent(sfAMMID))) + { + ammPoolChanged_ = true; + } + } + + if (before) + { + // AMM object changed + if (before->getType() == ltAMM) + { + lptAMMBalanceBefore_ = before->getFieldAmount(sfLPTokenBalance); + } + } +} + +static bool +validBalances( + STAmount const& amount, + STAmount const& amount2, + STAmount const& lptAMMBalance, + ValidAMM::ZeroAllowed zeroAllowed) +{ + bool const positive = amount > beast::zero && amount2 > beast::zero && + lptAMMBalance > beast::zero; + if (zeroAllowed == ValidAMM::ZeroAllowed::Yes) + return positive || + (amount == beast::zero && amount2 == beast::zero && + lptAMMBalance == beast::zero); + return positive; +} + +bool +ValidAMM::finalizeVote(bool enforce, beast::Journal const& j) const +{ + if (lptAMMBalanceAfter_ != lptAMMBalanceBefore_ || ammPoolChanged_) + { + // LPTokens and the pool can not change on vote + // LCOV_EXCL_START + JLOG(j.error()) << "AMMVote invariant failed: " + << lptAMMBalanceBefore_.value_or(STAmount{}) << " " + << lptAMMBalanceAfter_.value_or(STAmount{}) << " " + << ammPoolChanged_; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + + return true; +} + +bool +ValidAMM::finalizeBid(bool enforce, beast::Journal const& j) const +{ + if (ammPoolChanged_) + { + // The pool can not change on bid + // LCOV_EXCL_START + JLOG(j.error()) << "AMMBid invariant failed: pool changed"; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + // LPTokens are burnt, therefore there should be fewer LPTokens + else if ( + lptAMMBalanceBefore_ && lptAMMBalanceAfter_ && + (*lptAMMBalanceAfter_ > *lptAMMBalanceBefore_ || + *lptAMMBalanceAfter_ <= beast::zero)) + { + // LCOV_EXCL_START + JLOG(j.error()) << "AMMBid invariant failed: " << *lptAMMBalanceBefore_ + << " " << *lptAMMBalanceAfter_; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + + return true; +} + +bool +ValidAMM::finalizeCreate( + STTx const& tx, + ReadView const& view, + bool enforce, + beast::Journal const& j) const +{ + if (!ammAccount_) + { + // LCOV_EXCL_START + JLOG(j.error()) + << "AMMCreate invariant failed: AMM object is not created"; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + else + { + auto const [amount, amount2] = ammPoolHolds( + view, + *ammAccount_, + tx[sfAmount].get(), + tx[sfAmount2].get(), + fhIGNORE_FREEZE, + j); + // Create invariant: + // sqrt(amount * amount2) == LPTokens + // all balances are greater than zero + if (!validBalances( + amount, amount2, *lptAMMBalanceAfter_, ZeroAllowed::No) || + ammLPTokens(amount, amount2, lptAMMBalanceAfter_->issue()) != + *lptAMMBalanceAfter_) + { + JLOG(j.error()) << "AMMCreate invariant failed: " << amount << " " + << amount2 << " " << *lptAMMBalanceAfter_; + if (enforce) + return false; + } + } + + return true; +} + +bool +ValidAMM::finalizeDelete(bool enforce, TER res, beast::Journal const& j) const +{ + if (ammAccount_) + { + // LCOV_EXCL_START + std::string const msg = (res == tesSUCCESS) + ? "AMM object is not deleted on tesSUCCESS" + : "AMM object is changed on tecINCOMPLETE"; + JLOG(j.error()) << "AMMDelete invariant failed: " << msg; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + + return true; +} + +bool +ValidAMM::finalizeDEX(bool enforce, beast::Journal const& j) const +{ + if (ammAccount_) + { + // LCOV_EXCL_START + JLOG(j.error()) << "AMM swap invariant failed: AMM object changed"; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + + return true; +} + +bool +ValidAMM::generalInvariant( + ripple::STTx const& tx, + ripple::ReadView const& view, + ZeroAllowed zeroAllowed, + beast::Journal const& j) const +{ + auto const [amount, amount2] = ammPoolHolds( + view, + *ammAccount_, + tx[sfAsset].get(), + tx[sfAsset2].get(), + fhIGNORE_FREEZE, + j); + // Deposit and Withdrawal invariant: + // sqrt(amount * amount2) >= LPTokens + // all balances are greater than zero + // unless on last withdrawal + auto const poolProductMean = root2(amount * amount2); + bool const nonNegativeBalances = + validBalances(amount, amount2, *lptAMMBalanceAfter_, zeroAllowed); + bool const strongInvariantCheck = poolProductMean >= *lptAMMBalanceAfter_; + // Allow for a small relative error if strongInvariantCheck fails + auto weakInvariantCheck = [&]() { + return *lptAMMBalanceAfter_ != beast::zero && + withinRelativeDistance( + poolProductMean, Number{*lptAMMBalanceAfter_}, Number{1, -11}); + }; + if (!nonNegativeBalances || + (!strongInvariantCheck && !weakInvariantCheck())) + { + JLOG(j.error()) << "AMM " << tx.getTxnType() << " invariant failed: " + << tx.getHash(HashPrefix::transactionID) << " " + << ammPoolChanged_ << " " << amount << " " << amount2 + << " " << poolProductMean << " " + << lptAMMBalanceAfter_->getText() << " " + << ((*lptAMMBalanceAfter_ == beast::zero) + ? Number{1} + : ((*lptAMMBalanceAfter_ - poolProductMean) / + poolProductMean)); + return false; + } + + return true; +} + +bool +ValidAMM::finalizeDeposit( + ripple::STTx const& tx, + ripple::ReadView const& view, + bool enforce, + beast::Journal const& j) const +{ + if (!ammAccount_) + { + // LCOV_EXCL_START + JLOG(j.error()) << "AMMDeposit invariant failed: AMM object is deleted"; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + else if (!generalInvariant(tx, view, ZeroAllowed::No, j) && enforce) + return false; + + return true; +} + +bool +ValidAMM::finalizeWithdraw( + ripple::STTx const& tx, + ripple::ReadView const& view, + bool enforce, + beast::Journal const& j) const +{ + if (!ammAccount_) + { + // Last Withdraw or Clawback deleted AMM + } + else if (!generalInvariant(tx, view, ZeroAllowed::Yes, j)) + { + if (enforce) + return false; + } + + return true; +} + +bool +ValidAMM::finalize( + STTx const& tx, + TER const result, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + // Delete may return tecINCOMPLETE if there are too many + // trustlines to delete. + if (result != tesSUCCESS && result != tecINCOMPLETE) + return true; + + bool const enforce = view.rules().enabled(fixAMMv1_3); + + switch (tx.getTxnType()) + { + case ttAMM_CREATE: + return finalizeCreate(tx, view, enforce, j); + case ttAMM_DEPOSIT: + return finalizeDeposit(tx, view, enforce, j); + case ttAMM_CLAWBACK: + case ttAMM_WITHDRAW: + return finalizeWithdraw(tx, view, enforce, j); + case ttAMM_BID: + return finalizeBid(enforce, j); + case ttAMM_VOTE: + return finalizeVote(enforce, j); + case ttAMM_DELETE: + return finalizeDelete(enforce, result, j); + case ttCHECK_CASH: + case ttOFFER_CREATE: + case ttPAYMENT: + return finalizeDEX(enforce, j); + default: + break; + } + + return true; +} + } // namespace ripple diff --git a/src/xrpld/app/tx/detail/InvariantCheck.h b/src/xrpld/app/tx/detail/InvariantCheck.h index 364ece0364..e854fd649b 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.h +++ b/src/xrpld/app/tx/detail/InvariantCheck.h @@ -617,6 +617,69 @@ class ValidPermissionedDomain beast::Journal const&); }; +class ValidAMM +{ + std::optional ammAccount_; + std::optional lptAMMBalanceAfter_; + std::optional lptAMMBalanceBefore_; + bool ammPoolChanged_; + +public: + enum class ZeroAllowed : bool { No = false, Yes = true }; + + ValidAMM() : ammPoolChanged_{false} + { + } + void + visitEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const&); + + bool + finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const&); + +private: + bool + finalizeBid(bool enforce, beast::Journal const&) const; + bool + finalizeVote(bool enforce, beast::Journal const&) const; + bool + finalizeCreate( + STTx const&, + ReadView const&, + bool enforce, + beast::Journal const&) const; + bool + finalizeDelete(bool enforce, TER res, beast::Journal const&) const; + bool + finalizeDeposit( + STTx const&, + ReadView const&, + bool enforce, + beast::Journal const&) const; + // Includes clawback + bool + finalizeWithdraw( + STTx const&, + ReadView const&, + bool enforce, + beast::Journal const&) const; + bool + finalizeDEX(bool enforce, beast::Journal const&) const; + bool + generalInvariant( + STTx const&, + ReadView const&, + ZeroAllowed zeroAllowed, + beast::Journal const&) const; +}; + // additional invariant checks can be declared above and then added to this // tuple using InvariantChecks = std::tuple< @@ -636,7 +699,8 @@ using InvariantChecks = std::tuple< NFTokenCountTracking, ValidClawback, ValidMPTIssuance, - ValidPermissionedDomain>; + ValidPermissionedDomain, + ValidAMM>; /** * @brief get a tuple of all invariant checks diff --git a/src/xrpld/app/tx/detail/Offer.h b/src/xrpld/app/tx/detail/Offer.h index 23129952c3..a26a35e60b 100644 --- a/src/xrpld/app/tx/detail/Offer.h +++ b/src/xrpld/app/tx/detail/Offer.h @@ -21,6 +21,8 @@ #define RIPPLE_APP_BOOK_OFFER_H_INCLUDED #include + +#include #include #include #include @@ -169,8 +171,24 @@ class TOffer : private TOfferBase * always returns true. */ bool - checkInvariant(TAmounts const&, beast::Journal j) const + checkInvariant(TAmounts const& consumed, beast::Journal j) const { + if (!isFeatureEnabled(fixAMMv1_3)) + return true; + + if (consumed.in > m_amounts.in || consumed.out > m_amounts.out) + { + // LCOV_EXCL_START + JLOG(j.error()) + << "AMMOffer::checkInvariant failed: consumed " + << to_string(consumed.in) << " " << to_string(consumed.out) + << " amounts " << to_string(m_amounts.in) << " " + << to_string(m_amounts.out); + + return false; + // LCOV_EXCL_STOP + } + return true; } }; From ec65e622aac2557c803c58c0869a415d108e7329 Mon Sep 17 00:00:00 2001 From: tequ Date: Thu, 19 Feb 2026 12:14:41 +0900 Subject: [PATCH 02/17] Merge fixAMMv1_3 amendment into featureAMM amendment --- include/xrpl/protocol/Feature.h | 2 +- include/xrpl/protocol/detail/features.macro | 1 - src/test/app/AMMClawback_test.cpp | 554 ++++++-------------- src/test/app/AMMExtended_test.cpp | 7 - src/test/app/AMM_test.cpp | 482 +++++------------ src/test/jtx/impl/AMM.cpp | 6 - src/test/rpc/AMMInfo_test.cpp | 29 +- src/xrpld/app/misc/AMMHelpers.h | 7 - src/xrpld/app/misc/detail/AMMHelpers.cpp | 121 +---- src/xrpld/app/tx/detail/AMMBid.cpp | 2 +- src/xrpld/app/tx/detail/AMMDeposit.cpp | 32 +- src/xrpld/app/tx/detail/AMMWithdraw.cpp | 32 +- src/xrpld/app/tx/detail/InvariantCheck.cpp | 2 +- src/xrpld/app/tx/detail/Offer.h | 3 - 14 files changed, 328 insertions(+), 952 deletions(-) diff --git a/include/xrpl/protocol/Feature.h b/include/xrpl/protocol/Feature.h index 3f7a39778c..ed07329307 100644 --- a/include/xrpl/protocol/Feature.h +++ b/include/xrpl/protocol/Feature.h @@ -80,7 +80,7 @@ namespace detail { // Feature.cpp. Because it's only used to reserve storage, and determine how // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than // the actual number of amendments. A LogicError on startup will verify this. -static constexpr std::size_t numFeatures = 114; +static constexpr std::size_t numFeatures = 113; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index c04e40094c..3f48ac7145 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -31,7 +31,6 @@ // If you add an amendment here, then do not forget to increment `numFeatures` // in include/xrpl/protocol/Feature.h. -XRPL_FIX (AMMv1_3, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(HookAPISerializedType240, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(PermissionedDomains, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(DynamicNFT, Supported::no, VoteBehavior::DefaultNo) diff --git a/src/test/app/AMMClawback_test.cpp b/src/test/app/AMMClawback_test.cpp index 9ccd59b580..4d2a995934 100644 --- a/src/test/app/AMMClawback_test.cpp +++ b/src/test/app/AMMClawback_test.cpp @@ -581,12 +581,8 @@ class AMMClawback_test : public jtx::AMMTest AMM amm(env, alice, EUR(5000), USD(4000), ter(tesSUCCESS)); env.close(); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm.expectBalances( - USD(4000), EUR(5000), IOUAmount{4472135954999580, -12})); - else - BEAST_EXPECT(amm.expectBalances( - USD(4000), EUR(5000), IOUAmount{4472135954999579, -12})); + BEAST_EXPECT(amm.expectBalances( + USD(4000), EUR(5000), IOUAmount{4472135954999579, -12})); // gw clawback 1000 USD from the AMM pool env(amm::ammClawback(gw, alice, USD, EUR, USD(1000)), @@ -605,20 +601,12 @@ class AMMClawback_test : public jtx::AMMTest // 1000 USD and 1250 EUR was withdrawn from the AMM pool, so the // current balance is 3000 USD and 3750 EUR. - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm.expectBalances( - USD(3000), EUR(3750), IOUAmount{3354101966249685, -12})); - else - BEAST_EXPECT(amm.expectBalances( - USD(3000), EUR(3750), IOUAmount{3354101966249684, -12})); + BEAST_EXPECT(amm.expectBalances( + USD(3000), EUR(3750), IOUAmount{3354101966249684, -12})); // Alice has 3/4 of its initial lptokens Left. - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm.expectLPTokens( - alice, IOUAmount{3354101966249685, -12})); - else - BEAST_EXPECT(amm.expectLPTokens( - alice, IOUAmount{3354101966249684, -12})); + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{3354101966249684, -12})); // gw clawback another 500 USD from the AMM pool. env(amm::ammClawback(gw, alice, USD, EUR, USD(500)), @@ -629,21 +617,10 @@ class AMMClawback_test : public jtx::AMMTest // AMM pool. env.require(balance(alice, gw["USD"](2000))); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(2500000000000001), -12}, - STAmount{EUR, UINT64_C(3125000000000001), -12}, - IOUAmount{2795084971874738, -12})); - else - BEAST_EXPECT(amm.expectBalances( - USD(2500), EUR(3125), IOUAmount{2795084971874737, -12})); - - if (!features[fixAMMv1_3]) - BEAST_EXPECT( - env.balance(alice, EUR) == - STAmount(EUR, UINT64_C(2874999999999999), -12)); - else - BEAST_EXPECT(env.balance(alice, EUR) == EUR(2875)); + BEAST_EXPECT(amm.expectBalances( + USD(2500), EUR(3125), IOUAmount{2795084971874737, -12})); + + BEAST_EXPECT(env.balance(alice, EUR) == EUR(2875)); // gw clawback small amount, 1 USD. env(amm::ammClawback(gw, alice, USD, EUR, USD(1)), ter(tesSUCCESS)); @@ -652,21 +629,10 @@ class AMMClawback_test : public jtx::AMMTest // Another 1 USD / 1.25 EUR was withdrawn. env.require(balance(alice, gw["USD"](2000))); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(2499000000000002), -12}, - STAmount{EUR, UINT64_C(3123750000000002), -12}, - IOUAmount{2793966937885989, -12})); - else - BEAST_EXPECT(amm.expectBalances( - USD(2499), EUR(3123.75), IOUAmount{2793966937885987, -12})); - - if (!features[fixAMMv1_3]) - BEAST_EXPECT( - env.balance(alice, EUR) == - STAmount(EUR, UINT64_C(2'876'249999999998), -12)); - else - BEAST_EXPECT(env.balance(alice, EUR) == EUR(2876.25)); + BEAST_EXPECT(amm.expectBalances( + USD(2499), EUR(3123.75), IOUAmount{2793966937885987, -12})); + + BEAST_EXPECT(env.balance(alice, EUR) == EUR(2876.25)); // gw clawback 4000 USD, exceeding the current balance. We // will clawback all. @@ -739,26 +705,14 @@ class AMMClawback_test : public jtx::AMMTest // gw2 creates AMM pool of XRP/EUR, alice and bob deposit XRP/EUR. AMM amm2(env, gw2, XRP(3000), EUR(1000), ter(tesSUCCESS)); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm2.expectBalances( - EUR(1000), XRP(3000), IOUAmount{1732050807568878, -9})); - else - BEAST_EXPECT(amm2.expectBalances( - EUR(1000), XRP(3000), IOUAmount{1732050807568877, -9})); + BEAST_EXPECT(amm2.expectBalances( + EUR(1000), XRP(3000), IOUAmount{1732050807568877, -9})); amm2.deposit(alice, EUR(1000), XRP(3000)); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm2.expectBalances( - EUR(2000), XRP(6000), IOUAmount{3464101615137756, -9})); - else - BEAST_EXPECT(amm2.expectBalances( - EUR(2000), XRP(6000), IOUAmount{3464101615137754, -9})); + BEAST_EXPECT(amm2.expectBalances( + EUR(2000), XRP(6000), IOUAmount{3464101615137754, -9})); amm2.deposit(bob, EUR(1000), XRP(3000)); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm2.expectBalances( - EUR(3000), XRP(9000), IOUAmount{5196152422706634, -9})); - else - BEAST_EXPECT(amm2.expectBalances( - EUR(3000), XRP(9000), IOUAmount{5196152422706631, -9})); + BEAST_EXPECT(amm2.expectBalances( + EUR(3000), XRP(9000), IOUAmount{5196152422706631, -9})); env.close(); auto aliceXrpBalance = env.balance(alice, XRP); @@ -781,18 +735,10 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT( expectLedgerEntryRoot(env, alice, aliceXrpBalance + XRP(1000))); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm.expectBalances( - USD(2500), XRP(5000), IOUAmount{3535533905932738, -9})); - else - BEAST_EXPECT(amm.expectBalances( - USD(2500), XRP(5000), IOUAmount{3535533905932737, -9})); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm.expectLPTokens( - alice, IOUAmount{7071067811865480, -10})); - else - BEAST_EXPECT(amm.expectLPTokens( - alice, IOUAmount{7071067811865474, -10})); + BEAST_EXPECT(amm.expectBalances( + USD(2500), XRP(5000), IOUAmount{3535533905932737, -9})); + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{7071067811865474, -10})); BEAST_EXPECT( amm.expectLPTokens(bob, IOUAmount{1414213562373095, -9})); @@ -806,26 +752,12 @@ class AMMClawback_test : public jtx::AMMTest // Bob gets 20 XRP back. BEAST_EXPECT( expectLedgerEntryRoot(env, bob, bobXrpBalance + XRP(20))); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(2490000000000001), -12}, - XRP(4980), - IOUAmount{3521391770309008, -9})); - else - BEAST_EXPECT(amm.expectBalances( - USD(2'490), XRP(4980), IOUAmount{3521391770309006, -9})); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm.expectLPTokens( - alice, IOUAmount{7071067811865480, -10})); - else - BEAST_EXPECT(amm.expectLPTokens( - alice, IOUAmount{7071067811865474, -10})); - if (!features[fixAMMv1_3]) - BEAST_EXPECT( - amm.expectLPTokens(bob, IOUAmount{1400071426749365, -9})); - else - BEAST_EXPECT( - amm.expectLPTokens(bob, IOUAmount{1400071426749364, -9})); + BEAST_EXPECT(amm.expectBalances( + USD(2'490), XRP(4980), IOUAmount{3521391770309006, -9})); + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{7071067811865474, -10})); + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{1400071426749364, -9})); // gw2 clawback 200 EUR from amm2. env(amm::ammClawback(gw2, alice, EUR, XRP, EUR(200)), @@ -838,24 +770,12 @@ class AMMClawback_test : public jtx::AMMTest // Alice gets 600 XRP back. BEAST_EXPECT(expectLedgerEntryRoot( env, alice, aliceXrpBalance + XRP(1000) + XRP(600))); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm2.expectBalances( - EUR(2800), XRP(8400), IOUAmount{4849742261192859, -9})); - else - BEAST_EXPECT(amm2.expectBalances( - EUR(2800), XRP(8400), IOUAmount{4849742261192856, -9})); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm2.expectLPTokens( - alice, IOUAmount{1385640646055103, -9})); - else - BEAST_EXPECT(amm2.expectLPTokens( - alice, IOUAmount{1385640646055102, -9})); - if (!features[fixAMMv1_3]) - BEAST_EXPECT( - amm2.expectLPTokens(bob, IOUAmount{1732050807568878, -9})); - else - BEAST_EXPECT( - amm2.expectLPTokens(bob, IOUAmount{1732050807568877, -9})); + BEAST_EXPECT(amm2.expectBalances( + EUR(2800), XRP(8400), IOUAmount{4849742261192856, -9})); + BEAST_EXPECT( + amm2.expectLPTokens(alice, IOUAmount{1385640646055102, -9})); + BEAST_EXPECT( + amm2.expectLPTokens(bob, IOUAmount{1732050807568877, -9})); // gw claw back 1000 USD from alice in amm, which exceeds alice's // balance. This will clawback all the remaining LP tokens of alice @@ -868,34 +788,18 @@ class AMMClawback_test : public jtx::AMMTest env.require(balance(bob, gw["USD"](4000))); // Alice gets 1000 XRP back. - if (!features[fixAMMv1_3]) - BEAST_EXPECT(expectLedgerEntryRoot( - env, - alice, - aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000))); - else - BEAST_EXPECT(expectLedgerEntryRoot( - env, - alice, - aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) - - XRPAmount{1})); + BEAST_EXPECT(expectLedgerEntryRoot( + env, + alice, + aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) - + XRPAmount{1})); BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); - if (!features[fixAMMv1_3]) - BEAST_EXPECT( - amm.expectLPTokens(bob, IOUAmount{1400071426749365, -9})); - else - BEAST_EXPECT( - amm.expectLPTokens(bob, IOUAmount{1400071426749364, -9})); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(1990000000000001), -12}, - XRP(3980), - IOUAmount{2814284989122460, -9})); - else - BEAST_EXPECT(amm.expectBalances( - USD(1'990), - XRPAmount{3'980'000'001}, - IOUAmount{2814284989122459, -9})); + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{1400071426749364, -9})); + BEAST_EXPECT(amm.expectBalances( + USD(1'990), + XRPAmount{3'980'000'001}, + IOUAmount{2814284989122459, -9})); // gw clawback 1000 USD from bob in amm, which also exceeds bob's // balance in amm. All bob's lptoken in amm will be consumed, which @@ -907,17 +811,11 @@ class AMMClawback_test : public jtx::AMMTest env.require(balance(alice, gw["USD"](5000))); env.require(balance(bob, gw["USD"](4000))); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(expectLedgerEntryRoot( - env, - alice, - aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000))); - else - BEAST_EXPECT(expectLedgerEntryRoot( - env, - alice, - aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) - - XRPAmount{1})); + BEAST_EXPECT(expectLedgerEntryRoot( + env, + alice, + aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) - + XRPAmount{1})); BEAST_EXPECT(expectLedgerEntryRoot( env, bob, bobXrpBalance + XRP(20) + XRP(1980))); @@ -937,30 +835,19 @@ class AMMClawback_test : public jtx::AMMTest // Alice gets another 2400 XRP back, bob's XRP balance remains the // same. - if (!features[fixAMMv1_3]) - BEAST_EXPECT(expectLedgerEntryRoot( - env, - alice, - aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) + - XRP(2400))); - else - BEAST_EXPECT(expectLedgerEntryRoot( - env, - alice, - aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) + - XRP(2400) - XRPAmount{1})); + BEAST_EXPECT(expectLedgerEntryRoot( + env, + alice, + aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) + XRP(2400) - + XRPAmount{1})); BEAST_EXPECT(expectLedgerEntryRoot( env, bob, bobXrpBalance + XRP(20) + XRP(1980))); // Alice now does not have any lptoken in amm2 BEAST_EXPECT(amm2.expectLPTokens(alice, IOUAmount(0))); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm2.expectBalances( - EUR(2000), XRP(6000), IOUAmount{3464101615137756, -9})); - else - BEAST_EXPECT(amm2.expectBalances( - EUR(2000), XRP(6000), IOUAmount{3464101615137754, -9})); + BEAST_EXPECT(amm2.expectBalances( + EUR(2000), XRP(6000), IOUAmount{3464101615137754, -9})); // gw2 claw back 2000 EUR from bob in amm2, which exceeds bob's // balance. All bob's lptokens will be consumed, which corresponds @@ -974,18 +861,11 @@ class AMMClawback_test : public jtx::AMMTest // Bob gets another 3000 XRP back. Alice's XRP balance remains the // same. - if (!features[fixAMMv1_3]) - BEAST_EXPECT(expectLedgerEntryRoot( - env, - alice, - aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) + - XRP(2400))); - else - BEAST_EXPECT(expectLedgerEntryRoot( - env, - alice, - aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) + - XRP(2400) - XRPAmount{1})); + BEAST_EXPECT(expectLedgerEntryRoot( + env, + alice, + aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) + XRP(2400) - + XRPAmount{1})); BEAST_EXPECT(expectLedgerEntryRoot( env, bob, bobXrpBalance + XRP(20) + XRP(1980) + XRP(3000))); @@ -993,12 +873,8 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(amm2.expectLPTokens(alice, IOUAmount(0))); BEAST_EXPECT(amm2.expectLPTokens(bob, IOUAmount(0))); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm2.expectBalances( - EUR(1000), XRP(3000), IOUAmount{1732050807568878, -9})); - else - BEAST_EXPECT(amm2.expectBalances( - EUR(1000), XRP(3000), IOUAmount{1732050807568877, -9})); + BEAST_EXPECT(amm2.expectBalances( + EUR(1000), XRP(3000), IOUAmount{1732050807568877, -9})); } } @@ -1056,45 +932,21 @@ class AMMClawback_test : public jtx::AMMTest AMM amm(env, alice, EUR(5000), USD(4000), ter(tesSUCCESS)); env.close(); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm.expectBalances( - USD(4000), EUR(5000), IOUAmount{4472135954999580, -12})); - else - BEAST_EXPECT(amm.expectBalances( - USD(4000), EUR(5000), IOUAmount{4472135954999579, -12})); + BEAST_EXPECT(amm.expectBalances( + USD(4000), EUR(5000), IOUAmount{4472135954999579, -12})); amm.deposit(bob, USD(2000), EUR(2500)); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm.expectBalances( - USD(6000), EUR(7500), IOUAmount{6708203932499370, -12})); - else - BEAST_EXPECT(amm.expectBalances( - USD(6000), EUR(7500), IOUAmount{6708203932499368, -12})); + BEAST_EXPECT(amm.expectBalances( + USD(6000), EUR(7500), IOUAmount{6708203932499368, -12})); amm.deposit(carol, USD(1000), EUR(1250)); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm.expectBalances( - USD(7000), EUR(8750), IOUAmount{7826237921249265, -12})); - else - BEAST_EXPECT(amm.expectBalances( - USD(7000), EUR(8750), IOUAmount{7826237921249262, -12})); - - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm.expectLPTokens( - alice, IOUAmount{4472135954999580, -12})); - else - BEAST_EXPECT(amm.expectLPTokens( - alice, IOUAmount{4472135954999579, -12})); - if (!features[fixAMMv1_3]) - BEAST_EXPECT( - amm.expectLPTokens(bob, IOUAmount{2236067977499790, -12})); - else - BEAST_EXPECT( - amm.expectLPTokens(bob, IOUAmount{2236067977499789, -12})); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm.expectLPTokens( - carol, IOUAmount{1118033988749895, -12})); - else - BEAST_EXPECT(amm.expectLPTokens( - carol, IOUAmount{1118033988749894, -12})); + BEAST_EXPECT(amm.expectBalances( + USD(7000), EUR(8750), IOUAmount{7826237921249262, -12})); + + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{4472135954999579, -12})); + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{2236067977499789, -12})); + BEAST_EXPECT( + amm.expectLPTokens(carol, IOUAmount{1118033988749894, -12})); env.require(balance(alice, gw["USD"](2000))); env.require(balance(alice, gw2["EUR"](1000))); @@ -1108,30 +960,16 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(4999999999999999), -12}, - STAmount{EUR, UINT64_C(6249999999999999), -12}, - IOUAmount{5590169943749475, -12})); - else - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(5000000000000001), -12}, - STAmount{EUR, UINT64_C(6250000000000001), -12}, - IOUAmount{5590169943749473, -12})); - - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm.expectLPTokens( - alice, IOUAmount{4472135954999580, -12})); - else - BEAST_EXPECT(amm.expectLPTokens( - alice, IOUAmount{4472135954999579, -12})); + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(5000000000000001), -12}, + STAmount{EUR, UINT64_C(6250000000000001), -12}, + IOUAmount{5590169943749473, -12})); + + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{4472135954999579, -12})); BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount(0))); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm.expectLPTokens( - carol, IOUAmount{1118033988749895, -12})); - else - BEAST_EXPECT(amm.expectLPTokens( - carol, IOUAmount{1118033988749894, -12})); + BEAST_EXPECT( + amm.expectLPTokens(carol, IOUAmount{1118033988749894, -12})); // Bob will get 2500 EUR back. env.require(balance(alice, gw["USD"](2000))); @@ -1140,14 +978,9 @@ class AMMClawback_test : public jtx::AMMTest env.balance(bob, USD) == STAmount(USD, UINT64_C(3000000000000000), -12)); - if (!features[fixAMMv1_3]) - BEAST_EXPECT( - env.balance(bob, EUR) == - STAmount(EUR, UINT64_C(5000000000000001), -12)); - else - BEAST_EXPECT( - env.balance(bob, EUR) == - STAmount(EUR, UINT64_C(4999999999999999), -12)); + BEAST_EXPECT( + env.balance(bob, EUR) == + STAmount(EUR, UINT64_C(4999999999999999), -12)); env.require(balance(carol, gw["USD"](3000))); env.require(balance(carol, gw2["EUR"](2750))); @@ -1155,23 +988,13 @@ class AMMClawback_test : public jtx::AMMTest env(amm::ammClawback(gw2, carol, EUR, USD, std::nullopt), ter(tesSUCCESS)); env.close(); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(3999999999999999), -12}, - STAmount{EUR, UINT64_C(4999999999999999), -12}, - IOUAmount{4472135954999580, -12})); - else - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(4000000000000001), -12}, - STAmount{EUR, UINT64_C(5000000000000002), -12}, - IOUAmount{4472135954999579, -12})); - - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm.expectLPTokens( - alice, IOUAmount{4472135954999580, -12})); - else - BEAST_EXPECT(amm.expectLPTokens( - alice, IOUAmount{4472135954999579, -12})); + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(4000000000000001), -12}, + STAmount{EUR, UINT64_C(5000000000000002), -12}, + IOUAmount{4472135954999579, -12})); + + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{4472135954999579, -12})); BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount(0))); BEAST_EXPECT(amm.expectLPTokens(carol, IOUAmount(0))); @@ -1210,26 +1033,14 @@ class AMMClawback_test : public jtx::AMMTest // gw creates AMM pool of XRP/USD, alice and bob deposit XRP/USD. AMM amm(env, gw, XRP(2000), USD(10000), ter(tesSUCCESS)); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm.expectBalances( - USD(10000), XRP(2000), IOUAmount{4472135954999580, -9})); - else - BEAST_EXPECT(amm.expectBalances( - USD(10000), XRP(2000), IOUAmount{4472135954999579, -9})); + BEAST_EXPECT(amm.expectBalances( + USD(10000), XRP(2000), IOUAmount{4472135954999579, -9})); amm.deposit(alice, USD(1000), XRP(200)); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm.expectBalances( - USD(11000), XRP(2200), IOUAmount{4919349550499538, -9})); - else - BEAST_EXPECT(amm.expectBalances( - USD(11000), XRP(2200), IOUAmount{4919349550499536, -9})); + BEAST_EXPECT(amm.expectBalances( + USD(11000), XRP(2200), IOUAmount{4919349550499536, -9})); amm.deposit(bob, USD(2000), XRP(400)); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm.expectBalances( - USD(13000), XRP(2600), IOUAmount{5813776741499453, -9})); - else - BEAST_EXPECT(amm.expectBalances( - USD(13000), XRP(2600), IOUAmount{5813776741499451, -9})); + BEAST_EXPECT(amm.expectBalances( + USD(13000), XRP(2600), IOUAmount{5813776741499451, -9})); env.close(); auto aliceXrpBalance = env.balance(alice, XRP); @@ -1239,34 +1050,22 @@ class AMMClawback_test : public jtx::AMMTest env(amm::ammClawback(gw, alice, USD, XRP, std::nullopt), ter(tesSUCCESS)); env.close(); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm.expectBalances( - USD(12000), XRP(2400), IOUAmount{5366563145999495, -9})); - else - BEAST_EXPECT(amm.expectBalances( - USD(12000), - XRPAmount(2400000001), - IOUAmount{5366563145999494, -9})); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(expectLedgerEntryRoot( - env, alice, aliceXrpBalance + XRP(200))); - else - BEAST_EXPECT(expectLedgerEntryRoot( - env, alice, aliceXrpBalance + XRP(200) - XRPAmount{1})); + BEAST_EXPECT(amm.expectBalances( + USD(12000), + XRPAmount(2400000001), + IOUAmount{5366563145999494, -9})); + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(200) - XRPAmount{1})); BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); // gw clawback all bob's USD in amm. (2000 USD / 400 XRP) env(amm::ammClawback(gw, bob, USD, XRP, std::nullopt), ter(tesSUCCESS)); env.close(); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm.expectBalances( - USD(10000), XRP(2000), IOUAmount{4472135954999580, -9})); - else - BEAST_EXPECT(amm.expectBalances( - USD(10000), - XRPAmount(2000000001), - IOUAmount{4472135954999579, -9})); + BEAST_EXPECT(amm.expectBalances( + USD(10000), + XRPAmount(2000000001), + IOUAmount{4472135954999579, -9})); BEAST_EXPECT( expectLedgerEntryRoot(env, bob, bobXrpBalance + XRP(400))); BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); @@ -1322,10 +1121,7 @@ class AMMClawback_test : public jtx::AMMTest amm.deposit(bob, USD(4000), EUR(1000)); BEAST_EXPECT( amm.expectBalances(USD(12000), EUR(3000), IOUAmount(6000))); - if (!features[fixAMMv1_3]) - amm.deposit(carol, USD(2000), EUR(500)); - else - amm.deposit(carol, USD(2000.25), EUR(500)); + amm.deposit(carol, USD(2000.25), EUR(500)); BEAST_EXPECT( amm.expectBalances(USD(14000), EUR(3500), IOUAmount(7000))); // gw clawback 1000 USD from carol. @@ -1341,12 +1137,9 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(alice, EUR) == EUR(8000)); BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); - else - BEAST_EXPECT( - env.balance(carol, USD) == - STAmount(USD, UINT64_C(5999'999999999999), -12)); + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); // 250 EUR goes back to carol. BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); @@ -1368,12 +1161,9 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); // 250 EUR did not go back to bob because tfClawTwoAssets is set. BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); - else - BEAST_EXPECT( - env.balance(carol, USD) == - STAmount(USD, UINT64_C(5999'999999999999), -12)); + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); // gw clawback all USD from alice and set tfClawTwoAssets. @@ -1390,12 +1180,9 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(alice, EUR) == EUR(8000)); BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); - else - BEAST_EXPECT( - env.balance(carol, USD) == - STAmount(USD, UINT64_C(5999'999999999999), -12)); + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); } @@ -1580,21 +1367,10 @@ class AMMClawback_test : public jtx::AMMTest // gw2 claws back 1000 EUR from gw. env(amm::ammClawback(gw2, gw, EUR, USD, EUR(1000)), ter(tesSUCCESS)); env.close(); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm.expectBalances( - USD(4500), - STAmount(EUR, UINT64_C(9000000000000001), -12), - IOUAmount{6363961030678928, -12})); - else - BEAST_EXPECT(amm.expectBalances( - USD(4500), EUR(9000), IOUAmount{6363961030678928, -12})); + BEAST_EXPECT(amm.expectBalances( + USD(4500), EUR(9000), IOUAmount{6363961030678928, -12})); - if (!features[fixAMMv1_3]) - BEAST_EXPECT( - amm.expectLPTokens(gw, IOUAmount{7071067811865480, -13})); - else - BEAST_EXPECT( - amm.expectLPTokens(gw, IOUAmount{7071067811865475, -13})); + BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{7071067811865475, -13})); BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{1414213562373095, -12})); BEAST_EXPECT( amm.expectLPTokens(alice, IOUAmount{4242640687119285, -12})); @@ -1607,21 +1383,10 @@ class AMMClawback_test : public jtx::AMMTest // gw2 claws back 4000 EUR from alice. env(amm::ammClawback(gw2, alice, EUR, USD, EUR(4000)), ter(tesSUCCESS)); env.close(); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm.expectBalances( - USD(2500), - STAmount(EUR, UINT64_C(5000000000000001), -12), - IOUAmount{3535533905932738, -12})); - else - BEAST_EXPECT(amm.expectBalances( - USD(2500), EUR(5000), IOUAmount{3535533905932738, -12})); + BEAST_EXPECT(amm.expectBalances( + USD(2500), EUR(5000), IOUAmount{3535533905932738, -12})); - if (!features[fixAMMv1_3]) - BEAST_EXPECT( - amm.expectLPTokens(gw, IOUAmount{7071067811865480, -13})); - else - BEAST_EXPECT( - amm.expectLPTokens(gw, IOUAmount{7071067811865475, -13})); + BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{7071067811865475, -13})); BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{1414213562373095, -12})); BEAST_EXPECT( amm.expectLPTokens(alice, IOUAmount{1414213562373095, -12})); @@ -1885,10 +1650,7 @@ class AMMClawback_test : public jtx::AMMTest amm.deposit(bob, USD(4000), EUR(1000)); BEAST_EXPECT( amm.expectBalances(USD(12000), EUR(3000), IOUAmount(6000))); - if (!features[fixAMMv1_3]) - amm.deposit(carol, USD(2000), EUR(500)); - else - amm.deposit(carol, USD(2000.25), EUR(500)); + amm.deposit(carol, USD(2000.25), EUR(500)); BEAST_EXPECT( amm.expectBalances(USD(14000), EUR(3500), IOUAmount(7000))); @@ -1910,12 +1672,9 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(alice, EUR) == EUR(8000)); BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); - else - BEAST_EXPECT( - env.balance(carol, USD) == - STAmount(USD, UINT64_C(5999'999999999999), -12)); + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); // 250 EUR goes back to carol. BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); @@ -1937,12 +1696,9 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); // 250 EUR did not go back to bob because tfClawTwoAssets is set. BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); - else - BEAST_EXPECT( - env.balance(carol, USD) == - STAmount(USD, UINT64_C(5999'999999999999), -12)); + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); // gw clawback all USD from alice and set tfClawTwoAssets. @@ -1960,12 +1716,9 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(alice, EUR) == EUR(8000)); BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); - else - BEAST_EXPECT( - env.balance(carol, USD) == - STAmount(USD, UINT64_C(5999'999999999999), -12)); + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); } } @@ -2013,23 +1766,13 @@ class AMMClawback_test : public jtx::AMMTest env(amm::ammClawback(gw, alice, USD, XRP, USD(400)), ter(tesSUCCESS)); env.close(); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(amm.expectBalances( - STAmount(USD, UINT64_C(5656854249492380), -13), - XRP(70.710678), - IOUAmount(200000))); - else - BEAST_EXPECT(amm.expectBalances( - STAmount(USD, UINT64_C(565'685424949238), -12), - XRP(70.710679), - IOUAmount(200000))); + BEAST_EXPECT(amm.expectBalances( + STAmount(USD, UINT64_C(565'685424949238), -12), + XRP(70.710679), + IOUAmount(200000))); BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(expectLedgerEntryRoot( - env, alice, aliceXrpBalance + XRP(29.289322))); - else - BEAST_EXPECT(expectLedgerEntryRoot( - env, alice, aliceXrpBalance + XRP(29.289321))); + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(29.289321))); } void @@ -2040,18 +1783,13 @@ class AMMClawback_test : public jtx::AMMTest testFeatureDisabled(all - featureAMMClawback); testAMMClawbackSpecificAmount(all); testAMMClawbackExceedBalance(all); - testAMMClawbackExceedBalance(all - fixAMMv1_3); testAMMClawbackAll(all); - testAMMClawbackAll(all - fixAMMv1_3); testAMMClawbackSameIssuerAssets(all); - testAMMClawbackSameIssuerAssets(all - fixAMMv1_3); testAMMClawbackSameCurrency(all); testAMMClawbackIssuesEachOther(all); testNotHoldingLptoken(all); testAssetFrozen(all); - testAssetFrozen(all - fixAMMv1_3); testSingleDepositAndClawback(all); - testSingleDepositAndClawback(all - fixAMMv1_3); } }; BEAST_DEFINE_TESTSUITE(AMMClawback, app, ripple); diff --git a/src/test/app/AMMExtended_test.cpp b/src/test/app/AMMExtended_test.cpp index d6142d7d3e..16e66d803c 100644 --- a/src/test/app/AMMExtended_test.cpp +++ b/src/test/app/AMMExtended_test.cpp @@ -1405,7 +1405,6 @@ struct AMMExtended_test : public jtx::AMMTest using namespace jtx; FeatureBitset const all{supported_amendments()}; testRmFundedOffer(all); - testRmFundedOffer(all - fixAMMv1_3); testEnforceNoRipple(all); testFillModes(all); testOfferCrossWithXRP(all); @@ -1419,7 +1418,6 @@ struct AMMExtended_test : public jtx::AMMTest testOfferCreateThenCross(all); testSellFlagExceedLimit(all); testGatewayCrossCurrency(all); - testGatewayCrossCurrency(all - fixAMMv1_3); testBridgedCross(all); testSellWithFillOrKill(all); testTransferRateOffer(all); @@ -1427,7 +1425,6 @@ struct AMMExtended_test : public jtx::AMMTest testBadPathAssert(all); testSellFlagBasic(all); testDirectToDirectPath(all); - testDirectToDirectPath(all - fixAMMv1_3); testRequireAuth(all); testMissingAuth(all); } @@ -3838,9 +3835,7 @@ struct AMMExtended_test : public jtx::AMMTest testBookStep(all); testBookStep(all | ownerPaysFee); testTransferRate(all | ownerPaysFee); - testTransferRate((all - fixAMMv1_3) | ownerPaysFee); testTransferRateNoOwnerFee(all); - testTransferRateNoOwnerFee(all - fixAMMv1_3); testLimitQuality(); testXRPPathLoop(); } @@ -3851,7 +3846,6 @@ struct AMMExtended_test : public jtx::AMMTest using namespace jtx; FeatureBitset const all{supported_amendments()}; testStepLimit(all); - testStepLimit(all - fixAMMv1_3); } void @@ -3860,7 +3854,6 @@ struct AMMExtended_test : public jtx::AMMTest using namespace jtx; FeatureBitset const all{supported_amendments()}; test_convert_all_of_an_asset(all); - test_convert_all_of_an_asset(all - fixAMMv1_3); } void diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index 8d5595940a..f5f21fb1f6 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -833,10 +833,7 @@ struct AMM_test : public jtx::AMMTest // Tiny deposit testAMM( [&](AMM& ammAlice, Env& env) { - auto const enabledv1_3 = - env.current()->rules().enabled(fixAMMv1_3); - auto const err = - !enabledv1_3 ? ter(temBAD_AMOUNT) : ter(tesSUCCESS); + auto const err = ter(tesSUCCESS); // Pre-amendment XRP deposit side is rounded to 0 // and deposit fails. // Post-amendment XRP deposit side is rounded to 1 @@ -856,7 +853,7 @@ struct AMM_test : public jtx::AMMTest std::nullopt, 0, std::nullopt, - {features, features - fixAMMv1_3}); + {features}); // Invalid AMM testAMM([&](AMM& ammAlice, Env& env) { @@ -1322,15 +1319,6 @@ struct AMM_test : public jtx::AMMTest }); // Equal deposit limit, tokens rounded to 0 - testAMM( - [&](AMM& amm, Env& env) { - amm.deposit(DepositArg{ - .asset1In = STAmount{USD, 1, -15}, - .asset2In = XRPAmount{1}, - .err = ter(tecAMM_INVALID_TOKENS)}); - }, - {.pool = {{USD(1'000'000), XRP(1'000'000)}}, - .features = {features - fixAMMv1_3}}); testAMM([&](AMM& amm, Env& env) { amm.deposit(DepositArg{ .asset1In = STAmount{USD, 1, -15}, @@ -1573,7 +1561,7 @@ struct AMM_test : public jtx::AMMTest }); // Issuer create/deposit - for (auto const& feat : {all, all - fixAMMv1_3}) + for (auto const& feat : {all}) { Env env(*this, feat); env.fund(XRP(30000), gw); @@ -2003,22 +1991,20 @@ struct AMM_test : public jtx::AMMTest // while leaving a tiny amount in USD pool. // Post-amendment: // Most of the pool is withdrawn with remaining tiny amounts - auto err = env.enabled(fixAMMv1_3) ? ter(tesSUCCESS) - : ter(tecAMM_BALANCE); + auto err = ter(tesSUCCESS); ammAlice.withdraw( alice, IOUAmount{9'999'999'9999, -4}, std::nullopt, std::nullopt, err); - if (env.enabled(fixAMMv1_3)) - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount(1), STAmount{USD, 1, -7}, IOUAmount{1, -4})); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(1), STAmount{USD, 1, -7}, IOUAmount{1, -4})); }, std::nullopt, 0, std::nullopt, - {all, all - fixAMMv1_3}); + {all}); testAMM( [&](AMM& ammAlice, Env& env) { @@ -2028,22 +2014,20 @@ struct AMM_test : public jtx::AMMTest // Equal withdraw but due to XRP precision limit, // this results in full withdraw of XRP pool only, // while leaving a tiny amount in USD pool. - auto err = env.enabled(fixAMMv1_3) ? ter(tesSUCCESS) - : ter(tecAMM_BALANCE); + auto err = ter(tesSUCCESS); ammAlice.withdraw( alice, IOUAmount{9'999'999'999999999, -9}, std::nullopt, std::nullopt, err); - if (env.enabled(fixAMMv1_3)) - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount(1), STAmount{USD, 1, -11}, IOUAmount{1, -8})); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(1), STAmount{USD, 1, -11}, IOUAmount{1, -8})); }, std::nullopt, 0, std::nullopt, - {all, all - fixAMMv1_3}); + {all}); // Invalid AMM testAMM([&](AMM& ammAlice, Env& env) { @@ -2111,16 +2095,14 @@ struct AMM_test : public jtx::AMMTest testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit(carol, 1'000'000); - auto const err = env.enabled(fixAMMv1_3) - ? ter(tecAMM_INVALID_TOKENS) - : ter(tecAMM_FAILED); + auto const err = ter(tecAMM_INVALID_TOKENS); ammAlice.withdraw( carol, USD(100), std::nullopt, IOUAmount{500, 0}, err); }, std::nullopt, 0, std::nullopt, - {all, all - fixAMMv1_3}); + {all}); // Withdraw with EPrice limit. Fails to withdraw, calculated tokens // to withdraw are greater than the LP shares. @@ -2187,9 +2169,7 @@ struct AMM_test : public jtx::AMMTest // are rounded to all LP tokens. testAMM( [&](AMM& ammAlice, Env& env) { - auto const err = env.enabled(fixAMMv1_3) - ? ter(tecINVARIANT_FAILED) - : ter(tecAMM_BALANCE); + auto const err = ter(tecINVARIANT_FAILED); ammAlice.withdraw( alice, STAmount{USD, UINT64_C(9'999'999999999999), -12}, @@ -2197,7 +2177,7 @@ struct AMM_test : public jtx::AMMTest std::nullopt, err); }, - {.features = {all, all - fixAMMv1_3}, .noLog = true}); + {.features = {all}, .noLog = true}); // Tiny withdraw testAMM([&](AMM& ammAlice, Env&) { @@ -2305,21 +2285,15 @@ struct AMM_test : public jtx::AMMTest testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.withdraw(alice, XRP(1'000)); - if (!env.enabled(fixAMMv1_3)) - BEAST_EXPECT(ammAlice.expectBalances( - XRP(9'000), - USD(10'000), - IOUAmount{9'486'832'98050514, -8})); - else - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount{9'000'000'001}, - USD(10'000), - IOUAmount{9'486'832'98050514, -8})); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{9'000'000'001}, + USD(10'000), + IOUAmount{9'486'832'98050514, -8})); }, std::nullopt, 0, std::nullopt, - {all, all - fixAMMv1_3}); + {all}); // Single withdrawal by tokens 10000. testAMM([&](AMM& ammAlice, Env&) { @@ -2381,20 +2355,14 @@ struct AMM_test : public jtx::AMMTest ammAlice.withdraw(carol, lpTokens, USD(0)); lpTokens = ammAlice.deposit(carol, XRPAmount(1)); ammAlice.withdraw(carol, lpTokens, XRPAmount(0)); - if (!env.enabled(fixAMMv1_3)) - BEAST_EXPECT(ammAlice.expectBalances( - XRP(10'000), USD(10'000), ammAlice.tokens())); - else - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount(10'000'000'001), - USD(10'000), - ammAlice.tokens())); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(10'000'000'001), USD(10'000), ammAlice.tokens())); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0})); }, std::nullopt, 0, std::nullopt, - {all, all - fixAMMv1_3}); + {all}); // Single deposit by different accounts and then withdraw // in reverse. @@ -2445,20 +2413,14 @@ struct AMM_test : public jtx::AMMTest carol, USD(100), std::nullopt, IOUAmount{520, 0}); BEAST_EXPECT(ammAlice.expectLPTokens( carol, IOUAmount{153'846'15384616, -8})); - if (!env.enabled(fixAMMv1_3)) - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount(11'000'000'000), - STAmount{USD, UINT64_C(9'372'781065088769), -12}, - IOUAmount{10'153'846'15384616, -8})); - else if (env.enabled(fixAMMv1_3)) - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount(11'000'000'000), - STAmount{USD, UINT64_C(9'372'78106508877), -11}, - IOUAmount{10'153'846'15384616, -8})); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(11'000'000'000), + STAmount{USD, UINT64_C(9'372'78106508877), -11}, + IOUAmount{10'153'846'15384616, -8})); ammAlice.withdrawAll(carol); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0})); }, - {.features = {all, all - fixAMMv1_3}, .noLog = true}); + {.features = {all}, .noLog = true}); // Withdraw with EPrice limit. AssetOut is 0. testAMM( @@ -2468,21 +2430,15 @@ struct AMM_test : public jtx::AMMTest carol, USD(0), std::nullopt, IOUAmount{520, 0}); BEAST_EXPECT(ammAlice.expectLPTokens( carol, IOUAmount{153'846'15384616, -8})); - if (!env.enabled(fixAMMv1_3)) - BEAST_EXPECT(ammAlice.expectBalances( - XRP(11'000), - STAmount{USD, UINT64_C(9'372'781065088769), -12}, - IOUAmount{10'153'846'15384616, -8})); - else if (env.enabled(fixAMMv1_3)) - BEAST_EXPECT(ammAlice.expectBalances( - XRP(11'000), - STAmount{USD, UINT64_C(9'372'78106508877), -11}, - IOUAmount{10'153'846'15384616, -8})); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(11'000), + STAmount{USD, UINT64_C(9'372'78106508877), -11}, + IOUAmount{10'153'846'15384616, -8})); }, std::nullopt, 0, std::nullopt, - {all, all - fixAMMv1_3}); + {all}); // IOU to IOU + transfer fee { @@ -2525,21 +2481,13 @@ struct AMM_test : public jtx::AMMTest [&](AMM& ammAlice, Env& env) { // Single XRP pool ammAlice.withdraw(alice, std::nullopt, XRPAmount{1}); - if (!env.enabled(fixAMMv1_3)) - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount{9'999'999'999}, - USD(10'000), - IOUAmount{9'999'999'9995, -4})); - else - BEAST_EXPECT(ammAlice.expectBalances( - XRP(10'000), - USD(10'000), - IOUAmount{9'999'999'9995, -4})); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10'000), USD(10'000), IOUAmount{9'999'999'9995, -4})); }, std::nullopt, 0, std::nullopt, - {all, all - fixAMMv1_3}); + {all}); testAMM([&](AMM& ammAlice, Env&) { // Single USD pool ammAlice.withdraw(alice, std::nullopt, STAmount{USD, 1, -10}); @@ -2679,8 +2627,7 @@ struct AMM_test : public jtx::AMMTest // in order to ensure AMM invariant sqrt(asset1 * asset2) >= tokens // fund just one USD higher in this case, which is enough for // deposit to succeed - if (env.enabled(fixAMMv1_3)) - ++fundUSD; + ++fundUSD; fund(env, gw, {a}, {USD(fundUSD)}, Fund::Acct); ammAlice.deposit(a, tokens); ammAlice.vote(a, 50 * (i + 1)); @@ -3092,14 +3039,10 @@ struct AMM_test : public jtx::AMMTest fund(env, gw, {bob}, {USD(10'000)}, Fund::Acct); ammAlice.deposit(bob, 1'000'000); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(ammAlice.expectBalances( - XRP(12'000), USD(12'000), IOUAmount{12'000'000, 0})); - else - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount{12'000'000'001}, - USD(12'000), - IOUAmount{12'000'000, 0})); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{12'000'000'001}, + USD(12'000), + IOUAmount{12'000'000, 0})); // Initial state. Pay bidMin. env(ammAlice.bid({.account = carol, .bidMin = 110})).close(); @@ -3131,16 +3074,10 @@ struct AMM_test : public jtx::AMMTest BEAST_EXPECT(ammAlice.expectAuctionSlot( 0, std::nullopt, IOUAmount{110})); // ~321.09 tokens burnt on bidding fees. - if (!features[fixAMMv1_3]) - BEAST_EXPECT(ammAlice.expectBalances( - XRP(12'000), - USD(12'000), - IOUAmount{11'999'678'91, -2})); - else - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount{12'000'000'001}, - USD(12'000), - IOUAmount{11'999'678'91, -2})); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{12'000'000'001}, + USD(12'000), + IOUAmount{11'999'678'91, -2})); }, std::nullopt, 0, @@ -3169,12 +3106,8 @@ struct AMM_test : public jtx::AMMTest auto const slotPrice = IOUAmount{5'200}; ammTokens -= slotPrice; BEAST_EXPECT(ammAlice.expectAuctionSlot(100, 0, slotPrice)); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(ammAlice.expectBalances( - XRP(13'000), USD(13'000), ammTokens)); - else - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount{13'000'000'003}, USD(13'000), ammTokens)); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'000'000'003}, USD(13'000), ammTokens)); // Discounted trade for (int i = 0; i < 10; ++i) { @@ -3196,16 +3129,10 @@ struct AMM_test : public jtx::AMMTest env.balance(ed, USD) == STAmount(USD, UINT64_C(18'999'0057261184), -10)); // USD pool is slightly higher because of the fees. - if (!features[fixAMMv1_3]) - BEAST_EXPECT(ammAlice.expectBalances( - XRP(13'000), - STAmount(USD, UINT64_C(13'002'98282151422), -11), - ammTokens)); - else - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount{13'000'000'003}, - STAmount(USD, UINT64_C(13'002'98282151422), -11), - ammTokens)); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'000'000'003}, + STAmount(USD, UINT64_C(13'002'98282151422), -11), + ammTokens)); ammTokens = ammAlice.getLPTokensBalance(); // Trade with the fee for (int i = 0; i < 10; ++i) @@ -3217,80 +3144,44 @@ struct AMM_test : public jtx::AMMTest // carol, bob, ed. the discounted fee is 10 times less // than the trading fee. - if (!features[fixAMMv1_3]) - BEAST_EXPECT( - env.balance(dan, USD) == - STAmount(USD, UINT64_C(19'490'05672274399), -11)); - else - BEAST_EXPECT( - env.balance(dan, USD) == - STAmount(USD, UINT64_C(19'490'05672274398), -11)); + BEAST_EXPECT( + env.balance(dan, USD) == + STAmount(USD, UINT64_C(19'490'05672274398), -11)); // USD pool gains more in dan's fees. - if (!features[fixAMMv1_3]) - BEAST_EXPECT(ammAlice.expectBalances( - XRP(13'000), - STAmount{USD, UINT64_C(13'012'92609877023), -11}, - ammTokens)); - else - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount{13'000'000'003}, - STAmount{USD, UINT64_C(13'012'92609877024), -11}, - ammTokens)); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'000'000'003}, + STAmount{USD, UINT64_C(13'012'92609877024), -11}, + ammTokens)); // Discounted fee payment ammAlice.deposit(carol, USD(100)); ammTokens = ammAlice.getLPTokensBalance(); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(ammAlice.expectBalances( - XRP(13'000), - STAmount{USD, UINT64_C(13'112'92609877023), -11}, - ammTokens)); - else - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount{13'000'000'003}, - STAmount{USD, UINT64_C(13'112'92609877024), -11}, - ammTokens)); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'000'000'003}, + STAmount{USD, UINT64_C(13'112'92609877024), -11}, + ammTokens)); env(pay(carol, bob, USD(100)), path(~USD), sendmax(XRP(110))); env.close(); // carol pays 100000 drops in fees // 99900668XRP swapped in for 100USD - if (!features[fixAMMv1_3]) - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount{13'100'000'668}, - STAmount{USD, UINT64_C(13'012'92609877023), -11}, - ammTokens)); - else - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount{13'100'000'671}, - STAmount{USD, UINT64_C(13'012'92609877024), -11}, - ammTokens)); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'100'000'671}, + STAmount{USD, UINT64_C(13'012'92609877024), -11}, + ammTokens)); // Payment with the trading fee env(pay(alice, carol, XRP(100)), path(~XRP), sendmax(USD(110))); env.close(); // alice pays ~1.011USD in fees, which is ~10 times more // than carol's fee // 100.099431529USD swapped in for 100XRP - if (!features[fixAMMv1_3]) - { - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount{13'000'000'668}, - STAmount{USD, UINT64_C(13'114'03663047269), -11}, - ammTokens)); - } - else - { - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount{13'000'000'671}, - STAmount{USD, UINT64_C(13'114'03663044937), -11}, - ammTokens)); - } + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'000'000'671}, + STAmount{USD, UINT64_C(13'114'03663044937), -11}, + ammTokens)); + // Auction slot expired, no discounted fee env.close(seconds(TOTAL_TIME_SLOT_SECS + 1)); // clock is parent's based env.close(); - if (!features[fixAMMv1_3]) - BEAST_EXPECT( - env.balance(carol, USD) == - STAmount(USD, UINT64_C(29'399'00572620544), -11)); ammTokens = ammAlice.getLPTokensBalance(); for (int i = 0; i < 10; ++i) { @@ -3299,45 +3190,23 @@ struct AMM_test : public jtx::AMMTest } // carol pays ~9.94USD in fees, which is ~10 times more in // trading fees vs discounted fee. - if (!features[fixAMMv1_3]) - { - BEAST_EXPECT( - env.balance(carol, USD) == - STAmount(USD, UINT64_C(29'389'06197177124), -11)); - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount{13'000'000'668}, - STAmount{USD, UINT64_C(13'123'98038490689), -11}, - ammTokens)); - } - else - { - BEAST_EXPECT( - env.balance(carol, USD) == - STAmount(USD, UINT64_C(29'389'06197177129), -11)); - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount{13'000'000'671}, - STAmount{USD, UINT64_C(13'123'98038488352), -11}, - ammTokens)); - } + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(29'389'06197177129), -11)); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'000'000'671}, + STAmount{USD, UINT64_C(13'123'98038488352), -11}, + ammTokens)); + env(pay(carol, bob, USD(100)), path(~USD), sendmax(XRP(110))); env.close(); // carol pays ~1.008XRP in trading fee, which is // ~10 times more than the discounted fee. // 99.815876XRP is swapped in for 100USD - if (!features[fixAMMv1_3]) - { - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount(13'100'824'790), - STAmount{USD, UINT64_C(13'023'98038490689), -11}, - ammTokens)); - } - else - { - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount(13'100'824'793), - STAmount{USD, UINT64_C(13'023'98038488352), -11}, - ammTokens)); - } + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(13'100'824'793), + STAmount{USD, UINT64_C(13'023'98038488352), -11}, + ammTokens)); }, std::nullopt, 1'000, @@ -4819,10 +4688,7 @@ struct AMM_test : public jtx::AMMTest carol, USD(100), std::nullopt, IOUAmount{520, 0}); // carol withdraws ~1,443.44USD auto const balanceAfterWithdraw = [&]() { - if (!features[fixAMMv1_3]) - return STAmount(USD, UINT64_C(30'443'43891402714), -11); - else - return STAmount(USD, UINT64_C(30'443'43891402713), -11); + return STAmount(USD, UINT64_C(30'443'43891402713), -11); }(); BEAST_EXPECT(env.balance(carol, USD) == balanceAfterWithdraw); // Set to original pool size @@ -4832,22 +4698,12 @@ struct AMM_test : public jtx::AMMTest ammAlice.vote(alice, 0); BEAST_EXPECT(ammAlice.expectTradingFee(0)); auto const tokensNoFee = ammAlice.withdraw(carol, deposit); - if (!features[fixAMMv1_3]) - BEAST_EXPECT( - env.balance(carol, USD) == - STAmount(USD, UINT64_C(30'443'43891402716), -11)); - else - BEAST_EXPECT( - env.balance(carol, USD) == - STAmount(USD, UINT64_C(30'443'43891402713), -11)); + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(30'443'43891402713), -11)); // carol pays ~4008 LPTokens in fees or ~0.5% of the no-fee // LPTokens - if (!features[fixAMMv1_3]) - BEAST_EXPECT( - tokensNoFee == IOUAmount(746'579'80779912, -8)); - else - BEAST_EXPECT( - tokensNoFee == IOUAmount(746'579'80779911, -8)); + BEAST_EXPECT(tokensNoFee == IOUAmount(746'579'80779911, -8)); BEAST_EXPECT(tokensFee == IOUAmount(750'588'23529411, -8)); }, std::nullopt, @@ -5109,40 +4965,24 @@ struct AMM_test : public jtx::AMMTest // Due to round off some accounts have a tiny gain, while // other have a tiny loss. The last account to withdraw // gets everything in the pool. - if (features[fixAMMv1_3]) - BEAST_EXPECT(ammAlice.expectBalances( - XRP(10'000), - STAmount{USD, UINT64_C(10'000'0000000003), -10}, - IOUAmount{10'000'000})); - else - BEAST_EXPECT(ammAlice.expectBalances( - XRP(10'000), USD(10'000), IOUAmount{10'000'000})); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10'000), + STAmount{USD, UINT64_C(10'000'0000000003), -10}, + IOUAmount{10'000'000})); BEAST_EXPECT(expectLine(env, ben, USD(1'500'000))); BEAST_EXPECT(expectLine(env, simon, USD(1'500'000))); BEAST_EXPECT(expectLine(env, chris, USD(1'500'000))); BEAST_EXPECT(expectLine(env, dan, USD(1'500'000))); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(expectLine(env, carol, USD(30'000))); - else - BEAST_EXPECT(expectLine(env, carol, USD(30'000))); + BEAST_EXPECT(expectLine(env, carol, USD(30'000))); BEAST_EXPECT(expectLine(env, ed, USD(1'500'000))); BEAST_EXPECT(expectLine(env, paul, USD(1'500'000))); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(expectLine( - env, - nataly, - STAmount{USD, UINT64_C(1'500'000'000000005), -9})); - else - BEAST_EXPECT(expectLine(env, nataly, USD(1'500'000))); + BEAST_EXPECT(expectLine(env, nataly, USD(1'500'000))); ammAlice.withdrawAll(alice); BEAST_EXPECT(!ammAlice.ammExists()); - if (features[fixAMMv1_3]) - BEAST_EXPECT(expectLine( - env, - alice, - STAmount{USD, UINT64_C(30'000'0000000003), -10})); - else - BEAST_EXPECT(expectLine(env, alice, USD(30'000))); + BEAST_EXPECT(expectLine( + env, + alice, + STAmount{USD, UINT64_C(30'000'0000000003), -10})); // alice XRP balance is 30,000initial - 50 ammcreate fee - // 10drops fee BEAST_EXPECT(accountBalance(env, alice) == "29949999990"); @@ -5192,66 +5032,36 @@ struct AMM_test : public jtx::AMMTest ammAlice.withdrawAll(nataly, XRP(0)); } auto const baseFee = env.current()->fees().base.drops(); - if (!features[fixAMMv1_3]) - { - // No round off with XRP in this test - BEAST_EXPECT(ammAlice.expectBalances( - XRP(10'000), USD(10'000), IOUAmount{10'000'000})); - ammAlice.withdrawAll(alice); - BEAST_EXPECT(!ammAlice.ammExists()); - // 20,000 initial - (deposit+withdraw) * 10 - auto const xrpBalance = - (XRP(2'000'000) - txfee(env, 20)).getText(); - BEAST_EXPECT(accountBalance(env, ben) == xrpBalance); - BEAST_EXPECT(accountBalance(env, simon) == xrpBalance); - BEAST_EXPECT(accountBalance(env, chris) == xrpBalance); - BEAST_EXPECT(accountBalance(env, dan) == xrpBalance); - - // 30,000 initial - (deposit+withdraw) * 10 - BEAST_EXPECT( - accountBalance(env, carol) == - std::to_string(30'000'000'000 - 20 * baseFee)); - BEAST_EXPECT(accountBalance(env, ed) == xrpBalance); - BEAST_EXPECT(accountBalance(env, paul) == xrpBalance); - BEAST_EXPECT(accountBalance(env, nataly) == xrpBalance); - // 30,000 initial - 50 ammcreate fee - 10drops withdraw fee - BEAST_EXPECT( - accountBalance(env, alice) == - std::to_string(29'950'000'000 - baseFee)); - } - else - { - // post-amendment the rounding takes place to ensure - // AMM invariant - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount(10'000'000'080), - USD(10'000), - IOUAmount{10'000'000})); - ammAlice.withdrawAll(alice); - BEAST_EXPECT(!ammAlice.ammExists()); - auto const xrpBalance = - XRP(2'000'000) - txfee(env, 20) - drops(10); - auto const xrpBalanceText = xrpBalance.getText(); - BEAST_EXPECT(accountBalance(env, ben) == xrpBalanceText); - BEAST_EXPECT(accountBalance(env, simon) == xrpBalanceText); - BEAST_EXPECT(accountBalance(env, chris) == xrpBalanceText); - BEAST_EXPECT(accountBalance(env, dan) == xrpBalanceText); - BEAST_EXPECT( - accountBalance(env, carol) == - std::to_string(30'000'000'000 - 20 * baseFee - 10)); - BEAST_EXPECT( - accountBalance(env, ed) == - (xrpBalance + drops(2)).getText()); - BEAST_EXPECT( - accountBalance(env, paul) == - (xrpBalance + drops(3)).getText()); - BEAST_EXPECT( - accountBalance(env, nataly) == - (xrpBalance + drops(5)).getText()); - BEAST_EXPECT( - accountBalance(env, alice) == - std::to_string(29'950'000'000 - baseFee + 80)); - } + // post-amendment the rounding takes place to ensure + // AMM invariant + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(10'000'000'080), + USD(10'000), + IOUAmount{10'000'000})); + ammAlice.withdrawAll(alice); + BEAST_EXPECT(!ammAlice.ammExists()); + auto const xrpBalance = + XRP(2'000'000) - txfee(env, 20) - drops(10); + auto const xrpBalanceText = xrpBalance.getText(); + BEAST_EXPECT(accountBalance(env, ben) == xrpBalanceText); + BEAST_EXPECT(accountBalance(env, simon) == xrpBalanceText); + BEAST_EXPECT(accountBalance(env, chris) == xrpBalanceText); + BEAST_EXPECT(accountBalance(env, dan) == xrpBalanceText); + BEAST_EXPECT( + accountBalance(env, carol) == + std::to_string(30'000'000'000 - 20 * baseFee - 10)); + BEAST_EXPECT( + accountBalance(env, ed) == + (xrpBalance + drops(2)).getText()); + BEAST_EXPECT( + accountBalance(env, paul) == + (xrpBalance + drops(3)).getText()); + BEAST_EXPECT( + accountBalance(env, nataly) == + (xrpBalance + drops(5)).getText()); + BEAST_EXPECT( + accountBalance(env, alice) == + std::to_string(29'950'000'000 - baseFee + 80)); }, std::nullopt, 0, @@ -6374,7 +6184,7 @@ struct AMM_test : public jtx::AMMTest }) { testcase(input.testCase); - for (auto const& features : {all - fixAMMv1_3, all}) + for (auto const& features : {all}) { // Env env(*this, features, // std::make_unique(&logs)); @@ -6432,8 +6242,7 @@ struct AMM_test : public jtx::AMMTest auto const goodUsdGH = input.goodUsdGHr; auto const goodUsdBIT = input.goodUsdBITr; - auto const lpTokenBalance = - env.enabled(fixAMMv1_3) && input.lpTokenBalanceAlt + auto const lpTokenBalance = input.lpTokenBalanceAlt ? *input.lpTokenBalanceAlt : input.lpTokenBalance; @@ -6973,8 +6782,7 @@ struct AMM_test : public jtx::AMMTest }; test( [&](AMM& amm, Env& env) { - auto const err = env.enabled(fixAMMv1_3) ? ter(tesSUCCESS) - : ter(tecUNFUNDED_AMM); + auto const err = ter(tesSUCCESS); amm.deposit(DepositArg{ .account = alice, .asset1In = amount, .err = err}); }, @@ -6986,10 +6794,7 @@ struct AMM_test : public jtx::AMMTest amm.withdraw(WithdrawArg{.asset1Out = STAmount{XPM, 1, -5}}); auto const [amount_, amount2_, lptAMM_] = amm.balances(XRP, XPM); - if (!env.enabled(fixAMMv1_3)) - BEAST_EXPECT((amount2 - amount2_) > withdraw); - else - BEAST_EXPECT((amount2 - amount2_) <= withdraw); + BEAST_EXPECT((amount2 - amount2_) <= withdraw); }, 0); } @@ -7003,8 +6808,7 @@ struct AMM_test : public jtx::AMMTest { auto const [amount, amount2, lptBalance] = amm.balances(GBP, EUR); - NumberRoundModeGuard g( - env.enabled(fixAMMv1_3) ? Number::upward : Number::getround()); + NumberRoundModeGuard g(Number::upward); auto const res = root2(amount * amount2); if (shouldFail) @@ -7046,7 +6850,7 @@ struct AMM_test : public jtx::AMMTest env, "dep1", deposit == STAmount{EUR, 1, -3} && - !env.enabled(fixAMMv1_3)); + !true /*env.enabled(fixAMMv1_3)*/); }, {{GBP(30'000), EUR(30'000)}}, 0, @@ -7107,7 +6911,7 @@ struct AMM_test : public jtx::AMMTest ammAlice, env, "dep3", - exponent != -3 && !env.enabled(fixAMMv1_3)); + exponent != -3 && !true /*env.enabled(fixAMMv1_3)*/); }, {{GBP(10'000), EUR(30'000)}}, 0, @@ -7329,51 +7133,35 @@ struct AMM_test : public jtx::AMMTest testFeeVote(); testInvalidBid(); testBid(all); - testBid(all - fixAMMv1_3); testInvalidAMMPayment(); testBasicPaymentEngine(all); - testBasicPaymentEngine(all - fixAMMv1_3); testBasicPaymentEngine(all - fixReducedOffersV2); - testBasicPaymentEngine(all - fixAMMv1_3 - fixReducedOffersV2); testAMMTokens(); testAmendment(); testFlags(); testRippling(); testAMMAndCLOB(all); - testAMMAndCLOB(all - fixAMMv1_3); testTradingFee(all); - testTradingFee(all - fixAMMv1_3); testAdjustedTokens(all); - testAdjustedTokens(all - fixAMMv1_3); testAutoDelete(); testClawback(); testAMMID(); testSelection(all); - testSelection(all - fixAMMv1_3); testFixDefaultInnerObj(); testMalformed(); testFixOverflowOffer(all); - testFixOverflowOffer(all - fixAMMv1_3); testSwapRounding(); testFixChangeSpotPriceQuality(all); - testFixChangeSpotPriceQuality(all - fixAMMv1_3); testFixAMMOfferBlockedByLOB(all); - testFixAMMOfferBlockedByLOB(all - fixAMMv1_3); testLPTokenBalance(all); - testLPTokenBalance(all - fixAMMv1_3); testAMMClawback(all); testAMMClawback(all - featureAMMClawback); - testAMMClawback(all - fixAMMv1_3 - featureAMMClawback); testAMMDepositWithFrozenAssets(all); testAMMDepositWithFrozenAssets(all - featureAMMClawback); - testAMMDepositWithFrozenAssets(all - fixAMMv1_3 - featureAMMClawback); testFixReserveCheckOnWithdrawal(all); testDepositAndWithdrawRounding(all); - testDepositAndWithdrawRounding(all - fixAMMv1_3); testDepositRounding(all); - testDepositRounding(all - fixAMMv1_3); testWithdrawRounding(all); - testWithdrawRounding(all - fixAMMv1_3); // testFailedPseudoAccount(); } }; diff --git a/src/test/jtx/impl/AMM.cpp b/src/test/jtx/impl/AMM.cpp index cbb815994d..66a866ea0a 100644 --- a/src/test/jtx/impl/AMM.cpp +++ b/src/test/jtx/impl/AMM.cpp @@ -42,12 +42,6 @@ number(STAmount const& a) IOUAmount AMM::initialTokens() { - if (!env_.enabled(fixAMMv1_3)) - { - auto const product = number(asset1_) * number(asset2_); - return (IOUAmount)(product.mantissa() >= 0 ? root2(product) - : root2(-product)); - } return getLPTokensBalance(); } diff --git a/src/test/rpc/AMMInfo_test.cpp b/src/test/rpc/AMMInfo_test.cpp index 98e62d8929..6503de16ad 100644 --- a/src/test/rpc/AMMInfo_test.cpp +++ b/src/test/rpc/AMMInfo_test.cpp @@ -218,10 +218,7 @@ class AMMInfo_test : public jtx::AMMTestBase { Account a(std::to_string(i)); votes.insert({a.human(), 50 * (i + 1)}); - if (!features[fixAMMv1_3]) - fund(env, gw, {a}, {USD(10000)}, Fund::Acct); - else - fund(env, gw, {a}, {USD(10001)}, Fund::Acct); + fund(env, gw, {a}, {USD(10001)}, Fund::Acct); ammAlice.deposit(a, 10000000); ammAlice.vote(a, 50 * (i + 1)); } @@ -231,22 +228,13 @@ class AMMInfo_test : public jtx::AMMTestBase env.fund(XRP(1000), bob, ed, bill); env(ammAlice.bid( {.bidMin = 100, .authAccounts = {carol, bob, ed, bill}})); - if (!features[fixAMMv1_3]) - BEAST_EXPECT(ammAlice.expectAmmRpcInfo( - XRP(80000), - USD(80000), - IOUAmount{79994400}, - std::nullopt, - std::nullopt, - ammAlice.ammAccount())); - else - BEAST_EXPECT(ammAlice.expectAmmRpcInfo( - XRPAmount(80000000005), - STAmount{USD, UINT64_C(80'000'00000000005), -11}, - IOUAmount{79994400}, - std::nullopt, - std::nullopt, - ammAlice.ammAccount())); + BEAST_EXPECT(ammAlice.expectAmmRpcInfo( + XRPAmount(80000000005), + STAmount{USD, UINT64_C(80'000'00000000005), -11}, + IOUAmount{79994400}, + std::nullopt, + std::nullopt, + ammAlice.ammAccount())); for (auto i = 0; i < 2; ++i) { std::unordered_set authAccounts = { @@ -363,7 +351,6 @@ class AMMInfo_test : public jtx::AMMTestBase testErrors(); testSimpleRpc(); testVoteAndBid(all); - testVoteAndBid(all - fixAMMv1_3); testFreeze(); testInvalidAmmField(); } diff --git a/src/xrpld/app/misc/AMMHelpers.h b/src/xrpld/app/misc/AMMHelpers.h index 5825d6ad95..b5c257e330 100644 --- a/src/xrpld/app/misc/AMMHelpers.h +++ b/src/xrpld/app/misc/AMMHelpers.h @@ -590,13 +590,6 @@ getRoundedAsset( A const& frac, IsDeposit isDeposit) { - if (!rules.enabled(fixAMMv1_3)) - { - if constexpr (std::is_same_v) - return multiply(balance, frac, balance.issue()); - else - return toSTAmount(balance.issue(), balance * frac); - } auto const rm = detail::getAssetRounding(isDeposit); return multiply(balance, frac, rm); } diff --git a/src/xrpld/app/misc/detail/AMMHelpers.cpp b/src/xrpld/app/misc/detail/AMMHelpers.cpp index 97c355b1ff..644482bcc9 100644 --- a/src/xrpld/app/misc/detail/AMMHelpers.cpp +++ b/src/xrpld/app/misc/detail/AMMHelpers.cpp @@ -28,8 +28,7 @@ ammLPTokens( Issue const& lptIssue) { // AMM invariant: sqrt(asset1 * asset2) >= LPTokensBalance - auto const rounding = - isFeatureEnabled(fixAMMv1_3) ? Number::downward : Number::getround(); + auto const rounding = Number::downward; NumberRoundModeGuard g(rounding); auto const tokens = root2(asset1 * asset2); return toSTAmount(lptIssue, tokens); @@ -52,17 +51,10 @@ lpTokensOut( auto const f2 = feeMultHalf(tfee) / f1; Number const r = asset1Deposit / asset1Balance; auto const c = root2(f2 * f2 + r / f1) - f2; - if (!isFeatureEnabled(fixAMMv1_3)) - { - auto const t = lptAMMBalance * (r - c) / (1 + c); - return toSTAmount(lptAMMBalance.issue(), t); - } - else - { - // minimize tokens out - auto const frac = (r - c) / (1 + c); - return multiply(lptAMMBalance, frac, Number::downward); - } + + // minimize tokens out + auto const frac = (r - c) / (1 + c); + return multiply(lptAMMBalance, frac, Number::downward); } /* Equation 4 solves equation 3 for b: @@ -91,17 +83,10 @@ ammAssetIn( auto const a = 1 / (t2 * t2); auto const b = 2 * d / t2 - 1 / f1; auto const c = d * d - f2 * f2; - if (!isFeatureEnabled(fixAMMv1_3)) - { - return toSTAmount( - asset1Balance.issue(), asset1Balance * solveQuadraticEq(a, b, c)); - } - else - { - // maximize deposit - auto const frac = solveQuadraticEq(a, b, c); - return multiply(asset1Balance, frac, Number::upward); - } + + // maximize deposit + auto const frac = solveQuadraticEq(a, b, c); + return multiply(asset1Balance, frac, Number::upward); } /* Equation 7: @@ -118,17 +103,10 @@ lpTokensIn( Number const fr = asset1Withdraw / asset1Balance; auto const f1 = getFee(tfee); auto const c = fr * f1 + 2 - f1; - if (!isFeatureEnabled(fixAMMv1_3)) - { - auto const t = lptAMMBalance * (c - root2(c * c - 4 * fr)) / 2; - return toSTAmount(lptAMMBalance.issue(), t); - } - else - { - // maximize tokens in - auto const frac = (c - root2(c * c - 4 * fr)) / 2; - return multiply(lptAMMBalance, frac, Number::upward); - } + + // maximize tokens in + auto const frac = (c - root2(c * c - 4 * fr)) / 2; + return multiply(lptAMMBalance, frac, Number::upward); } /* Equation 8 solves equation 7 for b: @@ -150,17 +128,10 @@ ammAssetOut( { auto const f = getFee(tfee); Number const t1 = lpTokens / lptAMMBalance; - if (!isFeatureEnabled(fixAMMv1_3)) - { - auto const b = assetBalance * (t1 * t1 - t1 * (2 - f)) / (t1 * f - 1); - return toSTAmount(assetBalance.issue(), b); - } - else - { - // minimize withdraw - auto const frac = (t1 * t1 - t1 * (2 - f)) / (t1 * f - 1); - return multiply(assetBalance, frac, Number::downward); - } + + // minimize withdraw + auto const frac = (t1 * t1 - t1 * (2 - f)) / (t1 * f - 1); + return multiply(assetBalance, frac, Number::downward); } Number @@ -194,48 +165,7 @@ adjustAmountsByLPTokens( IsDeposit isDeposit) { // AMMv1_3 amendment adjusts tokens and amounts in deposit/withdraw - if (isFeatureEnabled(fixAMMv1_3)) - return std::make_tuple(amount, amount2, lpTokens); - - auto const lpTokensActual = - adjustLPTokens(lptAMMBalance, lpTokens, isDeposit); - - if (lpTokensActual == beast::zero) - { - auto const amount2Opt = - amount2 ? std::make_optional(STAmount{}) : std::nullopt; - return std::make_tuple(STAmount{}, amount2Opt, lpTokensActual); - } - - if (lpTokensActual < lpTokens) - { - // Equal trade - if (amount2) - { - Number const fr = lpTokensActual / lpTokens; - auto const amountActual = toSTAmount(amount.issue(), fr * amount); - auto const amount2Actual = - toSTAmount(amount2->issue(), fr * *amount2); - return std::make_tuple(amountActual, amount2Actual, lpTokensActual); - } - - // Single trade - auto const amountActual = [&]() { - if (isDeposit == IsDeposit::Yes) - return ammAssetIn( - amountBalance, lptAMMBalance, lpTokensActual, tfee); - return ammAssetOut( - amountBalance, lptAMMBalance, lpTokensActual, tfee); - }(); - - return std::make_tuple(amountActual, std::nullopt, lpTokensActual); - } - - XRPL_ASSERT( - lpTokensActual == lpTokens, - "ripple::adjustAmountsByLPTokens : LP tokens match actual"); - - return {amount, amount2, lpTokensActual}; + return std::make_tuple(amount, amount2, lpTokens); } Number @@ -275,9 +205,6 @@ getRoundedAsset( std::function&& productCb, IsDeposit isDeposit) { - if (!rules.enabled(fixAMMv1_3)) - return toSTAmount(balance.issue(), noRoundCb()); - auto const rm = detail::getAssetRounding(isDeposit); if (isDeposit == IsDeposit::Yes) return multiply(balance, productCb(), rm); @@ -292,9 +219,6 @@ getRoundedLPTokens( Number const& frac, IsDeposit isDeposit) { - if (!rules.enabled(fixAMMv1_3)) - return toSTAmount(balance.issue(), balance * frac); - auto const rm = detail::getLPTokenRounding(isDeposit); auto const tokens = multiply(balance, frac, rm); return adjustLPTokens(balance, tokens, isDeposit); @@ -308,9 +232,6 @@ getRoundedLPTokens( std::function&& productCb, IsDeposit isDeposit) { - if (!rules.enabled(fixAMMv1_3)) - return toSTAmount(lptAMMBalance.issue(), noRoundCb()); - auto const tokens = [&] { auto const rm = detail::getLPTokenRounding(isDeposit); if (isDeposit == IsDeposit::Yes) @@ -332,8 +253,6 @@ adjustAssetInByTokens( STAmount const& tokens, std::uint16_t tfee) { - if (!rules.enabled(fixAMMv1_3)) - return {tokens, amount}; auto assetAdj = ammAssetIn(balance, lptAMMBalance, tokens, tfee); auto tokensAdj = tokens; // Rounding didn't work the right way. @@ -358,8 +277,6 @@ adjustAssetOutByTokens( STAmount const& tokens, std::uint16_t tfee) { - if (!rules.enabled(fixAMMv1_3)) - return {tokens, amount}; auto assetAdj = ammAssetOut(balance, lptAMMBalance, tokens, tfee); auto tokensAdj = tokens; // Rounding didn't work the right way. @@ -382,8 +299,6 @@ adjustFracByTokens( STAmount const& tokens, Number const& frac) { - if (!rules.enabled(fixAMMv1_3)) - return frac; return tokens / lptAMMBalance; } diff --git a/src/xrpld/app/tx/detail/AMMBid.cpp b/src/xrpld/app/tx/detail/AMMBid.cpp index 9a9730228d..ee89432766 100644 --- a/src/xrpld/app/tx/detail/AMMBid.cpp +++ b/src/xrpld/app/tx/detail/AMMBid.cpp @@ -79,7 +79,7 @@ AMMBid::preflight(PreflightContext const& ctx) JLOG(ctx.j.debug()) << "AMM Bid: Invalid number of AuthAccounts."; return temMALFORMED; } - else if (ctx.rules.enabled(fixAMMv1_3)) + else { AccountID account = ctx.tx[sfAccount]; std::set unique; diff --git a/src/xrpld/app/tx/detail/AMMDeposit.cpp b/src/xrpld/app/tx/detail/AMMDeposit.cpp index 50152e4014..0dab0d68be 100644 --- a/src/xrpld/app/tx/detail/AMMDeposit.cpp +++ b/src/xrpld/app/tx/detail/AMMDeposit.cpp @@ -634,8 +634,6 @@ adjustLPTokensOut( STAmount const& lptAMMBalance, STAmount const& lpTokensDeposit) { - if (!rules.enabled(fixAMMv1_3)) - return lpTokensDeposit; return adjustLPTokens(lptAMMBalance, lpTokensDeposit, IsDeposit::Yes); } @@ -658,7 +656,7 @@ AMMDeposit::equalDepositTokens( { auto const tokensAdj = adjustLPTokensOut(view.rules(), lptAMMBalance, lpTokensDeposit); - if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + if (tokensAdj == beast::zero) return {tecAMM_INVALID_TOKENS, STAmount{}}; auto const frac = divide(tokensAdj, lptAMMBalance, lptAMMBalance.issue()); @@ -735,10 +733,7 @@ AMMDeposit::equalDepositLimit( getRoundedLPTokens(view.rules(), lptAMMBalance, frac, IsDeposit::Yes); if (tokensAdj == beast::zero) { - if (!view.rules().enabled(fixAMMv1_3)) - return {tecAMM_FAILED, STAmount{}}; // LCOV_EXCL_LINE - else - return {tecAMM_INVALID_TOKENS, STAmount{}}; + return {tecAMM_INVALID_TOKENS, STAmount{}}; } // factor in the adjusted tokens frac = adjustFracByTokens(view.rules(), lptAMMBalance, tokensAdj, frac); @@ -762,10 +757,7 @@ AMMDeposit::equalDepositLimit( getRoundedLPTokens(view.rules(), lptAMMBalance, frac, IsDeposit::Yes); if (tokensAdj == beast::zero) { - if (!view.rules().enabled(fixAMMv1_3)) - return {tecAMM_FAILED, STAmount{}}; // LCOV_EXCL_LINE - else - return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE + return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE } // factor in the adjusted tokens frac = adjustFracByTokens(view.rules(), lptAMMBalance, tokensAdj, frac); @@ -811,15 +803,12 @@ AMMDeposit::singleDeposit( lpTokensOut(amountBalance, amount, lptAMMBalance, tfee)); if (tokens == beast::zero) { - if (!view.rules().enabled(fixAMMv1_3)) - return {tecAMM_FAILED, STAmount{}}; // LCOV_EXCL_LINE - else - return {tecAMM_INVALID_TOKENS, STAmount{}}; + return {tecAMM_INVALID_TOKENS, STAmount{}}; } // factor in the adjusted tokens auto const [tokensAdj, amountDepositAdj] = adjustAssetInByTokens( view.rules(), amountBalance, amount, lptAMMBalance, tokens, tfee); - if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + if (tokensAdj == beast::zero) return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE return deposit( view, @@ -854,7 +843,7 @@ AMMDeposit::singleDepositTokens( { auto const tokensAdj = adjustLPTokensOut(view.rules(), lptAMMBalance, lpTokensDeposit); - if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + if (tokensAdj == beast::zero) return {tecAMM_INVALID_TOKENS, STAmount{}}; // the adjusted tokens are factored in auto const amountDeposit = @@ -918,15 +907,12 @@ AMMDeposit::singleDepositEPrice( lpTokensOut(amountBalance, amount, lptAMMBalance, tfee)); if (tokens <= beast::zero) { - if (!view.rules().enabled(fixAMMv1_3)) - return {tecAMM_FAILED, STAmount{}}; // LCOV_EXCL_LINE - else - return {tecAMM_INVALID_TOKENS, STAmount{}}; + return {tecAMM_INVALID_TOKENS, STAmount{}}; } // factor in the adjusted tokens auto const [tokensAdj, amountDepositAdj] = adjustAssetInByTokens( view.rules(), amountBalance, amount, lptAMMBalance, tokens, tfee); - if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + if (tokensAdj == beast::zero) return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE auto const ep = Number{amountDepositAdj} / tokensAdj; if (ep <= ePrice) @@ -988,7 +974,7 @@ AMMDeposit::singleDepositEPrice( lptAMMBalance, tokens, tfee); - if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + if (tokensAdj == beast::zero) return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE return deposit( diff --git a/src/xrpld/app/tx/detail/AMMWithdraw.cpp b/src/xrpld/app/tx/detail/AMMWithdraw.cpp index 233ebb4578..e8569b48c2 100644 --- a/src/xrpld/app/tx/detail/AMMWithdraw.cpp +++ b/src/xrpld/app/tx/detail/AMMWithdraw.cpp @@ -689,7 +689,7 @@ adjustLPTokensIn( STAmount const& lpTokensWithdraw, WithdrawAll withdrawAll) { - if (!rules.enabled(fixAMMv1_3) || withdrawAll == WithdrawAll::Yes) + if (withdrawAll == WithdrawAll::Yes) return lpTokensWithdraw; return adjustLPTokens(lptAMMBalance, lpTokensWithdraw, IsDeposit::No); } @@ -801,7 +801,7 @@ AMMWithdraw::equalWithdrawTokens( auto const tokensAdj = adjustLPTokensIn( view.rules(), lptAMMBalance, lpTokensWithdraw, withdrawAll); - if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + if (tokensAdj == beast::zero) return { tecAMM_INVALID_TOKENS, STAmount{}, STAmount{}, std::nullopt}; // the adjusted tokens are factored in @@ -885,7 +885,7 @@ AMMWithdraw::equalWithdrawLimit( getRoundedAsset(view.rules(), amount2Balance, frac, IsDeposit::No); auto tokensAdj = getRoundedLPTokens(view.rules(), lptAMMBalance, frac, IsDeposit::No); - if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + if (tokensAdj == beast::zero) return {tecAMM_INVALID_TOKENS, STAmount{}}; // factor in the adjusted tokens frac = adjustFracByTokens(view.rules(), lptAMMBalance, tokensAdj, frac); @@ -910,21 +910,13 @@ AMMWithdraw::equalWithdrawLimit( getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::No); tokensAdj = getRoundedLPTokens(view.rules(), lptAMMBalance, frac, IsDeposit::No); - if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + if (tokensAdj == beast::zero) return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE // factor in the adjusted tokens frac = adjustFracByTokens(view.rules(), lptAMMBalance, tokensAdj, frac); amountWithdraw = getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::No); - if (!view.rules().enabled(fixAMMv1_3)) - { - // LCOV_EXCL_START - XRPL_ASSERT( - amountWithdraw <= amount, - "ripple::AMMWithdraw::equalWithdrawLimit : maximum amountWithdraw"); - // LCOV_EXCL_STOP - } - else if (amountWithdraw > amount) + if (amountWithdraw > amount) return {tecAMM_FAILED, STAmount{}}; // LCOV_EXCL_LINE return withdraw( view, @@ -960,15 +952,12 @@ AMMWithdraw::singleWithdraw( isWithdrawAll(ctx_.tx)); if (tokens == beast::zero) { - if (!view.rules().enabled(fixAMMv1_3)) - return {tecAMM_FAILED, STAmount{}}; // LCOV_EXCL_LINE - else - return {tecAMM_INVALID_TOKENS, STAmount{}}; + return {tecAMM_INVALID_TOKENS, STAmount{}}; } // factor in the adjusted tokens auto const [tokensAdj, amountWithdrawAdj] = adjustAssetOutByTokens( view.rules(), amountBalance, amount, lptAMMBalance, tokens, tfee); - if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + if (tokensAdj == beast::zero) return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE return withdraw( view, @@ -1005,7 +994,7 @@ AMMWithdraw::singleWithdrawTokens( { auto const tokensAdj = adjustLPTokensIn( view.rules(), lptAMMBalance, lpTokensWithdraw, isWithdrawAll(ctx_.tx)); - if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + if (tokensAdj == beast::zero) return {tecAMM_INVALID_TOKENS, STAmount{}}; // the adjusted tokens are factored in auto const amountWithdraw = @@ -1080,10 +1069,7 @@ AMMWithdraw::singleWithdrawEPrice( view.rules(), tokNoRoundCb, lptAMMBalance, tokProdCb, IsDeposit::No); if (tokensAdj <= beast::zero) { - if (!view.rules().enabled(fixAMMv1_3)) - return {tecAMM_FAILED, STAmount{}}; - else - return {tecAMM_INVALID_TOKENS, STAmount{}}; + return {tecAMM_INVALID_TOKENS, STAmount{}}; } auto amtNoRoundCb = [&] { return tokensAdj / ePrice; }; auto amtProdCb = [&] { return tokensAdj / ePrice; }; diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 9484eefd76..85b899b900 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -1954,7 +1954,7 @@ ValidAMM::finalize( if (result != tesSUCCESS && result != tecINCOMPLETE) return true; - bool const enforce = view.rules().enabled(fixAMMv1_3); + bool const enforce = true; // view.rules().enabled(fixAMMv1_3); switch (tx.getTxnType()) { diff --git a/src/xrpld/app/tx/detail/Offer.h b/src/xrpld/app/tx/detail/Offer.h index a26a35e60b..0ebe5c08cc 100644 --- a/src/xrpld/app/tx/detail/Offer.h +++ b/src/xrpld/app/tx/detail/Offer.h @@ -173,9 +173,6 @@ class TOffer : private TOfferBase bool checkInvariant(TAmounts const& consumed, beast::Journal j) const { - if (!isFeatureEnabled(fixAMMv1_3)) - return true; - if (consumed.in > m_amounts.in || consumed.out > m_amounts.out) { // LCOV_EXCL_START From 8673599d2b8621d37fe9a7848107b7cba435cba8 Mon Sep 17 00:00:00 2001 From: yinyiqian1 Date: Fri, 11 Jul 2025 16:03:28 -0400 Subject: [PATCH 03/17] fixAMMClawbackRounding: adjust last holder's LPToken balance (#5513) Due to rounding, the LPTokenBalance of the last LP might not match the LP's trustline balance. This was fixed for `AMMWithdraw` in `fixAMMv1_1` by adjusting the LPTokenBalance to be the same as the trustline balance. Since `AMMClawback` is also performing a withdrawal, we need to adjust LPTokenBalance as well in `AMMClawback.` This change includes: 1. Refactored `verifyAndAdjustLPTokenBalance` function in `AMMUtils`, which both`AMMWithdraw` and `AMMClawback` call to adjust LPTokenBalance. 2. Added the unit test `testLastHolderLPTokenBalance` to test the scenario. 3. Modify the existing unit tests for `fixAMMClawbackRounding`. --- include/xrpl/protocol/detail/features.macro | 1 + src/test/app/AMMClawback_test.cpp | 795 ++++++++++++++++---- src/xrpld/app/misc/AMMUtils.h | 11 + src/xrpld/app/misc/detail/AMMUtils.cpp | 30 + src/xrpld/app/tx/detail/AMMClawback.cpp | 53 +- src/xrpld/app/tx/detail/AMMWithdraw.cpp | 17 +- 6 files changed, 728 insertions(+), 179 deletions(-) diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 3f48ac7145..738a8287b2 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -31,6 +31,7 @@ // If you add an amendment here, then do not forget to increment `numFeatures` // in include/xrpl/protocol/Feature.h. +XRPL_FIX (AMMClawbackRounding, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(HookAPISerializedType240, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(PermissionedDomains, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(DynamicNFT, Supported::no, VoteBehavior::DefaultNo) diff --git a/src/test/app/AMMClawback_test.cpp b/src/test/app/AMMClawback_test.cpp index 4d2a995934..4fec924f91 100644 --- a/src/test/app/AMMClawback_test.cpp +++ b/src/test/app/AMMClawback_test.cpp @@ -16,27 +16,25 @@ //============================================================================== #include #include -#include -#include -#include -#include -#include +#include + +#include + #include -#include -#include + namespace ripple { namespace test { -class AMMClawback_test : public jtx::AMMTest +class AMMClawback_test : public beast::unit_test::suite { void - testInvalidRequest(FeatureBitset features) + testInvalidRequest() { testcase("test invalid request"); using namespace jtx; // Test if holder does not exist. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(100000), gw, alice); @@ -47,8 +45,9 @@ class AMMClawback_test : public jtx::AMMTest env.close(); env.require(flags(gw, asfAllowTrustLineClawback)); + auto const USD = gw["USD"]; env.trust(USD(10000), alice); - env(pay(gw, alice, gw["USD"](100))); + env(pay(gw, alice, USD(100))); AMM amm(env, alice, XRP(100), USD(100)); env.close(); @@ -61,7 +60,7 @@ class AMMClawback_test : public jtx::AMMTest // Test if asset pair provided does not exist. This should // return terNO_AMM error. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(100000), gw, alice); @@ -87,14 +86,14 @@ class AMMClawback_test : public jtx::AMMTest // The AMM account does not exist at all now. // It should return terNO_AMM error. - env(amm::ammClawback(gw, alice, USD, EUR, std::nullopt), + env(amm::ammClawback(gw, alice, USD, gw["EUR"], std::nullopt), ter(terNO_AMM)); } // Test if the issuer field and holder field is the same. This should // return temMALFORMED error. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(10000), gw, alice); @@ -124,7 +123,7 @@ class AMMClawback_test : public jtx::AMMTest // Test if the Asset field matches the Account field. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(10000), gw, alice); @@ -156,7 +155,7 @@ class AMMClawback_test : public jtx::AMMTest // Test if the Amount field matches the Asset field. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(10000), gw, alice); @@ -189,7 +188,7 @@ class AMMClawback_test : public jtx::AMMTest // Test if the Amount is invalid, which is less than zero. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(10000), gw, alice); @@ -230,7 +229,7 @@ class AMMClawback_test : public jtx::AMMTest // Test if the issuer did not set asfAllowTrustLineClawback, AMMClawback // transaction is prohibited. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(10000), gw, alice); @@ -241,7 +240,7 @@ class AMMClawback_test : public jtx::AMMTest env.trust(USD(1000), alice); env(pay(gw, alice, USD(100))); env.close(); - env.require(balance(alice, gw["USD"](100))); + env.require(balance(alice, USD(100))); env.require(balance(gw, alice["USD"](-100))); // gw creates AMM pool of XRP/USD. @@ -255,7 +254,7 @@ class AMMClawback_test : public jtx::AMMTest // Test invalid flag. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(10000), gw, alice); @@ -283,7 +282,7 @@ class AMMClawback_test : public jtx::AMMTest // Test if tfClawTwoAssets is set when the two assets in the AMM pool // are not issued by the same issuer. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(10000), gw, alice); @@ -314,7 +313,7 @@ class AMMClawback_test : public jtx::AMMTest // Test clawing back XRP is being prohibited. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(1000000), gw, alice); @@ -400,7 +399,7 @@ class AMMClawback_test : public jtx::AMMTest env(pay(gw, alice, USD(3000))); env.close(); env.require(balance(gw, alice["USD"](-3000))); - env.require(balance(alice, gw["USD"](3000))); + env.require(balance(alice, USD(3000))); // gw2 issues 3000 EUR to Alice. auto const EUR = gw2["EUR"]; @@ -408,7 +407,7 @@ class AMMClawback_test : public jtx::AMMTest env(pay(gw2, alice, EUR(3000))); env.close(); env.require(balance(gw2, alice["EUR"](-3000))); - env.require(balance(alice, gw2["EUR"](3000))); + env.require(balance(alice, EUR(3000))); // Alice creates AMM pool of EUR/USD. AMM amm(env, alice, EUR(1000), USD(2000), ter(tesSUCCESS)); @@ -426,13 +425,13 @@ class AMMClawback_test : public jtx::AMMTest // USD into the pool, then she has 1000 USD. And 1000 USD was clawed // back from the AMM pool, so she still has 1000 USD. env.require(balance(gw, alice["USD"](-1000))); - env.require(balance(alice, gw["USD"](1000))); + env.require(balance(alice, USD(1000))); // Alice's initial balance for EUR is 3000 EUR. Alice deposited 1000 // EUR into the pool, 500 EUR was withdrawn proportionally. So she // has 2500 EUR now. env.require(balance(gw2, alice["EUR"](-2500))); - env.require(balance(alice, gw2["EUR"](2500))); + env.require(balance(alice, EUR(2500))); // 1000 USD and 500 EUR was withdrawn from the AMM pool, so the // current balance is 1000 USD and 500 EUR. @@ -452,12 +451,12 @@ class AMMClawback_test : public jtx::AMMTest // Alice should still has 1000 USD because gw clawed back from the // AMM pool. env.require(balance(gw, alice["USD"](-1000))); - env.require(balance(alice, gw["USD"](1000))); + env.require(balance(alice, USD(1000))); // Alice should has 3000 EUR now because another 500 EUR was // withdrawn. env.require(balance(gw2, alice["EUR"](-3000))); - env.require(balance(alice, gw2["EUR"](3000))); + env.require(balance(alice, EUR(3000))); // amm is automatically deleted. BEAST_EXPECT(!amm.ammExists()); @@ -483,7 +482,7 @@ class AMMClawback_test : public jtx::AMMTest env(pay(gw, alice, USD(3000))); env.close(); env.require(balance(gw, alice["USD"](-3000))); - env.require(balance(alice, gw["USD"](3000))); + env.require(balance(alice, USD(3000))); // Alice creates AMM pool of XRP/USD. AMM amm(env, alice, XRP(1000), USD(2000), ter(tesSUCCESS)); @@ -503,11 +502,12 @@ class AMMClawback_test : public jtx::AMMTest // USD into the pool, then she has 1000 USD. And 1000 USD was clawed // back from the AMM pool, so she still has 1000 USD. env.require(balance(gw, alice["USD"](-1000))); - env.require(balance(alice, gw["USD"](1000))); + env.require(balance(alice, USD(1000))); // Alice will get 500 XRP back. BEAST_EXPECT( expectLedgerEntryRoot(env, alice, aliceXrpBalance + XRP(500))); + aliceXrpBalance = env.balance(alice, XRP); // 1000 USD and 500 XRP was withdrawn from the AMM pool, so the // current balance is 1000 USD and 500 XRP. @@ -527,11 +527,11 @@ class AMMClawback_test : public jtx::AMMTest // Alice should still has 1000 USD because gw clawed back from the // AMM pool. env.require(balance(gw, alice["USD"](-1000))); - env.require(balance(alice, gw["USD"](1000))); + env.require(balance(alice, USD(1000))); - // Alice will get another 1000 XRP back. + // Alice will get another 500 XRP back. BEAST_EXPECT( - expectLedgerEntryRoot(env, alice, aliceXrpBalance + XRP(1000))); + expectLedgerEntryRoot(env, alice, aliceXrpBalance + XRP(500))); // amm is automatically deleted. BEAST_EXPECT(!amm.ammExists()); @@ -568,14 +568,14 @@ class AMMClawback_test : public jtx::AMMTest env.trust(USD(100000), alice); env(pay(gw, alice, USD(6000))); env.close(); - env.require(balance(alice, gw["USD"](6000))); + env.require(balance(alice, USD(6000))); // gw2 issues 6000 EUR to Alice. auto const EUR = gw2["EUR"]; env.trust(EUR(100000), alice); env(pay(gw2, alice, EUR(6000))); env.close(); - env.require(balance(alice, gw2["EUR"](6000))); + env.require(balance(alice, EUR(6000))); // Alice creates AMM pool of EUR/USD AMM amm(env, alice, EUR(5000), USD(4000), ter(tesSUCCESS)); @@ -592,12 +592,12 @@ class AMMClawback_test : public jtx::AMMTest // Alice's initial balance for USD is 6000 USD. Alice deposited 4000 // USD into the pool, then she has 2000 USD. And 1000 USD was clawed // back from the AMM pool, so she still has 2000 USD. - env.require(balance(alice, gw["USD"](2000))); + env.require(balance(alice, USD(2000))); // Alice's initial balance for EUR is 6000 EUR. Alice deposited 5000 // EUR into the pool, 1250 EUR was withdrawn proportionally. So she // has 2500 EUR now. - env.require(balance(alice, gw2["EUR"](2250))); + env.require(balance(alice, EUR(2250))); // 1000 USD and 1250 EUR was withdrawn from the AMM pool, so the // current balance is 3000 USD and 3750 EUR. @@ -615,7 +615,7 @@ class AMMClawback_test : public jtx::AMMTest // Alice should still has 2000 USD because gw clawed back from the // AMM pool. - env.require(balance(alice, gw["USD"](2000))); + env.require(balance(alice, USD(2000))); BEAST_EXPECT(amm.expectBalances( USD(2500), EUR(3125), IOUAmount{2795084971874737, -12})); @@ -627,12 +627,23 @@ class AMMClawback_test : public jtx::AMMTest env.close(); // Another 1 USD / 1.25 EUR was withdrawn. - env.require(balance(alice, gw["USD"](2000))); - - BEAST_EXPECT(amm.expectBalances( - USD(2499), EUR(3123.75), IOUAmount{2793966937885987, -12})); - - BEAST_EXPECT(env.balance(alice, EUR) == EUR(2876.25)); + env.require(balance(alice, USD(2000))); + + if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm.expectBalances( + USD(2499), EUR(3123.75), IOUAmount{2793966937885987, -12})); + else + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(2499000000000001), -12}, + STAmount{EUR, UINT64_C(3123750000000001), -12}, + IOUAmount{2793966937885988, -12})); + + if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT(env.balance(alice, EUR) == EUR(2876.25)); + else + BEAST_EXPECT( + env.balance(alice, EUR) == + STAmount(EUR, UINT64_C(2876'249999999999), -12)); // gw clawback 4000 USD, exceeding the current balance. We // will clawback all. @@ -640,7 +651,7 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw["USD"](2000))); + env.require(balance(alice, USD(2000))); // All alice's EUR in the pool goes back to alice. BEAST_EXPECT( @@ -707,6 +718,7 @@ class AMMClawback_test : public jtx::AMMTest AMM amm2(env, gw2, XRP(3000), EUR(1000), ter(tesSUCCESS)); BEAST_EXPECT(amm2.expectBalances( EUR(1000), XRP(3000), IOUAmount{1732050807568877, -9})); + amm2.deposit(alice, EUR(1000), XRP(3000)); BEAST_EXPECT(amm2.expectBalances( EUR(2000), XRP(6000), IOUAmount{3464101615137754, -9})); @@ -726,19 +738,36 @@ class AMMClawback_test : public jtx::AMMTest // Alice's initial balance for USD is 6000 USD. Alice deposited 1000 // USD into the pool, then she has 5000 USD. And 500 USD was clawed // back from the AMM pool, so she still has 5000 USD. - env.require(balance(alice, gw["USD"](5000))); + env.require(balance(alice, USD(5000))); // Bob's balance is not changed. - env.require(balance(bob, gw["USD"](4000))); + env.require(balance(bob, USD(4000))); // Alice gets 1000 XRP back. - BEAST_EXPECT( - expectLedgerEntryRoot(env, alice, aliceXrpBalance + XRP(1000))); + if (features[fixAMMClawbackRounding]) + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(1000) - XRPAmount(1))); + else + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(1000))); + aliceXrpBalance = env.balance(alice, XRP); + + if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm.expectBalances( + USD(2500), XRP(5000), IOUAmount{3535533905932737, -9})); + else + BEAST_EXPECT(amm.expectBalances( + USD(2500), + XRPAmount(5000000001), + IOUAmount{3'535'533'905932738, -9})); + + if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{7071067811865474, -10})); + else + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{707106781186548, -9})); - BEAST_EXPECT(amm.expectBalances( - USD(2500), XRP(5000), IOUAmount{3535533905932737, -9})); - BEAST_EXPECT( - amm.expectLPTokens(alice, IOUAmount{7071067811865474, -10})); BEAST_EXPECT( amm.expectLPTokens(bob, IOUAmount{1414213562373095, -9})); @@ -746,32 +775,62 @@ class AMMClawback_test : public jtx::AMMTest env(amm::ammClawback(gw, bob, USD, XRP, USD(10)), ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw["USD"](5000))); - env.require(balance(bob, gw["USD"](4000))); + env.require(balance(alice, USD(5000))); + env.require(balance(bob, USD(4000))); // Bob gets 20 XRP back. BEAST_EXPECT( expectLedgerEntryRoot(env, bob, bobXrpBalance + XRP(20))); - BEAST_EXPECT(amm.expectBalances( - USD(2'490), XRP(4980), IOUAmount{3521391770309006, -9})); - BEAST_EXPECT( - amm.expectLPTokens(alice, IOUAmount{7071067811865474, -10})); - BEAST_EXPECT( - amm.expectLPTokens(bob, IOUAmount{1400071426749364, -9})); + bobXrpBalance = env.balance(bob, XRP); + + if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm.expectBalances( + USD(2'490), XRP(4980), IOUAmount{3521391770309006, -9})); + else + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(2490000000000001), -12}, + XRPAmount(4980000001), + IOUAmount{3521391'770309008, -9})); + + if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{7071067811865474, -10})); + else + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{707106781186548, -9})); + + if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{1400071426749364, -9})); + else + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{1400071426749365, -9})); // gw2 clawback 200 EUR from amm2. env(amm::ammClawback(gw2, alice, EUR, XRP, EUR(200)), ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw2["EUR"](4000))); - env.require(balance(bob, gw2["EUR"](3000))); + env.require(balance(alice, EUR(4000))); + env.require(balance(bob, EUR(3000))); + + if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(600))); + else + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(600) - XRPAmount{1})); + aliceXrpBalance = env.balance(alice, XRP); + + if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm2.expectBalances( + EUR(2800), XRP(8400), IOUAmount{4849742261192856, -9})); + else + BEAST_EXPECT(amm2.expectBalances( + EUR(2800), + XRPAmount(8400000001), + IOUAmount{4849742261192856, -9})); - // Alice gets 600 XRP back. - BEAST_EXPECT(expectLedgerEntryRoot( - env, alice, aliceXrpBalance + XRP(1000) + XRP(600))); - BEAST_EXPECT(amm2.expectBalances( - EUR(2800), XRP(8400), IOUAmount{4849742261192856, -9})); BEAST_EXPECT( amm2.expectLPTokens(alice, IOUAmount{1385640646055102, -9})); BEAST_EXPECT( @@ -784,22 +843,36 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw["USD"](5000))); - env.require(balance(bob, gw["USD"](4000))); + env.require(balance(alice, USD(5000))); + env.require(balance(bob, USD(4000))); // Alice gets 1000 XRP back. - BEAST_EXPECT(expectLedgerEntryRoot( - env, - alice, - aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) - - XRPAmount{1})); + if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(1000) - XRPAmount{1})); + else + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(1000))); + aliceXrpBalance = env.balance(alice, XRP); + BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); - BEAST_EXPECT( - amm.expectLPTokens(bob, IOUAmount{1400071426749364, -9})); - BEAST_EXPECT(amm.expectBalances( - USD(1'990), - XRPAmount{3'980'000'001}, - IOUAmount{2814284989122459, -9})); + if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{1400071426749364, -9})); + else + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{1400071426749365, -9})); + + if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm.expectBalances( + USD(1'990), + XRPAmount{3'980'000'001}, + IOUAmount{2814284989122459, -9})); + else + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(1990000000000001), -12}, + XRPAmount{3'980'000'001}, + IOUAmount{2814284989122460, -9})); // gw clawback 1000 USD from bob in amm, which also exceeds bob's // balance in amm. All bob's lptoken in amm will be consumed, which @@ -808,16 +881,14 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw["USD"](5000))); - env.require(balance(bob, gw["USD"](4000))); + env.require(balance(alice, USD(5000))); + env.require(balance(bob, USD(4000))); - BEAST_EXPECT(expectLedgerEntryRoot( - env, - alice, - aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) - - XRPAmount{1})); - BEAST_EXPECT(expectLedgerEntryRoot( - env, bob, bobXrpBalance + XRP(20) + XRP(1980))); + BEAST_EXPECT(expectLedgerEntryRoot(env, alice, aliceXrpBalance)); + + BEAST_EXPECT( + expectLedgerEntryRoot(env, bob, bobXrpBalance + XRP(1980))); + bobXrpBalance = env.balance(bob, XRP); // Now neither alice nor bob has any lptoken in amm. BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); @@ -830,24 +901,28 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw2["EUR"](4000))); - env.require(balance(bob, gw2["EUR"](3000))); + env.require(balance(alice, EUR(4000))); + env.require(balance(bob, EUR(3000))); // Alice gets another 2400 XRP back, bob's XRP balance remains the // same. - BEAST_EXPECT(expectLedgerEntryRoot( - env, - alice, - aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) + XRP(2400) - - XRPAmount{1})); - BEAST_EXPECT(expectLedgerEntryRoot( - env, bob, bobXrpBalance + XRP(20) + XRP(1980))); + BEAST_EXPECT( + expectLedgerEntryRoot(env, alice, aliceXrpBalance + XRP(2400))); + + BEAST_EXPECT(expectLedgerEntryRoot(env, bob, bobXrpBalance)); + aliceXrpBalance = env.balance(alice, XRP); // Alice now does not have any lptoken in amm2 BEAST_EXPECT(amm2.expectLPTokens(alice, IOUAmount(0))); - BEAST_EXPECT(amm2.expectBalances( - EUR(2000), XRP(6000), IOUAmount{3464101615137754, -9})); + if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm2.expectBalances( + EUR(2000), XRP(6000), IOUAmount{3464101615137754, -9})); + else + BEAST_EXPECT(amm2.expectBalances( + EUR(2000), + XRPAmount(6000000001), + IOUAmount{3464101615137754, -9})); // gw2 claw back 2000 EUR from bob in amm2, which exceeds bob's // balance. All bob's lptokens will be consumed, which corresponds @@ -856,25 +931,29 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw2["EUR"](4000))); - env.require(balance(bob, gw2["EUR"](3000))); + env.require(balance(alice, EUR(4000))); + env.require(balance(bob, EUR(3000))); // Bob gets another 3000 XRP back. Alice's XRP balance remains the // same. - BEAST_EXPECT(expectLedgerEntryRoot( - env, - alice, - aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) + XRP(2400) - - XRPAmount{1})); - BEAST_EXPECT(expectLedgerEntryRoot( - env, bob, bobXrpBalance + XRP(20) + XRP(1980) + XRP(3000))); + BEAST_EXPECT(expectLedgerEntryRoot(env, alice, aliceXrpBalance)); + + BEAST_EXPECT( + expectLedgerEntryRoot(env, bob, bobXrpBalance + XRP(3000))); + bobXrpBalance = env.balance(bob, XRP); // Neither alice nor bob has any lptoken in amm2 BEAST_EXPECT(amm2.expectLPTokens(alice, IOUAmount(0))); BEAST_EXPECT(amm2.expectLPTokens(bob, IOUAmount(0))); - BEAST_EXPECT(amm2.expectBalances( - EUR(1000), XRP(3000), IOUAmount{1732050807568877, -9})); + if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm2.expectBalances( + EUR(1000), XRP(3000), IOUAmount{1732050807568877, -9})); + else + BEAST_EXPECT(amm2.expectBalances( + EUR(1000), + XRPAmount(3000000001), + IOUAmount{1732050807568877, -9})); } } @@ -948,12 +1027,12 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT( amm.expectLPTokens(carol, IOUAmount{1118033988749894, -12})); - env.require(balance(alice, gw["USD"](2000))); - env.require(balance(alice, gw2["EUR"](1000))); - env.require(balance(bob, gw["USD"](3000))); - env.require(balance(bob, gw2["EUR"](2500))); - env.require(balance(carol, gw["USD"](3000))); - env.require(balance(carol, gw2["EUR"](2750))); + env.require(balance(alice, USD(2000))); + env.require(balance(alice, EUR(1000))); + env.require(balance(bob, USD(3000))); + env.require(balance(bob, EUR(2500))); + env.require(balance(carol, USD(3000))); + env.require(balance(carol, EUR(2750))); // gw clawback all the bob's USD in amm. (2000 USD / 2500 EUR) env(amm::ammClawback(gw, bob, USD, EUR, std::nullopt), @@ -972,8 +1051,8 @@ class AMMClawback_test : public jtx::AMMTest amm.expectLPTokens(carol, IOUAmount{1118033988749894, -12})); // Bob will get 2500 EUR back. - env.require(balance(alice, gw["USD"](2000))); - env.require(balance(alice, gw2["EUR"](1000))); + env.require(balance(alice, USD(2000))); + env.require(balance(alice, EUR(1000))); BEAST_EXPECT( env.balance(bob, USD) == STAmount(USD, UINT64_C(3000000000000000), -12)); @@ -981,8 +1060,8 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT( env.balance(bob, EUR) == STAmount(EUR, UINT64_C(4999999999999999), -12)); - env.require(balance(carol, gw["USD"](3000))); - env.require(balance(carol, gw2["EUR"](2750))); + env.require(balance(carol, USD(3000))); + env.require(balance(carol, EUR(2750))); // gw2 clawback all carol's EUR in amm. (1000 USD / 1250 EUR) env(amm::ammClawback(gw2, carol, EUR, USD, std::nullopt), @@ -1003,8 +1082,8 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); - env.require(balance(carol, gw2["EUR"](2750))); - env.require(balance(carol, gw["USD"](4000))); + env.require(balance(carol, EUR(2750))); + env.require(balance(carol, USD(4000))); BEAST_EXPECT(!amm.ammExists()); } @@ -1351,11 +1430,20 @@ class AMMClawback_test : public jtx::AMMTest // gw claws back 1000 USD from gw2. env(amm::ammClawback(gw, gw2, USD, EUR, USD(1000)), ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - USD(5000), EUR(10000), IOUAmount{7071067811865475, -12})); + if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm.expectBalances( + USD(5000), EUR(10000), IOUAmount{7071067811865475, -12})); + else + BEAST_EXPECT(amm.expectBalances( + USD(5000), EUR(10000), IOUAmount{7071067811865474, -12})); BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{1414213562373095, -12})); - BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{1414213562373095, -12})); + if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT( + amm.expectLPTokens(gw2, IOUAmount{1414213562373095, -12})); + else + BEAST_EXPECT( + amm.expectLPTokens(gw2, IOUAmount{1414213562373094, -12})); BEAST_EXPECT( amm.expectLPTokens(alice, IOUAmount{4242640687119285, -12})); @@ -1367,11 +1455,29 @@ class AMMClawback_test : public jtx::AMMTest // gw2 claws back 1000 EUR from gw. env(amm::ammClawback(gw2, gw, EUR, USD, EUR(1000)), ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - USD(4500), EUR(9000), IOUAmount{6363961030678928, -12})); + if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm.expectBalances( + USD(4500), EUR(9000), IOUAmount{6363961030678928, -12})); + else + BEAST_EXPECT(amm.expectBalances( + USD(4500), + STAmount(EUR, UINT64_C(9000000000000001), -12), + IOUAmount{6363961030678927, -12})); + + if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT( + amm.expectLPTokens(gw, IOUAmount{7071067811865475, -13})); + else + BEAST_EXPECT( + amm.expectLPTokens(gw, IOUAmount{7071067811865480, -13})); + + if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT( + amm.expectLPTokens(gw2, IOUAmount{1414213562373095, -12})); + else + BEAST_EXPECT( + amm.expectLPTokens(gw2, IOUAmount{1414213562373094, -12})); - BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{7071067811865475, -13})); - BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{1414213562373095, -12})); BEAST_EXPECT( amm.expectLPTokens(alice, IOUAmount{4242640687119285, -12})); @@ -1383,11 +1489,28 @@ class AMMClawback_test : public jtx::AMMTest // gw2 claws back 4000 EUR from alice. env(amm::ammClawback(gw2, alice, EUR, USD, EUR(4000)), ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - USD(2500), EUR(5000), IOUAmount{3535533905932738, -12})); + if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT(amm.expectBalances( + USD(2500), EUR(5000), IOUAmount{3535533905932738, -12})); + else + BEAST_EXPECT(amm.expectBalances( + USD(2500), + STAmount(EUR, UINT64_C(5000000000000001), -12), + IOUAmount{3535533905932737, -12})); + + if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT( + amm.expectLPTokens(gw, IOUAmount{7071067811865475, -13})); + else + BEAST_EXPECT( + amm.expectLPTokens(gw, IOUAmount{7071067811865480, -13})); - BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{7071067811865475, -13})); - BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{1414213562373095, -12})); + if (!features[fixAMMClawbackRounding]) + BEAST_EXPECT( + amm.expectLPTokens(gw2, IOUAmount{1414213562373095, -12})); + else + BEAST_EXPECT( + amm.expectLPTokens(gw2, IOUAmount{1414213562373094, -12})); BEAST_EXPECT( amm.expectLPTokens(alice, IOUAmount{1414213562373095, -12})); @@ -1454,14 +1577,14 @@ class AMMClawback_test : public jtx::AMMTest env.trust(USD(100000), alice); env(pay(gw, alice, USD(3000))); env.close(); - env.require(balance(alice, gw["USD"](3000))); + env.require(balance(alice, USD(3000))); // gw2 issues 3000 EUR to Alice. auto const EUR = gw2["EUR"]; env.trust(EUR(100000), alice); env(pay(gw2, alice, EUR(3000))); env.close(); - env.require(balance(alice, gw2["EUR"](3000))); + env.require(balance(alice, EUR(3000))); // Alice creates AMM pool of EUR/USD. AMM amm(env, alice, EUR(1000), USD(2000), ter(tesSUCCESS)); @@ -1479,8 +1602,8 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw["USD"](1000))); - env.require(balance(alice, gw2["EUR"](2500))); + env.require(balance(alice, USD(1000))); + env.require(balance(alice, EUR(2500))); BEAST_EXPECT(amm.expectBalances( USD(1000), EUR(500), IOUAmount{7071067811865475, -13})); @@ -1496,8 +1619,8 @@ class AMMClawback_test : public jtx::AMMTest // Alice should still has 1000 USD because gw clawed back from the // AMM pool. - env.require(balance(alice, gw["USD"](1000))); - env.require(balance(alice, gw2["EUR"](3000))); + env.require(balance(alice, USD(1000))); + env.require(balance(alice, EUR(3000))); // amm is automatically deleted. BEAST_EXPECT(!amm.ammExists()); @@ -1522,14 +1645,14 @@ class AMMClawback_test : public jtx::AMMTest env.trust(USD(100000), alice); env(pay(gw, alice, USD(3000))); env.close(); - env.require(balance(alice, gw["USD"](3000))); + env.require(balance(alice, USD(3000))); // gw2 issues 3000 EUR to Alice. auto const EUR = gw2["EUR"]; env.trust(EUR(100000), alice); env(pay(gw2, alice, EUR(3000))); env.close(); - env.require(balance(alice, gw2["EUR"](3000))); + env.require(balance(alice, EUR(3000))); // Alice creates AMM pool of EUR/USD. AMM amm(env, alice, EUR(1000), USD(2000), ter(tesSUCCESS)); @@ -1548,8 +1671,8 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw["USD"](1000))); - env.require(balance(alice, gw2["EUR"](2500))); + env.require(balance(alice, USD(1000))); + env.require(balance(alice, EUR(2500))); BEAST_EXPECT(amm.expectBalances( USD(1000), EUR(500), IOUAmount{7071067811865475, -13})); BEAST_EXPECT( @@ -1575,14 +1698,14 @@ class AMMClawback_test : public jtx::AMMTest env.trust(USD(100000), alice); env(pay(gw, alice, USD(3000))); env.close(); - env.require(balance(alice, gw["USD"](3000))); + env.require(balance(alice, USD(3000))); // gw2 issues 3000 EUR to Alice. auto const EUR = gw2["EUR"]; env.trust(EUR(100000), alice); env(pay(gw2, alice, EUR(3000))); env.close(); - env.require(balance(alice, gw2["EUR"](3000))); + env.require(balance(alice, EUR(3000))); // Alice creates AMM pool of EUR/USD. AMM amm(env, alice, EUR(1000), USD(2000), ter(tesSUCCESS)); @@ -1600,8 +1723,8 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw["USD"](1000))); - env.require(balance(alice, gw2["EUR"](2500))); + env.require(balance(alice, USD(1000))); + env.require(balance(alice, EUR(2500))); BEAST_EXPECT(amm.expectBalances( USD(1000), EUR(500), IOUAmount{7071067811865475, -13})); BEAST_EXPECT( @@ -1728,10 +1851,17 @@ class AMMClawback_test : public jtx::AMMTest { testcase("test single depoit and clawback"); using namespace jtx; + std::string logs; // Test AMMClawback for USD/XRP pool. Claw back USD, and XRP goes back // to the holder. - Env env(*this, features); + // Env env(*this, features, std::make_unique(&logs)); + Env env( + *this, + envconfig(), + features, + nullptr, + beast::severities::kDisabled); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(1000000000), gw, alice); @@ -1747,7 +1877,7 @@ class AMMClawback_test : public jtx::AMMTest env.trust(USD(100000), alice); env(pay(gw, alice, USD(1000))); env.close(); - env.require(balance(alice, gw["USD"](1000))); + env.require(balance(alice, USD(1000))); // gw creates AMM pool of XRP/USD. AMM amm(env, gw, XRP(100), USD(400), ter(tesSUCCESS)); @@ -1775,21 +1905,362 @@ class AMMClawback_test : public jtx::AMMTest env, alice, aliceXrpBalance + XRP(29.289321))); } + void + testLastHolderLPTokenBalance(FeatureBitset features) + { + testcase( + "test last holder's lptoken balance not equal to AMM's lptoken " + "balance before clawback"); + using namespace jtx; + std::string logs; + + auto setupAccounts = + [&](Env& env, Account& gw, Account& alice, Account& bob) { + env.fund(XRP(100000), gw, alice, bob); + env.close(); + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + + auto const USD = gw["USD"]; + env.trust(USD(100000), alice); + env(pay(gw, alice, USD(50000))); + env.trust(USD(100000), bob); + env(pay(gw, bob, USD(40000))); + env.close(); + + return USD; + }; + + auto getLPTokenBalances = + [&](auto& env, + auto const& amm, + auto const& account) -> std::pair { + auto const lpToken = + getAccountLines( + env, account, amm.lptIssue())[jss::lines][0u][jss::balance] + .asString(); + auto const lpTokenBalance = + amm.ammRpcInfo()[jss::amm][jss::lp_token][jss::value] + .asString(); + return {lpToken, lpTokenBalance}; + }; + + // IOU/XRP pool. AMMClawback almost last holder's USD balance + { + // Env env(*this, features, std::make_unique(&logs)); + Env env( + *this, + envconfig(), + features, + nullptr, + beast::severities::kDisabled); + Account gw{"gateway"}, alice{"alice"}, bob{"bob"}; + auto const USD = setupAccounts(env, gw, alice, bob); + + AMM amm(env, alice, XRP(2), USD(1)); + amm.deposit(alice, IOUAmount{1'876123487565916, -15}); + amm.deposit(bob, IOUAmount{1'000'000}); + amm.withdraw(alice, IOUAmount{1'876123487565916, -15}); + amm.withdrawAll(bob); + + auto [lpToken, lpTokenBalance] = + getLPTokenBalances(env, amm, alice); + BEAST_EXPECT( + lpToken == "1414.21356237366" && + lpTokenBalance == "1414.213562374"); + + auto res = + isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice); + BEAST_EXPECT(res && res.value()); + + if (!features[fixAMMClawbackRounding]) + { + env(amm::ammClawback(gw, alice, USD, XRP, USD(1)), + ter(tecAMM_BALANCE)); + BEAST_EXPECT(amm.ammExists()); + } + else + { + auto const lpBalance = IOUAmount{989, -12}; + env(amm::ammClawback(gw, alice, USD, XRP, USD(1))); + BEAST_EXPECT(amm.expectBalances( + STAmount(USD, UINT64_C(7000000000000000), -28), + XRPAmount(1), + lpBalance)); + BEAST_EXPECT(amm.expectLPTokens(alice, lpBalance)); + } + } + + // IOU/XRP pool. AMMClawback part of last holder's USD balance + { + // Env env(*this, features, std::make_unique(&logs)); + Env env( + *this, + envconfig(), + features, + nullptr, + beast::severities::kDisabled); + Account gw{"gateway"}, alice{"alice"}, bob{"bob"}; + auto const USD = setupAccounts(env, gw, alice, bob); + + AMM amm(env, alice, XRP(2), USD(1)); + amm.deposit(alice, IOUAmount{1'876123487565916, -15}); + amm.deposit(bob, IOUAmount{1'000'000}); + amm.withdrawAll(bob); + + auto [lpToken, lpTokenBalance] = + getLPTokenBalances(env, amm, alice); + BEAST_EXPECT( + lpToken == "1416.08968586066" && + lpTokenBalance == "1416.089685861"); + + auto res = + isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice); + BEAST_EXPECT(res && res.value()); + + env(amm::ammClawback(gw, alice, USD, XRP, USD(0.5))); + + if (!features[fixAMMClawbackRounding]) + { + BEAST_EXPECT(amm.expectBalances( + STAmount(USD, UINT64_C(5013266196407), -13), + XRPAmount(1002654), + IOUAmount{708'9829046744941, -13})); + } + else + { + auto const lpBalance = IOUAmount{708'9829046743238, -13}; + BEAST_EXPECT(amm.expectBalances( + STAmount(USD, UINT64_C(5013266196406999), -16), + XRPAmount(1002655), + lpBalance)); + BEAST_EXPECT(amm.expectLPTokens(alice, lpBalance)); + } + } + + // IOU/XRP pool. AMMClawback all of last holder's USD balance + { + // Env env(*this, features, std::make_unique(&logs)); + Env env( + *this, + envconfig(), + features, + nullptr, + beast::severities::kDisabled); + Account gw{"gateway"}, alice{"alice"}, bob{"bob"}; + auto const USD = setupAccounts(env, gw, alice, bob); + + AMM amm(env, alice, XRP(2), USD(1)); + amm.deposit(alice, IOUAmount{1'876123487565916, -15}); + amm.deposit(bob, IOUAmount{1'000'000}); + amm.withdraw(alice, IOUAmount{1'876123487565916, -15}); + amm.withdrawAll(bob); + + auto [lpToken, lpTokenBalance] = + getLPTokenBalances(env, amm, alice); + BEAST_EXPECT( + lpToken == "1414.21356237366" && + lpTokenBalance == "1414.213562374"); + + auto res = + isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice); + BEAST_EXPECT(res && res.value()); + + if (!features[fixAMMClawbackRounding]) + { + env(amm::ammClawback(gw, alice, USD, XRP, std::nullopt)); + BEAST_EXPECT(amm.expectBalances( + STAmount(USD, UINT64_C(2410000000000000), -28), + XRPAmount(1), + IOUAmount{34, -11})); + } + else + { + env(amm::ammClawback(gw, alice, USD, XRP, std::nullopt)); + BEAST_EXPECT(!amm.ammExists()); + } + } + + // IOU/IOU pool, different issuers + { + // Env env(*this, features, std::make_unique(&logs)); + Env env( + *this, + envconfig(), + features, + nullptr, + beast::severities::kDisabled); + Account gw{"gateway"}, alice{"alice"}, bob{"bob"}; + auto const USD = setupAccounts(env, gw, alice, bob); + + Account gw2{"gateway2"}; + env.fund(XRP(100000), gw2); + env.close(); + auto const EUR = gw2["EUR"]; + env.trust(EUR(100000), alice); + env(pay(gw2, alice, EUR(50000))); + env.trust(EUR(100000), bob); + env(pay(gw2, bob, EUR(50000))); + env.close(); + + AMM amm(env, alice, USD(2), EUR(1)); + amm.deposit(alice, IOUAmount{1'576123487565916, -15}); + amm.deposit(bob, IOUAmount{1'000}); + amm.withdraw(alice, IOUAmount{1'576123487565916, -15}); + amm.withdrawAll(bob); + + auto [lpToken, lpTokenBalance] = + getLPTokenBalances(env, amm, alice); + BEAST_EXPECT( + lpToken == "1.414213562374011" && + lpTokenBalance == "1.414213562374"); + + auto res = + isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice); + BEAST_EXPECT(res && res.value()); + + if (features[fixAMMClawbackRounding]) + { + env(amm::ammClawback(gw, alice, USD, EUR, std::nullopt)); + BEAST_EXPECT(!amm.ammExists()); + } + else + { + env(amm::ammClawback(gw, alice, USD, EUR, std::nullopt), + ter(tecINTERNAL)); + BEAST_EXPECT(amm.ammExists()); + } + } + + // IOU/IOU pool, same issuer + { + // Env env(*this, features, std::make_unique(&logs)); + Env env( + *this, + envconfig(), + features, + nullptr, + beast::severities::kDisabled); + Account gw{"gateway"}, alice{"alice"}, bob{"bob"}; + auto const USD = setupAccounts(env, gw, alice, bob); + + auto const EUR = gw["EUR"]; + env.trust(EUR(100000), alice); + env(pay(gw, alice, EUR(50000))); + env.trust(EUR(100000), bob); + env(pay(gw, bob, EUR(50000))); + env.close(); + + AMM amm(env, alice, USD(1), EUR(2)); + amm.deposit(alice, IOUAmount{1'076123487565916, -15}); + amm.deposit(bob, IOUAmount{1'000}); + amm.withdraw(alice, IOUAmount{1'076123487565916, -15}); + amm.withdrawAll(bob); + + auto [lpToken, lpTokenBalance] = + getLPTokenBalances(env, amm, alice); + BEAST_EXPECT( + lpToken == "1.414213562374011" && + lpTokenBalance == "1.414213562374"); + + auto res = + isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice); + BEAST_EXPECT(res && res.value()); + + if (features[fixAMMClawbackRounding]) + { + env(amm::ammClawback(gw, alice, USD, EUR, std::nullopt), + txflags(tfClawTwoAssets)); + BEAST_EXPECT(!amm.ammExists()); + } + else + { + env(amm::ammClawback(gw, alice, USD, EUR, std::nullopt), + txflags(tfClawTwoAssets), + ter(tecINTERNAL)); + BEAST_EXPECT(amm.ammExists()); + } + } + + // IOU/IOU pool, larger asset ratio + { + // Env env(*this, features, std::make_unique(&logs)); + Env env( + *this, + envconfig(), + features, + nullptr, + beast::severities::kDisabled); + Account gw{"gateway"}, alice{"alice"}, bob{"bob"}; + auto const USD = setupAccounts(env, gw, alice, bob); + + auto const EUR = gw["EUR"]; + env.trust(EUR(1000000000), alice); + env(pay(gw, alice, EUR(500000000))); + env.trust(EUR(1000000000), bob); + env(pay(gw, bob, EUR(500000000))); + env.close(); + + AMM amm(env, alice, USD(1), EUR(2000000)); + amm.deposit(alice, IOUAmount{1'076123487565916, -12}); + amm.deposit(bob, IOUAmount{10000}); + amm.withdraw(alice, IOUAmount{1'076123487565916, -12}); + amm.withdrawAll(bob); + + auto [lpToken, lpTokenBalance] = + getLPTokenBalances(env, amm, alice); + + BEAST_EXPECT( + lpToken == "1414.213562373101" && + lpTokenBalance == "1414.2135623731"); + + auto res = + isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice); + BEAST_EXPECT(res && res.value()); + + if (!features[fixAMMClawbackRounding]) + { + // sqrt(amount * amount2) >= LPTokens and exceeds the allowed + // tolerance + env(amm::ammClawback(gw, alice, USD, EUR, USD(1)), + ter(tecINVARIANT_FAILED)); + BEAST_EXPECT(amm.ammExists()); + } + else + { + env(amm::ammClawback(gw, alice, USD, EUR, USD(1)), + txflags(tfClawTwoAssets)); + auto const lpBalance = IOUAmount{5, -12}; + BEAST_EXPECT(amm.expectBalances( + STAmount(USD, UINT64_C(4), -15), + STAmount(EUR, UINT64_C(8), -9), + lpBalance)); + BEAST_EXPECT(amm.expectLPTokens(alice, lpBalance)); + } + } + } + void run() override { - FeatureBitset const all{jtx::supported_amendments()}; - testInvalidRequest(all); + FeatureBitset const all{ + jtx::supported_amendments() | fixAMMClawbackRounding}; + + testInvalidRequest(); testFeatureDisabled(all - featureAMMClawback); - testAMMClawbackSpecificAmount(all); - testAMMClawbackExceedBalance(all); - testAMMClawbackAll(all); - testAMMClawbackSameIssuerAssets(all); - testAMMClawbackSameCurrency(all); - testAMMClawbackIssuesEachOther(all); - testNotHoldingLptoken(all); - testAssetFrozen(all); - testSingleDepositAndClawback(all); + for (auto const& features : {all - fixAMMClawbackRounding, all}) + { + testAMMClawbackSpecificAmount(features); + testAMMClawbackExceedBalance(features); + testAMMClawbackAll(features); + testAMMClawbackSameIssuerAssets(features); + testAMMClawbackSameCurrency(features); + testAMMClawbackIssuesEachOther(features); + testNotHoldingLptoken(features); + testAssetFrozen(features); + testSingleDepositAndClawback(features); + testLastHolderLPTokenBalance(features); + } } }; BEAST_DEFINE_TESTSUITE(AMMClawback, app, ripple); diff --git a/src/xrpld/app/misc/AMMUtils.h b/src/xrpld/app/misc/AMMUtils.h index 52fe819a28..a3d6ba39e8 100644 --- a/src/xrpld/app/misc/AMMUtils.h +++ b/src/xrpld/app/misc/AMMUtils.h @@ -123,6 +123,17 @@ isOnlyLiquidityProvider( Issue const& ammIssue, AccountID const& lpAccount); +/** Due to rounding, the LPTokenBalance of the last LP might + * not match the LP's trustline balance. If it's within the tolerance, + * update LPTokenBalance to match the LP's trustline balance. + */ +Expected +verifyAndAdjustLPTokenBalance( + Sandbox& sb, + STAmount const& lpTokens, + std::shared_ptr& ammSle, + AccountID const& account); + } // namespace ripple #endif // RIPPLE_APP_MISC_AMMUTILS_H_INLCUDED diff --git a/src/xrpld/app/misc/detail/AMMUtils.cpp b/src/xrpld/app/misc/detail/AMMUtils.cpp index 10a918098e..8a6917b8cc 100644 --- a/src/xrpld/app/misc/detail/AMMUtils.cpp +++ b/src/xrpld/app/misc/detail/AMMUtils.cpp @@ -16,6 +16,8 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ //============================================================================== + +#include #include #include #include @@ -462,4 +464,32 @@ isOnlyLiquidityProvider( return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE } +Expected +verifyAndAdjustLPTokenBalance( + Sandbox& sb, + STAmount const& lpTokens, + std::shared_ptr& ammSle, + AccountID const& account) +{ + if (auto const res = isOnlyLiquidityProvider(sb, lpTokens.issue(), account); + !res) + return Unexpected(res.error()); + else if (res.value()) + { + if (withinRelativeDistance( + lpTokens, + ammSle->getFieldAmount(sfLPTokenBalance), + Number{1, -3})) + { + ammSle->setFieldAmount(sfLPTokenBalance, lpTokens); + sb.update(ammSle); + } + else + { + return Unexpected(tecAMM_INVALID_TOKENS); + } + } + return true; +} + } // namespace ripple diff --git a/src/xrpld/app/tx/detail/AMMClawback.cpp b/src/xrpld/app/tx/detail/AMMClawback.cpp index 162224ff91..43b61b8552 100644 --- a/src/xrpld/app/tx/detail/AMMClawback.cpp +++ b/src/xrpld/app/tx/detail/AMMClawback.cpp @@ -151,6 +151,20 @@ AMMClawback::applyGuts(Sandbox& sb) if (!accountSle) return tecINTERNAL; // LCOV_EXCL_LINE + if (sb.rules().enabled(fixAMMClawbackRounding)) + { + // retrieve LP token balance inside the amendment gate to avoid + // inconsistent error behavior + auto const lpTokenBalance = ammLPHolds(sb, *ammSle, holder, j_); + if (lpTokenBalance == beast::zero) + return tecAMM_BALANCE; + + if (auto const res = verifyAndAdjustLPTokenBalance( + sb, lpTokenBalance, ammSle, holder); + !res) + return res.error(); // LCOV_EXCL_LINE + } + auto const expected = ammHolds( sb, *ammSle, @@ -248,10 +262,11 @@ AMMClawback::equalWithdrawMatchingOneAmount( STAmount const& amount) { auto frac = Number{amount} / amountBalance; - auto const amount2Withdraw = amount2Balance * frac; + auto amount2Withdraw = amount2Balance * frac; auto const lpTokensWithdraw = toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac); + if (lpTokensWithdraw > holdLPtokens) // if lptoken balance less than what the issuer intended to clawback, // clawback all the tokens. Because we are doing a two-asset withdrawal, @@ -272,6 +287,42 @@ AMMClawback::equalWithdrawMatchingOneAmount( mPriorBalance, ctx_.journal); + auto const& rules = sb.rules(); + if (rules.enabled(fixAMMClawbackRounding)) + { + auto tokensAdj = + getRoundedLPTokens(rules, lptAMMBalance, frac, IsDeposit::No); + + // LCOV_EXCL_START + if (tokensAdj == beast::zero) + return { + tecAMM_INVALID_TOKENS, STAmount{}, STAmount{}, std::nullopt}; + // LCOV_EXCL_STOP + + frac = adjustFracByTokens(rules, lptAMMBalance, tokensAdj, frac); + auto amount2Rounded = + getRoundedAsset(rules, amount2Balance, frac, IsDeposit::No); + + auto amountRounded = + getRoundedAsset(rules, amountBalance, frac, IsDeposit::No); + + return AMMWithdraw::withdraw( + sb, + ammSle, + ammAccount, + holder, + amountBalance, + amountRounded, + amount2Rounded, + lptAMMBalance, + tokensAdj, + 0, + FreezeHandling::fhIGNORE_FREEZE, + WithdrawAll::No, + mPriorBalance, + ctx_.journal); + } + // Because we are doing a two-asset withdrawal, // tfee is actually not used, so pass tfee as 0. return AMMWithdraw::withdraw( diff --git a/src/xrpld/app/tx/detail/AMMWithdraw.cpp b/src/xrpld/app/tx/detail/AMMWithdraw.cpp index e8569b48c2..c6a464f21b 100644 --- a/src/xrpld/app/tx/detail/AMMWithdraw.cpp +++ b/src/xrpld/app/tx/detail/AMMWithdraw.cpp @@ -313,24 +313,9 @@ AMMWithdraw::applyGuts(Sandbox& sb) // might not match the LP's trustline balance if (auto const res = - isOnlyLiquidityProvider(sb, lpTokens.issue(), account_); + verifyAndAdjustLPTokenBalance(sb, lpTokens, ammSle, account_); !res) return {res.error(), false}; - else if (res.value()) - { - if (withinRelativeDistance( - lpTokens, - ammSle->getFieldAmount(sfLPTokenBalance), - Number{1, -3})) - { - ammSle->setFieldAmount(sfLPTokenBalance, lpTokens); - sb.update(ammSle); - } - else - { - return {tecAMM_INVALID_TOKENS, false}; - } - } auto const tfee = getTradingFee(ctx_.view(), *ammSle, account_); From 8cfee6c8a318a2476199374aa7f46aef0b4e50ee Mon Sep 17 00:00:00 2001 From: tequ Date: Thu, 19 Feb 2026 13:51:18 +0900 Subject: [PATCH 04/17] Merge fixAMMClawbackRounding amendment into featureAMMClawback amendment --- include/xrpl/protocol/detail/features.macro | 1 - src/test/app/AMMClawback_test.cpp | 349 ++++++-------------- src/xrpld/app/tx/detail/AMMClawback.cpp | 72 ++-- 3 files changed, 119 insertions(+), 303 deletions(-) diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 738a8287b2..3f48ac7145 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -31,7 +31,6 @@ // If you add an amendment here, then do not forget to increment `numFeatures` // in include/xrpl/protocol/Feature.h. -XRPL_FIX (AMMClawbackRounding, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(HookAPISerializedType240, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(PermissionedDomains, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(DynamicNFT, Supported::no, VoteBehavior::DefaultNo) diff --git a/src/test/app/AMMClawback_test.cpp b/src/test/app/AMMClawback_test.cpp index 4fec924f91..d2ad29003f 100644 --- a/src/test/app/AMMClawback_test.cpp +++ b/src/test/app/AMMClawback_test.cpp @@ -629,21 +629,14 @@ class AMMClawback_test : public beast::unit_test::suite // Another 1 USD / 1.25 EUR was withdrawn. env.require(balance(alice, USD(2000))); - if (!features[fixAMMClawbackRounding]) - BEAST_EXPECT(amm.expectBalances( - USD(2499), EUR(3123.75), IOUAmount{2793966937885987, -12})); - else - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(2499000000000001), -12}, - STAmount{EUR, UINT64_C(3123750000000001), -12}, - IOUAmount{2793966937885988, -12})); - - if (!features[fixAMMClawbackRounding]) - BEAST_EXPECT(env.balance(alice, EUR) == EUR(2876.25)); - else - BEAST_EXPECT( - env.balance(alice, EUR) == - STAmount(EUR, UINT64_C(2876'249999999999), -12)); + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(2499000000000001), -12}, + STAmount{EUR, UINT64_C(3123750000000001), -12}, + IOUAmount{2793966937885988, -12})); + + BEAST_EXPECT( + env.balance(alice, EUR) == + STAmount(EUR, UINT64_C(2876'249999999999), -12)); // gw clawback 4000 USD, exceeding the current balance. We // will clawback all. @@ -744,29 +737,17 @@ class AMMClawback_test : public beast::unit_test::suite env.require(balance(bob, USD(4000))); // Alice gets 1000 XRP back. - if (features[fixAMMClawbackRounding]) - BEAST_EXPECT(expectLedgerEntryRoot( - env, alice, aliceXrpBalance + XRP(1000) - XRPAmount(1))); - else - BEAST_EXPECT(expectLedgerEntryRoot( - env, alice, aliceXrpBalance + XRP(1000))); + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(1000) - XRPAmount(1))); aliceXrpBalance = env.balance(alice, XRP); - if (!features[fixAMMClawbackRounding]) - BEAST_EXPECT(amm.expectBalances( - USD(2500), XRP(5000), IOUAmount{3535533905932737, -9})); - else - BEAST_EXPECT(amm.expectBalances( - USD(2500), - XRPAmount(5000000001), - IOUAmount{3'535'533'905932738, -9})); - - if (!features[fixAMMClawbackRounding]) - BEAST_EXPECT(amm.expectLPTokens( - alice, IOUAmount{7071067811865474, -10})); - else - BEAST_EXPECT( - amm.expectLPTokens(alice, IOUAmount{707106781186548, -9})); + BEAST_EXPECT(amm.expectBalances( + USD(2500), + XRPAmount(5000000001), + IOUAmount{3'535'533'905932738, -9})); + + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{707106781186548, -9})); BEAST_EXPECT( amm.expectLPTokens(bob, IOUAmount{1414213562373095, -9})); @@ -783,28 +764,16 @@ class AMMClawback_test : public beast::unit_test::suite expectLedgerEntryRoot(env, bob, bobXrpBalance + XRP(20))); bobXrpBalance = env.balance(bob, XRP); - if (!features[fixAMMClawbackRounding]) - BEAST_EXPECT(amm.expectBalances( - USD(2'490), XRP(4980), IOUAmount{3521391770309006, -9})); - else - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(2490000000000001), -12}, - XRPAmount(4980000001), - IOUAmount{3521391'770309008, -9})); - - if (!features[fixAMMClawbackRounding]) - BEAST_EXPECT(amm.expectLPTokens( - alice, IOUAmount{7071067811865474, -10})); - else - BEAST_EXPECT( - amm.expectLPTokens(alice, IOUAmount{707106781186548, -9})); - - if (!features[fixAMMClawbackRounding]) - BEAST_EXPECT( - amm.expectLPTokens(bob, IOUAmount{1400071426749364, -9})); - else - BEAST_EXPECT( - amm.expectLPTokens(bob, IOUAmount{1400071426749365, -9})); + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(2490000000000001), -12}, + XRPAmount(4980000001), + IOUAmount{3521391'770309008, -9})); + + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{707106781186548, -9})); + + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{1400071426749365, -9})); // gw2 clawback 200 EUR from amm2. env(amm::ammClawback(gw2, alice, EUR, XRP, EUR(200)), @@ -814,22 +783,14 @@ class AMMClawback_test : public beast::unit_test::suite env.require(balance(alice, EUR(4000))); env.require(balance(bob, EUR(3000))); - if (!features[fixAMMClawbackRounding]) - BEAST_EXPECT(expectLedgerEntryRoot( - env, alice, aliceXrpBalance + XRP(600))); - else - BEAST_EXPECT(expectLedgerEntryRoot( - env, alice, aliceXrpBalance + XRP(600) - XRPAmount{1})); + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(600) - XRPAmount{1})); aliceXrpBalance = env.balance(alice, XRP); - if (!features[fixAMMClawbackRounding]) - BEAST_EXPECT(amm2.expectBalances( - EUR(2800), XRP(8400), IOUAmount{4849742261192856, -9})); - else - BEAST_EXPECT(amm2.expectBalances( - EUR(2800), - XRPAmount(8400000001), - IOUAmount{4849742261192856, -9})); + BEAST_EXPECT(amm2.expectBalances( + EUR(2800), + XRPAmount(8400000001), + IOUAmount{4849742261192856, -9})); BEAST_EXPECT( amm2.expectLPTokens(alice, IOUAmount{1385640646055102, -9})); @@ -847,32 +808,18 @@ class AMMClawback_test : public beast::unit_test::suite env.require(balance(bob, USD(4000))); // Alice gets 1000 XRP back. - if (!features[fixAMMClawbackRounding]) - BEAST_EXPECT(expectLedgerEntryRoot( - env, alice, aliceXrpBalance + XRP(1000) - XRPAmount{1})); - else - BEAST_EXPECT(expectLedgerEntryRoot( - env, alice, aliceXrpBalance + XRP(1000))); + BEAST_EXPECT( + expectLedgerEntryRoot(env, alice, aliceXrpBalance + XRP(1000))); aliceXrpBalance = env.balance(alice, XRP); BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); - if (!features[fixAMMClawbackRounding]) - BEAST_EXPECT( - amm.expectLPTokens(bob, IOUAmount{1400071426749364, -9})); - else - BEAST_EXPECT( - amm.expectLPTokens(bob, IOUAmount{1400071426749365, -9})); - - if (!features[fixAMMClawbackRounding]) - BEAST_EXPECT(amm.expectBalances( - USD(1'990), - XRPAmount{3'980'000'001}, - IOUAmount{2814284989122459, -9})); - else - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(1990000000000001), -12}, - XRPAmount{3'980'000'001}, - IOUAmount{2814284989122460, -9})); + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{1400071426749365, -9})); + + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(1990000000000001), -12}, + XRPAmount{3'980'000'001}, + IOUAmount{2814284989122460, -9})); // gw clawback 1000 USD from bob in amm, which also exceeds bob's // balance in amm. All bob's lptoken in amm will be consumed, which @@ -915,14 +862,10 @@ class AMMClawback_test : public beast::unit_test::suite // Alice now does not have any lptoken in amm2 BEAST_EXPECT(amm2.expectLPTokens(alice, IOUAmount(0))); - if (!features[fixAMMClawbackRounding]) - BEAST_EXPECT(amm2.expectBalances( - EUR(2000), XRP(6000), IOUAmount{3464101615137754, -9})); - else - BEAST_EXPECT(amm2.expectBalances( - EUR(2000), - XRPAmount(6000000001), - IOUAmount{3464101615137754, -9})); + BEAST_EXPECT(amm2.expectBalances( + EUR(2000), + XRPAmount(6000000001), + IOUAmount{3464101615137754, -9})); // gw2 claw back 2000 EUR from bob in amm2, which exceeds bob's // balance. All bob's lptokens will be consumed, which corresponds @@ -946,14 +889,10 @@ class AMMClawback_test : public beast::unit_test::suite BEAST_EXPECT(amm2.expectLPTokens(alice, IOUAmount(0))); BEAST_EXPECT(amm2.expectLPTokens(bob, IOUAmount(0))); - if (!features[fixAMMClawbackRounding]) - BEAST_EXPECT(amm2.expectBalances( - EUR(1000), XRP(3000), IOUAmount{1732050807568877, -9})); - else - BEAST_EXPECT(amm2.expectBalances( - EUR(1000), - XRPAmount(3000000001), - IOUAmount{1732050807568877, -9})); + BEAST_EXPECT(amm2.expectBalances( + EUR(1000), + XRPAmount(3000000001), + IOUAmount{1732050807568877, -9})); } } @@ -1430,20 +1369,11 @@ class AMMClawback_test : public beast::unit_test::suite // gw claws back 1000 USD from gw2. env(amm::ammClawback(gw, gw2, USD, EUR, USD(1000)), ter(tesSUCCESS)); env.close(); - if (!features[fixAMMClawbackRounding]) - BEAST_EXPECT(amm.expectBalances( - USD(5000), EUR(10000), IOUAmount{7071067811865475, -12})); - else - BEAST_EXPECT(amm.expectBalances( - USD(5000), EUR(10000), IOUAmount{7071067811865474, -12})); + BEAST_EXPECT(amm.expectBalances( + USD(5000), EUR(10000), IOUAmount{7071067811865474, -12})); BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{1414213562373095, -12})); - if (!features[fixAMMClawbackRounding]) - BEAST_EXPECT( - amm.expectLPTokens(gw2, IOUAmount{1414213562373095, -12})); - else - BEAST_EXPECT( - amm.expectLPTokens(gw2, IOUAmount{1414213562373094, -12})); + BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{1414213562373094, -12})); BEAST_EXPECT( amm.expectLPTokens(alice, IOUAmount{4242640687119285, -12})); @@ -1455,28 +1385,14 @@ class AMMClawback_test : public beast::unit_test::suite // gw2 claws back 1000 EUR from gw. env(amm::ammClawback(gw2, gw, EUR, USD, EUR(1000)), ter(tesSUCCESS)); env.close(); - if (!features[fixAMMClawbackRounding]) - BEAST_EXPECT(amm.expectBalances( - USD(4500), EUR(9000), IOUAmount{6363961030678928, -12})); - else - BEAST_EXPECT(amm.expectBalances( - USD(4500), - STAmount(EUR, UINT64_C(9000000000000001), -12), - IOUAmount{6363961030678927, -12})); + BEAST_EXPECT(amm.expectBalances( + USD(4500), + STAmount(EUR, UINT64_C(9000000000000001), -12), + IOUAmount{6363961030678927, -12})); - if (!features[fixAMMClawbackRounding]) - BEAST_EXPECT( - amm.expectLPTokens(gw, IOUAmount{7071067811865475, -13})); - else - BEAST_EXPECT( - amm.expectLPTokens(gw, IOUAmount{7071067811865480, -13})); + BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{7071067811865480, -13})); - if (!features[fixAMMClawbackRounding]) - BEAST_EXPECT( - amm.expectLPTokens(gw2, IOUAmount{1414213562373095, -12})); - else - BEAST_EXPECT( - amm.expectLPTokens(gw2, IOUAmount{1414213562373094, -12})); + BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{1414213562373094, -12})); BEAST_EXPECT( amm.expectLPTokens(alice, IOUAmount{4242640687119285, -12})); @@ -1489,28 +1405,14 @@ class AMMClawback_test : public beast::unit_test::suite // gw2 claws back 4000 EUR from alice. env(amm::ammClawback(gw2, alice, EUR, USD, EUR(4000)), ter(tesSUCCESS)); env.close(); - if (!features[fixAMMClawbackRounding]) - BEAST_EXPECT(amm.expectBalances( - USD(2500), EUR(5000), IOUAmount{3535533905932738, -12})); - else - BEAST_EXPECT(amm.expectBalances( - USD(2500), - STAmount(EUR, UINT64_C(5000000000000001), -12), - IOUAmount{3535533905932737, -12})); + BEAST_EXPECT(amm.expectBalances( + USD(2500), + STAmount(EUR, UINT64_C(5000000000000001), -12), + IOUAmount{3535533905932737, -12})); - if (!features[fixAMMClawbackRounding]) - BEAST_EXPECT( - amm.expectLPTokens(gw, IOUAmount{7071067811865475, -13})); - else - BEAST_EXPECT( - amm.expectLPTokens(gw, IOUAmount{7071067811865480, -13})); + BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{7071067811865480, -13})); - if (!features[fixAMMClawbackRounding]) - BEAST_EXPECT( - amm.expectLPTokens(gw2, IOUAmount{1414213562373095, -12})); - else - BEAST_EXPECT( - amm.expectLPTokens(gw2, IOUAmount{1414213562373094, -12})); + BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{1414213562373094, -12})); BEAST_EXPECT( amm.expectLPTokens(alice, IOUAmount{1414213562373095, -12})); @@ -1973,22 +1875,13 @@ class AMMClawback_test : public beast::unit_test::suite isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice); BEAST_EXPECT(res && res.value()); - if (!features[fixAMMClawbackRounding]) - { - env(amm::ammClawback(gw, alice, USD, XRP, USD(1)), - ter(tecAMM_BALANCE)); - BEAST_EXPECT(amm.ammExists()); - } - else - { - auto const lpBalance = IOUAmount{989, -12}; - env(amm::ammClawback(gw, alice, USD, XRP, USD(1))); - BEAST_EXPECT(amm.expectBalances( - STAmount(USD, UINT64_C(7000000000000000), -28), - XRPAmount(1), - lpBalance)); - BEAST_EXPECT(amm.expectLPTokens(alice, lpBalance)); - } + auto const lpBalance = IOUAmount{989, -12}; + env(amm::ammClawback(gw, alice, USD, XRP, USD(1))); + BEAST_EXPECT(amm.expectBalances( + STAmount(USD, UINT64_C(7000000000000000), -28), + XRPAmount(1), + lpBalance)); + BEAST_EXPECT(amm.expectLPTokens(alice, lpBalance)); } // IOU/XRP pool. AMMClawback part of last holder's USD balance @@ -2020,22 +1913,12 @@ class AMMClawback_test : public beast::unit_test::suite env(amm::ammClawback(gw, alice, USD, XRP, USD(0.5))); - if (!features[fixAMMClawbackRounding]) - { - BEAST_EXPECT(amm.expectBalances( - STAmount(USD, UINT64_C(5013266196407), -13), - XRPAmount(1002654), - IOUAmount{708'9829046744941, -13})); - } - else - { - auto const lpBalance = IOUAmount{708'9829046743238, -13}; - BEAST_EXPECT(amm.expectBalances( - STAmount(USD, UINT64_C(5013266196406999), -16), - XRPAmount(1002655), - lpBalance)); - BEAST_EXPECT(amm.expectLPTokens(alice, lpBalance)); - } + auto const lpBalance = IOUAmount{708'9829046743238, -13}; + BEAST_EXPECT(amm.expectBalances( + STAmount(USD, UINT64_C(5013266196406999), -16), + XRPAmount(1002655), + lpBalance)); + BEAST_EXPECT(amm.expectLPTokens(alice, lpBalance)); } // IOU/XRP pool. AMMClawback all of last holder's USD balance @@ -2066,19 +1949,8 @@ class AMMClawback_test : public beast::unit_test::suite isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice); BEAST_EXPECT(res && res.value()); - if (!features[fixAMMClawbackRounding]) - { - env(amm::ammClawback(gw, alice, USD, XRP, std::nullopt)); - BEAST_EXPECT(amm.expectBalances( - STAmount(USD, UINT64_C(2410000000000000), -28), - XRPAmount(1), - IOUAmount{34, -11})); - } - else - { - env(amm::ammClawback(gw, alice, USD, XRP, std::nullopt)); - BEAST_EXPECT(!amm.ammExists()); - } + env(amm::ammClawback(gw, alice, USD, XRP, std::nullopt)); + BEAST_EXPECT(!amm.ammExists()); } // IOU/IOU pool, different issuers @@ -2119,17 +1991,8 @@ class AMMClawback_test : public beast::unit_test::suite isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice); BEAST_EXPECT(res && res.value()); - if (features[fixAMMClawbackRounding]) - { - env(amm::ammClawback(gw, alice, USD, EUR, std::nullopt)); - BEAST_EXPECT(!amm.ammExists()); - } - else - { - env(amm::ammClawback(gw, alice, USD, EUR, std::nullopt), - ter(tecINTERNAL)); - BEAST_EXPECT(amm.ammExists()); - } + env(amm::ammClawback(gw, alice, USD, EUR, std::nullopt)); + BEAST_EXPECT(!amm.ammExists()); } // IOU/IOU pool, same issuer @@ -2167,19 +2030,9 @@ class AMMClawback_test : public beast::unit_test::suite isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice); BEAST_EXPECT(res && res.value()); - if (features[fixAMMClawbackRounding]) - { - env(amm::ammClawback(gw, alice, USD, EUR, std::nullopt), - txflags(tfClawTwoAssets)); - BEAST_EXPECT(!amm.ammExists()); - } - else - { - env(amm::ammClawback(gw, alice, USD, EUR, std::nullopt), - txflags(tfClawTwoAssets), - ter(tecINTERNAL)); - BEAST_EXPECT(amm.ammExists()); - } + env(amm::ammClawback(gw, alice, USD, EUR, std::nullopt), + txflags(tfClawTwoAssets)); + BEAST_EXPECT(!amm.ammExists()); } // IOU/IOU pool, larger asset ratio @@ -2218,37 +2071,25 @@ class AMMClawback_test : public beast::unit_test::suite isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice); BEAST_EXPECT(res && res.value()); - if (!features[fixAMMClawbackRounding]) - { - // sqrt(amount * amount2) >= LPTokens and exceeds the allowed - // tolerance - env(amm::ammClawback(gw, alice, USD, EUR, USD(1)), - ter(tecINVARIANT_FAILED)); - BEAST_EXPECT(amm.ammExists()); - } - else - { - env(amm::ammClawback(gw, alice, USD, EUR, USD(1)), - txflags(tfClawTwoAssets)); - auto const lpBalance = IOUAmount{5, -12}; - BEAST_EXPECT(amm.expectBalances( - STAmount(USD, UINT64_C(4), -15), - STAmount(EUR, UINT64_C(8), -9), - lpBalance)); - BEAST_EXPECT(amm.expectLPTokens(alice, lpBalance)); - } + env(amm::ammClawback(gw, alice, USD, EUR, USD(1)), + txflags(tfClawTwoAssets)); + auto const lpBalance = IOUAmount{5, -12}; + BEAST_EXPECT(amm.expectBalances( + STAmount(USD, UINT64_C(4), -15), + STAmount(EUR, UINT64_C(8), -9), + lpBalance)); + BEAST_EXPECT(amm.expectLPTokens(alice, lpBalance)); } } void run() override { - FeatureBitset const all{ - jtx::supported_amendments() | fixAMMClawbackRounding}; + FeatureBitset const all{jtx::supported_amendments()}; testInvalidRequest(); testFeatureDisabled(all - featureAMMClawback); - for (auto const& features : {all - fixAMMClawbackRounding, all}) + for (auto const& features : {all}) { testAMMClawbackSpecificAmount(features); testAMMClawbackExceedBalance(features); diff --git a/src/xrpld/app/tx/detail/AMMClawback.cpp b/src/xrpld/app/tx/detail/AMMClawback.cpp index 43b61b8552..4287067cf0 100644 --- a/src/xrpld/app/tx/detail/AMMClawback.cpp +++ b/src/xrpld/app/tx/detail/AMMClawback.cpp @@ -151,19 +151,16 @@ AMMClawback::applyGuts(Sandbox& sb) if (!accountSle) return tecINTERNAL; // LCOV_EXCL_LINE - if (sb.rules().enabled(fixAMMClawbackRounding)) - { - // retrieve LP token balance inside the amendment gate to avoid - // inconsistent error behavior - auto const lpTokenBalance = ammLPHolds(sb, *ammSle, holder, j_); - if (lpTokenBalance == beast::zero) - return tecAMM_BALANCE; - - if (auto const res = verifyAndAdjustLPTokenBalance( - sb, lpTokenBalance, ammSle, holder); - !res) - return res.error(); // LCOV_EXCL_LINE - } + // retrieve LP token balance inside the amendment gate to avoid + // inconsistent error behavior + auto const lpTokenBalance = ammLPHolds(sb, *ammSle, holder, j_); + if (lpTokenBalance == beast::zero) + return tecAMM_BALANCE; + + if (auto const res = + verifyAndAdjustLPTokenBalance(sb, lpTokenBalance, ammSle, holder); + !res) + return res.error(); // LCOV_EXCL_LINE auto const expected = ammHolds( sb, @@ -288,53 +285,32 @@ AMMClawback::equalWithdrawMatchingOneAmount( ctx_.journal); auto const& rules = sb.rules(); - if (rules.enabled(fixAMMClawbackRounding)) - { - auto tokensAdj = - getRoundedLPTokens(rules, lptAMMBalance, frac, IsDeposit::No); - // LCOV_EXCL_START - if (tokensAdj == beast::zero) - return { - tecAMM_INVALID_TOKENS, STAmount{}, STAmount{}, std::nullopt}; - // LCOV_EXCL_STOP + auto tokensAdj = + getRoundedLPTokens(rules, lptAMMBalance, frac, IsDeposit::No); - frac = adjustFracByTokens(rules, lptAMMBalance, tokensAdj, frac); - auto amount2Rounded = - getRoundedAsset(rules, amount2Balance, frac, IsDeposit::No); + // LCOV_EXCL_START + if (tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}, STAmount{}, std::nullopt}; + // LCOV_EXCL_STOP - auto amountRounded = - getRoundedAsset(rules, amountBalance, frac, IsDeposit::No); + frac = adjustFracByTokens(rules, lptAMMBalance, tokensAdj, frac); + auto amount2Rounded = + getRoundedAsset(rules, amount2Balance, frac, IsDeposit::No); - return AMMWithdraw::withdraw( - sb, - ammSle, - ammAccount, - holder, - amountBalance, - amountRounded, - amount2Rounded, - lptAMMBalance, - tokensAdj, - 0, - FreezeHandling::fhIGNORE_FREEZE, - WithdrawAll::No, - mPriorBalance, - ctx_.journal); - } + auto amountRounded = + getRoundedAsset(rules, amountBalance, frac, IsDeposit::No); - // Because we are doing a two-asset withdrawal, - // tfee is actually not used, so pass tfee as 0. return AMMWithdraw::withdraw( sb, ammSle, ammAccount, holder, amountBalance, - amount, - toSTAmount(amount2Balance.issue(), amount2Withdraw), + amountRounded, + amount2Rounded, lptAMMBalance, - toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac), + tokensAdj, 0, FreezeHandling::fhIGNORE_FREEZE, WithdrawAll::No, From 355c9f9bbb5839f8ef030917667de68fa64aaf1a Mon Sep 17 00:00:00 2001 From: shortthefomo Date: Fri, 10 Apr 2026 23:18:32 -0400 Subject: [PATCH 05/17] port mutex fixes from XRPL port of RWDB --- .../xrpl/basics/ReaderPreferringSharedMutex.h | 106 ++++++++++++ src/xrpld/app/misc/NetworkOPs.cpp | 122 ++++++++------ src/xrpld/app/misc/SHAMapStoreImp.cpp | 154 +++++++++++------- src/xrpld/app/misc/SHAMapStoreImp.h | 1 + src/xrpld/nodestore/DatabaseRotating.h | 9 + src/xrpld/nodestore/backend/RWDBFactory.cpp | 29 ++-- .../nodestore/detail/DatabaseRotatingImp.cpp | 24 ++- .../nodestore/detail/DatabaseRotatingImp.h | 3 + 8 files changed, 328 insertions(+), 120 deletions(-) create mode 100644 include/xrpl/basics/ReaderPreferringSharedMutex.h diff --git a/include/xrpl/basics/ReaderPreferringSharedMutex.h b/include/xrpl/basics/ReaderPreferringSharedMutex.h new file mode 100644 index 0000000000..86ff327eac --- /dev/null +++ b/include/xrpl/basics/ReaderPreferringSharedMutex.h @@ -0,0 +1,106 @@ +#pragma once + +#include + +// On Linux (glibc), std::shared_mutex wraps pthread_rwlock_t initialised +// with PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP. This means a +// pending exclusive lock() blocks new shared (reader) acquisitions, +// causing reader starvation when writers contend frequently. +// +// On macOS / ARM (libc++), std::shared_mutex is already reader-preferring, +// so the same code behaves differently across platforms. +// +// This header provides reader_preferring_shared_mutex: +// - On Linux it wraps pthread_rwlock_t initialised with +// PTHREAD_RWLOCK_PREFER_READER_NP, matching macOS semantics. +// - On all other platforms it is a type alias for std::shared_mutex. +// +// The interface is identical to std::shared_mutex, so it works with +// std::shared_lock and std::unique_lock. + +#if defined(__linux__) + +#include +#include +#include + +namespace ripple { + +class reader_preferring_shared_mutex +{ + pthread_rwlock_t rwlock_; + +public: + reader_preferring_shared_mutex() + { + pthread_rwlockattr_t attr; + pthread_rwlockattr_init(&attr); + pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_READER_NP); + int rc = pthread_rwlock_init(&rwlock_, &attr); + pthread_rwlockattr_destroy(&attr); + if (rc != 0) + throw std::system_error( + rc, std::system_category(), "pthread_rwlock_init"); + } + + ~reader_preferring_shared_mutex() + { + pthread_rwlock_destroy(&rwlock_); + } + + reader_preferring_shared_mutex( + reader_preferring_shared_mutex const&) = delete; + reader_preferring_shared_mutex& + operator=(reader_preferring_shared_mutex const&) = delete; + + // Exclusive (writer) locking + void + lock() + { + pthread_rwlock_wrlock(&rwlock_); + } + + bool + try_lock() + { + return pthread_rwlock_trywrlock(&rwlock_) == 0; + } + + void + unlock() + { + pthread_rwlock_unlock(&rwlock_); + } + + // Shared (reader) locking + void + lock_shared() + { + pthread_rwlock_rdlock(&rwlock_); + } + + bool + try_lock_shared() + { + return pthread_rwlock_tryrdlock(&rwlock_) == 0; + } + + void + unlock_shared() + { + pthread_rwlock_unlock(&rwlock_); + } +}; + +} // namespace ripple + +#else // !__linux__ + +namespace ripple { + +// macOS, Windows, etc. — std::shared_mutex is already reader-preferring. +using reader_preferring_shared_mutex = std::shared_mutex; + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index 017e800ecf..f94cf45b69 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -897,9 +897,11 @@ NetworkOPsImp::setHeartbeatTimer() heartbeatTimer_, mConsensus.parms().ledgerGRANULARITY, [this]() { - m_job_queue.addJob(jtNETOP_TIMER, "NetOPs.heartbeat", [this]() { - processHeartbeatTimer(); - }); + // Run the heartbeat directly on the io_service thread instead + // of posting to the JobQueue. This prevents heavy RPC load + // from starving the consensus heartbeat timer — the io_service + // thread pool is independent of the JobQueue worker pool. + processHeartbeatTimer(); }, [this]() { setHeartbeatTimer(); }); } @@ -939,66 +941,84 @@ NetworkOPsImp::processHeartbeatTimer() RclConsensusLogger clog( "Heartbeat Timer", mConsensus.validating(), m_journal); { - std::unique_lock lock{app_.getMasterMutex()}; + // Use try_to_lock so the heartbeat never blocks on masterMutex. + // If apply() or another operation is holding it, skip the non-critical + // peer/mode checks and proceed directly to timerEntry() — ensuring + // consensus timing is never delayed by mutex contention. + std::unique_lock lock{app_.getMasterMutex(), std::try_to_lock}; - // VFALCO NOTE This is for diagnosing a crash on exit - LoadManager& mgr(app_.getLoadManager()); - mgr.resetDeadlockDetector(); + if (lock.owns_lock()) + { + // VFALCO NOTE This is for diagnosing a crash on exit + LoadManager& mgr(app_.getLoadManager()); + mgr.resetDeadlockDetector(); - std::size_t const numPeers = app_.overlay().size(); + std::size_t const numPeers = app_.overlay().size(); - // do we have sufficient peers? If not, we are disconnected. - if (numPeers < minPeerCount_) - { - if (mMode != OperatingMode::DISCONNECTED) + // do we have sufficient peers? If not, we are disconnected. + if (numPeers < minPeerCount_) { - setMode(OperatingMode::DISCONNECTED); - std::stringstream ss; - ss << "Node count (" << numPeers << ") has fallen " - << "below required minimum (" << minPeerCount_ << ")."; - JLOG(m_journal.warn()) << ss.str(); - CLOG(clog.ss()) << "set mode to DISCONNECTED: " << ss.str(); + if (mMode != OperatingMode::DISCONNECTED) + { + setMode(OperatingMode::DISCONNECTED); + std::stringstream ss; + ss << "Node count (" << numPeers << ") has fallen " + << "below required minimum (" << minPeerCount_ + << ")."; + JLOG(m_journal.warn()) << ss.str(); + CLOG(clog.ss()) + << "set mode to DISCONNECTED: " << ss.str(); + } + else + { + CLOG(clog.ss()) + << "already DISCONNECTED. too few peers (" + << numPeers << "), need at least " << minPeerCount_; + } + + // MasterMutex lock need not be held to call + // setHeartbeatTimer() + lock.unlock(); + // We do not call mConsensus.timerEntry until there are + // enough peers providing meaningful inputs to consensus + setHeartbeatTimer(); + + return; } - else + + if (mMode == OperatingMode::DISCONNECTED) { - CLOG(clog.ss()) - << "already DISCONNECTED. too few peers (" << numPeers - << "), need at least " << minPeerCount_; + setMode(OperatingMode::CONNECTED); + JLOG(m_journal.info()) + << "Node count (" << numPeers << ") is sufficient."; + CLOG(clog.ss()) << "setting mode to CONNECTED based on " + << numPeers << " peers. "; } - // MasterMutex lock need not be held to call setHeartbeatTimer() - lock.unlock(); - // We do not call mConsensus.timerEntry until there are enough - // peers providing meaningful inputs to consensus - setHeartbeatTimer(); - - return; - } - - if (mMode == OperatingMode::DISCONNECTED) - { - setMode(OperatingMode::CONNECTED); - JLOG(m_journal.info()) - << "Node count (" << numPeers << ") is sufficient."; - CLOG(clog.ss()) << "setting mode to CONNECTED based on " << numPeers - << " peers. "; + // Check if the last validated ledger forces a change between + // these states. + auto origMode = mMode.load(); + CLOG(clog.ss()) << "mode: " << strOperatingMode(origMode, true); + if (mMode == OperatingMode::SYNCING) + setMode(OperatingMode::SYNCING); + else if (mMode == OperatingMode::CONNECTED) + setMode(OperatingMode::CONNECTED); + auto newMode = mMode.load(); + if (origMode != newMode) + { + CLOG(clog.ss()) + << ", changing to " << strOperatingMode(newMode, true); + } + CLOG(clog.ss()) << ". "; } - - // Check if the last validated ledger forces a change between these - // states. - auto origMode = mMode.load(); - CLOG(clog.ss()) << "mode: " << strOperatingMode(origMode, true); - if (mMode == OperatingMode::SYNCING) - setMode(OperatingMode::SYNCING); - else if (mMode == OperatingMode::CONNECTED) - setMode(OperatingMode::CONNECTED); - auto newMode = mMode.load(); - if (origMode != newMode) + else { + JLOG(m_journal.debug()) + << "Heartbeat: masterMutex contended, skipping " + "peer/mode checks"; CLOG(clog.ss()) - << ", changing to " << strOperatingMode(newMode, true); + << "masterMutex contended, skipping peer/mode checks. "; } - CLOG(clog.ss()) << ". "; } mConsensus.timerEntry(app_.timeKeeper().closeTime(), clog.ss()); diff --git a/src/xrpld/app/misc/SHAMapStoreImp.cpp b/src/xrpld/app/misc/SHAMapStoreImp.cpp index aff3569d33..2bf8815acd 100644 --- a/src/xrpld/app/misc/SHAMapStoreImp.cpp +++ b/src/xrpld/app/misc/SHAMapStoreImp.cpp @@ -116,6 +116,17 @@ SHAMapStoreImp::SHAMapStoreImp( } get_if_exists(section, "online_delete", deleteInterval_); + isMemoryBackend_ = boost::iequals(get(section, "type"), "rwdb"); + + // For RWDB, default online_delete to ledger_history only if user did not + // explicitly set online_delete. Clamp to the minimum so an implicit + // value never triggers the "online_delete must be at least …" throw. + if (isMemoryBackend_ && deleteInterval_ == 0) + { + auto const minInterval = + config.standalone() ? minimumDeletionIntervalSA_ : minimumDeletionInterval_; + deleteInterval_ = std::max(config.LEDGER_HISTORY, minInterval); + } if (deleteInterval_) { @@ -154,7 +165,7 @@ SHAMapStoreImp::SHAMapStoreImp( } state_db_.init(config, dbName_); - if (!config.mem_backend()) + if (!isMemoryBackend_) dbPaths(); } } @@ -325,64 +336,97 @@ SHAMapStoreImp::run() if (healthWait() == stopping) return; - JLOG(journal_.debug()) << "copying ledger " << validatedSeq; - std::uint64_t nodeCount = 0; - - try + if (isMemoryBackend_) { - validatedLedger->stateMap().snapShot(false)->visitNodes( - std::bind( - &SHAMapStoreImp::copyNode, - this, - std::ref(nodeCount), - std::placeholders::_1)); + // For RWDB: skip the per-node copyNode promotion loop. + // Instead, bulk-copy the archive into a fresh backend that + // is not yet shared—so no exclusive-lock contention with + // live consensus reads/writes on the current writable backend. + // After rotate(), newBackend (archive copy) becomes the new + // writable, old writable becomes new archive, and old archive + // is destroyed. + JLOG(journal_.debug()) << "RWDB: creating pre-populated backend for rotation"; + auto newBackend = makeBackendRotating(); + dbRotating_->copyArchiveTo(*newBackend); + if (healthWait() == stopping) + return; + JLOG(journal_.debug()) << "RWDB: archive copied to new backend"; + + ledgerMaster_->clearLedgerCachePrior(validatedSeq); + lastRotated = validatedSeq; + + dbRotating_->rotate( + std::move(newBackend), + [&](std::string const& writableName, + std::string const& archiveName) { + SavedState savedState; + savedState.writableDb = writableName; + savedState.archiveDb = archiveName; + savedState.lastRotated = lastRotated; + state_db_.setState(savedState); + ledgerMaster_->clearLedgerCachePrior(validatedSeq); + }); + + JLOG(journal_.warn()) << "finished rotation " << validatedSeq; } - catch (SHAMapMissingNode const& e) + else { - JLOG(journal_.error()) - << "Missing node while copying ledger before rotate: " - << e.what(); - continue; + JLOG(journal_.debug()) << "copying ledger " << validatedSeq; + std::uint64_t nodeCount = 0; + + try + { + validatedLedger->stateMap().snapShot(false)->visitNodes( + std::bind( + &SHAMapStoreImp::copyNode, + this, + std::ref(nodeCount), + std::placeholders::_1)); + } + catch (SHAMapMissingNode const& e) + { + JLOG(journal_.error()) + << "Missing node while copying ledger before rotate: " + << e.what(); + continue; + } + + if (healthWait() == stopping) + return; + JLOG(journal_.debug()) << "copied ledger " << validatedSeq + << " nodecount " << nodeCount; + + JLOG(journal_.debug()) << "freshening caches"; + freshenCaches(); + if (healthWait() == stopping) + return; + JLOG(journal_.debug()) << validatedSeq << " freshened caches"; + + JLOG(journal_.trace()) << "Making a new backend"; + auto newBackend = makeBackendRotating(); + JLOG(journal_.debug()) + << validatedSeq << " new backend " << newBackend->getName(); + + clearCaches(validatedSeq); + if (healthWait() == stopping) + return; + + lastRotated = validatedSeq; + + dbRotating_->rotate( + std::move(newBackend), + [&](std::string const& writableName, + std::string const& archiveName) { + SavedState savedState; + savedState.writableDb = writableName; + savedState.archiveDb = archiveName; + savedState.lastRotated = lastRotated; + state_db_.setState(savedState); + clearCaches(validatedSeq); + }); + + JLOG(journal_.warn()) << "finished rotation " << validatedSeq; } - - if (healthWait() == stopping) - return; - // Only log if we completed without a "health" abort - JLOG(journal_.debug()) << "copied ledger " << validatedSeq - << " nodecount " << nodeCount; - - JLOG(journal_.debug()) << "freshening caches"; - freshenCaches(); - if (healthWait() == stopping) - return; - // Only log if we completed without a "health" abort - JLOG(journal_.debug()) << validatedSeq << " freshened caches"; - - JLOG(journal_.debug()) << "Making a new backend"; - auto newBackend = makeBackendRotating(); - JLOG(journal_.debug()) - << validatedSeq << " new backend " << newBackend->getName(); - - clearCaches(validatedSeq); - if (healthWait() == stopping) - return; - - lastRotated = validatedSeq; - - dbRotating_->rotate( - std::move(newBackend), - [&](std::string const& writableName, - std::string const& archiveName) { - SavedState savedState; - savedState.writableDb = writableName; - savedState.archiveDb = archiveName; - savedState.lastRotated = lastRotated; - state_db_.setState(savedState); - - clearCaches(validatedSeq); - }); - - JLOG(journal_.warn()) << "finished rotation " << validatedSeq; } } } diff --git a/src/xrpld/app/misc/SHAMapStoreImp.h b/src/xrpld/app/misc/SHAMapStoreImp.h index 7d36f092be..f372df5810 100644 --- a/src/xrpld/app/misc/SHAMapStoreImp.h +++ b/src/xrpld/app/misc/SHAMapStoreImp.h @@ -101,6 +101,7 @@ class SHAMapStoreImp : public SHAMapStore std::uint32_t deleteInterval_ = 0; bool advisoryDelete_ = false; + bool isMemoryBackend_ = false; std::uint32_t deleteBatch_ = 100; std::chrono::milliseconds backOff_{100}; std::chrono::seconds ageThreshold_{60}; diff --git a/src/xrpld/nodestore/DatabaseRotating.h b/src/xrpld/nodestore/DatabaseRotating.h index 3e8c6a7d5f..90c900e5a9 100644 --- a/src/xrpld/nodestore/DatabaseRotating.h +++ b/src/xrpld/nodestore/DatabaseRotating.h @@ -55,6 +55,15 @@ class DatabaseRotating : public Database std::function const& f) = 0; + + /** Populate @a dest with every object in the archive backend. + + Used by in-memory (RWDB) backends to pre-populate a new writable + backend before rotation, avoiding per-node write-lock contention on + the live writable backend. @a dest must not yet be shared. + */ + virtual void + copyArchiveTo(Backend& dest) = 0; }; } // namespace NodeStore diff --git a/src/xrpld/nodestore/backend/RWDBFactory.cpp b/src/xrpld/nodestore/backend/RWDBFactory.cpp index aae6a6c797..63193dde3f 100644 --- a/src/xrpld/nodestore/backend/RWDBFactory.cpp +++ b/src/xrpld/nodestore/backend/RWDBFactory.cpp @@ -4,11 +4,13 @@ #include #include #include +#include #include #include #include #include #include +#include namespace ripple { namespace NodeStore { @@ -34,8 +36,7 @@ class RWDBBackend : public Backend using DataStore = std::map>; // Store compressed blob // data - mutable std::recursive_mutex - mutex_; // Only needed for std::map implementation + mutable reader_preferring_shared_mutex mutex_; DataStore table_; @@ -65,7 +66,7 @@ class RWDBBackend : public Backend void open(bool createIfMissing) override { - std::lock_guard lock(mutex_); + std::unique_lock lock(mutex_); if (isOpen_) Throw("already open"); isOpen_ = true; @@ -74,26 +75,31 @@ class RWDBBackend : public Backend bool isOpen() override { + std::shared_lock lock(mutex_); return isOpen_; } void close() override { - std::lock_guard lock(mutex_); - table_.clear(); - isOpen_ = false; + DataStore old; + { + std::unique_lock lock(mutex_); + isOpen_ = false; + old.swap(table_); // O(1) swap; release lock before destructor runs + } + // 'old' is now destroyed outside the lock — no fetch() can be + // blocked by the (potentially millions-of-entries) map destructor. } Status fetch(void const* key, std::shared_ptr* pObject) override { - if (!isOpen_) - return notFound; - uint256 const hash(uint256::fromVoid(key)); - std::lock_guard lock(mutex_); + std::shared_lock lock(mutex_); + if (!isOpen_) + return notFound; auto it = table_.find(hash); if (it == table_.end()) return notFound; @@ -162,10 +168,9 @@ class RWDBBackend : public Backend void for_each(std::function)> f) override { + std::shared_lock lock(mutex_); if (!isOpen_) return; - - std::lock_guard lock(mutex_); for (const auto& entry : table_) { nudb::detail::buffer bf; diff --git a/src/xrpld/nodestore/detail/DatabaseRotatingImp.cpp b/src/xrpld/nodestore/detail/DatabaseRotatingImp.cpp index 1499976d05..83cb691974 100644 --- a/src/xrpld/nodestore/detail/DatabaseRotatingImp.cpp +++ b/src/xrpld/nodestore/detail/DatabaseRotatingImp.cpp @@ -44,6 +44,20 @@ DatabaseRotatingImp::DatabaseRotatingImp( fdRequired_ += archiveBackend_->fdRequired(); } +void +DatabaseRotatingImp::copyArchiveTo(Backend& dest) +{ + // Snapshot the archive backend pointer under lock, then iterate it + // outside the lock. dest is not yet shared so its store() calls are + // uncontested — no live-backend write-lock contention. + auto archive = [&] { + std::lock_guard const lock(mutex_); + return archiveBackend_; + }(); + + archive->for_each([&](std::shared_ptr obj) { dest.store(obj); }); +} + void DatabaseRotatingImp::rotate( std::unique_ptr&& newBackend, @@ -111,8 +125,11 @@ DatabaseRotatingImp::rotate( // Execute the lambda ensurePinnedLedgersInWritable(); - // Now it's safe to mark the archive backend for deletion - archiveBackend_->setDeletePath(); + // Do NOT call setDeletePath() inside this lock. For in-memory + // backends, setDeletePath() calls close() which destructs the entire + // table_ map (millions of shared_ptr ref-count decrements) + // while the lock is held, blocking every concurrent fetchNodeObject + // call for several seconds and starving consensus reads. oldArchiveBackend = std::move(archiveBackend_); // Complete the rotation @@ -122,6 +139,9 @@ DatabaseRotatingImp::rotate( writableBackend_ = std::move(newBackend); } + // Lock released — clear the old archive now without blocking fetches. + oldArchiveBackend->setDeletePath(); + f(newWritableBackendName, newArchiveBackendName); } diff --git a/src/xrpld/nodestore/detail/DatabaseRotatingImp.h b/src/xrpld/nodestore/detail/DatabaseRotatingImp.h index 9052de07e9..6e42bd1a21 100644 --- a/src/xrpld/nodestore/detail/DatabaseRotatingImp.h +++ b/src/xrpld/nodestore/detail/DatabaseRotatingImp.h @@ -51,6 +51,9 @@ class DatabaseRotatingImp : public DatabaseRotating stop(); } + void + copyArchiveTo(Backend& dest) override; + void rotate( std::unique_ptr&& newBackend, From 5280e5bc65553102bcef9f92cf968cb71e984c0d Mon Sep 17 00:00:00 2001 From: shortthefomo Date: Fri, 10 Apr 2026 23:29:35 -0400 Subject: [PATCH 06/17] clang-format fixes --- include/xrpl/basics/ReaderPreferringSharedMutex.h | 4 ++-- src/xrpld/app/misc/NetworkOPs.cpp | 10 ++++------ src/xrpld/app/misc/SHAMapStoreImp.cpp | 8 +++++--- src/xrpld/nodestore/backend/RWDBFactory.cpp | 2 +- src/xrpld/nodestore/detail/DatabaseRotatingImp.cpp | 3 ++- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/include/xrpl/basics/ReaderPreferringSharedMutex.h b/include/xrpl/basics/ReaderPreferringSharedMutex.h index 86ff327eac..cf32a1c51e 100644 --- a/include/xrpl/basics/ReaderPreferringSharedMutex.h +++ b/include/xrpl/basics/ReaderPreferringSharedMutex.h @@ -48,8 +48,8 @@ class reader_preferring_shared_mutex pthread_rwlock_destroy(&rwlock_); } - reader_preferring_shared_mutex( - reader_preferring_shared_mutex const&) = delete; + reader_preferring_shared_mutex(reader_preferring_shared_mutex const&) = + delete; reader_preferring_shared_mutex& operator=(reader_preferring_shared_mutex const&) = delete; diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index f94cf45b69..3dbddba86d 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -963,17 +963,15 @@ NetworkOPsImp::processHeartbeatTimer() setMode(OperatingMode::DISCONNECTED); std::stringstream ss; ss << "Node count (" << numPeers << ") has fallen " - << "below required minimum (" << minPeerCount_ - << ")."; + << "below required minimum (" << minPeerCount_ << ")."; JLOG(m_journal.warn()) << ss.str(); - CLOG(clog.ss()) - << "set mode to DISCONNECTED: " << ss.str(); + CLOG(clog.ss()) << "set mode to DISCONNECTED: " << ss.str(); } else { CLOG(clog.ss()) - << "already DISCONNECTED. too few peers (" - << numPeers << "), need at least " << minPeerCount_; + << "already DISCONNECTED. too few peers (" << numPeers + << "), need at least " << minPeerCount_; } // MasterMutex lock need not be held to call diff --git a/src/xrpld/app/misc/SHAMapStoreImp.cpp b/src/xrpld/app/misc/SHAMapStoreImp.cpp index 2bf8815acd..c0b876f383 100644 --- a/src/xrpld/app/misc/SHAMapStoreImp.cpp +++ b/src/xrpld/app/misc/SHAMapStoreImp.cpp @@ -123,8 +123,9 @@ SHAMapStoreImp::SHAMapStoreImp( // value never triggers the "online_delete must be at least …" throw. if (isMemoryBackend_ && deleteInterval_ == 0) { - auto const minInterval = - config.standalone() ? minimumDeletionIntervalSA_ : minimumDeletionInterval_; + auto const minInterval = config.standalone() + ? minimumDeletionIntervalSA_ + : minimumDeletionInterval_; deleteInterval_ = std::max(config.LEDGER_HISTORY, minInterval); } @@ -345,7 +346,8 @@ SHAMapStoreImp::run() // After rotate(), newBackend (archive copy) becomes the new // writable, old writable becomes new archive, and old archive // is destroyed. - JLOG(journal_.debug()) << "RWDB: creating pre-populated backend for rotation"; + JLOG(journal_.debug()) + << "RWDB: creating pre-populated backend for rotation"; auto newBackend = makeBackendRotating(); dbRotating_->copyArchiveTo(*newBackend); if (healthWait() == stopping) diff --git a/src/xrpld/nodestore/backend/RWDBFactory.cpp b/src/xrpld/nodestore/backend/RWDBFactory.cpp index 63193dde3f..92fd6a1671 100644 --- a/src/xrpld/nodestore/backend/RWDBFactory.cpp +++ b/src/xrpld/nodestore/backend/RWDBFactory.cpp @@ -3,8 +3,8 @@ #include #include #include -#include #include +#include #include #include #include diff --git a/src/xrpld/nodestore/detail/DatabaseRotatingImp.cpp b/src/xrpld/nodestore/detail/DatabaseRotatingImp.cpp index 83cb691974..aec2d4953b 100644 --- a/src/xrpld/nodestore/detail/DatabaseRotatingImp.cpp +++ b/src/xrpld/nodestore/detail/DatabaseRotatingImp.cpp @@ -55,7 +55,8 @@ DatabaseRotatingImp::copyArchiveTo(Backend& dest) return archiveBackend_; }(); - archive->for_each([&](std::shared_ptr obj) { dest.store(obj); }); + archive->for_each( + [&](std::shared_ptr obj) { dest.store(obj); }); } void From 4ff261156e70ecc835f03e9e9a85551193e72c26 Mon Sep 17 00:00:00 2001 From: shortthefomo Date: Sat, 11 Apr 2026 17:38:52 -0400 Subject: [PATCH 07/17] fix: RWDB rotation memory leak - copy only live state nodes instead of entire archive --- src/xrpld/app/misc/SHAMapStoreImp.cpp | 63 ++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/src/xrpld/app/misc/SHAMapStoreImp.cpp b/src/xrpld/app/misc/SHAMapStoreImp.cpp index c0b876f383..13ce2390fa 100644 --- a/src/xrpld/app/misc/SHAMapStoreImp.cpp +++ b/src/xrpld/app/misc/SHAMapStoreImp.cpp @@ -339,20 +339,59 @@ SHAMapStoreImp::run() if (isMemoryBackend_) { - // For RWDB: skip the per-node copyNode promotion loop. - // Instead, bulk-copy the archive into a fresh backend that - // is not yet shared—so no exclusive-lock contention with - // live consensus reads/writes on the current writable backend. - // After rotate(), newBackend (archive copy) becomes the new - // writable, old writable becomes new archive, and old archive - // is destroyed. - JLOG(journal_.debug()) - << "RWDB: creating pre-populated backend for rotation"; + // For RWDB: copy only the current validated ledger's live + // state nodes into a fresh backend that is not yet shared, + // avoiding both exclusive-lock contention on the live + // writable backend AND stale-node accumulation. + // + // copyArchiveTo would carry forward ALL archive entries + // (including stale nodes from older ledger versions that + // were promoted via fetch duplication), causing unbounded + // memory growth across rotation cycles. + JLOG(journal_.debug()) << "RWDB: copying live state for rotation"; auto newBackend = makeBackendRotating(); - dbRotating_->copyArchiveTo(*newBackend); - if (healthWait() == stopping) + std::uint64_t nodeCount = 0; + bool aborted = false; + + try + { + validatedLedger->stateMap().snapShot(false)->visitNodes( + [&](SHAMapTreeNode& node) -> bool { + auto const hash = node.getHash().as_uint256(); + // Fetch the NodeObject from the rotating DB + // (checks writable then archive) and store it + // directly in the new unshared backend. + auto obj = dbRotating_->fetchNodeObject( + hash, + 0, + NodeStore::FetchType::synchronous, + false); + if (obj) + newBackend->store(obj); + + if ((++nodeCount % checkHealthInterval_) == 0) + { + if (healthWait() == stopping) + { + aborted = true; + return false; + } + } + return true; + }); + } + catch (SHAMapMissingNode const& e) + { + JLOG(journal_.error()) + << "Missing node while copying state before rotate: " + << e.what(); + continue; + } + + if (aborted) return; - JLOG(journal_.debug()) << "RWDB: archive copied to new backend"; + JLOG(journal_.debug()) + << "RWDB: copied " << nodeCount << " live nodes"; ledgerMaster_->clearLedgerCachePrior(validatedSeq); lastRotated = validatedSeq; From b016c19a609301e7ceb97ca127eab5dfd905a000 Mon Sep 17 00:00:00 2001 From: Nicholas Dudfield Date: Mon, 13 Apr 2026 13:25:42 +0700 Subject: [PATCH 08/17] feat: experiment with in-memory graph retention for null node-store Introduces a 'NULL' node-store mode (via XAHAU_RWDB_NULL) that operates entirely in-memory by leveraging a sliding window of retained Ledger objects. Key changes: - SHAMapSync: Bypass FullBelowCache in null mode to force full tree wiring. - Ledger: Add 'fullyWired' state tracking and mandatory wiring before use. - LedgerMaster: Implement 'mRetainedLedgers' sliding window to pin SHAMap graphs. - PeerImp: Add fallbacks to TreeNodeCache and LedgerMaster for peer requests. - contract: Add boost::stacktrace to LogThrow for easier debugging of misses. - basics: Add ReaderPreferringSharedMutex to mitigate reader starvation. --- src/libxrpl/basics/contract.cpp | 12 +- src/xrpld/app/ledger/Ledger.cpp | 78 ++++++++- src/xrpld/app/ledger/Ledger.h | 17 ++ src/xrpld/app/ledger/LedgerMaster.h | 11 ++ src/xrpld/app/ledger/detail/InboundLedger.cpp | 160 ++++++++++++++++-- src/xrpld/app/ledger/detail/LedgerMaster.cpp | 130 +++++++++++++- src/xrpld/nodestore/backend/RWDBFactory.cpp | 26 +++ src/xrpld/overlay/detail/PeerImp.cpp | 46 ++++- src/xrpld/shamap/detail/SHAMapSync.cpp | 39 ++++- 9 files changed, 482 insertions(+), 37 deletions(-) diff --git a/src/libxrpl/basics/contract.cpp b/src/libxrpl/basics/contract.cpp index 4f0a74644a..679db05e5e 100644 --- a/src/libxrpl/basics/contract.cpp +++ b/src/libxrpl/basics/contract.cpp @@ -20,8 +20,13 @@ #include #include #include +#ifndef BOOST_STACKTRACE_GNU_SOURCE_NOT_REQUIRED +#define BOOST_STACKTRACE_GNU_SOURCE_NOT_REQUIRED +#endif +#include #include #include +#include namespace ripple { @@ -41,7 +46,12 @@ accessViolation() noexcept void LogThrow(std::string const& title) { - JLOG(debugLog().warn()) << title; + std::ostringstream oss; + oss << title << '\n' << boost::stacktrace::stacktrace(); + JLOG(debugLog().warn()) << oss.str(); + // Also mirror to stderr so uncaught exceptions leave a trace even when + // log output is buffered/lost before terminate(). + std::cerr << oss.str() << std::endl; } [[noreturn]] void diff --git a/src/xrpld/app/ledger/Ledger.cpp b/src/xrpld/app/ledger/Ledger.cpp index 10d1c44369..f2d71946aa 100644 --- a/src/xrpld/app/ledger/Ledger.cpp +++ b/src/xrpld/app/ledger/Ledger.cpp @@ -50,6 +50,8 @@ #include #include #include +#include +#include #include #include @@ -59,6 +61,33 @@ namespace ripple { create_genesis_t const create_genesis{}; +namespace { + +bool +isRWDBNullMode() +{ + static bool const v = [] { + char const* e = std::getenv("XAHAU_RWDB_NULL"); + return e && *e && std::string_view{e} != "0"; + }(); + return v; +} + +template +std::size_t +wireCompleteSHAMap(Map const& map) +{ + std::size_t leaves = 0; + for (auto const& item : map) + { + (void)item; + ++leaves; + } + return leaves; +} + +} // namespace + uint256 calculateLedgerHash(LedgerInfo const& info) { @@ -249,6 +278,7 @@ Ledger::Ledger( stateMap_.flushDirty(hotACCOUNT_NODE); setImmutable(); + setFullyWired(); } Ledger::Ledger( @@ -313,6 +343,7 @@ Ledger::Ledger( // Create a new ledger that follows this one Ledger::Ledger(Ledger const& prevLedger, NetClock::time_point closeTime) : mImmutable(false) + , fullyWired_(prevLedger.isFullyWired()) , txMap_(SHAMapType::TRANSACTION, prevLedger.txMap_.family()) , stateMap_(prevLedger.stateMap_, true) , fees_(prevLedger.fees_) @@ -390,6 +421,30 @@ Ledger::setImmutable(bool rehash) setup(); } +bool +Ledger::fullWireForUse(beast::Journal journal, char const* context) const +{ + if (!isRWDBNullMode() || isFullyWired()) + return true; + + try + { + auto const stateLeaves = wireCompleteSHAMap(stateMap_); + auto const txLeaves = wireCompleteSHAMap(txMap_); + setFullyWired(); + JLOG(journal.info()) + << context << ": fully wired ledger " << info_.seq << " (" + << stateLeaves << " state leaves, " << txLeaves << " tx leaves)"; + return true; + } + catch (SHAMapMissingNode const& e) + { + JLOG(journal.warn()) << context << ": incomplete ledger " << info_.seq + << ": " << e.what(); + return false; + } +} + // raw setters for catalogue void Ledger::setCloseFlags(int closeFlags) @@ -1130,14 +1185,17 @@ loadLedgerHelper(LedgerInfo const& info, Application& app, bool acquire) } static void -finishLoadByIndexOrHash( - std::shared_ptr const& ledger, - Config const& config, - beast::Journal j) +finishLoadByIndexOrHash(std::shared_ptr& ledger, beast::Journal j) { if (!ledger) return; + if (!ledger->fullWireForUse(j, "finishLoadByIndexOrHash")) + { + ledger.reset(); + return; + } + XRPL_ASSERT( ledger->read(keylet::fees()), "ripple::finishLoadByIndexOrHash : valid ledger fees"); @@ -1155,7 +1213,13 @@ getLatestLedger(Application& app) app.getRelationalDatabase().getNewestLedgerInfo(); if (!info) return {std::shared_ptr(), {}, {}}; - return {loadLedgerHelper(*info, app, true), info->seq, info->hash}; + auto ledger = loadLedgerHelper(*info, app, true); + if (ledger && + !ledger->fullWireForUse(app.journal("Ledger"), "getLatestLedger")) + { + ledger.reset(); + } + return {ledger, info->seq, info->hash}; } std::shared_ptr @@ -1165,7 +1229,7 @@ loadByIndex(std::uint32_t ledgerIndex, Application& app, bool acquire) app.getRelationalDatabase().getLedgerInfoByIndex(ledgerIndex)) { std::shared_ptr ledger = loadLedgerHelper(*info, app, acquire); - finishLoadByIndexOrHash(ledger, app.config(), app.journal("Ledger")); + finishLoadByIndexOrHash(ledger, app.journal("Ledger")); return ledger; } return {}; @@ -1178,7 +1242,7 @@ loadByHash(uint256 const& ledgerHash, Application& app, bool acquire) app.getRelationalDatabase().getLedgerInfoByHash(ledgerHash)) { std::shared_ptr ledger = loadLedgerHelper(*info, app, acquire); - finishLoadByIndexOrHash(ledger, app.config(), app.journal("Ledger")); + finishLoadByIndexOrHash(ledger, app.journal("Ledger")); XRPL_ASSERT( !ledger || ledger->info().hash == ledgerHash, "ripple::loadByHash : ledger hash match if loaded"); diff --git a/src/xrpld/app/ledger/Ledger.h b/src/xrpld/app/ledger/Ledger.h index 0476859c87..eaa42284c9 100644 --- a/src/xrpld/app/ledger/Ledger.h +++ b/src/xrpld/app/ledger/Ledger.h @@ -31,6 +31,7 @@ #include #include #include +#include #include namespace ripple { @@ -294,6 +295,21 @@ class Ledger final : public std::enable_shared_from_this, return mImmutable; } + bool + isFullyWired() const + { + return fullyWired_.load(std::memory_order_acquire); + } + + void + setFullyWired() const + { + fullyWired_.store(true, std::memory_order_release); + } + + bool + fullWireForUse(beast::Journal journal, char const* context) const; + /* Mark this ledger as "should be full". "Full" is metadata property of the ledger, it indicates @@ -417,6 +433,7 @@ class Ledger final : public std::enable_shared_from_this, defaultFees(Config const& config); bool mImmutable; + mutable std::atomic fullyWired_{false}; // A SHAMap containing the transactions associated with this ledger. SHAMap mutable txMap_; diff --git a/src/xrpld/app/ledger/LedgerMaster.h b/src/xrpld/app/ledger/LedgerMaster.h index e59b1b78fb..9a20ab307c 100644 --- a/src/xrpld/app/ledger/LedgerMaster.h +++ b/src/xrpld/app/ledger/LedgerMaster.h @@ -37,6 +37,7 @@ #include #include #include +#include #include #include @@ -183,6 +184,10 @@ class LedgerMaster : public AbstractFetchPackContainer std::shared_ptr getLedgerByHash(uint256 const& hash); + std::shared_ptr + getClosestFullyWiredLedger( + std::shared_ptr const& targetLedger); + void setLedgerRangePresent( std::uint32_t minV, @@ -347,6 +352,12 @@ class LedgerMaster : public AbstractFetchPackContainer // The last ledger we handled fetching history std::shared_ptr mHistLedger; + // Sliding window of recently validated ledgers pinned in memory so their + // SHAMap state trees remain reachable via shared_ptr. Required when the + // node store does not persist state nodes (e.g. RWDB with + // XAHAU_RWDB_DISCARD_HOT_ACCOUNT_NODE). Guarded by m_mutex. + std::deque> mRetainedLedgers; + // Fully validated ledger, whether or not we have the ledger resident. std::pair mLastValidLedger{uint256(), 0}; diff --git a/src/xrpld/app/ledger/detail/InboundLedger.cpp b/src/xrpld/app/ledger/detail/InboundLedger.cpp index ba01dce17d..d55f457024 100644 --- a/src/xrpld/app/ledger/detail/InboundLedger.cpp +++ b/src/xrpld/app/ledger/detail/InboundLedger.cpp @@ -35,12 +35,88 @@ #include #include +#include +#include #include +#include namespace ripple { using namespace std::chrono_literals; +namespace { + +bool +isRWDBNullMode() +{ + static bool const v = [] { + char const* e = std::getenv("XAHAU_RWDB_NULL"); + return e && *e && std::string_view{e} != "0"; + }(); + return v; +} + +template +std::size_t +wireCompleteSHAMap(Map const& map) +{ + std::size_t leaves = 0; + for (auto const& item : map) + { + (void)item; + ++leaves; + } + return leaves; +} + +bool +primeInboundLedgerForUse( + std::shared_ptr const& ledger, + std::shared_ptr const& baseLedger, + beast::Journal journal, + char const* context) +{ + if (!isRWDBNullMode()) + return true; + + if (ledger->isFullyWired()) + return true; + + if (!baseLedger || !baseLedger->isFullyWired()) + { + return ledger->fullWireForUse(journal, context); + } + + try + { + std::size_t stateNodes = 0; + // By the time an inbound ledger is marked complete, sync has already + // descended the current tree; this delta walk avoids rewalking + // unchanged state subtrees that are known-good via a fully wired + // same-chain base ledger. + ledger->stateMap().visitDifferences( + &baseLedger->stateMap(), [&stateNodes](SHAMapTreeNode const&) { + ++stateNodes; + return true; + }); + auto const txLeaves = wireCompleteSHAMap(ledger->txMap()); + ledger->setFullyWired(); + JLOG(journal.info()) + << context << ": fully wired ledger " << ledger->info().seq << " (" + << stateNodes << " changed state nodes vs base ledger, " << txLeaves + << " tx leaves)"; + return true; + } + catch (SHAMapMissingNode const& e) + { + JLOG(journal.warn()) << context << ": incomplete ledger " + << ledger->info().seq << ": " << e.what(); + return false; + } +} + +} // namespace + enum { // Number of peers to start with peerCountStart = 5 @@ -120,6 +196,16 @@ InboundLedger::init(ScopedLockType& collectionLock) JLOG(journal_.debug()) << "Acquiring ledger we already have in " << " local store. " << hash_; + auto const baseLedger = + app_.getLedgerMaster().getClosestFullyWiredLedger(mLedger); + if (!primeInboundLedgerForUse( + mLedger, baseLedger, journal_, "InboundLedger::init")) + { + complete_ = false; + failed_ = true; + done(); + return; + } XRPL_ASSERT( mLedger->read(keylet::fees()), "ripple::InboundLedger::init : valid ledger fees"); @@ -351,10 +437,6 @@ InboundLedger::tryDB(NodeStore::Database& srcDB) { JLOG(journal_.debug()) << "Had everything locally"; complete_ = true; - XRPL_ASSERT( - mLedger->read(keylet::fees()), - "ripple::InboundLedger::tryDB : valid ledger fees"); - mLedger->setImmutable(); } } @@ -453,18 +535,30 @@ InboundLedger::done() if (complete_ && !failed_ && mLedger) { - XRPL_ASSERT( - mLedger->read(keylet::fees()), - "ripple::InboundLedger::done : valid ledger fees"); - mLedger->setImmutable(); - switch (mReason) + auto const baseLedger = + app_.getLedgerMaster().getClosestFullyWiredLedger(mLedger); + if (!primeInboundLedgerForUse( + mLedger, baseLedger, journal_, "InboundLedger::done")) { - case Reason::HISTORY: - app_.getInboundLedgers().onLedgerFetched(); - break; - default: - app_.getLedgerMaster().storeLedger(mLedger); - break; + complete_ = false; + failed_ = true; + } + else + { + XRPL_ASSERT( + mLedger->read(keylet::fees()), + "ripple::InboundLedger::done : valid ledger fees"); + mLedger->setImmutable(); + + switch (mReason) + { + case Reason::HISTORY: + app_.getInboundLedgers().onLedgerFetched(); + break; + default: + app_.getLedgerMaster().storeLedger(mLedger); + break; + } } } @@ -473,6 +567,42 @@ InboundLedger::done() jtLEDGER_DATA, "AcquisitionDone", [self = shared_from_this()]() { if (self->complete_ && !self->failed_) { + if (!isRWDBNullMode() && self->mReason != Reason::HISTORY) + { + // Prime the state tree BEFORE checkAccept so consensus + // never sees a lazy tree. Runs off any inbound lock — + // this job is dispatched without mtx_ held. + // visitDifferences against prior validated walks only + // the delta; canonicalization means shared subtrees are + // the same inner objects (already wired). Gated on + // non-HISTORY to avoid paying on historical backfills. + auto const prior = + self->app_.getLedgerMaster().getValidatedLedger(); + SHAMap const* have = prior ? &prior->stateMap() : nullptr; + + try + { + std::size_t walked = 0; + self->mLedger->stateMap().visitDifferences( + have, [&walked](SHAMapTreeNode const&) { + ++walked; + return true; + }); + JLOG(self->journal_.info()) + << "Inbound prime: ledger " + << self->mLedger->info().seq << " wired " << walked + << (have ? " delta nodes vs prior validated" + : " nodes (first full walk)"); + } + catch (SHAMapMissingNode const& e) + { + JLOG(self->journal_.warn()) + << "Inbound prime: incomplete state tree for " + << "ledger " << self->mLedger->info().seq << ": " + << e.what(); + } + } + self->app_.getLedgerMaster().checkAccept(self->getLedger()); self->app_.getLedgerMaster().tryAdvance(); } diff --git a/src/xrpld/app/ledger/detail/LedgerMaster.cpp b/src/xrpld/app/ledger/detail/LedgerMaster.cpp index 2efc6dd9e7..b59148a4e9 100644 --- a/src/xrpld/app/ledger/detail/LedgerMaster.cpp +++ b/src/xrpld/app/ledger/detail/LedgerMaster.cpp @@ -697,11 +697,12 @@ LedgerMaster::tryFill(std::shared_ptr ledger) if (it == ledgerHashes.end()) break; + auto const& firstHash = ledgerHashes.begin()->second.ledgerHash; if (!nodeStore.fetchNodeObject( - ledgerHashes.begin()->second.ledgerHash, - ledgerHashes.begin()->first)) + firstHash, ledgerHashes.begin()->first) && + !getLedgerByHash(firstHash)) { - // The ledger is not backed by the node store + // Not in node store and not in memory — genuinely missing JLOG(m_journal.warn()) << "SQL DB ledger sequence " << seq << " mismatches node store"; break; @@ -865,6 +866,44 @@ LedgerMaster::setFullLedger( mCompleteLedgers.insert(ledger->info().seq); } + // Pin a sliding window of recently validated current ledgers so their + // SHAMap state trees stay resident via shared_ptr. This tracks the + // server's active online band rather than retaining arbitrary historical + // backfill ledgers. + if (isCurrent && ledger_history_ > 0) + { + std::lock_guard ml(m_mutex); + bool const isFirst = mRetainedLedgers.empty(); + mRetainedLedgers.push_back(ledger); + while (mRetainedLedgers.size() > ledger_history_) + mRetainedLedgers.pop_front(); + + // Legacy bootstrap for lazy trees. In null mode the ledger has + // already been fully wired before it reaches retention, so there is + // nothing left to do here. + if (isFirst && !ledger->isFullyWired()) + { + try + { + std::size_t leafCount = 0; + for (auto const& item : ledger->stateMap()) + { + (void)item; + ++leafCount; + } + JLOG(m_journal.info()) + << "Retention: primed state tree for ledger " + << ledger->info().seq << " (" << leafCount << " leaves)"; + } + catch (SHAMapMissingNode const& e) + { + JLOG(m_journal.warn()) + << "Retention: incomplete state tree for ledger " + << ledger->info().seq << ": " << e.what(); + } + } + } + { std::lock_guard ml(m_mutex); @@ -1663,6 +1702,12 @@ LedgerMaster::getCloseTimeByHash( LedgerHash const& ledgerHash, std::uint32_t index) { + // Prefer an in-memory Ledger (retained / history cache) over the node + // store so this works in RWDB-only configs where headers may not be + // persisted long-term. + if (auto ledger = getLedgerByHash(ledgerHash)) + return ledger->info().closeTime; + auto nodeObject = app_.getNodeStore().fetchNodeObject(ledgerHash, index); if (nodeObject && (nodeObject->getData().size() >= 120)) { @@ -1807,6 +1852,85 @@ LedgerMaster::getLedgerByHash(uint256 const& hash) return {}; } +std::shared_ptr +LedgerMaster::getClosestFullyWiredLedger( + std::shared_ptr const& targetLedger) +{ + if (!targetLedger) + return {}; + + std::vector> candidates; + { + std::lock_guard lock(m_mutex); + candidates.reserve(mRetainedLedgers.size() + 3); + for (auto const& ledger : mRetainedLedgers) + candidates.push_back(ledger); + if (auto const closed = mClosedLedger.get()) + candidates.push_back(closed); + if (auto const valid = mValidLedger.get()) + candidates.push_back(valid); + if (mPubLedger) + candidates.push_back(mPubLedger); + } + + auto const targetSeq = targetLedger->info().seq; + auto const targetHash = targetLedger->info().hash; + + std::shared_ptr best; + auto bestDistance = std::numeric_limits::max(); + + for (auto const& candidate : candidates) + { + if (!candidate || !candidate->isFullyWired()) + continue; + + if (candidate->info().hash == targetHash) + return candidate; + + bool sameChain = false; + try + { + if (candidate->info().seq < targetSeq) + { + if (auto const hash = hashOfSeq( + *targetLedger, candidate->info().seq, m_journal); + hash && *hash == candidate->info().hash) + { + sameChain = true; + } + } + else if (candidate->info().seq > targetSeq) + { + if (auto const hash = + hashOfSeq(*candidate, targetSeq, m_journal); + hash && *hash == targetHash) + { + sameChain = true; + } + } + } + catch (std::exception const&) + { + sameChain = false; + } + + if (!sameChain) + continue; + + auto const distance = candidate->info().seq < targetSeq + ? targetSeq - candidate->info().seq + : candidate->info().seq - targetSeq; + + if (!best || distance < bestDistance) + { + best = candidate; + bestDistance = distance; + } + } + + return best; +} + void LedgerMaster::setLedgerRangePresent( std::uint32_t minV, diff --git a/src/xrpld/nodestore/backend/RWDBFactory.cpp b/src/xrpld/nodestore/backend/RWDBFactory.cpp index 92fd6a1671..970fa869b8 100644 --- a/src/xrpld/nodestore/backend/RWDBFactory.cpp +++ b/src/xrpld/nodestore/backend/RWDBFactory.cpp @@ -8,9 +8,11 @@ #include #include #include +#include #include #include #include +#include namespace ripple { namespace NodeStore { @@ -92,9 +94,22 @@ class RWDBBackend : public Backend // blocked by the (potentially millions-of-entries) map destructor. } + static bool + nullMode() + { + static bool const v = [] { + char const* e = std::getenv("XAHAU_RWDB_NULL"); + return e && *e && std::string_view{e} != "0"; + }(); + return v; + } + Status fetch(void const* key, std::shared_ptr* pObject) override { + if (nullMode()) + return notFound; + uint256 const hash(uint256::fromVoid(key)); std::shared_lock lock(mutex_); @@ -140,6 +155,17 @@ class RWDBBackend : public Backend if (!object) return; + if (nullMode()) + return; + + static bool const discardHotAccountNode = [] { + char const* v = std::getenv("XAHAU_RWDB_DISCARD_HOT_ACCOUNT_NODE"); + return v && *v && std::string_view{v} != "0"; + }(); + + if (discardHotAccountNode && object->getType() == hotACCOUNT_NODE) + return; + EncodedBlob encoded(object); nudb::detail::buffer bf; auto const result = diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index 700966b6d0..66f2d01f21 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -32,11 +32,14 @@ #include #include #include +#include +#include #include #include #include #include #include +#include #include #include @@ -2464,13 +2467,50 @@ PeerImp::onMessage(std::shared_ptr const& m) // need to inject the NodeStore interfaces. std::uint32_t seq{obj.has_ledgerseq() ? obj.ledgerseq() : 0}; auto nodeObject{app_.getNodeStore().fetchNodeObject(hash, seq)}; + + void const* dataPtr = nullptr; + std::size_t dataSize = 0; + Blob treeBlob; + if (nodeObject) + { + dataPtr = nodeObject->getData().data(); + dataSize = nodeObject->getData().size(); + } + else if ( + auto treeNode = + app_.getNodeFamily().getTreeNodeCache()->fetch(hash)) + { + // SHAMap tree node fallback — works for state/tx nodes + // held via the retained Ledgers' SHAMap inner nodes. + Serializer s; + treeNode->serializeWithPrefix(s); + treeBlob = std::move(s.modData()); + dataPtr = treeBlob.data(); + dataSize = treeBlob.size(); + } + else if (packet.type() == protocol::TMGetObjectByHash::otLEDGER) + { + // Ledger header fallback — look up by hash in the + // in-memory ledger set and serialize the header in the + // same wire format used by the node store. + if (auto ledger = + app_.getLedgerMaster().getLedgerByHash(hash)) + { + Serializer s(sizeof(LedgerInfo) + 4); + s.add32(HashPrefix::ledgerMaster); + addRaw(ledger->info(), s); + treeBlob = std::move(s.modData()); + dataPtr = treeBlob.data(); + dataSize = treeBlob.size(); + } + } + + if (dataPtr) { protocol::TMIndexedObject& newObj = *reply.add_objects(); newObj.set_hash(hash.begin(), hash.size()); - newObj.set_data( - &nodeObject->getData().front(), - nodeObject->getData().size()); + newObj.set_data(dataPtr, dataSize); if (obj.has_nodeid()) newObj.set_index(obj.nodeid()); diff --git a/src/xrpld/shamap/detail/SHAMapSync.cpp b/src/xrpld/shamap/detail/SHAMapSync.cpp index 4f93825751..335acf50fb 100644 --- a/src/xrpld/shamap/detail/SHAMapSync.cpp +++ b/src/xrpld/shamap/detail/SHAMapSync.cpp @@ -21,8 +21,25 @@ #include #include +#include +#include + namespace ripple { +namespace { + +bool +useFullBelowCache() +{ + static bool const use = [] { + char const* e = std::getenv("XAHAU_RWDB_NULL"); + return !(e && *e && std::string_view{e} != "0"); + }(); + return use; +} + +} // namespace + void SHAMap::visitLeaves( std::function const& @@ -191,7 +208,7 @@ SHAMap::gmn_ProcessNodes(MissingNodes& mn, MissingNodes::StackEntry& se) fullBelow = false; } else if ( - !backed_ || + !backed_ || !useFullBelowCache() || !f_.getFullBelowCache()->touch_if_exists(childHash.as_uint256())) { bool pending = false; @@ -228,7 +245,9 @@ SHAMap::gmn_ProcessNodes(MissingNodes& mn, MissingNodes::StackEntry& se) } else if ( d->isInner() && - !static_cast(d)->isFullBelow(mn.generation_)) + (!useFullBelowCache() || + !static_cast(d)->isFullBelow( + mn.generation_))) { mn.stack_.push(se); @@ -248,7 +267,7 @@ SHAMap::gmn_ProcessNodes(MissingNodes& mn, MissingNodes::StackEntry& se) if (fullBelow) { // No partial node encountered below this node node->setFullBelowGen(mn.generation_); - if (backed_) + if (backed_ && useFullBelowCache()) { f_.getFullBelowCache()->insert(node->getHash().as_uint256()); } @@ -326,8 +345,9 @@ SHAMap::getMissingNodes(int max, SHAMapSyncFilter* filter) f_.getFullBelowCache()->getGeneration()); if (!root_->isInner() || - std::static_pointer_cast(root_)->isFullBelow( - mn.generation_)) + (useFullBelowCache() && + std::static_pointer_cast(root_)->isFullBelow( + mn.generation_))) { clearSynching(); return std::move(mn.missingNodes_); @@ -397,7 +417,8 @@ SHAMap::getMissingNodes(int max, SHAMapSyncFilter* filter) { // Recheck nodes we could not finish before for (auto const& [innerNode, nodeId] : mn.resumes_) - if (!innerNode->isFullBelow(mn.generation_)) + if (!useFullBelowCache() || + !innerNode->isFullBelow(mn.generation_)) mn.stack_.push(std::make_tuple( innerNode, nodeId, rand_int(255), 0, true)); @@ -592,7 +613,8 @@ SHAMap::addKnownNode( auto iNode = root_.get(); while (iNode->isInner() && - !static_cast(iNode)->isFullBelow(generation) && + (!useFullBelowCache() || + !static_cast(iNode)->isFullBelow(generation)) && (iNodeID.getDepth() < node.getDepth())) { int branch = selectBranch(iNodeID, node.getNodeID()); @@ -605,7 +627,8 @@ SHAMap::addKnownNode( } auto childHash = inner->getChildHash(branch); - if (f_.getFullBelowCache()->touch_if_exists(childHash.as_uint256())) + if (useFullBelowCache() && + f_.getFullBelowCache()->touch_if_exists(childHash.as_uint256())) { return SHAMapAddNode::duplicate(); } From 858cc20c51e6afdb1ffcd31b43e14b0c3d3b5971 Mon Sep 17 00:00:00 2001 From: Nicholas Dudfield Date: Mon, 13 Apr 2026 13:48:32 +0700 Subject: [PATCH 09/17] feat: improve base ledger selection for priming in InboundLedger - Search both LedgerMaster and InboundLedgers for the closest fully wired base. - Implement sameChainDistance helper to accurately calculate distance between ledgers on the same chain. - Use findBestFullyWiredBase to minimize the 'prime walk' delta. --- src/xrpld/app/ledger/InboundLedgers.h | 4 + src/xrpld/app/ledger/detail/InboundLedger.cpp | 91 +++++++++++++++++-- .../app/ledger/detail/InboundLedgers.cpp | 91 +++++++++++++++++++ 3 files changed, 180 insertions(+), 6 deletions(-) diff --git a/src/xrpld/app/ledger/InboundLedgers.h b/src/xrpld/app/ledger/InboundLedgers.h index 6e6f893697..1be097554d 100644 --- a/src/xrpld/app/ledger/InboundLedgers.h +++ b/src/xrpld/app/ledger/InboundLedgers.h @@ -87,6 +87,10 @@ class InboundLedgers virtual void onLedgerFetched() = 0; + virtual std::shared_ptr + getClosestFullyWiredLedger( + std::shared_ptr const& targetLedger) = 0; + virtual void gotFetchPack() = 0; virtual void diff --git a/src/xrpld/app/ledger/detail/InboundLedger.cpp b/src/xrpld/app/ledger/detail/InboundLedger.cpp index d55f457024..77d22aaf9a 100644 --- a/src/xrpld/app/ledger/detail/InboundLedger.cpp +++ b/src/xrpld/app/ledger/detail/InboundLedger.cpp @@ -69,6 +69,87 @@ wireCompleteSHAMap(Map const& map) return leaves; } +std::optional +sameChainDistance( + std::shared_ptr const& targetLedger, + std::shared_ptr const& candidate, + beast::Journal journal) +{ + if (!targetLedger || !candidate || !candidate->isFullyWired()) + return std::nullopt; + + if (candidate->info().hash == targetLedger->info().hash) + return 0; + + bool sameChain = false; + try + { + if (candidate->info().seq < targetLedger->info().seq) + { + if (auto const hash = + hashOfSeq(*targetLedger, candidate->info().seq, journal); + hash && *hash == candidate->info().hash) + { + sameChain = true; + } + } + else if (candidate->info().seq > targetLedger->info().seq) + { + if (auto const hash = + hashOfSeq(*candidate, targetLedger->info().seq, journal); + hash && *hash == targetLedger->info().hash) + { + sameChain = true; + } + } + } + catch (std::exception const&) + { + sameChain = false; + } + + if (!sameChain) + return std::nullopt; + + return candidate->info().seq < targetLedger->info().seq + ? targetLedger->info().seq - candidate->info().seq + : candidate->info().seq - targetLedger->info().seq; +} + +std::shared_ptr +chooseCloserBase( + std::shared_ptr const& targetLedger, + std::shared_ptr const& first, + std::shared_ptr const& second, + beast::Journal journal) +{ + auto const firstDistance = sameChainDistance(targetLedger, first, journal); + auto const secondDistance = + sameChainDistance(targetLedger, second, journal); + + if (firstDistance && secondDistance) + return *firstDistance <= *secondDistance ? first : second; + if (firstDistance) + return first; + if (secondDistance) + return second; + return {}; +} + +std::shared_ptr +findBestFullyWiredBase( + Application& app, + std::shared_ptr const& targetLedger, + beast::Journal journal) +{ + auto const ledgerMasterBase = + app.getLedgerMaster().getClosestFullyWiredLedger(targetLedger); + auto const inboundBase = + app.getInboundLedgers().getClosestFullyWiredLedger(targetLedger); + return chooseCloserBase( + targetLedger, inboundBase, ledgerMasterBase, journal); +} + bool primeInboundLedgerForUse( std::shared_ptr const& ledger, @@ -103,8 +184,8 @@ primeInboundLedgerForUse( ledger->setFullyWired(); JLOG(journal.info()) << context << ": fully wired ledger " << ledger->info().seq << " (" - << stateNodes << " changed state nodes vs base ledger, " << txLeaves - << " tx leaves)"; + << stateNodes << " changed state nodes vs base ledger " + << baseLedger->info().seq << ", " << txLeaves << " tx leaves)"; return true; } catch (SHAMapMissingNode const& e) @@ -196,8 +277,7 @@ InboundLedger::init(ScopedLockType& collectionLock) JLOG(journal_.debug()) << "Acquiring ledger we already have in " << " local store. " << hash_; - auto const baseLedger = - app_.getLedgerMaster().getClosestFullyWiredLedger(mLedger); + auto const baseLedger = findBestFullyWiredBase(app_, mLedger, journal_); if (!primeInboundLedgerForUse( mLedger, baseLedger, journal_, "InboundLedger::init")) { @@ -535,8 +615,7 @@ InboundLedger::done() if (complete_ && !failed_ && mLedger) { - auto const baseLedger = - app_.getLedgerMaster().getClosestFullyWiredLedger(mLedger); + auto const baseLedger = findBestFullyWiredBase(app_, mLedger, journal_); if (!primeInboundLedgerForUse( mLedger, baseLedger, journal_, "InboundLedger::done")) { diff --git a/src/xrpld/app/ledger/detail/InboundLedgers.cpp b/src/xrpld/app/ledger/detail/InboundLedgers.cpp index 99a26ce8f9..d0153e134a 100644 --- a/src/xrpld/app/ledger/detail/InboundLedgers.cpp +++ b/src/xrpld/app/ledger/detail/InboundLedgers.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -31,12 +32,64 @@ #include #include +#include #include #include #include namespace ripple { +namespace { + +std::optional +sameChainDistance( + std::shared_ptr const& targetLedger, + std::shared_ptr const& candidate, + beast::Journal journal) +{ + if (!targetLedger || !candidate || !candidate->isFullyWired()) + return std::nullopt; + + if (candidate->info().hash == targetLedger->info().hash) + return 0; + + bool sameChain = false; + try + { + if (candidate->info().seq < targetLedger->info().seq) + { + if (auto const hash = + hashOfSeq(*targetLedger, candidate->info().seq, journal); + hash && *hash == candidate->info().hash) + { + sameChain = true; + } + } + else if (candidate->info().seq > targetLedger->info().seq) + { + if (auto const hash = + hashOfSeq(*candidate, targetLedger->info().seq, journal); + hash && *hash == targetLedger->info().hash) + { + sameChain = true; + } + } + } + catch (std::exception const&) + { + sameChain = false; + } + + if (!sameChain) + return std::nullopt; + + return candidate->info().seq < targetLedger->info().seq + ? targetLedger->info().seq - candidate->info().seq + : candidate->info().seq - targetLedger->info().seq; +} + +} // namespace + class InboundLedgersImp : public InboundLedgers { private: @@ -315,6 +368,44 @@ class InboundLedgersImp : public InboundLedgers fetchRate_.add(1, m_clock.now()); } + std::shared_ptr + getClosestFullyWiredLedger( + std::shared_ptr const& targetLedger) override + { + if (!targetLedger) + return {}; + + std::vector> candidates; + { + ScopedLockType sl(mLock); + candidates.reserve(mLedgers.size()); + for (auto const& [hash, inbound] : mLedgers) + { + (void)hash; + if (auto const ledger = inbound->getLedger(); + ledger && ledger->isFullyWired()) + { + candidates.push_back(ledger); + } + } + } + + std::shared_ptr best; + auto bestDistance = std::numeric_limits::max(); + for (auto const& candidate : candidates) + { + if (auto const distance = + sameChainDistance(targetLedger, candidate, j_); + distance && *distance < bestDistance) + { + best = candidate; + bestDistance = *distance; + } + } + + return best; + } + Json::Value getInfo() override { From c9577788fecfda5d10f53dec3d1d3e2fe4593222 Mon Sep 17 00:00:00 2001 From: Nicholas Dudfield Date: Mon, 13 Apr 2026 13:58:37 +0700 Subject: [PATCH 10/17] fix: exclude self from priming base selection --- src/xrpld/app/ledger/detail/InboundLedger.cpp | 2 +- src/xrpld/app/ledger/detail/InboundLedgers.cpp | 2 +- src/xrpld/app/ledger/detail/LedgerMaster.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/xrpld/app/ledger/detail/InboundLedger.cpp b/src/xrpld/app/ledger/detail/InboundLedger.cpp index 77d22aaf9a..d3c4fdbc4a 100644 --- a/src/xrpld/app/ledger/detail/InboundLedger.cpp +++ b/src/xrpld/app/ledger/detail/InboundLedger.cpp @@ -79,7 +79,7 @@ sameChainDistance( return std::nullopt; if (candidate->info().hash == targetLedger->info().hash) - return 0; + return std::nullopt; bool sameChain = false; try diff --git a/src/xrpld/app/ledger/detail/InboundLedgers.cpp b/src/xrpld/app/ledger/detail/InboundLedgers.cpp index d0153e134a..9f596c6e1b 100644 --- a/src/xrpld/app/ledger/detail/InboundLedgers.cpp +++ b/src/xrpld/app/ledger/detail/InboundLedgers.cpp @@ -51,7 +51,7 @@ sameChainDistance( return std::nullopt; if (candidate->info().hash == targetLedger->info().hash) - return 0; + return std::nullopt; bool sameChain = false; try diff --git a/src/xrpld/app/ledger/detail/LedgerMaster.cpp b/src/xrpld/app/ledger/detail/LedgerMaster.cpp index b59148a4e9..52814712a3 100644 --- a/src/xrpld/app/ledger/detail/LedgerMaster.cpp +++ b/src/xrpld/app/ledger/detail/LedgerMaster.cpp @@ -1885,7 +1885,7 @@ LedgerMaster::getClosestFullyWiredLedger( continue; if (candidate->info().hash == targetHash) - return candidate; + continue; bool sameChain = false; try From 67e1db7adec9ed9df29d292997a81e42b0f1e316 Mon Sep 17 00:00:00 2001 From: Nicholas Dudfield Date: Mon, 13 Apr 2026 14:27:49 +0700 Subject: [PATCH 11/17] fix: bound history priming ledger residency --- src/xrpld/app/ledger/InboundLedgers.h | 4 +- src/xrpld/app/ledger/detail/InboundLedger.cpp | 5 +- .../app/ledger/detail/InboundLedgers.cpp | 58 ++++++++++++++++--- 3 files changed, 56 insertions(+), 11 deletions(-) diff --git a/src/xrpld/app/ledger/InboundLedgers.h b/src/xrpld/app/ledger/InboundLedgers.h index 1be097554d..9ddee14283 100644 --- a/src/xrpld/app/ledger/InboundLedgers.h +++ b/src/xrpld/app/ledger/InboundLedgers.h @@ -83,9 +83,9 @@ class InboundLedgers virtual std::size_t fetchRate() = 0; - /** Called when a complete ledger is obtained. */ + /** Called when a complete history ledger is obtained. */ virtual void - onLedgerFetched() = 0; + onLedgerFetched(std::shared_ptr const& inbound) = 0; virtual std::shared_ptr getClosestFullyWiredLedger( diff --git a/src/xrpld/app/ledger/detail/InboundLedger.cpp b/src/xrpld/app/ledger/detail/InboundLedger.cpp index d3c4fdbc4a..d9690635d2 100644 --- a/src/xrpld/app/ledger/detail/InboundLedger.cpp +++ b/src/xrpld/app/ledger/detail/InboundLedger.cpp @@ -292,7 +292,10 @@ InboundLedger::init(ScopedLockType& collectionLock) mLedger->setImmutable(); if (mReason == Reason::HISTORY) + { + app_.getInboundLedgers().onLedgerFetched(shared_from_this()); return; + } app_.getLedgerMaster().storeLedger(mLedger); @@ -632,7 +635,7 @@ InboundLedger::done() switch (mReason) { case Reason::HISTORY: - app_.getInboundLedgers().onLedgerFetched(); + app_.getInboundLedgers().onLedgerFetched(shared_from_this()); break; default: app_.getLedgerMaster().storeLedger(mLedger); diff --git a/src/xrpld/app/ledger/detail/InboundLedgers.cpp b/src/xrpld/app/ledger/detail/InboundLedgers.cpp index 9f596c6e1b..3cf26bb79f 100644 --- a/src/xrpld/app/ledger/detail/InboundLedgers.cpp +++ b/src/xrpld/app/ledger/detail/InboundLedgers.cpp @@ -31,6 +31,7 @@ #include #include +#include #include #include #include @@ -41,6 +42,15 @@ namespace ripple { namespace { +std::size_t +historyPrimingCacheSize(Application& app) +{ + auto const configured = static_cast(app.config().LEDGER_HISTORY); + auto const bounded = std::min( + configured == 0 ? 8 : configured, 32); + return std::max(1, bounded); +} + std::optional sameChainDistance( std::shared_ptr const& targetLedger, @@ -114,6 +124,7 @@ class InboundLedgersImp : public InboundLedgers , m_clock(clock) , mRecentFailures(clock) , mCounter(collector->make_counter("ledger_fetches")) + , historyPrimingCacheSize_(historyPrimingCacheSize(app)) , mPeerSetBuilder(std::move(peerSetBuilder)) { } @@ -349,6 +360,7 @@ class InboundLedgersImp : public InboundLedgers ScopedLockType sl(mLock); mRecentFailures.clear(); + recentHistoryLedgers_.clear(); mLedgers.clear(); } @@ -359,11 +371,40 @@ class InboundLedgersImp : public InboundLedgers return 60 * fetchRate_.value(m_clock.now()); } - // Should only be called with an inboundledger that has - // a reason of history + // Should only be called with a complete inbound ledger that has + // a reason of history. void - onLedgerFetched() override + onLedgerFetched(std::shared_ptr const& inbound) override { + if (!inbound) + return; + + auto const ledger = inbound->getLedger(); + if (!ledger || !ledger->isFullyWired()) + return; + + { + ScopedLockType sl(mLock); + if (auto const it = mLedgers.find(ledger->info().hash); + it != mLedgers.end() && it->second.get() == inbound.get()) + { + mLedgers.erase(it); + } + + for (auto it = recentHistoryLedgers_.begin(); + it != recentHistoryLedgers_.end();) + { + if (!*it || (*it)->info().hash == ledger->info().hash) + it = recentHistoryLedgers_.erase(it); + else + ++it; + } + + recentHistoryLedgers_.push_back(ledger); + while (recentHistoryLedgers_.size() > historyPrimingCacheSize_) + recentHistoryLedgers_.pop_front(); + } + std::lock_guard lock(fetchRateMutex_); fetchRate_.add(1, m_clock.now()); } @@ -378,12 +419,10 @@ class InboundLedgersImp : public InboundLedgers std::vector> candidates; { ScopedLockType sl(mLock); - candidates.reserve(mLedgers.size()); - for (auto const& [hash, inbound] : mLedgers) + candidates.reserve(recentHistoryLedgers_.size()); + for (auto const& ledger : recentHistoryLedgers_) { - (void)hash; - if (auto const ledger = inbound->getLedger(); - ledger && ledger->isFullyWired()) + if (ledger && ledger->isFullyWired()) { candidates.push_back(ledger); } @@ -525,6 +564,7 @@ class InboundLedgersImp : public InboundLedgers { ScopedLockType lock(mLock); stopping_ = true; + recentHistoryLedgers_.clear(); mLedgers.clear(); mRecentFailures.clear(); } @@ -545,10 +585,12 @@ class InboundLedgersImp : public InboundLedgers bool stopping_ = false; using MapType = hash_map>; MapType mLedgers; + std::deque> recentHistoryLedgers_; beast::aged_map mRecentFailures; beast::insight::Counter mCounter; + std::size_t const historyPrimingCacheSize_; std::unique_ptr mPeerSetBuilder; From e538a808e4b5d89e16cdab4094db10b96629e130 Mon Sep 17 00:00:00 2001 From: shortthefomo Date: Mon, 13 Apr 2026 09:21:03 -0400 Subject: [PATCH 12/17] Fix null-rdwb memory leak, TOCTOU race, and consolidate env vars --- src/test/app/LedgerReplay_test.cpp | 9 ++- src/xrpld/app/ledger/Ledger.cpp | 14 +---- src/xrpld/app/ledger/detail/InboundLedger.cpp | 17 +----- src/xrpld/app/misc/SHAMapStoreImp.cpp | 61 +++++++++++++++---- src/xrpld/app/misc/SHAMapStoreImp.h | 1 + src/xrpld/core/Config.h | 19 ++++++ src/xrpld/nodestore/DatabaseRotating.h | 8 --- src/xrpld/nodestore/backend/RWDBFactory.cpp | 24 ++------ .../nodestore/detail/DatabaseRotatingImp.cpp | 19 +----- .../nodestore/detail/DatabaseRotatingImp.h | 3 - src/xrpld/shamap/detail/SHAMapSync.cpp | 1 + 11 files changed, 88 insertions(+), 88 deletions(-) diff --git a/src/test/app/LedgerReplay_test.cpp b/src/test/app/LedgerReplay_test.cpp index 20b7f1d521..b94007cf17 100644 --- a/src/test/app/LedgerReplay_test.cpp +++ b/src/test/app/LedgerReplay_test.cpp @@ -169,10 +169,17 @@ class MagicInboundLedgers : public InboundLedgers } virtual void - onLedgerFetched() override + onLedgerFetched(std::shared_ptr const&) override { } + virtual std::shared_ptr + getClosestFullyWiredLedger( + std::shared_ptr const&) override + { + return {}; + } + virtual void gotFetchPack() override { diff --git a/src/xrpld/app/ledger/Ledger.cpp b/src/xrpld/app/ledger/Ledger.cpp index f2d71946aa..80a632a3d2 100644 --- a/src/xrpld/app/ledger/Ledger.cpp +++ b/src/xrpld/app/ledger/Ledger.cpp @@ -50,8 +50,6 @@ #include #include #include -#include -#include #include #include @@ -63,16 +61,6 @@ create_genesis_t const create_genesis{}; namespace { -bool -isRWDBNullMode() -{ - static bool const v = [] { - char const* e = std::getenv("XAHAU_RWDB_NULL"); - return e && *e && std::string_view{e} != "0"; - }(); - return v; -} - template std::size_t wireCompleteSHAMap(Map const& map) @@ -424,7 +412,7 @@ Ledger::setImmutable(bool rehash) bool Ledger::fullWireForUse(beast::Journal journal, char const* context) const { - if (!isRWDBNullMode() || isFullyWired()) + if (!Config::null_backend() || isFullyWired()) return true; try diff --git a/src/xrpld/app/ledger/detail/InboundLedger.cpp b/src/xrpld/app/ledger/detail/InboundLedger.cpp index d9690635d2..98110ffa13 100644 --- a/src/xrpld/app/ledger/detail/InboundLedger.cpp +++ b/src/xrpld/app/ledger/detail/InboundLedger.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -35,10 +36,8 @@ #include #include -#include #include #include -#include namespace ripple { @@ -46,16 +45,6 @@ using namespace std::chrono_literals; namespace { -bool -isRWDBNullMode() -{ - static bool const v = [] { - char const* e = std::getenv("XAHAU_RWDB_NULL"); - return e && *e && std::string_view{e} != "0"; - }(); - return v; -} - template std::size_t wireCompleteSHAMap(Map const& map) @@ -157,7 +146,7 @@ primeInboundLedgerForUse( beast::Journal journal, char const* context) { - if (!isRWDBNullMode()) + if (!Config::null_backend()) return true; if (ledger->isFullyWired()) @@ -649,7 +638,7 @@ InboundLedger::done() jtLEDGER_DATA, "AcquisitionDone", [self = shared_from_this()]() { if (self->complete_ && !self->failed_) { - if (!isRWDBNullMode() && self->mReason != Reason::HISTORY) + if (!Config::null_backend() && self->mReason != Reason::HISTORY) { // Prime the state tree BEFORE checkAccept so consensus // never sees a lazy tree. Runs off any inbound lock — diff --git a/src/xrpld/app/misc/SHAMapStoreImp.cpp b/src/xrpld/app/misc/SHAMapStoreImp.cpp index 13ce2390fa..8760428a71 100644 --- a/src/xrpld/app/misc/SHAMapStoreImp.cpp +++ b/src/xrpld/app/misc/SHAMapStoreImp.cpp @@ -117,6 +117,20 @@ SHAMapStoreImp::SHAMapStoreImp( get_if_exists(section, "online_delete", deleteInterval_); isMemoryBackend_ = boost::iequals(get(section, "type"), "rwdb"); + isNullBackend_ = isMemoryBackend_ && Config::null_backend(); + + if (isNullBackend_) + { + if (config.LEDGER_HISTORY == 0) + { + Throw( + "RWDB null mode requires ledger_history > 0"); + } + JLOG(journal_.info()) + << "RWDB null mode: node store is ephemeral, " + << "retaining " << config.LEDGER_HISTORY + << " ledgers in memory"; + } // For RWDB, default online_delete to ledger_history only if user did not // explicitly set online_delete. Clamp to the minimum so an implicit @@ -337,17 +351,41 @@ SHAMapStoreImp::run() if (healthWait() == stopping) return; - if (isMemoryBackend_) + if (isNullBackend_) + { + // In null mode the backend never stores anything, so + // rotation is just bookkeeping: update lastRotated and + // clear old ledger caches. + JLOG(journal_.debug()) + << "RWDB null: skipping rotation copy, updating " + "lastRotated to " + << validatedSeq; + + ledgerMaster_->clearLedgerCachePrior(validatedSeq); + lastRotated = validatedSeq; + + auto newBackend = makeBackendRotating(); + dbRotating_->rotate( + std::move(newBackend), + [&](std::string const& writableName, + std::string const& archiveName) { + SavedState savedState; + savedState.writableDb = writableName; + savedState.archiveDb = archiveName; + savedState.lastRotated = lastRotated; + state_db_.setState(savedState); + ledgerMaster_->clearLedgerCachePrior(validatedSeq); + }); + + JLOG(journal_.warn()) << "finished rotation " << validatedSeq; + } + else if (isMemoryBackend_) { - // For RWDB: copy only the current validated ledger's live - // state nodes into a fresh backend that is not yet shared, - // avoiding both exclusive-lock contention on the live - // writable backend AND stale-node accumulation. - // - // copyArchiveTo would carry forward ALL archive entries - // (including stale nodes from older ledger versions that - // were promoted via fetch duplication), causing unbounded - // memory growth across rotation cycles. + // For RWDB (non-null): copy only the current validated + // ledger's live state nodes into a fresh backend that is + // not yet shared, avoiding both exclusive-lock contention + // on the live writable backend AND stale-node + // accumulation. JLOG(journal_.debug()) << "RWDB: copying live state for rotation"; auto newBackend = makeBackendRotating(); std::uint64_t nodeCount = 0; @@ -358,9 +396,6 @@ SHAMapStoreImp::run() validatedLedger->stateMap().snapShot(false)->visitNodes( [&](SHAMapTreeNode& node) -> bool { auto const hash = node.getHash().as_uint256(); - // Fetch the NodeObject from the rotating DB - // (checks writable then archive) and store it - // directly in the new unshared backend. auto obj = dbRotating_->fetchNodeObject( hash, 0, diff --git a/src/xrpld/app/misc/SHAMapStoreImp.h b/src/xrpld/app/misc/SHAMapStoreImp.h index f372df5810..c5a6720df3 100644 --- a/src/xrpld/app/misc/SHAMapStoreImp.h +++ b/src/xrpld/app/misc/SHAMapStoreImp.h @@ -102,6 +102,7 @@ class SHAMapStoreImp : public SHAMapStore std::uint32_t deleteInterval_ = 0; bool advisoryDelete_ = false; bool isMemoryBackend_ = false; + bool isNullBackend_ = false; std::uint32_t deleteBatch_ = 100; std::chrono::milliseconds backOff_{100}; std::chrono::seconds ageThreshold_{60}; diff --git a/src/xrpld/core/Config.h b/src/xrpld/core/Config.h index f9573bab64..34d9a40640 100644 --- a/src/xrpld/core/Config.h +++ b/src/xrpld/core/Config.h @@ -36,9 +36,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -375,6 +377,23 @@ class Config : public BasicConfig return isMem; } + /** Returns true when the RWDB backend is running in null mode. + + In null mode the in-memory node store never persists or retrieves + objects — nodes are retained purely through the Ledger -> SHAMap + shared_ptr retention chain. Activated via the XAHAU_RWDB_NULL + environment variable. + */ + static bool + null_backend() + { + static bool const v = [] { + char const* e = std::getenv("XAHAU_RWDB_NULL"); + return e && *e && std::string_view(e) != "0"; + }(); + return v; + } + bool useTxTables() const { diff --git a/src/xrpld/nodestore/DatabaseRotating.h b/src/xrpld/nodestore/DatabaseRotating.h index 90c900e5a9..d0a160a734 100644 --- a/src/xrpld/nodestore/DatabaseRotating.h +++ b/src/xrpld/nodestore/DatabaseRotating.h @@ -56,14 +56,6 @@ class DatabaseRotating : public Database std::string const& writableName, std::string const& archiveName)> const& f) = 0; - /** Populate @a dest with every object in the archive backend. - - Used by in-memory (RWDB) backends to pre-populate a new writable - backend before rotation, avoiding per-node write-lock contention on - the live writable backend. @a dest must not yet be shared. - */ - virtual void - copyArchiveTo(Backend& dest) = 0; }; } // namespace NodeStore diff --git a/src/xrpld/nodestore/backend/RWDBFactory.cpp b/src/xrpld/nodestore/backend/RWDBFactory.cpp index 970fa869b8..93643a1724 100644 --- a/src/xrpld/nodestore/backend/RWDBFactory.cpp +++ b/src/xrpld/nodestore/backend/RWDBFactory.cpp @@ -3,16 +3,15 @@ #include #include #include +#include #include #include #include #include #include -#include #include #include #include -#include namespace ripple { namespace NodeStore { @@ -97,11 +96,7 @@ class RWDBBackend : public Backend static bool nullMode() { - static bool const v = [] { - char const* e = std::getenv("XAHAU_RWDB_NULL"); - return e && *e && std::string_view{e} != "0"; - }(); - return v; + return Config::null_backend(); } Status @@ -149,23 +144,12 @@ class RWDBBackend : public Backend void store(std::shared_ptr const& object) override { - if (!isOpen_) - return; - if (!object) return; if (nullMode()) return; - static bool const discardHotAccountNode = [] { - char const* v = std::getenv("XAHAU_RWDB_DISCARD_HOT_ACCOUNT_NODE"); - return v && *v && std::string_view{v} != "0"; - }(); - - if (discardHotAccountNode && object->getType() == hotACCOUNT_NODE) - return; - EncodedBlob encoded(object); nudb::detail::buffer bf; auto const result = @@ -175,7 +159,9 @@ class RWDBBackend : public Backend static_cast(result.first), static_cast(result.first) + result.second); - std::lock_guard lock(mutex_); + std::unique_lock lock(mutex_); + if (!isOpen_) + return; table_[object->getHash()] = std::move(compressed); } diff --git a/src/xrpld/nodestore/detail/DatabaseRotatingImp.cpp b/src/xrpld/nodestore/detail/DatabaseRotatingImp.cpp index aec2d4953b..b5097394c4 100644 --- a/src/xrpld/nodestore/detail/DatabaseRotatingImp.cpp +++ b/src/xrpld/nodestore/detail/DatabaseRotatingImp.cpp @@ -44,21 +44,6 @@ DatabaseRotatingImp::DatabaseRotatingImp( fdRequired_ += archiveBackend_->fdRequired(); } -void -DatabaseRotatingImp::copyArchiveTo(Backend& dest) -{ - // Snapshot the archive backend pointer under lock, then iterate it - // outside the lock. dest is not yet shared so its store() calls are - // uncontested — no live-backend write-lock contention. - auto archive = [&] { - std::lock_guard const lock(mutex_); - return archiveBackend_; - }(); - - archive->for_each( - [&](std::shared_ptr obj) { dest.store(obj); }); -} - void DatabaseRotatingImp::rotate( std::unique_ptr&& newBackend, @@ -272,8 +257,8 @@ DatabaseRotatingImp::fetchNodeObject( } // Update writable backend with data from the archive backend - // if (duplicate) - writable->store(nodeObject); + if (duplicate) + writable->store(nodeObject); } } diff --git a/src/xrpld/nodestore/detail/DatabaseRotatingImp.h b/src/xrpld/nodestore/detail/DatabaseRotatingImp.h index 6e42bd1a21..9052de07e9 100644 --- a/src/xrpld/nodestore/detail/DatabaseRotatingImp.h +++ b/src/xrpld/nodestore/detail/DatabaseRotatingImp.h @@ -51,9 +51,6 @@ class DatabaseRotatingImp : public DatabaseRotating stop(); } - void - copyArchiveTo(Backend& dest) override; - void rotate( std::unique_ptr&& newBackend, diff --git a/src/xrpld/shamap/detail/SHAMapSync.cpp b/src/xrpld/shamap/detail/SHAMapSync.cpp index 335acf50fb..2a2ebbffa2 100644 --- a/src/xrpld/shamap/detail/SHAMapSync.cpp +++ b/src/xrpld/shamap/detail/SHAMapSync.cpp @@ -28,6 +28,7 @@ namespace ripple { namespace { +// Mirror of Config::null_backend() — shamap cannot depend on xrpld.core. bool useFullBelowCache() { From 0edae6de14e7a85f0cee219727cd989932cc72f1 Mon Sep 17 00:00:00 2001 From: shortthefomo Date: Mon, 13 Apr 2026 19:58:12 -0400 Subject: [PATCH 13/17] Port fixes from rippled null-rdwb-experiment-mutex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace std::mutex with reader_preferring_shared_mutex in DatabaseRotatingImp (shared_lock for reads, unique_lock for writes) - Skip expensive full state tree walk when no base ledger exists in primeInboundLedgerForUse — trust sync pinning, just wire tx map - Allow null_backend to be set via [node_db] config section - Remove early RWDB online_delete requirement from Config (now defaulted by SHAMapStoreImp) - Fix SHAMapSync: only gate shared FullBelowCache operations behind useFullBelowCache(), not local per-node isFullBelow() checks - Update Config_test for removed RWDB online_delete requirement --- src/test/core/Config_test.cpp | 57 +++++++++---------- src/xrpld/app/ledger/detail/InboundLedger.cpp | 32 +++++++++-- src/xrpld/app/misc/SHAMapStoreImp.cpp | 18 +++++- src/xrpld/core/detail/Config.cpp | 17 ------ .../nodestore/detail/DatabaseRotatingImp.cpp | 22 +++---- .../nodestore/detail/DatabaseRotatingImp.h | 5 +- src/xrpld/shamap/detail/SHAMapSync.cpp | 20 +++---- 7 files changed, 94 insertions(+), 77 deletions(-) diff --git a/src/test/core/Config_test.cpp b/src/test/core/Config_test.cpp index a99d48f3aa..f270786727 100644 --- a/src/test/core/Config_test.cpp +++ b/src/test/core/Config_test.cpp @@ -1091,8 +1091,9 @@ trustthesevalidators.gov } catch (std::exception const& ex) { - BEAST_EXPECT(std::string_view(ex.what()).starts_with( - "Invalid value '0' for key 'port'")); + BEAST_EXPECT( + std::string_view(ex.what()).starts_with( + "Invalid value '0' for key 'port'")); } } @@ -1102,23 +1103,24 @@ trustthesevalidators.gov Config cfg; /* NOTE: this string includes some explicit * space chars in order to verify proper trimming */ - std::string toLoad(R"( + std::string toLoad( + R"( [port_rpc])" - "\x20" - R"( + "\x20" + R"( # comment # indented comment )" - "\x20\x20" - R"( + "\x20\x20" + R"( [ips])" - "\x20" - R"( + "\x20" + R"( r.ripple.com 51235 [ips_fixed])" - "\x20\x20" - R"( + "\x20\x20" + R"( # COMMENT s1.ripple.com 51235 s2.ripple.com 51235 @@ -1144,23 +1146,24 @@ r.ripple.com 51235 Config cfg; /* NOTE: this string includes some explicit * space chars in order to verify proper trimming */ - std::string toLoad(R"( + std::string toLoad( + R"( [port_rpc])" - "\x20" - R"( + "\x20" + R"( # comment # indented comment )" - "\x20\x20" - R"( + "\x20\x20" + R"( [ips])" - "\x20" - R"( + "\x20" + R"( r.ripple.com:51235 [ips_fixed])" - "\x20\x20" - R"( + "\x20\x20" + R"( # COMMENT s1.ripple.com:51235 s2.ripple.com 51235 @@ -1447,8 +1450,8 @@ r.ripple.com:51235 } } - // Test 2: RWDB without online_delete NOT in standalone mode (should - // throw) + // Test 2: RWDB without online_delete NOT in standalone mode + // (now allowed — SHAMapStoreImp defaults it to ledger_history) { Config c; std::string toLoad = @@ -1459,15 +1462,11 @@ r.ripple.com:51235 try { c.loadFromString(toLoad); - fail("Expected exception for RWDB without online_delete"); + pass(); } - catch (std::runtime_error const& e) + catch (std::runtime_error const&) { - BEAST_EXPECT( - std::string(e.what()).find( - "RWDB (in-memory backend) requires online_delete") != - std::string::npos); - pass(); + fail("Should not throw for RWDB without online_delete"); } } diff --git a/src/xrpld/app/ledger/detail/InboundLedger.cpp b/src/xrpld/app/ledger/detail/InboundLedger.cpp index 98110ffa13..1c6a29e3fe 100644 --- a/src/xrpld/app/ledger/detail/InboundLedger.cpp +++ b/src/xrpld/app/ledger/detail/InboundLedger.cpp @@ -24,8 +24,8 @@ #include #include #include -#include #include +#include #include #include #include @@ -154,7 +154,27 @@ primeInboundLedgerForUse( if (!baseLedger || !baseLedger->isFullyWired()) { - return ledger->fullWireForUse(journal, context); + // No base ledger available for a delta walk. The full state tree + // walk (wireCompleteSHAMap on 70M+ leaves) is too expensive — on + // x86 it takes longer than a consensus round, preventing the node + // from ever catching up. Sync already pinned every child in the + // tree via canonicalizeChild (in descendAsync/addKnownNode), so + // the state map is fully wired. Just wire the (tiny) tx map. + try + { + auto const txLeaves = wireCompleteSHAMap(ledger->txMap()); + ledger->setFullyWired(); + JLOG(journal.info()) + << context << ": wired ledger " << ledger->info().seq + << " (sync-pinned state, " << txLeaves << " tx leaves)"; + return true; + } + catch (SHAMapMissingNode const& e) + { + JLOG(journal.warn()) << context << ": incomplete ledger " + << ledger->info().seq << ": " << e.what(); + return false; + } } try @@ -624,7 +644,8 @@ InboundLedger::done() switch (mReason) { case Reason::HISTORY: - app_.getInboundLedgers().onLedgerFetched(shared_from_this()); + app_.getInboundLedgers().onLedgerFetched( + shared_from_this()); break; default: app_.getLedgerMaster().storeLedger(mLedger); @@ -1224,8 +1245,9 @@ InboundLedger::getNeededHashes() mLedger->txMap().family().db(), app_.getLedgerMaster()); for (auto const& h : neededTxHashes(4, &filter)) { - ret.push_back(std::make_pair( - protocol::TMGetObjectByHash::otTRANSACTION_NODE, h)); + ret.push_back( + std::make_pair( + protocol::TMGetObjectByHash::otTRANSACTION_NODE, h)); } } diff --git a/src/xrpld/app/misc/SHAMapStoreImp.cpp b/src/xrpld/app/misc/SHAMapStoreImp.cpp index 8760428a71..7fb81a0746 100644 --- a/src/xrpld/app/misc/SHAMapStoreImp.cpp +++ b/src/xrpld/app/misc/SHAMapStoreImp.cpp @@ -117,6 +117,18 @@ SHAMapStoreImp::SHAMapStoreImp( get_if_exists(section, "online_delete", deleteInterval_); isMemoryBackend_ = boost::iequals(get(section, "type"), "rwdb"); + + // Allow null_backend to be set via config as well as env var. + // If the config key is present, propagate it to the environment so + // that libxrpl helpers (which cannot access Config) pick it up. + // This runs single-threaded during startup, before worker threads. + if (isMemoryBackend_ && section.exists("null_backend")) + { + auto const val = get(section, "null_backend"); + if (val == "1" || boost::iequals(val, "true")) + ::setenv("XAHAU_RWDB_NULL", "1", 1); + } + isNullBackend_ = isMemoryBackend_ && Config::null_backend(); if (isNullBackend_) @@ -128,8 +140,7 @@ SHAMapStoreImp::SHAMapStoreImp( } JLOG(journal_.info()) << "RWDB null mode: node store is ephemeral, " - << "retaining " << config.LEDGER_HISTORY - << " ledgers in memory"; + << "retaining " << config.LEDGER_HISTORY << " ledgers in memory"; } // For RWDB, default online_delete to ledger_history only if user did not @@ -386,7 +397,8 @@ SHAMapStoreImp::run() // not yet shared, avoiding both exclusive-lock contention // on the live writable backend AND stale-node // accumulation. - JLOG(journal_.debug()) << "RWDB: copying live state for rotation"; + JLOG(journal_.debug()) + << "RWDB: copying live state for rotation"; auto newBackend = makeBackendRotating(); std::uint64_t nodeCount = 0; bool aborted = false; diff --git a/src/xrpld/core/detail/Config.cpp b/src/xrpld/core/detail/Config.cpp index 34ae49c35e..b642771e73 100644 --- a/src/xrpld/core/detail/Config.cpp +++ b/src/xrpld/core/detail/Config.cpp @@ -1060,23 +1060,6 @@ Config::loadFromString(std::string const& fileContents) "the maximum number of allowed peers (peers_max)"); } } - - if (!RUN_STANDALONE) - { - auto db_section = section(ConfigSection::nodeDatabase()); - if (auto type = get(db_section, "type", ""); type == "rwdb") - { - if (auto delete_interval = get(db_section, "online_delete", 0); - delete_interval == 0) - { - Throw( - "RWDB (in-memory backend) requires online_delete to " - "prevent OOM " - "Exception: standalone mode (used by tests) doesn't need " - "online_delete"); - } - } - } } boost::filesystem::path diff --git a/src/xrpld/nodestore/detail/DatabaseRotatingImp.cpp b/src/xrpld/nodestore/detail/DatabaseRotatingImp.cpp index b5097394c4..83432fbc15 100644 --- a/src/xrpld/nodestore/detail/DatabaseRotatingImp.cpp +++ b/src/xrpld/nodestore/detail/DatabaseRotatingImp.cpp @@ -22,6 +22,8 @@ #include #include +#include + namespace ripple { namespace NodeStore { @@ -59,7 +61,7 @@ DatabaseRotatingImp::rotate( // deleted. std::shared_ptr oldArchiveBackend; { - std::lock_guard lock(mutex_); + std::unique_lock const lock(mutex_); // Before rotating, ensure all pinned ledgers are in the writable // backend @@ -134,14 +136,14 @@ DatabaseRotatingImp::rotate( std::string DatabaseRotatingImp::getName() const { - std::lock_guard lock(mutex_); + std::shared_lock const lock(mutex_); return writableBackend_->getName(); } std::int32_t DatabaseRotatingImp::getWriteLoad() const { - std::lock_guard lock(mutex_); + std::shared_lock const lock(mutex_); return writableBackend_->getWriteLoad(); } @@ -149,7 +151,7 @@ void DatabaseRotatingImp::importDatabase(Database& source) { auto const backend = [&] { - std::lock_guard lock(mutex_); + std::shared_lock const lock(mutex_); return writableBackend_; }(); @@ -160,7 +162,7 @@ bool DatabaseRotatingImp::storeLedger(std::shared_ptr const& srcLedger) { auto const backend = [&] { - std::lock_guard lock(mutex_); + std::shared_lock const lock(mutex_); return writableBackend_; }(); @@ -170,7 +172,7 @@ DatabaseRotatingImp::storeLedger(std::shared_ptr const& srcLedger) void DatabaseRotatingImp::sync() { - std::lock_guard lock(mutex_); + std::unique_lock const lock(mutex_); writableBackend_->sync(); } @@ -184,7 +186,7 @@ DatabaseRotatingImp::store( auto nObj = NodeObject::createObject(type, std::move(data), hash); auto const backend = [&] { - std::lock_guard lock(mutex_); + std::shared_lock const lock(mutex_); return writableBackend_; }(); @@ -238,7 +240,7 @@ DatabaseRotatingImp::fetchNodeObject( std::shared_ptr nodeObject; auto [writable, archive] = [&] { - std::lock_guard lock(mutex_); + std::shared_lock const lock(mutex_); return std::make_pair(writableBackend_, archiveBackend_); }(); @@ -252,7 +254,7 @@ DatabaseRotatingImp::fetchNodeObject( { { // Refresh the writable backend pointer - std::lock_guard lock(mutex_); + std::shared_lock const lock(mutex_); writable = writableBackend_; } @@ -273,7 +275,7 @@ DatabaseRotatingImp::for_each( std::function)> f) { auto [writable, archive] = [&] { - std::lock_guard lock(mutex_); + std::shared_lock const lock(mutex_); return std::make_pair(writableBackend_, archiveBackend_); }(); diff --git a/src/xrpld/nodestore/detail/DatabaseRotatingImp.h b/src/xrpld/nodestore/detail/DatabaseRotatingImp.h index 9052de07e9..23969abce9 100644 --- a/src/xrpld/nodestore/detail/DatabaseRotatingImp.h +++ b/src/xrpld/nodestore/detail/DatabaseRotatingImp.h @@ -22,7 +22,10 @@ #include +#include + #include +#include namespace ripple { namespace NodeStore { @@ -90,7 +93,7 @@ class DatabaseRotatingImp : public DatabaseRotating private: std::shared_ptr writableBackend_; std::shared_ptr archiveBackend_; - mutable std::mutex mutex_; + mutable reader_preferring_shared_mutex mutex_; std::shared_ptr fetchNodeObject( diff --git a/src/xrpld/shamap/detail/SHAMapSync.cpp b/src/xrpld/shamap/detail/SHAMapSync.cpp index 2a2ebbffa2..64f8a9cda3 100644 --- a/src/xrpld/shamap/detail/SHAMapSync.cpp +++ b/src/xrpld/shamap/detail/SHAMapSync.cpp @@ -246,9 +246,7 @@ SHAMap::gmn_ProcessNodes(MissingNodes& mn, MissingNodes::StackEntry& se) } else if ( d->isInner() && - (!useFullBelowCache() || - !static_cast(d)->isFullBelow( - mn.generation_))) + !static_cast(d)->isFullBelow(mn.generation_)) { mn.stack_.push(se); @@ -346,9 +344,8 @@ SHAMap::getMissingNodes(int max, SHAMapSyncFilter* filter) f_.getFullBelowCache()->getGeneration()); if (!root_->isInner() || - (useFullBelowCache() && - std::static_pointer_cast(root_)->isFullBelow( - mn.generation_))) + std::static_pointer_cast(root_)->isFullBelow( + mn.generation_)) { clearSynching(); return std::move(mn.missingNodes_); @@ -418,10 +415,10 @@ SHAMap::getMissingNodes(int max, SHAMapSyncFilter* filter) { // Recheck nodes we could not finish before for (auto const& [innerNode, nodeId] : mn.resumes_) - if (!useFullBelowCache() || - !innerNode->isFullBelow(mn.generation_)) - mn.stack_.push(std::make_tuple( - innerNode, nodeId, rand_int(255), 0, true)); + if (!innerNode->isFullBelow(mn.generation_)) + mn.stack_.push( + std::make_tuple( + innerNode, nodeId, rand_int(255), 0, true)); mn.resumes_.clear(); } @@ -614,8 +611,7 @@ SHAMap::addKnownNode( auto iNode = root_.get(); while (iNode->isInner() && - (!useFullBelowCache() || - !static_cast(iNode)->isFullBelow(generation)) && + !static_cast(iNode)->isFullBelow(generation) && (iNodeID.getDepth() < node.getDepth())) { int branch = selectBranch(iNodeID, node.getNodeID()); From 4772bed73efec3cf8e2a49d465d8c66a0583611a Mon Sep 17 00:00:00 2001 From: shortthefomo Date: Mon, 13 Apr 2026 20:16:38 -0400 Subject: [PATCH 14/17] Fix null-mode rotation: use clearCaches to also clear FullBelowCache The null-mode rotation path was calling clearLedgerCachePrior() directly instead of clearCaches(), which also clears the FullBelowCache. Stale 'full below' markers from a previous sync pass persisted across rotation, causing SHAMap sync to skip subtrees that actually need re-fetching, leading to the node oscillating between 'full' and 'syncing' states. --- src/xrpld/app/misc/SHAMapStoreImp.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/xrpld/app/misc/SHAMapStoreImp.cpp b/src/xrpld/app/misc/SHAMapStoreImp.cpp index 7fb81a0746..12c0eb8984 100644 --- a/src/xrpld/app/misc/SHAMapStoreImp.cpp +++ b/src/xrpld/app/misc/SHAMapStoreImp.cpp @@ -372,7 +372,7 @@ SHAMapStoreImp::run() "lastRotated to " << validatedSeq; - ledgerMaster_->clearLedgerCachePrior(validatedSeq); + clearCaches(validatedSeq); lastRotated = validatedSeq; auto newBackend = makeBackendRotating(); @@ -385,7 +385,7 @@ SHAMapStoreImp::run() savedState.archiveDb = archiveName; savedState.lastRotated = lastRotated; state_db_.setState(savedState); - ledgerMaster_->clearLedgerCachePrior(validatedSeq); + clearCaches(validatedSeq); }); JLOG(journal_.warn()) << "finished rotation " << validatedSeq; From 308e89f8dfcc71dbe5cd0edce5332e055c70dd26 Mon Sep 17 00:00:00 2001 From: shortthefomo Date: Tue, 14 Apr 2026 20:57:51 -0400 Subject: [PATCH 15/17] RWDB type always implies null backend: use non-rotating Database and skip rotation entirely --- src/xrpld/app/misc/SHAMapStoreImp.cpp | 147 ++++++++------------------ src/xrpld/app/misc/SHAMapStoreImp.h | 1 - 2 files changed, 43 insertions(+), 105 deletions(-) diff --git a/src/xrpld/app/misc/SHAMapStoreImp.cpp b/src/xrpld/app/misc/SHAMapStoreImp.cpp index 12c0eb8984..e2bfda1fc1 100644 --- a/src/xrpld/app/misc/SHAMapStoreImp.cpp +++ b/src/xrpld/app/misc/SHAMapStoreImp.cpp @@ -116,20 +116,13 @@ SHAMapStoreImp::SHAMapStoreImp( } get_if_exists(section, "online_delete", deleteInterval_); - isMemoryBackend_ = boost::iequals(get(section, "type"), "rwdb"); + isNullBackend_ = boost::iequals(get(section, "type"), "rwdb"); - // Allow null_backend to be set via config as well as env var. - // If the config key is present, propagate it to the environment so - // that libxrpl helpers (which cannot access Config) pick it up. - // This runs single-threaded during startup, before worker threads. - if (isMemoryBackend_ && section.exists("null_backend")) - { - auto const val = get(section, "null_backend"); - if (val == "1" || boost::iequals(val, "true")) - ::setenv("XAHAU_RWDB_NULL", "1", 1); - } - - isNullBackend_ = isMemoryBackend_ && Config::null_backend(); + // RWDB is always null-backend: the in-memory node store never + // persists or retrieves objects. Set the env var so that libxrpl + // helpers (which cannot access Config) can detect null mode. + if (isNullBackend_) + ::setenv("XAHAU_RWDB_NULL", "1", 1); if (isNullBackend_) { @@ -146,7 +139,7 @@ SHAMapStoreImp::SHAMapStoreImp( // For RWDB, default online_delete to ledger_history only if user did not // explicitly set online_delete. Clamp to the minimum so an implicit // value never triggers the "online_delete must be at least …" throw. - if (isMemoryBackend_ && deleteInterval_ == 0) + if (isNullBackend_ && deleteInterval_ == 0) { auto const minInterval = config.standalone() ? minimumDeletionIntervalSA_ @@ -191,7 +184,7 @@ SHAMapStoreImp::SHAMapStoreImp( } state_db_.init(config, dbName_); - if (!isMemoryBackend_) + if (!isNullBackend_) dbPaths(); } } @@ -216,7 +209,21 @@ SHAMapStoreImp::makeNodeStore(int readThreads) std::unique_ptr db; - if (deleteInterval_) + if (isNullBackend_) + { + // Null mode: create a plain (non-rotating) Database with a + // single NullBackend. No DatabaseRotatingImp, no rotation + // thread artifacts. dbRotating_ stays nullptr. + db = NodeStore::Manager::instance().make_Database( + megabytes(app_.config().getValueFor( + SizedItem::burstSize, std::nullopt)), + scheduler_, + readThreads, + nscfg, + app_.getJournal(nodeStoreName_)); + fdRequired_ += db->fdRequired(); + } + else if (deleteInterval_) { SavedState state = state_db_.getState(); @@ -364,98 +371,21 @@ SHAMapStoreImp::run() if (isNullBackend_) { - // In null mode the backend never stores anything, so - // rotation is just bookkeeping: update lastRotated and - // clear old ledger caches. - JLOG(journal_.debug()) - << "RWDB null: skipping rotation copy, updating " - "lastRotated to " + // In null mode the backend never stores anything. + // Skip clearCaches / makeBackendRotating / rotate + // entirely — the TreeNodeCache IS the node store and + // evicting it causes irrecoverable SHAMapMissingNode. + // Only sqlite cleanup (clearPrior above) is needed. + JLOG(journal_.info()) + << "RWDB null: skipping rotation, " + "updating lastRotated to " << validatedSeq; - clearCaches(validatedSeq); lastRotated = validatedSeq; + state_db_.setLastRotated(lastRotated); - auto newBackend = makeBackendRotating(); - dbRotating_->rotate( - std::move(newBackend), - [&](std::string const& writableName, - std::string const& archiveName) { - SavedState savedState; - savedState.writableDb = writableName; - savedState.archiveDb = archiveName; - savedState.lastRotated = lastRotated; - state_db_.setState(savedState); - clearCaches(validatedSeq); - }); - - JLOG(journal_.warn()) << "finished rotation " << validatedSeq; - } - else if (isMemoryBackend_) - { - // For RWDB (non-null): copy only the current validated - // ledger's live state nodes into a fresh backend that is - // not yet shared, avoiding both exclusive-lock contention - // on the live writable backend AND stale-node - // accumulation. - JLOG(journal_.debug()) - << "RWDB: copying live state for rotation"; - auto newBackend = makeBackendRotating(); - std::uint64_t nodeCount = 0; - bool aborted = false; - - try - { - validatedLedger->stateMap().snapShot(false)->visitNodes( - [&](SHAMapTreeNode& node) -> bool { - auto const hash = node.getHash().as_uint256(); - auto obj = dbRotating_->fetchNodeObject( - hash, - 0, - NodeStore::FetchType::synchronous, - false); - if (obj) - newBackend->store(obj); - - if ((++nodeCount % checkHealthInterval_) == 0) - { - if (healthWait() == stopping) - { - aborted = true; - return false; - } - } - return true; - }); - } - catch (SHAMapMissingNode const& e) - { - JLOG(journal_.error()) - << "Missing node while copying state before rotate: " - << e.what(); - continue; - } - - if (aborted) - return; - JLOG(journal_.debug()) - << "RWDB: copied " << nodeCount << " live nodes"; - - ledgerMaster_->clearLedgerCachePrior(validatedSeq); - lastRotated = validatedSeq; - - dbRotating_->rotate( - std::move(newBackend), - [&](std::string const& writableName, - std::string const& archiveName) { - SavedState savedState; - savedState.writableDb = writableName; - savedState.archiveDb = archiveName; - savedState.lastRotated = lastRotated; - state_db_.setState(savedState); - ledgerMaster_->clearLedgerCachePrior(validatedSeq); - }); - - JLOG(journal_.warn()) << "finished rotation " << validatedSeq; + JLOG(journal_.warn()) + << "finished null-mode cleanup " << validatedSeq; } else { @@ -698,6 +628,15 @@ void SHAMapStoreImp::clearCaches(LedgerIndex validatedSeq) { ledgerMaster_->clearLedgerCachePrior(validatedSeq); + + if (isNullBackend_) + { + // In null mode the TreeNodeCache is the only node store. + // Do NOT clear FullBelowCache or freshen TreeNodeCache — + // evicted entries are irrecoverable without a real backend. + return; + } + fullBelowCache_->clear(); } diff --git a/src/xrpld/app/misc/SHAMapStoreImp.h b/src/xrpld/app/misc/SHAMapStoreImp.h index c5a6720df3..8606e70ccb 100644 --- a/src/xrpld/app/misc/SHAMapStoreImp.h +++ b/src/xrpld/app/misc/SHAMapStoreImp.h @@ -101,7 +101,6 @@ class SHAMapStoreImp : public SHAMapStore std::uint32_t deleteInterval_ = 0; bool advisoryDelete_ = false; - bool isMemoryBackend_ = false; bool isNullBackend_ = false; std::uint32_t deleteBatch_ = 100; std::chrono::milliseconds backOff_{100}; From 3d927b0eb58819b4c6a60aa628b2f7c266dd6ec3 Mon Sep 17 00:00:00 2001 From: shortthefomo Date: Tue, 14 Apr 2026 20:59:10 -0400 Subject: [PATCH 16/17] Fix getJournal -> logs().journal() in null-mode makeNodeStore --- src/xrpld/app/misc/SHAMapStoreImp.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrpld/app/misc/SHAMapStoreImp.cpp b/src/xrpld/app/misc/SHAMapStoreImp.cpp index e2bfda1fc1..8b31b97121 100644 --- a/src/xrpld/app/misc/SHAMapStoreImp.cpp +++ b/src/xrpld/app/misc/SHAMapStoreImp.cpp @@ -220,7 +220,7 @@ SHAMapStoreImp::makeNodeStore(int readThreads) scheduler_, readThreads, nscfg, - app_.getJournal(nodeStoreName_)); + app_.logs().journal(nodeStoreName_)); fdRequired_ += db->fdRequired(); } else if (deleteInterval_) From c079ba0e906dc1b7f2e1543139d74c600e32316a Mon Sep 17 00:00:00 2001 From: Lathan Britz Date: Tue, 14 Apr 2026 23:30:45 -0400 Subject: [PATCH 17/17] fix brain fart to set huge mode nudb cache to 64mib --- src/xrpld/core/detail/Config.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xrpld/core/detail/Config.cpp b/src/xrpld/core/detail/Config.cpp index b642771e73..24aa4a7836 100644 --- a/src/xrpld/core/detail/Config.cpp +++ b/src/xrpld/core/detail/Config.cpp @@ -118,7 +118,7 @@ sizedItems {SizedItem::txnDBCache, {{ 4, 12, 24, 64, 128 }}}, {SizedItem::lgrDBCache, {{ 4, 8, 16, 32, 128 }}}, {SizedItem::openFinalLimit, {{ 8, 16, 32, 64, 128 }}}, - {SizedItem::burstSize, {{ 4, 8, 16, 32, 64*1024*1024 }}}, + {SizedItem::burstSize, {{ 4, 8, 16, 32, 64 }}}, {SizedItem::ramSizeGB, {{ 8, 12, 16, 24, 32 }}}, {SizedItem::accountIdCacheSize, {{ 20047, 50053, 77081, 150061, 300007 }}} }};