xrpld
Loading...
Searching...
No Matches
AMM_test.cpp
1#include <test/jtx/AMM.h>
2#include <test/jtx/AMMTest.h>
3#include <test/jtx/CaptureLogs.h>
4#include <test/jtx/Env.h>
5#include <test/jtx/TestHelpers.h>
6#include <test/jtx/amount.h>
7#include <test/jtx/envconfig.h>
8#include <test/jtx/escrow.h>
9#include <test/jtx/fee.h>
10#include <test/jtx/flags.h>
11#include <test/jtx/offer.h>
12#include <test/jtx/paths.h>
13#include <test/jtx/pay.h>
14#include <test/jtx/rate.h>
15#include <test/jtx/sendmax.h>
16#include <test/jtx/seq.h>
17#include <test/jtx/sig.h>
18#include <test/jtx/tags.h>
19#include <test/jtx/ter.h>
20#include <test/jtx/trust.h>
21#include <test/jtx/txflags.h>
22
23#include <xrpl/basics/Number.h>
24#include <xrpl/basics/base_uint.h>
25#include <xrpl/basics/chrono.h>
26#include <xrpl/basics/safe_cast.h>
27#include <xrpl/beast/unit_test/suite.h>
28#include <xrpl/beast/utility/Journal.h>
29#include <xrpl/json/json_value.h>
30#include <xrpl/ledger/ApplyView.h>
31#include <xrpl/ledger/helpers/AMMHelpers.h>
32#include <xrpl/ledger/helpers/AccountRootHelpers.h>
33#include <xrpl/protocol/AMMCore.h>
34#include <xrpl/protocol/AccountID.h>
35#include <xrpl/protocol/AmountConversions.h>
36#include <xrpl/protocol/Feature.h>
37#include <xrpl/protocol/Indexes.h>
38#include <xrpl/protocol/Issue.h>
39#include <xrpl/protocol/LedgerFormats.h>
40#include <xrpl/protocol/Protocol.h>
41#include <xrpl/protocol/Quality.h>
42#include <xrpl/protocol/Rules.h>
43#include <xrpl/protocol/SField.h>
44#include <xrpl/protocol/STAmount.h>
45#include <xrpl/protocol/TER.h>
46#include <xrpl/protocol/TxFlags.h>
47#include <xrpl/protocol/UintTypes.h>
48#include <xrpl/protocol/jss.h>
49#include <xrpl/tx/Transactor.h>
50#include <xrpl/tx/transactors/dex/AMMBid.h>
51
52#include <boost/regex/v5/regex.hpp>
53#include <boost/regex/v5/regex_search.hpp>
54
55#include <array>
56#include <cassert>
57#include <chrono>
58#include <cstdint>
59#include <cstring>
60#include <functional>
61#include <initializer_list>
62#include <memory>
63#include <optional>
64#include <stdexcept>
65#include <string>
66#include <tuple>
67#include <utility>
68#include <vector>
69
70namespace xrpl::test {
71
76struct AMM_test : public jtx::AMMTest
77{
78 // Use small Number mantissas for the life of this test.
80
81private:
82 static FeatureBitset
84 {
85 // For now, just disable SAV entirely, which locks in the small Number
86 // mantissas
87 return jtx::testableAmendments() - featureSingleAssetVault - featureLendingProtocol;
88 }
89
90 // Seed from the local testableAmendments() which strips SAV and Lending.
96
97 void
99 {
100 testcase("Instance Create");
101
102 using namespace jtx;
103
104#if NUMBERTODO
105 // XRP to IOU, with featureSingleAssetVault
106 testAMM(
107 [&](AMM& ammAlice, Env&) {
108 BEAST_EXPECT(
109 ammAlice.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0}));
110 },
111 {},
112 0,
113 {},
115#endif
116
117 // XRP to IOU, without featureSingleAssetVault
118 testAMM(
119 [&](AMM& ammAlice, Env&) {
120 BEAST_EXPECT(
121 ammAlice.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0}));
122 },
123 {},
124 0,
125 {},
126 {testableAmendments() - featureSingleAssetVault});
127
128 // IOU to IOU
129 testAMM(
130 [&](AMM& ammAlice, Env&) {
131 BEAST_EXPECT(ammAlice.expectBalances(USD(20'000), BTC(0.5), IOUAmount{100, 0}));
132 },
133 {{USD(20'000), BTC(0.5)}});
134
135 // IOU to IOU + transfer fee
136 {
137 Env env{*this};
138 fund(env, gw_, {alice_}, {USD(20'000), BTC(0.5)}, Fund::All);
139 env(rate(gw_, 1.25));
140 env.close();
141 // no transfer fee on create
142 AMM const ammAlice(env, alice_, USD(20'000), BTC(0.5));
143 BEAST_EXPECT(ammAlice.expectBalances(USD(20'000), BTC(0.5), IOUAmount{100, 0}));
144 BEAST_EXPECT(expectHolding(env, alice_, USD(0)));
145 BEAST_EXPECT(expectHolding(env, alice_, BTC(0)));
146 }
147
148 // Require authorization is set, account is authorized
149 {
150 Env env{*this};
151 env.fund(XRP(30'000), gw_, alice_);
152 env.close();
153 env(fset(gw_, asfRequireAuth));
154 env(trust(alice_, gw_["USD"](30'000), 0));
155 env(trust(gw_, alice_["USD"](0), tfSetfAuth));
156 env.close();
157 env(pay(gw_, alice_, USD(10'000)));
158 env.close();
159 AMM const ammAlice(env, alice_, XRP(10'000), USD(10'000));
160 }
161
162 // Cleared global freeze
163 {
164 Env env{*this};
165 env.fund(XRP(30'000), gw_, alice_);
166 env.close();
167 env.trust(USD(30'000), alice_);
168 env.close();
169 env(pay(gw_, alice_, USD(10'000)));
170 env.close();
171 env(fset(gw_, asfGlobalFreeze));
172 env.close();
173 AMM const ammAliceFail(env, alice_, XRP(10'000), USD(10'000), Ter(tecFROZEN));
174 env(fclear(gw_, asfGlobalFreeze));
175 env.close();
176 AMM const ammAlice(env, alice_, XRP(10'000), USD(10'000));
177 }
178
179 // Trading fee
180 testAMM(
181 [&](AMM& amm, Env&) {
182 BEAST_EXPECT(amm.expectTradingFee(1'000));
183 BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{0}));
184 },
185 std::nullopt,
186 1'000);
187
188 // Make sure asset comparison works.
189 BEAST_EXPECT(
190 STIssue(sfAsset, STAmount(XRP(2'000)).asset()) ==
191 STIssue(sfAsset, STAmount(XRP(2'000)).asset()));
192 BEAST_EXPECT(
193 STIssue(sfAsset, STAmount(XRP(2'000)).asset()) !=
194 STIssue(sfAsset, STAmount(USD(2'000)).asset()));
195 }
196
197 void
199 {
200 testcase("Invalid Instance");
201
202 using namespace jtx;
203
204 // Can't have both XRP tokens
205 {
206 Env env{*this};
207 fund(env, gw_, {alice_}, {USD(30'000)}, Fund::All);
208 AMM const ammAlice(env, alice_, XRP(10'000), XRP(10'000), Ter(temBAD_AMM_TOKENS));
209 BEAST_EXPECT(!ammAlice.ammExists());
210 }
211
212 // Can't have both tokens the same IOU
213 {
214 Env env{*this};
215 fund(env, gw_, {alice_}, {USD(30'000)}, Fund::All);
216 AMM const ammAlice(env, alice_, USD(10'000), USD(10'000), Ter(temBAD_AMM_TOKENS));
217 BEAST_EXPECT(!ammAlice.ammExists());
218 }
219
220 // Can't have zero or negative amounts
221 {
222 Env env{*this};
223 fund(env, gw_, {alice_}, {USD(30'000)}, Fund::All);
224 AMM const ammAlice(env, alice_, XRP(0), USD(10'000), Ter(temBAD_AMOUNT));
225 BEAST_EXPECT(!ammAlice.ammExists());
226 AMM const ammAlice1(env, alice_, XRP(10'000), USD(0), Ter(temBAD_AMOUNT));
227 BEAST_EXPECT(!ammAlice1.ammExists());
228 AMM const ammAlice2(env, alice_, XRP(10'000), USD(-10'000), Ter(temBAD_AMOUNT));
229 BEAST_EXPECT(!ammAlice2.ammExists());
230 AMM const ammAlice3(env, alice_, XRP(-10'000), USD(10'000), Ter(temBAD_AMOUNT));
231 BEAST_EXPECT(!ammAlice3.ammExists());
232 }
233
234 // Bad currency
235 {
236 Env env{*this};
237 fund(env, gw_, {alice_}, {USD(30'000)}, Fund::All);
238 AMM const ammAlice(env, alice_, XRP(10'000), BAD(10'000), Ter(temBAD_CURRENCY));
239 BEAST_EXPECT(!ammAlice.ammExists());
240 }
241
242 // Insufficient IOU balance
243 {
244 Env env{*this};
245 fund(env, gw_, {alice_}, {USD(30'000)}, Fund::All);
246 AMM const ammAlice(env, alice_, XRP(10'000), USD(40'000), Ter(tecUNFUNDED_AMM));
247 BEAST_EXPECT(!ammAlice.ammExists());
248 }
249
250 // Insufficient XRP balance
251 {
252 Env env{*this};
253 fund(env, gw_, {alice_}, {USD(30'000)}, Fund::All);
254 AMM const ammAlice(env, alice_, XRP(40'000), USD(10'000), Ter(tecUNFUNDED_AMM));
255 BEAST_EXPECT(!ammAlice.ammExists());
256 }
257
258 // Invalid trading fee
259 {
260 Env env{*this};
261 fund(env, gw_, {alice_}, {USD(30'000)}, Fund::All);
262 AMM const ammAlice(
263 env,
264 alice_,
265 XRP(10'000),
266 USD(10'000),
267 false,
268 65'001,
269 10,
270 std::nullopt,
271 std::nullopt,
272 std::nullopt,
273 Ter(temBAD_FEE));
274 BEAST_EXPECT(!ammAlice.ammExists());
275 }
276
277 // AMM already exists
278 testAMM([&](AMM& ammAlice, Env& env) {
279 AMM const ammCarol(env, carol_, XRP(10'000), USD(10'000), Ter(tecDUPLICATE));
280 });
281
282 // Invalid flags
283 {
284 Env env{*this};
285 fund(env, gw_, {alice_}, {USD(30'000)}, Fund::All);
286 AMM const ammAlice(
287 env,
288 alice_,
289 XRP(10'000),
290 USD(10'000),
291 false,
292 0,
293 10,
294 tfWithdrawAll,
295 std::nullopt,
296 std::nullopt,
298 BEAST_EXPECT(!ammAlice.ammExists());
299 }
300
301 // Invalid Account
302 {
303 Env env{*this};
304 Account const bad("bad");
305 env.memoize(bad);
306 AMM const ammAlice(
307 env,
308 bad,
309 XRP(10'000),
310 USD(10'000),
311 false,
312 0,
313 10,
314 std::nullopt,
315 Seq(1),
316 std::nullopt,
318 BEAST_EXPECT(!ammAlice.ammExists());
319 }
320
321 // Require authorization is set
322 {
323 Env env{*this};
324 env.fund(XRP(30'000), gw_, alice_);
325 env.close();
326 env(fset(gw_, asfRequireAuth));
327 env.close();
328 env(trust(gw_, alice_["USD"](30'000)));
329 env.close();
330 AMM const ammAlice(env, alice_, XRP(10'000), USD(10'000), Ter(tecNO_AUTH));
331 BEAST_EXPECT(!ammAlice.ammExists());
332 }
333
334 // Globally frozen
335 {
336 Env env{*this};
337 env.fund(XRP(30'000), gw_, alice_);
338 env.close();
339 env(fset(gw_, asfGlobalFreeze));
340 env.close();
341 env(trust(gw_, alice_["USD"](30'000)));
342 env.close();
343 for (auto const& account : {alice_, gw_})
344 {
345 AMM const amm(env, account, XRP(10'000), USD(10'000), Ter(tecFROZEN));
346 BEAST_EXPECT(!amm.ammExists());
347 }
348 }
349
350 // Individually frozen
351 {
352 Env env{*this};
353 env.fund(XRP(30'000), gw_, alice_);
354 env.close();
355 env(trust(gw_, alice_["USD"](30'000)));
356 env.close();
357 env(trust(gw_, alice_["USD"](0), tfSetFreeze));
358 env.close();
359 AMM const ammAlice(env, alice_, XRP(10'000), USD(10'000), Ter(tecFROZEN));
360 BEAST_EXPECT(!ammAlice.ammExists());
361 // issuer can create
362 AMM const amm(env, gw_, XRP(10'000), USD(10'000));
363 }
364
365 // Insufficient reserve, XRP/IOU
366 {
367 Env env(*this);
368 auto const startingXrp = XRP(1'000) + reserve(env, 3) + env.current()->fees().base * 4;
369 env.fund(startingXrp, gw_);
370 env.fund(startingXrp, alice_);
371 env.trust(USD(2'000), alice_);
372 env.close();
373 env(pay(gw_, alice_, USD(2'000)));
374 env.close();
375 env(offer(alice_, XRP(101), USD(100)));
376 env(offer(alice_, XRP(102), USD(100)));
377 AMM const ammAlice(env, alice_, XRP(1'000), USD(1'000), Ter(tecUNFUNDED_AMM));
378 }
379
380 // Insufficient reserve, IOU/IOU
381 {
382 Env env(*this);
383 auto const startingXrp = reserve(env, 4) + env.current()->fees().base * 5;
384 env.fund(startingXrp, gw_);
385 env.fund(startingXrp, alice_);
386 env.trust(USD(2'000), alice_);
387 env.trust(EUR(2'000), alice_);
388 env.close();
389 env(pay(gw_, alice_, USD(2'000)));
390 env(pay(gw_, alice_, EUR(2'000)));
391 env.close();
392 env(offer(alice_, EUR(101), USD(100)));
393 env(offer(alice_, EUR(102), USD(100)));
394 AMM const ammAlice(env, alice_, EUR(1'000), USD(1'000), Ter(tecINSUF_RESERVE_LINE));
395 }
396
397 // Insufficient fee
398 {
399 Env env(*this);
400 fund(env, gw_, {alice_}, XRP(2'000), {USD(2'000), EUR(2'000)});
401 AMM const ammAlice(
402 env,
403 alice_,
404 EUR(1'000),
405 USD(1'000),
406 false,
407 0,
408 ammCrtFee(env).drops() - 1,
409 std::nullopt,
410 std::nullopt,
411 std::nullopt,
413 }
414
415 // AMM with LPTokens
416
417 // AMM with one LPToken from another AMM.
418 testAMM([&](AMM& ammAlice, Env& env) {
419 fund(env, gw_, {alice_}, {EUR(10'000)}, Fund::TokenOnly);
420 AMM const ammAMMToken(
421 env,
422 alice_,
423 EUR(10'000),
424 STAmount{ammAlice.lptIssue(), 1'000'000},
426 AMM const ammAMMToken1(
427 env,
428 alice_,
429 STAmount{ammAlice.lptIssue(), 1'000'000},
430 EUR(10'000),
432 });
433
434 // AMM with two LPTokens from other AMMs.
435 testAMM([&](AMM& ammAlice, Env& env) {
436 fund(env, gw_, {alice_}, {EUR(10'000)}, Fund::TokenOnly);
437 AMM const ammAlice1(env, alice_, XRP(10'000), EUR(10'000));
438 auto const token1 = ammAlice.lptIssue();
439 auto const token2 = ammAlice1.lptIssue();
440 AMM const ammAMMTokens(
441 env,
442 alice_,
443 STAmount{token1, 1'000'000},
444 STAmount{token2, 1'000'000},
446 });
447
448 // Issuer has DefaultRipple disabled
449 {
450 Env env(*this);
451 env.fund(XRP(30'000), gw_);
452 env(fclear(gw_, asfDefaultRipple));
453 AMM const ammGw(env, gw_, XRP(10'000), USD(10'000), Ter(terNO_RIPPLE));
454 env.fund(XRP(30'000), alice_);
455 env.trust(USD(30'000), alice_);
456 env(pay(gw_, alice_, USD(30'000)));
457 AMM const ammAlice(env, alice_, XRP(10'000), USD(10'000), Ter(terNO_RIPPLE));
458 Account const gw1("gw1");
459 env.fund(XRP(30'000), gw1);
460 env(fclear(gw1, asfDefaultRipple));
461 env.trust(USD(30'000), gw1);
462 env(pay(gw_, gw1, USD(30'000)));
463 auto const usD1 = gw1["USD"];
464 AMM const ammGwGw1(env, gw_, USD(10'000), usD1(10'000), Ter(terNO_RIPPLE));
465 env.trust(usD1(30'000), alice_);
466 env(pay(gw1, alice_, usD1(30'000)));
467 AMM const ammAlice1(env, alice_, USD(10'000), usD1(10'000), Ter(terNO_RIPPLE));
468 }
469 }
470
471 void
473 {
474 testcase("Invalid Deposit");
475
476 using namespace jtx;
477
478 testAMM([&](AMM& ammAlice, Env& env) {
479 // Invalid flags
480 ammAlice.deposit(alice_, 1'000'000, std::nullopt, tfWithdrawAll, Ter(temINVALID_FLAG));
481
482 // Invalid options
489 std::optional<std::uint16_t>>> const invalidOptions = {
490 // flags, tokens, asset1In, asset2in, EPrice, tfee
491 {tfLPToken, 1'000, std::nullopt, USD(100), std::nullopt, std::nullopt},
492 {tfLPToken, 1'000, XRP(100), std::nullopt, std::nullopt, std::nullopt},
493 {tfLPToken, 1'000, std::nullopt, std::nullopt, STAmount{USD, 1, -1}, std::nullopt},
494 {tfLPToken,
495 std::nullopt,
496 USD(100),
497 std::nullopt,
498 STAmount{USD, 1, -1},
499 std::nullopt},
500 {tfLPToken, 1'000, XRP(100), std::nullopt, STAmount{USD, 1, -1}, std::nullopt},
501 {tfLPToken, 1'000, std::nullopt, std::nullopt, std::nullopt, 1'000},
502 {tfSingleAsset, 1'000, std::nullopt, std::nullopt, std::nullopt, std::nullopt},
503 {tfSingleAsset, std::nullopt, std::nullopt, USD(100), std::nullopt, std::nullopt},
504 {tfSingleAsset,
505 std::nullopt,
506 std::nullopt,
507 std::nullopt,
508 STAmount{USD, 1, -1},
509 std::nullopt},
510 {tfSingleAsset, std::nullopt, USD(100), std::nullopt, std::nullopt, 1'000},
511 {tfTwoAsset, 1'000, std::nullopt, std::nullopt, std::nullopt, std::nullopt},
512 {tfTwoAsset, std::nullopt, XRP(100), USD(100), STAmount{USD, 1, -1}, std::nullopt},
513 {tfTwoAsset, std::nullopt, XRP(100), std::nullopt, std::nullopt, std::nullopt},
514 {tfTwoAsset, std::nullopt, XRP(100), USD(100), std::nullopt, 1'000},
515 {tfTwoAsset,
516 std::nullopt,
517 std::nullopt,
518 USD(100),
519 STAmount{USD, 1, -1},
520 std::nullopt},
521 {tfOneAssetLPToken, 1'000, std::nullopt, std::nullopt, std::nullopt, std::nullopt},
522 {tfOneAssetLPToken, std::nullopt, XRP(100), USD(100), std::nullopt, std::nullopt},
523 {tfOneAssetLPToken,
524 std::nullopt,
525 XRP(100),
526 std::nullopt,
527 STAmount{USD, 1, -1},
528 std::nullopt},
529 {tfOneAssetLPToken, 1'000, XRP(100), std::nullopt, std::nullopt, 1'000},
530 {tfLimitLPToken, 1'000, std::nullopt, std::nullopt, std::nullopt, std::nullopt},
531 {tfLimitLPToken, 1'000, USD(100), std::nullopt, std::nullopt, std::nullopt},
532 {tfLimitLPToken, std::nullopt, USD(100), XRP(100), std::nullopt, std::nullopt},
533 {tfLimitLPToken, std::nullopt, XRP(100), std::nullopt, STAmount{USD, 1, -1}, 1'000},
534 {tfTwoAssetIfEmpty, std::nullopt, std::nullopt, std::nullopt, std::nullopt, 1'000},
535 {tfTwoAssetIfEmpty, 1'000, std::nullopt, std::nullopt, std::nullopt, std::nullopt},
536 {tfTwoAssetIfEmpty,
537 std::nullopt,
538 XRP(100),
539 USD(100),
540 STAmount{USD, 1, -1},
541 std::nullopt},
542 {tfTwoAssetIfEmpty | tfLPToken,
543 std::nullopt,
544 XRP(100),
545 USD(100),
546 STAmount{USD, 1, -1},
547 std::nullopt}};
548 for (auto const& it : invalidOptions)
549 {
550 ammAlice.deposit(
551 alice_,
552 std::get<1>(it),
553 std::get<2>(it),
554 std::get<3>(it),
555 std::get<4>(it),
556 std::get<0>(it),
557 std::nullopt,
558 std::nullopt,
559 std::get<5>(it),
561 }
562
563 {
564 // bad preflight1
566 jv[jss::Account] = alice_.human();
567 jv[jss::TransactionType] = jss::AMMDeposit;
568 jv[jss::Asset] = STIssue(sfAsset, XRP).getJson(JsonOptions::Values::None);
569 jv[jss::Asset2] = STIssue(sfAsset, USD).getJson(JsonOptions::Values::None);
570 jv[jss::Fee] = "-1";
571 env(jv, Ter(temBAD_FEE));
572 }
573
574 // Invalid tokens
575 ammAlice.deposit(alice_, 0, std::nullopt, std::nullopt, Ter(temBAD_AMM_TOKENS));
576 ammAlice.deposit(
577 alice_, IOUAmount{-1}, std::nullopt, std::nullopt, Ter(temBAD_AMM_TOKENS));
578
579 {
581 jv[jss::Account] = alice_.human();
582 jv[jss::TransactionType] = jss::AMMDeposit;
583 jv[jss::Asset] = STIssue(sfAsset, XRP).getJson(JsonOptions::Values::None);
584 jv[jss::Asset2] = STIssue(sfAsset, USD).getJson(JsonOptions::Values::None);
585 jv[jss::LPTokenOut] = USD(100).value().getJson(JsonOptions::Values::None);
586 jv[jss::Flags] = tfLPToken;
587 env(jv, Ter(temBAD_AMM_TOKENS));
588 }
589
590 // Invalid trading fee
591 ammAlice.deposit(
592 carol_,
593 std::nullopt,
594 XRP(200),
595 USD(200),
596 std::nullopt,
597 tfTwoAssetIfEmpty,
598 std::nullopt,
599 std::nullopt,
600 10'000,
601 Ter(temBAD_FEE));
602
603 // Invalid tokens - bogus currency
604 {
605 auto const iss1 = Issue{Currency(0xabc), gw_.id()};
606 auto const iss2 = Issue{Currency(0xdef), gw_.id()};
607 ammAlice.deposit(
608 alice_,
609 1'000,
610 std::nullopt,
611 std::nullopt,
612 std::nullopt,
613 std::nullopt,
614 {{iss1, iss2}},
615 std::nullopt,
616 std::nullopt,
617 Ter(terNO_AMM));
618 }
619
620 // Depositing mismatched token, invalid Asset1In.issue
621 ammAlice.deposit(
622 alice_, GBP(100), std::nullopt, std::nullopt, std::nullopt, Ter(temBAD_AMM_TOKENS));
623
624 // Depositing mismatched token, invalid Asset2In.issue
625 ammAlice.deposit(
626 alice_, USD(100), GBP(100), std::nullopt, std::nullopt, Ter(temBAD_AMM_TOKENS));
627
628 // Depositing mismatched token, Asset1In.issue == Asset2In.issue
629 ammAlice.deposit(
630 alice_, USD(100), USD(100), std::nullopt, std::nullopt, Ter(temBAD_AMM_TOKENS));
631
632 // Invalid amount value
633 ammAlice.deposit(
634 alice_, USD(0), std::nullopt, std::nullopt, std::nullopt, Ter(temBAD_AMOUNT));
635 ammAlice.deposit(
636 alice_, USD(-1'000), std::nullopt, std::nullopt, std::nullopt, Ter(temBAD_AMOUNT));
637 ammAlice.deposit(
638 alice_, USD(10), std::nullopt, USD(-1), std::nullopt, Ter(temBAD_AMOUNT));
639
640 // Bad currency
641 ammAlice.deposit(
642 alice_, BAD(100), std::nullopt, std::nullopt, std::nullopt, Ter(temBAD_CURRENCY));
643
644 // Invalid Account
645 Account const bad("bad");
646 env.memoize(bad);
647 ammAlice.deposit(
648 bad,
649 1'000'000,
650 std::nullopt,
651 std::nullopt,
652 std::nullopt,
653 std::nullopt,
654 std::nullopt,
655 Seq(1),
656 std::nullopt,
658
659 // Invalid AMM
660 ammAlice.deposit(
661 alice_,
662 1'000,
663 std::nullopt,
664 std::nullopt,
665 std::nullopt,
666 std::nullopt,
667 {{USD, GBP}},
668 std::nullopt,
669 std::nullopt,
670 Ter(terNO_AMM));
671
672 // Single deposit: 100000 tokens worth of USD
673 // Amount to deposit exceeds Max
674 ammAlice.deposit(
675 carol_,
676 100'000,
677 USD(200),
678 std::nullopt,
679 std::nullopt,
680 std::nullopt,
681 std::nullopt,
682 std::nullopt,
683 std::nullopt,
685
686 // Single deposit: 100000 tokens worth of XRP
687 // Amount to deposit exceeds Max
688 ammAlice.deposit(
689 carol_,
690 100'000,
691 XRP(200),
692 std::nullopt,
693 std::nullopt,
694 std::nullopt,
695 std::nullopt,
696 std::nullopt,
697 std::nullopt,
699
700 // Deposit amount is invalid
701 // Calculated amount to deposit is 98,000,000
702 ammAlice.deposit(
703 alice_,
704 USD(0),
705 std::nullopt,
706 STAmount{USD, 1, -1},
707 std::nullopt,
709 // Calculated amount is 0
710 ammAlice.deposit(
711 alice_,
712 USD(0),
713 std::nullopt,
714 STAmount{USD, 2'000, -6},
715 std::nullopt,
717
718 // Deposit non-empty AMM
719 ammAlice.deposit(
720 carol_, XRP(100), USD(100), std::nullopt, tfTwoAssetIfEmpty, Ter(tecAMM_NOT_EMPTY));
721 });
722
723 // Tiny deposit
724 testAMM(
725 [&](AMM& ammAlice, Env& env) {
726 auto const enabledV13 = env.current()->rules().enabled(fixAMMv1_3);
727 auto const err = !enabledV13 ? Ter(temBAD_AMOUNT) : Ter(tesSUCCESS);
728 // Pre-amendment XRP deposit side is rounded to 0
729 // and deposit fails.
730 // Post-amendment XRP deposit side is rounded to 1
731 // and deposit succeeds.
732 ammAlice.deposit(carol_, IOUAmount{1, -4}, std::nullopt, std::nullopt, err);
733 // Pre/post-amendment LPTokens is rounded to 0 and deposit
734 // fails with tecAMM_INVALID_TOKENS.
735 ammAlice.deposit(
736 carol_,
737 STAmount{USD, 1, -12},
738 std::nullopt,
739 std::nullopt,
740 std::nullopt,
742 },
743 std::nullopt,
744 0,
745 std::nullopt,
746 {features, features - fixAMMv1_3});
747
748 // Invalid AMM
749 testAMM([&](AMM& ammAlice, Env& env) {
750 ammAlice.withdrawAll(alice_);
751 ammAlice.deposit(alice_, 10'000, std::nullopt, std::nullopt, Ter(terNO_AMM));
752 });
753
754 // Globally frozen asset
755 testAMM(
756 [&](AMM& ammAlice, Env& env) {
757 env(fset(gw_, asfGlobalFreeze));
758 auto const freezeBlocksAll =
759 features[featureAMMClawback] || features[fixCleanup3_3_0];
760 if (!freezeBlocksAll)
761 {
762 // If the issuer set global freeze, the holder still can
763 // deposit the other non-frozen token when neither
764 // AMMClawback nor fixCleanup3_3_0 is enabled.
765 ammAlice.deposit(carol_, XRP(100));
766 }
767 else
768 {
769 // If the issuer set global freeze, the holder cannot
770 // deposit the other non-frozen token.
771 ammAlice.deposit(
772 carol_, XRP(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN));
773 }
774 for (auto const& account : {carol_, gw_})
775 {
776 ammAlice.deposit(
777 account,
778 USD(100),
779 std::nullopt,
780 std::nullopt,
781 std::nullopt,
782 Ter(tecFROZEN));
783 ammAlice.deposit(
784 account, 1'000'000, std::nullopt, std::nullopt, Ter(tecFROZEN));
785 ammAlice.deposit(
786 account, XRP(100), USD(100), std::nullopt, std::nullopt, Ter(tecFROZEN));
787 }
788 },
789 std::nullopt,
790 0,
791 std::nullopt,
792 {features});
793
794 // Individually frozen (AMM) account
795 testAMM(
796 [&](AMM& ammAlice, Env& env) {
797 env(trust(gw_, carol_["USD"](0), tfSetFreeze));
798 env.close();
799 auto const freezeBlocksAll =
800 features[featureAMMClawback] || features[fixCleanup3_3_0];
801 if (!freezeBlocksAll)
802 {
803 // Can deposit non-frozen token if neither AMMClawback
804 // nor fixCleanup3_3_0 is enabled
805 ammAlice.deposit(carol_, XRP(100));
806 }
807 else
808 {
809 // Cannot deposit non-frozen token if the other token is
810 // frozen
811 ammAlice.deposit(
812 carol_, XRP(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN));
813 }
814
815 ammAlice.deposit(carol_, 1'000'000, std::nullopt, std::nullopt, Ter(tecFROZEN));
816 ammAlice.deposit(
817 carol_, USD(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN));
818 env(trust(gw_, carol_["USD"](0), tfClearFreeze));
819 // Individually frozen AMM
820 env(trust(
821 gw_,
822 STAmount{Issue{gw_["USD"].currency, ammAlice.ammAccount()}, 0},
823 tfSetFreeze));
824 env.close();
825 // Post-fixCleanup3_3_0: checkDepositFreeze checks both pool
826 // assets against the AMM account, so depositing the
827 // non-frozen token is also blocked.
828 if (!features[fixCleanup3_3_0])
829 {
830 ammAlice.deposit(carol_, XRP(100));
831 }
832 else
833 {
834 ammAlice.deposit(
835 carol_, XRP(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN));
836 }
837 ammAlice.deposit(carol_, 1'000'000, std::nullopt, std::nullopt, Ter(tecFROZEN));
838 ammAlice.deposit(
839 carol_, USD(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN));
840 },
841 std::nullopt,
842 0,
843 std::nullopt,
844 {features});
845
846 // Individually frozen (AMM) account with IOU/IOU AMM
847 testAMM(
848 [&](AMM& ammAlice, Env& env) {
849 env(trust(gw_, carol_["USD"](0), tfSetFreeze));
850 env(trust(gw_, carol_["BTC"](0), tfSetFreeze));
851 env.close();
852 ammAlice.deposit(carol_, 1'000'000, std::nullopt, std::nullopt, Ter(tecFROZEN));
853 ammAlice.deposit(
854 carol_, USD(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN));
855 env(trust(gw_, carol_["USD"](0), tfClearFreeze));
856 // Individually frozen AMM
857 env(trust(
858 gw_,
859 STAmount{Issue{gw_["USD"].currency, ammAlice.ammAccount()}, 0},
860 tfSetFreeze));
861 env.close();
862 // Cannot deposit non-frozen token
863 ammAlice.deposit(carol_, 1'000'000, std::nullopt, std::nullopt, Ter(tecFROZEN));
864 ammAlice.deposit(
865 carol_, USD(100), BTC(0.01), std::nullopt, std::nullopt, Ter(tecFROZEN));
866 },
867 {{USD(20'000), BTC(0.5)}});
868
869 // Deposit unauthorized token.
870 {
871 Env env(*this, features);
872 env.fund(XRP(1000), gw_, alice_, bob_);
873 env(fset(gw_, asfRequireAuth));
874 env.close();
875 env(trust(gw_, alice_["USD"](100)), Txflags(tfSetfAuth));
876 env(trust(alice_, gw_["USD"](20)));
877 env.close();
878 env(pay(gw_, alice_, gw_["USD"](10)));
879 env.close();
880 env(trust(gw_, bob_["USD"](100)));
881 env.close();
882
883 AMM amm(env, alice_, XRP(10), gw_["USD"](10), Ter(tesSUCCESS));
884 env.close();
885
886 if (features[featureAMMClawback] || features[fixCleanup3_3_0])
887 {
888 // bob_ can not deposit XRP because he's not authorized to
889 // hold the paired token gw_["USD"].
890 amm.deposit(
891 bob_, XRP(10), std::nullopt, std::nullopt, std::nullopt, Ter(tecNO_AUTH));
892 }
893 else
894 {
895 amm.deposit(
896 bob_, XRP(10), std::nullopt, std::nullopt, std::nullopt, Ter(tesSUCCESS));
897 }
898 }
899
900 // Insufficient XRP balance
901 testAMM([&](AMM& ammAlice, Env& env) {
902 env.fund(XRP(1'000), bob_);
903 env.close();
904 // Adds LPT trustline
905 ammAlice.deposit(bob_, XRP(10));
906 ammAlice.deposit(
907 bob_, XRP(1'000), std::nullopt, std::nullopt, std::nullopt, Ter(tecUNFUNDED_AMM));
908 });
909
910 // Insufficient USD balance
911 testAMM([&](AMM& ammAlice, Env& env) {
912 fund(env, gw_, {bob_}, {USD(1'000)}, Fund::Acct);
913 env.close();
914 ammAlice.deposit(
915 bob_, USD(1'001), std::nullopt, std::nullopt, std::nullopt, Ter(tecUNFUNDED_AMM));
916 });
917
918 // Insufficient USD balance by tokens
919 testAMM([&](AMM& ammAlice, Env& env) {
920 fund(env, gw_, {bob_}, {USD(1'000)}, Fund::Acct);
921 env.close();
922 ammAlice.deposit(
923 bob_,
924 10'000'000,
925 std::nullopt,
926 std::nullopt,
927 std::nullopt,
928 std::nullopt,
929 std::nullopt,
930 std::nullopt,
931 std::nullopt,
933 });
934
935 // Insufficient XRP balance by tokens
936 testAMM([&](AMM& ammAlice, Env& env) {
937 env.fund(XRP(1'000), bob_);
938 env.trust(USD(100'000), bob_);
939 env.close();
940 env(pay(gw_, bob_, USD(90'000)));
941 env.close();
942 ammAlice.deposit(
943 bob_,
944 10'000'000,
945 std::nullopt,
946 std::nullopt,
947 std::nullopt,
948 std::nullopt,
949 std::nullopt,
950 std::nullopt,
951 std::nullopt,
953 });
954
955 // Insufficient reserve, XRP/IOU
956 {
957 Env env(*this);
958 auto const startingXrp = reserve(env, 4) + env.current()->fees().base * 4;
959 env.fund(XRP(10'000), gw_);
960 env.fund(XRP(10'000), alice_);
961 env.fund(startingXrp, carol_);
962 env.trust(USD(2'000), alice_);
963 env.trust(USD(2'000), carol_);
964 env.close();
965 env(pay(gw_, alice_, USD(2'000)));
966 env(pay(gw_, carol_, USD(2'000)));
967 env.close();
968 env(offer(carol_, XRP(100), USD(101)));
969 env(offer(carol_, XRP(100), USD(102)));
970 AMM ammAlice(env, alice_, XRP(1'000), USD(1'000));
971 ammAlice.deposit(
972 carol_,
973 XRP(100),
974 std::nullopt,
975 std::nullopt,
976 std::nullopt,
978
979 env(offer(carol_, XRP(100), USD(103)));
980 ammAlice.deposit(
981 carol_,
982 USD(100),
983 std::nullopt,
984 std::nullopt,
985 std::nullopt,
987 }
988
989 // Insufficient reserve, IOU/IOU
990 {
991 Env env(*this);
992 auto const startingXrp = reserve(env, 4) + env.current()->fees().base * 4;
993 env.fund(XRP(10'000), gw_);
994 env.fund(XRP(10'000), alice_);
995 env.fund(startingXrp, carol_);
996 env.trust(USD(2'000), alice_);
997 env.trust(EUR(2'000), alice_);
998 env.trust(USD(2'000), carol_);
999 env.trust(EUR(2'000), carol_);
1000 env.close();
1001 env(pay(gw_, alice_, USD(2'000)));
1002 env(pay(gw_, alice_, EUR(2'000)));
1003 env(pay(gw_, carol_, USD(2'000)));
1004 env(pay(gw_, carol_, EUR(2'000)));
1005 env.close();
1006 env(offer(carol_, XRP(100), USD(101)));
1007 env(offer(carol_, XRP(100), USD(102)));
1008 AMM ammAlice(env, alice_, XRP(1'000), USD(1'000));
1009 ammAlice.deposit(
1010 carol_,
1011 XRP(100),
1012 std::nullopt,
1013 std::nullopt,
1014 std::nullopt,
1016 }
1017
1018 // Invalid min
1019 testAMM([&](AMM& ammAlice, Env& env) {
1020 // min tokens can't be <= zero
1021 ammAlice.deposit(carol_, 0, XRP(100), tfSingleAsset, Ter(temBAD_AMM_TOKENS));
1022 ammAlice.deposit(carol_, -1, XRP(100), tfSingleAsset, Ter(temBAD_AMM_TOKENS));
1023 ammAlice.deposit(
1024 carol_,
1025 0,
1026 XRP(100),
1027 USD(100),
1028 std::nullopt,
1029 tfTwoAsset,
1030 std::nullopt,
1031 std::nullopt,
1032 std::nullopt,
1034 // min amounts can't be <= zero
1035 ammAlice.deposit(
1036 carol_,
1037 1'000,
1038 XRP(0),
1039 USD(100),
1040 std::nullopt,
1041 tfTwoAsset,
1042 std::nullopt,
1043 std::nullopt,
1044 std::nullopt,
1046 ammAlice.deposit(
1047 carol_,
1048 1'000,
1049 XRP(100),
1050 USD(-1),
1051 std::nullopt,
1052 tfTwoAsset,
1053 std::nullopt,
1054 std::nullopt,
1055 std::nullopt,
1057 // min amount bad currency
1058 ammAlice.deposit(
1059 carol_,
1060 1'000,
1061 XRP(100),
1062 BAD(100),
1063 std::nullopt,
1064 tfTwoAsset,
1065 std::nullopt,
1066 std::nullopt,
1067 std::nullopt,
1069 // min amount bad token pair
1070 ammAlice.deposit(
1071 carol_,
1072 1'000,
1073 XRP(100),
1074 XRP(100),
1075 std::nullopt,
1076 tfTwoAsset,
1077 std::nullopt,
1078 std::nullopt,
1079 std::nullopt,
1081 ammAlice.deposit(
1082 carol_,
1083 1'000,
1084 XRP(100),
1085 GBP(100),
1086 std::nullopt,
1087 tfTwoAsset,
1088 std::nullopt,
1089 std::nullopt,
1090 std::nullopt,
1092 });
1093
1094 // Min deposit
1095 testAMM([&](AMM& ammAlice, Env& env) {
1096 // Equal deposit by tokens
1097 ammAlice.deposit(
1098 carol_,
1099 1'000'000,
1100 XRP(1'000),
1101 USD(1'001),
1102 std::nullopt,
1103 tfLPToken,
1104 std::nullopt,
1105 std::nullopt,
1106 std::nullopt,
1108 ammAlice.deposit(
1109 carol_,
1110 1'000'000,
1111 XRP(1'001),
1112 USD(1'000),
1113 std::nullopt,
1114 tfLPToken,
1115 std::nullopt,
1116 std::nullopt,
1117 std::nullopt,
1119 // Equal deposit by asset
1120 ammAlice.deposit(
1121 carol_,
1122 100'001,
1123 XRP(100),
1124 USD(100),
1125 std::nullopt,
1126 tfTwoAsset,
1127 std::nullopt,
1128 std::nullopt,
1129 std::nullopt,
1131 // Single deposit by asset
1132 ammAlice.deposit(
1133 carol_,
1134 488'090,
1135 XRP(1'000),
1136 std::nullopt,
1137 std::nullopt,
1138 tfSingleAsset,
1139 std::nullopt,
1140 std::nullopt,
1141 std::nullopt,
1143 });
1144
1145 // Equal deposit, tokens rounded to 0
1146 testAMM([&](AMM& amm, Env& env) {
1147 amm.deposit(DepositArg{.tokens = IOUAmount{1, -12}, .err = Ter(tecAMM_INVALID_TOKENS)});
1148 });
1149
1150 // Equal deposit limit, tokens rounded to 0
1151 testAMM(
1152 [&](AMM& amm, Env& env) {
1153 amm.deposit(
1154 DepositArg{
1155 .asset1In = STAmount{USD, 1, -15},
1156 .asset2In = XRPAmount{1},
1157 .err = Ter(tecAMM_INVALID_TOKENS)});
1158 },
1159 {.pool = {{USD(1'000'000), XRP(1'000'000)}}, .features = {features - fixAMMv1_3}});
1160 testAMM([&](AMM& amm, Env& env) {
1161 amm.deposit(
1162 DepositArg{
1163 .asset1In = STAmount{USD, 1, -15},
1164 .asset2In = XRPAmount{1},
1165 .err = Ter(tecAMM_INVALID_TOKENS)});
1166 });
1167
1168 // Single deposit by asset, tokens rounded to 0
1169 testAMM([&](AMM& amm, Env& env) {
1170 amm.deposit(
1171 DepositArg{.asset1In = STAmount{USD, 1, -15}, .err = Ter(tecAMM_INVALID_TOKENS)});
1172 });
1173
1174 // Single deposit by tokens, tokens rounded to 0
1175 testAMM([&](AMM& amm, Env& env) {
1176 amm.deposit(
1177 DepositArg{
1178 .tokens = IOUAmount{1, -10},
1179 .asset1In = STAmount{USD, 1, -15},
1180 .err = Ter(tecAMM_INVALID_TOKENS)});
1181 });
1182
1183 // Single deposit with EPrice, tokens rounded to 0
1184 testAMM([&](AMM& amm, Env& env) {
1185 amm.deposit(
1186 DepositArg{
1187 .asset1In = STAmount{USD, 1, -15},
1188 .maxEP = STAmount{USD, 1, -1},
1189 .err = Ter(tecAMM_INVALID_TOKENS)});
1190 });
1191 }
1192
1193 void
1195 {
1196 testcase("Deposit");
1197
1198 auto const all = testableAmendments();
1199 using namespace jtx;
1200
1201 // Equal deposit: 1000000 tokens, 10% of the current pool
1202 testAMM([&](AMM& ammAlice, Env& env) {
1203 auto const baseFee = env.current()->fees().base;
1204 ammAlice.deposit(carol_, 1'000'000);
1205 BEAST_EXPECT(
1206 ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
1207 // 30,000 less deposited 1,000
1208 BEAST_EXPECT(expectHolding(env, carol_, USD(29'000)));
1209 // 30,000 less deposited 1,000 and 10 drops tx fee
1210 BEAST_EXPECT(expectLedgerEntryRoot(env, carol_, XRPAmount{29'000'000'000 - baseFee}));
1211 });
1212
1213 // equal asset deposit: unit test to exercise the rounding-down of
1214 // LPTokens in the AMMHelpers.cpp: adjustLPTokens calculations
1215 // The LPTokens need to have 16 significant digits and a fractional part
1216 for (Number const& deltaLPTokens :
1217 {Number{UINT64_C(100000'0000000009), -10}, Number{UINT64_C(100000'0000000001), -10}})
1218 {
1219 testAMM([&](AMM& ammAlice, Env& env) {
1220 // initial LPToken balance
1221 IOUAmount const initLPToken = ammAlice.getLPTokensBalance();
1222 IOUAmount const newLPTokens{deltaLPTokens};
1223
1224 // carol_ performs a two-asset deposit
1225 ammAlice.deposit(DepositArg{.account = carol_, .tokens = newLPTokens});
1226
1227 IOUAmount const finalLPToken = ammAlice.getLPTokensBalance();
1228
1229 // Change in behavior due to rounding down of LPTokens:
1230 // there is a decrease in the observed return of LPTokens --
1231 // Inputs Number{UINT64_C(100000'0000000001), -10} and
1232 // Number{UINT64_C(100000'0000000009), -10} are both rounded
1233 // down to 1e5
1234 BEAST_EXPECT((finalLPToken - initLPToken == IOUAmount{1, 5}));
1235 BEAST_EXPECT(finalLPToken - initLPToken < deltaLPTokens);
1236
1237 // fraction of newLPTokens/(existing LPToken balance). The
1238 // existing LPToken balance is 1e7
1239 Number const fr = deltaLPTokens / 1e7;
1240
1241 // The below equations are based on Equation 1, 2 from XLS-30d
1242 // specification, Section: 2.3.1.2
1243 Number const deltaXRP = fr * 1e10;
1244 Number const deltaUSD = fr * 1e4;
1245
1246 STAmount const depositUSD = STAmount{USD, deltaUSD};
1247
1248 STAmount const depositXRP = STAmount{XRP, deltaXRP};
1249
1250 // initial LPTokens (1e7) + newLPTokens
1251 BEAST_EXPECT(ammAlice.expectBalances(
1252 XRP(10'000) + depositXRP,
1253 USD(10'000) + depositUSD,
1254 IOUAmount{1, 7} + newLPTokens));
1255
1256 // 30,000 less deposited depositUSD
1257 BEAST_EXPECT(expectHolding(env, carol_, USD(30'000) - depositUSD));
1258 // 30,000 less deposited depositXRP and 10 drops tx fee
1259 BEAST_EXPECT(
1260 expectLedgerEntryRoot(env, carol_, XRP(30'000) - depositXRP - txFee(env, 1)));
1261 });
1262 }
1263
1264 // Equal limit deposit: deposit USD100 and XRP proportionally
1265 // to the pool composition not to exceed 100XRP. If the amount
1266 // exceeds 100XRP then deposit 100XRP and USD proportionally
1267 // to the pool composition not to exceed 100USD. Fail if exceeded.
1268 // Deposit 100USD/100XRP
1269 testAMM([&](AMM& ammAlice, Env&) {
1270 ammAlice.deposit(carol_, USD(100), XRP(100));
1271 BEAST_EXPECT(
1272 ammAlice.expectBalances(XRP(10'100), USD(10'100), IOUAmount{10'100'000, 0}));
1273 });
1274
1275 // Equal limit deposit.
1276 // Try to deposit 200USD/100XRP. Is truncated to 100USD/100XRP.
1277 testAMM([&](AMM& ammAlice, Env&) {
1278 ammAlice.deposit(carol_, USD(200), XRP(100));
1279 BEAST_EXPECT(
1280 ammAlice.expectBalances(XRP(10'100), USD(10'100), IOUAmount{10'100'000, 0}));
1281 });
1282 // Try to deposit 100USD/200XRP. Is truncated to 100USD/100XRP.
1283 testAMM([&](AMM& ammAlice, Env&) {
1284 ammAlice.deposit(carol_, USD(100), XRP(200));
1285 BEAST_EXPECT(
1286 ammAlice.expectBalances(XRP(10'100), USD(10'100), IOUAmount{10'100'000, 0}));
1287 });
1288
1289 // Single deposit: 1000 USD
1290 testAMM([&](AMM& ammAlice, Env&) {
1291 ammAlice.deposit(carol_, USD(1'000));
1292 BEAST_EXPECT(ammAlice.expectBalances(
1293 XRP(10'000),
1294 STAmount{USD, UINT64_C(10'999'99999999999), -11},
1295 IOUAmount{10'488'088'48170151, -8}));
1296 });
1297
1298 // Single deposit: 1000 XRP
1299 testAMM([&](AMM& ammAlice, Env&) {
1300 ammAlice.deposit(carol_, XRP(1'000));
1301 BEAST_EXPECT(ammAlice.expectBalances(
1302 XRP(11'000), USD(10'000), IOUAmount{10'488'088'48170151, -8}));
1303 });
1304
1305 // Single deposit: 100000 tokens worth of USD
1306 testAMM([&](AMM& ammAlice, Env&) {
1307 ammAlice.deposit(carol_, 100000, USD(205));
1308 BEAST_EXPECT(
1309 ammAlice.expectBalances(XRP(10'000), USD(10'201), IOUAmount{10'100'000, 0}));
1310 });
1311
1312 // Single deposit: 100000 tokens worth of XRP
1313 testAMM([&](AMM& ammAlice, Env& env) {
1314 ammAlice.deposit(carol_, 100'000, XRP(205));
1315 BEAST_EXPECT(
1316 ammAlice.expectBalances(XRP(10'201), USD(10'000), IOUAmount{10'100'000, 0}));
1317 });
1318
1319 // Single deposit with EP not exceeding specified:
1320 // 100USD with EP not to exceed 0.1 (AssetIn/TokensOut)
1321 testAMM([&](AMM& ammAlice, Env&) {
1322 ammAlice.deposit(carol_, USD(1'000), std::nullopt, STAmount{USD, 1, -1});
1323 BEAST_EXPECT(ammAlice.expectBalances(
1324 XRP(10'000),
1325 STAmount{USD, UINT64_C(10'999'99999999999), -11},
1326 IOUAmount{10'488'088'48170151, -8}));
1327 });
1328
1329 // Single deposit with EP not exceeding specified:
1330 // 100USD with EP not to exceed 0.002004 (AssetIn/TokensOut)
1331 testAMM([&](AMM& ammAlice, Env&) {
1332 ammAlice.deposit(carol_, USD(100), std::nullopt, STAmount{USD, 2004, -6});
1333 BEAST_EXPECT(ammAlice.expectBalances(
1334 XRP(10'000), STAmount{USD, 10'080'16, -2}, IOUAmount{10'040'000, 0}));
1335 });
1336
1337 // Single deposit with EP not exceeding specified:
1338 // 0USD with EP not to exceed 0.002004 (AssetIn/TokensOut)
1339 testAMM([&](AMM& ammAlice, Env&) {
1340 ammAlice.deposit(carol_, USD(0), std::nullopt, STAmount{USD, 2004, -6});
1341 BEAST_EXPECT(ammAlice.expectBalances(
1342 XRP(10'000), STAmount{USD, 10'080'16, -2}, IOUAmount{10'040'000, 0}));
1343 });
1344
1345 // IOU to IOU + transfer fee
1346 {
1347 Env env{*this};
1348 fund(env, gw_, {alice_}, {USD(20'000), BTC(0.5)}, Fund::All);
1349 env(rate(gw_, 1.25));
1350 env.close();
1351 AMM ammAlice(env, alice_, USD(20'000), BTC(0.5));
1352 BEAST_EXPECT(ammAlice.expectBalances(USD(20'000), BTC(0.5), IOUAmount{100, 0}));
1353 BEAST_EXPECT(expectHolding(env, alice_, USD(0)));
1354 BEAST_EXPECT(expectHolding(env, alice_, BTC(0)));
1355 fund(env, gw_, {carol_}, {USD(2'000), BTC(0.05)}, Fund::Acct);
1356 // no transfer fee on deposit
1357 ammAlice.deposit(carol_, 10);
1358 BEAST_EXPECT(ammAlice.expectBalances(USD(22'000), BTC(0.55), IOUAmount{110, 0}));
1359 BEAST_EXPECT(expectHolding(env, carol_, USD(0)));
1360 BEAST_EXPECT(expectHolding(env, carol_, BTC(0)));
1361 }
1362
1363 // Tiny deposits
1364 testAMM([&](AMM& ammAlice, Env&) {
1365 ammAlice.deposit(carol_, IOUAmount{1, -3});
1366 BEAST_EXPECT(ammAlice.expectBalances(
1367 XRPAmount{10'000'000'001},
1368 STAmount{USD, UINT64_C(10'000'000001), -6},
1369 IOUAmount{10'000'000'001, -3}));
1370 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{1, -3}));
1371 });
1372 testAMM([&](AMM& ammAlice, Env&) {
1373 ammAlice.deposit(carol_, XRPAmount{1});
1374 BEAST_EXPECT(ammAlice.expectBalances(
1375 XRPAmount{10'000'000'001}, USD(10'000), IOUAmount{1'000'000'000049999, -8}));
1376 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{49999, -8}));
1377 });
1378 testAMM([&](AMM& ammAlice, Env&) {
1379 ammAlice.deposit(carol_, STAmount{USD, 1, -10});
1380 BEAST_EXPECT(ammAlice.expectBalances(
1381 XRP(10'000),
1382 STAmount{USD, UINT64_C(10'000'00000000008), -11},
1383 IOUAmount{10'000'000'00000004, -8}));
1384 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{4, -8}));
1385 });
1386
1387 // Issuer create/deposit
1388 for (auto const& feat : {all, all - fixAMMv1_3})
1389 {
1390 Env env(*this, feat);
1391 env.fund(XRP(30000), gw_);
1392 AMM ammGw(env, gw_, XRP(10'000), USD(10'000));
1393 BEAST_EXPECT(ammGw.expectBalances(XRP(10'000), USD(10'000), ammGw.tokens()));
1394 ammGw.deposit(gw_, 1'000'000);
1395 BEAST_EXPECT(ammGw.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000}));
1396 ammGw.deposit(gw_, USD(1'000));
1397 BEAST_EXPECT(ammGw.expectBalances(
1398 XRP(11'000),
1399 STAmount{USD, UINT64_C(11'999'99999999998), -11},
1400 IOUAmount{11'489'125'29307605, -8}));
1401 }
1402
1403 // Issuer deposit
1404 testAMM([&](AMM& ammAlice, Env& env) {
1405 ammAlice.deposit(gw_, 1'000'000);
1406 BEAST_EXPECT(ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000}));
1407 ammAlice.deposit(gw_, USD(1'000));
1408 BEAST_EXPECT(ammAlice.expectBalances(
1409 XRP(11'000),
1410 STAmount{USD, UINT64_C(11'999'99999999998), -11},
1411 IOUAmount{11'489'125'29307605, -8}));
1412 });
1413
1414 // Min deposit
1415 testAMM([&](AMM& ammAlice, Env& env) {
1416 // Equal deposit by tokens
1417 ammAlice.deposit(
1418 carol_,
1419 1'000'000,
1420 XRP(1'000),
1421 USD(1'000),
1422 std::nullopt,
1423 tfLPToken,
1424 std::nullopt,
1425 std::nullopt);
1426 BEAST_EXPECT(
1427 ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
1428 });
1429 testAMM([&](AMM& ammAlice, Env& env) {
1430 // Equal deposit by asset
1431 ammAlice.deposit(
1432 carol_,
1433 1'000'000,
1434 XRP(1'000),
1435 USD(1'000),
1436 std::nullopt,
1437 tfTwoAsset,
1438 std::nullopt,
1439 std::nullopt);
1440 BEAST_EXPECT(
1441 ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
1442 });
1443 testAMM([&](AMM& ammAlice, Env& env) {
1444 // Single deposit by asset
1445 ammAlice.deposit(
1446 carol_,
1447 488'088,
1448 XRP(1'000),
1449 std::nullopt,
1450 std::nullopt,
1451 tfSingleAsset,
1452 std::nullopt,
1453 std::nullopt);
1454 BEAST_EXPECT(ammAlice.expectBalances(
1455 XRP(11'000), USD(10'000), IOUAmount{10'488'088'48170151, -8}));
1456 });
1457 testAMM([&](AMM& ammAlice, Env& env) {
1458 // Single deposit by asset
1459 ammAlice.deposit(
1460 carol_,
1461 488'088,
1462 USD(1'000),
1463 std::nullopt,
1464 std::nullopt,
1465 tfSingleAsset,
1466 std::nullopt,
1467 std::nullopt);
1468 BEAST_EXPECT(ammAlice.expectBalances(
1469 XRP(10'000),
1470 STAmount{USD, UINT64_C(10'999'99999999999), -11},
1471 IOUAmount{10'488'088'48170151, -8}));
1472 });
1473 }
1474
1475 void
1477 {
1478 testcase("Invalid Withdraw");
1479
1480 auto const all = testableAmendments();
1481 using namespace jtx;
1482
1483 testAMM(
1484 [&](AMM& ammAlice, Env& env) {
1485 WithdrawArg const args{
1486 .asset1Out = XRP(100),
1487 .err = Ter(tecAMM_BALANCE),
1488 };
1489 ammAlice.withdraw(args);
1490 },
1491 {{XRP(99), USD(99)}});
1492
1493 testAMM(
1494 [&](AMM& ammAlice, Env& env) {
1495 WithdrawArg const args{
1496 .asset1Out = USD(100),
1497 .err = Ter(tecAMM_BALANCE),
1498 };
1499 ammAlice.withdraw(args);
1500 },
1501 {{XRP(99), USD(99)}});
1502
1503 {
1504 Env env{*this};
1505 env.fund(XRP(30'000), gw_, alice_, bob_);
1506 env.close();
1507 env(fset(gw_, asfRequireAuth));
1508 env.close();
1509 env(trust(alice_, gw_["USD"](30'000), 0));
1510 env(trust(gw_, alice_["USD"](0), tfSetfAuth));
1511 // Bob trusts Gateway to owe him USD...
1512 env(trust(bob_, gw_["USD"](30'000), 0));
1513 // ...but Gateway does not authorize Bob to hold its USD.
1514 env.close();
1515 env(pay(gw_, alice_, USD(10'000)));
1516 env.close();
1517 AMM ammAlice(env, alice_, XRP(10'000), USD(10'000));
1518 WithdrawArg const args{
1519 .account = bob_,
1520 .asset1Out = USD(100),
1521 .err = Ter(tecNO_AUTH),
1522 };
1523 ammAlice.withdraw(args);
1524 }
1525
1526 testAMM([&](AMM& ammAlice, Env& env) {
1527 // Invalid flags
1528 ammAlice.withdraw(
1529 alice_,
1530 1'000'000,
1531 std::nullopt,
1532 std::nullopt,
1533 std::nullopt,
1534 tfBurnable,
1535 std::nullopt,
1536 std::nullopt,
1538 ammAlice.withdraw(
1539 alice_,
1540 1'000'000,
1541 std::nullopt,
1542 std::nullopt,
1543 std::nullopt,
1544 tfTwoAssetIfEmpty,
1545 std::nullopt,
1546 std::nullopt,
1548
1549 // Invalid options
1556 NotTEC>> const invalidOptions = {
1557 // tokens, asset1Out, asset2Out, EPrice, flags, ter
1558 {std::nullopt,
1559 std::nullopt,
1560 std::nullopt,
1561 std::nullopt,
1562 std::nullopt,
1563 temMALFORMED},
1564 {std::nullopt,
1565 std::nullopt,
1566 std::nullopt,
1567 std::nullopt,
1568 tfSingleAsset | tfTwoAsset,
1569 temMALFORMED},
1570 {1'000, std::nullopt, std::nullopt, std::nullopt, tfWithdrawAll, temMALFORMED},
1571 {std::nullopt,
1572 USD(0),
1573 XRP(100),
1574 std::nullopt,
1575 tfWithdrawAll | tfLPToken,
1576 temMALFORMED},
1577 {std::nullopt, std::nullopt, USD(100), std::nullopt, tfWithdrawAll, temMALFORMED},
1578 {std::nullopt,
1579 std::nullopt,
1580 std::nullopt,
1581 std::nullopt,
1582 tfWithdrawAll | tfOneAssetWithdrawAll,
1583 temMALFORMED},
1584 {std::nullopt, USD(100), std::nullopt, std::nullopt, tfWithdrawAll, temMALFORMED},
1585 {std::nullopt,
1586 std::nullopt,
1587 std::nullopt,
1588 std::nullopt,
1589 tfOneAssetWithdrawAll,
1590 temMALFORMED},
1591 {1'000, std::nullopt, USD(100), std::nullopt, std::nullopt, temMALFORMED},
1592 {std::nullopt,
1593 std::nullopt,
1594 std::nullopt,
1595 IOUAmount{250, 0},
1596 tfWithdrawAll,
1597 temMALFORMED},
1598 {1'000, std::nullopt, std::nullopt, IOUAmount{250, 0}, std::nullopt, temMALFORMED},
1599 {std::nullopt,
1600 std::nullopt,
1601 USD(100),
1602 IOUAmount{250, 0},
1603 std::nullopt,
1604 temMALFORMED},
1605 {std::nullopt, XRP(100), USD(100), IOUAmount{250, 0}, std::nullopt, temMALFORMED},
1606 {1'000, XRP(100), USD(100), std::nullopt, std::nullopt, temMALFORMED},
1607 {std::nullopt, XRP(100), USD(100), std::nullopt, tfWithdrawAll, temMALFORMED}};
1608 for (auto const& it : invalidOptions)
1609 {
1610 ammAlice.withdraw(
1611 alice_,
1612 std::get<0>(it),
1613 std::get<1>(it),
1614 std::get<2>(it),
1615 std::get<3>(it),
1616 std::get<4>(it),
1617 std::nullopt,
1618 std::nullopt,
1619 Ter(std::get<5>(it)));
1620 }
1621
1622 // Invalid tokens
1623 ammAlice.withdraw(alice_, 0, std::nullopt, std::nullopt, Ter(temBAD_AMM_TOKENS));
1624 ammAlice.withdraw(
1625 alice_, IOUAmount{-1}, std::nullopt, std::nullopt, Ter(temBAD_AMM_TOKENS));
1626
1627 // Mismatched token, invalid Asset1Out issue
1628 ammAlice.withdraw(alice_, GBP(100), std::nullopt, std::nullopt, Ter(temBAD_AMM_TOKENS));
1629
1630 // Mismatched token, invalid Asset2Out issue
1631 ammAlice.withdraw(alice_, USD(100), GBP(100), std::nullopt, Ter(temBAD_AMM_TOKENS));
1632
1633 // Mismatched token, Asset1Out.issue == Asset2Out.issue
1634 ammAlice.withdraw(alice_, USD(100), USD(100), std::nullopt, Ter(temBAD_AMM_TOKENS));
1635
1636 // Invalid amount value
1637 ammAlice.withdraw(alice_, USD(0), std::nullopt, std::nullopt, Ter(temBAD_AMOUNT));
1638 ammAlice.withdraw(alice_, USD(-100), std::nullopt, std::nullopt, Ter(temBAD_AMOUNT));
1639 ammAlice.withdraw(alice_, USD(10), std::nullopt, IOUAmount{-1}, Ter(temBAD_AMOUNT));
1640
1641 // Invalid amount/token value, withdraw all tokens from one side
1642 // of the pool.
1643 ammAlice.withdraw(alice_, USD(10'000), std::nullopt, std::nullopt, Ter(tecAMM_BALANCE));
1644 ammAlice.withdraw(alice_, XRP(10'000), std::nullopt, std::nullopt, Ter(tecAMM_BALANCE));
1645 ammAlice.withdraw(
1646 alice_,
1647 std::nullopt,
1648 USD(0),
1649 std::nullopt,
1650 std::nullopt,
1651 tfOneAssetWithdrawAll,
1652 std::nullopt,
1653 std::nullopt,
1655
1656 // Bad currency
1657 ammAlice.withdraw(alice_, BAD(100), std::nullopt, std::nullopt, Ter(temBAD_CURRENCY));
1658
1659 // Invalid Account
1660 Account const bad("bad");
1661 env.memoize(bad);
1662 ammAlice.withdraw(
1663 bad,
1664 1'000'000,
1665 std::nullopt,
1666 std::nullopt,
1667 std::nullopt,
1668 std::nullopt,
1669 std::nullopt,
1670 Seq(1),
1672
1673 // Invalid AMM
1674 ammAlice.withdraw(
1675 alice_,
1676 1'000,
1677 std::nullopt,
1678 std::nullopt,
1679 std::nullopt,
1680 std::nullopt,
1681 {{USD, GBP}},
1682 std::nullopt,
1683 Ter(terNO_AMM));
1684
1685 // Carol is not a Liquidity Provider
1686 ammAlice.withdraw(carol_, 10'000, std::nullopt, std::nullopt, Ter(tecAMM_BALANCE));
1687
1688 // Withdrawing from one side.
1689 // XRP by tokens
1690 ammAlice.withdraw(
1691 alice_, IOUAmount(9'999'999'9999, -4), XRP(0), std::nullopt, Ter(tecAMM_BALANCE));
1692 // USD by tokens
1693 ammAlice.withdraw(
1694 alice_, IOUAmount(9'999'999'9, -1), USD(0), std::nullopt, Ter(tecAMM_BALANCE));
1695 // XRP
1696 ammAlice.withdraw(alice_, XRP(10'000), std::nullopt, std::nullopt, Ter(tecAMM_BALANCE));
1697 // USD
1698 ammAlice.withdraw(
1699 alice_,
1700 STAmount{USD, UINT64_C(9'999'9999999999999), -13},
1701 std::nullopt,
1702 std::nullopt,
1704 });
1705
1706 testAMM(
1707 [&](AMM& ammAlice, Env& env) {
1708 // Withdraw entire one side of the pool.
1709 // Pre-amendment:
1710 // Equal withdraw but due to XRP rounding
1711 // this results in full withdraw of XRP pool only,
1712 // while leaving a tiny amount in USD pool.
1713 // Post-amendment:
1714 // Most of the pool is withdrawn with remaining tiny amounts
1715 auto err = env.enabled(fixAMMv1_3) ? Ter(tesSUCCESS) : Ter(tecAMM_BALANCE);
1716 ammAlice.withdraw(
1717 alice_, IOUAmount{9'999'999'9999, -4}, std::nullopt, std::nullopt, err);
1718 if (env.enabled(fixAMMv1_3))
1719 {
1720 BEAST_EXPECT(ammAlice.expectBalances(
1721 XRPAmount(1), STAmount{USD, 1, -7}, IOUAmount{1, -4}));
1722 }
1723 },
1724 std::nullopt,
1725 0,
1726 std::nullopt,
1727 {all, all - fixAMMv1_3});
1728
1729 testAMM(
1730 [&](AMM& ammAlice, Env& env) {
1731 // Similar to above with even smaller remaining amount
1732 // is it ok that the pool is unbalanced?
1733 // Withdraw entire one side of the pool.
1734 // Equal withdraw but due to XRP precision limit,
1735 // this results in full withdraw of XRP pool only,
1736 // while leaving a tiny amount in USD pool.
1737 auto err = env.enabled(fixAMMv1_3) ? Ter(tesSUCCESS) : Ter(tecAMM_BALANCE);
1738 ammAlice.withdraw(
1739 alice_, IOUAmount{9'999'999'999999999, -9}, std::nullopt, std::nullopt, err);
1740 if (env.enabled(fixAMMv1_3))
1741 {
1742 BEAST_EXPECT(ammAlice.expectBalances(
1743 XRPAmount(1), STAmount{USD, 1, -11}, IOUAmount{1, -8}));
1744 }
1745 },
1746 std::nullopt,
1747 0,
1748 std::nullopt,
1749 {all, all - fixAMMv1_3});
1750
1751 // Invalid AMM
1752 testAMM([&](AMM& ammAlice, Env& env) {
1753 ammAlice.withdrawAll(alice_);
1754 ammAlice.withdraw(alice_, 10'000, std::nullopt, std::nullopt, Ter(terNO_AMM));
1755 });
1756
1757 // Globally frozen asset
1758 testAMM(
1759 [&](AMM& ammAlice, Env& env) {
1760 auto const fix330 = env.current()->rules().enabled(fixCleanup3_3_0);
1761 ammAlice.deposit({.account = gw_, .asset1In = USD(1'000), .asset2In = XRP(1'000)});
1762 env(fset(gw_, asfGlobalFreeze));
1763 env.close();
1764 // Can withdraw non-frozen token
1765 for (auto const& account : {alice_, gw_})
1766 {
1767 ammAlice.withdraw(account, XRP(100));
1768 // Post-fixCleanup3_3_0 the issuer can withdraw their own
1769 // frozen token from the pool.
1770 auto const frozenErr =
1771 (fix330 && account == gw_) ? Ter(tesSUCCESS) : Ter(tecFROZEN);
1772 ammAlice.withdraw(account, USD(100), std::nullopt, std::nullopt, frozenErr);
1773 ammAlice.withdraw(account, 1'000, std::nullopt, std::nullopt, frozenErr);
1774 }
1775 },
1776 std::nullopt,
1777 0,
1778 std::nullopt,
1779 amendmentCombinations({fixCleanup3_3_0}));
1780
1781 // Individually frozen (AMM) account
1782 testAMM(
1783 [&](AMM& ammAlice, Env& env) {
1784 auto const fix330 = env.current()->rules().enabled(fixCleanup3_3_0);
1785 env(trust(gw_, alice_["USD"](0), tfSetFreeze));
1786 env.close();
1787 // Can withdraw non-frozen token
1788 ammAlice.withdraw(alice_, XRP(100));
1789 // Post-fixCleanup3_3_0 regular freeze no longer blocks
1790 // self-withdrawal; only deep freeze does.
1791 auto const indivFreezeErr = fix330 ? Ter(tesSUCCESS) : Ter(tecFROZEN);
1792 ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt, indivFreezeErr);
1793 ammAlice.withdraw(alice_, USD(100), std::nullopt, std::nullopt, indivFreezeErr);
1794 env(trust(gw_, alice_["USD"](0), tfClearFreeze));
1795 // Individually frozen AMM — still blocked regardless of
1796 // fixCleanup3_3_0 because the AMM account itself is frozen.
1797 env(trust(
1798 gw_,
1799 STAmount{Issue{gw_["USD"].currency, ammAlice.ammAccount()}, 0},
1800 tfSetFreeze));
1801 ammAlice.withdraw(alice_, XRP(100));
1802 ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt, Ter(tecFROZEN));
1803 ammAlice.withdraw(alice_, USD(100), std::nullopt, std::nullopt, Ter(tecFROZEN));
1804 },
1805 std::nullopt,
1806 0,
1807 std::nullopt,
1808 amendmentCombinations({fixCleanup3_3_0}));
1809
1810 // Carol withdraws more than she owns
1811 testAMM([&](AMM& ammAlice, Env&) {
1812 // Single deposit of 100000 worth of tokens,
1813 // which is 10% of the pool. Carol is LP now.
1814 ammAlice.deposit(carol_, 1'000'000);
1815 BEAST_EXPECT(
1816 ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
1817
1818 ammAlice.withdraw(
1819 carol_, 2'000'000, std::nullopt, std::nullopt, Ter(tecAMM_INVALID_TOKENS));
1820 BEAST_EXPECT(
1821 ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
1822 });
1823
1824 // Withdraw with EPrice limit. Fails to withdraw, calculated tokens
1825 // to withdraw are 0.
1826 testAMM(
1827 [&](AMM& ammAlice, Env& env) {
1828 ammAlice.deposit(carol_, 1'000'000);
1829 auto const err =
1830 env.enabled(fixAMMv1_3) ? Ter(tecAMM_INVALID_TOKENS) : Ter(tecAMM_FAILED);
1831 ammAlice.withdraw(carol_, USD(100), std::nullopt, IOUAmount{500, 0}, err);
1832 },
1833 std::nullopt,
1834 0,
1835 std::nullopt,
1836 {all, all - fixAMMv1_3});
1837
1838 // Withdraw with EPrice limit. Fails to withdraw, calculated tokens
1839 // to withdraw are greater than the LP shares.
1840 testAMM([&](AMM& ammAlice, Env&) {
1841 ammAlice.deposit(carol_, 1'000'000);
1842 ammAlice.withdraw(
1843 carol_, USD(100), std::nullopt, IOUAmount{600, 0}, Ter(tecAMM_INVALID_TOKENS));
1844 });
1845
1846 // Withdraw with EPrice limit. Fails to withdraw, amount1
1847 // to withdraw is less than 1700USD.
1848 testAMM([&](AMM& ammAlice, Env&) {
1849 ammAlice.deposit(carol_, 1'000'000);
1850 ammAlice.withdraw(
1851 carol_, USD(1'700), std::nullopt, IOUAmount{520, 0}, Ter(tecAMM_FAILED));
1852 });
1853
1854 // Deposit/Withdraw the same amount with the trading fee
1855 testAMM(
1856 [&](AMM& ammAlice, Env&) {
1857 ammAlice.deposit(carol_, USD(1'000));
1858 ammAlice.withdraw(
1859 carol_, USD(1'000), std::nullopt, std::nullopt, Ter(tecAMM_INVALID_TOKENS));
1860 },
1861 std::nullopt,
1862 1'000);
1863 testAMM(
1864 [&](AMM& ammAlice, Env&) {
1865 ammAlice.deposit(carol_, XRP(1'000));
1866 ammAlice.withdraw(
1867 carol_, XRP(1'000), std::nullopt, std::nullopt, Ter(tecAMM_INVALID_TOKENS));
1868 },
1869 std::nullopt,
1870 1'000);
1871
1872 // Deposit/Withdraw the same amount fails due to the tokens adjustment
1873 testAMM([&](AMM& ammAlice, Env&) {
1874 ammAlice.deposit(carol_, STAmount{USD, 1, -6});
1875 ammAlice.withdraw(
1876 carol_,
1877 STAmount{USD, 1, -6},
1878 std::nullopt,
1879 std::nullopt,
1881 });
1882
1883 // Withdraw close to one side of the pool. Account's LP tokens
1884 // are rounded to all LP tokens.
1885 testAMM(
1886 [&](AMM& ammAlice, Env& env) {
1887 // Without fixAMMv1_3: sub-method returns tecAMM_BALANCE early.
1888 // With fixAMMv1_3 but without fixCleanup3_3_0: sub-method succeeds
1889 // but invariant check catches the precision violation.
1890 // With fixCleanup3_3_0: caught in the transaction layer before
1891 // the invariant checker runs.
1892 auto const err = [&] {
1893 if (!env.enabled(fixAMMv1_3))
1894 return Ter(tecAMM_BALANCE);
1895 if (env.enabled(fixCleanup3_3_0))
1896 return Ter(tecPRECISION_LOSS);
1897 return Ter(tecINVARIANT_FAILED);
1898 }();
1899 ammAlice.withdraw(
1900 alice_,
1901 STAmount{USD, UINT64_C(9'999'999999999999), -12},
1902 std::nullopt,
1903 std::nullopt,
1904 err);
1905 },
1906 {.features = {all, all - fixAMMv1_3, all - fixCleanup3_3_0}, .noLog = true});
1907
1908 // Tiny withdraw
1909 testAMM([&](AMM& ammAlice, Env&) {
1910 // XRP amount to withdraw is 0
1911 ammAlice.withdraw(
1912 alice_, IOUAmount{1, -5}, std::nullopt, std::nullopt, Ter(tecAMM_FAILED));
1913 // Calculated tokens to withdraw are 0
1914 ammAlice.withdraw(
1915 alice_,
1916 std::nullopt,
1917 STAmount{USD, 1, -11},
1918 std::nullopt,
1920 ammAlice.deposit(carol_, STAmount{USD, 1, -10});
1921 ammAlice.withdraw(
1922 carol_,
1923 std::nullopt,
1924 STAmount{USD, 1, -9},
1925 std::nullopt,
1927 ammAlice.withdraw(
1928 carol_, std::nullopt, XRPAmount{1}, std::nullopt, Ter(tecAMM_INVALID_TOKENS));
1929 ammAlice.withdraw(
1930 WithdrawArg{.tokens = IOUAmount{1, -10}, .err = Ter(tecAMM_INVALID_TOKENS)});
1931 ammAlice.withdraw(
1933 .asset1Out = STAmount{USD, 1, -15},
1934 .asset2Out = XRPAmount{1},
1935 .err = Ter(tecAMM_INVALID_TOKENS)});
1936 ammAlice.withdraw(
1938 .tokens = IOUAmount{1, -10},
1939 .asset1Out = STAmount{USD, 1, -15},
1940 .err = Ter(tecAMM_INVALID_TOKENS)});
1941 });
1942 }
1943
1944 void
1946 {
1947 testcase("Withdraw");
1948
1949 auto const all = testableAmendments();
1950 using namespace jtx;
1951
1952 // Equal withdrawal by Carol: 1000000 of tokens, 10% of the current
1953 // pool
1954 testAMM([&](AMM& ammAlice, Env& env) {
1955 auto const baseFee = env.current()->fees().base.drops();
1956 // Single deposit of 100000 worth of tokens,
1957 // which is 10% of the pool. Carol is LP now.
1958 ammAlice.deposit(carol_, 1'000'000);
1959 BEAST_EXPECT(
1960 ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
1961 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{1'000'000, 0}));
1962 // 30,000 less deposited 1,000
1963 BEAST_EXPECT(expectHolding(env, carol_, USD(29'000)));
1964 // 30,000 less deposited 1,000 and 10 drops tx fee
1965 BEAST_EXPECT(expectLedgerEntryRoot(env, carol_, XRPAmount{29'000'000'000 - baseFee}));
1966
1967 // Carol withdraws all tokens
1968 ammAlice.withdraw(carol_, 1'000'000);
1969 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount(beast::Zero())));
1970 BEAST_EXPECT(expectHolding(env, carol_, USD(30'000)));
1971 BEAST_EXPECT(
1972 expectLedgerEntryRoot(env, carol_, XRPAmount{30'000'000'000 - (2 * baseFee)}));
1973 });
1974
1975 // Equal withdrawal by tokens 1000000, 10%
1976 // of the current pool
1977 testAMM([&](AMM& ammAlice, Env&) {
1978 ammAlice.withdraw(alice_, 1'000'000);
1979 BEAST_EXPECT(ammAlice.expectBalances(XRP(9'000), USD(9'000), IOUAmount{9'000'000, 0}));
1980 });
1981
1982 // Equal withdrawal with a limit. Withdraw XRP200.
1983 // If proportional withdraw of USD is less than 100
1984 // then withdraw that amount, otherwise withdraw USD100
1985 // and proportionally withdraw XRP. It's the latter
1986 // in this case - XRP100/USD100.
1987 testAMM([&](AMM& ammAlice, Env&) {
1988 ammAlice.withdraw(alice_, XRP(200), USD(100));
1989 BEAST_EXPECT(ammAlice.expectBalances(XRP(9'900), USD(9'900), IOUAmount{9'900'000, 0}));
1990 });
1991
1992 // Equal withdrawal with a limit. XRP100/USD100.
1993 testAMM([&](AMM& ammAlice, Env&) {
1994 ammAlice.withdraw(alice_, XRP(100), USD(200));
1995 BEAST_EXPECT(ammAlice.expectBalances(XRP(9'900), USD(9'900), IOUAmount{9'900'000, 0}));
1996 });
1997
1998 // Single withdrawal by amount XRP1000
1999 testAMM(
2000 [&](AMM& ammAlice, Env& env) {
2001 ammAlice.withdraw(alice_, XRP(1'000));
2002 if (!env.enabled(fixAMMv1_3))
2003 {
2004 BEAST_EXPECT(ammAlice.expectBalances(
2005 XRP(9'000), USD(10'000), IOUAmount{9'486'832'98050514, -8}));
2006 }
2007 else
2008 {
2009 BEAST_EXPECT(ammAlice.expectBalances(
2010 XRPAmount{9'000'000'001}, USD(10'000), IOUAmount{9'486'832'98050514, -8}));
2011 }
2012 },
2013 std::nullopt,
2014 0,
2015 std::nullopt,
2016 {all, all - fixAMMv1_3});
2017
2018 // Single withdrawal by tokens 10000.
2019 testAMM([&](AMM& ammAlice, Env&) {
2020 ammAlice.withdraw(alice_, 10'000, USD(0));
2021 BEAST_EXPECT(
2022 ammAlice.expectBalances(XRP(10'000), USD(9980.01), IOUAmount{9'990'000, 0}));
2023 });
2024
2025 // Withdraw all tokens.
2026 testAMM([&](AMM& ammAlice, Env& env) {
2027 env(trust(carol_, STAmount{ammAlice.lptIssue(), 10'000}));
2028 // Can TrustSet only for AMM LP tokens
2029 env(trust(carol_, STAmount{Issue{EUR.currency, ammAlice.ammAccount()}, 10'000}),
2031 env.close();
2032 ammAlice.withdrawAll(alice_);
2033 BEAST_EXPECT(!ammAlice.ammExists());
2034
2035 BEAST_EXPECT(!env.le(keylet::ownerDir(ammAlice.ammAccount())));
2036
2037 // Can create AMM for the XRP/USD pair
2038 AMM const ammCarol(env, carol_, XRP(10'000), USD(10'000));
2039 BEAST_EXPECT(
2040 ammCarol.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0}));
2041 });
2042
2043 // Single deposit 1000USD, withdraw all tokens in USD
2044 testAMM([&](AMM& ammAlice, Env& env) {
2045 ammAlice.deposit(carol_, USD(1'000));
2046 ammAlice.withdrawAll(carol_, USD(0));
2047 BEAST_EXPECT(
2048 ammAlice.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0}));
2049 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount(beast::Zero())));
2050 });
2051
2052 // Single deposit 1000USD, withdraw all tokens in XRP
2053 testAMM([&](AMM& ammAlice, Env&) {
2054 ammAlice.deposit(carol_, USD(1'000));
2055 ammAlice.withdrawAll(carol_, XRP(0));
2056 BEAST_EXPECT(ammAlice.expectBalances(
2057 XRPAmount(9'090'909'091),
2058 STAmount{USD, UINT64_C(10'999'99999999999), -11},
2059 IOUAmount{10'000'000, 0}));
2060 });
2061
2062 // Single deposit/withdraw by the same account
2063 testAMM(
2064 [&](AMM& ammAlice, Env& env) {
2065 // Since a smaller amount might be deposited due to
2066 // the lp tokens adjustment, withdrawing by tokens
2067 // is generally preferred to withdrawing by amount.
2068 auto lpTokens = ammAlice.deposit(carol_, USD(1'000));
2069 ammAlice.withdraw(carol_, lpTokens, USD(0));
2070 lpTokens = ammAlice.deposit(carol_, STAmount(USD, 1, -6));
2071 ammAlice.withdraw(carol_, lpTokens, USD(0));
2072 lpTokens = ammAlice.deposit(carol_, XRPAmount(1));
2073 ammAlice.withdraw(carol_, lpTokens, XRPAmount(0));
2074 if (!env.enabled(fixAMMv1_3))
2075 {
2076 BEAST_EXPECT(
2077 ammAlice.expectBalances(XRP(10'000), USD(10'000), ammAlice.tokens()));
2078 }
2079 else
2080 {
2081 BEAST_EXPECT(ammAlice.expectBalances(
2082 XRPAmount(10'000'000'001), USD(10'000), ammAlice.tokens()));
2083 }
2084 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{0}));
2085 },
2086 std::nullopt,
2087 0,
2088 std::nullopt,
2089 {all, all - fixAMMv1_3});
2090
2091 // Single deposit by different accounts and then withdraw
2092 // in reverse.
2093 testAMM([&](AMM& ammAlice, Env&) {
2094 auto const carolTokens = ammAlice.deposit(carol_, USD(1'000));
2095 auto const aliceTokens = ammAlice.deposit(alice_, USD(1'000));
2096 ammAlice.withdraw(alice_, aliceTokens, USD(0));
2097 ammAlice.withdraw(carol_, carolTokens, USD(0));
2098 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), USD(10'000), ammAlice.tokens()));
2099 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{0}));
2100 BEAST_EXPECT(ammAlice.expectLPTokens(alice_, ammAlice.tokens()));
2101 });
2102
2103 // Equal deposit 10%, withdraw all tokens
2104 testAMM([&](AMM& ammAlice, Env&) {
2105 ammAlice.deposit(carol_, 1'000'000);
2106 ammAlice.withdrawAll(carol_);
2107 BEAST_EXPECT(
2108 ammAlice.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0}));
2109 });
2110
2111 // Equal deposit 10%, withdraw all tokens in USD
2112 testAMM([&](AMM& ammAlice, Env&) {
2113 ammAlice.deposit(carol_, 1'000'000);
2114 ammAlice.withdrawAll(carol_, USD(0));
2115 BEAST_EXPECT(ammAlice.expectBalances(
2116 XRP(11'000),
2117 STAmount{USD, UINT64_C(9'090'909090909092), -12},
2118 IOUAmount{10'000'000, 0}));
2119 });
2120
2121 // Equal deposit 10%, withdraw all tokens in XRP
2122 testAMM([&](AMM& ammAlice, Env&) {
2123 ammAlice.deposit(carol_, 1'000'000);
2124 ammAlice.withdrawAll(carol_, XRP(0));
2125 BEAST_EXPECT(ammAlice.expectBalances(
2126 XRPAmount(9'090'909'091), USD(11'000), IOUAmount{10'000'000, 0}));
2127 });
2128
2129 // Withdraw with EPrice limit.
2130 testAMM(
2131 [&](AMM& ammAlice, Env& env) {
2132 ammAlice.deposit(carol_, 1'000'000);
2133 ammAlice.withdraw(carol_, USD(100), std::nullopt, IOUAmount{520, 0});
2134 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{153'846'15384616, -8}));
2135 if (!env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3))
2136 {
2137 BEAST_EXPECT(ammAlice.expectBalances(
2138 XRPAmount(11'000'000'000),
2139 STAmount{USD, UINT64_C(9'372'781065088757), -12},
2140 IOUAmount{10'153'846'15384616, -8}));
2141 }
2142 else if (env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3))
2143 {
2144 BEAST_EXPECT(ammAlice.expectBalances(
2145 XRPAmount(11'000'000'000),
2146 STAmount{USD, UINT64_C(9'372'781065088769), -12},
2147 IOUAmount{10'153'846'15384616, -8}));
2148 }
2149 else if (env.enabled(fixAMMv1_3))
2150 {
2151 BEAST_EXPECT(ammAlice.expectBalances(
2152 XRPAmount(11'000'000'000),
2153 STAmount{USD, UINT64_C(9'372'78106508877), -11},
2154 IOUAmount{10'153'846'15384616, -8}));
2155 }
2156 ammAlice.withdrawAll(carol_);
2157 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{0}));
2158 },
2159 {.features = {all, all - fixAMMv1_3, all - fixAMMv1_1 - fixAMMv1_3}, .noLog = true});
2160
2161 // Withdraw with EPrice limit. AssetOut is 0.
2162 testAMM(
2163 [&](AMM& ammAlice, Env& env) {
2164 ammAlice.deposit(carol_, 1'000'000);
2165 ammAlice.withdraw(carol_, USD(0), std::nullopt, IOUAmount{520, 0});
2166 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{153'846'15384616, -8}));
2167 if (!env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3))
2168 {
2169 BEAST_EXPECT(ammAlice.expectBalances(
2170 XRP(11'000),
2171 STAmount{USD, UINT64_C(9'372'781065088757), -12},
2172 IOUAmount{10'153'846'15384616, -8}));
2173 }
2174 else if (env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3))
2175 {
2176 BEAST_EXPECT(ammAlice.expectBalances(
2177 XRP(11'000),
2178 STAmount{USD, UINT64_C(9'372'781065088769), -12},
2179 IOUAmount{10'153'846'15384616, -8}));
2180 }
2181 else if (env.enabled(fixAMMv1_3))
2182 {
2183 BEAST_EXPECT(ammAlice.expectBalances(
2184 XRP(11'000),
2185 STAmount{USD, UINT64_C(9'372'78106508877), -11},
2186 IOUAmount{10'153'846'15384616, -8}));
2187 }
2188 },
2189 std::nullopt,
2190 0,
2191 std::nullopt,
2192 {all, all - fixAMMv1_3, all - fixAMMv1_1 - fixAMMv1_3});
2193
2194 // IOU to IOU + transfer fee
2195 {
2196 Env env{*this};
2197 fund(env, gw_, {alice_}, {USD(20'000), BTC(0.5)}, Fund::All);
2198 env(rate(gw_, 1.25));
2199 env.close();
2200 // no transfer fee on create
2201 AMM ammAlice(env, alice_, USD(20'000), BTC(0.5));
2202 BEAST_EXPECT(ammAlice.expectBalances(USD(20'000), BTC(0.5), IOUAmount{100, 0}));
2203 BEAST_EXPECT(expectHolding(env, alice_, USD(0)));
2204 BEAST_EXPECT(expectHolding(env, alice_, BTC(0)));
2205 fund(env, gw_, {carol_}, {USD(2'000), BTC(0.05)}, Fund::Acct);
2206 // no transfer fee on deposit
2207 ammAlice.deposit(carol_, 10);
2208 BEAST_EXPECT(ammAlice.expectBalances(USD(22'000), BTC(0.55), IOUAmount{110, 0}));
2209 BEAST_EXPECT(expectHolding(env, carol_, USD(0)));
2210 BEAST_EXPECT(expectHolding(env, carol_, BTC(0)));
2211 // no transfer fee on withdraw
2212 ammAlice.withdraw(carol_, 10);
2213 BEAST_EXPECT(ammAlice.expectBalances(USD(20'000), BTC(0.5), IOUAmount{100, 0}));
2214 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{0, 0}));
2215 BEAST_EXPECT(expectHolding(env, carol_, USD(2'000)));
2216 BEAST_EXPECT(expectHolding(env, carol_, BTC(0.05)));
2217 }
2218
2219 // Tiny withdraw
2220 testAMM([&](AMM& ammAlice, Env&) {
2221 // By tokens
2222 ammAlice.withdraw(alice_, IOUAmount{1, -3});
2223 BEAST_EXPECT(ammAlice.expectBalances(
2224 XRPAmount{9'999'999'999},
2225 STAmount{USD, UINT64_C(9'999'999999), -6},
2226 IOUAmount{9'999'999'999, -3}));
2227 });
2228 testAMM(
2229 [&](AMM& ammAlice, Env& env) {
2230 // Single XRP pool
2231 ammAlice.withdraw(alice_, std::nullopt, XRPAmount{1});
2232 if (!env.enabled(fixAMMv1_3))
2233 {
2234 BEAST_EXPECT(ammAlice.expectBalances(
2235 XRPAmount{9'999'999'999}, USD(10'000), IOUAmount{9'999'999'9995, -4}));
2236 }
2237 else
2238 {
2239 BEAST_EXPECT(ammAlice.expectBalances(
2240 XRP(10'000), USD(10'000), IOUAmount{9'999'999'9995, -4}));
2241 }
2242 },
2243 std::nullopt,
2244 0,
2245 std::nullopt,
2246 {all, all - fixAMMv1_3});
2247 testAMM([&](AMM& ammAlice, Env&) {
2248 // Single USD pool
2249 ammAlice.withdraw(alice_, std::nullopt, STAmount{USD, 1, -10});
2250 BEAST_EXPECT(ammAlice.expectBalances(
2251 XRP(10'000),
2252 STAmount{USD, UINT64_C(9'999'9999999999), -10},
2253 IOUAmount{9'999'999'99999995, -8}));
2254 });
2255
2256 // Withdraw close to entire pool
2257 // Equal by tokens
2258 testAMM([&](AMM& ammAlice, Env&) {
2259 ammAlice.withdraw(alice_, IOUAmount{9'999'999'999, -3});
2260 BEAST_EXPECT(
2261 ammAlice.expectBalances(XRPAmount{1}, STAmount{USD, 1, -6}, IOUAmount{1, -3}));
2262 });
2263 // USD by tokens
2264 testAMM([&](AMM& ammAlice, Env&) {
2265 ammAlice.withdraw(alice_, IOUAmount{9'999'999}, USD(0));
2266 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), STAmount{USD, 1, -10}, IOUAmount{1}));
2267 });
2268 // XRP by tokens
2269 testAMM([&](AMM& ammAlice, Env&) {
2270 ammAlice.withdraw(alice_, IOUAmount{9'999'900}, XRP(0));
2271 BEAST_EXPECT(ammAlice.expectBalances(XRPAmount{1}, USD(10'000), IOUAmount{100}));
2272 });
2273 // USD
2274 testAMM([&](AMM& ammAlice, Env&) {
2275 ammAlice.withdraw(alice_, STAmount{USD, UINT64_C(9'999'99999999999), -11});
2276 BEAST_EXPECT(ammAlice.expectBalances(
2277 XRP(10000), STAmount{USD, 1, -11}, IOUAmount{316227765, -9}));
2278 });
2279 // XRP
2280 testAMM([&](AMM& ammAlice, Env&) {
2281 ammAlice.withdraw(alice_, XRPAmount{9'999'999'999});
2282 BEAST_EXPECT(ammAlice.expectBalances(XRPAmount{1}, USD(10'000), IOUAmount{100}));
2283 });
2284
2285 // singleWithdrawEPrice: crafted ePrice = lptAMMBalance*f/amountBalance
2286 // makes the denominator (T*f - A*E) exactly zero.
2287 // Pre-fixCleanup3_3_0: std::overflow_error escapes to the
2288 // transactor backstop and is returned as tefEXCEPTION.
2289 // Post-fixCleanup3_3_0: denominator check returns tecAMM_FAILED.
2290 //
2291 // Pool: USD(100)/EUR(100), baseFee=1000 (1%).
2292 // Alice is the creator so her discounted fee is 100 (0.1%), f=0.001.
2293 // ePrice = lptAMMBalance(100) * f(0.001) / amountBalance(100) = 0.001
2294 testAMM(
2295 [&](AMM& ammAlice, Env& env) {
2296 auto const err = env.enabled(fixCleanup3_3_0) || env.enabled(fixCleanup3_4_0)
2298 : Ter(tefEXCEPTION);
2299 ammAlice.withdraw(
2301 .account = alice_,
2302 .asset1Out = USD(0),
2303 .maxEP = IOUAmount{1, -3}, // ePrice=0.001 → denom=0
2304 .err = err});
2305 },
2306 {{USD(100), EUR(100)}},
2307 1000,
2308 std::nullopt,
2309 {all - fixCleanup3_3_0 - fixCleanup3_4_0, all - fixCleanup3_4_0, all});
2310 }
2311
2312 void
2314 {
2315 testcase("Invalid Fee Vote");
2316 using namespace jtx;
2317
2318 testAMM([&](AMM& ammAlice, Env& env) {
2319 // Invalid flags
2320 ammAlice.vote(
2321 std::nullopt,
2322 1'000,
2323 tfWithdrawAll,
2324 std::nullopt,
2325 std::nullopt,
2327
2328 // Invalid fee.
2329 ammAlice.vote(
2330 std::nullopt, 1'001, std::nullopt, std::nullopt, std::nullopt, Ter(temBAD_FEE));
2331 BEAST_EXPECT(ammAlice.expectTradingFee(0));
2332
2333 // Invalid Account
2334 Account const bad("bad");
2335 env.memoize(bad);
2336 ammAlice.vote(bad, 1'000, std::nullopt, Seq(1), std::nullopt, Ter(terNO_ACCOUNT));
2337
2338 // Invalid AMM
2339 ammAlice.vote(alice_, 1'000, std::nullopt, std::nullopt, {{USD, GBP}}, Ter(terNO_AMM));
2340
2341 // Account is not LP
2342 ammAlice.vote(
2343 carol_,
2344 1'000,
2345 std::nullopt,
2346 std::nullopt,
2347 std::nullopt,
2349 });
2350
2351 // Invalid AMM
2352 testAMM([&](AMM& ammAlice, Env& env) {
2353 ammAlice.withdrawAll(alice_);
2354 ammAlice.vote(alice_, 1'000, std::nullopt, std::nullopt, std::nullopt, Ter(terNO_AMM));
2355 });
2356 }
2357
2358 void
2360 {
2361 testcase("Fee Vote");
2362 auto const all = testableAmendments();
2363 using namespace jtx;
2364
2365 // One vote sets fee to 1%.
2366 testAMM([&](AMM& ammAlice, Env& env) {
2367 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{0}));
2368 ammAlice.vote({}, 1'000);
2369 BEAST_EXPECT(ammAlice.expectTradingFee(1'000));
2370 // Discounted fee is 1/10 of trading fee.
2371 BEAST_EXPECT(ammAlice.expectAuctionSlot(100, 0, IOUAmount{0}));
2372 });
2373
2374 auto vote = [&](AMM& ammAlice,
2375 Env& env,
2376 int i,
2377 int fundUSD = 100'000,
2378 std::uint32_t tokens = 10'000'000,
2379 std::vector<Account>* accounts = nullptr) {
2380 Account a(std::to_string(i));
2381 // post-amendment the amount to deposit is slightly higher
2382 // in order to ensure AMM invariant sqrt(asset1 * asset2) >= tokens
2383 // fund just one USD higher in this case, which is enough for
2384 // deposit to succeed
2385 if (env.enabled(fixAMMv1_3))
2386 ++fundUSD;
2387 fund(env, gw_, {a}, {USD(fundUSD)}, Fund::Acct);
2388 ammAlice.deposit(a, tokens);
2389 ammAlice.vote(a, 50 * (i + 1));
2390 if (accounts)
2391 accounts->push_back(std::move(a));
2392 };
2393
2394 // Eight votes fill all voting slots, set fee 0.175%.
2395 testAMM(
2396 [&](AMM& ammAlice, Env& env) {
2397 for (int i = 0; i < 7; ++i)
2398 vote(ammAlice, env, i, 10'000);
2399 BEAST_EXPECT(ammAlice.expectTradingFee(175));
2400 },
2401 std::nullopt,
2402 0,
2403 std::nullopt,
2404 {all});
2405
2406 // Eight votes fill all voting slots, set fee 0.175%.
2407 // New vote, same account, sets fee 0.225%
2408 testAMM([&](AMM& ammAlice, Env& env) {
2409 for (int i = 0; i < 7; ++i)
2410 vote(ammAlice, env, i);
2411 BEAST_EXPECT(ammAlice.expectTradingFee(175));
2412 Account const a("0");
2413 ammAlice.vote(a, 450);
2414 BEAST_EXPECT(ammAlice.expectTradingFee(225));
2415 });
2416
2417 // Eight votes fill all voting slots, set fee 0.175%.
2418 // New vote, new account, higher vote weight, set higher fee 0.244%
2419 testAMM([&](AMM& ammAlice, Env& env) {
2420 for (int i = 0; i < 7; ++i)
2421 vote(ammAlice, env, i);
2422 BEAST_EXPECT(ammAlice.expectTradingFee(175));
2423 vote(ammAlice, env, 7, 100'000, 20'000'000);
2424 BEAST_EXPECT(ammAlice.expectTradingFee(244));
2425 });
2426
2427 // Eight votes fill all voting slots, set fee 0.219%.
2428 // New vote, new account, higher vote weight, set smaller fee 0.206%
2429 testAMM([&](AMM& ammAlice, Env& env) {
2430 for (int i = 7; i > 0; --i)
2431 vote(ammAlice, env, i);
2432 BEAST_EXPECT(ammAlice.expectTradingFee(219));
2433 vote(ammAlice, env, 0, 100'000, 20'000'000);
2434 BEAST_EXPECT(ammAlice.expectTradingFee(206));
2435 });
2436
2437 // Eight votes fill all voting slots. The accounts then withdraw all
2438 // tokens. An account sets a new fee and the previous slots are
2439 // deleted.
2440 testAMM([&](AMM& ammAlice, Env& env) {
2441 std::vector<Account> accounts;
2442 for (int i = 0; i < 7; ++i)
2443 vote(ammAlice, env, i, 100'000, 10'000'000, &accounts);
2444 BEAST_EXPECT(ammAlice.expectTradingFee(175));
2445 for (int i = 0; i < 7; ++i)
2446 ammAlice.withdrawAll(accounts[i]);
2447 ammAlice.deposit(carol_, 10'000'000);
2448 ammAlice.vote(carol_, 1'000);
2449 // The initial LP set the fee to 1000. Carol gets 50% voting
2450 // power, and the new fee is 500.
2451 BEAST_EXPECT(ammAlice.expectTradingFee(500));
2452 });
2453
2454 // Eight votes fill all voting slots. The accounts then withdraw some
2455 // tokens. The new vote doesn't get the voting power but
2456 // the slots are refreshed and the fee is updated.
2457 testAMM([&](AMM& ammAlice, Env& env) {
2458 std::vector<Account> accounts;
2459 for (int i = 0; i < 7; ++i)
2460 vote(ammAlice, env, i, 100'000, 10'000'000, &accounts);
2461 BEAST_EXPECT(ammAlice.expectTradingFee(175));
2462 for (int i = 0; i < 7; ++i)
2463 ammAlice.withdraw(accounts[i], 9'000'000);
2464 ammAlice.deposit(carol_, 1'000);
2465 // The vote is not added to the slots
2466 ammAlice.vote(carol_, 1'000);
2467 auto const info = ammAlice.ammRpcInfo()[jss::amm][jss::vote_slots];
2468 for (auto const& entry : info)
2469 BEAST_EXPECT(entry[jss::account] != carol_.human());
2470 // But the slots are refreshed and the fee is changed
2471 BEAST_EXPECT(ammAlice.expectTradingFee(82));
2472 });
2473 }
2474
2475 void
2477 {
2478 testcase("Invalid Bid");
2479 using namespace jtx;
2480 using namespace std::chrono;
2481
2482 // burn all the LPTokens through a AMMBid transaction
2483 {
2484 Env env(*this);
2485 fund(env, gw_, {alice_}, XRP(2'000), {USD(2'000)});
2486 AMM amm(env, gw_, XRP(1'000), USD(1'000), false, 1'000);
2487
2488 // auction slot is owned by the creator of the AMM i.e. gw_
2489 BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{0}));
2490
2491 // gw_ attempts to burn all her LPTokens through a bid transaction
2492 // this transaction fails because AMMBid transaction can not burn
2493 // all the outstanding LPTokens
2494 env(amm.bid({
2495 .account = gw_,
2496 .bidMin = 1'000'000,
2497 }),
2499 }
2500
2501 // burn all the LPTokens through a AMMBid transaction
2502 {
2503 Env env(*this);
2504 fund(env, gw_, {alice_}, XRP(2'000), {USD(2'000)});
2505 AMM amm(env, gw_, XRP(1'000), USD(1'000), false, 1'000);
2506
2507 // auction slot is owned by the creator of the AMM i.e. gw_
2508 BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{0}));
2509
2510 // gw_ burns all but one of its LPTokens through a bid transaction
2511 // this transaction succeeds because the bid price is less than
2512 // the total outstanding LPToken balance
2513 env(amm.bid({
2514 .account = gw_,
2515 .bidMin = STAmount{amm.lptIssue(), UINT64_C(999'999)},
2516 }),
2517 Ter(tesSUCCESS))
2518 .close();
2519
2520 // gw_ must own the auction slot
2521 BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{999'999}));
2522
2523 // 999'999 tokens are burned, only 1 LPToken is owned by gw_
2524 BEAST_EXPECT(amm.expectBalances(XRP(1'000), USD(1'000), IOUAmount{1}));
2525
2526 // gw_ owns only 1 LPToken in its balance
2527 BEAST_EXPECT(Number{amm.getLPTokensBalance(gw_)} == 1);
2528
2529 // gw_ attempts to burn the last of its LPTokens in an AMMBid
2530 // transaction. This transaction fails because it would burn all
2531 // the remaining LPTokens
2532 env(amm.bid({
2533 .account = gw_,
2534 .bidMin = 1,
2535 }),
2537 }
2538
2539 testAMM([&](AMM& ammAlice, Env& env) {
2540 // Invalid flags
2541 env(ammAlice.bid({
2542 .account = carol_,
2543 .bidMin = 0,
2544 .flags = tfWithdrawAll,
2545 }),
2547
2548 ammAlice.deposit(carol_, 1'000'000);
2549 // Invalid Bid price <= 0
2550 for (auto bid : {0, -100})
2551 {
2552 env(ammAlice.bid({
2553 .account = carol_,
2554 .bidMin = bid,
2555 }),
2556 Ter(temBAD_AMOUNT));
2557 env(ammAlice.bid({
2558 .account = carol_,
2559 .bidMax = bid,
2560 }),
2561 Ter(temBAD_AMOUNT));
2562 }
2563
2564 // Invalid Min/Max combination
2565 env(ammAlice.bid({
2566 .account = carol_,
2567 .bidMin = 200,
2568 .bidMax = 100,
2569 }),
2571
2572 // Invalid Account
2573 Account const bad("bad");
2574 env.memoize(bad);
2575 env(ammAlice.bid({
2576 .account = bad,
2577 .bidMax = 100,
2578 }),
2579 Seq(1),
2580 Ter(terNO_ACCOUNT));
2581
2582 // Account is not LP
2583 Account const dan("dan");
2584 env.fund(XRP(1'000), dan);
2585 env(ammAlice.bid({
2586 .account = dan,
2587 .bidMin = 100,
2588 }),
2590 env(ammAlice.bid({
2591 .account = dan,
2592 }),
2594
2595 // Auth account is invalid.
2596 env(ammAlice.bid({
2597 .account = carol_,
2598 .bidMin = 100,
2599 .authAccounts = {bob_},
2600 }),
2601 Ter(terNO_ACCOUNT));
2602
2603 // Invalid Assets
2604 env(ammAlice.bid({
2605 .account = alice_,
2606 .bidMax = 100,
2607 .assets = {{USD, GBP}},
2608 }),
2609 Ter(terNO_AMM));
2610
2611 // Invalid Min/Max issue
2612 env(ammAlice.bid({
2613 .account = alice_,
2614 .bidMax = STAmount{USD, 100},
2615 }),
2616 Ter(temBAD_AMM_TOKENS));
2617 env(ammAlice.bid({
2618 .account = alice_,
2619 .bidMin = STAmount{USD, 100},
2620 }),
2621 Ter(temBAD_AMM_TOKENS));
2622 });
2623
2624 // Invalid AMM
2625 testAMM([&](AMM& ammAlice, Env& env) {
2626 ammAlice.withdrawAll(alice_);
2627 env(ammAlice.bid({
2628 .account = alice_,
2629 .bidMax = 100,
2630 }),
2631 Ter(terNO_AMM));
2632 });
2633
2634 // More than four Auth accounts.
2635 testAMM([&](AMM& ammAlice, Env& env) {
2636 Account const ed("ed");
2637 Account const bill("bill");
2638 Account const scott("scott");
2639 Account const james("james");
2640 env.fund(XRP(1'000), bob_, ed, bill, scott, james);
2641 env.close();
2642 ammAlice.deposit(carol_, 1'000'000);
2643 env(ammAlice.bid({
2644 .account = carol_,
2645 .bidMin = 100,
2646 .authAccounts = {bob_, ed, bill, scott, james},
2647 }),
2648 Ter(temMALFORMED));
2649 });
2650
2651 // Bid price exceeds LP owned tokens
2652 testAMM([&](AMM& ammAlice, Env& env) {
2653 fund(env, gw_, {bob_}, XRP(1'000), {USD(100)}, Fund::Acct);
2654 ammAlice.deposit(carol_, 1'000'000);
2655 ammAlice.deposit(bob_, 10);
2656 env(ammAlice.bid({
2657 .account = carol_,
2658 .bidMin = 1'000'001,
2659 }),
2660 Ter(tecAMM_INVALID_TOKENS));
2661 env(ammAlice.bid({
2662 .account = carol_,
2663 .bidMax = 1'000'001,
2664 }),
2665 Ter(tecAMM_INVALID_TOKENS));
2666 env(ammAlice.bid({
2667 .account = carol_,
2668 .bidMin = 1'000,
2669 }));
2670 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{1'000}));
2671 // Slot purchase price is more than 1000 but bob_ only has 10 tokens
2672 env(ammAlice.bid({
2673 .account = bob_,
2674 }),
2675 Ter(tecAMM_INVALID_TOKENS));
2676 });
2677
2678 // Bid all tokens, still own the slot
2679 {
2680 Env env(*this);
2681 fund(env, gw_, {alice_, bob_}, XRP(1'000), {USD(1'000)});
2682 AMM amm(env, gw_, XRP(10), USD(1'000));
2683 auto const lpIssue = amm.lptIssue();
2684 env.trust(STAmount{lpIssue, 100}, alice_);
2685 env.trust(STAmount{lpIssue, 50}, bob_);
2686 env(pay(gw_, alice_, STAmount{lpIssue, 100}));
2687 env(pay(gw_, bob_, STAmount{lpIssue, 50}));
2688 env(amm.bid({.account = alice_, .bidMin = 100}));
2689 // Alice doesn't have any more tokens, but
2690 // she still owns the slot.
2691 env(amm.bid({
2692 .account = bob_,
2693 .bidMax = 50,
2694 }),
2695 Ter(tecAMM_FAILED));
2696 }
2697 }
2698
2699 void
2701 {
2702 testcase("Bid");
2703 using namespace jtx;
2704 using namespace std::chrono;
2705
2706 // Auction slot initially is owned by AMM creator, who pays 0 price.
2707
2708 // Bid 110 tokens. Pay bidMin.
2709 testAMM(
2710 [&](AMM& ammAlice, Env& env) {
2711 ammAlice.deposit(carol_, 1'000'000);
2712 env(ammAlice.bid({.account = carol_, .bidMin = 110}));
2713 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110}));
2714 // 110 tokens are burned.
2715 BEAST_EXPECT(
2716 ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{10'999'890, 0}));
2717 },
2718 std::nullopt,
2719 0,
2720 std::nullopt,
2721 {features});
2722
2723 // Bid with min/max when the pay price is less than min.
2724 testAMM(
2725 [&](AMM& ammAlice, Env& env) {
2726 ammAlice.deposit(carol_, 1'000'000);
2727 // Bid exactly 110. Pay 110 because the pay price is < 110.
2728 env(ammAlice.bid({.account = carol_, .bidMin = 110, .bidMax = 110}));
2729 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110}));
2730 BEAST_EXPECT(
2731 ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{10'999'890}));
2732 // Bid exactly 180-200. Pay 180 because the pay price is < 180.
2733 env(ammAlice.bid({.account = alice_, .bidMin = 180, .bidMax = 200}));
2734 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{180}));
2735 BEAST_EXPECT(
2736 ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{10'999'814'5, -1}));
2737 },
2738 std::nullopt,
2739 0,
2740 std::nullopt,
2741 {features});
2742
2743 // Start bid at bidMin 110.
2744 testAMM(
2745 [&](AMM& ammAlice, Env& env) {
2746 ammAlice.deposit(carol_, 1'000'000);
2747 // Bid, pay bidMin.
2748 env(ammAlice.bid({.account = carol_, .bidMin = 110}));
2749 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110}));
2750
2751 fund(env, gw_, {bob_}, {USD(10'000)}, Fund::Acct);
2752 ammAlice.deposit(bob_, 1'000'000);
2753 // Bid, pay the computed price.
2754 env(ammAlice.bid({.account = bob_}));
2755 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount(1155, -1)));
2756
2757 // Bid bidMax fails because the computed price is higher.
2758 env(ammAlice.bid({
2759 .account = carol_,
2760 .bidMax = 120,
2761 }),
2763 // Bid MaxSlotPrice succeeds - pay computed price
2764 env(ammAlice.bid({.account = carol_, .bidMax = 600}));
2765 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{121'275, -3}));
2766
2767 // Bid Min/MaxSlotPrice fails because the computed price is not
2768 // in range
2769 env(ammAlice.bid({
2770 .account = carol_,
2771 .bidMin = 10,
2772 .bidMax = 100,
2773 }),
2775 // Bid Min/MaxSlotPrice succeeds - pay computed price
2776 env(ammAlice.bid({.account = carol_, .bidMin = 100, .bidMax = 600}));
2777 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{127'33875, -5}));
2778 },
2779 std::nullopt,
2780 0,
2781 std::nullopt,
2782 {features});
2783
2784 // Slot states.
2785 testAMM(
2786 [&](AMM& ammAlice, Env& env) {
2787 ammAlice.deposit(carol_, 1'000'000);
2788
2789 fund(env, gw_, {bob_}, {USD(10'000)}, Fund::Acct);
2790 ammAlice.deposit(bob_, 1'000'000);
2791 if (!features[fixAMMv1_3])
2792 {
2793 BEAST_EXPECT(ammAlice.expectBalances(
2794 XRP(12'000), USD(12'000), IOUAmount{12'000'000, 0}));
2795 }
2796 else
2797 {
2798 BEAST_EXPECT(ammAlice.expectBalances(
2799 XRPAmount{12'000'000'001}, USD(12'000), IOUAmount{12'000'000, 0}));
2800 }
2801
2802 // Initial state. Pay bidMin.
2803 env(ammAlice.bid({.account = carol_, .bidMin = 110})).close();
2804 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110}));
2805
2806 // 1st Interval after close, price for 0th interval.
2807 env(ammAlice.bid({.account = bob_}));
2809 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 1, IOUAmount{1'155, -1}));
2810
2811 // 10th Interval after close, price for 1st interval.
2812 env(ammAlice.bid({.account = carol_}));
2814 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 10, IOUAmount{121'275, -3}));
2815
2816 // 20th Interval (expired) after close, price for 10th interval.
2817 env(ammAlice.bid({.account = bob_}));
2819 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, std::nullopt, IOUAmount{127'33875, -5}));
2820
2821 // 0 Interval.
2822 env(ammAlice.bid({.account = carol_, .bidMin = 110})).close();
2823 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, std::nullopt, IOUAmount{110}));
2824 // ~321.09 tokens burnt on bidding fees.
2825 if (!features[fixAMMv1_3])
2826 {
2827 BEAST_EXPECT(ammAlice.expectBalances(
2828 XRP(12'000), USD(12'000), IOUAmount{11'999'678'91, -2}));
2829 }
2830 else
2831 {
2832 BEAST_EXPECT(ammAlice.expectBalances(
2833 XRPAmount{12'000'000'001}, USD(12'000), IOUAmount{11'999'678'91, -2}));
2834 }
2835 },
2836 std::nullopt,
2837 0,
2838 std::nullopt,
2839 {features});
2840
2841 // Pool's fee 1%. Bid bidMin.
2842 // Auction slot owner and auth account trade at discounted fee -
2843 // 1/10 of the trading fee.
2844 // Other accounts trade at 1% fee.
2845 testAMM(
2846 [&](AMM& ammAlice, Env& env) {
2847 Account const dan("dan");
2848 Account const ed("ed");
2849 fund(env, gw_, {bob_, dan, ed}, {USD(20'000)}, Fund::Acct);
2850 ammAlice.deposit(bob_, 1'000'000);
2851 ammAlice.deposit(ed, 1'000'000);
2852 ammAlice.deposit(carol_, 500'000);
2853 ammAlice.deposit(dan, 500'000);
2854 auto ammTokens = ammAlice.getLPTokensBalance();
2855 env(ammAlice.bid({
2856 .account = carol_,
2857 .bidMin = 120,
2858 .authAccounts = {bob_, ed},
2859 }));
2860 auto const slotPrice = IOUAmount{5'200};
2861 ammTokens -= slotPrice;
2862 BEAST_EXPECT(ammAlice.expectAuctionSlot(100, 0, slotPrice));
2863 if (!features[fixAMMv1_3])
2864 {
2865 BEAST_EXPECT(ammAlice.expectBalances(XRP(13'000), USD(13'000), ammTokens));
2866 }
2867 else
2868 {
2869 BEAST_EXPECT(
2870 ammAlice.expectBalances(XRPAmount{13'000'000'003}, USD(13'000), ammTokens));
2871 }
2872 // Discounted trade
2873 for (int i = 0; i < 10; ++i)
2874 {
2875 auto tokens = ammAlice.deposit(carol_, USD(100));
2876 ammAlice.withdraw(carol_, tokens, USD(0));
2877 tokens = ammAlice.deposit(bob_, USD(100));
2878 ammAlice.withdraw(bob_, tokens, USD(0));
2879 tokens = ammAlice.deposit(ed, USD(100));
2880 ammAlice.withdraw(ed, tokens, USD(0));
2881 }
2882 // carol_, bob_, and ed pay ~0.99USD in fees.
2883 if (!features[fixAMMv1_1])
2884 {
2885 BEAST_EXPECT(
2886 env.balance(carol_, USD) ==
2887 STAmount(USD, UINT64_C(29'499'00572620545), -11));
2888 BEAST_EXPECT(
2889 env.balance(bob_, USD) == STAmount(USD, UINT64_C(18'999'00572616195), -11));
2890 BEAST_EXPECT(
2891 env.balance(ed, USD) == STAmount(USD, UINT64_C(18'999'00572611841), -11));
2892 // USD pool is slightly higher because of the fees.
2893 BEAST_EXPECT(ammAlice.expectBalances(
2894 XRP(13'000), STAmount(USD, UINT64_C(13'002'98282151419), -11), ammTokens));
2895 }
2896 else
2897 {
2898 BEAST_EXPECT(
2899 env.balance(carol_, USD) ==
2900 STAmount(USD, UINT64_C(29'499'00572620544), -11));
2901 BEAST_EXPECT(
2902 env.balance(bob_, USD) == STAmount(USD, UINT64_C(18'999'00572616194), -11));
2903 BEAST_EXPECT(
2904 env.balance(ed, USD) == STAmount(USD, UINT64_C(18'999'0057261184), -10));
2905 // USD pool is slightly higher because of the fees.
2906 if (!features[fixAMMv1_3])
2907 {
2908 BEAST_EXPECT(ammAlice.expectBalances(
2909 XRP(13'000),
2910 STAmount(USD, UINT64_C(13'002'98282151422), -11),
2911 ammTokens));
2912 }
2913 else
2914 {
2915 BEAST_EXPECT(ammAlice.expectBalances(
2916 XRPAmount{13'000'000'003},
2917 STAmount(USD, UINT64_C(13'002'98282151422), -11),
2918 ammTokens));
2919 }
2920 }
2921 ammTokens = ammAlice.getLPTokensBalance();
2922 // Trade with the fee
2923 for (int i = 0; i < 10; ++i)
2924 {
2925 auto const tokens = ammAlice.deposit(dan, USD(100));
2926 ammAlice.withdraw(dan, tokens, USD(0));
2927 }
2928 // dan pays ~9.94USD, which is ~10 times more in fees than
2929 // carol_, bob_, ed. the discounted fee is 10 times less
2930 // than the trading fee.
2931 if (!features[fixAMMv1_1])
2932 {
2933 BEAST_EXPECT(
2934 env.balance(dan, USD) == STAmount(USD, UINT64_C(19'490'056722744), -9));
2935 // USD pool gains more in dan's fees.
2936 BEAST_EXPECT(ammAlice.expectBalances(
2937 XRP(13'000), STAmount{USD, UINT64_C(13'012'92609877019), -11}, ammTokens));
2938 // Discounted fee payment
2939 ammAlice.deposit(carol_, USD(100));
2940 ammTokens = ammAlice.getLPTokensBalance();
2941 BEAST_EXPECT(ammAlice.expectBalances(
2942 XRP(13'000), STAmount{USD, UINT64_C(13'112'92609877019), -11}, ammTokens));
2943 env(pay(carol_, bob_, USD(100)), Path(~USD), Sendmax(XRP(110)));
2944 env.close();
2945 // carol_ pays 100000 drops in fees
2946 // 99900668XRP swapped in for 100USD
2947 BEAST_EXPECT(ammAlice.expectBalances(
2948 XRPAmount{13'100'000'668},
2949 STAmount{USD, UINT64_C(13'012'92609877019), -11},
2950 ammTokens));
2951 }
2952 else
2953 {
2954 if (!features[fixAMMv1_3])
2955 {
2956 BEAST_EXPECT(
2957 env.balance(dan, USD) ==
2958 STAmount(USD, UINT64_C(19'490'05672274399), -11));
2959 }
2960 else
2961 {
2962 BEAST_EXPECT(
2963 env.balance(dan, USD) ==
2964 STAmount(USD, UINT64_C(19'490'05672274398), -11));
2965 }
2966 // USD pool gains more in dan's fees.
2967 if (!features[fixAMMv1_3])
2968 {
2969 BEAST_EXPECT(ammAlice.expectBalances(
2970 XRP(13'000),
2971 STAmount{USD, UINT64_C(13'012'92609877023), -11},
2972 ammTokens));
2973 }
2974 else
2975 {
2976 BEAST_EXPECT(ammAlice.expectBalances(
2977 XRPAmount{13'000'000'003},
2978 STAmount{USD, UINT64_C(13'012'92609877024), -11},
2979 ammTokens));
2980 }
2981 // Discounted fee payment
2982 ammAlice.deposit(carol_, USD(100));
2983 ammTokens = ammAlice.getLPTokensBalance();
2984 if (!features[fixAMMv1_3])
2985 {
2986 BEAST_EXPECT(ammAlice.expectBalances(
2987 XRP(13'000),
2988 STAmount{USD, UINT64_C(13'112'92609877023), -11},
2989 ammTokens));
2990 }
2991 else
2992 {
2993 BEAST_EXPECT(ammAlice.expectBalances(
2994 XRPAmount{13'000'000'003},
2995 STAmount{USD, UINT64_C(13'112'92609877024), -11},
2996 ammTokens));
2997 }
2998 env(pay(carol_, bob_, USD(100)), Path(~USD), Sendmax(XRP(110)));
2999 env.close();
3000 // carol_ pays 100000 drops in fees
3001 // 99900668XRP swapped in for 100USD
3002 if (!features[fixAMMv1_3])
3003 {
3004 BEAST_EXPECT(ammAlice.expectBalances(
3005 XRPAmount{13'100'000'668},
3006 STAmount{USD, UINT64_C(13'012'92609877023), -11},
3007 ammTokens));
3008 }
3009 else
3010 {
3011 BEAST_EXPECT(ammAlice.expectBalances(
3012 XRPAmount{13'100'000'671},
3013 STAmount{USD, UINT64_C(13'012'92609877024), -11},
3014 ammTokens));
3015 }
3016 }
3017 // Payment with the trading fee
3018 env(pay(alice_, carol_, XRP(100)), Path(~XRP), Sendmax(USD(110)));
3019 env.close();
3020 // alice_ pays ~1.011USD in fees, which is ~10 times more
3021 // than carol_'s fee
3022 // 100.099431529USD swapped in for 100XRP
3023 if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
3024 {
3025 BEAST_EXPECT(ammAlice.expectBalances(
3026 XRPAmount{13'000'000'668},
3027 STAmount{USD, UINT64_C(13'114'03663047264), -11},
3028 ammTokens));
3029 }
3030 else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
3031 {
3032 BEAST_EXPECT(ammAlice.expectBalances(
3033 XRPAmount{13'000'000'668},
3034 STAmount{USD, UINT64_C(13'114'03663047269), -11},
3035 ammTokens));
3036 }
3037 else
3038 {
3039 BEAST_EXPECT(ammAlice.expectBalances(
3040 XRPAmount{13'000'000'671},
3041 STAmount{USD, UINT64_C(13'114'03663044937), -11},
3042 ammTokens));
3043 }
3044 // Auction slot expired, no discounted fee
3046 // clock is parent's based
3047 env.close();
3048 if (!features[fixAMMv1_1])
3049 {
3050 BEAST_EXPECT(
3051 env.balance(carol_, USD) ==
3052 STAmount(USD, UINT64_C(29'399'00572620545), -11));
3053 }
3054 else if (!features[fixAMMv1_3])
3055 {
3056 BEAST_EXPECT(
3057 env.balance(carol_, USD) ==
3058 STAmount(USD, UINT64_C(29'399'00572620544), -11));
3059 }
3060 ammTokens = ammAlice.getLPTokensBalance();
3061 for (int i = 0; i < 10; ++i)
3062 {
3063 auto const tokens = ammAlice.deposit(carol_, USD(100));
3064 ammAlice.withdraw(carol_, tokens, USD(0));
3065 }
3066 // carol_ pays ~9.94USD in fees, which is ~10 times more in
3067 // trading fees vs discounted fee.
3068 if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
3069 {
3070 BEAST_EXPECT(
3071 env.balance(carol_, USD) ==
3072 STAmount(USD, UINT64_C(29'389'06197177128), -11));
3073 BEAST_EXPECT(ammAlice.expectBalances(
3074 XRPAmount{13'000'000'668},
3075 STAmount{USD, UINT64_C(13'123'98038490681), -11},
3076 ammTokens));
3077 }
3078 else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
3079 {
3080 BEAST_EXPECT(
3081 env.balance(carol_, USD) ==
3082 STAmount(USD, UINT64_C(29'389'06197177124), -11));
3083 BEAST_EXPECT(ammAlice.expectBalances(
3084 XRPAmount{13'000'000'668},
3085 STAmount{USD, UINT64_C(13'123'98038490689), -11},
3086 ammTokens));
3087 }
3088 else
3089 {
3090 BEAST_EXPECT(
3091 env.balance(carol_, USD) ==
3092 STAmount(USD, UINT64_C(29'389'06197177129), -11));
3093 BEAST_EXPECT(ammAlice.expectBalances(
3094 XRPAmount{13'000'000'671},
3095 STAmount{USD, UINT64_C(13'123'98038488352), -11},
3096 ammTokens));
3097 }
3098 env(pay(carol_, bob_, USD(100)), Path(~USD), Sendmax(XRP(110)));
3099 env.close();
3100 // carol_ pays ~1.008XRP in trading fee, which is
3101 // ~10 times more than the discounted fee.
3102 // 99.815876XRP is swapped in for 100USD
3103 if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
3104 {
3105 BEAST_EXPECT(ammAlice.expectBalances(
3106 XRPAmount(13'100'824'790),
3107 STAmount{USD, UINT64_C(13'023'98038490681), -11},
3108 ammTokens));
3109 }
3110 else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
3111 {
3112 BEAST_EXPECT(ammAlice.expectBalances(
3113 XRPAmount(13'100'824'790),
3114 STAmount{USD, UINT64_C(13'023'98038490689), -11},
3115 ammTokens));
3116 }
3117 else
3118 {
3119 BEAST_EXPECT(ammAlice.expectBalances(
3120 XRPAmount(13'100'824'793),
3121 STAmount{USD, UINT64_C(13'023'98038488352), -11},
3122 ammTokens));
3123 }
3124 },
3125 std::nullopt,
3126 1'000,
3127 std::nullopt,
3128 {features});
3129
3130 // Bid tiny amount
3131 testAMM(
3132 [&](AMM& ammAlice, Env& env) {
3133 // Bid a tiny amount
3135 env(ammAlice.bid({.account = alice_, .bidMin = IOUAmount{tiny}}));
3136 // Auction slot purchase price is equal to the tiny amount
3137 // since the minSlotPrice is 0 with no trading fee.
3138 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{tiny}));
3139 // The purchase price is too small to affect the total tokens
3140 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), USD(10'000), ammAlice.tokens()));
3141 // Bid the tiny amount
3142 env(ammAlice.bid({
3143 .account = alice_,
3144 .bidMin = IOUAmount{STAmount::kMinValue, STAmount::kMinOffset},
3145 }));
3146 // Pay slightly higher price
3147 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{tiny * Number{105, -2}}));
3148 // The purchase price is still too small to affect the total
3149 // tokens
3150 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), USD(10'000), ammAlice.tokens()));
3151 },
3152 std::nullopt,
3153 0,
3154 std::nullopt,
3155 {features});
3156
3157 // Reset auth account
3158 testAMM(
3159 [&](AMM& ammAlice, Env& env) {
3160 env(ammAlice.bid({
3161 .account = alice_,
3162 .bidMin = IOUAmount{100},
3163 .authAccounts = {carol_},
3164 }));
3165 BEAST_EXPECT(ammAlice.expectAuctionSlot({carol_}));
3166 env(ammAlice.bid({.account = alice_, .bidMin = IOUAmount{100}}));
3167 BEAST_EXPECT(ammAlice.expectAuctionSlot({}));
3168 Account const bob("bob");
3169 Account const dan("dan");
3170 fund(env, {bob, dan}, XRP(1'000));
3171 env(ammAlice.bid({
3172 .account = alice_,
3173 .bidMin = IOUAmount{100},
3174 .authAccounts = {bob, dan},
3175 }));
3176 BEAST_EXPECT(ammAlice.expectAuctionSlot({bob, dan}));
3177 },
3178 std::nullopt,
3179 0,
3180 std::nullopt,
3181 {features});
3182
3183 // Bid all tokens, still own the slot and trade at a discount
3184 {
3185 Env env(*this, features);
3186 fund(env, gw_, {alice_, bob_}, XRP(2'000), {USD(2'000)});
3187 AMM amm(env, gw_, XRP(1'000), USD(1'010), false, 1'000);
3188 auto const lpIssue = amm.lptIssue();
3189 env.trust(STAmount{lpIssue, 500}, alice_);
3190 env.trust(STAmount{lpIssue, 50}, bob_);
3191 env(pay(gw_, alice_, STAmount{lpIssue, 500}));
3192 env(pay(gw_, bob_, STAmount{lpIssue, 50}));
3193 // Alice doesn't have anymore lp tokens
3194 env(amm.bid({.account = alice_, .bidMin = 500}));
3195 BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{500}));
3196 BEAST_EXPECT(expectHolding(env, alice_, STAmount{lpIssue, 0}));
3197 // But trades with the discounted fee since she still owns the slot.
3198 // Alice pays 10011 drops in fees
3199 env(pay(alice_, bob_, USD(10)), Path(~USD), Sendmax(XRP(11)));
3200 BEAST_EXPECT(amm.expectBalances(
3201 XRPAmount{1'010'010'011}, USD(1'000), IOUAmount{1'004'487'562112089, -9}));
3202 // Bob pays the full fee ~0.1USD
3203 env(pay(bob_, alice_, XRP(10)), Path(~XRP), Sendmax(USD(11)));
3204 if (!features[fixAMMv1_1])
3205 {
3206 BEAST_EXPECT(amm.expectBalances(
3207 XRPAmount{1'000'010'011},
3208 STAmount{USD, UINT64_C(1'010'10090898081), -11},
3209 IOUAmount{1'004'487'562112089, -9}));
3210 }
3211 else
3212 {
3213 BEAST_EXPECT(amm.expectBalances(
3214 XRPAmount{1'000'010'011},
3215 STAmount{USD, UINT64_C(1'010'100908980811), -12},
3216 IOUAmount{1'004'487'562112089, -9}));
3217 }
3218 }
3219
3220 // preflight tests
3221 {
3222 Env env(*this, features);
3223 auto const baseFee = env.current()->fees().base;
3224
3225 fund(env, gw_, {alice_, bob_}, XRP(2'000), {USD(2'000)});
3226 AMM amm(env, gw_, XRP(1'000), USD(1'010), false, 1'000);
3227 json::Value const tx = amm.bid({.account = alice_, .bidMin = 500});
3228
3229 {
3230 auto jtx = env.jt(tx, Seq(1), Fee(baseFee));
3231 env.app().config().features.erase(featureAMM);
3232 PreflightContext const pfCtx(
3233 env.app(), *jtx.stx, env.current()->rules(), TapNone, env.journal);
3234 auto pf = Transactor::invokePreflight<AMMBid>(pfCtx);
3235 BEAST_EXPECT(pf == temDISABLED);
3236 env.app().config().features.insert(featureAMM);
3237 }
3238
3239 {
3240 auto jtx = env.jt(tx, Seq(1), Fee(baseFee));
3241 jtx.jv["TxnSignature"] = "deadbeef";
3242 jtx.stx = env.ust(jtx);
3243 PreflightContext const pfCtx(
3244 env.app(), *jtx.stx, env.current()->rules(), TapNone, env.journal);
3245 auto pf = Transactor::invokePreflight<AMMBid>(pfCtx);
3246 BEAST_EXPECT(!isTesSuccess(pf));
3247 }
3248
3249 {
3250 auto jtx = env.jt(tx, Seq(1), Fee(baseFee));
3251 jtx.jv["Asset2"]["currency"] = "XRP";
3252 jtx.jv["Asset2"].removeMember("issuer");
3253 jtx.stx = env.ust(jtx);
3254 PreflightContext const pfCtx(
3255 env.app(), *jtx.stx, env.current()->rules(), TapNone, env.journal);
3256 auto pf = Transactor::invokePreflight<AMMBid>(pfCtx);
3257 BEAST_EXPECT(pf == temBAD_AMM_TOKENS);
3258 }
3259 }
3260 }
3261
3262 void
3264 {
3265 testcase("Invalid AMM Payment");
3266 using namespace jtx;
3267 using namespace std::chrono;
3268 using namespace std::literals::chrono_literals;
3269
3270 // Can't pay into AMM account.
3271 // Can't pay out since there is no keys
3272 for (auto const& acct : {gw_, alice_})
3273 {
3274 {
3275 Env env(*this);
3276 fund(env, gw_, {alice_, carol_}, XRP(1'000), {USD(100)});
3277 // XRP balance is below reserve
3278 AMM const ammAlice(env, acct, XRP(10), USD(10));
3279 // Pay below reserve
3280 env(pay(carol_, ammAlice.ammAccount(), XRP(10)), Ter(tecNO_PERMISSION));
3281 // Pay above reserve
3282 env(pay(carol_, ammAlice.ammAccount(), XRP(300)), Ter(tecNO_PERMISSION));
3283 // Pay IOU
3284 env(pay(carol_, ammAlice.ammAccount(), USD(10)), Ter(tecNO_PERMISSION));
3285 }
3286 {
3287 Env env(*this);
3288 fund(env, gw_, {alice_, carol_}, XRP(10'000'000), {USD(10'000)});
3289 // XRP balance is above reserve
3290 AMM const ammAlice(env, acct, XRP(1'000'000), USD(100));
3291 // Pay below reserve
3292 env(pay(carol_, ammAlice.ammAccount(), XRP(10)), Ter(tecNO_PERMISSION));
3293 // Pay above reserve
3294 env(pay(carol_, ammAlice.ammAccount(), XRP(1'000'000)), Ter(tecNO_PERMISSION));
3295 }
3296 }
3297
3298 // Can't pay into AMM with escrow.
3299 testAMM([&](AMM& ammAlice, Env& env) {
3300 auto const baseFee = env.current()->fees().base;
3301 env(escrow::create(carol_, ammAlice.ammAccount(), XRP(1)),
3303 escrow::kFinishTime(env.now() + 1s),
3304 escrow::kCancelTime(env.now() + 2s),
3305 Fee(baseFee * 150),
3307 });
3308
3309 // Can't pay into AMM with paychan.
3310 testAMM([&](AMM& ammAlice, Env& env) {
3311 auto const pk = carol_.pk();
3312 auto const settleDelay = 100s;
3313 NetClock::time_point const cancelAfter = env.current()->header().parentCloseTime + 200s;
3314 env(paychan::create(
3315 carol_, ammAlice.ammAccount(), XRP(1'000), settleDelay, pk, cancelAfter),
3317 });
3318
3319 // Can't pay into AMM with checks.
3320 testAMM([&](AMM& ammAlice, Env& env) {
3321 env(check::create(env.master.id(), ammAlice.ammAccount(), XRP(100)),
3323 });
3324
3325 // Pay amounts close to one side of the pool
3326 testAMM(
3327 [&](AMM& ammAlice, Env& env) {
3328 // Can't consume whole pool
3329 env(pay(alice_, carol_, USD(100)),
3330 Path(~USD),
3331 Sendmax(XRP(1'000'000'000)),
3333 env(pay(alice_, carol_, XRP(100)),
3334 Path(~XRP),
3335 Sendmax(USD(1'000'000'000)),
3337 // Overflow
3338 env(pay(alice_, carol_, STAmount{USD, UINT64_C(99'999999999), -9}),
3339 Path(~USD),
3340 Sendmax(XRP(1'000'000'000)),
3342 env(pay(alice_, carol_, STAmount{USD, UINT64_C(999'99999999), -8}),
3343 Path(~USD),
3344 Sendmax(XRP(1'000'000'000)),
3346 env(pay(alice_, carol_, STAmount{xrpIssue(), 99'999'999}),
3347 Path(~XRP),
3348 Sendmax(USD(1'000'000'000)),
3350 // Sender doesn't have enough funds
3351 env(pay(alice_, carol_, USD(99.99)),
3352 Path(~USD),
3353 Sendmax(XRP(1'000'000'000)),
3355 env(pay(alice_, carol_, STAmount{xrpIssue(), 99'990'000}),
3356 Path(~XRP),
3357 Sendmax(USD(1'000'000'000)),
3359 },
3360 {{XRP(100), USD(100)}});
3361
3362 // Globally frozen
3363 testAMM([&](AMM& ammAlice, Env& env) {
3364 env(fset(gw_, asfGlobalFreeze));
3365 env.close();
3366 env(pay(alice_, carol_, USD(1)),
3367 Path(~USD),
3368 Txflags(tfPartialPayment | tfNoRippleDirect),
3369 Sendmax(XRP(10)),
3370 Ter(tecPATH_DRY));
3371 env(pay(alice_, carol_, XRP(1)),
3372 Path(~XRP),
3373 Txflags(tfPartialPayment | tfNoRippleDirect),
3374 Sendmax(USD(10)),
3375 Ter(tecPATH_DRY));
3376 });
3377
3378 // Individually frozen AMM
3379 testAMM([&](AMM& ammAlice, Env& env) {
3380 env(trust(
3381 gw_, STAmount{Issue{gw_["USD"].currency, ammAlice.ammAccount()}, 0}, tfSetFreeze));
3382 env.close();
3383 env(pay(alice_, carol_, USD(1)),
3384 Path(~USD),
3385 Txflags(tfPartialPayment | tfNoRippleDirect),
3386 Sendmax(XRP(10)),
3387 Ter(tecPATH_DRY));
3388 env(pay(alice_, carol_, XRP(1)),
3389 Path(~XRP),
3390 Txflags(tfPartialPayment | tfNoRippleDirect),
3391 Sendmax(USD(10)),
3392 Ter(tecPATH_DRY));
3393 });
3394
3395 // Individually frozen accounts
3396 testAMM([&](AMM& ammAlice, Env& env) {
3397 env(trust(gw_, carol_["USD"](0), tfSetFreeze));
3398 env(trust(gw_, alice_["USD"](0), tfSetFreeze));
3399 env.close();
3400 env(pay(alice_, carol_, XRP(1)),
3401 Path(~XRP),
3402 Sendmax(USD(10)),
3403 Txflags(tfNoRippleDirect | tfPartialPayment),
3404 Ter(tecPATH_DRY));
3405 });
3406 }
3407
3408 void
3410 {
3411 testcase("Basic Payment");
3412 using namespace jtx;
3413
3414 // Payment 100USD for 100XRP.
3415 // Force one path with tfNoRippleDirect.
3416 testAMM(
3417 [&](AMM& ammAlice, Env& env) {
3418 env.fund(jtx::XRP(30'000), bob_);
3419 env.close();
3420 env(pay(bob_, carol_, USD(100)),
3421 Path(~USD),
3422 Sendmax(XRP(100)),
3423 Txflags(tfNoRippleDirect));
3424 env.close();
3425 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), USD(10'000), ammAlice.tokens()));
3426 // Initial balance 30,000 + 100
3427 BEAST_EXPECT(expectHolding(env, carol_, USD(30'100)));
3428 // Initial balance 30,000 - 100(sendmax) - 10(tx fee)
3429 BEAST_EXPECT(
3430 expectLedgerEntryRoot(env, bob_, XRP(30'000) - XRP(100) - txFee(env, 1)));
3431 },
3432 {{XRP(10'000), USD(10'100)}},
3433 0,
3434 std::nullopt,
3435 {features});
3436
3437 // Payment 100USD for 100XRP, use default path.
3438 testAMM(
3439 [&](AMM& ammAlice, Env& env) {
3440 env.fund(jtx::XRP(30'000), bob_);
3441 env.close();
3442 env(pay(bob_, carol_, USD(100)), Sendmax(XRP(100)));
3443 env.close();
3444 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), USD(10'000), ammAlice.tokens()));
3445 // Initial balance 30,000 + 100
3446 BEAST_EXPECT(expectHolding(env, carol_, USD(30'100)));
3447 // Initial balance 30,000 - 100(sendmax) - 10(tx fee)
3448 BEAST_EXPECT(
3449 expectLedgerEntryRoot(env, bob_, XRP(30'000) - XRP(100) - txFee(env, 1)));
3450 },
3451 {{XRP(10'000), USD(10'100)}},
3452 0,
3453 std::nullopt,
3454 {features});
3455
3456 // This payment is identical to above. While it has
3457 // both default path and path, activeStrands has one path.
3458 testAMM(
3459 [&](AMM& ammAlice, Env& env) {
3460 env.fund(jtx::XRP(30'000), bob_);
3461 env.close();
3462 env(pay(bob_, carol_, USD(100)), Path(~USD), Sendmax(XRP(100)));
3463 env.close();
3464 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), USD(10'000), ammAlice.tokens()));
3465 // Initial balance 30,000 + 100
3466 BEAST_EXPECT(expectHolding(env, carol_, USD(30'100)));
3467 // Initial balance 30,000 - 100(sendmax) - 10(tx fee)
3468 BEAST_EXPECT(
3469 expectLedgerEntryRoot(env, bob_, XRP(30'000) - XRP(100) - txFee(env, 1)));
3470 },
3471 {{XRP(10'000), USD(10'100)}},
3472 0,
3473 std::nullopt,
3474 {features});
3475
3476 // Payment with limitQuality set.
3477 testAMM(
3478 [&](AMM& ammAlice, Env& env) {
3479 env.fund(jtx::XRP(30'000), bob_);
3480 env.close();
3481 // Pays 10USD for 10XRP. A larger payment of ~99.11USD/100XRP
3482 // would have been sent has it not been for limitQuality.
3483 env(pay(bob_, carol_, USD(100)),
3484 Path(~USD),
3485 Sendmax(XRP(100)),
3486 Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
3487 env.close();
3488 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'010), USD(10'000), ammAlice.tokens()));
3489 // Initial balance 30,000 + 10(limited by limitQuality)
3490 BEAST_EXPECT(expectHolding(env, carol_, USD(30'010)));
3491 // Initial balance 30,000 - 10(limited by limitQuality) - 10(tx
3492 // fee)
3493 BEAST_EXPECT(
3494 expectLedgerEntryRoot(env, bob_, XRP(30'000) - XRP(10) - txFee(env, 1)));
3495
3496 // Fails because of limitQuality. Would have sent
3497 // ~98.91USD/110XRP has it not been for limitQuality.
3498 env(pay(bob_, carol_, USD(100)),
3499 Path(~USD),
3500 Sendmax(XRP(100)),
3501 Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality),
3502 Ter(tecPATH_DRY));
3503 env.close();
3504 },
3505 {{XRP(10'000), USD(10'010)}},
3506 0,
3507 std::nullopt,
3508 {features});
3509
3510 // Payment with limitQuality and transfer fee set.
3511 testAMM(
3512 [&](AMM& ammAlice, Env& env) {
3513 env(rate(gw_, 1.1));
3514 env.close();
3515 env.fund(jtx::XRP(30'000), bob_);
3516 env.close();
3517 // Pays 10USD for 10XRP. A larger payment of ~99.11USD/100XRP
3518 // would have been sent has it not been for limitQuality and
3519 // the transfer fee.
3520 env(pay(bob_, carol_, USD(100)),
3521 Path(~USD),
3522 Sendmax(XRP(110)),
3523 Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
3524 env.close();
3525 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'010), USD(10'000), ammAlice.tokens()));
3526 // 10USD - 10% transfer fee
3527 BEAST_EXPECT(
3528 expectHolding(env, carol_, STAmount{USD, UINT64_C(30'009'09090909091), -11}));
3529 BEAST_EXPECT(
3530 expectLedgerEntryRoot(env, bob_, XRP(30'000) - XRP(10) - txFee(env, 1)));
3531 },
3532 {{XRP(10'000), USD(10'010)}},
3533 0,
3534 std::nullopt,
3535 {features});
3536
3537 // Fail when partial payment is not set.
3538 testAMM(
3539 [&](AMM& ammAlice, Env& env) {
3540 env.fund(jtx::XRP(30'000), bob_);
3541 env.close();
3542 env(pay(bob_, carol_, USD(100)),
3543 Path(~USD),
3544 Sendmax(XRP(100)),
3545 Txflags(tfNoRippleDirect),
3547 },
3548 {{XRP(10'000), USD(10'000)}},
3549 0,
3550 std::nullopt,
3551 {features});
3552
3553 // Non-default path (with AMM) has a better quality than default path.
3554 // The max possible liquidity is taken out of non-default
3555 // path ~29.9XRP/29.9EUR, ~29.9EUR/~29.99USD. The rest
3556 // is taken from the offer.
3557 {
3558 Env env(*this, features);
3559 fund(env, gw_, {alice_, carol_}, {USD(30'000), EUR(30'000)}, Fund::All);
3560 env.close();
3561 env.fund(XRP(1'000), bob_);
3562 env.close();
3563 auto ammEurXrp = AMM(env, alice_, XRP(10'000), EUR(10'000));
3564 auto ammUsdEur = AMM(env, alice_, EUR(10'000), USD(10'000));
3565 env(offer(alice_, XRP(101), USD(100)), Txflags(tfPassive));
3566 env.close();
3567 env(pay(bob_, carol_, USD(100)),
3568 Path(~EUR, ~USD),
3569 Sendmax(XRP(102)),
3570 Txflags(tfPartialPayment));
3571 env.close();
3572 BEAST_EXPECT(ammEurXrp.expectBalances(
3573 XRPAmount(10'030'082'730),
3574 STAmount(EUR, UINT64_C(9'970'007498125468), -12),
3575 ammEurXrp.tokens()));
3576 if (!features[fixAMMv1_1])
3577 {
3578 BEAST_EXPECT(ammUsdEur.expectBalances(
3579 STAmount(USD, UINT64_C(9'970'097277662122), -12),
3580 STAmount(EUR, UINT64_C(10'029'99250187452), -11),
3581 ammUsdEur.tokens()));
3582
3583 // fixReducedOffersV2 changes the expected results slightly.
3584 Amounts const expectedAmounts = env.closed()->rules().enabled(fixReducedOffersV2)
3585 ? Amounts{XRPAmount(30'201'749), STAmount(USD, UINT64_C(29'90272233787816), -14)}
3586 : Amounts{
3587 XRPAmount(30'201'749), STAmount(USD, UINT64_C(29'90272233787818), -14)};
3588
3589 BEAST_EXPECT(expectOffers(env, alice_, 1, {{expectedAmounts}}));
3590 }
3591 else
3592 {
3593 BEAST_EXPECT(ammUsdEur.expectBalances(
3594 STAmount(USD, UINT64_C(9'970'097277662172), -12),
3595 STAmount(EUR, UINT64_C(10'029'99250187452), -11),
3596 ammUsdEur.tokens()));
3597
3598 // fixReducedOffersV2 changes the expected results slightly.
3599 Amounts const expectedAmounts = env.closed()->rules().enabled(fixReducedOffersV2)
3600 ? Amounts{XRPAmount(30'201'749), STAmount(USD, UINT64_C(29'90272233782839), -14)}
3601 : Amounts{
3602 XRPAmount(30'201'749), STAmount(USD, UINT64_C(29'90272233782840), -14)};
3603
3604 BEAST_EXPECT(expectOffers(env, alice_, 1, {{expectedAmounts}}));
3605 }
3606 // Initial 30,000 + 100
3607 BEAST_EXPECT(expectHolding(env, carol_, STAmount{USD, 30'100}));
3608 // Initial 1,000 - 30082730(AMM pool) - 70798251(offer) - 10(tx fee)
3609 BEAST_EXPECT(expectLedgerEntryRoot(
3610 env,
3611 bob_,
3612 XRP(1'000) - XRPAmount{30'082'730} - XRPAmount{70'798'251} - txFee(env, 1)));
3613 }
3614
3615 // Default path (with AMM) has a better quality than a non-default path.
3616 // The max possible liquidity is taken out of default
3617 // path ~49XRP/49USD. The rest is taken from the offer.
3618 testAMM(
3619 [&](AMM& ammAlice, Env& env) {
3620 env.fund(XRP(1'000), bob_);
3621 env.close();
3622 env.trust(EUR(2'000), alice_);
3623 env.close();
3624 env(pay(gw_, alice_, EUR(1'000)));
3625 env(offer(alice_, XRP(101), EUR(100)), Txflags(tfPassive));
3626 env.close();
3627 env(offer(alice_, EUR(100), USD(100)), Txflags(tfPassive));
3628 env.close();
3629 env(pay(bob_, carol_, USD(100)),
3630 Path(~EUR, ~USD),
3631 Sendmax(XRP(102)),
3632 Txflags(tfPartialPayment));
3633 env.close();
3634 BEAST_EXPECT(ammAlice.expectBalances(
3635 XRPAmount(10'050'238'637),
3636 STAmount(USD, UINT64_C(9'950'01249687578), -11),
3637 ammAlice.tokens()));
3638 BEAST_EXPECT(expectOffers(
3639 env,
3640 alice_,
3641 2,
3642 {{Amounts{XRPAmount(50'487'378), STAmount(EUR, UINT64_C(49'98750312422), -11)},
3643 Amounts{
3644 STAmount(EUR, UINT64_C(49'98750312422), -11),
3645 STAmount(USD, UINT64_C(49'98750312422), -11)}}}));
3646 // Initial 30,000 + 99.99999999999
3647 BEAST_EXPECT(
3648 expectHolding(env, carol_, STAmount{USD, UINT64_C(30'099'99999999999), -11}));
3649 // Initial 1,000 - 50238637(AMM pool) - 50512622(offer) - 10(tx
3650 // fee)
3651 BEAST_EXPECT(expectLedgerEntryRoot(
3652 env,
3653 bob_,
3654 XRP(1'000) - XRPAmount{50'238'637} - XRPAmount{50'512'622} - txFee(env, 1)));
3655 },
3656 std::nullopt,
3657 0,
3658 std::nullopt,
3659 {features});
3660
3661 // Default path with AMM and Order Book offer. AMM is consumed first,
3662 // remaining amount is consumed by the offer.
3663 testAMM(
3664 [&](AMM& ammAlice, Env& env) {
3665 fund(env, gw_, {bob_}, {USD(100)}, Fund::Acct);
3666 env.close();
3667 env(offer(bob_, XRP(100), USD(100)), Txflags(tfPassive));
3668 env.close();
3669 env(pay(alice_, carol_, USD(200)), Sendmax(XRP(200)), Txflags(tfPartialPayment));
3670 env.close();
3671 if (!features[fixAMMv1_1])
3672 {
3673 BEAST_EXPECT(
3674 ammAlice.expectBalances(XRP(10'100), USD(10'000), ammAlice.tokens()));
3675 // Initial 30,000 + 200
3676 BEAST_EXPECT(expectHolding(env, carol_, USD(30'200)));
3677 }
3678 else
3679 {
3680 BEAST_EXPECT(ammAlice.expectBalances(
3681 XRP(10'100),
3682 STAmount(USD, UINT64_C(10'000'00000000001), -11),
3683 ammAlice.tokens()));
3684 BEAST_EXPECT(expectHolding(
3685 env, carol_, STAmount(USD, UINT64_C(30'199'99999999999), -11)));
3686 }
3687 // Initial 30,000 - 10000(AMM pool LP) - 100(AMM offer) -
3688 // - 100(offer) - 10(tx fee) - one reserve
3689 BEAST_EXPECT(expectLedgerEntryRoot(
3690 env,
3691 alice_,
3692 XRP(30'000) - XRP(10'000) - XRP(100) - XRP(100) - ammCrtFee(env) -
3693 txFee(env, 1)));
3694 BEAST_EXPECT(expectOffers(env, bob_, 0));
3695 },
3696 {{XRP(10'000), USD(10'100)}},
3697 0,
3698 std::nullopt,
3699 {features});
3700
3701 // Default path with AMM and Order Book offer.
3702 // Order Book offer is consumed first.
3703 // Remaining amount is consumed by AMM.
3704 {
3705 Env env(*this, features);
3706 fund(env, gw_, {alice_, bob_, carol_}, XRP(20'000), {USD(2'000)});
3707 env.close();
3708 env(offer(bob_, XRP(50), USD(150)), Txflags(tfPassive));
3709 env.close();
3710 AMM const ammAlice(env, alice_, XRP(1'000), USD(1'050));
3711 env(pay(alice_, carol_, USD(200)), Sendmax(XRP(200)), Txflags(tfPartialPayment));
3712 env.close();
3713 BEAST_EXPECT(ammAlice.expectBalances(XRP(1'050), USD(1'000), ammAlice.tokens()));
3714 BEAST_EXPECT(expectHolding(env, carol_, USD(2'200)));
3715 BEAST_EXPECT(expectOffers(env, bob_, 0));
3716 }
3717
3718 // Offer crossing XRP/IOU
3719 testAMM(
3720 [&](AMM& ammAlice, Env& env) {
3721 fund(env, gw_, {bob_}, {USD(1'000)}, Fund::Acct);
3722 env.close();
3723 env(offer(bob_, USD(100), XRP(100)));
3724 env.close();
3725 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), USD(10'000), ammAlice.tokens()));
3726 // Initial 1,000 + 100
3727 BEAST_EXPECT(expectHolding(env, bob_, USD(1'100)));
3728 // Initial 30,000 - 100(offer) - 10(tx fee)
3729 BEAST_EXPECT(
3730 expectLedgerEntryRoot(env, bob_, XRP(30'000) - XRP(100) - txFee(env, 1)));
3731 BEAST_EXPECT(expectOffers(env, bob_, 0));
3732 },
3733 {{XRP(10'000), USD(10'100)}},
3734 0,
3735 std::nullopt,
3736 {features});
3737
3738 // Offer crossing IOU/IOU and transfer rate
3739 // Single path AMM offer
3740 testAMM(
3741 [&](AMM& ammAlice, Env& env) {
3742 env(rate(gw_, 1.25));
3743 env.close();
3744 // This offer succeeds to cross pre- and post-amendment
3745 // because the strand's out amount is small enough to match
3746 // limitQuality value and limitOut() function in StrandFlow
3747 // doesn't require an adjustment to out value.
3748 env(offer(carol_, EUR(100), GBP(100)));
3749 env.close();
3750 // No transfer fee
3751 BEAST_EXPECT(ammAlice.expectBalances(GBP(1'100), EUR(1'000), ammAlice.tokens()));
3752 // Initial 30,000 - 100(offer) - 25% transfer fee
3753 BEAST_EXPECT(expectHolding(env, carol_, GBP(29'875)));
3754 // Initial 30,000 + 100(offer)
3755 BEAST_EXPECT(expectHolding(env, carol_, EUR(30'100)));
3756 BEAST_EXPECT(expectOffers(env, bob_, 0));
3757 },
3758 {{GBP(1'000), EUR(1'100)}},
3759 0,
3760 std::nullopt,
3761 {features});
3762 // Single-path AMM offer
3763 testAMM(
3764 [&](AMM& amm, Env& env) {
3765 env(rate(gw_, 1.001));
3766 env.close();
3767 env(offer(carol_, XRP(100), USD(55)));
3768 env.close();
3769 if (!features[fixAMMv1_1])
3770 {
3771 // Pre-amendment the transfer fee is not taken into
3772 // account when calculating the limit out based on
3773 // limitQuality. Carol pays 0.1% on the takerGets, which
3774 // lowers the overall quality. AMM offer is generated based
3775 // on higher limit out, which generates a larger offer
3776 // with lower quality. Consequently, the offer fails
3777 // to cross.
3778 BEAST_EXPECT(amm.expectBalances(XRP(1'000), USD(500), amm.tokens()));
3779 BEAST_EXPECT(expectOffers(env, carol_, 1, {{Amounts{XRP(100), USD(55)}}}));
3780 }
3781 else if (!features[featureMPTokensV2])
3782 {
3783 BEAST_EXPECT(amm.expectBalances(
3784 XRPAmount(909'090'909),
3785 STAmount{USD, UINT64_C(550'000000055), -9},
3786 amm.tokens()));
3787 BEAST_EXPECT(expectOffers(
3788 env,
3789 carol_,
3790 1,
3791 {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}}));
3792 BEAST_EXPECT(
3793 env.balance(carol_, USD) ==
3794 STAmount(USD, UINT64_C(29'949'94999999494), -11));
3795 }
3796 else
3797 {
3798 // Post-amendment the transfer fee is taken into account
3799 // when calculating the limit out based on limitQuality.
3800 // This increases the limitQuality and decreases
3801 // the limit out. Consequently, AMM offer size is decreased,
3802 // and the quality is increased, matching the overall
3803 // quality.
3804 // AMM offer ~50USD/91XRP
3805 BEAST_EXPECT(amm.expectBalances(
3806 XRPAmount(909'090'910),
3807 STAmount{USD, UINT64_C(549'99999945), -8},
3808 amm.tokens()));
3809 // Offer ~91XRP/50USD
3810 BEAST_EXPECT(expectOffers(
3811 env,
3812 carol_,
3813 1,
3814 {{Amounts{XRPAmount{9'090'910}, STAmount{USD, 5'0000005, -7}}}}));
3815 // Carol pays 0.1% fee on ~50USD =~ 0.05USD
3816 BEAST_EXPECT(
3817 env.balance(carol_, USD) ==
3818 STAmount(USD, UINT64_C(29'949'95000060055), -11));
3819 }
3820 },
3821 {{XRP(1'000), USD(500)}},
3822 0,
3823 std::nullopt,
3824 {features});
3825 testAMM(
3826 [&](AMM& amm, Env& env) {
3827 env(rate(gw_, 1.001));
3828 env.close();
3829 env(offer(carol_, XRP(10), USD(5.5)));
3830 env.close();
3831 if (!features[fixAMMv1_1])
3832 {
3833 BEAST_EXPECT(amm.expectBalances(
3834 XRP(990), STAmount{USD, UINT64_C(505'050505050505), -12}, amm.tokens()));
3835 BEAST_EXPECT(expectOffers(env, carol_, 0));
3836 }
3837 else
3838 {
3839 BEAST_EXPECT(amm.expectBalances(
3840 XRP(990), STAmount{USD, UINT64_C(505'0505050505051), -13}, amm.tokens()));
3841 BEAST_EXPECT(expectOffers(env, carol_, 0));
3842 }
3843 },
3844 {{XRP(1'000), USD(500)}},
3845 0,
3846 std::nullopt,
3847 {features});
3848 // Multi-path AMM offer
3849 testAMM(
3850 [&](AMM& ammAlice, Env& env) {
3851 Account const ed("ed");
3852 fund(env, gw_, {bob_, ed}, XRP(30'000), {GBP(2'000), EUR(2'000)}, Fund::Acct);
3853 env(rate(gw_, 1.25));
3854 env.close();
3855 // The auto-bridge is worse quality than AMM, is not consumed
3856 // first and initially forces multi-path AMM offer generation.
3857 // Multi-path AMM offers are consumed until their quality
3858 // is less than the auto-bridge offers quality. Auto-bridge
3859 // offers are consumed afterward. Then the behavior is
3860 // different pre-amendment and post-amendment.
3861 env(offer(bob_, GBP(10), XRP(10)), Txflags(tfPassive));
3862 env(offer(ed, XRP(10), EUR(10)), Txflags(tfPassive));
3863 env.close();
3864 env(offer(carol_, EUR(100), GBP(100)));
3865 env.close();
3866 if (!features[fixAMMv1_1])
3867 {
3868 // After the auto-bridge offers are consumed, single path
3869 // AMM offer is generated with the limit out not taking
3870 // into consideration the transfer fee. This results
3871 // in an overall lower quality offer than the limit quality
3872 // and the single path AMM offer fails to consume.
3873 // Total consumed ~37.06GBP/39.32EUR
3874 BEAST_EXPECT(ammAlice.expectBalances(
3875 STAmount{GBP, UINT64_C(1'037'06583722133), -11},
3876 STAmount{EUR, UINT64_C(1'060'684828792831), -12},
3877 ammAlice.tokens()));
3878 // Consumed offer ~49.32EUR/49.32GBP
3879 BEAST_EXPECT(expectOffers(
3880 env,
3881 carol_,
3882 1,
3883 {Amounts{
3884 STAmount{EUR, UINT64_C(50'684828792831), -12},
3885 STAmount{GBP, UINT64_C(50'684828792831), -12}}}));
3886 BEAST_EXPECT(expectOffers(env, bob_, 0));
3887 BEAST_EXPECT(expectOffers(env, ed, 0));
3888
3889 // Initial 30,000 - ~47.06(offers = 37.06(AMM) + 10(LOB))
3890 // * 1.25
3891 // = 58.825 = ~29941.17
3892 // carol_ bought ~72.93EUR at the cost of ~70.68GBP
3893 // the offer is partially consumed
3894 BEAST_EXPECT(expectHolding(
3895 env, carol_, STAmount{GBP, UINT64_C(29'941'16770347333), -11}));
3896 // Initial 30,000 + ~49.3(offers = 39.3(AMM) + 10(LOB))
3897 BEAST_EXPECT(expectHolding(
3898 env, carol_, STAmount{EUR, UINT64_C(30'049'31517120716), -11}));
3899 }
3900 else
3901 {
3902 // After the auto-bridge offers are consumed, single path
3903 // AMM offer is generated with the limit out taking
3904 // into consideration the transfer fee. This results
3905 // in an overall quality offer matching the limit quality
3906 // and the single path AMM offer is consumed. More
3907 // liquidity is consumed overall in post-amendment.
3908 // Total consumed ~60.68GBP/62.93EUR
3909 BEAST_EXPECT(ammAlice.expectBalances(
3910 STAmount{GBP, UINT64_C(1'060'684828792832), -12},
3911 STAmount{EUR, UINT64_C(1'037'06583722134), -11},
3912 ammAlice.tokens()));
3913 // Consumed offer ~72.93EUR/72.93GBP
3914 BEAST_EXPECT(expectOffers(
3915 env,
3916 carol_,
3917 1,
3918 {Amounts{
3919 STAmount{EUR, UINT64_C(27'06583722134028), -14},
3920 STAmount{GBP, UINT64_C(27'06583722134028), -14}}}));
3921 BEAST_EXPECT(expectOffers(env, bob_, 0));
3922 BEAST_EXPECT(expectOffers(env, ed, 0));
3923
3924 // Initial 30,000 - ~70.68(offers = 60.68(AMM) + 10(LOB))
3925 // * 1.25
3926 // = 88.35 = ~29911.64
3927 // carol_ bought ~72.93EUR at the cost of ~70.68GBP
3928 // the offer is partially consumed
3929 BEAST_EXPECT(expectHolding(
3930 env, carol_, STAmount{GBP, UINT64_C(29'911'64396400896), -11}));
3931 // Initial 30,000 + ~72.93(offers = 62.93(AMM) + 10(LOB))
3932 BEAST_EXPECT(expectHolding(
3933 env, carol_, STAmount{EUR, UINT64_C(30'072'93416277865), -11}));
3934 }
3935 // Initial 2000 + 10 = 2010
3936 BEAST_EXPECT(expectHolding(env, bob_, GBP(2'010)));
3937 // Initial 2000 - 10 * 1.25 = 1987.5
3938 BEAST_EXPECT(expectHolding(env, ed, EUR(1'987.5)));
3939 },
3940 {{GBP(1'000), EUR(1'100)}},
3941 0,
3942 std::nullopt,
3943 {features});
3944
3945 // Payment and transfer fee
3946 // Scenario:
3947 // Bob sends 125GBP to pay 80EUR to Carol
3948 // Payment execution:
3949 // bob_'s 125GBP/1.25 = 100GBP
3950 // 100GBP/100EUR AMM offer
3951 // 100EUR/1.25 = 80EUR paid to carol_
3952 testAMM(
3953 [&](AMM& ammAlice, Env& env) {
3954 fund(env, gw_, {bob_}, {GBP(200), EUR(200)}, Fund::Acct);
3955 env(rate(gw_, 1.25));
3956 env.close();
3957 env(pay(bob_, carol_, EUR(100)),
3958 Path(~EUR),
3959 Sendmax(GBP(125)),
3960 Txflags(tfPartialPayment));
3961 env.close();
3962 BEAST_EXPECT(ammAlice.expectBalances(GBP(1'100), EUR(1'000), ammAlice.tokens()));
3963 BEAST_EXPECT(expectHolding(env, bob_, GBP(75)));
3964 BEAST_EXPECT(expectHolding(env, carol_, EUR(30'080)));
3965 },
3966 {{GBP(1'000), EUR(1'100)}},
3967 0,
3968 std::nullopt,
3969 {features});
3970
3971 // Payment and transfer fee, multiple steps
3972 // Scenario:
3973 // Dan's offer 200CAN/200GBP
3974 // AMM 1000GBP/10125EUR
3975 // Ed's offer 200EUR/200USD
3976 // Bob sends 195.3125CAN to pay 100USD to Carol
3977 // Payment execution:
3978 // bob_'s 195.3125CAN/1.25 = 156.25CAN -> dan's offer
3979 // 156.25CAN/156.25GBP 156.25GBP/1.25 = 125GBP -> AMM's offer
3980 // 125GBP/125EUR 125EUR/1.25 = 100EUR -> ed's offer
3981 // 100EUR/100USD 100USD/1.25 = 80USD paid to carol_
3982 testAMM(
3983 [&](AMM& ammAlice, Env& env) {
3984 Account const dan("dan");
3985 Account const ed("ed");
3986 auto const can = gw_["CAN"];
3987 fund(env, gw_, {dan}, {can(200), GBP(200)}, Fund::Acct);
3988 fund(env, gw_, {ed}, {EUR(200), USD(200)}, Fund::Acct);
3989 fund(env, gw_, {bob_}, {can(195.3125)}, Fund::Acct);
3990 env(trust(carol_, USD(100)));
3991 env(rate(gw_, 1.25));
3992 env.close();
3993 env(offer(dan, can(200), GBP(200)));
3994 env(offer(ed, EUR(200), USD(200)));
3995 env.close();
3996 env(pay(bob_, carol_, USD(100)),
3997 Path(~GBP, ~EUR, ~USD),
3998 Sendmax(can(195.3125)),
3999 Txflags(tfPartialPayment));
4000 env.close();
4001 BEAST_EXPECT(expectHolding(env, bob_, can(0)));
4002 BEAST_EXPECT(expectHolding(env, dan, can(356.25), GBP(43.75)));
4003 BEAST_EXPECT(ammAlice.expectBalances(GBP(10'125), EUR(10'000), ammAlice.tokens()));
4004 BEAST_EXPECT(expectHolding(env, ed, EUR(300), USD(100)));
4005 BEAST_EXPECT(expectHolding(env, carol_, USD(80)));
4006 },
4007 {{GBP(10'000), EUR(10'125)}},
4008 0,
4009 std::nullopt,
4010 {features});
4011
4012 // Pay amounts close to one side of the pool
4013 testAMM(
4014 [&](AMM& ammAlice, Env& env) {
4015 env(pay(alice_, carol_, USD(99.99)),
4016 Path(~USD),
4017 Sendmax(XRP(1)),
4018 Txflags(tfPartialPayment),
4019 Ter(tesSUCCESS));
4020 env(pay(alice_, carol_, USD(100)),
4021 Path(~USD),
4022 Sendmax(XRP(1)),
4023 Txflags(tfPartialPayment),
4024 Ter(tesSUCCESS));
4025 env(pay(alice_, carol_, XRP(100)),
4026 Path(~XRP),
4027 Sendmax(USD(1)),
4028 Txflags(tfPartialPayment),
4029 Ter(tesSUCCESS));
4030 env(pay(alice_, carol_, STAmount{xrpIssue(), 99'999'900}),
4031 Path(~XRP),
4032 Sendmax(USD(1)),
4033 Txflags(tfPartialPayment),
4034 Ter(tesSUCCESS));
4035 },
4036 {{XRP(100), USD(100)}},
4037 0,
4038 std::nullopt,
4039 {features});
4040
4041 // Multiple paths/steps
4042 {
4043 Env env(*this, features);
4044 auto const eth = gw_["ETH"];
4045 fund(
4046 env,
4047 gw_,
4048 {alice_},
4049 XRP(100'000),
4050 {EUR(50'000), BTC(50'000), eth(50'000), USD(50'000)});
4051 fund(env, gw_, {carol_, bob_}, XRP(1'000), {USD(200)}, Fund::Acct);
4052 AMM const xrpEur(env, alice_, XRP(10'100), EUR(10'000));
4053 AMM const eurBtc(env, alice_, EUR(10'000), BTC(10'200));
4054 AMM const btcUsd(env, alice_, BTC(10'100), USD(10'000));
4055 AMM const xrpUsd(env, alice_, XRP(10'150), USD(10'200));
4056 AMM const xrpEth(env, alice_, XRP(10'000), eth(10'100));
4057 AMM const ethEur(env, alice_, eth(10'900), EUR(11'000));
4058 AMM const eurUsd(env, alice_, EUR(10'100), USD(10'000));
4059 env(pay(bob_, carol_, USD(100)),
4060 Path(~EUR, ~BTC, ~USD),
4061 Path(~USD),
4062 Path(~eth, ~EUR, ~USD),
4063 Sendmax(XRP(200)));
4064 if (!features[fixAMMv1_1])
4065 {
4066 // XRP-ETH-EUR-USD
4067 // This path provides ~26.06USD/26.2XRP
4068 BEAST_EXPECT(xrpEth.expectBalances(
4069 XRPAmount(10'026'208'900),
4070 STAmount{eth, UINT64_C(10'073'65779244494), -11},
4071 xrpEth.tokens()));
4072 BEAST_EXPECT(ethEur.expectBalances(
4073 STAmount{eth, UINT64_C(10'926'34220755506), -11},
4074 STAmount{EUR, UINT64_C(10'973'54232078752), -11},
4075 ethEur.tokens()));
4076 BEAST_EXPECT(eurUsd.expectBalances(
4077 STAmount{EUR, UINT64_C(10'126'45767921248), -11},
4078 STAmount{USD, UINT64_C(9'973'93151712086), -11},
4079 eurUsd.tokens()));
4080 // XRP-USD path
4081 // This path provides ~73.9USD/74.1XRP
4082 BEAST_EXPECT(xrpUsd.expectBalances(
4083 XRPAmount(10'224'106'246),
4084 STAmount{USD, UINT64_C(10'126'06848287914), -11},
4085 xrpUsd.tokens()));
4086 }
4087 else
4088 {
4089 BEAST_EXPECT(xrpEth.expectBalances(
4090 XRPAmount(10'026'208'900),
4091 STAmount{eth, UINT64_C(10'073'65779244461), -11},
4092 xrpEth.tokens()));
4093 BEAST_EXPECT(ethEur.expectBalances(
4094 STAmount{eth, UINT64_C(10'926'34220755539), -11},
4095 STAmount{EUR, UINT64_C(10'973'5423207872), -10},
4096 ethEur.tokens()));
4097 BEAST_EXPECT(eurUsd.expectBalances(
4098 STAmount{EUR, UINT64_C(10'126'4576792128), -10},
4099 STAmount{USD, UINT64_C(9'973'93151712057), -11},
4100 eurUsd.tokens()));
4101 // XRP-USD path
4102 // This path provides ~73.9USD/74.1XRP
4103 BEAST_EXPECT(xrpUsd.expectBalances(
4104 XRPAmount(10'224'106'246),
4105 STAmount{USD, UINT64_C(10'126'06848287943), -11},
4106 xrpUsd.tokens()));
4107 }
4108
4109 // XRP-EUR-BTC-USD
4110 // This path doesn't provide any liquidity due to how
4111 // offers are generated in multi-path. Analytical solution
4112 // shows a different distribution:
4113 // XRP-EUR-BTC-USD 11.6USD/11.64XRP, XRP-USD 60.7USD/60.8XRP,
4114 // XRP-ETH-EUR-USD 27.6USD/27.6XRP
4115 BEAST_EXPECT(xrpEur.expectBalances(XRP(10'100), EUR(10'000), xrpEur.tokens()));
4116 BEAST_EXPECT(eurBtc.expectBalances(EUR(10'000), BTC(10'200), eurBtc.tokens()));
4117 BEAST_EXPECT(btcUsd.expectBalances(BTC(10'100), USD(10'000), btcUsd.tokens()));
4118
4119 BEAST_EXPECT(expectHolding(env, carol_, USD(300)));
4120 }
4121
4122 // Dependent AMM
4123 {
4124 Env env(*this, features);
4125 auto const eth = gw_["ETH"];
4126 fund(
4127 env,
4128 gw_,
4129 {alice_},
4130 XRP(40'000),
4131 {EUR(50'000), BTC(50'000), eth(50'000), USD(50'000)});
4132 fund(env, gw_, {carol_, bob_}, XRP(1000), {USD(200)}, Fund::Acct);
4133 AMM const xrpEur(env, alice_, XRP(10'100), EUR(10'000));
4134 AMM const eurBtc(env, alice_, EUR(10'000), BTC(10'200));
4135 AMM const btcUsd(env, alice_, BTC(10'100), USD(10'000));
4136 AMM const xrpEth(env, alice_, XRP(10'000), eth(10'100));
4137 AMM const ethEur(env, alice_, eth(10'900), EUR(11'000));
4138 env(pay(bob_, carol_, USD(100)),
4139 Path(~EUR, ~BTC, ~USD),
4140 Path(~eth, ~EUR, ~BTC, ~USD),
4141 Sendmax(XRP(200)));
4142 if (!features[fixAMMv1_1])
4143 {
4144 // XRP-EUR-BTC-USD path provides ~17.8USD/~18.7XRP
4145 // XRP-ETH-EUR-BTC-USD path provides ~82.2USD/82.4XRP
4146 BEAST_EXPECT(xrpEur.expectBalances(
4147 XRPAmount(10'118'738'472),
4148 STAmount{EUR, UINT64_C(9'981'544436337968), -12},
4149 xrpEur.tokens()));
4150 BEAST_EXPECT(eurBtc.expectBalances(
4151 STAmount{EUR, UINT64_C(10'101'16096785173), -11},
4152 STAmount{BTC, UINT64_C(10'097'91426968066), -11},
4153 eurBtc.tokens()));
4154 BEAST_EXPECT(btcUsd.expectBalances(
4155 STAmount{BTC, UINT64_C(10'202'08573031934), -11}, USD(9'900), btcUsd.tokens()));
4156 BEAST_EXPECT(xrpEth.expectBalances(
4157 XRPAmount(10'082'446'397),
4158 STAmount{eth, UINT64_C(10'017'41072778012), -11},
4159 xrpEth.tokens()));
4160 BEAST_EXPECT(ethEur.expectBalances(
4161 STAmount{eth, UINT64_C(10'982'58927221988), -11},
4162 STAmount{EUR, UINT64_C(10'917'2945958103), -10},
4163 ethEur.tokens()));
4164 }
4165 else
4166 {
4167 BEAST_EXPECT(xrpEur.expectBalances(
4168 XRPAmount(10'118'738'472),
4169 STAmount{EUR, UINT64_C(9'981'544436337923), -12},
4170 xrpEur.tokens()));
4171 BEAST_EXPECT(eurBtc.expectBalances(
4172 STAmount{EUR, UINT64_C(10'101'16096785188), -11},
4173 STAmount{BTC, UINT64_C(10'097'91426968059), -11},
4174 eurBtc.tokens()));
4175 BEAST_EXPECT(btcUsd.expectBalances(
4176 STAmount{BTC, UINT64_C(10'202'08573031941), -11}, USD(9'900), btcUsd.tokens()));
4177 BEAST_EXPECT(xrpEth.expectBalances(
4178 XRPAmount(10'082'446'397),
4179 STAmount{eth, UINT64_C(10'017'41072777996), -11},
4180 xrpEth.tokens()));
4181 BEAST_EXPECT(ethEur.expectBalances(
4182 STAmount{eth, UINT64_C(10'982'58927222004), -11},
4183 STAmount{EUR, UINT64_C(10'917'2945958102), -10},
4184 ethEur.tokens()));
4185 }
4186 BEAST_EXPECT(expectHolding(env, carol_, USD(300)));
4187 }
4188
4189 // AMM offers limit
4190 // Consuming 30 CLOB offers, results in hitting 30 AMM offers limit.
4191 testAMM(
4192 [&](AMM& ammAlice, Env& env) {
4193 env.fund(XRP(1'000), bob_);
4194 fund(env, gw_, {bob_}, {EUR(400)}, Fund::TokenOnly);
4195 env(trust(alice_, EUR(200)));
4196 for (int i = 0; i < 30; ++i)
4197 env(offer(alice_, EUR(1.0 + (0.01 * i)), XRP(1)));
4198 // This is worse quality offer than 30 offers above.
4199 // It will not be consumed because of AMM offers limit.
4200 env(offer(alice_, EUR(140), XRP(100)));
4201 env(pay(bob_, carol_, USD(100)),
4202 Path(~XRP, ~USD),
4203 Sendmax(EUR(400)),
4204 Txflags(tfPartialPayment | tfNoRippleDirect));
4205 if (!features[fixAMMv1_1])
4206 {
4207 // Carol gets ~29.91USD because of the AMM offers limit
4208 BEAST_EXPECT(ammAlice.expectBalances(
4209 XRP(10'030),
4210 STAmount{USD, UINT64_C(9'970'089730807577), -12},
4211 ammAlice.tokens()));
4212 BEAST_EXPECT(expectHolding(
4213 env, carol_, STAmount{USD, UINT64_C(30'029'91026919241), -11}));
4214 }
4215 else
4216 {
4217 BEAST_EXPECT(ammAlice.expectBalances(
4218 XRP(10'030),
4219 STAmount{USD, UINT64_C(9'970'089730807827), -12},
4220 ammAlice.tokens()));
4221 BEAST_EXPECT(expectHolding(
4222 env, carol_, STAmount{USD, UINT64_C(30'029'91026919217), -11}));
4223 }
4224 BEAST_EXPECT(expectOffers(env, alice_, 1, {{{EUR(140), XRP(100)}}}));
4225 },
4226 std::nullopt,
4227 0,
4228 std::nullopt,
4229 {features});
4230 // This payment is fulfilled
4231 testAMM(
4232 [&](AMM& ammAlice, Env& env) {
4233 env.fund(XRP(1'000), bob_);
4234 fund(env, gw_, {bob_}, {EUR(400)}, Fund::TokenOnly);
4235 env(trust(alice_, EUR(200)));
4236 for (int i = 0; i < 29; ++i)
4237 env(offer(alice_, EUR(1.0 + (0.01 * i)), XRP(1)));
4238 // This is worse quality offer than 30 offers above.
4239 // It will not be consumed because of AMM offers limit.
4240 env(offer(alice_, EUR(140), XRP(100)));
4241 env(pay(bob_, carol_, USD(100)),
4242 Path(~XRP, ~USD),
4243 Sendmax(EUR(400)),
4244 Txflags(tfPartialPayment | tfNoRippleDirect));
4245 BEAST_EXPECT(ammAlice.expectBalances(
4246 XRPAmount{10'101'010'102}, USD(9'900), ammAlice.tokens()));
4247 if (!features[fixAMMv1_1])
4248 {
4249 // Carol gets ~100USD
4250 BEAST_EXPECT(expectHolding(
4251 env, carol_, STAmount{USD, UINT64_C(30'099'99999999999), -11}));
4252 }
4253 else
4254 {
4255 BEAST_EXPECT(expectHolding(env, carol_, USD(30'100)));
4256 }
4257 BEAST_EXPECT(expectOffers(
4258 env,
4259 alice_,
4260 1,
4261 {{{STAmount{EUR, UINT64_C(39'1858572), -7}, XRPAmount{27'989'898}}}}));
4262 },
4263 std::nullopt,
4264 0,
4265 std::nullopt,
4266 {features});
4267
4268 // Offer crossing with AMM and another offer. AMM has a better
4269 // quality and is consumed first.
4270 {
4271 Env env(*this, features);
4272 fund(env, gw_, {alice_, carol_, bob_}, XRP(30'000), {USD(30'000)});
4273 env(offer(bob_, XRP(100), USD(100.001)));
4274 AMM const ammAlice(env, alice_, XRP(10'000), USD(10'100));
4275 env(offer(carol_, USD(100), XRP(100)));
4276 if (!features[fixAMMv1_1])
4277 {
4278 BEAST_EXPECT(ammAlice.expectBalances(
4279 XRPAmount{10'049'825'373},
4280 STAmount{USD, UINT64_C(10'049'92586949302), -11},
4281 ammAlice.tokens()));
4282 BEAST_EXPECT(expectOffers(
4283 env,
4284 bob_,
4285 1,
4286 {{{XRPAmount{50'074'629}, STAmount{USD, UINT64_C(50'07513050698), -11}}}}));
4287 }
4288 else
4289 {
4290 BEAST_EXPECT(ammAlice.expectBalances(
4291 XRPAmount{10'049'825'372},
4292 STAmount{USD, UINT64_C(10'049'92587049303), -11},
4293 ammAlice.tokens()));
4294 BEAST_EXPECT(expectOffers(
4295 env,
4296 bob_,
4297 1,
4298 {{{XRPAmount{50'074'628}, STAmount{USD, UINT64_C(50'07512950697), -11}}}}));
4299 BEAST_EXPECT(expectHolding(env, carol_, USD(30'100)));
4300 }
4301 }
4302
4303 // Individually frozen account
4304 testAMM(
4305 [&](AMM& ammAlice, Env& env) {
4306 env(trust(gw_, carol_["USD"](0), tfSetFreeze));
4307 env(trust(gw_, alice_["USD"](0), tfSetFreeze));
4308 env.close();
4309 env(pay(alice_, carol_, USD(1)),
4310 Path(~USD),
4311 Sendmax(XRP(10)),
4312 Txflags(tfNoRippleDirect | tfPartialPayment),
4313 Ter(tesSUCCESS));
4314 },
4315 std::nullopt,
4316 0,
4317 std::nullopt,
4318 {features});
4319 }
4320
4321 void
4323 {
4324 testcase("AMM Tokens");
4325 using namespace jtx;
4326
4327 // Offer crossing with AMM LPTokens and XRP.
4328 testAMM([&](AMM& ammAlice, Env& env) {
4329 auto const baseFee = env.current()->fees().base.drops();
4330 auto const token1 = ammAlice.lptIssue();
4331 auto priceXRP = ammAssetOut(
4332 STAmount{XRPAmount{10'000'000'000}},
4333 STAmount{token1, 10'000'000},
4334 STAmount{token1, 5'000'000},
4335 0);
4336 // Carol places an order to buy LPTokens
4337 env(offer(carol_, STAmount{token1, 5'000'000}, priceXRP));
4338 // Alice places an order to sell LPTokens
4339 env(offer(alice_, priceXRP, STAmount{token1, 5'000'000}));
4340 // Pool's LPTokens balance doesn't change
4341 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000}));
4342 // Carol is Liquidity Provider
4343 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{5'000'000}));
4344 BEAST_EXPECT(ammAlice.expectLPTokens(alice_, IOUAmount{5'000'000}));
4345 // Carol votes
4346 ammAlice.vote(carol_, 1'000);
4347 BEAST_EXPECT(ammAlice.expectTradingFee(500));
4348 ammAlice.vote(carol_, 0);
4349 BEAST_EXPECT(ammAlice.expectTradingFee(0));
4350 // Carol bids
4351 env(ammAlice.bid({.account = carol_, .bidMin = 100}));
4352 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{4'999'900}));
4353 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{100}));
4354 BEAST_EXPECT(
4355 accountBalance(env, carol_) == std::to_string(22500000000 - (4 * baseFee)));
4356 priceXRP = ammAssetOut(
4357 STAmount{XRPAmount{10'000'000'000}},
4358 STAmount{token1, 9'999'900},
4359 STAmount{token1, 4'999'900},
4360 0);
4361 // Carol withdraws
4362 ammAlice.withdrawAll(carol_, XRP(0));
4363 BEAST_EXPECT(
4364 accountBalance(env, carol_) == std::to_string(29999949999 - (5 * baseFee)));
4365 BEAST_EXPECT(ammAlice.expectBalances(
4366 XRPAmount{10'000'000'000} - priceXRP, USD(10'000), IOUAmount{5'000'000}));
4367 BEAST_EXPECT(ammAlice.expectLPTokens(alice_, IOUAmount{5'000'000}));
4368 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{0}));
4369 });
4370
4371 // Offer crossing with two AMM LPTokens.
4372 testAMM([&](AMM& ammAlice, Env& env) {
4373 ammAlice.deposit(carol_, 1'000'000);
4374 fund(env, gw_, {alice_, carol_}, {EUR(10'000)}, Fund::TokenOnly);
4375 AMM ammAlice1(env, alice_, XRP(10'000), EUR(10'000));
4376 ammAlice1.deposit(carol_, 1'000'000);
4377 auto const token1 = ammAlice.lptIssue();
4378 auto const token2 = ammAlice1.lptIssue();
4379 env(offer(alice_, STAmount{token1, 100}, STAmount{token2, 100}), Txflags(tfPassive));
4380 env.close();
4381 BEAST_EXPECT(expectOffers(env, alice_, 1));
4382 env(offer(carol_, STAmount{token2, 100}, STAmount{token1, 100}));
4383 env.close();
4384 BEAST_EXPECT(
4385 expectHolding(env, alice_, STAmount{token1, 10'000'100}) &&
4386 expectHolding(env, alice_, STAmount{token2, 9'999'900}));
4387 BEAST_EXPECT(
4388 expectHolding(env, carol_, STAmount{token2, 1'000'100}) &&
4389 expectHolding(env, carol_, STAmount{token1, 999'900}));
4390 BEAST_EXPECT(expectOffers(env, alice_, 0) && expectOffers(env, carol_, 0));
4391 });
4392
4393 // LPs pay LPTokens directly. Must trust set because the trust line
4394 // is checked for the limit, which is 0 in the AMM auto-created
4395 // trust line.
4396 testAMM([&](AMM& ammAlice, Env& env) {
4397 auto const token1 = ammAlice.lptIssue();
4398 env.trust(STAmount{token1, 2'000'000}, carol_);
4399 env.close();
4400 ammAlice.deposit(carol_, 1'000'000);
4401 BEAST_EXPECT(
4402 ammAlice.expectLPTokens(alice_, IOUAmount{10'000'000, 0}) &&
4403 ammAlice.expectLPTokens(carol_, IOUAmount{1'000'000, 0}));
4404 // Pool balance doesn't change, only tokens moved from
4405 // one line to another.
4406 env(pay(alice_, carol_, STAmount{token1, 100}));
4407 env.close();
4408 BEAST_EXPECT(
4409 // Alice initial token1 10,000,000 - 100
4410 ammAlice.expectLPTokens(alice_, IOUAmount{9'999'900, 0}) &&
4411 // Carol initial token1 1,000,000 + 100
4412 ammAlice.expectLPTokens(carol_, IOUAmount{1'000'100, 0}));
4413
4414 env.trust(STAmount{token1, 20'000'000}, alice_);
4415 env.close();
4416 env(pay(carol_, alice_, STAmount{token1, 100}));
4417 env.close();
4418 // Back to the original balance
4419 BEAST_EXPECT(
4420 ammAlice.expectLPTokens(alice_, IOUAmount{10'000'000, 0}) &&
4421 ammAlice.expectLPTokens(carol_, IOUAmount{1'000'000, 0}));
4422 });
4423 }
4424
4425 void
4427 {
4428 testcase("Amendment");
4429 using namespace jtx;
4430 Env env{*this, testableAmendments() - featureAMM};
4431
4432 {
4433 fund(env, gw_, {alice_}, {USD(1'000)}, Fund::All);
4434 AMM amm(env, alice_, XRP(1'000), USD(1'000), Ter(temDISABLED));
4435
4436 env(amm.bid({.bidMax = 1000}), Ter(temMALFORMED));
4437 env(amm.bid({}), Ter(temDISABLED));
4438 amm.vote(VoteArg{.tfee = 100, .err = Ter(temDISABLED)});
4439 amm.withdraw(WithdrawArg{.tokens = 100, .err = Ter(temMALFORMED)});
4440 amm.withdraw(WithdrawArg{.err = Ter(temDISABLED)});
4441 amm.deposit(DepositArg{.asset1In = USD(100), .err = Ter(temDISABLED)});
4442 amm.ammDelete(alice_, Ter(temDISABLED));
4443 }
4444 }
4445
4446 void
4448 {
4449 testcase("Flags");
4450 using namespace jtx;
4451
4452 testAMM([&](AMM& ammAlice, Env& env) {
4453 auto const info = env.rpc(
4454 "json",
4455 "account_info",
4456 std::string(R"({"account": ")" + to_string(ammAlice.ammAccount()) + "\"}"));
4457 auto const flags = info[jss::result][jss::account_data][jss::Flags].asUInt();
4458 BEAST_EXPECT(flags == (lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth));
4459 });
4460 }
4461
4462 void
4464 {
4465 testcase("Rippling");
4466 using namespace jtx;
4467
4468 // Rippling via AMM fails because AMM trust line has 0 limit.
4469 // Set up two issuers, A and B. Have each issue a token called TST.
4470 // Have another account C hold TST from both issuers,
4471 // and create an AMM for this pair.
4472 // Have a fourth account, D, create a trust line to the AMM for TST.
4473 // Send a payment delivering TST.AMM from C to D, using SendMax in
4474 // TST.A (or B) and a path through the AMM account. By normal
4475 // rippling rules, this would have caused the AMM's balances
4476 // to shift at a 1:1 rate with no fee applied has it not been
4477 // for 0 limit.
4478 {
4479 Env env(*this);
4480 auto const a = Account("A");
4481 auto const b = Account("B");
4482 auto const tsta = a["TST"];
4483 auto const tstb = b["TST"];
4484 auto const c = Account("C");
4485 auto const d = Account("D");
4486
4487 env.fund(XRP(10'000), a);
4488 env.fund(XRP(10'000), b);
4489 env.fund(XRP(10'000), c);
4490 env.fund(XRP(10'000), d);
4491
4492 env.trust(tsta(10'000), c);
4493 env.trust(tstb(10'000), c);
4494 env(pay(a, c, tsta(10'000)));
4495 env(pay(b, c, tstb(10'000)));
4496 AMM const amm(env, c, tsta(5'000), tstb(5'000));
4497 auto const ammIss = Issue(tsta.currency, amm.ammAccount());
4498
4499 // Can TrustSet only for AMM LP tokens
4500 env(trust(d, STAmount{ammIss, 10'000}), Ter(tecNO_PERMISSION));
4501 env.close();
4502
4503 // The payment would fail because of above, but check just in case
4504 env(pay(c, d, STAmount{ammIss, 10}),
4505 Sendmax(tsta(100)),
4506 Path(amm.ammAccount()),
4507 Txflags(tfPartialPayment | tfNoRippleDirect),
4508 Ter(tecPATH_DRY));
4509 }
4510 }
4511
4512 void
4514 {
4515 testcase("AMMAndCLOB, offer quality change");
4516 using namespace jtx;
4517 auto const gw = Account("gw");
4518 auto const tst = gw["TST"];
4519 auto const lP1 = Account("LP1");
4520 auto const lP2 = Account("LP2");
4521
4522 auto prep = [&](auto const& offerCb, auto const& expectCb) {
4523 Env env(*this, features);
4524 env.fund(XRP(30'000'000'000), gw);
4525 env(offer(gw, XRP(11'500'000'000), tst(1'000'000'000)));
4526
4527 env.fund(XRP(10'000), lP1);
4528 env.fund(XRP(10'000), lP2);
4529 env(offer(lP1, tst(25), XRPAmount(287'500'000)));
4530
4531 // Either AMM or CLOB offer
4532 offerCb(env);
4533
4534 env(offer(lP2, tst(25), XRPAmount(287'500'000)));
4535
4536 expectCb(env);
4537 };
4538
4539 // If we replace AMM with an equivalent CLOB offer, which AMM generates
4540 // when it is consumed, then the result must be equivalent, too.
4541 std::string lp2TSTBalance;
4542 std::string lp2TakerGets;
4543 std::string lp2TakerPays;
4544 // Execute with AMM first
4545 prep(
4546 [&](Env& env) { AMM const amm(env, lP1, tst(25), XRP(250)); },
4547 [&](Env& env) {
4548 lp2TSTBalance = getAccountLines(env, lP2, tst)["lines"][0u]["balance"].asString();
4549 auto const offer = getAccountOffers(env, lP2)["offers"][0u];
4550 lp2TakerGets = offer["taker_gets"].asString();
4551 lp2TakerPays = offer["taker_pays"]["value"].asString();
4552 });
4553 // Execute with CLOB offer
4554 prep(
4555 [&](Env& env) {
4556 if (!features[fixAMMv1_1])
4557 {
4558 env(offer(
4559 lP1,
4560 XRPAmount{18'095'133},
4561 STAmount{tst, UINT64_C(1'68737984885388), -14}),
4562 Txflags(tfPassive));
4563 }
4564 else
4565 {
4566 env(offer(
4567 lP1,
4568 XRPAmount{18'095'132},
4569 STAmount{tst, UINT64_C(1'68737976189735), -14}),
4570 Txflags(tfPassive));
4571 }
4572 },
4573 [&](Env& env) {
4574 BEAST_EXPECT(
4575 lp2TSTBalance ==
4576 getAccountLines(env, lP2, tst)["lines"][0u]["balance"].asString());
4577 auto const offer = getAccountOffers(env, lP2)["offers"][0u];
4578 BEAST_EXPECT(lp2TakerGets == offer["taker_gets"].asString());
4579 BEAST_EXPECT(lp2TakerPays == offer["taker_pays"]["value"].asString());
4580 });
4581 }
4582
4583 void
4585 {
4586 testcase("Trading Fee");
4587 using namespace jtx;
4588
4589 // Single Deposit, 1% fee
4590 testAMM(
4591 [&](AMM& ammAlice, Env& env) {
4592 // No fee
4593 ammAlice.deposit(carol_, USD(3'000));
4594 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{1'000}));
4595 ammAlice.withdrawAll(carol_, USD(3'000));
4596 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{0}));
4597 BEAST_EXPECT(expectHolding(env, carol_, USD(30'000)));
4598 // Set fee to 1%
4599 ammAlice.vote(alice_, 1'000);
4600 BEAST_EXPECT(ammAlice.expectTradingFee(1'000));
4601 // Carol gets fewer LPToken ~994, because of the single deposit
4602 // fee
4603 ammAlice.deposit(carol_, USD(3'000));
4604 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{994'981155689671, -12}));
4605 BEAST_EXPECT(expectHolding(env, carol_, USD(27'000)));
4606 // Set fee to 0
4607 ammAlice.vote(alice_, 0);
4608 ammAlice.withdrawAll(carol_, USD(0));
4609 // Carol gets back less than the original deposit
4610 BEAST_EXPECT(
4611 expectHolding(env, carol_, STAmount{USD, UINT64_C(29'994'96220068281), -11}));
4612 },
4613 {{USD(1'000), EUR(1'000)}},
4614 0,
4615 std::nullopt,
4616 {features});
4617
4618 // Single deposit with EP not exceeding specified:
4619 // 100USD with EP not to exceed 0.1 (AssetIn/TokensOut). 1% fee.
4620 testAMM(
4621 [&](AMM& ammAlice, Env& env) {
4622 auto const balance = env.balance(carol_, USD);
4623 auto tokensFee =
4624 ammAlice.deposit(carol_, USD(1'000), std::nullopt, STAmount{USD, 1, -1});
4625 auto const deposit = balance - env.balance(carol_, USD);
4626 ammAlice.withdrawAll(carol_, USD(0));
4627 ammAlice.vote(alice_, 0);
4628 BEAST_EXPECT(ammAlice.expectTradingFee(0));
4629 auto const tokensNoFee = ammAlice.deposit(carol_, deposit);
4630 // carol_ pays ~2008 LPTokens in fees or ~0.5% of the no-fee
4631 // LPTokens
4632 BEAST_EXPECT(tokensFee == IOUAmount(485'636'0611129, -7));
4633 BEAST_EXPECT(tokensNoFee == IOUAmount(487'644'85901109, -8));
4634 },
4635 std::nullopt,
4636 1'000,
4637 std::nullopt,
4638 {features});
4639
4640 // Single deposit with EP not exceeding specified:
4641 // 200USD with EP not to exceed 0.002020 (AssetIn/TokensOut). 1% fee
4642 testAMM(
4643 [&](AMM& ammAlice, Env& env) {
4644 auto const balance = env.balance(carol_, USD);
4645 auto const tokensFee =
4646 ammAlice.deposit(carol_, USD(200), std::nullopt, STAmount{USD, 2020, -6});
4647 auto const deposit = balance - env.balance(carol_, USD);
4648 ammAlice.withdrawAll(carol_, USD(0));
4649 ammAlice.vote(alice_, 0);
4650 BEAST_EXPECT(ammAlice.expectTradingFee(0));
4651 auto const tokensNoFee = ammAlice.deposit(carol_, deposit);
4652 // carol_ pays ~475 LPTokens in fees or ~0.5% of the no-fee
4653 // LPTokens
4654 BEAST_EXPECT(tokensFee == IOUAmount(98'000'00000002, -8));
4655 BEAST_EXPECT(tokensNoFee == IOUAmount(98'475'81871545, -8));
4656 },
4657 std::nullopt,
4658 1'000,
4659 std::nullopt,
4660 {features});
4661
4662 // Single Withdrawal, 1% fee
4663 testAMM(
4664 [&](AMM& ammAlice, Env& env) {
4665 // No fee
4666 ammAlice.deposit(carol_, USD(3'000));
4667
4668 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{1'000}));
4669 BEAST_EXPECT(expectHolding(env, carol_, USD(27'000)));
4670 // Set fee to 1%
4671 ammAlice.vote(alice_, 1'000);
4672 BEAST_EXPECT(ammAlice.expectTradingFee(1'000));
4673 // Single withdrawal. Carol gets ~5USD less than deposited.
4674 ammAlice.withdrawAll(carol_, USD(0));
4675 BEAST_EXPECT(
4676 expectHolding(env, carol_, STAmount{USD, UINT64_C(29'994'97487437186), -11}));
4677 },
4678 {{USD(1'000), EUR(1'000)}},
4679 0,
4680 std::nullopt,
4681 {features});
4682
4683 // Withdraw with EPrice limit, 1% fee.
4684 testAMM(
4685 [&](AMM& ammAlice, Env& env) {
4686 ammAlice.deposit(carol_, 1'000'000);
4687 auto const tokensFee =
4688 ammAlice.withdraw(carol_, USD(100), std::nullopt, IOUAmount{520, 0});
4689 // carol_ withdraws ~1,443.44USD
4690 auto const balanceAfterWithdraw = [&]() {
4691 if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
4692 {
4693 return STAmount(USD, UINT64_C(30'443'43891402715), -11);
4694 }
4695 if (features[fixAMMv1_1] && !features[fixAMMv1_3])
4696 {
4697 return STAmount(USD, UINT64_C(30'443'43891402714), -11);
4698 }
4699
4700 return STAmount(USD, UINT64_C(30'443'43891402713), -11);
4701 }();
4702 BEAST_EXPECT(env.balance(carol_, USD) == balanceAfterWithdraw);
4703 // Set to original pool size
4704 auto const deposit = balanceAfterWithdraw - USD(29'000);
4705 ammAlice.deposit(carol_, deposit);
4706 // fee 0%
4707 ammAlice.vote(alice_, 0);
4708 BEAST_EXPECT(ammAlice.expectTradingFee(0));
4709 auto const tokensNoFee = ammAlice.withdraw(carol_, deposit);
4710 if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
4711 {
4712 BEAST_EXPECT(
4713 env.balance(carol_, USD) ==
4714 STAmount(USD, UINT64_C(30'443'43891402717), -11));
4715 }
4716 else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
4717 {
4718 BEAST_EXPECT(
4719 env.balance(carol_, USD) ==
4720 STAmount(USD, UINT64_C(30'443'43891402716), -11));
4721 }
4722 else
4723 {
4724 BEAST_EXPECT(
4725 env.balance(carol_, USD) ==
4726 STAmount(USD, UINT64_C(30'443'43891402713), -11));
4727 }
4728 // carol_ pays ~4008 LPTokens in fees or ~0.5% of the no-fee
4729 // LPTokens
4730 if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
4731 {
4732 BEAST_EXPECT(tokensNoFee == IOUAmount(746'579'80779913, -8));
4733 }
4734 else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
4735 {
4736 BEAST_EXPECT(tokensNoFee == IOUAmount(746'579'80779912, -8));
4737 }
4738 else
4739 {
4740 BEAST_EXPECT(tokensNoFee == IOUAmount(746'579'80779911, -8));
4741 }
4742 BEAST_EXPECT(tokensFee == IOUAmount(750'588'23529411, -8));
4743 },
4744 std::nullopt,
4745 1'000,
4746 std::nullopt,
4747 {features});
4748
4749 // Payment, 1% fee
4750 testAMM(
4751 [&](AMM& ammAlice, Env& env) {
4752 fund(env, gw_, {bob_}, XRP(1'000), {USD(1'000), EUR(1'000)}, Fund::Acct);
4753 // Alice contributed 1010EUR and 1000USD to the pool
4754 BEAST_EXPECT(expectHolding(env, alice_, EUR(28'990)));
4755 BEAST_EXPECT(expectHolding(env, alice_, USD(29'000)));
4756 BEAST_EXPECT(expectHolding(env, carol_, USD(30'000)));
4757 // Carol pays to Alice with no fee
4758 env(pay(carol_, alice_, EUR(10)),
4759 Path(~EUR),
4760 Sendmax(USD(10)),
4761 Txflags(tfNoRippleDirect));
4762 env.close();
4763 // Alice has 10EUR more and Carol has 10USD less
4764 BEAST_EXPECT(expectHolding(env, alice_, EUR(29'000)));
4765 BEAST_EXPECT(expectHolding(env, alice_, USD(29'000)));
4766 BEAST_EXPECT(expectHolding(env, carol_, USD(29'990)));
4767
4768 // Set fee to 1%
4769 ammAlice.vote(alice_, 1'000);
4770 BEAST_EXPECT(ammAlice.expectTradingFee(1'000));
4771 // Bob pays to Carol with 1% fee
4772 env(pay(bob_, carol_, USD(10)),
4773 Path(~USD),
4774 Sendmax(EUR(15)),
4775 Txflags(tfNoRippleDirect));
4776 env.close();
4777 // Bob sends 10.1~EUR to pay 10USD
4778 BEAST_EXPECT(
4779 expectHolding(env, bob_, STAmount{EUR, UINT64_C(989'8989898989899), -13}));
4780 // Carol got 10USD
4781 BEAST_EXPECT(expectHolding(env, carol_, USD(30'000)));
4782 BEAST_EXPECT(ammAlice.expectBalances(
4783 USD(1'000),
4784 STAmount{EUR, UINT64_C(1'010'10101010101), -11},
4785 ammAlice.tokens()));
4786 },
4787 {{USD(1'000), EUR(1'010)}},
4788 0,
4789 std::nullopt,
4790 {features});
4791
4792 // Offer crossing, 0.5% fee
4793 testAMM(
4794 [&](AMM& ammAlice, Env& env) {
4795 // No fee
4796 env(offer(carol_, EUR(10), USD(10)));
4797 env.close();
4798 BEAST_EXPECT(expectHolding(env, carol_, USD(29'990)));
4799 BEAST_EXPECT(expectHolding(env, carol_, EUR(30'010)));
4800 // Change pool composition back
4801 env(offer(carol_, USD(10), EUR(10)));
4802 env.close();
4803 // Set fee to 0.5%
4804 ammAlice.vote(alice_, 500);
4805 BEAST_EXPECT(ammAlice.expectTradingFee(500));
4806 env(offer(carol_, EUR(10), USD(10)));
4807 env.close();
4808 // Alice gets fewer ~4.97EUR for ~5.02USD, the difference goes
4809 // to the pool
4810 BEAST_EXPECT(
4811 expectHolding(env, carol_, STAmount{USD, UINT64_C(29'995'02512562814), -11}));
4812 BEAST_EXPECT(
4813 expectHolding(env, carol_, STAmount{EUR, UINT64_C(30'004'97487437186), -11}));
4814 BEAST_EXPECT(expectOffers(
4815 env,
4816 carol_,
4817 1,
4818 {{Amounts{
4819 STAmount{EUR, UINT64_C(5'025125628140703), -15},
4820 STAmount{USD, UINT64_C(5'025125628140703), -15}}}}));
4821 if (!features[fixAMMv1_1])
4822 {
4823 BEAST_EXPECT(ammAlice.expectBalances(
4824 STAmount{USD, UINT64_C(1'004'974874371859), -12},
4825 STAmount{EUR, UINT64_C(1'005'025125628141), -12},
4826 ammAlice.tokens()));
4827 }
4828 else
4829 {
4830 BEAST_EXPECT(ammAlice.expectBalances(
4831 STAmount{USD, UINT64_C(1'004'97487437186), -11},
4832 STAmount{EUR, UINT64_C(1'005'025125628141), -12},
4833 ammAlice.tokens()));
4834 }
4835 },
4836 {{USD(1'000), EUR(1'010)}},
4837 0,
4838 std::nullopt,
4839 {features});
4840
4841 // Payment with AMM and CLOB offer, 0 fee
4842 // AMM liquidity is consumed first up to CLOB offer quality
4843 // CLOB offer is fully consumed next
4844 // Remaining amount is consumed via AMM liquidity
4845 {
4846 Env env(*this, features);
4847 Account const ed("ed");
4848 fund(env, gw_, {alice_, bob_, carol_, ed}, XRP(1'000), {USD(2'000), EUR(2'000)});
4849 env(offer(carol_, EUR(5), USD(5)));
4850 AMM const ammAlice(env, alice_, USD(1'005), EUR(1'000));
4851 env(pay(bob_, ed, USD(10)), Path(~USD), Sendmax(EUR(15)), Txflags(tfNoRippleDirect));
4852 BEAST_EXPECT(expectHolding(env, ed, USD(2'010)));
4853 if (!features[fixAMMv1_1])
4854 {
4855 BEAST_EXPECT(expectHolding(env, bob_, EUR(1'990)));
4856 BEAST_EXPECT(ammAlice.expectBalances(USD(1'000), EUR(1'005), ammAlice.tokens()));
4857 }
4858 else
4859 {
4860 BEAST_EXPECT(
4861 expectHolding(env, bob_, STAmount(EUR, UINT64_C(1989'999999999999), -12)));
4862 BEAST_EXPECT(ammAlice.expectBalances(
4863 USD(1'000),
4864 STAmount(EUR, UINT64_C(1005'000000000001), -12),
4865 ammAlice.tokens()));
4866 }
4867 BEAST_EXPECT(expectOffers(env, carol_, 0));
4868 }
4869
4870 // Payment with AMM and CLOB offer. Same as above but with 0.25%
4871 // fee.
4872 {
4873 Env env(*this, features);
4874 Account const ed("ed");
4875 fund(env, gw_, {alice_, bob_, carol_, ed}, XRP(1'000), {USD(2'000), EUR(2'000)});
4876 env(offer(carol_, EUR(5), USD(5)));
4877 // Set 0.25% fee
4878 AMM const ammAlice(env, alice_, USD(1'005), EUR(1'000), false, 250);
4879 env(pay(bob_, ed, USD(10)), Path(~USD), Sendmax(EUR(15)), Txflags(tfNoRippleDirect));
4880 BEAST_EXPECT(expectHolding(env, ed, USD(2'010)));
4881 if (!features[fixAMMv1_1])
4882 {
4883 BEAST_EXPECT(
4884 expectHolding(env, bob_, STAmount{EUR, UINT64_C(1'989'987453007618), -12}));
4885 BEAST_EXPECT(ammAlice.expectBalances(
4886 USD(1'000),
4887 STAmount{EUR, UINT64_C(1'005'012546992382), -12},
4888 ammAlice.tokens()));
4889 }
4890 else
4891 {
4892 BEAST_EXPECT(
4893 expectHolding(env, bob_, STAmount{EUR, UINT64_C(1'989'987453007628), -12}));
4894 BEAST_EXPECT(ammAlice.expectBalances(
4895 USD(1'000),
4896 STAmount{EUR, UINT64_C(1'005'012546992372), -12},
4897 ammAlice.tokens()));
4898 }
4899 BEAST_EXPECT(expectOffers(env, carol_, 0));
4900 }
4901
4902 // Payment with AMM and CLOB offer. AMM has a better
4903 // spot price quality, but 1% fee offsets that. As the result
4904 // the entire trade is executed via LOB.
4905 {
4906 Env env(*this, features);
4907 Account const ed("ed");
4908 fund(env, gw_, {alice_, bob_, carol_, ed}, XRP(1'000), {USD(2'000), EUR(2'000)});
4909 env(offer(carol_, EUR(10), USD(10)));
4910 // Set 1% fee
4911 AMM const ammAlice(env, alice_, USD(1'005), EUR(1'000), false, 1'000);
4912 env(pay(bob_, ed, USD(10)), Path(~USD), Sendmax(EUR(15)), Txflags(tfNoRippleDirect));
4913 BEAST_EXPECT(expectHolding(env, ed, USD(2'010)));
4914 BEAST_EXPECT(expectHolding(env, bob_, EUR(1'990)));
4915 BEAST_EXPECT(ammAlice.expectBalances(USD(1'005), EUR(1'000), ammAlice.tokens()));
4916 BEAST_EXPECT(expectOffers(env, carol_, 0));
4917 }
4918
4919 // Payment with AMM and CLOB offer. AMM has a better
4920 // spot price quality, but 1% fee offsets that.
4921 // The CLOB offer is consumed first and the remaining
4922 // amount is consumed via AMM liquidity.
4923 {
4924 Env env(*this, features);
4925 Account const ed("ed");
4926 fund(env, gw_, {alice_, bob_, carol_, ed}, XRP(1'000), {USD(2'000), EUR(2'000)});
4927 env(offer(carol_, EUR(9), USD(9)));
4928 // Set 1% fee
4929 AMM const ammAlice(env, alice_, USD(1'005), EUR(1'000), false, 1'000);
4930 env(pay(bob_, ed, USD(10)), Path(~USD), Sendmax(EUR(15)), Txflags(tfNoRippleDirect));
4931 BEAST_EXPECT(expectHolding(env, ed, USD(2'010)));
4932 BEAST_EXPECT(
4933 expectHolding(env, bob_, STAmount{EUR, UINT64_C(1'989'993923296712), -12}));
4934 BEAST_EXPECT(ammAlice.expectBalances(
4935 USD(1'004), STAmount{EUR, UINT64_C(1'001'006076703288), -12}, ammAlice.tokens()));
4936 BEAST_EXPECT(expectOffers(env, carol_, 0));
4937 }
4938 }
4939
4940 void
4942 {
4943 testcase("Adjusted Deposit/Withdraw Tokens");
4944
4945 using namespace jtx;
4946
4947 // Deposit/Withdraw in USD
4948 testAMM(
4949 [&](AMM& ammAlice, Env& env) {
4950 Account const bob("bob");
4951 Account const ed("ed");
4952 Account const paul("paul");
4953 Account const dan("dan");
4954 Account const chris("chris");
4955 Account const simon("simon");
4956 Account const ben("ben");
4957 Account const natalie("natalie");
4958 fund(
4959 env,
4960 gw_,
4961 {bob, ed, paul, dan, chris, simon, ben, natalie},
4962 {USD(1'500'000)},
4963 Fund::Acct);
4964 for (int i = 0; i < 10; ++i)
4965 {
4966 ammAlice.deposit(ben, STAmount{USD, 1, -10});
4967 ammAlice.withdrawAll(ben, USD(0));
4968 ammAlice.deposit(simon, USD(0.1));
4969 ammAlice.withdrawAll(simon, USD(0));
4970 ammAlice.deposit(chris, USD(1));
4971 ammAlice.withdrawAll(chris, USD(0));
4972 ammAlice.deposit(dan, USD(10));
4973 ammAlice.withdrawAll(dan, USD(0));
4974 ammAlice.deposit(bob, USD(100));
4975 ammAlice.withdrawAll(bob, USD(0));
4976 ammAlice.deposit(carol_, USD(1'000));
4977 ammAlice.withdrawAll(carol_, USD(0));
4978 ammAlice.deposit(ed, USD(10'000));
4979 ammAlice.withdrawAll(ed, USD(0));
4980 ammAlice.deposit(paul, USD(100'000));
4981 ammAlice.withdrawAll(paul, USD(0));
4982 ammAlice.deposit(natalie, USD(1'000'000));
4983 ammAlice.withdrawAll(natalie, USD(0));
4984 }
4985 // Due to round off some accounts have a tiny gain, while
4986 // other have a tiny loss. The last account to withdraw
4987 // gets everything in the pool.
4988 if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
4989 {
4990 BEAST_EXPECT(ammAlice.expectBalances(
4991 XRP(10'000),
4992 STAmount{USD, UINT64_C(10'000'0000000013), -10},
4993 IOUAmount{10'000'000}));
4994 }
4995 else if (features[fixAMMv1_3])
4996 {
4997 BEAST_EXPECT(ammAlice.expectBalances(
4998 XRP(10'000),
4999 STAmount{USD, UINT64_C(10'000'0000000003), -10},
5000 IOUAmount{10'000'000}));
5001 }
5002 else
5003 {
5004 BEAST_EXPECT(
5005 ammAlice.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000}));
5006 }
5007 BEAST_EXPECT(expectHolding(env, ben, USD(1'500'000)));
5008 BEAST_EXPECT(expectHolding(env, simon, USD(1'500'000)));
5009 BEAST_EXPECT(expectHolding(env, chris, USD(1'500'000)));
5010 BEAST_EXPECT(expectHolding(env, dan, USD(1'500'000)));
5011 if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
5012 {
5013 BEAST_EXPECT(expectHolding(
5014 env, carol_, STAmount{USD, UINT64_C(30'000'00000000001), -11}));
5015 }
5016 else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
5017 {
5018 BEAST_EXPECT(expectHolding(env, carol_, USD(30'000)));
5019 }
5020 else
5021 {
5022 BEAST_EXPECT(expectHolding(env, carol_, USD(30'000)));
5023 }
5024 BEAST_EXPECT(expectHolding(env, ed, USD(1'500'000)));
5025 BEAST_EXPECT(expectHolding(env, paul, USD(1'500'000)));
5026 if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
5027 {
5028 BEAST_EXPECT(expectHolding(
5029 env, natalie, STAmount{USD, UINT64_C(1'500'000'000000002), -9}));
5030 }
5031 else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
5032 {
5033 BEAST_EXPECT(expectHolding(
5034 env, natalie, STAmount{USD, UINT64_C(1'500'000'000000005), -9}));
5035 }
5036 else
5037 {
5038 BEAST_EXPECT(expectHolding(env, natalie, USD(1'500'000)));
5039 }
5040 ammAlice.withdrawAll(alice_);
5041 BEAST_EXPECT(!ammAlice.ammExists());
5042 if (!features[fixAMMv1_1])
5043 {
5044 BEAST_EXPECT(expectHolding(
5045 env, alice_, STAmount{USD, UINT64_C(30'000'0000000013), -10}));
5046 }
5047 else if (features[fixAMMv1_3])
5048 {
5049 BEAST_EXPECT(expectHolding(
5050 env, alice_, STAmount{USD, UINT64_C(30'000'0000000003), -10}));
5051 }
5052 else
5053 {
5054 BEAST_EXPECT(expectHolding(env, alice_, USD(30'000)));
5055 }
5056 // alice_ XRP balance is 30,000 initial - 50 AMMCreate fee -
5057 // 10drops fee
5058 BEAST_EXPECT(
5059 accountBalance(env, alice_) ==
5060 std::to_string(29950000000 - env.current()->fees().base.drops()));
5061 },
5062 std::nullopt,
5063 0,
5064 std::nullopt,
5065 {features});
5066
5067 // Same as above but deposit/withdraw in XRP
5068 testAMM(
5069 [&](AMM& ammAlice, Env& env) {
5070 Account const bob("bob");
5071 Account const ed("ed");
5072 Account const paul("paul");
5073 Account const dan("dan");
5074 Account const chris("chris");
5075 Account const simon("simon");
5076 Account const ben("ben");
5077 Account const natalie("natalie");
5078 fund(
5079 env,
5080 gw_,
5081 {bob, ed, paul, dan, chris, simon, ben, natalie},
5082 XRP(2'000'000),
5083 {},
5084 Fund::Acct);
5085 for (int i = 0; i < 10; ++i)
5086 {
5087 ammAlice.deposit(ben, XRPAmount{1});
5088 ammAlice.withdrawAll(ben, XRP(0));
5089 ammAlice.deposit(simon, XRPAmount(1'000));
5090 ammAlice.withdrawAll(simon, XRP(0));
5091 ammAlice.deposit(chris, XRP(1));
5092 ammAlice.withdrawAll(chris, XRP(0));
5093 ammAlice.deposit(dan, XRP(10));
5094 ammAlice.withdrawAll(dan, XRP(0));
5095 ammAlice.deposit(bob, XRP(100));
5096 ammAlice.withdrawAll(bob, XRP(0));
5097 ammAlice.deposit(carol_, XRP(1'000));
5098 ammAlice.withdrawAll(carol_, XRP(0));
5099 ammAlice.deposit(ed, XRP(10'000));
5100 ammAlice.withdrawAll(ed, XRP(0));
5101 ammAlice.deposit(paul, XRP(100'000));
5102 ammAlice.withdrawAll(paul, XRP(0));
5103 ammAlice.deposit(natalie, XRP(1'000'000));
5104 ammAlice.withdrawAll(natalie, XRP(0));
5105 }
5106 auto const baseFee = env.current()->fees().base.drops();
5107 if (!features[fixAMMv1_3])
5108 {
5109 // No round off with XRP in this test
5110 BEAST_EXPECT(
5111 ammAlice.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000}));
5112 ammAlice.withdrawAll(alice_);
5113 BEAST_EXPECT(!ammAlice.ammExists());
5114 // 20,000 initial - (deposit+withdraw) * 10
5115 auto const xrpBalance = (XRP(2'000'000) - txFee(env, 20)).getText();
5116 BEAST_EXPECT(accountBalance(env, ben) == xrpBalance);
5117 BEAST_EXPECT(accountBalance(env, simon) == xrpBalance);
5118 BEAST_EXPECT(accountBalance(env, chris) == xrpBalance);
5119 BEAST_EXPECT(accountBalance(env, dan) == xrpBalance);
5120
5121 // 30,000 initial - (deposit+withdraw) * 10
5122 BEAST_EXPECT(
5123 accountBalance(env, carol_) ==
5124 std::to_string(30'000'000'000 - (20 * baseFee)));
5125 BEAST_EXPECT(accountBalance(env, ed) == xrpBalance);
5126 BEAST_EXPECT(accountBalance(env, paul) == xrpBalance);
5127 BEAST_EXPECT(accountBalance(env, natalie) == xrpBalance);
5128 // 30,000 initial - 50 AMMCreate fee - 10drops withdraw fee
5129 BEAST_EXPECT(
5130 accountBalance(env, alice_) == std::to_string(29'950'000'000 - baseFee));
5131 }
5132 else
5133 {
5134 // post-amendment the rounding takes place to ensure
5135 // AMM invariant
5136 BEAST_EXPECT(ammAlice.expectBalances(
5137 XRPAmount(10'000'000'080), USD(10'000), IOUAmount{10'000'000}));
5138 ammAlice.withdrawAll(alice_);
5139 BEAST_EXPECT(!ammAlice.ammExists());
5140 auto const xrpBalance = XRP(2'000'000) - txFee(env, 20) - drops(10);
5141 auto const xrpBalanceText = xrpBalance.getText();
5142 BEAST_EXPECT(accountBalance(env, ben) == xrpBalanceText);
5143 BEAST_EXPECT(accountBalance(env, simon) == xrpBalanceText);
5144 BEAST_EXPECT(accountBalance(env, chris) == xrpBalanceText);
5145 BEAST_EXPECT(accountBalance(env, dan) == xrpBalanceText);
5146 BEAST_EXPECT(
5147 accountBalance(env, carol_) ==
5148 std::to_string(30'000'000'000 - (20 * baseFee) - 10));
5149 BEAST_EXPECT(accountBalance(env, ed) == (xrpBalance + drops(2)).getText());
5150 BEAST_EXPECT(accountBalance(env, paul) == (xrpBalance + drops(3)).getText());
5151 BEAST_EXPECT(accountBalance(env, natalie) == (xrpBalance + drops(5)).getText());
5152 BEAST_EXPECT(
5153 accountBalance(env, alice_) ==
5154 std::to_string(29'950'000'000 - baseFee + 80));
5155 }
5156 },
5157 std::nullopt,
5158 0,
5159 std::nullopt,
5160 {features});
5161 }
5162
5163 void
5165 {
5166 testcase("Auto Delete");
5167
5168 using namespace jtx;
5169 FeatureBitset const all{testableAmendments()};
5170
5171 {
5172 Env env(
5173 *this,
5175 cfg->fees.referenceFee = XRPAmount(1);
5176 return cfg;
5177 }),
5178 all);
5179 fund(env, gw_, {alice_}, XRP(20'000), {USD(10'000)});
5180 AMM amm(env, gw_, XRP(10'000), USD(10'000));
5181 for (auto i = 0; i < kMaxDeletableAmmTrustLines + 10; ++i)
5182 {
5183 Account const a{std::to_string(i)};
5184 env.fund(XRP(1'000), a);
5185 env(trust(a, STAmount{amm.lptIssue(), 10'000}));
5186 env.close();
5187 }
5188 // The trustlines are partially deleted,
5189 // AMM is set to an empty state.
5190 amm.withdrawAll(gw_);
5191 BEAST_EXPECT(amm.ammExists());
5192
5193 // Bid,Vote,Deposit,Withdraw,TrustSet failing with
5194 // tecAMM_EMPTY. Deposit succeeds with tfTwoAssetIfEmpty option.
5195 env(amm.bid({
5196 .account = alice_,
5197 .bidMin = 1000,
5198 }),
5199 Ter(tecAMM_EMPTY));
5200 amm.vote(
5201 std::nullopt, 100, std::nullopt, std::nullopt, std::nullopt, Ter(tecAMM_EMPTY));
5202 amm.withdraw(alice_, 100, std::nullopt, std::nullopt, Ter(tecAMM_EMPTY));
5203 amm.deposit(
5204 alice_, USD(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecAMM_EMPTY));
5205 env(trust(alice_, STAmount{amm.lptIssue(), 10'000}), Ter(tecAMM_EMPTY));
5206
5207 // Can deposit with tfTwoAssetIfEmpty option
5208 amm.deposit(
5209 alice_,
5210 std::nullopt,
5211 XRP(10'000),
5212 USD(10'000),
5213 std::nullopt,
5214 tfTwoAssetIfEmpty,
5215 std::nullopt,
5216 std::nullopt,
5217 1'000);
5218 BEAST_EXPECT(amm.expectBalances(XRP(10'000), USD(10'000), amm.tokens()));
5219 BEAST_EXPECT(amm.expectTradingFee(1'000));
5220 BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{0}));
5221
5222 // Withdrawing all tokens deletes AMM since the number
5223 // of remaining trustlines is less than max
5224 amm.withdrawAll(alice_);
5225 BEAST_EXPECT(!amm.ammExists());
5226 BEAST_EXPECT(!env.le(keylet::ownerDir(amm.ammAccount())));
5227 }
5228
5229 {
5230 Env env(
5231 *this,
5233 cfg->fees.referenceFee = XRPAmount(1);
5234 return cfg;
5235 }),
5236 all);
5237 fund(env, gw_, {alice_}, XRP(20'000), {USD(10'000)});
5238 AMM amm(env, gw_, XRP(10'000), USD(10'000));
5239 for (auto i = 0; i < (kMaxDeletableAmmTrustLines * 2) + 10; ++i)
5240 {
5241 Account const a{std::to_string(i)};
5242 env.fund(XRP(1'000), a);
5243 env(trust(a, STAmount{amm.lptIssue(), 10'000}));
5244 env.close();
5245 }
5246 // The trustlines are partially deleted.
5247 amm.withdrawAll(gw_);
5248 BEAST_EXPECT(amm.ammExists());
5249
5250 // AMMDelete has to be called twice to delete AMM.
5251 amm.ammDelete(alice_, Ter(tecINCOMPLETE));
5252 BEAST_EXPECT(amm.ammExists());
5253 // Deletes remaining trustlines and deletes AMM.
5254 amm.ammDelete(alice_);
5255 BEAST_EXPECT(!amm.ammExists());
5256 BEAST_EXPECT(!env.le(keylet::ownerDir(amm.ammAccount())));
5257
5258 // Try redundant delete
5259 amm.ammDelete(alice_, Ter(terNO_AMM));
5260 }
5261 }
5262
5263 void
5265 {
5266 testcase("Clawback");
5267 using namespace jtx;
5268 Env env(*this);
5269 env.fund(XRP(2'000), gw_);
5270 env.fund(XRP(2'000), alice_);
5271 AMM const amm(env, gw_, XRP(1'000), USD(1'000));
5272 env(fset(gw_, asfAllowTrustLineClawback), Ter(tecOWNERS));
5273 }
5274
5275 void
5277 {
5278 testcase("AMMID");
5279 using namespace jtx;
5280 testAMM([&](AMM& amm, Env& env) {
5281 amm.setClose(false);
5282 auto const info = env.rpc(
5283 "json",
5284 "account_info",
5285 std::string(R"({"account": ")" + to_string(amm.ammAccount()) + "\"}"));
5286 try
5287 {
5288 BEAST_EXPECT(
5289 info[jss::result][jss::account_data][jss::AMMID].asString() ==
5290 to_string(amm.ammID()));
5291 }
5292 catch (...)
5293 {
5294 fail();
5295 }
5296 amm.deposit(carol_, 1'000);
5297 auto affected =
5298 env.meta()->getJson(JsonOptions::Values::None)[sfAffectedNodes.fieldName];
5299 try
5300 {
5301 bool found = false;
5302 for (auto const& node : affected)
5303 {
5304 if (node.isMember(sfModifiedNode.fieldName) &&
5305 node[sfModifiedNode.fieldName][sfLedgerEntryType.fieldName].asString() ==
5306 "AccountRoot" &&
5307 node[sfModifiedNode.fieldName][sfFinalFields.fieldName][jss::Account]
5308 .asString() == to_string(amm.ammAccount()))
5309 {
5310 found = node[sfModifiedNode.fieldName][sfFinalFields.fieldName][jss::AMMID]
5311 .asString() == to_string(amm.ammID());
5312 break;
5313 }
5314 }
5315 BEAST_EXPECT(found);
5316 }
5317 catch (...)
5318 {
5319 fail();
5320 }
5321 });
5322 }
5323
5324 void
5326 {
5327 testcase("Offer/Strand Selection");
5328 using namespace jtx;
5329 Account const ed("ed");
5330 Account const gw1("gw1");
5331 auto const eth = gw1["ETH"];
5332 auto const can = gw1["CAN"];
5333
5334 // These tests are expected to fail if the OwnerPaysFee feature
5335 // is ever supported. Updates will need to be made to AMM handling
5336 // in the payment engine, and these tests will need to be updated.
5337
5338 auto prep = [&](Env& env, auto gwRate, auto gw1Rate) {
5339 fund(env, gw_, {alice_, carol_, bob_, ed}, XRP(2'000), {USD(2'000)});
5340 env.fund(XRP(2'000), gw1);
5341 fund(env, gw1, {alice_, carol_, bob_, ed}, {eth(2'000), can(2'000)}, Fund::TokenOnly);
5342 env(rate(gw_, gwRate));
5343 env(rate(gw1, gw1Rate));
5344 env.close();
5345 };
5346
5347 for (auto const& rates : {std::make_pair(1.5, 1.9), std::make_pair(1.9, 1.5)})
5348 {
5349 // Offer Selection
5350
5351 // Cross-currency payment: AMM has the same spot price quality
5352 // as CLOB's offer and can't generate a better quality offer.
5353 // The transfer fee in this case doesn't change the CLOB quality
5354 // because trIn is ignored on adjustment and trOut on payment is
5355 // also ignored because ownerPaysTransferFee is false in this
5356 // case. Run test for 0) offer, 1) AMM, 2) offer and AMM to
5357 // verify that the quality is better in the first case, and CLOB
5358 // is selected in the second case.
5359 {
5361 for (auto i = 0; i < 3; ++i)
5362 {
5363 Env env(*this, features);
5364 prep(env, rates.first, rates.second);
5366 if (i == 0 || i == 2)
5367 {
5368 env(offer(ed, eth(400), USD(400)), Txflags(tfPassive));
5369 env.close();
5370 }
5371 if (i > 0)
5372 amm.emplace(env, ed, USD(1'000), eth(1'000));
5373 env(pay(carol_, bob_, USD(100)), Path(~USD), Sendmax(eth(500)));
5374 env.close();
5375 // CLOB and AMM, AMM is not selected
5376 if (i == 2)
5377 {
5378 BEAST_EXPECT(amm->expectBalances(USD(1'000), eth(1'000), amm->tokens()));
5379 }
5380 BEAST_EXPECT(expectHolding(env, bob_, USD(2'100)));
5381 q[i] = Quality(
5382 Amounts{
5383 eth(2'000) - env.balance(carol_, eth),
5384 env.balance(bob_, USD) - USD(2'000)});
5385 }
5386 // CLOB is better quality than AMM
5387 BEAST_EXPECT(q[0] > q[1]);
5388 // AMM is not selected with CLOB
5389 BEAST_EXPECT(q[0] == q[2]);
5390 }
5391 // Offer crossing: AMM has the same spot price quality
5392 // as CLOB's offer and can't generate a better quality offer.
5393 // The transfer fee in this case doesn't change the CLOB quality
5394 // because the quality adjustment is ignored for the offer
5395 // crossing.
5396 for (auto i = 0; i < 3; ++i)
5397 {
5398 Env env(*this, features);
5399 prep(env, rates.first, rates.second);
5401 if (i == 0 || i == 2)
5402 {
5403 env(offer(ed, eth(400), USD(400)), Txflags(tfPassive));
5404 env.close();
5405 }
5406 if (i > 0)
5407 amm.emplace(env, ed, USD(1'000), eth(1'000));
5408 env(offer(alice_, USD(400), eth(400)));
5409 env.close();
5410 // AMM is not selected
5411 if (i > 0)
5412 {
5413 BEAST_EXPECT(amm->expectBalances(USD(1'000), eth(1'000), amm->tokens()));
5414 }
5415 if (i == 0 || i == 2)
5416 {
5417 // Fully crosses
5418 BEAST_EXPECT(expectOffers(env, alice_, 0));
5419 }
5420 // Fails to cross because AMM is not selected
5421 else
5422 {
5423 BEAST_EXPECT(expectOffers(env, alice_, 1, {Amounts{USD(400), eth(400)}}));
5424 }
5425 BEAST_EXPECT(expectOffers(env, ed, 0));
5426 }
5427
5428 // Show that the CLOB quality reduction
5429 // results in AMM offer selection.
5430
5431 // Same as the payment but reduced offer quality
5432 {
5434 for (auto i = 0; i < 3; ++i)
5435 {
5436 Env env(*this, features);
5437 prep(env, rates.first, rates.second);
5439 if (i == 0 || i == 2)
5440 {
5441 env(offer(ed, eth(400), USD(300)), Txflags(tfPassive));
5442 env.close();
5443 }
5444 if (i > 0)
5445 amm.emplace(env, ed, USD(1'000), eth(1'000));
5446 env(pay(carol_, bob_, USD(100)), Path(~USD), Sendmax(eth(500)));
5447 env.close();
5448 // AMM and CLOB are selected
5449 if (i > 0)
5450 {
5451 BEAST_EXPECT(!amm->expectBalances(USD(1'000), eth(1'000), amm->tokens()));
5452 }
5453 if (i == 2 && !features[fixAMMv1_1])
5454 {
5455 if (rates.first == 1.5)
5456 {
5457 if (!features[fixAMMv1_1])
5458 {
5459 BEAST_EXPECT(expectOffers(
5460 env,
5461 ed,
5462 1,
5463 {{Amounts{
5464 STAmount{eth, UINT64_C(378'6327949540823), -13},
5465 STAmount{USD, UINT64_C(283'9745962155617), -13}}}}));
5466 }
5467 else
5468 {
5469 BEAST_EXPECT(expectOffers(
5470 env,
5471 ed,
5472 1,
5473 {{Amounts{
5474 STAmount{eth, UINT64_C(378'6327949540813), -13},
5475 STAmount{USD, UINT64_C(283'974596215561), -12}}}}));
5476 }
5477 }
5478 else
5479 {
5480 if (!features[fixAMMv1_1])
5481 {
5482 BEAST_EXPECT(expectOffers(
5483 env,
5484 ed,
5485 1,
5486 {{Amounts{
5487 STAmount{eth, UINT64_C(325'299461620749), -12},
5488 STAmount{USD, UINT64_C(243'9745962155617), -13}}}}));
5489 }
5490 else
5491 {
5492 BEAST_EXPECT(expectOffers(
5493 env,
5494 ed,
5495 1,
5496 {{Amounts{
5497 STAmount{eth, UINT64_C(325'299461620748), -12},
5498 STAmount{USD, UINT64_C(243'974596215561), -12}}}}));
5499 }
5500 }
5501 }
5502 else if (i == 2)
5503 {
5504 if (rates.first == 1.5)
5505 {
5506 BEAST_EXPECT(expectOffers(
5507 env,
5508 ed,
5509 1,
5510 {{Amounts{
5511 STAmount{eth, UINT64_C(378'6327949540812), -13},
5512 STAmount{USD, UINT64_C(283'9745962155609), -13}}}}));
5513 }
5514 else
5515 {
5516 BEAST_EXPECT(expectOffers(
5517 env,
5518 ed,
5519 1,
5520 {{Amounts{
5521 STAmount{eth, UINT64_C(325'2994616207479), -13},
5522 STAmount{USD, UINT64_C(243'9745962155609), -13}}}}));
5523 }
5524 }
5525 BEAST_EXPECT(expectHolding(env, bob_, USD(2'100)));
5526 q[i] = Quality(
5527 Amounts{
5528 eth(2'000) - env.balance(carol_, eth),
5529 env.balance(bob_, USD) - USD(2'000)});
5530 }
5531 // AMM is better quality
5532 BEAST_EXPECT(q[1] > q[0]);
5533 // AMM and CLOB produce better quality
5534 BEAST_EXPECT(q[2] > q[1]);
5535 }
5536
5537 // Same as the offer-crossing but reduced offer quality
5538 for (auto i = 0; i < 3; ++i)
5539 {
5540 Env env(*this, features);
5541 prep(env, rates.first, rates.second);
5543 if (i == 0 || i == 2)
5544 {
5545 env(offer(ed, eth(400), USD(250)), Txflags(tfPassive));
5546 env.close();
5547 }
5548 if (i > 0)
5549 amm.emplace(env, ed, USD(1'000), eth(1'000));
5550 env(offer(alice_, USD(250), eth(400)));
5551 env.close();
5552 // AMM is selected in both cases
5553 if (i > 0)
5554 {
5555 BEAST_EXPECT(!amm->expectBalances(USD(1'000), eth(1'000), amm->tokens()));
5556 }
5557 // Partially crosses, AMM is selected, CLOB fails
5558 // limitQuality
5559 if (i == 2)
5560 {
5561 if (rates.first == 1.5)
5562 {
5563 if (!features[fixAMMv1_1])
5564 {
5565 BEAST_EXPECT(expectOffers(env, ed, 1, {{Amounts{eth(400), USD(250)}}}));
5566 BEAST_EXPECT(expectOffers(
5567 env,
5568 alice_,
5569 1,
5570 {{Amounts{
5571 STAmount{USD, UINT64_C(40'5694150420947), -13},
5572 STAmount{eth, UINT64_C(64'91106406735152), -14},
5573 }}}));
5574 }
5575 else
5576 {
5577 // Ed offer is partially crossed.
5578 // The updated rounding makes limitQuality
5579 // work if both amendments are enabled
5580 BEAST_EXPECT(expectOffers(
5581 env,
5582 ed,
5583 1,
5584 {{Amounts{
5585 STAmount{eth, UINT64_C(335'0889359326475), -13},
5586 STAmount{USD, UINT64_C(209'4305849579047), -13},
5587 }}}));
5588 BEAST_EXPECT(expectOffers(env, alice_, 0));
5589 }
5590 }
5591 else
5592 {
5593 if (!features[fixAMMv1_1])
5594 {
5595 // Ed offer is partially crossed.
5596 BEAST_EXPECT(expectOffers(
5597 env,
5598 ed,
5599 1,
5600 {{Amounts{
5601 STAmount{eth, UINT64_C(335'0889359326485), -13},
5602 STAmount{USD, UINT64_C(209'4305849579053), -13},
5603 }}}));
5604 BEAST_EXPECT(expectOffers(env, alice_, 0));
5605 }
5606 else
5607 {
5608 // Ed offer is partially crossed.
5609 BEAST_EXPECT(expectOffers(
5610 env,
5611 ed,
5612 1,
5613 {{Amounts{
5614 STAmount{eth, UINT64_C(335'0889359326475), -13},
5615 STAmount{USD, UINT64_C(209'4305849579047), -13},
5616 }}}));
5617 BEAST_EXPECT(expectOffers(env, alice_, 0));
5618 }
5619 }
5620 }
5621 }
5622
5623 // Strand selection
5624
5625 // Two book steps strand quality is 1.
5626 // AMM strand's best quality is equal to AMM's spot price
5627 // quality, which is 1. Both strands (steps) are adjusted
5628 // for the transfer fee in qualityUpperBound. In case
5629 // of two strands, AMM offers have better quality and are
5630 // consumed first, remaining liquidity is generated by CLOB
5631 // offers. Liquidity from two strands is better in this case
5632 // than in case of one strand with two book steps. Liquidity
5633 // from one strand with AMM has better quality than either one
5634 // strand with two book steps or two strands. It may appear
5635 // unintuitive, but one strand with AMM is optimized and
5636 // generates one AMM offer, while in case of two strands,
5637 // multiple AMM offers are generated, which results in slightly
5638 // worse overall quality.
5639 {
5641 for (auto i = 0; i < 3; ++i)
5642 {
5643 Env env(*this, features);
5644 prep(env, rates.first, rates.second);
5646
5647 if (i == 0 || i == 2)
5648 {
5649 env(offer(ed, eth(400), can(400)), Txflags(tfPassive));
5650 env(offer(ed, can(400), USD(400))), Txflags(tfPassive);
5651 env.close();
5652 }
5653
5654 if (i > 0)
5655 amm.emplace(env, ed, eth(1'000), USD(1'000));
5656
5657 env(pay(carol_, bob_, USD(100)),
5658 Path(~USD),
5659 Path(~can, ~USD),
5660 Sendmax(eth(600)));
5661 env.close();
5662
5663 BEAST_EXPECT(expectHolding(env, bob_, USD(2'100)));
5664
5665 if (i == 2 && !features[fixAMMv1_1])
5666 {
5667 if (rates.first == 1.5)
5668 {
5669 // Liquidity is consumed from AMM strand only
5670 BEAST_EXPECT(amm->expectBalances(
5671 STAmount{eth, UINT64_C(1'176'66038955758), -11},
5672 USD(850),
5673 amm->tokens()));
5674 }
5675 else
5676 {
5677 BEAST_EXPECT(amm->expectBalances(
5678 STAmount{eth, UINT64_C(1'179'540094339627), -12},
5679 STAmount{USD, UINT64_C(847'7880529867501), -13},
5680 amm->tokens()));
5681 BEAST_EXPECT(expectOffers(
5682 env,
5683 ed,
5684 2,
5685 {{Amounts{
5686 STAmount{eth, UINT64_C(343'3179205198749), -13},
5687 STAmount{can, UINT64_C(343'3179205198749), -13},
5688 },
5689 Amounts{
5690 STAmount{can, UINT64_C(362'2119470132499), -13},
5691 STAmount{USD, UINT64_C(362'2119470132499), -13},
5692 }}}));
5693 }
5694 }
5695 else if (i == 2)
5696 {
5697 if (rates.first == 1.5)
5698 {
5699 // Liquidity is consumed from AMM strand only
5700 BEAST_EXPECT(amm->expectBalances(
5701 STAmount{eth, UINT64_C(1'176'660389557593), -12},
5702 USD(850),
5703 amm->tokens()));
5704 }
5705 else
5706 {
5707 BEAST_EXPECT(amm->expectBalances(
5708 STAmount{eth, UINT64_C(1'179'54009433964), -11},
5709 STAmount{USD, UINT64_C(847'7880529867501), -13},
5710 amm->tokens()));
5711 BEAST_EXPECT(expectOffers(
5712 env,
5713 ed,
5714 2,
5715 {{Amounts{
5716 STAmount{eth, UINT64_C(343'3179205198749), -13},
5717 STAmount{can, UINT64_C(343'3179205198749), -13},
5718 },
5719 Amounts{
5720 STAmount{can, UINT64_C(362'2119470132499), -13},
5721 STAmount{USD, UINT64_C(362'2119470132499), -13},
5722 }}}));
5723 }
5724 }
5725 q[i] = Quality(
5726 Amounts{
5727 eth(2'000) - env.balance(carol_, eth),
5728 env.balance(bob_, USD) - USD(2'000)});
5729 }
5730 BEAST_EXPECT(q[1] > q[0]);
5731 BEAST_EXPECT(q[2] > q[0] && q[2] < q[1]);
5732 }
5733 }
5734 }
5735
5736 void
5738 {
5739 testcase("Fix Default Inner Object");
5740 using namespace jtx;
5741 FeatureBitset const all{testableAmendments()};
5742
5743 auto test = [&](FeatureBitset features,
5744 TER const& err1,
5745 TER const& err2,
5746 TER const& err3,
5747 TER const& err4,
5748 std::uint16_t tfee,
5749 bool closeLedger,
5750 std::optional<std::uint16_t> extra = std::nullopt) {
5751 Env env(*this, features);
5752 fund(env, gw_, {alice_}, XRP(1'000), {USD(10)});
5753 AMM amm(env, gw_, XRP(10), USD(10), {.tfee = tfee, .close = closeLedger});
5754 amm.deposit(alice_, USD(10), XRP(10));
5755 amm.vote(VoteArg{.account = alice_, .tfee = tfee, .err = Ter(err1)});
5756 amm.withdraw(WithdrawArg{.account = gw_, .asset1Out = USD(1), .err = Ter(err2)});
5757 // with the amendment disabled and ledger not closed,
5758 // second vote succeeds if the first vote sets the trading fee
5759 // to non-zero; if the first vote sets the trading fee to >0 &&
5760 // <9 then the second withdraw succeeds if the second vote sets
5761 // the trading fee so that the discounted fee is non-zero
5762 amm.vote(VoteArg{.account = alice_, .tfee = 20, .err = Ter(err3)});
5763 amm.withdraw(WithdrawArg{.account = gw_, .asset1Out = USD(2), .err = Ter(err4)});
5764 };
5765
5766 // ledger is closed after each transaction, vote/withdraw don't fail
5768 // ledger is not closed after each transaction
5769 test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 0, false);
5770 // if non-zero trading/discounted fee then vote/withdraw
5771 // don't fail whether the ledger is closed or not
5772 test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 10, true);
5773 test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 10, false);
5774 // non-zero trading fee but discounted fee is 0, vote doesn't fail
5775 // but withdraw fails
5776 test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 9, false);
5777 }
5778
5779 void
5781 {
5782 testcase("Fix changeSpotPriceQuality");
5783 using namespace jtx;
5784
5785 std::string logs;
5786
5787 enum class Status {
5788 SucceedShouldSucceedResize, // Succeed in pre-fix because
5789 // error allowance, succeed post-fix
5790 // because of offer resizing
5791 FailShouldSucceed, // Fail in pre-fix due to rounding,
5792 // succeed after fix because of XRP
5793 // side is generated first
5794 SucceedShouldFail, // Succeed in pre-fix, fail after fix
5795 // due to small quality difference
5796 Fail, // Both fail because the quality can't be matched
5797 Succeed // Both succeed
5798 };
5799 using enum Status;
5800 auto const xrpIouAmounts10100 = TAmounts{XRPAmount{10}, IOUAmount{100}};
5801 auto const iouXrpAmounts10100 = TAmounts{IOUAmount{10}, XRPAmount{100}};
5802 // clang-format off
5804 //Pool In , Pool Out, Quality , Fee, Status
5805 {"0.001519763260828713", "1558701", Quality{5414253689393440221}, 1000, FailShouldSucceed},
5806 {"0.01099814367603737", "1892611", Quality{5482264816516900274}, 1000, FailShouldSucceed},
5807 {"0.78", "796599", Quality{5630392334958379008}, 1000, FailShouldSucceed},
5808 {"105439.2955578965", "49398693", Quality{5910869983721805038}, 400, FailShouldSucceed},
5809 {"12408293.23445213", "4340810521", Quality{5911611095910090752}, 997, FailShouldSucceed},
5810 {"1892611", "0.01099814367603737", Quality{6703103457950430139}, 1000, FailShouldSucceed},
5811 {"423028.8508101858", "3392804520", Quality{5837920340654162816}, 600, FailShouldSucceed},
5812 {"44565388.41001027", "73890647", Quality{6058976634606450001}, 1000, FailShouldSucceed},
5813 {"66831.68494832662", "16", Quality{6346111134641742975}, 0, FailShouldSucceed},
5814 {"675.9287302203422", "1242632304", Quality{5625960929244093294}, 300, FailShouldSucceed},
5815 {"7047.112186735699", "1649845866", Quality{5696855348026306945}, 504, FailShouldSucceed},
5816 {"840236.4402981238", "47419053", Quality{5982561601648018688}, 499, FailShouldSucceed},
5817 {"992715.618909774", "189445631733", Quality{5697835648288106944}, 815, SucceedShouldSucceedResize},
5818 {"504636667521", "185545883.9506651", Quality{6343802275337659280}, 503, SucceedShouldSucceedResize},
5819 {"992706.7218636649", "189447316000", Quality{5697835648288106944}, 797, SucceedShouldSucceedResize},
5820 {"1.068737911388205", "127860278877", Quality{5268604356368739396}, 293, SucceedShouldSucceedResize},
5821 {"17932506.56880419", "189308.6043676173", Quality{6206460598195440068}, 311, SucceedShouldSucceedResize},
5822 {"1.066379294658174", "128042251493", Quality{5268559341368739328}, 270, SucceedShouldSucceedResize},
5823 {"350131413924", "1576879.110907892", Quality{6487411636539049449}, 650, Fail},
5824 {"422093460", "2.731797662057464", Quality{6702911108534394924}, 1000, Fail},
5825 {"76128132223", "367172.7148422662", Quality{6487263463413514240}, 548, Fail},
5826 {"132701839250", "280703770.7695443", Quality{6273750681188885075}, 562, Fail},
5827 {"994165.7604612011", "189551302411", Quality{5697835592690668727}, 815, Fail},
5828 {"45053.33303227917", "86612695359", Quality{5625695218943638190}, 500, Fail},
5829 {"199649.077043865", "14017933007", Quality{5766034667318524880}, 324, Fail},
5830 {"27751824831.70903", "78896950", Quality{6272538159621630432}, 500, Fail},
5831 {"225.3731275781907", "156431793648", Quality{5477818047604078924}, 989, Fail},
5832 {"199649.077043865", "14017933007", Quality{5766036094462806309}, 324, Fail},
5833 {"3.590272027140361", "20677643641", Quality{5406056147042156356}, 808, Fail},
5834 {"1.070884664490231", "127604712776", Quality{5268620608623825741}, 293, Fail},
5835 {"3272.448829820197", "6275124076", Quality{5625710328924117902}, 81, Fail},
5836 {"0.009059512633902926", "7994028", Quality{5477511954775533172}, 1000, Fail},
5837 {"1", "1.0", Quality{0}, 100, Fail},
5838 {"1.0", "1", Quality{0}, 100, Fail},
5839 {"10", "10.0", Quality{xrpIouAmounts10100}, 100, Fail},
5840 {"10.0", "10", Quality{iouXrpAmounts10100}, 100, Fail},
5841 {"69864389131", "287631.4543025075", Quality{6487623473313516078}, 451, Succeed},
5842 {"4328342973", "12453825.99247381", Quality{6272522264364865181}, 997, Succeed},
5843 {"32347017", "7003.93031579449", Quality{6347261126087916670}, 1000, Succeed},
5844 {"61697206161", "36631.4583206413", Quality{6558965195382476659}, 500, Succeed},
5845 {"1654524979", "7028.659825511603", Quality{6487551345110052981}, 504, Succeed},
5846 {"88621.22277293179", "5128418948", Quality{5766347291552869205}, 380, Succeed},
5847 {"1892611", "0.01099814367603737", Quality{6703102780512015436}, 1000, Succeed},
5848 {"4542.639373338766", "24554809", Quality{5838994982188783710}, 0, Succeed},
5849 {"5132932546", "88542.99750172683", Quality{6419203342950054537}, 380, Succeed},
5850 {"78929964.1549083", "1506494795", Quality{5986890029845558688}, 589, Succeed},
5851 {"10096561906", "44727.72453735605", Quality{6487455290284644551}, 250, Succeed},
5852 {"5092.219565514988", "8768257694", Quality{5626349534958379008}, 503, Succeed},
5853 {"1819778294", "8305.084302902864", Quality{6487429398998540860}, 415, Succeed},
5854 {"6970462.633911943", "57359281", Quality{6054087899185946624}, 850, Succeed},
5855 {"3983448845", "2347.543644281467", Quality{6558965195382476659}, 856, Succeed},
5856 // This is a tiny offer 12drops/19321952e-15 it succeeds pre-amendment because of the error allowance.
5857 // Post amendment it is resized to 11drops/17711789e-15 but the quality is still less than
5858 // the target quality and the offer fails.
5859 {"771493171", "1.243473020567508", Quality{6707566798038544272}, 100, SucceedShouldFail},
5860 };
5861 // clang-format on
5862
5863 boost::regex const rx("^\\d+$");
5864 boost::smatch match;
5865 // tests that succeed should have the same amounts pre-fix and post-fix
5866 std::vector<std::pair<STAmount, STAmount>> const successAmounts;
5867 Env const env(*this, features, std::make_unique<CaptureLogs>(&logs));
5868 auto rules = env.current()->rules();
5869 CurrentTransactionRulesGuard const rg(rules);
5871
5872 for (auto const& t : tests)
5873 {
5874 auto getPool = [&](std::string const& v, bool isXRP) {
5875 if (isXRP)
5876 return amountFromString(xrpIssue(), v);
5877 return amountFromString(noIssue(), v);
5878 };
5879 auto const& quality = std::get<Quality>(t);
5880 auto const tfee = std::get<std::uint16_t>(t);
5881 auto const status = std::get<Status>(t);
5882 auto const poolInIsXRP = boost::regex_search(std::get<0>(t), match, rx);
5883 auto const poolOutIsXRP = boost::regex_search(std::get<1>(t), match, rx);
5884 assert(!(poolInIsXRP && poolOutIsXRP));
5885 auto const poolIn = getPool(std::get<0>(t), poolInIsXRP);
5886 auto const poolOut = getPool(std::get<1>(t), poolOutIsXRP);
5887 try
5888 {
5889 auto const amounts = changeSpotPriceQuality(
5890 Amounts{poolIn, poolOut}, quality, tfee, env.current()->rules(), env.journal);
5891 if (amounts)
5892 {
5893 if (status == SucceedShouldSucceedResize)
5894 {
5895 if (!features[fixAMMv1_1])
5896 {
5897 BEAST_EXPECT(Quality{*amounts} < quality);
5898 }
5899 else
5900 {
5901 BEAST_EXPECT(Quality{*amounts} >= quality);
5902 }
5903 }
5904 else if (status == Succeed)
5905 {
5906 if (!features[fixAMMv1_1])
5907 {
5908 BEAST_EXPECT(
5909 Quality{*amounts} >= quality ||
5910 withinRelativeDistance(Quality{*amounts}, quality, Number{1, -7}));
5911 }
5912 else
5913 {
5914 BEAST_EXPECT(Quality{*amounts} >= quality);
5915 }
5916 }
5917 else if (status == FailShouldSucceed)
5918 {
5919 BEAST_EXPECT(features[fixAMMv1_1] && Quality{*amounts} >= quality);
5920 }
5921 else if (status == SucceedShouldFail)
5922 {
5923 BEAST_EXPECT(
5924 !features[fixAMMv1_1] && Quality{*amounts} < quality &&
5925 withinRelativeDistance(Quality{*amounts}, quality, Number{1, -7}));
5926 }
5927 }
5928 else
5929 {
5930 // Fails pre- and post-amendment because the quality can't
5931 // be matched. Verify by generating a tiny offer, which
5932 // doesn't match the quality. Exclude zero quality since
5933 // no offer is generated in this case.
5934 if (status == Fail && quality != Quality{0})
5935 {
5936 auto tinyOffer = [&]() {
5937 if (isXRP(poolIn))
5938 {
5939 auto const takerPays = STAmount{xrpIssue(), 1};
5940 return Amounts{
5941 takerPays,
5942 swapAssetIn(Amounts{poolIn, poolOut}, takerPays, tfee)};
5943 }
5944 if (isXRP(poolOut))
5945 {
5946 auto const takerGets = STAmount{xrpIssue(), 1};
5947 return Amounts{
5948 swapAssetOut(Amounts{poolIn, poolOut}, takerGets, tfee),
5949 takerGets};
5950 }
5951 auto const takerPays =
5952 toAmount<STAmount>(getAsset(poolIn), Number{1, -10} * poolIn);
5953 return Amounts{
5954 takerPays, swapAssetIn(Amounts{poolIn, poolOut}, takerPays, tfee)};
5955 }();
5956 BEAST_EXPECT(Quality(tinyOffer) < quality);
5957 }
5958 else if (status == FailShouldSucceed)
5959 {
5960 BEAST_EXPECT(!features[fixAMMv1_1]);
5961 }
5962 else if (status == SucceedShouldFail)
5963 {
5964 BEAST_EXPECT(features[fixAMMv1_1]);
5965 }
5966 }
5967 }
5968 catch (std::runtime_error const& e)
5969 {
5970 BEAST_EXPECT(!strcmp(e.what(), "changeSpotPriceQuality failed"));
5971 BEAST_EXPECT(!features[fixAMMv1_1] && status == FailShouldSucceed);
5972 }
5973 }
5974
5975 // Test negative discriminant
5976 {
5977 // b**2 - 4 * a * c -> 1 * 1 - 4 * 1 * 1 = -3
5978 auto const res = solveQuadraticEqSmallest(Number{1}, Number{1}, Number{1});
5979 BEAST_EXPECT(!res.has_value());
5980 }
5981 }
5982
5983 void
5985 {
5986 using namespace jtx;
5987
5988 testAMM([&](AMM& ammAlice, Env& env) {
5989 WithdrawArg const args{
5990 .flags = tfSingleAsset,
5991 .err = Ter(temMALFORMED),
5992 };
5993 ammAlice.withdraw(args);
5994 });
5995
5996 testAMM([&](AMM& ammAlice, Env& env) {
5997 WithdrawArg const args{
5998 .flags = tfOneAssetLPToken,
5999 .err = Ter(temMALFORMED),
6000 };
6001 ammAlice.withdraw(args);
6002 });
6003
6004 testAMM([&](AMM& ammAlice, Env& env) {
6005 WithdrawArg const args{
6006 .flags = tfLimitLPToken,
6007 .err = Ter(temMALFORMED),
6008 };
6009 ammAlice.withdraw(args);
6010 });
6011
6012 testAMM([&](AMM& ammAlice, Env& env) {
6013 WithdrawArg const args{
6014 .asset1Out = XRP(100),
6015 .asset2Out = XRP(100),
6016 .err = Ter(temBAD_AMM_TOKENS),
6017 };
6018 ammAlice.withdraw(args);
6019 });
6020
6021 testAMM([&](AMM& ammAlice, Env& env) {
6022 WithdrawArg const args{
6023 .asset1Out = XRP(100),
6024 .asset2Out = BAD(100),
6025 .err = Ter(temBAD_CURRENCY),
6026 };
6027 ammAlice.withdraw(args);
6028 });
6029
6030 testAMM([&](AMM& ammAlice, Env& env) {
6031 json::Value jv;
6032 jv[jss::TransactionType] = jss::AMMWithdraw;
6033 jv[jss::Flags] = tfLimitLPToken;
6034 jv[jss::Account] = alice_.human();
6035 ammAlice.setTokens(jv);
6036 XRP(100).value().setJson(jv[jss::Amount]);
6037 USD(100).value().setJson(jv[jss::EPrice]);
6038 env(jv, Ter(temBAD_AMM_TOKENS));
6039 });
6040 }
6041
6042 void
6043 // NOLINTNEXTLINE(readability-convert-member-functions-to-static)
6045 {
6046 using namespace jtx;
6047 using namespace std::chrono;
6048 FeatureBitset const all{featuresInitial};
6049
6050 std::string logs;
6051
6052 Account const gatehub{"gatehub"};
6053 Account const bitstamp{"bitstamp"};
6054 Account const trader{"trader"};
6055 auto const usdGH = gatehub["USD"];
6056 auto const btcGH = gatehub["BTC"];
6057 auto const usdBIT = bitstamp["USD"];
6058
6059 struct InputSet
6060 {
6061 char const* testCase;
6062 double const poolUsdBIT;
6063 double const poolUsdGH;
6064 Sendmax const sendMaxUsdBIT;
6065 STAmount const sendUsdGH;
6066 STAmount const failUsdGH;
6067 STAmount const failUsdGHr;
6068 STAmount const failUsdBIT;
6069 STAmount const failUsdBITr;
6070 STAmount const goodUsdGH;
6071 STAmount const goodUsdGHr;
6072 STAmount const goodUsdBIT;
6073 STAmount const goodUsdBITr;
6074 IOUAmount const lpTokenBalance;
6075 std::optional<IOUAmount> const lpTokenBalanceAlt = std::nullopt;
6076 double const offer1BtcGH = 0.1;
6077 double const offer2BtcGH = 0.1;
6078 double const offer2UsdGH = 1;
6079 double const rateBIT = 0.0;
6080 double const rateGH = 0.0;
6081 };
6082
6083 using uint64_t = std::uint64_t;
6084
6085 for (auto const& input : {
6086 InputSet{
6087 .testCase = "Test Fix Overflow Offer", //
6088 .poolUsdBIT = 3, //
6089 .poolUsdGH = 273, //
6090 .sendMaxUsdBIT{usdBIT(50)}, //
6091 .sendUsdGH{usdGH, uint64_t(272'455089820359), -12}, //
6092 .failUsdGH = STAmount{0}, //
6093 .failUsdGHr = STAmount{0}, //
6094 .failUsdBIT{usdBIT, uint64_t(46'47826086956522), -14}, //
6095 .failUsdBITr{usdBIT, uint64_t(46'47826086956521), -14}, //
6096 .goodUsdGH{usdGH, uint64_t(96'7543114220382), -13}, //
6097 .goodUsdGHr{usdGH, uint64_t(96'7543114222965), -13}, //
6098 .goodUsdBIT{usdBIT, uint64_t(8'464739069120721), -15}, //
6099 .goodUsdBITr{usdBIT, uint64_t(8'464739069098152), -15}, //
6100 .lpTokenBalance = {28'61817604250837, -14}, //
6101 .lpTokenBalanceAlt = IOUAmount{28'61817604250836, -14}, //
6102 .offer1BtcGH = 0.1, //
6103 .offer2BtcGH = 0.1, //
6104 .offer2UsdGH = 1, //
6105 .rateBIT = 1.15, //
6106 .rateGH = 1.2, //
6107 },
6108 InputSet{
6109 .testCase = "Overflow test {1, 100, 0.111}", //
6110 .poolUsdBIT = 1, //
6111 .poolUsdGH = 100, //
6112 .sendMaxUsdBIT{usdBIT(0.111)}, //
6113 .sendUsdGH{usdGH, 100}, //
6114 .failUsdGH = STAmount{0}, //
6115 .failUsdGHr = STAmount{0}, //
6116 .failUsdBIT{usdBIT, uint64_t(1'111), -3}, //
6117 .failUsdBITr{usdBIT, uint64_t(1'111), -3}, //
6118 .goodUsdGH{usdGH, uint64_t(90'04347888284115), -14}, //
6119 .goodUsdGHr{usdGH, uint64_t(90'04347888284201), -14}, //
6120 .goodUsdBIT{usdBIT, uint64_t(1'111), -3}, //
6121 .goodUsdBITr{usdBIT, uint64_t(1'111), -3}, //
6122 .lpTokenBalance{10, 0}, //
6123 .offer1BtcGH = 1e-5, //
6124 .offer2BtcGH = 1, //
6125 .offer2UsdGH = 1e-5, //
6126 .rateBIT = 0, //
6127 .rateGH = 0, //
6128 },
6129 InputSet{
6130 .testCase = "Overflow test {1, 100, 1.00}", //
6131 .poolUsdBIT = 1, //
6132 .poolUsdGH = 100, //
6133 .sendMaxUsdBIT{usdBIT(1.00)}, //
6134 .sendUsdGH{usdGH, 100}, //
6135 .failUsdGH = STAmount{0}, //
6136 .failUsdGHr = STAmount{0}, //
6137 .failUsdBIT{usdBIT, uint64_t(2), 0}, //
6138 .failUsdBITr{usdBIT, uint64_t(2), 0}, //
6139 .goodUsdGH{usdGH, uint64_t(52'94379354424079), -14}, //
6140 .goodUsdGHr{usdGH, uint64_t(52'94379354424135), -14}, //
6141 .goodUsdBIT{usdBIT, uint64_t(2), 0}, //
6142 .goodUsdBITr{usdBIT, uint64_t(2), 0}, //
6143 .lpTokenBalance{10, 0}, //
6144 .offer1BtcGH = 1e-5, //
6145 .offer2BtcGH = 1, //
6146 .offer2UsdGH = 1e-5, //
6147 .rateBIT = 0, //
6148 .rateGH = 0, //
6149 },
6150 InputSet{
6151 .testCase = "Overflow test {1, 100, 4.6432}", //
6152 .poolUsdBIT = 1, //
6153 .poolUsdGH = 100, //
6154 .sendMaxUsdBIT{usdBIT(4.6432)}, //
6155 .sendUsdGH{usdGH, 100}, //
6156 .failUsdGH = STAmount{0}, //
6157 .failUsdGHr = STAmount{0}, //
6158 .failUsdBIT{usdBIT, uint64_t(5'6432), -4}, //
6159 .failUsdBITr{usdBIT, uint64_t(5'6432), -4}, //
6160 .goodUsdGH{usdGH, uint64_t(35'44113971506987), -14}, //
6161 .goodUsdGHr{usdGH, uint64_t(35'44113971506987), -14}, //
6162 .goodUsdBIT{usdBIT, uint64_t(2'821579689703915), -15}, //
6163 .goodUsdBITr{usdBIT, uint64_t(2'821579689703954), -15}, //
6164 .lpTokenBalance{10, 0}, //
6165 .offer1BtcGH = 1e-5, //
6166 .offer2BtcGH = 1, //
6167 .offer2UsdGH = 1e-5, //
6168 .rateBIT = 0, //
6169 .rateGH = 0, //
6170 },
6171 InputSet{
6172 .testCase = "Overflow test {1, 100, 10}", //
6173 .poolUsdBIT = 1, //
6174 .poolUsdGH = 100, //
6175 .sendMaxUsdBIT{usdBIT(10)}, //
6176 .sendUsdGH{usdGH, 100}, //
6177 .failUsdGH = STAmount{0}, //
6178 .failUsdGHr = STAmount{0}, //
6179 .failUsdBIT{usdBIT, uint64_t(11), 0}, //
6180 .failUsdBITr{usdBIT, uint64_t(11), 0}, //
6181 .goodUsdGH{usdGH, uint64_t(35'44113971506987), -14}, //
6182 .goodUsdGHr{usdGH, uint64_t(35'44113971506987), -14}, //
6183 .goodUsdBIT{usdBIT, uint64_t(2'821579689703915), -15}, //
6184 .goodUsdBITr{usdBIT, uint64_t(2'821579689703954), -15}, //
6185 .lpTokenBalance{10, 0}, //
6186 .offer1BtcGH = 1e-5, //
6187 .offer2BtcGH = 1, //
6188 .offer2UsdGH = 1e-5, //
6189 .rateBIT = 0, //
6190 .rateGH = 0, //
6191 },
6192 InputSet{
6193 .testCase = "Overflow test {50, 100, 5.55}", //
6194 .poolUsdBIT = 50, //
6195 .poolUsdGH = 100, //
6196 .sendMaxUsdBIT{usdBIT(5.55)}, //
6197 .sendUsdGH{usdGH, 100}, //
6198 .failUsdGH = STAmount{0}, //
6199 .failUsdGHr = STAmount{0}, //
6200 .failUsdBIT{usdBIT, uint64_t(55'55), -2}, //
6201 .failUsdBITr{usdBIT, uint64_t(55'55), -2}, //
6202 .goodUsdGH{usdGH, uint64_t(90'04347888284113), -14}, //
6203 .goodUsdGHr{usdGH, uint64_t(90'0434788828413), -13}, //
6204 .goodUsdBIT{usdBIT, uint64_t(55'55), -2}, //
6205 .goodUsdBITr{usdBIT, uint64_t(55'55), -2}, //
6206 .lpTokenBalance{uint64_t(70'71067811865475), -14}, //
6207 .offer1BtcGH = 1e-5, //
6208 .offer2BtcGH = 1, //
6209 .offer2UsdGH = 1e-5, //
6210 .rateBIT = 0, //
6211 .rateGH = 0, //
6212 },
6213 InputSet{
6214 .testCase = "Overflow test {50, 100, 50.00}", //
6215 .poolUsdBIT = 50, //
6216 .poolUsdGH = 100, //
6217 .sendMaxUsdBIT{usdBIT(50.00)}, //
6218 .sendUsdGH{usdGH, 100}, //
6219 .failUsdGH{usdGH, uint64_t(52'94379354424081), -14}, //
6220 .failUsdGHr{usdGH, uint64_t(52'94379354424092), -14}, //
6221 .failUsdBIT{usdBIT, uint64_t(100), 0}, //
6222 .failUsdBITr{usdBIT, uint64_t(100), 0}, //
6223 .goodUsdGH{usdGH, uint64_t(52'94379354424081), -14}, //
6224 .goodUsdGHr{usdGH, uint64_t(52'94379354424092), -14}, //
6225 .goodUsdBIT{usdBIT, uint64_t(100), 0}, //
6226 .goodUsdBITr{usdBIT, uint64_t(100), 0}, //
6227 .lpTokenBalance{uint64_t(70'71067811865475), -14}, //
6228 .offer1BtcGH = 1e-5, //
6229 .offer2BtcGH = 1, //
6230 .offer2UsdGH = 1e-5, //
6231 .rateBIT = 0, //
6232 .rateGH = 0, //
6233 },
6234 InputSet{
6235 .testCase = "Overflow test {50, 100, 232.16}", //
6236 .poolUsdBIT = 50, //
6237 .poolUsdGH = 100, //
6238 .sendMaxUsdBIT{usdBIT(232.16)}, //
6239 .sendUsdGH{usdGH, 100}, //
6240 .failUsdGH = STAmount{0}, //
6241 .failUsdGHr = STAmount{0}, //
6242 .failUsdBIT{usdBIT, uint64_t(282'16), -2}, //
6243 .failUsdBITr{usdBIT, uint64_t(282'16), -2}, //
6244 .goodUsdGH{usdGH, uint64_t(35'44113971506987), -14}, //
6245 .goodUsdGHr{usdGH, uint64_t(35'44113971506987), -14}, //
6246 .goodUsdBIT{usdBIT, uint64_t(141'0789844851958), -13}, //
6247 .goodUsdBITr{usdBIT, uint64_t(141'0789844851962), -13}, //
6248 .lpTokenBalance{70'71067811865475, -14}, //
6249 .offer1BtcGH = 1e-5, //
6250 .offer2BtcGH = 1, //
6251 .offer2UsdGH = 1e-5, //
6252 .rateBIT = 0, //
6253 .rateGH = 0, //
6254 },
6255 InputSet{
6256 .testCase = "Overflow test {50, 100, 500}", //
6257 .poolUsdBIT = 50, //
6258 .poolUsdGH = 100, //
6259 .sendMaxUsdBIT{usdBIT(500)}, //
6260 .sendUsdGH{usdGH, 100}, //
6261 .failUsdGH = STAmount{0}, //
6262 .failUsdGHr = STAmount{0}, //
6263 .failUsdBIT{usdBIT, uint64_t(550), 0}, //
6264 .failUsdBITr{usdBIT, uint64_t(550), 0}, //
6265 .goodUsdGH{usdGH, uint64_t(35'44113971506987), -14}, //
6266 .goodUsdGHr{usdGH, uint64_t(35'44113971506987), -14}, //
6267 .goodUsdBIT{usdBIT, uint64_t(141'0789844851958), -13}, //
6268 .goodUsdBITr{usdBIT, uint64_t(141'0789844851962), -13}, //
6269 .lpTokenBalance{70'71067811865475, -14}, //
6270 .offer1BtcGH = 1e-5, //
6271 .offer2BtcGH = 1, //
6272 .offer2UsdGH = 1e-5, //
6273 .rateBIT = 0, //
6274 .rateGH = 0, //
6275 },
6276 })
6277 {
6278 testcase(input.testCase);
6279 for (auto const& features : {all - fixAMMv1_1 - fixAMMv1_3, all})
6280 {
6281 Env env(*this, features, std::make_unique<CaptureLogs>(&logs));
6282
6283 env.fund(XRP(5'000), gatehub, bitstamp, trader);
6284 env.close();
6285
6286 if (input.rateGH != 0.0)
6287 env(rate(gatehub, input.rateGH));
6288 if (input.rateBIT != 0.0)
6289 env(rate(bitstamp, input.rateBIT));
6290
6291 env(trust(trader, usdGH(10'000'000)));
6292 env(trust(trader, usdBIT(10'000'000)));
6293 env(trust(trader, btcGH(10'000'000)));
6294 env.close();
6295
6296 env(pay(gatehub, trader, usdGH(100'000)));
6297 env(pay(gatehub, trader, btcGH(100'000)));
6298 env(pay(bitstamp, trader, usdBIT(100'000)));
6299 env.close();
6300
6301 AMM const amm{env, trader, usdGH(input.poolUsdGH), usdBIT(input.poolUsdBIT)};
6302 env.close();
6303
6304 IOUAmount const preSwapLPTokenBalance = amm.getLPTokensBalance();
6305
6306 env(offer(trader, usdBIT(1), btcGH(input.offer1BtcGH)));
6307 env(offer(trader, btcGH(input.offer2BtcGH), usdGH(input.offer2UsdGH)));
6308 env.close();
6309
6310 env(pay(trader, trader, input.sendUsdGH),
6311 Path(~usdGH),
6312 Path(~btcGH, ~usdGH),
6313 Sendmax(input.sendMaxUsdBIT),
6314 Txflags(tfPartialPayment));
6315 env.close();
6316
6317 auto const failUsdGH = features[fixAMMv1_1] ? input.failUsdGHr : input.failUsdGH;
6318 auto const failUsdBIT = features[fixAMMv1_1] ? input.failUsdBITr : input.failUsdBIT;
6319 auto const goodUsdGH = features[fixAMMv1_1] ? input.goodUsdGHr : input.goodUsdGH;
6320 auto const goodUsdBIT = features[fixAMMv1_1] ? input.goodUsdBITr : input.goodUsdBIT;
6321 auto const lpTokenBalance = [&] {
6322 if (not env.enabled(fixAMMv1_3))
6323 return input.lpTokenBalance;
6324
6325 return input.lpTokenBalanceAlt.value_or(input.lpTokenBalance);
6326 }();
6327
6328 {
6329 BEAST_EXPECT(amm.expectBalances(goodUsdGH, goodUsdBIT, lpTokenBalance));
6330
6331 // Invariant: LPToken balance must not change in a
6332 // payment or a swap transaction
6333 BEAST_EXPECT(amm.getLPTokensBalance() == preSwapLPTokenBalance);
6334
6335 // Invariant: The square root of (product of the pool
6336 // balances) must be at least the LPTokenBalance
6337 Number const sqrtPoolProduct = root2(goodUsdGH * goodUsdBIT);
6338
6339 // Include a tiny tolerance for the test cases using
6340 // .goodUsdGH{usdGH, uint64_t(35'44113971506987),
6341 // -14}, .goodUsdBIT{usdBIT,
6342 // uint64_t(2'821579689703915), -15},
6343 // These two values multiply
6344 // to 99.99999999999994227040383754105 which gets
6345 // internally rounded to 100, due to representation
6346 // error.
6347 BEAST_EXPECT((sqrtPoolProduct + Number{1, -14} >= input.lpTokenBalance));
6348 }
6349 }
6350 }
6351 }
6352
6353 void
6355 {
6356 testcase("swapRounding");
6357 using namespace jtx;
6358
6359 STAmount const xrpPool{XRP, UINT64_C(51600'000981)};
6360 STAmount const iouPool{USD, UINT64_C(803040'9987141784), -10};
6361
6362 STAmount const xrpBob{XRP, UINT64_C(1092'878933)};
6363 STAmount const iouBob{USD, UINT64_C(3'988035892323031), -28}; // 3.9...e-13
6364
6365 testAMM(
6366 [&](AMM& amm, Env& env) {
6367 // Check our AMM starting conditions.
6368 auto [xrpBegin, iouBegin, lptBegin] = amm.balances(XRP, USD);
6369
6370 // Set Bob's starting conditions.
6371 env.fund(xrpBob, bob_);
6372 env.trust(USD(1'000'000), bob_);
6373 env(pay(gw_, bob_, iouBob));
6374 env.close();
6375
6376 env(offer(bob_, XRP(6300), USD(100'000)));
6377 env.close();
6378
6379 // Assert that AMM is unchanged.
6380 BEAST_EXPECT(amm.expectBalances(xrpBegin, iouBegin, amm.tokens()));
6381 },
6382 {{xrpPool, iouPool}},
6383 889,
6384 std::nullopt,
6385 {testableAmendments() | fixAMMv1_1});
6386 }
6387
6388 void
6390 {
6391 testcase("AMM Offer Blocked By LOB");
6392 using namespace jtx;
6393
6394 // Low quality LOB offer blocks AMM liquidity
6395
6396 // USD/XRP crosses AMM
6397 {
6398 Env env(*this, features);
6399
6400 fund(env, gw_, {alice_, carol_}, XRP(1'000'000), {USD(1'000'000)});
6401 // This offer blocks AMM offer in pre-amendment
6402 env(offer(alice_, XRP(1), USD(0.01)));
6403 env.close();
6404
6405 AMM const amm(env, gw_, XRP(200'000), USD(100'000));
6406
6407 // The offer doesn't cross AMM in pre-amendment code
6408 // It crosses AMM in post-amendment code
6409 env(offer(carol_, USD(0.49), XRP(1)));
6410 env.close();
6411
6412 if (!features[fixAMMv1_1])
6413 {
6414 BEAST_EXPECT(amm.expectBalances(XRP(200'000), USD(100'000), amm.tokens()));
6415 BEAST_EXPECT(expectOffers(env, alice_, 1, {{Amounts{XRP(1), USD(0.01)}}}));
6416 // Carol's offer is blocked by alice_'s offer
6417 BEAST_EXPECT(expectOffers(env, carol_, 1, {{Amounts{USD(0.49), XRP(1)}}}));
6418 }
6419 else
6420 {
6421 BEAST_EXPECT(
6422 amm.expectBalances(XRPAmount(200'000'980'005), USD(99'999.51), amm.tokens()));
6423 BEAST_EXPECT(expectOffers(env, alice_, 1, {{Amounts{XRP(1), USD(0.01)}}}));
6424 // Carol's offer crosses AMM
6425 BEAST_EXPECT(expectOffers(env, carol_, 0));
6426 }
6427 }
6428
6429 // There is no blocking offer, the same AMM liquidity is consumed
6430 // pre- and post-amendment.
6431 {
6432 Env env(*this, features);
6433
6434 fund(env, gw_, {alice_, carol_}, XRP(1'000'000), {USD(1'000'000)});
6435 // There is no blocking offer
6436 // env(offer(alice_, XRP(1), USD(0.01)));
6437
6438 AMM const amm(env, gw_, XRP(200'000), USD(100'000));
6439
6440 // The offer crosses AMM
6441 env(offer(carol_, USD(0.49), XRP(1)));
6442 env.close();
6443
6444 // The same result as with the blocking offer
6445 BEAST_EXPECT(
6446 amm.expectBalances(XRPAmount(200'000'980'005), USD(99'999.51), amm.tokens()));
6447 // Carol's offer crosses AMM
6448 BEAST_EXPECT(expectOffers(env, carol_, 0));
6449 }
6450
6451 // XRP/USD crosses AMM
6452 {
6453 Env env(*this, features);
6454 fund(env, gw_, {alice_, carol_, bob_}, XRP(10'000), {USD(1'000)});
6455
6456 // This offer blocks AMM offer in pre-amendment
6457 // It crosses AMM in post-amendment code
6458 env(offer(bob_, USD(1), XRPAmount(500)));
6459 env.close();
6460 AMM const amm(env, alice_, XRP(1'000), USD(500));
6461 env(offer(carol_, XRP(100), USD(55)));
6462 env.close();
6463 if (!features[fixAMMv1_1])
6464 {
6465 BEAST_EXPECT(amm.expectBalances(XRP(1'000), USD(500), amm.tokens()));
6466 BEAST_EXPECT(expectOffers(env, bob_, 1, {{Amounts{USD(1), XRPAmount(500)}}}));
6467 BEAST_EXPECT(expectOffers(env, carol_, 1, {{Amounts{XRP(100), USD(55)}}}));
6468 }
6469 else if (!features[featureMPTokensV2])
6470 {
6471 BEAST_EXPECT(amm.expectBalances(
6472 XRPAmount(909'090'909),
6473 STAmount{USD, UINT64_C(550'000000055), -9},
6474 amm.tokens()));
6475 BEAST_EXPECT(expectOffers(
6476 env,
6477 carol_,
6478 1,
6479 {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}}));
6480 BEAST_EXPECT(expectOffers(env, bob_, 1, {{Amounts{USD(1), XRPAmount(500)}}}));
6481 }
6482 else
6483 {
6484 BEAST_EXPECT(amm.expectBalances(
6485 XRPAmount(909'090'910),
6486 STAmount{USD, UINT64_C(549'99999945), -8},
6487 amm.tokens()));
6488 BEAST_EXPECT(expectOffers(
6489 env,
6490 carol_,
6491 1,
6492 {{Amounts{XRPAmount{9'090'910}, STAmount{USD, 5'0000005, -7}}}}));
6493 BEAST_EXPECT(expectOffers(env, bob_, 1, {{Amounts{USD(1), XRPAmount(500)}}}));
6494 }
6495 }
6496
6497 // There is no blocking offer, the same AMM liquidity is consumed
6498 // pre- and post-amendment.
6499 {
6500 Env env(*this, features);
6501 fund(env, gw_, {alice_, carol_, bob_}, XRP(10'000), {USD(1'000)});
6502
6503 AMM const amm(env, alice_, XRP(1'000), USD(500));
6504 env(offer(carol_, XRP(100), USD(55)));
6505 env.close();
6506 if (!features[featureMPTokensV2])
6507 {
6508 BEAST_EXPECT(amm.expectBalances(
6509 XRPAmount(909'090'909),
6510 STAmount{USD, UINT64_C(550'000000055), -9},
6511 amm.tokens()));
6512 BEAST_EXPECT(expectOffers(
6513 env,
6514 carol_,
6515 1,
6516 {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}}));
6517 }
6518 else
6519 {
6520 BEAST_EXPECT(amm.expectBalances(
6521 XRPAmount(909'090'910),
6522 STAmount{USD, UINT64_C(549'99999945), -8},
6523 amm.tokens()));
6524 BEAST_EXPECT(expectOffers(
6525 env,
6526 carol_,
6527 1,
6528 {{Amounts{XRPAmount{9'090'910}, STAmount{USD, 5'0000005, -7}}}}));
6529 }
6530 }
6531 }
6532
6533 void
6535 {
6536 testcase("LPToken Balance");
6537 using namespace jtx;
6538
6539 // Last Liquidity Provider is the issuer of one token
6540 {
6541 std::string logs;
6542 Env env(*this, features, std::make_unique<CaptureLogs>(&logs));
6543 fund(env, gw_, {alice_, carol_}, XRP(1'000'000'000), {USD(1'000'000'000)});
6544 AMM amm(env, gw_, XRP(2), USD(1));
6545 amm.deposit(alice_, IOUAmount{1'876123487565916, -15});
6546 amm.deposit(carol_, IOUAmount{1'000'000});
6547 amm.withdrawAll(alice_);
6548 BEAST_EXPECT(amm.expectLPTokens(alice_, IOUAmount{0}));
6549 amm.withdrawAll(carol_);
6550 BEAST_EXPECT(amm.expectLPTokens(carol_, IOUAmount{0}));
6551 auto const lpToken =
6552 getAccountLines(env, gw_, amm.lptIssue())[jss::lines][0u][jss::balance];
6553 auto const lpTokenBalance = amm.ammRpcInfo()[jss::amm][jss::lp_token][jss::value];
6554 BEAST_EXPECT(lpToken == "1414.213562373095" && lpTokenBalance == "1414.213562373");
6555 if (!features[fixAMMv1_1])
6556 {
6557 amm.withdrawAll(gw_, std::nullopt, Ter(tecAMM_BALANCE));
6558 BEAST_EXPECT(amm.ammExists());
6559 }
6560 else
6561 {
6562 amm.withdrawAll(gw_);
6563 BEAST_EXPECT(!amm.ammExists());
6564 }
6565 }
6566
6567 // Last Liquidity Provider is the issuer of two tokens, or not
6568 // the issuer
6569 for (auto const& lp : {gw_, bob_})
6570 {
6571 Env env(*this, features);
6572 auto const abc = gw_["ABC"];
6573 fund(
6574 env,
6575 gw_,
6576 {alice_, carol_, bob_},
6577 XRP(1'000),
6578 {USD(1'000'000'000), abc(1'000'000'000'000)});
6579 AMM amm(env, lp, abc(2'000'000), USD(1));
6580 amm.deposit(alice_, IOUAmount{1'876123487565916, -15});
6581 amm.deposit(carol_, IOUAmount{1'000'000});
6582 amm.withdrawAll(alice_);
6583 amm.withdrawAll(carol_);
6584 auto const lpToken =
6585 getAccountLines(env, lp, amm.lptIssue())[jss::lines][0u][jss::balance];
6586 auto const lpTokenBalance = amm.ammRpcInfo()[jss::amm][jss::lp_token][jss::value];
6587 BEAST_EXPECT(lpToken == "1414.213562373095" && lpTokenBalance == "1414.213562373");
6588 if (!features[fixAMMv1_1])
6589 {
6590 amm.withdrawAll(lp, std::nullopt, Ter(tecAMM_BALANCE));
6591 BEAST_EXPECT(amm.ammExists());
6592 }
6593 else
6594 {
6595 amm.withdrawAll(lp);
6596 BEAST_EXPECT(!amm.ammExists());
6597 }
6598 }
6599
6600 // More than one Liquidity Provider
6601 // XRP/IOU
6602 {
6603 Env env(*this, features);
6604 fund(env, gw_, {alice_}, XRP(1'000), {USD(1'000)});
6605 AMM amm(env, gw_, XRP(10), USD(10));
6606 amm.deposit(alice_, 1'000);
6607 auto res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), gw_);
6608 BEAST_EXPECT(res && !res.value());
6609 res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice_);
6610 BEAST_EXPECT(res && !res.value());
6611 }
6612 // IOU/IOU, issuer of both IOU
6613 {
6614 Env env(*this, features);
6615 fund(env, gw_, {alice_}, XRP(1'000), {USD(1'000), EUR(1'000)});
6616 AMM amm(env, gw_, EUR(10), USD(10));
6617 amm.deposit(alice_, 1'000);
6618 auto res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), gw_);
6619 BEAST_EXPECT(res && !res.value());
6620 res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice_);
6621 BEAST_EXPECT(res && !res.value());
6622 }
6623 // IOU/IOU, issuer of one IOU
6624 {
6625 Env env(*this, features);
6626 Account const gw1("gw1");
6627 auto const yan = gw1["YAN"];
6628 fund(env, gw_, {gw1}, XRP(1'000), {USD(1'000)});
6629 fund(env, gw1, {gw_}, XRP(1'000), {yan(1'000)}, Fund::TokenOnly);
6630 AMM amm(env, gw1, yan(10), USD(10));
6631 amm.deposit(gw_, 1'000);
6632 auto res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), gw_);
6633 BEAST_EXPECT(res && !res.value());
6634 res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), gw1);
6635 BEAST_EXPECT(res && !res.value());
6636 }
6637 }
6638
6639 void
6641 {
6642 testcase("test clawback from AMM account");
6643 using namespace jtx;
6644
6645 // Issuer has clawback enabled
6646 Env env(*this, features);
6647 env.fund(XRP(1'000), gw_);
6648 env(fset(gw_, asfAllowTrustLineClawback));
6649 fund(env, gw_, {alice_}, XRP(1'000), {USD(1'000)}, Fund::Acct);
6650 env.close();
6651
6652 // If featureAMMClawback is not enabled, AMMCreate is not allowed for
6653 // clawback-enabled issuer
6654 if (!features[featureAMMClawback])
6655 {
6656 AMM const amm(env, gw_, XRP(100), USD(100), Ter(tecNO_PERMISSION));
6657 AMM const amm1(env, alice_, USD(100), XRP(100), Ter(tecNO_PERMISSION));
6658 env(fclear(gw_, asfAllowTrustLineClawback));
6659 env.close();
6660 // Can't be cleared
6661 AMM const amm2(env, gw_, XRP(100), USD(100), Ter(tecNO_PERMISSION));
6662 }
6663 // If featureAMMClawback is enabled, AMMCreate is allowed for
6664 // clawback-enabled issuer. Clawback from the AMM Account is not
6665 // allowed, which will return tecAMM_ACCOUNT or tecPSEUDO_ACCOUNT,
6666 // depending on whether SingleAssetVault is enabled. We can only use
6667 // AMMClawback transaction to claw back from AMM Account.
6668 else
6669 {
6670 AMM const amm(env, gw_, XRP(100), USD(100), Ter(tesSUCCESS));
6671 AMM const amm1(env, alice_, USD(100), XRP(200), Ter(tecDUPLICATE));
6672
6673 // Construct the amount being clawed back using AMM account.
6674 // By doing this, we make the clawback transaction's Amount field's
6675 // subfield `issuer` to be the AMM account, which means
6676 // we are clawing back from an AMM account. This should return an
6677 // error because regular Clawback transaction is not
6678 // allowed for clawing back from an AMM account. Please notice the
6679 // `issuer` subfield represents the account being clawed back, which
6680 // is confusing.
6681 auto const error =
6682 features[featureSingleAssetVault] ? Ter{tecPSEUDO_ACCOUNT} : Ter{tecAMM_ACCOUNT};
6683 Issue const usd(USD.issue().currency, amm.ammAccount());
6684 auto amount = amountFromString(usd, "10");
6685 env(claw(gw_, amount), error);
6686 }
6687 }
6688
6689 void
6691 {
6692 testcase("test AMMDeposit with frozen assets");
6693 using namespace jtx;
6694
6695 // This lambda function is used to create trustlines
6696 // between gw_ and alice_, and create an AMM account.
6697 // And also test the callback function.
6698 auto testAMMDeposit = [&](Env& env, std::function<void(AMM & amm)> cb) {
6699 env.fund(XRP(1'000), gw_);
6700 fund(env, gw_, {alice_}, XRP(1'000), {USD(1'000)}, Fund::Acct);
6701 env.close();
6702 AMM amm(env, alice_, XRP(100), USD(100), Ter(tesSUCCESS));
6703 env(trust(gw_, alice_["USD"](0), tfSetFreeze));
6704 cb(amm);
6705 };
6706
6707 // Deposit two assets, one of which is frozen,
6708 // then we should get tecFROZEN error.
6709 {
6710 Env env(*this, features);
6711 testAMMDeposit(env, [&](AMM& amm) {
6712 amm.deposit(alice_, USD(100), XRP(100), std::nullopt, tfTwoAsset, Ter(tecFROZEN));
6713 });
6714 }
6715
6716 // Deposit one asset, which is the frozen token,
6717 // then we should get tecFROZEN error.
6718 {
6719 Env env(*this, features);
6720 testAMMDeposit(env, [&](AMM& amm) {
6721 amm.deposit(
6722 alice_, USD(100), std::nullopt, std::nullopt, tfSingleAsset, Ter(tecFROZEN));
6723 });
6724 }
6725
6726 if (features[featureAMMClawback] || features[fixCleanup3_3_0])
6727 {
6728 // Deposit one asset which is not the frozen token,
6729 // but the other asset is frozen. tecFROZEN when either
6730 // AMMClawback or fixCleanup3_3_0 is enabled.
6731 Env env(*this, features);
6732 testAMMDeposit(env, [&](AMM& amm) {
6733 amm.deposit(
6734 alice_, XRP(100), std::nullopt, std::nullopt, tfSingleAsset, Ter(tecFROZEN));
6735 });
6736 }
6737 else
6738 {
6739 // Deposit one asset which is not the frozen token,
6740 // but the other asset is frozen. tesSUCCESS only when
6741 // neither AMMClawback nor fixCleanup3_3_0 is enabled.
6742 Env env(*this, features);
6743 testAMMDeposit(env, [&](AMM& amm) {
6744 amm.deposit(
6745 alice_, XRP(100), std::nullopt, std::nullopt, tfSingleAsset, Ter(tesSUCCESS));
6746 });
6747 }
6748 }
6749
6750 void
6752 {
6753 testcase("Fix Reserve Check On Withdrawal");
6754 using namespace jtx;
6755
6756 auto const err = features[fixAMMv1_2] ? Ter(tecINSUFFICIENT_RESERVE) : Ter(tesSUCCESS);
6757
6758 auto test = [&](auto&& cb) {
6759 Env env(*this, features);
6760 auto const startingXrp = reserve(env, 2) + env.current()->fees().base * 5;
6761 env.fund(startingXrp, gw_);
6762 env.fund(startingXrp, alice_);
6763 env.trust(USD(2'000), alice_);
6764 env.close();
6765 env(pay(gw_, alice_, USD(2'000)));
6766 env.close();
6767 AMM amm(env, gw_, EUR(1'000), USD(1'000));
6768 amm.deposit(alice_, USD(1));
6769 cb(amm);
6770 };
6771
6772 // Equal withdraw
6773 test([&](AMM& amm) { amm.withdrawAll(alice_, std::nullopt, err); });
6774
6775 // Equal withdraw with a limit
6776 test([&](AMM& amm) {
6777 amm.withdraw(
6779 .account = alice_, .asset1Out = EUR(0.1), .asset2Out = USD(0.1), .err = err});
6780 amm.withdraw(
6782 .account = alice_, .asset1Out = USD(0.1), .asset2Out = EUR(0.1), .err = err});
6783 });
6784
6785 // Single withdraw
6786 test([&](AMM& amm) {
6787 amm.withdraw(WithdrawArg{.account = alice_, .asset1Out = EUR(0.1), .err = err});
6788 amm.withdraw(WithdrawArg{.account = alice_, .asset1Out = USD(0.1)});
6789 });
6790 }
6791
6792 void
6794 {
6795 using namespace test::jtx;
6796
6797 auto const testCase = [&](std::string suffix, FeatureBitset features) {
6798 testcase("Fail pseudo-account allocation " + suffix);
6799 std::string logs;
6800 Env env{*this, features, std::make_unique<CaptureLogs>(&logs)};
6801 env.fund(XRP(30'000), gw_, alice_);
6802 env.close();
6803 env(trust(alice_, gw_["USD"](30'000), 0));
6804 env(pay(gw_, alice_, USD(10'000)));
6805 env.close();
6806
6807 STAmount const amount = XRP(10'000);
6808 STAmount const amount2 = USD(10'000);
6809 auto const keylet = keylet::amm(amount.asset(), amount2.asset());
6810 for (int i = 0; i < 256; ++i)
6811 {
6812 AccountID const accountId = xrpl::pseudoAccountAddress(*env.current(), keylet.key);
6813
6814 env(pay(env.master.id(), accountId, XRP(1000)),
6815 Seq(kAutofill),
6816 Fee(kAutofill),
6817 Sig(kAutofill));
6818 }
6819
6820 AMM const ammAlice(
6821 env,
6822 alice_,
6823 amount,
6824 amount2,
6825 features[featureSingleAssetVault] ? Ter{terADDRESS_COLLISION} : Ter{tecDUPLICATE});
6826 };
6827
6828 testCase("tecDUPLICATE", testableAmendments() - featureSingleAssetVault);
6829 testCase("terADDRESS_COLLISION", testableAmendments());
6830 }
6831
6832 void
6834 {
6835 testcase("Deposit and Withdraw Rounding V2");
6836 using namespace jtx;
6837
6838 auto const xpm = gw_["XPM"];
6839 STAmount xrpBalance{XRPAmount(692'614'492'126)};
6840 STAmount xpmBalance{xpm, UINT64_C(18'610'359'80246901), -8};
6841 STAmount amount{xpm, UINT64_C(6'566'496939465400), -12};
6842 std::uint16_t const tfee = 941;
6843
6844 auto test = [&](auto&& cb, std::uint16_t tfee) {
6845 Env env(*this, features);
6846 env.fund(XRP(1'000'000), gw_);
6847 env.fund(XRP(1'000), alice_);
6848 env(trust(alice_, xpm(7'000)));
6849 env(pay(gw_, alice_, amount));
6850
6851 AMM amm(env, gw_, xrpBalance, xpmBalance, CreateArg{.tfee = tfee});
6852 // AMM LPToken balance required to replicate single deposit failure
6853 STAmount const lptAMMBalance{amm.lptIssue(), UINT64_C(3'234'987'266'485968), -6};
6854 auto const burn = IOUAmount{amm.getLPTokensBalance() - lptAMMBalance};
6855 // burn tokens to get to the required AMM state
6856 env(amm.bid(BidArg{.account = gw_, .bidMin = burn, .bidMax = burn}));
6857 cb(amm, env);
6858 };
6859 test(
6860 [&](AMM& amm, Env& env) {
6861 auto const err = env.enabled(fixAMMv1_3) ? Ter(tesSUCCESS) : Ter(tecUNFUNDED_AMM);
6862 amm.deposit(DepositArg{.account = alice_, .asset1In = amount, .err = err});
6863 },
6864 tfee);
6865 test(
6866 [&](AMM& amm, Env& env) {
6867 auto const [amount, amount2, lptAMM] = amm.balances(XRP, xpm);
6868 auto const withdraw = STAmount{xpm, 1, -5};
6869 amm.withdraw(WithdrawArg{.asset1Out = STAmount{xpm, 1, -5}});
6870 auto const [amount_, amount2_, lptAMM_] = amm.balances(XRP, xpm);
6871 if (!env.enabled(fixAMMv1_3))
6872 {
6873 BEAST_EXPECT((amount2 - amount2_) > withdraw);
6874 }
6875 else
6876 {
6877 BEAST_EXPECT((amount2 - amount2_) <= withdraw);
6878 }
6879 },
6880 0);
6881 }
6882
6883 void
6884 invariant(jtx::AMM& amm, jtx::Env& env, std::string const& msg, bool shouldFail)
6885 {
6886 auto const [amount, amount2, lptBalance] = amm.balances(GBP, EUR);
6887
6889 NumberRoundModeGuard const g(
6891 auto const res = root2(amount * amount2);
6892
6893 if (shouldFail)
6894 {
6895 BEAST_EXPECT(res < lptBalance);
6896 }
6897 else
6898 {
6899 BEAST_EXPECT(res >= lptBalance);
6900 }
6901 }
6902
6903 void
6905 {
6906 testcase("Deposit Rounding");
6907 using namespace jtx;
6908
6909 // Single asset deposit
6910 for (auto const& deposit :
6911 {STAmount(EUR, 1, 1),
6912 STAmount(EUR, 1, 2),
6913 STAmount(EUR, 1, 5),
6914 STAmount(EUR, 1, -3), // fail
6915 STAmount(EUR, 1, -6),
6916 STAmount(EUR, 1, -9)})
6917 {
6918 testAMM(
6919 [&](AMM& ammAlice, Env& env) {
6920 fund(
6921 env,
6922 gw_,
6923 {bob_},
6924 XRP(10'000'000),
6925 {GBP(100'000), EUR(100'000)},
6926 Fund::Acct);
6927 env.close();
6928
6929 ammAlice.deposit(DepositArg{.account = bob_, .asset1In = deposit});
6930 invariant(
6931 ammAlice,
6932 env,
6933 "dep1",
6934 deposit == STAmount{EUR, 1, -3} && !env.enabled(fixAMMv1_3));
6935 },
6936 {{GBP(30'000), EUR(30'000)}},
6937 0,
6938 std::nullopt,
6939 {all});
6940 }
6941
6942 // Two-asset proportional deposit (1:1 pool ratio)
6943 testAMM(
6944 [&](AMM& ammAlice, Env& env) {
6945 fund(env, gw_, {bob_}, XRP(10'000'000), {GBP(100'000), EUR(100'000)}, Fund::Acct);
6946 env.close();
6947
6948 STAmount const depositEuro{EUR, UINT64_C(10'1234567890123456), -16};
6949 STAmount const depositGBP{GBP, UINT64_C(10'1234567890123456), -16};
6950
6951 ammAlice.deposit(
6952 DepositArg{.account = bob_, .asset1In = depositEuro, .asset2In = depositGBP});
6953 invariant(ammAlice, env, "dep2", false);
6954 },
6955 {{GBP(30'000), EUR(30'000)}},
6956 0,
6957 std::nullopt,
6958 {all});
6959
6960 // Two-asset proportional deposit (1:3 pool ratio)
6961 for (auto const& exponent : {1, 2, 3, 4, -3 /*fail*/, -6, -9})
6962 {
6963 testAMM(
6964 [&](AMM& ammAlice, Env& env) {
6965 fund(
6966 env,
6967 gw_,
6968 {bob_},
6969 XRP(10'000'000),
6970 {GBP(100'000), EUR(100'000)},
6971 Fund::Acct);
6972 env.close();
6973
6974 STAmount const depositEuro{EUR, 1, exponent};
6975 STAmount const depositGBP{GBP, 1, exponent};
6976
6977 ammAlice.deposit(
6978 DepositArg{
6979 .account = bob_, .asset1In = depositEuro, .asset2In = depositGBP});
6980 invariant(ammAlice, env, "dep3", exponent != -3 && !env.enabled(fixAMMv1_3));
6981 },
6982 {{GBP(10'000), EUR(30'000)}},
6983 0,
6984 std::nullopt,
6985 {all});
6986 }
6987
6988 // tfLPToken deposit
6989 testAMM(
6990 [&](AMM& ammAlice, Env& env) {
6991 fund(env, gw_, {bob_}, XRP(10'000'000), {GBP(100'000), EUR(100'000)}, Fund::Acct);
6992 env.close();
6993
6994 ammAlice.deposit(
6995 DepositArg{.account = bob_, .tokens = IOUAmount{10'1234567890123456, -16}});
6996 invariant(ammAlice, env, "dep4", false);
6997 },
6998 {{GBP(7'000), EUR(30'000)}},
6999 0,
7000 std::nullopt,
7001 {all});
7002
7003 // tfOneAssetLPToken deposit
7004 for (auto const& tokens :
7005 {IOUAmount{1, -3},
7006 IOUAmount{1, -2},
7007 IOUAmount{1, -1},
7008 IOUAmount{1},
7009 IOUAmount{10},
7010 IOUAmount{100},
7011 IOUAmount{1'000},
7012 IOUAmount{10'000}})
7013 {
7014 testAMM(
7015 [&](AMM& ammAlice, Env& env) {
7016 fund(
7017 env,
7018 gw_,
7019 {bob_},
7020 XRP(10'000'000),
7021 {GBP(100'000), EUR(1'000'000)},
7022 Fund::Acct);
7023 env.close();
7024
7025 ammAlice.deposit(
7026 DepositArg{
7027 .account = bob_, .tokens = tokens, .asset1In = STAmount{EUR, 1, 6}});
7028 invariant(ammAlice, env, "dep5", false);
7029 },
7030 {{GBP(7'000), EUR(30'000)}},
7031 0,
7032 std::nullopt,
7033 {all});
7034 }
7035
7036 // Single deposit with EP not exceeding specified:
7037 // 1'000 GBP with EP not to exceed 5 (GBP/TokensOut)
7038 testAMM(
7039 [&](AMM& ammAlice, Env& env) {
7040 fund(env, gw_, {bob_}, XRP(10'000'000), {GBP(100'000), EUR(100'000)}, Fund::Acct);
7041 env.close();
7042
7043 ammAlice.deposit(bob_, GBP(1'000), std::nullopt, STAmount{GBP, 5});
7044 invariant(ammAlice, env, "dep6", false);
7045 },
7046 {{GBP(30'000), EUR(30'000)}},
7047 0,
7048 std::nullopt,
7049 {all});
7050 }
7051
7052 void
7054 {
7055 testcase("Withdraw Rounding");
7056
7057 using namespace jtx;
7058
7059 // tfLPToken mode
7060 testAMM(
7061 [&](AMM& ammAlice, Env& env) {
7062 ammAlice.withdraw(alice_, 1'000);
7063 invariant(ammAlice, env, "with1", false);
7064 },
7065 {{GBP(7'000), EUR(30'000)}},
7066 0,
7067 std::nullopt,
7068 {all});
7069
7070 // tfWithdrawAll mode
7071 testAMM(
7072 [&](AMM& ammAlice, Env& env) {
7073 ammAlice.withdraw(WithdrawArg{.account = alice_, .flags = tfWithdrawAll});
7074 invariant(ammAlice, env, "with2", false);
7075 },
7076 {{GBP(7'000), EUR(30'000)}},
7077 0,
7078 std::nullopt,
7079 {all});
7080
7081 // tfTwoAsset withdraw mode
7082 testAMM(
7083 [&](AMM& ammAlice, Env& env) {
7084 ammAlice.withdraw(
7086 .account = alice_,
7087 .asset1Out = STAmount{GBP, 3'500},
7088 .asset2Out = STAmount{EUR, 15'000},
7089 .flags = tfTwoAsset});
7090 invariant(ammAlice, env, "with3", false);
7091 },
7092 {{GBP(7'000), EUR(30'000)}},
7093 0,
7094 std::nullopt,
7095 {all});
7096
7097 // tfSingleAsset withdraw mode
7098 // Note: This test fails with 0 trading fees, but doesn't fail if
7099 // trading fees is set to 1'000 -- I suspect the compound operations
7100 // in AMMHelpers.cpp:ammAssetOut compensate for the rounding
7101 // errors
7102 testAMM(
7103 [&](AMM& ammAlice, Env& env) {
7104 ammAlice.withdraw(
7106 .account = alice_,
7107 .asset1Out = STAmount{GBP, 1'234},
7108 .flags = tfSingleAsset});
7109 invariant(ammAlice, env, "with4", false);
7110 },
7111 {{GBP(7'000), EUR(30'000)}},
7112 0,
7113 std::nullopt,
7114 {all});
7115
7116 // tfOneAssetWithdrawAll mode
7117 testAMM(
7118 [&](AMM& ammAlice, Env& env) {
7119 fund(env, gw_, {bob_}, XRP(10'000'000), {GBP(100'000), EUR(100'000)}, Fund::Acct);
7120 env.close();
7121
7122 ammAlice.deposit(DepositArg{.account = bob_, .asset1In = STAmount{GBP, 3'456}});
7123
7124 ammAlice.withdraw(
7126 .account = bob_,
7127 .asset1Out = STAmount{GBP, 1'000},
7128 .flags = tfOneAssetWithdrawAll});
7129 invariant(ammAlice, env, "with5", false);
7130 },
7131 {{GBP(7'000), EUR(30'000)}},
7132 0,
7133 std::nullopt,
7134 {all});
7135
7136 // tfOneAssetLPToken mode
7137 testAMM(
7138 [&](AMM& ammAlice, Env& env) {
7139 ammAlice.withdraw(
7141 .account = alice_,
7142 .tokens = 1'000,
7143 .asset1Out = STAmount{GBP, 100},
7144 .flags = tfOneAssetLPToken});
7145 invariant(ammAlice, env, "with6", false);
7146 },
7147 {{GBP(7'000), EUR(30'000)}},
7148 0,
7149 std::nullopt,
7150 {all});
7151
7152 // tfLimitLPToken mode
7153 testAMM(
7154 [&](AMM& ammAlice, Env& env) {
7155 ammAlice.withdraw(
7157 .account = alice_,
7158 .asset1Out = STAmount{GBP, 100},
7159 .maxEP = IOUAmount{2},
7160 .flags = tfLimitLPToken});
7161 invariant(ammAlice, env, "with7", true);
7162 },
7163 {{GBP(7'000), EUR(30'000)}},
7164 0,
7165 std::nullopt,
7166 {all});
7167 }
7168
7169 void
7171 {
7172 testcase("Test AuthAccounts reset after empty pool reinitialization");
7173 using namespace jtx;
7174
7175 Env env(
7176 *this,
7178 cfg->fees.referenceFee = XRPAmount(1);
7179 return cfg;
7180 }),
7181 features);
7182 Account const dan("dan");
7183 Account const ed("ed");
7184 fund(env, gw_, {alice_, carol_, bob_, dan, ed}, XRP(50'000), {USD(50'000)});
7185 AMM amm(env, alice_, XRP(10'000), USD(10'000));
7186 // Create excess trustlines to prevent AMM auto-deletion on withdrawal.
7187 for (auto i = 0; i < kMaxDeletableAmmTrustLines + 10; ++i)
7188 {
7189 Account const a{std::to_string(i)};
7190 env.fund(XRP(1'000), a);
7191 env(trust(a, STAmount{amm.lptIssue(), 10'000}));
7192 env.close();
7193 }
7194 // Carol deposits so she has LP tokens to bid.
7195 amm.deposit(carol_, 1'000'000);
7196 // Carol wins the auction slot, authorizing bob and dan.
7197 env(amm.bid({
7198 .account = carol_,
7199 .bidMin = 100,
7200 .authAccounts = {bob_, dan},
7201 }));
7202 env.close();
7203 BEAST_EXPECT(amm.expectAuctionSlot({bob_.id(), dan.id()}));
7204 // Withdraw all — AMM enters empty state but is not deleted because
7205 // excess trustlines prevent auto-deletion.
7206 amm.withdrawAll(alice_);
7207 amm.withdrawAll(carol_);
7208 BEAST_EXPECT(amm.ammExists());
7209 // Pre-conditions before re-init: AMM is empty and stale sfAuthAccounts
7210 // from carol's bid are still present.
7211 BEAST_EXPECT(amm.getLPTokensBalance() == IOUAmount{0});
7212 BEAST_EXPECT(amm.expectAuctionSlot({bob_.id(), dan.id()}));
7213 // Ed re-initializes the AMM via tfTwoAssetIfEmpty with fee=500.
7214 amm.deposit(
7215 ed,
7216 std::nullopt,
7217 XRP(10'000),
7218 USD(10'000),
7219 std::nullopt,
7220 tfTwoAssetIfEmpty,
7221 std::nullopt,
7222 std::nullopt,
7223 500);
7224
7225 auto const ammSle = env.current()->read(keylet::amm(amm[0], amm[1]));
7226 BEAST_EXPECT(ammSle && ammSle->isFieldPresent(sfAuctionSlot));
7227 auto const& slot = safeDowncast<STObject const&>(ammSle->peekAtField(sfAuctionSlot));
7228
7229 // sfDiscountedFee = 500 / AUCTION_SLOT_DISCOUNTED_FEE_FRACTION = 50,
7230 // sfPrice = 0 (reset on init), time interval = 0 (freshly issued slot).
7231 BEAST_EXPECT(amm.expectAuctionSlot(50, 0, IOUAmount{0}));
7232 // sfAccount must be the re-initializing depositor, not the previous
7233 // slot holder (carol).
7234 BEAST_EXPECT(slot[sfAccount] == ed.id());
7235 // sfTradingFee on the AMM SLE must reflect ed's deposit fee.
7236 BEAST_EXPECT(ammSle->getFieldU16(sfTradingFee) == 500);
7237 // sfVoteSlots must be reset to a single entry for ed.
7238 auto const& votes = ammSle->getFieldArray(sfVoteSlots);
7239 BEAST_EXPECT(votes.size() == 1);
7240 if (!votes.empty())
7241 {
7242 BEAST_EXPECT(votes[0].getAccountID(sfAccount) == ed.id());
7243 BEAST_EXPECT(votes[0].getFieldU16(sfTradingFee) == 500);
7244 BEAST_EXPECT(votes[0].getFieldU32(sfVoteWeight) == kVoteWeightScaleFactor);
7245 }
7246 // sfAuthAccounts behaviour depends on the fix.
7247 if (features[fixCleanup3_2_0])
7248 {
7249 BEAST_EXPECT(!slot.isFieldPresent(sfAuthAccounts));
7250 }
7251 else
7252 {
7253 BEAST_EXPECT(
7254 slot.isFieldPresent(sfAuthAccounts) && !slot.getFieldArray(sfAuthAccounts).empty());
7255 }
7256 }
7257
7258 void
7260 {
7261 testcase("Deposit integral overflow");
7262
7263 using namespace jtx;
7264 auto const all = testableAmendments();
7265
7266 // Found by Antithesis: two-asset deposit with a huge Amount against a
7267 // tiny pool leg makes frac = Amount/amountBalance enormous, so the
7268 // computed XRP-side deposit exceeds the integral asset's range and the
7269 // conversion to an STAmount throws out of doApply.
7270 //
7271 // applyGuts catches std::runtime_error around the deposit math, which
7272 // covers both ways the conversion can throw:
7273 // - value beyond int64 range: Number::operator rep() throws
7274 // std::overflow_error (a std::runtime_error); and
7275 // - value within int64 but above the asset maximum (kMaxNativeN):
7276 // STAmount::canonicalize throws std::runtime_error.
7277 // XRP(10) is 1e7 drops, so the computed XRP leg is 1e7 * frac:
7278 // asset1In 1e15 => frac ~1e15 => ~1e22 drops, past int64max; and
7279 // asset1In 1e11 => frac ~1e11 => ~1e18 drops, in [kMaxNativeN=1e17,
7280 // int64max) - the canonicalize band, which would otherwise escape.
7281 //
7282 // Without fixCleanup3_4_0 the exception escapes and is converted to
7283 // tefEXCEPTION by applySteps. With the amendment, applyGuts guards it
7284 // and fails cleanly with tecAMM_FAILED.
7285 auto const test = [this](FeatureBitset features, STAmount const& asset1In, TER expected) {
7286 // These deposits intentionally trigger the overflow, which logs
7287 // at error (guarded) or fatal (legacy tefEXCEPTION). Disable the
7288 // log threshold to keep the test output clean.
7289 Env env(*this, envconfig(), features, nullptr, beast::Severity::Disabled);
7290 env.fund(XRP(30'000), gw_, alice_);
7291 env(trust(alice_, STAmount{USD, 1, 20}));
7292 env(pay(gw_, alice_, STAmount{USD, 1, 18}));
7293 env.close();
7294
7295 AMM amm(env, gw_, XRP(10), USD(1));
7296 amm.deposit(
7297 DepositArg{
7298 .account = alice_,
7299 .asset1In = asset1In,
7300 .asset2In = XRP(1),
7301 .err = Ter(expected)});
7302 };
7303
7304 // int64-range band (overflow_error): legacy escapes as tefEXCEPTION,
7305 // fixed returns a tec.
7306 test(all - fixCleanup3_4_0, STAmount{USD, 1, 15}, tefEXCEPTION);
7307 test(all, STAmount{USD, 1, 15}, tecAMM_FAILED);
7308 // canonicalize band (runtime_error): same behavior. Regression guard
7309 // for the band a plain overflow_error catch would miss.
7310 test(all - fixCleanup3_4_0, STAmount{USD, 1, 11}, tefEXCEPTION);
7311 test(all, STAmount{USD, 1, 11}, tecAMM_FAILED);
7312 }
7313
7314 void
7316 {
7317 testcase("Deposit EPrice integral overflow");
7318
7319 using namespace jtx;
7320 auto const all = testableAmendments();
7321
7322 // Found by Antithesis: a one-sided tfLimitLPToken deposit (Amount and
7323 // EPrice) with Amount = 0 and a large EPrice makes the solved pool-side
7324 // deposit enormous, so it exceeds the integral asset's range and the
7325 // conversion to an STAmount throws out of doApply. This is the
7326 // singleDepositEPrice sibling of testDepositIntegralOverflow.
7327 //
7328 // applyGuts catches std::runtime_error around the deposit math, which
7329 // covers both ways the conversion can throw:
7330 // - value beyond int64 range: Number::operator rep() throws
7331 // std::overflow_error (a std::runtime_error); and
7332 // - value within int64 but above the asset maximum (kMaxNativeN):
7333 // STAmount::canonicalize throws std::runtime_error.
7334 //
7335 // Without fixCleanup3_4_0 the exception escapes and is converted to
7336 // tefEXCEPTION by applySteps. With the amendment, applyGuts guards it
7337 // and fails cleanly with tecAMM_FAILED.
7338 auto const test = [this](FeatureBitset features, STAmount const& ePrice, TER expected) {
7339 // These deposits intentionally trigger the overflow, which logs
7340 // at error (guarded) or fatal (legacy tefEXCEPTION). Disable the
7341 // log threshold to keep the test output clean.
7342 Env env(*this, envconfig(), features, nullptr, beast::Severity::Disabled);
7343 env.fund(XRP(30'000), gw_, alice_);
7344 env(trust(alice_, STAmount{USD, 1, 20}));
7345 env(pay(gw_, alice_, STAmount{USD, 1, 18}));
7346 env.close();
7347
7348 AMM amm(env, gw_, XRP(10), USD(1));
7349 // Amount = 0 (XRP), EPrice large => tfLimitLPToken. The solved XRP
7350 // leg blows past the integral range.
7351 amm.deposit(
7352 DepositArg{
7353 .account = alice_, .asset1In = XRP(0), .maxEP = ePrice, .err = Ter(expected)});
7354 };
7355
7356 // For this XRP(10)/USD(1) pool the LPToken balance is
7357 // sqrt(1e7 drops * 1) = 3162, so T^2/B = 1e7/1e7 = 1 and the solved
7358 // XRP-side deposit is ~EPrice^2 drops.
7359 //
7360 // int64-range band (overflow_error): legacy escapes as tefEXCEPTION,
7361 // fixed returns a tec. EPrice ~1e17 drops => solved deposit ~1e34 drops,
7362 // past int64max, so Number::operator rep() throws.
7363 auto const bigEP = STAmount{XRPAmount{99'999'999'999'999'999}};
7364 test(all - fixCleanup3_4_0, bigEP, tefEXCEPTION);
7365 test(all, bigEP, tecAMM_FAILED);
7366 // canonicalize band (runtime_error): same behavior. Regression guard
7367 // for the band a plain overflow_error catch would miss. EPrice 1e9 drops
7368 // => solved deposit ~1e18 drops, in [kMaxNativeN=1e17, int64max), so
7369 // STAmount::canonicalize throws.
7370 auto const midEP = STAmount{XRPAmount{1'000'000'000}};
7371 test(all - fixCleanup3_4_0, midEP, tefEXCEPTION);
7372 test(all, midEP, tecAMM_FAILED);
7373 }
7374
7375 void
7377 {
7378 testcase("Withdraw integral no overflow");
7379
7380 using namespace jtx;
7381 auto const all = testableAmendments();
7382
7383 // Regression guard for the sibling of testDepositIntegralOverflow.
7384 // AMMWithdraw::equalWithdrawLimit has the same
7385 // getRoundedAsset(integralBalance, frac) structure as the deposit
7386 // path and is likewise not wrapped in a try/catch. It is safe only
7387 // because withdraw preclaim (checkAmount) rejects a requested Amount
7388 // greater than the pool balance with tecAMM_BALANCE *before* the math
7389 // runs, so frac = Amount / balance stays <= 1 and the Number ->
7390 // integral STAmount conversion cannot overflow. Deposit has no such
7391 // bound (depositing more than the pool holds is legal), which is why
7392 // only the deposit path was exposed.
7393 //
7394 // This asserts the withdrawal analog of the deposit repro fails cleanly
7395 // with a tec. If the preclaim bound is ever weakened, equalWithdrawLimit
7396 // would be reached with a huge frac and Number::operator rep() would
7397 // escape as tefEXCEPTION, failing this test.
7398 auto const test = [this](FeatureBitset features) {
7399 Env env(*this, features);
7400 env.fund(XRP(30'000), gw_, alice_);
7401 env(trust(alice_, STAmount{USD, 1, 20}));
7402 env(pay(gw_, alice_, STAmount{USD, 1, 18}));
7403 env.close();
7404
7405 // gw holds all LPTokens of a tiny XRP/USD pool.
7406 AMM amm(env, gw_, XRP(10), USD(1));
7407
7408 // Two-asset limit withdraw (tfTwoAsset) requesting far more of the
7409 // tiny USD leg than the pool holds - the mirror of the deposit
7410 // repro. Rejected upstream, so no overflow is possible.
7411 amm.withdraw(
7413 .account = gw_,
7414 .asset1Out = STAmount{USD, 1, 15},
7415 .asset2Out = XRP(1),
7416 .err = Ter(tecAMM_BALANCE)});
7417 };
7418
7419 // Bound holds regardless of the deposit-side fix amendment.
7420 test(all - featureMPTokensV2);
7421 test(all);
7422 }
7423
7424 void
7425 run() override
7426 {
7427 FeatureBitset const all{testableAmendments()};
7430 for (auto const& f : amendmentCombinations({fixCleanup3_3_0, featureAMMClawback}))
7432 testDeposit();
7434 testWithdraw();
7436 testFeeVote();
7438 testBid(all);
7439 testBid(all - fixAMMv1_3);
7440 testBid(all - fixAMMv1_1 - fixAMMv1_3);
7443 testBasicPaymentEngine(all - fixAMMv1_1 - fixAMMv1_3);
7444 testBasicPaymentEngine(all - fixReducedOffersV2);
7445 testBasicPaymentEngine(all - fixAMMv1_1 - fixAMMv1_3 - fixReducedOffersV2);
7446 testAMMTokens();
7447 testAmendment();
7448 testFlags();
7449 testRippling();
7450 testAMMAndCLOB(all);
7451 testAMMAndCLOB(all - featureMPTokensV2);
7452 testAMMAndCLOB(all - fixAMMv1_1 - fixAMMv1_3);
7453 testTradingFee(all);
7454 testTradingFee(all - fixAMMv1_3);
7455 testTradingFee(all - fixAMMv1_1 - fixAMMv1_3);
7456 testAdjustedTokens(all);
7457 testAdjustedTokens(all - fixAMMv1_3);
7458 testAdjustedTokens(all - fixAMMv1_1 - fixAMMv1_3);
7460 testClawback();
7461 testAMMID();
7462 testSelection(all);
7463 testSelection(all - fixAMMv1_1 - fixAMMv1_3);
7465 testMalformed();
7466 testOverflowOffer(all);
7467 testOverflowOffer(all - fixAMMv1_3);
7468 testOverflowOffer(all - fixAMMv1_1 - fixAMMv1_3);
7471 testFixChangeSpotPriceQuality(all - featureMPTokensV2);
7472 testFixChangeSpotPriceQuality(all - fixAMMv1_1 - fixAMMv1_3);
7474 testFixAMMOfferBlockedByLOB(all - featureMPTokensV2);
7475 testFixAMMOfferBlockedByLOB(all - fixAMMv1_1 - fixAMMv1_3);
7476 testLPTokenBalance(all);
7477 testLPTokenBalance(all - fixAMMv1_3);
7478 testLPTokenBalance(all - fixAMMv1_1 - fixAMMv1_3);
7479 testAMMClawback(all);
7480 testAMMClawback(all - featureSingleAssetVault);
7481 testAMMClawback(all - featureAMMClawback - featureSingleAssetVault);
7482 testAMMClawback(all - featureAMMClawback);
7483 testAMMClawback(all - fixAMMv1_1 - fixAMMv1_3 - featureAMMClawback);
7484 for (auto const& f : amendmentCombinations({fixCleanup3_3_0, featureAMMClawback}))
7486 testAMMDepositWithFrozenAssets(all - fixAMMv1_1 - featureAMMClawback);
7487 testAMMDepositWithFrozenAssets(all - fixAMMv1_1 - fixAMMv1_3 - featureAMMClawback);
7489 testFixReserveCheckOnWithdrawal(all - fixAMMv1_2);
7491 testDepositAndWithdrawRounding(all - fixAMMv1_3);
7493 testDepositRounding(all - fixAMMv1_3);
7495 testWithdrawRounding(all - fixAMMv1_3);
7498 testStaleAuthAccountsAfterReinit(all - fixCleanup3_2_0);
7502 }
7503};
7504
7506
7507} // namespace xrpl::test
void fail(String const &reason, char const *file, int line)
Record a failure.
Definition suite.h:554
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
Represents a JSON value.
Definition json_value.h:117
UInt asUInt() const
RAII class to set and restore the current transaction rules.
Definition Rules.h:113
Floating point representation of amounts with high dynamic range.
Definition IOUAmount.h:26
A currency issued by an account.
Definition Issue.h:18
std::chrono::time_point< NetClock > time_point
Definition chrono.h:48
Sets the new scale and restores the old scale when it leaves scope.
Definition Number.h:963
Number is a floating point type that can represent a wide range of values.
Definition Number.h:351
static RoundingMode getround()
Represents the logical ratio of output currency to input currency.
Definition Quality.h:90
static constexpr int kMinOffset
Definition STAmount.h:61
Asset const & asset() const
Definition STAmount.h:496
static constexpr std::uint64_t kMinValue
Definition STAmount.h:65
json::Value getJson(JsonOptions) const override
Definition STIssue.cpp:88
jtx::Account const alice_
Definition AMMTest.h:86
void testAMM(std::function< void(jtx::AMM &, jtx::Env &)> const &cb, std::optional< std::pair< STAmount, STAmount > > const &pool=std::nullopt, std::uint16_t tfee=0, std::optional< jtx::Ter > const &ter=std::nullopt, std::vector< FeatureBitset > const &features={testableAmendments()})
testAMM() funds 30,000XRP and 30,000IOU for each non-XRP asset to Alice and Carol
Definition AMMTest.cpp:120
jtx::Account const gw_
Definition AMMTest.h:84
jtx::Account const carol_
Definition AMMTest.h:85
jtx::Account const bob_
Definition AMMTest.h:87
static XRPAmount reserve(jtx::Env &env, std::uint32_t count)
Definition AMMTest.cpp:198
static XRPAmount ammCrtFee(jtx::Env &env)
Definition AMMTest.cpp:204
Convenience class to test AMM functionality.
bool expectTradingFee(std::uint16_t fee) const
Definition AMM.cpp:337
json::Value bid(BidArg const &arg)
Definition AMM.cpp:734
IOUAmount getLPTokensBalance(std::optional< AccountID > const &account=std::nullopt) const
Definition AMM.cpp:282
bool ammExists() const
Definition AMM.cpp:344
IOUAmount withdrawAll(std::optional< Account > const &account, std::optional< STAmount > const &asset1OutDetails=std::nullopt, std::optional< Ter > const &ter=std::nullopt)
bool expectAuctionSlot(std::uint32_t fee, std::optional< std::uint8_t > timeSlot, IOUAmount expectedPrice) const
Definition AMM.cpp:307
IOUAmount tokens() const
IOUAmount deposit(std::optional< Account > const &account, LPToken tokens, std::optional< STAmount > const &asset1InDetails=std::nullopt, std::optional< std::uint32_t > const &flags=std::nullopt, std::optional< Ter > const &ter=std::nullopt)
Definition AMM.cpp:463
IOUAmount withdraw(std::optional< Account > const &account, std::optional< LPToken > const &tokens, std::optional< STAmount > const &asset1OutDetails=std::nullopt, std::optional< std::uint32_t > const &flags=std::nullopt, std::optional< Ter > const &ter=std::nullopt)
Definition AMM.cpp:604
json::Value ammRpcInfo(std::optional< AccountID > const &account=std::nullopt, std::optional< std::string > const &ledgerIndex=std::nullopt, std::optional< Asset > const &asset1=std::nullopt, std::optional< Asset > const &asset2=std::nullopt, std::optional< AccountID > const &ammAccount=std::nullopt, bool ignoreParams=false, unsigned apiVersion=rpc::kApiInvalidVersion) const
Send amm_info RPC command.
Definition AMM.cpp:183
AccountID const & ammAccount() const
void setTokens(json::Value &jv, std::optional< std::pair< Asset, Asset > > const &assets=std::nullopt)
Definition AMM.cpp:392
void vote(std::optional< Account > const &account, std::uint32_t feeVal, std::optional< std::uint32_t > const &flags=std::nullopt, std::optional< jtx::Seq > const &seq=std::nullopt, std::optional< std::pair< Asset, Asset > > const &assets=std::nullopt, std::optional< Ter > const &ter=std::nullopt)
Definition AMM.cpp:708
bool expectLPTokens(AccountID const &account, IOUAmount const &tokens) const
Definition AMM.cpp:296
bool expectBalances(STAmount const &asset1, STAmount const &asset2, IOUAmount const &lpt, std::optional< AccountID > const &account=std::nullopt) const
Verify the AMM balances.
Definition AMM.cpp:269
Immutable cryptographic account descriptor.
Definition jtx/Account.h:21
AccountID id() const
Returns the Account ID.
A transaction testing environment.
Definition Env.h:161
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
SLE::const_pointer le(Account const &account) const
Return an account root.
Definition Env.cpp:311
std::shared_ptr< ReadView const > closed()
Returns the last closed ledger.
Definition Env.cpp:127
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:323
Account const & master
Definition Env.h:165
json::Value rpc(unsigned apiVersion, std::unordered_map< std::string, std::string > const &headers, std::string const &cmd, Args &&... args)
Execute an RPC command.
Definition Env.h:1056
PrettyAmount balance(Account const &account) const
Returns the XRP balance on an account.
Definition Env.cpp:201
void trust(STAmount const &amount, Account const &account)
Establish trust lines.
Definition Env.cpp:354
std::shared_ptr< STObject const > meta()
Return metadata for the last JTx.
Definition Env.cpp:538
bool enabled(uint256 feature) const
Definition Env.h:884
void memoize(Account const &account)
Associate AccountID with account.
Definition Env.cpp:174
beast::Journal const journal
Definition Env.h:204
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:377
NetClock::time_point now()
Returns the current network time.
Definition Env.h:326
Set the fee on a JTx.
Definition fee.h:20
Add a path.
Definition paths.h:47
Sets the SendMax on a JTx.
Definition sendmax.h:16
Set the regular signature on a JTx.
Definition sig.h:19
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition ter.h:18
Set the flags on a JTx.
Definition txflags.h:14
T make_pair(T... args)
T make_unique(T... args)
@ Object
object value (collection of name/value pairs).
Definition json_value.h:29
Keylet computation functions.
Definition Indexes.h:40
Keylet ownerDir(AccountID const &id) noexcept
The root page of an account's directory.
Definition Indexes.cpp:373
Keylet amm(Asset const &issue1, Asset const &issue2) noexcept
AMM entry.
Definition Indexes.cpp:441
json::Value create(A const &account, A const &dest, STAmount const &sendMax)
Create a check.
Deposit preauthorize operations.
Definition deposit.h:16
json::Value create(AccountID const &account, AccountID const &to, STAmount const &amount)
Definition escrow.cpp:24
auto const kCondition
Definition escrow.h:85
auto const kCancelTime
Set the "CancelAfter" time tag on a JTx.
Definition escrow.h:83
auto const kFinishTime
Set the "FinishAfter" time tag on a JTx.
Definition escrow.h:78
std::array< std::uint8_t, 39 > const kCb1
Definition escrow.h:54
json::Value create(AccountID const &account, AccountID const &to, STAmount const &amount, NetClock::duration const &settleDelay, PublicKey const &pk, std::optional< NetClock::time_point > const &cancelAfter, std::optional< std::uint32_t > const &dstTag)
json::Value claw(Account const &account, STAmount const &amount, std::optional< Account > const &mptHolder)
Definition trust.cpp:51
json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:14
std::vector< STAmount > fund(jtx::Env &env, jtx::Account const &gw, std::vector< jtx::Account > const &accounts, std::vector< STAmount > const &amts, Fund how)
Definition AMMTest.cpp:34
bool expectLedgerEntryRoot(Env &env, Account const &acct, STAmount const &expectedValue)
std::vector< FeatureBitset > amendmentCombinations(std::initializer_list< uint256 > features, FeatureBitset seed)
Returns all 2^N permutations of a seed FeatureBitset with each subset of the given features excluded.
Definition Env.h:123
bool expectOffers(Env &env, AccountID const &account, std::uint16_t size, std::vector< Amounts > const &toMatch)
bool expectHolding(Env &env, AccountID const &account, STAmount const &value, bool defaultLimits)
XrpT const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
XRPAmount txFee(Env const &env, std::uint16_t n)
json::Value fclear(Account const &account, std::uint32_t off)
Remove account flag.
Definition flags.h:110
FeatureBitset testableAmendments()
Definition Env.h:92
json::Value offer(Account const &account, STAmount const &takerPays, STAmount const &takerGets, std::uint32_t flags)
Create an offer.
Definition offer.cpp:14
std::unique_ptr< Config > envconfig()
creates and initializes a default configuration for jtx::Env
Definition envconfig.h:37
json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition trust.cpp:18
static AutofillT const kAutofill
Definition tags.h:15
PrettyAmount drops(Integer i)
Returns an XRP PrettyAmount, which is trivially convertible to STAmount.
json::Value accountBalance(Env &env, Account const &acct)
json::Value rate(Account const &account, double multiplier)
Set a transfer rate.
Definition rate.cpp:15
json::Value getAccountLines(Env &env, AccountID const &acctId)
json::Value getAccountOffers(Env &env, AccountID const &acct, bool current)
json::Value fset(Account const &account, std::uint32_t on, std::uint32_t off=0)
Add and/or remove flag.
Definition flags.cpp:15
constexpr XRPAmount
Convert XRP to drops (integral types).
Definition TxTest.h:54
BEAST_DEFINE_TESTSUITE_PRIO(AccountDelete, app, xrpl, 2)
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
@ telINSUF_FEE_P
Definition TER.h:43
@ terNO_AMM
Definition TER.h:223
@ terNO_RIPPLE
Definition TER.h:220
@ terADDRESS_COLLISION
Definition TER.h:224
@ terNO_ACCOUNT
Definition TER.h:213
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Definition Issue.h:108
constexpr std::uint32_t kTotalTimeSlotSecs
Definition AMMCore.h:19
AccountID pseudoAccountAddress(ReadView const &view, uint256 const &pseudoOwnerKey)
Generate a pseudo-account address from a pseudo owner key.
Dest safeDowncast(Src *s) noexcept
Definition safe_cast.h:84
bool isXRP(AccountID const &c)
Definition AccountID.h:84
std::expected< bool, TER > isOnlyLiquidityProvider(ReadView const &view, Issue const &ammIssue, AccountID const &lpAccount)
Return true if the Liquidity Provider is the only AMM provider, false otherwise.
TAmounts< STAmount, STAmount > Amounts
Definition Quality.h:69
@ tefEXCEPTION
Definition TER.h:164
@ Fail
Should not be retried in this ledger.
Definition apply.h:132
BaseUInt< 160, detail::CurrencyTag > Currency
Currency is a hash representing a specific currency.
Definition UintTypes.h:42
STAmount amountFromString(Asset const &asset, std::string const &amount)
Definition STAmount.cpp:907
TOut swapAssetIn(TAmounts< TIn, TOut > const &pool, TIn const &assetIn, std::uint16_t tfee)
AMM pool invariant - the product (A * B) after swap in/out has to remain at least the same: (A + in) ...
Definition AMMHelpers.h:462
STAmount ammAssetOut(STAmount const &assetBalance, STAmount const &lptAMMBalance, STAmount const &lpTokens, std::uint16_t tfee)
Calculate asset withdrawal by tokens.
constexpr std::uint16_t kMaxDeletableAmmTrustLines
The maximum number of trustlines to delete as part of AMM account deletion cleanup.
Definition Protocol.h:397
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:651
T toAmount(STAmount const &amt)=delete
Asset getAsset(T const &amt)
TERSubset< CanCvtToNotTEC > NotTEC
Definition TER.h:607
std::optional< Number > solveQuadraticEqSmallest(Number const &a, Number const &b, Number const &c)
Solve quadratic equation to find takerGets or takerPays.
Number root2(Number f)
std::optional< TAmounts< TIn, TOut > > changeSpotPriceQuality(TAmounts< TIn, TOut > const &pool, Quality const &quality, std::uint16_t tfee, Rules const &rules, beast::Journal j)
Generate AMM offer so that either updated Spot Price Quality (SPQ) is equal to LOB quality (in this c...
Definition AMMHelpers.h:330
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:34
constexpr std::uint16_t kAuctionSlotTimeIntervals
Definition AMMCore.h:20
@ temBAD_CURRENCY
Definition TER.h:78
@ temBAD_FEE
Definition TER.h:80
@ temBAD_AMM_TOKENS
Definition TER.h:117
@ temINVALID_FLAG
Definition TER.h:99
@ temMALFORMED
Definition TER.h:75
@ temDISABLED
Definition TER.h:102
@ temBAD_AMOUNT
Definition TER.h:77
bool isTesSuccess(TER x) noexcept
Definition TER.h:676
TIn swapAssetOut(TAmounts< TIn, TOut > const &pool, TOut const &assetOut, std::uint16_t tfee)
Swap assetOut out of the pool and swap in a proportional amount of the other asset.
Definition AMMHelpers.h:529
TERSubset< CanCvtToTER > TER
Definition TER.h:647
Issue const & noIssue()
Returns an asset specifier that represents no account and currency.
Definition Issue.h:118
bool withinRelativeDistance(Quality const &calcQuality, Quality const &reqQuality, Number const &dist)
Check if the relative distance between the qualities is within the requested distance.
Definition AMMHelpers.h:135
@ tecPSEUDO_ACCOUNT
Definition TER.h:365
@ tecAMM_EMPTY
Definition TER.h:335
@ tecPATH_PARTIAL
Definition TER.h:285
@ tecAMM_INVALID_TOKENS
Definition TER.h:334
@ tecINSUF_RESERVE_LINE
Definition TER.h:291
@ tecAMM_FAILED
Definition TER.h:333
@ tecAMM_NOT_EMPTY
Definition TER.h:336
@ tecPATH_DRY
Definition TER.h:297
@ tecINCOMPLETE
Definition TER.h:338
@ tecUNFUNDED_AMM
Definition TER.h:331
@ tecNO_AUTH
Definition TER.h:303
@ tecAMM_BALANCE
Definition TER.h:332
@ tecINVARIANT_FAILED
Definition TER.h:316
@ tecFROZEN
Definition TER.h:306
@ tecAMM_ACCOUNT
Definition TER.h:337
@ tecOWNERS
Definition TER.h:301
@ tecPRECISION_LOSS
Definition TER.h:366
@ tecINSUFFICIENT_RESERVE
Definition TER.h:310
@ tecNO_PERMISSION
Definition TER.h:308
@ tecDUPLICATE
Definition TER.h:318
constexpr std::uint32_t kVoteWeightScaleFactor
Definition AMMCore.h:30
@ tesSUCCESS
Definition TER.h:245
constexpr std::uint32_t kAuctionSlotIntervalDuration
Definition AMMCore.h:25
T push_back(T... args)
T strcmp(T... args)
Zero allows classes to offer efficient comparisons to zero.
Definition Zero.h:26
Represents a pair of input and output currencies.
Definition Quality.h:29
Basic tests of AMM that do not use offers.
Definition AMM_test.cpp:77
void testAMMAndCLOB(FeatureBitset features)
static FeatureBitset testableAmendments()
Definition AMM_test.cpp:83
void run() override
Runs the suite.
void testTradingFee(FeatureBitset features)
void testWithdrawRounding(FeatureBitset all)
void testWithdrawIntegralNoOverflow()
void testOverflowOffer(FeatureBitset featuresInitial)
void testFixAMMOfferBlockedByLOB(FeatureBitset features)
void testSelection(FeatureBitset features)
void testInvalidDeposit(FeatureBitset features)
Definition AMM_test.cpp:472
static std::vector< FeatureBitset > amendmentCombinations(std::initializer_list< uint256 > features)
Definition AMM_test.cpp:92
void invariant(jtx::AMM &amm, jtx::Env &env, std::string const &msg, bool shouldFail)
void testBid(FeatureBitset features)
void testDepositEPriceIntegralOverflow()
NumberMantissaScaleGuard const sg
Definition AMM_test.cpp:79
void testFixChangeSpotPriceQuality(FeatureBitset features)
void testDepositRounding(FeatureBitset all)
void testStaleAuthAccountsAfterReinit(FeatureBitset features)
void testAMMClawback(FeatureBitset features)
void testDepositIntegralOverflow()
void testLPTokenBalance(FeatureBitset features)
void testAMMDepositWithFrozenAssets(FeatureBitset features)
void testAdjustedTokens(FeatureBitset features)
void testBasicPaymentEngine(FeatureBitset features)
void testFixReserveCheckOnWithdrawal(FeatureBitset features)
void testDepositAndWithdrawRounding(FeatureBitset features)
Set the sequence number on a JTx.
Definition seq.h:16
T to_string(T... args)
T what(T... args)