mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
Amendments activated for more than 2 years can be retired. This change retires the Flow and FlowSortStrands amendments.
499 lines
20 KiB
C++
499 lines
20 KiB
C++
#include <test/jtx.h>
|
|
|
|
#include <xrpl/beast/unit_test.h>
|
|
#include <xrpl/protocol/Feature.h>
|
|
|
|
namespace ripple {
|
|
namespace test {
|
|
|
|
class CrossingLimits_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");
|
|
auto const USD = gw["USD"];
|
|
|
|
env.fund(XRP(100000000), gw, "alice", "bob", "carol", "dan");
|
|
env.trust(USD(1), "bob");
|
|
env(pay(gw, "bob", USD(1)));
|
|
env.trust(USD(1), "dan");
|
|
env(pay(gw, "dan", USD(1)));
|
|
n_offers(env, 2000, "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(1000), XRP(1000)));
|
|
env.require(balance("alice", USD(1)));
|
|
env.require(owners("alice", 2));
|
|
env.require(balance("bob", USD(0)));
|
|
env.require(owners("bob", 1001));
|
|
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(1000), XRP(1000)));
|
|
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");
|
|
auto const USD = gw["USD"];
|
|
|
|
// The payment engine allows 1000 offers to cross.
|
|
int const maxConsumed = 1000;
|
|
|
|
env.fund(XRP(100000000), gw, "alice", "bob", "carol");
|
|
int const bobsOfferCount = maxConsumed + 150;
|
|
env.trust(USD(bobsOfferCount), "bob");
|
|
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(1000), XRP(1000)));
|
|
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");
|
|
auto const USD = gw["USD"];
|
|
|
|
env.fund(XRP(100000000), gw, "alice", "bob", "carol", "dan", "evita");
|
|
|
|
// The payment engine allows 1000 offers to cross.
|
|
int const maxConsumed = 1000;
|
|
|
|
int const evitasOfferCount{maxConsumed + 49};
|
|
env.trust(USD(1000), "alice");
|
|
env(pay(gw, "alice", USD(1000)));
|
|
env.trust(USD(1000), "carol");
|
|
env(pay(gw, "carol", USD(1)));
|
|
env.trust(USD(evitasOfferCount + 1), "evita");
|
|
env(pay(gw, "evita", USD(evitasOfferCount + 1)));
|
|
|
|
// The payment engine has a limit of 1000 funded or unfunded offers.
|
|
int const carolsOfferCount{700};
|
|
n_offers(env, 400, "alice", XRP(1), USD(1));
|
|
n_offers(env, carolsOfferCount, "carol", XRP(1), USD(1));
|
|
n_offers(env, evitasOfferCount, "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", carolsOfferCount - 599));
|
|
env.require(balance("evita", USD(evitasOfferCount + 1)));
|
|
env.require(owners("evita", evitasOfferCount + 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
|
|
testAutoBridgedLimitsTaker(FeatureBitset features)
|
|
{
|
|
testcase("Auto Bridged Limits Taker");
|
|
|
|
using namespace jtx;
|
|
Env env(*this, features);
|
|
|
|
auto const gw = Account("gateway");
|
|
auto const USD = gw["USD"];
|
|
auto const EUR = gw["EUR"];
|
|
|
|
env.fund(XRP(100000000), gw, "alice", "bob", "carol", "dan", "evita");
|
|
|
|
env.trust(USD(2000), "alice");
|
|
env(pay(gw, "alice", USD(2000)));
|
|
env.trust(USD(1000), "carol");
|
|
env(pay(gw, "carol", USD(3)));
|
|
env.trust(USD(1000), "evita");
|
|
env(pay(gw, "evita", USD(1000)));
|
|
|
|
n_offers(env, 302, "alice", EUR(2), XRP(1));
|
|
n_offers(env, 300, "alice", XRP(1), USD(4));
|
|
n_offers(env, 497, "carol", XRP(1), USD(3));
|
|
n_offers(env, 1001, "evita", EUR(1), USD(1));
|
|
|
|
// Bob offers to buy 2000 USD for 2000 EUR, even though he only has
|
|
// 1000 EUR.
|
|
// 1. He spends 600 EUR taking Alice's auto-bridged offers and
|
|
// gets 1200 USD for that.
|
|
// 2. He spends another 2 EUR taking one of Alice's EUR->XRP and
|
|
// one of Carol's XRP-USD offers. He gets 3 USD for that.
|
|
// 3. The remainder of Carol's offers are now unfunded. We've
|
|
// consumed 602 offers so far. We now chew through 398 more
|
|
// of Carol's unfunded offers until we hit the 1000 offer limit.
|
|
// This sets have_bridge to false -- we will handle no more
|
|
// bridged offers.
|
|
// 4. However, have_direct is still true. So we go around one more
|
|
// time and take one of Evita's offers.
|
|
// 5. After taking one of Evita's offers we notice (again) that our
|
|
// offer count was exceeded. So we completely stop after taking
|
|
// one of Evita's offers.
|
|
env.trust(EUR(10000), "bob");
|
|
env.close();
|
|
env(pay(gw, "bob", EUR(1000)));
|
|
env.close();
|
|
env(offer("bob", USD(2000), EUR(2000)));
|
|
env.require(balance("bob", USD(1204)));
|
|
env.require(balance("bob", EUR(397)));
|
|
|
|
env.require(balance("alice", USD(800)));
|
|
env.require(balance("alice", EUR(602)));
|
|
env.require(offers("alice", 1));
|
|
env.require(owners("alice", 3));
|
|
|
|
env.require(balance("carol", USD(0)));
|
|
env.require(balance("carol", EUR(none)));
|
|
env.require(offers("carol", 100));
|
|
env.require(owners("carol", 101));
|
|
|
|
env.require(balance("evita", USD(999)));
|
|
env.require(balance("evita", EUR(1)));
|
|
env.require(offers("evita", 1000));
|
|
env.require(owners("evita", 1002));
|
|
|
|
// Dan offers to buy 900 EUR for 900 USD.
|
|
// 1. He removes all 100 of Carol's remaining unfunded offers.
|
|
// 2. Then takes 850 USD from Evita's offers.
|
|
// 3. Consuming 850 of Evita's funded offers hits the crossing
|
|
// limit. So Dan's offer crossing stops even though he would
|
|
// be willing to take another 50 of Evita's offers.
|
|
env.trust(EUR(10000), "dan");
|
|
env.close();
|
|
env(pay(gw, "dan", EUR(1000)));
|
|
env.close();
|
|
|
|
env(offer("dan", USD(900), EUR(900)));
|
|
env.require(balance("dan", USD(850)));
|
|
env.require(balance("dan", EUR(150)));
|
|
|
|
env.require(balance("alice", USD(800)));
|
|
env.require(balance("alice", EUR(602)));
|
|
env.require(offers("alice", 1));
|
|
env.require(owners("alice", 3));
|
|
|
|
env.require(balance("carol", USD(0)));
|
|
env.require(balance("carol", EUR(none)));
|
|
env.require(offers("carol", 0));
|
|
env.require(owners("carol", 1));
|
|
|
|
env.require(balance("evita", USD(149)));
|
|
env.require(balance("evita", EUR(851)));
|
|
env.require(offers("evita", 150));
|
|
env.require(owners("evita", 152));
|
|
}
|
|
|
|
void
|
|
testAutoBridgedLimits(FeatureBitset features)
|
|
{
|
|
testcase("Auto Bridged Limits");
|
|
|
|
// 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");
|
|
|
|
auto const USD = gw["USD"];
|
|
auto const EUR = gw["EUR"];
|
|
|
|
// 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.If the total number of offers consumed by all the steps
|
|
// combined exceeds 1500, the payment stops.
|
|
{
|
|
Env env(*this, features);
|
|
|
|
env.fund(XRP(100000000), gw, alice, bob, carol);
|
|
|
|
env.trust(USD(4000), alice);
|
|
env(pay(gw, alice, USD(4000)));
|
|
env.trust(USD(1000), carol);
|
|
env(pay(gw, carol, USD(3)));
|
|
|
|
// Notice the strand with the 800 unfunded offers has the initial
|
|
// best quality
|
|
n_offers(env, 2000, 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, 1000, 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 maxium 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 spents 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.trust(EUR(10000), bob);
|
|
env.close();
|
|
env(pay(gw, bob, EUR(2000)));
|
|
env.close();
|
|
env(offer(bob, USD(4000), EUR(4000)));
|
|
env.close();
|
|
|
|
env.require(balance(bob, USD(1500)));
|
|
env.require(balance(bob, EUR(900)));
|
|
env.require(offers(bob, 1));
|
|
env.require(owners(bob, 3));
|
|
|
|
env.require(balance(alice, USD(2503)));
|
|
env.require(balance(alice, EUR(1100)));
|
|
auto const numAOffers =
|
|
2000 + 100 + 1000 + 1 - (2 * 100 + 2 * 199 + 1 + 1);
|
|
env.require(offers(alice, numAOffers));
|
|
env.require(owners(alice, numAOffers + 2));
|
|
|
|
env.require(offers(carol, 0));
|
|
}
|
|
{
|
|
Env env(*this, features);
|
|
|
|
env.fund(XRP(100000000), gw, alice, bob, carol);
|
|
|
|
env.trust(USD(4000), alice);
|
|
env(pay(gw, alice, USD(4000)));
|
|
env.trust(USD(1000), carol);
|
|
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, 2000, 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, 1000, 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 maxium 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.trust(EUR(10000), bob);
|
|
env.close();
|
|
env(pay(gw, bob, EUR(2000)));
|
|
env.close();
|
|
env(offer(bob, USD(4000), EUR(4000)));
|
|
env.close();
|
|
|
|
env.require(balance(bob, USD(1509)));
|
|
env.require(balance(bob, EUR(900)));
|
|
env.require(offers(bob, 1));
|
|
env.require(owners(bob, 3));
|
|
|
|
env.require(balance(alice, USD(2494)));
|
|
env.require(balance(alice, EUR(1100)));
|
|
auto const numAOffers =
|
|
1 + 2000 + 100 + 1000 + 1 - (1 + 2 * 100 + 2 * 199 + 1 + 1);
|
|
env.require(offers(alice, numAOffers));
|
|
env.require(owners(alice, numAOffers + 2));
|
|
|
|
env.require(offers(carol, 0));
|
|
}
|
|
}
|
|
|
|
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");
|
|
|
|
auto const USD = gw["USD"];
|
|
|
|
Env env(*this, features);
|
|
|
|
env.fund(XRP(100000000), gw, alice, bob);
|
|
|
|
env.trust(USD(8000), alice);
|
|
env.trust(USD(8000), bob);
|
|
env.close();
|
|
|
|
env(pay(gw, alice, USD(8000)));
|
|
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 implementation allows any single step to consume at most 1000
|
|
// offers. 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.
|
|
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));
|
|
|
|
env(offer(bob, USD(8000), XRP(8000)), ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
env.require(balance(bob, USD(1996)));
|
|
}
|
|
|
|
void
|
|
run() override
|
|
{
|
|
auto testAll = [this](FeatureBitset features) {
|
|
testStepLimit(features);
|
|
testCrossingLimit(features);
|
|
testStepAndCrossingLimit(features);
|
|
testAutoBridgedLimits(features);
|
|
testOfferOverflow(features);
|
|
};
|
|
using namespace jtx;
|
|
auto const sa = testable_amendments();
|
|
testAll(sa);
|
|
testAll(sa - featurePermissionedDEX);
|
|
}
|
|
};
|
|
|
|
BEAST_DEFINE_TESTSUITE_MANUAL_PRIO(CrossingLimits, app, ripple, 10);
|
|
|
|
} // namespace test
|
|
} // namespace ripple
|