xrpld
Loading...
Searching...
No Matches
AMMClawbackMPT_test.cpp
1#include <test/jtx/AMM.h>
2#include <test/jtx/Account.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/balance.h>
8#include <test/jtx/flags.h>
9#include <test/jtx/mpt.h>
10#include <test/jtx/pay.h>
11#include <test/jtx/ter.h>
12#include <test/jtx/trust.h>
13#include <test/jtx/txflags.h>
14
15#include <xrpl/beast/unit_test/suite.h>
16#include <xrpl/ledger/helpers/AMMHelpers.h>
17#include <xrpl/protocol/Feature.h>
18#include <xrpl/protocol/Indexes.h>
19#include <xrpl/protocol/LedgerFormats.h>
20#include <xrpl/protocol/SField.h>
21#include <xrpl/protocol/STAmount.h>
22#include <xrpl/protocol/TER.h>
23#include <xrpl/protocol/TxFlags.h>
24#include <xrpl/protocol/XRPAmount.h>
25#include <xrpl/protocol/jss.h>
26
27#include <cstdint>
28#include <memory>
29#include <optional>
30
31namespace xrpl::test {
33{
34 // SingleAssetVault, LendingProtocol and MPTokensV2 enable the large
35 // Number mantissa.
36 static bool
38 {
39 return features[featureSingleAssetVault] || features[featureLendingProtocol] ||
40 features[featureMPTokensV2];
41 }
42
43 void
45 {
46 testcase("test invalid request");
47 using namespace jtx;
48
49 for (auto const& feature : {features, features - featureSingleAssetVault})
50 {
51 Env env(*this, feature);
52 Account const gw{"gateway"};
53 Account const alice{"alice"};
54 Account const bob{"bob"};
55 env.fund(XRP(100000), gw, alice, bob);
56 env.close();
57
58 env(fset(gw, asfAllowTrustLineClawback));
59 env.close();
60
61 MPT const btc = MPTTester(
62 {.env = env,
63 .issuer = gw,
64 .holders = {alice},
65 .pay = 40'000,
66 .flags = tfMPTCanClawback | kMptDexFlags});
67
68 auto const usd = gw["USD"];
69 env.trust(usd(10000), alice);
70 env(pay(gw, alice, usd(100)));
71 env.close();
72
73 AMM amm(env, gw, btc(100), usd(100));
74
75 // holder does not exist
76 env(amm::ammClawback(gw, Account("unknown"), usd, btc, std::nullopt),
78
79 // can not clawback from self.
80 env(amm::ammClawback(gw, gw, usd, btc, std::nullopt), Ter(temMALFORMED));
81
82 // provided Asset does not match issuer gw
83 {
85 gw, alice, Issue{gw["USD"].currency, alice.id()}, btc, std::nullopt),
87 env(amm::ammClawback(gw, alice, MPTIssue{makeMptID(1, alice)}, usd, std::nullopt),
89 }
90
91 // Amount does not match asset
92 {
94 gw, alice, usd, btc, STAmount{Issue{gw["USD"].currency, alice.id()}, 1}),
97 gw, alice, btc, usd, STAmount{MPTIssue{makeMptID(1, alice)}, 10}),
99 }
100
101 // Amount is not greater than 0
102 {
103 env(amm::ammClawback(gw, alice, btc, usd, btc(-1)), Ter(temBAD_AMOUNT));
104 env(amm::ammClawback(gw, alice, btc, usd, btc(0)), Ter(temBAD_AMOUNT));
105 }
106
107 // clawback from account not holding lptoken
108 env(amm::ammClawback(gw, bob, btc, usd, btc(1000)), Ter(tecAMM_BALANCE));
109
110 // can not perform regular claw from amm pool
111 {
112 Issue const ammUsd(usd.currency, amm.ammAccount());
113 auto amount = amountFromString(ammUsd, "10");
114 auto const err =
115 feature[featureSingleAssetVault] ? tecPSEUDO_ACCOUNT : tecAMM_ACCOUNT;
116 env(claw(gw, amount), Ter(err));
117 }
118
119 // AMM does not exist
120 {
121 // withdraw all tokens will delete the AMM
122 amm.withdrawAll(gw);
123 BEAST_EXPECT(!amm.ammExists());
124 env.close();
125 env(amm::ammClawback(gw, alice, usd, btc, std::nullopt), Ter(terNO_AMM));
126 }
127 }
128
129 // tfMPTCanClawback is not enabled
130 {
131 Env env(*this, features);
132 Account const gw{"gateway"};
133 Account const alice{"alice"};
134 env.fund(XRP(100000), gw, alice);
135 env.close();
136
137 env(fset(gw, asfAllowTrustLineClawback));
138 env.close();
139
140 MPT const btc =
141 MPTTester({.env = env, .issuer = gw, .holders = {alice}, .pay = 40'000});
142
143 auto const usd = gw["USD"];
144 env.trust(usd(10000), alice);
145 env(pay(gw, alice, usd(10000)));
146 env.close();
147
148 AMM amm(env, gw, btc(100), usd(100));
149 env.close();
150 amm.deposit(alice, 1'000);
151
152 // can not clawback when tfMPTCanClawback is not enabled
153 env(amm::ammClawback(gw, alice, btc, usd, std::nullopt), Ter(tecNO_PERMISSION));
154 }
155
156 // can not claw with tfClawTwoAssets if the assets are not issued by the
157 // same issuer
158 {
159 Env env(*this, features);
160 Account const gw{"gateway"};
161 Account const gw2{"gateway2"};
162 Account const alice{"alice"};
163 env.fund(XRP(100000), gw, gw2, alice);
164 env.close();
165
166 env(fset(gw, asfAllowTrustLineClawback));
167 env.close();
168
169 auto const usd = gw["USD"];
170 env.trust(usd(10000), alice);
171 env(pay(gw, alice, usd(10000)));
172 env.close();
173
174 // todo: check tfMPTCanTransfer in xrpl.org
175 MPT const btc = MPTTester(
176 {.env = env,
177 .issuer = gw2,
178 .holders = {alice},
179 .pay = 40'000,
180 .flags = tfMPTCanClawback | kMptDexFlags});
181
182 AMM const amm(env, alice, btc(100), usd(100));
183 env.close();
184
185 {
186 // Return temINVALID_FLAG because the issuer set
187 // tfClawTwoAssets, but the issuer only issues USD in the pool.
188 // The issuer is not allowed to set tfClawTwoAssets flag if he
189 // did not issue both assets in the pool.
190 env(amm::ammClawback(gw, alice, usd, btc, std::nullopt),
191 Txflags(tfClawTwoAssets),
193 }
194 }
195
196 // Test if the issuer did not set asfAllowTrustLineClawback, but the MPT
197 // is set tfMPTCanClawback, the issuer can claw MPT.
198 {
199 Env env(*this, features);
200 Account const gw{"gateway"};
201 Account const alice{"alice"};
202 env.fund(XRP(10000), gw, alice);
203 env.close();
204
205 MPT const btc = MPTTester(
206 {.env = env,
207 .issuer = gw,
208 .holders = {alice},
209 .pay = 40'000,
210 .flags = tfMPTCanClawback | kMptDexFlags});
211
212 AMM const amm(env, alice, btc(100), XRP(100));
213 env.close();
214
215 // If asfAllowTrustLineClawback is not set, the issuer can
216 // still claw MPT because the MPT's tfMPTCanClawback is set.
217 env(amm::ammClawback(gw, alice, btc, XRP, std::nullopt));
218 }
219 }
220
221 void
223 {
224 testcase("test feature disabled.");
225 using namespace jtx;
226 Env env{*this, features};
227 Account const gw("gateway"), alice("alice");
228 env.fund(XRP(30'000), gw, alice);
229 env.close();
230 env(fset(gw, asfAllowTrustLineClawback));
231 env.close();
232
233 MPT const btc = MPTTester(
234 {.env = env,
235 .issuer = gw,
236 .holders = {alice},
237 .pay = 10'000,
238 .flags = tfMPTCanClawback | kMptDexFlags});
239
240 AMM const amm(env, alice, XRP(1'000), btc(1'000));
241
242 // disable featureAMMClawback
243 env.disableFeature(featureAMMClawback);
244 env(amm::ammClawback(gw, alice, btc, XRP, std::nullopt), Ter(temDISABLED));
245
246 // enable featureAMMClawback and disable featureMPTokensV2
247 env.enableFeature(featureAMMClawback);
248 env.disableFeature(featureMPTokensV2);
249 env(amm::ammClawback(gw, alice, btc, XRP, btc(100)), Ter(temDISABLED));
250
251 // enable featureMPTokensV2
252 env.enableFeature(featureMPTokensV2);
253 env(amm::ammClawback(gw, alice, btc, XRP, btc(200)));
254 }
255
256 void
258 {
259 testcase("test AMMClawback specific amount");
260 using namespace jtx;
261
262 // AMMClawback from MPT/IOU issued by different issuers
263 {
264 Env env(*this, features);
265 Account const gw{"gateway"};
266 Account const gw2{"gateway2"};
267 Account const alice{"alice"};
268 env.fund(XRP(100000), gw, gw2, alice);
269 env.close();
270
271 env(fset(gw, asfAllowTrustLineClawback));
272 env(fset(gw2, asfAllowTrustLineClawback));
273 env.close();
274
275 auto const usd = gw["USD"];
276 env.trust(usd(100000), alice);
277 env(pay(gw, alice, usd(50000)));
278 env.close();
279
280 MPT const btc = MPTTester(
281 {.env = env,
282 .issuer = gw2,
283 .holders = {alice},
284 .pay = 40'000'000000,
285 .flags = tfMPTCanClawback | kMptDexFlags});
286
287 AMM const amm(env, alice, btc(1000000000), usd(2000));
288 env.close();
289 BEAST_EXPECT(amm.expectBalances(
290 btc(1'000'000000), usd(2000), IOUAmount{1414'213'562373095, -9}));
291
292 // can not set tfClawTwoAssets because the assets are not issued by
293 // the same issuer.
294 env(amm::ammClawback(gw2, alice, btc, usd, btc(1000)),
295 Txflags(tfClawTwoAssets),
297
298 auto aliceUSD = env.balance(alice, usd);
299 auto aliceBTC = env.balance(alice, btc);
300 // gw clawback 1000 USD from alice
301 env(amm::ammClawback(gw, alice, usd, btc, usd(1000)));
302 env.close();
303
304 BEAST_EXPECT(
305 amm.expectBalances(btc(500'000000), usd(1000), IOUAmount{707'106'7811865475, -10}));
306 // USD is clawed back,
307 env.require(Balance(alice, aliceUSD));
308 // a proportional amount of BTC is returned to alice
309 env.require(Balance(alice, aliceBTC + btc(500'000000)));
310 aliceBTC = env.balance(alice, btc);
311
312 // gw2 clawback 250'000000 BTC from alice
313 env(amm::ammClawback(gw2, alice, btc, usd, btc(250'000000)));
314 env.close();
315 BEAST_EXPECT(
316 amm.expectBalances(btc(250'000000), usd(500), IOUAmount{353'553'3905932737, -10}));
317 env.require(Balance(alice, aliceUSD + usd(500)));
318 env.require(Balance(alice, aliceBTC));
319 aliceUSD = env.balance(alice, usd);
320
321 // gw2 clawback 500'000000 BTC which exceeds the balance,
322 // this will clawback all and the amm will be deleted.
323 env(amm::ammClawback(gw2, alice, btc, usd, btc(500'000000)));
324 env.close();
325 BEAST_EXPECT(!amm.ammExists());
326 env.require(Balance(alice, aliceUSD + usd(500)));
327 env.require(Balance(alice, aliceBTC));
328 }
329
330 // AMMClawback from MPT/XRP pool
331 {
332 Env env(*this, features);
333 Account const gw{"gateway"};
334 Account const alice{"alice"};
335 Account const bob{"bob"};
336 env.fund(XRP(100000), gw, alice, bob);
337 env.close();
338
339 env(fset(gw, asfAllowTrustLineClawback));
340 env.close();
341
342 MPT const btc = MPTTester(
343 {.env = env,
344 .issuer = gw,
345 .holders = {alice, bob},
346 .pay = 40'000'000000,
347 .flags = tfMPTCanClawback | kMptDexFlags});
348
349 AMM amm(env, alice, btc(1000000000), XRP(2000));
350 env.close();
351 BEAST_EXPECT(amm.expectBalances(
352 btc(1'000'000000), XRP(2000), IOUAmount{1'414'213'562'373095, -6}));
353
354 amm.deposit(bob, btc(2'000'000000), XRP(4000));
355 BEAST_EXPECT(amm.expectBalances(
356 btc(3'000'000000), XRP(6000), IOUAmount{4'242'640'687'119285, -6}));
357
358 auto aliceXRP = env.balance(alice, XRP);
359 auto aliceBTC = env.balance(alice, btc);
360 auto bobXRP = env.balance(bob, XRP);
361 auto bobBTC = env.balance(bob, btc);
362
363 // can not claw XRP
364 env(amm::ammClawback(gw, alice, XRP, btc, XRP(1000)), Ter(temMALFORMED));
365 // can not set tfClawTwoAssets
366 env(amm::ammClawback(gw, alice, btc, XRP, btc(1000)),
367 Txflags(tfClawTwoAssets),
369
370 // gw clawback 500 BTC from alice
371 env(amm::ammClawback(gw, alice, btc, XRP, btc(500)));
372 env.close();
373 BEAST_EXPECT(amm.expectBalances(
374 btc(2'999'999501),
375 STAmount{XRP, UINT64_C(5'999'999001)},
376 IOUAmount{4'242'639'980'012504, -6}));
377 env.require(Balance(alice, aliceXRP + drops(999)));
378 env.require(Balance(alice, aliceBTC));
379 env.require(Balance(bob, bobXRP));
380 env.require(Balance(bob, bobBTC));
381 BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{1'414'212'855'266314, -6}));
382 BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount{2'828'427'124'74619, -5}));
383 aliceXRP = env.balance(alice, XRP);
384
385 // gw clawback 1000'000000 BTC from bob
386 env(amm::ammClawback(gw, bob, btc, XRP, btc(1'000'000000)));
387 env.close();
388 BEAST_EXPECT(amm.expectBalances(
389 btc(1'999'999501),
390 STAmount{XRP, UINT64_C(3'999'999002)},
391 IOUAmount{2828426418'110813, -6}));
392 env.require(Balance(alice, aliceXRP));
393 env.require(Balance(alice, aliceBTC));
394 env.require(Balance(bob, bobXRP + XRPAmount(1999999999)));
395 env.require(Balance(bob, bobBTC));
396 BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{1'414'212'855'266314, -6}));
397 BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount{1'414'213'562'844499, -6}));
398 bobXRP = env.balance(bob, XRP);
399
400 // gw clawback 1000'000000 BTC from alice, which exceeds her balance
401 // will clawback all her balance
402 env(amm::ammClawback(gw, alice, btc, XRP, btc(1'000'000000)));
403 env.close();
404 BEAST_EXPECT(amm.expectBalances(
405 btc(1'000'000001), XRPAmount(2'000'000002), IOUAmount{1'414'213'562'844499, -6}));
406 env.require(Balance(alice, aliceXRP + STAmount{XRP, UINT64_C(1'999'999000)}));
407 env.require(Balance(alice, aliceBTC));
408 env.require(Balance(bob, bobXRP));
409 env.require(Balance(bob, bobBTC));
410 BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0)));
411 BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount{1'414'213'562'844499, -6}));
412 aliceXRP = env.balance(alice, XRP);
413
414 // gw clawback from bob, which exceeds his balance
415 env(amm::ammClawback(gw, bob, btc, XRP, btc(2'000'000000)));
416 env.close();
417 // amm is empty and deleted
418 BEAST_EXPECT(!amm.ammExists());
419 env.require(Balance(alice, aliceXRP));
420 env.require(Balance(alice, aliceBTC));
421 env.require(Balance(bob, bobXRP + XRPAmount(2000000002)));
422 env.require(Balance(bob, bobBTC));
423 }
424
425 // AMMClawback from MPT/MPT pool, different issuers
426 {
427 Env env(*this, features);
428 Account const gw{"gateway"};
429 Account const gw2{"gateway2"};
430 Account const alice{"alice"};
431 Account const bob{"bob"};
432 env.fund(XRP(100000), gw, gw2, alice, bob);
433 env.close();
434
435 env(fset(gw, asfAllowTrustLineClawback));
436 env(fset(gw2, asfAllowTrustLineClawback));
437 env.close();
438
439 MPT const btc = MPTTester(
440 {.env = env,
441 .issuer = gw,
442 .holders = {alice, bob},
443 .pay = 40'000'000000,
444 .flags = tfMPTCanClawback | kMptDexFlags});
445
446 MPT const eth = MPTTester(
447 {.env = env,
448 .issuer = gw2,
449 .holders = {alice, bob},
450 .pay = 30'000'000000,
451 .flags = tfMPTCanClawback | kMptDexFlags});
452
453 AMM amm(env, alice, btc(2'000'000000), eth(3'000'000000));
454 env.close();
455 BEAST_EXPECT(amm.expectBalances(
456 btc(2'000'000000), eth(3'000'000000), IOUAmount{2'449'489'742'783178, -6}));
457
458 amm.deposit(bob, btc(4'000'000000), eth(6'000'000000));
459 BEAST_EXPECT(amm.expectBalances(
460 btc(6'000'000000), eth(9'000'000000), IOUAmount{7'348'469'228'349534, -6}));
461
462 auto aliceBTC = env.balance(alice, btc);
463 auto aliceETH = env.balance(alice, eth);
464 auto bobBTC = env.balance(bob, btc);
465 auto bobETH = env.balance(bob, eth);
466
467 // gw clawback BTC from alice
468 env(amm::ammClawback(gw, alice, btc, eth, btc(1'000'000000)));
469 env.close();
470 BEAST_EXPECT(amm.expectBalances(
471 btc(5'000'000000), eth(7'500'000000), IOUAmount{6'123'724'356'957944, -6}));
472 env.require(Balance(alice, aliceBTC));
473 env.require(Balance(alice, aliceETH + eth(1'500'000000)));
474 env.require(Balance(bob, bobBTC));
475 env.require(Balance(bob, bobETH));
476 BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{1'224'744'871'391588, -6}));
477 BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount{4'898'979'485'566356, -6}));
478 aliceETH = env.balance(alice, eth);
479
480 // gw2 clawback ETH from bob
481 env(amm::ammClawback(gw2, bob, eth, btc, eth(3'000'000000)));
482 env.close();
483 BEAST_EXPECT(amm.expectBalances(
484 btc(3'000'000000), eth(4'500'000000), IOUAmount{3'674'234'614'174766, -6}));
485 env.require(Balance(alice, aliceBTC));
486 env.require(Balance(alice, aliceETH));
487 env.require(Balance(bob, bobBTC + btc(2'000'000000)));
488 env.require(Balance(bob, bobETH));
489 BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{1'224'744'871'391588, -6}));
490 BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount{2'449'489'742'783178, -6}));
491 bobBTC = env.balance(bob, btc);
492
493 // gw2 clawback ETH from alice, which exceeds her balance
494 env(amm::ammClawback(gw2, alice, eth, btc, eth(4'000'000000)));
495 env.close();
496 BEAST_EXPECT(amm.expectBalances(
497 btc(2'000'000001), eth(3'000'000001), IOUAmount{2'449'489'742'783178, -6}));
498 env.require(Balance(alice, aliceBTC + btc(999'999999)));
499 env.require(Balance(alice, aliceETH));
500 env.require(Balance(bob, bobBTC));
501 env.require(Balance(bob, bobETH));
502 aliceBTC = env.balance(alice, btc);
503
504 // gw clawback BTC from bob, which exceeds his balance
505 env(amm::ammClawback(gw, bob, btc, eth, btc(3'000'000000)));
506 env.close();
507 // amm is empty and deleted
508 BEAST_EXPECT(!amm.ammExists());
509 env.require(Balance(alice, aliceBTC));
510 env.require(Balance(alice, aliceETH));
511 env.require(Balance(bob, bobBTC));
512 env.require(Balance(bob, bobETH + eth(3'000'000001)));
513 }
514 }
515
516 void
518 {
519 // Ensure a clawback that rounds down to zero MPT fails with
520 // tecAMM_FAILED instead of silently burning the holder's LP.
521 testcase("test AMMClawback amount that rounds down to zero");
522 using namespace jtx;
523
524 Env env(*this, features);
525 Account const gw{"gateway"};
526 Account const alice{"alice"};
527 Account const bob{"bob"};
528 env.fund(XRP(10'000'000), gw, alice, bob);
529 env.close();
530
531 env(fset(gw, asfAllowTrustLineClawback));
532 env.close();
533
534 // The clawed asset (amountRounded) rounds to zero while its XRP
535 // counterpart is always large.
536 {
537 MPTTester const mptBtc(
538 {.env = env,
539 .issuer = gw,
540 .holders = {alice, bob},
541 .pay = 1'000,
542 .flags = tfMPTCanClawback | kMptDexFlags});
543 MPT const btc = mptBtc;
544
545 AMM amm(env, alice, btc(3), XRP(333'000));
546 amm.deposit(bob, btc(3), XRP(333'000));
547
548 [[maybe_unused]] auto const [poolBtcBefore, poolXrpBefore, lptBefore] = amm.balances();
549 BEAST_EXPECT(poolBtcBefore == btc(6));
550
551 auto const issuerOABefore = mptBtc.getBalance(gw);
552 auto const aliceLpBefore = amm.getLPTokensBalance(alice.id());
553 auto const bobLpBefore = amm.getLPTokensBalance(bob.id());
554
555 // Attempt to clawback 1/6th of the BTC pool. When the zero-rounding
556 // guard is active (gated by fixCleanup3_4_0) the rounded amount
557 // drops to 0 and should trigger tecAMM_FAILED.
558 env(amm::ammClawback(gw, alice, btc, XRP, btc(1)),
559 Ter(features[fixCleanup3_4_0] ? TER{tecAMM_FAILED} : TER{tesSUCCESS}));
560 env.close();
561
562 [[maybe_unused]] auto const [poolBtcAfter, poolXrpAfter, lptAfter] = amm.balances();
563 auto const issuerOAAfter = mptBtc.getBalance(gw);
564 auto const aliceLpAfter = amm.getLPTokensBalance(alice.id());
565 auto const bobLpAfter = amm.getLPTokensBalance(bob.id());
566
567 if (features[fixCleanup3_4_0])
568 {
569 // Post-fixCleanup3_4_0: Clawback fails because the BTC balance
570 // would round to zero. All balances must remain untouched.
571 BEAST_EXPECT(poolBtcAfter == poolBtcBefore);
572 BEAST_EXPECT(poolXrpAfter == poolXrpBefore);
573 BEAST_EXPECT(issuerOAAfter == issuerOABefore);
574 BEAST_EXPECT(aliceLpAfter == aliceLpBefore);
575 BEAST_EXPECT(bobLpAfter == bobLpBefore);
576 }
577 else
578 {
579 // Pre-fixCleanup3_4_0: BTC rounds to zero and the clawback
580 // silently burns alice's LP without clawing back any BTC.
581 BEAST_EXPECT(poolBtcAfter == poolBtcBefore);
582 BEAST_EXPECT(poolXrpAfter < poolXrpBefore);
583 BEAST_EXPECT(issuerOAAfter == issuerOABefore);
584 BEAST_EXPECT(aliceLpAfter < aliceLpBefore);
585 BEAST_EXPECT(bobLpAfter == bobLpBefore);
586 }
587 }
588
589 // The pool above only ever rounds the clawed asset (amountRounded) to
590 // zero; its XRP counterpart is always large. Exercise the other operand
591 // of the guard (amount2Rounded == 0) with an MPT/MPT pool where the
592 // *paired* asset is the tiny integer that floors to zero while the
593 // clawed asset still rounds non-zero.
594 {
595 Account const carol{"carol"};
596 Account const dan{"dan"};
597 env.fund(XRP(10'000'000), carol, dan);
598 env.close();
599
600 MPTTester const mptBtc(
601 {.env = env,
602 .issuer = gw,
603 .holders = {carol, dan},
604 .pay = 100'000,
605 .flags = tfMPTCanClawback | kMptDexFlags});
606 MPT const btc = mptBtc;
607
608 MPTTester const mptEth(
609 {.env = env,
610 .issuer = gw,
611 .holders = {carol, dan},
612 .pay = 1'000,
613 .flags = tfMPTCanClawback | kMptDexFlags});
614 MPT const eth = mptEth;
615
616 // btc pool dwarfs the eth pool, so a ~1/12th claw withdraws a
617 // non-zero btc amount while the eth counterpart rounds to zero.
618 AMM amm(env, carol, btc(3'000), eth(3));
619 amm.deposit(dan, btc(3'000), eth(3));
620
621 [[maybe_unused]] auto const [poolBtcBefore, poolEthBefore, lptBefore] = amm.balances();
622 BEAST_EXPECT(poolBtcBefore == btc(6'000));
623 BEAST_EXPECT(poolEthBefore == eth(6));
624
625 auto const carolLpBefore = amm.getLPTokensBalance(carol.id());
626 auto const danLpBefore = amm.getLPTokensBalance(dan.id());
627
628 env(amm::ammClawback(gw, carol, btc, eth, btc(500)),
629 Ter(features[fixCleanup3_4_0] ? TER{tecAMM_FAILED} : TER{tesSUCCESS}));
630 env.close();
631
632 [[maybe_unused]] auto const [poolBtcAfter, poolEthAfter, lptAfter] = amm.balances();
633 auto const carolLpAfter = amm.getLPTokensBalance(carol.id());
634 auto const danLpAfter = amm.getLPTokensBalance(dan.id());
635
636 if (features[fixCleanup3_4_0])
637 {
638 // Post-fixCleanup3_4_0: clawback fails because the ETH (Asset2)
639 // balance would round to zero (guard fires via
640 // amount2Rounded == 0). All balances must remain untouched.
641 BEAST_EXPECT(poolBtcAfter == poolBtcBefore);
642 BEAST_EXPECT(poolEthAfter == poolEthBefore);
643 BEAST_EXPECT(carolLpAfter == carolLpBefore);
644 BEAST_EXPECT(danLpAfter == danLpBefore);
645 }
646 else
647 {
648 // Pre-fixCleanup3_4_0: the asymmetric round-off goes through.
649 // btc is clawed (non-zero) but eth rounds to zero, so the eth
650 // pool is untouched while carol's LP is burned. This asymmetry
651 // proves amount2Rounded == 0 is the trigger.
652 BEAST_EXPECT(poolBtcAfter < poolBtcBefore);
653 BEAST_EXPECT(poolEthAfter == poolEthBefore);
654 BEAST_EXPECT(carolLpAfter < carolLpBefore);
655 BEAST_EXPECT(danLpAfter == danLpBefore);
656 }
657 }
658 }
659
660 void
662 {
663 testcase("test AMMClawback all");
664 using namespace jtx;
665
666 // AMMClawback all from MPT/IOU issued by different issuers
667 {
668 Env env(*this, features);
669 Account const gw{"gateway"};
670 Account const gw2{"gateway2"};
671 Account const alice{"alice"};
672 Account const bob{"bob"};
673 env.fund(XRP(100000), gw, gw2, alice, bob);
674 env.close();
675
676 env(fset(gw, asfAllowTrustLineClawback));
677 env(fset(gw2, asfAllowTrustLineClawback));
678 env.close();
679
680 auto const usd = gw["USD"];
681 env.trust(usd(100000), alice);
682 env(pay(gw, alice, usd(50000)));
683 env.trust(usd(200000), bob);
684 env(pay(gw, bob, usd(60000)));
685 env.close();
686
687 MPT const btc = MPTTester(
688 {.env = env,
689 .issuer = gw2,
690 .holders = {alice, bob},
691 .pay = 40'000'000000,
692 .flags = tfMPTCanClawback | kMptDexFlags});
693
694 AMM amm(env, alice, btc(2000000000), usd(2000));
695 env.close();
696 BEAST_EXPECT(amm.expectBalances(btc(2'000'000000), usd(2000), IOUAmount(2000000)));
697
698 // gw clawback all BTC from alice
699 amm.deposit(bob, btc(1'000'000000), usd(2000));
700 BEAST_EXPECT(amm.expectBalances(btc(3'000'000000), usd(3000), IOUAmount(3000000)));
701
702 auto aliceBTC = env.balance(alice, btc);
703 auto aliceUSD = env.balance(alice, usd);
704 auto bobBTC = env.balance(bob, btc);
705 auto bobUSD = env.balance(bob, usd);
706
707 // gw2 clawback all BTC from alice
708 env(amm::ammClawback(gw2, alice, btc, usd, std::nullopt));
709 env.close();
710 BEAST_EXPECT(amm.expectBalances(btc(1'000'000000), usd(1000), IOUAmount(1000000)));
711 env.require(Balance(alice, aliceBTC));
712 env.require(Balance(alice, aliceUSD + usd(2000)));
713 env.require(Balance(bob, bobBTC));
714 env.require(Balance(bob, bobUSD));
715 aliceUSD = env.balance(alice, usd);
716
717 // gw clawback all USD from bob
718 env(amm::ammClawback(gw, bob, usd, btc, std::nullopt));
719 env.close();
720 // amm is empty and deleted
721 BEAST_EXPECT(!amm.ammExists());
722 env.require(Balance(alice, aliceBTC));
723 env.require(Balance(alice, aliceUSD));
724 env.require(Balance(bob, bobBTC + btc(1'000'000000)));
725 env.require(Balance(bob, bobUSD));
726 }
727
728 // AMMClawback all from MPT/XRP pool
729 {
730 Env env(*this, features);
731 Account const gw{"gateway"};
732 Account const alice{"alice"};
733 Account const bob{"bob"};
734 env.fund(XRP(100000), gw, alice, bob);
735 env.close();
736
737 env(fset(gw, asfAllowTrustLineClawback));
738 env.close();
739
740 MPT const btc = MPTTester(
741 {.env = env,
742 .issuer = gw,
743 .holders = {alice, bob},
744 .pay = 40'000'000000,
745 .flags = tfMPTCanClawback | kMptDexFlags});
746
747 AMM amm(env, alice, btc(5000), XRP(10'000));
748 env.close();
749 BEAST_EXPECT(
750 amm.expectBalances(btc(5'000), XRP(10'000), IOUAmount{7'071'067'811865475, -9}));
751
752 amm.deposit(bob, btc(10'000), XRP(20'000));
753 BEAST_EXPECT(
754 amm.expectBalances(btc(15'000), XRP(30'000), IOUAmount{21'213'203'43559642, -8}));
755
756 auto aliceXRP = env.balance(alice, XRP);
757 auto aliceBTC = env.balance(alice, btc);
758 auto bobXRP = env.balance(bob, XRP);
759 auto bobBTC = env.balance(bob, btc);
760
761 // gw clawback all BTC from alice
762 env(amm::ammClawback(gw, alice, btc, XRP, std::nullopt));
763 env.close();
764 BEAST_EXPECT(
765 amm.expectBalances(btc(10'000), XRP(20'000), IOUAmount{14'142'135'62373094, -8}));
766 env.require(Balance(alice, aliceBTC));
767 env.require(Balance(alice, aliceXRP + XRP(10'000)));
768 env.require(Balance(bob, bobBTC));
769 env.require(Balance(bob, bobXRP));
770 aliceXRP = env.balance(alice, XRP);
771
772 // gw clawback all BTC from bob
773 env(amm::ammClawback(gw, bob, btc, XRP, std::nullopt));
774 env.close();
775 // amm is empty and deleted
776 BEAST_EXPECT(!amm.ammExists());
777 env.require(Balance(alice, aliceBTC));
778 env.require(Balance(alice, aliceXRP));
779 env.require(Balance(bob, bobBTC));
780 env.require(Balance(bob, bobXRP + XRP(20'000)));
781 }
782
783 // AMMClawback all from MPT/MPT pool, different issuers
784 {
785 Env env(*this, features);
786 Account const gw{"gateway"};
787 Account const gw2{"gateway2"};
788 Account const alice{"alice"};
789 Account const bob{"bob"};
790 env.fund(XRP(100000), gw, gw2, alice, bob);
791 env.close();
792
793 env(fset(gw, asfAllowTrustLineClawback));
794 env(fset(gw2, asfAllowTrustLineClawback));
795 env.close();
796
797 MPT const btc = MPTTester(
798 {.env = env,
799 .issuer = gw,
800 .holders = {alice, bob},
801 .pay = 40'000'000000,
802 .flags = tfMPTCanClawback | kMptDexFlags});
803
804 MPT const eth = MPTTester(
805 {.env = env,
806 .issuer = gw2,
807 .holders = {alice, bob},
808 .pay = 30'000'000000,
809 .flags = tfMPTCanClawback | kMptDexFlags});
810
811 AMM amm(env, alice, btc(20'000), eth(50'000));
812 env.close();
813 BEAST_EXPECT(
814 amm.expectBalances(btc(20'000), eth(50'000), IOUAmount{31'622'77660168379, -11}));
815
816 amm.deposit(bob, btc(40'000), eth(100'000));
817 BEAST_EXPECT(
818 amm.expectBalances(btc(60'000), eth(150'000), IOUAmount{94'868'32980505137, -11}));
819
820 auto aliceBTC = env.balance(alice, btc);
821 auto aliceETH = env.balance(alice, eth);
822 auto bobBTC = env.balance(bob, btc);
823 auto bobETH = env.balance(bob, eth);
824
825 // gw clawback all BTC from bob
826 env(amm::ammClawback(gw, bob, btc, eth, std::nullopt));
827 env.close();
828 BEAST_EXPECT(
829 amm.expectBalances(btc(20'000), eth(50'000), IOUAmount{31'622'77660168379, -11}));
830 env.require(Balance(alice, aliceBTC));
831 env.require(Balance(alice, aliceETH));
832 env.require(Balance(bob, bobBTC));
833 env.require(Balance(bob, bobETH + eth(100'000)));
834 bobETH = env.balance(bob, eth);
835
836 // gw2 clawback all ETH from alice
837 env(amm::ammClawback(gw2, alice, eth, btc, std::nullopt));
838 env.close();
839 // amm is empty and deleted
840 BEAST_EXPECT(!amm.ammExists());
841 env.require(Balance(alice, aliceBTC + btc(20'000)));
842 env.require(Balance(alice, aliceETH));
843 env.require(Balance(bob, bobBTC));
844 env.require(Balance(bob, bobETH));
845 }
846 }
847
848 void
850 {
851 testcase("test AMMClawback specific amount, assets have the same issuer");
852 using namespace jtx;
853
854 // AMMClawback from MPT/IOU issued by the same issuer
855 {
856 Env env(*this, features);
857 Account const gw{"gateway"};
858 Account const alice{"alice"};
859 Account const bob{"bob"};
860 env.fund(XRP(100000), gw, alice, bob);
861 env.close();
862
863 env(fset(gw, asfAllowTrustLineClawback));
864 env.close();
865
866 auto const usd = gw["USD"];
867 env.trust(usd(100000), alice);
868 env(pay(gw, alice, usd(50000)));
869 env.trust(usd(100000), bob);
870 env(pay(gw, bob, usd(40000)));
871 env.close();
872
873 MPT const btc = MPTTester(
874 {.env = env,
875 .issuer = gw,
876 .holders = {alice, bob},
877 .pay = 40'000'000000,
878 .flags = tfMPTCanClawback | kMptDexFlags});
879
880 AMM amm(env, alice, btc(1'000'000000), usd(2000));
881 env.close();
882 BEAST_EXPECT(amm.expectBalances(
883 btc(1'000'000000), usd(2000), IOUAmount{1414'213'562373095, -9}));
884
885 amm.deposit(bob, btc(500'000000), usd(1000));
886 BEAST_EXPECT(amm.expectBalances(
887 btc(1'500'000000),
888 STAmount{usd, UINT64_C(2'999'999999999999), -12},
889 IOUAmount{2'121'320'343559642, -9}));
890
891 auto aliceUSD = env.balance(alice, usd);
892 auto aliceBTC = env.balance(alice, btc);
893 auto bobUSD = env.balance(bob, usd);
894 auto bobBTC = env.balance(bob, btc);
895
896 // gw clawback 500 USD from alice.
897 env(amm::ammClawback(gw, alice, usd, btc, usd(500)));
898 env.close();
899 BEAST_EXPECT(amm.expectBalances(
900 btc(1250'000001), usd(2500), IOUAmount{1'767'766'952966369, -9}));
901 env.require(Balance(alice, aliceUSD));
902 env.require(Balance(alice, aliceBTC + btc(249'999999)));
903 env.require(Balance(bob, bobUSD));
904 env.require(Balance(bob, bobBTC));
905 aliceBTC = env.balance(alice, btc);
906 // gw clawback 250'000000 BTC and 500 USD from bob
907 // with tfClawTwoAssets
908 env(amm::ammClawback(gw, bob, btc, usd, btc(250'000000)), Txflags(tfClawTwoAssets));
909 env.close();
910 BEAST_EXPECT(amm.expectBalances(
911 btc(1'000'000002),
912 STAmount{usd, UINT64_C(2000'0000004), -7},
913 IOUAmount{1'414'213'562655938, -9}));
914 env.require(Balance(alice, aliceUSD));
915 env.require(Balance(alice, aliceBTC));
916 env.require(Balance(bob, bobUSD));
917 env.require(Balance(bob, bobBTC));
918 BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{1'060'660'171779822, -9}));
919 BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount{353'553'390876116, -9}));
920
921 // gw clawback USD from alice exceeding her balance
922 env(amm::ammClawback(gw, alice, usd, btc, usd(5'000)));
923 env.close();
924 BEAST_EXPECT(amm.expectBalances(
925 btc(250'000001),
926 STAmount{usd, UINT64_C(500'0000004), -7},
927 IOUAmount{353'553'390876116, -9}));
928 env.require(Balance(alice, aliceUSD));
929 env.require(Balance(alice, aliceBTC + btc(750'000001)));
930 env.require(Balance(bob, bobUSD));
931 env.require(Balance(bob, bobBTC));
932 BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0)));
933 BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount{353'553'390876116, -9}));
934 aliceBTC = env.balance(alice, btc);
935
936 // gw clawback BTC from bob which exceeds his balance with
937 // tfClawTwoAssets
938 env(amm::ammClawback(gw, bob, btc, usd, btc(300'000000)), Txflags(tfClawTwoAssets));
939 env.close();
940 // amm is empty and deleted
941 BEAST_EXPECT(!amm.ammExists());
942 env.require(Balance(alice, aliceUSD));
943 env.require(Balance(alice, aliceBTC));
944 // USD is also clawed back from bob because of tfClawTwoAssets,
945 // bob's USD balance will not change
946 env.require(Balance(bob, bobUSD));
947 env.require(Balance(bob, bobBTC));
948 }
949
950 // AMMClawback from MPT/MPT issued by the same issuer
951 {
952 Env env(*this, features);
953 Account const gw{"gateway"};
954 Account const alice{"alice"};
955 Account const bob{"bob"};
956 env.fund(XRP(100000), gw, alice, bob);
957 env.close();
958
959 env(fset(gw, asfAllowTrustLineClawback));
960 env.close();
961
962 MPT const btc = MPTTester(
963 {.env = env,
964 .issuer = gw,
965 .holders = {alice, bob},
966 .pay = 40'000'000000,
967 .flags = tfMPTCanClawback | kMptDexFlags});
968
969 MPT const eth = MPTTester(
970 {.env = env,
971 .issuer = gw,
972 .holders = {alice, bob},
973 .pay = 30'000'000000,
974 .flags = tfMPTCanClawback | kMptDexFlags});
975
976 AMM amm(env, alice, btc(2'000'000000), eth(3'000'000000));
977 env.close();
978 BEAST_EXPECT(amm.expectBalances(
979 btc(2'000'000000), eth(3'000'000000), IOUAmount{2'449'489'742'783178, -6}));
980
981 amm.deposit(bob, btc(4'000'000000), eth(6'000'000000));
982 BEAST_EXPECT(amm.expectBalances(
983 btc(6'000'000000), eth(9'000'000000), IOUAmount{7'348'469'228'349534, -6}));
984
985 auto aliceBTC = env.balance(alice, btc);
986 auto aliceETH = env.balance(alice, eth);
987 auto bobBTC = env.balance(bob, btc);
988 auto bobETH = env.balance(bob, eth);
989
990 // gw clawback BTC from alice
991 env(amm::ammClawback(gw, alice, btc, eth, btc(1'000'000000)));
992 env.close();
993 BEAST_EXPECT(amm.expectBalances(
994 btc(5'000'000000), eth(7'500'000000), IOUAmount{6'123'724'356'957944, -6}));
995 env.require(Balance(alice, aliceBTC));
996 env.require(Balance(alice, aliceETH + eth(1'500'000000)));
997 env.require(Balance(bob, bobBTC));
998 env.require(Balance(bob, bobETH));
999 BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{1'224'744'871'391588, -6}));
1000 BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount{4'898'979'485'566356, -6}));
1001 aliceETH = env.balance(alice, eth);
1002
1003 // gw clawback ETH and BTC from bob with tfClawTwoAssets
1004 env(amm::ammClawback(gw, bob, eth, btc, eth(3'000'000000)), Txflags(tfClawTwoAssets));
1005 env.close();
1006 BEAST_EXPECT(amm.expectBalances(
1007 btc(3'000'000000), eth(4'500'000000), IOUAmount{3'674'234'614'174766, -6}));
1008 env.require(Balance(alice, aliceBTC));
1009 env.require(Balance(alice, aliceETH));
1010 env.require(Balance(bob, bobBTC));
1011 env.require(Balance(bob, bobETH));
1012 BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{1'224'744'871'391588, -6}));
1013 BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount{2'449'489'742'783178, -6}));
1014
1015 // gw clawback BTC from alice, which exceeds her balance with
1016 // tfClawTwoAssets
1017 env(amm::ammClawback(gw, alice, btc, eth, btc(3'000'000000)), Txflags(tfClawTwoAssets));
1018 env.close();
1019 BEAST_EXPECT(amm.expectBalances(
1020 btc(2'000'000001), eth(3'000'000001), IOUAmount{2'449'489'742'783178, -6}));
1021 env.require(Balance(alice, aliceBTC));
1022 env.require(Balance(alice, aliceETH));
1023 env.require(Balance(bob, bobBTC));
1024 env.require(Balance(bob, bobETH));
1025 BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0)));
1026 BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount{2'449'489'742'783178, -6}));
1027
1028 // gw clawback ETH from bob, which is the same as his balance
1029 env(amm::ammClawback(gw, bob, eth, btc, eth(3'000'000001)));
1030 env.close();
1031 // amm is empty and deleted
1032 BEAST_EXPECT(!amm.ammExists());
1033 env.require(Balance(alice, aliceBTC));
1034 env.require(Balance(alice, aliceETH));
1035 env.require(Balance(bob, bobBTC + btc(2'000'000001)));
1036 env.require(Balance(bob, bobETH));
1037 }
1038 }
1039
1040 void
1042 {
1043 testcase("test AMMClawback all, assets have the same issuer");
1044 using namespace jtx;
1045
1046 // AMMClawback all from MPT/IOU issued by the same issuer
1047 {
1048 Env env(*this, features);
1049 Account const gw{"gateway"};
1050 Account const alice{"alice"};
1051 Account const bob{"bob"};
1052 env.fund(XRP(100000), gw, alice, bob);
1053 env.close();
1054
1055 env(fset(gw, asfAllowTrustLineClawback));
1056 env.close();
1057
1058 auto const usd = gw["USD"];
1059 env.trust(usd(100000), alice);
1060 env(pay(gw, alice, usd(50000)));
1061 env.trust(usd(200000), bob);
1062 env(pay(gw, bob, usd(60000)));
1063 env.close();
1064
1065 MPT const btc = MPTTester(
1066 {.env = env,
1067 .issuer = gw,
1068 .holders = {alice, bob},
1069 .pay = 40'000'000000,
1070 .flags = tfMPTCanClawback | kMptDexFlags});
1071
1072 AMM amm(env, alice, btc(2'000'000000), usd(8'000));
1073 env.close();
1074 BEAST_EXPECT(amm.expectBalances(btc(2'000'000000), usd(8'000), IOUAmount(4'000'000)));
1075
1076 amm.deposit(bob, btc(1'000'000000), usd(4'000));
1077 BEAST_EXPECT(amm.expectBalances(btc(3'000'000000), usd(12'000), IOUAmount(6'000'000)));
1078
1079 auto aliceBTC = env.balance(alice, btc);
1080 auto aliceUSD = env.balance(alice, usd);
1081 auto bobBTC = env.balance(bob, btc);
1082 auto bobUSD = env.balance(bob, usd);
1083
1084 // gw clawback all BTC and USD from alice
1085 env(amm::ammClawback(gw, alice, btc, usd, std::nullopt), Txflags(tfClawTwoAssets));
1086 env.close();
1087
1088 BEAST_EXPECT(amm.expectBalances(btc(1'000'000000), usd(4'000), IOUAmount(2'000'000)));
1089 BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount(2'000'000)));
1090 BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0)));
1091 env.require(Balance(alice, aliceBTC));
1092 env.require(Balance(alice, aliceUSD));
1093 env.require(Balance(bob, bobBTC));
1094 env.require(Balance(bob, bobUSD));
1095
1096 // gw clawback all USD from bob
1097 env(amm::ammClawback(gw, bob, usd, btc, std::nullopt));
1098 env.close();
1099 // amm is empty and deleted
1100 BEAST_EXPECT(!amm.ammExists());
1101 env.require(Balance(alice, aliceBTC));
1102 env.require(Balance(alice, aliceUSD));
1103 env.require(Balance(bob, bobBTC + btc(1'000'000000)));
1104 env.require(Balance(bob, bobUSD));
1105 }
1106
1107 // AMMClawback all from MPT/MPT issued by the same issuer
1108 {
1109 Env env(*this, features);
1110 Account const gw{"gateway"};
1111 Account const alice{"alice"};
1112 Account const bob{"bob"};
1113 env.fund(XRP(100000), gw, alice, bob);
1114 env.close();
1115
1116 env(fset(gw, asfAllowTrustLineClawback));
1117 env.close();
1118
1119 MPT const btc = MPTTester(
1120 {.env = env,
1121 .issuer = gw,
1122 .holders = {alice, bob},
1123 .pay = 40'000'000000,
1124 .flags = tfMPTCanClawback | kMptDexFlags});
1125
1126 MPT const eth = MPTTester(
1127 {.env = env,
1128 .issuer = gw,
1129 .holders = {alice, bob},
1130 .pay = 30'000'000000,
1131 .flags = tfMPTCanClawback | kMptDexFlags});
1132
1133 AMM amm(env, alice, btc(20'000), eth(10'000));
1134 env.close();
1135 BEAST_EXPECT(
1136 amm.expectBalances(btc(20'000), eth(10'000), IOUAmount{14'142'13562373095, -11}));
1137
1138 amm.deposit(bob, btc(40'000), eth(20'000));
1139 BEAST_EXPECT(
1140 amm.expectBalances(btc(60'000), eth(30'000), IOUAmount{42'426'40687119285, -11}));
1141
1142 auto aliceBTC = env.balance(alice, btc);
1143 auto aliceETH = env.balance(alice, eth);
1144 auto bobBTC = env.balance(bob, btc);
1145 auto bobETH = env.balance(bob, eth);
1146
1147 // gw clawback all ETH from bob
1148 env(amm::ammClawback(gw, bob, eth, btc, std::nullopt));
1149 env.close();
1150 BEAST_EXPECT(
1151 amm.expectBalances(btc(20'000), eth(10'000), IOUAmount{14'142'13562373095, -11}));
1152 env.require(Balance(alice, aliceBTC));
1153 env.require(Balance(alice, aliceETH));
1154 env.require(Balance(bob, bobBTC + btc(40'000)));
1155 env.require(Balance(bob, bobETH));
1156 BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{14'142'13562373095, -11}));
1157 BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount(0)));
1158 bobBTC = env.balance(bob, btc);
1159
1160 // gw clawback all ETH and BTC from alice with tfClawTwoAssets
1161 env(amm::ammClawback(gw, alice, eth, btc, std::nullopt), Txflags(tfClawTwoAssets));
1162 env.close();
1163
1164 // amm is empty and deleted
1165 BEAST_EXPECT(!amm.ammExists());
1166 env.require(Balance(alice, aliceBTC));
1167 env.require(Balance(alice, aliceETH));
1168 env.require(Balance(bob, bobBTC));
1169 env.require(Balance(bob, bobETH));
1170 }
1171 }
1172
1173 void
1175 {
1176 testcase("test AMMClawback when issuing token for each other");
1177 using namespace jtx;
1178
1179 // AMMClawback from MPT/IOU issued by each other
1180 {
1181 Env env(*this, features);
1182 Account const gw{"gateway"};
1183 Account const gw2{"gateway2"};
1184 Account const alice{"alice"};
1185 env.fund(XRP(1000000), gw, gw2, alice);
1186 env.close();
1187
1188 env(fset(gw, asfAllowTrustLineClawback));
1189 env(fset(gw2, asfAllowTrustLineClawback));
1190 env.close();
1191
1192 auto const usd = gw["USD"];
1193 env.trust(usd(100000), gw2);
1194 env(pay(gw, gw2, usd(5000)));
1195 env.trust(usd(100000), alice);
1196 env(pay(gw, alice, usd(5000)));
1197
1198 MPT const btc = MPTTester(
1199 {.env = env,
1200 .issuer = gw2,
1201 .holders = {alice, gw},
1202 .pay = 40'000'000000,
1203 .flags = tfMPTCanClawback | kMptDexFlags});
1204
1205 AMM amm(env, gw, usd(1000), btc(2000));
1206 env.close();
1207 BEAST_EXPECT(
1208 amm.expectBalances(usd(1000), btc(2000), IOUAmount{1414'213562373095, -12}));
1209
1210 amm.deposit(gw2, usd(2000), btc(4000));
1211 BEAST_EXPECT(
1212 amm.expectBalances(usd(3000), btc(6000), IOUAmount{4242'640687119285, -12}));
1213
1214 amm.deposit(alice, usd(3000), btc(6000));
1215 BEAST_EXPECT(
1216 amm.expectBalances(usd(6000), btc(12000), IOUAmount{8485'281374238570, -12}));
1217
1218 BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{1414'213562373095, -12}));
1219 BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{2828'427124746190, -12}));
1220 BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{4242'640687119285, -12}));
1221
1222 auto aliceBTC = env.balance(alice, btc);
1223 auto aliceUSD = env.balance(alice, usd);
1224 auto gwBTC = env.balance(gw, btc);
1225 auto gw2USD = env.balance(gw2, usd);
1226
1227 // gw claws back 1000 USD from gw2.
1228 env(amm::ammClawback(gw, gw2, usd, btc, usd(1000)));
1229 env.close();
1230 BEAST_EXPECT(
1231 amm.expectBalances(usd(5000), btc(10000), IOUAmount{7071'067811865474, -12}));
1232 BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{1414'213562373095, -12}));
1233 BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{1414'213562373094, -12}));
1234 BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{4242'640687119285, -12}));
1235 env.require(Balance(alice, aliceBTC));
1236 env.require(Balance(alice, aliceUSD));
1237 env.require(Balance(gw, gwBTC));
1238 env.require(Balance(gw2, gw2USD));
1239
1240 // gw2 claws back 1000 BTC from gw.
1241 env(amm::ammClawback(gw2, gw, btc, usd, btc(1000)), Ter(tesSUCCESS));
1242 env.close();
1243 BEAST_EXPECT(
1244 amm.expectBalances(usd(4500), btc(9001), IOUAmount{6363'961030678927, -12}));
1245
1246 BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{707'1067811865480, -13}));
1247 BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{1414'213562373094, -12}));
1248 BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{4242'640687119285, -12}));
1249 env.require(Balance(alice, aliceBTC));
1250 env.require(Balance(alice, aliceUSD));
1251 env.require(Balance(gw, gwBTC));
1252 env.require(Balance(gw2, gw2USD));
1253
1254 // gw2 claws back 4000 BTC from alice
1255 env(amm::ammClawback(gw2, alice, btc, usd, btc(4000)));
1256 env.close();
1257 BEAST_EXPECT(amm.expectBalances(
1258 STAmount{usd, UINT64_C(2500'222197533607), -12},
1259 btc(5001),
1260 IOUAmount{3535'84814069829, -11}));
1261
1262 BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{707'1067811865480, -13}));
1263 BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{1414'213562373094, -12}));
1264 BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{1414'527797138648, -12}));
1265 env.require(Balance(alice, aliceBTC));
1266 env.require(Balance(alice, aliceUSD + STAmount{usd, UINT64_C(1999'777802466393), -12}));
1267 env.require(Balance(gw, gwBTC));
1268 env.require(Balance(gw2, gw2USD));
1269 }
1270
1271 // AMMClawback from MPT/MPT issued by each other
1272 {
1273 Env env(*this, features);
1274 Account const gw{"gateway"};
1275 Account const gw2{"gateway2"};
1276 Account const alice{"alice"};
1277 env.fund(XRP(100000), gw, gw2, alice);
1278 env.close();
1279
1280 env(fset(gw, asfAllowTrustLineClawback));
1281 env(fset(gw2, asfAllowTrustLineClawback));
1282 env.close();
1283
1284 MPT const btc = MPTTester(
1285 {.env = env,
1286 .issuer = gw,
1287 .holders = {gw2, alice},
1288 .pay = 40'000'000000,
1289 .flags = tfMPTCanClawback | kMptDexFlags});
1290
1291 MPT const eth = MPTTester(
1292 {.env = env,
1293 .issuer = gw2,
1294 .holders = {gw, alice},
1295 .pay = 30'000'000000,
1296 .flags = tfMPTCanClawback | kMptDexFlags});
1297
1298 AMM amm(env, gw, btc(10'000), eth(50'000));
1299 env.close();
1300 BEAST_EXPECT(
1301 amm.expectBalances(btc(10'000), eth(50'000), IOUAmount{22'360'67977499789, -11}));
1302
1303 amm.deposit(gw2, btc(20'000), eth(100'000));
1304 BEAST_EXPECT(
1305 amm.expectBalances(btc(30'000), eth(150'000), IOUAmount{67'082'03932499367, -11}));
1306
1307 amm.deposit(alice, btc(40'000), eth(200'000));
1308 BEAST_EXPECT(
1309 amm.expectBalances(btc(70'000), eth(350'000), IOUAmount{156'524'7584249852, -10}));
1310
1311 auto aliceBTC = env.balance(alice, btc);
1312 auto aliceETH = env.balance(alice, eth);
1313 auto gw2BTC = env.balance(gw2, btc);
1314 auto gwETH = env.balance(gw, eth);
1315
1316 // gw claws back 1000 BTC from gw2.
1317 env(amm::ammClawback(gw, gw2, btc, eth, btc(1000)));
1318 env.close();
1319 BEAST_EXPECT(
1320 amm.expectBalances(btc(69'001), eth(345'001), IOUAmount{154'288'6904474855, -10}));
1321 env.require(Balance(alice, aliceBTC));
1322 env.require(Balance(alice, aliceETH));
1323 env.require(Balance(gw, gwETH));
1324 env.require(Balance(gw2, gw2BTC));
1325
1326 // gw2 claws back all ETH from gw
1327 env(amm::ammClawback(gw2, gw, eth, btc, std::nullopt));
1328 env.close();
1329 BEAST_EXPECT(
1330 amm.expectBalances(btc(59'001), eth(295'001), IOUAmount{131'928'0106724876, -10}));
1331 env.require(Balance(alice, aliceBTC));
1332 env.require(Balance(alice, aliceETH));
1333 env.require(Balance(gw, gwETH));
1334 env.require(Balance(gw2, gw2BTC));
1335
1336 // gw claws back all BTC from alice
1337 env(amm::ammClawback(gw, alice, btc, eth, std::nullopt));
1338 env.close();
1339 BEAST_EXPECT(
1340 amm.expectBalances(btc(19'001), eth(95'001), IOUAmount{42'485'29157249607, -11}));
1341 env.require(Balance(alice, aliceBTC));
1342 env.require(Balance(alice, aliceETH + eth(200'000)));
1343 env.require(Balance(gw, gwETH));
1344 env.require(Balance(gw2, gw2BTC));
1345 }
1346 }
1347
1348 void
1350 {
1351 testcase("test AMMClawback when asset is frozen or locked");
1352 using namespace jtx;
1353
1354 // test AMMClawback when MPT globally locked or IOU globally frozen
1355 {
1356 Env env{*this, features};
1357 Account const gw{"gateway"};
1358 Account const alice{"alice"};
1359 env.fund(XRP(1'000'000), gw, alice);
1360
1361 env(fset(gw, asfAllowTrustLineClawback));
1362 env.close();
1363 auto const usd = gw["USD"];
1364 env.trust(usd(1'000'000), alice);
1365 env(pay(gw, alice, usd(500'000)));
1366
1367 MPTTester btc(
1368 {.env = env,
1369 .issuer = gw,
1370 .holders = {alice},
1371 .pay = 30'000,
1372 .flags = tfMPTCanClawback | tfMPTCanLock | kMptDexFlags});
1373 AMM const ammAlice(env, alice, usd(10'000), btc(10'000));
1374 BEAST_EXPECT(ammAlice.expectBalances(usd(10'000), btc(10'000), IOUAmount(10'000)));
1375 env.close();
1376
1377 auto aliceBTC = env.balance(alice, MPT(btc));
1378 auto aliceUSD = env.balance(alice, usd);
1379
1380 // globally locked and claw back 1000 BTC.
1381 // this should be successful
1382 btc.set({.flags = tfMPTLock});
1383 env(amm::ammClawback(gw, alice, MPT(btc), usd, btc(1'000)));
1384 BEAST_EXPECT(ammAlice.expectBalances(usd(9'000), btc(9'000), IOUAmount(9'000)));
1385 env.require(Balance(alice, aliceBTC));
1386 env.require(Balance(alice, aliceUSD + usd(1'000)));
1387 aliceUSD = env.balance(alice, usd);
1388
1389 // unlock and claw back 2000 BTC
1390 btc.set({.flags = tfMPTUnlock});
1391 env(amm::ammClawback(gw, alice, MPT(btc), usd, btc(2'000)));
1392 BEAST_EXPECT(ammAlice.expectBalances(
1393 STAmount(usd, UINT64_C(7'000'000000000001), -12), btc(7'001), IOUAmount(7'000)));
1394 env.require(Balance(alice, aliceBTC));
1395 env.require(Balance(alice, aliceUSD + usd(2'000)));
1396 aliceUSD = env.balance(alice, usd);
1397
1398 // globally freeze trustline and claw back 1000 USD.
1399 // this should be successful
1400 env(trust(gw, alice["USD"](0), tfSetFreeze));
1401 env.close();
1402 env(amm::ammClawback(gw, alice, usd, MPT(btc), usd(1'000)));
1403 BEAST_EXPECT(ammAlice.expectBalances(
1404 STAmount(usd, UINT64_C(6000'000000000002), -12),
1405 btc(6'001),
1406 IOUAmount(6'000'000000000001, -12)));
1407 env.require(Balance(alice, aliceBTC + btc(1'000)));
1408 env.require(Balance(alice, aliceUSD));
1409 aliceBTC = env.balance(alice, MPT(btc));
1410
1411 // globally unfreeze trustline and claw back 2000 USD
1412 // and 2000 BTC with tfClawTwoAssets
1413 env(fset(gw, asfGlobalFreeze));
1414 env.close();
1415 env(amm::ammClawback(gw, alice, usd, MPT(btc), usd(2'000)), Txflags(tfClawTwoAssets));
1416 BEAST_EXPECT(ammAlice.expectBalances(
1417 STAmount(usd, UINT64_C(4'000'000000000002), -12),
1418 btc(4'001),
1419 IOUAmount(4'000'000000000001, -12)));
1420 env.require(Balance(alice, aliceBTC));
1421 env.require(Balance(alice, aliceUSD));
1422 }
1423
1424 // test AMMClawback when MPT individually locked or IOU individually
1425 // frozen
1426 {
1427 Env env{*this, features};
1428 Account const gw{"gateway"};
1429 Account const alice{"alice"};
1430 env.fund(XRP(1'000'000), gw, alice);
1431
1432 env(fset(gw, asfAllowTrustLineClawback));
1433 env.close();
1434 auto const usd = gw["USD"];
1435 env.trust(usd(1'000'000), alice);
1436 env(pay(gw, alice, usd(500'000)));
1437
1438 MPTTester btc(
1439 {.env = env,
1440 .issuer = gw,
1441 .holders = {alice},
1442 .pay = 30'000,
1443 .flags = tfMPTCanClawback | tfMPTCanLock | kMptDexFlags});
1444 AMM const ammAlice(env, alice, usd(10'000), btc(10'000));
1445 BEAST_EXPECT(ammAlice.expectBalances(usd(10'000), btc(10'000), IOUAmount(10'000)));
1446 env.close();
1447
1448 auto aliceBTC = env.balance(alice, MPT(btc));
1449 auto aliceUSD = env.balance(alice, usd);
1450
1451 // individually locked and claw back 2000 BTC from alice
1452 btc.set({.holder = alice, .flags = tfMPTLock});
1453 env(amm::ammClawback(gw, alice, MPT(btc), usd, btc(2'000)));
1454 BEAST_EXPECT(ammAlice.expectBalances(usd(8'000), btc(8'000), IOUAmount(8'000)));
1455 env.require(Balance(alice, aliceBTC));
1456 env.require(Balance(alice, aliceUSD + usd(2'000)));
1457 aliceUSD = env.balance(alice, usd);
1458
1459 // individually freeze trustline and claw back 1000 USD from alice
1460 env(trust(gw, alice["USD"](0), tfSetFreeze));
1461 env.close();
1462 env(amm::ammClawback(gw, alice, usd, MPT(btc), usd(1'000)));
1463 BEAST_EXPECT(ammAlice.expectBalances(usd(7'000), btc(7'000), IOUAmount(7'000)));
1464 env.require(Balance(alice, aliceBTC + btc(1'000)));
1465 env.require(Balance(alice, aliceUSD));
1466 aliceBTC = env.balance(alice, MPT(btc));
1467
1468 // unlock MPT and claw back 3000 BTC from alice
1469 btc.set({.holder = alice, .flags = tfMPTUnlock});
1470 env(amm::ammClawback(gw, alice, MPT(btc), usd, btc(3'000)));
1471 BEAST_EXPECT(ammAlice.expectBalances(
1472 STAmount{usd, UINT64_C(4000'000000000001), -12}, btc(4'001), IOUAmount(4'000)));
1473 env.require(Balance(alice, aliceBTC));
1474 env.require(Balance(alice, aliceUSD + usd(3'000)));
1475 aliceUSD = env.balance(alice, usd);
1476
1477 // unlock trustline and claw back 1000 USD from alice
1478 env(trust(gw, alice["USD"](0), tfClearFreeze));
1479 env.close();
1480 env(amm::ammClawback(gw, alice, usd, MPT(btc), usd(1'000)));
1481 BEAST_EXPECT(ammAlice.expectBalances(
1482 STAmount(usd, UINT64_C(3'000'000000000002), -12),
1483 btc(3'001),
1484 IOUAmount(3000'000000000001, -12)));
1485 env.require(Balance(alice, aliceBTC + btc(1'000)));
1486 env.require(Balance(alice, aliceUSD));
1487 }
1488 }
1489
1490 void
1492 {
1493 testcase("test AMMClawback creates missing MPToken");
1494 using namespace jtx;
1495
1496 auto test = [&](std::optional<std::uint64_t> const clawAmount) {
1497 Env env{*this, features};
1498 Account const gw{"gateway"};
1499 Account const alice{"alice"};
1500 env.fund(XRP(1'000'000), gw, alice);
1501 env.close();
1502
1504 {.env = env,
1505 .issuer = gw,
1506 .holders = {alice},
1507 .pay = 1'000,
1508 .flags = tfMPTCanClawback | tfMPTRequireAuth | kMptDexFlags,
1509 .authHolder = true});
1510
1511 AMM ammAlice(env, alice, token(1'000), XRP(1'000));
1512 env.close();
1513 BEAST_EXPECT(env.balance(alice, token) == token(0));
1514
1515 // The holder can delete the zero-balance MPToken while still
1516 // holding LP tokens. A regular AMMWithdraw remains subject to
1517 // RequireAuth and cannot recreate the missing token.
1518 token.authorize({.account = alice, .flags = tfMPTUnauthorize});
1519 env.close();
1520 BEAST_EXPECT(!env.le(keylet::mptoken(token.issuanceID(), alice.id())));
1521 ammAlice.withdrawAll(alice, std::nullopt, Ter(tecNO_AUTH));
1522 env.close();
1523 BEAST_EXPECT(!env.le(keylet::mptoken(token.issuanceID(), alice.id())));
1524
1525 // AMMClawback ignores authorization and must be able to recreate
1526 // the holder MPToken so the issuer can recover MPT from the pool.
1528 if (clawAmount)
1529 amount = token(*clawAmount);
1530 env(amm::ammClawback(gw, alice, token, XRP, amount));
1531 env.close();
1532
1533 auto const sleMpt = env.le(keylet::mptoken(token.issuanceID(), alice.id()));
1534 BEAST_EXPECT(sleMpt && sleMpt->isFlag(lsfMPTAuthorized));
1535 env.require(Balance(alice, token(0)));
1536
1537 BEAST_EXPECT(clawAmount ? ammAlice.ammExists() : !ammAlice.ammExists());
1538 };
1539
1540 test(std::nullopt);
1541 test(400);
1542 }
1543
1544 void
1546 {
1547 testcase("test single depoit and clawback");
1548 using namespace jtx;
1549
1550 // MPT/XRP
1551 {
1552 Env env(*this, features);
1553 Account const gw{"gateway"};
1554 Account const alice{"alice"};
1555 env.fund(XRP(1000000000), gw, alice);
1556 env.close();
1557
1558 MPT const btc = MPTTester(
1559 {.env = env,
1560 .issuer = gw,
1561 .holders = {alice},
1562 .pay = 40'000'000000,
1563 .flags = tfMPTCanClawback | kMptDexFlags});
1564
1565 // gw creates AMM pool of BTC/XRP.
1566 AMM amm(env, gw, XRP(100), btc(400), Ter(tesSUCCESS));
1567 env.close();
1568 BEAST_EXPECT(amm.expectBalances(XRP(100), btc(400), IOUAmount(200000)));
1569 amm.deposit(alice, btc(400));
1570 BEAST_EXPECT(amm.expectBalances(XRP(100), btc(800), IOUAmount{282842'712474619, -9}));
1571
1572 auto aliceBTC = env.balance(alice, MPT(btc));
1573 auto aliceXRP = env.balance(alice, XRP);
1574
1575 // gw clawback 100 BTC from alice
1576 env(amm::ammClawback(gw, alice, MPT(btc), XRP, btc(100)));
1577 BEAST_EXPECT(amm.expectBalances(
1578 XRPAmount(87500001), btc(701), IOUAmount{247'487'3734152917, -10}));
1579
1580 env.require(Balance(alice, aliceBTC));
1581 env.require(Balance(alice, aliceXRP + XRPAmount(12'499999)));
1582 }
1583
1584 // MPT/IOU
1585 {
1586 Env env(*this, features);
1587 Account const gw{"gateway"};
1588 Account const alice{"alice"};
1589 env.fund(XRP(1000000000), gw, alice);
1590 env.close();
1591
1592 // gw sets asfAllowTrustLineClawback.
1593 env(fset(gw, asfAllowTrustLineClawback));
1594 env.close();
1595 env.require(Flags(gw, asfAllowTrustLineClawback));
1596
1597 // gw issues 1000 USD to Alice.
1598 auto const usd = gw["USD"];
1599 env.trust(usd(100000), alice);
1600 env(pay(gw, alice, usd(1000)));
1601 env.close();
1602
1603 MPT const btc = MPTTester(
1604 {.env = env,
1605 .issuer = gw,
1606 .holders = {alice},
1607 .pay = 40'000'000000,
1608 .flags = tfMPTCanClawback | kMptDexFlags});
1609
1610 // gw creates AMM pool of BTC/USD.
1611 AMM amm(env, gw, usd(100), btc(400), Ter(tesSUCCESS));
1612 env.close();
1613 BEAST_EXPECT(amm.expectBalances(usd(100), btc(400), IOUAmount(200)));
1614 amm.deposit(alice, btc(400));
1615 BEAST_EXPECT(amm.expectBalances(usd(100), btc(800), IOUAmount{282'842712474619, -12}));
1616
1617 auto aliceBTC = env.balance(alice, MPT(btc));
1618 auto aliceUSD = env.balance(alice, usd);
1619
1620 // gw clawback 100 BTC from alice
1621 env(amm::ammClawback(gw, alice, MPT(btc), usd, btc(100)));
1622 BEAST_EXPECT(amm.expectBalances(
1623 STAmount{usd, UINT64_C(87'50000000000003), -14},
1624 btc(701),
1625 IOUAmount{247'4873734152917, -13}));
1626
1627 env.require(Balance(alice, aliceBTC));
1628 env.require(Balance(alice, aliceUSD + usd(12.5)));
1629 aliceUSD = env.balance(alice, usd);
1630
1631 // gw clawback 30 USD from alice with tfClawTwoAssets, which exceeds
1632 // her balance
1633 env(amm::ammClawback(gw, alice, usd, MPT(btc), usd(30)), Txflags(tfClawTwoAssets));
1634 BEAST_EXPECT(amm.expectBalances(
1635 STAmount{usd, UINT64_C(70'71067811865476), -14}, btc(567), IOUAmount(200)));
1636 env.require(Balance(alice, aliceBTC));
1637 env.require(Balance(alice, aliceUSD));
1638 BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0)));
1639 BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount(200)));
1640 }
1641
1642 // MPT/MPT
1643 {
1644 Env env(*this, features);
1645 Account const gw{"gateway"};
1646 Account const alice{"alice"};
1647 env.fund(XRP(1000000000), gw, alice);
1648 env.close();
1649
1650 MPT const usd = MPTTester(
1651 {.env = env,
1652 .issuer = gw,
1653 .holders = {alice},
1654 .pay = 40'000'000000,
1655 .flags = tfMPTCanClawback | kMptDexFlags});
1656
1657 MPT const btc = MPTTester(
1658 {.env = env,
1659 .issuer = gw,
1660 .holders = {alice},
1661 .pay = 40'000'000000,
1662 .flags = tfMPTCanClawback | kMptDexFlags});
1663
1664 // gw creates AMM pool of BTC/USD.
1665 AMM amm(env, gw, usd(100), btc(400), Ter(tesSUCCESS));
1666 env.close();
1667 BEAST_EXPECT(amm.expectBalances(usd(100), btc(400), IOUAmount(200)));
1668 amm.deposit(alice, btc(400));
1669 BEAST_EXPECT(amm.expectBalances(usd(100), btc(800), IOUAmount{282'842712474619, -12}));
1670
1671 auto aliceBTC = env.balance(alice, MPT(btc));
1672 auto aliceUSD = env.balance(alice, usd);
1673
1674 // gw clawback 100 BTC from alice
1675 env(amm::ammClawback(gw, alice, MPT(btc), usd, btc(100)));
1676 BEAST_EXPECT(amm.expectBalances(usd(88), btc(701), IOUAmount{247'4873734152917, -13}));
1677
1678 env.require(Balance(alice, aliceBTC));
1679 env.require(Balance(alice, aliceUSD + usd(12)));
1680 aliceUSD = env.balance(alice, usd);
1681
1682 // gw clawback 30 USD from alice with tfClawTwoAssets, which exceeds
1683 // her balance
1684 env(amm::ammClawback(gw, alice, usd, MPT(btc), usd(30)), Txflags(tfClawTwoAssets));
1685 BEAST_EXPECT(amm.expectBalances(usd(72), btc(567), IOUAmount(200)));
1686 env.require(Balance(alice, aliceBTC));
1687 env.require(Balance(alice, aliceUSD));
1688 BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0)));
1689 BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount(200)));
1690 }
1691 }
1692
1693 void
1695 {
1696 testcase(
1697 "test last holder's lptoken balance not equal to AMM's lptoken "
1698 "balance before clawback");
1699 using namespace jtx;
1700 std::string logs;
1701
1702 // MPT/IOU
1703 {
1704 Env env(*this, features, std::make_unique<CaptureLogs>(&logs));
1705 Account const gw{"gateway"}, alice{"alice"}, bob{"bob"};
1706 env.fund(XRP(100000), gw, alice, bob);
1707 env.close();
1708 env(fset(gw, asfAllowTrustLineClawback));
1709 env.close();
1710
1711 auto const usd = gw["USD"];
1712 env.trust(usd(100000), alice);
1713 env(pay(gw, alice, usd(50000)));
1714 env.trust(usd(100000), bob);
1715 env(pay(gw, bob, usd(40000)));
1716 env.close();
1717
1718 MPT const eur = MPTTester(
1719 {.env = env,
1720 .issuer = gw,
1721 .holders = {alice, bob},
1722 .pay = 40'000'000000,
1723 .flags = tfMPTCanClawback | kMptDexFlags});
1724
1725 AMM amm(env, alice, usd(2), eur(1));
1726 amm.deposit(alice, IOUAmount{1'576123487565916, -15});
1727 amm.deposit(bob, IOUAmount{1'000});
1728 amm.withdraw(alice, IOUAmount{1'576123487565916, -15});
1729 amm.withdrawAll(bob);
1730
1731 auto const lpToken =
1732 getAccountLines(env, alice, amm.lptIssue())[jss::lines][0u][jss::balance]
1733 .asString();
1734 auto const lpTokenBalance =
1735 amm.ammRpcInfo()[jss::amm][jss::lp_token][jss::value].asString();
1736 if (largeMantissa(features))
1737 {
1738 BEAST_EXPECT(lpToken == "1.414213562374011" && lpTokenBalance == "1.4142135623741");
1739 }
1740 else
1741 {
1742 BEAST_EXPECT(lpToken == "1.414213562374011" && lpTokenBalance == "1.414213562374");
1743 }
1744
1745 auto res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice);
1746 BEAST_EXPECT(res && res.value());
1747
1748 if (features[fixAMMv1_3] && features[fixAMMClawbackRounding])
1749 {
1750 env(amm::ammClawback(gw, alice, usd, eur, std::nullopt));
1751 BEAST_EXPECT(!amm.ammExists());
1752 }
1753 else if (features[fixAMMv1_3] && largeMantissa(features))
1754 {
1755 env(amm::ammClawback(gw, alice, usd, eur, std::nullopt));
1756 // Without the Rounding feature and with new Number a dust pool
1757 // amount remains
1758 BEAST_EXPECT(amm.ammExists());
1759 }
1760 else if (!largeMantissa(features))
1761 {
1762 env(amm::ammClawback(gw, alice, usd, eur, std::nullopt), Ter(tecINTERNAL));
1763 BEAST_EXPECT(amm.ammExists());
1764 }
1765 else
1766 {
1767 env(amm::ammClawback(gw, alice, usd, eur, std::nullopt), Ter(tecAMM_BALANCE));
1768 BEAST_EXPECT(amm.ammExists());
1769 }
1770 }
1771
1772 // MPT/MPT
1773 {
1774 Env env(*this, features, std::make_unique<CaptureLogs>(&logs));
1775 Account const gw{"gateway"}, alice{"alice"}, bob{"bob"};
1776 env.fund(XRP(100000), gw, alice, bob);
1777 env.close();
1778
1779 MPT const usd = MPTTester(
1780 {.env = env,
1781 .issuer = gw,
1782 .holders = {alice, bob},
1783 .pay = 40'000'000000,
1784 .flags = tfMPTCanClawback | kMptDexFlags});
1785
1786 MPT const eur = MPTTester(
1787 {.env = env,
1788 .issuer = gw,
1789 .holders = {alice, bob},
1790 .pay = 40'000'000000,
1791 .flags = tfMPTCanClawback | kMptDexFlags});
1792
1793 AMM amm(env, alice, usd(2), eur(1));
1794 amm.deposit(alice, IOUAmount{1'576123487565916, -15});
1795 amm.deposit(bob, IOUAmount{1'000});
1796 amm.withdraw(alice, IOUAmount{1'576123487565916, -15});
1797 amm.withdrawAll(bob);
1798
1799 auto const lpToken =
1800 getAccountLines(env, alice, amm.lptIssue())[jss::lines][0u][jss::balance]
1801 .asString();
1802 auto const lpTokenBalance =
1803 amm.ammRpcInfo()[jss::amm][jss::lp_token][jss::value].asString();
1804 if (!largeMantissa(features))
1805 {
1806 BEAST_EXPECT(lpToken == "1.414213562374011" && lpTokenBalance == "1.414213562374");
1807 }
1808 else
1809 {
1810 BEAST_EXPECT(lpToken == "1.414213562374011" && lpTokenBalance == "1.4142135623741");
1811 }
1812
1813 auto res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice);
1814 BEAST_EXPECT(res && res.value());
1815
1816 if (features[fixAMMv1_3] && features[fixAMMClawbackRounding])
1817 {
1818 env(amm::ammClawback(gw, alice, usd, eur, std::nullopt));
1819 BEAST_EXPECT(!amm.ammExists());
1820 }
1821 else if (features[fixAMMv1_3] && largeMantissa(features))
1822 {
1823 // Without the Rounding feature and with new Number a dust pool
1824 // amount remains
1825 env(amm::ammClawback(gw, alice, usd, eur, std::nullopt));
1826 BEAST_EXPECT(amm.ammExists());
1827 }
1828 else if (!largeMantissa(features))
1829 {
1830 env(amm::ammClawback(gw, alice, usd, eur, std::nullopt), Ter(tecINTERNAL));
1831 BEAST_EXPECT(amm.ammExists());
1832 }
1833 else if (features[featureMPTokensV2])
1834 {
1835 env(amm::ammClawback(gw, alice, usd, eur, std::nullopt), Ter(tecAMM_BALANCE));
1836 BEAST_EXPECT(amm.ammExists());
1837 }
1838 }
1839 }
1840
1841 void
1843 {
1844 testcase("claw asset check for MPT and IOU");
1845 using namespace jtx;
1846
1847 // IOU/MPT, MPT not clawable
1848 {
1849 Env env(*this, features);
1850 Account const gw{"gateway"};
1851 Account const alice{"alice"};
1852 env.fund(XRP(100000), gw, alice);
1853 env.close();
1854
1855 env(fset(gw, asfAllowTrustLineClawback));
1856 env.close();
1857
1858 auto const usd = gw["USD"];
1859 env.trust(usd(100000), alice);
1860 env(pay(gw, alice, usd(1000)));
1861 env.close();
1862
1863 MPT const btc =
1864 MPTTester({.env = env, .issuer = gw, .holders = {alice}, .pay = 40'000});
1865
1866 AMM const amm(env, alice, usd(200), btc(100));
1867 // Asset BTC is not clawable without tfMPTCanClawback.
1868 env(amm::ammClawback(gw, alice, btc, usd, std::nullopt), Ter(tecNO_PERMISSION));
1869
1870 // Although USD is clawable with asfAllowTrustLineClawback.
1871 // When tfClawTwoAssets is set, we will claw Asset2 as well.
1872 // But Asset2 is not clawable. tfMPTCanClawback was not set for BTC.
1873 env(amm::ammClawback(gw, alice, usd, btc, std::nullopt),
1874 Txflags(tfClawTwoAssets),
1876
1877 // Can only claw the other asset
1878 env(amm::ammClawback(gw, alice, usd, btc, std::nullopt));
1879 }
1880
1881 // IOU/MPT, IOU not clawable
1882 {
1883 Env env(*this, features);
1884 Account const gw{"gateway"};
1885 Account const alice{"alice"};
1886 env.fund(XRP(100000), gw, alice);
1887 env.close();
1888
1889 auto const usd = gw["USD"];
1890 env.trust(usd(100000), alice);
1891 env(pay(gw, alice, usd(1000)));
1892 env.close();
1893
1894 MPT const btc = MPTTester(
1895 {.env = env,
1896 .issuer = gw,
1897 .holders = {alice},
1898 .pay = 40'000,
1899 .flags = tfMPTCanClawback | kMptDexFlags});
1900
1901 // Asset USD is not clawable without asfAllowTrustLineClawback.
1902 AMM const amm(env, alice, usd(200), btc(100));
1903 env(amm::ammClawback(gw, alice, usd, btc, std::nullopt), Ter(tecNO_PERMISSION));
1904
1905 // Although BTC is clawable with tfMPTCanClawback.
1906 // When tfClawTwoAssets is set, we will claw Asset2 as well.
1907 // But Asset2 is not clawable. asfAllowTrustLineClawback was not set
1908 // by the issuer.
1909 env(amm::ammClawback(gw, alice, btc, usd, std::nullopt),
1910 Txflags(tfClawTwoAssets),
1912
1913 // Can only claw the other asset
1914 env(amm::ammClawback(gw, alice, btc, usd, std::nullopt));
1915 }
1916
1917 // IOU/MPT both clawable
1918 {
1919 Env env(*this, features);
1920 Account const gw{"gateway"};
1921 Account const gw2{"gateway2"};
1922 Account const alice{"alice"};
1923 env.fund(XRP(100000), gw, gw2, alice);
1924 env.close();
1925
1926 env(fset(gw, asfAllowTrustLineClawback));
1927 env.close();
1928
1929 auto const usd = gw["USD"];
1930 env.trust(usd(100000), alice);
1931 env(pay(gw, alice, usd(1000)));
1932 env.close();
1933
1934 MPT const btc = MPTTester(
1935 {.env = env,
1936 .issuer = gw2,
1937 .holders = {alice},
1938 .pay = 40'000,
1939 .flags = tfMPTCanClawback | kMptDexFlags});
1940
1941 AMM const amm(env, alice, usd(200), btc(100));
1942
1943 // the account trying to claw MPT is not its issuer
1944 // will return temMALFORMED in preflight.
1945 env(amm::ammClawback(gw, alice, btc, usd, std::nullopt), Ter(temMALFORMED));
1946 }
1947
1948 // only issuer can claw. IOU/MPT mix
1949 {
1950 auto test = [&](auto&& issue1, auto&& issue2) {
1951 Env env(*this);
1952 Account const gw("gateway"), alice("alice"), bob("bob");
1953 env.fund(XRP(30'000), alice, bob, gw);
1954 env.close();
1955 auto const usd = issue1(
1956 {.env = env,
1957 .token = "USD",
1958 .issuer = gw,
1959 .holders = {alice},
1960 .limit = 1'000'000});
1961 auto const btc = issue2(
1962 {.env = env,
1963 .token = "BTC",
1964 .issuer = bob,
1965 .holders = {alice},
1966 .limit = 1'000'000});
1967 env(pay(gw, alice, usd(50000)));
1968 env(pay(bob, alice, btc(50000)));
1969 env.close();
1970
1971 auto ammAlice = AMM(env, alice, usd(10000), btc(10100));
1972 // BTC's issuer is bob, alice can not clawback
1973 env(amm::ammClawback(gw, alice, btc, usd, std::nullopt), Ter(temMALFORMED));
1974 };
1976 }
1977
1978 // set tfClawTwoAssets, but the two assets are from different issuer.
1979 {
1980 auto test = [&](auto&& issue1, auto&& issue2) {
1981 Env env(*this);
1982 Account const gw("gateway"), alice("alice"), bob("bob");
1983 env.fund(XRP(30'000), alice, bob, gw);
1984 env.close();
1985 auto const usd = issue1(
1986 {.env = env,
1987 .token = "USD",
1988 .issuer = gw,
1989 .holders = {alice},
1990 .limit = 1'000'000});
1991 auto const btc = issue2(
1992 {.env = env,
1993 .token = "BTC",
1994 .issuer = bob,
1995 .holders = {alice},
1996 .limit = 1'000'000});
1997 env(pay(gw, alice, usd(50000)));
1998 env(pay(bob, alice, btc(50000)));
1999 env.close();
2000
2001 auto ammAlice = AMM(env, alice, usd(10000), btc(10100));
2002 // BTC's issuer is bob. But with tfClawTwoAssets, we will claw
2003 // both. It will fail because the other asset USD's issuer is
2004 // gw.
2005 env(amm::ammClawback(bob, alice, btc, usd, std::nullopt),
2006 Txflags(tfClawTwoAssets),
2008 };
2010 }
2011 }
2012
2013 // Test that AMMClawback succeeds when the LP has previously deleted both
2014 // zero-balance MPToken objects in an MPT/MPT pool. The fix changes the
2015 // ValidMPTIssuance invariant threshold from > 1 to > 2 so that the two
2016 // MPToken creations triggered by the internal AMMWithdraw are permitted.
2017 void
2019 {
2020 testcase("test AMMClawback after holder deletes zero-balance MPTokens");
2021 using namespace jtx;
2022
2023 // Partial clawback (one asset): verify both MPTokens are recreated and
2024 // the non-claw asset is returned to alice.
2025 {
2026 Env env(*this, features);
2027 Account const gw{"gateway"};
2028 Account const alice{"alice"};
2029 env.fund(XRP(100'000), gw, alice);
2030 env.close();
2031
2032 MPTTester btc(
2033 {.env = env,
2034 .issuer = gw,
2035 .holders = {alice},
2036 .pay = 10'000,
2037 .flags = tfMPTCanClawback | kMptDexFlags});
2038
2039 MPTTester eth(
2040 {.env = env,
2041 .issuer = gw,
2042 .holders = {alice},
2043 .pay = 10'000,
2044 .flags = tfMPTCanClawback | kMptDexFlags});
2045
2046 // Alice deposits everything into the MPT/MPT pool; her MPT
2047 // balances drop to zero.
2048 AMM const amm(env, alice, btc(10'000), eth(10'000));
2049 env.close();
2050 BEAST_EXPECT(amm.expectBalances(btc(10'000), eth(10'000), IOUAmount{10'000}));
2051
2052 auto aliceBTC = env.balance(alice, btc);
2053 auto aliceETH = env.balance(alice, eth);
2054 BEAST_EXPECT(aliceBTC == btc(0));
2055 BEAST_EXPECT(aliceETH == eth(0));
2056
2057 // Alice deletes both zero-balance MPTokens to reclaim reserves.
2058 btc.authorize({.account = alice, .flags = tfMPTUnauthorize});
2059 eth.authorize({.account = alice, .flags = tfMPTUnauthorize});
2060 BEAST_EXPECT(!env.le(keylet::mptoken(btc.issuanceID(), alice.id())));
2061 BEAST_EXPECT(!env.le(keylet::mptoken(eth.issuanceID(), alice.id())));
2062
2063 // gw claws back some BTC from alice's share in the pool.
2064 // AMMWithdraw internally creates both missing MPTokens
2065 // (mptokensCreated_ == 2); the invariant (> 2) allows this.
2066 env(amm::ammClawback(gw, alice, btc, eth, btc(1'000)));
2067 env.close();
2068
2069 // Both MPToken objects must have been recreated.
2070 BEAST_EXPECT(env.le(keylet::mptoken(btc.issuanceID(), alice.id())));
2071 BEAST_EXPECT(env.le(keylet::mptoken(eth.issuanceID(), alice.id())));
2072
2073 // The non-claw asset (eth) was returned to alice.
2074 BEAST_EXPECT(env.balance(alice, eth) > aliceETH);
2075 // The claw asset (btc) was burned; alice's btc balance stays 0.
2076 env.require(Balance(alice, aliceBTC));
2077 BEAST_EXPECT(amm.ammExists());
2078 }
2079
2080 // Full clawback (two assets, tfClawTwoAssets): verify both MPTokens
2081 // are recreated and the AMM is deleted when fully drained.
2082 {
2083 Env env(*this, features);
2084 Account const gw{"gateway"};
2085 Account const alice{"alice"};
2086 env.fund(XRP(100'000), gw, alice);
2087 env.close();
2088
2089 MPTTester btc(
2090 {.env = env,
2091 .issuer = gw,
2092 .holders = {alice},
2093 .pay = 10'000,
2094 .flags = tfMPTCanClawback | kMptDexFlags});
2095
2096 MPTTester eth(
2097 {.env = env,
2098 .issuer = gw,
2099 .holders = {alice},
2100 .pay = 10'000,
2101 .flags = tfMPTCanClawback | kMptDexFlags});
2102
2103 AMM const amm(env, alice, btc(10'000), eth(10'000));
2104 env.close();
2105
2106 auto aliceBTC = env.balance(alice, btc);
2107 auto aliceETH = env.balance(alice, eth);
2108
2109 btc.authorize({.account = alice, .flags = tfMPTUnauthorize});
2110 eth.authorize({.account = alice, .flags = tfMPTUnauthorize});
2111 BEAST_EXPECT(!env.le(keylet::mptoken(btc.issuanceID(), alice.id())));
2112 BEAST_EXPECT(!env.le(keylet::mptoken(eth.issuanceID(), alice.id())));
2113
2114 // Full two-asset clawback: both assets are clawed and alice
2115 // receives nothing back. The AMM should be empty and deleted.
2116 env(amm::ammClawback(gw, alice, btc, eth, std::nullopt), Txflags(tfClawTwoAssets));
2117 env.close();
2118
2119 BEAST_EXPECT(!amm.ammExists());
2120 // Both assets were clawed; alice's balances remain at zero.
2121 env.require(Balance(alice, aliceBTC));
2122 env.require(Balance(alice, aliceETH));
2123 }
2124 }
2125
2126 void
2128 {
2129 testcase("test AMMClawback recreates paired-issuer MPToken unauthorized");
2130 using namespace jtx;
2131
2132 // Cross-issuer MPT/MPT pool: btc is issued by gw, eth by gw2, and both
2133 // require authorization. Alice deposits her entire balance of both and
2134 // deletes the resulting zero-balance MPTokens. When gw claws back its
2135 // own asset (btc), the two-asset withdrawal must recreate both of
2136 // Alice's MPTokens so the pool can pay her the paired asset. The
2137 // recreated MPToken may only be auto-authorized for the clawback
2138 // issuer's own asset (btc); the paired asset's issuer (gw2) never
2139 // consented, so eth must be recreated *unauthorized*, leaving gw2 in
2140 // control of its own token and preserving its RequireAuth guarantee.
2141 Env env(*this, features);
2142 Account const gw{"gateway"};
2143 Account const gw2{"gateway2"};
2144 Account const alice{"alice"};
2145 env.fund(XRP(100'000), gw, gw2, alice);
2146 env.close();
2147
2148 MPTTester btc(
2149 {.env = env,
2150 .issuer = gw,
2151 .holders = {alice},
2152 .pay = 10'000,
2153 .flags = tfMPTCanClawback | tfMPTRequireAuth | kMptDexFlags,
2154 .authHolder = true});
2155
2156 MPTTester eth(
2157 {.env = env,
2158 .issuer = gw2,
2159 .holders = {alice},
2160 .pay = 10'000,
2161 .flags = tfMPTCanClawback | tfMPTRequireAuth | kMptDexFlags,
2162 .authHolder = true});
2163
2164 // Alice deposits everything into the pool; her MPT balances drop to 0.
2165 AMM const amm(env, alice, btc(10'000), eth(10'000));
2166 env.close();
2167 BEAST_EXPECT(amm.expectBalances(btc(10'000), eth(10'000), IOUAmount{10'000}));
2168 BEAST_EXPECT(env.balance(alice, btc) == btc(0));
2169 BEAST_EXPECT(env.balance(alice, eth) == eth(0));
2170
2171 // Alice deletes both zero-balance MPTokens to reclaim reserves.
2172 btc.authorize({.account = alice, .flags = tfMPTUnauthorize});
2173 eth.authorize({.account = alice, .flags = tfMPTUnauthorize});
2174 BEAST_EXPECT(!env.le(keylet::mptoken(btc.issuanceID(), alice.id())));
2175 BEAST_EXPECT(!env.le(keylet::mptoken(eth.issuanceID(), alice.id())));
2176
2177 // gw (issuer of btc) claws back part of Alice's btc. This is a
2178 // cross-issuer pool, so tfClawTwoAssets is not permitted: only btc is
2179 // clawed back, while the paired eth is returned to Alice.
2180 env(amm::ammClawback(gw, alice, btc, eth, btc(1'000)));
2181 env.close();
2182
2183 // Both MPTokens were recreated so the withdrawal could pay Alice.
2184 auto const sleBtc = env.le(keylet::mptoken(btc.issuanceID(), alice.id()));
2185 auto const sleEth = env.le(keylet::mptoken(eth.issuanceID(), alice.id()));
2186 BEAST_EXPECT(sleBtc);
2187 BEAST_EXPECT(sleEth);
2188
2189 // The clawback issuer's own asset (btc) may be recreated authorized:
2190 // gw has authority over its own token.
2191 BEAST_EXPECT(sleBtc && sleBtc->isFlag(lsfMPTAuthorized));
2192
2193 // The paired asset (eth) is issued by gw2, who did not sign this
2194 // transaction. It must be recreated *unauthorized* so gw2's RequireAuth
2195 // is not bypassed. This is the core assertion for the cross-issuer fix.
2196 BEAST_EXPECT(sleEth && !sleEth->isFlag(lsfMPTAuthorized));
2197
2198 // The clawback still completed: btc was clawed back (Alice keeps a zero
2199 // btc balance) and the paired eth was delivered into Alice's now
2200 // unauthorized, gw2-gated MPToken (non-zero raw balance).
2201 BEAST_EXPECT(sleBtc && sleBtc->getFieldU64(sfMPTAmount) == 0);
2202 BEAST_EXPECT(sleEth && sleEth->getFieldU64(sfMPTAmount) > 0);
2203 BEAST_EXPECT(amm.ammExists());
2204 }
2205
2206 void
2208 {
2209 // Same as the IOU case, but the paired asset is an MPT alice does not
2210 // hold yet. The reserve check is skipped on the clawback path while
2211 // createMPToken() still runs, so alice's MPToken is created even though
2212 // neither she nor the low-XRP issuer can cover the owner reserve.
2213 testcase("test clawback bypasses recipient reserve (MPT)");
2214 using namespace jtx;
2215
2216 Env env(*this, features);
2217 Account const gw{"gateway"}; // IOU issuer + claw authority, low XRP
2218 Account const gw2{"gateway2"}; // MPT issuer of the paired asset
2219 Account const carol{"carol"};
2220 Account const alice{"alice"};
2221
2222 auto const usd = gw["USD"];
2223 auto const baseFee = env.current()->fees().base;
2224
2225 env.fund(XRP(1'000'000), gw2, carol);
2226 // Low XRP so the legacy issuer-balance check cannot pass.
2227 env.fund(env.current()->fees().accountReserve(0, 1) + baseFee * 10, gw);
2228 // Reserve for the USD trustline and LP token trustline.
2229 env.fund(env.current()->fees().accountReserve(2, 1) + baseFee * 5, alice);
2230 env.close();
2231
2232 env(fset(gw, asfAllowTrustLineClawback));
2233 env.close();
2234
2235 // The paired MPT: transferable so an AMM can hold it, and no
2236 // RequireAuth so createMPToken()'s WeakAuth check passes.
2237 MPT const btc = MPTTester(
2238 {.env = env,
2239 .issuer = gw2,
2240 .holders = {carol},
2241 .pay = 1'000'000,
2242 .flags = kMptDexFlags});
2243
2244 env.trust(usd(1'000'000), carol);
2245 env(pay(gw, carol, usd(100'000)));
2246 env.close();
2247 AMM amm(env, carol, usd(1'000), btc(1'000), Ter(tesSUCCESS));
2248 env.close();
2249
2250 // alice holds a USD trustline and LP tokens, but no BTC MPToken.
2251 env.trust(usd(100'000), alice);
2252 env(pay(gw, alice, usd(1'000)));
2253 env.close();
2254 amm.deposit(alice, usd(100));
2255
2256 BEAST_EXPECT(env.ownerCount(alice) == 2);
2257 BEAST_EXPECT(!env.le(keylet::mptoken(btc.issuanceID, alice.id())));
2258
2259 // AMMWithdraw still enforces the reserve check.
2260 amm.withdrawAll(alice, std::nullopt, Ter(tecINSUFFICIENT_RESERVE));
2261 BEAST_EXPECT(!env.le(keylet::mptoken(btc.issuanceID, alice.id())));
2262 BEAST_EXPECT(env.ownerCount(alice) == 2);
2263 // alice cannot afford a third owner object.
2264 BEAST_EXPECT(env.balance(alice) < STAmount(env.current()->fees().accountReserve(3, 1)));
2265
2266 if (features[fixCleanup3_4_0])
2267 {
2268 // Reserve check skipped; the paired BTC returns to alice on a
2269 // newly created MPToken.
2270 env(amm::ammClawback(gw, alice, usd, btc, usd(10)), Ter(tesSUCCESS));
2271 env.close();
2272
2273 BEAST_EXPECT(env.le(keylet::mptoken(btc.issuanceID, alice.id())));
2274 BEAST_EXPECT(env.balance(alice, btc) > btc(0));
2275 BEAST_EXPECT(env.ownerCount(alice) == 3);
2276 }
2277 else
2278 {
2279 // Legacy path: the check runs against max(issuer, holder) XRP,
2280 // neither of which covers a third owner object.
2281 env(amm::ammClawback(gw, alice, usd, btc, usd(10)), Ter(tecINSUFFICIENT_RESERVE));
2282 env.close();
2283
2284 BEAST_EXPECT(!env.le(keylet::mptoken(btc.issuanceID, alice.id())));
2285 BEAST_EXPECT(env.ownerCount(alice) == 2);
2286 }
2287 }
2288
2289 void
2290 run() override
2291 {
2292 FeatureBitset const all{jtx::testableAmendments() | fixAMMClawbackRounding};
2293
2294 testInvalidRequest(all);
2297 testAMMClawbackAmount(all - fixCleanup3_4_0);
2299 testAMMClawbackAmountRoundsToZero(all - fixCleanup3_4_0);
2300 testAMMClawbackAll(all);
2310 testLastHolderLPTokenBalance(all - fixAMMv1_3 - fixAMMClawbackRounding);
2312 all - fixAMMv1_3 - fixAMMClawbackRounding - featureSingleAssetVault -
2313 featureLendingProtocol);
2314 testLastHolderLPTokenBalance(all - fixAMMClawbackRounding);
2315 testClawAssetCheck(all);
2317 testClawbackBypassesReserve(all - fixCleanup3_4_0);
2318 }
2319};
2320
2321BEAST_DEFINE_TESTSUITE(AMMClawbackMPT, app, xrpl);
2322
2323} // namespace xrpl::test
A testsuite class.
Definition suite.h:52
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
std::string asString() const
Returns the unquoted string value.
Floating point representation of amounts with high dynamic range.
Definition IOUAmount.h:26
A currency issued by an account.
Definition Issue.h:18
void testAMMClawbackIssuesEachOther(FeatureBitset features)
void testAMMClawbackAmount(FeatureBitset features)
void testAMMClawbackAmountRoundsToZero(FeatureBitset features)
void testClawbackAfterDeletingMPTokens(FeatureBitset features)
void testClawbackBypassesReserve(FeatureBitset features)
void testClawbackCreatesMissingMPToken(FeatureBitset features)
void testInvalidRequest(FeatureBitset features)
void testAssetFrozenOrLocked(FeatureBitset features)
void testAMMClawbackAllSameIssuer(FeatureBitset features)
void testClawbackCrossIssuerPairedAssetAuth(FeatureBitset features)
void testAMMClawbackAmountSameIssuer(FeatureBitset features)
void testLastHolderLPTokenBalance(FeatureBitset features)
void testSingleDepositAndClawback(FeatureBitset features)
static bool largeMantissa(FeatureBitset const &features)
void testClawAssetCheck(FeatureBitset features)
void run() override
Runs the suite.
void testAMMClawbackAll(FeatureBitset features)
void testFeatureDisabled(FeatureBitset features)
Convenience class to test AMM functionality.
Immutable cryptographic account descriptor.
Definition jtx/Account.h:21
AccountID id() const
Returns the Account ID.
A transaction testing environment.
Definition Env.h:161
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
SLE::const_pointer le(Account const &account) const
Return an account root.
Definition Env.cpp:311
std::uint32_t ownerCount(Account const &account) const
Return the number of objects owned by an account.
Definition Env.cpp:266
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:323
PrettyAmount limit(Account const &account, Issue const &issue) const
Returns the IOU limit on an account.
Definition Env.cpp:254
PrettyAmount balance(Account const &account) const
Returns the XRP balance on an account.
Definition Env.cpp:201
void disableFeature(UInt256 const feature)
Definition Env.cpp:717
void trust(STAmount const &amount, Account const &account)
Establish trust lines.
Definition Env.cpp:354
void enableFeature(UInt256 const feature)
Definition Env.cpp:709
void require(Args const &... args)
Check a set of requirements.
Definition Env.h:766
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:377
Match set account flags.
Definition flags.h:119
Test helper for creating, mutating, and asserting MPT and confidential MPT ledger state.
Definition mpt.h:512
Converts to MPT Issue or STAmount.
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_unique(T... args)
Keylet mptoken(MPTID const &issuanceID, AccountID const &holder) noexcept
Definition Indexes.cpp:573
json::Value ammClawback(Account const &issuer, Account const &holder, Asset const &asset, Asset const &asset2, std::optional< STAmount > const &amount)
Definition AMM.cpp:911
auto const kMptDexFlags
Definition mpt.h:47
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
void testHelper2TokensMix(TTester &&tester)
XrpT const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
FeatureBitset testableAmendments()
Definition Env.h:92
json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition trust.cpp:18
PrettyAmount drops(Integer i)
Returns an XRP PrettyAmount, which is trivially convertible to STAmount.
json::Value getAccountLines(Env &env, AccountID const &acctId)
json::Value fset(Account const &account, std::uint32_t on, std::uint32_t off=0)
Add and/or remove flag.
Definition flags.cpp:15
BEAST_DEFINE_TESTSUITE(AMMClawback, app, xrpl)
constexpr XRPAmount
Convert XRP to drops (integral types).
Definition TxTest.h:54
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
@ terNO_AMM
Definition TER.h:228
@ terNO_ACCOUNT
Definition TER.h:218
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.
STAmount amountFromString(Asset const &asset, std::string const &amount)
Definition STAmount.cpp:908
MPTID makeMptID(std::uint32_t const sequence, AccountID const &account)
Definition Indexes.cpp:206
@ temINVALID_FLAG
Definition TER.h:99
@ temMALFORMED
Definition TER.h:75
@ temDISABLED
Definition TER.h:102
@ temBAD_AMOUNT
Definition TER.h:77
TERSubset< CanCvtToTER > TER
Definition TER.h:654
@ tecPSEUDO_ACCOUNT
Definition TER.h:370
@ tecAMM_FAILED
Definition TER.h:338
@ tecNO_AUTH
Definition TER.h:308
@ tecINTERNAL
Definition TER.h:318
@ tecAMM_BALANCE
Definition TER.h:337
@ tecAMM_ACCOUNT
Definition TER.h:342
@ tecINSUFFICIENT_RESERVE
Definition TER.h:315
@ tecNO_PERMISSION
Definition TER.h:313
@ tesSUCCESS
Definition TER.h:250