xrpld
Loading...
Searching...
No Matches
VaultLifecycle_test.cpp
1#include <test/app/vault/VaultTestBase.h>
2#include <test/jtx/AMM.h>
3#include <test/jtx/AMMTest.h>
4#include <test/jtx/Account.h>
5#include <test/jtx/Env.h>
6#include <test/jtx/TestHelpers.h>
7#include <test/jtx/amount.h>
8#include <test/jtx/flags.h>
9#include <test/jtx/mpt.h>
10#include <test/jtx/offer.h>
11#include <test/jtx/paths.h>
12#include <test/jtx/pay.h>
13#include <test/jtx/rate.h>
14#include <test/jtx/sendmax.h>
15#include <test/jtx/ter.h>
16#include <test/jtx/ticket.h>
17#include <test/jtx/trust.h>
18#include <test/jtx/vault.h>
19
20#include <xrpl/basics/Number.h>
21#include <xrpl/basics/base_uint.h>
22#include <xrpl/beast/unit_test/suite.h>
23#include <xrpl/beast/utility/Zero.h>
24#include <xrpl/json/json_forwards.h>
25#include <xrpl/json/json_value.h>
26#include <xrpl/protocol/AccountID.h>
27#include <xrpl/protocol/Asset.h>
28#include <xrpl/protocol/Feature.h>
29#include <xrpl/protocol/Indexes.h>
30#include <xrpl/protocol/Issue.h>
31#include <xrpl/protocol/Keylet.h>
32#include <xrpl/protocol/LedgerFormats.h>
33#include <xrpl/protocol/MPTIssue.h>
34#include <xrpl/protocol/SField.h>
35#include <xrpl/protocol/STAmount.h>
36#include <xrpl/protocol/TER.h>
37#include <xrpl/protocol/TxFlags.h>
38#include <xrpl/protocol/UintTypes.h>
39#include <xrpl/protocol/XRPAmount.h>
40#include <xrpl/protocol/jss.h>
41
42#include <cstdint>
43#include <functional>
44#include <string>
45#include <tuple>
46#include <utility>
47
48namespace xrpl {
49
51{
52private:
53 void
55 {
56 using namespace test::jtx;
57 Account const issuer{"issuer"};
58 Account const owner{"owner"};
59 Account const depositor{"depositor"};
60 Account const charlie{"charlie"}; // authorized 3rd party
61 Account const dave{"dave"};
62
63 auto const testSequence = [&, this](
64 std::string const& prefix,
65 Env& env,
66 Vault& vault,
67 PrettyAsset const& asset) {
68 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
69 tx[sfData] = "AFEED00E";
70 tx[sfAssetsMaximum] = asset(100).number();
71 env(tx);
72 env.close();
73 BEAST_EXPECT(env.le(keylet));
74 std::uint64_t const scale = asset.raw().holds<MPTIssue>() ? 1 : 1e6;
75
76 auto const [share, vaultAccount] =
77 [&env, keylet = keylet, asset, this]() -> std::tuple<PrettyAsset, Account> {
78 auto const vault = env.le(keylet);
79 BEAST_EXPECT(vault != nullptr);
80 if (!asset.integral())
81 {
82 BEAST_EXPECT(vault->at(sfScale) == 6);
83 }
84 else
85 {
86 BEAST_EXPECT(vault->at(sfScale) == 0);
87 }
88 auto const shares = env.le(keylet::mptokenIssuance(vault->at(sfShareMPTID)));
89 BEAST_EXPECT(shares != nullptr);
90 if (!asset.integral())
91 {
92 BEAST_EXPECT(shares->at(sfAssetScale) == 6);
93 }
94 else
95 {
96 BEAST_EXPECT(shares->at(sfAssetScale) == 0);
97 }
98 return {MPTIssue(vault->at(sfShareMPTID)), Account("vault", vault->at(sfAccount))};
99 }();
100 auto const shares = share.raw().get<MPTIssue>();
101 env.memoize(vaultAccount);
102
103 // Several 3rd party accounts which cannot receive funds
104 Account const alice{"alice"};
105 Account const erin{"erin"}; // not authorized by issuer
106 env.fund(XRP(1000), alice, erin);
107 env(fset(alice, asfDepositAuth));
108 env.close();
109
110 {
111 testcase(prefix + " fail to deposit more than assets held");
112 auto tx = vault.deposit(
113 {.depositor = depositor, .id = keylet.key, .amount = asset(10000)});
114 env(tx, Ter(tecINSUFFICIENT_FUNDS));
115 env.close();
116 }
117
118 {
119 testcase(prefix + " deposit non-zero amount");
120 auto tx =
121 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
122 env(tx);
123 env.close();
124 BEAST_EXPECT(env.balance(depositor, shares) == share(50 * scale));
125 }
126
127 {
128 testcase(prefix + " deposit non-zero amount again");
129 auto tx =
130 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
131 env(tx);
132 env.close();
133 BEAST_EXPECT(env.balance(depositor, shares) == share(100 * scale));
134 }
135
136 {
137 testcase(prefix + " fail to delete non-empty vault");
138 auto tx = vault.del({.owner = owner, .id = keylet.key});
139 env(tx, Ter(tecHAS_OBLIGATIONS));
140 env.close();
141 }
142
143 {
144 testcase(prefix + " fail to update because wrong owner");
145 auto tx = vault.set({.owner = issuer, .id = keylet.key});
146 tx[sfAssetsMaximum] = asset(50).number();
147 env(tx, Ter(tecNO_PERMISSION));
148 env.close();
149 }
150
151 {
152 testcase(prefix + " fail to set maximum lower than current amount");
153 auto tx = vault.set({.owner = owner, .id = keylet.key});
154 tx[sfAssetsMaximum] = asset(50).number();
155 env(tx, Ter(tecLIMIT_EXCEEDED));
156 env.close();
157 }
158
159 {
160 testcase(prefix + " set maximum higher than current amount");
161 auto tx = vault.set({.owner = owner, .id = keylet.key});
162 tx[sfAssetsMaximum] = asset(150).number();
163 env(tx);
164 env.close();
165 }
166
167 {
168 testcase(prefix + " set maximum is idempotent, set it again");
169 auto tx = vault.set({.owner = owner, .id = keylet.key});
170 tx[sfAssetsMaximum] = asset(150).number();
171 env(tx);
172 env.close();
173 }
174
175 {
176 testcase(prefix + " set data");
177 auto tx = vault.set({.owner = owner, .id = keylet.key});
178 tx[sfData] = "0";
179 env(tx);
180 env.close();
181 }
182
183 {
184 testcase(prefix + " fail to set domain on public vault");
185 auto tx = vault.set({.owner = owner, .id = keylet.key});
186 tx[sfDomainID] = to_string(BaseUInt<256>(42ul));
187 env(tx, Ter{tecNO_PERMISSION});
188 env.close();
189 }
190
191 {
192 testcase(prefix + " fail to deposit more than maximum");
193 auto tx =
194 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
195 env(tx, Ter(tecLIMIT_EXCEEDED));
196 env.close();
197 }
198
199 {
200 testcase(prefix + " reset maximum to zero i.e. not enforced");
201 auto tx = vault.set({.owner = owner, .id = keylet.key});
202 tx[sfAssetsMaximum] = asset(0).number();
203 env(tx);
204 env.close();
205 }
206
207 {
208 testcase(prefix + " fail to withdraw more than assets held");
209 auto tx = vault.withdraw(
210 {.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
211 env(tx, Ter(tecINSUFFICIENT_FUNDS));
212 env.close();
213 }
214
215 {
216 testcase(prefix + " deposit some more");
217 auto tx =
218 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
219 env(tx);
220 env.close();
221 BEAST_EXPECT(env.balance(depositor, shares) == share(200 * scale));
222 }
223
224 {
225 testcase(prefix + " clawback some");
226 auto code = asset.raw().native() ? Ter(temMALFORMED) : Ter(tesSUCCESS);
227 auto tx = vault.clawback(
228 {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(10)});
229 env(tx, code);
230 env.close();
231 if (!asset.raw().native())
232 {
233 BEAST_EXPECT(env.balance(depositor, shares) == share(190 * scale));
234 }
235 }
236
237 {
238 testcase(prefix + " clawback all");
239 auto code = asset.raw().native() ? Ter(tecNO_PERMISSION) : Ter(tesSUCCESS);
240 auto tx = vault.clawback({.issuer = issuer, .id = keylet.key, .holder = depositor});
241 env(tx, code);
242 env.close();
243 if (!asset.raw().native())
244 {
245 BEAST_EXPECT(env.balance(depositor, shares) == share(0));
246
247 {
248 auto tx = vault.clawback(
249 {.issuer = issuer,
250 .id = keylet.key,
251 .holder = depositor,
252 .amount = asset(10)});
253 env(tx, Ter{tecPRECISION_LOSS});
254 env.close();
255 }
256
257 {
258 auto tx = vault.withdraw(
259 {.depositor = depositor, .id = keylet.key, .amount = asset(10)});
260 env(tx, Ter{tecPRECISION_LOSS});
261 env.close();
262 }
263 }
264 }
265
266 if (!asset.raw().native())
267 {
268 testcase(prefix + " deposit again");
269 auto tx =
270 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(200)});
271 env(tx);
272 env.close();
273 BEAST_EXPECT(env.balance(depositor, shares) == share(200 * scale));
274 }
275 else
276 {
277 testcase(prefix + " deposit/withdrawal same or less than fee");
278 auto const amount = env.current()->fees().base;
279
280 auto tx =
281 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = amount});
282 env(tx);
283 env.close();
284
285 tx = vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = amount});
286 env(tx);
287 env.close();
288
289 tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = amount});
290 env(tx);
291 env.close();
292
293 // Withdraw to 3rd party
294 tx = vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = amount});
295 tx[sfDestination] = charlie.human();
296 env(tx);
297 env.close();
298
299 tx =
300 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = amount - 1});
301 env(tx);
302 env.close();
303
304 tx = vault.withdraw(
305 {.depositor = depositor, .id = keylet.key, .amount = amount - 1});
306 env(tx);
307 env.close();
308 }
309
310 {
311 testcase(prefix + " fail to withdraw to 3rd party lsfDepositAuth");
312 auto tx = vault.withdraw(
313 {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
314 tx[sfDestination] = alice.human();
315 env(tx, Ter{tecNO_PERMISSION});
316 env.close();
317 }
318
319 {
320 testcase(prefix + " fail to withdraw to zero destination");
321 auto tx = vault.withdraw(
322 {.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
323 tx[sfDestination] = "0";
324 env(tx, Ter(temMALFORMED));
325 env.close();
326 }
327
328 if (!asset.raw().native())
329 {
330 testcase(prefix + " fail to withdraw to 3rd party no authorization");
331 auto tx = vault.withdraw(
332 {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
333 tx[sfDestination] = erin.human();
334 env(tx, Ter{asset.raw().holds<Issue>() ? tecNO_LINE : tecNO_AUTH});
335 env.close();
336 }
337
338 {
339 testcase(prefix + " fail to withdraw to 3rd party lsfRequireDestTag");
340 auto tx = vault.withdraw(
341 {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
342 tx[sfDestination] = dave.human();
343 env(tx, Ter{tecDST_TAG_NEEDED});
344 env.close();
345 }
346
347 {
348 testcase(prefix + " withdraw to 3rd party lsfRequireDestTag");
349 auto tx =
350 vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
351 tx[sfDestination] = dave.human();
352 tx[sfDestinationTag] = "0";
353 env(tx);
354 env.close();
355 }
356
357 {
358 testcase(prefix + " deposit again");
359 auto tx = vault.deposit({.depositor = dave, .id = keylet.key, .amount = asset(50)});
360 env(tx);
361 env.close();
362 }
363
364 {
365 testcase(prefix + " fail to withdraw lsfRequireDestTag");
366 auto tx =
367 vault.withdraw({.depositor = dave, .id = keylet.key, .amount = asset(50)});
368 env(tx, Ter{tecDST_TAG_NEEDED});
369 env.close();
370 }
371
372 {
373 testcase(prefix + " withdraw with tag");
374 auto tx =
375 vault.withdraw({.depositor = dave, .id = keylet.key, .amount = asset(50)});
376 tx[sfDestinationTag] = "0";
377 env(tx);
378 env.close();
379 }
380
381 {
382 testcase(prefix + " withdraw to authorized 3rd party");
383 auto tx =
384 vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
385 tx[sfDestination] = charlie.human();
386 env(tx);
387 env.close();
388 BEAST_EXPECT(env.balance(depositor, shares) == share(100 * scale));
389 }
390
391 {
392 testcase(prefix + " withdraw to issuer");
393 auto tx =
394 vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
395 tx[sfDestination] = issuer.human();
396 env(tx);
397 env.close();
398 BEAST_EXPECT(env.balance(depositor, shares) == share(50 * scale));
399 }
400
401 if (!asset.raw().native())
402 {
403 testcase(prefix + " issuer deposits");
404 auto tx =
405 vault.deposit({.depositor = issuer, .id = keylet.key, .amount = asset(10)});
406 env(tx);
407 env.close();
408 BEAST_EXPECT(env.balance(issuer, shares) == share(10 * scale));
409
410 testcase(prefix + " issuer withdraws");
411 tx = vault.withdraw(
412 {.depositor = issuer, .id = keylet.key, .amount = share(10 * scale)});
413 env(tx);
414 env.close();
415 BEAST_EXPECT(env.balance(issuer, shares) == share(0 * scale));
416 }
417
418 {
419 testcase(prefix + " withdraw remaining assets");
420 auto tx =
421 vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
422 env(tx);
423 env.close();
424 BEAST_EXPECT(env.balance(depositor, shares) == share(0));
425
426 if (!asset.raw().native())
427 {
428 auto tx = vault.clawback(
429 {.issuer = issuer,
430 .id = keylet.key,
431 .holder = depositor,
432 .amount = asset(0)});
433 env(tx, Ter{tecPRECISION_LOSS});
434 env.close();
435 }
436
437 {
438 auto tx = vault.withdraw(
439 {.depositor = depositor, .id = keylet.key, .amount = share(10)});
440 env(tx, Ter{tecINSUFFICIENT_FUNDS});
441 env.close();
442 }
443 }
444
445 if (!asset.integral())
446 {
447 testcase(prefix + " temporary authorization for 3rd party");
448 env(trust(erin, asset(1000)));
449 env(trust(issuer, asset(0), erin, tfSetfAuth));
450 env(pay(issuer, erin, asset(10)));
451
452 // Erin deposits all in vault, then sends shares to depositor
453 auto tx = vault.deposit({.depositor = erin, .id = keylet.key, .amount = asset(10)});
454 env(tx);
455 env.close();
456 {
457 auto tx = pay(erin, depositor, share(10 * scale));
458
459 // depositor no longer has MPToken for shares
460 env(tx, Ter{tecNO_AUTH});
461 env.close();
462
463 // depositor will gain MPToken for shares again
464 env(vault.deposit(
465 {.depositor = depositor, .id = keylet.key, .amount = asset(1)}));
466 env.close();
467
468 env(tx);
469 env.close();
470 }
471
472 testcase(prefix + " withdraw to authorized 3rd party");
473 // Depositor withdraws assets, destined to Erin
474 tx =
475 vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(10)});
476 tx[sfDestination] = erin.human();
477 env(tx);
478 env.close();
479
480 // Erin returns assets to issuer
481 env(pay(erin, issuer, asset(10)));
482 env.close();
483
484 testcase(prefix + " fail to pay to unauthorized 3rd party");
485 env(trust(erin, asset(0)));
486 env.close();
487
488 // Erin has MPToken but is no longer authorized to hold assets
489 env(pay(depositor, erin, share(1)), Ter{tecNO_LINE});
490 env.close();
491
492 // Depositor withdraws remaining single asset
493 tx = vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(1)});
494 env(tx);
495 env.close();
496 }
497
498 {
499 testcase(prefix + " fail to delete because wrong owner");
500 auto tx = vault.del({.owner = issuer, .id = keylet.key});
501 env(tx, Ter(tecNO_PERMISSION));
502 env.close();
503 }
504
505 {
506 testcase(prefix + " delete empty vault");
507 auto tx = vault.del({.owner = owner, .id = keylet.key});
508 env(tx);
509 env.close();
510 BEAST_EXPECT(!env.le(keylet));
511 }
512 };
513
514 auto testCases = [&, this](
515 std::string prefix, std::function<PrettyAsset(Env & env)> setup) {
516 Env env{*this, testableAmendments()};
517
518 Vault vault{env};
519 env.fund(XRP(1000), issuer, owner, depositor, charlie, dave);
520 env.close();
521 env(fset(issuer, asfAllowTrustLineClawback));
522 env(fset(issuer, asfRequireAuth));
523 env(fset(dave, asfRequireDest));
524 env.close();
525 env.require(Flags(issuer, asfAllowTrustLineClawback));
526 env.require(Flags(issuer, asfRequireAuth));
527
528 PrettyAsset const asset = setup(env);
529 testSequence(prefix, env, vault, asset);
530 };
531
532 testCases("XRP", [&](Env& env) -> PrettyAsset { return {xrpIssue(), 1'000'000}; });
533
534 testCases("IOU", [&](Env& env) -> Asset {
535 PrettyAsset const asset = issuer["IOU"];
536 env(trust(owner, asset(1000)));
537 env(trust(depositor, asset(1000)));
538 env(trust(charlie, asset(1000)));
539 env(trust(dave, asset(1000)));
540 env(trust(issuer, asset(0), owner, tfSetfAuth));
541 env(trust(issuer, asset(0), depositor, tfSetfAuth));
542 env(trust(issuer, asset(0), charlie, tfSetfAuth));
543 env(trust(issuer, asset(0), dave, tfSetfAuth));
544 env(pay(issuer, depositor, asset(1000)));
545 env.close();
546 return asset;
547 });
548
549 testCases("MPT", [&](Env& env) -> Asset {
550 MPTTester mptt{env, issuer, kMptInitNoFund};
551 mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
552 PrettyAsset const asset = mptt.issuanceID();
553 mptt.authorize({.account = depositor});
554 mptt.authorize({.account = charlie});
555 mptt.authorize({.account = dave});
556 env(pay(issuer, depositor, asset(1000)));
557 env.close();
558 return asset;
559 });
560 }
561
562 void
564 {
565 using namespace test::jtx;
566
567 struct CaseArgs
568 {
569 bool enableClawback = true;
570 bool requireAuth = true;
571 int initialXRP = 1000;
572 FeatureBitset features = testableAmendments();
573 };
574
575 auto testCase = [this](
576 std::function<void(
577 Env & env,
578 Account const& issuer,
579 Account const& owner,
580 Account const& depositor,
581 Asset const& asset,
582 Vault& vault,
583 MPTTester& mptt)> test,
584 CaseArgs args = {}) {
585 Env env{*this, args.features};
586 Account const issuer{"issuer"};
587 Account const owner{"owner"};
588 Account const depositor{"depositor"};
589 env.fund(XRP(args.initialXRP), issuer, owner, depositor);
590 env.close();
591 Vault vault{env};
592
593 MPTTester mptt{env, issuer, kMptInitNoFund};
594 auto const kNone = LedgerSpecificFlags(0);
595 mptt.create(
596 {.flags = tfMPTCanTransfer | tfMPTCanLock |
597 (args.enableClawback ? tfMPTCanClawback : kNone) |
598 (args.requireAuth ? tfMPTRequireAuth : kNone)});
599 PrettyAsset const asset = mptt.issuanceID();
600 mptt.authorize({.account = owner});
601 mptt.authorize({.account = depositor});
602 if (args.requireAuth)
603 {
604 mptt.authorize({.account = issuer, .holder = owner});
605 mptt.authorize({.account = issuer, .holder = depositor});
606 }
607
608 env(pay(issuer, depositor, asset(1000)));
609 env.close();
610
611 test(env, issuer, owner, depositor, asset, vault, mptt);
612 };
613
614 testCase([this](
615 Env& env,
616 Account const& issuer,
617 Account const& owner,
618 Account const& depositor,
619 PrettyAsset const& asset,
620 Vault& vault,
621 MPTTester& mptt) {
622 testcase("MPT nothing to clawback from");
623 auto tx = vault.clawback(
624 {.issuer = issuer,
625 .id = keylet::skip().key,
626 .holder = depositor,
627 .amount = asset(10)});
628 env(tx, Ter(tecNO_ENTRY));
629 });
630
631 testCase([this](
632 Env& env,
633 Account const& issuer,
634 Account const& owner,
635 Account const& depositor,
636 Asset const& asset,
637 Vault& vault,
638 MPTTester& mptt) {
639 testcase("MPT global lock blocks create");
640 mptt.set({.account = issuer, .flags = tfMPTLock});
641 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
642 env(tx, Ter(tecLOCKED));
643 });
644
645 testCase([this](
646 Env& env,
647 Account const& issuer,
648 Account const& owner,
649 Account const& depositor,
650 PrettyAsset const& asset,
651 Vault& vault,
652 MPTTester& mptt) {
653 testcase("MPT only issuer can clawback");
654
655 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
656 env(tx);
657 env.close();
658
659 tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
660 env(tx);
661 env.close();
662
663 {
664 auto tx = vault.clawback({
665 .issuer = depositor,
666 .id = keylet.key,
667 .holder = depositor,
668 });
669 env(tx, Ter(tecNO_PERMISSION));
670 }
671
672 {
673 auto tx = vault.clawback({
674 .issuer = owner,
675 .id = keylet.key,
676 .holder = depositor,
677 });
678 env(tx, Ter(tecNO_PERMISSION));
679 }
680 });
681
682 testCase(
683 [this](
684 Env& env,
685 Account const& issuer,
686 Account const& owner,
687 Account const& depositor,
688 PrettyAsset const& asset,
689 Vault& vault,
690 MPTTester& mptt) {
691 testcase("MPT depositor without MPToken, auth required");
692
693 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
694 env(tx);
695 env.close();
696
697 tx = vault.deposit(
698 {.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
699 env(tx);
700 env.close();
701
702 {
703 // Remove depositor MPToken and it will not be re-created
704 mptt.authorize({.account = depositor, .flags = tfMPTUnauthorize});
705 env.close();
706
707 auto const mptoken = keylet::mptoken(mptt.issuanceID(), depositor);
708 auto const sleMPT1 = env.le(mptoken);
709 BEAST_EXPECT(sleMPT1 == nullptr);
710
711 tx = vault.withdraw(
712 {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
713 env(tx, Ter{tecNO_AUTH});
714 env.close();
715
716 auto const sleMPT2 = env.le(mptoken);
717 BEAST_EXPECT(sleMPT2 == nullptr);
718 }
719
720 {
721 // Set destination to 3rd party without MPToken
722 Account const charlie{"charlie"};
723 env.fund(XRP(1000), charlie);
724 env.close();
725
726 tx = vault.withdraw(
727 {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
728 tx[sfDestination] = charlie.human();
729 env(tx, Ter(tecNO_AUTH));
730 }
731 },
732 {.requireAuth = true});
733
734 testCase(
735 [this](
736 Env& env,
737 Account const& issuer,
738 Account const& owner,
739 Account const& depositor,
740 PrettyAsset const& asset,
741 Vault& vault,
742 MPTTester& mptt) {
743 testcase("MPT depositor without MPToken, no auth required");
744
745 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
746 env(tx);
747 env.close();
748 auto v = env.le(keylet);
749 BEAST_EXPECT(v);
750
751 tx = vault.deposit(
752 {.depositor = depositor,
753 .id = keylet.key,
754 .amount = asset(1000)}); // all assets held by depositor
755 env(tx);
756 env.close();
757
758 {
759 // Remove depositor's MPToken and it will be re-created
760 mptt.authorize({.account = depositor, .flags = tfMPTUnauthorize});
761 env.close();
762
763 auto const mptoken = keylet::mptoken(mptt.issuanceID(), depositor);
764 auto const sleMPT1 = env.le(mptoken);
765 BEAST_EXPECT(sleMPT1 == nullptr);
766
767 tx = vault.withdraw(
768 {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
769 env(tx);
770 env.close();
771
772 auto const sleMPT2 = env.le(mptoken);
773 BEAST_EXPECT(sleMPT2 != nullptr);
774 BEAST_EXPECT(sleMPT2->at(sfMPTAmount) == 100);
775 }
776
777 {
778 // Remove 3rd party MPToken and it will not be re-created
779 mptt.authorize({.account = owner, .flags = tfMPTUnauthorize});
780 env.close();
781
782 auto const mptoken = keylet::mptoken(mptt.issuanceID(), owner);
783 auto const sleMPT1 = env.le(mptoken);
784 BEAST_EXPECT(sleMPT1 == nullptr);
785
786 tx = vault.withdraw(
787 {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
788 tx[sfDestination] = owner.human();
789 env(tx, Ter(tecNO_AUTH));
790 env.close();
791
792 auto const sleMPT2 = env.le(mptoken);
793 BEAST_EXPECT(sleMPT2 == nullptr);
794 }
795 },
796 {.requireAuth = false});
797
798 auto const redeemAllNoAssetMpt = [this](TER expected) {
799 return [this, expected](
800 Env& env,
801 Account const&,
802 Account const& owner,
803 Account const& depositor,
804 Asset const& asset,
805 Vault& vault,
806 MPTTester& mptt) {
807 testcase << "MPT non-owner redeems all shares with no asset MPToken"
808 << (isTesSuccess(expected) ? "" : " pre-fixCleanup3_4_0");
809
810 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
811 env(tx);
812 env.close();
813
814 tx = vault.deposit(
815 {.depositor = depositor,
816 .id = keylet.key,
817 .amount = asset(1000)}); // all assets held by depositor
818 env(tx);
819 env.close();
820
821 auto const vaultSle = env.le(keylet);
822 if (!BEAST_EXPECT(vaultSle))
823 return;
824 auto const shareMPTID = vaultSle->at(sfShareMPTID);
825
826 // Depositor's asset MPToken balance is now zero; delete it.
827 mptt.authorize({.account = depositor, .flags = tfMPTUnauthorize});
828 env.close();
829
830 auto const mptoken = keylet::mptoken(mptt.issuanceID(), depositor);
831
832 auto const shareKeylet = keylet::mptoken(shareMPTID, depositor.id());
833 auto const sleShareBefore = env.le(shareKeylet);
834 if (!BEAST_EXPECT(sleShareBefore))
835 return;
836 auto const shareAmountBefore = sleShareBefore->at(sfMPTAmount);
837 auto const assetsTotalBefore = vaultSle->at(sfAssetsTotal);
838 auto const assetsAvailableBefore = vaultSle->at(sfAssetsAvailable);
839
840 // Redeeming ALL shares in one transaction both erases the
841 // now-empty share MPToken and re-creates the asset MPToken.
842 tx = vault.withdraw(
843 {.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
844 env(tx, Ter{expected});
845 env.close();
846
847 auto const sleAsset = env.le(mptoken);
848 auto const sleShare = env.le(shareKeylet);
849 auto const vaultAfter = env.le(keylet);
850 if (!BEAST_EXPECT(vaultAfter))
851 return;
852 if (isTesSuccess(expected))
853 {
854 if (!BEAST_EXPECT(sleAsset))
855 return;
856 BEAST_EXPECT(sleAsset->at(sfMPTAmount) == 1000);
857 BEAST_EXPECT(!sleShare);
858 BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == beast::kZero);
859 BEAST_EXPECT(vaultAfter->at(sfAssetsAvailable) == beast::kZero);
860 }
861 else
862 {
863 BEAST_EXPECT(!sleAsset);
864 if (!BEAST_EXPECT(sleShare))
865 return;
866 BEAST_EXPECT(sleShare->at(sfMPTAmount) == shareAmountBefore);
867 BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == assetsTotalBefore);
868 BEAST_EXPECT(vaultAfter->at(sfAssetsAvailable) == assetsAvailableBefore);
869 }
870 };
871 };
872
873 testCase(redeemAllNoAssetMpt(tesSUCCESS), {.requireAuth = false});
874 testCase(
875 redeemAllNoAssetMpt(tecINVARIANT_FAILED),
876 {.requireAuth = false, .features = testableAmendments() - fixCleanup3_4_0});
877
878 auto const [acctReserve, incReserve] = [this]() -> std::pair<int, int> {
879 Env const env{*this, testableAmendments()};
880 return {
881 env.current()->fees().accountReserve(0, 1).drops() / kDropsPerXrp.drops(),
882 env.current()->fees().increment.drops() / kDropsPerXrp.drops()};
883 }();
884
885 testCase(
886 [&, this](
887 Env& env,
888 Account const& issuer,
889 Account const& owner,
890 Account const& depositor,
891 PrettyAsset const& asset,
892 Vault& vault,
893 MPTTester& mptt) {
894 testcase("MPT fail reserve to re-create MPToken");
895
896 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
897 env(tx);
898 env.close();
899 auto v = env.le(keylet);
900 BEAST_EXPECT(v);
901
902 env(pay(depositor, owner, asset(1000)));
903 env.close();
904
905 tx = vault.deposit(
906 {.depositor = owner,
907 .id = keylet.key,
908 .amount = asset(1000)}); // all assets held by owner
909 env(tx);
910 env.close();
911
912 {
913 // Remove owners's MPToken and it will not be re-created
914 mptt.authorize({.account = owner, .flags = tfMPTUnauthorize});
915 env.close();
916
917 auto const mptoken = keylet::mptoken(mptt.issuanceID(), owner);
918 auto const sleMPT = env.le(mptoken);
919 BEAST_EXPECT(sleMPT == nullptr);
920
921 // Use one reserve so the next transaction fails
922 env(ticket::create(owner, 1));
923 env.close();
924
925 // No reserve to create MPToken for asset in VaultWithdraw
926 tx = vault.withdraw(
927 {.depositor = owner, .id = keylet.key, .amount = asset(100)});
928 env(tx, Ter{tecINSUFFICIENT_RESERVE});
929 env.close();
930
931 env(pay(depositor, owner, XRP(incReserve)));
932 env.close();
933
934 // Withdraw can now create asset MPToken, tx will succeed
935 env(tx);
936 env.close();
937 }
938 },
939 {.requireAuth = false, .initialXRP = acctReserve + (incReserve * 4) + 1});
940
941 testCase([this](
942 Env& env,
943 Account const& issuer,
944 Account const& owner,
945 Account const& depositor,
946 PrettyAsset const& asset,
947 Vault& vault,
948 MPTTester& mptt) {
949 testcase("MPT issuance deleted");
950
951 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
952 env(tx);
953 env.close();
954
955 tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
956 env(tx);
957 env.close();
958
959 {
960 auto tx = vault.clawback(
961 {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(0)});
962 env(tx);
963 }
964
965 mptt.destroy({.issuer = issuer, .id = mptt.issuanceID()});
966 env.close();
967
968 {
969 auto [tx, keylet] = vault.create({.owner = depositor, .asset = asset});
970 env(tx, Ter{tecOBJECT_NOT_FOUND});
971 }
972
973 {
974 auto tx =
975 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(10)});
976 env(tx, Ter{tecOBJECT_NOT_FOUND});
977 }
978
979 {
980 auto tx =
981 vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(10)});
982 env(tx, Ter{tecOBJECT_NOT_FOUND});
983 }
984
985 {
986 auto tx = vault.clawback(
987 {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(0)});
988 env(tx, Ter{tecOBJECT_NOT_FOUND});
989 }
990
991 env(vault.del({.owner = owner, .id = keylet.key}));
992 });
993
994 testCase([this](
995 Env& env,
996 Account const& issuer,
997 Account const& owner,
998 Account const& depositor,
999 PrettyAsset const& asset,
1000 Vault& vault,
1001 MPTTester& mptt) {
1002 testcase("MPT vault owner can receive shares unless unauthorized");
1003
1004 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1005 env(tx);
1006 env.close();
1007
1008 tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
1009 env(tx);
1010 env.close();
1011
1012 auto const issuanceId = [&env](xrpl::Keylet keylet) -> MPTID {
1013 auto const vault = env.le(keylet);
1014 return vault->at(sfShareMPTID);
1015 }(keylet);
1016 PrettyAsset const shares = MPTIssue(issuanceId);
1017
1018 {
1019 // owner has MPToken for shares they did not explicitly create
1020 env(pay(depositor, owner, shares(1)));
1021 env.close();
1022
1023 tx = vault.withdraw({.depositor = owner, .id = keylet.key, .amount = shares(1)});
1024 env(tx);
1025 env.close();
1026
1027 // owner's MPToken for vault shares not destroyed by withdraw
1028 env(pay(depositor, owner, shares(1)));
1029 env.close();
1030
1031 tx = vault.clawback(
1032 {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(0)});
1033 env(tx);
1034 env.close();
1035
1036 // owner's MPToken for vault shares not destroyed by clawback
1037 env(pay(depositor, owner, shares(1)));
1038 env.close();
1039
1040 // pay back, so we can destroy owner's MPToken now
1041 env(pay(owner, depositor, shares(1)));
1042 env.close();
1043
1044 {
1045 // explicitly destroy vault owners MPToken with zero balance
1046 json::Value jv;
1047 jv[sfAccount] = owner.human();
1048 jv[sfMPTokenIssuanceID] = to_string(issuanceId);
1049 jv[sfFlags] = tfMPTUnauthorize;
1050 jv[sfTransactionType] = jss::MPTokenAuthorize;
1051 env(jv);
1052 env.close();
1053 }
1054
1055 // owner no longer has MPToken for vault shares
1056 tx = pay(depositor, owner, shares(1));
1057 env(tx, Ter{tecNO_AUTH});
1058 env.close();
1059
1060 // destroy all remaining shares, so we can delete vault
1061 tx = vault.clawback(
1062 {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(0)});
1063 env(tx);
1064 env.close();
1065
1066 // will soft fail destroying MPToken for vault owner
1067 env(vault.del({.owner = owner, .id = keylet.key}));
1068 env.close();
1069 }
1070 });
1071
1072 testCase(
1073 [this](
1074 Env& env,
1075 Account const& issuer,
1076 Account const& owner,
1077 Account const& depositor,
1078 PrettyAsset const& asset,
1079 Vault& vault,
1080 MPTTester& mptt) {
1081 testcase("MPT clawback disabled");
1082
1083 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1084 env(tx);
1085 env.close();
1086
1087 tx = vault.deposit(
1088 {.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
1089 env(tx);
1090 env.close();
1091
1092 {
1093 auto tx = vault.clawback(
1094 {.issuer = issuer,
1095 .id = keylet.key,
1096 .holder = depositor,
1097 .amount = asset(0)});
1098 env(tx, Ter{tecNO_PERMISSION});
1099 }
1100 },
1101 {.enableClawback = false});
1102
1103 testCase([this](
1104 Env& env,
1105 Account const& issuer,
1106 Account const& owner,
1107 Account const& depositor,
1108 Asset const& asset,
1109 Vault& vault,
1110 MPTTester& mptt) {
1111 testcase("MPT un-authorization");
1112 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1113 env(tx);
1114 env.close();
1115 tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
1116 env(tx);
1117 env.close();
1118
1119 mptt.authorize({.account = issuer, .holder = depositor, .flags = tfMPTUnauthorize});
1120 env.close();
1121
1122 {
1123 auto tx = vault.withdraw(
1124 {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
1125 env(tx, Ter(tecNO_AUTH));
1126
1127 // Withdrawal to other (authorized) accounts works
1128 tx[sfDestination] = issuer.human();
1129 env(tx);
1130 env.close();
1131
1132 tx[sfDestination] = owner.human();
1133 env(tx);
1134 env.close();
1135 }
1136
1137 {
1138 // Cannot deposit some more
1139 auto tx =
1140 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
1141 env(tx, Ter(tecNO_AUTH));
1142 }
1143
1144 {
1145 // Cannot clawback if issuer is the holder
1146 tx = vault.clawback(
1147 {.issuer = issuer, .id = keylet.key, .holder = issuer, .amount = asset(800)});
1148 env(tx, Ter(tecNO_PERMISSION));
1149 }
1150 // Clawback works
1151 tx = vault.clawback(
1152 {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(800)});
1153 env(tx);
1154 env.close();
1155
1156 env(vault.del({.owner = owner, .id = keylet.key}));
1157 });
1158
1159 {
1160 testcase("MPT shares to a vault");
1161
1162 Env env{*this, testableAmendments()};
1163 Account const owner{"owner"};
1164 Account const issuer{"issuer"};
1165 env.fund(XRP(1000000), owner, issuer);
1166 env.close();
1167 Vault const vault{env};
1168
1169 MPTTester mptt{env, issuer, kMptInitNoFund};
1170 mptt.create(
1171 {.flags = tfMPTCanTransfer | tfMPTCanLock | lsfMPTCanClawback | tfMPTRequireAuth});
1172 mptt.authorize({.account = owner});
1173 mptt.authorize({.account = issuer, .holder = owner});
1174 PrettyAsset const asset = mptt.issuanceID();
1175 env(pay(issuer, owner, asset(100)));
1176 auto [tx1, k1] = vault.create({.owner = owner, .asset = asset});
1177 env(tx1);
1178 env.close();
1179
1180 auto const shares = [&env, keylet = k1, this]() -> Asset {
1181 auto const vault = env.le(keylet);
1182 BEAST_EXPECT(vault != nullptr);
1183 return MPTIssue(vault->at(sfShareMPTID));
1184 }();
1185
1186 auto [tx2, k2] = vault.create({.owner = owner, .asset = shares});
1187 env(tx2, Ter{tecWRONG_ASSET});
1188 env.close();
1189 }
1190
1191 {
1192 testcase("MPT locked: vault shares inherit underlying lock");
1193
1194 Env env{*this, testableAmendments()};
1195 Account const issuer{"issuer"};
1196 Account const owner{"owner"};
1197 Account const alice{"alice"};
1198 Account const bob{"bob"};
1199 Account const carol{"carol"};
1200 env.fund(XRP(10'000), issuer, owner, alice, bob, carol);
1201 env.close();
1202 Vault const vault{env};
1203
1204 MPTTester asset{
1205 {.env = env,
1206 .issuer = issuer,
1207 .holders = {owner, alice, bob, carol},
1208 .flags = tfMPTCanTransfer | tfMPTCanTrade | tfMPTCanLock}};
1209 env(pay(issuer, alice, asset(1'000)));
1210 env(pay(issuer, bob, asset(1'000)));
1211 env.close();
1212
1213 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1214 env(tx);
1215 env.close();
1216
1217 env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(500)}));
1218 // Bob also deposits so he has a share MPToken to receive into.
1219 env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(500)}));
1220 env.close();
1221
1222 auto const shares = [&]() -> PrettyAsset {
1223 auto const sle = env.le(keylet);
1224 BEAST_EXPECT(sle != nullptr);
1225 return MPTIssue(sle->at(sfShareMPTID));
1226 }();
1227 auto const shareMptID = shares.raw().get<MPTIssue>().getMptID();
1228 auto const shareBalance = [&](Account const& account) {
1229 auto const sle = env.le(keylet::mptoken(shareMptID, account));
1230 return sle ? sle->at(sfMPTAmount) : 0;
1231 };
1232
1233 // Sanity: before the underlying lock, peer-to-peer share
1234 // transfers are allowed.
1235 env(pay(alice, bob, shares(1)));
1236 env.close();
1237
1238 // Create the offer while shares are spendable, then lock the
1239 // underlying to test whether a stale offer can still be crossed.
1240 env(offer(alice, XRP(1), shares(1)));
1241 env.close();
1242
1243 // Lock the underlying after the vault and share balances exist.
1244 asset.set({.account = issuer, .flags = tfMPTLock});
1245 env.close();
1246
1247 // Direct vault share payment inherits the underlying lock via
1248 // sfReferenceHolding.
1249 BEAST_EXPECT(shareBalance(alice) == 499);
1250 BEAST_EXPECT(shareBalance(bob) == 501);
1251 env(pay(alice, bob, shares(1)), Ter{tecLOCKED});
1252 env.close();
1253 BEAST_EXPECT(shareBalance(alice) == 499);
1254 BEAST_EXPECT(shareBalance(bob) == 501);
1255
1256 // The same inherited lock must also block DEX payment paths that
1257 // would consume an offer selling vault shares.
1258 env(pay(carol, bob, shares(1)),
1259 Sendmax(XRP(1)),
1260 Path(BookSpec{shares.raw()}),
1261 Ter{tecPATH_PARTIAL});
1262 env.close();
1263 BEAST_EXPECT(shareBalance(alice) == 499);
1264 BEAST_EXPECT(shareBalance(bob) == 501);
1265 BEAST_EXPECT(expectOffers(env, alice, 1));
1266 }
1267
1268 {
1269 testcase("MPT CanTrade governance: share inherits underlying on DEX and AMM");
1270
1271 Env env{*this, testableAmendments()};
1272 Account const issuer{"issuer"};
1273 Account const owner{"owner"};
1274 Account const alice{"alice"};
1275 Account const bob{"bob"};
1276 env.fund(XRP(100'000), issuer, owner, alice, bob);
1277 env.close();
1278 Vault const vault{env};
1279
1280 MPTTester mptt{env, issuer, kMptInitNoFund};
1281 mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
1282 PrettyAsset const asset = mptt.issuanceID();
1283 mptt.authorize({.account = owner});
1284 mptt.authorize({.account = alice});
1285 mptt.authorize({.account = bob});
1286 env(pay(issuer, alice, asset(10'000)));
1287 env(pay(issuer, bob, asset(10'000)));
1288 env.close();
1289
1290 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1291 env(tx);
1292 env.close();
1293
1294 // Seed shares so we can later place them on trading venues.
1295 env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(5'000)}));
1296 env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(5'000)}));
1297 env.close();
1298
1299 auto const shares = [&]() -> PrettyAsset {
1300 auto const sle = env.le(keylet);
1301 BEAST_EXPECT(sle != nullptr);
1302 return MPTIssue(sle->at(sfShareMPTID));
1303 }();
1304
1305 // CanTrade is not set on the underlying, both the asset and
1306 // the vault share are blocked on the DEX.
1307 env(offer(alice, XRP(1), asset(10)), Ter{tecNO_PERMISSION});
1308 env(offer(alice, XRP(1), shares(1)), Ter{tecNO_PERMISSION});
1309 env.close();
1310
1311 // Deposit still works before enabling CanTrade.
1312 env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(100)}));
1313 env.close();
1314
1315 // Peer-to-peer share transfers still work (CanTransfer is set on
1316 // both layers).
1317 env(pay(alice, bob, shares(1)));
1318 env.close();
1319
1320 // Withdraw still works before enabling CanTrade.
1321 env(vault.withdraw({.depositor = alice, .id = keylet.key, .amount = asset(100)}));
1322 env.close();
1323
1324 // Enable CanTrade on the underlying.
1325 mptt.set({.flags = tfMPTSetCanTrade});
1326 env.close();
1327
1328 env(offer(alice, XRP(1), asset(10)));
1329 env(offer(alice, XRP(1), shares(1)));
1330 env.close();
1331
1332 AMM const ammUnderlying(env, alice, XRP(1'000), asset(1'000));
1333 }
1334
1335 {
1336 testcase("MPT OutstandingAmount > MaximumAmount");
1337
1338 Env env{*this, testableAmendments() | featureSingleAssetVault};
1339 Account const alice{"alice"};
1340 Account const issuer{"issuer"};
1341 env.fund(XRP(1'000), alice, issuer);
1342 env.close();
1343 Vault const vault{env};
1344
1345 MPTTester const btc({.env = env, .issuer = issuer, .holders = {alice}, .maxAmt = 100});
1346
1347 auto [tx, k] = vault.create({.owner = issuer, .asset = btc});
1348 env(tx);
1349 env.close();
1350
1351 tx = vault.deposit({.depositor = issuer, .id = k.key, .amount = btc(110)});
1352 // accountHolds is the first check and the issuer has only BTC(100)
1353 // available
1354 env(tx, Ter{tecINSUFFICIENT_FUNDS});
1355 env.close();
1356
1357 // OutstandingAmount == MaximumAmount
1358 env(pay(issuer, alice, btc(100)));
1359 env.close();
1360
1361 tx = vault.deposit({.depositor = issuer, .id = k.key, .amount = btc(100)});
1362 // the issuer has BTC(0) available
1363 env(tx, Ter{tecINSUFFICIENT_FUNDS});
1364 env.close();
1365
1366 tx = vault.deposit({.depositor = alice, .id = k.key, .amount = btc(100)});
1367 // alice transfers BTC(100), OutstandingAmount is 100
1368 env(tx);
1369 env.close();
1370 }
1371 }
1372
1373 void
1375 {
1376 using namespace test::jtx;
1377
1378 struct CaseArgs
1379 {
1380 int initialXRP = 1000;
1381 Number initialIOU = 200;
1382 double transferRate = 1.0;
1383 bool charlieRipple = true;
1384 FeatureBitset features = testableAmendments();
1385 };
1386
1387 auto testCase = [&, this](
1388 std::function<void(
1389 Env & env,
1390 Account const& owner,
1391 Account const& issuer,
1392 Account const& charlie,
1393 std::function<Account(xrpl::Keylet)> vaultAccount,
1394 Vault& vault,
1395 PrettyAsset const& asset,
1396 std::function<MPTID(xrpl::Keylet)> issuanceId)> test,
1397 CaseArgs args = {}) {
1398 Env env{*this, args.features};
1399 Account const owner{"owner"};
1400 Account const issuer{"issuer"};
1401 Account const charlie{"charlie"};
1402 Vault vault{env};
1403 env.fund(XRP(args.initialXRP), issuer, owner, charlie);
1404 env(fset(issuer, asfAllowTrustLineClawback));
1405 env.close();
1406
1407 PrettyAsset const asset = issuer["IOU"];
1408 env.trust(asset(1000), owner);
1409 env(pay(issuer, owner, asset(args.initialIOU)));
1410 env.close();
1411 if (!args.charlieRipple)
1412 {
1413 env(fset(issuer, 0, asfDefaultRipple));
1414 env.close();
1415 env.trust(asset(1000), charlie);
1416 env.close();
1417 env(pay(issuer, charlie, asset(args.initialIOU)));
1418 env.close();
1419 env(fset(issuer, asfDefaultRipple));
1420 }
1421 else
1422 {
1423 env.trust(asset(1000), charlie);
1424 }
1425 env.close();
1426 env(rate(issuer, args.transferRate));
1427 env.close();
1428
1429 auto const vaultAccount = [&env](xrpl::Keylet keylet) -> Account {
1430 return Account("vault", env.le(keylet)->at(sfAccount));
1431 };
1432 auto const issuanceId = [&env](xrpl::Keylet keylet) -> MPTID {
1433 return env.le(keylet)->at(sfShareMPTID);
1434 };
1435
1436 test(env, owner, issuer, charlie, vaultAccount, vault, asset, issuanceId);
1437 };
1438
1439 testCase([&, this](
1440 Env& env,
1441 Account const& owner,
1442 Account const& issuer,
1443 Account const&,
1444 auto vaultAccount,
1445 Vault& vault,
1446 PrettyAsset const& asset,
1447 auto&&...) {
1448 testcase("IOU cannot use different asset");
1449 PrettyAsset const foo = issuer["FOO"];
1450
1451 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1452 env(tx);
1453 env.close();
1454
1455 {
1456 // Cannot create new trustline to a vault
1457 auto tx = [&, account = vaultAccount(keylet)]() {
1458 json::Value jv;
1459 jv[jss::Account] = issuer.human();
1460 {
1461 auto& ja = jv[jss::LimitAmount] =
1462 foo(0).value().getJson(JsonOptions::Values::None);
1463 ja[jss::issuer] = toBase58(account);
1464 }
1465 jv[jss::TransactionType] = jss::TrustSet;
1466 jv[jss::Flags] = tfSetFreeze;
1467 return jv;
1468 }();
1469 env(tx, Ter{tecNO_PERMISSION});
1470 env.close();
1471 }
1472
1473 {
1474 auto tx = vault.deposit({.depositor = issuer, .id = keylet.key, .amount = foo(20)});
1475 env(tx, Ter{tecWRONG_ASSET});
1476 env.close();
1477 }
1478
1479 {
1480 auto tx =
1481 vault.withdraw({.depositor = issuer, .id = keylet.key, .amount = foo(20)});
1482 env(tx, Ter{tecWRONG_ASSET});
1483 env.close();
1484 }
1485
1486 env(vault.del({.owner = owner, .id = keylet.key}));
1487 env.close();
1488 });
1489
1490 testCase(
1491 [&, this](
1492 Env& env,
1493 Account const& owner,
1494 Account const& issuer,
1495 Account const& charlie,
1496 auto vaultAccount,
1497 Vault& vault,
1498 PrettyAsset const& asset,
1499 auto issuanceId) {
1500 testcase("IOU transfer fees not applied");
1501
1502 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1503 env(tx);
1504 env.close();
1505
1506 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
1507 env.close();
1508
1509 auto const issue = asset.raw().get<Issue>();
1510 Asset const share = Asset(issuanceId(keylet));
1511
1512 // transfer fees ignored on deposit
1513 BEAST_EXPECT(env.balance(owner, issue) == asset(100));
1514 BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(100));
1515
1516 {
1517 auto tx = vault.clawback(
1518 {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(50)});
1519 env(tx);
1520 env.close();
1521 }
1522
1523 // transfer fees ignored on clawback
1524 BEAST_EXPECT(env.balance(owner, issue) == asset(100));
1525 BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(50));
1526
1527 env(vault.withdraw(
1528 {.depositor = owner, .id = keylet.key, .amount = share(20'000'000)}));
1529
1530 // transfer fees ignored on withdraw
1531 BEAST_EXPECT(env.balance(owner, issue) == asset(120));
1532 BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(30));
1533
1534 {
1535 auto tx = vault.withdraw(
1536 {.depositor = owner, .id = keylet.key, .amount = share(30'000'000)});
1537 tx[sfDestination] = charlie.human();
1538 env(tx);
1539 }
1540
1541 // transfer fees ignored on withdraw to 3rd party
1542 BEAST_EXPECT(env.balance(owner, issue) == asset(120));
1543 BEAST_EXPECT(env.balance(charlie, issue) == asset(30));
1544 BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(0));
1545
1546 env(vault.del({.owner = owner, .id = keylet.key}));
1547 env.close();
1548 },
1549 CaseArgs{.transferRate = 1.25});
1550
1551 testCase([&, this](
1552 Env& env,
1553 Account const& owner,
1554 Account const& issuer,
1555 Account const& charlie,
1556 auto,
1557 Vault& vault,
1558 PrettyAsset const& asset,
1559 auto&&...) {
1560 testcase("IOU no trust line to 3rd party");
1561
1562 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1563 env(tx);
1564 env.close();
1565
1566 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
1567 env.close();
1568
1569 Account const erin{"erin"};
1570 env.fund(XRP(1000), erin);
1571 env.close();
1572
1573 // Withdraw to 3rd party without trust line
1574 auto const tx1 = [&](xrpl::Keylet keylet) {
1575 auto tx =
1576 vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
1577 tx[sfDestination] = erin.human();
1578 return tx;
1579 }(keylet);
1580 env(tx1, Ter{tecNO_LINE});
1581 });
1582
1583 testCase([&, this](
1584 Env& env,
1585 Account const& owner,
1586 Account const& issuer,
1587 Account const& charlie,
1588 auto,
1589 Vault& vault,
1590 PrettyAsset const& asset,
1591 auto&&...) {
1592 testcase("IOU no trust line to depositor");
1593
1594 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1595 env(tx);
1596 env.close();
1597
1598 // reset limit, so deposit of all funds will delete the trust line
1599 env.trust(asset(0), owner);
1600 env.close();
1601
1602 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(200)}));
1603 env.close();
1604
1605 auto trustline = env.le(keylet::trustLine(owner, asset.raw().get<Issue>()));
1606 BEAST_EXPECT(trustline == nullptr);
1607
1608 // Withdraw without trust line, will succeed
1609 auto const tx1 = [&](xrpl::Keylet keylet) {
1610 auto tx =
1611 vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
1612 return tx;
1613 }(keylet);
1614 env(tx1);
1615 });
1616
1617 testCase(
1618 [&, this](
1619 Env& env,
1620 Account const& owner,
1621 Account const& issuer,
1622 Account const& charlie,
1623 auto vaultAccount,
1624 Vault& vault,
1625 PrettyAsset const& asset,
1626 std::function<MPTID(xrpl::Keylet)> issuanceId) {
1627 testcase("IOU non-transferable");
1628
1629 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1630 tx[sfScale] = 0;
1631 env(tx);
1632 env.close();
1633
1634 // Turn on noripple on the pseudo account's trust line.
1635 // Charlie's is already set.
1636 env(trust(issuer, vaultAccount(keylet)["IOU"], tfSetNoRipple));
1637
1638 {
1639 // Charlie cannot deposit
1640 auto tx = vault.deposit(
1641 {.depositor = charlie, .id = keylet.key, .amount = asset(100)});
1642 env(tx, Ter{terNO_RIPPLE});
1643 env.close();
1644 }
1645
1646 {
1647 PrettyAsset const shares = issuanceId(keylet);
1648 auto tx1 =
1649 vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)});
1650 env(tx1);
1651 env.close();
1652
1653 // Charlie cannot receive funds
1654 auto tx2 = vault.withdraw(
1655 {.depositor = owner, .id = keylet.key, .amount = shares(100)});
1656 tx2[sfDestination] = charlie.human();
1657 env(tx2, Ter{terNO_RIPPLE});
1658 env.close();
1659
1660 {
1661 // Create MPToken for shares held by Charlie
1663 tx[sfAccount] = charlie.human();
1664 tx[sfMPTokenIssuanceID] =
1665 to_string(shares.raw().get<MPTIssue>().getMptID());
1666 tx[sfTransactionType] = jss::MPTokenAuthorize;
1667 env(tx);
1668 env.close();
1669 }
1670 // Behavioral shift introduced by share inheritance:
1671 // before fixCleanup3_2_0 this share Payment succeeded
1672 // and the underlying IOU's NoRipple restriction surfaced
1673 // only later on Charlie's withdrawal (terNO_RIPPLE).
1674 // Post-amendment, canTransfer reads the share's
1675 // sfReferenceHolding and dispatches to the underlying IOU;
1676 // rippling is disabled between owner and charlie so the
1677 // share payment itself is now blocked. tecPATH_DRY is
1678 // the path-find layer's translation of the underlying
1679 // terNO_RIPPLE under featureMPTokensV2.
1680 env(pay(owner, charlie, shares(100)), Ter{tecPATH_DRY});
1681 env.close();
1682 }
1683
1684 tx = vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(100)});
1685 env(tx);
1686 env.close();
1687
1688 // Delete vault with zero balance
1689 env(vault.del({.owner = owner, .id = keylet.key}));
1690 },
1691 {.charlieRipple = false});
1692
1693 testCase(
1694 [&, this](
1695 Env& env,
1696 Account const& owner,
1697 Account const& issuer,
1698 Account const& charlie,
1699 auto const& vaultAccount,
1700 Vault& vault,
1701 PrettyAsset const& asset,
1702 auto&&...) {
1703 testcase("IOU calculation rounding");
1704
1705 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1706 tx[sfScale] = 1;
1707 env(tx);
1708 env.close();
1709
1710 auto const startingOwnerBalance = env.balance(owner, asset);
1711 BEAST_EXPECT((startingOwnerBalance.value() == STAmount{asset, 11875, -2}));
1712
1713 // This operation (first deposit 100, then 3.75 x 5) is known to
1714 // have triggered calculation rounding errors in Number
1715 // (addition and division), causing the last deposit to be
1716 // blocked by Vault invariants.
1717 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
1718
1719 auto const tx1 = vault.deposit(
1720 {.depositor = owner, .id = keylet.key, .amount = asset(Number(375, -2))});
1721 for (auto i = 0; i < 5; ++i)
1722 {
1723 env(tx1);
1724 }
1725 env.close();
1726
1727 {
1728 STAmount const xfer{asset, 1185, -1};
1729 BEAST_EXPECT(env.balance(owner, asset) == startingOwnerBalance.value() - xfer);
1730 BEAST_EXPECT(env.balance(vaultAccount(keylet), asset) == xfer);
1731
1732 auto const vault = env.le(keylet);
1733 BEAST_EXPECT(vault->at(sfAssetsAvailable) == xfer);
1734 BEAST_EXPECT(vault->at(sfAssetsTotal) == xfer);
1735 }
1736
1737 // Total vault balance should be 118.5 IOU. Withdraw and delete
1738 // the vault to verify this exact amount was deposited and the
1739 // owner has matching shares
1740 env(vault.withdraw(
1741 {.depositor = owner,
1742 .id = keylet.key,
1743 .amount = asset(Number(1000 + (37 * 5), -1))}));
1744
1745 {
1746 BEAST_EXPECT(env.balance(owner, asset) == startingOwnerBalance.value());
1747 BEAST_EXPECT(env.balance(vaultAccount(keylet), asset) == beast::kZero);
1748 auto const vault = env.le(keylet);
1749 BEAST_EXPECT(vault->at(sfAssetsAvailable) == beast::kZero);
1750 BEAST_EXPECT(vault->at(sfAssetsTotal) == beast::kZero);
1751 }
1752
1753 env(vault.del({.owner = owner, .id = keylet.key}));
1754 env.close();
1755 },
1756 {.initialIOU = Number(11875, -2)});
1757
1758 auto const [acctReserve, incReserve] = [this]() -> std::pair<int, int> {
1759 Env const env{*this, testableAmendments()};
1760 return {
1761 env.current()->fees().accountReserve(0, 1).drops() / kDropsPerXrp.drops(),
1762 env.current()->fees().increment.drops() / kDropsPerXrp.drops()};
1763 }();
1764
1765 testCase(
1766 [&, this](
1767 Env& env,
1768 Account const& owner,
1769 Account const& issuer,
1770 Account const& charlie,
1771 auto,
1772 Vault& vault,
1773 PrettyAsset const& asset,
1774 auto&&...) {
1775 testcase("IOU no trust line to depositor no reserve");
1776 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1777 env(tx);
1778 env.close();
1779
1780 // reset limit, so deposit of all funds will delete the trust
1781 // line
1782 env.trust(asset(0), owner);
1783 env.close();
1784
1785 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(200)}));
1786 env.close();
1787
1788 auto trustline = env.le(keylet::trustLine(owner, asset.raw().get<Issue>()));
1789 BEAST_EXPECT(trustline == nullptr);
1790
1791 env(ticket::create(owner, 1));
1792 env.close();
1793
1794 // Fail because not enough reserve to create trust line
1795 tx = vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
1796 env(tx, Ter{tecNO_LINE_INSUF_RESERVE});
1797 env.close();
1798
1799 env(pay(charlie, owner, XRP(incReserve)));
1800 env.close();
1801
1802 // Withdraw can now create trust line, will succeed
1803 env(tx);
1804 env.close();
1805 },
1806 CaseArgs{.initialXRP = acctReserve + (incReserve * 4) + 1});
1807
1808 testCase(
1809 [&, this](
1810 Env& env,
1811 Account const& owner,
1812 Account const& issuer,
1813 Account const& charlie,
1814 auto,
1815 Vault& vault,
1816 PrettyAsset const& asset,
1817 auto&&...) {
1818 testcase("IOU no reserve for share MPToken");
1819 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1820 env(tx);
1821 env.close();
1822
1823 env(pay(owner, charlie, asset(100)));
1824 env.close();
1825
1826 env(ticket::create(charlie, 3));
1827 env.close();
1828
1829 // Fail because not enough reserve to create MPToken for shares
1830 tx = vault.deposit({.depositor = charlie, .id = keylet.key, .amount = asset(100)});
1831 env(tx, Ter{tecINSUFFICIENT_RESERVE});
1832 env.close();
1833
1834 env(pay(issuer, charlie, XRP(incReserve)));
1835 env.close();
1836
1837 // Deposit can now create MPToken, will succeed
1838 env(tx);
1839 env.close();
1840 },
1841 CaseArgs{.initialXRP = acctReserve + (incReserve * 4) + 1});
1842 }
1843
1844public:
1845 void
1846 run() override
1847 {
1848 testSequences();
1849 testWithMPT();
1850 testWithIOU();
1851 }
1852};
1853
1854BEAST_DEFINE_TESTSUITE_PRIO(VaultLifecycle, app, xrpl, 1);
1855
1856} // namespace xrpl
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
Represents a JSON value.
Definition json_value.h:117
constexpr TIss const & get() const
Integers of any length that is a multiple of 32-bits.
Definition base_uint.h:82
A currency issued by an account.
Definition Issue.h:18
constexpr MPTID const & getMptID() const
Definition MPTIssue.h:43
Number is a floating point type that can represent a wide range of values.
Definition Number.h:351
void run() override
Runs the suite.
Shared base for the Vault*_test family under src/test/app/vault/.
test::jtx::PrettyAsset PrettyAsset
constexpr Zero kZero
Definition Zero.h:30
@ Object
object value (collection of name/value pairs).
Definition json_value.h:29
Keylet computation functions.
Definition Indexes.h:40
Keylet const & skip() noexcept
The index of the "short" skip list.
Definition Indexes.cpp:232
Keylet mptoken(MPTID const &issuanceID, AccountID const &holder) noexcept
Definition Indexes.cpp:573
Keylet mptokenIssuance(MPTID const &issuanceID) noexcept
Definition Indexes.cpp:567
Keylet trustLine(AccountID const &id0, AccountID const &id1, Currency const &currency) noexcept
The index of a trust line for a given currency.
Definition Indexes.cpp:275
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
@ terNO_RIPPLE
Definition TER.h:225
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Definition Issue.h:108
constexpr XRPAmount kDropsPerXrp
Number of drops per 1 XRP.
Definition XRPAmount.h:254
int scale(Number const &number, Asset const &asset)
Get the scale of a Number for a given asset.
Definition STAmount.h:794
std::string toBase58(AccountID const &v)
Convert AccountID to base58 checked string.
Definition AccountID.cpp:95
BEAST_DEFINE_TESTSUITE_PRIO(AccountSet, app, xrpl, 1)
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:657
LedgerSpecificFlags
Rate transferRate(ReadView const &view, AccountID const &issuer)
Returns IOU issuer transfer fee as Rate.
BaseUInt< 192 > MPTID
MPTID is a 192-bit value representing MPT Issuance ID, which is a concatenation of a 32-bit sequence ...
Definition UintTypes.h:54
@ temMALFORMED
Definition TER.h:75
bool isTesSuccess(TER x) noexcept
Definition TER.h:683
TERSubset< CanCvtToTER > TER
Definition TER.h:654
TER requireAuth(ReadView const &view, MPTIssue const &mptIssue, AccountID const &account, AuthType authType=AuthType::Legacy, std::uint8_t depth=0)
Check if the account lacks required authorization for MPT.
@ tecWRONG_ASSET
Definition TER.h:368
@ tecLOCKED
Definition TER.h:366
@ tecNO_LINE_INSUF_RESERVE
Definition TER.h:300
@ tecPATH_PARTIAL
Definition TER.h:290
@ tecNO_ENTRY
Definition TER.h:314
@ tecPATH_DRY
Definition TER.h:302
@ tecOBJECT_NOT_FOUND
Definition TER.h:334
@ tecNO_AUTH
Definition TER.h:308
@ tecINVARIANT_FAILED
Definition TER.h:321
@ tecINSUFFICIENT_FUNDS
Definition TER.h:333
@ tecNO_LINE
Definition TER.h:309
@ tecPRECISION_LOSS
Definition TER.h:371
@ tecINSUFFICIENT_RESERVE
Definition TER.h:315
@ tecLIMIT_EXCEEDED
Definition TER.h:369
@ tecNO_PERMISSION
Definition TER.h:313
@ tecDST_TAG_NEEDED
Definition TER.h:317
@ tecHAS_OBLIGATIONS
Definition TER.h:325
@ tesSUCCESS
Definition TER.h:250
A pair of SHAMap key and LedgerEntryType.
Definition Keylet.h:20
UInt256 key
Definition Keylet.h:21