#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 { /** * Tests of AMM MPT that use offers. */ struct AMMExtendedMPT_test : public jtx::AMMTest { private: void testRmFundedOffer(FeatureBitset features) { testcase("Incorrect Removal of Funded Offers"); // We need at least two paths. One at good quality and one at bad // quality. The bad quality path needs two offer books in a row. // Each offer book should have two offers at the same quality, the // offers should be completely consumed, and the payment should // require both offers to be satisfied. The first offer must // be "taker gets" XRP. Ensure that the payment engine does not remove // the first "taker gets" xrp offer, because the offer is still // funded and not used for the payment. using namespace jtx; Env env{*this, features}; fund(env, gw_, {alice_, bob_, carol_}, XRP(10'000)); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 200'000'000'000'000'000, .flags = kMptDexFlags}); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 2'000'000'000'000'000, .flags = kMptDexFlags}); // Must be two offers at the same quality // "taker gets" must be XRP // (Different amounts so I can distinguish the offers) env(offer(carol_, btc(49'000'000'000'000), XRP(49))); env(offer(carol_, btc(51'000'000'000'000), XRP(51))); // Offers for the poor quality path // Must be two offers at the same quality env(offer(carol_, XRP(50), eth(50'000'000'000'000))); env(offer(carol_, XRP(50), eth(50'000'000'000'000))); // Good quality path AMM const ammCarol(env, carol_, btc(1'000'000'000'000'000), eth(100'100'000'000'000'000)); PathSet const paths(TestPath(XRP, MPT(eth)), TestPath(MPT(eth))); env(pay(alice_, bob_, eth(100'000'000'000'000)), Json(paths.json()), Sendmax(btc(1'000'000'000'000'000)), Txflags(tfPartialPayment)); BEAST_EXPECT(ammCarol.expectBalances( btc(1'001'000'000'374'816), eth(100'000'000'000'000'000), ammCarol.tokens())); env.require(Balance(bob_, eth(200'100'000'000'000'000))); BEAST_EXPECT(isOffer(env, carol_, btc(49'000'000'000'000), XRP(49))); } void testFillModes(FeatureBitset features) { testcase("Fill Modes"); using namespace jtx; auto const startBalance = XRP(1'000'000); // Fill or Kill - unless we fully cross, just charge a fee and don't // place the offer on the books. But also clean up expired offers // that are discovered along the way. testAMM( [&](AMM& ammAlice, Env& env) { auto const& btc = MPT(ammAlice[1]); auto const baseFee = env.current()->fees().base; auto carolBTC = env.balance(carol_, btc); auto carolXRP = env.balance(carol_, XRP); // Order that can't be filled env(offer(carol_, btc(100), XRP(100)), Txflags(tfFillOrKill), Ter(tecKILLED)); env.close(); BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), btc(10'000), ammAlice.tokens())); // fee = AMM env.require(Balance(carol_, carolXRP - baseFee)); env.require(Balance(carol_, carolBTC)); BEAST_EXPECT(expectOffers(env, carol_, 0)); carolXRP = env.balance(carol_, XRP); // Order that can be filled env(offer(carol_, XRP(100), btc(100)), Txflags(tfFillOrKill), Ter(tesSUCCESS)); BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), btc(10'100), ammAlice.tokens())); env.require(Balance(carol_, carolXRP + XRP(100) - baseFee)); env.require(Balance(carol_, carolBTC - btc(100))); BEAST_EXPECT(expectOffers(env, carol_, 0)); }, {{XRP(10'100), gAmmmpt(10'000)}}, 0, std::nullopt, {features}); // Immediate or Cancel - cross as much as possible // and add nothing on the books. testAMM( [&](AMM& ammAlice, Env& env) { auto const& btc = MPT(ammAlice[1]); auto const baseFee = env.current()->fees().base; auto carolBTC = env.balance(carol_, btc); auto carolXRP = env.balance(carol_, XRP); env(offer(carol_, XRP(200), btc(200)), Txflags(tfImmediateOrCancel), Ter(tesSUCCESS)); // AMM generates a synthetic offer of 100BTC/100XRP // to match the CLOB offer quality. BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), btc(10'100), ammAlice.tokens())); // +AMM - offer * fee env.require(Balance(carol_, carolXRP + XRP(100) - baseFee)); env.require(Balance(carol_, carolBTC - btc(100))); BEAST_EXPECT(expectOffers(env, carol_, 0)); }, {{XRP(10'100), gAmmmpt(10'000)}}, 0, std::nullopt, {features}); // tfPassive -- place the offer without crossing it. testAMM( [&](AMM& ammAlice, Env& env) { // Carol creates a passive offer that could cross AMM. // Carol's offer should stay in the ledger. auto const& btc = MPT(ammAlice[1]); env(offer(carol_, XRP(100), btc(100), tfPassive)); env.close(); BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), btc(10'000), ammAlice.tokens())); BEAST_EXPECT(expectOffers(env, carol_, 1, {{{XRP(100), btc(100)}}})); }, {{XRP(10'100), gAmmmpt(10'000)}}, 0, std::nullopt, {features}); // tfPassive -- cross only offers of better quality. testAMM( [&](AMM& ammAlice, Env& env) { auto const& btc = MPT(ammAlice[1]); env(offer(alice_, btc(110), XRP(100))); env.close(); // Carol creates a passive offer. That offer should cross // AMM and leave Alice's offer untouched. env(offer(carol_, XRP(100), btc(100), tfPassive)); env.close(); BEAST_EXPECT(ammAlice.expectBalances(XRP(10'900), btc(9083), ammAlice.tokens())); BEAST_EXPECT(expectOffers(env, carol_, 0)); BEAST_EXPECT(expectOffers(env, alice_, 1)); }, {{XRP(11'000), gAmmmpt(9'000)}}, 0, std::nullopt, {features}); } void testOfferCrossWithXRP(FeatureBitset features) { testcase("Offer Crossing with XRP, Normal order"); using namespace jtx; Env env{*this, features}; fund(env, gw_, {bob_, alice_}, XRP(300'000)); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .pay = 100'000'000, .flags = kMptDexFlags}); AMM const ammAlice(env, alice_, XRP(150'000), btc(50'000'000)); // Existing offer pays better than this wants. // Partially consume existing offer. // Pay 1'000'000 BTC, get 3061224490 Drops. auto const xrpTransferred = XRPAmount{3'061'224'490}; env(offer(bob_, btc(1'000'000), XRP(4'000))); BEAST_EXPECT(ammAlice.expectBalances( XRP(150'000) + xrpTransferred, btc(49'000'000), IOUAmount{273'861'278752583, -5})); env.require(Balance(bob_, btc(101'000'000))); BEAST_EXPECT( expectLedgerEntryRoot(env, bob_, XRP(300'000) - xrpTransferred - 2 * txFee(env, 1))); BEAST_EXPECT(expectOffers(env, bob_, 0)); } void testOfferCrossWithLimitOverride(FeatureBitset features) { testcase("Offer Crossing with Limit Override"); using namespace jtx; Env env{*this, features}; env.fund(XRP(200'000), gw_, alice_, bob_); env.close(); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .flags = kMptDexFlags}); env(pay(gw_, alice_, btc(500'000'000))); AMM const ammAlice(env, alice_, XRP(150'000), btc(51'000'000)); env(offer(bob_, btc(1'000'000), XRP(3'000))); BEAST_EXPECT(ammAlice.expectBalances(XRP(153'000), btc(50'000'000), ammAlice.tokens())); env.require(Balance(bob_, btc(1'000'000))); env.require(Balance(bob_, XRP(200'000) - XRP(3'000) - env.current()->fees().base * 2)); } void testCurrencyConversionEntire(FeatureBitset features) { testcase("Currency Conversion: Entire Offer"); using namespace jtx; Env env{*this, features}; fund(env, gw_, {alice_, bob_}, XRP(10'000)); env.require(Owners(bob_, 0)); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .flags = kMptDexFlags}); env(pay(gw_, bob_, btc(1'000'000'000))); env.require(Owners(alice_, 1), Owners(bob_, 1)); env(pay(gw_, alice_, btc(100'000'000))); AMM const ammBob(env, bob_, btc(200'000'000), XRP(1'500)); env(pay(alice_, alice_, XRP(500)), Sendmax(btc(100'000'000))); BEAST_EXPECT(ammBob.expectBalances(btc(300'000'000), XRP(1'000), ammBob.tokens())); env.require(Balance(alice_, btc(0))); auto jrr = ledgerEntryRoot(env, alice_); env.require(Balance(alice_, XRP(10'000) + XRP(500) - env.current()->fees().base * 2)); } void testCurrencyConversionInParts(FeatureBitset features) { testcase("Currency Conversion: In Parts"); using namespace jtx; Env env{*this, features}; env.fund(XRP(30'000), gw_, bob_); env.fund(XRP(40'000), alice_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .pay = 30'000'000'000, .flags = kMptDexFlags}); env(pay(gw_, alice_, btc(10'000'000'000))); AMM const ammAlice(env, alice_, XRP(10'000), btc(10'000'000'000)); env.close(); // Alice converts BTC to XRP which should fail // due to PartialPayment. env(pay(alice_, alice_, XRP(100)), Sendmax(btc(100'000'000)), Ter(tecPATH_PARTIAL)); // Alice converts BTC to XRP, should succeed because // we permit partial payment env(pay(alice_, alice_, XRP(100)), Sendmax(btc(100'000'000)), Txflags(tfPartialPayment)); env.close(); BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{9'900'990'100}, btc(10'100'000'000), ammAlice.tokens())); // initial 40,000'000'000 - 10,000'000'000AMM - 100'000'000pay env.require(Balance(alice_, btc(29'900'000'000))); // initial 40,000 - 10,0000AMM + 99.009900pay - fee*3 BEAST_EXPECT(expectLedgerEntryRoot( env, alice_, XRP(40'000) - XRP(10'000) + XRPAmount{99'009'900} - ammCrtFee(env) - txFee(env, 3))); } void testCrossCurrencyStartXRP(FeatureBitset features) { testcase("Cross Currency Payment: Start with XRP"); using namespace jtx; Env env{*this, features}; env.fund(XRP(30'000), gw_); env.fund(XRP(40'000), alice_); env.fund(XRP(1'000), bob_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .flags = kMptDexFlags}); env(pay(gw_, alice_, btc(10'100'000'000))); AMM const ammAlice(env, alice_, XRP(10'000), btc(10'100'000'000)); env.close(); env(pay(alice_, bob_, btc(100'000'000)), Sendmax(XRP(100))); BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), btc(10'000'000'000), ammAlice.tokens())); env.require(Balance(bob_, btc(100'000'000))); } void testCrossCurrencyEndXRP(FeatureBitset features) { testcase("Cross Currency Payment: End with XRP"); using namespace jtx; Env env{*this, features}; env.fund(XRP(30'000), gw_); env.fund(XRP(40'100), alice_); env.fund(XRP(1'000), bob_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .flags = kMptDexFlags}); env(pay(gw_, alice_, btc(40'000'000'000))); AMM const ammAlice(env, alice_, XRP(10'100), btc(10'000'000'000)); env.close(); env(pay(alice_, bob_, XRP(100)), Sendmax(btc(100'000'000))); BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), btc(10'100'000'000), ammAlice.tokens())); BEAST_EXPECT(expectLedgerEntryRoot(env, bob_, XRP(1'000) + XRP(100) - txFee(env, 1))); } void testCrossCurrencyBridged(FeatureBitset features) { testcase("Cross Currency Payment: Bridged"); using namespace jtx; auto test = [&](auto&& issue1, auto&& issue2) { Env env(*this); auto const dan = Account{"dan"}; env.fund(XRP(60'000), alice_, bob_, carol_, gw_, dan); env.close(); auto const eth = issue1( {.env = env, .token = "ETH", .issuer = gw_, .holders = {alice_, bob_, carol_, dan}, .limit = 10'000'000'000'000'000}); auto const btc = issue2( {.env = env, .token = "BTC", .issuer = gw_, .holders = {alice_, bob_, carol_, dan}, .limit = 10'000'000'000'000'000}); env(pay(gw_, alice_, btc(500'000'000'000'000))); env(pay(gw_, carol_, btc(6'000'000'000'000'000))); env(pay(gw_, dan, eth(400'000'000'000'000))); env.close(); env.close(); AMM const ammCarol(env, carol_, btc(5'000'000'000'000'000), XRP(50'000)); env(offer(dan, XRP(500), eth(50'000'000'000'000))); env.close(); json::Value jtp{json::ValueType::Array}; jtp[0u][0u][jss::currency] = "XRP"; env(pay(alice_, bob_, eth(30'000'000'000'000)), Json(jss::Paths, jtp), Sendmax(btc(333'000'000'000'000))); env.close(); BEAST_EXPECT(ammCarol.expectBalances( XRP(49'700), btc(5'030'181'086'519'115), ammCarol.tokens())); BEAST_EXPECT(expectOffers(env, dan, 1, {{Amounts{XRP(200), eth(20'000'000'000'000)}}})); env.require(Balance(bob_, eth(30'000'000'000'000))); }; testHelper2TokensMix(test); } void testOfferFeesConsumeFunds(FeatureBitset features) { testcase("Offer Fees Consume Funds"); using namespace jtx; Env env{*this, features}; // Provide micro amounts to compensate for fees to make results round // nice. auto const startingXrp = XRP(100) + env.current()->fees().accountReserve(2) + env.current()->fees().base * 3; env.fund(startingXrp, gw_, alice_); env.fund(XRP(2'000), bob_); env.close(); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .flags = kMptDexFlags}); // Created only to increase one reserve count for alice MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_}, .flags = kMptDexFlags}); env(pay(gw_, bob_, btc(1'200'000'000'000'000))); AMM const ammBob(env, bob_, XRP(1'000), btc(1'200'000'000'000'000)); // Alice has 400 - (2 reserve of 50 = 300 reserve) = 100 available. // Ask for more than available to prove reserve works. env(offer(alice_, btc(200'000'000'000'000), XRP(200))); // The pool gets only 100XRP for ~109.09e12BTC, even though // it can exchange more. BEAST_EXPECT( ammBob.expectBalances(XRP(1'100), btc(1'090'909'090'909'091), ammBob.tokens())); env.require(Balance(alice_, btc(109'090'909'090'909))); env.require(Balance(alice_, XRP(300))); } void testOfferCreateThenCross(FeatureBitset features) { testcase("Offer Create, then Cross"); using namespace jtx; Env env{*this, features}; fund(env, gw_, {alice_, bob_}, XRP(200'000)); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .transferFee = 500, .flags = kMptDexFlags}); env(pay(gw_, bob_, btc(1'000'000'000'000))); env(pay(gw_, alice_, btc(200'000'000'000'000))); AMM const ammAlice(env, alice_, btc(150'000'000'000'000), XRP(150'100)); env(offer(bob_, XRP(100), btc(100'000'000'000))); BEAST_EXPECT( ammAlice.expectBalances(btc(150'100'000'000'000), XRP(150'000), ammAlice.tokens())); // Bob pays 0.005 transfer fee. env.require(Balance(bob_, btc(899'500'000'000))); } void testSellFlagBasic(FeatureBitset features) { testcase("Offer tfSell: Basic Sell"); using namespace jtx; Env env{*this, features}; env.fund(XRP(30'000), gw_, bob_, carol_); env.fund(XRP(39'900), alice_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 30'000, .flags = kMptDexFlags}); env(pay(gw_, alice_, btc(10'100))); AMM const ammAlice(env, alice_, XRP(9'900), btc(10'100)); env(offer(carol_, btc(100), XRP(100)), Json(jss::Flags, tfSell)); env.close(); BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), btc(9'999), ammAlice.tokens())); BEAST_EXPECT(expectOffers(env, carol_, 0)); env.require(Balance(carol_, btc(30'101))); BEAST_EXPECT( expectLedgerEntryRoot(env, carol_, XRP(30'000) - XRP(100) - 2 * txFee(env, 1))); } void testSellFlagExceedLimit(FeatureBitset features) { testcase("Offer tfSell: 2x Sell Exceed Limit"); using namespace jtx; Env env{*this, features}; auto const startingXrp = XRP(100) + reserve(env, 1) + env.current()->fees().base * 2; env.fund(startingXrp, gw_, alice_); env.fund(XRP(2'000), bob_); env.close(); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .flags = kMptDexFlags}); env(pay(gw_, bob_, btc(2'200'000'000))); AMM const ammBob(env, bob_, XRP(1'000), btc(2'200'000'000)); // Alice has 350 fees - a reserve of 50 = 250 reserve = 100 available. // Ask for more than available to prove reserve works. // Taker pays 100'000'000 BTC for 100 XRP. // Selling XRP. // Will sell all 100 XRP and get more BTC than asked for. env(offer(alice_, btc(100'000'000), XRP(200)), Json(jss::Flags, tfSell)); BEAST_EXPECT(ammBob.expectBalances(XRP(1'100), btc(2'000'000'000), ammBob.tokens())); env.require(Balance(alice_, btc(200'000'000))); BEAST_EXPECT(expectLedgerEntryRoot(env, alice_, XRP(250))); BEAST_EXPECT(expectOffers(env, alice_, 0)); } void testGatewayCrossCurrency(FeatureBitset features) { testcase("Client Issue: Gateway Cross Currency"); using namespace jtx; Env env{*this, features}; auto const startingXrp = XRP(100.1) + reserve(env, 1) + env.current()->fees().base * 2; env.fund(startingXrp, gw_, alice_, bob_); MPTTester const xts( {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .pay = 1'000'000'000'000'000, .flags = kMptDexFlags}); MPTTester const xxx( {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .pay = 1'000'000'000'000'000, .flags = kMptDexFlags}); AMM const ammAlice(env, alice_, xts(1'000'000'000'000'000), xxx(1'000'000'000'000'000)); json::Value payment; payment[jss::secret] = toBase58(generateSeed("bob")); payment[jss::id] = env.seq(bob_); payment[jss::build_path] = true; payment[jss::tx_json] = pay(bob_, bob_, xxx(10'000'000'000'000)); payment[jss::tx_json][jss::Sequence] = env.current()->read(keylet::account(bob_.id()))->getFieldU32(sfSequence); payment[jss::tx_json][jss::Fee] = to_string(env.current()->fees().base); payment[jss::tx_json][jss::SendMax] = xts(15'000'000'000'000).value().getJson(JsonOptions::Values::None); payment[jss::tx_json][jss::Flags] = tfPartialPayment; auto const jrr = env.rpc("json", "submit", to_string(payment)); BEAST_EXPECT(jrr[jss::result][jss::status] == "success"); BEAST_EXPECT(jrr[jss::result][jss::engine_result] == "tesSUCCESS"); BEAST_EXPECT(ammAlice.expectBalances( xts(1'010'101'010'101'011), xxx(990'000'000'000'000), ammAlice.tokens())); env.require(Balance(bob_, xts(989'898'989'898'989))); env.require(Balance(bob_, xxx(1'010'000'000'000'000))); } void testBridgedCross(FeatureBitset features) { testcase("Bridged Crossing"); using namespace jtx; { Env env{*this, features}; env.fund(XRP(30'000), gw_, alice_, bob_, carol_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 15'000'000'000, .flags = kMptDexFlags}); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 15'000'000'000, .flags = kMptDexFlags}); // The scenario: // o BTC/XRP AMM is created. // o ETH/XRP AMM is created. // o carol has ETH but wants BTC. // Note that carol's offer must come last. If carol's offer is // placed before AMM is created, then autobridging will not occur. AMM const ammAlice(env, alice_, XRP(10'000), btc(10'100'000'000)); AMM const ammBob(env, bob_, eth(10'000'000'000), XRP(10'100)); // Carol makes an offer that consumes AMM liquidity and // fully consumes Carol's offer. env(offer(carol_, btc(100'000'000), eth(100'000'000))); env.close(); BEAST_EXPECT( ammAlice.expectBalances(XRP(10'100), btc(10'000'000'000), ammAlice.tokens())); BEAST_EXPECT(ammBob.expectBalances(XRP(10'000), eth(10'100'000'000), ammBob.tokens())); env.require(Balance(carol_, btc(15'100'000'000))); env.require(Balance(carol_, eth(14'900'000'000))); BEAST_EXPECT(expectOffers(env, carol_, 0)); } { Env env{*this, features}; env.fund(XRP(30'000), gw_, alice_, bob_, carol_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 15'000'000'000, .flags = kMptDexFlags}); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 15'000'000'000, .flags = kMptDexFlags}); // The scenario: // o BTC/XRP AMM is created. // o ETH/XRP offer is created. // o carol has ETH but wants BTC. // Note that carol's offer must come last. If carol's offer is // placed before AMM and bob's offer are created, then autobridging // will not occur. AMM const ammAlice(env, alice_, XRP(10'000), btc(10'100'000'000)); env(offer(bob_, eth(100'000'000), XRP(100))); env.close(); // Carol makes an offer that consumes AMM liquidity and // fully consumes Carol's offer. env(offer(carol_, btc(100'000'000), eth(100'000'000))); env.close(); BEAST_EXPECT( ammAlice.expectBalances(XRP(10'100), btc(10'000'000'000), ammAlice.tokens())); env.require(Balance(carol_, btc(15'100'000'000))); env.require(Balance(carol_, eth(14'900'000'000))); BEAST_EXPECT(expectOffers(env, carol_, 0)); BEAST_EXPECT(expectOffers(env, bob_, 0)); } { Env env{*this, features}; env.fund(XRP(30'000), gw_, alice_, bob_, carol_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 15'000'000'000, .flags = kMptDexFlags}); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 15'000'000'000, .flags = kMptDexFlags}); // The scenario: // o BTC/XRP offer is created. // o ETH/XRP AMM is created. // o carol has ETH but wants BTC. // Note that carol's offer must come last. If carol's offer is // placed before AMM and alice's offer are created, then // autobridging will not occur. env(offer(alice_, XRP(100), btc(100'000'000))); env.close(); AMM const ammBob(env, bob_, eth(10'000'000'000), XRP(10'100)); // Carol makes an offer that consumes AMM liquidity and // fully consumes Carol's offer. env(offer(carol_, btc(100'000'000), eth(100'000'000))); env.close(); BEAST_EXPECT(ammBob.expectBalances(XRP(10'000), eth(10'100'000'000), ammBob.tokens())); env.require(Balance(carol_, btc(15'100'000'000))); env.require(Balance(carol_, eth(14'900'000'000))); BEAST_EXPECT(expectOffers(env, carol_, 0)); BEAST_EXPECT(expectOffers(env, alice_, 0)); } } void testSellWithFillOrKill(FeatureBitset features) { // Test a number of different corner cases regarding offer crossing // when both the tfSell flag and tfFillOrKill flags are set. testcase("Combine tfSell with tfFillOrKill"); using namespace jtx; { Env env{*this, features}; env.fund(XRP(30'000), gw_, alice_, bob_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .pay = 20'000'000'000, .flags = kMptDexFlags}); AMM const ammBob(env, bob_, XRP(20'000), btc(200'000'000)); // alice submits a tfSell | tfFillOrKill offer that does not cross. env(offer(alice_, btc(2'100'000), XRP(210), tfSell | tfFillOrKill), Ter(tecKILLED)); BEAST_EXPECT(ammBob.expectBalances(XRP(20'000), btc(200'000'000), ammBob.tokens())); BEAST_EXPECT(expectOffers(env, bob_, 0)); } { Env env{*this, features}; env.fund(XRP(30'000), gw_, alice_, bob_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .pay = 1'000'000'000'000'000, .flags = kMptDexFlags}); AMM const ammBob(env, bob_, XRP(20'000), btc(200'000'000'000'000)); // alice submits a tfSell | tfFillOrKill offer that crosses. // Even though tfSell is present it doesn't matter this time. env(offer(alice_, btc(2'000'000'000'000), XRP(220), tfSell | tfFillOrKill)); env.close(); BEAST_EXPECT( ammBob.expectBalances(XRP(20'220), btc(197'823'936'696'341), ammBob.tokens())); env.require(Balance(alice_, btc(1'002'176'063'303'659))); BEAST_EXPECT(expectOffers(env, alice_, 0)); } { // alice submits a tfSell | tfFillOrKill offer that crosses and // returns more than was asked for (because of the tfSell flag). Env env{*this, features}; env.fund(XRP(30'000), gw_, alice_, bob_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .pay = 1'000'000'000'000'000, .flags = kMptDexFlags}); AMM const ammBob(env, bob_, XRP(20'000), btc(200'000'000'000'000)); env(offer(alice_, btc(10'000'000'000'000), XRP(1'500), tfSell | tfFillOrKill)); env.close(); BEAST_EXPECT( ammBob.expectBalances(XRP(21'500), btc(186'046'511'627'907), ammBob.tokens())); env.require(Balance(alice_, btc(1'013'953'488'372'093))); BEAST_EXPECT(expectOffers(env, alice_, 0)); } { // alice submits a tfSell | tfFillOrKill offer that doesn't cross. // This would have succeeded with a regular tfSell, but the // fillOrKill prevents the transaction from crossing since not // all of the offer is consumed because AMM generated offer, // which matches alice's offer quality is ~ 10XRP/0.01996e3BTC. Env env{*this, features}; env.fund(XRP(30'000), gw_, alice_, bob_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .pay = 10'000'000'000, .flags = kMptDexFlags}); AMM const ammBob(env, bob_, XRP(5000), btc(10'000'000)); env(offer(alice_, btc(1'000'000), XRP(501), tfSell | tfFillOrKill), Ter(tecKILLED)); env.close(); BEAST_EXPECT(expectOffers(env, alice_, 0)); BEAST_EXPECT(expectOffers(env, bob_, 0)); } } void testTransferRateOffer(FeatureBitset features) { testcase("Transfer Rate Offer"); using namespace jtx; // AMM XRP/BTC. Alice places BTC/XRP offer. { Env env(*this, features); env.fund(XRP(30'000), gw_, bob_, carol_); env.fund(XRP(40'000), alice_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .transferFee = 25'000, .pay = 30'000'000, .flags = kMptDexFlags}); env(pay(gw_, alice_, btc(10'100'000))); AMM const ammAlice(env, alice_, XRP(10'000), btc(10'100'000)); env.close(); env(offer(carol_, btc(100'000), XRP(100))); env.close(); // AMM doesn't pay the transfer fee BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), btc(10'000'000), ammAlice.tokens())); env.require(Balance(carol_, btc(30'100'000))); BEAST_EXPECT(expectOffers(env, carol_, 0)); } { Env env(*this, features); env.fund(XRP(30'000), gw_, bob_, carol_); env.fund(XRP(40'100), alice_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .transferFee = 25'000, .pay = 30'000'000, .flags = kMptDexFlags}); env(pay(gw_, alice_, btc(10'000'000))); AMM const ammAlice(env, alice_, XRP(10'100), btc(10'000'000)); env.close(); env(offer(carol_, XRP(100), btc(100'000))); env.close(); BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), btc(10'100'000), ammAlice.tokens())); // Carol pays 25% transfer fee env.require(Balance(carol_, btc(29'875'000))); BEAST_EXPECT(expectOffers(env, carol_, 0)); } { // Bridged crossing. Env env{*this, features}; env.fund(XRP(30'000), gw_, alice_, bob_, carol_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .transferFee = 25'000, .pay = 15'000'000, .flags = kMptDexFlags}); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .transferFee = 25'000, .pay = 15'000'000, .flags = kMptDexFlags}); // The scenario: // o BTC/XRP AMM is created. // o ETH/XRP Offer is created. // o carol has ETH but wants BTC. // Note that Carol's offer must come last. If Carol's offer is // placed before AMM is created, then autobridging will not occur. AMM const ammAlice(env, alice_, XRP(10'000), btc(10'100'000)); env(offer(bob_, eth(100'000), XRP(100))); env.close(); // Carol makes an offer that consumes AMM liquidity and // fully consumes Bob's offer. env(offer(carol_, btc(100'000), eth(100'000))); env.close(); // AMM doesn't pay the transfer fee BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), btc(10'000'000), ammAlice.tokens())); env.require(Balance(carol_, btc(15'100'000))); // Carol pays 25% transfer fee. env.require(Balance(carol_, eth(14'875'000))); BEAST_EXPECT(expectOffers(env, carol_, 0)); BEAST_EXPECT(expectOffers(env, bob_, 0)); } { // Bridged crossing. The transfer fee is paid on the step not // involving AMM as src/dst. Env env{*this, features}; env.fund(XRP(30'000), gw_, alice_, bob_, carol_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .transferFee = 25'000, .pay = 15'000'000, .flags = kMptDexFlags}); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .transferFee = 25'000, .pay = 15'000'000, .flags = kMptDexFlags}); // The scenario: // o BTC/XRP AMM is created. // o ETH/XRP Offer is created. // o carol has ETH but wants BTC. // Note that Carol's offer must come last. If Carol's offer is // placed before AMM is created, then autobridging will not occur. AMM const ammAlice(env, alice_, XRP(10'000), btc(10'050'000)); env(offer(bob_, eth(100'000), XRP(100))); env.close(); // Carol makes an offer that consumes AMM liquidity and // partially consumes Bob's offer. env(offer(carol_, btc(50'000), eth(50'000))); env.close(); // This test verifies that the amount removed from an offer // accounts for the transfer fee that is removed from the // account but not from the remaining offer. // AMM doesn't pay the transfer fee BEAST_EXPECT(ammAlice.expectBalances(XRP(10'050), btc(10'000'000), ammAlice.tokens())); env.require(Balance(carol_, btc(15'050'000))); // Carol pays 25% transfer fee. env.require(Balance(carol_, eth(14'937'500))); BEAST_EXPECT(expectOffers(env, carol_, 0)); BEAST_EXPECT(expectOffers(env, bob_, 1, {{Amounts{eth(50'000), XRP(50)}}})); } } void testSelfIssueOffer(FeatureBitset features) { // This test is not the same as corresponding testSelfIssueOffer() // in the Offer_test. It simply tests AMM with self issue and // offer crossing. using namespace jtx; Env env{*this, features}; auto const f = env.current()->fees().base; env.fund(XRP(30'000) + f, alice_, bob_); env.close(); MPTTester const btc( {.env = env, .issuer = bob_, .holders = {alice_}, .flags = kMptDexFlags}); AMM const ammBob(env, bob_, XRP(10'000), btc(10'100)); env(offer(alice_, btc(100), XRP(100))); env.close(); BEAST_EXPECT(ammBob.expectBalances(XRP(10'100), btc(10'000), ammBob.tokens())); BEAST_EXPECT(expectOffers(env, alice_, 0)); env.require(Balance(alice_, btc(100))); } void testDirectToDirectPath(FeatureBitset features) { // The offer crossing code expects that a DirectStep is always // preceded by a BookStep. In one instance the default path // was not matching that assumption. Here we recreate that case // so we can prove the bug stays fixed. testcase("Direct to Direct path"); using namespace jtx; Env env{*this, features}; auto const ann = Account("ann"); auto const bob = Account("bob"); auto const cam = Account("cam"); auto const carol = Account("carol"); auto const fee = env.current()->fees().base; env.fund(XRP(1'000), carol); env.fund(reserve(env, 4) + (fee * 5), ann, bob, cam); env.close(); MPTTester const aBux( {.env = env, .issuer = ann, .holders = {bob, cam, carol}, .flags = kMptDexFlags}); MPTTester const bBux( {.env = env, .issuer = bob, .holders = {ann, cam, carol}, .flags = kMptDexFlags}); env(pay(ann, cam, aBux(350'000'000'000'000))); env(pay(bob, cam, bBux(350'000'000'000'000))); env(pay(bob, carol, bBux(4'000'000'000'000'000))); env(pay(ann, carol, aBux(4'000'000'000'000'000))); AMM const ammCarol(env, carol, aBux(3'000'000'000'000'000), bBux(3'300'000'000'000'000)); // cam puts an offer on the books that her upcoming offer could cross. // But this offer should be deleted, not crossed, by her upcoming // offer. env(offer(cam, aBux(290'000'000'000'000), bBux(300'000'000'000'000), tfPassive)); env.close(); env.require(Balance(cam, aBux(350'000'000'000'000))); env.require(Balance(cam, bBux(350'000'000'000'000))); env.require(offers(cam, 1)); // This offer caused the assert. env(offer(cam, bBux(300'000'000'000'000), aBux(300'000'000'000'000))); // AMM is consumed up to the first cam Offer quality BEAST_EXPECT(ammCarol.expectBalances( aBux(3'093'541'659'651'604), bBux(3'200'215'509'984'418), ammCarol.tokens())); BEAST_EXPECT(expectOffers( env, cam, 1, {{Amounts{bBux(200'215'509'984'418), aBux(200'215'509'984'419)}}})); } void testRequireAuth(FeatureBitset features) { testcase("RequireAuth"); using namespace jtx; Env env{*this, features}; env.fund(XRP(400'000), gw_, alice_, bob_); MPTTester btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .flags = tfMPTRequireAuth | kMptDexFlags}); // Authorize bob and alice btc.authorize({.holder = alice_}); btc.authorize({.holder = bob_}); env(pay(gw_, alice_, btc(1'000))); env.close(); // Alice is able to create AMM since the GW has authorized her AMM const ammAlice(env, alice_, btc(1'000), XRP(1'050)); env(pay(gw_, bob_, btc(50))); env.close(); env.require(Balance(bob_, btc(50))); // Bob's offer should cross Alice's AMM env(offer(bob_, XRP(50), btc(50))); env.close(); BEAST_EXPECT(ammAlice.expectBalances(btc(1'050), XRP(1'000), ammAlice.tokens())); BEAST_EXPECT(expectOffers(env, bob_, 0)); env.require(Balance(bob_, btc(0))); } void testMissingAuth(FeatureBitset features) { testcase("Missing Auth"); using namespace jtx; Env env{*this, features}; env.fund(XRP(400'000), gw_, alice_, bob_); env.close(); MPTTester btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .flags = tfMPTRequireAuth | kMptDexFlags}); // Alice doesn't have the funds { AMM const ammAlice(env, alice_, btc(1'000), XRP(1'000), Ter(tecNO_AUTH)); } btc.authorize({.holder = bob_}); env(pay(gw_, bob_, btc(50))); env.close(); env.require(Balance(bob_, btc(50))); // Alice should not be able to create AMM without authorization. { AMM const ammAlice(env, alice_, btc(1'000), XRP(1'000), Ter(tecNO_AUTH)); } // Finally, authorize alice. Now alice's AMM create should succeed. btc.authorize({.holder = alice_}); env(pay(gw_, alice_, btc(1'000))); env.close(); AMM const ammAlice(env, alice_, btc(1'000), XRP(1'050)); // Authorize AMM. // BTC.authorize({.account = ammAlice.ammAccount()}); // env.close(); // Now bob creates his offer again, which crosses with alice's AMM. env(offer(bob_, XRP(50), btc(50))); env.close(); BEAST_EXPECT(ammAlice.expectBalances(btc(1'050), XRP(1'000), ammAlice.tokens())); BEAST_EXPECT(expectOffers(env, bob_, 0)); env.require(Balance(bob_, btc(0))); } void testOffers() { using namespace jtx; FeatureBitset const all{testableAmendments()}; testRmFundedOffer(all); testFillModes(all); testOfferCrossWithXRP(all); testOfferCrossWithLimitOverride(all); testCurrencyConversionEntire(all); testCurrencyConversionInParts(all); testCrossCurrencyStartXRP(all); testCrossCurrencyEndXRP(all); testCrossCurrencyBridged(all); testOfferFeesConsumeFunds(all); testOfferCreateThenCross(all); testSellFlagExceedLimit(all); testGatewayCrossCurrency(all); testBridgedCross(all); testSellWithFillOrKill(all); testTransferRateOffer(all); testSelfIssueOffer(all); testSellFlagBasic(all); testDirectToDirectPath(all); testRequireAuth(all); testMissingAuth(all); } void pathFindConsumeAll() { testcase("path find consume all"); using namespace jtx; Env env = pathTestEnv(); env.fund(XRP(100'000'260), alice_); env.fund(XRP(30'000), gw_, bob_, carol_); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 100'000'000'000'000, .flags = kMptDexFlags}); AMM const ammCarol(env, carol_, XRP(100), eth(100'000'000'000'000)); STPathSet st; STAmount sa; STAmount da; std::tie(st, sa, da) = findPaths( env, alice_, bob_, bob_["AUD"](-1), std::optional(XRP(100'000'000))); BEAST_EXPECT(st.empty()); std::tie(st, sa, da) = findPaths(env, alice_, bob_, eth(-1), std::optional(XRP(100'000'000))); // Alice sends all requested 100,000,000XRP BEAST_EXPECT(sa == XRP(100'000'000)); // Bob gets ~99.99e12ETH. This is the amount Bob // can get out of AMM for 100,000,000XRP. BEAST_EXPECT(equal(da, eth(99'999'900'000'100))); } // carol holds ETH, sells ETH for XRP // bob will hold ETH // alice pays bob ETH using XRP void viaOffersViaGateway() { testcase("via gateway"); using namespace jtx; Env env = pathTestEnv(); env.fund(XRP(10'000), alice_, bob_, carol_, gw_); env.close(); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .transferFee = 10'000, .flags = kMptDexFlags}); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .transferFee = 10'000, .flags = kMptDexFlags}); env(pay(gw_, carol_, eth(51))); env.close(); AMM const ammCarol(env, carol_, XRP(40), eth(51)); env(pay(alice_, bob_, eth(10)), Sendmax(XRP(100)), Paths(XRP)); env.close(); // AMM offer is 51.282052XRP/11ETH, 11ETH/1.1 = 10ETH to bob BEAST_EXPECT(ammCarol.expectBalances(XRP(51), eth(40), ammCarol.tokens())); env.require(Balance(bob_, eth(10))); auto const result = findPaths(env, alice_, bob_, btc(25)); BEAST_EXPECT(std::get<0>(result).empty()); } void receiveMax() { testcase("Receive max"); using namespace jtx; auto const charlie = Account("charlie"); { // XRP -> MPT receive max Env env = pathTestEnv(); env.fund(XRP(30'000), alice_, bob_, charlie, gw_); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_, charlie}, .pay = 11'000'000'000'000, .flags = kMptDexFlags}); AMM const ammCharlie(env, charlie, XRP(10), eth(11'000'000'000'000)); auto [st, sa, da] = findPaths(env, alice_, bob_, eth(-1), XRP(1).value()); BEAST_EXPECT(sa == XRP(1)); BEAST_EXPECT(equal(da, eth(1'000'000'000'000))); if (BEAST_EXPECT(st.size() == 1 && st[0].size() == 1)) { auto const& pathElem = st[0][0]; BEAST_EXPECT( pathElem.isOffer() && pathElem.getIssuerID() == gw_.id() && pathElem.getMPTID() == eth.issuanceID()); } } { // MPT -> XRP receive max Env env = pathTestEnv(); env.fund(XRP(30'000), alice_, bob_, charlie, gw_); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_, charlie}, .pay = 11'000'000'000'000, .flags = kMptDexFlags}); AMM const ammCharlie(env, charlie, XRP(11), eth(10'000'000'000'000)); env.close(); auto [st, sa, da] = findPaths(env, alice_, bob_, drops(-1), eth(1'000'000'000'000).value()); BEAST_EXPECT(sa == eth(1'000'000'000'000)); BEAST_EXPECT(equal(da, XRP(1))); if (BEAST_EXPECT(st.size() == 1 && st[0].size() == 1)) { auto const& pathElem = st[0][0]; BEAST_EXPECT( pathElem.isOffer() && pathElem.getIssuerID() == xrpAccount() && pathElem.getCurrency() == xrpCurrency()); } } } void pathFind01() { testcase("Path Find: XRP -> XRP and XRP -> MPT"); using namespace jtx; Env env = pathTestEnv(); Account a1{"A1"}; Account a2{"A2"}; Account a3{"A3"}; Account const g1{"G1"}; Account const g2{"G2"}; Account g3{"G3"}; Account m1{"M1"}; env.fund(XRP(100'000), a1); env.fund(XRP(10'000), a2); env.fund(XRP(1'000), a3, g1, g2, g3); env.fund(XRP(20'000), m1); env.close(); MPTTester const xyzG1( {.env = env, .issuer = g1, .holders = {a1, m1, a2}, .flags = kMptDexFlags}); MPTTester const xyzG2( {.env = env, .issuer = g2, .holders = {a2, m1, a1}, .flags = kMptDexFlags}); MPTTester const abcG3( {.env = env, .issuer = g3, .holders = {a1, a2, m1, a3}, .flags = kMptDexFlags}); MPTTester const abcA2( {.env = env, .issuer = a2, .holders = {g3, a1}, .flags = kMptDexFlags}); env(pay(g1, a1, xyzG1(3'500'000'000))); env(pay(g3, a1, abcG3(1'200'000'000))); env(pay(g1, m1, xyzG1(25'000'000'000))); env(pay(g2, m1, xyzG2(25'000'000'000))); env(pay(g3, m1, abcG3(25'000'000'000))); env(pay(a2, g3, abcA2(101'000'000))); env.close(); AMM const ammM1XyzG1XyzG2(env, m1, xyzG1(1'000'000'000), xyzG2(1'000'000'000)); AMM const ammM1XrpAbcG3(env, m1, XRP(10'000), abcG3(1'000'000'000)); AMM const ammG3AbcG3AbcA2(env, g3, abcG3(100'000'000), abcA2(101'000'000)); env.close(); STPathSet st; STAmount sa, da; { auto const& sendAmt = XRP(10); std::tie(st, sa, da) = findPaths(env, a1, a2, sendAmt, std::nullopt, xrpCurrency()); BEAST_EXPECT(equal(da, sendAmt)); BEAST_EXPECT(st.empty()); } { // no path should exist for this since dest account // does not exist. auto const& sendAmt = XRP(200); std::tie(st, sa, da) = findPaths(env, a1, Account{"A0"}, sendAmt, std::nullopt, xrpCurrency()); BEAST_EXPECT(equal(da, sendAmt)); BEAST_EXPECT(st.empty()); } { auto const& sendAmt = abcG3(10'000'000); std::tie(st, sa, da) = findPaths(env, a2, g3, sendAmt, std::nullopt, xrpCurrency()); BEAST_EXPECT(equal(da, sendAmt)); BEAST_EXPECT(equal(sa, XRPAmount{101'010'102})); BEAST_EXPECT(same(st, stpath(ipe(MPT(abcG3))))); } { auto const& sendAmt = abcA2(1'000'000); std::tie(st, sa, da) = findPaths(env, a1, a2, sendAmt, std::nullopt, xrpCurrency()); BEAST_EXPECT(equal(da, sendAmt)); BEAST_EXPECT(equal(sa, XRPAmount{10'010'011})); BEAST_EXPECT(same(st, stpath(ipe(MPT(abcG3)), ipe(MPT(abcA2))))); } } void pathFind02() { testcase("Path Find: non-XRP -> XRP"); using namespace jtx; Env env = pathTestEnv(); Account a1{"A1"}; Account a2{"A2"}; Account const g3{"G3"}; Account m1{"M1"}; env.fund(XRP(1'000), a1, a2, g3); env.fund(XRP(11'000), m1); env.close(); MPTTester const eth( {.env = env, .issuer = g3, .holders = {a1, a2, m1}, .pay = 1'000'000'000, .flags = kMptDexFlags}); AMM const ammM1(env, m1, eth(1'000'000'000), XRP(10'010)); STPathSet st; STAmount sa, da; auto const& sendAmt = XRP(10); std::tie(st, sa, da) = findPathsByElement(env, a1, a2, sendAmt, std::nullopt, ipe(MPT(eth))); BEAST_EXPECT(equal(da, sendAmt)); BEAST_EXPECT(equal(sa, eth(1'000'000))); BEAST_EXPECT(same(st, stpath(ipe(xrpIssue())))); } void pathFind06() { testcase("Path Find: non-XRP -> non-XRP, same issuanceID"); using namespace jtx; { Env env = pathTestEnv(); Account a1{"A1"}; Account a2{"A2"}; Account const a3{"A3"}; Account const g1{"G1"}; Account const g2{"G2"}; Account m1{"M1"}; env.fund(XRP(11'000), m1); env.fund(XRP(1'000), a1, a2, a3, g1, g2); env.close(); MPTTester const hkdG1( {.env = env, .issuer = g1, .holders = {a1, m1}, .pay = 5'000'000'000, .flags = kMptDexFlags}); MPTTester const hkdG2( {.env = env, .issuer = g2, .holders = {a2, m1}, .pay = 5'000'000'000, .flags = kMptDexFlags}); AMM const ammM1(env, m1, hkdG1(1'000'000'000), hkdG2(1'010'000'000)); auto const& sendAmt = hkdG2(10'000'000); STPathSet st; STAmount sa, da; std::tie(st, sa, da) = jtx::findPaths( env, g1, a2, sendAmt, std::nullopt, hkdG1.issuanceID(), std::nullopt, std::nullopt); BEAST_EXPECT(equal(da, sendAmt)); BEAST_EXPECT(equal(sa, hkdG1(10'000'000))); BEAST_EXPECT(same(st, stpath(ipe(MPT(hkdG2))))); } } void testFalseDry(FeatureBitset features) { testcase("falseDryChanges"); using namespace jtx; Env env(*this, features); env.memoize(bob_); env.fund(XRP(10'000), alice_, gw_); fund(env, gw_, {carol_}, XRP(10'000), {}, Fund::Acct); auto const ammxrpPool = env.current()->fees().increment * 2; env.fund(reserve(env, 5) + ammCrtFee(env) + ammxrpPool, bob_); env.close(); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .flags = kMptDexFlags}); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .flags = kMptDexFlags}); env(pay(gw_, alice_, eth(50'000))); env(pay(gw_, bob_, btc(150'000))); // Bob has _just_ slightly less than 50 xrp available // If his owner count changes, he will have more liquidity. // This is one error case to test (when Flow is used). // Computing the incoming xrp to the XRP/BTC offer will require two // recursive calls to the ETH/XRP offer. The second call will return // tecPATH_DRY, but the entire path should not be marked as dry. // This is the second error case to test (when flowV1 is used). env(offer(bob_, eth(50'000), XRP(50))); AMM const ammBob(env, bob_, ammxrpPool, btc(150'000)); env(pay(alice_, carol_, btc(1'000'000'000)), Path(~XRP, ~MPT(btc)), Sendmax(eth(500'000)), Txflags(tfNoRippleDirect | tfPartialPayment)); auto const carolBTC = env.balance(carol_, MPT(btc)); BEAST_EXPECT(carolBTC > btc(0) && carolBTC < btc(50'000)); } void testBookStep(FeatureBitset features) { testcase("Book Step"); using namespace jtx; // simple MPT/IOU mix offer { auto test = [&](auto&& issue1, auto&& issue2) { Env env(*this); env.fund(XRP(30'000), alice_, bob_, carol_, gw_); env.close(); auto const eth = issue1( {.env = env, .token = "ETH", .issuer = gw_, .holders = {alice_, bob_, carol_}, .limit = 100'000'000}); auto const btc = issue2( {.env = env, .token = "BTC", .issuer = gw_, .holders = {alice_, bob_, carol_}, .limit = 100'000'000}); env(pay(gw_, alice_, btc(500000))); env(pay(gw_, bob_, btc(500000))); env(pay(gw_, carol_, btc(500000))); env(pay(gw_, alice_, eth(500000))); env(pay(gw_, bob_, eth(500000))); env(pay(gw_, carol_, eth(500000))); env.close(); AMM const ammBob(env, bob_, btc(100'000), eth(150'000)); env(pay(alice_, carol_, eth(50'000)), Path(~eth), Sendmax(btc(50'000))); env.require(Balance(alice_, btc(450'000))); env.require(Balance(bob_, btc(400'000))); env.require(Balance(bob_, eth(350'000))); env.require(Balance(carol_, eth(550'000))); BEAST_EXPECT(ammBob.expectBalances(btc(150'000), eth(100'000), ammBob.tokens())); }; testHelper2TokensMix(test); } { // simple MPT/XRP XRP/MPT offer Env env(*this, features); env.fund(XRP(10'000), gw_, alice_, bob_, carol_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 100'000, .flags = kMptDexFlags}); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 150'000, .flags = kMptDexFlags}); AMM const ammBobBtcXrp(env, bob_, btc(100'000), XRP(150)); AMM const ammBobXrpEth(env, bob_, XRP(100), eth(150'000)); env(pay(alice_, carol_, eth(50'000)), Path(~XRP, ~MPT(eth)), Sendmax(btc(50'000))); env.require(Balance(alice_, btc(50'000))); env.require(Balance(bob_, btc(0))); env.require(Balance(bob_, eth(0))); env.require(Balance(carol_, eth(200'000))); BEAST_EXPECT( ammBobBtcXrp.expectBalances(btc(150'000), XRP(100), ammBobBtcXrp.tokens())); BEAST_EXPECT( ammBobXrpEth.expectBalances(XRP(150), eth(100'000), ammBobXrpEth.tokens())); } { // simple XRP -> MPT through offer and sendmax Env env(*this, features); XRPAmount const baseFee{env.current()->fees().base}; env.fund(XRP(10'000), gw_, alice_, bob_, carol_); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 150'000, .flags = kMptDexFlags}); AMM const ammBob(env, bob_, XRP(100), eth(150'000)); env(pay(alice_, carol_, eth(50'000)), Path(~MPT(eth)), Sendmax(XRP(50))); BEAST_EXPECT(expectLedgerEntryRoot(env, alice_, XRP(10'000) - XRP(50) - 2 * baseFee)); BEAST_EXPECT(expectLedgerEntryRoot( env, bob_, XRP(10'000) - XRP(100) - ammCrtFee(env) - baseFee)); env.require(Balance(bob_, eth(0))); env.require(Balance(carol_, eth(200'000))); BEAST_EXPECT(ammBob.expectBalances(XRP(150), eth(100'000), ammBob.tokens())); } { // simple MPT -> XRP through offer and sendmax Env env(*this, features); XRPAmount const baseFee{env.current()->fees().base}; env.fund(XRP(10'000), gw_, alice_, bob_, carol_); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 100'000, .flags = kMptDexFlags}); AMM const ammBob(env, bob_, eth(100'000), XRP(150)); env(pay(alice_, carol_, XRP(50)), Path(~XRP), Sendmax(eth(50'000))); env.require(Balance(alice_, eth(50'000))); BEAST_EXPECT(expectLedgerEntryRoot( env, bob_, XRP(10'000) - XRP(150) - ammCrtFee(env) - baseFee)); env.require(Balance(bob_, eth(0))); BEAST_EXPECT(expectLedgerEntryRoot(env, carol_, XRP(10'000 + 50) - baseFee)); BEAST_EXPECT(ammBob.expectBalances(eth(150'000), XRP(100), ammBob.tokens())); } // test unfunded offers are removed when payment succeeds { auto test = [&](auto&& issue1, auto&& issue2, auto&& issue3) { Env env(*this, features); env.fund(XRP(10'000), alice_, bob_, carol_, gw_); env.close(); auto const btc = issue1( {.env = env, .token = "BTC", .issuer = gw_, .holders = {alice_, bob_, carol_}, .limit = 1000'000000}); auto const eth = issue2( {.env = env, .token = "ETH", .issuer = gw_, .holders = {alice_, bob_, carol_}, .limit = 1000'000000}); auto const gbp = issue3( {.env = env, .token = "GBP", .issuer = gw_, .holders = {alice_, bob_, carol_}, .limit = 1000'000000}); env(pay(gw_, alice_, btc(60'000))); env(pay(gw_, bob_, eth(200'000))); env(pay(gw_, bob_, gbp(150'000))); env(offer(bob_, btc(50'000), eth(50'000))); env(offer(bob_, btc(40'000), gbp(50'000))); env.close(); AMM const ammBob(env, bob_, gbp(100'000), eth(150'000)); // unfund offer env(pay(bob_, gw_, gbp(50'000))); BEAST_EXPECT(isOffer(env, bob_, btc(50'000), eth(50'000))); BEAST_EXPECT(isOffer(env, bob_, btc(40'000), gbp(50'000))); env(pay(alice_, carol_, eth(50'000)), Path(~eth), Path(~gbp, ~eth), Sendmax(btc(60'000))); env.require(Balance(alice_, btc(10'000))); env.require(Balance(bob_, btc(50'000))); env.require(Balance(bob_, eth(0))); env.require(Balance(bob_, gbp(0))); env.require(Balance(carol_, eth(50'000))); // used in the payment BEAST_EXPECT(!isOffer(env, bob_, btc(50'000), eth(50'000))); // found unfunded BEAST_EXPECT(!isOffer(env, bob_, btc(40'000), gbp(50'000))); // unchanged BEAST_EXPECT(ammBob.expectBalances(gbp(100'000), eth(150'000), ammBob.tokens())); }; testHelper3TokensMix(test); } { // test unfunded offers are removed when the payment fails. // bob makes two offers: a funded 50'000'000 ETH for 50'000'000 BTC // and an unfunded 50'000'000 GBP for 60'000'000 BTC. alice pays // carol 61'000'000 ETH with 61'000'000 BTC. alice only has // 60'000'000 BTC, so the payment will fail. The payment uses two // paths: one through bob's funded offer and one through his // unfunded offer. When the payment fails `flow` should return the // unfunded offer. This test is intentionally similar to the one // that removes unfunded offers when the payment succeeds. Env env(*this, features); env.fund(XRP(10'000), bob_, carol_, gw_); env.close(); // Sets rippling on, this is different from // the original test fund(env, gw_, {alice_}, XRP(10'000), {}, Fund::Acct); MPTTester btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .flags = kMptDexFlags}); MPTTester eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .flags = kMptDexFlags}); MPTTester gbp( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .flags = kMptDexFlags}); env(pay(gw_, alice_, btc(60'000'000))); env(pay(gw_, bob_, btc(100'000'000))); env(pay(gw_, bob_, eth(100'000'000))); env(pay(gw_, bob_, gbp(50'000'000))); env(pay(gw_, carol_, gbp(1'000'000))); env.close(); // This is multiplath, which generates limited # of offers AMM const ammBobBtcEth(env, bob_, btc(50'000'000), eth(50'000'000)); env(offer(bob_, btc(60'000'000), gbp(50'000'000))); env(offer(carol_, btc(1'000'000'000), gbp(1'000'000))); env(offer(bob_, gbp(50'000'000), eth(50'000'000))); // unfund offer env(pay(bob_, gw_, gbp(50'000'000))); BEAST_EXPECT(ammBobBtcEth.expectBalances( btc(50'000'000), eth(50'000'000), ammBobBtcEth.tokens())); BEAST_EXPECT(isOffer(env, bob_, btc(60'000'000), gbp(50'000'000))); BEAST_EXPECT(isOffer(env, carol_, btc(1'000'000'000), gbp(1'000'000))); BEAST_EXPECT(isOffer(env, bob_, gbp(50'000'000), eth(50'000'000))); auto flowJournal = env.app().getLogs().journal("Flow"); auto const flowResult = [&] { STAmount const deliver(eth(51'000'000)); STAmount smax(btc(61'000'000)); PaymentSandbox sb(env.current().get(), TapNone); STPathSet paths; auto ipe = [](MPTTester const& iss) { return STPathElement( STPathElement::TypeMpt | STPathElement::TypeIssuer, xrpAccount(), PathAsset{iss.issuanceID()}, iss.issuer()); }; { // BTC -> ETH STPath const p1({ipe(eth)}); paths.pushBack(p1); // BTC -> GBP -> ETH STPath const p2({ipe(gbp), ipe(eth)}); paths.pushBack(p2); } return flow( sb, deliver, alice_, carol_, paths, false, false, true, OfferCrossing::No, std::nullopt, smax, std::nullopt, flowJournal); }(); BEAST_EXPECT(flowResult.removableOffers.size() == 1); env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) { if (flowResult.removableOffers.empty()) return false; Sandbox sb(&view, TapNone); for (auto const& o : flowResult.removableOffers) { if (auto ok = sb.peek(keylet::offer(o))) { offerDelete(sb, ok, flowJournal); } } sb.apply(view); return true; }); // used in payment, but since payment failed should be untouched BEAST_EXPECT(ammBobBtcEth.expectBalances( btc(50'000'000), eth(50'000'000), ammBobBtcEth.tokens())); BEAST_EXPECT(isOffer(env, carol_, btc(1'000'000'000), gbp(1'000'000))); // found unfunded BEAST_EXPECT(!isOffer(env, bob_, btc(60'000'000), gbp(50'000'000))); } { // Do not produce more in the forward pass than the reverse pass // This test uses a path that whose reverse pass will compute a // 500 ETH input required for a 1'000 BTC output. It sets a sendmax // of 400 ETH, so the payment engine will need to do a forward // pass. Without limits, the 400 ETH would produce 1'000 BTC in // the forward pass. This test checks that the payment produces // 1'000 BTC, as expected. auto test = [&](auto&& issue1, auto&& issue2) { Env env(*this); env.fund(XRP(30'000), alice_, bob_, carol_, gw_); env.close(); auto const eth = issue1( {.env = env, .token = "ETH", .issuer = gw_, .holders = {alice_, bob_, carol_}, .limit = 10'000'000}); auto const btc = issue2( {.env = env, .token = "BTC", .issuer = gw_, .holders = {alice_, bob_, carol_}, .limit = 10'000'000}); env(pay(gw_, alice_, eth(1'000'000))); env(pay(gw_, bob_, btc(1'000'000))); env(pay(gw_, bob_, eth(1'000'000))); env.close(); AMM const ammBob(env, bob_, eth(8'000), XRPAmount{21}); env(offer(bob_, drops(1), btc(1'000'000)), Txflags(tfPassive)); env(pay(alice_, carol_, btc(1'000)), Path(~XRP, ~btc), Sendmax(eth(400)), Txflags(tfNoRippleDirect | tfPartialPayment)); env.require(Balance(carol_, btc(1'000))); BEAST_EXPECT(ammBob.expectBalances(eth(8400), XRPAmount{20}, ammBob.tokens())); }; testHelper2TokensMix(test); } } void testTransferRateNoOwnerFee(FeatureBitset features) { testcase("No Owner Fee"); using namespace jtx; { // payment via AMM Env env(*this, features); env.fund(XRP(1'000), gw_, alice_, bob_, carol_); MPTTester const gbp( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .transferFee = 25'000, .pay = 1'000'000'000'000'000, .flags = kMptDexFlags}); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .transferFee = 25'000, .pay = 1'000'000'000'000'000, .flags = kMptDexFlags}); AMM const amm(env, bob_, gbp(1'000'000'000'000'000), btc(1'000'000'000'000'000)); env(pay(alice_, carol_, btc(100'000'000'000'000)), Path(~MPT(btc)), Sendmax(gbp(150'000'000'000'000)), Txflags(tfNoRippleDirect | tfPartialPayment)); env.close(); // alice buys 107.1428e12BTC with 120e12GBP and pays 25% tr fee on // 120e12GBP 1,000e12 - 120e12*1.25 = 850e12GBP env.require(Balance(alice_, gbp(850'000'000'000'000))); BEAST_EXPECT(amm.expectBalances( gbp(1'120'000'000'000'000), btc(892'857'142'857'143), amm.tokens())); // 25% of 85.7142e12BTC is paid in tr fee // 85.7142e12*1.25 = 107.1428e12BTC env.require(Balance(carol_, btc(1'085'714'285'714'285))); } { // Payment via offer and AMM Env env(*this, features); Account const ed("ed"); env.fund(XRP(1'000), gw_, alice_, bob_, carol_, ed); MPTTester const gbp( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_, ed}, .transferFee = 25'000, .pay = 1'000'000'000'000'000, .flags = kMptDexFlags}); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_, ed}, .transferFee = 25'000, .pay = 1'000'000'000'000'000, .flags = kMptDexFlags}); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_, ed}, .transferFee = 25'000, .pay = 1'000'000'000'000'000, .flags = kMptDexFlags}); env(offer(ed, gbp(1'000'000'000'000'000), eth(1'000'000'000'000'000)), Txflags(tfPassive)); env.close(); AMM const amm(env, bob_, eth(1'000'000'000'000'000), btc(1'000'000'000'000'000)); env(pay(alice_, carol_, btc(100'000'000'000'000)), Path(~MPT(eth), ~MPT(btc)), Sendmax(gbp(150'000'000'000'000)), Txflags(tfNoRippleDirect | tfPartialPayment)); env.close(); // alice buys 120e12ETH with 120e12GBP via the offer // and pays 25% tr fee on 120e12GBP // 1,000e12 - 120e12*1.25 = 850e12GBP env.require(Balance(alice_, gbp(850'000'000'000'000))); // consumed offer is 120e12GBP/120e12ETH // ed doesn't pay tr fee env.require(Balance(ed, eth(880'000'000'000'000))); env.require(Balance(ed, gbp(1'120'000'000'000'000))); BEAST_EXPECT(expectOffers( env, ed, 1, {Amounts{gbp(880'000'000'000'000), eth(880'000'000'000'000)}})); // 25% on 96e12ETH is paid in tr fee 96e12*1.25 = 120e12ETH // 96e12ETH is swapped in for 87.5912e12BTC BEAST_EXPECT(amm.expectBalances( eth(1'096'000'000'000'000), btc(912'408'759'124'088), amm.tokens())); // 25% on 70.0729e12BTC is paid in tr fee 70.0729e12*1.25 // = 87.5912e12BTC env.require(Balance(carol_, btc(1'070'072'992'700'729))); } { // Payment via AMM, AMM Env env(*this, features); Account const ed("ed"); env.fund(XRP(1'000), gw_, alice_, bob_, carol_, ed); MPTTester const gbp( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_, ed}, .transferFee = 25'000, .pay = 1'000'000'000'000'000, .flags = kMptDexFlags}); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_, ed}, .transferFee = 25'000, .pay = 1'000'000'000'000'000, .flags = kMptDexFlags}); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_, ed}, .transferFee = 25'000, .pay = 1'000'000'000'000'000, .flags = kMptDexFlags}); AMM const amm1(env, bob_, gbp(1'000'000'000'000'000), eth(1'000'000'000'000'000)); AMM const amm2(env, ed, eth(1'000'000'000'000'000), btc(1'000'000'000'000'000)); env(pay(alice_, carol_, btc(100'000'000'000'000)), Path(~MPT(eth), ~MPT(btc)), Sendmax(gbp(150'000'000'000'000)), Txflags(tfNoRippleDirect | tfPartialPayment)); env.close(); env.require(Balance(alice_, gbp(850'000'000'000'000))); // alice buys 107.1428e12ETH with 120e12GBP and pays 25% tr fee on // 120e12GBP 1,000e12 - 120e12*1.25 = 850e12GBP 120e12GBP is swapped // in for 107.1428e12ETH BEAST_EXPECT(amm1.expectBalances( gbp(1'120'000'000'000'000), eth(892'857'142'857'143), amm1.tokens())); // 25% on 85.7142e12ETH is paid in tr fee 85.7142e12*1.25 = // 107.1428e12ETH 85.7142e12ETH is swapped in for 78.9473e12BTC BEAST_EXPECT(amm2.expectBalances( eth(1'085'714'285'714'285), btc(921'052'631'578'948), amm2.tokens())); // 25% on 63.1578e12BTC is paid in tr fee 63.1578e12*1.25 // = 78.9473e12BTC env.require(Balance(carol_, btc(1'063'157'894'736'841))); } { // AMM offer crossing Env env(*this, features); env.fund(XRP(1'000), gw_, alice_, bob_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .transferFee = 25'000, .pay = 1'100'000, .flags = kMptDexFlags}); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .transferFee = 25'000, .pay = 1'100'000, .flags = kMptDexFlags}); AMM const amm(env, bob_, btc(1'000'000), eth(1'100'000)); env(offer(alice_, eth(100'000), btc(100'000))); env.close(); // 100e3BTC is swapped in for 100e3ETH BEAST_EXPECT(amm.expectBalances(btc(1'100'000), eth(1'000'000), amm.tokens())); // alice pays 25% tr fee on 100e3BTC 1100e3-100e3*1.25 = 975e3BTC env.require(Balance(alice_, btc(975'000))); env.require(Balance(alice_, eth(1'200'000))); BEAST_EXPECT(expectOffers(env, alice_, 0)); } { // Payment via AMM with limit quality Env env(*this, features); env.fund(XRP(1'000), gw_, alice_, bob_, carol_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .transferFee = 25'000, .pay = 1'000'000'000'000'000, .flags = kMptDexFlags}); MPTTester const gbp( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .transferFee = 25'000, .pay = 1'000'000'000'000'000, .flags = kMptDexFlags}); AMM const amm(env, bob_, gbp(1'000'000'000'000'000), btc(1'000'000'000'000'000)); // requested quality limit is 100e12BTC/178.58e12GBP = 0.55997 // trade quality is 100e12BTC/178.5714 = 0.55999e12 env(pay(alice_, carol_, btc(100'000'000'000'000)), Path(~MPT(btc)), Sendmax(gbp(178'580'000'000'000)), Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality)); env.close(); // alice buys 125e12BTC with 142.8571e12GBP and pays 25% tr fee // on 142.8571e12GBP // 1,000e12 - 142.8571e12*1.25 = 821.4285e12GBP env.require(Balance(alice_, gbp(821'428'571'428'571))); // 142.8571e12GBP is swapped in for 125e12BTC BEAST_EXPECT(amm.expectBalances( gbp(1'142'857'142'857'143), btc(875'000'000'000'000), amm.tokens())); // 25% on 100e12BTC is paid in tr fee // 100e12*1.25 = 125e12BTC env.require(Balance(carol_, btc(1'100'000'000'000'000))); } { // Payment via AMM with limit quality, deliver less // than requested Env env(*this, features); env.fund(XRP(1'000), gw_, alice_, bob_, carol_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .transferFee = 25'000, .pay = 1'200'000'000'000'000, .flags = kMptDexFlags}); MPTTester const gbp( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .transferFee = 25'000, .pay = 1'200'000'000'000'000, .flags = kMptDexFlags}); AMM const amm(env, bob_, gbp(1'000'000'000'000'000), btc(1'200'000'000'000'000)); // requested quality limit is 90e12BTC/120e12GBP = 0.75 // trade quality is 22.5e12BTC/30e12GBP = 0.75 env(pay(alice_, carol_, btc(90'000'000'000'000)), Path(~MPT(btc)), Sendmax(gbp(120'000'000'000'000)), Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality)); env.close(); // alice buys 28.125e12BTC with 24e12GBP and pays 25% tr fee // on 24e12GBP // 1,200e12 - 24e12*1.25 =~ 1,170e12GBP env.require(Balance(alice_, gbp(1'170'000'000'000'000))); // 24e12GBP is swapped in for 28.125e12BTC BEAST_EXPECT(amm.expectBalances( gbp(1'024'000'000'000'000), btc(1'171'875'000'000'000), amm.tokens())); // 25% on 22.5e12BTC is paid in tr fee // 22.5*1.25 = 28.125e12BTC env.require(Balance(carol_, btc(1'222'500'000'000'000))); } { // Payment via offer and AMM with limit quality, deliver less // than requested Env env(*this, features); Account const ed("ed"); env.fund(XRP(1'000), gw_, alice_, bob_, carol_, ed); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_, ed}, .transferFee = 25'000, .pay = 1'400'000'000'000'000, .flags = kMptDexFlags}); MPTTester const gbp( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_, ed}, .transferFee = 25'000, .pay = 1'400'000'000'000'000, .flags = kMptDexFlags}); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_, ed}, .transferFee = 25'000, .pay = 1'400'000'000'000'000, .flags = kMptDexFlags}); env(offer(ed, gbp(1'000'000'000'000'000), eth(1'000'000'000'000'000)), Txflags(tfPassive)); env.close(); AMM const amm(env, bob_, eth(1'000'000'000'000'000), btc(1'400'000'000'000'000)); // requested quality limit is 95e12BTC/140e12GBP = 0.6785 // trade quality is 59.7321e12BTC/88.0262e12GBP = 0.6785 env(pay(alice_, carol_, btc(95'000'000'000'000)), Path(~MPT(eth), ~MPT(btc)), Sendmax(gbp(140'000'000'000'000)), Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality)); env.close(); // alice buys 70.4210e12ETH with 70.4210e12GBP via the offer // and pays 25% tr fee on 70.4210e12GBP // 1,400e12 - 70.4210e12*1.25 = 1400e12 - 88.0262e12 = // 1311.9736e12GBP env.require(Balance(alice_, gbp(1'311'973'684'210'525))); // ed doesn't pay tr fee, the balances reflect consumed offer // 70.4210e12GBP/70.4210e12ETH env.require(Balance(ed, eth(1'329'578'947'368'420))); env.require(Balance(ed, gbp(1'470'421'052'631'580))); BEAST_EXPECT(expectOffers( env, ed, 1, {Amounts{gbp(929'578'947'368'420), eth(929'578'947'368'420)}})); // 25% on 56.3368e12ETH is paid in tr fee 56.3368e12*1.25 // = 70.4210e12ETH 56.3368e12ETH is swapped in for 74.6651e12BTC BEAST_EXPECT(amm.expectBalances( eth(1'056'336'842'105'264), btc(1'325'334'821'428'571), amm.tokens())); // 25% on 59.7321e12BTC is paid in tr fee 59.7321e12*1.25 // = 74.6651e12BTC env.require(Balance(carol_, btc(1'459'732'142'857'143))); } { // Payment via AMM and offer with limit quality, deliver less // than requested Env env(*this, features); Account const ed("ed"); env.fund(XRP(1'000), gw_, alice_, bob_, carol_, ed); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_, ed}, .transferFee = 25'000, .pay = 1'400'000'000'000'000, .flags = kMptDexFlags}); MPTTester const gbp( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_, ed}, .transferFee = 25'000, .pay = 1'400'000'000'000'000, .flags = kMptDexFlags}); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_, ed}, .transferFee = 25'000, .pay = 1'400'000'000'000'000, .flags = kMptDexFlags}); AMM const amm(env, bob_, gbp(1'000'000'000'000'000), eth(1'000'000'000'000'000)); env(offer(ed, eth(1'000'000'000'000'000), btc(1'400'000'000'000'000)), Txflags(tfPassive)); env.close(); // requested quality limit is 95e12BTC/140e12GBP = 0.6785 // trade quality is 47.7857e12BTC/70.4210e12GBP = 0.6785 env(pay(alice_, carol_, btc(95'000'000'000'000)), Path(~MPT(eth), ~MPT(btc)), Sendmax(gbp(140'000'000'000'000)), Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality)); env.close(); // alice buys 53.3322e12ETH with 56.3368e12GBP via the amm // and pays 25% tr fee on 56.3368e12GBP // 1,400e12 - 56.3368e12*1.25 = 1400e12 - 70.4210e12 = // 1329.5789e12GBP env.require(Balance(alice_, gbp(1'329'578'947'368'420))); //// 25% on 56.3368e12ETH is paid in tr fee 56.3368e12*1.25 ///= 70.4210e12ETH // 56.3368e12GBP is swapped in for 53.3322e12ETH BEAST_EXPECT(amm.expectBalances( gbp(1'056'336'842'105'264), eth(946'667'729'591'836), amm.tokens())); // 25% on 42.6658e12ETH is paid in tr fee 42.6658e12*1.25 // = 53.3322e12ETH 42.6658e12ETH/59.7321e12BTC env.require(Balance(ed, btc(1'340'267'857'142'857))); env.require(Balance(ed, eth(1'442'665'816'326'531))); BEAST_EXPECT(expectOffers( env, ed, 1, {Amounts{eth(957'334'183'673'469), btc(1'340'267'857'142'857)}})); // 25% on 47.7857e12BTC is paid in tr fee 47.7857e12*1.25 // = 59.7321e12BTC env.require(Balance(carol_, btc(1'447'785714285714))); } { // Payment via AMM, AMM with limit quality, deliver less // than requested Env env(*this, features); Account const ed("ed"); env.fund(XRP(1'000), gw_, alice_, bob_, carol_, ed); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_, ed}, .transferFee = 25'000, .pay = 1'400'000'000'000'000, .flags = kMptDexFlags}); MPTTester const gbp( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_, ed}, .transferFee = 25'000, .pay = 1'400'000'000'000'000, .flags = kMptDexFlags}); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_, ed}, .transferFee = 25'000, .pay = 1'400'000'000'000'000, .flags = kMptDexFlags}); AMM const amm1(env, bob_, gbp(1'000'000'000'000'000), eth(1'000'000'000'000'000)); AMM const amm2(env, ed, eth(1'000'000'000'000'000), btc(1'400'000'000'000'000)); // requested quality limit is 90e12BTC/145e12GBP = 0.6206 // trade quality is 66.7432e12BTC/107.5308e12GBP = 0.6206 env(pay(alice_, carol_, btc(90'000'000'000'000)), Path(~MPT(eth), ~MPT(btc)), Sendmax(gbp(145'000'000'000'000)), Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality)); env.close(); // alice buys 53.3322e12ETH with 107.5308e12GBP // 25% on 86.0246e12GBP is paid in tr fee // 1,400e12 - 86.0246e12*1.25 = 1400e12 - 107.5308e12 = // 1229.4691e12GBP env.require(Balance(alice_, gbp(1'292'469'135'802'465))); // 86.0246e12GBP is swapped in for 79.2106e12ETH BEAST_EXPECT(amm1.expectBalances( gbp(1'086'024'691'358'028), eth(920'789'377'955'618), amm1.tokens())); // 25% on 63.3684e12ETH is paid in tr fee 63.3684e12*1.25 // = 79.2106e12ETH 63.3684e12ETH is swapped in for 83.4291e12BTC BEAST_EXPECT(amm2.expectBalances( eth(1'063'368'497'635'505), btc(1'316'570'881'226'053), amm2.tokens())); // 25% on 66.7432e12BTC is paid in tr fee 66.7432e12*1.25 // = 83.4291e12BTC env.require(Balance(carol_, btc(1'466'743'295'019'157))); } { // Payment by the issuer via AMM, AMM with limit quality, // deliver less than requested Env env(*this, features); env.fund(XRP(1'000), gw_, alice_, bob_, carol_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .transferFee = 25'000, .pay = 1'400'000'000'000'000, .flags = kMptDexFlags}); MPTTester const gbp( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .transferFee = 25'000, .pay = 1'400'000'000'000'000, .flags = kMptDexFlags}); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .transferFee = 25'000, .pay = 1'400'000'000'000'000, .flags = kMptDexFlags}); AMM const amm1(env, alice_, gbp(1'000'000'000'000'000), eth(1'000'000'000'000'000)); AMM const amm2(env, bob_, eth(1'000'000'000'000'000), btc(1'400'000'000'000'000)); // requested quality limit is 90e12BTC/120e12GBP = 0.75 // trade quality is 81.1111e12BTC/108.1481e12GBP = 0.75 env(pay(gw_, carol_, btc(90'000'000'000'000)), Path(~MPT(eth), ~MPT(btc)), Sendmax(gbp(120'000'000'000'000)), Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality)); env.close(); // 108.1481e12GBP is swapped in for 97.5935e12ETH BEAST_EXPECT(amm1.expectBalances( gbp(1'108'148'148'148'150), eth(902'406'417'112'298), amm1.tokens())); // 25% on 78.0748e12ETH is paid in tr fee 78.0748e12*1.25 // = 97.5935e12ETH 78.0748e12ETH is swapped in for 101.3888e12BTC BEAST_EXPECT(amm2.expectBalances( eth(1'078'074'866'310'161), btc(1'298'611'111'111'111), amm2.tokens())); // 25% on 81.1111e12BTC is paid in tr fee 81.1111e12*1.25 = // 101.3888e12BTC env.require(Balance(carol_, btc(1'481'111'111'111'111))); } } void testLimitQuality() { // Single path with amm, offer, and limit quality. The quality limit // is such that the first offer should be taken but the second // should not. The total amount delivered should be the sum of the // two offers and sendMax should be more than the first offer. testcase("limitQuality"); using namespace jtx; { Env env(*this); env.fund(XRP(10'000), gw_, alice_, bob_, carol_); MPTTester const eth( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 2'000'000, .flags = kMptDexFlags}); AMM const ammBob(env, bob_, XRP(1'000), eth(1'050'000)); env(offer(bob_, XRP(100), eth(50'000))); env(pay(alice_, carol_, eth(100'000)), Path(~MPT(eth)), Sendmax(XRP(100)), Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality)); BEAST_EXPECT(ammBob.expectBalances(XRP(1'050), eth(1'000'000), ammBob.tokens())); env.require(Balance(carol_, eth(2'050'000))); BEAST_EXPECT(expectOffers(env, bob_, 1, {{{XRP(100), eth(50'000)}}})); } } void testXRPPathLoop() { testcase("Circular XRP"); using namespace jtx; // Payment path starting with XRP { auto test = [&](auto&& issue1, auto&& issue2) { Env env(*this); env.fund(XRP(30'000), alice_, bob_, gw_); env.close(); auto const eth = issue1( {.env = env, .token = "ETH", .issuer = gw_, .holders = {alice_, bob_}, .limit = 2000'000}); auto const btc = issue2( {.env = env, .token = "BTC", .issuer = gw_, .holders = {alice_, bob_}, .limit = 2000'000}); env(pay(gw_, alice_, btc(200'000))); env(pay(gw_, bob_, btc(200'000))); env(pay(gw_, alice_, eth(200'000))); env(pay(gw_, bob_, eth(200'000))); env.close(); AMM const ammAliceXrpBtc(env, alice_, XRP(100), btc(101'000)); AMM const ammAliceXrpEth(env, alice_, XRP(100), eth(101'000)); env(pay(alice_, bob_, eth(1'000)), Path(~btc, ~XRP, ~eth), Sendmax(XRP(1)), Txflags(tfNoRippleDirect), Ter(temBAD_PATH_LOOP)); }; testHelper2TokensMix(test); } // Payment path ending with XRP { auto test = [&](auto&& issue1, auto&& issue2) { Env env(*this); env.fund(XRP(30'000), alice_, bob_, gw_); env.close(); auto const eth = issue1( {.env = env, .token = "ETH", .issuer = gw_, .holders = {alice_, bob_}, .limit = 2000'000}); auto const btc = issue2( {.env = env, .token = "BTC", .issuer = gw_, .holders = {alice_, bob_}, .limit = 2000'000}); env(pay(gw_, alice_, btc(200'000))); env(pay(gw_, bob_, btc(200'000))); env(pay(gw_, alice_, eth(200'000))); env(pay(gw_, bob_, eth(200'000))); env.close(); AMM const ammAliceXrpBtc(env, alice_, XRP(100), btc(100'000)); AMM const ammAliceXrpEth(env, alice_, XRP(100), eth(100'000)); // ETH -> //XRP -> //BTC ->XRP env(pay(alice_, bob_, XRP(1)), Path(~XRP, ~btc, ~XRP), Sendmax(eth(1'000)), Txflags(tfNoRippleDirect), Ter(temBAD_PATH_LOOP)); }; testHelper2TokensMix(test); } // Payment where loop is formed in the middle of the path, not // on an endpoint { auto test = [&](auto&& issue1, auto&& issue2, auto&& issue3) { Env env(*this); env.fund(XRP(10'000), gw_, alice_, bob_); env.close(); auto const eth = issue1( {.env = env, .token = "ETH", .issuer = gw_, .holders = {alice_, bob_}, .limit = 2000'000}); auto const btc = issue2( {.env = env, .token = "BTC", .issuer = gw_, .holders = {alice_, bob_}, .limit = 2000'000}); auto const jpy = issue2( {.env = env, .token = "JPY", .issuer = gw_, .holders = {alice_, bob_}, .limit = 2000'000}); env(pay(gw_, alice_, btc(200'000))); env(pay(gw_, bob_, btc(200'000))); env(pay(gw_, alice_, eth(200'000))); env(pay(gw_, bob_, eth(200'000))); env(pay(gw_, alice_, jpy(200'000))); env(pay(gw_, bob_, jpy(200'000))); env.close(); AMM const ammAliceXrpBtc(env, alice_, XRP(100), btc(100'000)); AMM const ammAliceXrpEth(env, alice_, XRP(100), eth(100'000)); AMM const ammAliceXrpJpy(env, alice_, XRP(100), jpy(100'000)); env(pay(alice_, bob_, jpy(1'000)), Path(~XRP, ~eth, ~XRP, ~jpy), Sendmax(btc(1'000)), Txflags(tfNoRippleDirect), Ter(temBAD_PATH_LOOP)); }; testHelper3TokensMix(test); } } void testStepLimit(FeatureBitset features) { testcase("Step Limit"); using namespace jtx; { Env env(*this, features); auto const dan = Account("dan"); auto const ed = Account("ed"); env.fund(XRP(100'000'000), gw_, alice_, bob_, carol_, dan, ed); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {bob_, dan, ed}, .flags = kMptDexFlags}); env(pay(gw_, ed, btc(11'000'000'000'000))); env(pay(gw_, bob_, btc(1'000'000'000'000))); env(pay(gw_, dan, btc(1'000'000'000'000))); nOffers(env, 2'000, bob_, XRP(1), btc(1'000'000'000'000)); nOffers(env, 1, dan, XRP(1), btc(1'000'000'000'000)); AMM const ammEd(env, ed, XRP(9), btc(11'000'000'000'000)); // Alice offers to buy 1000 XRP for 1000e12 BTC. She takes Bob's // first offer, removes 999 more as unfunded, then hits the step // limit. env(offer(alice_, btc(1'000'000'000'000'000), XRP(1'000))); env.require(Balance(alice_, btc(2'050'125'257'867))); env.require(Owners(alice_, 2)); env.require(Balance(bob_, btc(0))); env.require(Owners(bob_, 1'001)); env.require(Balance(dan, btc(1'000'000'000'000))); env.require(Owners(dan, 2)); // Carol offers to buy 1000 XRP for 1000e12 BTC. She removes Bob's // next 1000 offers as unfunded and hits the step limit. env(offer(carol_, btc(1'000'000'000'000'000), XRP(1'000))); env.require(Balance(carol_, MPT(btc)(kNone))); env.require(Owners(carol_, 1)); env.require(Balance(bob_, btc(0))); env.require(Owners(bob_, 1)); env.require(Balance(dan, btc(1'000'000'000'000))); env.require(Owners(dan, 2)); } // MPT/IOU, similar to the case above { Env env(*this, features); auto const dan = Account("dan"); auto const ed = Account("ed"); env.fund(XRP(100'000), gw_, alice_, bob_, carol_, dan, ed); env.close(); MPTTester const usd( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_, dan, ed}, .pay = 10000'000'000, .flags = kMptDexFlags}); env.trust(BTC(11'000'000'000'000), ed); env(pay(gw_, ed, BTC(11'000'000'000'000))); env.trust(BTC(1'000'000'000'000), bob_); env(pay(gw_, bob_, BTC(1'000'000'000'000))); env.trust(BTC(1'000'000'000'000), dan); env(pay(gw_, dan, BTC(1'000'000'000'000))); env.close(); nOffers(env, 2'000, bob_, usd(1000000), BTC(1'000'000'000'000)); nOffers(env, 1, dan, usd(1000000), BTC(1'000'000'000'000)); AMM const ammEd(env, ed, usd(9000000), BTC(11'000'000'000'000)); env(offer(alice_, BTC(1'000'000'000'000'000), usd(1'000000000))); env.require(Balance(alice_, STAmount{BTC, UINT64_C(2050125257867'587), -3})); env.require(Owners(alice_, 3)); env.require(Balance(bob_, BTC(0))); env.require(Owners(bob_, 1'002)); env.require(Balance(dan, BTC(1000000000000))); env.require(Owners(dan, 3)); } // IOU/MPT, similar to the case above { Env env(*this, features); auto const dan = Account("dan"); auto const ed = Account("ed"); env.fund(XRP(100'000), gw_, alice_, bob_, carol_, dan, ed); env.close(); env.trust(USD(10000'000'000), alice_); env(pay(gw_, alice_, USD(10000'000'000))); env.trust(USD(10000'000'000), bob_); env(pay(gw_, bob_, USD(10000'000'000))); env.trust(USD(10000'000'000), carol_); env(pay(gw_, carol_, USD(10000'000'000))); env.trust(USD(10000'000'000), dan); env(pay(gw_, dan, USD(10000'000'000))); env.trust(USD(10000'000'000), ed); env(pay(gw_, ed, USD(10000'000'000))); env.close(); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {bob_, dan, ed}, .flags = kMptDexFlags}); env(pay(gw_, ed, btc(11'000'000'000'000))); env(pay(gw_, bob_, btc(1'000'000'000'000))); env(pay(gw_, dan, btc(1'000'000'000'000))); env.close(); nOffers(env, 2'000, bob_, USD(1000000), btc(1'000'000'000'000)); nOffers(env, 1, dan, USD(1000000), btc(1'000'000'000'000)); AMM const ammEd(env, ed, USD(9000000), btc(11'000'000'000'000)); env(offer(alice_, btc(1'000'000'000'000'000), USD(1'000000000))); env.require(Balance(alice_, btc(2050125628933))); env.require(Owners(alice_, 3)); env.require(Balance(bob_, btc(0))); env.require(Owners(bob_, 1'002)); env.require(Balance(dan, btc(1000000000000))); env.require(Owners(dan, 3)); } // MPT/MPT, similar to the case above { Env env(*this, features); auto const dan = Account("dan"); auto const ed = Account("ed"); env.fund(XRP(100'000), gw_, alice_, bob_, carol_, dan, ed); env.close(); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {bob_, dan, ed}, .flags = kMptDexFlags}); MPTTester const usd( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_, dan, ed}, .pay = 10000'000'000, .flags = kMptDexFlags}); env(pay(gw_, ed, btc(11'000'000'000'000))); env(pay(gw_, bob_, btc(1'000'000'000'000))); env(pay(gw_, dan, btc(1'000'000'000'000))); env.close(); nOffers(env, 2'000, bob_, usd(1000000), btc(1'000'000'000'000)); nOffers(env, 1, dan, usd(1000000), btc(1'000'000'000'000)); AMM const ammEd(env, ed, usd(9000000), btc(11'000'000'000'000)); env(offer(alice_, btc(1'000'000'000'000'000), usd(1'000000000))); env.require(Balance(alice_, btc(2050125257867))); env.require(Owners(alice_, 3)); env.require(Balance(bob_, btc(0))); env.require(Owners(bob_, 1'002)); env.require(Balance(dan, btc(1000000000000))); env.require(Owners(dan, 3)); } } void testConvertAllOfAnAsset(FeatureBitset features) { testcase("Convert all of an asset using DeliverMin"); using namespace jtx; { Env env(*this, features); fund(env, gw_, {alice_, bob_, carol_}, XRP(10'000)); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .flags = kMptDexFlags}); env(pay(alice_, bob_, btc(10'000)), DeliverMin(btc(10'000)), Ter(temBAD_AMOUNT)); env(pay(alice_, bob_, btc(10'000)), DeliverMin(btc(-5'000)), Txflags(tfPartialPayment), Ter(temBAD_AMOUNT)); env(pay(alice_, bob_, btc(10'000)), DeliverMin(XRP(5)), Txflags(tfPartialPayment), Ter(temBAD_AMOUNT)); env(pay(alice_, bob_, btc(10'000)), DeliverMin(btc(5'000)), Txflags(tfPartialPayment), Ter(tecPATH_DRY)); env(pay(alice_, bob_, btc(10'000)), DeliverMin(btc(15'000)), Txflags(tfPartialPayment), Ter(temBAD_AMOUNT)); env(pay(gw_, carol_, btc(50'000))); AMM const ammCarol(env, carol_, XRP(10), btc(15'000)); env(pay(alice_, bob_, btc(10'000)), Paths(XRP), DeliverMin(btc(7'000)), Txflags(tfPartialPayment), Sendmax(XRP(5)), Ter(tecPATH_PARTIAL)); env.require( Balance(alice_, drops(10'000'000'000 - (3 * env.current()->fees().base.drops())))); env.require(Balance(bob_, drops(10'000'000'000 - env.current()->fees().base.drops()))); } { Env env(*this, features); fund(env, gw_, {alice_, bob_}, XRP(10'000)); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .flags = kMptDexFlags}); env(pay(gw_, bob_, btc(1'100'000))); AMM const ammBob(env, bob_, XRP(1'000), btc(1'100'000)); env(pay(alice_, alice_, btc(10'000'000)), Paths(XRP), DeliverMin(btc(100'000)), Txflags(tfPartialPayment), Sendmax(XRP(100))); env.require(Balance(alice_, btc(100'000))); } // IOU/MPT mix, similar to the above case { 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 = 3000'000}); auto const btc = issue2( {.env = env, .token = "BTC", .issuer = gw_, .holders = {alice_, bob_, carol_}, .limit = 1'000'000}); env(pay(gw_, alice_, usd(10'000))); env(pay(gw_, bob_, usd(10'000))); env(pay(gw_, bob_, btc(1'200))); env.close(); AMM const ammBob(env, bob_, usd(1'000), btc(1'100)); env(pay(alice_, alice_, btc(10'000)), Paths(usd), DeliverMin(btc(100)), Txflags(tfPartialPayment), Sendmax(usd(100))); env.require(Balance(alice_, btc(100))); }; testHelper2TokensMix(test); } { Env env(*this, features); fund(env, gw_, {alice_, bob_, carol_}, XRP(10'000)); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {bob_, carol_}, .flags = kMptDexFlags}); env(pay(gw_, bob_, btc(1'200'000))); AMM const ammBob(env, bob_, XRP(5'500), btc(1'200'000)); env(pay(alice_, carol_, btc(10'000'000)), Paths(XRP), DeliverMin(btc(200'000)), Txflags(tfPartialPayment), Sendmax(XRP(1'000)), Ter(tecPATH_PARTIAL)); env(pay(alice_, carol_, btc(10'000'000)), Paths(XRP), DeliverMin(btc(200'000)), Txflags(tfPartialPayment), Sendmax(XRP(1'100))); BEAST_EXPECT(ammBob.expectBalances(XRP(6'600), btc(1'000'000), ammBob.tokens())); env.require(Balance(carol_, btc(200'000))); } // IOU/MPT mix, similar to the above case { 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 = 3000'000}); auto const btc = issue2( {.env = env, .token = "BTC", .issuer = gw_, .holders = {alice_, bob_, carol_}, .limit = 1'000'000}); env(pay(gw_, alice_, usd(100'000))); env(pay(gw_, bob_, usd(100'000))); env(pay(gw_, carol_, usd(100'000))); env(pay(gw_, bob_, btc(1'200))); env.close(); AMM const ammBob(env, bob_, usd(5'500), btc(1'200)); env(pay(alice_, carol_, btc(10'000)), Paths(usd), DeliverMin(btc(200)), Txflags(tfPartialPayment), Sendmax(usd(1'000)), Ter(tecPATH_PARTIAL)); env(pay(alice_, carol_, btc(10'000)), Paths(usd), DeliverMin(btc(200)), Txflags(tfPartialPayment), Sendmax(usd(1'100))); BEAST_EXPECT(ammBob.expectBalances(usd(6'600), btc(1'000), ammBob.tokens())); env.require(Balance(carol_, btc(200))); }; testHelper2TokensMix(test); } { auto const dan = Account("dan"); Env env(*this, features); fund(env, gw_, {alice_, bob_, carol_, dan}, XRP(10'000)); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {bob_, carol_, dan}, .flags = kMptDexFlags}); env(pay(gw_, bob_, btc(100'000'000))); env(pay(gw_, dan, btc(1'100'000'000))); env(offer(bob_, XRP(100), btc(100'000'000))); env(offer(bob_, XRP(1'000), btc(100'000'000))); AMM const ammDan(env, dan, XRP(1'000), btc(1'100'000'000)); env(pay(alice_, carol_, btc(10'000'000'000)), Paths(XRP), DeliverMin(btc(200'000'000)), Txflags(tfPartialPayment), Sendmax(XRPAmount(200'000'001))); env.require(Balance(bob_, btc(0))); env.require(Balance(carol_, btc(200'000'000))); BEAST_EXPECT( ammDan.expectBalances(XRPAmount{1'100'000'001}, btc(1000'000000), ammDan.tokens())); } } void testPayment(FeatureBitset features) { testcase("Payment"); using namespace jtx; Account const becky{"becky"}; Env env(*this, features); fund(env, gw_, {alice_, becky}, XRP(5'000)); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, becky}, .flags = kMptDexFlags}); env(pay(gw_, alice_, btc(500'000))); env.close(); AMM const ammAlice(env, alice_, XRP(100), btc(140'000)); // becky pays herself BTC (10'000) by consuming part of alice's offer. // Make sure the payment works if PaymentAuth is not involved. env(pay(becky, becky, btc(10'000)), Path(~MPT(btc)), Sendmax(XRP(10))); env.close(); BEAST_EXPECT( ammAlice.expectBalances(XRPAmount(107'692'308), btc(130'000), ammAlice.tokens())); // becky decides to require authorization for deposits. env(fset(becky, asfDepositAuth)); env.close(); // becky pays herself again. env(pay(becky, becky, btc(10'000)), Path(~MPT(btc)), Sendmax(XRP(10)), Ter(tesSUCCESS)); env.close(); } void testPayMPT() { // Exercise MPT payments and non-direct XRP payments to an account // that has the lsfDepositAuth flag set. testcase("Pay MPT"); using namespace jtx; Env env(*this); fund(env, gw_, {alice_, bob_, carol_}, XRP(10'000)); MPTTester btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .flags = kMptDexFlags}); env(pay(gw_, alice_, btc(150'000))); env(pay(gw_, carol_, btc(150'000))); AMM const ammCarol(env, carol_, btc(100'000), XRPAmount(101)); env(pay(alice_, bob_, btc(50'000))); env.close(); // bob sets the lsfDepositAuth flag. env(fset(bob_, asfDepositAuth), Require(Flags(bob_, asfDepositAuth))); env.close(); // None of the following payments should succeed. auto failedMptPayments = [this, &env, &btc]() { env.require(Flags(bob_, asfDepositAuth)); // Capture bob's balances before hand to confirm they don't // change. PrettyAmount const bobXrpBalance{env.balance(bob_, XRP)}; PrettyAmount const bobBTCBalance{env.balance(bob_, MPT(btc))}; env(pay(alice_, bob_, btc(50'000)), Ter(tecNO_PERMISSION)); env.close(); // Note that even though alice is paying bob in XRP, the payment // is still not allowed since the payment passes through an // offer. env(pay(alice_, bob_, drops(1)), Sendmax(btc(1'000)), Ter(tecNO_PERMISSION)); env.close(); BEAST_EXPECT(bobXrpBalance == env.balance(bob_, XRP)); BEAST_EXPECT(bobBTCBalance == env.balance(bob_, MPT(btc))); }; // Test when bob has an XRP balance > base reserve. failedMptPayments(); // Set bob's XRP balance == base reserve. Also demonstrate that // bob can make payments while his lsfDepositAuth flag is set. env(pay(bob_, alice_, btc(25'000))); env.close(); { STAmount const bobPaysXRP{env.balance(bob_, XRP) - reserve(env, 1)}; XRPAmount const bobPaysFee{reserve(env, 1) - reserve(env, 0)}; env(pay(bob_, alice_, bobPaysXRP), Fee(bobPaysFee)); env.close(); } // Test when bob's XRP balance == base reserve. BEAST_EXPECT(env.balance(bob_, XRP) == reserve(env, 0)); BEAST_EXPECT(env.balance(bob_, MPT(btc)) == btc(25'000)); failedMptPayments(); // Test when bob has an XRP balance == 0. env(noop(bob_), Fee(reserve(env, 0))); env.close(); BEAST_EXPECT(env.balance(bob_, XRP) == XRP(0)); failedMptPayments(); // Give bob enough XRP for the fee to clear the lsfDepositAuth flag. env(pay(alice_, bob_, drops(env.current()->fees().base))); // bob clears the lsfDepositAuth and the next payment succeeds. env(fclear(bob_, asfDepositAuth)); env.close(); env(pay(alice_, bob_, btc(50'000))); env.close(); env(pay(alice_, bob_, drops(1)), Sendmax(btc(1'000))); env.close(); BEAST_EXPECT(ammCarol.expectBalances(btc(101'000), XRPAmount(100), ammCarol.tokens())); } void testIndividualLock(FeatureBitset features) { testcase("Individual Lock"); using namespace test::jtx; Env env(*this, features); Account const g1{"G1"}; Account const alice{"alice"}; Account const bob{"bob"}; env.fund(XRP(1'000), g1, alice, bob); MPTTester btc( {.env = env, .issuer = g1, .holders = {alice, bob}, .flags = tfMPTCanLock | kMptDexFlags}); env(pay(g1, bob, btc(10))); env(pay(g1, alice, btc(205))); env.close(); AMM const ammAlice(env, alice, XRP(500), btc(105)); env.require(Balance(bob, btc(10))); env.require(Balance(alice, btc(100))); // Account with MPT unlocked (proving operations normally work) // can make Payment env(pay(alice, bob, btc(1))); // can receive Payment env(pay(bob, alice, btc(1))); env.close(); // Lock MPT for bob btc.set({.holder = bob, .flags = tfMPTLock}); { // different from IOU. The offer is created but not crossed. env(offer(bob, btc(5), XRP(25))); env.close(); BEAST_EXPECT(expectOffers(env, bob, 1, {{{btc(5), XRP(25)}}})); BEAST_EXPECT(ammAlice.expectBalances(XRP(500), btc(105), ammAlice.tokens())); } { // can not sell assets env(offer(bob, XRP(1), btc(5)), Ter(tecUNFUNDED_OFFER)); // different from IOU // can not receive Payment when locked env(pay(alice, bob, btc(1)), Ter(tecPATH_DRY)); // can not make Payment when locked env(pay(bob, alice, btc(1)), Ter(tecPATH_DRY)); env.require(Balance(bob, btc(10))); } { // Unlock btc.set({.holder = bob, .flags = tfMPTUnlock}); env(offer(bob, XRP(1), btc(5))); env(pay(bob, alice, btc(1))); env(pay(alice, bob, btc(1))); env.close(); } } void testGlobalLock(FeatureBitset features) { testcase("Global Lock"); using namespace test::jtx; Env env(*this, features); Account const g1{"G1"}; Account a1{"A1"}; Account a2{"A2"}; Account a3{"A3"}; Account a4{"A4"}; env.fund(XRP(12'000), g1); env.fund(XRP(1'000), a1); env.fund(XRP(20'000), a2, a3, a4); MPTTester const eth( {.env = env, .issuer = g1, .holders = {a1, a2, a3, a4}, .flags = tfMPTCanLock | kMptDexFlags}); MPTTester btc( {.env = env, .issuer = g1, .holders = {a1, a2, a3, a4}, .flags = tfMPTCanLock | kMptDexFlags}); env(pay(g1, a1, eth(1'000))); env(pay(g1, a2, eth(100))); env(pay(g1, a3, btc(100))); env(pay(g1, a4, btc(100))); env.close(); AMM const ammG1(env, g1, XRP(10'000), eth(100)); env(offer(a1, XRP(10'000), eth(100)), Txflags(tfPassive)); env(offer(a2, eth(100), XRP(10'000)), Txflags(tfPassive)); env.close(); { // Account without Global Lock (proving operations normally // work) // visible offers where taker_pays is unlocked issuer auto offers = getAccountOffers(env, a2)[jss::offers]; if (!BEAST_EXPECT(checkArraySize(offers, 1u))) return; // visible offers where taker_gets is unlocked issuer offers = getAccountOffers(env, a1)[jss::offers]; if (!BEAST_EXPECT(checkArraySize(offers, 1u))) return; } { // Offers/Payments // assets can be bought on the market AMM ammA3(env, a3, btc(1), XRP(1)); // assets can be sold on the market // AMM is bidirectional env(pay(g1, a2, eth(1))); env(pay(a2, g1, eth(1))); env(pay(a2, a1, eth(1))); env(pay(a1, a2, eth(1))); ammA3.withdrawAll(std::nullopt); } { // Account with Global Lock // set Global Lock first btc.set({.flags = tfMPTLock}); // assets can't be bought on the market AMM const ammA3(env, a3, btc(1), XRP(1), Ter(tecFROZEN)); // direct issues can be sent env(pay(g1, a2, btc(1))); env(pay(a2, g1, btc(1))); // locked env(pay(a2, a1, btc(1)), Ter(tecPATH_DRY)); env(pay(a1, a2, btc(1)), Ter(tecPATH_DRY)); } { auto offers = getAccountOffers(env, a2)[jss::offers]; if (!BEAST_EXPECT(checkArraySize(offers, 1u))) return; offers = getAccountOffers(env, a1)[jss::offers]; if (!BEAST_EXPECT(checkArraySize(offers, 1u))) return; } } void testOffersWhenLocked(FeatureBitset features) { testcase("Offers for Locked MPTs"); using namespace test::jtx; Env env(*this, features); Account const g1{"G1"}; Account a2{"A2"}; Account a3{"A3"}; Account a4{"A4"}; env.fund(XRP(2'000), g1, a3, a4); env.fund(XRP(2'000), a2); env.close(); MPTTester btc( {.env = env, .issuer = g1, .holders = {a2, a3, a4}, .flags = tfMPTCanLock | kMptDexFlags}); env(pay(g1, a3, btc(2'000))); env(pay(g1, a4, btc(2'001))); env.close(); AMM const ammA3(env, a3, XRP(1'000), btc(1'001)); // removal after successful payment // test: make a payment with partially consuming offer env(pay(a2, g1, btc(1)), Paths(MPT(btc)), Sendmax(XRP(1))); env.close(); BEAST_EXPECT(ammA3.expectBalances(XRP(1'001), btc(1'000), ammA3.tokens())); // test: someone else creates an offer providing liquidity env(offer(a4, XRP(999), btc(999))); env.close(); // The offer consumes AMM offer BEAST_EXPECT(ammA3.expectBalances(XRP(1'000), btc(1'001), ammA3.tokens())); // test: AMM is Locked btc.set({.holder = ammA3.ammAccount(), .flags = tfMPTLock}); auto const info = ammA3.ammRpcInfo(); BEAST_EXPECT(info[jss::amm][jss::asset2_frozen].asBool()); env.close(); // test: Can make a payment via the new offer env(pay(a2, g1, btc(1)), Paths(MPT(btc)), Sendmax(XRP(1))); env.close(); // AMM is not consumed BEAST_EXPECT(ammA3.expectBalances(XRP(1'000), btc(1'001), ammA3.tokens())); // removal buy successful OfferCreate // test: lock the new offer btc.set({.holder = a4, .flags = tfMPTUnlock}); env.close(); // test: can no longer create a crossing offer env(offer(a2, btc(999), XRP(999))); env.close(); // test: offer was removed by offer_create auto offers = getAccountOffers(env, a4)[jss::offers]; if (!BEAST_EXPECT(checkArraySize(offers, 0u))) return; } void testTxMultisign(FeatureBitset features) { testcase("Multisign AMM Transactions"); using namespace jtx; Env env{*this, features}; Account const bogie{"bogie", KeyType::Secp256k1}; Account const alice{"alice", KeyType::Secp256k1}; Account const becky{"becky", KeyType::Ed25519}; Account const zelda{"zelda", KeyType::Secp256k1}; fund(env, gw_, {alice, becky, zelda}, XRP(20'000)); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice, becky, zelda}, .pay = 20'000'000'000, .flags = kMptDexFlags}); // alice uses a regular key with the master disabled. Account const alie{"alie", KeyType::Secp256k1}; env(regkey(alice, alie)); env(fset(alice, asfDisableMaster), Sig(alice)); // Attach signers to alice. env(signers(alice, 2, {{becky, 1}, {bogie, 1}}), Sig(alie)); env.close(); static constexpr int kSignerListOwners{2}; env.require(Owners(alice, kSignerListOwners + 0)); Msig const ms{becky, bogie}; // Multisign all AMM transactions AMM ammAlice( env, alice, XRP(10'000), btc(10'000), false, 0, ammCrtFee(env).drops(), std::nullopt, std::nullopt, ms, Ter(tesSUCCESS)); BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), btc(10'000), ammAlice.tokens())); ammAlice.deposit(alice, 1'000'000); BEAST_EXPECT(ammAlice.expectBalances(XRP(11'000), btc(11'000), IOUAmount{11'000'000, 0})); ammAlice.withdraw(alice, 1'000'000); BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), btc(10'000), ammAlice.tokens())); ammAlice.vote({}, 1'000); BEAST_EXPECT(ammAlice.expectTradingFee(1'000)); env(ammAlice.bid({.account = alice, .bidMin = 100}), ms).close(); BEAST_EXPECT(ammAlice.expectAuctionSlot(100, 0, IOUAmount{4'000})); // 4000 tokens burnt BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), btc(10'000), IOUAmount{9'996'000, 0})); } void testToStrand(FeatureBitset features) { testcase("To Strand"); using namespace jtx; // cannot have more than one offer with the same output issue { auto test = [&](auto&& issue1, auto&& issue2) { Env env(*this); env.fund(XRP(30'000), alice_, bob_, carol_, gw_); env.close(); auto const eth = issue1( {.env = env, .token = "ETH", .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_, eth(50000))); env(pay(gw_, bob_, eth(50000))); env(pay(gw_, carol_, eth(50000))); env.close(); AMM const bobXrpBtc(env, bob_, XRP(1'000), btc(1'000)); AMM const bobBtcEth(env, bob_, btc(1'000), eth(1'000)); // payment path: XRP -> XRP/BTC -> BTC/ETH -> ETH/BTC env(pay(alice_, carol_, btc(100)), Path(~btc, ~eth, ~btc), Sendmax(XRP(200)), Txflags(tfNoRippleDirect), Ter(temBAD_PATH_LOOP)); }; testHelper2TokensMix(test); } } void testRIPD1373(FeatureBitset features) { using namespace jtx; testcase("RIPD1373"); { Env env(*this, features); fund(env, gw_, {alice_, bob_}, XRP(10'000)); MPTTester btc( {.env = env, .issuer = bob_, .holders = {alice_, gw_}, .pay = 100'000'000, .flags = kMptDexFlags}); MPTTester eth( {.env = env, .issuer = bob_, .holders = {alice_, gw_}, .pay = 100'000'000, .flags = kMptDexFlags}); AMM const ammXrpBtc(env, bob_, XRP(100), btc(100'000)); env(offer(gw_, XRP(100), btc(100'000)), Txflags(tfPassive)); AMM const ammBtcEth(env, bob_, btc(100'000), eth(100'000)); env(offer(gw_, btc(100'000), eth(100'000)), Txflags(tfPassive)); TestPath const p = [&] { TestPath result; result.pushBack(allPathElements(gw_, MPT(btc))); result.pushBack(cpe(eth.issuanceID())); return result; }(); PathSet const paths(p); env(pay(alice_, alice_, eth(1'000)), Json(paths.json()), Sendmax(XRP(10)), Txflags(tfNoRippleDirect | tfPartialPayment), Ter(temBAD_PATH)); } { Env env(*this, features); fund(env, gw_, {alice_, bob_, carol_}, XRP(10'000)); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 100'000, .flags = kMptDexFlags}); AMM const ammBob(env, bob_, XRP(100), btc(100)); // payment path: XRP -> XRP/BTC -> BTC/XRP env(pay(alice_, carol_, XRP(100)), Path(~MPT(btc), ~XRP), Txflags(tfNoRippleDirect), Ter(temBAD_SEND_XRP_PATHS)); } { Env env(*this, features); fund(env, gw_, {alice_, bob_, carol_}, XRP(10'000)); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .pay = 100'000, .flags = kMptDexFlags}); AMM const ammBob(env, bob_, XRP(100), btc(100)); // payment path: XRP -> XRP/BTC -> BTC/XRP env(pay(alice_, carol_, XRP(100)), Path(~MPT(btc), ~XRP), Sendmax(XRP(200)), Txflags(tfNoRippleDirect), Ter(temBAD_SEND_XRP_MAX)); } } void testLoop(FeatureBitset features) { testcase("test loop"); using namespace jtx; { Env env(*this, features); env.fund(XRP(10'000), alice_, bob_, carol_, gw_); MPTTester const btc( {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .flags = kMptDexFlags}); env(pay(gw_, bob_, btc(100'000'000))); env(pay(gw_, alice_, btc(100'000'000))); env.close(); AMM const ammBob(env, bob_, XRP(100), btc(100'000'000)); // payment path: BTC -> BTC/XRP -> XRP/BTC env(pay(alice_, carol_, btc(100'000'000)), Sendmax(btc(100'000'000)), Path(~XRP, ~MPT(btc)), Txflags(tfNoRippleDirect), Ter(temBAD_PATH_LOOP)); } { auto test = [&](auto&& issue1, auto&& issue2, auto&& issue3) { Env env(*this, features); env.fund(XRP(10'000), alice_, bob_, carol_, gw_); env.close(); auto const btc = issue1( {.env = env, .token = "BTC", .issuer = gw_, .holders = {alice_, bob_, carol_}}); auto const eth = issue2( {.env = env, .token = "ETH", .issuer = gw_, .holders = {alice_, bob_, carol_}}); auto const cny = issue3( {.env = env, .token = "CNY", .issuer = gw_, .holders = {alice_, bob_, carol_}}); env(pay(gw_, bob_, btc(200))); env(pay(gw_, bob_, eth(200))); env(pay(gw_, bob_, cny(100))); env.close(); AMM const ammBobXrpBtc(env, bob_, XRP(100), btc(100)); AMM const ammBobBtcEth(env, bob_, btc(100), eth(100)); AMM const ammBobEthCny(env, bob_, eth(100), cny(100)); // payment path: XRP->XRP/BTC->BTC/ETH->BTC/CNY env(pay(alice_, carol_, cny(100)), Sendmax(XRP(100)), Path(~btc, ~eth, ~btc, ~cny), Txflags(tfNoRippleDirect), Ter(temBAD_PATH_LOOP)); }; testHelper3TokensMix(test); } } void testPaths() { pathFindConsumeAll(); viaOffersViaGateway(); receiveMax(); pathFind01(); pathFind02(); pathFind06(); } void testFlow() { using namespace jtx; FeatureBitset const all{testableAmendments()}; testFalseDry(all); testBookStep(all); testTransferRateNoOwnerFee(all); testLimitQuality(); testXRPPathLoop(); } void testCrossingLimits() { using namespace jtx; FeatureBitset const all{testableAmendments()}; testStepLimit(all); } void testDeliverMin() { using namespace jtx; FeatureBitset const all{testableAmendments()}; testConvertAllOfAnAsset(all); } void testDepositAuth() { auto const supported{jtx::testableAmendments()}; testPayment(supported); testPayMPT(); } void testLock() { using namespace test::jtx; auto const sa = testableAmendments(); testIndividualLock(sa); testGlobalLock(sa); testOffersWhenLocked(sa); } void testMultisign() { using namespace jtx; auto const all = testableAmendments(); testTxMultisign(all); } void testPayStrand() { using namespace jtx; auto const all = testableAmendments(); testToStrand(all); testRIPD1373(all); testLoop(all); } void run() override { testOffers(); testPaths(); testFlow(); testCrossingLimits(); testDeliverMin(); testDepositAuth(); testLock(); testMultisign(); testPayStrand(); } }; BEAST_DEFINE_TESTSUITE_PRIO(AMMExtendedMPT, app, xrpl, 1); } // namespace xrpl::test