#include #include #include namespace xrpl { namespace test { class CrossingLimitsMPT_test : public beast::unit_test::suite { public: void testStepLimit(FeatureBitset features) { testcase("Step Limit"); using namespace jtx; Env env(*this, features); auto const gw = Account("gateway"); env.fund(XRP(100'000'000), gw, "alice", "bob", "carol", "dan"); MPT const USD = MPTTester({.env = env, .issuer = gw, .holders = {"bob", "dan"}, .maxAmt = 2}); env(pay(gw, "bob", USD(1))); env(pay(gw, "dan", USD(1))); n_offers(env, 2'000, "bob", XRP(1), USD(1)); n_offers(env, 1, "dan", XRP(1), USD(1)); // Alice offers to buy 1000 XRP for 1000 USD. She takes Bob's first // offer, removes 999 more as unfunded, then hits the step limit. env(offer("alice", USD(1'000), XRP(1'000))); env.require(balance("alice", USD(1))); env.require(owners("alice", 2)); env.require(balance("bob", USD(0))); env.require(owners("bob", 1'001)); env.require(balance("dan", USD(1))); env.require(owners("dan", 2)); // Carol offers to buy 1000 XRP for 1000 USD. She removes Bob's next // 1000 offers as unfunded and hits the step limit. env(offer("carol", USD(1'000), XRP(1'000))); env.require(balance("carol", USD(none))); env.require(owners("carol", 1)); env.require(balance("bob", USD(0))); env.require(owners("bob", 1)); env.require(balance("dan", USD(1))); env.require(owners("dan", 2)); } void testCrossingLimit(FeatureBitset features) { testcase("Crossing Limit"); using namespace jtx; Env env(*this, features); auto const gw = Account("gateway"); int const maxConsumed = 1'000; env.fund(XRP(100'000'000), gw, "alice", "bob", "carol"); int const bobsOfferCount = maxConsumed + 150; MPT const USD = MPTTester({.env = env, .issuer = gw, .holders = {"bob"}, .maxAmt = bobsOfferCount}); env(pay(gw, "bob", USD(bobsOfferCount))); env.close(); n_offers(env, bobsOfferCount, "bob", XRP(1), USD(1)); // Alice offers to buy Bob's offers. However, she hits the offer // crossing limit, so she can't buy them all at once. env(offer("alice", USD(bobsOfferCount), XRP(bobsOfferCount))); env.close(); env.require(balance("alice", USD(maxConsumed))); env.require(balance("bob", USD(150))); env.require(owners("bob", 150 + 1)); // Carol offers to buy 1000 XRP for 1000 USD. She takes Bob's // remaining 150 offers without hitting a limit. env(offer("carol", USD(1'000), XRP(1'000))); env.close(); env.require(balance("carol", USD(150))); env.require(balance("bob", USD(0))); env.require(owners("bob", 1)); } void testStepAndCrossingLimit(FeatureBitset features) { testcase("Step And Crossing Limit"); using namespace jtx; Env env(*this, features); auto const gw = Account("gateway"); env.fund(XRP(100'000'000), gw, "alice", "bob", "carol", "dan", "evita"); int const maxConsumed = 1'000; int const evitaOfferCount{maxConsumed + 49}; MPT const USD = MPTTester( {.env = env, .issuer = gw, .holders = {"bob", "alice", "carol", "evita"}, .maxAmt = 2'000 + evitaOfferCount + 1}); env(pay(gw, "alice", USD(1000))); env(pay(gw, "carol", USD(1))); env(pay(gw, "evita", USD(evitaOfferCount + 1))); // Give carol an extra 150 (unfunded) offers when we're using Taker // to accommodate that difference. int const carolOfferCount{700}; n_offers(env, 400, "alice", XRP(1), USD(1)); n_offers(env, carolOfferCount, "carol", XRP(1), USD(1)); n_offers(env, evitaOfferCount, "evita", XRP(1), USD(1)); // Bob offers to buy 1000 XRP for 1000 USD. He takes all 400 USD from // Alice's offers, 1 USD from Carol's and then removes 599 of Carol's // offers as unfunded, before hitting the step limit. env(offer("bob", USD(1000), XRP(1000))); env.require(balance("bob", USD(401))); env.require(balance("alice", USD(600))); env.require(owners("alice", 1)); env.require(balance("carol", USD(0))); env.require(owners("carol", carolOfferCount - 599)); env.require(balance("evita", USD(evitaOfferCount + 1))); env.require(owners("evita", evitaOfferCount + 1)); // Dan offers to buy maxConsumed + 50 XRP USD. He removes all of // Carol's remaining offers as unfunded, then takes // (maxConsumed - 100) USD from Evita's, hitting the crossing limit. env(offer("dan", USD(maxConsumed + 50), XRP(maxConsumed + 50))); env.require(balance("dan", USD(maxConsumed - 100))); env.require(owners("dan", 2)); env.require(balance("alice", USD(600))); env.require(owners("alice", 1)); env.require(balance("carol", USD(0))); env.require(owners("carol", 1)); env.require(balance("evita", USD(150))); env.require(owners("evita", 150)); } void testAutoBridgedLimits(FeatureBitset features) { testcase("Auto Bridged Limits"); // Extracts as much as possible in one book at one Quality // before proceeding to the other book. This reduces the number of // times we change books. // If any book step in a payment strand consumes 1000 offers, the // liquidity from the offers is used, but that strand will be marked as // dry for the remainder of the transaction. using namespace jtx; auto const gw = Account("gateway"); auto const alice = Account("alice"); auto const bob = Account("bob"); auto const carol = Account("carol"); // There are two almost identical tests. There is a strand with a large // number of unfunded offers that will cause the strand to be marked dry // even though there will still be liquidity available on that strand. // In the first test, the strand has the best initial quality. In the // second test the strand does not have the best quality (the // implementation has to handle this case correct and not mark the // strand dry until the liquidity is actually used) // The implementation allows any single step to consume at most 1000 // offers. With the `FlowSortStrands` feature enabled, if the total // number of offers consumed by all the steps combined exceeds 1500, the // payment stops. { auto test = [&](auto&& issue1, auto&& issue2) { Env env(*this, features); env.fund(XRP(100'000'000), gw, alice, bob, carol); auto const USD = issue1( {.env = env, .token = "USD", .issuer = gw, .holders = {alice, carol}, .limit = maxMPTokenAmount}); auto const EUR = issue2( {.env = env, .token = "EUR", .issuer = gw, .holders = {bob}, .limit = maxMPTokenAmount}); env(pay(gw, alice, USD(4'000))); env(pay(gw, carol, USD(3))); // Notice the strand with the 800 unfunded offers has the // initial best quality n_offers(env, 2'000, alice, EUR(2), XRP(1)); n_offers(env, 100, alice, XRP(1), USD(4)); n_offers(env, 801, carol, XRP(1), USD(3)); // only one offer is funded n_offers(env, 1'000, alice, XRP(1), USD(3)); n_offers(env, 1, alice, EUR(500), USD(500)); // Bob offers to buy 2000 USD for 2000 EUR; He starts with 2000 // EUR // 1. The best quality is the autobridged offers that take 2 // EUR and give 4 USD. // Bob spends 200 EUR and receives 400 USD. // 100 EUR->XRP offers consumed. // 100 XRP->USD offers consumed. // 200 total offers consumed. // // 2. The best quality is the autobridged offers that take 2 // EUR and give 3 USD. // a. One of Carol's offers is taken. This leaves her other // offers unfunded. // b. Carol's remaining 800 offers are consumed as unfunded. // c. 199 of alice's XRP(1) to USD(3) offers are consumed. // A book step is allowed to consume a maximum of 1000 // offers at a given quality, and that limit is now // reached. // d. Now the strand is dry, even though there are still // funded XRP(1) to USD(3) offers available. // Bob has spent 400 EUR and received 600 USD in this // step. 200 EUR->XRP offers consumed 800 unfunded // XRP->USD offers consumed 200 funded XRP->USD offers // consumed (1 carol, 199 alice) 1400 total offers // consumed so far (100 left before the limit) // 3. The best is the non-autobridged offers that takes 500 EUR // and gives 500 USD. // Bob started with 2000 EUR // Bob spent 500 EUR (100+400) // Bob has 1500 EUR left // In this step: // Bob spends 500 EUR and receives 500 USD. // In total: // Bob spent 1100 EUR (200 + 400 + 500) // Bob has 900 EUR remaining (2000 - 1100) // Bob received 1500 USD (400 + 600 + 500) // Alice spent 1497 USD (100*4 + 199*3 + 500) // Alice has 2503 remaining (4000 - 1497) // Alice received 1100 EUR (200 + 400 + 500) env(pay(gw, bob, EUR(2'000))); env.close(); env(offer(bob, USD(4'000), EUR(4'000))); env.close(); env.require(balance(bob, USD(1'500))); env.require(balance(bob, EUR(900))); env.require(offers(bob, 1)); env.require(owners(bob, 3)); env.require(balance(alice, USD(2'503))); env.require(balance(alice, EUR(1'100))); auto const numAOffers = 2'000 + 100 + 1'000 + 1 - (2 * 100 + 2 * 199 + 1 + 1); env.require(offers(alice, numAOffers)); env.require(owners(alice, numAOffers + 2)); env.require(offers(carol, 0)); }; testHelper2TokensMix(test); } { auto test = [&](auto&& issue1, auto&& issue2) { Env env(*this, features); env.fund(XRP(100'000'000), gw, alice, bob, carol); auto const USD = issue1( {.env = env, .token = "USD", .issuer = gw, .holders = {alice, carol}, .limit = maxMPTokenAmount}); auto const EUR = issue2( {.env = env, .token = "EUR", .issuer = gw, .holders = {bob}, .limit = maxMPTokenAmount}); env(pay(gw, alice, USD(4'000))); env(pay(gw, carol, USD(3))); // Notice the strand with the 800 unfunded offers does not have // the initial best quality n_offers(env, 1, alice, EUR(1), USD(10)); n_offers(env, 2'000, alice, EUR(2), XRP(1)); n_offers(env, 100, alice, XRP(1), USD(4)); n_offers(env, 801, carol, XRP(1), USD(3)); // only one offer is funded n_offers(env, 1'000, alice, XRP(1), USD(3)); n_offers(env, 1, alice, EUR(499), USD(499)); // Bob offers to buy 2000 USD for 2000 EUR; He starts with 2000 // EUR // 1. The best quality is the offer that takes 1 EUR and gives // 10 USD // Bob spends 1 EUR and receives 10 USD. // // 2. The best quality is the autobridged offers that takes 2 // EUR and gives 4 USD. // Bob spends 200 EUR and receives 400 USD. // // 3. The best quality is the autobridged offers that takes 2 // EUR and gives 3 USD. // a. One of Carol's offers is taken. This leaves her other // offers unfunded. // b. Carol's remaining 800 offers are consumed as unfunded. // c. 199 of alice's XRP(1) to USD(3) offers are consumed. // A book step is allowed to consume a maximum of 1000 // offers at a given quality, and that limit is now // reached. // d. Now the strand is dry, even though there are still // funded XRP(1) to USD(3) offers available. Bob has spent // 400 EUR and received 600 USD in this step. (200 funded // offers consumed 800 unfunded offers) // 4. The best is the non-autobridged offers that takes 499 EUR // and gives 499 USD. // Bob has 2000 EUR, and has spent 1+200+400=601 EUR. He has // 1399 left. Bob spent 499 EUR and receives 499 USD. // In total: Bob spent EUR(1 + 200 + 400 + 499) = EUR(1100). He // started with 2000 so has 900 remaining // Bob received USD(10 + 400 + 600 + 499) = USD(1509). // Alice spent 10 + 100*4 + 199*3 + 499 = 1506 USD. // She started with 4000 so has 2494 USD remaining. // Alice received 200 + 400 + 500 = 1100 EUR env.close(); env(pay(gw, bob, EUR(2'000))); env.close(); env(offer(bob, USD(4'000), EUR(4'000))); env.close(); env.require(balance(bob, USD(1'509))); env.require(balance(bob, EUR(900))); env.require(offers(bob, 1)); env.require(owners(bob, 3)); env.require(balance(alice, USD(2'494))); env.require(balance(alice, EUR(1'100))); auto const numAOffers = 1 + 2'000 + 100 + 1'000 + 1 - (1 + 2 * 100 + 2 * 199 + 1 + 1); env.require(offers(alice, numAOffers)); env.require(owners(alice, numAOffers + 2)); env.require(offers(carol, 0)); }; testHelper2TokensMix(test); } } void testOfferOverflow(FeatureBitset features) { testcase("Offer Overflow"); using namespace jtx; auto const gw = Account("gateway"); auto const alice = Account("alice"); auto const bob = Account("bob"); Env env(*this, features); env.fund(XRP(100'000'000), gw, alice, bob); MPT const USD = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}}); env(pay(gw, alice, USD(8'000))); env.close(); // The new flow cross handles consuming excessive offers differently // than the old offer crossing code. In the old code, the total number // of consumed offers is tracked, and the crossings will stop after this // limit is hit. In the new code, the number of offers is tracked per // offerbook and per quality. This test shows how they can differ. Set // up a book with many offers. At each quality keep the number of offers // below the limit. However, if all the offers are consumed it would // create a tecOVERSIZE error. // The featureFlowSortStrands introduces a way of tracking the total // number of consumed offers; with this feature the transaction no // longer fails with a tecOVERSIZE error. // The implementation allows any single step to consume at most 1000 // offers. With the `FlowSortStrands` feature enabled, if the total // number of offers consumed by all the steps combined exceeds 1500, the // payment stops. Since the first set of offers consumes 998 offers, the // second set will consume 998, which is not over the limit and the // payment stops. So 2*998, or 1996 is the expected value when // `FlowSortStrands` is enabled. n_offers(env, 998, alice, XRP(1.00), USD(1)); n_offers(env, 998, alice, XRP(0.99), USD(1)); n_offers(env, 998, alice, XRP(0.98), USD(1)); n_offers(env, 998, alice, XRP(0.97), USD(1)); n_offers(env, 998, alice, XRP(0.96), USD(1)); n_offers(env, 998, alice, XRP(0.95), USD(1)); auto const expectedTER = tesSUCCESS; env(offer(bob, USD(8'000), XRP(8'000)), ter(expectedTER)); env.close(); auto const expectedUSD = USD(1'996); env.require(balance(bob, expectedUSD)); } void run() override { using namespace jtx; auto const features = testable_amendments(); testStepLimit(features); testCrossingLimit(features); testStepAndCrossingLimit(features); testAutoBridgedLimits(features); testOfferOverflow(features); } }; BEAST_DEFINE_TESTSUITE_MANUAL_PRIO(CrossingLimitsMPT, tx, xrpl, 10); } // namespace test } // namespace xrpl