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