xrpld
Loading...
Searching...
No Matches
CrossingLimitsMPT_test.cpp
1
2#include <test/jtx/Account.h>
3#include <test/jtx/Env.h>
4#include <test/jtx/TestHelpers.h>
5#include <test/jtx/amount.h>
6#include <test/jtx/balance.h>
7#include <test/jtx/mpt.h>
8#include <test/jtx/offer.h>
9#include <test/jtx/owners.h>
10#include <test/jtx/pay.h>
11#include <test/jtx/tags.h>
12#include <test/jtx/ter.h>
13
14#include <xrpl/beast/unit_test/suite.h>
15#include <xrpl/protocol/Feature.h>
16#include <xrpl/protocol/Protocol.h>
17#include <xrpl/protocol/TER.h>
18
19namespace xrpl::test {
20
22{
23public:
24 void
26 {
27 testcase("Step Limit");
28
29 using namespace jtx;
30 Env env(*this, features);
31
32 auto const gw = Account("gateway");
33
34 env.fund(XRP(100'000'000), gw, "alice", "bob", "carol", "dan");
35 MPT const usd =
36 MPTTester({.env = env, .issuer = gw, .holders = {"bob", "dan"}, .maxAmt = 2});
37 env(pay(gw, "bob", usd(1)));
38 env(pay(gw, "dan", usd(1)));
39 nOffers(env, 2'000, "bob", XRP(1), usd(1));
40 nOffers(env, 1, "dan", XRP(1), usd(1));
41
42 // Alice offers to buy 1000 XRP for 1000 USD. She takes Bob's first
43 // offer, removes 999 more as unfunded, then hits the step limit.
44 env(offer("alice", usd(1'000), XRP(1'000)));
45 env.require(Balance("alice", usd(1)));
46 env.require(Owners("alice", 2));
47 env.require(Balance("bob", usd(0)));
48 env.require(Owners("bob", 1'001));
49 env.require(Balance("dan", usd(1)));
50 env.require(Owners("dan", 2));
51
52 // Carol offers to buy 1000 XRP for 1000 USD. She removes Bob's next
53 // 1000 offers as unfunded and hits the step limit.
54 env(offer("carol", usd(1'000), XRP(1'000)));
55 env.require(Balance("carol", usd(kNone)));
56 env.require(Owners("carol", 1));
57 env.require(Balance("bob", usd(0)));
58 env.require(Owners("bob", 1));
59 env.require(Balance("dan", usd(1)));
60 env.require(Owners("dan", 2));
61 }
62
63 void
65 {
66 testcase("Crossing Limit");
67
68 using namespace jtx;
69 Env env(*this, features);
70
71 auto const gw = Account("gateway");
72
73 int const maxConsumed = 1'000;
74
75 env.fund(XRP(100'000'000), gw, "alice", "bob", "carol");
76 int const bobsOfferCount = maxConsumed + 150;
77 MPT const usd =
78 MPTTester({.env = env, .issuer = gw, .holders = {"bob"}, .maxAmt = bobsOfferCount});
79 env(pay(gw, "bob", usd(bobsOfferCount)));
80 env.close();
81 nOffers(env, bobsOfferCount, "bob", XRP(1), usd(1));
82
83 // Alice offers to buy Bob's offers. However, she hits the offer
84 // crossing limit, so she can't buy them all at once.
85 env(offer("alice", usd(bobsOfferCount), XRP(bobsOfferCount)));
86 env.close();
87 env.require(Balance("alice", usd(maxConsumed)));
88 env.require(Balance("bob", usd(150)));
89 env.require(Owners("bob", 150 + 1));
90
91 // Carol offers to buy 1000 XRP for 1000 USD. She takes Bob's
92 // remaining 150 offers without hitting a limit.
93 env(offer("carol", usd(1'000), XRP(1'000)));
94 env.close();
95 env.require(Balance("carol", usd(150)));
96 env.require(Balance("bob", usd(0)));
97 env.require(Owners("bob", 1));
98 }
99
100 void
102 {
103 testcase("Step And Crossing Limit");
104
105 using namespace jtx;
106 Env env(*this, features);
107
108 auto const gw = Account("gateway");
109
110 env.fund(XRP(100'000'000), gw, "alice", "bob", "carol", "dan", "evita");
111
112 int const maxConsumed = 1'000;
113 int const evitaOfferCount{maxConsumed + 49};
114
115 MPT const usd = MPTTester(
116 {.env = env,
117 .issuer = gw,
118 .holders = {"bob", "alice", "carol", "evita"},
119 .maxAmt = 2'000 + evitaOfferCount + 1});
120
121 env(pay(gw, "alice", usd(1000)));
122 env(pay(gw, "carol", usd(1)));
123 env(pay(gw, "evita", usd(evitaOfferCount + 1)));
124
125 // Give carol an extra 150 (unfunded) offers when we're using Taker
126 // to accommodate that difference.
127 int const carolOfferCount{700};
128 nOffers(env, 400, "alice", XRP(1), usd(1));
129 nOffers(env, carolOfferCount, "carol", XRP(1), usd(1));
130 nOffers(env, evitaOfferCount, "evita", XRP(1), usd(1));
131
132 // Bob offers to buy 1000 XRP for 1000 USD. He takes all 400 USD from
133 // Alice's offers, 1 USD from Carol's and then removes 599 of Carol's
134 // offers as unfunded, before hitting the step limit.
135 env(offer("bob", usd(1000), XRP(1000)));
136 env.require(Balance("bob", usd(401)));
137 env.require(Balance("alice", usd(600)));
138 env.require(Owners("alice", 1));
139 env.require(Balance("carol", usd(0)));
140 env.require(Owners("carol", carolOfferCount - 599));
141 env.require(Balance("evita", usd(evitaOfferCount + 1)));
142 env.require(Owners("evita", evitaOfferCount + 1));
143
144 // Dan offers to buy maxConsumed + 50 XRP USD. He removes all of
145 // Carol's remaining offers as unfunded, then takes
146 // (maxConsumed - 100) USD from Evita's, hitting the crossing limit.
147 env(offer("dan", usd(maxConsumed + 50), XRP(maxConsumed + 50)));
148 env.require(Balance("dan", usd(maxConsumed - 100)));
149 env.require(Owners("dan", 2));
150 env.require(Balance("alice", usd(600)));
151 env.require(Owners("alice", 1));
152 env.require(Balance("carol", usd(0)));
153 env.require(Owners("carol", 1));
154 env.require(Balance("evita", usd(150)));
155 env.require(Owners("evita", 150));
156 }
157
158 void
160 {
161 testcase("Auto Bridged Limits");
162
163 // Extracts as much as possible in one book at one Quality
164 // before proceeding to the other book. This reduces the number of
165 // times we change books.
166
167 // If any book step in a payment strand consumes 1000 offers, the
168 // liquidity from the offers is used, but that strand will be marked as
169 // dry for the remainder of the transaction.
170
171 using namespace jtx;
172
173 auto const gw = Account("gateway");
174 auto const alice = Account("alice");
175 auto const bob = Account("bob");
176 auto const carol = Account("carol");
177
178 // There are two almost identical tests. There is a strand with a large
179 // number of unfunded offers that will cause the strand to be marked dry
180 // even though there will still be liquidity available on that strand.
181 // In the first test, the strand has the best initial quality. In the
182 // second test the strand does not have the best quality (the
183 // implementation has to handle this case correct and not mark the
184 // strand dry until the liquidity is actually used)
185
186 // The implementation allows any single step to consume at most 1000
187 // offers. With the `FlowSortStrands` feature enabled, if the total
188 // number of offers consumed by all the steps combined exceeds 1500, the
189 // payment stops.
190 {
191 auto test = [&](auto&& issue1, auto&& issue2) {
192 Env env(*this, features);
193
194 env.fund(XRP(100'000'000), gw, alice, bob, carol);
195
196 auto const usd = issue1(
197 {.env = env,
198 .token = "USD",
199 .issuer = gw,
200 .holders = {alice, carol},
202 auto const eur = issue2(
203 {.env = env,
204 .token = "EUR",
205 .issuer = gw,
206 .holders = {bob},
208
209 env(pay(gw, alice, usd(4'000)));
210 env(pay(gw, carol, usd(3)));
211
212 // Notice the strand with the 800 unfunded offers has the
213 // initial best quality
214 nOffers(env, 2'000, alice, eur(2), XRP(1));
215 nOffers(env, 100, alice, XRP(1), usd(4));
216 nOffers(env, 801, carol, XRP(1),
217 usd(3)); // only one offer is funded
218 nOffers(env, 1'000, alice, XRP(1), usd(3));
219
220 nOffers(env, 1, alice, eur(500), usd(500));
221
222 // Bob offers to buy 2000 USD for 2000 EUR; He starts with 2000
223 // EUR
224 // 1. The best quality is the autobridged offers that take 2
225 // EUR and give 4 USD.
226 // Bob spends 200 EUR and receives 400 USD.
227 // 100 EUR->XRP offers consumed.
228 // 100 XRP->USD offers consumed.
229 // 200 total offers consumed.
230 //
231 // 2. The best quality is the autobridged offers that take 2
232 // EUR and give 3 USD.
233 // a. One of Carol's offers is taken. This leaves her other
234 // offers unfunded.
235 // b. Carol's remaining 800 offers are consumed as unfunded.
236 // c. 199 of alice's XRP(1) to USD(3) offers are consumed.
237 // A book step is allowed to consume a maximum of 1000
238 // offers at a given quality, and that limit is now
239 // reached.
240 // d. Now the strand is dry, even though there are still
241 // funded XRP(1) to USD(3) offers available.
242 // Bob has spent 400 EUR and received 600 USD in this
243 // step. 200 EUR->XRP offers consumed 800 unfunded
244 // XRP->USD offers consumed 200 funded XRP->USD offers
245 // consumed (1 carol, 199 alice) 1400 total offers
246 // consumed so far (100 left before the limit)
247 // 3. The best is the non-autobridged offers that takes 500 EUR
248 // and gives 500 USD.
249 // Bob started with 2000 EUR
250 // Bob spent 500 EUR (100+400)
251 // Bob has 1500 EUR left
252 // In this step:
253 // Bob spends 500 EUR and receives 500 USD.
254 // In total:
255 // Bob spent 1100 EUR (200 + 400 + 500)
256 // Bob has 900 EUR remaining (2000 - 1100)
257 // Bob received 1500 USD (400 + 600 + 500)
258 // Alice spent 1497 USD (100*4 + 199*3 + 500)
259 // Alice has 2503 remaining (4000 - 1497)
260 // Alice received 1100 EUR (200 + 400 + 500)
261 env(pay(gw, bob, eur(2'000)));
262 env.close();
263 env(offer(bob, usd(4'000), eur(4'000)));
264 env.close();
265
266 env.require(Balance(bob, usd(1'500)));
267 env.require(Balance(bob, eur(900)));
268 env.require(offers(bob, 1));
269 env.require(Owners(bob, 3));
270
271 env.require(Balance(alice, usd(2'503)));
272 env.require(Balance(alice, eur(1'100)));
273 auto const numAOffers = 2'000 + 100 + 1'000 + 1 - ((2 * 100) + (2 * 199) + 1 + 1);
274 env.require(offers(alice, numAOffers));
275 env.require(Owners(alice, numAOffers + 2));
276
277 env.require(offers(carol, 0));
278 };
280 }
281 {
282 auto test = [&](auto&& issue1, auto&& issue2) {
283 Env env(*this, features);
284
285 env.fund(XRP(100'000'000), gw, alice, bob, carol);
286
287 auto const usd = issue1(
288 {.env = env,
289 .token = "USD",
290 .issuer = gw,
291 .holders = {alice, carol},
293 auto const eur = issue2(
294 {.env = env,
295 .token = "EUR",
296 .issuer = gw,
297 .holders = {bob},
299
300 env(pay(gw, alice, usd(4'000)));
301 env(pay(gw, carol, usd(3)));
302
303 // Notice the strand with the 800 unfunded offers does not have
304 // the initial best quality
305 nOffers(env, 1, alice, eur(1), usd(10));
306 nOffers(env, 2'000, alice, eur(2), XRP(1));
307 nOffers(env, 100, alice, XRP(1), usd(4));
308 nOffers(env, 801, carol, XRP(1),
309 usd(3)); // only one offer is funded
310 nOffers(env, 1'000, alice, XRP(1), usd(3));
311
312 nOffers(env, 1, alice, eur(499), usd(499));
313
314 // Bob offers to buy 2000 USD for 2000 EUR; He starts with 2000
315 // EUR
316 // 1. The best quality is the offer that takes 1 EUR and gives
317 // 10 USD
318 // Bob spends 1 EUR and receives 10 USD.
319 //
320 // 2. The best quality is the autobridged offers that takes 2
321 // EUR and gives 4 USD.
322 // Bob spends 200 EUR and receives 400 USD.
323 //
324 // 3. The best quality is the autobridged offers that takes 2
325 // EUR and gives 3 USD.
326 // a. One of Carol's offers is taken. This leaves her other
327 // offers unfunded.
328 // b. Carol's remaining 800 offers are consumed as unfunded.
329 // c. 199 of alice's XRP(1) to USD(3) offers are consumed.
330 // A book step is allowed to consume a maximum of 1000
331 // offers at a given quality, and that limit is now
332 // reached.
333 // d. Now the strand is dry, even though there are still
334 // funded XRP(1) to USD(3) offers available. Bob has spent
335 // 400 EUR and received 600 USD in this step. (200 funded
336 // offers consumed 800 unfunded offers)
337 // 4. The best is the non-autobridged offers that takes 499 EUR
338 // and gives 499 USD.
339 // Bob has 2000 EUR, and has spent 1+200+400=601 EUR. He has
340 // 1399 left. Bob spent 499 EUR and receives 499 USD.
341 // In total: Bob spent EUR(1 + 200 + 400 + 499) = EUR(1100). He
342 // started with 2000 so has 900 remaining
343 // Bob received USD(10 + 400 + 600 + 499) = USD(1509).
344 // Alice spent 10 + 100*4 + 199*3 + 499 = 1506 USD.
345 // She started with 4000 so has 2494 USD remaining.
346 // Alice received 200 + 400 + 500 = 1100 EUR
347 env.close();
348 env(pay(gw, bob, eur(2'000)));
349 env.close();
350 env(offer(bob, usd(4'000), eur(4'000)));
351 env.close();
352
353 env.require(Balance(bob, usd(1'509)));
354 env.require(Balance(bob, eur(900)));
355 env.require(offers(bob, 1));
356 env.require(Owners(bob, 3));
357
358 env.require(Balance(alice, usd(2'494)));
359 env.require(Balance(alice, eur(1'100)));
360 auto const numAOffers =
361 1 + 2'000 + 100 + 1'000 + 1 - (1 + (2 * 100) + (2 * 199) + 1 + 1);
362 env.require(offers(alice, numAOffers));
363 env.require(Owners(alice, numAOffers + 2));
364
365 env.require(offers(carol, 0));
366 };
368 }
369 }
370
371 void
373 {
374 testcase("Offer Overflow");
375
376 using namespace jtx;
377
378 auto const gw = Account("gateway");
379 auto const alice = Account("alice");
380 auto const bob = Account("bob");
381
382 Env env(*this, features);
383
384 env.fund(XRP(100'000'000), gw, alice, bob);
385
386 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}});
387
388 env(pay(gw, alice, usd(8'000)));
389 env.close();
390
391 // The new flow cross handles consuming excessive offers differently
392 // than the old offer crossing code. In the old code, the total number
393 // of consumed offers is tracked, and the crossings will stop after this
394 // limit is hit. In the new code, the number of offers is tracked per
395 // offerbook and per quality. This test shows how they can differ. Set
396 // up a book with many offers. At each quality keep the number of offers
397 // below the limit. However, if all the offers are consumed it would
398 // create a tecOVERSIZE error.
399
400 // The featureFlowSortStrands introduces a way of tracking the total
401 // number of consumed offers; with this feature the transaction no
402 // longer fails with a tecOVERSIZE error.
403 // The implementation allows any single step to consume at most 1000
404 // offers. With the `FlowSortStrands` feature enabled, if the total
405 // number of offers consumed by all the steps combined exceeds 1500, the
406 // payment stops. Since the first set of offers consumes 998 offers, the
407 // second set will consume 998, which is not over the limit and the
408 // payment stops. So 2*998, or 1996 is the expected value when
409 // `FlowSortStrands` is enabled.
410 nOffers(env, 998, alice, XRP(1.00), usd(1));
411 nOffers(env, 998, alice, XRP(0.99), usd(1));
412 nOffers(env, 998, alice, XRP(0.98), usd(1));
413 nOffers(env, 998, alice, XRP(0.97), usd(1));
414 nOffers(env, 998, alice, XRP(0.96), usd(1));
415 nOffers(env, 998, alice, XRP(0.95), usd(1));
416
417 auto const expectedTER = tesSUCCESS;
418
419 env(offer(bob, usd(8'000), XRP(8'000)), Ter(expectedTER));
420 env.close();
421
422 auto const expectedUSD = usd(1'996);
423
424 env.require(Balance(bob, expectedUSD));
425 }
426
427 void
428 run() override
429 {
430 using namespace jtx;
431 auto const features = testableAmendments();
432 testStepLimit(features);
433 testCrossingLimit(features);
434 testStepAndCrossingLimit(features);
435 testAutoBridgedLimits(features);
436 testOfferOverflow(features);
437 }
438};
439
440BEAST_DEFINE_TESTSUITE_MANUAL_PRIO(CrossingLimitsMPT, tx, xrpl, 10);
441
442} // namespace xrpl::test
A testsuite class.
Definition suite.h:50
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:149
void testAutoBridgedLimits(FeatureBitset features)
void testStepLimit(FeatureBitset features)
void testStepAndCrossingLimit(FeatureBitset features)
void testCrossingLimit(FeatureBitset features)
void testOfferOverflow(FeatureBitset features)
A transaction testing environment.
Definition Env.h:143
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:296
PrettyAmount limit(Account const &account, Issue const &issue) const
Returns the IOU limit on an account.
Definition Env.cpp:254
void require(Args const &... args)
Check a set of requirements.
Definition Env.h:605
Test helper for creating, mutating, and asserting MPT and confidential MPT ledger state.
Definition mpt.h:385
Converts to MPT Issue or STAmount.
Match the number of items in the account's owner directory.
Definition owners.h:52
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition ter.h:13
void nOffers(Env &env, std::size_t n, Account const &account, STAmount const &in, STAmount const &out)
static NoneT const kNone
Definition tags.h:9
json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:14
void testHelper2TokensMix(TTester &&tester)
XrpT const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
FeatureBitset testableAmendments()
Definition Env.h:76
json::Value offer(Account const &account, STAmount const &takerPays, STAmount const &takerGets, std::uint32_t flags)
Create an offer.
Definition offer.cpp:14
OwnerCount< ltOFFER > offers
Match the number of offers in the account's owner directory.
Definition owners.h:70
BEAST_DEFINE_TESTSUITE_MANUAL_PRIO(CrossingLimits, app, xrpl, 10)
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
constexpr std::uint64_t kMaxMpTokenAmount
The maximum amount of MPTokenIssuance.
Definition Protocol.h:238
@ tesSUCCESS
Definition TER.h:240