#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 functionality involving MPT assets, excluding those that * use offers. Tests incorporating offers are in `AMMExtended_test`. */ struct AMMMPT_test : public jtx::AMMTest { private: void testInstanceCreate() { testcase("Instance Create"); using namespace jtx; // XRP to MPT testAMM( [&](AMM& ammAlice, Env&) { BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), MPT(ammAlice[1])(10'000), IOUAmount{10'000'000})); }, {{XRP(10'000), gAmmmpt(10'000)}}); // IOU to MPT testAMM( [&](AMM& ammAlice, Env&) { BEAST_EXPECT(ammAlice.expectBalances( USD(20'000), MPT(ammAlice[1])(20'000), IOUAmount{20'000})); }, {{USD(20'000), gAmmmpt(20'000)}}); // MPT to MPT testAMM( [&](AMM& ammAlice, Env&) { BEAST_EXPECT(ammAlice.expectBalances( MPT(ammAlice[0])(20'000), MPT(ammAlice[1])(20'000), IOUAmount{20'000})); }, {{gAmmmpt(20'000), gAmmmpt(20'000)}}); // IOU to MPT + transfer fee { Env env{*this}; fund(env, gw_, {alice_}, {USD(20'000)}, Fund::All); env(rate(gw_, 1.25)); env.close(); MPT const btc = MPTTester( {.env = env, .issuer = gw_, .holders = {alice_}, .transferFee = 1'500, .pay = 30'000}); // no transfer fee on create AMM const ammAlice(env, alice_, USD(20'000), btc(20'000)); BEAST_EXPECT(ammAlice.expectBalances(USD(20'000), btc(20'000), IOUAmount{20'000, 0})); BEAST_EXPECT(expectHolding(env, alice_, USD(0))); // alice initially had 30'000 BEAST_EXPECT(expectMPT(env, alice_, btc(10'000))); } // 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(pay(gw_, alice_, USD(10'000))); env.close(); MPT const btc = MPTTester( {.env = env, .issuer = gw_, .holders = {alice_}, .pay = 30'000, .flags = tfMPTRequireAuth | kMptDexFlags, .authHolder = true}); AMM const ammAlice(env, alice_, USD(10'000), btc(10'000)); } // Cleared global freeze { Env env{*this}; env.fund(XRP(30'000), gw_, alice_); MPTTester usd( {.env = env, .issuer = gw_, .holders = {alice_}, .pay = 30'000, .flags = tfMPTCanLock | kMptDexFlags}); usd.set({.flags = tfMPTLock}); AMM const ammAliceFail(env, alice_, XRP(10'000), usd(10'000), Ter(tecFROZEN)); usd.set({.flags = tfMPTUnlock}); AMM const ammAlice(env, alice_, XRP(10'000), usd(10'000)); } } void testInvalidInstance() { testcase("Invalid Instance"); using namespace jtx; // Can't have both tokens the same MPT { Env env{*this}; env.fund(XRP(30'000), alice_, gw_); env.close(); MPT const btc = MPTTester( {.env = env, .issuer = gw_, .holders = {alice_}, .transferFee = 1'500, .pay = 40'000}); AMM const ammAlice(env, alice_, btc(20'000), btc(10'000), Ter(temBAD_AMM_TOKENS)); } // MPTCanTrade is not set and AMM creator is not the issuer of MPT { Env env{*this}; env.fund(XRP(30'000), alice_, gw_); env.close(); MPT const btc = MPTTester( {.env = env, .issuer = gw_, .holders = {alice_}, .pay = 40'000, .flags = tfMPTCanLock}); AMM const ammAlice(env, alice_, btc(20'000), XRP(10'000), Ter(tecNO_PERMISSION)); } // MPTokenIssuance doesn't exist { Env env{*this}; env.fund(XRP(30'000), alice_, gw_); env.close(); AMM const ammAlice( env, alice_, MPT(gw_, 1'000)(20'000), XRP(10'000), Ter(tecOBJECT_NOT_FOUND)); } // MPToken doesn't exist and amm creator is not the issuer { Env env{*this}; env.fund(XRP(30'000), alice_, gw_); env.close(); MPT const btc = MPTTester({ .env = env, .issuer = gw_, }); AMM const ammAlice(env, alice_, btc(20'000), XRP(10'000), Ter(tecNO_AUTH)); } // Can't have zero or negative amounts { Env env{*this}; fund(env, gw_, {alice_}, {USD(20'000)}, Fund::All); env(rate(gw_, 1.25)); env.close(); MPT const btc = MPTTester( {.env = env, .issuer = gw_, .holders = {alice_}, .transferFee = 1'500, .pay = 30'000}); AMM const ammAlice(env, alice_, XRP(0), btc(10'000), Ter(temBAD_AMOUNT)); BEAST_EXPECT(!ammAlice.ammExists()); AMM const ammAlice1(env, alice_, XRP(10'000), btc(0), Ter(temBAD_AMOUNT)); BEAST_EXPECT(!ammAlice1.ammExists()); AMM const ammAlice2(env, alice_, USD(0), btc(10'000), Ter(temBAD_AMOUNT)); BEAST_EXPECT(!ammAlice2.ammExists()); AMM const ammAlice3(env, alice_, USD(10'000), btc(0), Ter(temBAD_AMOUNT)); BEAST_EXPECT(!ammAlice3.ammExists()); AMM const ammAlice4(env, alice_, XRP(-10'0000), btc(10'000), Ter(temBAD_AMOUNT)); BEAST_EXPECT(!ammAlice4.ammExists()); AMM const ammAlice5(env, alice_, XRP(10'000), btc(-10'000), Ter(temBAD_AMOUNT)); BEAST_EXPECT(!ammAlice5.ammExists()); AMM const ammAlice6(env, alice_, USD(-10'000), btc(10'000), Ter(temBAD_AMOUNT)); BEAST_EXPECT(!ammAlice6.ammExists()); AMM const ammAlice7(env, alice_, USD(10'000), btc(-10'000), Ter(temBAD_AMOUNT)); BEAST_EXPECT(!ammAlice7.ammExists()); } // Bad MPT { Env env{*this}; fund(env, gw_, {alice_}, {USD(20'000)}, Fund::All); AMM const ammAlice(env, alice_, XRP(10'000), MPT(badMPT())(100), Ter(temBAD_MPT)); BEAST_EXPECT(!ammAlice.ammExists()); } // Insufficient MPT balance { Env env{*this}; fund(env, gw_, {alice_}, {USD(30'000)}, Fund::All); MPT const btc = MPTTester( {.env = env, .issuer = gw_, .holders = {alice_}, .transferFee = 1'500, .pay = 30'000}); AMM const ammAlice(env, alice_, XRP(10'000), btc(40'000), Ter(tecUNFUNDED_AMM)); BEAST_EXPECT(!ammAlice.ammExists()); } // Invalid trading fee { Env env{*this}; fund(env, gw_, {alice_}, {USD(20'000)}, Fund::All); MPT const btc = MPTTester( {.env = env, .issuer = gw_, .holders = {alice_}, .transferFee = 1'500, .pay = 30'000}); AMM const ammAlice( env, alice_, USD(10'000), btc(10'000), false, 65'001, 10, std::nullopt, std::nullopt, std::nullopt, Ter(temBAD_FEE)); BEAST_EXPECT(!ammAlice.ammExists()); } // AMM already exists XRP/MPT testAMM( [&](AMM& ammAlice, Env& env) { // Account bob1("bob1"); env.fund(XRP(30'000), bob_); env.close(); AMM const ammBob(env, bob_, XRP(1'000), MPT(ammAlice[1])(1'000), Ter(tecDUPLICATE)); }, {{XRP(10'000), gAmmmpt(10'000)}}); // AMM already exists IOU/MPT testAMM( [&](AMM& ammAlice, Env& env) { env.fund(XRP(30'000), bob_); env.close(); env.trust(USD(10000), bob_); env(pay(gw_, bob_, USD(100))); AMM const ammBob(env, bob_, USD(1'000), MPT(ammAlice[1])(1'000), Ter(tecDUPLICATE)); }, {{USD(10'000), gAmmmpt(10'000)}}); // AMM already exists MPT/MPT testAMM( [&](AMM& ammAlice, Env& env) { AMM const ammCarol( env, carol_, MPT(ammAlice[0])(1'000), MPT(ammAlice[1])(2'000), Ter(tecDUPLICATE)); }, {{gAmmmpt(20'000), gAmmmpt(10'000)}}); // MPTRequireAuth flag is set and AMM creator is not 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(); MPT const btc = MPTTester( {.env = env, .issuer = gw_, .holders = {alice_}, .flags = tfMPTRequireAuth}); AMM const ammAlice(env, alice_, USD(10'000), btc(10'000), Ter(tecNO_AUTH)); BEAST_EXPECT(!ammAlice.ammExists()); } // MPTLocked flag is set { Env env{*this}; fund(env, gw_, {alice_}, {USD(20'000)}, Fund::All); MPTTester btc( {.env = env, .issuer = gw_, .holders = {alice_}, .pay = 30'000, .flags = tfMPTCanLock | kMptDexFlags}); btc.set({.flags = tfMPTLock}); AMM const ammAlice(env, alice_, USD(10'000), btc(10'000), Ter(tecFROZEN)); BEAST_EXPECT(!ammAlice.ammExists()); } // MPT individually locked { Env env{*this}; fund(env, gw_, {alice_, bob_}, {USD(20'000)}, Fund::All); MPTTester btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .pay = 30'000, .flags = tfMPTCanLock | kMptDexFlags}); btc.set({.holder = alice_, .flags = tfMPTLock}); // alice's token is locked AMM const ammAlice(env, alice_, USD(10'000), btc(10'000), Ter(tecFROZEN)); BEAST_EXPECT(!ammAlice.ammExists()); // bob can create AMM const ammBob(env, bob_, USD(10'000), btc(10'000)); BEAST_EXPECT(ammBob.ammExists()); } // OutstandingAmount > MaximumAmount { Env env{*this}; env.fund(XRP(1'000), gw_, alice_); MPTTester const btc({.env = env, .issuer = gw_, .holders = {alice_}, .maxAmt = 100}); // OutstandingAmount is 0, issuer issues 10 over MaximumAmount AMM const amm(env, gw_, XRP(100), btc(110), Ter(tecUNFUNDED_AMM)); env(pay(gw_, alice_, btc(100))); // OutstandingAmount is 100, issuer issues 100 over MaximumAmount AMM const amm1(env, gw_, XRP(100), btc(100), Ter(tecUNFUNDED_AMM)); // This is fine - alice transfers 100 to AMM. OutstandingAmount // is 100. AMM const ammAlice(env, alice_, XRP(100), btc(100)); } } void testInvalidDeposit(FeatureBitset features) { testcase("Invalid Deposit"); using namespace jtx; testAMM( [&](AMM& ammAlice, Env& env) { // LPTokenOut can not be zero ammAlice.deposit(alice_, 0, std::nullopt, std::nullopt, Ter(temBAD_AMM_TOKENS)); // LPTokenOut can not be negative ammAlice.deposit( alice_, IOUAmount{-1}, std::nullopt, std::nullopt, Ter(temBAD_AMM_TOKENS)); // LPTokenOut can not be MPT { json::Value jv = json::ValueType::Object; jv[jss::Account] = alice_.human(); jv[jss::TransactionType] = jss::AMMDeposit; jv[jss::Asset] = STIssue(sfAsset, XRP).getJson(JsonOptions::Values::None); jv[jss::Asset2] = STIssue(sfAsset, MPT(ammAlice[1])).getJson(JsonOptions::Values::None); jv[jss::LPTokenOut] = MPT(ammAlice[1])(100).value().getJson(JsonOptions::Values::None); jv[jss::Flags] = tfLPToken; env(jv, Ter(telENV_RPC_FAILED)); } // Provided LPTokenOut does not match AMM pool's LPToken // asset { json::Value jv = json::ValueType::Object; jv[jss::Account] = alice_.human(); jv[jss::TransactionType] = jss::AMMDeposit; jv[jss::Asset] = STIssue(sfAsset, XRP).getJson(JsonOptions::Values::None); jv[jss::Asset2] = STIssue(sfAsset, MPT(ammAlice[1])).getJson(JsonOptions::Values::None); jv[jss::LPTokenOut] = USD(100).value().getJson(JsonOptions::Values::None); jv[jss::Flags] = tfLPToken; env(jv, Ter(temBAD_AMM_TOKENS)); } // Invalid trading fee ammAlice.deposit( carol_, std::nullopt, XRP(200), MPT(ammAlice[1])(200), std::nullopt, tfTwoAssetIfEmpty, std::nullopt, std::nullopt, 10'000, Ter(temBAD_FEE)); // Invalid tokens { auto const mpt1 = MPTIssue{MPTID(0xabc)}; auto const mpt2 = MPTIssue{MPTID(0xdef)}; ammAlice.deposit( alice_, 1'000, std::nullopt, std::nullopt, std::nullopt, std::nullopt, {{mpt1, mpt2}}, std::nullopt, std::nullopt, Ter(terNO_AMM)); } // invalid MPT ammAlice.deposit( alice_, badMPT(), std::nullopt, std::nullopt, std::nullopt, Ter(temBAD_MPT)); ammAlice.deposit( alice_, XRP(100), badMPT(), std::nullopt, std::nullopt, Ter(temBAD_MPT)); // MPTokenIssuance object doesn't exist ammAlice.deposit( alice_, XRP(100), MPT(gw_, 1'000)(100), std::nullopt, tfTwoAsset, Ter(temBAD_AMM_TOKENS)); // MPToken object doesn't exist env.fund(XRP(1'000), bob_); ammAlice.deposit( bob_, XRP(100), MPT(ammAlice[1])(200), std::nullopt, tfTwoAsset, Ter(tecNO_AUTH)); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .flags = tfMPTCanLock | kMptDexFlags, .authHolder = true}); // Depositing mismatched token, invalid Asset1In.issue ammAlice.deposit( alice_, btc(100), std::nullopt, std::nullopt, std::nullopt, Ter(temBAD_AMM_TOKENS)); // Depositing mismatched token, invalid Asset2In.issue ammAlice.deposit( alice_, XRP(100), btc(100), std::nullopt, std::nullopt, Ter(temBAD_AMM_TOKENS)); // Assets can not be the same ammAlice.deposit( alice_, MPT(ammAlice[1])(100), MPT(ammAlice[1])(200), std::nullopt, tfTwoAsset, Ter(temBAD_AMM_TOKENS)); // Invalid amount value ammAlice.deposit( alice_, MPT(ammAlice[1])(0), std::nullopt, std::nullopt, std::nullopt, Ter(temBAD_AMOUNT)); ammAlice.deposit( alice_, MPT(ammAlice[1])(-1'000), std::nullopt, std::nullopt, std::nullopt, Ter(temBAD_AMOUNT)); // Invalid Account { Account bad("bad"); env.memoize(bad); ammAlice.deposit( bad, std::nullopt, MPT(ammAlice[1])(1'000), 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, {{MPT(btc), MPT(ammAlice[1])}}, std::nullopt, std::nullopt, Ter(terNO_AMM)); // Single deposit: 100000 tokens worth of MPT // Amount to deposit exceeds Max ammAlice.deposit( carol_, 100'000, MPT(ammAlice[1])(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 ammAlice.deposit( alice_, MPT(ammAlice[1])(0), std::nullopt, STAmount{ammAlice.lptIssue(), 1, -1}, std::nullopt, Ter(tecUNFUNDED_AMM)); // Calculated amount is 0 ammAlice.deposit( alice_, MPT(ammAlice[1])(0), std::nullopt, STAmount{ammAlice.lptIssue(), 2'000, -6}, std::nullopt, Ter(tecAMM_FAILED)); // Tiny deposit ammAlice.deposit( carol_, IOUAmount{1, -10}, std::nullopt, std::nullopt, Ter(tecAMM_INVALID_TOKENS)); // Deposit non-empty AMM ammAlice.deposit( carol_, XRP(100), MPT(ammAlice[1])(100), std::nullopt, tfTwoAssetIfEmpty, Ter(tecAMM_NOT_EMPTY)); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Tiny deposit testAMM( [&](AMM& ammAlice, Env& env) { auto const enabledV13 = env.current()->rules().enabled(fixAMMv1_3); auto const err = !enabledV13 ? Ter(temBAD_AMOUNT) : Ter(tesSUCCESS); // Pre-amendment XRP deposit side is rounded to 0 // and deposit fails. ammAlice.deposit(carol_, IOUAmount{1, -1}, std::nullopt, std::nullopt, err); }, {{XRP(10'000), gAmmmpt(10'000)}}, 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)); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Single deposit MPT with eprice // the calculated amount to deposit is negative. testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit( carol_, MPT(ammAlice[1])(100), std::nullopt, STAmount{ammAlice.lptIssue(), 1, -1}, tfLimitLPToken, Ter(tecAMM_FAILED)); // although we should use lptoken unit for eprice, // we don't check the currency any more, we just use // the value ammAlice.deposit( carol_, MPT(ammAlice[1])(100), std::nullopt, STAmount{USD, 1, -1}, tfLimitLPToken, Ter(tecAMM_FAILED)); }, {{USD(10'000), gAmmmpt(10'000)}}); // Globally locked MPT { Env env{*this}; fund(env, gw_, {alice_, carol_}, {USD(20'000)}, Fund::All); MPTTester btc( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .pay = 30'000, .flags = tfMPTCanLock | kMptDexFlags}); AMM ammAlice(env, alice_, USD(10'000), btc(10'000)); btc.set({.flags = tfMPTLock}); ammAlice.deposit( carol_, btc(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN)); ammAlice.deposit( carol_, USD(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN)); ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt, Ter(tecFROZEN)); ammAlice.deposit( carol_, USD(100), btc(100), std::nullopt, std::nullopt, Ter(tecFROZEN)); } // Individually lock MPT or freeze IOU (AMM) with IOU/MPT AMM { Env env{*this, features}; fund(env, gw_, {alice_, carol_}, {USD(20'000)}, Fund::All); MPTTester btc( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .pay = 30'000, .flags = tfMPTCanLock | kMptDexFlags}); AMM ammAlice(env, alice_, USD(10'000), btc(10'000)); // Carol's mpt is locked btc.set({.holder = carol_, .flags = tfMPTLock}); // Carol can not deposit locked mpt ammAlice.deposit( carol_, btc(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN)); ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt, Ter(tecFROZEN)); if (!features[featureAMMClawback]) { ammAlice.deposit( carol_, USD(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecLOCKED)); } else { // Carol can not deposit non-forzen token either ammAlice.deposit( carol_, USD(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN)); } // Alice can deposit because she's not individually locked ammAlice.deposit(alice_, btc(100), std::nullopt, std::nullopt, std::nullopt); ammAlice.deposit(alice_, 1'000, std::nullopt, std::nullopt); ammAlice.deposit(alice_, USD(100), std::nullopt, std::nullopt, std::nullopt); // Unlock btc.set({.holder = carol_, .flags = tfMPTUnlock}); // Carol can deposit after unlock ammAlice.deposit(carol_, btc(100), std::nullopt, std::nullopt, std::nullopt); ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt); // 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_, btc(100), std::nullopt, std::nullopt, std::nullopt); // Cannot deposit frozen token 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)); // unfreeze IOU env(trust( gw_, STAmount{Issue{gw_["USD"].currency, ammAlice.ammAccount()}, 0}, tfClearFreeze)); env.close(); // Can deposit ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt); // Individually lock AMM btc.set({.holder = ammAlice.ammAccount(), .flags = tfMPTLock}); // Can deposit non-frozen token ammAlice.deposit(carol_, USD(100), std::nullopt, std::nullopt, std::nullopt); // Can not deposit locked token ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt, Ter(tecFROZEN)); ammAlice.deposit( carol_, btc(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN)); // Unlock AMM MPT btc.set({.holder = ammAlice.ammAccount(), .flags = tfMPTUnlock}); // can deposit ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt); ammAlice.deposit(carol_, btc(100), std::nullopt, std::nullopt, std::nullopt); } // Individually lock MPT (AMM) account with MPT/MPT AMM { Env env{*this}; env.fund(XRP(10'000), gw_, alice_, carol_); env.close(); MPTTester btc( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .pay = 30'000, .flags = tfMPTCanLock | kMptDexFlags}); MPTTester usd( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .pay = 40'000, .flags = tfMPTCanLock | kMptDexFlags}); AMM ammAlice(env, alice_, usd(10'000), btc(10'000)); // Carol's BTC is locked btc.set({.holder = carol_, .flags = tfMPTLock}); ammAlice.deposit( carol_, usd(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN)); ammAlice.deposit( carol_, btc(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN)); // Unlock carol's BTC btc.set({.holder = carol_, .flags = tfMPTUnlock}); // Can deposit ammAlice.deposit(carol_, usd(100), std::nullopt, std::nullopt, std::nullopt); ammAlice.deposit(carol_, btc(100), std::nullopt, std::nullopt, std::nullopt); // Individually lock MPT BTC (AMM) account btc.set({.holder = ammAlice.ammAccount(), .flags = tfMPTLock}); // Can deposit non-locked token USD ammAlice.deposit(carol_, usd(100), std::nullopt, std::nullopt, std::nullopt); // Can not deposit locked token BTC ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt, Ter(tecFROZEN)); ammAlice.deposit( carol_, btc(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN)); // Unlock AMM MPT BTC btc.set({.holder = ammAlice.ammAccount(), .flags = tfMPTUnlock}); // Can deposit BTC ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt); ammAlice.deposit(carol_, btc(100), std::nullopt, std::nullopt, std::nullopt); // Individually Lock MPT USD (AMM) account usd.set({.holder = ammAlice.ammAccount(), .flags = tfMPTLock}); // Can deposit non-locked token BTC ammAlice.deposit(carol_, btc(100), std::nullopt, std::nullopt, std::nullopt); // Can not deposit locked token USD ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt, Ter(tecFROZEN)); ammAlice.deposit( carol_, usd(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN)); // Unlock AMM MPT USD usd.set({.holder = ammAlice.ammAccount(), .flags = tfMPTUnlock}); // Can deposit USD ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt); ammAlice.deposit(carol_, usd(100), std::nullopt, std::nullopt, std::nullopt); } // Deposit unauthorized token { Env env{*this, features}; Account const gw("gateway"), alice{"alice"}, carol{"carol"}; env.fund(XRP(30'000), alice, carol, gw); env.close(); MPTTester btc(env, gw, {.holders = {alice, carol}, .fund = false}); btc.create( {.maxAmt = 1'000'000, .authorize = {{alice}}, .pay = {{{alice}, 10'000}}, .flags = tfMPTRequireAuth | kMptDexFlags, .authHolder = true}); AMM amm(env, alice, XRP(10'000), btc(10'000)); env.close(); if (!features[featureAMMClawback]) { amm.deposit(carol, XRP(10), std::nullopt, std::nullopt, std::nullopt); } else { amm.deposit( carol, XRP(10), std::nullopt, std::nullopt, std::nullopt, Ter(tecNO_AUTH)); } } // MPTCanTransfer is not set and the account is not the issuer of MPT { Env env{*this, features}; Account const gw("gateway"), alice{"alice"}, carol{"carol"}; env.fund(XRP(30'000), alice, carol, gw); env.close(); MPTTester const btc( {.env = env, .issuer = gw, .holders = {alice}, .pay = 1'000, .flags = tfMPTCanTrade}); AMM amm(env, gw, XRP(10'000), btc(10'000)); amm.deposit({.account = alice, .asset1In = btc(10), .err = Ter(tecNO_PERMISSION)}); } // Insufficient XRP balance testAMM( [&](AMM& ammAlice, Env& env) { env.fund(XRP(1'000), bob_); env.close(); ammAlice.deposit(bob_, XRP(10)); ammAlice.deposit( bob_, XRP(1'000), std::nullopt, std::nullopt, std::nullopt, Ter(tecUNFUNDED_AMM)); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Insufficient MPT balance testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit( carol_, MPT(ammAlice[1])(450'000), std::nullopt, std::nullopt, std::nullopt, Ter(tecUNFUNDED_AMM)); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Insufficient IOU balance testAMM( [&](AMM& ammAlice, Env& env) { fund(env, gw_, {bob_}, {USD(1'000)}, Fund::Acct); ammAlice.deposit( bob_, USD(1'001), std::nullopt, std::nullopt, std::nullopt, Ter(tecUNFUNDED_AMM)); }, {{USD(1000), gAmmmpt(1000)}}); // Insufficient MPT balance by tokens { Env env{*this}; env.fund(XRP(30'000), alice_, bob_, gw_); env.close(); MPT const btc = MPTTester( {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .transferFee = 1'500, .pay = 1000}); AMM ammAlice(env, alice_, XRP(20'000), btc(1000)); 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/MPT { Env env(*this); auto const startingXrp = reserve(env, 4) + env.current()->fees().base * 4; env.fund(XRP(10'000), gw_); env.fund(XRP(10'000), alice_); env.fund(startingXrp, carol_); MPT const btc = MPTTester( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .transferFee = 1'500, .pay = 40'000}); env(offer(carol_, XRP(100), btc(101))); AMM ammAlice(env, alice_, XRP(1000), btc(1000)); ammAlice.deposit( carol_, XRP(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecINSUF_RESERVE_LINE)); env(offer(carol_, XRP(100), btc(102))); ammAlice.deposit( carol_, btc(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), MPT(ammAlice[1])(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), MPT(ammAlice[1])(100), std::nullopt, tfTwoAsset, std::nullopt, std::nullopt, std::nullopt, Ter(temBAD_AMOUNT)); ammAlice.deposit( carol_, 1'000, XRP(100), MPT(ammAlice[1])(-1), std::nullopt, tfTwoAsset, std::nullopt, std::nullopt, std::nullopt, Ter(temBAD_AMOUNT)); ammAlice.deposit( carol_, 1'000, XRP(100), badMPT(), std::nullopt, tfTwoAsset, std::nullopt, std::nullopt, std::nullopt, Ter(temBAD_MPT)); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Min deposit testAMM( [&](AMM& ammAlice, Env& env) { // Equal deposit by tokens ammAlice.deposit( carol_, 1'000'000, XRP(1'000), MPT(ammAlice[1])(1'001), std::nullopt, tfLPToken, std::nullopt, std::nullopt, std::nullopt, Ter(tecAMM_FAILED)); ammAlice.deposit( carol_, 1'000'000, XRP(1'001), MPT(ammAlice[1])(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), MPT(ammAlice[1])(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)); }, {{XRP(1000), gAmmmpt(1000)}}); // OutstandingAmount > MaximumAmount { Env env{*this}; env.fund(XRP(1'000), gw_, alice_); MPTTester const btc({.env = env, .issuer = gw_, .holders = {alice_}, .maxAmt = 100}); AMM amm(env, gw_, XRP(100), btc(90)); // OutstandingAmount is 90, issuer issues 1 over MaximumAmount amm.deposit( DepositArg{.account = gw_, .asset1In = btc(11), .err = Ter(tecUNFUNDED_AMM)}); env(pay(gw_, alice_, btc(10))); // OutstandingAmount is 100, issuer issues 10 over MaximumAmount amm.deposit( DepositArg{.account = gw_, .asset1In = btc(10), .err = Ter(tecUNFUNDED_AMM)}); // This is fine - alice transfers 10 to AMM. OutstandingAmount // is 100. amm.deposit(DepositArg{.account = alice_, .asset1In = btc(10)}); } } void testDeposit() { testcase("Deposit"); using namespace jtx; // Equal deposit: 1000000 tokens. XRP/MPT AMM. testAMM( [&](AMM& ammAlice, Env& env) { XRPAmount const baseFee{env.current()->fees().base}; auto carolXRP = env.balance(carol_, XRP); auto carolMPT = env.balance(carol_, MPT(ammAlice[1])); ammAlice.deposit(carol_, 1'000'000); BEAST_EXPECT(ammAlice.expectBalances( XRP(11'000), MPT(ammAlice[1])(11'000), IOUAmount{11'000'000, 0})); // carol deposited 1000 XRP and pays the transaction fee env.require(Balance(carol_, carolXRP - XRP(1000) - drops(baseFee))); // carol deposited 1000 MPT env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(1000))); }, {{XRP(10'000), gAmmmpt(10'000)}}); // single deposit MPT with eprice testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit( carol_, MPT(ammAlice[1])(100), std::nullopt, STAmount{ammAlice.lptIssue(), 1, -1}, tfLimitLPToken); // although we should use lptoken unit for eprice, // we don't check the currency any more, we just use // the value ammAlice.deposit( carol_, MPT(ammAlice[1])(100), std::nullopt, STAmount{USD, 1, -1}, tfLimitLPToken); }, {{USD(10'000'000), gAmmmpt(10'000)}}); // Equal deposit: 1000000 tokens. IOU/MPT combination { auto test = [&](auto&& issue1, auto&& issue2) { Env env(*this); env.fund(XRP(30'000), alice_, carol_, gw_); env.close(); auto const usd = issue1( {.env = env, .token = "USD", .issuer = gw_, .holders = {alice_, carol_}, .limit = 1'000'000}); auto const btc = issue2( {.env = env, .token = "BTC", .issuer = gw_, .holders = {alice_, carol_}, .limit = 1'000'000}); env(pay(gw_, alice_, btc(50000))); env(pay(gw_, carol_, btc(50000))); env(pay(gw_, alice_, usd(50000))); env(pay(gw_, carol_, usd(50000))); env.close(); auto ammAlice = AMM(env, alice_, usd(10000), btc(10000)); auto carolBTC = env.balance(carol_, btc); auto carolUSD = env.balance(carol_, usd); ammAlice.deposit(carol_, 1'000); BEAST_EXPECT(ammAlice.expectBalances(btc(11'000), usd(11'000), IOUAmount(11'000))); env.require(Balance(carol_, carolBTC - btc(1000))); env.require(Balance(carol_, carolUSD - usd(1000))); }; testHelper2TokensMix(test); } // Deposit 100MPT/100XRP. XRP/MPT AMM. testAMM( [&](AMM& ammAlice, Env& env) { XRPAmount const baseFee{env.current()->fees().base}; auto carolXRP = env.balance(carol_, XRP); auto carolMPT = env.balance(carol_, MPT(ammAlice[1])); ammAlice.deposit(carol_, MPT(ammAlice[1])(100), XRP(100)); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'100), MPT(ammAlice[1])(10'100), IOUAmount{10'100'000, 0})); env.require(Balance(carol_, carolXRP - XRP(100) - drops(baseFee))); env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(100))); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Deposit MPT/IOU combination { auto test = [&](auto&& issue1, auto&& issue2) { Env env(*this); env.fund(XRP(30'000), alice_, carol_, gw_); env.close(); auto const usd = issue1( {.env = env, .token = "USD", .issuer = gw_, .holders = {alice_, carol_}, .limit = 1'000'000}); auto const btc = issue2( {.env = env, .token = "BTC", .issuer = gw_, .holders = {alice_, carol_}, .limit = 1'000'000}); env(pay(gw_, alice_, btc(50000))); env(pay(gw_, carol_, btc(50000))); env(pay(gw_, alice_, usd(50000))); env(pay(gw_, carol_, usd(50000))); env.close(); auto ammAlice = AMM(env, alice_, usd(10000), btc(10000)); auto carolBTC = env.balance(carol_, btc); auto carolUSD = env.balance(carol_, usd); ammAlice.deposit(carol_, btc(100), usd(100)); BEAST_EXPECT(ammAlice.expectBalances(btc(10'100), usd(10'100), IOUAmount(10'100))); env.require(Balance(carol_, carolBTC - btc(100))); env.require(Balance(carol_, carolUSD - usd(100))); }; testHelper2TokensMix(test); } // Equal limit deposit. // Try to deposit 200MPT/100XRP. Is truncated to 100MPT/100XRP. testAMM( [&](AMM& ammAlice, Env& env) { XRPAmount const baseFee{env.current()->fees().base}; auto carolXRP = env.balance(carol_, XRP); auto carolMPT = env.balance(carol_, MPT(ammAlice[1])); ammAlice.deposit(carol_, MPT(ammAlice[1])(200), XRP(100)); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'100), MPT(ammAlice[1])(10'100), IOUAmount{10'100'000, 0})); env.require(Balance(carol_, carolXRP - XRP(100) - drops(baseFee))); env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(100))); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Equal limit deposit. MPT/IOU combination. { auto test = [&](auto&& issue1, auto&& issue2) { Env env(*this); env.fund(XRP(30'000), alice_, carol_, gw_); env.close(); auto const usd = issue1( {.env = env, .token = "USD", .issuer = gw_, .holders = {alice_, carol_}, .limit = 1'000'000}); auto const btc = issue2( {.env = env, .token = "BTC", .issuer = gw_, .holders = {alice_, carol_}, .limit = 1'000'000}); env(pay(gw_, alice_, btc(50000))); env(pay(gw_, carol_, btc(50000))); env(pay(gw_, alice_, usd(50000))); env(pay(gw_, carol_, usd(50000))); env.close(); auto ammAlice = AMM(env, alice_, usd(10000), btc(10000)); auto carolBTC = env.balance(carol_, btc); auto carolUSD = env.balance(carol_, usd); ammAlice.deposit(carol_, btc(200), usd(100)); BEAST_EXPECT(ammAlice.expectBalances(btc(10'100), usd(10'100), IOUAmount(10'100))); env.require(Balance(carol_, carolBTC - btc(100))); env.require(Balance(carol_, carolUSD - usd(100))); }; testHelper2TokensMix(test); } // Single deposit: 1000 MPT into MPT/XRP testAMM( [&](AMM& ammAlice, Env& env) { XRPAmount const baseFee{env.current()->fees().base}; auto carolXRP = env.balance(carol_, XRP); auto carolMPT = env.balance(carol_, MPT(ammAlice[1])); ammAlice.deposit(carol_, MPT(ammAlice[1])(1000)); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), MPT(ammAlice[1])(11'000), IOUAmount{10'488'088'48170151, -8})); env.require(Balance(carol_, carolXRP - drops(baseFee))); env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(1000))); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Single deposit: 1000 XRP into MPT/XRP testAMM( [&](AMM& ammAlice, Env& env) { XRPAmount const baseFee{env.current()->fees().base}; auto carolXRP = env.balance(carol_, XRP); auto carolMPT = env.balance(carol_, MPT(ammAlice[1])); ammAlice.deposit(carol_, XRP(1000)); BEAST_EXPECT(ammAlice.expectBalances( XRP(11'000), MPT(ammAlice[1])(10'000), IOUAmount{10'488'088'48170151, -8})); env.require(Balance(carol_, carolXRP - XRP(1000) - drops(baseFee))); env.require(Balance(carol_, carolMPT)); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Single deposit: 1000 MPT0 into MPT/MPT testAMM( [&](AMM& ammAlice, Env& env) { auto carolMPT0 = env.balance(carol_, MPT(ammAlice[0])); auto carolMPT1 = env.balance(carol_, MPT(ammAlice[1])); ammAlice.deposit(carol_, MPT(ammAlice[0])(1000)); BEAST_EXPECT(ammAlice.expectBalances( MPT(ammAlice[0])(11'000), MPT(ammAlice[1])(10'000), IOUAmount{10'488'08848170151, -11})); env.require(Balance(carol_, carolMPT0 - MPT(ammAlice[0])(1000))); env.require(Balance(carol_, carolMPT1)); }, {{gAmmmpt(10'000), gAmmmpt(10'000)}}); // Single deposit: 1000 MPT into MPT/IOU testAMM( [&](AMM& ammAlice, Env& env) { auto carolMPT = env.balance(carol_, MPT(ammAlice[0])); auto carolUSD = env.balance(carol_, USD); ammAlice.deposit(carol_, MPT(ammAlice[0])(1000)); BEAST_EXPECT(ammAlice.expectBalances( MPT(ammAlice[0])(11'000), USD(10'000), IOUAmount{10'488'08848170151, -11})); env.require(Balance(carol_, carolMPT - MPT(ammAlice[0])(1000))); env.require(Balance(carol_, carolUSD)); }, {{gAmmmpt(10'000), USD(10'000)}}); // Single deposit: 1000 IOU into MPT/IOU testAMM( [&](AMM& ammAlice, Env& env) { auto carolMPT = env.balance(carol_, MPT(ammAlice[0])); auto carolUSD = env.balance(carol_, USD); ammAlice.deposit(carol_, USD(1000)); BEAST_EXPECT(ammAlice.expectBalances( MPT(ammAlice[0])(10'000), STAmount{USD, UINT64_C(10999'99999999999), -11}, IOUAmount{10'488'08848170151, -11})); env.require(Balance(carol_, carolMPT)); env.require( Balance(carol_, carolUSD - STAmount{USD, UINT64_C(999'99999999999), -11})); }, {{gAmmmpt(10'000), USD(10'000)}}); // Single deposit: 100000 tokens worth of MPT into XRP/MPT testAMM( [&](AMM& ammAlice, Env& env) { XRPAmount const baseFee{env.current()->fees().base}; auto carolXRP = env.balance(carol_, XRP); auto carolMPT = env.balance(carol_, MPT(ammAlice[1])); ammAlice.deposit(carol_, 100'000, MPT(ammAlice[1])(205)); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), MPT(ammAlice[1])(10'201), IOUAmount{10'100'000, 0})); env.require(Balance(carol_, carolXRP - drops(baseFee))); env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(201))); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Single deposit: 100000 tokens worth of XRP into XRP/MPT testAMM( [&](AMM& ammAlice, Env& env) { XRPAmount const baseFee{env.current()->fees().base}; auto carolXRP = env.balance(carol_, XRP); auto carolMPT = env.balance(carol_, MPT(ammAlice[1])); ammAlice.deposit(carol_, 100'000, XRP(205)); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'201), MPT(ammAlice[1])(10'000), IOUAmount{10'100'000, 0})); env.require(Balance(carol_, carolXRP - XRP(201) - drops(baseFee))); env.require(Balance(carol_, carolMPT)); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Single deposit: 100 tokens worth of MPT/IOU into pool of MPT/IOU // combination { auto test = [&](auto&& issue1, auto&& issue2) { Env env(*this); env.fund(XRP(30'000), alice_, carol_, gw_); env.close(); auto const usd = issue1( {.env = env, .token = "USD", .issuer = gw_, .holders = {alice_, carol_}, .limit = 1'000'000}); auto const btc = issue2( {.env = env, .token = "BTC", .issuer = gw_, .holders = {alice_, carol_}, .limit = 1'000'000}); env(pay(gw_, alice_, btc(50000))); env(pay(gw_, carol_, btc(50000))); env(pay(gw_, alice_, usd(50000))); env(pay(gw_, carol_, usd(50000))); env.close(); auto ammAlice = AMM(env, alice_, usd(10000), btc(10000)); auto carolBTC = env.balance(carol_, btc); auto carolUSD = env.balance(carol_, usd); ammAlice.deposit(carol_, 100, usd(205)); auto deltaUSD = [&]() { if constexpr (std::is_same_v>) return usd(202); return usd(201); }(); BEAST_EXPECT(ammAlice.expectBalances( btc(10'000), usd(10'000) + deltaUSD, IOUAmount{10'100, 0})); env.require(Balance(carol_, carolBTC)); env.require(Balance(carol_, carolUSD - deltaUSD)); }; testHelper2TokensMix(test); } // Single deposit with EP not exceeding specified: // 100 MPT with EP not to exceed 0.1 (AssetIn/TokensOut) // for XRP/MPT testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit( carol_, MPT(ammAlice[1])(100), std::nullopt, STAmount{ammAlice.lptIssue(), 1, -1}); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), MPT(ammAlice[1])(10100), IOUAmount{10'049'875'62112089, -8})); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Single deposit with EP not exceeding specified: // 100 MPT with EP not to exceed 0.002004 (AssetIn/TokensOut) testAMM( [&](AMM& ammAlice, Env& env) { XRPAmount const baseFee{env.current()->fees().base}; auto carolXRP = env.balance(carol_, XRP); auto carolMPT = env.balance(carol_, MPT(ammAlice[1])); ammAlice.deposit( carol_, MPT(ammAlice[1])(100), std::nullopt, STAmount{ammAlice.lptIssue(), 2004, -6}); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), MPT(ammAlice[1])(10'081), IOUAmount{10'039'920'31840891, -8})); env.require(Balance(carol_, carolXRP - drops(baseFee))); env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(81))); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Single deposit with EP not exceeding specified: // 0 MPT with EP not to exceed 0.002004 (AssetIn/TokensOut) testAMM( [&](AMM& ammAlice, Env& env) { XRPAmount const baseFee{env.current()->fees().base}; auto carolXRP = env.balance(carol_, XRP); auto carolMPT = env.balance(carol_, MPT(ammAlice[1])); ammAlice.deposit( carol_, MPT(ammAlice[1])(0), std::nullopt, STAmount{ammAlice.lptIssue(), 2004, -6}); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), MPT(ammAlice[1])(10'081), IOUAmount{10'039'920'31840891, -8})); env.require(Balance(carol_, carolXRP - drops(baseFee))); env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(81))); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Single deposit with EP not exceeding specified: // 100 MPT with EP not to exceed 0.1 (AssetIn/TokensOut) // for IOU/MPT testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit( carol_, MPT(ammAlice[1])(100), std::nullopt, STAmount{ammAlice.lptIssue(), 1, -1}); BEAST_EXPECT(ammAlice.expectBalances( USD(10'000'000'000), MPT(ammAlice[1])(10100), IOUAmount{10'049'875'62112089, -8})); }, {{USD(10'000'000'000), gAmmmpt(10'000)}}); // Single deposit with EP not exceeding specified: // 100 IOU with EP not to exceed 0.1 (AssetIn/TokensOut) // for IOU/MPT testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit(carol_, USD(100), std::nullopt, STAmount{USD, 1, -1}); BEAST_EXPECT(ammAlice.expectBalances( MPT(ammAlice[1])(10'000'000'000), USD(10100), IOUAmount{10'049'875'62112089, -8})); }, {{USD(10'000), gAmmmpt(10'000'000'000)}}); // Single deposit with EP not exceeding specified: // 100 IOU with EP not to exceed 0.1 (AssetIn/TokensOut) // for MPT/MPT testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit( carol_, MPT(ammAlice[0])(100), std::nullopt, STAmount{ammAlice.lptIssue(), 1, -1}); BEAST_EXPECT(ammAlice.expectBalances( MPT(ammAlice[1])(10'000'000'000), MPT(ammAlice[0])(10100), IOUAmount{10'049'875'62112089, -8})); }, {{gAmmmpt(10'000), gAmmmpt(10'000'000'000)}}); // MPT/MPT with transfer fee { Env env(*this); env.fund(XRP(30'000), gw_, alice_, carol_); env.close(); MPT const btc = MPTTester( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .transferFee = 25'000, .pay = 400'000}); MPT const usd = MPTTester( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .transferFee = 25'000, .pay = 400'000}); AMM ammAlice(env, alice_, usd(200'000), btc(5)); BEAST_EXPECT(ammAlice.expectBalances(usd(200'000), btc(5), IOUAmount{1000, 0})); ammAlice.deposit(carol_, 100, std::nullopt, std::nullopt); BEAST_EXPECT(ammAlice.expectBalances(usd(220'000), btc(6), IOUAmount{1100, 0})); } // IOU/MPT with transfer fee { Env env(*this); env.fund(XRP(30'000), gw_, alice_, bob_, carol_); env.close(); env(rate(gw_, 1.25)); env.close(); MPT const btc = MPTTester( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .transferFee = 25'000, .pay = 400'000}); auto const usd = gw_["USD"]; env.trust(usd(1000000), alice_); env(pay(gw_, alice_, usd(1000000))); env.trust(usd(1000000), bob_); env(pay(gw_, bob_, usd(1000000))); env.trust(usd(1000000), carol_); env(pay(gw_, carol_, usd(1000000))); env.close(); // IOU/MPT AMM ammAlice(env, alice_, usd(200'000), btc(5)); BEAST_EXPECT(ammAlice.expectBalances(usd(200'000), btc(5), IOUAmount{1000, 0})); ammAlice.deposit(carol_, 100, std::nullopt, std::nullopt); BEAST_EXPECT(ammAlice.expectBalances(usd(220'000), btc(6), IOUAmount{1100, 0})); MPT const eth = MPTTester( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .transferFee = 25'000, .pay = 400'000}); // MPT/IOU AMM ammBob(env, bob_, eth(20'000), usd(0.5)); BEAST_EXPECT(ammBob.expectBalances(eth(20'000), usd(0.5), IOUAmount{100, 0})); ammBob.deposit(carol_, 10); BEAST_EXPECT(ammBob.expectBalances(eth(22'000), usd(0.55), IOUAmount{110, 0})); } // Tiny deposits for IOU/MPT testAMM( [&](AMM& ammAlice, Env& env) { // tiny amount causes MPT to deposit rounded to 0 ammAlice.deposit(carol_, IOUAmount{1, -3}, std::nullopt, std::nullopt); BEAST_EXPECT(ammAlice.expectBalances( STAmount{USD, UINT64_C(10'000'001), -3}, MPT(ammAlice[1])(10'001), IOUAmount{10'000'001, -3})); ammAlice.deposit(carol_, IOUAmount{1}); BEAST_EXPECT(ammAlice.expectBalances( STAmount{USD, UINT64_C(10'001'001), -3}, MPT(ammAlice[1])(10'003), IOUAmount{10'001'001, -3})); }, {{USD(10'000), gAmmmpt(10'000)}}); testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit(carol_, STAmount{USD, 1, -10}); BEAST_EXPECT(ammAlice.expectBalances( STAmount{USD, UINT64_C(10'000'00000000008), -11}, MPT(ammAlice[1])(10'000), IOUAmount{1'000'000'000000004, -11})); ammAlice.deposit(carol_, MPT(ammAlice[1])(1)); BEAST_EXPECT(ammAlice.expectBalances( STAmount{USD, UINT64_C(10'000'00000000008), -11}, MPT(ammAlice[1])(10'001), IOUAmount{10'000'49998750066, -11})); }, {{USD(10'000), gAmmmpt(10'000)}}); // Tiny deposits for XRP/MPT testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit(carol_, XRPAmount{1}); BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{10'000'000'001}, MPT(ammAlice[1])(10'000), IOUAmount{1'000'000'000049999, -8})); }, {{XRP(10'000), gAmmmpt(10'000)}}); testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit(carol_, MPT(ammAlice[1])(1)); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), MPT(ammAlice[1])(10'001), IOUAmount{10'000'499'98750062, -8})); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Tiny deposits for MPT/MPT testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit(carol_, MPT(ammAlice[1])(1)); BEAST_EXPECT(ammAlice.expectBalances( MPT(ammAlice[0])(10'000), MPT(ammAlice[1])(10'001), IOUAmount{1'000'049'998750062, -11})); }, {{gAmmmpt(10'000), gAmmmpt(10'000)}}); // MPT Issuer create/deposit { Env env(*this); env.fund(XRP(30'000), gw_); env.close(); MPT const btc = MPTTester({.env = env, .issuer = gw_, .holders = {}}); AMM ammGw(env, gw_, XRP(10'000), btc(10'000'000'000)); BEAST_EXPECT( ammGw.expectBalances(XRP(10'000), btc(10'000'000'000), IOUAmount{10'000'000'000})); ammGw.deposit(gw_, 1'000'000); BEAST_EXPECT( ammGw.expectBalances(XRP(10'001), btc(10'001000000), IOUAmount{10'001000000})); ammGw.deposit(gw_, btc(1'000000000)); BEAST_EXPECT(ammGw.expectBalances( XRP(10'001), btc(11'001000000), IOUAmount{1048'908'961731188, -5})); } // Issuer deposit in MPT/MPT pool testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit(gw_, 1'000'000); BEAST_EXPECT(ammAlice.expectBalances( MPT(ammAlice[0])(1'010'000), MPT(ammAlice[1])(1'010'000), IOUAmount{1'010'000})); ammAlice.deposit(gw_, MPT(ammAlice[0])(1000)); BEAST_EXPECT(ammAlice.expectBalances( MPT(ammAlice[0])(1'010'999), MPT(ammAlice[1])(1'010'000), IOUAmount{1'010'499'376546071, -9})); }, {{gAmmmpt(10'000), gAmmmpt(10'000)}}); // Issuer deposit in MPT/XRP pool testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit(gw_, 1'000'000); BEAST_EXPECT(ammAlice.expectBalances( XRP(11'000), MPT(ammAlice[1])(11'000), IOUAmount{11'000'000})); ammAlice.deposit(gw_, MPT(ammAlice[1])(1'000)); BEAST_EXPECT(ammAlice.expectBalances( XRP(11'000), MPT(ammAlice[1])(12'000), IOUAmount{11'489'125'29307605, -8})); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Equal deposit by tokens MPT/XRP testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit( carol_, 1'000'000, XRP(1'000), MPT(ammAlice[1])(1'000), std::nullopt, tfLPToken, std::nullopt, std::nullopt); BEAST_EXPECT(ammAlice.expectBalances( XRP(11'000), MPT(ammAlice[1])(11'000), IOUAmount{11'000'000, 0})); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Equal deposit by tokens MPT/IOU combination { auto test = [&](auto&& issue1, auto&& issue2) { Env env(*this); env.fund(XRP(30'000), alice_, carol_, gw_); env.close(); auto const usd = issue1( {.env = env, .token = "USD", .issuer = gw_, .holders = {alice_, carol_}, .limit = 1'000'000}); auto const btc = issue2( {.env = env, .token = "BTC", .issuer = gw_, .holders = {alice_, carol_}, .limit = 1'000'000}); env(pay(gw_, alice_, btc(50000))); env(pay(gw_, carol_, btc(50000))); env(pay(gw_, alice_, usd(50000))); env(pay(gw_, carol_, usd(50000))); env.close(); auto ammAlice = AMM(env, alice_, usd(10000), btc(10000)); auto carolBTC = env.balance(carol_, btc); auto carolUSD = env.balance(carol_, usd); ammAlice.deposit( carol_, 1'000, usd(1'000), btc(1'000), std::nullopt, tfLPToken, std::nullopt, std::nullopt); BEAST_EXPECT(ammAlice.expectBalances(usd(11'000), btc(11'000), IOUAmount{11'000})); env.require(Balance(carol_, carolBTC - btc(1000))); env.require(Balance(carol_, carolUSD - usd(1000))); }; testHelper2TokensMix(test); } // Equal deposit by asset XRP/MPT testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit( carol_, 1'000'000, XRP(1'000), MPT(ammAlice[1])(1'000), std::nullopt, tfTwoAsset, std::nullopt, std::nullopt); BEAST_EXPECT(ammAlice.expectBalances( XRP(11'000), MPT(ammAlice[1])(11'000), IOUAmount{11'000'000, 0})); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Equal deposit by asset IOU/MPT combination { auto test = [&](auto&& issue1, auto&& issue2) { Env env(*this); env.fund(XRP(30'000), alice_, carol_, gw_); env.close(); auto const usd = issue1( {.env = env, .token = "USD", .issuer = gw_, .holders = {alice_, carol_}, .limit = 1'000'000}); auto const btc = issue2( {.env = env, .token = "BTC", .issuer = gw_, .holders = {alice_, carol_}, .limit = 1'000'000}); env(pay(gw_, alice_, btc(50000))); env(pay(gw_, carol_, btc(50000))); env(pay(gw_, alice_, usd(50000))); env(pay(gw_, carol_, usd(50000))); env.close(); auto ammAlice = AMM(env, alice_, usd(10000), btc(10000)); auto carolBTC = env.balance(carol_, btc); auto carolUSD = env.balance(carol_, usd); ammAlice.deposit( carol_, 1'000, usd(1'000), btc(1'000), std::nullopt, tfTwoAsset, std::nullopt, std::nullopt); BEAST_EXPECT( ammAlice.expectBalances(usd(11'000), btc(11'000), IOUAmount{11'000, 0})); env.require(Balance(carol_, carolBTC - btc(1000))); env.require(Balance(carol_, carolUSD - usd(1000))); }; testHelper2TokensMix(test); } // Single deposit XRP by asset MPT/XRP testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit( carol_, 488'088, XRP(1'000), std::nullopt, std::nullopt, tfSingleAsset, std::nullopt, std::nullopt); BEAST_EXPECT(ammAlice.expectBalances( XRP(11'000), MPT(ammAlice[1])(10'000), IOUAmount{10'488'088'48170151, -8})); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Single deposit MPT by asset MPT/XRP testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit( carol_, 488'088, MPT(ammAlice[1])(1'000), std::nullopt, std::nullopt, tfSingleAsset, std::nullopt, std::nullopt); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), MPT(ammAlice[1])(11'000), IOUAmount{10'488'088'48170151, -8})); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Single deposit IOU by asset MPT/IOU testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit( carol_, 488, USD(1'000), std::nullopt, std::nullopt, tfSingleAsset, std::nullopt, std::nullopt); BEAST_EXPECT(ammAlice.expectBalances( STAmount{USD, UINT64_C(10'999'99999999999), -11}, MPT(ammAlice[1])(10'000), IOUAmount{10'488'08848170151, -11})); }, {{USD(10'000), gAmmmpt(10'000)}}); // Single deposit MPT by asset MPT/IOU testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit( carol_, 488, MPT(ammAlice[1])(1'000), std::nullopt, std::nullopt, tfSingleAsset, std::nullopt, std::nullopt); BEAST_EXPECT(ammAlice.expectBalances( USD(10'000), MPT(ammAlice[1])(11'000), IOUAmount{10'488'088'48170151, -11})); }, {{USD(10'000), gAmmmpt(10'000)}}); // Single deposit MPT by asset MPT/MPT testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit( carol_, 488, MPT(ammAlice[1])(1'000), std::nullopt, std::nullopt, tfSingleAsset, std::nullopt, std::nullopt); BEAST_EXPECT(ammAlice.expectBalances( MPT(ammAlice[0])(10'000), MPT(ammAlice[1])(11'000), IOUAmount{10'488'088'48170151, -11})); }, {{gAmmmpt(10'000), gAmmmpt(10'000)}}); } void testInvalidWithdraw() { testcase("Invalid AMMWithdraw"); using namespace jtx; auto const all = testableAmendments(); testAMM( [&](AMM& ammAlice, Env& env) { WithdrawArg const args{ .asset1Out = XRP(100), .err = Ter(tecAMM_BALANCE), }; ammAlice.withdraw(args); }, {{XRP(99), gAmmmpt(99)}}); testAMM( [&](AMM& ammAlice, Env& env) { WithdrawArg const args{ .asset1Out = MPT(ammAlice[1])(100), .err = Ter(tecAMM_BALANCE), }; ammAlice.withdraw(args); }, {{XRP(99), gAmmmpt(99)}}); { Env env{*this}; env.fund(XRP(30'000), gw_, alice_, bob_); env.close(); // alice is authorized to hold gw MPT, bob is not authorized MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_}, .pay = 30'000, .flags = tfMPTRequireAuth | kMptDexFlags, .authHolder = true}); AMM ammAlice(env, alice_, XRP(10'000), btc(10'000)); WithdrawArg const args{ .account = bob_, .asset1Out = btc(100), .err = Ter(tecNO_AUTH), }; ammAlice.withdraw(args); } testAMM( [&](AMM& ammAlice, Env& env) { MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .pay = 2'000, .flags = tfMPTCanLock | kMptDexFlags}); // 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_, btc(100), std::nullopt, std::nullopt, Ter(temBAD_AMM_TOKENS)); ammAlice.withdraw( alice_, MPT(badMPT())(100), std::nullopt, std::nullopt, Ter(temBAD_MPT)); // Mismatched token, invalid Asset2Out issue ammAlice.withdraw(alice_, XRP(100), btc(100), std::nullopt, Ter(temBAD_AMM_TOKENS)); // Mismatched token, Asset1Out.issue == Asset2Out.issue ammAlice.withdraw( alice_, MPT(ammAlice[1])(100), MPT(ammAlice[1])(100), std::nullopt, Ter(temBAD_AMM_TOKENS)); // Invalid amount value ammAlice.withdraw( alice_, MPT(ammAlice[1])(0), std::nullopt, std::nullopt, Ter(temBAD_AMOUNT)); ammAlice.withdraw( alice_, MPT(ammAlice[1])(-100), std::nullopt, std::nullopt, Ter(temBAD_AMOUNT)); ammAlice.withdraw( alice_, MPT(ammAlice[1])(10), std::nullopt, IOUAmount{-1}, Ter(temBAD_AMOUNT)); // Invalid amount/token value, withdraw all tokens from one side // of the pool. ammAlice.withdraw( alice_, MPT(ammAlice[1])(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, MPT(ammAlice[1])(0), std::nullopt, std::nullopt, tfOneAssetWithdrawAll, std::nullopt, std::nullopt, Ter(tecAMM_BALANCE)); // Bad MPT ammAlice.withdraw( alice_, XRP(100), MPT(badMPT())(100), std::nullopt, Ter(temBAD_MPT)); // Specified MPToken doesn't match the pool assets ammAlice.withdraw( alice_, XRP(100), MPT(noMPT())(100), std::nullopt, Ter(temBAD_AMM_TOKENS)); // Invalid Account Account 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, {{MPT(ammAlice[1]), GBP}}, std::nullopt, Ter(terNO_AMM)); // Carol is not a Liquidity Provider ammAlice.withdraw(carol_, 10'000, std::nullopt, std::nullopt, Ter(tecAMM_BALANCE)); }, {{XRP(10'000), gAmmmpt(10'000)}}); testAMM( [&](AMM& ammAlice, Env& env) { // Withdraw entire one side of the pool. // Pre-fixAMMv1_3: // Equal withdraw but due to MPT precision limit, // this results in full withdraw of MPT pool only, // while leaving a tiny amount in USD pool. // Post-fixAMMv1_3: // 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( MPT(ammAlice[0])(1), STAmount{USD, 1, -7}, IOUAmount{1, -4})); } }, {{gAmmmpt(10'000'000'000), USD(10'000)}}, 0, std::nullopt, {all, all - fixAMMv1_3}); testAMM( [&](AMM& ammAlice, Env& env) { // Similar to above with even smaller remaining amount // Pre-fixAMMv1_3: results in full withdraw of MPT pool only, // returning tecAMM_BALANCE. Post-fixAMMv1_3: 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'999999999, -9}, std::nullopt, std::nullopt, err); if (env.enabled(fixAMMv1_3)) { BEAST_EXPECT(ammAlice.expectBalances( MPT(ammAlice[0])(1), STAmount{USD, 1, -11}, IOUAmount{1, -8})); } }, {{gAmmmpt(10'000'000'000), USD(10'000)}}, 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)); }, {{XRP(10'000), gAmmmpt(10'000)}}); // MPTokenIssuance object doesn't exist { Env env{*this}; env.fund(XRP(30'000), gw_, alice_); env.close(); MPT const btc = MPTTester({.env = env, .issuer = gw_, .holders = {alice_}, .pay = 30'000}); AMM ammAlice(env, alice_, XRP(10'000), btc(10'000)); ammAlice.withdraw( WithdrawArg{ .account = alice_, .asset1Out = MPT(gw_, 1'000)(10), .assets = {{XRP, MPT(gw_, 1'000)}}, .err = Ter(terNO_AMM)}); } // MPTRequireAuth flag is set and the account is not authorized { Env env{*this}; env.fund(XRP(30'000), gw_, alice_); env.close(); auto btcm = MPTTester( {.env = env, .issuer = gw_, .holders = {alice_}, .pay = 30'000, .flags = tfMPTRequireAuth | kMptDexFlags, .authHolder = true}); MPT const btc = btcm; AMM amm(env, alice_, XRP(10'000), btc(10'000)); btcm.authorize({.account = gw_, .holder = alice_, .flags = tfMPTUnauthorize}); amm.withdraw( WithdrawArg{ .account = alice_, .asset1Out = btc(100), .assets = {{XRP, btc}}, .err = Ter(tecNO_AUTH)}); } // MPTCanTransfer is not set and the account is not the issuer of MPT { Env env{*this}; env.fund(XRP(30'000), gw_, alice_); env.close(); auto btcm = MPTTester( {.env = env, .issuer = gw_, .holders = {alice_}, .pay = 30'000, .flags = tfMPTCanTrade, .authHolder = true}); MPT const btc = btcm; AMM amm(env, gw_, XRP(10'000), btc(10'000)); amm.withdraw( WithdrawArg{ .account = alice_, .asset1Out = btc(100), .assets = {{XRP, btc}}, .err = Ter(tecNO_PERMISSION)}); } // Globally locked MPT // MPTLocked flag is set and the account is not the issuer of MPT { Env env{*this}; env.fund(XRP(30'000), gw_, alice_); env.close(); MPTTester btc( {.env = env, .issuer = gw_, .holders = {alice_}, .pay = 30'000, .flags = tfMPTCanLock | kMptDexFlags, .authHolder = true}); AMM ammAlice(env, alice_, XRP(10'000), btc(10'000)); btc.set({.flags = tfMPTLock}); ammAlice.withdraw( alice_, MPT(ammAlice[1])(100), std::nullopt, std::nullopt, Ter(tecFROZEN)); ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt, Ter(tecFROZEN)); // can single withdraw the other asset ammAlice.withdraw({.account = alice_, .asset1Out = XRP(100)}); } // Individually frozen (AMM) account with MPT/MPT AMM { Env env{*this}; env.fund(XRP(10'000), gw_, alice_); env.close(); MPTTester btc( {.env = env, .issuer = gw_, .holders = {alice_}, .pay = 30'000, .flags = tfMPTCanLock | kMptDexFlags}); MPTTester const usd( {.env = env, .issuer = gw_, .holders = {alice_}, .pay = 40'000, .flags = tfMPTCanLock | kMptDexFlags}); AMM ammAlice(env, alice_, usd(10'000), btc(10'000)); // Alice's BTC is locked btc.set({.holder = alice_, .flags = tfMPTLock}); ammAlice.withdraw(alice_, 1000, std::nullopt, std::nullopt, Ter(tecFROZEN)); ammAlice.withdraw(alice_, btc(100), std::nullopt, std::nullopt, Ter(tecFROZEN)); // can withdraw the other asset ammAlice.withdraw(alice_, usd(100), std::nullopt, std::nullopt); // Unlock and then alice can withdraw btc.set({.holder = alice_, .flags = tfMPTUnlock}); ammAlice.withdraw(alice_, 1000, std::nullopt, std::nullopt); ammAlice.withdraw(alice_, btc(100), std::nullopt, std::nullopt); ammAlice.withdraw(alice_, usd(100), std::nullopt, std::nullopt); } // Individually lock MPT or freeze IOU (AMM) { Env env{*this}; fund(env, gw_, {alice_}, {USD(20'000)}, Fund::All); MPTTester btc( {.env = env, .issuer = gw_, .holders = {alice_}, .pay = 30'000, .flags = tfMPTCanLock | kMptDexFlags}); AMM ammAlice(env, alice_, USD(10'000), btc(10'000)); // Alice's BTC is locked btc.set({.holder = alice_, .flags = tfMPTLock}); ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt, Ter(tecFROZEN)); ammAlice.withdraw(alice_, btc(100), std::nullopt, std::nullopt, Ter(tecFROZEN)); // can still single withdraw the unlocked other asset ammAlice.withdraw(alice_, USD(100), std::nullopt, std::nullopt); // Unlock alice's BTC btc.set({.holder = alice_, .flags = tfMPTUnlock}); // Now alice can withdraw ammAlice.withdraw(alice_, USD(100), std::nullopt, std::nullopt); ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt); ammAlice.withdraw(alice_, btc(100), std::nullopt, std::nullopt); // Individually lock MPT BTC (AMM) account btc.set({.holder = ammAlice.ammAccount(), .flags = tfMPTLock}); // Can withdraw non-frozen token USD ammAlice.withdraw(alice_, USD(100), std::nullopt, std::nullopt); // Can not withdraw locked token BTC ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt, Ter(tecFROZEN)); ammAlice.withdraw(alice_, btc(100), std::nullopt, std::nullopt, Ter(tecFROZEN)); // Unlock AMM MPT btc.set({.holder = ammAlice.ammAccount(), .flags = tfMPTUnlock}); // Can withdraw ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt); ammAlice.withdraw(alice_, btc(100), std::nullopt, std::nullopt); // Individually frozen AMM env(trust( gw_, STAmount{Issue{gw_["USD"].currency, ammAlice.ammAccount()}, 0}, tfSetFreeze)); env.close(); // Can withdraw non-locked token BTC ammAlice.withdraw(alice_, btc(100), std::nullopt, std::nullopt); // Can not withdraw frozen token USD ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt, Ter(tecFROZEN)); ammAlice.withdraw(alice_, USD(100), std::nullopt, std::nullopt, Ter(tecFROZEN)); // Unfreeze env(trust( gw_, STAmount{Issue{gw_["USD"].currency, ammAlice.ammAccount()}, 0}, tfClearFreeze)); env.close(); // Can withdraw ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt); ammAlice.withdraw(alice_, USD(100), std::nullopt, std::nullopt); } // 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), MPT(ammAlice[1])(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), MPT(ammAlice[1])(11'000), IOUAmount{11'000'000, 0})); }, {{XRP(10'000), gAmmmpt(10'000)}}); // 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_, MPT(ammAlice[1])(100), std::nullopt, IOUAmount{500, 0}, err); }, {{XRP(10'000), gAmmmpt(10'000)}}, 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_, MPT(ammAlice[1])(100), std::nullopt, IOUAmount{600, 0}, Ter(tecAMM_INVALID_TOKENS)); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Withdraw with EPrice limit. Fails to withdraw, amount1 // to withdraw is less than 1700 MPT. testAMM( [&](AMM& ammAlice, Env&) { ammAlice.deposit(carol_, 1'000'000); ammAlice.withdraw( carol_, MPT(ammAlice[1])(1'700), std::nullopt, IOUAmount{520, 0}, Ter(tecAMM_FAILED)); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Deposit/Withdraw the same amount with the trading fee testAMM( [&](AMM& ammAlice, Env&) { ammAlice.deposit(carol_, MPT(ammAlice[1])(1'000)); ammAlice.withdraw( carol_, MPT(ammAlice[1])(1'000), std::nullopt, std::nullopt, Ter(tecAMM_INVALID_TOKENS)); }, {{XRP(10'000), gAmmmpt(10'000)}}, 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)); }, {{XRP(10'000), gAmmmpt(10'000)}}, 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)); }, {{gAmmmpt(10'000'000'000), USD(10'000)}}); // Withdraw close to one side of the pool. Account's LP tokens // are rounded to all LP tokens. testAMM( [&](AMM& ammAlice, Env&) { ammAlice.withdraw( alice_, STAmount{MPT(ammAlice[1]), UINT64_C(9'999'999999999999), -12}, std::nullopt, std::nullopt, Ter(tecAMM_BALANCE)); }, {{XRP(10'000), gAmmmpt(10'000)}}); // 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, MPT(ammAlice[0])(1), std::nullopt, Ter(tecAMM_INVALID_TOKENS)); }, {{gAmmmpt(10'000'000'000), USD(10'000)}}); } void testWithdraw() { testcase("Withdraw"); using namespace jtx; // Equal withdrawal by Carol: 1'000'000 of tokens, 10% of the current // pool testAMM( [&](AMM& ammAlice, Env& env) { // XRP/MPT XRPAmount const baseFee{env.current()->fees().base}; auto carolXRP = env.balance(carol_, XRP); auto carolMPT = env.balance(carol_, MPT(ammAlice[1])); // Single deposit of 1'000'000 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), MPT(ammAlice[1])(11'000), IOUAmount{11'000'000, 0})); BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{1'000'000, 0})); env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(1'000))); env.require(Balance(carol_, carolXRP - XRP(1'000) - drops(baseFee))); // Carol withdraws all tokens ammAlice.withdraw(carol_, 1'000'000); BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount(beast::Zero()))); env.require(Balance(carol_, carolMPT)); env.require(Balance(carol_, carolXRP - drops(2 * baseFee))); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Equal withdrawal by tokens 1000000, 10% // of the current pool, XRP/MPT testAMM( [&](AMM& ammAlice, Env&) { // XRP/MPT ammAlice.withdraw(alice_, 1'000'000); BEAST_EXPECT(ammAlice.expectBalances( XRP(9'000), MPT(ammAlice[1])(9'000), IOUAmount{9'000'000, 0})); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Equal withdrawal by tokens, 10% of the current pool, IOU/MPT // combination { auto test = [&](auto&& issue1, auto&& issue2) { Env env(*this); env.fund(XRP(30'000), alice_, gw_); env.close(); auto const usd = issue1( {.env = env, .token = "USD", .issuer = gw_, .holders = {alice_}, .limit = 1'000'000}); auto const btc = issue2( {.env = env, .token = "BTC", .issuer = gw_, .holders = {alice_}, .limit = 1'000'000}); env(pay(gw_, alice_, btc(50000))); env(pay(gw_, alice_, usd(50000))); env.close(); auto ammAlice = AMM(env, alice_, usd(10000), btc(10000)); auto aliceBTC = env.balance(alice_, btc); auto aliceUSD = env.balance(alice_, usd); ammAlice.withdraw(alice_, 1'000); BEAST_EXPECT(ammAlice.expectBalances(btc(9'000), usd(9'000), IOUAmount(9'000))); env.require(Balance(alice_, aliceBTC + btc(1000))); env.require(Balance(alice_, aliceUSD + usd(1000))); }; testHelper2TokensMix(test); } // Equal withdrawal with a limit. Withdraw XRP200. // If proportional withdraw of MPT is less than 100 // then withdraw that amount, otherwise withdraw MPT100 // and proportionally withdraw XRP. It's the latter // in this case - XRP100/MPT100. testAMM( [&](AMM& ammAlice, Env&) { // XRP/MPT ammAlice.withdraw(alice_, XRP(200), MPT(ammAlice[1])(100)); BEAST_EXPECT(ammAlice.expectBalances( XRP(9'900), MPT(ammAlice[1])(9'900), IOUAmount{9'900'000, 0})); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Equal withdrawal with a limit. XRP100/MPT200 truncated to // XRP100/MPT100 testAMM( [&](AMM& ammAlice, Env&) { // XRP/MPT ammAlice.withdraw(alice_, XRP(100), MPT(ammAlice[1])(200)); BEAST_EXPECT(ammAlice.expectBalances( XRP(9'900), MPT(ammAlice[1])(9'900), IOUAmount{9'900'000, 0})); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Equal withdrawal with a limit. IOU/MPT combination. { auto test = [&](auto&& issue1, auto&& issue2) { Env env(*this); env.fund(XRP(30'000), alice_, gw_); env.close(); auto const usd = issue1( {.env = env, .token = "USD", .issuer = gw_, .holders = {alice_}, .limit = 1'000'000}); auto const btc = issue2( {.env = env, .token = "BTC", .issuer = gw_, .holders = {alice_}, .limit = 1'000'000}); env(pay(gw_, alice_, btc(50000))); env(pay(gw_, alice_, usd(50000))); env.close(); auto ammAlice = AMM(env, alice_, usd(10000), btc(10000)); auto aliceBTC = env.balance(alice_, btc); auto aliceUSD = env.balance(alice_, usd); ammAlice.withdraw(alice_, btc(200), usd(100)); BEAST_EXPECT(ammAlice.expectBalances(btc(9'900), usd(9'900), IOUAmount(9'900))); env.require(Balance(alice_, aliceBTC + btc(100))); env.require(Balance(alice_, aliceUSD + usd(100))); }; testHelper2TokensMix(test); } // Single withdrawal by amount testAMM( [&](AMM& ammAlice, Env&) { // single withdraw XRP from XRP/MPT ammAlice.withdraw(alice_, XRP(1'000)); BEAST_EXPECT(ammAlice.expectBalances( XRPAmount(9000'000001), MPT(ammAlice[1])(10'000), IOUAmount{9'486'832'98050514, -8})); }, {{XRP(10'000), gAmmmpt(10'000)}}); testAMM( [&](AMM& ammAlice, Env&) { // single withdraw MPT from XRP/MPT ammAlice.withdraw(alice_, MPT(ammAlice[1])(1'000)); BEAST_EXPECT(ammAlice.expectBalances( XRP(10000), MPT(ammAlice[1])(9001), IOUAmount{9'486'832'98050514, -8})); }, {{XRP(10'000), gAmmmpt(10'000)}}); testAMM( [&](AMM& ammAlice, Env&) { // single withdraw IOU from IOU/MPT ammAlice.withdraw(alice_, USD(1'000)); BEAST_EXPECT(ammAlice.expectBalances( STAmount{USD, UINT64_C(9000'000000000004), -12}, MPT(ammAlice[1])(10'000), IOUAmount{9486'83298050514, -11})); }, {{USD(10'000), gAmmmpt(10'000)}}); testAMM( [&](AMM& ammAlice, Env&) { // single withdraw MPT from IOU/MPT ammAlice.withdraw(alice_, MPT(ammAlice[1])(1'000)); BEAST_EXPECT(ammAlice.expectBalances( USD(10'000), MPT(ammAlice[1])(9001), IOUAmount{9486'83298050514, -11})); }, {{USD(10'000), gAmmmpt(10'000)}}); testAMM( [&](AMM& ammAlice, Env&) { // single withdraw MPT from MPT/MPT ammAlice.withdraw(alice_, MPT(ammAlice[0])(1'000)); BEAST_EXPECT(ammAlice.expectBalances( MPT(ammAlice[0])(9001), MPT(ammAlice[1])(10'000), IOUAmount{9486'83298050514, -11})); }, {{gAmmmpt(10'000), gAmmmpt(10'000)}}); // Single withdrawal MPT by tokens 10000. XRP/MPT testAMM( [&](AMM& ammAlice, Env&) { ammAlice.withdraw(alice_, 10'000, MPT(ammAlice[1])(0)); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), MPT(ammAlice[1])(9981), IOUAmount{9'990'000, 0})); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Single withdrawal XRP by tokens 10000. XRP/MPT testAMM( [&](AMM& ammAlice, Env&) { ammAlice.withdraw(alice_, 10'000, XRP(0)); BEAST_EXPECT(ammAlice.expectBalances( XRPAmount(9980010000), MPT(ammAlice[1])(10'000), IOUAmount{9'990'000, 0})); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Single withdrawal by tokens 10000. MPT/IOU combination. { auto test = [&](auto&& issue1, auto&& issue2) { Env env(*this); env.fund(XRP(30'000), alice_, gw_); env.close(); auto const usd = issue1( {.env = env, .token = "USD", .issuer = gw_, .holders = {alice_}, .limit = 1'000'000}); auto const btc = issue2( {.env = env, .token = "BTC", .issuer = gw_, .holders = {alice_}, .limit = 1'000'000}); env(pay(gw_, alice_, btc(50000))); env(pay(gw_, alice_, usd(50000))); env.close(); auto ammAlice = AMM(env, alice_, usd(10000), btc(10000)); auto aliceBTC = env.balance(alice_, btc); auto aliceUSD = env.balance(alice_, usd); ammAlice.withdraw(alice_, 1000, btc(0)); BEAST_EXPECT(ammAlice.expectBalances(usd(10'000), btc(8100), IOUAmount{9000, 0})); env.require(Balance(alice_, aliceBTC + btc(1900))); env.require(Balance(alice_, aliceUSD)); }; testHelper2TokensMix(test); } // Withdraw all tokens. testAMM( [&](AMM& ammAlice, Env& env) { env(trust(carol_, STAmount{ammAlice.lptIssue(), 10'000})); // Can SetTrust 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/MPT pair AMM const ammCarol(env, carol_, XRP(10'000), MPT(ammAlice[1])(10'000)); BEAST_EXPECT(ammCarol.expectBalances( XRP(10'000), MPT(ammAlice[1])(10'000), IOUAmount{10'000'000, 0})); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Single deposit 1000MPT, withdraw all tokens in MPT from XRP/MPT testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit(carol_, MPT(ammAlice[1])(1'000)); ammAlice.withdrawAll(carol_, MPT(ammAlice[1])(0)); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), MPT(ammAlice[1])(10'001), IOUAmount{10'000'000, 0})); BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount(beast::Zero()))); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Single deposit 1000MPT, withdraw all tokens in XRP from XRP/MPT testAMM( [&](AMM& ammAlice, Env&) { ammAlice.deposit(carol_, MPT(ammAlice[1])(1'000)); ammAlice.withdrawAll(carol_, XRP(0)); BEAST_EXPECT(ammAlice.expectBalances( XRPAmount(9'090'909'091), MPT(ammAlice[1])(11000), IOUAmount{10'000'000, 0})); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Single deposit 1000MPT, withdraw all tokens in MPT from USD/MPT testAMM( [&](AMM& ammAlice, Env& env) { // USD/MPT ammAlice.deposit(carol_, MPT(ammAlice[1])(1'000)); ammAlice.withdrawAll(carol_, MPT(ammAlice[1])(0)); BEAST_EXPECT(ammAlice.expectBalances( USD(10'000), MPT(ammAlice[1])(10'001), IOUAmount{10'000, 0})); BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount(beast::Zero()))); }, {{USD(10'000), gAmmmpt(10'000)}}); // Single deposit 1000USD, withdraw all tokens in USD from USD/MPT testAMM( [&](AMM& ammAlice, Env& env) { // USD/MPT ammAlice.deposit(carol_, USD(1'000)); ammAlice.withdrawAll(carol_, USD(0)); BEAST_EXPECT(ammAlice.expectBalances( USD(10'000), MPT(ammAlice[1])(10'000), IOUAmount{10'000, 0})); BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount(beast::Zero()))); }, {{USD(10'000), gAmmmpt(10'000)}}); // Single deposit 1000MPT, withdraw all tokens in MPT from MPT/MPT testAMM( [&](AMM& ammAlice, Env& env) { // MPT/MPT ammAlice.deposit(carol_, MPT(ammAlice[1])(1'000)); ammAlice.withdrawAll(carol_, MPT(ammAlice[1])(0)); BEAST_EXPECT(ammAlice.expectBalances( MPT(ammAlice[0])(10'000), MPT(ammAlice[1])(10'001), IOUAmount{10'000, 0})); BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount(beast::Zero()))); }, {{gAmmmpt(10'000), gAmmmpt(10'000)}}); // Single deposit 1000MPT, withdraw all tokens in MPT testAMM( [&](AMM& ammAlice, Env&) { ammAlice.deposit(carol_, MPT(ammAlice[1])(1'000)); ammAlice.withdrawAll(carol_, MPT(ammAlice[1])(0)); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), MPT(ammAlice[1])(10001), IOUAmount{10'000'000, 0})); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Single deposit 1000MPT, withdraw all tokens in USD testAMM( [&](AMM& ammAlice, Env&) { ammAlice.deposit(carol_, MPT(ammAlice[1])(1'000)); ammAlice.withdrawAll(carol_, USD(0)); BEAST_EXPECT(ammAlice.expectBalances( STAmount{USD, UINT64_C(9'090'9090909091), -10}, MPT(ammAlice[1])(11000), IOUAmount{10'000, 0})); }, {{USD(10'000), gAmmmpt(10'000)}}); // Single deposit 1000USD, withdraw all tokens in MPT testAMM( [&](AMM& ammAlice, Env&) { ammAlice.deposit(carol_, USD(1'000)); ammAlice.withdrawAll(carol_, MPT(ammAlice[1])(0)); BEAST_EXPECT(ammAlice.expectBalances( STAmount{USD, UINT64_C(10'999'99999999999), -11}, MPT(ammAlice[1])(9091), IOUAmount{10'000, 0})); }, {{USD(10'000), gAmmmpt(10'000)}}); // Single deposit/withdraw by the same account testAMM( [&](AMM& ammAlice, Env&) { 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_, MPT(ammAlice[0])(1)); ammAlice.withdraw(carol_, lpTokens, MPT(ammAlice[0])(0)); BEAST_EXPECT(ammAlice.expectBalances( MPT(ammAlice[0])(10'000'000'001), USD(10'000), ammAlice.tokens())); BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{0})); }, {{gAmmmpt(10'000'000'000), USD(10'000)}}); testAMM( [&](AMM& ammAlice, Env&) { auto const& btc = MPT(ammAlice[1]); auto lpTokens = ammAlice.deposit(carol_, btc(1'000)); ammAlice.withdraw(carol_, lpTokens, btc(0)); lpTokens = ammAlice.deposit(carol_, btc(1)); ammAlice.withdraw(carol_, lpTokens, btc(0)); lpTokens = ammAlice.deposit(carol_, btc(1)); ammAlice.withdraw(carol_, lpTokens, btc(0)); BEAST_EXPECT(ammAlice.expectBalances(btc(10'003), XRP(10'000), ammAlice.tokens())); BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{0})); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Single deposit by different accounts and then withdraw // in reverse. testAMM( [&](AMM& ammAlice, Env&) { auto const carolTokens = ammAlice.deposit(carol_, MPT(ammAlice[1])(1'000)); auto const aliceTokens = ammAlice.deposit(alice_, MPT(ammAlice[1])(1'000)); ammAlice.withdraw(alice_, aliceTokens, MPT(ammAlice[1])(0)); ammAlice.withdraw(carol_, carolTokens, MPT(ammAlice[1])(0)); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), MPT(ammAlice[1])(10'001), ammAlice.tokens())); BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{0})); BEAST_EXPECT(ammAlice.expectLPTokens(alice_, ammAlice.tokens())); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Equal deposit 10%, withdraw all tokens. XRP/MPT testAMM( [&](AMM& ammAlice, Env&) { ammAlice.deposit(carol_, 1'000'000); ammAlice.withdrawAll(carol_); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), MPT(ammAlice[1])(10'000), IOUAmount{10'000'000, 0})); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Equal deposit 10%, withdraw all tokens. IOU/MPT combination. { auto test = [&](auto&& issue1, auto&& issue2) { Env env(*this); env.fund(XRP(30'000), alice_, carol_, gw_); env.close(); auto const usd = issue1( {.env = env, .token = "USD", .issuer = gw_, .holders = {alice_, carol_}, .limit = 1'000'000}); auto const btc = issue2( {.env = env, .token = "BTC", .issuer = gw_, .holders = {alice_, carol_}, .limit = 1'000'000}); env(pay(gw_, alice_, btc(50000))); env(pay(gw_, carol_, btc(50000))); env(pay(gw_, alice_, usd(50000))); env(pay(gw_, carol_, usd(50000))); env.close(); auto ammAlice = AMM(env, alice_, usd(10000), btc(10000)); auto carolBTC = env.balance(carol_, btc); auto carolUSD = env.balance(carol_, usd); ammAlice.deposit(carol_, 1'000); ammAlice.withdrawAll(carol_); BEAST_EXPECT( ammAlice.expectBalances(usd(10'000), btc(10'000), IOUAmount{10'000, 0})); env.require(Balance(carol_, carolBTC)); env.require(Balance(carol_, carolUSD)); }; testHelper2TokensMix(test); } // Equal deposit 10%, withdraw all tokens in MPT from XRP/MPT testAMM( [&](AMM& ammAlice, Env&) { ammAlice.deposit(carol_, 1'000'000); ammAlice.withdrawAll(carol_, MPT(ammAlice[1])(0)); BEAST_EXPECT(ammAlice.expectBalances( XRP(11'000), STAmount{MPT(ammAlice[1]), UINT64_C(9'090'909090909092), -12}, IOUAmount{10'000'000, 0})); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Equal deposit 10%, withdraw all tokens in XRP from XRP/MPT testAMM( [&](AMM& ammAlice, Env&) { ammAlice.deposit(carol_, 1'000'000); ammAlice.withdrawAll(carol_, XRP(0)); BEAST_EXPECT(ammAlice.expectBalances( XRPAmount(9'090'909'091), MPT(ammAlice[1])(11'000), IOUAmount{10'000'000, 0})); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Equal deposit 10%, withdraw all tokens in USD from USD/MPT testAMM( [&](AMM& ammAlice, Env&) { ammAlice.deposit(carol_, 1'000); ammAlice.withdrawAll(carol_, USD(0)); BEAST_EXPECT(ammAlice.expectBalances( STAmount{USD, UINT64_C(9'090'909090909092), -12}, MPT(ammAlice[1])(11'000), IOUAmount{10'000})); }, {{USD(10'000), gAmmmpt(10'000)}}); // Equal deposit 10%, withdraw all tokens in MPT from MPT/MPT testAMM( [&](AMM& ammAlice, Env&) { ammAlice.deposit(carol_, 1'000); ammAlice.withdrawAll(carol_, MPT(ammAlice[1])(0)); BEAST_EXPECT(ammAlice.expectBalances( MPT(ammAlice[0])(11'000), MPT(ammAlice[1])(9'091), IOUAmount{10'000})); }, {{gAmmmpt(10'000), gAmmmpt(10'000)}}); auto const all = testableAmendments(); // Withdraw with EPrice limit. testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit(carol_, 1'000'000'000'000); ammAlice.withdraw( carol_, MPT(ammAlice[1])(100'000000), std::nullopt, IOUAmount{520, 0}); if (!env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3)) { BEAST_EXPECT( ammAlice.expectBalances( XRP(11'000'000000), MPT(ammAlice[1])(9372781065), IOUAmount{10'153'846'15384616, -2}) && ammAlice.expectLPTokens(carol_, IOUAmount{153'846'15384616, -2})); } else if (env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3)) { BEAST_EXPECT( ammAlice.expectBalances( XRP(11'000'000000), MPT(ammAlice[1])(9372781065), IOUAmount{10'153'846'15384616, -2}) && ammAlice.expectLPTokens(carol_, IOUAmount{153'846'15384616, -2})); } else if (env.enabled(fixAMMv1_3)) { BEAST_EXPECT( ammAlice.expectBalances( XRP(11'000'000000), MPT(ammAlice[1])(9372781066), IOUAmount{10'153'846'15384616, -2}) && ammAlice.expectLPTokens(carol_, IOUAmount{153'846'15384616, -2})); } ammAlice.withdrawAll(carol_); BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{0})); }, {{XRP(10'000'000'000), gAmmmpt(10'000'000'000)}}, 0, std::nullopt, {all, all - fixAMMv1_3, all - fixAMMv1_1 - fixAMMv1_3}); // Withdraw with EPrice limit. AssetOut is 0. testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit(carol_, 1'000'000'000'000); ammAlice.withdraw(carol_, MPT(ammAlice[1])(0), std::nullopt, IOUAmount{520, 0}); if (!env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3)) { BEAST_EXPECT( ammAlice.expectBalances( XRP(11'000'000000), MPT(ammAlice[1])(9372781065), IOUAmount{10'153'846'15384616, -2}) && ammAlice.expectLPTokens(carol_, IOUAmount{153'846'15384616, -2})); } else if (env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3)) { BEAST_EXPECT( ammAlice.expectBalances( XRP(11'000'000000), MPT(ammAlice[1])(9372781065), IOUAmount{10'153'846'15384616, -2}) && ammAlice.expectLPTokens(carol_, IOUAmount{153'846'15384616, -2})); } else if (env.enabled(fixAMMv1_3)) { BEAST_EXPECT( ammAlice.expectBalances( XRP(11'000'000000), MPT(ammAlice[1])(9372781066), IOUAmount{10'153'846'15384616, -2}) && ammAlice.expectLPTokens(carol_, IOUAmount{153'846'15384616, -2})); } ammAlice.withdrawAll(carol_); BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{0})); }, {{XRP(10'000'000'000), gAmmmpt(10'000'000'000)}}, 0, std::nullopt, {all, all - fixAMMv1_3, all - fixAMMv1_1 - fixAMMv1_3}); // IOU/MPT combination + transfer fee { auto test = [&](auto&& issue1, auto&& issue2) { Env env(*this); env.fund(XRP(30'000), alice_, bob_, carol_, gw_); env.close(); auto const usd = issue1( {.env = env, .token = "USD", .issuer = gw_, .holders = {alice_, bob_, carol_}, .limit = 1'000'000, .transferFee = 25'000}); auto const btc = issue2( {.env = env, .token = "BTC", .issuer = gw_, .holders = {alice_, bob_, carol_}, .limit = 1'000'000, .transferFee = 25'000}); env(pay(gw_, alice_, btc(10000))); env(pay(gw_, bob_, btc(10000))); env(pay(gw_, carol_, btc(10000))); env(pay(gw_, alice_, usd(10000))); env(pay(gw_, bob_, usd(10000))); env(pay(gw_, carol_, usd(10000))); env.close(); // no transfer fee on create AMM ammAlice(env, alice_, btc(2'000), usd(5)); BEAST_EXPECT(ammAlice.expectBalances(btc(2'000), usd(5), IOUAmount{100, 0})); env.require(Balance(alice_, btc(8000))); env.require(Balance(alice_, usd(9995))); // no transfer fee on deposit ammAlice.deposit(carol_, 100); BEAST_EXPECT(ammAlice.expectBalances(btc(4000), usd(10), IOUAmount{200, 0})); env.require(Balance(carol_, btc(8000))); env.require(Balance(carol_, usd(9995))); // no transfer fee on withdraw ammAlice.withdraw(carol_, 100); BEAST_EXPECT(ammAlice.expectBalances(btc(2'000), usd(5), IOUAmount{100, 0})); BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{0, 0})); env.require(Balance(carol_, btc(10000))); env.require(Balance(carol_, usd(10000))); }; testHelper2TokensMix(test); } // Tiny withdraw testAMM( [&](AMM& ammAlice, Env&) { // By tokens ammAlice.withdraw(alice_, IOUAmount{1, -3}); BEAST_EXPECT(ammAlice.expectBalances( MPT(ammAlice[0])(9'999'999'999), STAmount{USD, UINT64_C(9'999'999999), -6}, IOUAmount{9'999'999'999, -3})); }, {{gAmmmpt(10'000'000'000), USD(10'000)}}); testAMM( [&](AMM& ammAlice, Env&) { // Single withdraw MPT from MPT/IOU ammAlice.withdraw(alice_, std::nullopt, MPT(ammAlice[0])(1)); BEAST_EXPECT(ammAlice.expectBalances( MPT(ammAlice[0])(10000'000000), USD(10'000), IOUAmount{9'999'999'9995, -4})); }, {{gAmmmpt(10'000'000'000), USD(10'000)}}); testAMM( [&](AMM& ammAlice, Env&) { // Single withdraw IOU from MPT/IOU ammAlice.withdraw(alice_, std::nullopt, STAmount{USD, 1, -10}); BEAST_EXPECT(ammAlice.expectBalances( MPT(ammAlice[0])(10'000'000'000), STAmount{USD, UINT64_C(9'999'9999999999), -10}, IOUAmount{9'999'999'99999995, -8})); }, {{gAmmmpt(10'000'000'000), USD(10'000)}}); testAMM( [&](AMM& ammAlice, Env&) { // Single withdraw XRP from MPT/XRP ammAlice.withdraw(alice_, std::nullopt, XRPAmount(1)); BEAST_EXPECT(ammAlice.expectBalances( MPT(ammAlice[1])(10'000), XRP(10'000), IOUAmount{9999999'9995, -4})); }, {{XRP(10'000), gAmmmpt(10'000)}}); // 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( MPT(ammAlice[0])(1), STAmount{USD, 1, -6}, IOUAmount{1, -3})); }, {{gAmmmpt(10'000'000'000), USD(10'000)}}); // USD by tokens testAMM( [&](AMM& ammAlice, Env&) { ammAlice.withdraw(alice_, IOUAmount{9'999'999}, USD(0)); BEAST_EXPECT(ammAlice.expectBalances( MPT(ammAlice[0])(10'000'000'000), STAmount{USD, 1, -10}, IOUAmount{1})); }, {{gAmmmpt(10'000'000'000), USD(10'000)}}); // MPT by tokens testAMM( [&](AMM& ammAlice, Env&) { ammAlice.withdraw(alice_, IOUAmount{9'999'900}, MPT(ammAlice[0])(0)); BEAST_EXPECT( ammAlice.expectBalances(MPT(ammAlice[0])(1), USD(10'000), IOUAmount{100})); }, {{gAmmmpt(10'000'000'000), USD(10'000)}}); // XRP by tokens testAMM( [&](AMM& ammAlice, Env&) { ammAlice.withdraw(alice_, IOUAmount{9'999'900}, XRP(0)); BEAST_EXPECT( ammAlice.expectBalances(MPT(ammAlice[1])(10000), XRPAmount(1), IOUAmount{100})); }, {{XRP(10'000), gAmmmpt(10'000)}}); // USD testAMM( [&](AMM& ammAlice, Env&) { ammAlice.withdraw(alice_, STAmount{USD, UINT64_C(9'999'99999999999), -11}); BEAST_EXPECT(ammAlice.expectBalances( MPT(ammAlice[0])(10'000'000'000), STAmount{USD, 1, -11}, IOUAmount{316227765, -9})); }, {{gAmmmpt(10'000'000'000), USD(10'000)}}); // XRP testAMM( [&](AMM& ammAlice, Env&) { ammAlice.withdraw(alice_, XRPAmount{9'999'999'999}); BEAST_EXPECT( ammAlice.expectBalances(MPT(ammAlice[1])(10000), XRPAmount(1), IOUAmount{100})); }, {{XRP(10'000), gAmmmpt(10'000)}}); // MPT testAMM( [&](AMM& ammAlice, Env&) { ammAlice.withdraw(alice_, MPT(ammAlice[0])(9'999'999'999)); BEAST_EXPECT( ammAlice.expectBalances(MPT(ammAlice[0])(1), USD(10'000), IOUAmount{100})); }, {{gAmmmpt(10'000'000'000), USD(10'000)}}); testAMM( [&](AMM& ammAlice, Env&) { ammAlice.withdraw(alice_, MPT(ammAlice[1])(9'999)); BEAST_EXPECT( ammAlice.expectBalances(MPT(ammAlice[1])(1), XRP(10'000), IOUAmount{100000})); }, {{XRP(10'000), gAmmmpt(10'000)}}); } void testInvalidFeeVote() { testcase("Invalid Fee Vote"); using namespace jtx; testAMM( [&](AMM& ammAlice, Env& env) { // Invalid Account Account 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, {{MPT(ammAlice[1]), GBP}}, Ter(terNO_AMM)); // Account is not LP ammAlice.vote( carol_, 1'000, std::nullopt, std::nullopt, std::nullopt, Ter(tecAMM_INVALID_TOKENS)); // Invalid asset pair ammAlice.vote( alice_, 1'000, std::nullopt, std::nullopt, {{MPT(ammAlice[1]), MPT(ammAlice[1])}}, Ter(temBAD_AMM_TOKENS)); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Invalid AMM testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.withdrawAll(alice_); ammAlice.vote( alice_, 1'000, std::nullopt, std::nullopt, std::nullopt, Ter(terNO_AMM)); }, {{XRP(10'000), gAmmmpt(10'000)}}); // MPTokenInstance object doesn't exist { Env env(*this); env.fund(XRP(1'000), alice_); env(AMM::voteJv({.account = alice_, .tfee = 1'000, .assets = {{XRP, MPT(alice_, 0)}}}), Ter(terNO_AMM)); } } void testFeeVote() { testcase("Fee Vote"); 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})); }, {{XRP(10'000), gAmmmpt(10'000)}}); auto vote = [&](AMM& ammAlice, Env& env, int i, std::uint32_t tokens = 10'000'000, std::vector* accounts = nullptr) { Account a(std::to_string(i)); 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%. // New vote, same account, sets fee 0.225% Env env{*this}; env.fund(XRP(30'000), gw_, alice_); std::vector holders = {alice_}; for (int i = 0; i <= 7; ++i) { Account const a(std::to_string(i)); holders.push_back(a); env.fund(XRP(30'000), a); } env.close(); // create MPT and pay 30'000 to all the accounts MPTTester const btc({.env = env, .issuer = gw_, .holders = holders, .pay = 30'000}); AMM ammAlice(env, alice_, XRP(10'000), btc(10'000)); 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% Env env{*this}; env.fund(XRP(30'000), gw_, alice_); std::vector holders = {alice_}; for (int i = 0; i < 8; ++i) { Account const a(std::to_string(i)); holders.push_back(a); env.fund(XRP(30'000), a); } env.close(); MPTTester const btc({.env = env, .issuer = gw_, .holders = holders, .pay = 30'000}); AMM ammAlice(env, alice_, XRP(10'000), btc(10'000)); for (int i = 0; i < 7; ++i) vote(ammAlice, env, i); BEAST_EXPECT(ammAlice.expectTradingFee(175)); vote(ammAlice, env, 7, 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% Env env{*this}; env.fund(XRP(30'000), gw_, alice_); std::vector holders = {alice_}; for (int i = 0; i < 8; ++i) { Account const a(std::to_string(i)); holders.push_back(a); env.fund(XRP(30'000), a); } env.close(); MPTTester const btc({.env = env, .issuer = gw_, .holders = holders, .pay = 30'000}); AMM ammAlice(env, alice_, XRP(10'000), btc(10'000)); for (int i = 7; i > 0; --i) vote(ammAlice, env, i); BEAST_EXPECT(ammAlice.expectTradingFee(219)); vote(ammAlice, env, 0, 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. Env env{*this}; env.fund(XRP(30'000), gw_, alice_, carol_); std::vector holders = {alice_, carol_}; for (int i = 0; i < 7; ++i) { Account const a(std::to_string(i)); holders.push_back(a); env.fund(XRP(30'000), a); } env.close(); MPTTester const btc({.env = env, .issuer = gw_, .holders = holders, .pay = 30'000}); AMM ammAlice(env, alice_, XRP(10'000), btc(10'000)); std::vector accounts; for (int i = 0; i < 7; ++i) vote(ammAlice, env, i, 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. Env env{*this}; env.fund(XRP(30'000), gw_, alice_, carol_); std::vector holders = {alice_, carol_}; for (int i = 0; i < 7; ++i) { Account const a(std::to_string(i)); holders.push_back(a); env.fund(XRP(30'000), a); } env.close(); MPTTester const btc({.env = env, .issuer = gw_, .holders = holders, .pay = 30'000}); AMM ammAlice(env, alice_, XRP(10'000), btc(10'000)); std::vector accounts; for (int i = 0; i < 7; ++i) vote(ammAlice, env, i, 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 (auto 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); env.fund(XRP(2'000), gw_, alice_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_}, .pay = 2'000, .flags = kMptDexFlags}); AMM amm(env, gw_, XRP(1'000), btc(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); env.fund(XRP(2'000), gw_, alice_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_}, .pay = 2'000, .flags = kMptDexFlags}); AMM amm(env, gw_, XRP(1'000), btc(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), btc(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) { ammAlice.deposit(carol_, 1'000'000); // Invlaid Min/Max combination env(ammAlice.bid({ .account = carol_, .bidMin = 200, .bidMax = 100, }), Ter(tecAMM_INVALID_TOKENS)); // Invalid Account Account 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 = {{MPT(ammAlice[1]), GBP}}, }), Ter(terNO_AMM)); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Invalid AMM testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.withdrawAll(alice_); env(ammAlice.bid({ .account = alice_, .bidMax = 100, }), Ter(terNO_AMM)); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Bid price exceeds LP owned tokens { Env env(*this); fund(env, gw_, {alice_, bob_, carol_}, XRP(1'000), {USD(30'000)}, Fund::All); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, carol_, bob_}, .pay = 30'000'000'000, .flags = kMptDexFlags}); AMM ammAlice(env, alice_, btc(10'000'000'000), USD(10'000)); 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); env.fund(XRP(1'000), gw_, alice_, bob_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .pay = 1'000, .flags = kMptDexFlags}); AMM amm(env, gw_, XRP(10), btc(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; // 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), MPT(ammAlice[1])(11'000), IOUAmount{10'999'890, 0})); }, {{XRP(10'000), gAmmmpt(10'000)}}); // 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), MPT(ammAlice[1])(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), MPT(ammAlice[1])(11'000), IOUAmount{10'999'814'5, -1})); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Start bid at bidMin 110. { Env env(*this); env.fund(XRP(30'000), gw_, alice_, carol_, bob_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, carol_, bob_}, .pay = 30'000, .flags = kMptDexFlags}); AMM ammAlice(env, alice_, XRP(10'000), btc(10'000)); ammAlice.deposit(carol_, 1'000'000); // Bid, pay bidMin. env(ammAlice.bid({.account = carol_, .bidMin = 110})); BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110})); 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})); } // Slot states. { Env env(*this); env.fund(XRP(30'000), gw_, alice_, carol_, bob_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, carol_, bob_}, .pay = 30'000, .flags = kMptDexFlags}); AMM ammAlice(env, alice_, XRP(10'000), btc(10'000)); ammAlice.deposit(carol_, 1'000'000); ammAlice.deposit(bob_, 1'000'000); BEAST_EXPECT(ammAlice.expectBalances( XRPAmount(12'000'000001), btc(12'001), 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(kAuctionSlotIntervalDuration + 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 * kAuctionSlotIntervalDuration) + 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((kAuctionSlotTimeIntervals * kAuctionSlotIntervalDuration) + 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. BEAST_EXPECT(ammAlice.expectBalances( XRPAmount(12'000'000001), btc(12'001), IOUAmount{11'999'678'91000001, -8})); } // 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. { Env env(*this); Account const dan("dan"); Account const ed("ed"); env.fund(XRP(2'000), gw_, alice_, bob_, carol_, dan, ed); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_, dan, ed}, .pay = 30'000'000'000}); fund(env, gw_, {alice_, carol_}, {USD(30'000)}, Fund::TokenOnly); fund(env, gw_, {bob_, dan, ed}, {USD(20'000)}, Fund::TokenOnly); AMM ammAlice(env, alice_, btc(10'000'000'000), USD(10'000), CreateArg{.tfee = 1'000}); 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)); BEAST_EXPECT(ammAlice.expectBalances(btc(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. 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( btc(13'000'000'003), STAmount(USD, UINT64_C(13'002'98282151419), -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. BEAST_EXPECT(env.balance(dan, USD) == STAmount(USD, UINT64_C(19'490'05672274398), -11)); // USD pool gains more in dan's fees. BEAST_EXPECT(ammAlice.expectBalances( btc(13'000'000'003), STAmount{USD, UINT64_C(13'012'92609877021), -11}, ammTokens)); // Discounted fee payment ammAlice.deposit(carol_, USD(100)); ammTokens = ammAlice.getLPTokensBalance(); BEAST_EXPECT(ammAlice.expectBalances( MPT(ammAlice[0])(13'000'000'003), STAmount{USD, UINT64_C(13'112'92609877019), -11}, ammTokens)); env(pay(carol_, bob_, USD(100)), Path(~USD), Sendmax(btc(110'000'000))); env.close(); // carol pays 100000 drops in fees // 99900668MPT swapped in for 100USD BEAST_EXPECT(ammAlice.expectBalances( btc(13'100'000'671), STAmount{USD, UINT64_C(13'012'92609877019), -11}, ammTokens)); // Payment with the trading fee env(pay(alice_, carol_, btc(100'000'000)), Path(~MPT(ammAlice[0])), 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 100MPT BEAST_EXPECT(ammAlice.expectBalances( btc(13'000'000'671), STAmount{USD, UINT64_C(13'114'03663044931), -11}, ammTokens)); // Auction slot expired, no discounted fee env.close(seconds(kTotalTimeSlotSecs + 1)); // clock is parent's based env.close(); BEAST_EXPECT( env.balance(carol_, USD) == STAmount(USD, UINT64_C(29'399'00572620547), -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. BEAST_EXPECT( env.balance(carol_, USD) == STAmount(USD, UINT64_C(29'389'06197177122), -11)); BEAST_EXPECT(ammAlice.expectBalances( btc(13'000'000'671), STAmount{USD, UINT64_C(13'123'98038488356), -11}, ammTokens)); env(pay(carol_, bob_, USD(100)), Path(~USD), Sendmax(btc(110'000'000))); env.close(); // carol pays ~1.008MPT in trading fee, which is // ~10 times more than the discounted fee. // 99.815876MPT is swapped in for 100USD BEAST_EXPECT(ammAlice.expectBalances( btc(13'100'824'793), STAmount{USD, UINT64_C(13'023'98038488356), -11}, ammTokens)); } // Bid tiny amount testAMM( [&](AMM& ammAlice, Env& env) { // Bid a tiny amount auto const tiny = Number{STAmount::kMinValue, STAmount::kMinOffset}; 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( MPT(ammAlice[0])(10'000'000'000), USD(10'000), ammAlice.tokens())); // Bid the tiny amount env(ammAlice.bid({ .account = alice_, .bidMin = IOUAmount{STAmount::kMinValue, STAmount::kMinOffset}, })); // 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( MPT(ammAlice[0])(10'000'000'000), USD(10'000), ammAlice.tokens())); }, {{gAmmmpt(10'000'000'000), USD(10'000)}}); // 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 bob("bob"); Account 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})); }, {{gAmmmpt(10'000'000'000), USD(10'000)}}); // Bid all tokens, still own the slot and trade at a discount { Env env(*this); env.fund(XRP(2'000), gw_, alice_, bob_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .pay = 2'000'000'000, .flags = kMptDexFlags}); fund(env, gw_, {alice_, bob_}, {USD(2'000)}, Fund::TokenOnly); AMM amm(env, gw_, btc(1'000'000'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 MPT in fees env(pay(alice_, bob_, USD(10)), Path(~USD), Sendmax(btc(11'000'000))); BEAST_EXPECT(amm.expectBalances( btc(1'010'010'011), USD(1'000), IOUAmount{1'004'487'562112089, -9})); // Bob pays the full fee ~0.1USD env(pay(bob_, alice_, btc(10'000'000)), Path(~MPT(btc)), Sendmax(USD(11))); BEAST_EXPECT(amm.expectBalances( btc(1'000'010'011), STAmount{USD, UINT64_C(1'010'10090898081), -11}, IOUAmount{1'004'487'562112089, -9})); } // preflight tests { Env env(*this, features); env.fund(XRP(2'000), gw_, alice_, bob_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .pay = 2'000, .flags = kMptDexFlags}); AMM amm(env, gw_, XRP(1'000), btc(1'010), false, 1'000); json::Value const tx = amm.bid({.account = alice_, .bidMin = 500}); { auto jtx = env.jt(tx, Seq(1), Fee(10)); env.app().config().features.erase(featureMPTokensV2); PreflightContext const pfctx( env.app(), *jtx.stx, env.current()->rules(), TapNone, env.journal); auto pf = AMMBid::checkExtraFeatures(pfctx); BEAST_EXPECT(pf == false); env.app().config().features.insert(featureMPTokensV2); } { auto jtx = env.jt(tx, Seq(1), Fee(10)); jtx.jv["Asset2"]["currency"] = "XRP"; jtx.jv["Asset2"].removeMember("mpt_issuance_id"); jtx.stx = env.ust(jtx); PreflightContext const pfctx( env.app(), *jtx.stx, env.current()->rules(), TapNone, env.journal); auto pf = AMMBid::preflight(pfctx); BEAST_EXPECT(pf == temBAD_AMM_TOKENS); } } } void testClawback() { testcase("Clawback"); using namespace jtx; Env env(*this); env.fund(XRP(2'000), gw_, alice_); MPT const btc = MPTTester( {.env = env, .issuer = gw_, .holders = {alice_}, .transferFee = 1'500, .pay = 40'000}); // alice creates AMM AMM const amm(env, alice_, XRP(1'000), btc(1'000)); // gw owns MPTIssuance, not allowed to set asfAllowTrustLineClawback env(fset(gw_, asfAllowTrustLineClawback), Ter(tecOWNERS)); } void testClawbackFromAMMAccount(FeatureBitset features) { testcase("test clawback from AMM account"); using namespace jtx; 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); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_}, .pay = 30'000, .flags = tfMPTCanLock | kMptDexFlags}); // to clawback from AMM account, must use AMMClawback instead of // Clawback auto const err = features[featureSingleAssetVault] ? tecPSEUDO_ACCOUNT : tecAMM_ACCOUNT; AMM const amm(env, gw_, XRP(100), btc(100)); auto amount = amountFromString(amm.lptIssue(), "10"); env(claw(gw_, amount), Ter(err)); AMM const amm1(env, alice_, USD(100), btc(200)); auto amount1 = amountFromString(amm1.lptIssue(), "10"); env(claw(gw_, amount1), Ter(err)); } void testInvalidAMMPayment() { testcase("Invalid AMM Payment"); using namespace jtx; using namespace jtx::paychan; 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)); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .pay = 100, .flags = kMptDexFlags}); // XRP balance is below reserve AMM const ammAlice(env, acct, XRP(10), btc(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 MPT env(pay(carol_, ammAlice.ammAccount(), btc(10)), Ter(tecNO_PERMISSION)); } { Env env(*this); fund(env, gw_, {alice_, carol_}, XRP(10'000'000)); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .pay = 20'000, .flags = kMptDexFlags}); // XRP balance is above reserve AMM const ammAlice(env, acct, XRP(1'000'000), btc(10'000)); // 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)); // Pay MPT env(pay(carol_, ammAlice.ammAccount(), btc(1'000)), Ter(tecNO_PERMISSION)); } } // Can't pay into AMM with escrow. testAMM( [&](AMM& ammAlice, Env& env) { env(escrow::create(carol_, ammAlice.ammAccount(), MPT(ammAlice[1])(1)), escrow::kCondition(escrow::kCb1), escrow::kFinishTime(env.now() + 1s), escrow::kCancelTime(env.now() + 2s), Fee(1'500), Ter(tecNO_PERMISSION)); env(escrow::create(carol_, ammAlice.ammAccount(), XRP(1)), escrow::kCondition(escrow::kCb1), escrow::kFinishTime(env.now() + 1s), escrow::kCancelTime(env.now() + 2s), Fee(1'500), Ter(tecNO_PERMISSION)); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Can't pay into AMM with paychan. testAMM( [&](AMM& ammAlice, Env& env) { auto const pk = carol_.pk(); auto const settleDelay = 10s; NetClock::time_point const cancelAfter = env.current()->header().parentCloseTime + 20s; env(create( carol_, ammAlice.ammAccount(), MPT(ammAlice[1])(1'000), settleDelay, pk, cancelAfter), Ter(telENV_RPC_FAILED)); env(create(carol_, ammAlice.ammAccount(), XRP(1'000), settleDelay, pk, cancelAfter), Ter(tecNO_PERMISSION)); }, {{XRP(10'000), gAmmmpt(10'000)}}); // 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)); env(check::create(env.master.id(), ammAlice.ammAccount(), MPT(ammAlice[1])(100)), Ter(tecNO_PERMISSION)); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Pay amounts close to one side of the pool testAMM( [&](AMM& ammAlice, Env& env) { auto const& btc = MPT(ammAlice[1]); // Can't consume whole pool env(pay(alice_, carol_, USD(100)), Path(~USD), Sendmax(btc(1'000'000'000)), Ter(tecPATH_PARTIAL)); env(pay(alice_, carol_, btc(100'000'000)), Path(~btc), Sendmax(USD(1'000'000'000)), Ter(tecPATH_PARTIAL)); // Overflow env(pay(alice_, carol_, STAmount{USD, UINT64_C(99'999999999), -9}), Path(~USD), Sendmax(btc(1'000'000'000)), Ter(tecPATH_PARTIAL)); env(pay(alice_, carol_, STAmount{USD, UINT64_C(999'99999999), -8}), Path(~USD), Sendmax(btc(1'000'000'000)), Ter(tecPATH_PARTIAL)); env(pay(alice_, carol_, btc(99'999'999)), Path(~btc), 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(btc(1'000'000'000)), Ter(tecPATH_PARTIAL)); env(pay(alice_, carol_, btc(99'990'000)), Path(~btc), Sendmax(USD(1'000'000'000)), Ter(tecPATH_PARTIAL)); }, {{USD(100), gAmmmpt(100'000'000)}}); // Globally locked MPT. { Env env(*this); env.fund(XRP(30'000), gw_, alice_, carol_); MPTTester btc( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .pay = 30'000, .flags = tfMPTCanLock | kMptDexFlags}); AMM const ammAlice(env, alice_, XRP(10'000), btc(10'000)); btc.set({.flags = tfMPTLock}); env(pay(alice_, carol_, btc(1)), Path(~static_cast(btc)), Txflags(tfPartialPayment | tfNoRippleDirect), Sendmax(XRP(10)), Ter(tecPATH_DRY)); env(pay(alice_, carol_, XRP(1)), Path(~XRP), Txflags(tfPartialPayment | tfNoRippleDirect), Sendmax(btc(10)), Ter(tecPATH_DRY)); } // Individually locked MPT destination account. { Env env(*this); env.fund(XRP(30'000), gw_, alice_, carol_); MPTTester btc( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .pay = 30'000, .flags = tfMPTCanLock | kMptDexFlags}); AMM const ammAlice(env, alice_, XRP(10'000), btc(10'000)); btc.set({.holder = carol_, .flags = tfMPTLock}); env(pay(alice_, carol_, btc(1)), Path(~static_cast(btc)), Txflags(tfPartialPayment | tfNoRippleDirect), Sendmax(XRP(10)), Ter(tecPATH_DRY)); } // Individually locked MPT source account { Env env(*this); env.fund(XRP(30'000), gw_, alice_, carol_); MPTTester btc( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .pay = 30'000, .flags = tfMPTCanLock | kMptDexFlags}); AMM const ammAlice(env, alice_, XRP(10'000), btc(10'000)); btc.set({.holder = alice_, .flags = tfMPTLock}); env(pay(alice_, carol_, XRP(1)), Path(~XRP), Txflags(tfPartialPayment | tfNoRippleDirect), Sendmax(btc(10)), Ter(tecPATH_DRY)); } // lock on both sides { Env env(*this); env.fund(XRP(30'000), gw_, alice_, carol_); MPTTester btc( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .pay = 30'000, .flags = tfMPTCanLock | kMptDexFlags}); MPTTester eth( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .pay = 30'000, .flags = tfMPTCanLock | kMptDexFlags}); AMM const ammAlice(env, alice_, eth(10'000), btc(10'000)); btc.set({.holder = carol_, .flags = tfMPTLock}); btc.set({.holder = alice_, .flags = tfMPTLock}); eth.set({.holder = carol_, .flags = tfMPTLock}); eth.set({.holder = alice_, .flags = tfMPTLock}); env(pay(alice_, carol_, eth(1)), Path(~MPT(eth)), Txflags(tfPartialPayment | tfNoRippleDirect), Sendmax(btc(10)), Ter(tecPATH_DRY)); env(pay(alice_, carol_, btc(1)), Path(~MPT(btc)), Txflags(tfPartialPayment | tfNoRippleDirect), Sendmax(eth(10)), Ter(tecPATH_DRY)); } // Individually locked AMM MPT { Env env(*this); env.fund(XRP(30'000), gw_, alice_, carol_); MPTTester btc( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .pay = 30'000, .flags = tfMPTCanLock | kMptDexFlags}); AMM const ammAlice(env, alice_, XRP(10'000), btc(10'000)); btc.set({.holder = ammAlice.ammAccount(), .flags = tfMPTLock}); env(pay(alice_, carol_, XRP(1)), Path(~XRP), Txflags(tfPartialPayment | tfNoRippleDirect), Sendmax(btc(10)), Ter(tecPATH_DRY)); } } void testBasicPaymentEngine() { testcase("Basic Payment"); using namespace jtx; // Payment 100MPT for 100XRP. // Force one path with tfNoRippleDirect. testAMM( [&](AMM& ammAlice, Env& env) { auto carolMPT = env.balance(carol_, MPT(ammAlice[1])); env.fund(XRP(30'000), bob_); env.close(); env(pay(bob_, carol_, MPT(ammAlice[1])(100)), Path(~MPT(ammAlice[1])), Sendmax(XRP(100)), Txflags(tfNoRippleDirect)); env.close(); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'100), MPT(ammAlice[1])(10'000), ammAlice.tokens())); // Initial balance + 100 env.require(Balance(carol_, carolMPT + MPT(ammAlice[1])(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), gAmmmpt(10'100)}}); // Payment 100IOU/MPT for 100IOU/MPT. Test IOU/MPT mix. // Force one path with tfNoRippleDirect. { auto test = [&](auto&& issue1, auto&& issue2) { Env env(*this); env.fund(XRP(30'000), alice_, bob_, carol_, gw_); env.close(); auto const usd = issue1( {.env = env, .token = "USD", .issuer = gw_, .holders = {alice_, bob_, carol_}, .limit = 1'000'000}); auto const btc = issue2( {.env = env, .token = "BTC", .issuer = gw_, .holders = {alice_, bob_, carol_}, .limit = 1'000'000}); env(pay(gw_, alice_, btc(50000))); env(pay(gw_, bob_, btc(50000))); env(pay(gw_, carol_, btc(50000))); env(pay(gw_, alice_, usd(50000))); env(pay(gw_, bob_, usd(50000))); env(pay(gw_, carol_, usd(50000))); env.close(); auto ammAlice = AMM(env, alice_, usd(10000), btc(10100)); auto carolBTC = env.balance(carol_, btc); auto bobUSD = env.balance(bob_, usd); env(pay(bob_, carol_, btc(100)), Path(~btc), Sendmax(usd(100)), Txflags(tfNoRippleDirect | tfPartialPayment)); env.close(); BEAST_EXPECT(ammAlice.expectBalances(usd(10'100), btc(10'000), ammAlice.tokens())); env.require(Balance(carol_, carolBTC + btc(100))); env.require(Balance(bob_, bobUSD - usd(100))); }; testHelper2TokensMix(test); } // Payment 100MPT for 100XRP, use default path. testAMM( [&](AMM& ammAlice, Env& env) { auto carolMPT = env.balance(carol_, MPT(ammAlice[1])); env.fund(XRP(30'000), bob_); env.close(); env(pay(bob_, carol_, MPT(ammAlice[1])(100)), Sendmax(XRP(100))); env.close(); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'100), MPT(ammAlice[1])(10'000), ammAlice.tokens())); // Initial balance + 100 env.require(Balance(carol_, carolMPT + MPT(ammAlice[1])(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), gAmmmpt(10'100)}}); // Payment 100IOU/MPT for 100IOU/MPT using default path. // Test IOU/MPT mix. { auto test = [&](auto&& issue1, auto&& issue2) { Env env(*this); env.fund(XRP(30'000), alice_, bob_, carol_, gw_); env.close(); auto const usd = issue1( {.env = env, .token = "USD", .issuer = gw_, .holders = {alice_, bob_, carol_}, .limit = 1'000'000}); auto const btc = issue2( {.env = env, .token = "BTC", .issuer = gw_, .holders = {alice_, bob_, carol_}, .limit = 1'000'000}); env(pay(gw_, alice_, btc(50000))); env(pay(gw_, bob_, btc(50000))); env(pay(gw_, carol_, btc(50000))); env(pay(gw_, alice_, usd(50000))); env(pay(gw_, bob_, usd(50000))); env(pay(gw_, carol_, usd(50000))); env.close(); auto ammAlice = AMM(env, alice_, usd(10000), btc(10100)); auto carolBTC = env.balance(carol_, btc); auto bobUSD = env.balance(bob_, usd); env(pay(bob_, carol_, btc(100)), Sendmax(usd(100))); env.close(); BEAST_EXPECT(ammAlice.expectBalances(usd(10'100), btc(10'000), ammAlice.tokens())); env.require(Balance(carol_, carolBTC + btc(100))); env.require(Balance(bob_, bobUSD - usd(100))); }; testHelper2TokensMix(test); } // This payment is identical to above. While it has // both default path and path, activeStrands has one path. testAMM( [&](AMM& ammAlice, Env& env) { auto carolMPT = env.balance(carol_, MPT(ammAlice[1])); env.fund(XRP(30'000), bob_); env.close(); env(pay(bob_, carol_, MPT(ammAlice[1])(100)), Path(~MPT(ammAlice[1])), Sendmax(XRP(100))); env.close(); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'100), MPT(ammAlice[1])(10'000), ammAlice.tokens())); // Initial balance + 100 env.require(Balance(carol_, carolMPT + MPT(ammAlice[1])(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), gAmmmpt(10'100)}}); // Test MPT/IOU combination for the case above. { auto test = [&](auto&& issue1, auto&& issue2) { Env env(*this); env.fund(XRP(30'000), alice_, bob_, carol_, gw_); env.close(); auto const usd = issue1( {.env = env, .token = "USD", .issuer = gw_, .holders = {alice_, bob_, carol_}, .limit = 1'000'000}); auto const btc = issue2( {.env = env, .token = "BTC", .issuer = gw_, .holders = {alice_, bob_, carol_}, .limit = 1'000'000}); env(pay(gw_, alice_, btc(50000))); env(pay(gw_, bob_, btc(50000))); env(pay(gw_, carol_, btc(50000))); env(pay(gw_, alice_, usd(50000))); env(pay(gw_, bob_, usd(50000))); env(pay(gw_, carol_, usd(50000))); env.close(); auto ammAlice = AMM(env, alice_, usd(10000), btc(10100)); auto carolBTC = env.balance(carol_, btc); auto bobUSD = env.balance(bob_, usd); env(pay(bob_, carol_, btc(100)), Path(~btc), Sendmax(usd(100))); env.close(); BEAST_EXPECT(ammAlice.expectBalances(usd(10'100), btc(10'000), ammAlice.tokens())); env.require(Balance(carol_, carolBTC + btc(100))); env.require(Balance(bob_, bobUSD - usd(100))); }; testHelper2TokensMix(test); } // Payment with limitQuality set. testAMM( [&](AMM& ammAlice, Env& env) { auto carolMPT = env.balance(carol_, MPT(ammAlice[1])); env.fund(jtx::XRP(30'000), bob_); env.close(); // Pays 10MPT for 10XRP. A larger payment of ~99.11MPT/100XRP // would have been sent has it not been for limitQuality. env(pay(bob_, carol_, MPT(ammAlice[1])(100)), Path(~MPT(ammAlice[1])), Sendmax(XRP(100)), Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality)); env.close(); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'010), MPT(ammAlice[1])(10'000), ammAlice.tokens())); // Initial balance + 10(limited by limitQuality) env.require(Balance(carol_, carolMPT + MPT(ammAlice[1])(10))); // 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.91MPT/110XRP has it not been for limitQuality. env(pay(bob_, carol_, MPT(ammAlice[1])(100)), Path(~MPT(ammAlice[1])), Sendmax(XRP(100)), Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality), Ter(tecPATH_DRY)); env.close(); }, {{XRP(10'000), gAmmmpt(10'010)}}); // Payment with limitQuality set. MPT/IOU combination. { auto test = [&](auto&& issue1, auto&& issue2) { Env env(*this); env.fund(XRP(30'000), alice_, bob_, carol_, gw_); env.close(); auto const usd = issue1( {.env = env, .token = "USD", .issuer = gw_, .holders = {alice_, bob_, carol_}, .limit = 1'000'000}); auto const btc = issue2( {.env = env, .token = "BTC", .issuer = gw_, .holders = {alice_, bob_, carol_}, .limit = 1'000'000}); env(pay(gw_, alice_, btc(50000))); env(pay(gw_, bob_, btc(50000))); env(pay(gw_, carol_, btc(50000))); env(pay(gw_, alice_, usd(50000))); env(pay(gw_, bob_, usd(50000))); env(pay(gw_, carol_, usd(50000))); env.close(); auto ammAlice = AMM(env, alice_, usd(10000), btc(10010)); auto carolBTC = env.balance(carol_, btc); auto bobUSD = env.balance(bob_, usd); env(pay(bob_, carol_, btc(100)), Path(~btc), Sendmax(usd(100)), Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality)); env.close(); BEAST_EXPECT(ammAlice.expectBalances(usd(10'010), btc(10'000), ammAlice.tokens())); env.require(Balance(carol_, carolBTC + btc(10))); env.require(Balance(bob_, bobUSD - usd(10))); env(pay(bob_, carol_, btc(100)), Path(~btc), Sendmax(usd(100)), Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality), Ter(tecPATH_DRY)); env.close(); }; testHelper2TokensMix(test); } // Payment with limitQuality and transfer fee set. { Env env(*this); env.fund(XRP(30'000), gw_, alice_, bob_, carol_); env.close(); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .transferFee = 10'000, .pay = 30'000'000'000'000'000, .flags = kMptDexFlags}); auto ammAlice = AMM(env, alice_, XRP(10'000), btc(10'010'000'000'000'000)); env.close(); auto carolMPT = env.balance(carol_, MPT(btc)); // Pays 10'000'000'000'000MPT for 10XRP. A larger payment of // ~99'110'000'000'000MPT/100XRP would have been sent has it not // been for limitQuality and the transfer fee. env(pay(bob_, carol_, btc(100'000'000'000'000)), Path(~MPT(btc)), Sendmax(XRP(110)), Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality)); env.close(); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'010), btc(10'000'000'000'000'000), ammAlice.tokens())); // 10'000'000'000'000MPT - 10% transfer fee env.require(Balance(carol_, carolMPT + btc(9'090'909'090'909))); BEAST_EXPECT(expectLedgerEntryRoot(env, bob_, XRP(30'000) - XRP(10) - txFee(env, 1))); } // Payment with limitQuality and transfer fee set. MPT/IOU combination. { auto test = [&](auto&& issue1, auto&& issue2) { Env env(*this); env.fund(XRP(30'000), alice_, bob_, carol_, gw_); env.close(); auto const usd = issue1({ .env = env, .token = "USD", .issuer = gw_, .holders = {alice_, bob_, carol_}, .limit = 1'000'000 //.transferFee = 10'000 }); auto const btc = issue2( {.env = env, .token = "BTC", .issuer = gw_, .holders = {alice_, bob_, carol_}, .limit = 300'000'000'000'000'000, .transferFee = 10'000}); env(pay(gw_, alice_, btc(30'000'000'000'000'000))); env(pay(gw_, bob_, btc(30'000'000'000'000'000))); env(pay(gw_, carol_, btc(30'000'000'000'000'000))); env(pay(gw_, alice_, usd(50000))); env(pay(gw_, bob_, usd(50000))); env(pay(gw_, carol_, usd(50000))); env.close(); auto ammAlice = AMM(env, alice_, usd(10'000), btc(10'010'000'000'000'000)); auto carolBTC = env.balance(carol_, btc); auto bobUSD = env.balance(bob_, usd); env(pay(bob_, carol_, btc(100'000'000'000'000)), Path(~btc), Sendmax(usd(110)), Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality)); env.close(); BEAST_EXPECT(ammAlice.expectBalances( usd(10'010), btc(10'000'000'000'000'000), ammAlice.tokens())); env.require(Balance(carol_, carolBTC + btc(9'090'909'090'909))); env.require(Balance(bob_, bobUSD - usd(10))); }; testHelper2TokensMix(test); } // 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_, MPT(ammAlice[1])(100)), Path(~MPT(ammAlice[1])), Sendmax(XRP(100)), Txflags(tfNoRippleDirect), Ter(tecPATH_PARTIAL)); }, {{XRP(10'000), gAmmmpt(10'000)}}); // Fail when partial payment is not set. MPT/IOU combination. { auto test = [&](auto&& issue1, auto&& issue2) { Env env(*this); env.fund(XRP(30'000), alice_, bob_, carol_, gw_); env.close(); auto const usd = issue1( {.env = env, .token = "USD", .issuer = gw_, .holders = {alice_, bob_, carol_}, .limit = 1'000'000}); auto const btc = issue2( {.env = env, .token = "BTC", .issuer = gw_, .holders = {alice_, bob_, carol_}, .limit = 1'000'000}); env(pay(gw_, alice_, btc(50000))); env(pay(gw_, bob_, btc(50000))); env(pay(gw_, carol_, btc(50000))); env(pay(gw_, alice_, usd(50000))); env(pay(gw_, bob_, usd(50000))); env(pay(gw_, carol_, usd(50000))); env.close(); auto ammAlice = AMM(env, alice_, usd(10000), btc(10000)); env(pay(bob_, carol_, btc(100)), Path(~btc), Sendmax(usd(100)), Txflags(tfNoRippleDirect), Ter(tecPATH_PARTIAL)); }; testHelper2TokensMix(test); } // Non-default path (with AMM) has a better quality than default path. // The max possible liquidity is taken out of non-default // path ~29.9e14XRP/29.9e14ETH, ~29.9e14ETH/~29.99e14btc. The rest // is taken from the offer. { Env env(*this); env.fund(XRP(30'000), gw_, alice_, carol_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .pay = 3'000'000'000'000'000'000, .flags = kMptDexFlags}); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .pay = 3'000'000'000'000'000'000, .flags = kMptDexFlags}); env.fund(XRP(1'000), bob_); env.close(); auto ammEthXrp = AMM(env, alice_, XRP(10'000), eth(1'000'000'000'000'000'000)); auto ammBtcEth = AMM(env, alice_, eth(1'000'000'000'000'000'000), btc(1'000'000'000'000'000'000)); env(offer(alice_, XRP(101), btc(10'000'000'000'000'000)), Txflags(tfPassive)); env.close(); env(pay(bob_, carol_, btc(10'000'000'000'000'000)), Path(~MPT(eth), ~MPT(btc)), Sendmax(XRP(102)), Txflags(tfPartialPayment)); env.close(); BEAST_EXPECT(ammEthXrp.expectBalances( XRPAmount(10'030'082'730), eth(9'970'00749812546872), ammEthXrp.tokens())); BEAST_EXPECT(ammBtcEth.expectBalances( btc(9'970'09727766213961), eth(10'029'99250187453128), ammBtcEth.tokens())); Amounts const expectedAmounts = Amounts{XRPAmount(30'201'749), btc(29'90272233786039)}; BEAST_EXPECT(expectOffers(env, alice_, 1, {{expectedAmounts}})); // Initial (30,000 + 100)e14 env.require(Balance(carol_, btc(3'010'000'000'000'000'000))); // 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/49e14BTC. The rest is taken from the offer. { Env env(*this); env.fund(XRP(30'000), gw_, alice_, carol_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .pay = 3'000'000'000'000'000'000, .flags = kMptDexFlags}); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_}, .pay = 1'000'000'000'000'000'000, .flags = kMptDexFlags}); auto ammAlice = AMM(env, alice_, XRP(10'000), btc(1'000'000'000'000'000'000)); env.fund(XRP(1'000), bob_); env.close(); env(offer(alice_, XRP(101), eth(10'000'000'000'000'000)), Txflags(tfPassive)); env.close(); env(offer(alice_, eth(10'000'000'000'000'000), btc(10'000'000'000'000'000)), Txflags(tfPassive)); env.close(); env(pay(bob_, carol_, btc(10'000'000'000'000'000)), Path(~MPT(eth), ~MPT(btc)), Sendmax(XRP(102)), Txflags(tfPartialPayment)); env.close(); BEAST_EXPECT(ammAlice.expectBalances( XRPAmount(10'050'238'637), btc(9'950'01249687578120), ammAlice.tokens())); BEAST_EXPECT(expectOffers( env, alice_, 2, {{Amounts{XRPAmount(50'487'378), eth(49'98750312421880)}, Amounts{eth(49'98750312421880), btc(49'98750312421880)}}})); // Initial (30,000 + 100)e14 env.require(Balance(carol_, btc(30'100'00000000000000))); // 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))); } // Default path with AMM and Order Book offer. AMM is consumed first, // remaining amount is consumed by the offer. { Env env(*this); fund(env, gw_, {alice_, bob_, carol_}, XRP(30'000)); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 30'000'000'000'000'000, .flags = kMptDexFlags}); AMM ammAlice(env, alice_, XRP(10'000), btc(10'100'000'000'000'000)); env(offer(bob_, XRP(100), MPT(ammAlice[1])(100'000'000'000'000)), Txflags(tfPassive)); env.close(); env(pay(alice_, carol_, MPT(ammAlice[1])(200'000'000'000'000)), Sendmax(XRP(200)), Txflags(tfPartialPayment)); env.close(); BEAST_EXPECT(ammAlice.expectBalances( XRP(10'100), MPT(ammAlice[1])(10'000'000000000001), ammAlice.tokens())); env.require(Balance(carol_, MPT(ammAlice[1])(30'199'999999999999))); // Initial 30,000 - 10000(AMM pool LP) - 100(AMMoffer) - // - 100(offer) - 10(tx fee) - 10(tx fee of MPTTester init as // holder) - one reserve BEAST_EXPECT(expectLedgerEntryRoot( env, alice_, XRP(30'000) - XRP(10'000) - XRP(100) - XRP(100) - ammCrtFee(env) - 2 * txFee(env, 1))); BEAST_EXPECT(expectOffers(env, bob_, 0)); } // Default path with AMM and Order Book offer. // Order Book offer is consumed first. // Remaining amount is consumed by AMM. { Env env(*this); fund(env, gw_, {alice_, bob_, carol_}, XRP(20'000)); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 2'000, .flags = kMptDexFlags}); env(offer(bob_, XRP(50), btc(150)), Txflags(tfPassive)); env.close(); AMM const ammAlice(env, alice_, XRP(1'000), btc(1'050)); env(pay(alice_, carol_, btc(200)), Sendmax(XRP(200)), Txflags(tfPartialPayment)); BEAST_EXPECT(ammAlice.expectBalances(XRP(1'050), btc(1'000), ammAlice.tokens())); env.require(Balance(carol_, btc(2'200))); BEAST_EXPECT(expectOffers(env, bob_, 0)); } // Offer crossing XRP/MPT { Env env(*this); fund(env, gw_, {alice_, bob_, carol_}, XRP(30'000)); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 30'000, .flags = kMptDexFlags}); AMM ammAlice(env, alice_, XRP(10'000), btc(10'100)); env(offer(bob_, MPT(ammAlice[1])(100), XRP(100))); env.close(); BEAST_EXPECT( ammAlice.expectBalances(XRP(10'100), MPT(ammAlice[1])(10'000), ammAlice.tokens())); // Initial 30,000 + 100 env.require(Balance(bob_, MPT(ammAlice[1])(30'100))); // Initial 30,000 - 100(offer) - 10(tx fee) - 1(tx fee for MPTTester // holder) BEAST_EXPECT( expectLedgerEntryRoot(env, bob_, XRP(30'000) - XRP(100) - 2 * txFee(env, 1))); BEAST_EXPECT(expectOffers(env, bob_, 0)); } // Offer crossing MPT/MPT and transfer rate // Single path AMM offer { Env env(*this); env.fund(XRP(30'000), gw_, alice_, carol_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .transferFee = 25'000, .pay = 30'000'000'000'000'000, .flags = kMptDexFlags}); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .transferFee = 25'000, .pay = 30'000'000'000'000'000, .flags = kMptDexFlags}); AMM const ammAlice(env, alice_, btc(1'000'000'000'000'000), eth(1'100'000'000'000'000)); // 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_, eth(100'000'000'000'000), btc(100'000'000'000'000))); env.close(); // No transfer fee BEAST_EXPECT(ammAlice.expectBalances( btc(1'100'000'000'000'000), eth(1'000'000'000'000'000), ammAlice.tokens())); // Initial 30,000'000'000'000'000 - 100'000'000'000'000(offer)-25 % // transfer fee env.require(Balance(carol_, btc(29'875'000'000'000'000))); // Initial 30,000'000'000'000'000 + 100'000'000'000'000(offer) env.require(Balance(carol_, eth(30'100'000'000'000'000))); BEAST_EXPECT(expectOffers(env, carol_, 0)); } // Single-path AMM offer { Env env(*this); env.fund(XRP(30'000), gw_, alice_, bob_, carol_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .transferFee = 100, .pay = 30'000'000'000'000'000, .flags = kMptDexFlags}); AMM const amm(env, alice_, XRP(1'000), btc(500'000'000'000'000)); env(offer(carol_, XRP(100), btc(55'000'000'000'000))); env.close(); BEAST_EXPECT( amm.expectBalances(XRPAmount(909'090'909), btc(550'000000055001), amm.tokens())); // Offer ~91XRP/49.99e12BTC BEAST_EXPECT(expectOffers( env, carol_, 1, {{Amounts{XRPAmount{9'090'909}, btc(4'999999950000)}}})); // Carol pays 0.1% fee on 50'000000055000BTC = 50'000000055BTC env.require(Balance(carol_, btc(29'949'949'999'944'943))); } { Env env(*this); env.fund(XRP(30'000), gw_, alice_, carol_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .transferFee = 100, .pay = 3'000'000'000'000'000'000, .flags = kMptDexFlags}); AMM const amm(env, alice_, XRP(1'000), btc(50'000'000'000'000'000)); env(offer(carol_, XRP(10), btc(5'500'000'000'000'000))); env.close(); BEAST_EXPECT(amm.expectBalances(XRP(990), btc(505'05050505050506), amm.tokens())); BEAST_EXPECT(expectOffers(env, carol_, 0)); } // Multi-path AMM offer { Env env(*this); Account const ed("ed"); env.fund(XRP(30'000), gw_, alice_, bob_, carol_, ed); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_, ed}, .transferFee = 25'000, .pay = 20'000'000'000'000'000, .flags = kMptDexFlags}); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_, ed}, .transferFee = 25'000, .pay = 20'000'000'000'000'000, .flags = kMptDexFlags}); AMM const ammAlice( env, alice_, btc(10'000'000'000'000'000), eth(11'000'000'000'000'000)); env(offer(bob_, btc(100'000'000'000'000), XRP(10)), Txflags(tfPassive)); env(offer(ed, XRP(10), eth(100'000'000'000'000)), Txflags(tfPassive)); env.close(); env(offer(carol_, eth(1'000'000'000'000'000), btc(1'000'000'000'000'000))); env.close(); BEAST_EXPECT(ammAlice.expectBalances( btc(1'060'6848287928025), eth(1'037'0658372213582), ammAlice.tokens())); // Consumed offer ~72.93e13ETH/72.93e13BTC BEAST_EXPECT(expectOffers( env, carol_, 1, {Amounts{eth(27'0658372213582), btc(27'0658372213582)}})); BEAST_EXPECT(expectOffers(env, bob_, 0)); BEAST_EXPECT(expectOffers(env, ed, 0)); env.require(Balance(carol_, btc(19'116'439'640'089'965))); env.require(Balance(carol_, eth(20'729'341'627'786'418))); env.require(Balance(bob_, btc(20'100'000'000'000'000))); env.require(Balance(ed, eth(19'875'000'000'000'000))); } // Payment and transfer fee // Scenario: // Bob sends 125BTC to pay 80EUR to Carol // Payment execution: // bob's 125BTC/1.25 = 100BTC // 100BTC/100EUR AMM offer // 100EUR/1.25 = 80EUR paid to carol { Env env(*this); env.fund(XRP(30'000), gw_, alice_, bob_, carol_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .transferFee = 25'000, .pay = 30'000, .flags = kMptDexFlags}); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .transferFee = 25'000, .pay = 30'000, .flags = kMptDexFlags}); AMM const ammAlice(env, alice_, btc(1'000), eth(1'100)); env(rate(gw_, 1.25)); env.close(); env(pay(bob_, carol_, eth(100)), Path(~MPT(eth)), Sendmax(btc(125)), Txflags(tfPartialPayment)); env.close(); BEAST_EXPECT(ammAlice.expectBalances(btc(1'100), eth(1'000), ammAlice.tokens())); env.require(Balance(bob_, btc(29'875))); env.require(Balance(carol_, eth(30'080))); } // Payment and transfer fee, multiple steps // Scenario: // Dan's offer 200CAN/200GBP // AMM 1000GBP/10125ETH // Ed's offer 200ETH/BTC // Bob sends 195.3125CAN to pay 100BTC 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/125ETH 125ETH/1.25 = 100ETH -> ed's offer // 100ETH/100BTC 100BTC/1.25 = 80BTC paid to carol { Env env(*this); Account const dan("dan"); Account const ed("ed"); env.fund(XRP(30'000), gw_, alice_, bob_, carol_, dan, ed); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_, dan, ed}, .transferFee = 25'000, .pay = 30'000, .flags = kMptDexFlags}); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_, dan, ed}, .transferFee = 25'000, .pay = 30'000, .flags = kMptDexFlags}); MPTTester const can( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_, dan, ed}, .transferFee = 25'000, .pay = 2'000'000, .flags = kMptDexFlags}); MPTTester const gbp( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_, dan, ed}, .transferFee = 25'000, .pay = 3'000'000, .flags = kMptDexFlags}); AMM const ammAlice(env, alice_, gbp(1'000'000), eth(10'125)); env(pay(gw_, bob_, can(1'953'125))); env.close(); env(offer(dan, can(2'000'000), gbp(20'000))); env(offer(ed, eth(200), btc(200))); env.close(); env(pay(bob_, carol_, btc(100)), Path(~MPT(gbp), ~MPT(eth), ~MPT(btc)), Sendmax(can(1'953'125)), Txflags(tfPartialPayment)); env.close(); env.require(Balance(bob_, can(2'000'000))); env.require(Balance(dan, can(3'562'500))); env.require(Balance(dan, gbp(2'984'375))); BEAST_EXPECT(ammAlice.expectBalances(gbp(1'012'500), eth(10'000), ammAlice.tokens())); env.require(Balance(ed, eth(30'100))); env.require(Balance(ed, btc(29'900))); env.require(Balance(carol_, btc(30'080))); } // Pay amounts close to one side of the pool testAMM( [&](AMM& ammAlice, Env& env) { auto const& btc = MPT(ammAlice[1]); env(pay(alice_, carol_, btc(9999)), Path(~btc), Sendmax(XRP(1)), Txflags(tfPartialPayment), Ter(tesSUCCESS)); env(pay(alice_, carol_, btc(10'000)), Path(~btc), Sendmax(XRP(1)), Txflags(tfPartialPayment), Ter(tesSUCCESS)); env(pay(alice_, carol_, XRP(100)), Path(~XRP), Sendmax(btc(100)), Txflags(tfPartialPayment), Ter(tesSUCCESS)); env(pay(alice_, carol_, STAmount{xrpIssue(), 99'999'900}), Path(~XRP), Sendmax(btc(100)), Txflags(tfPartialPayment), Ter(tesSUCCESS)); }, {{XRP(100), gAmmmpt(10'000)}}); // Multiple paths/steps { Env env(*this); env.fund(XRP(100'000), gw_, alice_); env.fund(XRP(1'000), bob_, carol_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 500'000'000'000'000'000, .flags = kMptDexFlags}); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 500'000'000'000'000'000, .flags = kMptDexFlags}); MPTTester const usd( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 500'000'000'000'000'000, .flags = kMptDexFlags}); MPTTester const eur( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 500'000'000'000'000'000, .flags = kMptDexFlags}); AMM const xrpEur(env, alice_, XRP(10'100), eur(100'000'000'000'000'000)); AMM const eurBtc( env, alice_, eur(100'000'000'000'000'000), btc(102'000'000'000'000'000)); AMM const btcUsd( env, alice_, btc(101'000'000'000'000'000), usd(100'000'000'000'000'000)); AMM const xrpUsd(env, alice_, XRP(10'150), usd(102'000'000'000'000'000)); AMM const xrpEth(env, alice_, XRP(10'000), eth(101'000'000'000'000'000)); AMM const ethEur( env, alice_, eth(109'000'000'000'000'000), eur(110'000'000'000'000'000)); AMM const eurUsd( env, alice_, eur(101'000'000'000'000'000), usd(100'000'000'000'000'000)); env(pay(bob_, carol_, usd(1'000'000'000'000'000)), Path(~MPT(eur), ~MPT(btc), ~MPT(usd)), Path(~MPT(usd)), Path(~MPT(eth), ~MPT(eur), ~MPT(usd)), Sendmax(XRP(200))); BEAST_EXPECT(xrpEth.expectBalances( XRPAmount(10'026'208'900), eth(10'073'6577924447994), xrpEth.tokens())); BEAST_EXPECT(ethEur.expectBalances( eth(10'926'3422075552006), eur(10'973'5423207873690), ethEur.tokens())); BEAST_EXPECT(eurUsd.expectBalances( eur(10'126'4576792126310), usd(9'973'9315171207179), eurUsd.tokens())); // XRP-USD path // This path provides ~73.9e12USD/74.1XRP BEAST_EXPECT(xrpUsd.expectBalances( XRPAmount(10'224'106'246), usd(10'126'0684828792821), xrpUsd.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.6e12USD/11.64XRP, // XRP-USD 60.7e12USD/60.8XRP, // XRP-ETH-EUR-USD 27.6e12USD/27.6XRP BEAST_EXPECT( xrpEur.expectBalances(XRP(10'100), eur(100'000'000'000'000'000), xrpEur.tokens())); BEAST_EXPECT(eurBtc.expectBalances( eur(100'000'000'000'000'000), btc(102'000'000'000'000'000), eurBtc.tokens())); BEAST_EXPECT(btcUsd.expectBalances( btc(101'000'000'000'000'000), usd(100'000'000'000'000'000), btcUsd.tokens())); env.require(Balance(carol_, usd(501'000'000'000'000'000))); } // Dependent AMM { Env env(*this); env.fund(XRP(40'000), gw_, alice_); env.fund(XRP(1'000), bob_, carol_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 50'000'000'000'000'000, .flags = kMptDexFlags}); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 50'000'000'000'000'000, .flags = kMptDexFlags}); MPTTester const usd( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 50'000'000'000'000'000, .flags = kMptDexFlags}); MPTTester const eur( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 50'000'000'000'000'000, .flags = kMptDexFlags}); AMM const xrpEur(env, alice_, XRP(10'100), eur(10'000'000'000'000'000)); AMM const eurBtc(env, alice_, eur(10'000'000'000'000'000), btc(10'200'000'000'000'000)); AMM const btcUsd(env, alice_, btc(10'100'000'000'000'000), usd(10'000'000'000'000'000)); AMM const xrpEth(env, alice_, XRP(10'000), eth(10'100'000'000'000'000)); AMM const ethEur(env, alice_, eth(10'900'000'000'000'000), eur(11'000'000'000'000'000)); env(pay(bob_, carol_, usd(100'000'000'000'000)), Path(~MPT(eur), ~MPT(btc), ~MPT(usd)), Path(~MPT(eth), ~MPT(eur), ~MPT(btc), ~MPT(usd)), Sendmax(XRP(200))); BEAST_EXPECT(xrpEur.expectBalances( XRPAmount(10'118'738'472), eur(9'981'544436337981), xrpEur.tokens())); BEAST_EXPECT(eurBtc.expectBalances( eur(10'101'160967851758), btc(10'097'914269680647), eurBtc.tokens())); BEAST_EXPECT(btcUsd.expectBalances( btc(10'202'085730319353), usd(9'900'000'000'000'000), btcUsd.tokens())); BEAST_EXPECT(xrpEth.expectBalances( XRPAmount(10'082'446'397), eth(10'017'410727780081), xrpEth.tokens())); BEAST_EXPECT(ethEur.expectBalances( eth(10'982'589272219919), eur(10'917'294595810261), ethEur.tokens())); env.require(Balance(carol_, usd(50'100'000'000'000'000))); } // AMM offers limit // Consuming 30 CLOB offers, results in hitting 30 AMM offers limit. { Env env(*this); env.fund(XRP(30'000), gw_, alice_, carol_); env.fund(XRP(1'000), bob_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 30'000'000'000'000'000, .flags = kMptDexFlags}); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 400'000'000'000'000, .flags = kMptDexFlags}); AMM const ammAlice(env, alice_, XRP(10'000), btc(10'000'000'000'000'000)); for (int i = 0; i < 30; ++i) env(offer(alice_, eth(1'000'000'000'000 + (10'000'000'000 * 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_, eth(140'000'000'000'000), XRP(100))); env(pay(bob_, carol_, btc(100'000'000'000'000)), Path(~XRP, ~MPT(btc)), Sendmax(eth(400'000'000'000'000)), Txflags(tfPartialPayment | tfNoRippleDirect)); BEAST_EXPECT( ammAlice.expectBalances(XRP(10'030), btc(9'970'089730807592), ammAlice.tokens())); env.require(Balance(carol_, btc(30'029'910269192408))); BEAST_EXPECT(expectOffers(env, alice_, 1, {{{eth(140'000'000'000'000), XRP(100)}}})); } // This payment is fulfilled { Env env(*this); env.fund(XRP(30'000), gw_, alice_, carol_); env.fund(XRP(1'000), bob_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 30'000'000'000'000'000, .flags = kMptDexFlags}); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 400'000'000'000'000, .flags = kMptDexFlags}); AMM const ammAlice(env, alice_, XRP(10'000), btc(10'000'000'000'000'000)); for (int i = 0; i < 29; ++i) env(offer(alice_, eth(1'000'000'000'000 + (10'000'000'000 * 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_, eth(140'000'000'000'000), XRP(100))); env(pay(bob_, carol_, btc(100'000'000'000'000)), Path(~XRP, ~MPT(btc)), Sendmax(eth(400'000'000'000'000)), Txflags(tfPartialPayment | tfNoRippleDirect)); BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{10'101'010'102}, btc(9'900'000'000'000'000), ammAlice.tokens())); env.require(Balance(carol_, btc(30'100'000'000'000'000))); BEAST_EXPECT( expectOffers(env, alice_, 1, {{{eth(39'185857200000), XRPAmount{27'989'898}}}})); } // Offer crossing with AMM and another offer. // AMM has a better quality and is consumed first. { Env env(*this); env.fund(XRP(30'000), gw_, alice_, carol_); env.fund(XRP(1'000), bob_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 30'000'000'000'000'000, .flags = kMptDexFlags}); env(offer(bob_, XRP(100), btc(100'001'000'000'000))); AMM const ammAlice(env, alice_, XRP(10'000), btc(10'100'000'000'000'000)); env(offer(carol_, btc(100'000'000'000'000), XRP(100))); BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{10'049'825'372}, btc(10'049'925870493027), ammAlice.tokens())); BEAST_EXPECT( expectOffers(env, bob_, 1, {{{XRPAmount{50'074'628}, btc(50'075129506973)}}})); env.require(Balance(carol_, btc(30'100'000'000'000'000))); } // Individually locked MPT destination account { Env env(*this); env.fund(XRP(30'000), gw_, alice_, carol_); MPTTester btc( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .pay = 30'000, .flags = tfMPTCanLock | kMptDexFlags}); AMM const ammAlice(env, alice_, XRP(10'000), btc(10'000)); btc.set({.holder = carol_, .flags = tfMPTLock}); env(pay(alice_, carol_, XRP(1)), Path(~XRP), Sendmax(btc(10)), Txflags(tfNoRippleDirect | tfPartialPayment), Ter(tesSUCCESS)); } // Individually locked MPT source account { Env env(*this); env.fund(XRP(30'000), gw_, alice_, carol_); MPTTester btc( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .pay = 30'000, .flags = tfMPTCanLock | kMptDexFlags}); AMM const ammAlice(env, alice_, XRP(10'000), btc(10'000)); btc.set({.holder = alice_, .flags = tfMPTLock}); env(pay(alice_, carol_, btc(1)), Path(~MPT(btc)), Sendmax(XRP(10)), Txflags(tfNoRippleDirect | tfPartialPayment), Ter(tesSUCCESS)); } } void testAMMTokens() { testcase("AMM Tokens"); using namespace jtx; // Offer crossing with AMM LPTokens and XRP. // AMM LPTokens come from MPT/XRP pool. testAMM( [&](AMM& ammAlice, Env& env) { 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), MPT(ammAlice[1])(10'000), IOUAmount{10'000'000})); 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 auto const baseFee = env.current()->fees().base; auto const carolXRP = env.balance(carol_); 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(env.balance(carol_) == (carolXRP - 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(env.balance(carol_) == (carolXRP - baseFee * 2 + priceXRP)); BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{10'000'000'000} - priceXRP, MPT(ammAlice[1])(10'000), IOUAmount{5'000'000})); BEAST_EXPECT(ammAlice.expectLPTokens(alice_, IOUAmount{5'000'000})); BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{0})); }, {{XRP(10000), gAmmmpt(10000)}}); // Offer crossing with two AMM LPTokens. // token1 comes from MPT/XRP pool. // token2 comes from XRP/IOU pool. 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)); }, {{XRP(10000), gAmmmpt(10000)}}); // 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})); }, {{XRP(10000), gAmmmpt(10000)}}); } void testAmendment() { testcase("Amendment"); using namespace jtx; FeatureBitset const feature{testableAmendments() - featureMPTokensV2}; Env env{*this, feature}; env.fund(XRP(30'000), gw_, alice_); env.close(); MPT const btc = MPTTester( {.env = env, .issuer = gw_, .holders = {alice_}, .pay = 10'000, .flags = tfMPTCanClawback | tfMPTCanTransfer}); AMM amm(env, alice_, XRP(1'000), btc(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 testAMMAndCLOB(FeatureBitset features) { testcase("AMMAndCLOB, offer quality change"); using namespace jtx; auto const gw = Account("gw"); 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.fund(XRP(10'000), lP1); env.fund(XRP(10'000), lP2); MPTTester const tst( {.env = env, .issuer = gw, .holders = {lP1, lP2}, .flags = kMptDexFlags}); env(offer(gw, XRP(11'500'000'000), tst(1'000'000'000'000'000))); env(offer(lP1, tst(25'000'000), XRPAmount(287'500'000))); // Either AMM or CLOB offer offerCb(env, tst); env(offer(lP2, tst(25'000'000), XRPAmount(287'500'000))); expectCb(env, tst); }; // If we replace AMM with an equivalent CLOB offer, which AMM generates // when it is consumed, then the result must be equivalent, too. STAmount lp2TSTBalance; std::string lp2TakerGets; std::string lp2TakerPays; // Execute with AMM first prep( [&](Env& env, MPTTester tst) { AMM const amm(env, lP1, tst(25'000'000), XRP(250)); }, [&](Env& env, MPTTester tst) { lp2TSTBalance = env.balance(lP2, MPT(tst)); 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, MPTTester tst) { env(offer(lP1, XRPAmount{18'095'131}, tst(1'687'379)), Txflags(tfPassive)); }, [&](Env& env, MPTTester tst) { BEAST_EXPECT(lp2TSTBalance == env.balance(lP2, MPT(tst))); auto const offer = getAccountOffers(env, lP2)["offers"][0u]; BEAST_EXPECT(lp2TakerGets == offer["taker_gets"].asString()); BEAST_EXPECT(lp2TakerPays == offer["taker_pays"]["value"].asString()); }); } void testAMMOfferGenerationPolicy(FeatureBitset features) { testcase("AMM payment offer generation picks economically coarser integral side"); using namespace jtx; enum class GeneratedFirst { TakerPays, TakerGets }; auto const check = [&](std::uint64_t mptUnitsPerXRP, GeneratedFirst generatedFirst) { TAmounts const pool{ XRPAmount{1'000'000}, MPTAmount{1'000'000'125}}; TAmounts const clobOffer{ kDropsPerXrp, MPTAmount{static_cast(mptUnitsPerXRP)}}; Quality const clobQuality{clobOffer}; auto const expectedAmounts = generatedFirst == GeneratedFirst::TakerGets ? getAMMOfferStartWithTakerGets(pool, clobQuality, 0) : getAMMOfferStartWithTakerPays(pool, clobQuality, 0); auto const otherAmounts = generatedFirst == GeneratedFirst::TakerGets ? getAMMOfferStartWithTakerPays(pool, clobQuality, 0) : getAMMOfferStartWithTakerGets(pool, clobQuality, 0); BEAST_EXPECT(expectedAmounts); BEAST_EXPECT(otherAmounts); if (!expectedAmounts || !otherAmounts) return; // Make the tested branch observable: these cases are chosen so the // payment consumes different AMM amounts depending on which side // is generated first. BEAST_EXPECT(*expectedAmounts != *otherAmounts); Env env(*this, features); auto const gw = Account("gw"); auto const lp = Account("lp"); auto const maker = Account("maker"); auto const taker = Account("taker"); auto const dst = Account("dst"); env.fund(XRP(10'000), gw, lp, maker, taker, dst); env.close(); MPTTester const token( {.env = env, .issuer = gw, .holders = {lp, maker, dst}, .flags = kMptDexFlags}); env(pay(gw, lp, token(pool.out.value()))); env(pay(gw, maker, token(10'000'000))); env.close(); AMM const amm(env, lp, drops(pool.in), token(pool.out.value())); auto const makerOfferSeq = env.seq(maker); env(offer(maker, XRP(1), token(mptUnitsPerXRP)), Txflags(tfPassive)); env.close(); env(pay(taker, dst, token(expectedAmounts->out.value())), Sendmax(drops(expectedAmounts->in))); env.close(); BEAST_EXPECT(amm.expectBalances( drops(pool.in + expectedAmounts->in), token((pool.out - expectedAmounts->out).value()), amm.tokens())); env.require(Balance(dst, token(expectedAmounts->out.value()))); BEAST_EXPECT(env.le(keylet::offer(maker.id(), makerOfferSeq))); }; // CLOB price: 10'000'000 MPT per 1 XRP, so one raw MPT unit is worth // 0.1 drops. One drop is the economically coarser unit and the AMM // offer is generated from takerPays. check(10 * kDropsPerXrp.drops(), GeneratedFirst::TakerPays); // CLOB price: 1'000'000 MPT per 1 XRP, so one raw MPT unit is worth // one drop. Ties use takerGets to preserve the historical XRP-output // behavior. check(kDropsPerXrp.drops(), GeneratedFirst::TakerGets); // CLOB price: 100'000 MPT per 1 XRP, so one raw MPT unit is worth // 10 drops. MPT is the economically coarser unit and the AMM offer is // generated from takerGets. check(kDropsPerXrp.drops() / 10, GeneratedFirst::TakerGets); } void testTradingFee(FeatureBitset features) { testcase("Trading Fee"); using namespace jtx; // Single Deposit, 1% fee testAMM( [&](AMM& ammAlice, Env& env) { // No fee ammAlice.deposit(carol_, MPT(ammAlice[1])(3'000)); BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{1'000})); ammAlice.withdrawAll(carol_, MPT(ammAlice[1])(3'000)); BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{0})); env.require(Balance(carol_, MPT(ammAlice[1])(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_, MPT(ammAlice[1])(3'000)); BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{994'981155689671, -12})); env.require(Balance(carol_, MPT(ammAlice[1])(27'000))); // Set fee to 0 ammAlice.vote(alice_, 0); ammAlice.withdrawAll(carol_, MPT(ammAlice[1])(0)); // Carol gets back less than the original deposit if (!features[fixAMMv1_3]) { env.require(Balance(carol_, MPT(ammAlice[1])(29'995))); } else { env.require(Balance(carol_, MPT(ammAlice[1])(29'994))); } }, {{USD(1000), gAmmmpt(1000)}}, 0, std::nullopt, {features}); // Single deposit with EP not exceeding specified: // 100MPT with EP not to exceed 0.1 (AssetIn/TokensOut). 1% fee. testAMM( [&](AMM& ammAlice, Env& env) { BEAST_EXPECT(ammAlice.expectTradingFee(1'000)); auto const balance = env.balance(carol_, MPT(ammAlice[1])); auto const tokensFee = ammAlice.deposit( carol_, MPT(ammAlice[1])(1000), std::nullopt, STAmount{ammAlice.lptIssue(), 1, -1}); auto const deposit = balance - env.balance(carol_, MPT(ammAlice[1])); ammAlice.withdrawAll(carol_, MPT(ammAlice[1])(0)); ammAlice.vote(alice_, 0); BEAST_EXPECT(ammAlice.expectTradingFee(0)); auto const tokensNoFee = ammAlice.deposit(carol_, deposit); BEAST_EXPECT(tokensFee == IOUAmount(485636'0611129, -7)); if (!features[fixAMMv1_3]) { BEAST_EXPECT(tokensNoFee == IOUAmount(487659'8005807, -7)); } else { BEAST_EXPECT(tokensNoFee == IOUAmount(487612'21584827, -8)); } }, {{XRP(10'000), gAmmmpt(10'000)}}, 1'000, std::nullopt, {features}); // Single deposit with EP not exceeding specified: // 200MPT with EP not to exceed 0.002020 (AssetIn/TokensOut). 1% fee testAMM( [&](AMM& ammAlice, Env& env) { BEAST_EXPECT(ammAlice.expectTradingFee(1'000)); auto const balance = env.balance(carol_, MPT(ammAlice[1])); auto const tokensFee = ammAlice.deposit( carol_, MPT(ammAlice[1])(200), std::nullopt, STAmount{ammAlice.lptIssue(), 2020, -6}); auto const deposit = balance - env.balance(carol_, MPT(ammAlice[1])); ammAlice.withdrawAll(carol_, MPT(ammAlice[1])(0)); ammAlice.vote(alice_, 0); BEAST_EXPECT(ammAlice.expectTradingFee(0)); auto const tokensNoFee = ammAlice.deposit(carol_, deposit); if (!features[fixAMMv1_3]) { BEAST_EXPECT(tokensFee == IOUAmount(98'019'80198019, -8)); BEAST_EXPECT(tokensNoFee == IOUAmount(98'495'13933556, -8)); } else { BEAST_EXPECT(tokensFee == IOUAmount(97527'05893345, -8)); BEAST_EXPECT(tokensNoFee == IOUAmount(98000'10293049, -8)); } }, {{XRP(10'000), gAmmmpt(10'000)}}, 1'000, std::nullopt, {features}); // Single Withdrawal, 1% fee testAMM( [&](AMM& ammAlice, Env& env) { // No fee ammAlice.deposit(carol_, MPT(ammAlice[1])(3'000)); BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{1'000})); env.require(Balance(carol_, MPT(ammAlice[1])(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_, MPT(ammAlice[1])(0)); if (!features[fixAMMv1_3]) { env.require(Balance(carol_, MPT(ammAlice[1])(29'995))); } else { env.require(Balance(carol_, MPT(ammAlice[1])(29'994))); } }, {{USD(1000), gAmmmpt(1000)}}, 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_, MPT(ammAlice[1])(100), std::nullopt, IOUAmount{520, 0}); env.require(Balance(carol_, MPT(ammAlice[1])(30443))); // Set to original pool size auto const deposit = env.balance(carol_, MPT(ammAlice[1])) - MPT(ammAlice[1])(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_3]) { env.require(Balance(carol_, MPT(ammAlice[1])(30443))); } else { env.require(Balance(carol_, MPT(ammAlice[1])(30442))); } BEAST_EXPECT(tokensNoFee == IOUAmount(746'327'46496649, -8)); BEAST_EXPECT(tokensFee == IOUAmount(750'588'23529411, -8)); }, {{XRP(10'000), gAmmmpt(10'000)}}, 1'000, std::nullopt, {features}); // Payment, 1% fee { Env env{*this, features}; env.fund(XRP(30'000), gw_, alice_, bob_, carol_); env.close(); MPT const btc = MPTTester( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 30'000, .flags = kMptDexFlags}); auto const usd = gw_["USD"]; env.trust(usd(30'000), alice_); env(pay(gw_, alice_, usd(30'000))); env.trust(usd(30'000), bob_); env(pay(gw_, bob_, usd(1'000))); env.trust(usd(30'000), carol_); env(pay(gw_, carol_, usd(30'000))); env.close(); AMM amm(env, alice_, btc(1000), usd(1010)); env.require(Balance(alice_, btc(29'000))); env.require(Balance(alice_, usd(28'990))); env.require(Balance(carol_, btc(30'000))); // Carol pays to Alice with no fee env(pay(carol_, alice_, usd(10)), Path(~usd), Sendmax(btc(10)), Txflags(tfNoRippleDirect)); env.close(); // Alice has 10USD more and Carol has 10BTC less env.require(Balance(alice_, btc(29'000))); env.require(Balance(alice_, usd(29'000))); env.require(Balance(carol_, btc(29'990))); // Set fee to 1% amm.vote(alice_, 1'000); BEAST_EXPECT(amm.expectTradingFee(1'000)); // Bob pays to Carol with 1% fee env(pay(bob_, carol_, btc(10)), Path(~btc), Sendmax(usd(15)), Txflags(tfNoRippleDirect)); env.close(); // Bob sends 10.1~USD to pay 10BTC env.require(Balance(bob_, STAmount{usd, UINT64_C(989'8989898989899), -13})); // Carol got 10BTC env.require(Balance(carol_, btc(30'000))); BEAST_EXPECT(amm.expectBalances( btc(1'000), STAmount{usd, UINT64_C(1'010'10101010101), -11}, amm.tokens())); } // Offer crossing, 0.05% fee MPT/XRP testAMM( [&](AMM& ammAlice, Env& env) { auto const btc = MPT(ammAlice[1]); auto const carolXRP = env.balance(carol_); auto const baseFee = env.current()->fees().base; env(offer(carol_, btc(10), XRP(10))); env.close(); env.require(Balance(carol_, btc(30'010))); env.require(Balance(carol_, carolXRP - baseFee - XRP(10))); // Change pool composition back env(offer(carol_, XRP(10), btc(10))); env.close(); env.require(Balance(carol_, btc(30'000))); env.require(Balance(carol_, carolXRP - baseFee * 2)); // set fee to 0.05% ammAlice.vote(alice_, 50); BEAST_EXPECT(ammAlice.expectTradingFee(50)); env(offer(carol_, btc(10), XRP(10))); env.close(); env.require(Balance(carol_, btc(30'009))); env.require(Balance(carol_, carolXRP - baseFee * 3 - XRPAmount(8'995'507))); BEAST_EXPECT(expectOffers(env, carol_, 1, {{Amounts{btc(1), XRP(1)}}})); }, {{XRP(1000), gAmmmpt(1010)}}, 0, std::nullopt, {features}); // Offer crossing, 0.5% fee MPT/IOU testAMM( [&](AMM& ammAlice, Env& env) { auto const btc = MPT(ammAlice[1]); env(offer(carol_, btc(10'000'000), USD(10))); env.close(); env.require(Balance(carol_, btc(1'020'001'000))); env.require(Balance(carol_, USD(29'990))); // Change pool composition back env(offer(carol_, USD(10), btc(10'000'000))); env.close(); env.require(Balance(carol_, btc(1'010'001'000))); env.require(Balance(carol_, USD(30'000))); // set fee to 0.5% ammAlice.vote(alice_, 500); BEAST_EXPECT(ammAlice.expectTradingFee(500)); env(offer(carol_, btc(10'000'000), USD(10))); env.close(); env.require(Balance(carol_, btc(1'014'975'874))); env.require(Balance(carol_, STAmount{USD, UINT64_C(29'995'02512600184), -11})); BEAST_EXPECT(expectOffers( env, carol_, 1, {{Amounts{btc(5'025126), STAmount{USD, UINT64_C(5'025126), -6}}}})); }, {{USD(1000), gAmmmpt(1010000000)}}, 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(30'000), {USD(2'000)}); MPT const btc = MPTTester( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_, ed}, .pay = 30'000'000000, .flags = kMptDexFlags}); env(offer(carol_, btc(5'000000), USD(5))); AMM const ammAlice(env, alice_, USD(1'005), btc(1'000'000000)); env(pay(bob_, ed, USD(10)), Path(~USD), Sendmax(btc(15'000000)), Txflags(tfNoRippleDirect)); env.require(Balance(ed, USD(2'010))); env.require(Balance(bob_, btc(29'989'999999))); BEAST_EXPECT(ammAlice.expectBalances( btc(1'005'000001), STAmount{USD, UINT64_C(999'9999999999999), -13}, 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(30'000), {USD(2'000)}); MPT const btc = MPTTester( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_, ed}, .pay = 30'000'000000, .flags = kMptDexFlags}); env(offer(carol_, btc(5'000000), USD(5))); // Set 0.25% fee AMM const ammAlice(env, alice_, USD(1'005), btc(1'000'000000), false, 250); env(pay(bob_, ed, USD(10)), Path(~USD), Sendmax(btc(15'000000)), Txflags(tfNoRippleDirect)); env.require(Balance(ed, USD(2'010))); env.require(Balance(bob_, btc(29'989'987453))); BEAST_EXPECT(ammAlice.expectBalances(btc(1'005'012547), USD(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. 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(30'000), {USD(2'000)}); MPT const btc = MPTTester( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_, ed}, .pay = 30'000'000000, .flags = kMptDexFlags}); env(offer(carol_, btc(10'000000), USD(10))); // Set 1% fee AMM const ammAlice(env, alice_, USD(1'005), btc(1'000'000000), false, 1'000); env(pay(bob_, ed, USD(10)), Path(~USD), Sendmax(btc(15'000000)), Txflags(tfNoRippleDirect)); env.require(Balance(ed, USD(2'010))); env.require(Balance(bob_, btc(29'990'000000))); BEAST_EXPECT(ammAlice.expectBalances(btc(1'000'000000), USD(1'005), 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(30'000), {USD(2'000)}); MPT const btc = MPTTester( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_, ed}, .pay = 30'000'000000, .flags = kMptDexFlags}); env(offer(carol_, btc(9'000000), USD(9))); // Set 1% fee AMM const ammAlice(env, alice_, USD(1'005), btc(1'000'000000), false, 1'000); env(pay(bob_, ed, USD(10)), Path(~USD), Sendmax(btc(15'000000)), Txflags(tfNoRippleDirect)); env.require(Balance(ed, USD(2'010))); env.require(Balance(bob_, btc(29'989'993923))); BEAST_EXPECT(ammAlice.expectBalances(btc(1'001'006077), USD(1'004), ammAlice.tokens())); BEAST_EXPECT(expectOffers(env, carol_, 0)); } } void testAdjustedTokens(FeatureBitset features) { testcase("Adjusted Deposit/Withdraw Tokens"); using namespace jtx; // Deposit/Withdraw USD from USD/MPT pool { Env env(*this); Account const gw{"gateway"}; Account const alice{"alice"}; Account const bob("bob"); Account const carol("carol"); 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"); std::vector const holders{ alice, bob, carol, ed, paul, dan, chris, simon, ben, natalie}; env.fund(XRP(100000), gw, alice, bob, carol, ed, paul, dan, chris, simon, ben, natalie); env.close(); MPT const btc = MPTTester( {.env = env, .issuer = gw, .holders = holders, .pay = 40'000'000000, .flags = kMptDexFlags}); auto const usd = gw["USD"]; for (auto const& holder : holders) { env.trust(usd(1'500'000), holder); env(pay(gw, holder, usd(1'500'000))); } env.close(); auto aliceUSD = env.balance(alice, usd); auto bobUSD = env.balance(bob, usd); auto carolUSD = env.balance(carol, usd); auto edUSD = env.balance(ed, usd); auto paulUSD = env.balance(paul, usd); auto danUSD = env.balance(dan, usd); auto chrisUSD = env.balance(chris, usd); auto simonUSD = env.balance(simon, usd); auto benUSD = env.balance(ben, usd); auto natalieUSD = env.balance(natalie, usd); AMM ammAlice(env, alice, btc(10'000'000000), usd(10000)); BEAST_EXPECT( ammAlice.expectBalances(btc(10'000'000000), usd(10'000), IOUAmount{10'000'000})); 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)); } BEAST_EXPECT(ammAlice.expectBalances( btc(10'000'000000), STAmount{usd, UINT64_C(10000'0000000001), -10}, IOUAmount{10'000'000})); env.require(Balance(bob, bobUSD)); env.require(Balance(carol, carolUSD)); env.require(Balance(ed, edUSD)); env.require(Balance(paul, paulUSD)); env.require(Balance(dan, danUSD)); env.require(Balance(chris, chrisUSD)); env.require(Balance(simon, simonUSD)); env.require(Balance(ben, benUSD)); env.require(Balance(natalie, natalieUSD)); ammAlice.withdrawAll(alice); BEAST_EXPECT(!ammAlice.ammExists()); env.require(Balance(alice, aliceUSD)); } // Same as above but deposit/withdraw MPT from USD/MPT pool { Env env(*this); Account const gw{"gateway"}; Account const alice{"alice"}; Account const bob("bob"); Account const carol("carol"); 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"); std::vector const holders{ alice, bob, carol, ed, paul, dan, chris, simon, ben, natalie}; env.fund(XRP(100000), gw, alice, bob, carol, ed, paul, dan, chris, simon, ben, natalie); env.close(); MPT const btc = MPTTester( {.env = env, .issuer = gw, .holders = holders, .pay = 40'000'000000, .flags = kMptDexFlags}); auto const usd = gw["USD"]; for (auto const& holder : holders) { env.trust(usd(1'500'000), holder); env(pay(gw, holder, usd(1'500'000))); } env.close(); auto aliceBTC = env.balance(alice, btc); auto bobBTC = env.balance(bob, btc); auto carolBTC = env.balance(carol, btc); auto edBTC = env.balance(ed, btc); auto paulBTC = env.balance(paul, btc); auto danBTC = env.balance(dan, btc); auto chrisBTC = env.balance(chris, btc); auto simonBTC = env.balance(simon, btc); auto benBTC = env.balance(ben, btc); auto natalieBTC = env.balance(natalie, btc); AMM ammAlice(env, alice, btc(10'000'000000), usd(10000)); BEAST_EXPECT( ammAlice.expectBalances(btc(10'000'000000), usd(10'000), IOUAmount{10'000'000})); for (int i = 0; i < 10; ++i) { ammAlice.deposit(ben, btc(1)); ammAlice.withdrawAll(ben, btc(0)); ammAlice.deposit(simon, btc(1'000)); ammAlice.withdrawAll(simon, btc(0)); ammAlice.deposit(chris, btc(1)); ammAlice.withdrawAll(chris, btc(0)); ammAlice.deposit(dan, btc(10)); ammAlice.withdrawAll(dan, btc(0)); ammAlice.deposit(bob, btc(100)); ammAlice.withdrawAll(bob, btc(0)); ammAlice.deposit(carol, btc(1'000)); ammAlice.withdrawAll(carol, btc(0)); ammAlice.deposit(ed, btc(10'000)); ammAlice.withdrawAll(ed, btc(0)); ammAlice.deposit(paul, btc(100'000)); ammAlice.withdrawAll(paul, btc(0)); ammAlice.deposit(natalie, btc(1'000'000)); ammAlice.withdrawAll(natalie, btc(0)); } BEAST_EXPECT( ammAlice.expectBalances(btc(10'000'000090), usd(10'000), IOUAmount{10'000'000})); env.require(Balance(bob, bobBTC - btc(10))); env.require(Balance(carol, carolBTC - btc(10))); env.require(Balance(ed, edBTC - btc(10))); env.require(Balance(paul, paulBTC - btc(10))); env.require(Balance(dan, danBTC - btc(10))); env.require(Balance(chris, chrisBTC - btc(10))); env.require(Balance(simon, simonBTC - btc(10))); env.require(Balance(ben, benBTC - btc(10))); env.require(Balance(natalie, natalieBTC - btc(10))); ammAlice.withdrawAll(alice); BEAST_EXPECT(!ammAlice.ammExists()); env.require(Balance(alice, aliceBTC + btc(90))); } } void testAMMID() { testcase("AMMID"); using namespace jtx; // MPT/XRP 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::Values::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(); } }, {{XRP(1000), gAmmmpt(1000'000)}}); } void testSelection(FeatureBitset features) { testcase("Offer/Strand Selection"); using namespace jtx; Account const ed("ed"); Account const gw1("gw1"); // 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. struct MPTList { MPTTester const USD; MPTTester const ETH; MPTTester const CAN; }; auto prep = [&](Env& env, uint16_t gwTransferFee, uint16_t gw1TransferFee) -> MPTList { env.fund(XRP(2'000), gw_, gw1, alice_, bob_, carol_, ed); MPTTester usd( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_, ed}, .transferFee = gwTransferFee, .pay = 2'000'000'000, .flags = kMptDexFlags}); MPTTester eth( {.env = env, .issuer = gw1, .holders = {alice_, bob_, carol_, ed}, .transferFee = gw1TransferFee, .pay = 2'000'000'000, .flags = kMptDexFlags}); MPTTester can( {.env = env, .issuer = gw1, .holders = {alice_, bob_, carol_, ed}, .transferFee = gw1TransferFee, .pay = 2'000'000'000, .flags = kMptDexFlags}); env.close(); return MPTList{ .USD = std::move(usd), .ETH = std::move(eth), .CAN = std::move(can), }; }; static constexpr std::uint32_t kLowRate = 10'000; static constexpr std::uint32_t kHighRate = 50'000; for (auto const& rates : {std::make_pair(kLowRate, kHighRate), std::make_pair(kHighRate, kLowRate)}) { // 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); auto mpts = prep(env, rates.first, rates.second); auto usd = mpts.USD; auto eth = mpts.ETH; auto can = mpts.CAN; std::optional amm; if (i == 0 || i == 2) { env(offer(ed, eth(400'000'000), usd(400'000'000)), Txflags(tfPassive)); env.close(); } if (i > 0) amm.emplace(env, ed, usd(1'000'000'000), eth(1'000'000'000)); env(pay(carol_, bob_, usd(100'000'000)), Path(~MPT(usd)), Sendmax(eth(500'000'000))); env.close(); // CLOB and AMM, AMM is not selected if (i == 2) { // NOLINTBEGIN(bugprone-unchecked-optional-access) i==2 implies amm is // emplaced (i>0) BEAST_EXPECT(amm->expectBalances( usd(1'000'000'000), eth(1'000'000'000), amm->tokens())); // NOLINTEND(bugprone-unchecked-optional-access) } env.require(Balance(bob_, usd(2'100'000'000))); q[i] = Quality( Amounts{ eth(2'000'000'000) - env.balance(carol_, MPT(eth)), env.balance(bob_, MPT(usd)) - usd(2'000'000'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); auto mpts = prep(env, rates.first, rates.second); auto usd = mpts.USD; auto eth = mpts.ETH; auto can = mpts.CAN; std::optional amm; if (i == 0 || i == 2) { env(offer(ed, eth(400'000'000), usd(400'000'000)), Txflags(tfPassive)); env.close(); } if (i > 0) amm.emplace(env, ed, usd(1'000'000'000), eth(1'000'000'000)); env(offer(alice_, usd(400'000'000), eth(400'000'000))); env.close(); // AMM is not selected if (i > 0) { // NOLINTBEGIN(bugprone-unchecked-optional-access) emplaced when i > 0 BEAST_EXPECT( amm->expectBalances(usd(1'000'000'000), eth(1'000'000'000), amm->tokens())); // NOLINTEND(bugprone-unchecked-optional-access) } 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'000'000), eth(400'000'000)}})); } 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); auto mpts = prep(env, rates.first, rates.second); auto usd = mpts.USD; auto eth = mpts.ETH; auto can = mpts.CAN; std::optional amm; if (i == 0 || i == 2) { env(offer(ed, eth(400'000'000), usd(330'000'000)), Txflags(tfPassive)); env.close(); } if (i > 0) amm.emplace(env, ed, usd(1'000'000'000), eth(1'000'000'000)); env(pay(carol_, bob_, usd(100'000'000)), Path(~MPT(usd)), Sendmax(eth(500'000'000))); env.close(); // AMM and CLOB are selected if (i > 0) { // NOLINTBEGIN(bugprone-unchecked-optional-access) emplaced when i > 0 BEAST_EXPECT(!amm->expectBalances( usd(1'000'000'000), eth(1'000'000'000), amm->tokens())); // NOLINTEND(bugprone-unchecked-optional-access) } if (i == 2) { if (rates.first == kLowRate) { BEAST_EXPECT(expectOffers( env, ed, 1, {{Amounts{ eth(377'824'111), usd(311'704'892), }}})); } else { BEAST_EXPECT(expectOffers( env, ed, 1, {{Amounts{ eth(329'339'263), usd(271'704'892), }}})); } } env.require(Balance(bob_, usd(2'100'000'000))); q[i] = Quality( Amounts{ eth(2'000'000'000) - env.balance(carol_, MPT(eth)), env.balance(bob_, MPT(usd)) - usd(2'000'000'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); auto mpts = prep(env, rates.first, rates.second); auto usd = mpts.USD; auto eth = mpts.ETH; auto can = mpts.CAN; std::optional amm; if (i == 0 || i == 2) { env(offer(ed, eth(400'000'000), usd(325'000'002)), Txflags(tfPassive)); env.close(); } if (i > 0) amm.emplace(env, ed, usd(1'000'000'000), eth(1'000'000'000)); env(offer(alice_, usd(325'000'000), eth(400'000'000))); env.close(); // AMM is selected in both cases if (i > 0) { // NOLINTBEGIN(bugprone-unchecked-optional-access) emplaced when i > 0 BEAST_EXPECT(!amm->expectBalances( usd(1'000'000'000), eth(1'000'000'000), amm->tokens())); // NOLINTEND(bugprone-unchecked-optional-access) } // Partially crosses, AMM is selected, CLOB fails // limitQuality if (i == 2) { if (rates.first == kLowRate) { // Ed offer is partially crossed. // The updated rounding makes limitQuality // work if both amendments are enabled BEAST_EXPECT(expectOffers( env, ed, 1, {{Amounts{ eth(121'368'836), usd(98'612'180), }}})); BEAST_EXPECT(expectOffers(env, alice_, 0)); } else { // Ed offer is partially crossed. BEAST_EXPECT(expectOffers( env, ed, 1, {{Amounts{ eth(121'368'836), usd(98'612'180), }}})); 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); auto mpts = prep(env, rates.first, rates.second); auto usd = mpts.USD; auto eth = mpts.ETH; auto can = mpts.CAN; std::optional amm; if (i == 0 || i == 2) { env(offer(ed, eth(400'000'000), can(375'000'000)), Txflags(tfPassive)); env(offer(ed, can(375'000'000), usd(338'000'000))), Txflags(tfPassive); } if (i > 0) amm.emplace(env, ed, eth(1'000'000'000), usd(1'000'000'000)); env(pay(carol_, bob_, usd(100'000'000)), Path(~MPT(usd)), Path(~MPT(can), ~MPT(usd)), Sendmax(eth(600'000'000))); env.close(); env.require(Balance(bob_, usd(2'100'000'000))); if (i == 2) { // NOLINTBEGIN(bugprone-unchecked-optional-access) i==2 implies amm is // emplaced (i>0) if (rates.first == kLowRate) { // Liquidity is consumed from AMM strand only BEAST_EXPECT(amm->expectBalances( eth(1'124'584'936), usd(889'999'993), amm->tokens())); } else { BEAST_EXPECT(amm->expectBalances( eth(1'103'723'909), usd(906'023'688), amm->tokens())); BEAST_EXPECT(expectOffers( env, ed, 2, {{Amounts{ eth(327'069'745), can(306'627'886), }, Amounts{ can(312'843'533), usd(281'976'305), }}})); } // NOLINTEND(bugprone-unchecked-optional-access) } q[i] = Quality( Amounts{ eth(2'000'000'000) - env.balance(carol_, MPT(eth)), env.balance(bob_, MPT(usd)) - usd(2'000'000'000)}); } BEAST_EXPECT(q[1] > q[0]); BEAST_EXPECT(q[2] > q[0] && q[2] < q[1]); } } } void testMalformed() { testcase("Malformed"); using namespace jtx; testAMM( [&](AMM& ammAlice, Env& env) { WithdrawArg const args{ .flags = tfSingleAsset, .err = Ter(temMALFORMED), }; ammAlice.withdraw(args); }, {{XRP(10'000), gAmmmpt(10'000)}}); testAMM( [&](AMM& ammAlice, Env& env) { WithdrawArg const args{ .flags = tfOneAssetLPToken, .err = Ter(temMALFORMED), }; ammAlice.withdraw(args); }, {{XRP(10'000), gAmmmpt(10'000)}}); testAMM( [&](AMM& ammAlice, Env& env) { WithdrawArg const args{ .flags = tfLimitLPToken, .err = Ter(temMALFORMED), }; ammAlice.withdraw(args); }, {{XRP(10'000), gAmmmpt(10'000)}}); testAMM( [&](AMM& ammAlice, Env& env) { WithdrawArg const args{ .asset1Out = MPT(ammAlice[1])(100), .asset2Out = MPT(ammAlice[1])(100), .err = Ter(temBAD_AMM_TOKENS), }; ammAlice.withdraw(args); }, {{XRP(10'000), gAmmmpt(10'000)}}); testAMM( [&](AMM& ammAlice, Env& env) { MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .pay = 2'000, .flags = tfMPTCanLock | kMptDexFlags}); WithdrawArg const args{ .asset1Out = XRP(100), .asset2Out = btc(100), .err = Ter(temBAD_AMM_TOKENS), }; ammAlice.withdraw(args); }, {{XRP(10'000), gAmmmpt(10'000)}}); 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]); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .pay = 2'000, .flags = tfMPTCanLock | kMptDexFlags}); btc(100).value().setJson(jv[jss::EPrice]); env(jv, Ter(telENV_RPC_FAILED)); }, {{XRP(10'000), gAmmmpt(10'000)}}); } void testFixAMMOfferBlockedByLOB(FeatureBitset features) { testcase("AMM Offer Blocked By LOB"); using namespace jtx; // Low quality LOB offer blocks AMM liquidity // USD/MPT crosses AMM despite of low quality LOB { Env env(*this, features); fund(env, gw_, {alice_, carol_}, XRP(1'000'000), {USD(1'000'000)}); MPT const btc = MPTTester( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .pay = 40'000'000000, .flags = kMptDexFlags}); env(offer(alice_, btc(1), USD(0.01))); env.close(); AMM const amm(env, gw_, btc(200'000), USD(100'000)); env(offer(carol_, USD(0.49), btc(1))); env.close(); if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) { BEAST_EXPECT(amm.expectBalances(btc(200'000), USD(100'000), amm.tokens())); BEAST_EXPECT(expectOffers(env, alice_, 1, {{Amounts{btc(1), USD(0.01)}}})); BEAST_EXPECT(expectOffers(env, carol_, 1, {{Amounts{USD(0.49), btc(1)}}})); } if (features[fixAMMv1_1] && features[fixAMMv1_3]) { BEAST_EXPECT(amm.expectBalances(btc(200'001), USD(99'999.51), amm.tokens())); BEAST_EXPECT(expectOffers(env, alice_, 1, {{Amounts{btc(1), USD(0.01)}}})); // Carol's offer crosses AMM BEAST_EXPECT(expectOffers(env, carol_, 0)); } } // XRP/MPT crosses AMM despite of low quality LOB { Env env(*this, features); fund(env, gw_, {alice_, carol_}, XRP(1'000'000), {USD(1'000'000)}); MPT const btc = MPTTester( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .pay = 40'000'000000, .flags = kMptDexFlags}); env(offer(alice_, btc(1), XRP(0.01))); env.close(); AMM const amm(env, gw_, btc(200'000), XRP(100'000)); env(offer(carol_, XRP(0.49), btc(1))); env.close(); if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) { BEAST_EXPECT(amm.expectBalances(btc(200'000), XRP(100'000), amm.tokens())); BEAST_EXPECT(expectOffers(env, alice_, 1, {{Amounts{btc(1), XRP(0.01)}}})); BEAST_EXPECT(expectOffers(env, carol_, 1, {{Amounts{XRP(0.49), btc(1)}}})); } if (features[fixAMMv1_1] && features[fixAMMv1_3]) { BEAST_EXPECT(amm.expectBalances(btc(200'001), XRP(99'999.51), amm.tokens())); BEAST_EXPECT(expectOffers(env, alice_, 1, {{Amounts{btc(1), XRP(0.01)}}})); // Carol's offer crosses AMM BEAST_EXPECT(expectOffers(env, carol_, 0)); } } } void testLPTokenBalance(FeatureBitset features) { testcase("LPToken Balance"); using namespace jtx; Env env(*this, features); Account const gw{"gateway"}, alice{"alice"}, bob{"bob"}; env.fund(XRP(100000), gw, alice, bob); env.close(); env(fset(gw, asfAllowTrustLineClawback)); env.close(); auto const usd = gw["USD"]; env.trust(usd(100000), alice); env(pay(gw, alice, usd(50000))); env.trust(usd(100000), bob); env(pay(gw, bob, usd(40000))); env.close(); MPT const btc = MPTTester( {.env = env, .issuer = gw, .holders = {alice, bob}, .pay = 40'000'000000, .flags = tfMPTCanClawback | tfMPTCanLock | kMptDexFlags}); AMM amm(env, alice, btc(2), usd(1)); amm.deposit(alice, IOUAmount{1'876123487565916, -15}); amm.deposit(bob, IOUAmount{1'000}); amm.withdraw(alice, IOUAmount{1'876123487565916, -15}); amm.withdrawAll(bob); auto const lpToken = getAccountLines(env, alice, amm.lptIssue())[jss::lines][0u][jss::balance].asString(); auto const lpTokenBalance = amm.ammRpcInfo()[jss::amm][jss::lp_token][jss::value].asString(); BEAST_EXPECT(lpToken == "1.414213562374011" && lpTokenBalance == "1.4142135623741"); auto res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice); BEAST_EXPECT(res && res.value()); amm.withdrawAll(alice); BEAST_EXPECT(!amm.ammExists()); } void testAMMDepositWithFrozenAssets() { testcase("test AMMDeposit with frozen assets"); using namespace jtx; // This lambda function is used to create trustline, MPT. // and create an AMM account. // And also test the callback function. auto testAMMDeposit = [&](Env& env, std::function cb) { env.fund(XRP(1'000), gw_, alice_); env.close(); MPTTester btc( {.env = env, .issuer = gw_, .holders = {alice_}, .pay = 30'000, .flags = tfMPTCanLock | kMptDexFlags}); AMM amm(env, alice_, btc(100), XRP(100)); env.close(); btc.set({.holder = alice_, .flags = tfMPTLock}); cb(amm, btc); }; // Deposit two assets, one of which is frozen, // then we should get tecFROZEN error. { Env env(*this); testAMMDeposit(env, [&](AMM& amm, MPTTester& btc) { amm.deposit(alice_, btc(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); testAMMDeposit(env, [&](AMM& amm, MPTTester& btc) { amm.deposit( alice_, btc(100), std::nullopt, std::nullopt, tfSingleAsset, Ter(tecFROZEN)); }); } // 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); testAMMDeposit(env, [&](AMM& amm, MPTTester& btc) { amm.deposit( alice_, XRP(100), std::nullopt, std::nullopt, tfSingleAsset, Ter(tecFROZEN)); }); } } void testAutoDelete() { testcase("Auto Delete"); using namespace jtx; FeatureBitset const all{testableAmendments()}; { Env env( *this, envconfig([](std::unique_ptr cfg) { cfg->FEES.reference_fee = XRPAmount(1); return cfg; }), all); env.fund(XRP(1'000), gw_, alice_); MPTTester const usd({.env = env, .issuer = gw_, .holders = {alice_}, .pay = 20'000}); MPTTester const btc({.env = env, .issuer = gw_, .holders = {alice_}, .pay = 20'000}); AMM amm(env, gw_, usd(10'000), btc(10'000)); for (auto i = 0; i < kMaxDeletableAmmTrustLines + 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,SetTrust failing with // tecAMM_EMPTY. Deposit succeeds with tfTwoAssetIfEmpty option. env(amm.bid({ .account = alice_, .bidMin = 1000, }), Ter(tecAMM_EMPTY)); amm.vote({.account = alice_, .tfee = 100, .err = Ter(tecAMM_EMPTY)}); amm.withdraw({.account = alice_, .tokens = 100, .err = Ter(tecAMM_EMPTY)}); amm.deposit({.account = alice_, .asset1In = usd(100), .err = Ter(tecAMM_EMPTY)}); env(trust(alice_, STAmount{amm.lptIssue(), 10'000}), Ter(tecAMM_EMPTY)); // Can deposit with tfTwoAssetIfEmpty option amm.deposit( {.account = alice_, .asset1In = usd(1'000), .asset2In = btc(1'000), .flags = tfTwoAssetIfEmpty, .tfee = 1'000}); BEAST_EXPECT(amm.expectBalances(usd(1'000), btc(1'000), IOUAmount{1'000})); 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); env.fund(XRP(1'000), gw_, alice_); MPTTester const usd({.env = env, .issuer = gw_, .holders = {alice_}, .pay = 20'000}); MPTTester const btc({.env = env, .issuer = gw_, .holders = {alice_}, .pay = 20'000}); AMM amm(env, gw_, usd(10'000), btc(10'000)); for (auto i = 0; i < (kMaxDeletableAmmTrustLines * 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)); } { Env env( *this, envconfig([](std::unique_ptr cfg) { cfg->FEES.reference_fee = XRPAmount(1); return cfg; }), all); env.fund(XRP(1'000), gw_, alice_, carol_); MPTTester const usd( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .pay = 20'000}); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .pay = 20'000}); AMM amm(env, gw_, usd(10'000), btc(10'000)); amm.deposit({.account = alice_, .tokens = 1'000}); amm.deposit({.account = carol_, .tokens = 1'000}); amm.withdrawAll(alice_); amm.withdrawAll(carol_); amm.withdrawAll(gw_); BEAST_EXPECT(!amm.ammExists()); } // This test validates both invariant changes work together for // the specific case of MPT/MPT pools with > kMaxDeletableAmmTrustLines. { Env env( *this, envconfig([](std::unique_ptr cfg) { cfg->FEES.reference_fee = XRPAmount(1); return cfg; }), all); env.fund(XRP(1'000), gw_, alice_); MPT const usd = MPTTester({.env = env, .issuer = gw_, .holders = {alice_}, .pay = 20'000}); MPT const btc = MPTTester({.env = env, .issuer = gw_, .holders = {alice_}, .pay = 20'000}); // MPT/MPT pool with MANY trustlines AMM amm(env, gw_, usd(10'000), btc(10'000)); for (auto i = 0; i < (kMaxDeletableAmmTrustLines * 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(); } amm.withdrawAll(gw_); // AMM is in empty state, but can't be auto-deleted because of the LPTokens trustlines. BEAST_EXPECT(amm.expectBalances(usd(0), btc(0), IOUAmount(0))); BEAST_EXPECT(amm.ammExists()); // Critical: MPT/MPT pool + tecINCOMPLETE amm.ammDelete(alice_, Ter(tecINCOMPLETE)); BEAST_EXPECT(amm.ammExists()); amm.ammDelete(alice_); BEAST_EXPECT(!amm.ammExists()); } } void run() override { FeatureBitset const all{jtx::testableAmendments()}; testInstanceCreate(); testInvalidInstance(); testInvalidDeposit(all); testInvalidDeposit(all - featureAMMClawback); testDeposit(); testInvalidWithdraw(); testWithdraw(); testInvalidFeeVote(); testFeeVote(); testInvalidBid(); testBid(all); testClawback(); testClawbackFromAMMAccount(all); testClawbackFromAMMAccount(all - featureSingleAssetVault); testInvalidAMMPayment(); testBasicPaymentEngine(); testAMMTokens(); testAmendment(); testAMMAndCLOB(all); testAMMOfferGenerationPolicy(all); testTradingFee(all); testTradingFee(all - fixAMMv1_3); testAdjustedTokens(all); testAMMID(); testSelection(all); testMalformed(); testFixAMMOfferBlockedByLOB(all - fixAMMv1_1 - fixAMMv1_3); testFixAMMOfferBlockedByLOB(all); testLPTokenBalance(all); testLPTokenBalance(all - fixAMMv1_3); testAMMDepositWithFrozenAssets(); testAutoDelete(); } }; BEAST_DEFINE_TESTSUITE_PRIO(AMMMPT, app, xrpl, 1); } // namespace xrpl::test