diff --git a/include/xrpl/protocol/Feature.h b/include/xrpl/protocol/Feature.h index eb975f39ae0..a2510c63000 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 = 80; +static constexpr std::size_t numFeatures = 81; /** 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/TxFlags.h b/include/xrpl/protocol/TxFlags.h index 4894f48a7f9..c293798f7d7 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -207,6 +207,10 @@ constexpr std::uint32_t tfDepositSubTx = constexpr std::uint32_t tfWithdrawMask = ~(tfUniversal | tfWithdrawSubTx); constexpr std::uint32_t tfDepositMask = ~(tfUniversal | tfDepositSubTx); +// AMMClawback flags: +constexpr std::uint32_t tfClawTwoAssets = 0x00000001; +constexpr std::uint32_t tfAMMClawbackMask = ~(tfUniversal | tfClawTwoAssets); + // BridgeModify flags: constexpr std::uint32_t tfClearAccountCreateAmount = 0x00010000; constexpr std::uint32_t tfBridgeModifyMask = ~(tfUniversal | tfClearAccountCreateAmount); diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 3a8d77e2bab..e5351be11c0 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -95,6 +95,7 @@ XRPL_FIX (1513, Supported::yes, VoteBehavior::DefaultYe XRPL_FEATURE(FlowCross, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(Flow, Supported::yes, VoteBehavior::DefaultYes) XRPL_FEATURE(OwnerPaysFee, Supported::no, VoteBehavior::DefaultNo) +XRPL_FEATURE(AMMClawback, Supported::yes, VoteBehavior::DefaultYes) // The following amendments are obsolete, but must remain supported // because they could potentially get enabled. diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 30e27da4167..a064abbc12b 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -227,6 +227,14 @@ TRANSACTION(ttCLAWBACK, 30, Clawback, ({ {sfHolder, soeOPTIONAL}, })) +/** This transaction claws back tokens from an AMM pool. */ +TRANSACTION(ttAMM_CLAWBACK, 31, AMMClawback, ({ + {sfHolder, soeREQUIRED}, + {sfAsset, soeREQUIRED}, + {sfAsset2, soeREQUIRED}, + {sfAmount, soeOPTIONAL}, +})) + /** This transaction type creates an AMM instance */ TRANSACTION(ttAMM_CREATE, 35, AMMCreate, ({ {sfAmount, soeREQUIRED}, diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index bafdde4fbcc..90e5b1c6e47 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -73,6 +73,7 @@ JSS(Escrow); // ledger type. JSS(Fee); // in/out: TransactionSign; field. JSS(FeeSettings); // ledger type. JSS(Flags); // in/out: TransactionSign; field. +JSS(Holder); // field. JSS(Invalid); // JSS(LastLedgerSequence); // in: TransactionSign; field JSS(LastUpdateTime); // field. diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index 788b3a86152..90809b29981 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -160,7 +160,7 @@ transResults() MAKE_ERROR(temMALFORMED, "Malformed transaction."), MAKE_ERROR(temBAD_AMM_TOKENS, "Malformed: Invalid LPTokens."), - MAKE_ERROR(temBAD_AMOUNT, "Can only send positive amounts."), + MAKE_ERROR(temBAD_AMOUNT, "Malformed: Bad amount."), MAKE_ERROR(temBAD_CURRENCY, "Malformed: Bad currency."), MAKE_ERROR(temBAD_EXPIRATION, "Malformed: Bad expiration."), MAKE_ERROR(temBAD_FEE, "Invalid fee, negative or not XRP."), diff --git a/src/test/app/AMMClawback_test.cpp b/src/test/app/AMMClawback_test.cpp new file mode 100644 index 00000000000..705a1274073 --- /dev/null +++ b/src/test/app/AMMClawback_test.cpp @@ -0,0 +1,1794 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace ripple { +namespace test { +class AMMClawback_test : public jtx::AMMTest +{ + void + testInvalidRequest(FeatureBitset features) + { + testcase("test invalid request"); + using namespace jtx; + + // Test if holder does not exist. + { + Env env(*this, features); + Account gw{"gateway"}; + Account alice{"alice"}; + env.fund(XRP(100000), gw, alice); + env.close(); + + // gw sets asfAllowTrustLineClawback. + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw, asfAllowTrustLineClawback)); + + env.trust(USD(10000), alice); + env(pay(gw, alice, gw["USD"](100))); + + AMM amm(env, alice, XRP(100), USD(100)); + env.close(); + + env(amm::ammClawback( + gw, Account("unknown"), USD, XRP, std::nullopt), + ter(terNO_ACCOUNT)); + } + + // Test if asset pair provided does not exist. This should + // return terNO_AMM error. + { + Env env(*this, features); + Account gw{"gateway"}; + Account alice{"alice"}; + env.fund(XRP(100000), gw, alice); + env.close(); + + // gw sets asfAllowTrustLineClawback. + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw, asfAllowTrustLineClawback)); + + // gw issues 100 USD to Alice. + auto const USD = gw["USD"]; + env.trust(USD(10000), alice); + env(pay(gw, alice, USD(100))); + env.close(); + + // Withdraw all the tokens from the AMMAccount. + // The AMMAccount will be auto deleted. + AMM amm(env, gw, XRP(100), USD(100)); + amm.withdrawAll(gw); + BEAST_EXPECT(!amm.ammExists()); + env.close(); + + // The AMM account does not exist at all now. + // It should return terNO_AMM error. + env(amm::ammClawback(gw, alice, USD, 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); + Account gw{"gateway"}; + Account alice{"alice"}; + env.fund(XRP(10000), gw, alice); + env.close(); + + // gw sets asfAllowTrustLineClawback. + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw, asfAllowTrustLineClawback)); + + // gw issues 100 USD to Alice. + auto const USD = gw["USD"]; + env.trust(USD(1000), alice); + env(pay(gw, alice, USD(100))); + env.close(); + + AMM amm(env, gw, XRP(100), USD(100), ter(tesSUCCESS)); + + // Issuer can not clawback from himself. + env(amm::ammClawback(gw, gw, USD, XRP, std::nullopt), + ter(temMALFORMED)); + + // Holder can not clawback from himself. + env(amm::ammClawback(alice, alice, USD, XRP, std::nullopt), + ter(temMALFORMED)); + } + + // Test if the Asset field matches the Account field. + { + Env env(*this, features); + Account gw{"gateway"}; + Account alice{"alice"}; + env.fund(XRP(10000), gw, alice); + env.close(); + + // gw sets asfAllowTrustLineClawback. + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw, asfAllowTrustLineClawback)); + + // gw issues 100 USD to Alice. + auto const USD = gw["USD"]; + env.trust(USD(1000), alice); + env(pay(gw, alice, USD(100))); + env.close(); + + AMM amm(env, gw, XRP(100), USD(100), ter(tesSUCCESS)); + + // The Asset's issuer field is alice, while the Account field is gw. + // This should return temMALFORMED because they do not match. + env(amm::ammClawback( + gw, + alice, + Issue{gw["USD"].currency, alice.id()}, + XRP, + std::nullopt), + ter(temMALFORMED)); + } + + // Test if the Amount field matches the Asset field. + { + Env env(*this, features); + Account gw{"gateway"}; + Account alice{"alice"}; + env.fund(XRP(10000), gw, alice); + env.close(); + + // gw sets asfAllowTrustLineClawback. + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw, asfAllowTrustLineClawback)); + + // gw issues 100 USD to Alice. + auto const USD = gw["USD"]; + env.trust(USD(1000), alice); + env(pay(gw, alice, USD(100))); + env.close(); + + AMM amm(env, gw, XRP(100), USD(100), ter(tesSUCCESS)); + + // The Asset's issuer subfield is gw account and Amount's issuer + // subfield is alice account. Return temBAD_AMOUNT because + // they do not match. + env(amm::ammClawback( + gw, + alice, + USD, + XRP, + STAmount{Issue{gw["USD"].currency, alice.id()}, 1}), + ter(temBAD_AMOUNT)); + } + + // Test if the Amount is invalid, which is less than zero. + { + Env env(*this, features); + Account gw{"gateway"}; + Account alice{"alice"}; + env.fund(XRP(10000), gw, alice); + env.close(); + + // gw sets asfAllowTrustLineClawback. + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw, asfAllowTrustLineClawback)); + + // gw issues 100 USD to Alice. + auto const USD = gw["USD"]; + env.trust(USD(1000), alice); + env(pay(gw, alice, USD(100))); + env.close(); + + AMM amm(env, gw, XRP(100), USD(100), ter(tesSUCCESS)); + + // Return temBAD_AMOUNT if the Amount value is less than 0. + env(amm::ammClawback( + gw, + alice, + USD, + XRP, + STAmount{Issue{gw["USD"].currency, gw.id()}, -1}), + ter(temBAD_AMOUNT)); + + // Return temBAD_AMOUNT if the Amount value is 0. + env(amm::ammClawback( + gw, + alice, + USD, + XRP, + STAmount{Issue{gw["USD"].currency, gw.id()}, 0}), + ter(temBAD_AMOUNT)); + } + + // Test if the issuer did not set asfAllowTrustLineClawback, AMMClawback + // transaction is prohibited. + { + Env env(*this, features); + Account gw{"gateway"}; + Account alice{"alice"}; + env.fund(XRP(10000), gw, alice); + env.close(); + + // gw issues 100 USD to Alice. + auto const USD = gw["USD"]; + env.trust(USD(1000), alice); + env(pay(gw, alice, USD(100))); + env.close(); + env.require(balance(alice, gw["USD"](100))); + env.require(balance(gw, alice["USD"](-100))); + + // gw creates AMM pool of XRP/USD. + AMM amm(env, gw, XRP(100), USD(100), ter(tesSUCCESS)); + + // If asfAllowTrustLineClawback is not set, the issuer is not + // allowed to send the AMMClawback transaction. + env(amm::ammClawback(gw, alice, USD, XRP, std::nullopt), + ter(tecNO_PERMISSION)); + } + + // Test invalid flag. + { + Env env(*this, features); + Account gw{"gateway"}; + Account alice{"alice"}; + env.fund(XRP(10000), gw, alice); + env.close(); + + // gw sets asfAllowTrustLineClawback. + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw, asfAllowTrustLineClawback)); + + // gw issues 100 USD to Alice. + auto const USD = gw["USD"]; + env.trust(USD(1000), alice); + env(pay(gw, alice, USD(100))); + env.close(); + + AMM amm(env, gw, XRP(100), USD(100), ter(tesSUCCESS)); + + // Return temINVALID_FLAG when providing invalid flag. + env(amm::ammClawback(gw, alice, USD, XRP, std::nullopt), + txflags(tfTwoAssetIfEmpty), + ter(temINVALID_FLAG)); + } + + // Test if tfClawTwoAssets is set when the two assets in the AMM pool + // are not issued by the same issuer. + { + Env env(*this, features); + Account gw{"gateway"}; + Account alice{"alice"}; + env.fund(XRP(10000), gw, alice); + env.close(); + + // gw sets asfAllowTrustLineClawback. + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw, asfAllowTrustLineClawback)); + + // gw issues 100 USD to Alice. + auto const USD = gw["USD"]; + env.trust(USD(1000), alice); + env(pay(gw, alice, USD(100))); + env.close(); + + // gw creates AMM pool of XRP/USD. + AMM amm(env, gw, XRP(100), USD(100), ter(tesSUCCESS)); + + // Return tecNO_PERMISSION because the issuer set tfClawTwoAssets, + // but the issuer only issues USD in the pool. The issuer is not + // allowed to set tfClawTwoAssets flag if he did not issue both + // assts in the pool. + env(amm::ammClawback(gw, alice, USD, XRP, std::nullopt), + txflags(tfClawTwoAssets), + ter(tecNO_PERMISSION)); + } + + // Test clawing back XRP is being prohibited. + { + Env env(*this, features); + Account gw{"gateway"}; + Account alice{"alice"}; + env.fund(XRP(1000000), gw, alice); + env.close(); + + // gw sets asfAllowTrustLineClawback. + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw, asfAllowTrustLineClawback)); + + // gw issues 3000 USD to Alice. + auto const USD = gw["USD"]; + env.trust(USD(100000), alice); + env(pay(gw, alice, USD(3000))); + env.close(); + + // Alice creates AMM pool of XRP/USD. + AMM amm(env, alice, XRP(1000), USD(2000), ter(tesSUCCESS)); + env.close(); + + // Clawback XRP is prohibited. + env(amm::ammClawback(gw, alice, XRP, USD, std::nullopt), + ter(temMALFORMED)); + } + } + + void + testFeatureDisabled(FeatureBitset features) + { + testcase("test featureAMMClawback is not enabled."); + using namespace jtx; + if (!features[featureAMMClawback]) + { + Env env(*this, features); + Account gw{"gateway"}; + Account alice{"alice"}; + env.fund(XRP(1000000), gw, alice); + env.close(); + + // gw sets asfAllowTrustLineClawback. + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw, asfAllowTrustLineClawback)); + + // gw issues 3000 USD to Alice. + auto const USD = gw["USD"]; + env.trust(USD(100000), alice); + env(pay(gw, alice, USD(3000))); + env.close(); + + // When featureAMMClawback is not enabled, AMMClawback is disabled. + // Because when featureAMMClawback is disabled, we can not create + // amm account, call amm::ammClawback directly for testing purpose. + env(amm::ammClawback(gw, alice, USD, XRP, std::nullopt), + ter(temDISABLED)); + } + } + + void + testAMMClawbackSpecificAmount(FeatureBitset features) + { + testcase("test AMMClawback specific amount"); + using namespace jtx; + + // Test AMMClawback for USD/EUR pool. The assets are issued by different + // issuer. Claw back USD, and EUR goes back to the holder. + { + Env env(*this, features); + Account gw{"gateway"}; + Account gw2{"gateway2"}; + Account alice{"alice"}; + env.fund(XRP(1000000), gw, gw2, alice); + env.close(); + + // gw sets asfAllowTrustLineClawback. + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw, asfAllowTrustLineClawback)); + + // gw issues 3000 USD to Alice. + auto const USD = gw["USD"]; + env.trust(USD(100000), alice); + env(pay(gw, alice, USD(3000))); + env.close(); + env.require(balance(gw, alice["USD"](-3000))); + env.require(balance(alice, gw["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(gw2, alice["EUR"](-3000))); + env.require(balance(alice, gw2["EUR"](3000))); + + // Alice creates AMM pool of EUR/USD. + AMM amm(env, alice, EUR(1000), USD(2000), ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(amm.expectBalances( + USD(2000), EUR(1000), IOUAmount{1414213562373095, -12})); + + // gw clawback 1000 USD from the AMM pool. + env(amm::ammClawback(gw, alice, USD, EUR, USD(1000)), + ter(tesSUCCESS)); + env.close(); + + // Alice's initial balance for USD is 3000 USD. Alice deposited 2000 + // 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))); + + // 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))); + + // 1000 USD and 500 EUR was withdrawn from the AMM pool, so the + // current balance is 1000 USD and 500 EUR. + BEAST_EXPECT(amm.expectBalances( + USD(1000), EUR(500), IOUAmount{7071067811865475, -13})); + + // Alice has half of its initial lptokens Left. + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{7071067811865475, -13})); + + // gw clawback another 1000 USD from the AMM pool. The AMM pool will + // be empty and get deleted. + env(amm::ammClawback(gw, alice, USD, EUR, USD(1000)), + ter(tesSUCCESS)); + env.close(); + + // 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))); + + // 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))); + + // amm is automatically deleted. + BEAST_EXPECT(!amm.ammExists()); + } + + // Test AMMClawback for USD/XRP pool. Claw back USD, and XRP goes back + // to the holder. + { + Env env(*this, features); + Account gw{"gateway"}; + Account alice{"alice"}; + env.fund(XRP(1000000), gw, alice); + env.close(); + + // gw sets asfAllowTrustLineClawback. + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw, asfAllowTrustLineClawback)); + + // gw issues 3000 USD to Alice. + auto const USD = gw["USD"]; + env.trust(USD(100000), alice); + env(pay(gw, alice, USD(3000))); + env.close(); + env.require(balance(gw, alice["USD"](-3000))); + env.require(balance(alice, gw["USD"](3000))); + + // Alice creates AMM pool of XRP/USD. + AMM amm(env, alice, XRP(1000), USD(2000), ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(amm.expectBalances( + USD(2000), XRP(1000), IOUAmount{1414213562373095, -9})); + + auto aliceXrpBalance = env.balance(alice, XRP); + + // gw clawback 1000 USD from the AMM pool. + env(amm::ammClawback(gw, alice, USD, XRP, USD(1000)), + ter(tesSUCCESS)); + env.close(); + + // Alice's initial balance for USD is 3000 USD. Alice deposited 2000 + // 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))); + + // Alice will get 500 XRP back. + BEAST_EXPECT( + expectLedgerEntryRoot(env, alice, aliceXrpBalance + XRP(500))); + + // 1000 USD and 500 XRP was withdrawn from the AMM pool, so the + // current balance is 1000 USD and 500 XRP. + BEAST_EXPECT(amm.expectBalances( + USD(1000), XRP(500), IOUAmount{7071067811865475, -10})); + + // Alice has half of its initial lptokens Left. + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{7071067811865475, -10})); + + // gw clawback another 1000 USD from the AMM pool. The AMM pool will + // be empty and get deleted. + env(amm::ammClawback(gw, alice, USD, XRP, USD(1000)), + ter(tesSUCCESS)); + env.close(); + + // 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))); + + // Alice will get another 1000 XRP back. + BEAST_EXPECT( + expectLedgerEntryRoot(env, alice, aliceXrpBalance + XRP(1000))); + + // amm is automatically deleted. + BEAST_EXPECT(!amm.ammExists()); + } + } + + void + testAMMClawbackExceedBalance(FeatureBitset features) + { + testcase( + "test AMMClawback specific amount which exceeds the current " + "balance"); + using namespace jtx; + + // Test AMMClawback for USD/EUR pool. The assets are issued by different + // issuer. Claw back USD for multiple times, and EUR goes back to the + // holder. The last AMMClawback transaction exceeds the holder's USD + // balance in AMM pool. + { + Env env(*this, features); + Account gw{"gateway"}; + Account gw2{"gateway2"}; + Account alice{"alice"}; + env.fund(XRP(1000000), gw, gw2, alice); + env.close(); + + // gw sets asfAllowTrustLineClawback. + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw, asfAllowTrustLineClawback)); + + // gw issues 6000 USD to Alice. + auto const USD = gw["USD"]; + env.trust(USD(100000), alice); + env(pay(gw, alice, USD(6000))); + env.close(); + env.require(balance(alice, gw["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))); + + // Alice creates AMM pool of EUR/USD + AMM amm(env, alice, EUR(5000), USD(4000), ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(amm.expectBalances( + USD(4000), EUR(5000), IOUAmount{4472135954999580, -12})); + + // gw clawback 1000 USD from the AMM pool + env(amm::ammClawback(gw, alice, USD, EUR, USD(1000)), + ter(tesSUCCESS)); + env.close(); + + // 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))); + + // 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))); + + // 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})); + + // Alice has 3/4 of its initial lptokens Left. + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{3354101966249685, -12})); + + // gw clawback another 500 USD from the AMM pool. + env(amm::ammClawback(gw, alice, USD, EUR, USD(500)), + ter(tesSUCCESS)); + env.close(); + + // Alice should still has 2000 USD because gw clawed back from the + // 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)); + + // gw clawback small amount, 1 USD. + env(amm::ammClawback(gw, alice, USD, EUR, USD(1)), ter(tesSUCCESS)); + env.close(); + + // 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)); + + // gw clawback 4000 USD, exceeding the current balance. We + // will clawback all. + env(amm::ammClawback(gw, alice, USD, EUR, USD(4000)), + ter(tesSUCCESS)); + env.close(); + + env.require(balance(alice, gw["USD"](2000))); + + // All alice's EUR in the pool goes back to alice. + BEAST_EXPECT( + env.balance(alice, EUR) == + STAmount(EUR, UINT64_C(6000000000000000), -12)); + + // amm is automatically deleted. + BEAST_EXPECT(!amm.ammExists()); + } + + // Test AMMClawback for USD/XRP pool. Claw back USD for multiple times, + // and XRP goes back to the holder. The last AMMClawback transaction + // exceeds the holder's USD balance in AMM pool. In this case, gw + // creates the AMM pool USD/XRP, both alice and bob deposit into it. gw2 + // creates the AMM pool EUR/XRP. + { + Env env(*this, features); + Account gw{"gateway"}; + Account gw2{"gateway2"}; + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(1000000), gw, gw2, alice, bob); + env.close(); + + // gw sets asfAllowTrustLineClawback. + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw, asfAllowTrustLineClawback)); + + // gw2 sets asfAllowTrustLineClawback. + env(fset(gw2, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw2, asfAllowTrustLineClawback)); + + // gw issues 6000 USD to Alice and 5000 USD to Bob. + auto const USD = gw["USD"]; + env.trust(USD(100000), alice); + env(pay(gw, alice, USD(6000))); + env.trust(USD(100000), bob); + env(pay(gw, bob, USD(5000))); + env.close(); + + // gw2 issues 5000 EUR to Alice and 4000 EUR to Bob. + auto const EUR = gw2["EUR"]; + env.trust(EUR(100000), alice); + env(pay(gw2, alice, EUR(5000))); + env.trust(EUR(100000), bob); + env(pay(gw2, bob, EUR(4000))); + env.close(); + + // gw creates AMM pool of XRP/USD, alice and bob deposit XRP/USD. + AMM amm(env, gw, XRP(2000), USD(1000), ter(tesSUCCESS)); + BEAST_EXPECT(amm.expectBalances( + USD(1000), XRP(2000), IOUAmount{1414213562373095, -9})); + amm.deposit(alice, USD(1000), XRP(2000)); + BEAST_EXPECT(amm.expectBalances( + USD(2000), XRP(4000), IOUAmount{2828427124746190, -9})); + amm.deposit(bob, USD(1000), XRP(2000)); + BEAST_EXPECT(amm.expectBalances( + USD(3000), XRP(6000), IOUAmount{4242640687119285, -9})); + env.close(); + + // 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})); + amm2.deposit(alice, EUR(1000), XRP(3000)); + BEAST_EXPECT(amm2.expectBalances( + EUR(2000), XRP(6000), IOUAmount{3464101615137756, -9})); + amm2.deposit(bob, EUR(1000), XRP(3000)); + BEAST_EXPECT(amm2.expectBalances( + EUR(3000), XRP(9000), IOUAmount{5196152422706634, -9})); + env.close(); + + auto aliceXrpBalance = env.balance(alice, XRP); + auto bobXrpBalance = env.balance(bob, XRP); + + // gw clawback 500 USD from alice in amm + env(amm::ammClawback(gw, alice, USD, XRP, USD(500)), + ter(tesSUCCESS)); + env.close(); + + // 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))); + + // Bob's balance is not changed. + env.require(balance(bob, gw["USD"](4000))); + + // Alice gets 1000 XRP back. + 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})); + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{1414213562373095, -9})); + + // gw clawback 10 USD from bob in amm. + 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))); + + // 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})); + + // 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))); + + // 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})); + + // gw claw back 1000 USD from alice in amm, which exceeds alice's + // balance. This will clawback all the remaining LP tokens of alice + // (corresponding 500 USD / 1000 XRP). + env(amm::ammClawback(gw, alice, USD, XRP, USD(1000)), + ter(tesSUCCESS)); + env.close(); + + env.require(balance(alice, gw["USD"](5000))); + env.require(balance(bob, gw["USD"](4000))); + + // Alice gets 1000 XRP back. + BEAST_EXPECT(expectLedgerEntryRoot( + env, + alice, + aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000))); + 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})); + + // 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 + // corresponds to 990 USD / 1980 XRP + env(amm::ammClawback(gw, bob, USD, XRP, USD(1000)), + ter(tesSUCCESS)); + env.close(); + + 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))); + BEAST_EXPECT(expectLedgerEntryRoot( + env, bob, bobXrpBalance + XRP(20) + XRP(1980))); + + // Now neither alice nor bob has any lptoken in amm. + BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); + BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount(0))); + + // gw2 claw back 1000 EUR from alice in amm2, which exceeds alice's + // balance. All alice's lptokens will be consumed, which corresponds + // to 800EUR / 2400 XRP. + env(amm::ammClawback(gw2, alice, EUR, XRP, EUR(1000)), + ter(tesSUCCESS)); + env.close(); + + env.require(balance(alice, gw2["EUR"](4000))); + env.require(balance(bob, gw2["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))); + 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})); + + // gw2 claw back 2000 EUR from bib 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)), + ter(tesSUCCESS)); + env.close(); + + env.require(balance(alice, gw2["EUR"](4000))); + env.require(balance(bob, gw2["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))); + BEAST_EXPECT(expectLedgerEntryRoot( + env, bob, bobXrpBalance + XRP(20) + XRP(1980) + XRP(3000))); + + // 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{1732050807568878, -9})); + } + } + + void + testAMMClawbackAll(FeatureBitset features) + { + testcase("test AMMClawback all the tokens in the AMM pool"); + using namespace jtx; + + // Test AMMClawback for USD/EUR pool. The assets are issued by different + // issuer. Claw back all the USD for different users. + { + Env env(*this, features); + Account gw{"gateway"}; + Account gw2{"gateway2"}; + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + env.fund(XRP(1000000), gw, gw2, alice, bob, carol); + env.close(); + + // gw sets asfAllowTrustLineClawback. + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw, asfAllowTrustLineClawback)); + + // gw2 sets asfAllowTrustLineClawback. + env(fset(gw2, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw2, asfAllowTrustLineClawback)); + + // gw issues 6000 USD to Alice, 5000 USD to Bob, and 4000 USD + // to Carol. + auto const USD = gw["USD"]; + env.trust(USD(100000), alice); + env(pay(gw, alice, USD(6000))); + env.trust(USD(100000), bob); + env(pay(gw, bob, USD(5000))); + env.trust(USD(100000), carol); + env(pay(gw, carol, USD(4000))); + env.close(); + + // gw2 issues 6000 EUR to Alice and 5000 EUR to Bob and 4000 + // EUR to Carol. + auto const EUR = gw2["EUR"]; + env.trust(EUR(100000), alice); + env(pay(gw2, alice, EUR(6000))); + env.trust(EUR(100000), bob); + env(pay(gw2, bob, EUR(5000))); + env.trust(EUR(100000), carol); + env(pay(gw2, carol, EUR(4000))); + env.close(); + + // Alice creates AMM pool of EUR/USD + AMM amm(env, alice, EUR(5000), USD(4000), ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(amm.expectBalances( + USD(4000), EUR(5000), IOUAmount{4472135954999580, -12})); + amm.deposit(bob, USD(2000), EUR(2500)); + BEAST_EXPECT(amm.expectBalances( + USD(6000), EUR(7500), IOUAmount{6708203932499370, -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})); + + 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))); + + // gw clawback all the bob's USD in amm. (2000 USD / 2500 EUR) + env(amm::ammClawback(gw, bob, USD, EUR, std::nullopt), + 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})); + BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount(0))); + BEAST_EXPECT( + amm.expectLPTokens(carol, IOUAmount{1118033988749895, -12})); + + // Bob will get 2500 EUR back. + env.require(balance(alice, gw["USD"](2000))); + env.require(balance(alice, gw2["EUR"](1000))); + BEAST_EXPECT( + env.balance(bob, USD) == + STAmount(USD, UINT64_C(3000000000000000), -12)); + + BEAST_EXPECT( + env.balance(bob, EUR) == + STAmount(EUR, UINT64_C(5000000000000001), -12)); + env.require(balance(carol, gw["USD"](3000))); + env.require(balance(carol, gw2["EUR"](2750))); + + // gw2 clawback all carol's EUR in amm. (1000 USD / 1250 EUR) + 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})); + BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount(0))); + BEAST_EXPECT(amm.expectLPTokens(carol, IOUAmount(0))); + + // gw2 clawback all alice's EUR in amm. (4000 USD / 5000 EUR) + env(amm::ammClawback(gw2, alice, EUR, USD, std::nullopt), + ter(tesSUCCESS)); + env.close(); + + env.require(balance(carol, gw2["EUR"](2750))); + env.require(balance(carol, gw["USD"](4000))); + BEAST_EXPECT(!amm.ammExists()); + } + + // Test AMMClawback for USD/XRP pool. Claw back all the USD for + // different users. + { + Env env(*this, features); + Account gw{"gateway"}; + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(1000000), gw, alice, bob); + env.close(); + + // gw sets asfAllowTrustLineClawback + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw, asfAllowTrustLineClawback)); + + // gw issues 600000 USD to Alice and 500000 USD to Bob. + auto const USD = gw["USD"]; + env.trust(USD(1000000), alice); + env(pay(gw, alice, USD(600000))); + env.trust(USD(1000000), bob); + env(pay(gw, bob, USD(500000))); + env.close(); + + // 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})); + amm.deposit(alice, USD(1000), XRP(200)); + BEAST_EXPECT(amm.expectBalances( + USD(11000), XRP(2200), IOUAmount{4919349550499538, -9})); + amm.deposit(bob, USD(2000), XRP(400)); + BEAST_EXPECT(amm.expectBalances( + USD(13000), XRP(2600), IOUAmount{5813776741499453, -9})); + env.close(); + + auto aliceXrpBalance = env.balance(alice, XRP); + auto bobXrpBalance = env.balance(bob, XRP); + + // gw clawback all alice's USD in amm. (1000 USD / 200 XRP) + 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))); + 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})); + BEAST_EXPECT( + expectLedgerEntryRoot(env, bob, bobXrpBalance + XRP(400))); + BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); + BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount(0))); + } + } + + void + testAMMClawbackSameIssuerAssets(FeatureBitset features) + { + testcase( + "test AMMClawback from AMM pool with assets having the same " + "issuer"); + using namespace jtx; + + // Test AMMClawback for USD/EUR pool. The assets are issued by different + // issuer. Claw back all the USD for different users. + Env env(*this, features); + Account gw{"gateway"}; + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + env.fund(XRP(1000000), gw, alice, bob, carol); + env.close(); + + // gw sets asfAllowTrustLineClawback. + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw, asfAllowTrustLineClawback)); + + auto const USD = gw["USD"]; + env.trust(USD(100000), alice); + env(pay(gw, alice, USD(10000))); + env.trust(USD(100000), bob); + env(pay(gw, bob, USD(9000))); + env.trust(USD(100000), carol); + env(pay(gw, carol, USD(8000))); + env.close(); + + auto const EUR = gw["EUR"]; + env.trust(EUR(100000), alice); + env(pay(gw, alice, EUR(10000))); + env.trust(EUR(100000), bob); + env(pay(gw, bob, EUR(9000))); + env.trust(EUR(100000), carol); + env(pay(gw, carol, EUR(8000))); + env.close(); + + AMM amm(env, alice, EUR(2000), USD(8000), ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(amm.expectBalances(USD(8000), EUR(2000), IOUAmount(4000))); + amm.deposit(bob, USD(4000), EUR(1000)); + BEAST_EXPECT( + amm.expectBalances(USD(12000), EUR(3000), IOUAmount(6000))); + amm.deposit(carol, USD(2000), 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(); + BEAST_EXPECT( + amm.expectBalances(USD(13000), EUR(3250), IOUAmount(6500))); + + BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(4000))); + BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount(2000))); + BEAST_EXPECT(amm.expectLPTokens(carol, IOUAmount(500))); + BEAST_EXPECT(env.balance(alice, USD) == USD(2000)); + 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)); + // 250 EUR goes back to carol. + BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); + + // gw clawback 1000 USD from bob with tfClawTwoAssets flag. + // then the corresponding EUR will also be clawed back + // by gw. + env(amm::ammClawback(gw, bob, USD, EUR, USD(1000)), + txflags(tfClawTwoAssets), + ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT( + amm.expectBalances(USD(12000), EUR(3000), IOUAmount(6000))); + + BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(4000))); + BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount(1500))); + BEAST_EXPECT(amm.expectLPTokens(carol, IOUAmount(500))); + BEAST_EXPECT(env.balance(alice, USD) == USD(2000)); + BEAST_EXPECT(env.balance(alice, EUR) == EUR(8000)); + 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)); + BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); + + // gw clawback all USD from alice and set tfClawTwoAssets. + env(amm::ammClawback(gw, alice, USD, EUR, std::nullopt), + txflags(tfClawTwoAssets), + ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT(amm.expectBalances(USD(4000), EUR(1000), IOUAmount(2000))); + + BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); + BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount(1500))); + BEAST_EXPECT(amm.expectLPTokens(carol, IOUAmount(500))); + BEAST_EXPECT(env.balance(alice, USD) == USD(2000)); + 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)); + BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); + } + + void + testAMMClawbackSameCurrency(FeatureBitset features) + { + testcase( + "test AMMClawback from AMM pool with assets having the same " + "currency, but from different issuer"); + using namespace jtx; + + // Test AMMClawback for USD/EUR pool. The assets are issued by different + // issuer. Claw back all the USD for different users. + Env env(*this, features); + Account gw{"gateway"}; + Account gw2{"gateway2"}; + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(1000000), gw, gw2, alice, bob); + env.close(); + + // gw sets asfAllowTrustLineClawback. + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw, asfAllowTrustLineClawback)); + + // gw2 sets asfAllowTrustLineClawback. + env(fset(gw2, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw2, asfAllowTrustLineClawback)); + + env.trust(gw["USD"](100000), alice); + env(pay(gw, alice, gw["USD"](8000))); + env.trust(gw["USD"](100000), bob); + env(pay(gw, bob, gw["USD"](7000))); + + env.trust(gw2["USD"](100000), alice); + env(pay(gw2, alice, gw2["USD"](6000))); + env.trust(gw2["USD"](100000), bob); + env(pay(gw2, bob, gw2["USD"](5000))); + env.close(); + + AMM amm(env, alice, gw["USD"](1000), gw2["USD"](1500), ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(amm.expectBalances( + gw["USD"](1000), + gw2["USD"](1500), + IOUAmount{1224744871391589, -12})); + amm.deposit(bob, gw["USD"](2000), gw2["USD"](3000)); + BEAST_EXPECT(amm.expectBalances( + gw["USD"](3000), + gw2["USD"](4500), + IOUAmount{3674234614174767, -12})); + + // Issuer does not match with asset. + env(amm::ammClawback( + gw, + alice, + gw2["USD"], + gw["USD"], + STAmount{Issue{gw2["USD"].currency, gw2.id()}, 500}), + ter(temMALFORMED)); + + // gw2 clawback 500 gw2[USD] from alice. + env(amm::ammClawback( + gw2, + alice, + gw2["USD"], + gw["USD"], + STAmount{Issue{gw2["USD"].currency, gw2.id()}, 500}), + ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT(amm.expectBalances( + STAmount{gw["USD"], UINT64_C(2666666666666667), -12}, + gw2["USD"](4000), + IOUAmount{3265986323710904, -12})); + + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{8164965809277260, -13})); + BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount{2449489742783178, -12})); + BEAST_EXPECT( + env.balance(alice, gw["USD"]) == + STAmount(gw["USD"], UINT64_C(7333333333333333), -12)); + BEAST_EXPECT(env.balance(alice, gw2["USD"]) == gw2["USD"](4500)); + BEAST_EXPECT(env.balance(bob, gw["USD"]) == gw["USD"](5000)); + BEAST_EXPECT(env.balance(bob, gw2["USD"]) == gw2["USD"](2000)); + + // gw clawback all gw["USD"] from bob. + env(amm::ammClawback(gw, bob, gw["USD"], gw2["USD"], std::nullopt), + ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT(amm.expectBalances( + STAmount{gw["USD"], UINT64_C(6666666666666670), -13}, + gw2["USD"](1000), + IOUAmount{8164965809277260, -13})); + + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{8164965809277260, -13})); + BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount(0))); + BEAST_EXPECT( + env.balance(alice, gw["USD"]) == + STAmount(gw["USD"], UINT64_C(7333333333333333), -12)); + BEAST_EXPECT(env.balance(alice, gw2["USD"]) == gw2["USD"](4500)); + BEAST_EXPECT(env.balance(bob, gw["USD"]) == gw["USD"](5000)); + // Bob gets 3000 gw2["USD"] back and now his balance is 5000. + BEAST_EXPECT(env.balance(bob, gw2["USD"]) == gw2["USD"](5000)); + } + + void + testAMMClawbackIssuesEachOther(FeatureBitset features) + { + testcase("test AMMClawback when issuing token for each other"); + using namespace jtx; + + // gw and gw2 issues token for each other. Test AMMClawback from + // each other. + Env env(*this, features); + Account gw{"gateway"}; + Account gw2{"gateway2"}; + Account alice{"alice"}; + env.fund(XRP(1000000), gw, gw2, alice); + env.close(); + + // gw sets asfAllowTrustLineClawback. + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw, asfAllowTrustLineClawback)); + + // gw2 sets asfAllowTrustLineClawback. + env(fset(gw2, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw2, asfAllowTrustLineClawback)); + + auto const USD = gw["USD"]; + env.trust(USD(100000), gw2); + env(pay(gw, gw2, USD(5000))); + env.trust(USD(100000), alice); + env(pay(gw, alice, USD(5000))); + + auto const EUR = gw2["EUR"]; + env.trust(EUR(100000), gw); + env(pay(gw2, gw, EUR(6000))); + env.trust(EUR(100000), alice); + env(pay(gw2, alice, EUR(6000))); + env.close(); + + AMM amm(env, gw, USD(1000), EUR(2000), ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT(amm.expectBalances( + USD(1000), EUR(2000), IOUAmount{1414213562373095, -12})); + + amm.deposit(gw2, USD(2000), EUR(4000)); + BEAST_EXPECT(amm.expectBalances( + USD(3000), EUR(6000), IOUAmount{4242640687119285, -12})); + + amm.deposit(alice, USD(3000), EUR(6000)); + BEAST_EXPECT(amm.expectBalances( + USD(6000), EUR(12000), IOUAmount{8485281374238570, -12})); + + BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{1414213562373095, -12})); + BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{2828427124746190, -12})); + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{4242640687119285, -12})); + + // 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})); + + BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{1414213562373095, -12})); + BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{1414213562373095, -12})); + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{4242640687119285, -12})); + + BEAST_EXPECT(env.balance(alice, USD) == USD(2000)); + BEAST_EXPECT(env.balance(alice, EUR) == EUR(0)); + BEAST_EXPECT(env.balance(gw, EUR) == EUR(4000)); + BEAST_EXPECT(env.balance(gw2, USD) == USD(3000)); + + // 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})); + + BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{7071067811865480, -13})); + BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{1414213562373095, -12})); + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{4242640687119285, -12})); + + BEAST_EXPECT(env.balance(alice, USD) == USD(2000)); + BEAST_EXPECT(env.balance(alice, EUR) == EUR(0)); + BEAST_EXPECT(env.balance(gw, EUR) == EUR(4000)); + BEAST_EXPECT(env.balance(gw2, USD) == USD(3000)); + + // 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})); + + BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{7071067811865480, -13})); + BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{1414213562373095, -12})); + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{1414213562373095, -12})); + + BEAST_EXPECT(env.balance(alice, USD) == USD(4000)); + BEAST_EXPECT(env.balance(alice, EUR) == EUR(0)); + BEAST_EXPECT(env.balance(gw, EUR) == EUR(4000)); + BEAST_EXPECT(env.balance(gw2, USD) == USD(3000)); + } + + void + testNotHoldingLptoken(FeatureBitset features) + { + testcase( + "test AMMClawback from account which does not own any lptoken in " + "the pool"); + using namespace jtx; + + Env env(*this, features); + Account gw{"gateway"}; + Account alice{"alice"}; + env.fund(XRP(1000000), gw, alice); + env.close(); + + // gw sets asfAllowTrustLineClawback. + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw, asfAllowTrustLineClawback)); + + auto const USD = gw["USD"]; + env.trust(USD(100000), alice); + env(pay(gw, alice, USD(5000))); + + AMM amm(env, gw, USD(1000), XRP(2000), ter(tesSUCCESS)); + env.close(); + + // Alice did not deposit in the amm pool. So AMMClawback from Alice + // will fail. + env(amm::ammClawback(gw, alice, USD, XRP, USD(1000)), + ter(tecAMM_BALANCE)); + } + + void + testAssetFrozen(FeatureBitset features) + { + testcase("test assets frozen"); + using namespace jtx; + + // test individually frozen trustline. + { + Env env(*this, features); + Account gw{"gateway"}; + Account gw2{"gateway2"}; + Account alice{"alice"}; + env.fund(XRP(1000000), gw, gw2, alice); + env.close(); + + // gw sets asfAllowTrustLineClawback. + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw, asfAllowTrustLineClawback)); + + // gw issues 3000 USD to Alice. + auto const USD = gw["USD"]; + env.trust(USD(100000), alice); + env(pay(gw, alice, USD(3000))); + env.close(); + env.require(balance(alice, gw["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))); + + // Alice creates AMM pool of EUR/USD. + AMM amm(env, alice, EUR(1000), USD(2000), ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(amm.expectBalances( + USD(2000), EUR(1000), IOUAmount{1414213562373095, -12})); + + // freeze trustline + env(trust(gw, alice["USD"](0), tfSetFreeze)); + env.close(); + + // gw clawback 1000 USD from the AMM pool. + env(amm::ammClawback(gw, alice, USD, EUR, USD(1000)), + ter(tesSUCCESS)); + env.close(); + + env.require(balance(alice, gw["USD"](1000))); + env.require(balance(alice, gw2["EUR"](2500))); + BEAST_EXPECT(amm.expectBalances( + USD(1000), EUR(500), IOUAmount{7071067811865475, -13})); + + // Alice has half of its initial lptokens Left. + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{7071067811865475, -13})); + + // gw clawback another 1000 USD from the AMM pool. The AMM pool will + // be empty and get deleted. + env(amm::ammClawback(gw, alice, USD, EUR, USD(1000)), + ter(tesSUCCESS)); + env.close(); + + // 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))); + + // amm is automatically deleted. + BEAST_EXPECT(!amm.ammExists()); + } + + // test individually frozen trustline of both USD and EUR currency. + { + Env env(*this, features); + Account gw{"gateway"}; + Account gw2{"gateway2"}; + Account alice{"alice"}; + env.fund(XRP(1000000), gw, gw2, alice); + env.close(); + + // gw sets asfAllowTrustLineClawback. + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw, asfAllowTrustLineClawback)); + + // gw issues 3000 USD to Alice. + auto const USD = gw["USD"]; + env.trust(USD(100000), alice); + env(pay(gw, alice, USD(3000))); + env.close(); + env.require(balance(alice, gw["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))); + + // Alice creates AMM pool of EUR/USD. + AMM amm(env, alice, EUR(1000), USD(2000), ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(amm.expectBalances( + USD(2000), EUR(1000), IOUAmount{1414213562373095, -12})); + + // freeze trustlines + env(trust(gw, alice["USD"](0), tfSetFreeze)); + env(trust(gw2, alice["EUR"](0), tfSetFreeze)); + env.close(); + + // gw clawback 1000 USD from the AMM pool. + env(amm::ammClawback(gw, alice, USD, EUR, USD(1000)), + ter(tesSUCCESS)); + env.close(); + + env.require(balance(alice, gw["USD"](1000))); + env.require(balance(alice, gw2["EUR"](2500))); + BEAST_EXPECT(amm.expectBalances( + USD(1000), EUR(500), IOUAmount{7071067811865475, -13})); + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{7071067811865475, -13})); + } + + // test gw global freeze. + { + Env env(*this, features); + Account gw{"gateway"}; + Account gw2{"gateway2"}; + Account alice{"alice"}; + env.fund(XRP(1000000), gw, gw2, alice); + env.close(); + + // gw sets asfAllowTrustLineClawback. + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw, asfAllowTrustLineClawback)); + + // gw issues 3000 USD to Alice. + auto const USD = gw["USD"]; + env.trust(USD(100000), alice); + env(pay(gw, alice, USD(3000))); + env.close(); + env.require(balance(alice, gw["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))); + + // Alice creates AMM pool of EUR/USD. + AMM amm(env, alice, EUR(1000), USD(2000), ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(amm.expectBalances( + USD(2000), EUR(1000), IOUAmount{1414213562373095, -12})); + + // global freeze + env(fset(gw, asfGlobalFreeze)); + env.close(); + + // gw clawback 1000 USD from the AMM pool. + env(amm::ammClawback(gw, alice, USD, EUR, USD(1000)), + ter(tesSUCCESS)); + env.close(); + + env.require(balance(alice, gw["USD"](1000))); + env.require(balance(alice, gw2["EUR"](2500))); + BEAST_EXPECT(amm.expectBalances( + USD(1000), EUR(500), IOUAmount{7071067811865475, -13})); + BEAST_EXPECT( + amm.expectLPTokens(alice, IOUAmount{7071067811865475, -13})); + } + + // Test both assets are issued by the same issuer. And issuer sets + // global freeze. + { + Env env(*this, features); + Account gw{"gateway"}; + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + env.fund(XRP(1000000), gw, alice, bob, carol); + env.close(); + + // gw sets asfAllowTrustLineClawback. + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw, asfAllowTrustLineClawback)); + + auto const USD = gw["USD"]; + env.trust(USD(100000), alice); + env(pay(gw, alice, USD(10000))); + env.trust(USD(100000), bob); + env(pay(gw, bob, USD(9000))); + env.trust(USD(100000), carol); + env(pay(gw, carol, USD(8000))); + env.close(); + + auto const EUR = gw["EUR"]; + env.trust(EUR(100000), alice); + env(pay(gw, alice, EUR(10000))); + env.trust(EUR(100000), bob); + env(pay(gw, bob, EUR(9000))); + env.trust(EUR(100000), carol); + env(pay(gw, carol, EUR(8000))); + env.close(); + + AMM amm(env, alice, EUR(2000), USD(8000), ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT( + amm.expectBalances(USD(8000), EUR(2000), IOUAmount(4000))); + amm.deposit(bob, USD(4000), EUR(1000)); + BEAST_EXPECT( + amm.expectBalances(USD(12000), EUR(3000), IOUAmount(6000))); + amm.deposit(carol, USD(2000), EUR(500)); + BEAST_EXPECT( + amm.expectBalances(USD(14000), EUR(3500), IOUAmount(7000))); + + // global freeze + env(fset(gw, asfGlobalFreeze)); + env.close(); + + // gw clawback 1000 USD from carol. + env(amm::ammClawback(gw, carol, USD, EUR, USD(1000)), + ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT( + amm.expectBalances(USD(13000), EUR(3250), IOUAmount(6500))); + + BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(4000))); + BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount(2000))); + BEAST_EXPECT(amm.expectLPTokens(carol, IOUAmount(500))); + BEAST_EXPECT(env.balance(alice, USD) == USD(2000)); + 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)); + // 250 EUR goes back to carol. + BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); + + // gw clawback 1000 USD from bob with tfClawTwoAssets flag. + // then the corresponding EUR will also be clawed back + // by gw. + env(amm::ammClawback(gw, bob, USD, EUR, USD(1000)), + txflags(tfClawTwoAssets), + ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT( + amm.expectBalances(USD(12000), EUR(3000), IOUAmount(6000))); + + BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(4000))); + BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount(1500))); + BEAST_EXPECT(amm.expectLPTokens(carol, IOUAmount(500))); + BEAST_EXPECT(env.balance(alice, USD) == USD(2000)); + BEAST_EXPECT(env.balance(alice, EUR) == EUR(8000)); + 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)); + BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); + + // gw clawback all USD from alice and set tfClawTwoAssets. + env(amm::ammClawback(gw, alice, USD, EUR, std::nullopt), + txflags(tfClawTwoAssets), + ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT( + amm.expectBalances(USD(4000), EUR(1000), IOUAmount(2000))); + + BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); + BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount(1500))); + BEAST_EXPECT(amm.expectLPTokens(carol, IOUAmount(500))); + BEAST_EXPECT(env.balance(alice, USD) == USD(2000)); + 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)); + BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); + } + } + + void + testSingleDepositAndClawback(FeatureBitset features) + { + testcase("test single depoit and clawback"); + using namespace jtx; + + // Test AMMClawback for USD/XRP pool. Claw back USD, and XRP goes back + // to the holder. + Env env(*this, features); + Account gw{"gateway"}; + Account alice{"alice"}; + env.fund(XRP(1000000000), gw, alice); + env.close(); + + // gw sets asfAllowTrustLineClawback. + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(flags(gw, asfAllowTrustLineClawback)); + + // gw issues 1000 USD to Alice. + auto const USD = gw["USD"]; + env.trust(USD(100000), alice); + env(pay(gw, alice, USD(1000))); + env.close(); + env.require(balance(alice, gw["USD"](1000))); + + // gw creates AMM pool of XRP/USD. + AMM amm(env, gw, XRP(100), USD(400), ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(amm.expectBalances(USD(400), XRP(100), IOUAmount(200000))); + + amm.deposit(alice, USD(400)); + env.close(); + + BEAST_EXPECT(amm.expectBalances( + USD(800), XRP(100), IOUAmount{2828427124746190, -10})); + + auto aliceXrpBalance = env.balance(alice, XRP); + + 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))); + BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(29.289322))); + } + + void + run() override + { + FeatureBitset const all{jtx::supported_amendments()}; + testInvalidRequest(all); + testFeatureDisabled(all - featureAMMClawback); + testAMMClawbackSpecificAmount(all); + testAMMClawbackExceedBalance(all); + testAMMClawbackAll(all); + testAMMClawbackSameIssuerAssets(all); + testAMMClawbackSameCurrency(all); + testAMMClawbackIssuesEachOther(all); + testNotHoldingLptoken(all); + testAssetFrozen(all); + testSingleDepositAndClawback(all); + } +}; +BEAST_DEFINE_TESTSUITE(AMMClawback, app, ripple); +} // namespace test +} // namespace ripple diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index ceddc019504..8e764390e9a 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -416,25 +416,10 @@ struct AMM_test : public jtx::AMMTest AMM ammAlice1( env, alice, USD(10'000), USD1(10'000), ter(terNO_RIPPLE)); } - - // Issuer has clawback enabled - { - Env env(*this); - env.fund(XRP(1'000), gw); - env(fset(gw, asfAllowTrustLineClawback)); - fund(env, gw, {alice}, XRP(1'000), {USD(1'000)}, Fund::Acct); - env.close(); - AMM amm(env, gw, XRP(100), USD(100), ter(tecNO_PERMISSION)); - AMM amm1(env, alice, USD(100), XRP(100), ter(tecNO_PERMISSION)); - env(fclear(gw, asfAllowTrustLineClawback)); - env.close(); - // Can't be cleared - AMM amm2(env, gw, XRP(100), USD(100), ter(tecNO_PERMISSION)); - } } void - testInvalidDeposit() + testInvalidDeposit(FeatureBitset features) { testcase("Invalid Deposit"); @@ -869,62 +854,112 @@ struct AMM_test : public jtx::AMMTest }); // Globally frozen asset - testAMM([&](AMM& ammAlice, Env& env) { - env(fset(gw, asfGlobalFreeze)); - // Can deposit non-frozen token - ammAlice.deposit(carol, XRP(100)); - ammAlice.deposit( - carol, - USD(100), - std::nullopt, - std::nullopt, - std::nullopt, - ter(tecFROZEN)); - ammAlice.deposit( - carol, 1'000'000, std::nullopt, std::nullopt, ter(tecFROZEN)); - ammAlice.deposit( - carol, - XRP(100), - USD(100), - std::nullopt, - std::nullopt, - ter(tecFROZEN)); - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + env(fset(gw, asfGlobalFreeze)); + if (!features[featureAMMClawback]) + // If the issuer set global freeze, the holder still can + // deposit the other non-frozen token when AMMClawback is + // not enabled. + ammAlice.deposit(carol, XRP(100)); + else + // If the issuer set global freeze, the holder cannot + // deposit the other non-frozen token when AMMClawback is + // enabled. + ammAlice.deposit( + carol, + XRP(100), + std::nullopt, + std::nullopt, + std::nullopt, + ter(tecFROZEN)); + ammAlice.deposit( + carol, + USD(100), + std::nullopt, + std::nullopt, + std::nullopt, + ter(tecFROZEN)); + ammAlice.deposit( + carol, + 1'000'000, + std::nullopt, + std::nullopt, + ter(tecFROZEN)); + ammAlice.deposit( + carol, + XRP(100), + USD(100), + std::nullopt, + std::nullopt, + ter(tecFROZEN)); + }, + std::nullopt, + 0, + std::nullopt, + {features}); // Individually frozen (AMM) account - testAMM([&](AMM& ammAlice, Env& env) { - env(trust(gw, carol["USD"](0), tfSetFreeze)); - env.close(); - // Can deposit non-frozen token - ammAlice.deposit(carol, XRP(100)); - ammAlice.deposit( - carol, 1'000'000, std::nullopt, std::nullopt, ter(tecFROZEN)); - ammAlice.deposit( - carol, - USD(100), - std::nullopt, - std::nullopt, - std::nullopt, - ter(tecFROZEN)); - env(trust(gw, carol["USD"](0), tfClearFreeze)); - // Individually frozen AMM - env(trust( - gw, - STAmount{Issue{gw["USD"].currency, ammAlice.ammAccount()}, 0}, - tfSetFreeze)); - env.close(); - // Can deposit non-frozen token - ammAlice.deposit(carol, XRP(100)); - ammAlice.deposit( - carol, 1'000'000, std::nullopt, std::nullopt, ter(tecFROZEN)); - ammAlice.deposit( - carol, - USD(100), - std::nullopt, - std::nullopt, - std::nullopt, - ter(tecFROZEN)); - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + env(trust(gw, carol["USD"](0), tfSetFreeze)); + env.close(); + if (!features[featureAMMClawback]) + // Can deposit non-frozen token if AMMClawback is not + // enabled + ammAlice.deposit(carol, XRP(100)); + else + // Cannot deposit non-frozen token if the other token is + // frozen when AMMClawback is enabled + ammAlice.deposit( + carol, + XRP(100), + std::nullopt, + std::nullopt, + std::nullopt, + ter(tecFROZEN)); + + ammAlice.deposit( + carol, + 1'000'000, + std::nullopt, + std::nullopt, + ter(tecFROZEN)); + ammAlice.deposit( + carol, + USD(100), + std::nullopt, + std::nullopt, + std::nullopt, + ter(tecFROZEN)); + env(trust(gw, carol["USD"](0), tfClearFreeze)); + // Individually frozen AMM + env(trust( + gw, + STAmount{ + Issue{gw["USD"].currency, ammAlice.ammAccount()}, 0}, + tfSetFreeze)); + env.close(); + // Can deposit non-frozen token + ammAlice.deposit(carol, XRP(100)); + ammAlice.deposit( + carol, + 1'000'000, + std::nullopt, + std::nullopt, + ter(tecFROZEN)); + ammAlice.deposit( + carol, + USD(100), + std::nullopt, + std::nullopt, + std::nullopt, + ter(tecFROZEN)); + }, + std::nullopt, + 0, + std::nullopt, + {features}); // Individually frozen (AMM) account with IOU/IOU AMM testAMM( @@ -970,6 +1005,44 @@ struct AMM_test : public jtx::AMMTest }, {{USD(20'000), BTC(0.5)}}); + // Deposit unauthorized token. + { + Env env(*this, features); + env.fund(XRP(1000), gw, alice, bob); + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(gw, alice["USD"](100)), txflags(tfSetfAuth)); + env(trust(alice, gw["USD"](20))); + env.close(); + env(pay(gw, alice, gw["USD"](10))); + env.close(); + env(trust(gw, bob["USD"](100))); + env.close(); + + AMM amm(env, alice, XRP(10), gw["USD"](10), ter(tesSUCCESS)); + env.close(); + + if (features[featureAMMClawback]) + // if featureAMMClawback is enabled, bob can not deposit XRP + // because he's not authorized to hold the paired token + // gw["USD"]. + amm.deposit( + bob, + XRP(10), + std::nullopt, + std::nullopt, + std::nullopt, + ter(tecNO_AUTH)); + else + amm.deposit( + bob, + XRP(10), + std::nullopt, + std::nullopt, + std::nullopt, + ter(tesSUCCESS)); + } + // Insufficient XRP balance testAMM([&](AMM& ammAlice, Env& env) { env.fund(XRP(1'000), bob); @@ -6862,13 +6935,143 @@ struct AMM_test : public jtx::AMMTest } } + void + testAMMClawback(FeatureBitset features) + { + testcase("test clawback from AMM account"); + using namespace jtx; + + // Issuer has clawback enabled + Env env(*this, features); + env.fund(XRP(1'000), gw); + env(fset(gw, asfAllowTrustLineClawback)); + fund(env, gw, {alice}, XRP(1'000), {USD(1'000)}, Fund::Acct); + env.close(); + + // If featureAMMClawback is not enabled, AMMCreate is not allowed for + // clawback-enabled issuer + if (!features[featureAMMClawback]) + { + AMM amm(env, gw, XRP(100), USD(100), ter(tecNO_PERMISSION)); + AMM amm1(env, alice, USD(100), XRP(100), ter(tecNO_PERMISSION)); + env(fclear(gw, asfAllowTrustLineClawback)); + env.close(); + // Can't be cleared + AMM amm2(env, gw, XRP(100), USD(100), ter(tecNO_PERMISSION)); + } + // If featureAMMClawback is enabled, AMMCreate is allowed for + // clawback-enabled issuer. Clawback from the AMM Account is not + // allowed, which will return tecAMM_ACCOUNT. We can only use + // AMMClawback transaction to claw back from AMM Account. + else + { + AMM amm(env, gw, XRP(100), USD(100), ter(tesSUCCESS)); + AMM amm1(env, alice, USD(100), XRP(200), ter(tecDUPLICATE)); + + // Construct the amount being clawed back using AMM account. + // By doing this, we make the clawback transaction's Amount field's + // subfield `issuer` to be the AMM account, which means + // we are clawing back from an AMM account. This should return an + // tecAMM_ACCOUNT error because regular Clawback transaction is not + // allowed for clawing back from an AMM account. Please notice the + // `issuer` subfield represents the account being clawed back, which + // is confusing. + Issue usd(USD.issue().currency, amm.ammAccount()); + auto amount = amountFromString(usd, "10"); + env(claw(gw, amount), ter(tecAMM_ACCOUNT)); + } + } + + void + testAMMDepositWithFrozenAssets(FeatureBitset features) + { + testcase("test AMMDeposit with frozen assets"); + using namespace jtx; + + // This lambda function is used to create trustlines + // between gw and alice, and create an AMM account. + // And also test the callback function. + auto testAMMDeposit = [&](Env& env, std::function cb) { + env.fund(XRP(1'000), gw); + fund(env, gw, {alice}, XRP(1'000), {USD(1'000)}, Fund::Acct); + env.close(); + AMM amm(env, alice, XRP(100), USD(100), ter(tesSUCCESS)); + env(trust(gw, alice["USD"](0), tfSetFreeze)); + cb(amm); + }; + + // Deposit two assets, one of which is frozen, + // then we should get tecFROZEN error. + { + Env env(*this, features); + testAMMDeposit(env, [&](AMM& amm) { + amm.deposit( + alice, + USD(100), + XRP(100), + std::nullopt, + tfTwoAsset, + ter(tecFROZEN)); + }); + } + + // Deposit one asset, which is the frozen token, + // then we should get tecFROZEN error. + { + Env env(*this, features); + testAMMDeposit(env, [&](AMM& amm) { + amm.deposit( + alice, + USD(100), + std::nullopt, + std::nullopt, + tfSingleAsset, + ter(tecFROZEN)); + }); + } + + if (features[featureAMMClawback]) + { + // Deposit one asset which is not the frozen token, + // but the other asset is frozen. We should get tecFROZEN error + // when feature AMMClawback is enabled. + Env env(*this, features); + testAMMDeposit(env, [&](AMM& amm) { + amm.deposit( + alice, + XRP(100), + std::nullopt, + std::nullopt, + tfSingleAsset, + ter(tecFROZEN)); + }); + } + else + { + // Deposit one asset which is not the frozen token, + // but the other asset is frozen. We will get tecSUCCESS + // when feature AMMClawback is not enabled. + Env env(*this, features); + testAMMDeposit(env, [&](AMM& amm) { + amm.deposit( + alice, + XRP(100), + std::nullopt, + std::nullopt, + tfSingleAsset, + ter(tesSUCCESS)); + }); + } + } + void run() override { FeatureBitset const all{jtx::supported_amendments()}; testInvalidInstance(); testInstanceCreate(); - testInvalidDeposit(); + testInvalidDeposit(all); + testInvalidDeposit(all - featureAMMClawback); testDeposit(); testInvalidWithdraw(); testWithdraw(); @@ -6908,6 +7111,12 @@ struct AMM_test : public jtx::AMMTest testFixAMMOfferBlockedByLOB(all - fixAMMv1_1); testLPTokenBalance(all); testLPTokenBalance(all - fixAMMv1_1); + testAMMClawback(all); + testAMMClawback(all - featureAMMClawback); + testAMMClawback(all - fixAMMv1_1 - featureAMMClawback); + testAMMDepositWithFrozenAssets(all); + testAMMDepositWithFrozenAssets(all - featureAMMClawback); + testAMMDepositWithFrozenAssets(all - fixAMMv1_1 - featureAMMClawback); } }; diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index fa888faea17..9fdad6a0743 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -1443,6 +1443,17 @@ class MPToken_test : public beast::unit_test::suite }; ammBid(sfBidMin); ammBid(sfBidMax); + // AMMClawback + { + Json::Value jv; + jv[jss::TransactionType] = jss::AMMClawback; + jv[jss::Account] = alice.human(); + jv[jss::Holder] = carol.human(); + jv[jss::Asset] = to_json(xrpIssue()); + jv[jss::Asset2] = to_json(USD.issue()); + jv[jss::Amount] = mpt.getJson(JsonOptions::none); + test(jv, jss::Amount.c_str()); + } // CheckCash auto checkCash = [&](SField const& field) { Json::Value jv; diff --git a/src/test/jtx/AMM.h b/src/test/jtx/AMM.h index 77b9c8c9ec6..52039f74aea 100644 --- a/src/test/jtx/AMM.h +++ b/src/test/jtx/AMM.h @@ -438,6 +438,14 @@ trust( std::uint32_t flags = 0); Json::Value pay(Account const& account, AccountID const& to, STAmount const& amount); + +Json::Value +ammClawback( + Account const& issuer, + Account const& holder, + Issue const& asset, + Issue const& asset2, + std::optional const& amount); } // namespace amm } // namespace jtx diff --git a/src/test/jtx/impl/AMM.cpp b/src/test/jtx/impl/AMM.cpp index 4ef4fef7c1e..089d3508d70 100644 --- a/src/test/jtx/impl/AMM.cpp +++ b/src/test/jtx/impl/AMM.cpp @@ -823,6 +823,26 @@ pay(Account const& account, AccountID const& to, STAmount const& amount) jv[jss::Flags] = tfUniversal; return jv; } + +Json::Value +ammClawback( + Account const& issuer, + Account const& holder, + Issue const& asset, + Issue const& asset2, + std::optional const& amount) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::AMMClawback; + jv[jss::Account] = issuer.human(); + jv[jss::Holder] = holder.human(); + jv[jss::Asset] = to_json(asset); + jv[jss::Asset2] = to_json(asset2); + if (amount) + jv[jss::Amount] = amount->getJson(JsonOptions::none); + + return jv; +} } // namespace amm } // namespace jtx } // namespace test diff --git a/src/test/rpc/Status_test.cpp b/src/test/rpc/Status_test.cpp index 1ae8b23c66c..c68131e8131 100644 --- a/src/test/rpc/Status_test.cpp +++ b/src/test/rpc/Status_test.cpp @@ -76,7 +76,7 @@ class codeString_test : public beast::unit_test::suite { auto s = codeString(temBAD_AMOUNT); - expect(s == "temBAD_AMOUNT: Can only send positive amounts.", s); + expect(s == "temBAD_AMOUNT: Malformed: Bad amount.", s); } { @@ -176,7 +176,7 @@ class fillJson_test : public beast::unit_test::suite "temBAD_AMOUNT", temBAD_AMOUNT, {}, - "temBAD_AMOUNT: Can only send positive amounts."); + "temBAD_AMOUNT: Malformed: Bad amount."); expectFill( "rpcBAD_SYNTAX", diff --git a/src/xrpld/app/tx/detail/AMMClawback.cpp b/src/xrpld/app/tx/detail/AMMClawback.cpp new file mode 100644 index 00000000000..468a5a4c6a2 --- /dev/null +++ b/src/xrpld/app/tx/detail/AMMClawback.cpp @@ -0,0 +1,290 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +NotTEC +AMMClawback::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureAMMClawback)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; // LCOV_EXCL_LINE + + if (ctx.tx.getFlags() & tfAMMClawbackMask) + return temINVALID_FLAG; + + AccountID const issuer = ctx.tx[sfAccount]; + AccountID const holder = ctx.tx[sfHolder]; + + if (issuer == holder) + { + JLOG(ctx.j.trace()) + << "AMMClawback: holder cannot be the same as issuer."; + return temMALFORMED; + } + + std::optional const clawAmount = ctx.tx[~sfAmount]; + auto const asset = ctx.tx[sfAsset]; + + if (isXRP(asset)) + return temMALFORMED; + + if (asset.account != issuer) + { + JLOG(ctx.j.trace()) << "AMMClawback: Asset's account does not " + "match Account field."; + return temMALFORMED; + } + + if (clawAmount && clawAmount->issue() != asset) + { + JLOG(ctx.j.trace()) << "AMMClawback: Amount's issuer/currency subfield " + "does not match Asset field"; + return temBAD_AMOUNT; + } + + if (clawAmount && *clawAmount <= beast::zero) + return temBAD_AMOUNT; + + return preflight2(ctx); +} + +TER +AMMClawback::preclaim(PreclaimContext const& ctx) +{ + auto const asset = ctx.tx[sfAsset]; + auto const asset2 = ctx.tx[sfAsset2]; + auto const sleIssuer = ctx.view.read(keylet::account(ctx.tx[sfAccount])); + if (!sleIssuer) + return terNO_ACCOUNT; // LCOV_EXCL_LINE + + if (!ctx.view.read(keylet::account(ctx.tx[sfHolder]))) + return terNO_ACCOUNT; + + auto const ammSle = ctx.view.read(keylet::amm(asset, asset2)); + if (!ammSle) + { + JLOG(ctx.j.debug()) << "AMM Clawback: Invalid asset pair."; + return terNO_AMM; + } + + std::uint32_t const issuerFlagsIn = sleIssuer->getFieldU32(sfFlags); + + // If AllowTrustLineClawback is not set or NoFreeze is set, return no + // permission + if (!(issuerFlagsIn & lsfAllowTrustLineClawback) || + (issuerFlagsIn & lsfNoFreeze)) + return tecNO_PERMISSION; + + auto const flags = ctx.tx.getFlags(); + if (flags & tfClawTwoAssets && asset.account != asset2.account) + { + JLOG(ctx.j.trace()) + << "AMMClawback: tfClawTwoAssets can only be enabled when two " + "assets in the AMM pool are both issued by the issuer"; + return tecNO_PERMISSION; + } + + return tesSUCCESS; +} + +TER +AMMClawback::doApply() +{ + Sandbox sb(&ctx_.view()); + + auto const ter = applyGuts(sb); + if (ter == tesSUCCESS) + sb.apply(ctx_.rawView()); + + return ter; +} + +TER +AMMClawback::applyGuts(Sandbox& sb) +{ + std::optional const clawAmount = ctx_.tx[~sfAmount]; + AccountID const issuer = ctx_.tx[sfAccount]; + AccountID const holder = ctx_.tx[sfHolder]; + Issue const asset = ctx_.tx[sfAsset]; + Issue const asset2 = ctx_.tx[sfAsset2]; + + auto ammSle = sb.peek(keylet::amm(asset, asset2)); + if (!ammSle) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const ammAccount = (*ammSle)[sfAccount]; + auto const accountSle = sb.read(keylet::account(ammAccount)); + if (!accountSle) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const expected = ammHolds( + sb, + *ammSle, + asset, + asset2, + FreezeHandling::fhIGNORE_FREEZE, + ctx_.journal); + + if (!expected) + return expected.error(); // LCOV_EXCL_LINE + auto const [amountBalance, amount2Balance, lptAMMBalance] = *expected; + + TER result; + STAmount newLPTokenBalance; + STAmount amountWithdraw; + std::optional amount2Withdraw; + + auto const holdLPtokens = ammLPHolds(sb, *ammSle, holder, j_); + if (holdLPtokens == beast::zero) + return tecAMM_BALANCE; + + if (!clawAmount) + // Because we are doing a two-asset withdrawal, + // tfee is actually not used, so pass tfee as 0. + std::tie(result, newLPTokenBalance, amountWithdraw, amount2Withdraw) = + AMMWithdraw::equalWithdrawTokens( + sb, + *ammSle, + holder, + ammAccount, + amountBalance, + amount2Balance, + lptAMMBalance, + holdLPtokens, + holdLPtokens, + 0, + FreezeHandling::fhIGNORE_FREEZE, + WithdrawAll::Yes, + ctx_.journal); + else + std::tie(result, newLPTokenBalance, amountWithdraw, amount2Withdraw) = + equalWithdrawMatchingOneAmount( + sb, + *ammSle, + holder, + ammAccount, + amountBalance, + amount2Balance, + lptAMMBalance, + holdLPtokens, + *clawAmount); + + if (result != tesSUCCESS) + return result; // LCOV_EXCL_LINE + + auto const res = AMMWithdraw::deleteAMMAccountIfEmpty( + sb, ammSle, newLPTokenBalance, asset, asset2, j_); + if (!res.second) + return res.first; // LCOV_EXCL_LINE + + JLOG(ctx_.journal.trace()) + << "AMM Withdraw during AMMClawback: lptoken new balance: " + << to_string(newLPTokenBalance.iou()) + << " old balance: " << to_string(lptAMMBalance.iou()); + + auto const ter = rippleCredit(sb, holder, issuer, amountWithdraw, true, j_); + if (ter != tesSUCCESS) + return ter; // LCOV_EXCL_LINE + + // if the issuer issues both assets and sets flag tfClawTwoAssets, we + // will claw the paired asset as well. We already checked if + // tfClawTwoAssets is enabled, the two assets have to be issued by the + // same issuer. + if (!amount2Withdraw) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const flags = ctx_.tx.getFlags(); + if (flags & tfClawTwoAssets) + return rippleCredit(sb, holder, issuer, *amount2Withdraw, true, j_); + + return tesSUCCESS; +} + +std::tuple> +AMMClawback::equalWithdrawMatchingOneAmount( + Sandbox& sb, + SLE const& ammSle, + AccountID const& holder, + AccountID const& ammAccount, + STAmount const& amountBalance, + STAmount const& amount2Balance, + STAmount const& lptAMMBalance, + STAmount const& holdLPtokens, + STAmount const& amount) +{ + auto frac = Number{amount} / amountBalance; + auto const 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, + // tfee is actually not used, so pass tfee as 0. + return AMMWithdraw::equalWithdrawTokens( + sb, + ammSle, + holder, + ammAccount, + amountBalance, + amount2Balance, + lptAMMBalance, + holdLPtokens, + holdLPtokens, + 0, + FreezeHandling::fhIGNORE_FREEZE, + WithdrawAll::Yes, + ctx_.journal); + + // 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), + lptAMMBalance, + toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac), + 0, + FreezeHandling::fhIGNORE_FREEZE, + WithdrawAll::No, + ctx_.journal); +} + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/AMMClawback.h b/src/xrpld/app/tx/detail/AMMClawback.h new file mode 100644 index 00000000000..fdcfc53e2ca --- /dev/null +++ b/src/xrpld/app/tx/detail/AMMClawback.h @@ -0,0 +1,75 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_AMMCLAWBACK_H_INCLUDED +#define RIPPLE_TX_AMMCLAWBACK_H_INCLUDED + +#include + +namespace ripple { +class Sandbox; +class AMMClawback : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit AMMClawback(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; + +private: + TER + applyGuts(Sandbox& view); + + /** Withdraw both assets by providing maximum amount of asset1, + * asset2's amount will be calculated according to the current proportion. + * Since it is two-asset withdrawal, tfee is omitted. + * @param view + * @param ammAccount current AMM account + * @param amountBalance current AMM asset1 balance + * @param amount2Balance current AMM asset2 balance + * @param lptAMMBalance current AMM LPT balance + * @param amount asset1 withdraw amount + * @return + */ + std::tuple> + equalWithdrawMatchingOneAmount( + Sandbox& view, + SLE const& ammSle, + AccountID const& holder, + AccountID const& ammAccount, + STAmount const& amountBalance, + STAmount const& amount2Balance, + STAmount const& lptAMMBalance, + STAmount const& holdLPtokens, + STAmount const& amount); +}; + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/AMMCreate.cpp b/src/xrpld/app/tx/detail/AMMCreate.cpp index 237e1afa240..31773166d4a 100644 --- a/src/xrpld/app/tx/detail/AMMCreate.cpp +++ b/src/xrpld/app/tx/detail/AMMCreate.cpp @@ -184,7 +184,13 @@ AMMCreate::preclaim(PreclaimContext const& ctx) return tecAMM_INVALID_TOKENS; } - // Disallow AMM if the issuer has clawback enabled + // If featureAMMClawback is enabled, allow AMMCreate without checking + // if the issuer has clawback enabled + if (ctx.view.rules().enabled(featureAMMClawback)) + return tesSUCCESS; + + // Disallow AMM if the issuer has clawback enabled when featureAMMClawback + // is not enabled auto clawbackDisabled = [&](Issue const& issue) -> TER { if (isXRP(issue)) return tesSUCCESS; diff --git a/src/xrpld/app/tx/detail/AMMDeposit.cpp b/src/xrpld/app/tx/detail/AMMDeposit.cpp index 9bbf5b4a60a..3448401eb79 100644 --- a/src/xrpld/app/tx/detail/AMMDeposit.cpp +++ b/src/xrpld/app/tx/detail/AMMDeposit.cpp @@ -244,6 +244,37 @@ AMMDeposit::preclaim(PreclaimContext const& ctx) : tecUNFUNDED_AMM; }; + if (ctx.view.rules().enabled(featureAMMClawback)) + { + // Check if either of the assets is frozen, AMMDeposit is not allowed + // if either asset is frozen + auto checkAsset = [&](Issue const& asset) -> TER { + if (auto const ter = requireAuth(ctx.view, asset, accountID)) + { + JLOG(ctx.j.debug()) + << "AMM Deposit: account is not authorized, " << asset; + return ter; + } + + if (isFrozen(ctx.view, accountID, asset)) + { + JLOG(ctx.j.debug()) + << "AMM Deposit: account or currency is frozen, " + << to_string(accountID) << " " << to_string(asset.currency); + + return tecFROZEN; + } + + return tesSUCCESS; + }; + + if (auto const ter = checkAsset(ctx.tx[sfAsset])) + return ter; + + if (auto const ter = checkAsset(ctx.tx[sfAsset2])) + return ter; + } + auto const amount = ctx.tx[~sfAmount]; auto const amount2 = ctx.tx[~sfAmount2]; auto const ammAccountID = ammSle->getAccountID(sfAccount); diff --git a/src/xrpld/app/tx/detail/AMMWithdraw.cpp b/src/xrpld/app/tx/detail/AMMWithdraw.cpp index 51b512aba0a..0a6f3291b78 100644 --- a/src/xrpld/app/tx/detail/AMMWithdraw.cpp +++ b/src/xrpld/app/tx/detail/AMMWithdraw.cpp @@ -22,7 +22,6 @@ #include #include #include -#include #include #include #include @@ -358,6 +357,7 @@ AMMWithdraw::applyGuts(Sandbox& sb) if (subTxType & tfTwoAsset) return equalWithdrawLimit( sb, + *ammSle, ammAccountID, amountBalance, amount2Balance, @@ -368,6 +368,7 @@ AMMWithdraw::applyGuts(Sandbox& sb) if (subTxType & tfOneAssetLPToken || subTxType & tfOneAssetWithdrawAll) return singleWithdrawTokens( sb, + *ammSle, ammAccountID, amountBalance, lptAMMBalance, @@ -377,6 +378,7 @@ AMMWithdraw::applyGuts(Sandbox& sb) if (subTxType & tfLimitLPToken) return singleWithdrawEPrice( sb, + *ammSle, ammAccountID, amountBalance, lptAMMBalance, @@ -385,10 +387,18 @@ AMMWithdraw::applyGuts(Sandbox& sb) tfee); if (subTxType & tfSingleAsset) return singleWithdraw( - sb, ammAccountID, amountBalance, lptAMMBalance, *amount, tfee); + sb, + *ammSle, + ammAccountID, + amountBalance, + lptAMMBalance, + *amount, + tfee); if (subTxType & tfLPToken || subTxType & tfWithdrawAll) + { return equalWithdrawTokens( sb, + *ammSle, ammAccountID, amountBalance, amount2Balance, @@ -396,6 +406,7 @@ AMMWithdraw::applyGuts(Sandbox& sb) lpTokens, *lpTokensWithdraw, tfee); + } // should not happen. // LCOV_EXCL_START JLOG(j_.error()) << "AMM Withdraw: invalid options."; @@ -406,22 +417,12 @@ AMMWithdraw::applyGuts(Sandbox& sb) if (result != tesSUCCESS) return {result, false}; - bool updateBalance = true; - if (newLPTokenBalance == beast::zero) - { - if (auto const ter = - deleteAMMAccount(sb, ctx_.tx[sfAsset], ctx_.tx[sfAsset2], j_); - ter != tesSUCCESS && ter != tecINCOMPLETE) - return {ter, false}; - else - updateBalance = (ter == tecINCOMPLETE); - } - - if (updateBalance) - { - ammSle->setFieldAmount(sfLPTokenBalance, newLPTokenBalance); - sb.update(ammSle); - } + auto const res = deleteAMMAccountIfEmpty( + sb, ammSle, newLPTokenBalance, ctx_.tx[sfAsset], ctx_.tx[sfAsset2], j_); + // LCOV_EXCL_START + if (!res.second) + return {res.first, false}; + // LCOV_EXCL_STOP JLOG(ctx_.journal.trace()) << "AMM Withdraw: tokens " << to_string(newLPTokenBalance.iou()) << " " @@ -447,6 +448,7 @@ AMMWithdraw::doApply() std::pair AMMWithdraw::withdraw( Sandbox& view, + SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, STAmount const& amountWithdraw, @@ -455,27 +457,60 @@ AMMWithdraw::withdraw( STAmount const& lpTokensWithdraw, std::uint16_t tfee) { - auto const ammSle = - ctx_.view().read(keylet::amm(ctx_.tx[sfAsset], ctx_.tx[sfAsset2])); - if (!ammSle) - return {tecINTERNAL, STAmount{}}; // LCOV_EXCL_LINE - auto const lpTokens = ammLPHolds(view, *ammSle, account_, ctx_.journal); + TER ter; + STAmount newLPTokenBalance; + std::tie(ter, newLPTokenBalance, std::ignore, std::ignore) = withdraw( + view, + ammSle, + ammAccount, + account_, + amountBalance, + amountWithdraw, + amount2Withdraw, + lpTokensAMMBalance, + lpTokensWithdraw, + tfee, + FreezeHandling::fhZERO_IF_FROZEN, + isWithdrawAll(ctx_.tx), + j_); + return {ter, newLPTokenBalance}; +} + +std::tuple> +AMMWithdraw::withdraw( + Sandbox& view, + SLE const& ammSle, + AccountID const& ammAccount, + AccountID const& account, + STAmount const& amountBalance, + STAmount const& amountWithdraw, + std::optional const& amount2Withdraw, + STAmount const& lpTokensAMMBalance, + STAmount const& lpTokensWithdraw, + std::uint16_t tfee, + FreezeHandling freezeHandling, + WithdrawAll withdrawAll, + beast::Journal const& journal) +{ + auto const lpTokens = ammLPHolds(view, ammSle, account, journal); auto const expected = ammHolds( view, - *ammSle, + ammSle, amountWithdraw.issue(), std::nullopt, - FreezeHandling::fhZERO_IF_FROZEN, - j_); + freezeHandling, + journal); + // LCOV_EXCL_START if (!expected) - return {expected.error(), STAmount{}}; + return {expected.error(), STAmount{}, STAmount{}, STAmount{}}; + // LCOV_EXCL_STOP auto const [curBalance, curBalance2, _] = *expected; (void)_; auto const [amountWithdrawActual, amount2WithdrawActual, lpTokensWithdrawActual] = [&]() -> std::tuple, STAmount> { - if (!(ctx_.tx[sfFlags] & (tfWithdrawAll | tfOneAssetWithdrawAll))) + if (withdrawAll == WithdrawAll::No) return adjustAmountsByLPTokens( amountBalance, amountWithdraw, @@ -491,11 +526,11 @@ AMMWithdraw::withdraw( if (lpTokensWithdrawActual <= beast::zero || lpTokensWithdrawActual > lpTokens) { - JLOG(ctx_.journal.debug()) + JLOG(journal.debug()) << "AMM Withdraw: failed to withdraw, invalid LP tokens: " << lpTokensWithdrawActual << " " << lpTokens << " " << lpTokensAMMBalance; - return {tecAMM_INVALID_TOKENS, STAmount{}}; + return {tecAMM_INVALID_TOKENS, STAmount{}, STAmount{}, STAmount{}}; } // Should not happen since the only LP on last withdraw @@ -503,11 +538,13 @@ AMMWithdraw::withdraw( if (view.rules().enabled(fixAMMv1_1) && lpTokensWithdrawActual > lpTokensAMMBalance) { - JLOG(ctx_.journal.debug()) + // LCOV_EXCL_START + JLOG(journal.debug()) << "AMM Withdraw: failed to withdraw, unexpected LP tokens: " << lpTokensWithdrawActual << " " << lpTokens << " " << lpTokensAMMBalance; - return {tecINTERNAL, STAmount{}}; + return {tecINTERNAL, STAmount{}, STAmount{}, STAmount{}}; + // LCOV_EXCL_STOP } // Withdrawing one side of the pool @@ -516,12 +553,12 @@ AMMWithdraw::withdraw( (amount2WithdrawActual == curBalance2 && amountWithdrawActual != curBalance)) { - JLOG(ctx_.journal.debug()) + JLOG(journal.debug()) << "AMM Withdraw: failed to withdraw one side of the pool " << " curBalance: " << curBalance << " " << amountWithdrawActual << " lpTokensBalance: " << lpTokensWithdraw << " lptBalance " << lpTokensAMMBalance; - return {tecAMM_BALANCE, STAmount{}}; + return {tecAMM_BALANCE, STAmount{}, STAmount{}, STAmount{}}; } // May happen if withdrawing an amount close to one side of the pool @@ -529,42 +566,44 @@ AMMWithdraw::withdraw( (amountWithdrawActual != curBalance || amount2WithdrawActual != curBalance2)) { - JLOG(ctx_.journal.debug()) + JLOG(journal.debug()) << "AMM Withdraw: failed to withdraw all tokens " << " curBalance: " << curBalance << " " << amountWithdrawActual << " curBalance2: " << amount2WithdrawActual.value_or(STAmount{0}) << " lpTokensBalance: " << lpTokensWithdraw << " lptBalance " << lpTokensAMMBalance; - return {tecAMM_BALANCE, STAmount{}}; + return {tecAMM_BALANCE, STAmount{}, STAmount{}, STAmount{}}; } // Withdrawing more than the pool's balance if (amountWithdrawActual > curBalance || amount2WithdrawActual > curBalance2) { - JLOG(ctx_.journal.debug()) + JLOG(journal.debug()) << "AMM Withdraw: withdrawing more than the pool's balance " << " curBalance: " << curBalance << " " << amountWithdrawActual << " curBalance2: " << curBalance2 << " " << (amount2WithdrawActual ? *amount2WithdrawActual : STAmount{}) << " lpTokensBalance: " << lpTokensWithdraw << " lptBalance " << lpTokensAMMBalance; - return {tecAMM_BALANCE, STAmount{}}; + return {tecAMM_BALANCE, STAmount{}, STAmount{}, STAmount{}}; } // Withdraw amountWithdraw auto res = accountSend( view, ammAccount, - account_, + account, amountWithdrawActual, - ctx_.journal, + journal, WaiveTransferFee::Yes); if (res != tesSUCCESS) { - JLOG(ctx_.journal.debug()) + // LCOV_EXCL_START + JLOG(journal.debug()) << "AMM Withdraw: failed to withdraw " << amountWithdrawActual; - return {res, STAmount{}}; + return {res, STAmount{}, STAmount{}, STAmount{}}; + // LCOV_EXCL_STOP } // Withdraw amount2Withdraw @@ -573,40 +612,46 @@ AMMWithdraw::withdraw( res = accountSend( view, ammAccount, - account_, + account, *amount2WithdrawActual, - ctx_.journal, + journal, WaiveTransferFee::Yes); if (res != tesSUCCESS) { - JLOG(ctx_.journal.debug()) << "AMM Withdraw: failed to withdraw " - << *amount2WithdrawActual; - return {res, STAmount{}}; + // LCOV_EXCL_START + JLOG(journal.debug()) << "AMM Withdraw: failed to withdraw " + << *amount2WithdrawActual; + return {res, STAmount{}, STAmount{}, STAmount{}}; + // LCOV_EXCL_STOP } } // Withdraw LP tokens res = redeemIOU( view, - account_, + account, lpTokensWithdrawActual, lpTokensWithdrawActual.issue(), - ctx_.journal); + journal); if (res != tesSUCCESS) { - JLOG(ctx_.journal.debug()) - << "AMM Withdraw: failed to withdraw LPTokens"; - return {res, STAmount{}}; + // LCOV_EXCL_START + JLOG(journal.debug()) << "AMM Withdraw: failed to withdraw LPTokens"; + return {res, STAmount{}, STAmount{}, STAmount{}}; + // LCOV_EXCL_STOP } - return {tesSUCCESS, lpTokensAMMBalance - lpTokensWithdrawActual}; + return std::make_tuple( + tesSUCCESS, + lpTokensAMMBalance - lpTokensWithdrawActual, + amountWithdrawActual, + amount2WithdrawActual); } -/** Proportional withdrawal of pool assets for the amount of LPTokens. - */ std::pair AMMWithdraw::equalWithdrawTokens( Sandbox& view, + SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, STAmount const& amount2Balance, @@ -614,20 +659,94 @@ AMMWithdraw::equalWithdrawTokens( STAmount const& lpTokens, STAmount const& lpTokensWithdraw, std::uint16_t tfee) +{ + TER ter; + STAmount newLPTokenBalance; + std::tie(ter, newLPTokenBalance, std::ignore, std::ignore) = + equalWithdrawTokens( + view, + ammSle, + account_, + ammAccount, + amountBalance, + amount2Balance, + lptAMMBalance, + lpTokens, + lpTokensWithdraw, + tfee, + FreezeHandling::fhZERO_IF_FROZEN, + isWithdrawAll(ctx_.tx), + ctx_.journal); + return {ter, newLPTokenBalance}; +} + +std::pair +AMMWithdraw::deleteAMMAccountIfEmpty( + Sandbox& sb, + std::shared_ptr const ammSle, + STAmount const& lpTokenBalance, + Issue const& issue1, + Issue const& issue2, + beast::Journal const& journal) +{ + TER ter; + bool updateBalance = true; + if (lpTokenBalance == beast::zero) + { + ter = deleteAMMAccount(sb, issue1, issue2, journal); + if (ter != tesSUCCESS && ter != tecINCOMPLETE) + return {ter, false}; // LCOV_EXCL_LINE + else + updateBalance = (ter == tecINCOMPLETE); + } + + if (updateBalance) + { + ammSle->setFieldAmount(sfLPTokenBalance, lpTokenBalance); + sb.update(ammSle); + } + + return {ter, true}; +} + +/** Proportional withdrawal of pool assets for the amount of LPTokens. + */ +std::tuple> +AMMWithdraw::equalWithdrawTokens( + Sandbox& view, + SLE const& ammSle, + AccountID const account, + AccountID const& ammAccount, + STAmount const& amountBalance, + STAmount const& amount2Balance, + STAmount const& lptAMMBalance, + STAmount const& lpTokens, + STAmount const& lpTokensWithdraw, + std::uint16_t tfee, + FreezeHandling freezeHanding, + WithdrawAll withdrawAll, + beast::Journal const& journal) { try { // Withdrawing all tokens in the pool if (lpTokensWithdraw == lptAMMBalance) + { return withdraw( view, + ammSle, ammAccount, + account, amountBalance, amountBalance, amount2Balance, lptAMMBalance, lpTokensWithdraw, - tfee); + tfee, + freezeHanding, + WithdrawAll::Yes, + journal); + } auto const frac = divide(lpTokensWithdraw, lptAMMBalance, noIssue()); auto const withdrawAmount = @@ -639,25 +758,30 @@ AMMWithdraw::equalWithdrawTokens( // withdrawal due to round off. Fail so the user withdraws // more tokens. if (withdrawAmount == beast::zero || withdraw2Amount == beast::zero) - return {tecAMM_FAILED, STAmount{}}; + return {tecAMM_FAILED, STAmount{}, STAmount{}, STAmount{}}; return withdraw( view, + ammSle, ammAccount, + account, amountBalance, withdrawAmount, withdraw2Amount, lptAMMBalance, lpTokensWithdraw, - tfee); + tfee, + freezeHanding, + withdrawAll, + journal); } // LCOV_EXCL_START catch (std::exception const& e) { - JLOG(j_.error()) << "AMMWithdraw::equalWithdrawTokens exception " - << e.what(); + JLOG(journal.error()) + << "AMMWithdraw::equalWithdrawTokens exception " << e.what(); } - return {tecINTERNAL, STAmount{}}; + return {tecINTERNAL, STAmount{}, STAmount{}, STAmount{}}; // LCOV_EXCL_STOP } @@ -689,6 +813,7 @@ AMMWithdraw::equalWithdrawTokens( std::pair AMMWithdraw::equalWithdrawLimit( Sandbox& view, + SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, STAmount const& amount2Balance, @@ -700,8 +825,10 @@ AMMWithdraw::equalWithdrawLimit( auto frac = Number{amount} / amountBalance; auto const amount2Withdraw = amount2Balance * frac; if (amount2Withdraw <= amount2) + { return withdraw( view, + ammSle, ammAccount, amountBalance, amount, @@ -709,11 +836,14 @@ AMMWithdraw::equalWithdrawLimit( lptAMMBalance, toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac), tfee); + } + frac = Number{amount2} / amount2Balance; auto const amountWithdraw = amountBalance * frac; assert(amountWithdraw <= amount); return withdraw( view, + ammSle, ammAccount, amountBalance, toSTAmount(amount.issue(), amountWithdraw), @@ -731,6 +861,7 @@ AMMWithdraw::equalWithdrawLimit( std::pair AMMWithdraw::singleWithdraw( Sandbox& view, + SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, STAmount const& lptAMMBalance, @@ -740,8 +871,10 @@ AMMWithdraw::singleWithdraw( auto const tokens = lpTokensOut(amountBalance, amount, lptAMMBalance, tfee); if (tokens == beast::zero) return {tecAMM_FAILED, STAmount{}}; + return withdraw( view, + ammSle, ammAccount, amountBalance, amount, @@ -764,6 +897,7 @@ AMMWithdraw::singleWithdraw( std::pair AMMWithdraw::singleWithdrawTokens( Sandbox& view, + SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, STAmount const& lptAMMBalance, @@ -774,8 +908,10 @@ AMMWithdraw::singleWithdrawTokens( auto const amountWithdraw = withdrawByTokens(amountBalance, lptAMMBalance, lpTokensWithdraw, tfee); if (amount == beast::zero || amountWithdraw >= amount) + { return withdraw( view, + ammSle, ammAccount, amountBalance, amountWithdraw, @@ -783,6 +919,8 @@ AMMWithdraw::singleWithdrawTokens( lptAMMBalance, lpTokensWithdraw, tfee); + } + return {tecAMM_FAILED, STAmount{}}; } @@ -808,6 +946,7 @@ AMMWithdraw::singleWithdrawTokens( std::pair AMMWithdraw::singleWithdrawEPrice( Sandbox& view, + SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, STAmount const& lptAMMBalance, @@ -833,8 +972,10 @@ AMMWithdraw::singleWithdrawEPrice( return {tecAMM_FAILED, STAmount{}}; auto const amountWithdraw = toSTAmount(amount.issue(), tokens / ePrice); if (amount == beast::zero || amountWithdraw >= amount) + { return withdraw( view, + ammSle, ammAccount, amountBalance, amountWithdraw, @@ -842,8 +983,16 @@ AMMWithdraw::singleWithdrawEPrice( lptAMMBalance, toSTAmount(lptAMMBalance.issue(), tokens), tfee); + } return {tecAMM_FAILED, STAmount{}}; } +WithdrawAll +AMMWithdraw::isWithdrawAll(STTx const& tx) +{ + if (tx[sfFlags] & (tfWithdrawAll | tfOneAssetWithdrawAll)) + return WithdrawAll::Yes; + return WithdrawAll::No; +} } // namespace ripple diff --git a/src/xrpld/app/tx/detail/AMMWithdraw.h b/src/xrpld/app/tx/detail/AMMWithdraw.h index 9e9920aa5f6..f5b6b52e5ba 100644 --- a/src/xrpld/app/tx/detail/AMMWithdraw.h +++ b/src/xrpld/app/tx/detail/AMMWithdraw.h @@ -21,6 +21,7 @@ #define RIPPLE_TX_AMMWITHDRAW_H_INCLUDED #include +#include namespace ripple { @@ -62,6 +63,9 @@ class Sandbox; * @see [XLS30d:AMMWithdraw * transaction](https://github.com/XRPLF/XRPL-Standards/discussions/78) */ + +enum class WithdrawAll : bool { No = false, Yes }; + class AMMWithdraw : public Transactor { public: @@ -80,6 +84,76 @@ class AMMWithdraw : public Transactor TER doApply() override; + /** Equal-asset withdrawal (LPTokens) of some AMM instance pools + * shares represented by the number of LPTokens . + * The trading fee is not charged. + * @param view + * @param ammAccount + * @param amountBalance current LP asset1 balance + * @param amount2Balance current LP asset2 balance + * @param lptAMMBalance current AMM LPT balance + * @param lpTokens current LPT balance + * @param lpTokensWithdraw amount of tokens to withdraw + * @param tfee trading fee in basis points + * @param withdrawAll if withdrawing all lptokens + * @return + */ + static std::tuple> + equalWithdrawTokens( + Sandbox& view, + SLE const& ammSle, + AccountID const account, + AccountID const& ammAccount, + STAmount const& amountBalance, + STAmount const& amount2Balance, + STAmount const& lptAMMBalance, + STAmount const& lpTokens, + STAmount const& lpTokensWithdraw, + std::uint16_t tfee, + FreezeHandling freezeHanding, + WithdrawAll withdrawAll, + beast::Journal const& journal); + + /** Withdraw requested assets and token from AMM into LP account. + * Return new total LPToken balance and the withdrawn amounts for both + * assets. + * @param view + * @param ammSle AMM ledger entry + * @param ammAccount AMM account + * @param amountBalance current LP asset1 balance + * @param amountWithdraw asset1 withdraw amount + * @param amount2Withdraw asset2 withdraw amount + * @param lpTokensAMMBalance current AMM LPT balance + * @param lpTokensWithdraw amount of lptokens to withdraw + * @param tfee trading fee in basis points + * @param withdrawAll if withdraw all lptokens + * @return + */ + static std::tuple> + withdraw( + Sandbox& view, + SLE const& ammSle, + AccountID const& ammAccount, + AccountID const& account, + STAmount const& amountBalance, + STAmount const& amountWithdraw, + std::optional const& amount2Withdraw, + STAmount const& lpTokensAMMBalance, + STAmount const& lpTokensWithdraw, + std::uint16_t tfee, + FreezeHandling freezeHandling, + WithdrawAll withdrawAll, + beast::Journal const& journal); + + static std::pair + deleteAMMAccountIfEmpty( + Sandbox& sb, + std::shared_ptr const ammSle, + STAmount const& lpTokenBalance, + Issue const& issue1, + Issue const& issue2, + beast::Journal const& journal); + private: std::pair applyGuts(Sandbox& view); @@ -87,21 +161,22 @@ class AMMWithdraw : public Transactor /** Withdraw requested assets and token from AMM into LP account. * Return new total LPToken balance. * @param view - * @param ammAccount - * @param amountBalance - * @param amountWithdraw - * @param amount2Withdraw + * @param ammSle AMM ledger entry + * @param ammAccount AMM account + * @param amountBalance current LP asset1 balance + * @param amountWithdraw asset1 withdraw amount + * @param amount2Withdraw asset2 withdraw amount * @param lpTokensAMMBalance current AMM LPT balance - * @param lpTokensWithdraw - * @param tfee + * @param lpTokensWithdraw amount of lptokens to withdraw * @return */ std::pair withdraw( Sandbox& view, + SLE const& ammSle, AccountID const& ammAccount, - STAmount const& amountWithdraw, STAmount const& amountBalance, + STAmount const& amountWithdraw, std::optional const& amount2Withdraw, STAmount const& lpTokensAMMBalance, STAmount const& lpTokensWithdraw, @@ -123,6 +198,7 @@ class AMMWithdraw : public Transactor std::pair equalWithdrawTokens( Sandbox& view, + SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, STAmount const& amount2Balance, @@ -147,6 +223,7 @@ class AMMWithdraw : public Transactor std::pair equalWithdrawLimit( Sandbox& view, + SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, STAmount const& amount2Balance, @@ -168,6 +245,7 @@ class AMMWithdraw : public Transactor std::pair singleWithdraw( Sandbox& view, + SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, STAmount const& lptAMMBalance, @@ -188,6 +266,7 @@ class AMMWithdraw : public Transactor std::pair singleWithdrawTokens( Sandbox& view, + SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, STAmount const& lptAMMBalance, @@ -209,12 +288,17 @@ class AMMWithdraw : public Transactor std::pair singleWithdrawEPrice( Sandbox& view, + SLE const& ammSle, AccountID const& ammAccount, STAmount const& amountBalance, STAmount const& lptAMMBalance, STAmount const& amount, STAmount const& ePrice, std::uint16_t tfee); + + /** Check from the flags if it's withdraw all */ + WithdrawAll + isWithdrawAll(STTx const& tx); }; } // namespace ripple diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index e8bbd0283b5..d1eaf86844d 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -342,11 +342,12 @@ AccountRootsNotDeleted::finalize( return false; } - // A successful AMMWithdraw MAY delete one account root + // A successful AMMWithdraw/AMMClawback MAY delete one account root // when the total AMM LP Tokens balance goes to 0. Not every AMM withdraw // deletes the AMM account, accountsDeleted_ is set if it is deleted. - if (tx.getTxnType() == ttAMM_WITHDRAW && result == tesSUCCESS && - accountsDeleted_ == 1) + if ((tx.getTxnType() == ttAMM_WITHDRAW || + tx.getTxnType() == ttAMM_CLAWBACK) && + result == tesSUCCESS && accountsDeleted_ == 1) return true; if (accountsDeleted_ == 0) diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index f59cd73378b..44c25cb22ef 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include