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