#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace xrpl::test { /** * Basic tests of AMM that do not use offers. * Tests incorporating offers are in `AMMExtended_test`. */ struct AMM_test : public jtx::AMMTest { // Use small Number mantissas for the life of this test. NumberMantissaScaleGuard const sg_{xrpl::MantissaRange::small}; private: static FeatureBitset testable_amendments() { // For now, just disable SAV entirely, which locks in the small Number // mantissas return jtx::testable_amendments() - featureSingleAssetVault - featureLendingProtocol; } void testInstanceCreate() { testcase("Instance Create"); using namespace jtx; #if NUMBERTODO // XRP to IOU, with featureSingleAssetVault testAMM( [&](AMM& ammAlice, Env&) { BEAST_EXPECT( ammAlice.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0})); }, {}, 0, {}, {testable_amendments() | featureSingleAssetVault}); #endif // XRP to IOU, without featureSingleAssetVault testAMM( [&](AMM& ammAlice, Env&) { BEAST_EXPECT( ammAlice.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0})); }, {}, 0, {}, {testable_amendments() - featureSingleAssetVault}); // IOU to IOU testAMM( [&](AMM& ammAlice, Env&) { BEAST_EXPECT(ammAlice.expectBalances(USD(20'000), BTC(0.5), IOUAmount{100, 0})); }, {{USD(20'000), BTC(0.5)}}); // IOU to IOU + transfer fee { Env env{*this}; fund(env, gw, {alice}, {USD(20'000), BTC(0.5)}, Fund::All); env(rate(gw, 1.25)); env.close(); // no transfer fee on create AMM const ammAlice(env, alice, USD(20'000), BTC(0.5)); BEAST_EXPECT(ammAlice.expectBalances(USD(20'000), BTC(0.5), IOUAmount{100, 0})); BEAST_EXPECT(expectHolding(env, alice, USD(0))); BEAST_EXPECT(expectHolding(env, alice, BTC(0))); } // Require authorization is set, account is authorized { Env env{*this}; env.fund(XRP(30'000), gw, alice); env.close(); env(fset(gw, asfRequireAuth)); env(trust(alice, gw["USD"](30'000), 0)); env(trust(gw, alice["USD"](0), tfSetfAuth)); env.close(); env(pay(gw, alice, USD(10'000))); env.close(); AMM const ammAlice(env, alice, XRP(10'000), USD(10'000)); } // Cleared global freeze { Env env{*this}; env.fund(XRP(30'000), gw, alice); env.close(); env.trust(USD(30'000), alice); env.close(); env(pay(gw, alice, USD(10'000))); env.close(); env(fset(gw, asfGlobalFreeze)); env.close(); AMM const ammAliceFail(env, alice, XRP(10'000), USD(10'000), ter(tecFROZEN)); env(fclear(gw, asfGlobalFreeze)); env.close(); AMM const ammAlice(env, alice, XRP(10'000), USD(10'000)); } // Trading fee testAMM( [&](AMM& amm, Env&) { BEAST_EXPECT(amm.expectTradingFee(1'000)); BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{0})); }, std::nullopt, 1'000); // Make sure asset comparison works. BEAST_EXPECT( STIssue(sfAsset, STAmount(XRP(2'000)).asset()) == STIssue(sfAsset, STAmount(XRP(2'000)).asset())); BEAST_EXPECT( STIssue(sfAsset, STAmount(XRP(2'000)).asset()) != STIssue(sfAsset, STAmount(USD(2'000)).asset())); } void testInvalidInstance() { testcase("Invalid Instance"); using namespace jtx; // Can't have both XRP tokens { Env env{*this}; fund(env, gw, {alice}, {USD(30'000)}, Fund::All); AMM const ammAlice(env, alice, XRP(10'000), XRP(10'000), ter(temBAD_AMM_TOKENS)); BEAST_EXPECT(!ammAlice.ammExists()); } // Can't have both tokens the same IOU { Env env{*this}; fund(env, gw, {alice}, {USD(30'000)}, Fund::All); AMM const ammAlice(env, alice, USD(10'000), USD(10'000), ter(temBAD_AMM_TOKENS)); BEAST_EXPECT(!ammAlice.ammExists()); } // Can't have zero or negative amounts { Env env{*this}; fund(env, gw, {alice}, {USD(30'000)}, Fund::All); AMM const ammAlice(env, alice, XRP(0), USD(10'000), ter(temBAD_AMOUNT)); BEAST_EXPECT(!ammAlice.ammExists()); AMM const ammAlice1(env, alice, XRP(10'000), USD(0), ter(temBAD_AMOUNT)); BEAST_EXPECT(!ammAlice1.ammExists()); AMM const ammAlice2(env, alice, XRP(10'000), USD(-10'000), ter(temBAD_AMOUNT)); BEAST_EXPECT(!ammAlice2.ammExists()); AMM const ammAlice3(env, alice, XRP(-10'000), USD(10'000), ter(temBAD_AMOUNT)); BEAST_EXPECT(!ammAlice3.ammExists()); } // Bad currency { Env env{*this}; fund(env, gw, {alice}, {USD(30'000)}, Fund::All); AMM const ammAlice(env, alice, XRP(10'000), BAD(10'000), ter(temBAD_CURRENCY)); BEAST_EXPECT(!ammAlice.ammExists()); } // Insufficient IOU balance { Env env{*this}; fund(env, gw, {alice}, {USD(30'000)}, Fund::All); AMM const ammAlice(env, alice, XRP(10'000), USD(40'000), ter(tecUNFUNDED_AMM)); BEAST_EXPECT(!ammAlice.ammExists()); } // Insufficient XRP balance { Env env{*this}; fund(env, gw, {alice}, {USD(30'000)}, Fund::All); AMM const ammAlice(env, alice, XRP(40'000), USD(10'000), ter(tecUNFUNDED_AMM)); BEAST_EXPECT(!ammAlice.ammExists()); } // Invalid trading fee { Env env{*this}; fund(env, gw, {alice}, {USD(30'000)}, Fund::All); AMM const ammAlice( env, alice, XRP(10'000), USD(10'000), false, 65'001, 10, std::nullopt, std::nullopt, std::nullopt, ter(temBAD_FEE)); BEAST_EXPECT(!ammAlice.ammExists()); } // AMM already exists testAMM([&](AMM& ammAlice, Env& env) { AMM const ammCarol(env, carol, XRP(10'000), USD(10'000), ter(tecDUPLICATE)); }); // Invalid flags { Env env{*this}; fund(env, gw, {alice}, {USD(30'000)}, Fund::All); AMM const ammAlice( env, alice, XRP(10'000), USD(10'000), false, 0, 10, tfWithdrawAll, std::nullopt, std::nullopt, ter(temINVALID_FLAG)); BEAST_EXPECT(!ammAlice.ammExists()); } // Invalid Account { Env env{*this}; Account const bad("bad"); env.memoize(bad); AMM const ammAlice( env, bad, XRP(10'000), USD(10'000), false, 0, 10, std::nullopt, seq(1), std::nullopt, ter(terNO_ACCOUNT)); BEAST_EXPECT(!ammAlice.ammExists()); } // Require authorization is set { Env env{*this}; env.fund(XRP(30'000), gw, alice); env.close(); env(fset(gw, asfRequireAuth)); env.close(); env(trust(gw, alice["USD"](30'000))); env.close(); AMM const ammAlice(env, alice, XRP(10'000), USD(10'000), ter(tecNO_AUTH)); BEAST_EXPECT(!ammAlice.ammExists()); } // Globally frozen { Env env{*this}; env.fund(XRP(30'000), gw, alice); env.close(); env(fset(gw, asfGlobalFreeze)); env.close(); env(trust(gw, alice["USD"](30'000))); env.close(); for (auto const& account : {alice, gw}) { AMM const amm(env, account, XRP(10'000), USD(10'000), ter(tecFROZEN)); BEAST_EXPECT(!amm.ammExists()); } } // Individually frozen { Env env{*this}; env.fund(XRP(30'000), gw, alice); env.close(); env(trust(gw, alice["USD"](30'000))); env.close(); env(trust(gw, alice["USD"](0), tfSetFreeze)); env.close(); AMM const ammAlice(env, alice, XRP(10'000), USD(10'000), ter(tecFROZEN)); BEAST_EXPECT(!ammAlice.ammExists()); // issuer can create AMM const amm(env, gw, XRP(10'000), USD(10'000)); } // Insufficient reserve, XRP/IOU { Env env(*this); auto const starting_xrp = XRP(1'000) + reserve(env, 3) + env.current()->fees().base * 4; env.fund(starting_xrp, gw); env.fund(starting_xrp, alice); env.trust(USD(2'000), alice); env.close(); env(pay(gw, alice, USD(2'000))); env.close(); env(offer(alice, XRP(101), USD(100))); env(offer(alice, XRP(102), USD(100))); AMM const ammAlice(env, alice, XRP(1'000), USD(1'000), ter(tecUNFUNDED_AMM)); } // Insufficient reserve, IOU/IOU { Env env(*this); auto const starting_xrp = reserve(env, 4) + env.current()->fees().base * 5; env.fund(starting_xrp, gw); env.fund(starting_xrp, alice); env.trust(USD(2'000), alice); env.trust(EUR(2'000), alice); env.close(); env(pay(gw, alice, USD(2'000))); env(pay(gw, alice, EUR(2'000))); env.close(); env(offer(alice, EUR(101), USD(100))); env(offer(alice, EUR(102), USD(100))); AMM const ammAlice(env, alice, EUR(1'000), USD(1'000), ter(tecINSUF_RESERVE_LINE)); } // Insufficient fee { Env env(*this); fund(env, gw, {alice}, XRP(2'000), {USD(2'000), EUR(2'000)}); AMM const ammAlice( env, alice, EUR(1'000), USD(1'000), false, 0, ammCrtFee(env).drops() - 1, std::nullopt, std::nullopt, std::nullopt, ter(telINSUF_FEE_P)); } // AMM with LPTokens // AMM with one LPToken from another AMM. testAMM([&](AMM& ammAlice, Env& env) { fund(env, gw, {alice}, {EUR(10'000)}, Fund::TokenOnly); AMM const ammAMMToken( env, alice, EUR(10'000), STAmount{ammAlice.lptIssue(), 1'000'000}, ter(tecAMM_INVALID_TOKENS)); AMM const ammAMMToken1( env, alice, STAmount{ammAlice.lptIssue(), 1'000'000}, EUR(10'000), ter(tecAMM_INVALID_TOKENS)); }); // AMM with two LPTokens from other AMMs. testAMM([&](AMM& ammAlice, Env& env) { fund(env, gw, {alice}, {EUR(10'000)}, Fund::TokenOnly); AMM const ammAlice1(env, alice, XRP(10'000), EUR(10'000)); auto const token1 = ammAlice.lptIssue(); auto const token2 = ammAlice1.lptIssue(); AMM const ammAMMTokens( env, alice, STAmount{token1, 1'000'000}, STAmount{token2, 1'000'000}, ter(tecAMM_INVALID_TOKENS)); }); // Issuer has DefaultRipple disabled { Env env(*this); env.fund(XRP(30'000), gw); env(fclear(gw, asfDefaultRipple)); AMM const ammGw(env, gw, XRP(10'000), USD(10'000), ter(terNO_RIPPLE)); env.fund(XRP(30'000), alice); env.trust(USD(30'000), alice); env(pay(gw, alice, USD(30'000))); AMM const ammAlice(env, alice, XRP(10'000), USD(10'000), ter(terNO_RIPPLE)); Account const gw1("gw1"); env.fund(XRP(30'000), gw1); env(fclear(gw1, asfDefaultRipple)); env.trust(USD(30'000), gw1); env(pay(gw, gw1, USD(30'000))); auto const USD1 = gw1["USD"]; AMM const ammGwGw1(env, gw, USD(10'000), USD1(10'000), ter(terNO_RIPPLE)); env.trust(USD1(30'000), alice); env(pay(gw1, alice, USD1(30'000))); AMM const ammAlice1(env, alice, USD(10'000), USD1(10'000), ter(terNO_RIPPLE)); } } void testInvalidDeposit(FeatureBitset features) { testcase("Invalid Deposit"); using namespace jtx; testAMM([&](AMM& ammAlice, Env& env) { // Invalid flags ammAlice.deposit(alice, 1'000'000, std::nullopt, tfWithdrawAll, ter(temINVALID_FLAG)); // Invalid options std::vector, std::optional, std::optional, std::optional, std::optional, std::optional>> const invalidOptions = { // flags, tokens, asset1In, asset2in, EPrice, tfee {tfLPToken, 1'000, std::nullopt, USD(100), std::nullopt, std::nullopt}, {tfLPToken, 1'000, XRP(100), std::nullopt, std::nullopt, std::nullopt}, {tfLPToken, 1'000, std::nullopt, std::nullopt, STAmount{USD, 1, -1}, std::nullopt}, {tfLPToken, std::nullopt, USD(100), std::nullopt, STAmount{USD, 1, -1}, std::nullopt}, {tfLPToken, 1'000, XRP(100), std::nullopt, STAmount{USD, 1, -1}, std::nullopt}, {tfLPToken, 1'000, std::nullopt, std::nullopt, std::nullopt, 1'000}, {tfSingleAsset, 1'000, std::nullopt, std::nullopt, std::nullopt, std::nullopt}, {tfSingleAsset, std::nullopt, std::nullopt, USD(100), std::nullopt, std::nullopt}, {tfSingleAsset, std::nullopt, std::nullopt, std::nullopt, STAmount{USD, 1, -1}, std::nullopt}, {tfSingleAsset, std::nullopt, USD(100), std::nullopt, std::nullopt, 1'000}, {tfTwoAsset, 1'000, std::nullopt, std::nullopt, std::nullopt, std::nullopt}, {tfTwoAsset, std::nullopt, XRP(100), USD(100), STAmount{USD, 1, -1}, std::nullopt}, {tfTwoAsset, std::nullopt, XRP(100), std::nullopt, std::nullopt, std::nullopt}, {tfTwoAsset, std::nullopt, XRP(100), USD(100), std::nullopt, 1'000}, {tfTwoAsset, std::nullopt, std::nullopt, USD(100), STAmount{USD, 1, -1}, std::nullopt}, {tfOneAssetLPToken, 1'000, std::nullopt, std::nullopt, std::nullopt, std::nullopt}, {tfOneAssetLPToken, std::nullopt, XRP(100), USD(100), std::nullopt, std::nullopt}, {tfOneAssetLPToken, std::nullopt, XRP(100), std::nullopt, STAmount{USD, 1, -1}, std::nullopt}, {tfOneAssetLPToken, 1'000, XRP(100), std::nullopt, std::nullopt, 1'000}, {tfLimitLPToken, 1'000, std::nullopt, std::nullopt, std::nullopt, std::nullopt}, {tfLimitLPToken, 1'000, USD(100), std::nullopt, std::nullopt, std::nullopt}, {tfLimitLPToken, std::nullopt, USD(100), XRP(100), std::nullopt, std::nullopt}, {tfLimitLPToken, std::nullopt, XRP(100), std::nullopt, STAmount{USD, 1, -1}, 1'000}, {tfTwoAssetIfEmpty, std::nullopt, std::nullopt, std::nullopt, std::nullopt, 1'000}, {tfTwoAssetIfEmpty, 1'000, std::nullopt, std::nullopt, std::nullopt, std::nullopt}, {tfTwoAssetIfEmpty, std::nullopt, XRP(100), USD(100), STAmount{USD, 1, -1}, std::nullopt}, {tfTwoAssetIfEmpty | tfLPToken, std::nullopt, XRP(100), USD(100), STAmount{USD, 1, -1}, std::nullopt}}; for (auto const& it : invalidOptions) { ammAlice.deposit( alice, std::get<1>(it), std::get<2>(it), std::get<3>(it), std::get<4>(it), std::get<0>(it), std::nullopt, std::nullopt, std::get<5>(it), ter(temMALFORMED)); } { // bad preflight1 Json::Value jv = Json::objectValue; jv[jss::Account] = alice.human(); jv[jss::TransactionType] = jss::AMMDeposit; jv[jss::Asset] = STIssue(sfAsset, XRP).getJson(JsonOptions::none); jv[jss::Asset2] = STIssue(sfAsset, USD).getJson(JsonOptions::none); jv[jss::Fee] = "-1"; env(jv, ter(temBAD_FEE)); } // Invalid tokens ammAlice.deposit(alice, 0, std::nullopt, std::nullopt, ter(temBAD_AMM_TOKENS)); ammAlice.deposit( alice, IOUAmount{-1}, std::nullopt, std::nullopt, ter(temBAD_AMM_TOKENS)); { Json::Value jv = Json::objectValue; jv[jss::Account] = alice.human(); jv[jss::TransactionType] = jss::AMMDeposit; jv[jss::Asset] = STIssue(sfAsset, XRP).getJson(JsonOptions::none); jv[jss::Asset2] = STIssue(sfAsset, USD).getJson(JsonOptions::none); jv[jss::LPTokenOut] = USD(100).value().getJson(JsonOptions::none); jv[jss::Flags] = tfLPToken; env(jv, ter(temBAD_AMM_TOKENS)); } // Invalid trading fee ammAlice.deposit( carol, std::nullopt, XRP(200), USD(200), std::nullopt, tfTwoAssetIfEmpty, std::nullopt, std::nullopt, 10'000, ter(temBAD_FEE)); // Invalid tokens - bogus currency { auto const iss1 = Issue{Currency(0xabc), gw.id()}; auto const iss2 = Issue{Currency(0xdef), gw.id()}; ammAlice.deposit( alice, 1'000, std::nullopt, std::nullopt, std::nullopt, std::nullopt, {{iss1, iss2}}, std::nullopt, std::nullopt, ter(terNO_AMM)); } // Depositing mismatched token, invalid Asset1In.issue ammAlice.deposit( alice, GBP(100), std::nullopt, std::nullopt, std::nullopt, ter(temBAD_AMM_TOKENS)); // Depositing mismatched token, invalid Asset2In.issue ammAlice.deposit( alice, USD(100), GBP(100), std::nullopt, std::nullopt, ter(temBAD_AMM_TOKENS)); // Depositing mismatched token, Asset1In.issue == Asset2In.issue ammAlice.deposit( alice, USD(100), USD(100), std::nullopt, std::nullopt, ter(temBAD_AMM_TOKENS)); // Invalid amount value ammAlice.deposit( alice, USD(0), std::nullopt, std::nullopt, std::nullopt, ter(temBAD_AMOUNT)); ammAlice.deposit( alice, USD(-1'000), std::nullopt, std::nullopt, std::nullopt, ter(temBAD_AMOUNT)); ammAlice.deposit( alice, USD(10), std::nullopt, USD(-1), std::nullopt, ter(temBAD_AMOUNT)); // Bad currency ammAlice.deposit( alice, BAD(100), std::nullopt, std::nullopt, std::nullopt, ter(temBAD_CURRENCY)); // Invalid Account Account const bad("bad"); env.memoize(bad); ammAlice.deposit( bad, 1'000'000, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, seq(1), std::nullopt, ter(terNO_ACCOUNT)); // Invalid AMM ammAlice.deposit( alice, 1'000, std::nullopt, std::nullopt, std::nullopt, std::nullopt, {{USD, GBP}}, std::nullopt, std::nullopt, ter(terNO_AMM)); // Single deposit: 100000 tokens worth of USD // Amount to deposit exceeds Max ammAlice.deposit( carol, 100'000, USD(200), std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, ter(tecAMM_FAILED)); // Single deposit: 100000 tokens worth of XRP // Amount to deposit exceeds Max ammAlice.deposit( carol, 100'000, XRP(200), std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, ter(tecAMM_FAILED)); // Deposit amount is invalid // Calculated amount to deposit is 98,000,000 ammAlice.deposit( alice, USD(0), std::nullopt, STAmount{USD, 1, -1}, std::nullopt, ter(tecUNFUNDED_AMM)); // Calculated amount is 0 ammAlice.deposit( alice, USD(0), std::nullopt, STAmount{USD, 2'000, -6}, std::nullopt, ter(tecAMM_FAILED)); // Deposit non-empty AMM ammAlice.deposit( carol, XRP(100), USD(100), std::nullopt, tfTwoAssetIfEmpty, ter(tecAMM_NOT_EMPTY)); }); // Tiny deposit testAMM( [&](AMM& ammAlice, Env& env) { auto const enabledV1_3 = env.current()->rules().enabled(fixAMMv1_3); auto const err = !enabledV1_3 ? ter(temBAD_AMOUNT) : ter(tesSUCCESS); // Pre-amendment XRP deposit side is rounded to 0 // and deposit fails. // Post-amendment XRP deposit side is rounded to 1 // and deposit succeeds. ammAlice.deposit(carol, IOUAmount{1, -4}, std::nullopt, std::nullopt, err); // Pre/post-amendment LPTokens is rounded to 0 and deposit // fails with tecAMM_INVALID_TOKENS. ammAlice.deposit( carol, STAmount{USD, 1, -12}, std::nullopt, std::nullopt, std::nullopt, ter(tecAMM_INVALID_TOKENS)); }, std::nullopt, 0, std::nullopt, {features, features - fixAMMv1_3}); // Invalid AMM testAMM([&](AMM& ammAlice, Env& env) { ammAlice.withdrawAll(alice); ammAlice.deposit(alice, 10'000, std::nullopt, std::nullopt, ter(terNO_AMM)); }); // Globally frozen asset 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)); } for (auto const& account : {carol, gw}) { ammAlice.deposit( account, USD(100), std::nullopt, std::nullopt, std::nullopt, ter(tecFROZEN)); ammAlice.deposit( account, 1'000'000, std::nullopt, std::nullopt, ter(tecFROZEN)); ammAlice.deposit( account, 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(); 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( [&](AMM& ammAlice, Env& env) { env(trust(gw, carol["USD"](0), tfSetFreeze)); env(trust(gw, carol["BTC"](0), tfSetFreeze)); env.close(); 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(); // Cannot deposit non-frozen token ammAlice.deposit(carol, 1'000'000, std::nullopt, std::nullopt, ter(tecFROZEN)); ammAlice.deposit( carol, USD(100), BTC(0.01), std::nullopt, std::nullopt, ter(tecFROZEN)); }, {{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); env.close(); // Adds LPT trustline ammAlice.deposit(bob, XRP(10)); ammAlice.deposit( bob, XRP(1'000), std::nullopt, std::nullopt, std::nullopt, ter(tecUNFUNDED_AMM)); }); // Insufficient USD balance testAMM([&](AMM& ammAlice, Env& env) { fund(env, gw, {bob}, {USD(1'000)}, Fund::Acct); env.close(); ammAlice.deposit( bob, USD(1'001), std::nullopt, std::nullopt, std::nullopt, ter(tecUNFUNDED_AMM)); }); // Insufficient USD balance by tokens testAMM([&](AMM& ammAlice, Env& env) { fund(env, gw, {bob}, {USD(1'000)}, Fund::Acct); env.close(); ammAlice.deposit( bob, 10'000'000, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, ter(tecUNFUNDED_AMM)); }); // Insufficient XRP balance by tokens testAMM([&](AMM& ammAlice, Env& env) { env.fund(XRP(1'000), bob); env.trust(USD(100'000), bob); env.close(); env(pay(gw, bob, USD(90'000))); env.close(); ammAlice.deposit( bob, 10'000'000, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, ter(tecUNFUNDED_AMM)); }); // Insufficient reserve, XRP/IOU { Env env(*this); auto const starting_xrp = reserve(env, 4) + env.current()->fees().base * 4; env.fund(XRP(10'000), gw); env.fund(XRP(10'000), alice); env.fund(starting_xrp, carol); env.trust(USD(2'000), alice); env.trust(USD(2'000), carol); env.close(); env(pay(gw, alice, USD(2'000))); env(pay(gw, carol, USD(2'000))); env.close(); env(offer(carol, XRP(100), USD(101))); env(offer(carol, XRP(100), USD(102))); AMM ammAlice(env, alice, XRP(1'000), USD(1'000)); ammAlice.deposit( carol, XRP(100), std::nullopt, std::nullopt, std::nullopt, ter(tecINSUF_RESERVE_LINE)); env(offer(carol, XRP(100), USD(103))); ammAlice.deposit( carol, USD(100), std::nullopt, std::nullopt, std::nullopt, ter(tecINSUF_RESERVE_LINE)); } // Insufficient reserve, IOU/IOU { Env env(*this); auto const starting_xrp = reserve(env, 4) + env.current()->fees().base * 4; env.fund(XRP(10'000), gw); env.fund(XRP(10'000), alice); env.fund(starting_xrp, carol); env.trust(USD(2'000), alice); env.trust(EUR(2'000), alice); env.trust(USD(2'000), carol); env.trust(EUR(2'000), carol); env.close(); env(pay(gw, alice, USD(2'000))); env(pay(gw, alice, EUR(2'000))); env(pay(gw, carol, USD(2'000))); env(pay(gw, carol, EUR(2'000))); env.close(); env(offer(carol, XRP(100), USD(101))); env(offer(carol, XRP(100), USD(102))); AMM ammAlice(env, alice, XRP(1'000), USD(1'000)); ammAlice.deposit( carol, XRP(100), std::nullopt, std::nullopt, std::nullopt, ter(tecINSUF_RESERVE_LINE)); } // Invalid min testAMM([&](AMM& ammAlice, Env& env) { // min tokens can't be <= zero ammAlice.deposit(carol, 0, XRP(100), tfSingleAsset, ter(temBAD_AMM_TOKENS)); ammAlice.deposit(carol, -1, XRP(100), tfSingleAsset, ter(temBAD_AMM_TOKENS)); ammAlice.deposit( carol, 0, XRP(100), USD(100), std::nullopt, tfTwoAsset, std::nullopt, std::nullopt, std::nullopt, ter(temBAD_AMM_TOKENS)); // min amounts can't be <= zero ammAlice.deposit( carol, 1'000, XRP(0), USD(100), std::nullopt, tfTwoAsset, std::nullopt, std::nullopt, std::nullopt, ter(temBAD_AMOUNT)); ammAlice.deposit( carol, 1'000, XRP(100), USD(-1), std::nullopt, tfTwoAsset, std::nullopt, std::nullopt, std::nullopt, ter(temBAD_AMOUNT)); // min amount bad currency ammAlice.deposit( carol, 1'000, XRP(100), BAD(100), std::nullopt, tfTwoAsset, std::nullopt, std::nullopt, std::nullopt, ter(temBAD_CURRENCY)); // min amount bad token pair ammAlice.deposit( carol, 1'000, XRP(100), XRP(100), std::nullopt, tfTwoAsset, std::nullopt, std::nullopt, std::nullopt, ter(temBAD_AMM_TOKENS)); ammAlice.deposit( carol, 1'000, XRP(100), GBP(100), std::nullopt, tfTwoAsset, std::nullopt, std::nullopt, std::nullopt, ter(temBAD_AMM_TOKENS)); }); // Min deposit testAMM([&](AMM& ammAlice, Env& env) { // Equal deposit by tokens ammAlice.deposit( carol, 1'000'000, XRP(1'000), USD(1'001), std::nullopt, tfLPToken, std::nullopt, std::nullopt, std::nullopt, ter(tecAMM_FAILED)); ammAlice.deposit( carol, 1'000'000, XRP(1'001), USD(1'000), std::nullopt, tfLPToken, std::nullopt, std::nullopt, std::nullopt, ter(tecAMM_FAILED)); // Equal deposit by asset ammAlice.deposit( carol, 100'001, XRP(100), USD(100), std::nullopt, tfTwoAsset, std::nullopt, std::nullopt, std::nullopt, ter(tecAMM_FAILED)); // Single deposit by asset ammAlice.deposit( carol, 488'090, XRP(1'000), std::nullopt, std::nullopt, tfSingleAsset, std::nullopt, std::nullopt, std::nullopt, ter(tecAMM_FAILED)); }); // Equal deposit, tokens rounded to 0 testAMM([&](AMM& amm, Env& env) { amm.deposit(DepositArg{.tokens = IOUAmount{1, -12}, .err = ter(tecAMM_INVALID_TOKENS)}); }); // Equal deposit limit, tokens rounded to 0 testAMM( [&](AMM& amm, Env& env) { amm.deposit( DepositArg{ .asset1In = STAmount{USD, 1, -15}, .asset2In = XRPAmount{1}, .err = ter(tecAMM_INVALID_TOKENS)}); }, {.pool = {{USD(1'000'000), XRP(1'000'000)}}, .features = {features - fixAMMv1_3}}); testAMM([&](AMM& amm, Env& env) { amm.deposit( DepositArg{ .asset1In = STAmount{USD, 1, -15}, .asset2In = XRPAmount{1}, .err = ter(tecAMM_INVALID_TOKENS)}); }); // Single deposit by asset, tokens rounded to 0 testAMM([&](AMM& amm, Env& env) { amm.deposit( DepositArg{.asset1In = STAmount{USD, 1, -15}, .err = ter(tecAMM_INVALID_TOKENS)}); }); // Single deposit by tokens, tokens rounded to 0 testAMM([&](AMM& amm, Env& env) { amm.deposit( DepositArg{ .tokens = IOUAmount{1, -10}, .asset1In = STAmount{USD, 1, -15}, .err = ter(tecAMM_INVALID_TOKENS)}); }); // Single deposit with EPrice, tokens rounded to 0 testAMM([&](AMM& amm, Env& env) { amm.deposit( DepositArg{ .asset1In = STAmount{USD, 1, -15}, .maxEP = STAmount{USD, 1, -1}, .err = ter(tecAMM_INVALID_TOKENS)}); }); } void testDeposit() { testcase("Deposit"); auto const all = testable_amendments(); using namespace jtx; // Equal deposit: 1000000 tokens, 10% of the current pool testAMM([&](AMM& ammAlice, Env& env) { auto const baseFee = env.current()->fees().base; ammAlice.deposit(carol, 1'000'000); BEAST_EXPECT( ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0})); // 30,000 less deposited 1,000 BEAST_EXPECT(expectHolding(env, carol, USD(29'000))); // 30,000 less deposited 1,000 and 10 drops tx fee BEAST_EXPECT(expectLedgerEntryRoot(env, carol, XRPAmount{29'000'000'000 - baseFee})); }); // equal asset deposit: unit test to exercise the rounding-down of // LPTokens in the AMMHelpers.cpp: adjustLPTokens calculations // The LPTokens need to have 16 significant digits and a fractional part for (Number const& deltaLPTokens : {Number{UINT64_C(100000'0000000009), -10}, Number{UINT64_C(100000'0000000001), -10}}) { testAMM([&](AMM& ammAlice, Env& env) { // initial LPToken balance IOUAmount const initLPToken = ammAlice.getLPTokensBalance(); IOUAmount const newLPTokens{deltaLPTokens}; // carol performs a two-asset deposit ammAlice.deposit(DepositArg{.account = carol, .tokens = newLPTokens}); IOUAmount const finalLPToken = ammAlice.getLPTokensBalance(); // Change in behavior due to rounding down of LPTokens: // there is a decrease in the observed return of LPTokens -- // Inputs Number{UINT64_C(100000'0000000001), -10} and // Number{UINT64_C(100000'0000000009), -10} are both rounded // down to 1e5 BEAST_EXPECT((finalLPToken - initLPToken == IOUAmount{1, 5})); BEAST_EXPECT(finalLPToken - initLPToken < deltaLPTokens); // fraction of newLPTokens/(existing LPToken balance). The // existing LPToken balance is 1e7 Number const fr = deltaLPTokens / 1e7; // The below equations are based on Equation 1, 2 from XLS-30d // specification, Section: 2.3.1.2 Number const deltaXRP = fr * 1e10; Number const deltaUSD = fr * 1e4; STAmount const depositUSD = STAmount{USD, deltaUSD}; STAmount const depositXRP = STAmount{XRP, deltaXRP}; // initial LPTokens (1e7) + newLPTokens BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000) + depositXRP, USD(10'000) + depositUSD, IOUAmount{1, 7} + newLPTokens)); // 30,000 less deposited depositUSD BEAST_EXPECT(expectHolding(env, carol, USD(30'000) - depositUSD)); // 30,000 less deposited depositXRP and 10 drops tx fee BEAST_EXPECT( expectLedgerEntryRoot(env, carol, XRP(30'000) - depositXRP - txfee(env, 1))); }); } // Equal limit deposit: deposit USD100 and XRP proportionally // to the pool composition not to exceed 100XRP. If the amount // exceeds 100XRP then deposit 100XRP and USD proportionally // to the pool composition not to exceed 100USD. Fail if exceeded. // Deposit 100USD/100XRP testAMM([&](AMM& ammAlice, Env&) { ammAlice.deposit(carol, USD(100), XRP(100)); BEAST_EXPECT( ammAlice.expectBalances(XRP(10'100), USD(10'100), IOUAmount{10'100'000, 0})); }); // Equal limit deposit. // Try to deposit 200USD/100XRP. Is truncated to 100USD/100XRP. testAMM([&](AMM& ammAlice, Env&) { ammAlice.deposit(carol, USD(200), XRP(100)); BEAST_EXPECT( ammAlice.expectBalances(XRP(10'100), USD(10'100), IOUAmount{10'100'000, 0})); }); // Try to deposit 100USD/200XRP. Is truncated to 100USD/100XRP. testAMM([&](AMM& ammAlice, Env&) { ammAlice.deposit(carol, USD(100), XRP(200)); BEAST_EXPECT( ammAlice.expectBalances(XRP(10'100), USD(10'100), IOUAmount{10'100'000, 0})); }); // Single deposit: 1000 USD testAMM([&](AMM& ammAlice, Env&) { ammAlice.deposit(carol, USD(1'000)); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), STAmount{USD, UINT64_C(10'999'99999999999), -11}, IOUAmount{10'488'088'48170151, -8})); }); // Single deposit: 1000 XRP testAMM([&](AMM& ammAlice, Env&) { ammAlice.deposit(carol, XRP(1'000)); BEAST_EXPECT(ammAlice.expectBalances( XRP(11'000), USD(10'000), IOUAmount{10'488'088'48170151, -8})); }); // Single deposit: 100000 tokens worth of USD testAMM([&](AMM& ammAlice, Env&) { ammAlice.deposit(carol, 100000, USD(205)); BEAST_EXPECT( ammAlice.expectBalances(XRP(10'000), USD(10'201), IOUAmount{10'100'000, 0})); }); // Single deposit: 100000 tokens worth of XRP testAMM([&](AMM& ammAlice, Env& env) { ammAlice.deposit(carol, 100'000, XRP(205)); BEAST_EXPECT( ammAlice.expectBalances(XRP(10'201), USD(10'000), IOUAmount{10'100'000, 0})); }); // Single deposit with EP not exceeding specified: // 100USD with EP not to exceed 0.1 (AssetIn/TokensOut) testAMM([&](AMM& ammAlice, Env&) { ammAlice.deposit(carol, USD(1'000), std::nullopt, STAmount{USD, 1, -1}); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), STAmount{USD, UINT64_C(10'999'99999999999), -11}, IOUAmount{10'488'088'48170151, -8})); }); // Single deposit with EP not exceeding specified: // 100USD with EP not to exceed 0.002004 (AssetIn/TokensOut) testAMM([&](AMM& ammAlice, Env&) { ammAlice.deposit(carol, USD(100), std::nullopt, STAmount{USD, 2004, -6}); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), STAmount{USD, 10'080'16, -2}, IOUAmount{10'040'000, 0})); }); // Single deposit with EP not exceeding specified: // 0USD with EP not to exceed 0.002004 (AssetIn/TokensOut) testAMM([&](AMM& ammAlice, Env&) { ammAlice.deposit(carol, USD(0), std::nullopt, STAmount{USD, 2004, -6}); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), STAmount{USD, 10'080'16, -2}, IOUAmount{10'040'000, 0})); }); // IOU to IOU + transfer fee { Env env{*this}; fund(env, gw, {alice}, {USD(20'000), BTC(0.5)}, Fund::All); env(rate(gw, 1.25)); env.close(); AMM ammAlice(env, alice, USD(20'000), BTC(0.5)); BEAST_EXPECT(ammAlice.expectBalances(USD(20'000), BTC(0.5), IOUAmount{100, 0})); BEAST_EXPECT(expectHolding(env, alice, USD(0))); BEAST_EXPECT(expectHolding(env, alice, BTC(0))); fund(env, gw, {carol}, {USD(2'000), BTC(0.05)}, Fund::Acct); // no transfer fee on deposit ammAlice.deposit(carol, 10); BEAST_EXPECT(ammAlice.expectBalances(USD(22'000), BTC(0.55), IOUAmount{110, 0})); BEAST_EXPECT(expectHolding(env, carol, USD(0))); BEAST_EXPECT(expectHolding(env, carol, BTC(0))); } // Tiny deposits testAMM([&](AMM& ammAlice, Env&) { ammAlice.deposit(carol, IOUAmount{1, -3}); BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{10'000'000'001}, STAmount{USD, UINT64_C(10'000'000001), -6}, IOUAmount{10'000'000'001, -3})); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{1, -3})); }); testAMM([&](AMM& ammAlice, Env&) { ammAlice.deposit(carol, XRPAmount{1}); BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{10'000'000'001}, USD(10'000), IOUAmount{1'000'000'000049999, -8})); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{49999, -8})); }); testAMM([&](AMM& ammAlice, Env&) { ammAlice.deposit(carol, STAmount{USD, 1, -10}); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), STAmount{USD, UINT64_C(10'000'00000000008), -11}, IOUAmount{10'000'000'00000004, -8})); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{4, -8})); }); // Issuer create/deposit for (auto const& feat : {all, all - fixAMMv1_3}) { Env env(*this, feat); env.fund(XRP(30000), gw); AMM ammGw(env, gw, XRP(10'000), USD(10'000)); BEAST_EXPECT(ammGw.expectBalances(XRP(10'000), USD(10'000), ammGw.tokens())); ammGw.deposit(gw, 1'000'000); BEAST_EXPECT(ammGw.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000})); ammGw.deposit(gw, USD(1'000)); BEAST_EXPECT(ammGw.expectBalances( XRP(11'000), STAmount{USD, UINT64_C(11'999'99999999998), -11}, IOUAmount{11'489'125'29307605, -8})); } // Issuer deposit testAMM([&](AMM& ammAlice, Env& env) { ammAlice.deposit(gw, 1'000'000); BEAST_EXPECT(ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000})); ammAlice.deposit(gw, USD(1'000)); BEAST_EXPECT(ammAlice.expectBalances( XRP(11'000), STAmount{USD, UINT64_C(11'999'99999999998), -11}, IOUAmount{11'489'125'29307605, -8})); }); // Min deposit testAMM([&](AMM& ammAlice, Env& env) { // Equal deposit by tokens ammAlice.deposit( carol, 1'000'000, XRP(1'000), USD(1'000), std::nullopt, tfLPToken, std::nullopt, std::nullopt); BEAST_EXPECT( ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0})); }); testAMM([&](AMM& ammAlice, Env& env) { // Equal deposit by asset ammAlice.deposit( carol, 1'000'000, XRP(1'000), USD(1'000), std::nullopt, tfTwoAsset, std::nullopt, std::nullopt); BEAST_EXPECT( ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0})); }); testAMM([&](AMM& ammAlice, Env& env) { // Single deposit by asset ammAlice.deposit( carol, 488'088, XRP(1'000), std::nullopt, std::nullopt, tfSingleAsset, std::nullopt, std::nullopt); BEAST_EXPECT(ammAlice.expectBalances( XRP(11'000), USD(10'000), IOUAmount{10'488'088'48170151, -8})); }); testAMM([&](AMM& ammAlice, Env& env) { // Single deposit by asset ammAlice.deposit( carol, 488'088, USD(1'000), std::nullopt, std::nullopt, tfSingleAsset, std::nullopt, std::nullopt); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), STAmount{USD, UINT64_C(10'999'99999999999), -11}, IOUAmount{10'488'088'48170151, -8})); }); } void testInvalidWithdraw() { testcase("Invalid Withdraw"); auto const all = testable_amendments(); using namespace jtx; testAMM( [&](AMM& ammAlice, Env& env) { WithdrawArg const args{ .asset1Out = XRP(100), .err = ter(tecAMM_BALANCE), }; ammAlice.withdraw(args); }, {{XRP(99), USD(99)}}); testAMM( [&](AMM& ammAlice, Env& env) { WithdrawArg const args{ .asset1Out = USD(100), .err = ter(tecAMM_BALANCE), }; ammAlice.withdraw(args); }, {{XRP(99), USD(99)}}); { Env env{*this}; env.fund(XRP(30'000), gw, alice, bob); env.close(); env(fset(gw, asfRequireAuth)); env.close(); env(trust(alice, gw["USD"](30'000), 0)); env(trust(gw, alice["USD"](0), tfSetfAuth)); // Bob trusts Gateway to owe him USD... env(trust(bob, gw["USD"](30'000), 0)); // ...but Gateway does not authorize Bob to hold its USD. env.close(); env(pay(gw, alice, USD(10'000))); env.close(); AMM ammAlice(env, alice, XRP(10'000), USD(10'000)); WithdrawArg const args{ .account = bob, .asset1Out = USD(100), .err = ter(tecNO_AUTH), }; ammAlice.withdraw(args); } testAMM([&](AMM& ammAlice, Env& env) { // Invalid flags ammAlice.withdraw( alice, 1'000'000, std::nullopt, std::nullopt, std::nullopt, tfBurnable, std::nullopt, std::nullopt, ter(temINVALID_FLAG)); ammAlice.withdraw( alice, 1'000'000, std::nullopt, std::nullopt, std::nullopt, tfTwoAssetIfEmpty, std::nullopt, std::nullopt, ter(temINVALID_FLAG)); // Invalid options std::vector, std::optional, std::optional, std::optional, std::optional, NotTEC>> const invalidOptions = { // tokens, asset1Out, asset2Out, EPrice, flags, ter {std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, temMALFORMED}, {std::nullopt, std::nullopt, std::nullopt, std::nullopt, tfSingleAsset | tfTwoAsset, temMALFORMED}, {1'000, std::nullopt, std::nullopt, std::nullopt, tfWithdrawAll, temMALFORMED}, {std::nullopt, USD(0), XRP(100), std::nullopt, tfWithdrawAll | tfLPToken, temMALFORMED}, {std::nullopt, std::nullopt, USD(100), std::nullopt, tfWithdrawAll, temMALFORMED}, {std::nullopt, std::nullopt, std::nullopt, std::nullopt, tfWithdrawAll | tfOneAssetWithdrawAll, temMALFORMED}, {std::nullopt, USD(100), std::nullopt, std::nullopt, tfWithdrawAll, temMALFORMED}, {std::nullopt, std::nullopt, std::nullopt, std::nullopt, tfOneAssetWithdrawAll, temMALFORMED}, {1'000, std::nullopt, USD(100), std::nullopt, std::nullopt, temMALFORMED}, {std::nullopt, std::nullopt, std::nullopt, IOUAmount{250, 0}, tfWithdrawAll, temMALFORMED}, {1'000, std::nullopt, std::nullopt, IOUAmount{250, 0}, std::nullopt, temMALFORMED}, {std::nullopt, std::nullopt, USD(100), IOUAmount{250, 0}, std::nullopt, temMALFORMED}, {std::nullopt, XRP(100), USD(100), IOUAmount{250, 0}, std::nullopt, temMALFORMED}, {1'000, XRP(100), USD(100), std::nullopt, std::nullopt, temMALFORMED}, {std::nullopt, XRP(100), USD(100), std::nullopt, tfWithdrawAll, temMALFORMED}}; for (auto const& it : invalidOptions) { ammAlice.withdraw( alice, std::get<0>(it), std::get<1>(it), std::get<2>(it), std::get<3>(it), std::get<4>(it), std::nullopt, std::nullopt, ter(std::get<5>(it))); } // Invalid tokens ammAlice.withdraw(alice, 0, std::nullopt, std::nullopt, ter(temBAD_AMM_TOKENS)); ammAlice.withdraw( alice, IOUAmount{-1}, std::nullopt, std::nullopt, ter(temBAD_AMM_TOKENS)); // Mismatched token, invalid Asset1Out issue ammAlice.withdraw(alice, GBP(100), std::nullopt, std::nullopt, ter(temBAD_AMM_TOKENS)); // Mismatched token, invalid Asset2Out issue ammAlice.withdraw(alice, USD(100), GBP(100), std::nullopt, ter(temBAD_AMM_TOKENS)); // Mismatched token, Asset1Out.issue == Asset2Out.issue ammAlice.withdraw(alice, USD(100), USD(100), std::nullopt, ter(temBAD_AMM_TOKENS)); // Invalid amount value ammAlice.withdraw(alice, USD(0), std::nullopt, std::nullopt, ter(temBAD_AMOUNT)); ammAlice.withdraw(alice, USD(-100), std::nullopt, std::nullopt, ter(temBAD_AMOUNT)); ammAlice.withdraw(alice, USD(10), std::nullopt, IOUAmount{-1}, ter(temBAD_AMOUNT)); // Invalid amount/token value, withdraw all tokens from one side // of the pool. ammAlice.withdraw(alice, USD(10'000), std::nullopt, std::nullopt, ter(tecAMM_BALANCE)); ammAlice.withdraw(alice, XRP(10'000), std::nullopt, std::nullopt, ter(tecAMM_BALANCE)); ammAlice.withdraw( alice, std::nullopt, USD(0), std::nullopt, std::nullopt, tfOneAssetWithdrawAll, std::nullopt, std::nullopt, ter(tecAMM_BALANCE)); // Bad currency ammAlice.withdraw(alice, BAD(100), std::nullopt, std::nullopt, ter(temBAD_CURRENCY)); // Invalid Account Account const bad("bad"); env.memoize(bad); ammAlice.withdraw( bad, 1'000'000, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, seq(1), ter(terNO_ACCOUNT)); // Invalid AMM ammAlice.withdraw( alice, 1'000, std::nullopt, std::nullopt, std::nullopt, std::nullopt, {{USD, GBP}}, std::nullopt, ter(terNO_AMM)); // Carol is not a Liquidity Provider ammAlice.withdraw(carol, 10'000, std::nullopt, std::nullopt, ter(tecAMM_BALANCE)); // Withdrawing from one side. // XRP by tokens ammAlice.withdraw( alice, IOUAmount(9'999'999'9999, -4), XRP(0), std::nullopt, ter(tecAMM_BALANCE)); // USD by tokens ammAlice.withdraw( alice, IOUAmount(9'999'999'9, -1), USD(0), std::nullopt, ter(tecAMM_BALANCE)); // XRP ammAlice.withdraw(alice, XRP(10'000), std::nullopt, std::nullopt, ter(tecAMM_BALANCE)); // USD ammAlice.withdraw( alice, STAmount{USD, UINT64_C(9'999'9999999999999), -13}, std::nullopt, std::nullopt, ter(tecAMM_BALANCE)); }); testAMM( [&](AMM& ammAlice, Env& env) { // Withdraw entire one side of the pool. // Pre-amendment: // Equal withdraw but due to XRP rounding // this results in full withdraw of XRP pool only, // while leaving a tiny amount in USD pool. // Post-amendment: // Most of the pool is withdrawn with remaining tiny amounts auto err = env.enabled(fixAMMv1_3) ? ter(tesSUCCESS) : ter(tecAMM_BALANCE); ammAlice.withdraw( alice, IOUAmount{9'999'999'9999, -4}, std::nullopt, std::nullopt, err); if (env.enabled(fixAMMv1_3)) { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount(1), STAmount{USD, 1, -7}, IOUAmount{1, -4})); } }, std::nullopt, 0, std::nullopt, {all, all - fixAMMv1_3}); testAMM( [&](AMM& ammAlice, Env& env) { // Similar to above with even smaller remaining amount // is it ok that the pool is unbalanced? // Withdraw entire one side of the pool. // Equal withdraw but due to XRP precision limit, // this results in full withdraw of XRP pool only, // while leaving a tiny amount in USD pool. auto err = env.enabled(fixAMMv1_3) ? ter(tesSUCCESS) : ter(tecAMM_BALANCE); ammAlice.withdraw( alice, IOUAmount{9'999'999'999999999, -9}, std::nullopt, std::nullopt, err); if (env.enabled(fixAMMv1_3)) { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount(1), STAmount{USD, 1, -11}, IOUAmount{1, -8})); } }, std::nullopt, 0, std::nullopt, {all, all - fixAMMv1_3}); // Invalid AMM testAMM([&](AMM& ammAlice, Env& env) { ammAlice.withdrawAll(alice); ammAlice.withdraw(alice, 10'000, std::nullopt, std::nullopt, ter(terNO_AMM)); }); // Globally frozen asset testAMM([&](AMM& ammAlice, Env& env) { ammAlice.deposit({.account = gw, .asset1In = USD(1'000), .asset2In = XRP(1'000)}); env(fset(gw, asfGlobalFreeze)); env.close(); // Can withdraw non-frozen token for (auto const& account : {alice, gw}) { ammAlice.withdraw(account, XRP(100)); ammAlice.withdraw(account, USD(100), std::nullopt, std::nullopt, ter(tecFROZEN)); ammAlice.withdraw(account, 1'000, std::nullopt, std::nullopt, ter(tecFROZEN)); } }); // Individually frozen (AMM) account testAMM([&](AMM& ammAlice, Env& env) { env(trust(gw, alice["USD"](0), tfSetFreeze)); env.close(); // Can withdraw non-frozen token ammAlice.withdraw(alice, XRP(100)); ammAlice.withdraw(alice, 1'000, std::nullopt, std::nullopt, ter(tecFROZEN)); ammAlice.withdraw(alice, USD(100), std::nullopt, std::nullopt, ter(tecFROZEN)); env(trust(gw, alice["USD"](0), tfClearFreeze)); // Individually frozen AMM env(trust( gw, STAmount{Issue{gw["USD"].currency, ammAlice.ammAccount()}, 0}, tfSetFreeze)); // Can withdraw non-frozen token ammAlice.withdraw(alice, XRP(100)); ammAlice.withdraw(alice, 1'000, std::nullopt, std::nullopt, ter(tecFROZEN)); ammAlice.withdraw(alice, USD(100), std::nullopt, std::nullopt, ter(tecFROZEN)); }); // Carol withdraws more than she owns testAMM([&](AMM& ammAlice, Env&) { // Single deposit of 100000 worth of tokens, // which is 10% of the pool. Carol is LP now. ammAlice.deposit(carol, 1'000'000); BEAST_EXPECT( ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0})); ammAlice.withdraw( carol, 2'000'000, std::nullopt, std::nullopt, ter(tecAMM_INVALID_TOKENS)); BEAST_EXPECT( ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0})); }); // Withdraw with EPrice limit. Fails to withdraw, calculated tokens // to withdraw are 0. testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit(carol, 1'000'000); auto const err = env.enabled(fixAMMv1_3) ? ter(tecAMM_INVALID_TOKENS) : ter(tecAMM_FAILED); ammAlice.withdraw(carol, USD(100), std::nullopt, IOUAmount{500, 0}, err); }, std::nullopt, 0, std::nullopt, {all, all - fixAMMv1_3}); // Withdraw with EPrice limit. Fails to withdraw, calculated tokens // to withdraw are greater than the LP shares. testAMM([&](AMM& ammAlice, Env&) { ammAlice.deposit(carol, 1'000'000); ammAlice.withdraw( carol, USD(100), std::nullopt, IOUAmount{600, 0}, ter(tecAMM_INVALID_TOKENS)); }); // Withdraw with EPrice limit. Fails to withdraw, amount1 // to withdraw is less than 1700USD. testAMM([&](AMM& ammAlice, Env&) { ammAlice.deposit(carol, 1'000'000); ammAlice.withdraw( carol, USD(1'700), std::nullopt, IOUAmount{520, 0}, ter(tecAMM_FAILED)); }); // Deposit/Withdraw the same amount with the trading fee testAMM( [&](AMM& ammAlice, Env&) { ammAlice.deposit(carol, USD(1'000)); ammAlice.withdraw( carol, USD(1'000), std::nullopt, std::nullopt, ter(tecAMM_INVALID_TOKENS)); }, std::nullopt, 1'000); testAMM( [&](AMM& ammAlice, Env&) { ammAlice.deposit(carol, XRP(1'000)); ammAlice.withdraw( carol, XRP(1'000), std::nullopt, std::nullopt, ter(tecAMM_INVALID_TOKENS)); }, std::nullopt, 1'000); // Deposit/Withdraw the same amount fails due to the tokens adjustment testAMM([&](AMM& ammAlice, Env&) { ammAlice.deposit(carol, STAmount{USD, 1, -6}); ammAlice.withdraw( carol, STAmount{USD, 1, -6}, std::nullopt, std::nullopt, ter(tecAMM_INVALID_TOKENS)); }); // Withdraw close to one side of the pool. Account's LP tokens // are rounded to all LP tokens. testAMM( [&](AMM& ammAlice, Env& env) { auto const err = env.enabled(fixAMMv1_3) ? ter(tecINVARIANT_FAILED) : ter(tecAMM_BALANCE); ammAlice.withdraw( alice, STAmount{USD, UINT64_C(9'999'999999999999), -12}, std::nullopt, std::nullopt, err); }, {.features = {all, all - fixAMMv1_3}, .noLog = true}); // Tiny withdraw testAMM([&](AMM& ammAlice, Env&) { // XRP amount to withdraw is 0 ammAlice.withdraw( alice, IOUAmount{1, -5}, std::nullopt, std::nullopt, ter(tecAMM_FAILED)); // Calculated tokens to withdraw are 0 ammAlice.withdraw( alice, std::nullopt, STAmount{USD, 1, -11}, std::nullopt, ter(tecAMM_INVALID_TOKENS)); ammAlice.deposit(carol, STAmount{USD, 1, -10}); ammAlice.withdraw( carol, std::nullopt, STAmount{USD, 1, -9}, std::nullopt, ter(tecAMM_INVALID_TOKENS)); ammAlice.withdraw( carol, std::nullopt, XRPAmount{1}, std::nullopt, ter(tecAMM_INVALID_TOKENS)); ammAlice.withdraw( WithdrawArg{.tokens = IOUAmount{1, -10}, .err = ter(tecAMM_INVALID_TOKENS)}); ammAlice.withdraw( WithdrawArg{ .asset1Out = STAmount{USD, 1, -15}, .asset2Out = XRPAmount{1}, .err = ter(tecAMM_INVALID_TOKENS)}); ammAlice.withdraw( WithdrawArg{ .tokens = IOUAmount{1, -10}, .asset1Out = STAmount{USD, 1, -15}, .err = ter(tecAMM_INVALID_TOKENS)}); }); } void testWithdraw() { testcase("Withdraw"); auto const all = testable_amendments(); using namespace jtx; // Equal withdrawal by Carol: 1000000 of tokens, 10% of the current // pool testAMM([&](AMM& ammAlice, Env& env) { auto const baseFee = env.current()->fees().base.drops(); // Single deposit of 100000 worth of tokens, // which is 10% of the pool. Carol is LP now. ammAlice.deposit(carol, 1'000'000); BEAST_EXPECT( ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0})); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{1'000'000, 0})); // 30,000 less deposited 1,000 BEAST_EXPECT(expectHolding(env, carol, USD(29'000))); // 30,000 less deposited 1,000 and 10 drops tx fee BEAST_EXPECT(expectLedgerEntryRoot(env, carol, XRPAmount{29'000'000'000 - baseFee})); // Carol withdraws all tokens ammAlice.withdraw(carol, 1'000'000); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount(beast::Zero()))); BEAST_EXPECT(expectHolding(env, carol, USD(30'000))); BEAST_EXPECT( expectLedgerEntryRoot(env, carol, XRPAmount{30'000'000'000 - (2 * baseFee)})); }); // Equal withdrawal by tokens 1000000, 10% // of the current pool testAMM([&](AMM& ammAlice, Env&) { ammAlice.withdraw(alice, 1'000'000); BEAST_EXPECT(ammAlice.expectBalances(XRP(9'000), USD(9'000), IOUAmount{9'000'000, 0})); }); // Equal withdrawal with a limit. Withdraw XRP200. // If proportional withdraw of USD is less than 100 // then withdraw that amount, otherwise withdraw USD100 // and proportionally withdraw XRP. It's the latter // in this case - XRP100/USD100. testAMM([&](AMM& ammAlice, Env&) { ammAlice.withdraw(alice, XRP(200), USD(100)); BEAST_EXPECT(ammAlice.expectBalances(XRP(9'900), USD(9'900), IOUAmount{9'900'000, 0})); }); // Equal withdrawal with a limit. XRP100/USD100. testAMM([&](AMM& ammAlice, Env&) { ammAlice.withdraw(alice, XRP(100), USD(200)); BEAST_EXPECT(ammAlice.expectBalances(XRP(9'900), USD(9'900), IOUAmount{9'900'000, 0})); }); // Single withdrawal by amount XRP1000 testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.withdraw(alice, XRP(1'000)); if (!env.enabled(fixAMMv1_3)) { BEAST_EXPECT(ammAlice.expectBalances( XRP(9'000), USD(10'000), IOUAmount{9'486'832'98050514, -8})); } else { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{9'000'000'001}, USD(10'000), IOUAmount{9'486'832'98050514, -8})); } }, std::nullopt, 0, std::nullopt, {all, all - fixAMMv1_3}); // Single withdrawal by tokens 10000. testAMM([&](AMM& ammAlice, Env&) { ammAlice.withdraw(alice, 10'000, USD(0)); BEAST_EXPECT( ammAlice.expectBalances(XRP(10'000), USD(9980.01), IOUAmount{9'990'000, 0})); }); // Withdraw all tokens. testAMM([&](AMM& ammAlice, Env& env) { env(trust(carol, STAmount{ammAlice.lptIssue(), 10'000})); // Can TrustSet only for AMM LP tokens env(trust(carol, STAmount{Issue{EUR.currency, ammAlice.ammAccount()}, 10'000}), ter(tecNO_PERMISSION)); env.close(); ammAlice.withdrawAll(alice); BEAST_EXPECT(!ammAlice.ammExists()); BEAST_EXPECT(!env.le(keylet::ownerDir(ammAlice.ammAccount()))); // Can create AMM for the XRP/USD pair AMM const ammCarol(env, carol, XRP(10'000), USD(10'000)); BEAST_EXPECT( ammCarol.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0})); }); // Single deposit 1000USD, withdraw all tokens in USD testAMM([&](AMM& ammAlice, Env& env) { ammAlice.deposit(carol, USD(1'000)); ammAlice.withdrawAll(carol, USD(0)); BEAST_EXPECT( ammAlice.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0})); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount(beast::Zero()))); }); // Single deposit 1000USD, withdraw all tokens in XRP testAMM([&](AMM& ammAlice, Env&) { ammAlice.deposit(carol, USD(1'000)); ammAlice.withdrawAll(carol, XRP(0)); BEAST_EXPECT(ammAlice.expectBalances( XRPAmount(9'090'909'091), STAmount{USD, UINT64_C(10'999'99999999999), -11}, IOUAmount{10'000'000, 0})); }); // Single deposit/withdraw by the same account testAMM( [&](AMM& ammAlice, Env& env) { // Since a smaller amount might be deposited due to // the lp tokens adjustment, withdrawing by tokens // is generally preferred to withdrawing by amount. auto lpTokens = ammAlice.deposit(carol, USD(1'000)); ammAlice.withdraw(carol, lpTokens, USD(0)); lpTokens = ammAlice.deposit(carol, STAmount(USD, 1, -6)); ammAlice.withdraw(carol, lpTokens, USD(0)); lpTokens = ammAlice.deposit(carol, XRPAmount(1)); ammAlice.withdraw(carol, lpTokens, XRPAmount(0)); if (!env.enabled(fixAMMv1_3)) { BEAST_EXPECT( ammAlice.expectBalances(XRP(10'000), USD(10'000), ammAlice.tokens())); } else { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount(10'000'000'001), USD(10'000), ammAlice.tokens())); } BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0})); }, std::nullopt, 0, std::nullopt, {all, all - fixAMMv1_3}); // Single deposit by different accounts and then withdraw // in reverse. testAMM([&](AMM& ammAlice, Env&) { auto const carolTokens = ammAlice.deposit(carol, USD(1'000)); auto const aliceTokens = ammAlice.deposit(alice, USD(1'000)); ammAlice.withdraw(alice, aliceTokens, USD(0)); ammAlice.withdraw(carol, carolTokens, USD(0)); BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), USD(10'000), ammAlice.tokens())); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0})); BEAST_EXPECT(ammAlice.expectLPTokens(alice, ammAlice.tokens())); }); // Equal deposit 10%, withdraw all tokens testAMM([&](AMM& ammAlice, Env&) { ammAlice.deposit(carol, 1'000'000); ammAlice.withdrawAll(carol); BEAST_EXPECT( ammAlice.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0})); }); // Equal deposit 10%, withdraw all tokens in USD testAMM([&](AMM& ammAlice, Env&) { ammAlice.deposit(carol, 1'000'000); ammAlice.withdrawAll(carol, USD(0)); BEAST_EXPECT(ammAlice.expectBalances( XRP(11'000), STAmount{USD, UINT64_C(9'090'909090909092), -12}, IOUAmount{10'000'000, 0})); }); // Equal deposit 10%, withdraw all tokens in XRP testAMM([&](AMM& ammAlice, Env&) { ammAlice.deposit(carol, 1'000'000); ammAlice.withdrawAll(carol, XRP(0)); BEAST_EXPECT(ammAlice.expectBalances( XRPAmount(9'090'909'091), USD(11'000), IOUAmount{10'000'000, 0})); }); // Withdraw with EPrice limit. testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit(carol, 1'000'000); ammAlice.withdraw(carol, USD(100), std::nullopt, IOUAmount{520, 0}); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{153'846'15384616, -8})); if (!env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3)) { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount(11'000'000'000), STAmount{USD, UINT64_C(9'372'781065088757), -12}, IOUAmount{10'153'846'15384616, -8})); } else if (env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3)) { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount(11'000'000'000), STAmount{USD, UINT64_C(9'372'781065088769), -12}, IOUAmount{10'153'846'15384616, -8})); } else if (env.enabled(fixAMMv1_3)) { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount(11'000'000'000), STAmount{USD, UINT64_C(9'372'78106508877), -11}, IOUAmount{10'153'846'15384616, -8})); } ammAlice.withdrawAll(carol); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0})); }, {.features = {all, all - fixAMMv1_3, all - fixAMMv1_1 - fixAMMv1_3}, .noLog = true}); // Withdraw with EPrice limit. AssetOut is 0. testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit(carol, 1'000'000); ammAlice.withdraw(carol, USD(0), std::nullopt, IOUAmount{520, 0}); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{153'846'15384616, -8})); if (!env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3)) { BEAST_EXPECT(ammAlice.expectBalances( XRP(11'000), STAmount{USD, UINT64_C(9'372'781065088757), -12}, IOUAmount{10'153'846'15384616, -8})); } else if (env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3)) { BEAST_EXPECT(ammAlice.expectBalances( XRP(11'000), STAmount{USD, UINT64_C(9'372'781065088769), -12}, IOUAmount{10'153'846'15384616, -8})); } else if (env.enabled(fixAMMv1_3)) { BEAST_EXPECT(ammAlice.expectBalances( XRP(11'000), STAmount{USD, UINT64_C(9'372'78106508877), -11}, IOUAmount{10'153'846'15384616, -8})); } }, std::nullopt, 0, std::nullopt, {all, all - fixAMMv1_3, all - fixAMMv1_1 - fixAMMv1_3}); // IOU to IOU + transfer fee { Env env{*this}; fund(env, gw, {alice}, {USD(20'000), BTC(0.5)}, Fund::All); env(rate(gw, 1.25)); env.close(); // no transfer fee on create AMM ammAlice(env, alice, USD(20'000), BTC(0.5)); BEAST_EXPECT(ammAlice.expectBalances(USD(20'000), BTC(0.5), IOUAmount{100, 0})); BEAST_EXPECT(expectHolding(env, alice, USD(0))); BEAST_EXPECT(expectHolding(env, alice, BTC(0))); fund(env, gw, {carol}, {USD(2'000), BTC(0.05)}, Fund::Acct); // no transfer fee on deposit ammAlice.deposit(carol, 10); BEAST_EXPECT(ammAlice.expectBalances(USD(22'000), BTC(0.55), IOUAmount{110, 0})); BEAST_EXPECT(expectHolding(env, carol, USD(0))); BEAST_EXPECT(expectHolding(env, carol, BTC(0))); // no transfer fee on withdraw ammAlice.withdraw(carol, 10); BEAST_EXPECT(ammAlice.expectBalances(USD(20'000), BTC(0.5), IOUAmount{100, 0})); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0, 0})); BEAST_EXPECT(expectHolding(env, carol, USD(2'000))); BEAST_EXPECT(expectHolding(env, carol, BTC(0.05))); } // Tiny withdraw testAMM([&](AMM& ammAlice, Env&) { // By tokens ammAlice.withdraw(alice, IOUAmount{1, -3}); BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{9'999'999'999}, STAmount{USD, UINT64_C(9'999'999999), -6}, IOUAmount{9'999'999'999, -3})); }); testAMM( [&](AMM& ammAlice, Env& env) { // Single XRP pool ammAlice.withdraw(alice, std::nullopt, XRPAmount{1}); if (!env.enabled(fixAMMv1_3)) { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{9'999'999'999}, USD(10'000), IOUAmount{9'999'999'9995, -4})); } else { BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), USD(10'000), IOUAmount{9'999'999'9995, -4})); } }, std::nullopt, 0, std::nullopt, {all, all - fixAMMv1_3}); testAMM([&](AMM& ammAlice, Env&) { // Single USD pool ammAlice.withdraw(alice, std::nullopt, STAmount{USD, 1, -10}); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), STAmount{USD, UINT64_C(9'999'9999999999), -10}, IOUAmount{9'999'999'99999995, -8})); }); // Withdraw close to entire pool // Equal by tokens testAMM([&](AMM& ammAlice, Env&) { ammAlice.withdraw(alice, IOUAmount{9'999'999'999, -3}); BEAST_EXPECT( ammAlice.expectBalances(XRPAmount{1}, STAmount{USD, 1, -6}, IOUAmount{1, -3})); }); // USD by tokens testAMM([&](AMM& ammAlice, Env&) { ammAlice.withdraw(alice, IOUAmount{9'999'999}, USD(0)); BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), STAmount{USD, 1, -10}, IOUAmount{1})); }); // XRP by tokens testAMM([&](AMM& ammAlice, Env&) { ammAlice.withdraw(alice, IOUAmount{9'999'900}, XRP(0)); BEAST_EXPECT(ammAlice.expectBalances(XRPAmount{1}, USD(10'000), IOUAmount{100})); }); // USD testAMM([&](AMM& ammAlice, Env&) { ammAlice.withdraw(alice, STAmount{USD, UINT64_C(9'999'99999999999), -11}); BEAST_EXPECT(ammAlice.expectBalances( XRP(10000), STAmount{USD, 1, -11}, IOUAmount{316227765, -9})); }); // XRP testAMM([&](AMM& ammAlice, Env&) { ammAlice.withdraw(alice, XRPAmount{9'999'999'999}); BEAST_EXPECT(ammAlice.expectBalances(XRPAmount{1}, USD(10'000), IOUAmount{100})); }); } void testInvalidFeeVote() { testcase("Invalid Fee Vote"); using namespace jtx; testAMM([&](AMM& ammAlice, Env& env) { // Invalid flags ammAlice.vote( std::nullopt, 1'000, tfWithdrawAll, std::nullopt, std::nullopt, ter(temINVALID_FLAG)); // Invalid fee. ammAlice.vote( std::nullopt, 1'001, std::nullopt, std::nullopt, std::nullopt, ter(temBAD_FEE)); BEAST_EXPECT(ammAlice.expectTradingFee(0)); // Invalid Account Account const bad("bad"); env.memoize(bad); ammAlice.vote(bad, 1'000, std::nullopt, seq(1), std::nullopt, ter(terNO_ACCOUNT)); // Invalid AMM ammAlice.vote(alice, 1'000, std::nullopt, std::nullopt, {{USD, GBP}}, ter(terNO_AMM)); // Account is not LP ammAlice.vote( carol, 1'000, std::nullopt, std::nullopt, std::nullopt, ter(tecAMM_INVALID_TOKENS)); }); // Invalid AMM testAMM([&](AMM& ammAlice, Env& env) { ammAlice.withdrawAll(alice); ammAlice.vote(alice, 1'000, std::nullopt, std::nullopt, std::nullopt, ter(terNO_AMM)); }); } void testFeeVote() { testcase("Fee Vote"); auto const all = testable_amendments(); using namespace jtx; // One vote sets fee to 1%. testAMM([&](AMM& ammAlice, Env& env) { BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{0})); ammAlice.vote({}, 1'000); BEAST_EXPECT(ammAlice.expectTradingFee(1'000)); // Discounted fee is 1/10 of trading fee. BEAST_EXPECT(ammAlice.expectAuctionSlot(100, 0, IOUAmount{0})); }); auto vote = [&](AMM& ammAlice, Env& env, int i, int fundUSD = 100'000, std::uint32_t tokens = 10'000'000, std::vector* accounts = nullptr) { Account a(std::to_string(i)); // post-amendment the amount to deposit is slightly higher // in order to ensure AMM invariant sqrt(asset1 * asset2) >= tokens // fund just one USD higher in this case, which is enough for // deposit to succeed if (env.enabled(fixAMMv1_3)) ++fundUSD; fund(env, gw, {a}, {USD(fundUSD)}, Fund::Acct); ammAlice.deposit(a, tokens); ammAlice.vote(a, 50 * (i + 1)); if (accounts) accounts->push_back(std::move(a)); }; // Eight votes fill all voting slots, set fee 0.175%. testAMM( [&](AMM& ammAlice, Env& env) { for (int i = 0; i < 7; ++i) vote(ammAlice, env, i, 10'000); BEAST_EXPECT(ammAlice.expectTradingFee(175)); }, std::nullopt, 0, std::nullopt, {all}); // Eight votes fill all voting slots, set fee 0.175%. // New vote, same account, sets fee 0.225% testAMM([&](AMM& ammAlice, Env& env) { for (int i = 0; i < 7; ++i) vote(ammAlice, env, i); BEAST_EXPECT(ammAlice.expectTradingFee(175)); Account const a("0"); ammAlice.vote(a, 450); BEAST_EXPECT(ammAlice.expectTradingFee(225)); }); // Eight votes fill all voting slots, set fee 0.175%. // New vote, new account, higher vote weight, set higher fee 0.244% testAMM([&](AMM& ammAlice, Env& env) { for (int i = 0; i < 7; ++i) vote(ammAlice, env, i); BEAST_EXPECT(ammAlice.expectTradingFee(175)); vote(ammAlice, env, 7, 100'000, 20'000'000); BEAST_EXPECT(ammAlice.expectTradingFee(244)); }); // Eight votes fill all voting slots, set fee 0.219%. // New vote, new account, higher vote weight, set smaller fee 0.206% testAMM([&](AMM& ammAlice, Env& env) { for (int i = 7; i > 0; --i) vote(ammAlice, env, i); BEAST_EXPECT(ammAlice.expectTradingFee(219)); vote(ammAlice, env, 0, 100'000, 20'000'000); BEAST_EXPECT(ammAlice.expectTradingFee(206)); }); // Eight votes fill all voting slots. The accounts then withdraw all // tokens. An account sets a new fee and the previous slots are // deleted. testAMM([&](AMM& ammAlice, Env& env) { std::vector accounts; for (int i = 0; i < 7; ++i) vote(ammAlice, env, i, 100'000, 10'000'000, &accounts); BEAST_EXPECT(ammAlice.expectTradingFee(175)); for (int i = 0; i < 7; ++i) ammAlice.withdrawAll(accounts[i]); ammAlice.deposit(carol, 10'000'000); ammAlice.vote(carol, 1'000); // The initial LP set the fee to 1000. Carol gets 50% voting // power, and the new fee is 500. BEAST_EXPECT(ammAlice.expectTradingFee(500)); }); // Eight votes fill all voting slots. The accounts then withdraw some // tokens. The new vote doesn't get the voting power but // the slots are refreshed and the fee is updated. testAMM([&](AMM& ammAlice, Env& env) { std::vector accounts; for (int i = 0; i < 7; ++i) vote(ammAlice, env, i, 100'000, 10'000'000, &accounts); BEAST_EXPECT(ammAlice.expectTradingFee(175)); for (int i = 0; i < 7; ++i) ammAlice.withdraw(accounts[i], 9'000'000); ammAlice.deposit(carol, 1'000); // The vote is not added to the slots ammAlice.vote(carol, 1'000); auto const info = ammAlice.ammRpcInfo()[jss::amm][jss::vote_slots]; for (std::uint32_t i = 0; i < info.size(); ++i) BEAST_EXPECT(info[i][jss::account] != carol.human()); // But the slots are refreshed and the fee is changed BEAST_EXPECT(ammAlice.expectTradingFee(82)); }); } void testInvalidBid() { testcase("Invalid Bid"); using namespace jtx; using namespace std::chrono; // burn all the LPTokens through a AMMBid transaction { Env env(*this); fund(env, gw, {alice}, XRP(2'000), {USD(2'000)}); AMM amm(env, gw, XRP(1'000), USD(1'000), false, 1'000); // auction slot is owned by the creator of the AMM i.e. gw BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{0})); // gw attempts to burn all her LPTokens through a bid transaction // this transaction fails because AMMBid transaction can not burn // all the outstanding LPTokens env(amm.bid({ .account = gw, .bidMin = 1'000'000, }), ter(tecAMM_INVALID_TOKENS)); } // burn all the LPTokens through a AMMBid transaction { Env env(*this); fund(env, gw, {alice}, XRP(2'000), {USD(2'000)}); AMM amm(env, gw, XRP(1'000), USD(1'000), false, 1'000); // auction slot is owned by the creator of the AMM i.e. gw BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{0})); // gw burns all but one of its LPTokens through a bid transaction // this transaction succeeds because the bid price is less than // the total outstanding LPToken balance env(amm.bid({ .account = gw, .bidMin = STAmount{amm.lptIssue(), UINT64_C(999'999)}, }), ter(tesSUCCESS)) .close(); // gw must own the auction slot BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{999'999})); // 999'999 tokens are burned, only 1 LPToken is owned by gw BEAST_EXPECT(amm.expectBalances(XRP(1'000), USD(1'000), IOUAmount{1})); // gw owns only 1 LPToken in its balance BEAST_EXPECT(Number{amm.getLPTokensBalance(gw)} == 1); // gw attempts to burn the last of its LPTokens in an AMMBid // transaction. This transaction fails because it would burn all // the remaining LPTokens env(amm.bid({ .account = gw, .bidMin = 1, }), ter(tecAMM_INVALID_TOKENS)); } testAMM([&](AMM& ammAlice, Env& env) { // Invalid flags env(ammAlice.bid({ .account = carol, .bidMin = 0, .flags = tfWithdrawAll, }), ter(temINVALID_FLAG)); ammAlice.deposit(carol, 1'000'000); // Invalid Bid price <= 0 for (auto bid : {0, -100}) { env(ammAlice.bid({ .account = carol, .bidMin = bid, }), ter(temBAD_AMOUNT)); env(ammAlice.bid({ .account = carol, .bidMax = bid, }), ter(temBAD_AMOUNT)); } // Invalid Min/Max combination env(ammAlice.bid({ .account = carol, .bidMin = 200, .bidMax = 100, }), ter(tecAMM_INVALID_TOKENS)); // Invalid Account Account const bad("bad"); env.memoize(bad); env(ammAlice.bid({ .account = bad, .bidMax = 100, }), seq(1), ter(terNO_ACCOUNT)); // Account is not LP Account const dan("dan"); env.fund(XRP(1'000), dan); env(ammAlice.bid({ .account = dan, .bidMin = 100, }), ter(tecAMM_INVALID_TOKENS)); env(ammAlice.bid({ .account = dan, }), ter(tecAMM_INVALID_TOKENS)); // Auth account is invalid. env(ammAlice.bid({ .account = carol, .bidMin = 100, .authAccounts = {bob}, }), ter(terNO_ACCOUNT)); // Invalid Assets env(ammAlice.bid({ .account = alice, .bidMax = 100, .assets = {{USD, GBP}}, }), ter(terNO_AMM)); // Invalid Min/Max issue env(ammAlice.bid({ .account = alice, .bidMax = STAmount{USD, 100}, }), ter(temBAD_AMM_TOKENS)); env(ammAlice.bid({ .account = alice, .bidMin = STAmount{USD, 100}, }), ter(temBAD_AMM_TOKENS)); }); // Invalid AMM testAMM([&](AMM& ammAlice, Env& env) { ammAlice.withdrawAll(alice); env(ammAlice.bid({ .account = alice, .bidMax = 100, }), ter(terNO_AMM)); }); // More than four Auth accounts. testAMM([&](AMM& ammAlice, Env& env) { Account const ed("ed"); Account const bill("bill"); Account const scott("scott"); Account const james("james"); env.fund(XRP(1'000), bob, ed, bill, scott, james); env.close(); ammAlice.deposit(carol, 1'000'000); env(ammAlice.bid({ .account = carol, .bidMin = 100, .authAccounts = {bob, ed, bill, scott, james}, }), ter(temMALFORMED)); }); // Bid price exceeds LP owned tokens testAMM([&](AMM& ammAlice, Env& env) { fund(env, gw, {bob}, XRP(1'000), {USD(100)}, Fund::Acct); ammAlice.deposit(carol, 1'000'000); ammAlice.deposit(bob, 10); env(ammAlice.bid({ .account = carol, .bidMin = 1'000'001, }), ter(tecAMM_INVALID_TOKENS)); env(ammAlice.bid({ .account = carol, .bidMax = 1'000'001, }), ter(tecAMM_INVALID_TOKENS)); env(ammAlice.bid({ .account = carol, .bidMin = 1'000, })); BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{1'000})); // Slot purchase price is more than 1000 but bob only has 10 tokens env(ammAlice.bid({ .account = bob, }), ter(tecAMM_INVALID_TOKENS)); }); // Bid all tokens, still own the slot { Env env(*this); fund(env, gw, {alice, bob}, XRP(1'000), {USD(1'000)}); AMM amm(env, gw, XRP(10), USD(1'000)); auto const lpIssue = amm.lptIssue(); env.trust(STAmount{lpIssue, 100}, alice); env.trust(STAmount{lpIssue, 50}, bob); env(pay(gw, alice, STAmount{lpIssue, 100})); env(pay(gw, bob, STAmount{lpIssue, 50})); env(amm.bid({.account = alice, .bidMin = 100})); // Alice doesn't have any more tokens, but // she still owns the slot. env(amm.bid({ .account = bob, .bidMax = 50, }), ter(tecAMM_FAILED)); } } void testBid(FeatureBitset features) { testcase("Bid"); using namespace jtx; using namespace std::chrono; // For now, just disable SAV entirely, which locks in the small Number // mantissas features = features - featureSingleAssetVault - featureLendingProtocol; // Auction slot initially is owned by AMM creator, who pays 0 price. // Bid 110 tokens. Pay bidMin. testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit(carol, 1'000'000); env(ammAlice.bid({.account = carol, .bidMin = 110})); BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110})); // 110 tokens are burned. BEAST_EXPECT( ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{10'999'890, 0})); }, std::nullopt, 0, std::nullopt, {features}); // Bid with min/max when the pay price is less than min. testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit(carol, 1'000'000); // Bid exactly 110. Pay 110 because the pay price is < 110. env(ammAlice.bid({.account = carol, .bidMin = 110, .bidMax = 110})); BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110})); BEAST_EXPECT( ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{10'999'890})); // Bid exactly 180-200. Pay 180 because the pay price is < 180. env(ammAlice.bid({.account = alice, .bidMin = 180, .bidMax = 200})); BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{180})); BEAST_EXPECT( ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{10'999'814'5, -1})); }, std::nullopt, 0, std::nullopt, {features}); // Start bid at bidMin 110. testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit(carol, 1'000'000); // Bid, pay bidMin. env(ammAlice.bid({.account = carol, .bidMin = 110})); BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110})); fund(env, gw, {bob}, {USD(10'000)}, Fund::Acct); ammAlice.deposit(bob, 1'000'000); // Bid, pay the computed price. env(ammAlice.bid({.account = bob})); BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount(1155, -1))); // Bid bidMax fails because the computed price is higher. env(ammAlice.bid({ .account = carol, .bidMax = 120, }), ter(tecAMM_FAILED)); // Bid MaxSlotPrice succeeds - pay computed price env(ammAlice.bid({.account = carol, .bidMax = 600})); BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{121'275, -3})); // Bid Min/MaxSlotPrice fails because the computed price is not // in range env(ammAlice.bid({ .account = carol, .bidMin = 10, .bidMax = 100, }), ter(tecAMM_FAILED)); // Bid Min/MaxSlotPrice succeeds - pay computed price env(ammAlice.bid({.account = carol, .bidMin = 100, .bidMax = 600})); BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{127'33875, -5})); }, std::nullopt, 0, std::nullopt, {features}); // Slot states. testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit(carol, 1'000'000); fund(env, gw, {bob}, {USD(10'000)}, Fund::Acct); ammAlice.deposit(bob, 1'000'000); if (!features[fixAMMv1_3]) { BEAST_EXPECT(ammAlice.expectBalances( XRP(12'000), USD(12'000), IOUAmount{12'000'000, 0})); } else { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{12'000'000'001}, USD(12'000), IOUAmount{12'000'000, 0})); } // Initial state. Pay bidMin. env(ammAlice.bid({.account = carol, .bidMin = 110})).close(); BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110})); // 1st Interval after close, price for 0th interval. env(ammAlice.bid({.account = bob})); env.close(seconds(AUCTION_SLOT_INTERVAL_DURATION + 1)); BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 1, IOUAmount{1'155, -1})); // 10th Interval after close, price for 1st interval. env(ammAlice.bid({.account = carol})); env.close(seconds((10 * AUCTION_SLOT_INTERVAL_DURATION) + 1)); BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 10, IOUAmount{121'275, -3})); // 20th Interval (expired) after close, price for 10th interval. env(ammAlice.bid({.account = bob})); env.close( seconds((AUCTION_SLOT_TIME_INTERVALS * AUCTION_SLOT_INTERVAL_DURATION) + 1)); BEAST_EXPECT(ammAlice.expectAuctionSlot(0, std::nullopt, IOUAmount{127'33875, -5})); // 0 Interval. env(ammAlice.bid({.account = carol, .bidMin = 110})).close(); BEAST_EXPECT(ammAlice.expectAuctionSlot(0, std::nullopt, IOUAmount{110})); // ~321.09 tokens burnt on bidding fees. if (!features[fixAMMv1_3]) { BEAST_EXPECT(ammAlice.expectBalances( XRP(12'000), USD(12'000), IOUAmount{11'999'678'91, -2})); } else { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{12'000'000'001}, USD(12'000), IOUAmount{11'999'678'91, -2})); } }, std::nullopt, 0, std::nullopt, {features}); // Pool's fee 1%. Bid bidMin. // Auction slot owner and auth account trade at discounted fee - // 1/10 of the trading fee. // Other accounts trade at 1% fee. testAMM( [&](AMM& ammAlice, Env& env) { Account const dan("dan"); Account const ed("ed"); fund(env, gw, {bob, dan, ed}, {USD(20'000)}, Fund::Acct); ammAlice.deposit(bob, 1'000'000); ammAlice.deposit(ed, 1'000'000); ammAlice.deposit(carol, 500'000); ammAlice.deposit(dan, 500'000); auto ammTokens = ammAlice.getLPTokensBalance(); env(ammAlice.bid({ .account = carol, .bidMin = 120, .authAccounts = {bob, ed}, })); auto const slotPrice = IOUAmount{5'200}; ammTokens -= slotPrice; BEAST_EXPECT(ammAlice.expectAuctionSlot(100, 0, slotPrice)); if (!features[fixAMMv1_3]) { BEAST_EXPECT(ammAlice.expectBalances(XRP(13'000), USD(13'000), ammTokens)); } else { BEAST_EXPECT( ammAlice.expectBalances(XRPAmount{13'000'000'003}, USD(13'000), ammTokens)); } // Discounted trade for (int i = 0; i < 10; ++i) { auto tokens = ammAlice.deposit(carol, USD(100)); ammAlice.withdraw(carol, tokens, USD(0)); tokens = ammAlice.deposit(bob, USD(100)); ammAlice.withdraw(bob, tokens, USD(0)); tokens = ammAlice.deposit(ed, USD(100)); ammAlice.withdraw(ed, tokens, USD(0)); } // carol, bob, and ed pay ~0.99USD in fees. if (!features[fixAMMv1_1]) { BEAST_EXPECT( env.balance(carol, USD) == STAmount(USD, UINT64_C(29'499'00572620545), -11)); BEAST_EXPECT( env.balance(bob, USD) == STAmount(USD, UINT64_C(18'999'00572616195), -11)); BEAST_EXPECT( env.balance(ed, USD) == STAmount(USD, UINT64_C(18'999'00572611841), -11)); // USD pool is slightly higher because of the fees. BEAST_EXPECT(ammAlice.expectBalances( XRP(13'000), STAmount(USD, UINT64_C(13'002'98282151419), -11), ammTokens)); } else { BEAST_EXPECT( env.balance(carol, USD) == STAmount(USD, UINT64_C(29'499'00572620544), -11)); BEAST_EXPECT( env.balance(bob, USD) == STAmount(USD, UINT64_C(18'999'00572616194), -11)); BEAST_EXPECT( env.balance(ed, USD) == STAmount(USD, UINT64_C(18'999'0057261184), -10)); // USD pool is slightly higher because of the fees. if (!features[fixAMMv1_3]) { BEAST_EXPECT(ammAlice.expectBalances( XRP(13'000), STAmount(USD, UINT64_C(13'002'98282151422), -11), ammTokens)); } else { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{13'000'000'003}, STAmount(USD, UINT64_C(13'002'98282151422), -11), ammTokens)); } } ammTokens = ammAlice.getLPTokensBalance(); // Trade with the fee for (int i = 0; i < 10; ++i) { auto const tokens = ammAlice.deposit(dan, USD(100)); ammAlice.withdraw(dan, tokens, USD(0)); } // dan pays ~9.94USD, which is ~10 times more in fees than // carol, bob, ed. the discounted fee is 10 times less // than the trading fee. if (!features[fixAMMv1_1]) { BEAST_EXPECT( env.balance(dan, USD) == STAmount(USD, UINT64_C(19'490'056722744), -9)); // USD pool gains more in dan's fees. BEAST_EXPECT(ammAlice.expectBalances( XRP(13'000), STAmount{USD, UINT64_C(13'012'92609877019), -11}, ammTokens)); // Discounted fee payment ammAlice.deposit(carol, USD(100)); ammTokens = ammAlice.getLPTokensBalance(); BEAST_EXPECT(ammAlice.expectBalances( XRP(13'000), STAmount{USD, UINT64_C(13'112'92609877019), -11}, ammTokens)); env(pay(carol, bob, USD(100)), path(~USD), sendmax(XRP(110))); env.close(); // carol pays 100000 drops in fees // 99900668XRP swapped in for 100USD BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{13'100'000'668}, STAmount{USD, UINT64_C(13'012'92609877019), -11}, ammTokens)); } else { if (!features[fixAMMv1_3]) { BEAST_EXPECT( env.balance(dan, USD) == STAmount(USD, UINT64_C(19'490'05672274399), -11)); } else { BEAST_EXPECT( env.balance(dan, USD) == STAmount(USD, UINT64_C(19'490'05672274398), -11)); } // USD pool gains more in dan's fees. if (!features[fixAMMv1_3]) { BEAST_EXPECT(ammAlice.expectBalances( XRP(13'000), STAmount{USD, UINT64_C(13'012'92609877023), -11}, ammTokens)); } else { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{13'000'000'003}, STAmount{USD, UINT64_C(13'012'92609877024), -11}, ammTokens)); } // Discounted fee payment ammAlice.deposit(carol, USD(100)); ammTokens = ammAlice.getLPTokensBalance(); if (!features[fixAMMv1_3]) { BEAST_EXPECT(ammAlice.expectBalances( XRP(13'000), STAmount{USD, UINT64_C(13'112'92609877023), -11}, ammTokens)); } else { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{13'000'000'003}, STAmount{USD, UINT64_C(13'112'92609877024), -11}, ammTokens)); } env(pay(carol, bob, USD(100)), path(~USD), sendmax(XRP(110))); env.close(); // carol pays 100000 drops in fees // 99900668XRP swapped in for 100USD if (!features[fixAMMv1_3]) { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{13'100'000'668}, STAmount{USD, UINT64_C(13'012'92609877023), -11}, ammTokens)); } else { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{13'100'000'671}, STAmount{USD, UINT64_C(13'012'92609877024), -11}, ammTokens)); } } // Payment with the trading fee env(pay(alice, carol, XRP(100)), path(~XRP), sendmax(USD(110))); env.close(); // alice pays ~1.011USD in fees, which is ~10 times more // than carol's fee // 100.099431529USD swapped in for 100XRP if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{13'000'000'668}, STAmount{USD, UINT64_C(13'114'03663047264), -11}, ammTokens)); } else if (features[fixAMMv1_1] && !features[fixAMMv1_3]) { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{13'000'000'668}, STAmount{USD, UINT64_C(13'114'03663047269), -11}, ammTokens)); } else { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{13'000'000'671}, STAmount{USD, UINT64_C(13'114'03663044937), -11}, ammTokens)); } // Auction slot expired, no discounted fee env.close(seconds(TOTAL_TIME_SLOT_SECS + 1)); // clock is parent's based env.close(); if (!features[fixAMMv1_1]) { BEAST_EXPECT( env.balance(carol, USD) == STAmount(USD, UINT64_C(29'399'00572620545), -11)); } else if (!features[fixAMMv1_3]) { BEAST_EXPECT( env.balance(carol, USD) == STAmount(USD, UINT64_C(29'399'00572620544), -11)); } ammTokens = ammAlice.getLPTokensBalance(); for (int i = 0; i < 10; ++i) { auto const tokens = ammAlice.deposit(carol, USD(100)); ammAlice.withdraw(carol, tokens, USD(0)); } // carol pays ~9.94USD in fees, which is ~10 times more in // trading fees vs discounted fee. if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) { BEAST_EXPECT( env.balance(carol, USD) == STAmount(USD, UINT64_C(29'389'06197177128), -11)); BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{13'000'000'668}, STAmount{USD, UINT64_C(13'123'98038490681), -11}, ammTokens)); } else if (features[fixAMMv1_1] && !features[fixAMMv1_3]) { BEAST_EXPECT( env.balance(carol, USD) == STAmount(USD, UINT64_C(29'389'06197177124), -11)); BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{13'000'000'668}, STAmount{USD, UINT64_C(13'123'98038490689), -11}, ammTokens)); } else { BEAST_EXPECT( env.balance(carol, USD) == STAmount(USD, UINT64_C(29'389'06197177129), -11)); BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{13'000'000'671}, STAmount{USD, UINT64_C(13'123'98038488352), -11}, ammTokens)); } env(pay(carol, bob, USD(100)), path(~USD), sendmax(XRP(110))); env.close(); // carol pays ~1.008XRP in trading fee, which is // ~10 times more than the discounted fee. // 99.815876XRP is swapped in for 100USD if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount(13'100'824'790), STAmount{USD, UINT64_C(13'023'98038490681), -11}, ammTokens)); } else if (features[fixAMMv1_1] && !features[fixAMMv1_3]) { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount(13'100'824'790), STAmount{USD, UINT64_C(13'023'98038490689), -11}, ammTokens)); } else { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount(13'100'824'793), STAmount{USD, UINT64_C(13'023'98038488352), -11}, ammTokens)); } }, std::nullopt, 1'000, std::nullopt, {features}); // Bid tiny amount testAMM( [&](AMM& ammAlice, Env& env) { // Bid a tiny amount auto const tiny = Number{STAmount::cMinValue, STAmount::cMinOffset}; env(ammAlice.bid({.account = alice, .bidMin = IOUAmount{tiny}})); // Auction slot purchase price is equal to the tiny amount // since the minSlotPrice is 0 with no trading fee. BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{tiny})); // The purchase price is too small to affect the total tokens BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), USD(10'000), ammAlice.tokens())); // Bid the tiny amount env(ammAlice.bid({ .account = alice, .bidMin = IOUAmount{STAmount::cMinValue, STAmount::cMinOffset}, })); // Pay slightly higher price BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{tiny * Number{105, -2}})); // The purchase price is still too small to affect the total // tokens BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), USD(10'000), ammAlice.tokens())); }, std::nullopt, 0, std::nullopt, {features}); // Reset auth account testAMM( [&](AMM& ammAlice, Env& env) { env(ammAlice.bid({ .account = alice, .bidMin = IOUAmount{100}, .authAccounts = {carol}, })); BEAST_EXPECT(ammAlice.expectAuctionSlot({carol})); env(ammAlice.bid({.account = alice, .bidMin = IOUAmount{100}})); BEAST_EXPECT(ammAlice.expectAuctionSlot({})); Account const bob("bob"); Account const dan("dan"); fund(env, {bob, dan}, XRP(1'000)); env(ammAlice.bid({ .account = alice, .bidMin = IOUAmount{100}, .authAccounts = {bob, dan}, })); BEAST_EXPECT(ammAlice.expectAuctionSlot({bob, dan})); }, std::nullopt, 0, std::nullopt, {features}); // Bid all tokens, still own the slot and trade at a discount { Env env(*this, features); fund(env, gw, {alice, bob}, XRP(2'000), {USD(2'000)}); AMM amm(env, gw, XRP(1'000), USD(1'010), false, 1'000); auto const lpIssue = amm.lptIssue(); env.trust(STAmount{lpIssue, 500}, alice); env.trust(STAmount{lpIssue, 50}, bob); env(pay(gw, alice, STAmount{lpIssue, 500})); env(pay(gw, bob, STAmount{lpIssue, 50})); // Alice doesn't have anymore lp tokens env(amm.bid({.account = alice, .bidMin = 500})); BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{500})); BEAST_EXPECT(expectHolding(env, alice, STAmount{lpIssue, 0})); // But trades with the discounted fee since she still owns the slot. // Alice pays 10011 drops in fees env(pay(alice, bob, USD(10)), path(~USD), sendmax(XRP(11))); BEAST_EXPECT(amm.expectBalances( XRPAmount{1'010'010'011}, USD(1'000), IOUAmount{1'004'487'562112089, -9})); // Bob pays the full fee ~0.1USD env(pay(bob, alice, XRP(10)), path(~XRP), sendmax(USD(11))); if (!features[fixAMMv1_1]) { BEAST_EXPECT(amm.expectBalances( XRPAmount{1'000'010'011}, STAmount{USD, UINT64_C(1'010'10090898081), -11}, IOUAmount{1'004'487'562112089, -9})); } else { BEAST_EXPECT(amm.expectBalances( XRPAmount{1'000'010'011}, STAmount{USD, UINT64_C(1'010'100908980811), -12}, IOUAmount{1'004'487'562112089, -9})); } } // preflight tests { Env env(*this, features); auto const baseFee = env.current()->fees().base; fund(env, gw, {alice, bob}, XRP(2'000), {USD(2'000)}); AMM amm(env, gw, XRP(1'000), USD(1'010), false, 1'000); Json::Value const tx = amm.bid({.account = alice, .bidMin = 500}); { auto jtx = env.jt(tx, seq(1), fee(baseFee)); env.app().config().features.erase(featureAMM); PreflightContext const pfCtx( env.app(), *jtx.stx, env.current()->rules(), tapNONE, env.journal); auto pf = Transactor::invokePreflight(pfCtx); BEAST_EXPECT(pf == temDISABLED); env.app().config().features.insert(featureAMM); } { auto jtx = env.jt(tx, seq(1), fee(baseFee)); jtx.jv["TxnSignature"] = "deadbeef"; jtx.stx = env.ust(jtx); PreflightContext const pfCtx( env.app(), *jtx.stx, env.current()->rules(), tapNONE, env.journal); auto pf = Transactor::invokePreflight(pfCtx); BEAST_EXPECT(!isTesSuccess(pf)); } { auto jtx = env.jt(tx, seq(1), fee(baseFee)); jtx.jv["Asset2"]["currency"] = "XRP"; jtx.jv["Asset2"].removeMember("issuer"); jtx.stx = env.ust(jtx); PreflightContext const pfCtx( env.app(), *jtx.stx, env.current()->rules(), tapNONE, env.journal); auto pf = Transactor::invokePreflight(pfCtx); BEAST_EXPECT(pf == temBAD_AMM_TOKENS); } } } void testInvalidAMMPayment() { testcase("Invalid AMM Payment"); using namespace jtx; using namespace std::chrono; using namespace std::literals::chrono_literals; // Can't pay into AMM account. // Can't pay out since there is no keys for (auto const& acct : {gw, alice}) { { Env env(*this); fund(env, gw, {alice, carol}, XRP(1'000), {USD(100)}); // XRP balance is below reserve AMM const ammAlice(env, acct, XRP(10), USD(10)); // Pay below reserve env(pay(carol, ammAlice.ammAccount(), XRP(10)), ter(tecNO_PERMISSION)); // Pay above reserve env(pay(carol, ammAlice.ammAccount(), XRP(300)), ter(tecNO_PERMISSION)); // Pay IOU env(pay(carol, ammAlice.ammAccount(), USD(10)), ter(tecNO_PERMISSION)); } { Env env(*this); fund(env, gw, {alice, carol}, XRP(10'000'000), {USD(10'000)}); // XRP balance is above reserve AMM const ammAlice(env, acct, XRP(1'000'000), USD(100)); // Pay below reserve env(pay(carol, ammAlice.ammAccount(), XRP(10)), ter(tecNO_PERMISSION)); // Pay above reserve env(pay(carol, ammAlice.ammAccount(), XRP(1'000'000)), ter(tecNO_PERMISSION)); } } // Can't pay into AMM with escrow. testAMM([&](AMM& ammAlice, Env& env) { auto const baseFee = env.current()->fees().base; env(escrow::create(carol, ammAlice.ammAccount(), XRP(1)), escrow::condition(escrow::cb1), escrow::finish_time(env.now() + 1s), escrow::cancel_time(env.now() + 2s), fee(baseFee * 150), ter(tecNO_PERMISSION)); }); // Can't pay into AMM with paychan. testAMM([&](AMM& ammAlice, Env& env) { auto const pk = carol.pk(); auto const settleDelay = 100s; NetClock::time_point const cancelAfter = env.current()->header().parentCloseTime + 200s; env(paychan::create( carol, ammAlice.ammAccount(), XRP(1'000), settleDelay, pk, cancelAfter), ter(tecNO_PERMISSION)); }); // Can't pay into AMM with checks. testAMM([&](AMM& ammAlice, Env& env) { env(check::create(env.master.id(), ammAlice.ammAccount(), XRP(100)), ter(tecNO_PERMISSION)); }); // Pay amounts close to one side of the pool testAMM( [&](AMM& ammAlice, Env& env) { // Can't consume whole pool env(pay(alice, carol, USD(100)), path(~USD), sendmax(XRP(1'000'000'000)), ter(tecPATH_PARTIAL)); env(pay(alice, carol, XRP(100)), path(~XRP), sendmax(USD(1'000'000'000)), ter(tecPATH_PARTIAL)); // Overflow env(pay(alice, carol, STAmount{USD, UINT64_C(99'999999999), -9}), path(~USD), sendmax(XRP(1'000'000'000)), ter(tecPATH_PARTIAL)); env(pay(alice, carol, STAmount{USD, UINT64_C(999'99999999), -8}), path(~USD), sendmax(XRP(1'000'000'000)), ter(tecPATH_PARTIAL)); env(pay(alice, carol, STAmount{xrpIssue(), 99'999'999}), path(~XRP), sendmax(USD(1'000'000'000)), ter(tecPATH_PARTIAL)); // Sender doesn't have enough funds env(pay(alice, carol, USD(99.99)), path(~USD), sendmax(XRP(1'000'000'000)), ter(tecPATH_PARTIAL)); env(pay(alice, carol, STAmount{xrpIssue(), 99'990'000}), path(~XRP), sendmax(USD(1'000'000'000)), ter(tecPATH_PARTIAL)); }, {{XRP(100), USD(100)}}); // Globally frozen testAMM([&](AMM& ammAlice, Env& env) { env(fset(gw, asfGlobalFreeze)); env.close(); env(pay(alice, carol, USD(1)), path(~USD), txflags(tfPartialPayment | tfNoRippleDirect), sendmax(XRP(10)), ter(tecPATH_DRY)); env(pay(alice, carol, XRP(1)), path(~XRP), txflags(tfPartialPayment | tfNoRippleDirect), sendmax(USD(10)), ter(tecPATH_DRY)); }); // Individually frozen AMM testAMM([&](AMM& ammAlice, Env& env) { env(trust( gw, STAmount{Issue{gw["USD"].currency, ammAlice.ammAccount()}, 0}, tfSetFreeze)); env.close(); env(pay(alice, carol, USD(1)), path(~USD), txflags(tfPartialPayment | tfNoRippleDirect), sendmax(XRP(10)), ter(tecPATH_DRY)); env(pay(alice, carol, XRP(1)), path(~XRP), txflags(tfPartialPayment | tfNoRippleDirect), sendmax(USD(10)), ter(tecPATH_DRY)); }); // Individually frozen accounts testAMM([&](AMM& ammAlice, Env& env) { env(trust(gw, carol["USD"](0), tfSetFreeze)); env(trust(gw, alice["USD"](0), tfSetFreeze)); env.close(); env(pay(alice, carol, XRP(1)), path(~XRP), sendmax(USD(10)), txflags(tfNoRippleDirect | tfPartialPayment), ter(tecPATH_DRY)); }); } void testBasicPaymentEngine(FeatureBitset features) { testcase("Basic Payment"); using namespace jtx; // For now, just disable SAV entirely, which locks in the small Number // mantissas features = features - featureSingleAssetVault - featureLendingProtocol - featureLendingProtocol; // Payment 100USD for 100XRP. // Force one path with tfNoRippleDirect. testAMM( [&](AMM& ammAlice, Env& env) { env.fund(jtx::XRP(30'000), bob); env.close(); env(pay(bob, carol, USD(100)), path(~USD), sendmax(XRP(100)), txflags(tfNoRippleDirect)); env.close(); BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), USD(10'000), ammAlice.tokens())); // Initial balance 30,000 + 100 BEAST_EXPECT(expectHolding(env, carol, USD(30'100))); // Initial balance 30,000 - 100(sendmax) - 10(tx fee) BEAST_EXPECT( expectLedgerEntryRoot(env, bob, XRP(30'000) - XRP(100) - txfee(env, 1))); }, {{XRP(10'000), USD(10'100)}}, 0, std::nullopt, {features}); // Payment 100USD for 100XRP, use default path. testAMM( [&](AMM& ammAlice, Env& env) { env.fund(jtx::XRP(30'000), bob); env.close(); env(pay(bob, carol, USD(100)), sendmax(XRP(100))); env.close(); BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), USD(10'000), ammAlice.tokens())); // Initial balance 30,000 + 100 BEAST_EXPECT(expectHolding(env, carol, USD(30'100))); // Initial balance 30,000 - 100(sendmax) - 10(tx fee) BEAST_EXPECT( expectLedgerEntryRoot(env, bob, XRP(30'000) - XRP(100) - txfee(env, 1))); }, {{XRP(10'000), USD(10'100)}}, 0, std::nullopt, {features}); // This payment is identical to above. While it has // both default path and path, activeStrands has one path. testAMM( [&](AMM& ammAlice, Env& env) { env.fund(jtx::XRP(30'000), bob); env.close(); env(pay(bob, carol, USD(100)), path(~USD), sendmax(XRP(100))); env.close(); BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), USD(10'000), ammAlice.tokens())); // Initial balance 30,000 + 100 BEAST_EXPECT(expectHolding(env, carol, USD(30'100))); // Initial balance 30,000 - 100(sendmax) - 10(tx fee) BEAST_EXPECT( expectLedgerEntryRoot(env, bob, XRP(30'000) - XRP(100) - txfee(env, 1))); }, {{XRP(10'000), USD(10'100)}}, 0, std::nullopt, {features}); // Payment with limitQuality set. testAMM( [&](AMM& ammAlice, Env& env) { env.fund(jtx::XRP(30'000), bob); env.close(); // Pays 10USD for 10XRP. A larger payment of ~99.11USD/100XRP // would have been sent has it not been for limitQuality. env(pay(bob, carol, USD(100)), path(~USD), sendmax(XRP(100)), txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality)); env.close(); BEAST_EXPECT(ammAlice.expectBalances(XRP(10'010), USD(10'000), ammAlice.tokens())); // Initial balance 30,000 + 10(limited by limitQuality) BEAST_EXPECT(expectHolding(env, carol, USD(30'010))); // Initial balance 30,000 - 10(limited by limitQuality) - 10(tx // fee) BEAST_EXPECT( expectLedgerEntryRoot(env, bob, XRP(30'000) - XRP(10) - txfee(env, 1))); // Fails because of limitQuality. Would have sent // ~98.91USD/110XRP has it not been for limitQuality. env(pay(bob, carol, USD(100)), path(~USD), sendmax(XRP(100)), txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality), ter(tecPATH_DRY)); env.close(); }, {{XRP(10'000), USD(10'010)}}, 0, std::nullopt, {features}); // Payment with limitQuality and transfer fee set. testAMM( [&](AMM& ammAlice, Env& env) { env(rate(gw, 1.1)); env.close(); env.fund(jtx::XRP(30'000), bob); env.close(); // Pays 10USD for 10XRP. A larger payment of ~99.11USD/100XRP // would have been sent has it not been for limitQuality and // the transfer fee. env(pay(bob, carol, USD(100)), path(~USD), sendmax(XRP(110)), txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality)); env.close(); BEAST_EXPECT(ammAlice.expectBalances(XRP(10'010), USD(10'000), ammAlice.tokens())); // 10USD - 10% transfer fee BEAST_EXPECT( expectHolding(env, carol, STAmount{USD, UINT64_C(30'009'09090909091), -11})); BEAST_EXPECT( expectLedgerEntryRoot(env, bob, XRP(30'000) - XRP(10) - txfee(env, 1))); }, {{XRP(10'000), USD(10'010)}}, 0, std::nullopt, {features}); // Fail when partial payment is not set. testAMM( [&](AMM& ammAlice, Env& env) { env.fund(jtx::XRP(30'000), bob); env.close(); env(pay(bob, carol, USD(100)), path(~USD), sendmax(XRP(100)), txflags(tfNoRippleDirect), ter(tecPATH_PARTIAL)); }, {{XRP(10'000), USD(10'000)}}, 0, std::nullopt, {features}); // Non-default path (with AMM) has a better quality than default path. // The max possible liquidity is taken out of non-default // path ~29.9XRP/29.9EUR, ~29.9EUR/~29.99USD. The rest // is taken from the offer. { Env env(*this, features); fund(env, gw, {alice, carol}, {USD(30'000), EUR(30'000)}, Fund::All); env.close(); env.fund(XRP(1'000), bob); env.close(); auto ammEUR_XRP = AMM(env, alice, XRP(10'000), EUR(10'000)); auto ammUSD_EUR = AMM(env, alice, EUR(10'000), USD(10'000)); env(offer(alice, XRP(101), USD(100)), txflags(tfPassive)); env.close(); env(pay(bob, carol, USD(100)), path(~EUR, ~USD), sendmax(XRP(102)), txflags(tfPartialPayment)); env.close(); BEAST_EXPECT(ammEUR_XRP.expectBalances( XRPAmount(10'030'082'730), STAmount(EUR, UINT64_C(9'970'007498125468), -12), ammEUR_XRP.tokens())); if (!features[fixAMMv1_1]) { BEAST_EXPECT(ammUSD_EUR.expectBalances( STAmount(USD, UINT64_C(9'970'097277662122), -12), STAmount(EUR, UINT64_C(10'029'99250187452), -11), ammUSD_EUR.tokens())); // fixReducedOffersV2 changes the expected results slightly. Amounts const expectedAmounts = env.closed()->rules().enabled(fixReducedOffersV2) ? Amounts{XRPAmount(30'201'749), STAmount(USD, UINT64_C(29'90272233787816), -14)} : Amounts{ XRPAmount(30'201'749), STAmount(USD, UINT64_C(29'90272233787818), -14)}; BEAST_EXPECT(expectOffers(env, alice, 1, {{expectedAmounts}})); } else { BEAST_EXPECT(ammUSD_EUR.expectBalances( STAmount(USD, UINT64_C(9'970'097277662172), -12), STAmount(EUR, UINT64_C(10'029'99250187452), -11), ammUSD_EUR.tokens())); // fixReducedOffersV2 changes the expected results slightly. Amounts const expectedAmounts = env.closed()->rules().enabled(fixReducedOffersV2) ? Amounts{XRPAmount(30'201'749), STAmount(USD, UINT64_C(29'90272233782839), -14)} : Amounts{ XRPAmount(30'201'749), STAmount(USD, UINT64_C(29'90272233782840), -14)}; BEAST_EXPECT(expectOffers(env, alice, 1, {{expectedAmounts}})); } // Initial 30,000 + 100 BEAST_EXPECT(expectHolding(env, carol, STAmount{USD, 30'100})); // Initial 1,000 - 30082730(AMM pool) - 70798251(offer) - 10(tx fee) BEAST_EXPECT(expectLedgerEntryRoot( env, bob, XRP(1'000) - XRPAmount{30'082'730} - XRPAmount{70'798'251} - txfee(env, 1))); } // Default path (with AMM) has a better quality than a non-default path. // The max possible liquidity is taken out of default // path ~49XRP/49USD. The rest is taken from the offer. testAMM( [&](AMM& ammAlice, Env& env) { env.fund(XRP(1'000), bob); env.close(); env.trust(EUR(2'000), alice); env.close(); env(pay(gw, alice, EUR(1'000))); env(offer(alice, XRP(101), EUR(100)), txflags(tfPassive)); env.close(); env(offer(alice, EUR(100), USD(100)), txflags(tfPassive)); env.close(); env(pay(bob, carol, USD(100)), path(~EUR, ~USD), sendmax(XRP(102)), txflags(tfPartialPayment)); env.close(); BEAST_EXPECT(ammAlice.expectBalances( XRPAmount(10'050'238'637), STAmount(USD, UINT64_C(9'950'01249687578), -11), ammAlice.tokens())); BEAST_EXPECT(expectOffers( env, alice, 2, {{Amounts{XRPAmount(50'487'378), STAmount(EUR, UINT64_C(49'98750312422), -11)}, Amounts{ STAmount(EUR, UINT64_C(49'98750312422), -11), STAmount(USD, UINT64_C(49'98750312422), -11)}}})); // Initial 30,000 + 99.99999999999 BEAST_EXPECT( expectHolding(env, carol, STAmount{USD, UINT64_C(30'099'99999999999), -11})); // Initial 1,000 - 50238637(AMM pool) - 50512622(offer) - 10(tx // fee) BEAST_EXPECT(expectLedgerEntryRoot( env, bob, XRP(1'000) - XRPAmount{50'238'637} - XRPAmount{50'512'622} - txfee(env, 1))); }, std::nullopt, 0, std::nullopt, {features}); // Default path with AMM and Order Book offer. AMM is consumed first, // remaining amount is consumed by the offer. testAMM( [&](AMM& ammAlice, Env& env) { fund(env, gw, {bob}, {USD(100)}, Fund::Acct); env.close(); env(offer(bob, XRP(100), USD(100)), txflags(tfPassive)); env.close(); env(pay(alice, carol, USD(200)), sendmax(XRP(200)), txflags(tfPartialPayment)); env.close(); if (!features[fixAMMv1_1]) { BEAST_EXPECT( ammAlice.expectBalances(XRP(10'100), USD(10'000), ammAlice.tokens())); // Initial 30,000 + 200 BEAST_EXPECT(expectHolding(env, carol, USD(30'200))); } else { BEAST_EXPECT(ammAlice.expectBalances( XRP(10'100), STAmount(USD, UINT64_C(10'000'00000000001), -11), ammAlice.tokens())); BEAST_EXPECT(expectHolding( env, carol, STAmount(USD, UINT64_C(30'199'99999999999), -11))); } // Initial 30,000 - 10000(AMM pool LP) - 100(AMM offer) - // - 100(offer) - 10(tx fee) - one reserve BEAST_EXPECT(expectLedgerEntryRoot( env, alice, XRP(30'000) - XRP(10'000) - XRP(100) - XRP(100) - ammCrtFee(env) - txfee(env, 1))); BEAST_EXPECT(expectOffers(env, bob, 0)); }, {{XRP(10'000), USD(10'100)}}, 0, std::nullopt, {features}); // Default path with AMM and Order Book offer. // Order Book offer is consumed first. // Remaining amount is consumed by AMM. { Env env(*this, features); fund(env, gw, {alice, bob, carol}, XRP(20'000), {USD(2'000)}); env.close(); env(offer(bob, XRP(50), USD(150)), txflags(tfPassive)); env.close(); AMM const ammAlice(env, alice, XRP(1'000), USD(1'050)); env(pay(alice, carol, USD(200)), sendmax(XRP(200)), txflags(tfPartialPayment)); env.close(); BEAST_EXPECT(ammAlice.expectBalances(XRP(1'050), USD(1'000), ammAlice.tokens())); BEAST_EXPECT(expectHolding(env, carol, USD(2'200))); BEAST_EXPECT(expectOffers(env, bob, 0)); } // Offer crossing XRP/IOU testAMM( [&](AMM& ammAlice, Env& env) { fund(env, gw, {bob}, {USD(1'000)}, Fund::Acct); env.close(); env(offer(bob, USD(100), XRP(100))); env.close(); BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), USD(10'000), ammAlice.tokens())); // Initial 1,000 + 100 BEAST_EXPECT(expectHolding(env, bob, USD(1'100))); // Initial 30,000 - 100(offer) - 10(tx fee) BEAST_EXPECT( expectLedgerEntryRoot(env, bob, XRP(30'000) - XRP(100) - txfee(env, 1))); BEAST_EXPECT(expectOffers(env, bob, 0)); }, {{XRP(10'000), USD(10'100)}}, 0, std::nullopt, {features}); // Offer crossing IOU/IOU and transfer rate // Single path AMM offer testAMM( [&](AMM& ammAlice, Env& env) { env(rate(gw, 1.25)); env.close(); // This offer succeeds to cross pre- and post-amendment // because the strand's out amount is small enough to match // limitQuality value and limitOut() function in StrandFlow // doesn't require an adjustment to out value. env(offer(carol, EUR(100), GBP(100))); env.close(); // No transfer fee BEAST_EXPECT(ammAlice.expectBalances(GBP(1'100), EUR(1'000), ammAlice.tokens())); // Initial 30,000 - 100(offer) - 25% transfer fee BEAST_EXPECT(expectHolding(env, carol, GBP(29'875))); // Initial 30,000 + 100(offer) BEAST_EXPECT(expectHolding(env, carol, EUR(30'100))); BEAST_EXPECT(expectOffers(env, bob, 0)); }, {{GBP(1'000), EUR(1'100)}}, 0, std::nullopt, {features}); // Single-path AMM offer testAMM( [&](AMM& amm, Env& env) { env(rate(gw, 1.001)); env.close(); env(offer(carol, XRP(100), USD(55))); env.close(); if (!features[fixAMMv1_1]) { // Pre-amendment the transfer fee is not taken into // account when calculating the limit out based on // limitQuality. Carol pays 0.1% on the takerGets, which // lowers the overall quality. AMM offer is generated based // on higher limit out, which generates a larger offer // with lower quality. Consequently, the offer fails // to cross. BEAST_EXPECT(amm.expectBalances(XRP(1'000), USD(500), amm.tokens())); BEAST_EXPECT(expectOffers(env, carol, 1, {{Amounts{XRP(100), USD(55)}}})); } else { // Post-amendment the transfer fee is taken into account // when calculating the limit out based on limitQuality. // This increases the limitQuality and decreases // the limit out. Consequently, AMM offer size is decreased, // and the quality is increased, matching the overall // quality. // AMM offer ~50USD/91XRP BEAST_EXPECT(amm.expectBalances( XRPAmount(909'090'909), STAmount{USD, UINT64_C(550'000000055), -9}, amm.tokens())); // Offer ~91XRP/49.99USD BEAST_EXPECT(expectOffers( env, carol, 1, {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}})); // Carol pays 0.1% fee on ~50USD =~ 0.05USD BEAST_EXPECT( env.balance(carol, USD) == STAmount(USD, UINT64_C(29'949'94999999494), -11)); } }, {{XRP(1'000), USD(500)}}, 0, std::nullopt, {features}); testAMM( [&](AMM& amm, Env& env) { env(rate(gw, 1.001)); env.close(); env(offer(carol, XRP(10), USD(5.5))); env.close(); if (!features[fixAMMv1_1]) { BEAST_EXPECT(amm.expectBalances( XRP(990), STAmount{USD, UINT64_C(505'050505050505), -12}, amm.tokens())); BEAST_EXPECT(expectOffers(env, carol, 0)); } else { BEAST_EXPECT(amm.expectBalances( XRP(990), STAmount{USD, UINT64_C(505'0505050505051), -13}, amm.tokens())); BEAST_EXPECT(expectOffers(env, carol, 0)); } }, {{XRP(1'000), USD(500)}}, 0, std::nullopt, {features}); // Multi-path AMM offer testAMM( [&](AMM& ammAlice, Env& env) { Account const ed("ed"); fund(env, gw, {bob, ed}, XRP(30'000), {GBP(2'000), EUR(2'000)}, Fund::Acct); env(rate(gw, 1.25)); env.close(); // The auto-bridge is worse quality than AMM, is not consumed // first and initially forces multi-path AMM offer generation. // Multi-path AMM offers are consumed until their quality // is less than the auto-bridge offers quality. Auto-bridge // offers are consumed afterward. Then the behavior is // different pre-amendment and post-amendment. env(offer(bob, GBP(10), XRP(10)), txflags(tfPassive)); env(offer(ed, XRP(10), EUR(10)), txflags(tfPassive)); env.close(); env(offer(carol, EUR(100), GBP(100))); env.close(); if (!features[fixAMMv1_1]) { // After the auto-bridge offers are consumed, single path // AMM offer is generated with the limit out not taking // into consideration the transfer fee. This results // in an overall lower quality offer than the limit quality // and the single path AMM offer fails to consume. // Total consumed ~37.06GBP/39.32EUR BEAST_EXPECT(ammAlice.expectBalances( STAmount{GBP, UINT64_C(1'037'06583722133), -11}, STAmount{EUR, UINT64_C(1'060'684828792831), -12}, ammAlice.tokens())); // Consumed offer ~49.32EUR/49.32GBP BEAST_EXPECT(expectOffers( env, carol, 1, {Amounts{ STAmount{EUR, UINT64_C(50'684828792831), -12}, STAmount{GBP, UINT64_C(50'684828792831), -12}}})); BEAST_EXPECT(expectOffers(env, bob, 0)); BEAST_EXPECT(expectOffers(env, ed, 0)); // Initial 30,000 - ~47.06(offers = 37.06(AMM) + 10(LOB)) // * 1.25 // = 58.825 = ~29941.17 // carol bought ~72.93EUR at the cost of ~70.68GBP // the offer is partially consumed BEAST_EXPECT(expectHolding( env, carol, STAmount{GBP, UINT64_C(29'941'16770347333), -11})); // Initial 30,000 + ~49.3(offers = 39.3(AMM) + 10(LOB)) BEAST_EXPECT(expectHolding( env, carol, STAmount{EUR, UINT64_C(30'049'31517120716), -11})); } else { // After the auto-bridge offers are consumed, single path // AMM offer is generated with the limit out taking // into consideration the transfer fee. This results // in an overall quality offer matching the limit quality // and the single path AMM offer is consumed. More // liquidity is consumed overall in post-amendment. // Total consumed ~60.68GBP/62.93EUR BEAST_EXPECT(ammAlice.expectBalances( STAmount{GBP, UINT64_C(1'060'684828792832), -12}, STAmount{EUR, UINT64_C(1'037'06583722134), -11}, ammAlice.tokens())); // Consumed offer ~72.93EUR/72.93GBP BEAST_EXPECT(expectOffers( env, carol, 1, {Amounts{ STAmount{EUR, UINT64_C(27'06583722134028), -14}, STAmount{GBP, UINT64_C(27'06583722134028), -14}}})); BEAST_EXPECT(expectOffers(env, bob, 0)); BEAST_EXPECT(expectOffers(env, ed, 0)); // Initial 30,000 - ~70.68(offers = 60.68(AMM) + 10(LOB)) // * 1.25 // = 88.35 = ~29911.64 // carol bought ~72.93EUR at the cost of ~70.68GBP // the offer is partially consumed BEAST_EXPECT(expectHolding( env, carol, STAmount{GBP, UINT64_C(29'911'64396400896), -11})); // Initial 30,000 + ~72.93(offers = 62.93(AMM) + 10(LOB)) BEAST_EXPECT(expectHolding( env, carol, STAmount{EUR, UINT64_C(30'072'93416277865), -11})); } // Initial 2000 + 10 = 2010 BEAST_EXPECT(expectHolding(env, bob, GBP(2'010))); // Initial 2000 - 10 * 1.25 = 1987.5 BEAST_EXPECT(expectHolding(env, ed, EUR(1'987.5))); }, {{GBP(1'000), EUR(1'100)}}, 0, std::nullopt, {features}); // Payment and transfer fee // Scenario: // Bob sends 125GBP to pay 80EUR to Carol // Payment execution: // bob's 125GBP/1.25 = 100GBP // 100GBP/100EUR AMM offer // 100EUR/1.25 = 80EUR paid to carol testAMM( [&](AMM& ammAlice, Env& env) { fund(env, gw, {bob}, {GBP(200), EUR(200)}, Fund::Acct); env(rate(gw, 1.25)); env.close(); env(pay(bob, carol, EUR(100)), path(~EUR), sendmax(GBP(125)), txflags(tfPartialPayment)); env.close(); BEAST_EXPECT(ammAlice.expectBalances(GBP(1'100), EUR(1'000), ammAlice.tokens())); BEAST_EXPECT(expectHolding(env, bob, GBP(75))); BEAST_EXPECT(expectHolding(env, carol, EUR(30'080))); }, {{GBP(1'000), EUR(1'100)}}, 0, std::nullopt, {features}); // Payment and transfer fee, multiple steps // Scenario: // Dan's offer 200CAN/200GBP // AMM 1000GBP/10125EUR // Ed's offer 200EUR/200USD // Bob sends 195.3125CAN to pay 100USD to Carol // Payment execution: // bob's 195.3125CAN/1.25 = 156.25CAN -> dan's offer // 156.25CAN/156.25GBP 156.25GBP/1.25 = 125GBP -> AMM's offer // 125GBP/125EUR 125EUR/1.25 = 100EUR -> ed's offer // 100EUR/100USD 100USD/1.25 = 80USD paid to carol testAMM( [&](AMM& ammAlice, Env& env) { Account const dan("dan"); Account const ed("ed"); auto const CAN = gw["CAN"]; fund(env, gw, {dan}, {CAN(200), GBP(200)}, Fund::Acct); fund(env, gw, {ed}, {EUR(200), USD(200)}, Fund::Acct); fund(env, gw, {bob}, {CAN(195.3125)}, Fund::Acct); env(trust(carol, USD(100))); env(rate(gw, 1.25)); env.close(); env(offer(dan, CAN(200), GBP(200))); env(offer(ed, EUR(200), USD(200))); env.close(); env(pay(bob, carol, USD(100)), path(~GBP, ~EUR, ~USD), sendmax(CAN(195.3125)), txflags(tfPartialPayment)); env.close(); BEAST_EXPECT(expectHolding(env, bob, CAN(0))); BEAST_EXPECT(expectHolding(env, dan, CAN(356.25), GBP(43.75))); BEAST_EXPECT(ammAlice.expectBalances(GBP(10'125), EUR(10'000), ammAlice.tokens())); BEAST_EXPECT(expectHolding(env, ed, EUR(300), USD(100))); BEAST_EXPECT(expectHolding(env, carol, USD(80))); }, {{GBP(10'000), EUR(10'125)}}, 0, std::nullopt, {features}); // Pay amounts close to one side of the pool testAMM( [&](AMM& ammAlice, Env& env) { env(pay(alice, carol, USD(99.99)), path(~USD), sendmax(XRP(1)), txflags(tfPartialPayment), ter(tesSUCCESS)); env(pay(alice, carol, USD(100)), path(~USD), sendmax(XRP(1)), txflags(tfPartialPayment), ter(tesSUCCESS)); env(pay(alice, carol, XRP(100)), path(~XRP), sendmax(USD(1)), txflags(tfPartialPayment), ter(tesSUCCESS)); env(pay(alice, carol, STAmount{xrpIssue(), 99'999'900}), path(~XRP), sendmax(USD(1)), txflags(tfPartialPayment), ter(tesSUCCESS)); }, {{XRP(100), USD(100)}}, 0, std::nullopt, {features}); // Multiple paths/steps { Env env(*this, features); auto const ETH = gw["ETH"]; fund( env, gw, {alice}, XRP(100'000), {EUR(50'000), BTC(50'000), ETH(50'000), USD(50'000)}); fund(env, gw, {carol, bob}, XRP(1'000), {USD(200)}, Fund::Acct); AMM const xrp_eur(env, alice, XRP(10'100), EUR(10'000)); AMM const eur_btc(env, alice, EUR(10'000), BTC(10'200)); AMM const btc_usd(env, alice, BTC(10'100), USD(10'000)); AMM const xrp_usd(env, alice, XRP(10'150), USD(10'200)); AMM const xrp_eth(env, alice, XRP(10'000), ETH(10'100)); AMM const eth_eur(env, alice, ETH(10'900), EUR(11'000)); AMM const eur_usd(env, alice, EUR(10'100), USD(10'000)); env(pay(bob, carol, USD(100)), path(~EUR, ~BTC, ~USD), path(~USD), path(~ETH, ~EUR, ~USD), sendmax(XRP(200))); if (!features[fixAMMv1_1]) { // XRP-ETH-EUR-USD // This path provides ~26.06USD/26.2XRP BEAST_EXPECT(xrp_eth.expectBalances( XRPAmount(10'026'208'900), STAmount{ETH, UINT64_C(10'073'65779244494), -11}, xrp_eth.tokens())); BEAST_EXPECT(eth_eur.expectBalances( STAmount{ETH, UINT64_C(10'926'34220755506), -11}, STAmount{EUR, UINT64_C(10'973'54232078752), -11}, eth_eur.tokens())); BEAST_EXPECT(eur_usd.expectBalances( STAmount{EUR, UINT64_C(10'126'45767921248), -11}, STAmount{USD, UINT64_C(9'973'93151712086), -11}, eur_usd.tokens())); // XRP-USD path // This path provides ~73.9USD/74.1XRP BEAST_EXPECT(xrp_usd.expectBalances( XRPAmount(10'224'106'246), STAmount{USD, UINT64_C(10'126'06848287914), -11}, xrp_usd.tokens())); } else { BEAST_EXPECT(xrp_eth.expectBalances( XRPAmount(10'026'208'900), STAmount{ETH, UINT64_C(10'073'65779244461), -11}, xrp_eth.tokens())); BEAST_EXPECT(eth_eur.expectBalances( STAmount{ETH, UINT64_C(10'926'34220755539), -11}, STAmount{EUR, UINT64_C(10'973'5423207872), -10}, eth_eur.tokens())); BEAST_EXPECT(eur_usd.expectBalances( STAmount{EUR, UINT64_C(10'126'4576792128), -10}, STAmount{USD, UINT64_C(9'973'93151712057), -11}, eur_usd.tokens())); // XRP-USD path // This path provides ~73.9USD/74.1XRP BEAST_EXPECT(xrp_usd.expectBalances( XRPAmount(10'224'106'246), STAmount{USD, UINT64_C(10'126'06848287943), -11}, xrp_usd.tokens())); } // XRP-EUR-BTC-USD // This path doesn't provide any liquidity due to how // offers are generated in multi-path. Analytical solution // shows a different distribution: // XRP-EUR-BTC-USD 11.6USD/11.64XRP, XRP-USD 60.7USD/60.8XRP, // XRP-ETH-EUR-USD 27.6USD/27.6XRP BEAST_EXPECT(xrp_eur.expectBalances(XRP(10'100), EUR(10'000), xrp_eur.tokens())); BEAST_EXPECT(eur_btc.expectBalances(EUR(10'000), BTC(10'200), eur_btc.tokens())); BEAST_EXPECT(btc_usd.expectBalances(BTC(10'100), USD(10'000), btc_usd.tokens())); BEAST_EXPECT(expectHolding(env, carol, USD(300))); } // Dependent AMM { Env env(*this, features); auto const ETH = gw["ETH"]; fund( env, gw, {alice}, XRP(40'000), {EUR(50'000), BTC(50'000), ETH(50'000), USD(50'000)}); fund(env, gw, {carol, bob}, XRP(1000), {USD(200)}, Fund::Acct); AMM const xrp_eur(env, alice, XRP(10'100), EUR(10'000)); AMM const eur_btc(env, alice, EUR(10'000), BTC(10'200)); AMM const btc_usd(env, alice, BTC(10'100), USD(10'000)); AMM const xrp_eth(env, alice, XRP(10'000), ETH(10'100)); AMM const eth_eur(env, alice, ETH(10'900), EUR(11'000)); env(pay(bob, carol, USD(100)), path(~EUR, ~BTC, ~USD), path(~ETH, ~EUR, ~BTC, ~USD), sendmax(XRP(200))); if (!features[fixAMMv1_1]) { // XRP-EUR-BTC-USD path provides ~17.8USD/~18.7XRP // XRP-ETH-EUR-BTC-USD path provides ~82.2USD/82.4XRP BEAST_EXPECT(xrp_eur.expectBalances( XRPAmount(10'118'738'472), STAmount{EUR, UINT64_C(9'981'544436337968), -12}, xrp_eur.tokens())); BEAST_EXPECT(eur_btc.expectBalances( STAmount{EUR, UINT64_C(10'101'16096785173), -11}, STAmount{BTC, UINT64_C(10'097'91426968066), -11}, eur_btc.tokens())); BEAST_EXPECT(btc_usd.expectBalances( STAmount{BTC, UINT64_C(10'202'08573031934), -11}, USD(9'900), btc_usd.tokens())); BEAST_EXPECT(xrp_eth.expectBalances( XRPAmount(10'082'446'397), STAmount{ETH, UINT64_C(10'017'41072778012), -11}, xrp_eth.tokens())); BEAST_EXPECT(eth_eur.expectBalances( STAmount{ETH, UINT64_C(10'982'58927221988), -11}, STAmount{EUR, UINT64_C(10'917'2945958103), -10}, eth_eur.tokens())); } else { BEAST_EXPECT(xrp_eur.expectBalances( XRPAmount(10'118'738'472), STAmount{EUR, UINT64_C(9'981'544436337923), -12}, xrp_eur.tokens())); BEAST_EXPECT(eur_btc.expectBalances( STAmount{EUR, UINT64_C(10'101'16096785188), -11}, STAmount{BTC, UINT64_C(10'097'91426968059), -11}, eur_btc.tokens())); BEAST_EXPECT(btc_usd.expectBalances( STAmount{BTC, UINT64_C(10'202'08573031941), -11}, USD(9'900), btc_usd.tokens())); BEAST_EXPECT(xrp_eth.expectBalances( XRPAmount(10'082'446'397), STAmount{ETH, UINT64_C(10'017'41072777996), -11}, xrp_eth.tokens())); BEAST_EXPECT(eth_eur.expectBalances( STAmount{ETH, UINT64_C(10'982'58927222004), -11}, STAmount{EUR, UINT64_C(10'917'2945958102), -10}, eth_eur.tokens())); } BEAST_EXPECT(expectHolding(env, carol, USD(300))); } // AMM offers limit // Consuming 30 CLOB offers, results in hitting 30 AMM offers limit. testAMM( [&](AMM& ammAlice, Env& env) { env.fund(XRP(1'000), bob); fund(env, gw, {bob}, {EUR(400)}, Fund::TokenOnly); env(trust(alice, EUR(200))); for (int i = 0; i < 30; ++i) env(offer(alice, EUR(1.0 + (0.01 * i)), XRP(1))); // This is worse quality offer than 30 offers above. // It will not be consumed because of AMM offers limit. env(offer(alice, EUR(140), XRP(100))); env(pay(bob, carol, USD(100)), path(~XRP, ~USD), sendmax(EUR(400)), txflags(tfPartialPayment | tfNoRippleDirect)); if (!features[fixAMMv1_1]) { // Carol gets ~29.91USD because of the AMM offers limit BEAST_EXPECT(ammAlice.expectBalances( XRP(10'030), STAmount{USD, UINT64_C(9'970'089730807577), -12}, ammAlice.tokens())); BEAST_EXPECT(expectHolding( env, carol, STAmount{USD, UINT64_C(30'029'91026919241), -11})); } else { BEAST_EXPECT(ammAlice.expectBalances( XRP(10'030), STAmount{USD, UINT64_C(9'970'089730807827), -12}, ammAlice.tokens())); BEAST_EXPECT(expectHolding( env, carol, STAmount{USD, UINT64_C(30'029'91026919217), -11})); } BEAST_EXPECT(expectOffers(env, alice, 1, {{{EUR(140), XRP(100)}}})); }, std::nullopt, 0, std::nullopt, {features}); // This payment is fulfilled testAMM( [&](AMM& ammAlice, Env& env) { env.fund(XRP(1'000), bob); fund(env, gw, {bob}, {EUR(400)}, Fund::TokenOnly); env(trust(alice, EUR(200))); for (int i = 0; i < 29; ++i) env(offer(alice, EUR(1.0 + (0.01 * i)), XRP(1))); // This is worse quality offer than 30 offers above. // It will not be consumed because of AMM offers limit. env(offer(alice, EUR(140), XRP(100))); env(pay(bob, carol, USD(100)), path(~XRP, ~USD), sendmax(EUR(400)), txflags(tfPartialPayment | tfNoRippleDirect)); BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{10'101'010'102}, USD(9'900), ammAlice.tokens())); if (!features[fixAMMv1_1]) { // Carol gets ~100USD BEAST_EXPECT(expectHolding( env, carol, STAmount{USD, UINT64_C(30'099'99999999999), -11})); } else { BEAST_EXPECT(expectHolding(env, carol, USD(30'100))); } BEAST_EXPECT(expectOffers( env, alice, 1, {{{STAmount{EUR, UINT64_C(39'1858572), -7}, XRPAmount{27'989'898}}}})); }, std::nullopt, 0, std::nullopt, {features}); // Offer crossing with AMM and another offer. AMM has a better // quality and is consumed first. { Env env(*this, features); fund(env, gw, {alice, carol, bob}, XRP(30'000), {USD(30'000)}); env(offer(bob, XRP(100), USD(100.001))); AMM const ammAlice(env, alice, XRP(10'000), USD(10'100)); env(offer(carol, USD(100), XRP(100))); if (!features[fixAMMv1_1]) { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{10'049'825'373}, STAmount{USD, UINT64_C(10'049'92586949302), -11}, ammAlice.tokens())); BEAST_EXPECT(expectOffers( env, bob, 1, {{{XRPAmount{50'074'629}, STAmount{USD, UINT64_C(50'07513050698), -11}}}})); } else { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{10'049'825'372}, STAmount{USD, UINT64_C(10'049'92587049303), -11}, ammAlice.tokens())); BEAST_EXPECT(expectOffers( env, bob, 1, {{{XRPAmount{50'074'628}, STAmount{USD, UINT64_C(50'07512950697), -11}}}})); BEAST_EXPECT(expectHolding(env, carol, USD(30'100))); } } // Individually frozen account testAMM( [&](AMM& ammAlice, Env& env) { env(trust(gw, carol["USD"](0), tfSetFreeze)); env(trust(gw, alice["USD"](0), tfSetFreeze)); env.close(); env(pay(alice, carol, USD(1)), path(~USD), sendmax(XRP(10)), txflags(tfNoRippleDirect | tfPartialPayment), ter(tesSUCCESS)); }, std::nullopt, 0, std::nullopt, {features}); } void testAMMTokens() { testcase("AMM Tokens"); using namespace jtx; // Offer crossing with AMM LPTokens and XRP. testAMM([&](AMM& ammAlice, Env& env) { auto const baseFee = env.current()->fees().base.drops(); auto const token1 = ammAlice.lptIssue(); auto priceXRP = ammAssetOut( STAmount{XRPAmount{10'000'000'000}}, STAmount{token1, 10'000'000}, STAmount{token1, 5'000'000}, 0); // Carol places an order to buy LPTokens env(offer(carol, STAmount{token1, 5'000'000}, priceXRP)); // Alice places an order to sell LPTokens env(offer(alice, priceXRP, STAmount{token1, 5'000'000})); // Pool's LPTokens balance doesn't change BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000})); // Carol is Liquidity Provider BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{5'000'000})); BEAST_EXPECT(ammAlice.expectLPTokens(alice, IOUAmount{5'000'000})); // Carol votes ammAlice.vote(carol, 1'000); BEAST_EXPECT(ammAlice.expectTradingFee(500)); ammAlice.vote(carol, 0); BEAST_EXPECT(ammAlice.expectTradingFee(0)); // Carol bids env(ammAlice.bid({.account = carol, .bidMin = 100})); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{4'999'900})); BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{100})); BEAST_EXPECT(accountBalance(env, carol) == std::to_string(22500000000 - (4 * baseFee))); priceXRP = ammAssetOut( STAmount{XRPAmount{10'000'000'000}}, STAmount{token1, 9'999'900}, STAmount{token1, 4'999'900}, 0); // Carol withdraws ammAlice.withdrawAll(carol, XRP(0)); BEAST_EXPECT(accountBalance(env, carol) == std::to_string(29999949999 - (5 * baseFee))); BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{10'000'000'000} - priceXRP, USD(10'000), IOUAmount{5'000'000})); BEAST_EXPECT(ammAlice.expectLPTokens(alice, IOUAmount{5'000'000})); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0})); }); // Offer crossing with two AMM LPTokens. testAMM([&](AMM& ammAlice, Env& env) { ammAlice.deposit(carol, 1'000'000); fund(env, gw, {alice, carol}, {EUR(10'000)}, Fund::TokenOnly); AMM ammAlice1(env, alice, XRP(10'000), EUR(10'000)); ammAlice1.deposit(carol, 1'000'000); auto const token1 = ammAlice.lptIssue(); auto const token2 = ammAlice1.lptIssue(); env(offer(alice, STAmount{token1, 100}, STAmount{token2, 100}), txflags(tfPassive)); env.close(); BEAST_EXPECT(expectOffers(env, alice, 1)); env(offer(carol, STAmount{token2, 100}, STAmount{token1, 100})); env.close(); BEAST_EXPECT( expectHolding(env, alice, STAmount{token1, 10'000'100}) && expectHolding(env, alice, STAmount{token2, 9'999'900})); BEAST_EXPECT( expectHolding(env, carol, STAmount{token2, 1'000'100}) && expectHolding(env, carol, STAmount{token1, 999'900})); BEAST_EXPECT(expectOffers(env, alice, 0) && expectOffers(env, carol, 0)); }); // LPs pay LPTokens directly. Must trust set because the trust line // is checked for the limit, which is 0 in the AMM auto-created // trust line. testAMM([&](AMM& ammAlice, Env& env) { auto const token1 = ammAlice.lptIssue(); env.trust(STAmount{token1, 2'000'000}, carol); env.close(); ammAlice.deposit(carol, 1'000'000); BEAST_EXPECT( ammAlice.expectLPTokens(alice, IOUAmount{10'000'000, 0}) && ammAlice.expectLPTokens(carol, IOUAmount{1'000'000, 0})); // Pool balance doesn't change, only tokens moved from // one line to another. env(pay(alice, carol, STAmount{token1, 100})); env.close(); BEAST_EXPECT( // Alice initial token1 10,000,000 - 100 ammAlice.expectLPTokens(alice, IOUAmount{9'999'900, 0}) && // Carol initial token1 1,000,000 + 100 ammAlice.expectLPTokens(carol, IOUAmount{1'000'100, 0})); env.trust(STAmount{token1, 20'000'000}, alice); env.close(); env(pay(carol, alice, STAmount{token1, 100})); env.close(); // Back to the original balance BEAST_EXPECT( ammAlice.expectLPTokens(alice, IOUAmount{10'000'000, 0}) && ammAlice.expectLPTokens(carol, IOUAmount{1'000'000, 0})); }); } void testAmendment() { testcase("Amendment"); FeatureBitset const all{testable_amendments()}; FeatureBitset const noAMM{all - featureAMM}; FeatureBitset const noNumber{all - fixUniversalNumber}; FeatureBitset const noAMMAndNumber{all - featureAMM - fixUniversalNumber}; using namespace jtx; for (auto const& feature : {noAMM, noNumber, noAMMAndNumber}) { Env env{*this, feature}; fund(env, gw, {alice}, {USD(1'000)}, Fund::All); AMM amm(env, alice, XRP(1'000), USD(1'000), ter(temDISABLED)); env(amm.bid({.bidMax = 1000}), ter(temMALFORMED)); env(amm.bid({}), ter(temDISABLED)); amm.vote(VoteArg{.tfee = 100, .err = ter(temDISABLED)}); amm.withdraw(WithdrawArg{.tokens = 100, .err = ter(temMALFORMED)}); amm.withdraw(WithdrawArg{.err = ter(temDISABLED)}); amm.deposit(DepositArg{.asset1In = USD(100), .err = ter(temDISABLED)}); amm.ammDelete(alice, ter(temDISABLED)); } } void testFlags() { testcase("Flags"); using namespace jtx; testAMM([&](AMM& ammAlice, Env& env) { auto const info = env.rpc( "json", "account_info", std::string("{\"account\": \"" + to_string(ammAlice.ammAccount()) + "\"}")); auto const flags = info[jss::result][jss::account_data][jss::Flags].asUInt(); BEAST_EXPECT(flags == (lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth)); }); } void testRippling() { testcase("Rippling"); using namespace jtx; // Rippling via AMM fails because AMM trust line has 0 limit. // Set up two issuers, A and B. Have each issue a token called TST. // Have another account C hold TST from both issuers, // and create an AMM for this pair. // Have a fourth account, D, create a trust line to the AMM for TST. // Send a payment delivering TST.AMM from C to D, using SendMax in // TST.A (or B) and a path through the AMM account. By normal // rippling rules, this would have caused the AMM's balances // to shift at a 1:1 rate with no fee applied has it not been // for 0 limit. { Env env(*this); auto const A = Account("A"); auto const B = Account("B"); auto const TSTA = A["TST"]; auto const TSTB = B["TST"]; auto const C = Account("C"); auto const D = Account("D"); env.fund(XRP(10'000), A); env.fund(XRP(10'000), B); env.fund(XRP(10'000), C); env.fund(XRP(10'000), D); env.trust(TSTA(10'000), C); env.trust(TSTB(10'000), C); env(pay(A, C, TSTA(10'000))); env(pay(B, C, TSTB(10'000))); AMM const amm(env, C, TSTA(5'000), TSTB(5'000)); auto const ammIss = Issue(TSTA.currency, amm.ammAccount()); // Can TrustSet only for AMM LP tokens env(trust(D, STAmount{ammIss, 10'000}), ter(tecNO_PERMISSION)); env.close(); // The payment would fail because of above, but check just in case env(pay(C, D, STAmount{ammIss, 10}), sendmax(TSTA(100)), path(amm.ammAccount()), txflags(tfPartialPayment | tfNoRippleDirect), ter(tecPATH_DRY)); } } void testAMMAndCLOB(FeatureBitset features) { testcase("AMMAndCLOB, offer quality change"); using namespace jtx; auto const gw = Account("gw"); auto const TST = gw["TST"]; auto const LP1 = Account("LP1"); auto const LP2 = Account("LP2"); auto prep = [&](auto const& offerCb, auto const& expectCb) { Env env(*this, features); env.fund(XRP(30'000'000'000), gw); env(offer(gw, XRP(11'500'000'000), TST(1'000'000'000))); env.fund(XRP(10'000), LP1); env.fund(XRP(10'000), LP2); env(offer(LP1, TST(25), XRPAmount(287'500'000))); // Either AMM or CLOB offer offerCb(env); env(offer(LP2, TST(25), XRPAmount(287'500'000))); expectCb(env); }; // If we replace AMM with an equivalent CLOB offer, which AMM generates // when it is consumed, then the result must be equivalent, too. std::string lp2TSTBalance; std::string lp2TakerGets; std::string lp2TakerPays; // Execute with AMM first prep( [&](Env& env) { AMM const amm(env, LP1, TST(25), XRP(250)); }, [&](Env& env) { lp2TSTBalance = getAccountLines(env, LP2, TST)["lines"][0u]["balance"].asString(); auto const offer = getAccountOffers(env, LP2)["offers"][0u]; lp2TakerGets = offer["taker_gets"].asString(); lp2TakerPays = offer["taker_pays"]["value"].asString(); }); // Execute with CLOB offer prep( [&](Env& env) { if (!features[fixAMMv1_1]) { env(offer( LP1, XRPAmount{18'095'133}, STAmount{TST, UINT64_C(1'68737984885388), -14}), txflags(tfPassive)); } else { env(offer( LP1, XRPAmount{18'095'132}, STAmount{TST, UINT64_C(1'68737976189735), -14}), txflags(tfPassive)); } }, [&](Env& env) { BEAST_EXPECT( lp2TSTBalance == getAccountLines(env, LP2, TST)["lines"][0u]["balance"].asString()); auto const offer = getAccountOffers(env, LP2)["offers"][0u]; BEAST_EXPECT(lp2TakerGets == offer["taker_gets"].asString()); BEAST_EXPECT(lp2TakerPays == offer["taker_pays"]["value"].asString()); }); } void testTradingFee(FeatureBitset features) { testcase("Trading Fee"); using namespace jtx; // Single Deposit, 1% fee testAMM( [&](AMM& ammAlice, Env& env) { // No fee ammAlice.deposit(carol, USD(3'000)); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{1'000})); ammAlice.withdrawAll(carol, USD(3'000)); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0})); BEAST_EXPECT(expectHolding(env, carol, USD(30'000))); // Set fee to 1% ammAlice.vote(alice, 1'000); BEAST_EXPECT(ammAlice.expectTradingFee(1'000)); // Carol gets fewer LPToken ~994, because of the single deposit // fee ammAlice.deposit(carol, USD(3'000)); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{994'981155689671, -12})); BEAST_EXPECT(expectHolding(env, carol, USD(27'000))); // Set fee to 0 ammAlice.vote(alice, 0); ammAlice.withdrawAll(carol, USD(0)); // Carol gets back less than the original deposit BEAST_EXPECT( expectHolding(env, carol, STAmount{USD, UINT64_C(29'994'96220068281), -11})); }, {{USD(1'000), EUR(1'000)}}, 0, std::nullopt, {features}); // Single deposit with EP not exceeding specified: // 100USD with EP not to exceed 0.1 (AssetIn/TokensOut). 1% fee. testAMM( [&](AMM& ammAlice, Env& env) { auto const balance = env.balance(carol, USD); auto tokensFee = ammAlice.deposit(carol, USD(1'000), std::nullopt, STAmount{USD, 1, -1}); auto const deposit = balance - env.balance(carol, USD); ammAlice.withdrawAll(carol, USD(0)); ammAlice.vote(alice, 0); BEAST_EXPECT(ammAlice.expectTradingFee(0)); auto const tokensNoFee = ammAlice.deposit(carol, deposit); // carol pays ~2008 LPTokens in fees or ~0.5% of the no-fee // LPTokens BEAST_EXPECT(tokensFee == IOUAmount(485'636'0611129, -7)); BEAST_EXPECT(tokensNoFee == IOUAmount(487'644'85901109, -8)); }, std::nullopt, 1'000, std::nullopt, {features}); // Single deposit with EP not exceeding specified: // 200USD with EP not to exceed 0.002020 (AssetIn/TokensOut). 1% fee testAMM( [&](AMM& ammAlice, Env& env) { auto const balance = env.balance(carol, USD); auto const tokensFee = ammAlice.deposit(carol, USD(200), std::nullopt, STAmount{USD, 2020, -6}); auto const deposit = balance - env.balance(carol, USD); ammAlice.withdrawAll(carol, USD(0)); ammAlice.vote(alice, 0); BEAST_EXPECT(ammAlice.expectTradingFee(0)); auto const tokensNoFee = ammAlice.deposit(carol, deposit); // carol pays ~475 LPTokens in fees or ~0.5% of the no-fee // LPTokens BEAST_EXPECT(tokensFee == IOUAmount(98'000'00000002, -8)); BEAST_EXPECT(tokensNoFee == IOUAmount(98'475'81871545, -8)); }, std::nullopt, 1'000, std::nullopt, {features}); // Single Withdrawal, 1% fee testAMM( [&](AMM& ammAlice, Env& env) { // No fee ammAlice.deposit(carol, USD(3'000)); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{1'000})); BEAST_EXPECT(expectHolding(env, carol, USD(27'000))); // Set fee to 1% ammAlice.vote(alice, 1'000); BEAST_EXPECT(ammAlice.expectTradingFee(1'000)); // Single withdrawal. Carol gets ~5USD less than deposited. ammAlice.withdrawAll(carol, USD(0)); BEAST_EXPECT( expectHolding(env, carol, STAmount{USD, UINT64_C(29'994'97487437186), -11})); }, {{USD(1'000), EUR(1'000)}}, 0, std::nullopt, {features}); // Withdraw with EPrice limit, 1% fee. testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit(carol, 1'000'000); auto const tokensFee = ammAlice.withdraw(carol, USD(100), std::nullopt, IOUAmount{520, 0}); // carol withdraws ~1,443.44USD auto const balanceAfterWithdraw = [&]() { if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) { return STAmount(USD, UINT64_C(30'443'43891402715), -11); } if (features[fixAMMv1_1] && !features[fixAMMv1_3]) { return STAmount(USD, UINT64_C(30'443'43891402714), -11); } return STAmount(USD, UINT64_C(30'443'43891402713), -11); }(); BEAST_EXPECT(env.balance(carol, USD) == balanceAfterWithdraw); // Set to original pool size auto const deposit = balanceAfterWithdraw - USD(29'000); ammAlice.deposit(carol, deposit); // fee 0% ammAlice.vote(alice, 0); BEAST_EXPECT(ammAlice.expectTradingFee(0)); auto const tokensNoFee = ammAlice.withdraw(carol, deposit); if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) { BEAST_EXPECT( env.balance(carol, USD) == STAmount(USD, UINT64_C(30'443'43891402717), -11)); } else if (features[fixAMMv1_1] && !features[fixAMMv1_3]) { BEAST_EXPECT( env.balance(carol, USD) == STAmount(USD, UINT64_C(30'443'43891402716), -11)); } else { BEAST_EXPECT( env.balance(carol, USD) == STAmount(USD, UINT64_C(30'443'43891402713), -11)); } // carol pays ~4008 LPTokens in fees or ~0.5% of the no-fee // LPTokens if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) { BEAST_EXPECT(tokensNoFee == IOUAmount(746'579'80779913, -8)); } else if (features[fixAMMv1_1] && !features[fixAMMv1_3]) { BEAST_EXPECT(tokensNoFee == IOUAmount(746'579'80779912, -8)); } else { BEAST_EXPECT(tokensNoFee == IOUAmount(746'579'80779911, -8)); } BEAST_EXPECT(tokensFee == IOUAmount(750'588'23529411, -8)); }, std::nullopt, 1'000, std::nullopt, {features}); // Payment, 1% fee testAMM( [&](AMM& ammAlice, Env& env) { fund(env, gw, {bob}, XRP(1'000), {USD(1'000), EUR(1'000)}, Fund::Acct); // Alice contributed 1010EUR and 1000USD to the pool BEAST_EXPECT(expectHolding(env, alice, EUR(28'990))); BEAST_EXPECT(expectHolding(env, alice, USD(29'000))); BEAST_EXPECT(expectHolding(env, carol, USD(30'000))); // Carol pays to Alice with no fee env(pay(carol, alice, EUR(10)), path(~EUR), sendmax(USD(10)), txflags(tfNoRippleDirect)); env.close(); // Alice has 10EUR more and Carol has 10USD less BEAST_EXPECT(expectHolding(env, alice, EUR(29'000))); BEAST_EXPECT(expectHolding(env, alice, USD(29'000))); BEAST_EXPECT(expectHolding(env, carol, USD(29'990))); // Set fee to 1% ammAlice.vote(alice, 1'000); BEAST_EXPECT(ammAlice.expectTradingFee(1'000)); // Bob pays to Carol with 1% fee env(pay(bob, carol, USD(10)), path(~USD), sendmax(EUR(15)), txflags(tfNoRippleDirect)); env.close(); // Bob sends 10.1~EUR to pay 10USD BEAST_EXPECT( expectHolding(env, bob, STAmount{EUR, UINT64_C(989'8989898989899), -13})); // Carol got 10USD BEAST_EXPECT(expectHolding(env, carol, USD(30'000))); BEAST_EXPECT(ammAlice.expectBalances( USD(1'000), STAmount{EUR, UINT64_C(1'010'10101010101), -11}, ammAlice.tokens())); }, {{USD(1'000), EUR(1'010)}}, 0, std::nullopt, {features}); // Offer crossing, 0.5% fee testAMM( [&](AMM& ammAlice, Env& env) { // No fee env(offer(carol, EUR(10), USD(10))); env.close(); BEAST_EXPECT(expectHolding(env, carol, USD(29'990))); BEAST_EXPECT(expectHolding(env, carol, EUR(30'010))); // Change pool composition back env(offer(carol, USD(10), EUR(10))); env.close(); // Set fee to 0.5% ammAlice.vote(alice, 500); BEAST_EXPECT(ammAlice.expectTradingFee(500)); env(offer(carol, EUR(10), USD(10))); env.close(); // Alice gets fewer ~4.97EUR for ~5.02USD, the difference goes // to the pool BEAST_EXPECT( expectHolding(env, carol, STAmount{USD, UINT64_C(29'995'02512562814), -11})); BEAST_EXPECT( expectHolding(env, carol, STAmount{EUR, UINT64_C(30'004'97487437186), -11})); BEAST_EXPECT(expectOffers( env, carol, 1, {{Amounts{ STAmount{EUR, UINT64_C(5'025125628140703), -15}, STAmount{USD, UINT64_C(5'025125628140703), -15}}}})); if (!features[fixAMMv1_1]) { BEAST_EXPECT(ammAlice.expectBalances( STAmount{USD, UINT64_C(1'004'974874371859), -12}, STAmount{EUR, UINT64_C(1'005'025125628141), -12}, ammAlice.tokens())); } else { BEAST_EXPECT(ammAlice.expectBalances( STAmount{USD, UINT64_C(1'004'97487437186), -11}, STAmount{EUR, UINT64_C(1'005'025125628141), -12}, ammAlice.tokens())); } }, {{USD(1'000), EUR(1'010)}}, 0, std::nullopt, {features}); // Payment with AMM and CLOB offer, 0 fee // AMM liquidity is consumed first up to CLOB offer quality // CLOB offer is fully consumed next // Remaining amount is consumed via AMM liquidity { Env env(*this, features); Account const ed("ed"); fund(env, gw, {alice, bob, carol, ed}, XRP(1'000), {USD(2'000), EUR(2'000)}); env(offer(carol, EUR(5), USD(5))); AMM const ammAlice(env, alice, USD(1'005), EUR(1'000)); env(pay(bob, ed, USD(10)), path(~USD), sendmax(EUR(15)), txflags(tfNoRippleDirect)); BEAST_EXPECT(expectHolding(env, ed, USD(2'010))); if (!features[fixAMMv1_1]) { BEAST_EXPECT(expectHolding(env, bob, EUR(1'990))); BEAST_EXPECT(ammAlice.expectBalances(USD(1'000), EUR(1'005), ammAlice.tokens())); } else { BEAST_EXPECT( expectHolding(env, bob, STAmount(EUR, UINT64_C(1989'999999999999), -12))); BEAST_EXPECT(ammAlice.expectBalances( USD(1'000), STAmount(EUR, UINT64_C(1005'000000000001), -12), ammAlice.tokens())); } BEAST_EXPECT(expectOffers(env, carol, 0)); } // Payment with AMM and CLOB offer. Same as above but with 0.25% // fee. { Env env(*this, features); Account const ed("ed"); fund(env, gw, {alice, bob, carol, ed}, XRP(1'000), {USD(2'000), EUR(2'000)}); env(offer(carol, EUR(5), USD(5))); // Set 0.25% fee AMM const ammAlice(env, alice, USD(1'005), EUR(1'000), false, 250); env(pay(bob, ed, USD(10)), path(~USD), sendmax(EUR(15)), txflags(tfNoRippleDirect)); BEAST_EXPECT(expectHolding(env, ed, USD(2'010))); if (!features[fixAMMv1_1]) { BEAST_EXPECT( expectHolding(env, bob, STAmount{EUR, UINT64_C(1'989'987453007618), -12})); BEAST_EXPECT(ammAlice.expectBalances( USD(1'000), STAmount{EUR, UINT64_C(1'005'012546992382), -12}, ammAlice.tokens())); } else { BEAST_EXPECT( expectHolding(env, bob, STAmount{EUR, UINT64_C(1'989'987453007628), -12})); BEAST_EXPECT(ammAlice.expectBalances( USD(1'000), STAmount{EUR, UINT64_C(1'005'012546992372), -12}, ammAlice.tokens())); } BEAST_EXPECT(expectOffers(env, carol, 0)); } // Payment with AMM and CLOB offer. AMM has a better // spot price quality, but 1% fee offsets that. As the result // the entire trade is executed via LOB. { Env env(*this, features); Account const ed("ed"); fund(env, gw, {alice, bob, carol, ed}, XRP(1'000), {USD(2'000), EUR(2'000)}); env(offer(carol, EUR(10), USD(10))); // Set 1% fee AMM const ammAlice(env, alice, USD(1'005), EUR(1'000), false, 1'000); env(pay(bob, ed, USD(10)), path(~USD), sendmax(EUR(15)), txflags(tfNoRippleDirect)); BEAST_EXPECT(expectHolding(env, ed, USD(2'010))); BEAST_EXPECT(expectHolding(env, bob, EUR(1'990))); BEAST_EXPECT(ammAlice.expectBalances(USD(1'005), EUR(1'000), ammAlice.tokens())); BEAST_EXPECT(expectOffers(env, carol, 0)); } // Payment with AMM and CLOB offer. AMM has a better // spot price quality, but 1% fee offsets that. // The CLOB offer is consumed first and the remaining // amount is consumed via AMM liquidity. { Env env(*this, features); Account const ed("ed"); fund(env, gw, {alice, bob, carol, ed}, XRP(1'000), {USD(2'000), EUR(2'000)}); env(offer(carol, EUR(9), USD(9))); // Set 1% fee AMM const ammAlice(env, alice, USD(1'005), EUR(1'000), false, 1'000); env(pay(bob, ed, USD(10)), path(~USD), sendmax(EUR(15)), txflags(tfNoRippleDirect)); BEAST_EXPECT(expectHolding(env, ed, USD(2'010))); BEAST_EXPECT(expectHolding(env, bob, STAmount{EUR, UINT64_C(1'989'993923296712), -12})); BEAST_EXPECT(ammAlice.expectBalances( USD(1'004), STAmount{EUR, UINT64_C(1'001'006076703288), -12}, ammAlice.tokens())); BEAST_EXPECT(expectOffers(env, carol, 0)); } } void testAdjustedTokens(FeatureBitset features) { testcase("Adjusted Deposit/Withdraw Tokens"); using namespace jtx; // Deposit/Withdraw in USD testAMM( [&](AMM& ammAlice, Env& env) { Account const bob("bob"); Account const ed("ed"); Account const paul("paul"); Account const dan("dan"); Account const chris("chris"); Account const simon("simon"); Account const ben("ben"); Account const natalie("natalie"); fund( env, gw, {bob, ed, paul, dan, chris, simon, ben, natalie}, {USD(1'500'000)}, Fund::Acct); for (int i = 0; i < 10; ++i) { ammAlice.deposit(ben, STAmount{USD, 1, -10}); ammAlice.withdrawAll(ben, USD(0)); ammAlice.deposit(simon, USD(0.1)); ammAlice.withdrawAll(simon, USD(0)); ammAlice.deposit(chris, USD(1)); ammAlice.withdrawAll(chris, USD(0)); ammAlice.deposit(dan, USD(10)); ammAlice.withdrawAll(dan, USD(0)); ammAlice.deposit(bob, USD(100)); ammAlice.withdrawAll(bob, USD(0)); ammAlice.deposit(carol, USD(1'000)); ammAlice.withdrawAll(carol, USD(0)); ammAlice.deposit(ed, USD(10'000)); ammAlice.withdrawAll(ed, USD(0)); ammAlice.deposit(paul, USD(100'000)); ammAlice.withdrawAll(paul, USD(0)); ammAlice.deposit(natalie, USD(1'000'000)); ammAlice.withdrawAll(natalie, USD(0)); } // Due to round off some accounts have a tiny gain, while // other have a tiny loss. The last account to withdraw // gets everything in the pool. if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) { BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), STAmount{USD, UINT64_C(10'000'0000000013), -10}, IOUAmount{10'000'000})); } else if (features[fixAMMv1_3]) { BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), STAmount{USD, UINT64_C(10'000'0000000003), -10}, IOUAmount{10'000'000})); } else { BEAST_EXPECT( ammAlice.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000})); } BEAST_EXPECT(expectHolding(env, ben, USD(1'500'000))); BEAST_EXPECT(expectHolding(env, simon, USD(1'500'000))); BEAST_EXPECT(expectHolding(env, chris, USD(1'500'000))); BEAST_EXPECT(expectHolding(env, dan, USD(1'500'000))); if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) { BEAST_EXPECT(expectHolding( env, carol, STAmount{USD, UINT64_C(30'000'00000000001), -11})); } else if (features[fixAMMv1_1] && !features[fixAMMv1_3]) { BEAST_EXPECT(expectHolding(env, carol, USD(30'000))); } else { BEAST_EXPECT(expectHolding(env, carol, USD(30'000))); } BEAST_EXPECT(expectHolding(env, ed, USD(1'500'000))); BEAST_EXPECT(expectHolding(env, paul, USD(1'500'000))); if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) { BEAST_EXPECT(expectHolding( env, natalie, STAmount{USD, UINT64_C(1'500'000'000000002), -9})); } else if (features[fixAMMv1_1] && !features[fixAMMv1_3]) { BEAST_EXPECT(expectHolding( env, natalie, STAmount{USD, UINT64_C(1'500'000'000000005), -9})); } else { BEAST_EXPECT(expectHolding(env, natalie, USD(1'500'000))); } ammAlice.withdrawAll(alice); BEAST_EXPECT(!ammAlice.ammExists()); if (!features[fixAMMv1_1]) { BEAST_EXPECT( expectHolding(env, alice, STAmount{USD, UINT64_C(30'000'0000000013), -10})); } else if (features[fixAMMv1_3]) { BEAST_EXPECT( expectHolding(env, alice, STAmount{USD, UINT64_C(30'000'0000000003), -10})); } else { BEAST_EXPECT(expectHolding(env, alice, USD(30'000))); } // alice XRP balance is 30,000 initial - 50 AMMCreate fee - // 10drops fee BEAST_EXPECT( accountBalance(env, alice) == std::to_string(29950000000 - env.current()->fees().base.drops())); }, std::nullopt, 0, std::nullopt, {features}); // Same as above but deposit/withdraw in XRP testAMM( [&](AMM& ammAlice, Env& env) { Account const bob("bob"); Account const ed("ed"); Account const paul("paul"); Account const dan("dan"); Account const chris("chris"); Account const simon("simon"); Account const ben("ben"); Account const natalie("natalie"); fund( env, gw, {bob, ed, paul, dan, chris, simon, ben, natalie}, XRP(2'000'000), {}, Fund::Acct); for (int i = 0; i < 10; ++i) { ammAlice.deposit(ben, XRPAmount{1}); ammAlice.withdrawAll(ben, XRP(0)); ammAlice.deposit(simon, XRPAmount(1'000)); ammAlice.withdrawAll(simon, XRP(0)); ammAlice.deposit(chris, XRP(1)); ammAlice.withdrawAll(chris, XRP(0)); ammAlice.deposit(dan, XRP(10)); ammAlice.withdrawAll(dan, XRP(0)); ammAlice.deposit(bob, XRP(100)); ammAlice.withdrawAll(bob, XRP(0)); ammAlice.deposit(carol, XRP(1'000)); ammAlice.withdrawAll(carol, XRP(0)); ammAlice.deposit(ed, XRP(10'000)); ammAlice.withdrawAll(ed, XRP(0)); ammAlice.deposit(paul, XRP(100'000)); ammAlice.withdrawAll(paul, XRP(0)); ammAlice.deposit(natalie, XRP(1'000'000)); ammAlice.withdrawAll(natalie, XRP(0)); } auto const baseFee = env.current()->fees().base.drops(); if (!features[fixAMMv1_3]) { // No round off with XRP in this test BEAST_EXPECT( ammAlice.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000})); ammAlice.withdrawAll(alice); BEAST_EXPECT(!ammAlice.ammExists()); // 20,000 initial - (deposit+withdraw) * 10 auto const xrpBalance = (XRP(2'000'000) - txfee(env, 20)).getText(); BEAST_EXPECT(accountBalance(env, ben) == xrpBalance); BEAST_EXPECT(accountBalance(env, simon) == xrpBalance); BEAST_EXPECT(accountBalance(env, chris) == xrpBalance); BEAST_EXPECT(accountBalance(env, dan) == xrpBalance); // 30,000 initial - (deposit+withdraw) * 10 BEAST_EXPECT( accountBalance(env, carol) == std::to_string(30'000'000'000 - (20 * baseFee))); BEAST_EXPECT(accountBalance(env, ed) == xrpBalance); BEAST_EXPECT(accountBalance(env, paul) == xrpBalance); BEAST_EXPECT(accountBalance(env, natalie) == xrpBalance); // 30,000 initial - 50 AMMCreate fee - 10drops withdraw fee BEAST_EXPECT( accountBalance(env, alice) == std::to_string(29'950'000'000 - baseFee)); } else { // post-amendment the rounding takes place to ensure // AMM invariant BEAST_EXPECT(ammAlice.expectBalances( XRPAmount(10'000'000'080), USD(10'000), IOUAmount{10'000'000})); ammAlice.withdrawAll(alice); BEAST_EXPECT(!ammAlice.ammExists()); auto const xrpBalance = XRP(2'000'000) - txfee(env, 20) - drops(10); auto const xrpBalanceText = xrpBalance.getText(); BEAST_EXPECT(accountBalance(env, ben) == xrpBalanceText); BEAST_EXPECT(accountBalance(env, simon) == xrpBalanceText); BEAST_EXPECT(accountBalance(env, chris) == xrpBalanceText); BEAST_EXPECT(accountBalance(env, dan) == xrpBalanceText); BEAST_EXPECT( accountBalance(env, carol) == std::to_string(30'000'000'000 - (20 * baseFee) - 10)); BEAST_EXPECT(accountBalance(env, ed) == (xrpBalance + drops(2)).getText()); BEAST_EXPECT(accountBalance(env, paul) == (xrpBalance + drops(3)).getText()); BEAST_EXPECT(accountBalance(env, natalie) == (xrpBalance + drops(5)).getText()); BEAST_EXPECT( accountBalance(env, alice) == std::to_string(29'950'000'000 - baseFee + 80)); } }, std::nullopt, 0, std::nullopt, {features}); } void testAutoDelete() { testcase("Auto Delete"); using namespace jtx; FeatureBitset const all{testable_amendments()}; { Env env( *this, envconfig([](std::unique_ptr cfg) { cfg->FEES.reference_fee = XRPAmount(1); return cfg; }), all); fund(env, gw, {alice}, XRP(20'000), {USD(10'000)}); AMM amm(env, gw, XRP(10'000), USD(10'000)); for (auto i = 0; i < maxDeletableAMMTrustLines + 10; ++i) { Account const a{std::to_string(i)}; env.fund(XRP(1'000), a); env(trust(a, STAmount{amm.lptIssue(), 10'000})); env.close(); } // The trustlines are partially deleted, // AMM is set to an empty state. amm.withdrawAll(gw); BEAST_EXPECT(amm.ammExists()); // Bid,Vote,Deposit,Withdraw,TrustSet failing with // tecAMM_EMPTY. Deposit succeeds with tfTwoAssetIfEmpty option. env(amm.bid({ .account = alice, .bidMin = 1000, }), ter(tecAMM_EMPTY)); amm.vote( std::nullopt, 100, std::nullopt, std::nullopt, std::nullopt, ter(tecAMM_EMPTY)); amm.withdraw(alice, 100, std::nullopt, std::nullopt, ter(tecAMM_EMPTY)); amm.deposit( alice, USD(100), std::nullopt, std::nullopt, std::nullopt, ter(tecAMM_EMPTY)); env(trust(alice, STAmount{amm.lptIssue(), 10'000}), ter(tecAMM_EMPTY)); // Can deposit with tfTwoAssetIfEmpty option amm.deposit( alice, std::nullopt, XRP(10'000), USD(10'000), std::nullopt, tfTwoAssetIfEmpty, std::nullopt, std::nullopt, 1'000); BEAST_EXPECT(amm.expectBalances(XRP(10'000), USD(10'000), amm.tokens())); BEAST_EXPECT(amm.expectTradingFee(1'000)); BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{0})); // Withdrawing all tokens deletes AMM since the number // of remaining trustlines is less than max amm.withdrawAll(alice); BEAST_EXPECT(!amm.ammExists()); BEAST_EXPECT(!env.le(keylet::ownerDir(amm.ammAccount()))); } { Env env( *this, envconfig([](std::unique_ptr cfg) { cfg->FEES.reference_fee = XRPAmount(1); return cfg; }), all); fund(env, gw, {alice}, XRP(20'000), {USD(10'000)}); AMM amm(env, gw, XRP(10'000), USD(10'000)); for (auto i = 0; i < (maxDeletableAMMTrustLines * 2) + 10; ++i) { Account const a{std::to_string(i)}; env.fund(XRP(1'000), a); env(trust(a, STAmount{amm.lptIssue(), 10'000})); env.close(); } // The trustlines are partially deleted. amm.withdrawAll(gw); BEAST_EXPECT(amm.ammExists()); // AMMDelete has to be called twice to delete AMM. amm.ammDelete(alice, ter(tecINCOMPLETE)); BEAST_EXPECT(amm.ammExists()); // Deletes remaining trustlines and deletes AMM. amm.ammDelete(alice); BEAST_EXPECT(!amm.ammExists()); BEAST_EXPECT(!env.le(keylet::ownerDir(amm.ammAccount()))); // Try redundant delete amm.ammDelete(alice, ter(terNO_AMM)); } } void testClawback() { testcase("Clawback"); using namespace jtx; Env env(*this); env.fund(XRP(2'000), gw); env.fund(XRP(2'000), alice); AMM const amm(env, gw, XRP(1'000), USD(1'000)); env(fset(gw, asfAllowTrustLineClawback), ter(tecOWNERS)); } void testAMMID() { testcase("AMMID"); using namespace jtx; testAMM([&](AMM& amm, Env& env) { amm.setClose(false); auto const info = env.rpc( "json", "account_info", std::string("{\"account\": \"" + to_string(amm.ammAccount()) + "\"}")); try { BEAST_EXPECT( info[jss::result][jss::account_data][jss::AMMID].asString() == to_string(amm.ammID())); } catch (...) { fail(); } amm.deposit(carol, 1'000); auto affected = env.meta()->getJson(JsonOptions::none)[sfAffectedNodes.fieldName]; try { bool found = false; for (auto const& node : affected) { if (node.isMember(sfModifiedNode.fieldName) && node[sfModifiedNode.fieldName][sfLedgerEntryType.fieldName].asString() == "AccountRoot" && node[sfModifiedNode.fieldName][sfFinalFields.fieldName][jss::Account] .asString() == to_string(amm.ammAccount())) { found = node[sfModifiedNode.fieldName][sfFinalFields.fieldName][jss::AMMID] .asString() == to_string(amm.ammID()); break; } } BEAST_EXPECT(found); } catch (...) { fail(); } }); } void testSelection(FeatureBitset features) { testcase("Offer/Strand Selection"); using namespace jtx; Account const ed("ed"); Account const gw1("gw1"); auto const ETH = gw1["ETH"]; auto const CAN = gw1["CAN"]; // These tests are expected to fail if the OwnerPaysFee feature // is ever supported. Updates will need to be made to AMM handling // in the payment engine, and these tests will need to be updated. auto prep = [&](Env& env, auto gwRate, auto gw1Rate) { fund(env, gw, {alice, carol, bob, ed}, XRP(2'000), {USD(2'000)}); env.fund(XRP(2'000), gw1); fund(env, gw1, {alice, carol, bob, ed}, {ETH(2'000), CAN(2'000)}, Fund::TokenOnly); env(rate(gw, gwRate)); env(rate(gw1, gw1Rate)); env.close(); }; for (auto const& rates : {std::make_pair(1.5, 1.9), std::make_pair(1.9, 1.5)}) { // Offer Selection // Cross-currency payment: AMM has the same spot price quality // as CLOB's offer and can't generate a better quality offer. // The transfer fee in this case doesn't change the CLOB quality // because trIn is ignored on adjustment and trOut on payment is // also ignored because ownerPaysTransferFee is false in this // case. Run test for 0) offer, 1) AMM, 2) offer and AMM to // verify that the quality is better in the first case, and CLOB // is selected in the second case. { std::array q{}; for (auto i = 0; i < 3; ++i) { Env env(*this, features); prep(env, rates.first, rates.second); std::optional amm; if (i == 0 || i == 2) { env(offer(ed, ETH(400), USD(400)), txflags(tfPassive)); env.close(); } if (i > 0) amm.emplace(env, ed, USD(1'000), ETH(1'000)); env(pay(carol, bob, USD(100)), path(~USD), sendmax(ETH(500))); env.close(); // CLOB and AMM, AMM is not selected if (i == 2) { BEAST_EXPECT(amm->expectBalances(USD(1'000), ETH(1'000), amm->tokens())); } BEAST_EXPECT(expectHolding(env, bob, USD(2'100))); q[i] = Quality( Amounts{ ETH(2'000) - env.balance(carol, ETH), env.balance(bob, USD) - USD(2'000)}); } // CLOB is better quality than AMM BEAST_EXPECT(q[0] > q[1]); // AMM is not selected with CLOB BEAST_EXPECT(q[0] == q[2]); } // Offer crossing: AMM has the same spot price quality // as CLOB's offer and can't generate a better quality offer. // The transfer fee in this case doesn't change the CLOB quality // because the quality adjustment is ignored for the offer // crossing. for (auto i = 0; i < 3; ++i) { Env env(*this, features); prep(env, rates.first, rates.second); std::optional amm; if (i == 0 || i == 2) { env(offer(ed, ETH(400), USD(400)), txflags(tfPassive)); env.close(); } if (i > 0) amm.emplace(env, ed, USD(1'000), ETH(1'000)); env(offer(alice, USD(400), ETH(400))); env.close(); // AMM is not selected if (i > 0) { BEAST_EXPECT(amm->expectBalances(USD(1'000), ETH(1'000), amm->tokens())); } if (i == 0 || i == 2) { // Fully crosses BEAST_EXPECT(expectOffers(env, alice, 0)); } // Fails to cross because AMM is not selected else { BEAST_EXPECT(expectOffers(env, alice, 1, {Amounts{USD(400), ETH(400)}})); } BEAST_EXPECT(expectOffers(env, ed, 0)); } // Show that the CLOB quality reduction // results in AMM offer selection. // Same as the payment but reduced offer quality { std::array q{}; for (auto i = 0; i < 3; ++i) { Env env(*this, features); prep(env, rates.first, rates.second); std::optional amm; if (i == 0 || i == 2) { env(offer(ed, ETH(400), USD(300)), txflags(tfPassive)); env.close(); } if (i > 0) amm.emplace(env, ed, USD(1'000), ETH(1'000)); env(pay(carol, bob, USD(100)), path(~USD), sendmax(ETH(500))); env.close(); // AMM and CLOB are selected if (i > 0) { BEAST_EXPECT(!amm->expectBalances(USD(1'000), ETH(1'000), amm->tokens())); } if (i == 2 && !features[fixAMMv1_1]) { if (rates.first == 1.5) { if (!features[fixAMMv1_1]) { BEAST_EXPECT(expectOffers( env, ed, 1, {{Amounts{ STAmount{ETH, UINT64_C(378'6327949540823), -13}, STAmount{USD, UINT64_C(283'9745962155617), -13}}}})); } else { BEAST_EXPECT(expectOffers( env, ed, 1, {{Amounts{ STAmount{ETH, UINT64_C(378'6327949540813), -13}, STAmount{USD, UINT64_C(283'974596215561), -12}}}})); } } else { if (!features[fixAMMv1_1]) { BEAST_EXPECT(expectOffers( env, ed, 1, {{Amounts{ STAmount{ETH, UINT64_C(325'299461620749), -12}, STAmount{USD, UINT64_C(243'9745962155617), -13}}}})); } else { BEAST_EXPECT(expectOffers( env, ed, 1, {{Amounts{ STAmount{ETH, UINT64_C(325'299461620748), -12}, STAmount{USD, UINT64_C(243'974596215561), -12}}}})); } } } else if (i == 2) { if (rates.first == 1.5) { BEAST_EXPECT(expectOffers( env, ed, 1, {{Amounts{ STAmount{ETH, UINT64_C(378'6327949540812), -13}, STAmount{USD, UINT64_C(283'9745962155609), -13}}}})); } else { BEAST_EXPECT(expectOffers( env, ed, 1, {{Amounts{ STAmount{ETH, UINT64_C(325'2994616207479), -13}, STAmount{USD, UINT64_C(243'9745962155609), -13}}}})); } } BEAST_EXPECT(expectHolding(env, bob, USD(2'100))); q[i] = Quality( Amounts{ ETH(2'000) - env.balance(carol, ETH), env.balance(bob, USD) - USD(2'000)}); } // AMM is better quality BEAST_EXPECT(q[1] > q[0]); // AMM and CLOB produce better quality BEAST_EXPECT(q[2] > q[1]); } // Same as the offer-crossing but reduced offer quality for (auto i = 0; i < 3; ++i) { Env env(*this, features); prep(env, rates.first, rates.second); std::optional amm; if (i == 0 || i == 2) { env(offer(ed, ETH(400), USD(250)), txflags(tfPassive)); env.close(); } if (i > 0) amm.emplace(env, ed, USD(1'000), ETH(1'000)); env(offer(alice, USD(250), ETH(400))); env.close(); // AMM is selected in both cases if (i > 0) { BEAST_EXPECT(!amm->expectBalances(USD(1'000), ETH(1'000), amm->tokens())); } // Partially crosses, AMM is selected, CLOB fails // limitQuality if (i == 2) { if (rates.first == 1.5) { if (!features[fixAMMv1_1]) { BEAST_EXPECT(expectOffers(env, ed, 1, {{Amounts{ETH(400), USD(250)}}})); BEAST_EXPECT(expectOffers( env, alice, 1, {{Amounts{ STAmount{USD, UINT64_C(40'5694150420947), -13}, STAmount{ETH, UINT64_C(64'91106406735152), -14}, }}})); } else { // Ed offer is partially crossed. // The updated rounding makes limitQuality // work if both amendments are enabled BEAST_EXPECT(expectOffers( env, ed, 1, {{Amounts{ STAmount{ETH, UINT64_C(335'0889359326475), -13}, STAmount{USD, UINT64_C(209'4305849579047), -13}, }}})); BEAST_EXPECT(expectOffers(env, alice, 0)); } } else { if (!features[fixAMMv1_1]) { // Ed offer is partially crossed. BEAST_EXPECT(expectOffers( env, ed, 1, {{Amounts{ STAmount{ETH, UINT64_C(335'0889359326485), -13}, STAmount{USD, UINT64_C(209'4305849579053), -13}, }}})); BEAST_EXPECT(expectOffers(env, alice, 0)); } else { // Ed offer is partially crossed. BEAST_EXPECT(expectOffers( env, ed, 1, {{Amounts{ STAmount{ETH, UINT64_C(335'0889359326475), -13}, STAmount{USD, UINT64_C(209'4305849579047), -13}, }}})); BEAST_EXPECT(expectOffers(env, alice, 0)); } } } } // Strand selection // Two book steps strand quality is 1. // AMM strand's best quality is equal to AMM's spot price // quality, which is 1. Both strands (steps) are adjusted // for the transfer fee in qualityUpperBound. In case // of two strands, AMM offers have better quality and are // consumed first, remaining liquidity is generated by CLOB // offers. Liquidity from two strands is better in this case // than in case of one strand with two book steps. Liquidity // from one strand with AMM has better quality than either one // strand with two book steps or two strands. It may appear // unintuitive, but one strand with AMM is optimized and // generates one AMM offer, while in case of two strands, // multiple AMM offers are generated, which results in slightly // worse overall quality. { std::array q{}; for (auto i = 0; i < 3; ++i) { Env env(*this, features); prep(env, rates.first, rates.second); std::optional amm; if (i == 0 || i == 2) { env(offer(ed, ETH(400), CAN(400)), txflags(tfPassive)); env(offer(ed, CAN(400), USD(400))), txflags(tfPassive); env.close(); } if (i > 0) amm.emplace(env, ed, ETH(1'000), USD(1'000)); env(pay(carol, bob, USD(100)), path(~USD), path(~CAN, ~USD), sendmax(ETH(600))); env.close(); BEAST_EXPECT(expectHolding(env, bob, USD(2'100))); if (i == 2 && !features[fixAMMv1_1]) { if (rates.first == 1.5) { // Liquidity is consumed from AMM strand only BEAST_EXPECT(amm->expectBalances( STAmount{ETH, UINT64_C(1'176'66038955758), -11}, USD(850), amm->tokens())); } else { BEAST_EXPECT(amm->expectBalances( STAmount{ETH, UINT64_C(1'179'540094339627), -12}, STAmount{USD, UINT64_C(847'7880529867501), -13}, amm->tokens())); BEAST_EXPECT(expectOffers( env, ed, 2, {{Amounts{ STAmount{ETH, UINT64_C(343'3179205198749), -13}, STAmount{CAN, UINT64_C(343'3179205198749), -13}, }, Amounts{ STAmount{CAN, UINT64_C(362'2119470132499), -13}, STAmount{USD, UINT64_C(362'2119470132499), -13}, }}})); } } else if (i == 2) { if (rates.first == 1.5) { // Liquidity is consumed from AMM strand only BEAST_EXPECT(amm->expectBalances( STAmount{ETH, UINT64_C(1'176'660389557593), -12}, USD(850), amm->tokens())); } else { BEAST_EXPECT(amm->expectBalances( STAmount{ETH, UINT64_C(1'179'54009433964), -11}, STAmount{USD, UINT64_C(847'7880529867501), -13}, amm->tokens())); BEAST_EXPECT(expectOffers( env, ed, 2, {{Amounts{ STAmount{ETH, UINT64_C(343'3179205198749), -13}, STAmount{CAN, UINT64_C(343'3179205198749), -13}, }, Amounts{ STAmount{CAN, UINT64_C(362'2119470132499), -13}, STAmount{USD, UINT64_C(362'2119470132499), -13}, }}})); } } q[i] = Quality( Amounts{ ETH(2'000) - env.balance(carol, ETH), env.balance(bob, USD) - USD(2'000)}); } BEAST_EXPECT(q[1] > q[0]); BEAST_EXPECT(q[2] > q[0] && q[2] < q[1]); } } } void testFixDefaultInnerObj() { testcase("Fix Default Inner Object"); using namespace jtx; FeatureBitset const all{testable_amendments()}; auto test = [&](FeatureBitset features, TER const& err1, TER const& err2, TER const& err3, TER const& err4, std::uint16_t tfee, bool closeLedger, std::optional extra = std::nullopt) { Env env(*this, features); fund(env, gw, {alice}, XRP(1'000), {USD(10)}); AMM amm(env, gw, XRP(10), USD(10), {.tfee = tfee, .close = closeLedger}); amm.deposit(alice, USD(10), XRP(10)); amm.vote(VoteArg{.account = alice, .tfee = tfee, .err = ter(err1)}); amm.withdraw(WithdrawArg{.account = gw, .asset1Out = USD(1), .err = ter(err2)}); // with the amendment disabled and ledger not closed, // second vote succeeds if the first vote sets the trading fee // to non-zero; if the first vote sets the trading fee to >0 && // <9 then the second withdraw succeeds if the second vote sets // the trading fee so that the discounted fee is non-zero amm.vote(VoteArg{.account = alice, .tfee = 20, .err = ter(err3)}); amm.withdraw(WithdrawArg{.account = gw, .asset1Out = USD(2), .err = ter(err4)}); }; // ledger is closed after each transaction, vote/withdraw don't fail // regardless whether the amendment is enabled or not test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 0, true); test(all - fixInnerObjTemplate, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 0, true); // ledger is not closed after each transaction // vote/withdraw don't fail if the amendment is enabled test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 0, false); // vote/withdraw fail if the amendment is not enabled // second vote/withdraw still fail: second vote fails because // the initial trading fee is 0, consequently second withdraw fails // because the second vote fails test( all - fixInnerObjTemplate, tefEXCEPTION, tefEXCEPTION, tefEXCEPTION, tefEXCEPTION, 0, false); // if non-zero trading/discounted fee then vote/withdraw // don't fail whether the ledger is closed or not and // the amendment is enabled or not test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 10, true); test(all - fixInnerObjTemplate, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 10, true); test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 10, false); test(all - fixInnerObjTemplate, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 10, false); // non-zero trading fee but discounted fee is 0, vote doesn't fail // but withdraw fails test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 9, false); // second vote sets the trading fee to non-zero, consequently // second withdraw doesn't fail even if the amendment is not // enabled and the ledger is not closed test(all - fixInnerObjTemplate, tesSUCCESS, tefEXCEPTION, tesSUCCESS, tesSUCCESS, 9, false); } void testFixChangeSpotPriceQuality(FeatureBitset features) { testcase("Fix changeSpotPriceQuality"); using namespace jtx; std::string logs; enum class Status { SucceedShouldSucceedResize, // Succeed in pre-fix because // error allowance, succeed post-fix // because of offer resizing FailShouldSucceed, // Fail in pre-fix due to rounding, // succeed after fix because of XRP // side is generated first SucceedShouldFail, // Succeed in pre-fix, fail after fix // due to small quality difference Fail, // Both fail because the quality can't be matched Succeed // Both succeed }; using enum Status; auto const xrpIouAmounts10_100 = TAmounts{XRPAmount{10}, IOUAmount{100}}; auto const iouXrpAmounts10_100 = TAmounts{IOUAmount{10}, XRPAmount{100}}; // clang-format off std::vector> const tests = { //Pool In , Pool Out, Quality , Fee, Status {"0.001519763260828713", "1558701", Quality{5414253689393440221}, 1000, FailShouldSucceed}, {"0.01099814367603737", "1892611", Quality{5482264816516900274}, 1000, FailShouldSucceed}, {"0.78", "796599", Quality{5630392334958379008}, 1000, FailShouldSucceed}, {"105439.2955578965", "49398693", Quality{5910869983721805038}, 400, FailShouldSucceed}, {"12408293.23445213", "4340810521", Quality{5911611095910090752}, 997, FailShouldSucceed}, {"1892611", "0.01099814367603737", Quality{6703103457950430139}, 1000, FailShouldSucceed}, {"423028.8508101858", "3392804520", Quality{5837920340654162816}, 600, FailShouldSucceed}, {"44565388.41001027", "73890647", Quality{6058976634606450001}, 1000, FailShouldSucceed}, {"66831.68494832662", "16", Quality{6346111134641742975}, 0, FailShouldSucceed}, {"675.9287302203422", "1242632304", Quality{5625960929244093294}, 300, FailShouldSucceed}, {"7047.112186735699", "1649845866", Quality{5696855348026306945}, 504, FailShouldSucceed}, {"840236.4402981238", "47419053", Quality{5982561601648018688}, 499, FailShouldSucceed}, {"992715.618909774", "189445631733", Quality{5697835648288106944}, 815, SucceedShouldSucceedResize}, {"504636667521", "185545883.9506651", Quality{6343802275337659280}, 503, SucceedShouldSucceedResize}, {"992706.7218636649", "189447316000", Quality{5697835648288106944}, 797, SucceedShouldSucceedResize}, {"1.068737911388205", "127860278877", Quality{5268604356368739396}, 293, SucceedShouldSucceedResize}, {"17932506.56880419", "189308.6043676173", Quality{6206460598195440068}, 311, SucceedShouldSucceedResize}, {"1.066379294658174", "128042251493", Quality{5268559341368739328}, 270, SucceedShouldSucceedResize}, {"350131413924", "1576879.110907892", Quality{6487411636539049449}, 650, Fail}, {"422093460", "2.731797662057464", Quality{6702911108534394924}, 1000, Fail}, {"76128132223", "367172.7148422662", Quality{6487263463413514240}, 548, Fail}, {"132701839250", "280703770.7695443", Quality{6273750681188885075}, 562, Fail}, {"994165.7604612011", "189551302411", Quality{5697835592690668727}, 815, Fail}, {"45053.33303227917", "86612695359", Quality{5625695218943638190}, 500, Fail}, {"199649.077043865", "14017933007", Quality{5766034667318524880}, 324, Fail}, {"27751824831.70903", "78896950", Quality{6272538159621630432}, 500, Fail}, {"225.3731275781907", "156431793648", Quality{5477818047604078924}, 989, Fail}, {"199649.077043865", "14017933007", Quality{5766036094462806309}, 324, Fail}, {"3.590272027140361", "20677643641", Quality{5406056147042156356}, 808, Fail}, {"1.070884664490231", "127604712776", Quality{5268620608623825741}, 293, Fail}, {"3272.448829820197", "6275124076", Quality{5625710328924117902}, 81, Fail}, {"0.009059512633902926", "7994028", Quality{5477511954775533172}, 1000, Fail}, {"1", "1.0", Quality{0}, 100, Fail}, {"1.0", "1", Quality{0}, 100, Fail}, {"10", "10.0", Quality{xrpIouAmounts10_100}, 100, Fail}, {"10.0", "10", Quality{iouXrpAmounts10_100}, 100, Fail}, {"69864389131", "287631.4543025075", Quality{6487623473313516078}, 451, Succeed}, {"4328342973", "12453825.99247381", Quality{6272522264364865181}, 997, Succeed}, {"32347017", "7003.93031579449", Quality{6347261126087916670}, 1000, Succeed}, {"61697206161", "36631.4583206413", Quality{6558965195382476659}, 500, Succeed}, {"1654524979", "7028.659825511603", Quality{6487551345110052981}, 504, Succeed}, {"88621.22277293179", "5128418948", Quality{5766347291552869205}, 380, Succeed}, {"1892611", "0.01099814367603737", Quality{6703102780512015436}, 1000, Succeed}, {"4542.639373338766", "24554809", Quality{5838994982188783710}, 0, Succeed}, {"5132932546", "88542.99750172683", Quality{6419203342950054537}, 380, Succeed}, {"78929964.1549083", "1506494795", Quality{5986890029845558688}, 589, Succeed}, {"10096561906", "44727.72453735605", Quality{6487455290284644551}, 250, Succeed}, {"5092.219565514988", "8768257694", Quality{5626349534958379008}, 503, Succeed}, {"1819778294", "8305.084302902864", Quality{6487429398998540860}, 415, Succeed}, {"6970462.633911943", "57359281", Quality{6054087899185946624}, 850, Succeed}, {"3983448845", "2347.543644281467", Quality{6558965195382476659}, 856, Succeed}, // This is a tiny offer 12drops/19321952e-15 it succeeds pre-amendment because of the error allowance. // Post amendment it is resized to 11drops/17711789e-15 but the quality is still less than // the target quality and the offer fails. {"771493171", "1.243473020567508", Quality{6707566798038544272}, 100, SucceedShouldFail}, }; // clang-format on boost::regex const rx("^\\d+$"); boost::smatch match; // tests that succeed should have the same amounts pre-fix and post-fix std::vector> const successAmounts; Env const env(*this, features, std::make_unique(&logs)); auto rules = env.current()->rules(); CurrentTransactionRulesGuard const rg(rules); NumberMantissaScaleGuard const sg(MantissaRange::small); for (auto const& t : tests) { auto getPool = [&](std::string const& v, bool isXRP) { if (isXRP) return amountFromString(xrpIssue(), v); return amountFromString(noIssue(), v); }; auto const& quality = std::get(t); auto const tfee = std::get(t); auto const status = std::get(t); auto const poolInIsXRP = boost::regex_search(std::get<0>(t), match, rx); auto const poolOutIsXRP = boost::regex_search(std::get<1>(t), match, rx); assert(!(poolInIsXRP && poolOutIsXRP)); auto const poolIn = getPool(std::get<0>(t), poolInIsXRP); auto const poolOut = getPool(std::get<1>(t), poolOutIsXRP); try { auto const amounts = changeSpotPriceQuality( Amounts{poolIn, poolOut}, quality, tfee, env.current()->rules(), env.journal); if (amounts) { if (status == SucceedShouldSucceedResize) { if (!features[fixAMMv1_1]) { BEAST_EXPECT(Quality{*amounts} < quality); } else { BEAST_EXPECT(Quality{*amounts} >= quality); } } else if (status == Succeed) { if (!features[fixAMMv1_1]) { BEAST_EXPECT( Quality{*amounts} >= quality || withinRelativeDistance(Quality{*amounts}, quality, Number{1, -7})); } else { BEAST_EXPECT(Quality{*amounts} >= quality); } } else if (status == FailShouldSucceed) { BEAST_EXPECT(features[fixAMMv1_1] && Quality{*amounts} >= quality); } else if (status == SucceedShouldFail) { BEAST_EXPECT( !features[fixAMMv1_1] && Quality{*amounts} < quality && withinRelativeDistance(Quality{*amounts}, quality, Number{1, -7})); } } else { // Fails pre- and post-amendment because the quality can't // be matched. Verify by generating a tiny offer, which // doesn't match the quality. Exclude zero quality since // no offer is generated in this case. if (status == Fail && quality != Quality{0}) { auto tinyOffer = [&]() { if (isXRP(poolIn)) { auto const takerPays = STAmount{xrpIssue(), 1}; return Amounts{ takerPays, swapAssetIn(Amounts{poolIn, poolOut}, takerPays, tfee)}; } if (isXRP(poolOut)) { auto const takerGets = STAmount{xrpIssue(), 1}; return Amounts{ swapAssetOut(Amounts{poolIn, poolOut}, takerGets, tfee), takerGets}; } auto const takerPays = toAmount(getAsset(poolIn), Number{1, -10} * poolIn); return Amounts{ takerPays, swapAssetIn(Amounts{poolIn, poolOut}, takerPays, tfee)}; }(); BEAST_EXPECT(Quality(tinyOffer) < quality); } else if (status == FailShouldSucceed) { BEAST_EXPECT(!features[fixAMMv1_1]); } else if (status == SucceedShouldFail) { BEAST_EXPECT(features[fixAMMv1_1]); } } } catch (std::runtime_error const& e) { BEAST_EXPECT(!strcmp(e.what(), "changeSpotPriceQuality failed")); BEAST_EXPECT(!features[fixAMMv1_1] && status == FailShouldSucceed); } } // Test negative discriminant { // b**2 - 4 * a * c -> 1 * 1 - 4 * 1 * 1 = -3 auto const res = solveQuadraticEqSmallest(Number{1}, Number{1}, Number{1}); BEAST_EXPECT(!res.has_value()); } } void testMalformed() { using namespace jtx; testAMM([&](AMM& ammAlice, Env& env) { WithdrawArg const args{ .flags = tfSingleAsset, .err = ter(temMALFORMED), }; ammAlice.withdraw(args); }); testAMM([&](AMM& ammAlice, Env& env) { WithdrawArg const args{ .flags = tfOneAssetLPToken, .err = ter(temMALFORMED), }; ammAlice.withdraw(args); }); testAMM([&](AMM& ammAlice, Env& env) { WithdrawArg const args{ .flags = tfLimitLPToken, .err = ter(temMALFORMED), }; ammAlice.withdraw(args); }); testAMM([&](AMM& ammAlice, Env& env) { WithdrawArg const args{ .asset1Out = XRP(100), .asset2Out = XRP(100), .err = ter(temBAD_AMM_TOKENS), }; ammAlice.withdraw(args); }); testAMM([&](AMM& ammAlice, Env& env) { WithdrawArg const args{ .asset1Out = XRP(100), .asset2Out = BAD(100), .err = ter(temBAD_CURRENCY), }; ammAlice.withdraw(args); }); testAMM([&](AMM& ammAlice, Env& env) { Json::Value jv; jv[jss::TransactionType] = jss::AMMWithdraw; jv[jss::Flags] = tfLimitLPToken; jv[jss::Account] = alice.human(); ammAlice.setTokens(jv); XRP(100).value().setJson(jv[jss::Amount]); USD(100).value().setJson(jv[jss::EPrice]); env(jv, ter(temBAD_AMM_TOKENS)); }); } void // NOLINTNEXTLINE(readability-convert-member-functions-to-static) testFixOverflowOffer(FeatureBitset featuresInitial) { using namespace jtx; using namespace std::chrono; FeatureBitset const all{featuresInitial}; std::string logs; Account const gatehub{"gatehub"}; Account const bitstamp{"bitstamp"}; Account const trader{"trader"}; auto const usdGH = gatehub["USD"]; auto const btcGH = gatehub["BTC"]; auto const usdBIT = bitstamp["USD"]; struct InputSet { char const* testCase; double const poolUsdBIT; double const poolUsdGH; sendmax const sendMaxUsdBIT; STAmount const sendUsdGH; STAmount const failUsdGH; STAmount const failUsdGHr; STAmount const failUsdBIT; STAmount const failUsdBITr; STAmount const goodUsdGH; STAmount const goodUsdGHr; STAmount const goodUsdBIT; STAmount const goodUsdBITr; IOUAmount const lpTokenBalance; std::optional const lpTokenBalanceAlt = std::nullopt; double const offer1BtcGH = 0.1; double const offer2BtcGH = 0.1; double const offer2UsdGH = 1; double const rateBIT = 0.0; double const rateGH = 0.0; }; using uint64_t = std::uint64_t; for (auto const& input : { InputSet{ .testCase = "Test Fix Overflow Offer", // .poolUsdBIT = 3, // .poolUsdGH = 273, // .sendMaxUsdBIT{usdBIT(50)}, // .sendUsdGH{usdGH, uint64_t(272'455089820359), -12}, // .failUsdGH = STAmount{0}, // .failUsdGHr = STAmount{0}, // .failUsdBIT{usdBIT, uint64_t(46'47826086956522), -14}, // .failUsdBITr{usdBIT, uint64_t(46'47826086956521), -14}, // .goodUsdGH{usdGH, uint64_t(96'7543114220382), -13}, // .goodUsdGHr{usdGH, uint64_t(96'7543114222965), -13}, // .goodUsdBIT{usdBIT, uint64_t(8'464739069120721), -15}, // .goodUsdBITr{usdBIT, uint64_t(8'464739069098152), -15}, // .lpTokenBalance = {28'61817604250837, -14}, // .lpTokenBalanceAlt = IOUAmount{28'61817604250836, -14}, // .offer1BtcGH = 0.1, // .offer2BtcGH = 0.1, // .offer2UsdGH = 1, // .rateBIT = 1.15, // .rateGH = 1.2, // }, InputSet{ .testCase = "Overflow test {1, 100, 0.111}", // .poolUsdBIT = 1, // .poolUsdGH = 100, // .sendMaxUsdBIT{usdBIT(0.111)}, // .sendUsdGH{usdGH, 100}, // .failUsdGH = STAmount{0}, // .failUsdGHr = STAmount{0}, // .failUsdBIT{usdBIT, uint64_t(1'111), -3}, // .failUsdBITr{usdBIT, uint64_t(1'111), -3}, // .goodUsdGH{usdGH, uint64_t(90'04347888284115), -14}, // .goodUsdGHr{usdGH, uint64_t(90'04347888284201), -14}, // .goodUsdBIT{usdBIT, uint64_t(1'111), -3}, // .goodUsdBITr{usdBIT, uint64_t(1'111), -3}, // .lpTokenBalance{10, 0}, // .offer1BtcGH = 1e-5, // .offer2BtcGH = 1, // .offer2UsdGH = 1e-5, // .rateBIT = 0, // .rateGH = 0, // }, InputSet{ .testCase = "Overflow test {1, 100, 1.00}", // .poolUsdBIT = 1, // .poolUsdGH = 100, // .sendMaxUsdBIT{usdBIT(1.00)}, // .sendUsdGH{usdGH, 100}, // .failUsdGH = STAmount{0}, // .failUsdGHr = STAmount{0}, // .failUsdBIT{usdBIT, uint64_t(2), 0}, // .failUsdBITr{usdBIT, uint64_t(2), 0}, // .goodUsdGH{usdGH, uint64_t(52'94379354424079), -14}, // .goodUsdGHr{usdGH, uint64_t(52'94379354424135), -14}, // .goodUsdBIT{usdBIT, uint64_t(2), 0}, // .goodUsdBITr{usdBIT, uint64_t(2), 0}, // .lpTokenBalance{10, 0}, // .offer1BtcGH = 1e-5, // .offer2BtcGH = 1, // .offer2UsdGH = 1e-5, // .rateBIT = 0, // .rateGH = 0, // }, InputSet{ .testCase = "Overflow test {1, 100, 4.6432}", // .poolUsdBIT = 1, // .poolUsdGH = 100, // .sendMaxUsdBIT{usdBIT(4.6432)}, // .sendUsdGH{usdGH, 100}, // .failUsdGH = STAmount{0}, // .failUsdGHr = STAmount{0}, // .failUsdBIT{usdBIT, uint64_t(5'6432), -4}, // .failUsdBITr{usdBIT, uint64_t(5'6432), -4}, // .goodUsdGH{usdGH, uint64_t(35'44113971506987), -14}, // .goodUsdGHr{usdGH, uint64_t(35'44113971506987), -14}, // .goodUsdBIT{usdBIT, uint64_t(2'821579689703915), -15}, // .goodUsdBITr{usdBIT, uint64_t(2'821579689703954), -15}, // .lpTokenBalance{10, 0}, // .offer1BtcGH = 1e-5, // .offer2BtcGH = 1, // .offer2UsdGH = 1e-5, // .rateBIT = 0, // .rateGH = 0, // }, InputSet{ .testCase = "Overflow test {1, 100, 10}", // .poolUsdBIT = 1, // .poolUsdGH = 100, // .sendMaxUsdBIT{usdBIT(10)}, // .sendUsdGH{usdGH, 100}, // .failUsdGH = STAmount{0}, // .failUsdGHr = STAmount{0}, // .failUsdBIT{usdBIT, uint64_t(11), 0}, // .failUsdBITr{usdBIT, uint64_t(11), 0}, // .goodUsdGH{usdGH, uint64_t(35'44113971506987), -14}, // .goodUsdGHr{usdGH, uint64_t(35'44113971506987), -14}, // .goodUsdBIT{usdBIT, uint64_t(2'821579689703915), -15}, // .goodUsdBITr{usdBIT, uint64_t(2'821579689703954), -15}, // .lpTokenBalance{10, 0}, // .offer1BtcGH = 1e-5, // .offer2BtcGH = 1, // .offer2UsdGH = 1e-5, // .rateBIT = 0, // .rateGH = 0, // }, InputSet{ .testCase = "Overflow test {50, 100, 5.55}", // .poolUsdBIT = 50, // .poolUsdGH = 100, // .sendMaxUsdBIT{usdBIT(5.55)}, // .sendUsdGH{usdGH, 100}, // .failUsdGH = STAmount{0}, // .failUsdGHr = STAmount{0}, // .failUsdBIT{usdBIT, uint64_t(55'55), -2}, // .failUsdBITr{usdBIT, uint64_t(55'55), -2}, // .goodUsdGH{usdGH, uint64_t(90'04347888284113), -14}, // .goodUsdGHr{usdGH, uint64_t(90'0434788828413), -13}, // .goodUsdBIT{usdBIT, uint64_t(55'55), -2}, // .goodUsdBITr{usdBIT, uint64_t(55'55), -2}, // .lpTokenBalance{uint64_t(70'71067811865475), -14}, // .offer1BtcGH = 1e-5, // .offer2BtcGH = 1, // .offer2UsdGH = 1e-5, // .rateBIT = 0, // .rateGH = 0, // }, InputSet{ .testCase = "Overflow test {50, 100, 50.00}", // .poolUsdBIT = 50, // .poolUsdGH = 100, // .sendMaxUsdBIT{usdBIT(50.00)}, // .sendUsdGH{usdGH, 100}, // .failUsdGH{usdGH, uint64_t(52'94379354424081), -14}, // .failUsdGHr{usdGH, uint64_t(52'94379354424092), -14}, // .failUsdBIT{usdBIT, uint64_t(100), 0}, // .failUsdBITr{usdBIT, uint64_t(100), 0}, // .goodUsdGH{usdGH, uint64_t(52'94379354424081), -14}, // .goodUsdGHr{usdGH, uint64_t(52'94379354424092), -14}, // .goodUsdBIT{usdBIT, uint64_t(100), 0}, // .goodUsdBITr{usdBIT, uint64_t(100), 0}, // .lpTokenBalance{uint64_t(70'71067811865475), -14}, // .offer1BtcGH = 1e-5, // .offer2BtcGH = 1, // .offer2UsdGH = 1e-5, // .rateBIT = 0, // .rateGH = 0, // }, InputSet{ .testCase = "Overflow test {50, 100, 232.16}", // .poolUsdBIT = 50, // .poolUsdGH = 100, // .sendMaxUsdBIT{usdBIT(232.16)}, // .sendUsdGH{usdGH, 100}, // .failUsdGH = STAmount{0}, // .failUsdGHr = STAmount{0}, // .failUsdBIT{usdBIT, uint64_t(282'16), -2}, // .failUsdBITr{usdBIT, uint64_t(282'16), -2}, // .goodUsdGH{usdGH, uint64_t(35'44113971506987), -14}, // .goodUsdGHr{usdGH, uint64_t(35'44113971506987), -14}, // .goodUsdBIT{usdBIT, uint64_t(141'0789844851958), -13}, // .goodUsdBITr{usdBIT, uint64_t(141'0789844851962), -13}, // .lpTokenBalance{70'71067811865475, -14}, // .offer1BtcGH = 1e-5, // .offer2BtcGH = 1, // .offer2UsdGH = 1e-5, // .rateBIT = 0, // .rateGH = 0, // }, InputSet{ .testCase = "Overflow test {50, 100, 500}", // .poolUsdBIT = 50, // .poolUsdGH = 100, // .sendMaxUsdBIT{usdBIT(500)}, // .sendUsdGH{usdGH, 100}, // .failUsdGH = STAmount{0}, // .failUsdGHr = STAmount{0}, // .failUsdBIT{usdBIT, uint64_t(550), 0}, // .failUsdBITr{usdBIT, uint64_t(550), 0}, // .goodUsdGH{usdGH, uint64_t(35'44113971506987), -14}, // .goodUsdGHr{usdGH, uint64_t(35'44113971506987), -14}, // .goodUsdBIT{usdBIT, uint64_t(141'0789844851958), -13}, // .goodUsdBITr{usdBIT, uint64_t(141'0789844851962), -13}, // .lpTokenBalance{70'71067811865475, -14}, // .offer1BtcGH = 1e-5, // .offer2BtcGH = 1, // .offer2UsdGH = 1e-5, // .rateBIT = 0, // .rateGH = 0, // }, }) { testcase(input.testCase); for (auto const& features : {all - fixAMMOverflowOffer - fixAMMv1_1 - fixAMMv1_3, all}) { Env env(*this, features, std::make_unique(&logs)); env.fund(XRP(5'000), gatehub, bitstamp, trader); env.close(); if (input.rateGH != 0.0) env(rate(gatehub, input.rateGH)); if (input.rateBIT != 0.0) env(rate(bitstamp, input.rateBIT)); env(trust(trader, usdGH(10'000'000))); env(trust(trader, usdBIT(10'000'000))); env(trust(trader, btcGH(10'000'000))); env.close(); env(pay(gatehub, trader, usdGH(100'000))); env(pay(gatehub, trader, btcGH(100'000))); env(pay(bitstamp, trader, usdBIT(100'000))); env.close(); AMM const amm{env, trader, usdGH(input.poolUsdGH), usdBIT(input.poolUsdBIT)}; env.close(); IOUAmount const preSwapLPTokenBalance = amm.getLPTokensBalance(); env(offer(trader, usdBIT(1), btcGH(input.offer1BtcGH))); env(offer(trader, btcGH(input.offer2BtcGH), usdGH(input.offer2UsdGH))); env.close(); env(pay(trader, trader, input.sendUsdGH), path(~usdGH), path(~btcGH, ~usdGH), sendmax(input.sendMaxUsdBIT), txflags(tfPartialPayment)); env.close(); auto const failUsdGH = features[fixAMMv1_1] ? input.failUsdGHr : input.failUsdGH; auto const failUsdBIT = features[fixAMMv1_1] ? input.failUsdBITr : input.failUsdBIT; auto const goodUsdGH = features[fixAMMv1_1] ? input.goodUsdGHr : input.goodUsdGH; auto const goodUsdBIT = features[fixAMMv1_1] ? input.goodUsdBITr : input.goodUsdBIT; auto const lpTokenBalance = [&] { if (not env.enabled(fixAMMv1_3)) return input.lpTokenBalance; return input.lpTokenBalanceAlt.value_or(input.lpTokenBalance); }(); if (!features[fixAMMOverflowOffer]) { BEAST_EXPECT(amm.expectBalances(failUsdGH, failUsdBIT, lpTokenBalance)); } else { BEAST_EXPECT(amm.expectBalances(goodUsdGH, goodUsdBIT, lpTokenBalance)); // Invariant: LPToken balance must not change in a // payment or a swap transaction BEAST_EXPECT(amm.getLPTokensBalance() == preSwapLPTokenBalance); // Invariant: The square root of (product of the pool // balances) must be at least the LPTokenBalance Number const sqrtPoolProduct = root2(goodUsdGH * goodUsdBIT); // Include a tiny tolerance for the test cases using // .goodUsdGH{usdGH, uint64_t(35'44113971506987), // -14}, .goodUsdBIT{usdBIT, // uint64_t(2'821579689703915), -15}, // These two values multiply // to 99.99999999999994227040383754105 which gets // internally rounded to 100, due to representation // error. BEAST_EXPECT((sqrtPoolProduct + Number{1, -14} >= input.lpTokenBalance)); } } } } void testSwapRounding() { testcase("swapRounding"); using namespace jtx; STAmount const xrpPool{XRP, UINT64_C(51600'000981)}; STAmount const iouPool{USD, UINT64_C(803040'9987141784), -10}; STAmount const xrpBob{XRP, UINT64_C(1092'878933)}; STAmount const iouBob{USD, UINT64_C(3'988035892323031), -28}; // 3.9...e-13 testAMM( [&](AMM& amm, Env& env) { // Check our AMM starting conditions. auto [xrpBegin, iouBegin, lptBegin] = amm.balances(XRP, USD); // Set Bob's starting conditions. env.fund(xrpBob, bob); env.trust(USD(1'000'000), bob); env(pay(gw, bob, iouBob)); env.close(); env(offer(bob, XRP(6300), USD(100'000))); env.close(); // Assert that AMM is unchanged. BEAST_EXPECT(amm.expectBalances(xrpBegin, iouBegin, amm.tokens())); }, {{xrpPool, iouPool}}, 889, std::nullopt, {testable_amendments() | fixAMMv1_1}); } void testFixAMMOfferBlockedByLOB(FeatureBitset features) { testcase("AMM Offer Blocked By LOB"); using namespace jtx; // Low quality LOB offer blocks AMM liquidity // USD/XRP crosses AMM { Env env(*this, features); fund(env, gw, {alice, carol}, XRP(1'000'000), {USD(1'000'000)}); // This offer blocks AMM offer in pre-amendment env(offer(alice, XRP(1), USD(0.01))); env.close(); AMM const amm(env, gw, XRP(200'000), USD(100'000)); // The offer doesn't cross AMM in pre-amendment code // It crosses AMM in post-amendment code env(offer(carol, USD(0.49), XRP(1))); env.close(); if (!features[fixAMMv1_1]) { BEAST_EXPECT(amm.expectBalances(XRP(200'000), USD(100'000), amm.tokens())); BEAST_EXPECT(expectOffers(env, alice, 1, {{Amounts{XRP(1), USD(0.01)}}})); // Carol's offer is blocked by alice's offer BEAST_EXPECT(expectOffers(env, carol, 1, {{Amounts{USD(0.49), XRP(1)}}})); } else { BEAST_EXPECT( amm.expectBalances(XRPAmount(200'000'980'005), USD(99'999.51), amm.tokens())); BEAST_EXPECT(expectOffers(env, alice, 1, {{Amounts{XRP(1), USD(0.01)}}})); // Carol's offer crosses AMM BEAST_EXPECT(expectOffers(env, carol, 0)); } } // There is no blocking offer, the same AMM liquidity is consumed // pre- and post-amendment. { Env env(*this, features); fund(env, gw, {alice, carol}, XRP(1'000'000), {USD(1'000'000)}); // There is no blocking offer // env(offer(alice, XRP(1), USD(0.01))); AMM const amm(env, gw, XRP(200'000), USD(100'000)); // The offer crosses AMM env(offer(carol, USD(0.49), XRP(1))); env.close(); // The same result as with the blocking offer BEAST_EXPECT( amm.expectBalances(XRPAmount(200'000'980'005), USD(99'999.51), amm.tokens())); // Carol's offer crosses AMM BEAST_EXPECT(expectOffers(env, carol, 0)); } // XRP/USD crosses AMM { Env env(*this, features); fund(env, gw, {alice, carol, bob}, XRP(10'000), {USD(1'000)}); // This offer blocks AMM offer in pre-amendment // It crosses AMM in post-amendment code env(offer(bob, USD(1), XRPAmount(500))); env.close(); AMM const amm(env, alice, XRP(1'000), USD(500)); env(offer(carol, XRP(100), USD(55))); env.close(); if (!features[fixAMMv1_1]) { BEAST_EXPECT(amm.expectBalances(XRP(1'000), USD(500), amm.tokens())); BEAST_EXPECT(expectOffers(env, bob, 1, {{Amounts{USD(1), XRPAmount(500)}}})); BEAST_EXPECT(expectOffers(env, carol, 1, {{Amounts{XRP(100), USD(55)}}})); } else { BEAST_EXPECT(amm.expectBalances( XRPAmount(909'090'909), STAmount{USD, UINT64_C(550'000000055), -9}, amm.tokens())); BEAST_EXPECT(expectOffers( env, carol, 1, {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}})); BEAST_EXPECT(expectOffers(env, bob, 1, {{Amounts{USD(1), XRPAmount(500)}}})); } } // There is no blocking offer, the same AMM liquidity is consumed // pre- and post-amendment. { Env env(*this, features); fund(env, gw, {alice, carol, bob}, XRP(10'000), {USD(1'000)}); AMM const amm(env, alice, XRP(1'000), USD(500)); env(offer(carol, XRP(100), USD(55))); env.close(); BEAST_EXPECT(amm.expectBalances( XRPAmount(909'090'909), STAmount{USD, UINT64_C(550'000000055), -9}, amm.tokens())); BEAST_EXPECT(expectOffers( env, carol, 1, {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}})); } } void testLPTokenBalance(FeatureBitset features) { testcase("LPToken Balance"); using namespace jtx; // Last Liquidity Provider is the issuer of one token { std::string logs; Env env(*this, features, std::make_unique(&logs)); fund(env, gw, {alice, carol}, XRP(1'000'000'000), {USD(1'000'000'000)}); AMM amm(env, gw, XRP(2), USD(1)); amm.deposit(alice, IOUAmount{1'876123487565916, -15}); amm.deposit(carol, IOUAmount{1'000'000}); amm.withdrawAll(alice); BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{0})); amm.withdrawAll(carol); BEAST_EXPECT(amm.expectLPTokens(carol, IOUAmount{0})); auto const lpToken = getAccountLines(env, gw, amm.lptIssue())[jss::lines][0u][jss::balance]; auto const lpTokenBalance = amm.ammRpcInfo()[jss::amm][jss::lp_token][jss::value]; BEAST_EXPECT(lpToken == "1414.213562373095" && lpTokenBalance == "1414.213562373"); if (!features[fixAMMv1_1]) { amm.withdrawAll(gw, std::nullopt, ter(tecAMM_BALANCE)); BEAST_EXPECT(amm.ammExists()); } else { amm.withdrawAll(gw); BEAST_EXPECT(!amm.ammExists()); } } // Last Liquidity Provider is the issuer of two tokens, or not // the issuer for (auto const& lp : {gw, bob}) { Env env(*this, features); auto const ABC = gw["ABC"]; fund( env, gw, {alice, carol, bob}, XRP(1'000), {USD(1'000'000'000), ABC(1'000'000'000'000)}); AMM amm(env, lp, ABC(2'000'000), USD(1)); amm.deposit(alice, IOUAmount{1'876123487565916, -15}); amm.deposit(carol, IOUAmount{1'000'000}); amm.withdrawAll(alice); amm.withdrawAll(carol); auto const lpToken = getAccountLines(env, lp, amm.lptIssue())[jss::lines][0u][jss::balance]; auto const lpTokenBalance = amm.ammRpcInfo()[jss::amm][jss::lp_token][jss::value]; BEAST_EXPECT(lpToken == "1414.213562373095" && lpTokenBalance == "1414.213562373"); if (!features[fixAMMv1_1]) { amm.withdrawAll(lp, std::nullopt, ter(tecAMM_BALANCE)); BEAST_EXPECT(amm.ammExists()); } else { amm.withdrawAll(lp); BEAST_EXPECT(!amm.ammExists()); } } // More than one Liquidity Provider // XRP/IOU { Env env(*this, features); fund(env, gw, {alice}, XRP(1'000), {USD(1'000)}); AMM amm(env, gw, XRP(10), USD(10)); amm.deposit(alice, 1'000); auto res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), gw); BEAST_EXPECT(res && !res.value()); res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice); BEAST_EXPECT(res && !res.value()); } // IOU/IOU, issuer of both IOU { Env env(*this, features); fund(env, gw, {alice}, XRP(1'000), {USD(1'000), EUR(1'000)}); AMM amm(env, gw, EUR(10), USD(10)); amm.deposit(alice, 1'000); auto res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), gw); BEAST_EXPECT(res && !res.value()); res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice); BEAST_EXPECT(res && !res.value()); } // IOU/IOU, issuer of one IOU { Env env(*this, features); Account const gw1("gw1"); auto const YAN = gw1["YAN"]; fund(env, gw, {gw1}, XRP(1'000), {USD(1'000)}); fund(env, gw1, {gw}, XRP(1'000), {YAN(1'000)}, Fund::TokenOnly); AMM amm(env, gw1, YAN(10), USD(10)); amm.deposit(gw, 1'000); auto res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), gw); BEAST_EXPECT(res && !res.value()); res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), gw1); BEAST_EXPECT(res && !res.value()); } } 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 const amm(env, gw, XRP(100), USD(100), ter(tecNO_PERMISSION)); AMM const amm1(env, alice, USD(100), XRP(100), ter(tecNO_PERMISSION)); env(fclear(gw, asfAllowTrustLineClawback)); env.close(); // Can't be cleared AMM const 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 or tecPSEUDO_ACCOUNT, // depending on whether SingleAssetVault is enabled. We can only use // AMMClawback transaction to claw back from AMM Account. else { AMM const amm(env, gw, XRP(100), USD(100), ter(tesSUCCESS)); AMM const 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 // 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. auto const error = features[featureSingleAssetVault] ? ter{tecPSEUDO_ACCOUNT} : ter{tecAMM_ACCOUNT}; Issue const usd(USD.issue().currency, amm.ammAccount()); auto amount = amountFromString(usd, "10"); env(claw(gw, amount), error); } } 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 testFixReserveCheckOnWithdrawal(FeatureBitset features) { testcase("Fix Reserve Check On Withdrawal"); using namespace jtx; auto const err = features[fixAMMv1_2] ? ter(tecINSUFFICIENT_RESERVE) : ter(tesSUCCESS); auto test = [&](auto&& cb) { Env env(*this, features); auto const starting_xrp = reserve(env, 2) + env.current()->fees().base * 5; env.fund(starting_xrp, gw); env.fund(starting_xrp, alice); env.trust(USD(2'000), alice); env.close(); env(pay(gw, alice, USD(2'000))); env.close(); AMM amm(env, gw, EUR(1'000), USD(1'000)); amm.deposit(alice, USD(1)); cb(amm); }; // Equal withdraw test([&](AMM& amm) { amm.withdrawAll(alice, std::nullopt, err); }); // Equal withdraw with a limit test([&](AMM& amm) { amm.withdraw( WithdrawArg{ .account = alice, .asset1Out = EUR(0.1), .asset2Out = USD(0.1), .err = err}); amm.withdraw( WithdrawArg{ .account = alice, .asset1Out = USD(0.1), .asset2Out = EUR(0.1), .err = err}); }); // Single withdraw test([&](AMM& amm) { amm.withdraw(WithdrawArg{.account = alice, .asset1Out = EUR(0.1), .err = err}); amm.withdraw(WithdrawArg{.account = alice, .asset1Out = USD(0.1)}); }); } void testFailedPseudoAccount() { using namespace test::jtx; auto const testCase = [&](std::string suffix, FeatureBitset features) { testcase("Fail pseudo-account allocation " + suffix); std::string logs; Env env{*this, features, std::make_unique(&logs)}; env.fund(XRP(30'000), gw, alice); env.close(); env(trust(alice, gw["USD"](30'000), 0)); env(pay(gw, alice, USD(10'000))); env.close(); STAmount const amount = XRP(10'000); STAmount const amount2 = USD(10'000); auto const keylet = keylet::amm(amount.asset(), amount2.asset()); for (int i = 0; i < 256; ++i) { AccountID const accountId = xrpl::pseudoAccountAddress(*env.current(), keylet.key); env(pay(env.master.id(), accountId, XRP(1000)), seq(autofill), fee(autofill), sig(autofill)); } AMM const ammAlice( env, alice, amount, amount2, features[featureSingleAssetVault] ? ter{terADDRESS_COLLISION} : ter{tecDUPLICATE}); }; testCase("tecDUPLICATE", testable_amendments() - featureSingleAssetVault); testCase("terADDRESS_COLLISION", testable_amendments() | featureSingleAssetVault); } void testDepositAndWithdrawRounding(FeatureBitset features) { testcase("Deposit and Withdraw Rounding V2"); using namespace jtx; auto const XPM = gw["XPM"]; STAmount xrpBalance{XRPAmount(692'614'492'126)}; STAmount xpmBalance{XPM, UINT64_C(18'610'359'80246901), -8}; STAmount amount{XPM, UINT64_C(6'566'496939465400), -12}; std::uint16_t const tfee = 941; auto test = [&](auto&& cb, std::uint16_t tfee_) { Env env(*this, features); env.fund(XRP(1'000'000), gw); env.fund(XRP(1'000), alice); env(trust(alice, XPM(7'000))); env(pay(gw, alice, amount)); AMM amm(env, gw, xrpBalance, xpmBalance, CreateArg{.tfee = tfee_}); // AMM LPToken balance required to replicate single deposit failure STAmount const lptAMMBalance{amm.lptIssue(), UINT64_C(3'234'987'266'485968), -6}; auto const burn = IOUAmount{amm.getLPTokensBalance() - lptAMMBalance}; // burn tokens to get to the required AMM state env(amm.bid(BidArg{.account = gw, .bidMin = burn, .bidMax = burn})); cb(amm, env); }; test( [&](AMM& amm, Env& env) { auto const err = env.enabled(fixAMMv1_3) ? ter(tesSUCCESS) : ter(tecUNFUNDED_AMM); amm.deposit(DepositArg{.account = alice, .asset1In = amount, .err = err}); }, tfee); test( [&](AMM& amm, Env& env) { auto const [amount, amount2, lptAMM] = amm.balances(XRP, XPM); auto const withdraw = STAmount{XPM, 1, -5}; amm.withdraw(WithdrawArg{.asset1Out = STAmount{XPM, 1, -5}}); auto const [amount_, amount2_, lptAMM_] = amm.balances(XRP, XPM); if (!env.enabled(fixAMMv1_3)) { BEAST_EXPECT((amount2 - amount2_) > withdraw); } else { BEAST_EXPECT((amount2 - amount2_) <= withdraw); } }, 0); } void invariant(jtx::AMM& amm, jtx::Env& env, std::string const& msg, bool shouldFail) { auto const [amount, amount2, lptBalance] = amm.balances(GBP, EUR); NumberMantissaScaleGuard const sg(MantissaRange::small); NumberRoundModeGuard const g(env.enabled(fixAMMv1_3) ? Number::upward : Number::getround()); auto const res = root2(amount * amount2); if (shouldFail) { BEAST_EXPECT(res < lptBalance); } else { BEAST_EXPECT(res >= lptBalance); } } void testDepositRounding(FeatureBitset all) { testcase("Deposit Rounding"); using namespace jtx; // Single asset deposit for (auto const& deposit : {STAmount(EUR, 1, 1), STAmount(EUR, 1, 2), STAmount(EUR, 1, 5), STAmount(EUR, 1, -3), // fail STAmount(EUR, 1, -6), STAmount(EUR, 1, -9)}) { testAMM( [&](AMM& ammAlice, Env& env) { fund(env, gw, {bob}, XRP(10'000'000), {GBP(100'000), EUR(100'000)}, Fund::Acct); env.close(); ammAlice.deposit(DepositArg{.account = bob, .asset1In = deposit}); invariant( ammAlice, env, "dep1", deposit == STAmount{EUR, 1, -3} && !env.enabled(fixAMMv1_3)); }, {{GBP(30'000), EUR(30'000)}}, 0, std::nullopt, {all}); } // Two-asset proportional deposit (1:1 pool ratio) testAMM( [&](AMM& ammAlice, Env& env) { fund(env, gw, {bob}, XRP(10'000'000), {GBP(100'000), EUR(100'000)}, Fund::Acct); env.close(); STAmount const depositEuro{EUR, UINT64_C(10'1234567890123456), -16}; STAmount const depositGBP{GBP, UINT64_C(10'1234567890123456), -16}; ammAlice.deposit( DepositArg{.account = bob, .asset1In = depositEuro, .asset2In = depositGBP}); invariant(ammAlice, env, "dep2", false); }, {{GBP(30'000), EUR(30'000)}}, 0, std::nullopt, {all}); // Two-asset proportional deposit (1:3 pool ratio) for (auto const& exponent : {1, 2, 3, 4, -3 /*fail*/, -6, -9}) { testAMM( [&](AMM& ammAlice, Env& env) { fund(env, gw, {bob}, XRP(10'000'000), {GBP(100'000), EUR(100'000)}, Fund::Acct); env.close(); STAmount const depositEuro{EUR, 1, exponent}; STAmount const depositGBP{GBP, 1, exponent}; ammAlice.deposit( DepositArg{ .account = bob, .asset1In = depositEuro, .asset2In = depositGBP}); invariant(ammAlice, env, "dep3", exponent != -3 && !env.enabled(fixAMMv1_3)); }, {{GBP(10'000), EUR(30'000)}}, 0, std::nullopt, {all}); } // tfLPToken deposit testAMM( [&](AMM& ammAlice, Env& env) { fund(env, gw, {bob}, XRP(10'000'000), {GBP(100'000), EUR(100'000)}, Fund::Acct); env.close(); ammAlice.deposit( DepositArg{.account = bob, .tokens = IOUAmount{10'1234567890123456, -16}}); invariant(ammAlice, env, "dep4", false); }, {{GBP(7'000), EUR(30'000)}}, 0, std::nullopt, {all}); // tfOneAssetLPToken deposit for (auto const& tokens : {IOUAmount{1, -3}, IOUAmount{1, -2}, IOUAmount{1, -1}, IOUAmount{1}, IOUAmount{10}, IOUAmount{100}, IOUAmount{1'000}, IOUAmount{10'000}}) { testAMM( [&](AMM& ammAlice, Env& env) { fund( env, gw, {bob}, XRP(10'000'000), {GBP(100'000), EUR(1'000'000)}, Fund::Acct); env.close(); ammAlice.deposit( DepositArg{ .account = bob, .tokens = tokens, .asset1In = STAmount{EUR, 1, 6}}); invariant(ammAlice, env, "dep5", false); }, {{GBP(7'000), EUR(30'000)}}, 0, std::nullopt, {all}); } // Single deposit with EP not exceeding specified: // 1'000 GBP with EP not to exceed 5 (GBP/TokensOut) testAMM( [&](AMM& ammAlice, Env& env) { fund(env, gw, {bob}, XRP(10'000'000), {GBP(100'000), EUR(100'000)}, Fund::Acct); env.close(); ammAlice.deposit(bob, GBP(1'000), std::nullopt, STAmount{GBP, 5}); invariant(ammAlice, env, "dep6", false); }, {{GBP(30'000), EUR(30'000)}}, 0, std::nullopt, {all}); } void testWithdrawRounding(FeatureBitset all) { testcase("Withdraw Rounding"); using namespace jtx; // tfLPToken mode testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.withdraw(alice, 1'000); invariant(ammAlice, env, "with1", false); }, {{GBP(7'000), EUR(30'000)}}, 0, std::nullopt, {all}); // tfWithdrawAll mode testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.withdraw(WithdrawArg{.account = alice, .flags = tfWithdrawAll}); invariant(ammAlice, env, "with2", false); }, {{GBP(7'000), EUR(30'000)}}, 0, std::nullopt, {all}); // tfTwoAsset withdraw mode testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.withdraw( WithdrawArg{ .account = alice, .asset1Out = STAmount{GBP, 3'500}, .asset2Out = STAmount{EUR, 15'000}, .flags = tfTwoAsset}); invariant(ammAlice, env, "with3", false); }, {{GBP(7'000), EUR(30'000)}}, 0, std::nullopt, {all}); // tfSingleAsset withdraw mode // Note: This test fails with 0 trading fees, but doesn't fail if // trading fees is set to 1'000 -- I suspect the compound operations // in AMMHelpers.cpp:ammAssetOut compensate for the rounding // errors testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.withdraw( WithdrawArg{ .account = alice, .asset1Out = STAmount{GBP, 1'234}, .flags = tfSingleAsset}); invariant(ammAlice, env, "with4", false); }, {{GBP(7'000), EUR(30'000)}}, 0, std::nullopt, {all}); // tfOneAssetWithdrawAll mode testAMM( [&](AMM& ammAlice, Env& env) { fund(env, gw, {bob}, XRP(10'000'000), {GBP(100'000), EUR(100'000)}, Fund::Acct); env.close(); ammAlice.deposit(DepositArg{.account = bob, .asset1In = STAmount{GBP, 3'456}}); ammAlice.withdraw( WithdrawArg{ .account = bob, .asset1Out = STAmount{GBP, 1'000}, .flags = tfOneAssetWithdrawAll}); invariant(ammAlice, env, "with5", false); }, {{GBP(7'000), EUR(30'000)}}, 0, std::nullopt, {all}); // tfOneAssetLPToken mode testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.withdraw( WithdrawArg{ .account = alice, .tokens = 1'000, .asset1Out = STAmount{GBP, 100}, .flags = tfOneAssetLPToken}); invariant(ammAlice, env, "with6", false); }, {{GBP(7'000), EUR(30'000)}}, 0, std::nullopt, {all}); // tfLimitLPToken mode testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.withdraw( WithdrawArg{ .account = alice, .asset1Out = STAmount{GBP, 100}, .maxEP = IOUAmount{2}, .flags = tfLimitLPToken}); invariant(ammAlice, env, "with7", true); }, {{GBP(7'000), EUR(30'000)}}, 0, std::nullopt, {all}); } void run() override { FeatureBitset const all{testable_amendments()}; testInvalidInstance(); testInstanceCreate(); testInvalidDeposit(all); testInvalidDeposit(all - featureAMMClawback); testDeposit(); testInvalidWithdraw(); testWithdraw(); testInvalidFeeVote(); testFeeVote(); testInvalidBid(); testBid(all); testBid(all - fixAMMv1_3); testBid(all - fixAMMv1_1 - fixAMMv1_3); testInvalidAMMPayment(); testBasicPaymentEngine(all); testBasicPaymentEngine(all - fixAMMv1_1 - fixAMMv1_3); testBasicPaymentEngine(all - fixReducedOffersV2); testBasicPaymentEngine(all - fixAMMv1_1 - fixAMMv1_3 - fixReducedOffersV2); testAMMTokens(); testAmendment(); testFlags(); testRippling(); testAMMAndCLOB(all); testAMMAndCLOB(all - fixAMMv1_1 - fixAMMv1_3); testTradingFee(all); testTradingFee(all - fixAMMv1_3); testTradingFee(all - fixAMMv1_1 - fixAMMv1_3); testAdjustedTokens(all); testAdjustedTokens(all - fixAMMv1_3); testAdjustedTokens(all - fixAMMv1_1 - fixAMMv1_3); testAutoDelete(); testClawback(); testAMMID(); testSelection(all); testSelection(all - fixAMMv1_1 - fixAMMv1_3); testFixDefaultInnerObj(); testMalformed(); testFixOverflowOffer(all); testFixOverflowOffer(all - fixAMMv1_3); testFixOverflowOffer(all - fixAMMv1_1 - fixAMMv1_3); testSwapRounding(); testFixChangeSpotPriceQuality(all); testFixChangeSpotPriceQuality(all - fixAMMv1_1 - fixAMMv1_3); testFixAMMOfferBlockedByLOB(all); testFixAMMOfferBlockedByLOB(all - fixAMMv1_1 - fixAMMv1_3); testLPTokenBalance(all); testLPTokenBalance(all - fixAMMv1_3); testLPTokenBalance(all - fixAMMv1_1 - fixAMMv1_3); testAMMClawback(all); testAMMClawback(all - featureSingleAssetVault); testAMMClawback(all - featureAMMClawback - featureSingleAssetVault); testAMMClawback(all - featureAMMClawback); testAMMClawback(all - fixAMMv1_1 - fixAMMv1_3 - featureAMMClawback); testAMMDepositWithFrozenAssets(all); testAMMDepositWithFrozenAssets(all - featureAMMClawback); testAMMDepositWithFrozenAssets(all - fixAMMv1_1 - featureAMMClawback); testAMMDepositWithFrozenAssets(all - fixAMMv1_1 - fixAMMv1_3 - featureAMMClawback); testFixReserveCheckOnWithdrawal(all); testFixReserveCheckOnWithdrawal(all - fixAMMv1_2); testDepositAndWithdrawRounding(all); testDepositAndWithdrawRounding(all - fixAMMv1_3); testDepositRounding(all); testDepositRounding(all - fixAMMv1_3); testWithdrawRounding(all); testWithdrawRounding(all - fixAMMv1_3); testFailedPseudoAccount(); } }; BEAST_DEFINE_TESTSUITE_PRIO(AMM, app, xrpl, 1); } // namespace xrpl::test