xrpld
Loading...
Searching...
No Matches
Vault_test.cpp
1#include <test/jtx/AMM.h>
2#include <test/jtx/AMMTest.h>
3#include <test/jtx/Account.h>
4#include <test/jtx/CaptureLogs.h>
5#include <test/jtx/Env.h>
6#include <test/jtx/TestHelpers.h>
7#include <test/jtx/amount.h>
8#include <test/jtx/credentials.h>
9#include <test/jtx/escrow.h>
10#include <test/jtx/fee.h>
11#include <test/jtx/flags.h>
12#include <test/jtx/mpt.h>
13#include <test/jtx/offer.h>
14#include <test/jtx/paths.h>
15#include <test/jtx/pay.h>
16#include <test/jtx/permissioned_domains.h>
17#include <test/jtx/rate.h>
18#include <test/jtx/sendmax.h>
19#include <test/jtx/seq.h>
20#include <test/jtx/sig.h>
21#include <test/jtx/tags.h>
22#include <test/jtx/ter.h>
23#include <test/jtx/ticket.h>
24#include <test/jtx/trust.h>
25#include <test/jtx/utility.h>
26#include <test/jtx/vault.h>
27
28#include <xrpl/basics/Number.h>
29#include <xrpl/basics/base_uint.h>
30#include <xrpl/basics/strHex.h>
31#include <xrpl/beast/unit_test/suite.h>
32#include <xrpl/beast/utility/Journal.h>
33#include <xrpl/beast/utility/Zero.h>
34#include <xrpl/core/ServiceRegistry.h>
35#include <xrpl/json/json_forwards.h>
36#include <xrpl/json/json_value.h>
37#include <xrpl/json/to_string.h>
38#include <xrpl/ledger/ApplyView.h>
39#include <xrpl/ledger/OpenView.h>
40#include <xrpl/ledger/Sandbox.h>
41#include <xrpl/ledger/helpers/AccountRootHelpers.h>
42#include <xrpl/ledger/helpers/MPTokenHelpers.h>
43#include <xrpl/protocol/AccountID.h>
44#include <xrpl/protocol/Asset.h>
45#include <xrpl/protocol/Feature.h>
46#include <xrpl/protocol/Indexes.h>
47#include <xrpl/protocol/Issue.h>
48#include <xrpl/protocol/LedgerFormats.h>
49#include <xrpl/protocol/MPTIssue.h>
50#include <xrpl/protocol/Protocol.h>
51#include <xrpl/protocol/SField.h>
52#include <xrpl/protocol/STAmount.h>
53#include <xrpl/protocol/SystemParameters.h>
54#include <xrpl/protocol/TER.h>
55#include <xrpl/protocol/TxFlags.h>
56#include <xrpl/protocol/UintTypes.h>
57#include <xrpl/protocol/Units.h>
58#include <xrpl/protocol/XRPAmount.h>
59#include <xrpl/protocol/jss.h>
60
61#include <chrono>
62#include <cstdint>
63#include <functional>
64#include <limits>
65#include <memory>
66#include <optional>
67#include <string>
68#include <tuple>
69#include <utility>
70
71namespace xrpl {
72
74{
77
78 static constexpr auto kNegativeAmount = [](PrettyAsset const& asset) -> PrettyAmount {
79 return {STAmount{asset.raw(), 1ul, 0, true, STAmount::Unchecked{}}, ""};
80 };
81
82 void
84 {
85 using namespace test::jtx;
86 Account const issuer{"issuer"};
87 Account const owner{"owner"};
88 Account const depositor{"depositor"};
89 Account const charlie{"charlie"}; // authorized 3rd party
90 Account const dave{"dave"};
91
92 auto const testSequence = [&, this](
93 std::string const& prefix,
94 Env& env,
95 Vault& vault,
96 PrettyAsset const& asset) {
97 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
98 tx[sfData] = "AFEED00E";
99 tx[sfAssetsMaximum] = asset(100).number();
100 env(tx);
101 env.close();
102 BEAST_EXPECT(env.le(keylet));
103 std::uint64_t const scale = asset.raw().holds<MPTIssue>() ? 1 : 1e6;
104
105 auto const [share, vaultAccount] =
106 [&env, keylet = keylet, asset, this]() -> std::tuple<PrettyAsset, Account> {
107 auto const vault = env.le(keylet);
108 BEAST_EXPECT(vault != nullptr);
109 if (!asset.integral())
110 {
111 BEAST_EXPECT(vault->at(sfScale) == 6);
112 }
113 else
114 {
115 BEAST_EXPECT(vault->at(sfScale) == 0);
116 }
117 auto const shares = env.le(keylet::mptokenIssuance(vault->at(sfShareMPTID)));
118 BEAST_EXPECT(shares != nullptr);
119 if (!asset.integral())
120 {
121 BEAST_EXPECT(shares->at(sfAssetScale) == 6);
122 }
123 else
124 {
125 BEAST_EXPECT(shares->at(sfAssetScale) == 0);
126 }
127 return {MPTIssue(vault->at(sfShareMPTID)), Account("vault", vault->at(sfAccount))};
128 }();
129 auto const shares = share.raw().get<MPTIssue>();
130 env.memoize(vaultAccount);
131
132 // Several 3rd party accounts which cannot receive funds
133 Account const alice{"alice"};
134 Account const erin{"erin"}; // not authorized by issuer
135 env.fund(XRP(1000), alice, erin);
136 env(fset(alice, asfDepositAuth));
137 env.close();
138
139 {
140 testcase(prefix + " fail to deposit more than assets held");
141 auto tx = vault.deposit(
142 {.depositor = depositor, .id = keylet.key, .amount = asset(10000)});
143 env(tx, Ter(tecINSUFFICIENT_FUNDS));
144 env.close();
145 }
146
147 {
148 testcase(prefix + " deposit non-zero amount");
149 auto tx =
150 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
151 env(tx);
152 env.close();
153 BEAST_EXPECT(env.balance(depositor, shares) == share(50 * scale));
154 }
155
156 {
157 testcase(prefix + " deposit non-zero amount again");
158 auto tx =
159 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
160 env(tx);
161 env.close();
162 BEAST_EXPECT(env.balance(depositor, shares) == share(100 * scale));
163 }
164
165 {
166 testcase(prefix + " fail to delete non-empty vault");
167 auto tx = vault.del({.owner = owner, .id = keylet.key});
168 env(tx, Ter(tecHAS_OBLIGATIONS));
169 env.close();
170 }
171
172 {
173 testcase(prefix + " fail to update because wrong owner");
174 auto tx = vault.set({.owner = issuer, .id = keylet.key});
175 tx[sfAssetsMaximum] = asset(50).number();
176 env(tx, Ter(tecNO_PERMISSION));
177 env.close();
178 }
179
180 {
181 testcase(prefix + " fail to set maximum lower than current amount");
182 auto tx = vault.set({.owner = owner, .id = keylet.key});
183 tx[sfAssetsMaximum] = asset(50).number();
184 env(tx, Ter(tecLIMIT_EXCEEDED));
185 env.close();
186 }
187
188 {
189 testcase(prefix + " set maximum higher than current amount");
190 auto tx = vault.set({.owner = owner, .id = keylet.key});
191 tx[sfAssetsMaximum] = asset(150).number();
192 env(tx);
193 env.close();
194 }
195
196 {
197 testcase(prefix + " set maximum is idempotent, set it again");
198 auto tx = vault.set({.owner = owner, .id = keylet.key});
199 tx[sfAssetsMaximum] = asset(150).number();
200 env(tx);
201 env.close();
202 }
203
204 {
205 testcase(prefix + " set data");
206 auto tx = vault.set({.owner = owner, .id = keylet.key});
207 tx[sfData] = "0";
208 env(tx);
209 env.close();
210 }
211
212 {
213 testcase(prefix + " fail to set domain on public vault");
214 auto tx = vault.set({.owner = owner, .id = keylet.key});
215 tx[sfDomainID] = to_string(BaseUInt<256>(42ul));
216 env(tx, Ter{tecNO_PERMISSION});
217 env.close();
218 }
219
220 {
221 testcase(prefix + " fail to deposit more than maximum");
222 auto tx =
223 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
224 env(tx, Ter(tecLIMIT_EXCEEDED));
225 env.close();
226 }
227
228 {
229 testcase(prefix + " reset maximum to zero i.e. not enforced");
230 auto tx = vault.set({.owner = owner, .id = keylet.key});
231 tx[sfAssetsMaximum] = asset(0).number();
232 env(tx);
233 env.close();
234 }
235
236 {
237 testcase(prefix + " fail to withdraw more than assets held");
238 auto tx = vault.withdraw(
239 {.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
240 env(tx, Ter(tecINSUFFICIENT_FUNDS));
241 env.close();
242 }
243
244 {
245 testcase(prefix + " deposit some more");
246 auto tx =
247 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
248 env(tx);
249 env.close();
250 BEAST_EXPECT(env.balance(depositor, shares) == share(200 * scale));
251 }
252
253 {
254 testcase(prefix + " clawback some");
255 auto code = asset.raw().native() ? Ter(temMALFORMED) : Ter(tesSUCCESS);
256 auto tx = vault.clawback(
257 {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(10)});
258 env(tx, code);
259 env.close();
260 if (!asset.raw().native())
261 {
262 BEAST_EXPECT(env.balance(depositor, shares) == share(190 * scale));
263 }
264 }
265
266 {
267 testcase(prefix + " clawback all");
268 auto code = asset.raw().native() ? Ter(tecNO_PERMISSION) : Ter(tesSUCCESS);
269 auto tx = vault.clawback({.issuer = issuer, .id = keylet.key, .holder = depositor});
270 env(tx, code);
271 env.close();
272 if (!asset.raw().native())
273 {
274 BEAST_EXPECT(env.balance(depositor, shares) == share(0));
275
276 {
277 auto tx = vault.clawback(
278 {.issuer = issuer,
279 .id = keylet.key,
280 .holder = depositor,
281 .amount = asset(10)});
282 env(tx, Ter{tecPRECISION_LOSS});
283 env.close();
284 }
285
286 {
287 auto tx = vault.withdraw(
288 {.depositor = depositor, .id = keylet.key, .amount = asset(10)});
289 env(tx, Ter{tecPRECISION_LOSS});
290 env.close();
291 }
292 }
293 }
294
295 if (!asset.raw().native())
296 {
297 testcase(prefix + " deposit again");
298 auto tx =
299 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(200)});
300 env(tx);
301 env.close();
302 BEAST_EXPECT(env.balance(depositor, shares) == share(200 * scale));
303 }
304 else
305 {
306 testcase(prefix + " deposit/withdrawal same or less than fee");
307 auto const amount = env.current()->fees().base;
308
309 auto tx =
310 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = amount});
311 env(tx);
312 env.close();
313
314 tx = vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = amount});
315 env(tx);
316 env.close();
317
318 tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = amount});
319 env(tx);
320 env.close();
321
322 // Withdraw to 3rd party
323 tx = vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = amount});
324 tx[sfDestination] = charlie.human();
325 env(tx);
326 env.close();
327
328 tx =
329 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = amount - 1});
330 env(tx);
331 env.close();
332
333 tx = vault.withdraw(
334 {.depositor = depositor, .id = keylet.key, .amount = amount - 1});
335 env(tx);
336 env.close();
337 }
338
339 {
340 testcase(prefix + " fail to withdraw to 3rd party lsfDepositAuth");
341 auto tx = vault.withdraw(
342 {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
343 tx[sfDestination] = alice.human();
344 env(tx, Ter{tecNO_PERMISSION});
345 env.close();
346 }
347
348 {
349 testcase(prefix + " fail to withdraw to zero destination");
350 auto tx = vault.withdraw(
351 {.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
352 tx[sfDestination] = "0";
353 env(tx, Ter(temMALFORMED));
354 env.close();
355 }
356
357 if (!asset.raw().native())
358 {
359 testcase(prefix + " fail to withdraw to 3rd party no authorization");
360 auto tx = vault.withdraw(
361 {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
362 tx[sfDestination] = erin.human();
363 env(tx, Ter{asset.raw().holds<Issue>() ? tecNO_LINE : tecNO_AUTH});
364 env.close();
365 }
366
367 {
368 testcase(prefix + " fail to withdraw to 3rd party lsfRequireDestTag");
369 auto tx = vault.withdraw(
370 {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
371 tx[sfDestination] = dave.human();
372 env(tx, Ter{tecDST_TAG_NEEDED});
373 env.close();
374 }
375
376 {
377 testcase(prefix + " withdraw to 3rd party lsfRequireDestTag");
378 auto tx =
379 vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
380 tx[sfDestination] = dave.human();
381 tx[sfDestinationTag] = "0";
382 env(tx);
383 env.close();
384 }
385
386 {
387 testcase(prefix + " deposit again");
388 auto tx = vault.deposit({.depositor = dave, .id = keylet.key, .amount = asset(50)});
389 env(tx);
390 env.close();
391 }
392
393 {
394 testcase(prefix + " fail to withdraw lsfRequireDestTag");
395 auto tx =
396 vault.withdraw({.depositor = dave, .id = keylet.key, .amount = asset(50)});
397 env(tx, Ter{tecDST_TAG_NEEDED});
398 env.close();
399 }
400
401 {
402 testcase(prefix + " withdraw with tag");
403 auto tx =
404 vault.withdraw({.depositor = dave, .id = keylet.key, .amount = asset(50)});
405 tx[sfDestinationTag] = "0";
406 env(tx);
407 env.close();
408 }
409
410 {
411 testcase(prefix + " withdraw to authorized 3rd party");
412 auto tx =
413 vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
414 tx[sfDestination] = charlie.human();
415 env(tx);
416 env.close();
417 BEAST_EXPECT(env.balance(depositor, shares) == share(100 * scale));
418 }
419
420 {
421 testcase(prefix + " withdraw to issuer");
422 auto tx =
423 vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
424 tx[sfDestination] = issuer.human();
425 env(tx);
426 env.close();
427 BEAST_EXPECT(env.balance(depositor, shares) == share(50 * scale));
428 }
429
430 if (!asset.raw().native())
431 {
432 testcase(prefix + " issuer deposits");
433 auto tx =
434 vault.deposit({.depositor = issuer, .id = keylet.key, .amount = asset(10)});
435 env(tx);
436 env.close();
437 BEAST_EXPECT(env.balance(issuer, shares) == share(10 * scale));
438
439 testcase(prefix + " issuer withdraws");
440 tx = vault.withdraw(
441 {.depositor = issuer, .id = keylet.key, .amount = share(10 * scale)});
442 env(tx);
443 env.close();
444 BEAST_EXPECT(env.balance(issuer, shares) == share(0 * scale));
445 }
446
447 {
448 testcase(prefix + " withdraw remaining assets");
449 auto tx =
450 vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
451 env(tx);
452 env.close();
453 BEAST_EXPECT(env.balance(depositor, shares) == share(0));
454
455 if (!asset.raw().native())
456 {
457 auto tx = vault.clawback(
458 {.issuer = issuer,
459 .id = keylet.key,
460 .holder = depositor,
461 .amount = asset(0)});
462 env(tx, Ter{tecPRECISION_LOSS});
463 env.close();
464 }
465
466 {
467 auto tx = vault.withdraw(
468 {.depositor = depositor, .id = keylet.key, .amount = share(10)});
469 env(tx, Ter{tecINSUFFICIENT_FUNDS});
470 env.close();
471 }
472 }
473
474 if (!asset.integral())
475 {
476 testcase(prefix + " temporary authorization for 3rd party");
477 env(trust(erin, asset(1000)));
478 env(trust(issuer, asset(0), erin, tfSetfAuth));
479 env(pay(issuer, erin, asset(10)));
480
481 // Erin deposits all in vault, then sends shares to depositor
482 auto tx = vault.deposit({.depositor = erin, .id = keylet.key, .amount = asset(10)});
483 env(tx);
484 env.close();
485 {
486 auto tx = pay(erin, depositor, share(10 * scale));
487
488 // depositor no longer has MPToken for shares
489 env(tx, Ter{tecNO_AUTH});
490 env.close();
491
492 // depositor will gain MPToken for shares again
493 env(vault.deposit(
494 {.depositor = depositor, .id = keylet.key, .amount = asset(1)}));
495 env.close();
496
497 env(tx);
498 env.close();
499 }
500
501 testcase(prefix + " withdraw to authorized 3rd party");
502 // Depositor withdraws assets, destined to Erin
503 tx =
504 vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(10)});
505 tx[sfDestination] = erin.human();
506 env(tx);
507 env.close();
508
509 // Erin returns assets to issuer
510 env(pay(erin, issuer, asset(10)));
511 env.close();
512
513 testcase(prefix + " fail to pay to unauthorized 3rd party");
514 env(trust(erin, asset(0)));
515 env.close();
516
517 // Erin has MPToken but is no longer authorized to hold assets
518 env(pay(depositor, erin, share(1)), Ter{tecNO_LINE});
519 env.close();
520
521 // Depositor withdraws remaining single asset
522 tx = vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(1)});
523 env(tx);
524 env.close();
525 }
526
527 {
528 testcase(prefix + " fail to delete because wrong owner");
529 auto tx = vault.del({.owner = issuer, .id = keylet.key});
530 env(tx, Ter(tecNO_PERMISSION));
531 env.close();
532 }
533
534 {
535 testcase(prefix + " delete empty vault");
536 auto tx = vault.del({.owner = owner, .id = keylet.key});
537 env(tx);
538 env.close();
539 BEAST_EXPECT(!env.le(keylet));
540 }
541 };
542
543 auto testCases = [&, this](
544 std::string prefix, std::function<PrettyAsset(Env & env)> setup) {
545 Env env{*this, testableAmendments()};
546
547 Vault vault{env};
548 env.fund(XRP(1000), issuer, owner, depositor, charlie, dave);
549 env.close();
550 env(fset(issuer, asfAllowTrustLineClawback));
551 env(fset(issuer, asfRequireAuth));
552 env(fset(dave, asfRequireDest));
553 env.close();
554 env.require(Flags(issuer, asfAllowTrustLineClawback));
555 env.require(Flags(issuer, asfRequireAuth));
556
557 PrettyAsset const asset = setup(env);
558 testSequence(prefix, env, vault, asset);
559 };
560
561 testCases("XRP", [&](Env& env) -> PrettyAsset { return {xrpIssue(), 1'000'000}; });
562
563 testCases("IOU", [&](Env& env) -> Asset {
564 PrettyAsset const asset = issuer["IOU"];
565 env(trust(owner, asset(1000)));
566 env(trust(depositor, asset(1000)));
567 env(trust(charlie, asset(1000)));
568 env(trust(dave, asset(1000)));
569 env(trust(issuer, asset(0), owner, tfSetfAuth));
570 env(trust(issuer, asset(0), depositor, tfSetfAuth));
571 env(trust(issuer, asset(0), charlie, tfSetfAuth));
572 env(trust(issuer, asset(0), dave, tfSetfAuth));
573 env(pay(issuer, depositor, asset(1000)));
574 env.close();
575 return asset;
576 });
577
578 testCases("MPT", [&](Env& env) -> Asset {
579 MPTTester mptt{env, issuer, kMptInitNoFund};
580 mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
581 PrettyAsset const asset = mptt.issuanceID();
582 mptt.authorize({.account = depositor});
583 mptt.authorize({.account = charlie});
584 mptt.authorize({.account = dave});
585 env(pay(issuer, depositor, asset(1000)));
586 env.close();
587 return asset;
588 });
589 }
590
591 void
593 {
594 using namespace test::jtx;
595
596 struct CaseArgs
597 {
598 FeatureBitset features = testableAmendments();
599 };
600
601 auto testCase = [&, this](
602 std::function<void(
603 Env & env,
604 Account const& issuer,
605 Account const& owner,
606 Asset const& asset,
607 Vault& vault)> test,
608 CaseArgs args = {}) {
609 Env env{*this, args.features};
610 Account const issuer{"issuer"};
611 Account const owner{"owner"};
612 Vault vault{env};
613 env.fund(XRP(1000), issuer, owner);
614 env.close();
615
616 env(fset(issuer, asfAllowTrustLineClawback));
617 env(fset(issuer, asfRequireAuth));
618 env.close();
619
620 PrettyAsset const asset = issuer["IOU"];
621 env(trust(owner, asset(1000)));
622 env(trust(issuer, asset(0), owner, tfSetfAuth));
623 env(pay(issuer, owner, asset(1000)));
624 env.close();
625
626 test(env, issuer, owner, asset, vault);
627 };
628
629 auto testDisabled = [&](TER resultAfterCreate = temDISABLED) {
630 return [&, resultAfterCreate](
631 Env& env,
632 Account const& issuer,
633 Account const& owner,
634 Asset const& asset,
635 Vault& vault) {
636 testcase("disabled single asset vault");
637
638 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
639 env(tx, Ter{temDISABLED});
640
641 {
642 auto tx = vault.set({.owner = owner, .id = keylet.key});
643 env(tx, kData("test"), Ter{resultAfterCreate});
644 }
645
646 {
647 auto tx =
648 vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(10)});
649 env(tx, Ter{resultAfterCreate});
650 }
651
652 {
653 auto tx =
654 vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
655 env(tx, Ter{resultAfterCreate});
656 }
657
658 {
659 auto tx = vault.clawback(
660 {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(10)});
661 env(tx, Ter{resultAfterCreate});
662 }
663
664 {
665 auto tx = vault.del({.owner = owner, .id = keylet.key});
666 env(tx, Ter{resultAfterCreate});
667 }
668 };
669 };
670
671 testCase(testDisabled(), {.features = testableAmendments() - featureSingleAssetVault});
672
673 testCase(testDisabled(tecNO_ENTRY), {.features = testableAmendments() - featureMPTokensV1});
674
675 testCase(
676 [&](Env& env,
677 Account const& issuer,
678 Account const& owner,
679 Asset const& asset,
680 Vault& vault) {
681 testcase("disabled permissioned domains");
682
683 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
684 env(tx);
685
686 tx[sfFlags] = tx[sfFlags].asUInt() | tfVaultPrivate;
687 tx[sfDomainID] = to_string(BaseUInt<256>(42ul));
688 env(tx, Ter{temDISABLED});
689
690 {
691 auto tx = vault.set({.owner = owner, .id = keylet.key});
692 env(tx, kData("Test"));
693
694 tx[sfDomainID] = to_string(BaseUInt<256>(13ul));
695 env(tx, Ter{temDISABLED});
696 }
697 },
698 {.features = testableAmendments() - featurePermissionedDomains});
699
700 testCase([&](Env& env,
701 Account const& issuer,
702 Account const& owner,
703 Asset const& asset,
704 Vault& vault) {
705 testcase("invalid flags");
706
707 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
708 tx[sfFlags] = tfClearDeepFreeze;
709 env(tx, Ter{temINVALID_FLAG});
710
711 {
712 auto tx = vault.set({.owner = owner, .id = keylet.key});
713 tx[sfFlags] = tfClearDeepFreeze;
714 env(tx, Ter{temINVALID_FLAG});
715 }
716
717 {
718 auto tx =
719 vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(10)});
720 tx[sfFlags] = tfClearDeepFreeze;
721 env(tx, Ter{temINVALID_FLAG});
722 }
723
724 {
725 auto tx =
726 vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
727 tx[sfFlags] = tfClearDeepFreeze;
728 env(tx, Ter{temINVALID_FLAG});
729 }
730
731 {
732 auto tx = vault.clawback(
733 {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(10)});
734 tx[sfFlags] = tfClearDeepFreeze;
735 env(tx, Ter{temINVALID_FLAG});
736 }
737
738 {
739 auto tx = vault.del({.owner = owner, .id = keylet.key});
740 tx[sfFlags] = tfClearDeepFreeze;
741 env(tx, Ter{temINVALID_FLAG});
742 }
743 });
744
745 testCase([&](Env& env,
746 Account const& issuer,
747 Account const& owner,
748 Asset const& asset,
749 Vault& vault) {
750 testcase("invalid fee");
751
752 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
753 tx[jss::Fee] = "-1";
754 env(tx, Ter{temBAD_FEE});
755
756 {
757 auto tx = vault.set({.owner = owner, .id = keylet.key});
758 tx[jss::Fee] = "-1";
759 env(tx, Ter{temBAD_FEE});
760 }
761
762 {
763 auto tx =
764 vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(10)});
765 tx[jss::Fee] = "-1";
766 env(tx, Ter{temBAD_FEE});
767 }
768
769 {
770 auto tx =
771 vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
772 tx[jss::Fee] = "-1";
773 env(tx, Ter{temBAD_FEE});
774 }
775
776 {
777 auto tx = vault.clawback(
778 {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(10)});
779 tx[jss::Fee] = "-1";
780 env(tx, Ter{temBAD_FEE});
781 }
782
783 {
784 auto tx = vault.del({.owner = owner, .id = keylet.key});
785 tx[jss::Fee] = "-1";
786 env(tx, Ter{temBAD_FEE});
787 }
788 });
789
790 testCase(
791 [&](Env& env, Account const&, Account const& owner, Asset const&, Vault& vault) {
792 testcase("disabled permissioned domain");
793
794 auto [tx, keylet] = vault.create({.owner = owner, .asset = xrpIssue()});
795 tx[sfDomainID] = to_string(BaseUInt<256>(42ul));
796 env(tx, Ter{temDISABLED});
797
798 {
799 auto tx = vault.set({.owner = owner, .id = keylet.key});
800 tx[sfDomainID] = to_string(BaseUInt<256>(42ul));
801 env(tx, Ter{temDISABLED});
802 }
803
804 {
805 auto tx = vault.set({.owner = owner, .id = keylet.key});
806 tx[sfDomainID] = "0";
807 env(tx, Ter{temDISABLED});
808 }
809 },
810 {.features = (testableAmendments()) - featurePermissionedDomains});
811
812 testCase([&](Env& env,
813 Account const& issuer,
814 Account const& owner,
815 Asset const& asset,
816 Vault& vault) {
817 testcase("use zero vault");
818
819 auto [tx, keylet] = vault.create({.owner = owner, .asset = xrpIssue()});
820
821 {
822 auto tx = vault.set({
823 .owner = owner,
824 .id = beast::kZero,
825 });
826 env(tx, Ter{temMALFORMED});
827 }
828
829 {
830 auto tx =
831 vault.deposit({.depositor = owner, .id = beast::kZero, .amount = asset(10)});
832 env(tx, Ter(temMALFORMED));
833 }
834
835 {
836 auto tx =
837 vault.withdraw({.depositor = owner, .id = beast::kZero, .amount = asset(10)});
838 env(tx, Ter{temMALFORMED});
839 }
840
841 {
842 auto tx = vault.clawback(
843 {.issuer = issuer, .id = beast::kZero, .holder = owner, .amount = asset(10)});
844 env(tx, Ter{temMALFORMED});
845 }
846
847 {
848 auto tx = vault.del({
849 .owner = owner,
850 .id = beast::kZero,
851 });
852 env(tx, Ter{temMALFORMED});
853 }
854 });
855
856 testCase(
857 [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
858 testcase("withdraw to bad destination");
859
860 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
861
862 {
863 auto tx =
864 vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
865 tx[jss::Destination] = "0";
866 env(tx, Ter{temMALFORMED});
867 }
868 });
869
870 testCase(
871 [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
872 testcase("create with Scale");
873
874 {
875 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
876 tx[sfScale] = 255;
877 env(tx, Ter(temMALFORMED));
878 }
879
880 {
881 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
882 tx[sfScale] = 19;
883 env(tx, Ter(temMALFORMED));
884 }
885
886 // accepted range from 0 to 18
887 {
888 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
889 tx[sfScale] = 18;
890 env(tx);
891 env.close();
892 auto const sleVault = env.le(keylet);
893 BEAST_EXPECT(sleVault);
894 BEAST_EXPECT((*sleVault)[sfScale] == 18);
895 }
896
897 {
898 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
899 tx[sfScale] = 0;
900 env(tx);
901 env.close();
902 auto const sleVault = env.le(keylet);
903 BEAST_EXPECT(sleVault);
904 BEAST_EXPECT((*sleVault)[sfScale] == 0);
905 }
906
907 {
908 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
909 env(tx);
910 env.close();
911 auto const sleVault = env.le(keylet);
912 BEAST_EXPECT(sleVault);
913 BEAST_EXPECT((*sleVault)[sfScale] == 6);
914 }
915 });
916
917 testCase(
918 [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
919 testcase("create or set invalid data");
920
921 auto [tx1, keylet] = vault.create({.owner = owner, .asset = asset});
922
923 {
924 auto tx = tx1;
925 tx[sfData] = "";
926 env(tx, Ter(temMALFORMED));
927 }
928
929 {
930 auto tx = tx1;
931 // A hexadecimal string of 257 bytes.
932 tx[sfData] = std::string(514, 'A');
933 env(tx, Ter(temMALFORMED));
934 }
935
936 {
937 auto tx = vault.set({.owner = owner, .id = keylet.key});
938 tx[sfData] = "";
939 env(tx, Ter{temMALFORMED});
940 }
941
942 {
943 auto tx = vault.set({.owner = owner, .id = keylet.key});
944 // A hexadecimal string of 257 bytes.
945 tx[sfData] = std::string(514, 'A');
946 env(tx, Ter{temMALFORMED});
947 }
948 });
949
950 testCase(
951 [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
952 testcase("set nothing updated");
953
954 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
955
956 {
957 auto tx = vault.set({.owner = owner, .id = keylet.key});
958 env(tx, Ter{temMALFORMED});
959 }
960 });
961
962 testCase(
963 [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
964 testcase("create with invalid metadata");
965
966 auto [tx1, keylet] = vault.create({.owner = owner, .asset = asset});
967
968 {
969 auto tx = tx1;
970 tx[sfMPTokenMetadata] = "";
971 env(tx, Ter(temMALFORMED));
972 }
973
974 {
975 auto tx = tx1;
976 // This metadata is for the share token.
977 // A hexadecimal string of 1025 bytes.
978 tx[sfMPTokenMetadata] = std::string(2050, 'B');
979 env(tx, Ter(temMALFORMED));
980 }
981 });
982
983 testCase(
984 [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
985 testcase("set negative maximum");
986
987 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
988
989 {
990 auto tx = vault.set({.owner = owner, .id = keylet.key});
991 tx[sfAssetsMaximum] = kNegativeAmount(asset).number();
992 env(tx, Ter{temMALFORMED});
993 }
994 });
995
996 testCase(
997 [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
998 testcase("invalid deposit amount");
999
1000 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1001
1002 {
1003 auto tx = vault.deposit(
1004 {.depositor = owner, .id = keylet.key, .amount = kNegativeAmount(asset)});
1005 env(tx, Ter(temBAD_AMOUNT));
1006 }
1007
1008 {
1009 auto tx =
1010 vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(0)});
1011 env(tx, Ter(temBAD_AMOUNT));
1012 }
1013 });
1014
1015 testCase(
1016 [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
1017 testcase("invalid set immutable flag");
1018
1019 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1020
1021 {
1022 auto tx = vault.set({.owner = owner, .id = keylet.key});
1023 tx[sfFlags] = tfVaultPrivate;
1024 env(tx, Ter(temINVALID_FLAG));
1025 }
1026 });
1027
1028 testCase(
1029 [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
1030 testcase("invalid withdraw amount");
1031
1032 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1033
1034 {
1035 auto tx = vault.withdraw(
1036 {.depositor = owner, .id = keylet.key, .amount = kNegativeAmount(asset)});
1037 env(tx, Ter(temBAD_AMOUNT));
1038 }
1039
1040 {
1041 auto tx =
1042 vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(0)});
1043 env(tx, Ter(temBAD_AMOUNT));
1044 }
1045 });
1046
1047 testCase([&](Env& env,
1048 Account const& issuer,
1049 Account const& owner,
1050 Asset const& asset,
1051 Vault& vault) {
1052 testcase("invalid clawback");
1053
1054 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1055
1056 // Preclaim only checks for native assets.
1057 if (asset.native())
1058 {
1059 auto tx = vault.clawback(
1060 {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(50)});
1061 env(tx, Ter(temMALFORMED));
1062 }
1063
1064 {
1065 auto tx = vault.clawback(
1066 {.issuer = issuer,
1067 .id = keylet.key,
1068 .holder = owner,
1069 .amount = kNegativeAmount(asset)});
1070 env(tx, Ter(temBAD_AMOUNT));
1071 }
1072 });
1073
1074 testCase(
1075 [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
1076 testcase("invalid create");
1077
1078 auto [tx1, keylet] = vault.create({.owner = owner, .asset = asset});
1079
1080 {
1081 auto tx = tx1;
1082 tx[sfWithdrawalPolicy] = 0;
1083 env(tx, Ter(temMALFORMED));
1084 }
1085
1086 {
1087 auto tx = tx1;
1088 tx[sfDomainID] = to_string(BaseUInt<256>(42ul));
1089 env(tx, Ter{temMALFORMED});
1090 }
1091
1092 {
1093 auto tx = tx1;
1094 tx[sfAssetsMaximum] = kNegativeAmount(asset).number();
1095 env(tx, Ter{temMALFORMED});
1096 }
1097
1098 {
1099 auto tx = tx1;
1100 tx[sfFlags] = tfVaultPrivate;
1101 tx[sfDomainID] = "0";
1102 env(tx, Ter{temMALFORMED});
1103 }
1104 });
1105 }
1106
1107 // Test for non-asset specific behaviors.
1108 void
1110 {
1111 using namespace test::jtx;
1112
1113 auto testCase = [this](
1114 std::function<void(
1115 Env & env,
1116 Account const& issuer,
1117 Account const& owner,
1118 Account const& depositor,
1119 Asset const& asset,
1120 Vault& vault)> test) {
1121 Env env{*this, testableAmendments()};
1122 Account const issuer{"issuer"};
1123 Account const owner{"owner"};
1124 Account const depositor{"depositor"};
1125
1126 env.fund(XRP(1000), issuer, owner, depositor);
1127 env.close();
1128 Vault vault{env};
1129 Asset const asset = xrpIssue();
1130
1131 test(env, issuer, owner, depositor, asset, vault);
1132 };
1133
1134 testCase([this](
1135 Env& env,
1136 Account const& issuer,
1137 Account const& owner,
1138 Account const& depositor,
1139 PrettyAsset const& asset,
1140 Vault& vault) {
1141 testcase("nothing to set");
1142 auto tx = vault.set({.owner = owner, .id = keylet::skip().key});
1143 tx[sfAssetsMaximum] = asset(0).number();
1144 env(tx, Ter(tecNO_ENTRY));
1145 });
1146
1147 testCase([this](
1148 Env& env,
1149 Account const& issuer,
1150 Account const& owner,
1151 Account const& depositor,
1152 PrettyAsset const& asset,
1153 Vault& vault) {
1154 testcase("nothing to deposit to");
1155 auto tx = vault.deposit(
1156 {.depositor = depositor, .id = keylet::skip().key, .amount = asset(10)});
1157 env(tx, Ter(tecNO_ENTRY));
1158 });
1159
1160 testCase([this](
1161 Env& env,
1162 Account const& issuer,
1163 Account const& owner,
1164 Account const& depositor,
1165 PrettyAsset const& asset,
1166 Vault& vault) {
1167 testcase("nothing to withdraw from");
1168 auto tx = vault.withdraw(
1169 {.depositor = depositor, .id = keylet::skip().key, .amount = asset(10)});
1170 env(tx, Ter(tecNO_ENTRY));
1171 });
1172
1173 testCase([this](
1174 Env& env,
1175 Account const& issuer,
1176 Account const& owner,
1177 Account const& depositor,
1178 Asset const& asset,
1179 Vault& vault) {
1180 testcase("nothing to delete");
1181 auto tx = vault.del({.owner = owner, .id = keylet::skip().key});
1182 env(tx, Ter(tecNO_ENTRY));
1183 });
1184
1185 testCase([this](
1186 Env& env,
1187 Account const& issuer,
1188 Account const& owner,
1189 Account const& depositor,
1190 Asset const& asset,
1191 Vault& vault) {
1192 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1193 testcase("transaction is good");
1194 env(tx);
1195 });
1196
1197 testCase([this](
1198 Env& env,
1199 Account const& issuer,
1200 Account const& owner,
1201 Account const& depositor,
1202 Asset const& asset,
1203 Vault& vault) {
1204 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1205 tx[sfWithdrawalPolicy] = 1;
1206 testcase("explicitly select withdrawal policy");
1207 env(tx);
1208 });
1209
1210 testCase([this](
1211 Env& env,
1212 Account const& issuer,
1213 Account const& owner,
1214 Account const& depositor,
1215 Asset const& asset,
1216 Vault& vault) {
1217 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1218 testcase("insufficient fee");
1219 env(tx, Fee(env.current()->fees().base - 1), Ter(telINSUF_FEE_P));
1220 });
1221
1222 testCase([this](
1223 Env& env,
1224 Account const& issuer,
1225 Account const& owner,
1226 Account const& depositor,
1227 Asset const& asset,
1228 Vault& vault) {
1229 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1230 testcase("insufficient reserve");
1231 // It is possible to construct a complicated mathematical
1232 // expression for this amount, but it is sadly not easy.
1233 env(pay(owner, issuer, XRP(775)));
1234 env.close();
1235 env(tx, Ter(tecINSUFFICIENT_RESERVE));
1236 });
1237
1238 testCase([this](
1239 Env& env,
1240 Account const& issuer,
1241 Account const& owner,
1242 Account const& depositor,
1243 Asset const& asset,
1244 Vault& vault) {
1245 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1246 tx[sfFlags] = tfVaultPrivate;
1247 tx[sfDomainID] = to_string(BaseUInt<256>(42ul));
1248 testcase("non-existing domain");
1249 env(tx, Ter{tecOBJECT_NOT_FOUND});
1250 });
1251
1252 testCase([this](
1253 Env& env,
1254 Account const& issuer,
1255 Account const& owner,
1256 Account const& depositor,
1257 Asset const& asset,
1258 Vault& vault) {
1259 testcase("cannot set Scale=0");
1260 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1261 tx[sfScale] = 0;
1262 env(tx, Ter{temMALFORMED});
1263 });
1264
1265 testCase([this](
1266 Env& env,
1267 Account const& issuer,
1268 Account const& owner,
1269 Account const& depositor,
1270 Asset const& asset,
1271 Vault& vault) {
1272 testcase("cannot set Scale=1");
1273 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1274 tx[sfScale] = 1;
1275 env(tx, Ter{temMALFORMED});
1276 });
1277 }
1278
1279 void
1281 {
1282 using namespace test::jtx;
1283 {
1284 {
1285 testcase("IOU fail because MPT is disabled");
1286 Env env{*this, (testableAmendments() - featureMPTokensV1)};
1287 Account const issuer{"issuer"};
1288 Account const owner{"owner"};
1289 env.fund(XRP(1000), issuer, owner);
1290 env.close();
1291
1292 Vault const vault{env};
1293 Asset const asset = issuer["IOU"].asset();
1294 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1295
1296 env(tx, Ter(temDISABLED));
1297 env.close();
1298 }
1299
1300 {
1301 testcase("IOU fail create frozen");
1302 Env env{*this, testableAmendments()};
1303 Account const issuer{"issuer"};
1304 Account const owner{"owner"};
1305 env.fund(XRP(1000), issuer, owner);
1306 env.close();
1307 env(fset(issuer, asfGlobalFreeze));
1308 env.close();
1309
1310 Vault const vault{env};
1311 Asset const asset = issuer["IOU"].asset();
1312 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1313
1314 env(tx, Ter(tecFROZEN));
1315 env.close();
1316 }
1317
1318 {
1319 testcase("IOU fail create no ripling");
1320 Env env{*this, testableAmendments()};
1321 Account const issuer{"issuer"};
1322 Account const owner{"owner"};
1323 env.fund(XRP(1000), issuer, owner);
1324 env.close();
1325 env(fclear(issuer, asfDefaultRipple));
1326 env.close();
1327
1328 Vault const vault{env};
1329 Asset const asset = issuer["IOU"].asset();
1330 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1331 env(tx, Ter(terNO_RIPPLE));
1332 env.close();
1333 }
1334
1335 {
1336 testcase("IOU no issuer");
1337 Env env{*this, testableAmendments()};
1338 Account const issuer{"issuer"};
1339 Account const owner{"owner"};
1340 env.fund(XRP(1000), owner);
1341 env.close();
1342
1343 Vault const vault{env};
1344 Asset const asset = issuer["IOU"].asset();
1345 {
1346 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1347 env(tx, Ter(terNO_ACCOUNT));
1348 env.close();
1349 }
1350 }
1351 }
1352
1353 {
1354 testcase("IOU fail create vault for AMM LPToken");
1355 Env env{*this, testableAmendments()};
1356 Account const gw("gateway");
1357 Account const alice("alice");
1358 Account const carol("carol");
1359 IOU const usd = gw["USD"];
1360
1361 auto const [asset1, asset2] = std::pair<STAmount, STAmount>(XRP(10000), usd(10000));
1362 auto toFund = [&](STAmount const& a) -> STAmount {
1363 if (a.native())
1364 {
1365 auto const defXRP = XRP(30000);
1366 if (a <= defXRP)
1367 return defXRP;
1368 return a + XRP(1000);
1369 }
1370 auto defIOU = STAmount{a.asset(), 30000};
1371 if (a <= defIOU)
1372 return defIOU;
1373 return a + STAmount{a.asset(), 1000};
1374 };
1375 auto const toFund1 = toFund(asset1);
1376 auto const toFund2 = toFund(asset2);
1377 BEAST_EXPECT(asset1 <= toFund1 && asset2 <= toFund2);
1378
1379 if (!asset1.native() && !asset2.native())
1380 {
1381 fund(env, gw, {alice, carol}, {toFund1, toFund2}, Fund::All);
1382 }
1383 else if (asset1.native())
1384 {
1385 fund(env, gw, {alice, carol}, toFund1, {toFund2}, Fund::All);
1386 }
1387 else if (asset2.native())
1388 {
1389 fund(env, gw, {alice, carol}, toFund2, {toFund1}, Fund::All);
1390 }
1391
1392 AMM const ammAlice(env, alice, asset1, asset2, CreateArg{.log = false, .tfee = 0});
1393
1394 Account const owner{"owner"};
1395 env.fund(XRP(1000000), owner);
1396
1397 Vault const vault{env};
1398 auto [tx, k] = vault.create({.owner = owner, .asset = ammAlice.lptIssue()});
1399 env(tx, Ter{tecWRONG_ASSET});
1400 env.close();
1401 }
1402 }
1403
1404 void
1406 {
1407 using namespace test::jtx;
1408
1409 auto testCase = [this](
1410 std::function<void(
1411 Env & env,
1412 Account const& issuer,
1413 Account const& owner,
1414 Account const& depositor,
1415 Asset const& asset,
1416 Vault& vault)> test) {
1417 Env env{*this, testableAmendments()};
1418 Account const issuer{"issuer"};
1419 Account const owner{"owner"};
1420 Account const depositor{"depositor"};
1421 env.fund(XRP(1000), issuer, owner, depositor);
1422 env.close();
1423 Vault vault{env};
1424 MPTTester mptt{env, issuer, kMptInitNoFund};
1425 // Locked because that is the default flag.
1426 mptt.create();
1427 Asset const asset = mptt.issuanceID();
1428
1429 test(env, issuer, owner, depositor, asset, vault);
1430 };
1431
1432 testCase([this](
1433 Env& env,
1434 Account const& issuer,
1435 Account const& owner,
1436 Account const& depositor,
1437 Asset const& asset,
1438 Vault& vault) {
1439 testcase("MPT no authorization");
1440 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1441 env(tx, Ter(tecNO_AUTH));
1442 });
1443
1444 testCase([this](
1445 Env& env,
1446 Account const& issuer,
1447 Account const& owner,
1448 Account const& depositor,
1449 Asset const& asset,
1450 Vault& vault) {
1451 testcase("MPT cannot set Scale=0");
1452 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1453 tx[sfScale] = 0;
1454 env(tx, Ter{temMALFORMED});
1455 });
1456
1457 testCase([this](
1458 Env& env,
1459 Account const& issuer,
1460 Account const& owner,
1461 Account const& depositor,
1462 Asset const& asset,
1463 Vault& vault) {
1464 testcase("MPT cannot set Scale=1");
1465 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1466 tx[sfScale] = 1;
1467 env(tx, Ter{temMALFORMED});
1468 });
1469 }
1470
1471 void
1473 {
1474 using namespace test::jtx;
1475
1476 Env env{*this, testableAmendments()};
1477 Account const issuer{"issuer"};
1478 Account const owner{"owner"};
1479 Account const depositor{"depositor"};
1480 env.fund(XRP(1000), issuer, owner, depositor);
1481 env.close();
1482
1483 Vault const vault{env};
1484 PrettyAsset const asset = issuer["IOU"];
1485 env.trust(asset(1000), owner);
1486 env(pay(issuer, owner, asset(100)));
1487 env.trust(asset(1000), depositor);
1488 env(pay(issuer, depositor, asset(100)));
1489 env.close();
1490
1491 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1492 tx[sfFlags] = tfVaultShareNonTransferable;
1493 env(tx);
1494 env.close();
1495
1496 {
1497 testcase("nontransferable deposits");
1498 auto tx1 =
1499 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(40)});
1500 env(tx1);
1501
1502 auto tx2 = vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(60)});
1503 env(tx2);
1504 env.close();
1505 }
1506
1507 auto const vaultAccount = //
1508 [&env, key = keylet.key, this]() -> AccountID {
1509 auto jvVault = env.rpc("vault_info", strHex(key));
1510
1511 BEAST_EXPECT(jvVault[jss::result][jss::vault][sfAssetsTotal] == "100");
1512 BEAST_EXPECT(
1513 jvVault[jss::result][jss::vault][jss::shares][sfOutstandingAmount] == "100000000");
1514
1515 // Vault pseudo-account
1516 return parseBase58<AccountID>(jvVault[jss::result][jss::vault][jss::Account].asString())
1517 .value();
1518 }();
1519
1520 auto const mptId = makeMptID(1, vaultAccount);
1521 Asset const shares = mptId;
1522
1523 {
1524 testcase("nontransferable shares cannot be moved");
1525 env(pay(owner, depositor, shares(10)), Ter{tecNO_AUTH});
1526 env(pay(depositor, owner, shares(10)), Ter{tecNO_AUTH});
1527 }
1528
1529 {
1530 testcase("nontransferable shares can be used to withdraw");
1531 auto tx1 =
1532 vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(20)});
1533 env(tx1);
1534
1535 auto tx2 = vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(30)});
1536 env(tx2);
1537 env.close();
1538 }
1539
1540 {
1541 testcase("nontransferable shares balance check");
1542 auto jvVault = env.rpc("vault_info", strHex(keylet.key));
1543 BEAST_EXPECT(jvVault[jss::result][jss::vault][sfAssetsTotal] == "50");
1544 BEAST_EXPECT(
1545 jvVault[jss::result][jss::vault][jss::shares][sfOutstandingAmount] == "50000000");
1546 }
1547
1548 {
1549 testcase("nontransferable shares withdraw rest");
1550 auto tx1 =
1551 vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(20)});
1552 env(tx1);
1553
1554 auto tx2 = vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(30)});
1555 env(tx2);
1556 env.close();
1557 }
1558
1559 {
1560 testcase("nontransferable shares delete empty vault");
1561 auto tx = vault.del({.owner = owner, .id = keylet.key});
1562 env(tx);
1563 BEAST_EXPECT(!env.le(keylet));
1564 }
1565 }
1566
1567 void
1569 {
1570 using namespace test::jtx;
1571
1572 struct CaseArgs
1573 {
1574 bool enableClawback = true;
1575 bool requireAuth = true;
1576 int initialXRP = 1000;
1577 FeatureBitset features = testableAmendments();
1578 };
1579
1580 auto testCase = [this](
1581 std::function<void(
1582 Env & env,
1583 Account const& issuer,
1584 Account const& owner,
1585 Account const& depositor,
1586 Asset const& asset,
1587 Vault& vault,
1588 MPTTester& mptt)> test,
1589 CaseArgs args = {}) {
1590 Env env{*this, args.features};
1591 Account const issuer{"issuer"};
1592 Account const owner{"owner"};
1593 Account const depositor{"depositor"};
1594 env.fund(XRP(args.initialXRP), issuer, owner, depositor);
1595 env.close();
1596 Vault vault{env};
1597
1598 MPTTester mptt{env, issuer, kMptInitNoFund};
1599 auto const kNone = LedgerSpecificFlags(0);
1600 mptt.create(
1601 {.flags = tfMPTCanTransfer | tfMPTCanLock |
1602 (args.enableClawback ? tfMPTCanClawback : kNone) |
1603 (args.requireAuth ? tfMPTRequireAuth : kNone),
1604 .mutableFlags = tmfMPTCanEnableCanTransfer});
1605 PrettyAsset const asset = mptt.issuanceID();
1606 mptt.authorize({.account = owner});
1607 mptt.authorize({.account = depositor});
1608 if (args.requireAuth)
1609 {
1610 mptt.authorize({.account = issuer, .holder = owner});
1611 mptt.authorize({.account = issuer, .holder = depositor});
1612 }
1613
1614 env(pay(issuer, depositor, asset(1000)));
1615 env.close();
1616
1617 test(env, issuer, owner, depositor, asset, vault, mptt);
1618 };
1619
1620 testCase([this](
1621 Env& env,
1622 Account const& issuer,
1623 Account const& owner,
1624 Account const& depositor,
1625 PrettyAsset const& asset,
1626 Vault& vault,
1627 MPTTester& mptt) {
1628 testcase("MPT nothing to clawback from");
1629 auto tx = vault.clawback(
1630 {.issuer = issuer,
1631 .id = keylet::skip().key,
1632 .holder = depositor,
1633 .amount = asset(10)});
1634 env(tx, Ter(tecNO_ENTRY));
1635 });
1636
1637 // Freeze/lock tests are in testVaultDepositFreeze/testVaultWithdrawFreeze
1638
1639 testCase([this](
1640 Env& env,
1641 Account const& issuer,
1642 Account const& owner,
1643 Account const& depositor,
1644 Asset const& asset,
1645 Vault& vault,
1646 MPTTester& mptt) {
1647 testcase("MPT global lock blocks create");
1648 mptt.set({.account = issuer, .flags = tfMPTLock});
1649 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1650 env(tx, Ter(tecLOCKED));
1651 });
1652
1653 testCase([this](
1654 Env& env,
1655 Account const& issuer,
1656 Account const& owner,
1657 Account const& depositor,
1658 PrettyAsset const& asset,
1659 Vault& vault,
1660 MPTTester& mptt) {
1661 testcase("MPT only issuer can clawback");
1662
1663 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1664 env(tx);
1665 env.close();
1666
1667 tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
1668 env(tx);
1669 env.close();
1670
1671 {
1672 auto tx = vault.clawback({
1673 .issuer = depositor,
1674 .id = keylet.key,
1675 .holder = depositor,
1676 });
1677 env(tx, Ter(tecNO_PERMISSION));
1678 }
1679
1680 {
1681 auto tx = vault.clawback({
1682 .issuer = owner,
1683 .id = keylet.key,
1684 .holder = depositor,
1685 });
1686 env(tx, Ter(tecNO_PERMISSION));
1687 }
1688 });
1689
1690 testCase(
1691 [this](
1692 Env& env,
1693 Account const& issuer,
1694 Account const& owner,
1695 Account const& depositor,
1696 PrettyAsset const& asset,
1697 Vault& vault,
1698 MPTTester& mptt) {
1699 testcase("MPT depositor without MPToken, auth required");
1700
1701 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1702 env(tx);
1703 env.close();
1704
1705 tx = vault.deposit(
1706 {.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
1707 env(tx);
1708 env.close();
1709
1710 {
1711 // Remove depositor MPToken and it will not be re-created
1712 mptt.authorize({.account = depositor, .flags = tfMPTUnauthorize});
1713 env.close();
1714
1715 auto const mptoken = keylet::mptoken(mptt.issuanceID(), depositor);
1716 auto const sleMPT1 = env.le(mptoken);
1717 BEAST_EXPECT(sleMPT1 == nullptr);
1718
1719 tx = vault.withdraw(
1720 {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
1721 env(tx, Ter{tecNO_AUTH});
1722 env.close();
1723
1724 auto const sleMPT2 = env.le(mptoken);
1725 BEAST_EXPECT(sleMPT2 == nullptr);
1726 }
1727
1728 {
1729 // Set destination to 3rd party without MPToken
1730 Account const charlie{"charlie"};
1731 env.fund(XRP(1000), charlie);
1732 env.close();
1733
1734 tx = vault.withdraw(
1735 {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
1736 tx[sfDestination] = charlie.human();
1737 env(tx, Ter(tecNO_AUTH));
1738 }
1739 },
1740 {.requireAuth = true});
1741
1742 testCase(
1743 [this](
1744 Env& env,
1745 Account const& issuer,
1746 Account const& owner,
1747 Account const& depositor,
1748 PrettyAsset const& asset,
1749 Vault& vault,
1750 MPTTester& mptt) {
1751 testcase("MPT depositor without MPToken, no auth required");
1752
1753 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1754 env(tx);
1755 env.close();
1756 auto v = env.le(keylet);
1757 BEAST_EXPECT(v);
1758
1759 tx = vault.deposit(
1760 {.depositor = depositor,
1761 .id = keylet.key,
1762 .amount = asset(1000)}); // all assets held by depositor
1763 env(tx);
1764 env.close();
1765
1766 {
1767 // Remove depositor's MPToken and it will be re-created
1768 mptt.authorize({.account = depositor, .flags = tfMPTUnauthorize});
1769 env.close();
1770
1771 auto const mptoken = keylet::mptoken(mptt.issuanceID(), depositor);
1772 auto const sleMPT1 = env.le(mptoken);
1773 BEAST_EXPECT(sleMPT1 == nullptr);
1774
1775 tx = vault.withdraw(
1776 {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
1777 env(tx);
1778 env.close();
1779
1780 auto const sleMPT2 = env.le(mptoken);
1781 BEAST_EXPECT(sleMPT2 != nullptr);
1782 BEAST_EXPECT(sleMPT2->at(sfMPTAmount) == 100);
1783 }
1784
1785 {
1786 // Remove 3rd party MPToken and it will not be re-created
1787 mptt.authorize({.account = owner, .flags = tfMPTUnauthorize});
1788 env.close();
1789
1790 auto const mptoken = keylet::mptoken(mptt.issuanceID(), owner);
1791 auto const sleMPT1 = env.le(mptoken);
1792 BEAST_EXPECT(sleMPT1 == nullptr);
1793
1794 tx = vault.withdraw(
1795 {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
1796 tx[sfDestination] = owner.human();
1797 env(tx, Ter(tecNO_AUTH));
1798 env.close();
1799
1800 auto const sleMPT2 = env.le(mptoken);
1801 BEAST_EXPECT(sleMPT2 == nullptr);
1802 }
1803 },
1804 {.requireAuth = false});
1805
1806 auto const [acctReserve, incReserve] = [this]() -> std::pair<int, int> {
1807 Env const env{*this, testableAmendments()};
1808 return {
1809 env.current()->fees().accountReserve(0).drops() / kDropsPerXrp.drops(),
1810 env.current()->fees().increment.drops() / kDropsPerXrp.drops()};
1811 }();
1812
1813 testCase(
1814 [&, this](
1815 Env& env,
1816 Account const& issuer,
1817 Account const& owner,
1818 Account const& depositor,
1819 PrettyAsset const& asset,
1820 Vault& vault,
1821 MPTTester& mptt) {
1822 testcase("MPT fail reserve to re-create MPToken");
1823
1824 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1825 env(tx);
1826 env.close();
1827 auto v = env.le(keylet);
1828 BEAST_EXPECT(v);
1829
1830 env(pay(depositor, owner, asset(1000)));
1831 env.close();
1832
1833 tx = vault.deposit(
1834 {.depositor = owner,
1835 .id = keylet.key,
1836 .amount = asset(1000)}); // all assets held by owner
1837 env(tx);
1838 env.close();
1839
1840 {
1841 // Remove owners's MPToken and it will not be re-created
1842 mptt.authorize({.account = owner, .flags = tfMPTUnauthorize});
1843 env.close();
1844
1845 auto const mptoken = keylet::mptoken(mptt.issuanceID(), owner);
1846 auto const sleMPT = env.le(mptoken);
1847 BEAST_EXPECT(sleMPT == nullptr);
1848
1849 // Use one reserve so the next transaction fails
1850 env(ticket::create(owner, 1));
1851 env.close();
1852
1853 // No reserve to create MPToken for asset in VaultWithdraw
1854 tx = vault.withdraw(
1855 {.depositor = owner, .id = keylet.key, .amount = asset(100)});
1856 env(tx, Ter{tecINSUFFICIENT_RESERVE});
1857 env.close();
1858
1859 env(pay(depositor, owner, XRP(incReserve)));
1860 env.close();
1861
1862 // Withdraw can now create asset MPToken, tx will succeed
1863 env(tx);
1864 env.close();
1865 }
1866 },
1867 {.requireAuth = false, .initialXRP = acctReserve + (incReserve * 4) + 1});
1868
1869 testCase([this](
1870 Env& env,
1871 Account const& issuer,
1872 Account const& owner,
1873 Account const& depositor,
1874 PrettyAsset const& asset,
1875 Vault& vault,
1876 MPTTester& mptt) {
1877 testcase("MPT issuance deleted");
1878
1879 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1880 env(tx);
1881 env.close();
1882
1883 tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
1884 env(tx);
1885 env.close();
1886
1887 {
1888 auto tx = vault.clawback(
1889 {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(0)});
1890 env(tx);
1891 }
1892
1893 mptt.destroy({.issuer = issuer, .id = mptt.issuanceID()});
1894 env.close();
1895
1896 {
1897 auto [tx, keylet] = vault.create({.owner = depositor, .asset = asset});
1898 env(tx, Ter{tecOBJECT_NOT_FOUND});
1899 }
1900
1901 {
1902 auto tx =
1903 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(10)});
1904 env(tx, Ter{tecOBJECT_NOT_FOUND});
1905 }
1906
1907 {
1908 auto tx =
1909 vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(10)});
1910 env(tx, Ter{tecOBJECT_NOT_FOUND});
1911 }
1912
1913 {
1914 auto tx = vault.clawback(
1915 {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(0)});
1916 env(tx, Ter{tecOBJECT_NOT_FOUND});
1917 }
1918
1919 env(vault.del({.owner = owner, .id = keylet.key}));
1920 });
1921
1922 testCase([this](
1923 Env& env,
1924 Account const& issuer,
1925 Account const& owner,
1926 Account const& depositor,
1927 PrettyAsset const& asset,
1928 Vault& vault,
1929 MPTTester& mptt) {
1930 testcase("MPT vault owner can receive shares unless unauthorized");
1931
1932 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1933 env(tx);
1934 env.close();
1935
1936 tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
1937 env(tx);
1938 env.close();
1939
1940 auto const issuanceId = [&env](xrpl::Keylet keylet) -> MPTID {
1941 auto const vault = env.le(keylet);
1942 return vault->at(sfShareMPTID);
1943 }(keylet);
1944 PrettyAsset const shares = MPTIssue(issuanceId);
1945
1946 {
1947 // owner has MPToken for shares they did not explicitly create
1948 env(pay(depositor, owner, shares(1)));
1949 env.close();
1950
1951 tx = vault.withdraw({.depositor = owner, .id = keylet.key, .amount = shares(1)});
1952 env(tx);
1953 env.close();
1954
1955 // owner's MPToken for vault shares not destroyed by withdraw
1956 env(pay(depositor, owner, shares(1)));
1957 env.close();
1958
1959 tx = vault.clawback(
1960 {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(0)});
1961 env(tx);
1962 env.close();
1963
1964 // owner's MPToken for vault shares not destroyed by clawback
1965 env(pay(depositor, owner, shares(1)));
1966 env.close();
1967
1968 // pay back, so we can destroy owner's MPToken now
1969 env(pay(owner, depositor, shares(1)));
1970 env.close();
1971
1972 {
1973 // explicitly destroy vault owners MPToken with zero balance
1974 json::Value jv;
1975 jv[sfAccount] = owner.human();
1976 jv[sfMPTokenIssuanceID] = to_string(issuanceId);
1977 jv[sfFlags] = tfMPTUnauthorize;
1978 jv[sfTransactionType] = jss::MPTokenAuthorize;
1979 env(jv);
1980 env.close();
1981 }
1982
1983 // owner no longer has MPToken for vault shares
1984 tx = pay(depositor, owner, shares(1));
1985 env(tx, Ter{tecNO_AUTH});
1986 env.close();
1987
1988 // destroy all remaining shares, so we can delete vault
1989 tx = vault.clawback(
1990 {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(0)});
1991 env(tx);
1992 env.close();
1993
1994 // will soft fail destroying MPToken for vault owner
1995 env(vault.del({.owner = owner, .id = keylet.key}));
1996 env.close();
1997 }
1998 });
1999
2000 testCase(
2001 [this](
2002 Env& env,
2003 Account const& issuer,
2004 Account const& owner,
2005 Account const& depositor,
2006 PrettyAsset const& asset,
2007 Vault& vault,
2008 MPTTester& mptt) {
2009 testcase("MPT clawback disabled");
2010
2011 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
2012 env(tx);
2013 env.close();
2014
2015 tx = vault.deposit(
2016 {.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
2017 env(tx);
2018 env.close();
2019
2020 {
2021 auto tx = vault.clawback(
2022 {.issuer = issuer,
2023 .id = keylet.key,
2024 .holder = depositor,
2025 .amount = asset(0)});
2026 env(tx, Ter{tecNO_PERMISSION});
2027 }
2028 },
2029 {.enableClawback = false});
2030
2031 testCase([this](
2032 Env& env,
2033 Account const& issuer,
2034 Account const& owner,
2035 Account const& depositor,
2036 Asset const& asset,
2037 Vault& vault,
2038 MPTTester& mptt) {
2039 testcase("MPT un-authorization");
2040 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
2041 env(tx);
2042 env.close();
2043 tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
2044 env(tx);
2045 env.close();
2046
2047 mptt.authorize({.account = issuer, .holder = depositor, .flags = tfMPTUnauthorize});
2048 env.close();
2049
2050 {
2051 auto tx = vault.withdraw(
2052 {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
2053 env(tx, Ter(tecNO_AUTH));
2054
2055 // Withdrawal to other (authorized) accounts works
2056 tx[sfDestination] = issuer.human();
2057 env(tx);
2058 env.close();
2059
2060 tx[sfDestination] = owner.human();
2061 env(tx);
2062 env.close();
2063 }
2064
2065 {
2066 // Cannot deposit some more
2067 auto tx =
2068 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
2069 env(tx, Ter(tecNO_AUTH));
2070 }
2071
2072 {
2073 // Cannot clawback if issuer is the holder
2074 tx = vault.clawback(
2075 {.issuer = issuer, .id = keylet.key, .holder = issuer, .amount = asset(800)});
2076 env(tx, Ter(tecNO_PERMISSION));
2077 }
2078 // Clawback works
2079 tx = vault.clawback(
2080 {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(800)});
2081 env(tx);
2082 env.close();
2083
2084 env(vault.del({.owner = owner, .id = keylet.key}));
2085 });
2086
2087 {
2088 testcase("MPT shares to a vault");
2089
2090 Env env{*this, testableAmendments()};
2091 Account const owner{"owner"};
2092 Account const issuer{"issuer"};
2093 env.fund(XRP(1000000), owner, issuer);
2094 env.close();
2095 Vault const vault{env};
2096
2097 MPTTester mptt{env, issuer, kMptInitNoFund};
2098 mptt.create(
2099 {.flags = tfMPTCanTransfer | tfMPTCanLock | lsfMPTCanClawback | tfMPTRequireAuth});
2100 mptt.authorize({.account = owner});
2101 mptt.authorize({.account = issuer, .holder = owner});
2102 PrettyAsset const asset = mptt.issuanceID();
2103 env(pay(issuer, owner, asset(100)));
2104 auto [tx1, k1] = vault.create({.owner = owner, .asset = asset});
2105 env(tx1);
2106 env.close();
2107
2108 auto const shares = [&env, keylet = k1, this]() -> Asset {
2109 auto const vault = env.le(keylet);
2110 BEAST_EXPECT(vault != nullptr);
2111 return MPTIssue(vault->at(sfShareMPTID));
2112 }();
2113
2114 auto [tx2, k2] = vault.create({.owner = owner, .asset = shares});
2115 env(tx2, Ter{tecWRONG_ASSET});
2116 env.close();
2117 }
2118
2119 {
2120 testcase("MPT locked: vault shares inherit underlying lock");
2121
2122 Env env{*this, testableAmendments()};
2123 Account const issuer{"issuer"};
2124 Account const owner{"owner"};
2125 Account const alice{"alice"};
2126 Account const bob{"bob"};
2127 Account const carol{"carol"};
2128 env.fund(XRP(10'000), issuer, owner, alice, bob, carol);
2129 env.close();
2130 Vault const vault{env};
2131
2132 MPTTester asset{
2133 {.env = env,
2134 .issuer = issuer,
2135 .holders = {owner, alice, bob, carol},
2136 .flags = tfMPTCanTransfer | tfMPTCanTrade | tfMPTCanLock}};
2137 env(pay(issuer, alice, asset(1'000)));
2138 env(pay(issuer, bob, asset(1'000)));
2139 env.close();
2140
2141 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
2142 env(tx);
2143 env.close();
2144
2145 env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(500)}));
2146 // Bob also deposits so he has a share MPToken to receive into.
2147 env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(500)}));
2148 env.close();
2149
2150 auto const shares = [&]() -> PrettyAsset {
2151 auto const sle = env.le(keylet);
2152 BEAST_EXPECT(sle != nullptr);
2153 return MPTIssue(sle->at(sfShareMPTID));
2154 }();
2155 auto const shareMptID = shares.raw().get<MPTIssue>().getMptID();
2156 auto const shareBalance = [&](Account const& account) {
2157 auto const sle = env.le(keylet::mptoken(shareMptID, account));
2158 return sle ? sle->at(sfMPTAmount) : 0;
2159 };
2160
2161 // Sanity: before the underlying lock, peer-to-peer share
2162 // transfers are allowed.
2163 env(pay(alice, bob, shares(1)));
2164 env.close();
2165
2166 // Create the offer while shares are spendable, then lock the
2167 // underlying to test whether a stale offer can still be crossed.
2168 env(offer(alice, XRP(1), shares(1)));
2169 env.close();
2170
2171 // Lock the underlying after the vault and share balances exist.
2172 asset.set({.account = issuer, .flags = tfMPTLock});
2173 env.close();
2174
2175 // Direct vault share payment inherits the underlying lock via
2176 // sfReferenceHolding.
2177 BEAST_EXPECT(shareBalance(alice) == 499);
2178 BEAST_EXPECT(shareBalance(bob) == 501);
2179 env(pay(alice, bob, shares(1)), Ter{tecLOCKED});
2180 env.close();
2181 BEAST_EXPECT(shareBalance(alice) == 499);
2182 BEAST_EXPECT(shareBalance(bob) == 501);
2183
2184 // The same inherited lock must also block DEX payment paths that
2185 // would consume an offer selling vault shares.
2186 env(pay(carol, bob, shares(1)),
2187 Sendmax(XRP(1)),
2188 Path(BookSpec{shares.raw()}),
2189 Ter{tecPATH_PARTIAL});
2190 env.close();
2191 BEAST_EXPECT(shareBalance(alice) == 499);
2192 BEAST_EXPECT(shareBalance(bob) == 501);
2193 BEAST_EXPECT(expectOffers(env, alice, 1));
2194 }
2195
2196 {
2197 testcase("MPT CanTrade governance: share inherits underlying on DEX and AMM");
2198
2199 Env env{*this, testableAmendments()};
2200 Account const issuer{"issuer"};
2201 Account const owner{"owner"};
2202 Account const alice{"alice"};
2203 Account const bob{"bob"};
2204 env.fund(XRP(100'000), issuer, owner, alice, bob);
2205 env.close();
2206 Vault const vault{env};
2207
2208 MPTTester mptt{env, issuer, kMptInitNoFund};
2209 mptt.create(
2210 {.flags = tfMPTCanTransfer | tfMPTCanLock,
2211 .mutableFlags = tmfMPTCanEnableCanTrade});
2212 PrettyAsset const asset = mptt.issuanceID();
2213 mptt.authorize({.account = owner});
2214 mptt.authorize({.account = alice});
2215 mptt.authorize({.account = bob});
2216 env(pay(issuer, alice, asset(10'000)));
2217 env(pay(issuer, bob, asset(10'000)));
2218 env.close();
2219
2220 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
2221 env(tx);
2222 env.close();
2223
2224 // Seed shares so we can later place them on trading venues.
2225 env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(5'000)}));
2226 env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(5'000)}));
2227 env.close();
2228
2229 auto const shares = [&]() -> PrettyAsset {
2230 auto const sle = env.le(keylet);
2231 BEAST_EXPECT(sle != nullptr);
2232 return MPTIssue(sle->at(sfShareMPTID));
2233 }();
2234
2235 // CanTrade is not set on the underlying, both the asset and
2236 // the vault share are blocked on the DEX.
2237 env(offer(alice, XRP(1), asset(10)), Ter{tecNO_PERMISSION});
2238 env(offer(alice, XRP(1), shares(1)), Ter{tecNO_PERMISSION});
2239 env.close();
2240
2241 // The inherited CanTrade restriction also blocks AMM creation.
2242 AMM const ammUnderlyingFail(
2243 env, alice, XRP(1'000), asset(1'000), Ter{tecNO_PERMISSION});
2244 AMM const ammShares(env, alice, XRP(1'000), shares(100), Ter{tecNO_PERMISSION});
2245
2246 // Deposit still works before enabling CanTrade.
2247 env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(100)}));
2248 env.close();
2249
2250 // Peer-to-peer share transfers still work (CanTransfer is set on
2251 // both layers).
2252 env(pay(alice, bob, shares(1)));
2253 env.close();
2254
2255 // Withdraw still works before enabling CanTrade.
2256 env(vault.withdraw({.depositor = alice, .id = keylet.key, .amount = asset(100)}));
2257 env.close();
2258
2259 // Enable CanTrade on the underlying.
2260 mptt.set({.mutableFlags = tmfMPTSetCanTrade});
2261 env.close();
2262
2263 env(offer(alice, XRP(1), asset(10)));
2264 env(offer(alice, XRP(1), shares(1)));
2265 env.close();
2266
2267 AMM const ammUnderlying(env, alice, XRP(1'000), asset(1'000));
2268 }
2269
2270 {
2271 testcase("MPT OutstandingAmount > MaximumAmount");
2272
2273 Env env{*this, testableAmendments() | featureSingleAssetVault};
2274 Account const alice{"alice"};
2275 Account const issuer{"issuer"};
2276 env.fund(XRP(1'000), alice, issuer);
2277 env.close();
2278 Vault const vault{env};
2279
2280 MPTTester const btc({.env = env, .issuer = issuer, .holders = {alice}, .maxAmt = 100});
2281
2282 auto [tx, k] = vault.create({.owner = issuer, .asset = btc});
2283 env(tx);
2284 env.close();
2285
2286 tx = vault.deposit({.depositor = issuer, .id = k.key, .amount = btc(110)});
2287 // accountHolds is the first check and the issuer has only BTC(100)
2288 // available
2289 env(tx, Ter{tecINSUFFICIENT_FUNDS});
2290 env.close();
2291
2292 // OutstandingAmount == MaximumAmount
2293 env(pay(issuer, alice, btc(100)));
2294 env.close();
2295
2296 tx = vault.deposit({.depositor = issuer, .id = k.key, .amount = btc(100)});
2297 // the issuer has BTC(0) available
2298 env(tx, Ter{tecINSUFFICIENT_FUNDS});
2299 env.close();
2300
2301 tx = vault.deposit({.depositor = alice, .id = k.key, .amount = btc(100)});
2302 // alice transfers BTC(100), OutstandingAmount is 100
2303 env(tx);
2304 env.close();
2305 }
2306 }
2307
2308 void
2310 {
2311 using namespace test::jtx;
2312
2313 struct CaseArgs
2314 {
2315 int initialXRP = 1000;
2316 Number initialIOU = 200;
2317 double transferRate = 1.0;
2318 bool charlieRipple = true;
2319 FeatureBitset features = testableAmendments();
2320 };
2321
2322 auto testCase = [&, this](
2323 std::function<void(
2324 Env & env,
2325 Account const& owner,
2326 Account const& issuer,
2327 Account const& charlie,
2328 std::function<Account(xrpl::Keylet)> vaultAccount,
2329 Vault& vault,
2330 PrettyAsset const& asset,
2331 std::function<MPTID(xrpl::Keylet)> issuanceId)> test,
2332 CaseArgs args = {}) {
2333 Env env{*this, args.features};
2334 Account const owner{"owner"};
2335 Account const issuer{"issuer"};
2336 Account const charlie{"charlie"};
2337 Vault vault{env};
2338 env.fund(XRP(args.initialXRP), issuer, owner, charlie);
2339 env(fset(issuer, asfAllowTrustLineClawback));
2340 env.close();
2341
2342 PrettyAsset const asset = issuer["IOU"];
2343 env.trust(asset(1000), owner);
2344 env(pay(issuer, owner, asset(args.initialIOU)));
2345 env.close();
2346 if (!args.charlieRipple)
2347 {
2348 env(fset(issuer, 0, asfDefaultRipple));
2349 env.close();
2350 env.trust(asset(1000), charlie);
2351 env.close();
2352 env(pay(issuer, charlie, asset(args.initialIOU)));
2353 env.close();
2354 env(fset(issuer, asfDefaultRipple));
2355 }
2356 else
2357 {
2358 env.trust(asset(1000), charlie);
2359 }
2360 env.close();
2361 env(rate(issuer, args.transferRate));
2362 env.close();
2363
2364 auto const vaultAccount = [&env](xrpl::Keylet keylet) -> Account {
2365 return Account("vault", env.le(keylet)->at(sfAccount));
2366 };
2367 auto const issuanceId = [&env](xrpl::Keylet keylet) -> MPTID {
2368 return env.le(keylet)->at(sfShareMPTID);
2369 };
2370
2371 test(env, owner, issuer, charlie, vaultAccount, vault, asset, issuanceId);
2372 };
2373
2374 testCase([&, this](
2375 Env& env,
2376 Account const& owner,
2377 Account const& issuer,
2378 Account const&,
2379 auto vaultAccount,
2380 Vault& vault,
2381 PrettyAsset const& asset,
2382 auto&&...) {
2383 testcase("IOU cannot use different asset");
2384 PrettyAsset const foo = issuer["FOO"];
2385
2386 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
2387 env(tx);
2388 env.close();
2389
2390 {
2391 // Cannot create new trustline to a vault
2392 auto tx = [&, account = vaultAccount(keylet)]() {
2393 json::Value jv;
2394 jv[jss::Account] = issuer.human();
2395 {
2396 auto& ja = jv[jss::LimitAmount] =
2397 foo(0).value().getJson(JsonOptions::Values::None);
2398 ja[jss::issuer] = toBase58(account);
2399 }
2400 jv[jss::TransactionType] = jss::TrustSet;
2401 jv[jss::Flags] = tfSetFreeze;
2402 return jv;
2403 }();
2404 env(tx, Ter{tecNO_PERMISSION});
2405 env.close();
2406 }
2407
2408 {
2409 auto tx = vault.deposit({.depositor = issuer, .id = keylet.key, .amount = foo(20)});
2410 env(tx, Ter{tecWRONG_ASSET});
2411 env.close();
2412 }
2413
2414 {
2415 auto tx =
2416 vault.withdraw({.depositor = issuer, .id = keylet.key, .amount = foo(20)});
2417 env(tx, Ter{tecWRONG_ASSET});
2418 env.close();
2419 }
2420
2421 env(vault.del({.owner = owner, .id = keylet.key}));
2422 env.close();
2423 });
2424
2425 testCase(
2426 [&, this](
2427 Env& env,
2428 Account const& owner,
2429 Account const& issuer,
2430 Account const& charlie,
2431 auto vaultAccount,
2432 Vault& vault,
2433 PrettyAsset const& asset,
2434 auto issuanceId) {
2435 testcase("IOU transfer fees not applied");
2436
2437 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
2438 env(tx);
2439 env.close();
2440
2441 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
2442 env.close();
2443
2444 auto const issue = asset.raw().get<Issue>();
2445 Asset const share = Asset(issuanceId(keylet));
2446
2447 // transfer fees ignored on deposit
2448 BEAST_EXPECT(env.balance(owner, issue) == asset(100));
2449 BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(100));
2450
2451 {
2452 auto tx = vault.clawback(
2453 {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(50)});
2454 env(tx);
2455 env.close();
2456 }
2457
2458 // transfer fees ignored on clawback
2459 BEAST_EXPECT(env.balance(owner, issue) == asset(100));
2460 BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(50));
2461
2462 env(vault.withdraw(
2463 {.depositor = owner, .id = keylet.key, .amount = share(20'000'000)}));
2464
2465 // transfer fees ignored on withdraw
2466 BEAST_EXPECT(env.balance(owner, issue) == asset(120));
2467 BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(30));
2468
2469 {
2470 auto tx = vault.withdraw(
2471 {.depositor = owner, .id = keylet.key, .amount = share(30'000'000)});
2472 tx[sfDestination] = charlie.human();
2473 env(tx);
2474 }
2475
2476 // transfer fees ignored on withdraw to 3rd party
2477 BEAST_EXPECT(env.balance(owner, issue) == asset(120));
2478 BEAST_EXPECT(env.balance(charlie, issue) == asset(30));
2479 BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(0));
2480
2481 env(vault.del({.owner = owner, .id = keylet.key}));
2482 env.close();
2483 },
2484 CaseArgs{.transferRate = 1.25});
2485
2486 testCase([&, this](
2487 Env& env,
2488 Account const& owner,
2489 Account const& issuer,
2490 Account const& charlie,
2491 auto,
2492 Vault& vault,
2493 PrettyAsset const& asset,
2494 auto&&...) {
2495 testcase("IOU no trust line to 3rd party");
2496
2497 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
2498 env(tx);
2499 env.close();
2500
2501 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
2502 env.close();
2503
2504 Account const erin{"erin"};
2505 env.fund(XRP(1000), erin);
2506 env.close();
2507
2508 // Withdraw to 3rd party without trust line
2509 auto const tx1 = [&](xrpl::Keylet keylet) {
2510 auto tx =
2511 vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
2512 tx[sfDestination] = erin.human();
2513 return tx;
2514 }(keylet);
2515 env(tx1, Ter{tecNO_LINE});
2516 });
2517
2518 testCase([&, this](
2519 Env& env,
2520 Account const& owner,
2521 Account const& issuer,
2522 Account const& charlie,
2523 auto,
2524 Vault& vault,
2525 PrettyAsset const& asset,
2526 auto&&...) {
2527 testcase("IOU no trust line to depositor");
2528
2529 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
2530 env(tx);
2531 env.close();
2532
2533 // reset limit, so deposit of all funds will delete the trust line
2534 env.trust(asset(0), owner);
2535 env.close();
2536
2537 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(200)}));
2538 env.close();
2539
2540 auto trustline = env.le(keylet::trustLine(owner, asset.raw().get<Issue>()));
2541 BEAST_EXPECT(trustline == nullptr);
2542
2543 // Withdraw without trust line, will succeed
2544 auto const tx1 = [&](xrpl::Keylet keylet) {
2545 auto tx =
2546 vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
2547 return tx;
2548 }(keylet);
2549 env(tx1);
2550 });
2551
2552 testCase(
2553 [&, this](
2554 Env& env,
2555 Account const& owner,
2556 Account const& issuer,
2557 Account const& charlie,
2558 auto vaultAccount,
2559 Vault& vault,
2560 PrettyAsset const& asset,
2561 std::function<MPTID(xrpl::Keylet)> issuanceId) {
2562 testcase("IOU non-transferable");
2563
2564 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
2565 tx[sfScale] = 0;
2566 env(tx);
2567 env.close();
2568
2569 // Turn on noripple on the pseudo account's trust line.
2570 // Charlie's is already set.
2571 env(trust(issuer, vaultAccount(keylet)["IOU"], tfSetNoRipple));
2572
2573 {
2574 // Charlie cannot deposit
2575 auto tx = vault.deposit(
2576 {.depositor = charlie, .id = keylet.key, .amount = asset(100)});
2577 env(tx, Ter{terNO_RIPPLE});
2578 env.close();
2579 }
2580
2581 {
2582 PrettyAsset const shares = issuanceId(keylet);
2583 auto tx1 =
2584 vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)});
2585 env(tx1);
2586 env.close();
2587
2588 // Charlie cannot receive funds
2589 auto tx2 = vault.withdraw(
2590 {.depositor = owner, .id = keylet.key, .amount = shares(100)});
2591 tx2[sfDestination] = charlie.human();
2592 env(tx2, Ter{terNO_RIPPLE});
2593 env.close();
2594
2595 {
2596 // Create MPToken for shares held by Charlie
2598 tx[sfAccount] = charlie.human();
2599 tx[sfMPTokenIssuanceID] =
2600 to_string(shares.raw().get<MPTIssue>().getMptID());
2601 tx[sfTransactionType] = jss::MPTokenAuthorize;
2602 env(tx);
2603 env.close();
2604 }
2605 // Behavioral shift introduced by share inheritance:
2606 // before fixCleanup3_2_0 this share Payment succeeded
2607 // and the underlying IOU's NoRipple restriction surfaced
2608 // only later on Charlie's withdrawal (terNO_RIPPLE).
2609 // Post-amendment, canTransfer reads the share's
2610 // sfReferenceHolding and dispatches to the underlying IOU;
2611 // rippling is disabled between owner and charlie so the
2612 // share payment itself is now blocked. tecPATH_DRY is
2613 // the path-find layer's translation of the underlying
2614 // terNO_RIPPLE under featureMPTokensV2.
2615 env(pay(owner, charlie, shares(100)), Ter{tecPATH_DRY});
2616 env.close();
2617 }
2618
2619 tx = vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(100)});
2620 env(tx);
2621 env.close();
2622
2623 // Delete vault with zero balance
2624 env(vault.del({.owner = owner, .id = keylet.key}));
2625 },
2626 {.charlieRipple = false});
2627
2628 testCase(
2629 [&, this](
2630 Env& env,
2631 Account const& owner,
2632 Account const& issuer,
2633 Account const& charlie,
2634 auto const& vaultAccount,
2635 Vault& vault,
2636 PrettyAsset const& asset,
2637 auto&&...) {
2638 testcase("IOU calculation rounding");
2639
2640 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
2641 tx[sfScale] = 1;
2642 env(tx);
2643 env.close();
2644
2645 auto const startingOwnerBalance = env.balance(owner, asset);
2646 BEAST_EXPECT((startingOwnerBalance.value() == STAmount{asset, 11875, -2}));
2647
2648 // This operation (first deposit 100, then 3.75 x 5) is known to
2649 // have triggered calculation rounding errors in Number
2650 // (addition and division), causing the last deposit to be
2651 // blocked by Vault invariants.
2652 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
2653
2654 auto const tx1 = vault.deposit(
2655 {.depositor = owner, .id = keylet.key, .amount = asset(Number(375, -2))});
2656 for (auto i = 0; i < 5; ++i)
2657 {
2658 env(tx1);
2659 }
2660 env.close();
2661
2662 {
2663 STAmount const xfer{asset, 1185, -1};
2664 BEAST_EXPECT(env.balance(owner, asset) == startingOwnerBalance.value() - xfer);
2665 BEAST_EXPECT(env.balance(vaultAccount(keylet), asset) == xfer);
2666
2667 auto const vault = env.le(keylet);
2668 BEAST_EXPECT(vault->at(sfAssetsAvailable) == xfer);
2669 BEAST_EXPECT(vault->at(sfAssetsTotal) == xfer);
2670 }
2671
2672 // Total vault balance should be 118.5 IOU. Withdraw and delete
2673 // the vault to verify this exact amount was deposited and the
2674 // owner has matching shares
2675 env(vault.withdraw(
2676 {.depositor = owner,
2677 .id = keylet.key,
2678 .amount = asset(Number(1000 + (37 * 5), -1))}));
2679
2680 {
2681 BEAST_EXPECT(env.balance(owner, asset) == startingOwnerBalance.value());
2682 BEAST_EXPECT(env.balance(vaultAccount(keylet), asset) == beast::kZero);
2683 auto const vault = env.le(keylet);
2684 BEAST_EXPECT(vault->at(sfAssetsAvailable) == beast::kZero);
2685 BEAST_EXPECT(vault->at(sfAssetsTotal) == beast::kZero);
2686 }
2687
2688 env(vault.del({.owner = owner, .id = keylet.key}));
2689 env.close();
2690 },
2691 {.initialIOU = Number(11875, -2)});
2692
2693 auto const [acctReserve, incReserve] = [this]() -> std::pair<int, int> {
2694 Env const env{*this, testableAmendments()};
2695 return {
2696 env.current()->fees().accountReserve(0).drops() / kDropsPerXrp.drops(),
2697 env.current()->fees().increment.drops() / kDropsPerXrp.drops()};
2698 }();
2699
2700 testCase(
2701 [&, this](
2702 Env& env,
2703 Account const& owner,
2704 Account const& issuer,
2705 Account const& charlie,
2706 auto,
2707 Vault& vault,
2708 PrettyAsset const& asset,
2709 auto&&...) {
2710 testcase("IOU no trust line to depositor no reserve");
2711 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
2712 env(tx);
2713 env.close();
2714
2715 // reset limit, so deposit of all funds will delete the trust
2716 // line
2717 env.trust(asset(0), owner);
2718 env.close();
2719
2720 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(200)}));
2721 env.close();
2722
2723 auto trustline = env.le(keylet::trustLine(owner, asset.raw().get<Issue>()));
2724 BEAST_EXPECT(trustline == nullptr);
2725
2726 env(ticket::create(owner, 1));
2727 env.close();
2728
2729 // Fail because not enough reserve to create trust line
2730 tx = vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
2731 env(tx, Ter{tecNO_LINE_INSUF_RESERVE});
2732 env.close();
2733
2734 env(pay(charlie, owner, XRP(incReserve)));
2735 env.close();
2736
2737 // Withdraw can now create trust line, will succeed
2738 env(tx);
2739 env.close();
2740 },
2741 CaseArgs{.initialXRP = acctReserve + (incReserve * 4) + 1});
2742
2743 testCase(
2744 [&, this](
2745 Env& env,
2746 Account const& owner,
2747 Account const& issuer,
2748 Account const& charlie,
2749 auto,
2750 Vault& vault,
2751 PrettyAsset const& asset,
2752 auto&&...) {
2753 testcase("IOU no reserve for share MPToken");
2754 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
2755 env(tx);
2756 env.close();
2757
2758 env(pay(owner, charlie, asset(100)));
2759 env.close();
2760
2761 env(ticket::create(charlie, 3));
2762 env.close();
2763
2764 // Fail because not enough reserve to create MPToken for shares
2765 tx = vault.deposit({.depositor = charlie, .id = keylet.key, .amount = asset(100)});
2766 env(tx, Ter{tecINSUFFICIENT_RESERVE});
2767 env.close();
2768
2769 env(pay(issuer, charlie, XRP(incReserve)));
2770 env.close();
2771
2772 // Deposit can now create MPToken, will succeed
2773 env(tx);
2774 env.close();
2775 },
2776 CaseArgs{.initialXRP = acctReserve + (incReserve * 4) + 1});
2777 }
2778
2779 void
2781 {
2782 using namespace test::jtx;
2783
2784 testcase("private vault");
2785
2786 Env env{*this, testableAmendments()};
2787 Account const issuer{"issuer"};
2788 Account const owner{"owner"};
2789 Account const depositor{"depositor"};
2790 Account const charlie{"charlie"};
2791 Account const pdOwner{"pdOwner"};
2792 Account const credIssuer1{"credIssuer1"};
2793 Account const credIssuer2{"credIssuer2"};
2794 std::string const credType = "credential";
2795 Vault const vault{env};
2796 env.fund(XRP(1000), issuer, owner, depositor, charlie, pdOwner, credIssuer1, credIssuer2);
2797 env.close();
2798 env(fset(issuer, asfAllowTrustLineClawback));
2799 env.close();
2800 env.require(Flags(issuer, asfAllowTrustLineClawback));
2801
2802 PrettyAsset const asset = issuer["IOU"];
2803 env.trust(asset(1000), owner);
2804 env(pay(issuer, owner, asset(500)));
2805 env.trust(asset(1000), depositor);
2806 env(pay(issuer, depositor, asset(500)));
2807 env.trust(asset(1000), charlie);
2808 env(pay(issuer, charlie, asset(5)));
2809 env.close();
2810
2811 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset, .flags = tfVaultPrivate});
2812 env(tx);
2813 env.close();
2814 BEAST_EXPECT(env.le(keylet));
2815
2816 {
2817 testcase("private vault owner can deposit");
2818 auto tx = vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(50)});
2819 env(tx);
2820 }
2821
2822 {
2823 testcase("private vault depositor not authorized yet");
2824 auto tx =
2825 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
2826 env(tx, Ter{tecNO_AUTH});
2827 }
2828
2829 {
2830 testcase("private vault cannot set non-existing domain");
2831 auto tx = vault.set({.owner = owner, .id = keylet.key});
2832 tx[sfDomainID] = to_string(BaseUInt<256>(42ul));
2833 env(tx, Ter{tecOBJECT_NOT_FOUND});
2834 }
2835
2836 {
2837 testcase("private vault set domainId");
2838
2839 {
2840 pdomain::Credentials const credentials1{
2841 {.issuer = credIssuer1, .credType = credType}};
2842
2843 env(pdomain::setTx(pdOwner, credentials1));
2844 auto const domainId1 = [&]() {
2845 auto tx = env.tx()->getJson(JsonOptions::Values::None);
2846 return pdomain::getNewDomain(env.meta());
2847 }();
2848
2849 auto tx = vault.set({.owner = owner, .id = keylet.key});
2850 tx[sfDomainID] = to_string(domainId1);
2851 env(tx);
2852 env.close();
2853
2854 // Update domain second time, should be harmless
2855 env(tx);
2856 env.close();
2857 }
2858
2859 {
2860 pdomain::Credentials const credentials{
2861 {.issuer = credIssuer1, .credType = credType},
2862 {.issuer = credIssuer2, .credType = credType}};
2863
2864 env(pdomain::setTx(pdOwner, credentials));
2865 auto const domainId = [&]() {
2866 auto tx = env.tx()->getJson(JsonOptions::Values::None);
2867 return pdomain::getNewDomain(env.meta());
2868 }();
2869
2870 auto tx = vault.set({.owner = owner, .id = keylet.key});
2871 tx[sfDomainID] = to_string(domainId);
2872 env(tx);
2873 env.close();
2874
2875 // Should be idempotent
2876 tx = vault.set({.owner = owner, .id = keylet.key});
2877 tx[sfDomainID] = to_string(domainId);
2878 env(tx);
2879 env.close();
2880 }
2881 }
2882
2883 {
2884 testcase("private vault depositor still not authorized");
2885 auto tx =
2886 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
2887 env(tx, Ter{tecNO_AUTH});
2888 env.close();
2889 }
2890
2891 auto const credKeylet = credentials::keylet(depositor, credIssuer1, credType);
2892 {
2893 testcase("private vault depositor now authorized");
2894 env(credentials::create(depositor, credIssuer1, credType));
2895 env(credentials::accept(depositor, credIssuer1, credType));
2896 env(credentials::create(charlie, credIssuer1, credType));
2897 // charlie's credential not accepted
2898 env.close();
2899 auto credSle = env.le(credKeylet);
2900 BEAST_EXPECT(credSle != nullptr);
2901
2902 auto tx =
2903 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
2904 env(tx);
2905 env.close();
2906
2907 tx = vault.deposit({.depositor = charlie, .id = keylet.key, .amount = asset(50)});
2908 env(tx, Ter{tecNO_AUTH});
2909 env.close();
2910 }
2911
2912 {
2913 testcase("private vault depositor lost authorization");
2914 env(credentials::deleteCred(credIssuer1, depositor, credIssuer1, credType));
2915 env(credentials::deleteCred(credIssuer1, charlie, credIssuer1, credType));
2916 env.close();
2917 auto credSle = env.le(credKeylet);
2918 BEAST_EXPECT(credSle == nullptr);
2919
2920 auto tx =
2921 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
2922 env(tx, Ter{tecNO_AUTH});
2923 env.close();
2924 }
2925
2926 auto const shares = [&env, keylet = keylet, this]() -> Asset {
2927 auto const vault = env.le(keylet);
2928 BEAST_EXPECT(vault != nullptr);
2929 return MPTIssue(vault->at(sfShareMPTID));
2930 }();
2931
2932 {
2933 testcase("private vault expired authorization");
2934 uint32_t const closeTime =
2935 env.current()->header().parentCloseTime.time_since_epoch().count();
2936 {
2937 auto tx0 = credentials::create(depositor, credIssuer2, credType);
2938 tx0[sfExpiration] = closeTime + 20;
2939 env(tx0);
2940 tx0 = credentials::create(charlie, credIssuer2, credType);
2941 tx0[sfExpiration] = closeTime + 20;
2942 env(tx0);
2943 env.close();
2944
2945 env(credentials::accept(depositor, credIssuer2, credType));
2946 env(credentials::accept(charlie, credIssuer2, credType));
2947 env.close();
2948 }
2949
2950 {
2951 auto tx1 =
2952 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
2953 env(tx1);
2954 env.close();
2955
2956 auto const tokenKeylet =
2957 keylet::mptoken(shares.get<MPTIssue>().getMptID(), depositor.id());
2958 BEAST_EXPECT(env.le(tokenKeylet) != nullptr);
2959 }
2960
2961 {
2962 // time advance
2963 env.close();
2964 env.close();
2965 env.close();
2966
2967 auto const credsKeylet = credentials::keylet(depositor, credIssuer2, credType);
2968 BEAST_EXPECT(env.le(credsKeylet) != nullptr);
2969
2970 auto tx2 =
2971 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(1)});
2972 env(tx2, Ter{tecEXPIRED});
2973 env.close();
2974
2975 BEAST_EXPECT(env.le(credsKeylet) == nullptr);
2976 }
2977
2978 {
2979 auto const credsKeylet = credentials::keylet(charlie, credIssuer2, credType);
2980 BEAST_EXPECT(env.le(credsKeylet) != nullptr);
2981 auto const tokenKeylet =
2982 keylet::mptoken(shares.get<MPTIssue>().getMptID(), charlie.id());
2983 BEAST_EXPECT(env.le(tokenKeylet) == nullptr);
2984
2985 auto tx3 =
2986 vault.deposit({.depositor = charlie, .id = keylet.key, .amount = asset(2)});
2987 env(tx3, Ter{tecEXPIRED});
2988
2989 env.close();
2990 BEAST_EXPECT(env.le(credsKeylet) == nullptr);
2991 BEAST_EXPECT(env.le(tokenKeylet) == nullptr);
2992 }
2993 }
2994
2995 {
2996 testcase("private vault reset domainId");
2997 auto tx = vault.set({.owner = owner, .id = keylet.key});
2998 tx[sfDomainID] = "0";
2999 env(tx);
3000 env.close();
3001
3002 tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
3003 env(tx, Ter{tecNO_AUTH});
3004 env.close();
3005
3006 tx = vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
3007 env(tx);
3008 env.close();
3009
3010 tx = vault.clawback(
3011 {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(0)});
3012 env(tx);
3013
3014 tx = vault.clawback(
3015 {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(0)});
3016 env(tx);
3017 env.close();
3018
3019 tx = vault.del({
3020 .owner = owner,
3021 .id = keylet.key,
3022 });
3023 env(tx);
3024 }
3025 }
3026
3027 void
3029 {
3030 using namespace test::jtx;
3031
3032 testcase("private XRP vault");
3033
3034 Env env{*this, testableAmendments()};
3035 Account const owner{"owner"};
3036 Account const depositor{"depositor"};
3037 Account const alice{"charlie"};
3038 std::string const credType = "credential";
3039 Vault const vault{env};
3040 env.fund(XRP(100000), owner, depositor, alice);
3041 env.close();
3042
3043 PrettyAsset const asset = xrpIssue();
3044 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset, .flags = tfVaultPrivate});
3045 env(tx);
3046 env.close();
3047
3048 auto const [vaultAccount, issuanceId] =
3049 [&env, keylet = keylet, this]() -> std::tuple<AccountID, uint192> {
3050 auto const vault = env.le(keylet);
3051 BEAST_EXPECT(vault != nullptr);
3052 return {vault->at(sfAccount), vault->at(sfShareMPTID)};
3053 }();
3054 BEAST_EXPECT(env.le(keylet::account(vaultAccount)));
3055 BEAST_EXPECT(env.le(keylet::mptokenIssuance(issuanceId)));
3056 PrettyAsset const shares{issuanceId};
3057
3058 {
3059 testcase("private XRP vault owner can deposit");
3060 auto tx = vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(50)});
3061 env(tx);
3062 env.close();
3063 }
3064
3065 {
3066 testcase("private XRP vault cannot pay shares to depositor yet");
3067 env(pay(owner, depositor, shares(1)), Ter{tecNO_AUTH});
3068 }
3069
3070 {
3071 testcase("private XRP vault depositor not authorized yet");
3072 auto tx =
3073 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
3074 env(tx, Ter{tecNO_AUTH});
3075 }
3076
3077 {
3078 testcase("private XRP vault set DomainID");
3079 pdomain::Credentials const credentials{{.issuer = owner, .credType = credType}};
3080
3081 env(pdomain::setTx(owner, credentials));
3082 auto const domainId = [&]() {
3083 auto tx = env.tx()->getJson(JsonOptions::Values::None);
3084 return pdomain::getNewDomain(env.meta());
3085 }();
3086
3087 auto tx = vault.set({.owner = owner, .id = keylet.key});
3088 tx[sfDomainID] = to_string(domainId);
3089 env(tx);
3090 env.close();
3091 }
3092
3093 auto const credKeylet = credentials::keylet(depositor, owner, credType);
3094 {
3095 testcase("private XRP vault depositor now authorized");
3096 env(credentials::create(depositor, owner, credType));
3097 env(credentials::accept(depositor, owner, credType));
3098 env.close();
3099
3100 BEAST_EXPECT(env.le(credKeylet));
3101 auto tx =
3102 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
3103 env(tx);
3104 env.close();
3105 }
3106
3107 {
3108 testcase("private XRP vault can pay shares to depositor");
3109 env(pay(owner, depositor, shares(1)));
3110 }
3111
3112 {
3113 testcase("private XRP vault cannot pay shares to 3rd party");
3114 json::Value jv;
3115 jv[sfAccount] = alice.human();
3116 jv[sfTransactionType] = jss::MPTokenAuthorize;
3117 jv[sfMPTokenIssuanceID] = to_string(issuanceId);
3118 env(jv);
3119 env.close();
3120
3121 env(pay(owner, alice, shares(1)), Ter{tecNO_AUTH});
3122 }
3123 }
3124
3125 void
3127 {
3128 using namespace test::jtx;
3129
3130 testcase("fail pseudo-account allocation");
3131 Env env{*this, testableAmendments()};
3132 Account const owner{"owner"};
3133 Vault const vault{env};
3134 env.fund(XRP(1000), owner);
3135
3136 auto const keylet = keylet::vault(owner.id(), env.seq(owner));
3137 for (int i = 0; i < 256; ++i)
3138 {
3139 AccountID const accountId = xrpl::pseudoAccountAddress(*env.current(), keylet.key);
3140
3141 env(pay(env.master.id(), accountId, XRP(1000)),
3142 Seq(kAutofill),
3143 Fee(kAutofill),
3144 Sig(kAutofill));
3145 }
3146
3147 auto [tx, keylet1] = vault.create({.owner = owner, .asset = xrpIssue()});
3148 BEAST_EXPECT(keylet.key == keylet1.key);
3149 env(tx, Ter{terADDRESS_COLLISION});
3150 }
3151
3152 void
3154 {
3155 using namespace test::jtx;
3156
3157 struct Data
3158 {
3159 Account const& owner;
3160 Account const& issuer;
3161 Account const& depositor;
3162 Account const& vaultAccount;
3163 MPTIssue shares;
3164 PrettyAsset const& share;
3165 Vault& vault;
3167 Issue assets;
3168 PrettyAsset const& asset;
3169 std::function<bool(std::function<bool(SLE&, SLE&)>)> peek;
3170 };
3171
3172 auto testCase = [&, this](
3173 std::uint8_t scale, std::function<void(Env & env, Data data)> test) {
3174 Env env{*this, testableAmendments()};
3175 Account const owner{"owner"};
3176 Account const issuer{"issuer"};
3177 Account const depositor{"depositor"};
3178 Vault vault{env};
3179 env.fund(XRP(1000), issuer, owner, depositor);
3180 env(fset(issuer, asfAllowTrustLineClawback));
3181 env.close();
3182
3183 PrettyAsset const asset = issuer["IOU"];
3184 env.trust(asset(1000), owner);
3185 env.trust(asset(1000), depositor);
3186 env(pay(issuer, owner, asset(200)));
3187 env(pay(issuer, depositor, asset(200)));
3188 env.close();
3189
3190 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
3191 tx[sfScale] = scale;
3192 env(tx);
3193
3194 auto const [vaultAccount, issuanceId] =
3196 auto const vault = env.le(keylet);
3197 return {Account("vault", vault->at(sfAccount)), vault->at(sfShareMPTID)};
3198 }(keylet);
3199 MPTIssue const shares(issuanceId);
3200 env.memoize(vaultAccount);
3201
3202 auto const peek = [keylet, &env, this](std::function<bool(SLE&, SLE&)> fn) -> bool {
3203 return env.app().getOpenLedger().modify(
3204 [&](OpenView& view, beast::Journal j) -> bool {
3205 Sandbox sb(&view, TapNone);
3206 auto vault = sb.peek(keylet::vault(keylet.key));
3207 if (!BEAST_EXPECT(vault))
3208 return false;
3209 auto shares = sb.peek(keylet::mptokenIssuance(vault->at(sfShareMPTID)));
3210 if (!BEAST_EXPECT(shares))
3211 return false;
3212 if (fn(*vault, *shares))
3213 {
3214 sb.update(vault);
3215 sb.update(shares);
3216 sb.apply(view);
3217 return true;
3218 }
3219 return false;
3220 });
3221 };
3222
3223 test(
3224 env,
3225 {.owner = owner,
3226 .issuer = issuer,
3227 .depositor = depositor,
3228 .vaultAccount = vaultAccount,
3229 .shares = shares,
3230 .share = PrettyAsset(shares),
3231 .vault = vault,
3232 .keylet = keylet,
3233 .assets = asset.raw().get<Issue>(),
3234 .asset = asset,
3235 .peek = peek});
3236 };
3237
3238 testCase(18, [&, this](Env& env, Data d) {
3239 testcase("Scale deposit overflow on first deposit");
3240 auto tx = d.vault.deposit(
3241 {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(10)});
3242 env(tx, Ter{tecPATH_DRY});
3243 env.close();
3244 });
3245
3246 testCase(18, [&, this](Env& env, Data d) {
3247 testcase("Scale deposit overflow on second deposit");
3248
3249 {
3250 auto tx = d.vault.deposit(
3251 {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(5)});
3252 env(tx);
3253 env.close();
3254 }
3255
3256 {
3257 auto tx = d.vault.deposit(
3258 {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(10)});
3259 env(tx, Ter{tecPATH_DRY});
3260 env.close();
3261 }
3262 });
3263
3264 testCase(18, [&, this](Env& env, Data d) {
3265 testcase("Scale deposit overflow on total shares");
3266
3267 {
3268 auto tx = d.vault.deposit(
3269 {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(5)});
3270 env(tx);
3271 env.close();
3272 }
3273
3274 {
3275 auto tx = d.vault.deposit(
3276 {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(5)});
3277 env(tx, Ter{tecPATH_DRY});
3278 env.close();
3279 }
3280 });
3281
3282 testCase(1, [&, this](Env& env, Data d) {
3283 testcase("Scale deposit exact");
3284
3285 auto const start = env.balance(d.depositor, d.assets).number();
3286 auto tx = d.vault.deposit(
3287 {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(1)});
3288 env(tx);
3289 env.close();
3290 BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(10));
3291 BEAST_EXPECT(env.balance(d.depositor, d.assets) == STAmount(d.asset, start - 1));
3292 });
3293
3294 testCase(1, [&, this](Env& env, Data d) {
3295 testcase("Scale deposit insignificant amount");
3296
3297 auto tx = d.vault.deposit(
3298 {.depositor = d.depositor,
3299 .id = d.keylet.key,
3300 .amount = STAmount(d.asset, Number(9, -2))});
3301 env(tx, Ter{tecPRECISION_LOSS});
3302 });
3303
3304 testCase(1, [&, this](Env& env, Data d) {
3305 testcase("Scale deposit exact, using full precision");
3306
3307 auto const start = env.balance(d.depositor, d.assets).number();
3308 auto tx = d.vault.deposit(
3309 {.depositor = d.depositor,
3310 .id = d.keylet.key,
3311 .amount = STAmount(d.asset, Number(15, -1))});
3312 env(tx);
3313 env.close();
3314 BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(15));
3315 BEAST_EXPECT(
3316 env.balance(d.depositor, d.assets) == STAmount(d.asset, start - Number(15, -1)));
3317 });
3318
3319 testCase(1, [&, this](Env& env, Data d) {
3320 testcase("Scale deposit exact, truncating from .5");
3321
3322 auto const start = env.balance(d.depositor, d.assets).number();
3323 // Each of the cases below will transfer exactly 1.2 IOU to the
3324 // vault and receive 12 shares in exchange
3325 {
3326 auto tx = d.vault.deposit(
3327 {.depositor = d.depositor,
3328 .id = d.keylet.key,
3329 .amount = STAmount(d.asset, Number(125, -2))});
3330 env(tx);
3331 env.close();
3332 BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(12));
3333 BEAST_EXPECT(
3334 env.balance(d.depositor, d.assets) ==
3335 STAmount(d.asset, start - Number(12, -1)));
3336 }
3337
3338 {
3339 auto tx = d.vault.deposit(
3340 {.depositor = d.depositor,
3341 .id = d.keylet.key,
3342 .amount = STAmount(d.asset, Number(1201, -3))});
3343 env(tx);
3344 env.close();
3345 BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(24));
3346 BEAST_EXPECT(
3347 env.balance(d.depositor, d.assets) ==
3348 STAmount(d.asset, start - Number(24, -1)));
3349 }
3350
3351 {
3352 auto tx = d.vault.deposit(
3353 {.depositor = d.depositor,
3354 .id = d.keylet.key,
3355 .amount = STAmount(d.asset, Number(1299, -3))});
3356 env(tx);
3357 env.close();
3358 BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(36));
3359 BEAST_EXPECT(
3360 env.balance(d.depositor, d.assets) ==
3361 STAmount(d.asset, start - Number(36, -1)));
3362 }
3363 });
3364
3365 testCase(1, [&, this](Env& env, Data d) {
3366 testcase("Scale deposit exact, truncating from .01");
3367
3368 auto const start = env.balance(d.depositor, d.assets).number();
3369 // round to 12
3370 auto tx = d.vault.deposit(
3371 {.depositor = d.depositor,
3372 .id = d.keylet.key,
3373 .amount = STAmount(d.asset, Number(1201, -3))});
3374 env(tx);
3375 env.close();
3376 BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(12));
3377 BEAST_EXPECT(
3378 env.balance(d.depositor, d.assets) == STAmount(d.asset, start - Number(12, -1)));
3379
3380 {
3381 // round to 6
3382 auto tx = d.vault.deposit(
3383 {.depositor = d.depositor,
3384 .id = d.keylet.key,
3385 .amount = STAmount(d.asset, Number(69, -2))});
3386 env(tx);
3387 env.close();
3388 BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(18));
3389 BEAST_EXPECT(
3390 env.balance(d.depositor, d.assets) ==
3391 STAmount(d.asset, start - Number(18, -1)));
3392 }
3393 });
3394
3395 testCase(1, [&, this](Env& env, Data d) {
3396 testcase("Scale deposit exact, truncating from .99");
3397
3398 auto const start = env.balance(d.depositor, d.assets).number();
3399 // round to 12
3400 auto tx = d.vault.deposit(
3401 {.depositor = d.depositor,
3402 .id = d.keylet.key,
3403 .amount = STAmount(d.asset, Number(1299, -3))});
3404 env(tx);
3405 env.close();
3406 BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(12));
3407 BEAST_EXPECT(
3408 env.balance(d.depositor, d.assets) == STAmount(d.asset, start - Number(12, -1)));
3409
3410 {
3411 // round to 6
3412 auto tx = d.vault.deposit(
3413 {.depositor = d.depositor,
3414 .id = d.keylet.key,
3415 .amount = STAmount(d.asset, Number(62, -2))});
3416 env(tx);
3417 env.close();
3418 BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(18));
3419 BEAST_EXPECT(
3420 env.balance(d.depositor, d.assets) ==
3421 STAmount(d.asset, start - Number(18, -1)));
3422 }
3423 });
3424
3425 testCase(1, [&, this](Env& env, Data d) {
3426 // initial setup: deposit 100 IOU, receive 1000 shares
3427 auto const start = env.balance(d.depositor, d.assets).number();
3428 auto tx = d.vault.deposit(
3429 {.depositor = d.depositor,
3430 .id = d.keylet.key,
3431 .amount = STAmount(d.asset, Number(100, 0))});
3432 env(tx);
3433 env.close();
3434 BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(1000));
3435 BEAST_EXPECT(
3436 env.balance(d.depositor, d.assets) == STAmount(d.asset, start - Number(100, 0)));
3437 BEAST_EXPECT(
3438 env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(100, 0)));
3439 BEAST_EXPECT(
3440 env.balance(d.vaultAccount, d.shares) == STAmount(d.share, Number(-1000, 0)));
3441
3442 {
3443 testcase("Scale redeem exact");
3444 // sharesToAssetsWithdraw:
3445 // assets = assetsTotal * (shares / sharesTotal)
3446 // assets = 100 * 100 / 1000 = 100 * 0.1 = 10
3447
3448 auto const start = env.balance(d.depositor, d.assets).number();
3449 auto tx = d.vault.withdraw(
3450 {.depositor = d.depositor,
3451 .id = d.keylet.key,
3452 .amount = STAmount(d.share, Number(100, 0))});
3453 env(tx);
3454 env.close();
3455 BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(900));
3456 BEAST_EXPECT(
3457 env.balance(d.depositor, d.assets) == STAmount(d.asset, start + Number(10, 0)));
3458 BEAST_EXPECT(
3459 env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(90, 0)));
3460 BEAST_EXPECT(
3461 env.balance(d.vaultAccount, d.shares) == STAmount(d.share, Number(-900, 0)));
3462 }
3463
3464 {
3465 testcase("Scale redeem with rounding");
3466 // sharesToAssetsWithdraw:
3467 // assets = assetsTotal * (shares / sharesTotal)
3468 // assets = 90 * 25 / 900 = 90 * 0.02777... = 2.5
3469
3470 auto const start = env.balance(d.depositor, d.assets).number();
3471 d.peek([](SLE& vault, auto&) -> bool {
3472 vault[sfAssetsAvailable] = Number(1);
3473 return true;
3474 });
3475
3476 // Note, this transaction fails first (because of above change
3477 // in the open ledger) but then succeeds when the ledger is
3478 // closed (because a modification like above is not persistent),
3479 // which is why the checks below are expected to pass.
3480 auto tx = d.vault.withdraw(
3481 {.depositor = d.depositor,
3482 .id = d.keylet.key,
3483 .amount = STAmount(d.share, Number(25, 0))});
3484 env(tx, Ter{tecINSUFFICIENT_FUNDS});
3485 env.close();
3486 BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(900 - 25));
3487 BEAST_EXPECT(
3488 env.balance(d.depositor, d.assets) ==
3489 STAmount(d.asset, start + Number(25, -1)));
3490 BEAST_EXPECT(
3491 env.balance(d.vaultAccount, d.assets) ==
3492 STAmount(d.asset, Number(900 - 25, -1)));
3493 BEAST_EXPECT(
3494 env.balance(d.vaultAccount, d.shares) ==
3495 STAmount(d.share, -Number(900 - 25, 0)));
3496 }
3497
3498 {
3499 testcase("Scale redeem exact");
3500 // sharesToAssetsWithdraw:
3501 // assets = assetsTotal * (shares / sharesTotal)
3502 // assets = 87.5 * 21 / 875 = 87.5 * 0.024 = 2.1
3503
3504 auto const start = env.balance(d.depositor, d.assets).number();
3505
3506 tx = d.vault.withdraw(
3507 {.depositor = d.depositor,
3508 .id = d.keylet.key,
3509 .amount = STAmount(d.share, Number(21, 0))});
3510 env(tx);
3511 env.close();
3512 BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(875 - 21));
3513 BEAST_EXPECT(
3514 env.balance(d.depositor, d.assets) ==
3515 STAmount(d.asset, start + Number(21, -1)));
3516 BEAST_EXPECT(
3517 env.balance(d.vaultAccount, d.assets) ==
3518 STAmount(d.asset, Number(875 - 21, -1)));
3519 BEAST_EXPECT(
3520 env.balance(d.vaultAccount, d.shares) ==
3521 STAmount(d.share, -Number(875 - 21, 0)));
3522 }
3523
3524 {
3525 testcase("Scale redeem rest");
3526 auto const rest = env.balance(d.depositor, d.shares).number();
3527
3528 tx = d.vault.withdraw(
3529 {.depositor = d.depositor,
3530 .id = d.keylet.key,
3531 .amount = STAmount(d.share, rest)});
3532 env(tx);
3533 env.close();
3534 BEAST_EXPECT(env.balance(d.depositor, d.shares).number() == 0);
3535 BEAST_EXPECT(env.balance(d.vaultAccount, d.assets).number() == 0);
3536 BEAST_EXPECT(env.balance(d.vaultAccount, d.shares).number() == 0);
3537 }
3538 });
3539
3540 testCase(18, [&, this](Env& env, Data d) {
3541 testcase("Scale withdraw overflow");
3542
3543 {
3544 auto tx = d.vault.deposit(
3545 {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(5)});
3546 env(tx);
3547 env.close();
3548 }
3549
3550 {
3551 auto tx = d.vault.withdraw(
3552 {.depositor = d.depositor,
3553 .id = d.keylet.key,
3554 .amount = STAmount(d.asset, Number(10, 0))});
3555 env(tx, Ter{tecPATH_DRY});
3556 env.close();
3557 }
3558 });
3559
3560 testCase(1, [&, this](Env& env, Data d) {
3561 // initial setup: deposit 100 IOU, receive 1000 shares
3562 auto const start = env.balance(d.depositor, d.assets).number();
3563 auto tx = d.vault.deposit(
3564 {.depositor = d.depositor,
3565 .id = d.keylet.key,
3566 .amount = STAmount(d.asset, Number(100, 0))});
3567 env(tx);
3568 env.close();
3569 BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(1000));
3570 BEAST_EXPECT(
3571 env.balance(d.depositor, d.assets) == STAmount(d.asset, start - Number(100, 0)));
3572 BEAST_EXPECT(
3573 env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(100, 0)));
3574 BEAST_EXPECT(
3575 env.balance(d.vaultAccount, d.shares) == STAmount(d.share, Number(-1000, 0)));
3576
3577 {
3578 testcase("Scale withdraw exact");
3579 // assetsToSharesWithdraw:
3580 // shares = sharesTotal * (assets / assetsTotal)
3581 // shares = 1000 * 10 / 100 = 1000 * 0.1 = 100
3582 // sharesToAssetsWithdraw:
3583 // assets = assetsTotal * (shares / sharesTotal)
3584 // assets = 100 * 100 / 1000 = 100 * 0.1 = 10
3585
3586 auto const start = env.balance(d.depositor, d.assets).number();
3587 auto tx = d.vault.withdraw(
3588 {.depositor = d.depositor,
3589 .id = d.keylet.key,
3590 .amount = STAmount(d.asset, Number(10, 0))});
3591 env(tx);
3592 env.close();
3593 BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(900));
3594 BEAST_EXPECT(
3595 env.balance(d.depositor, d.assets) == STAmount(d.asset, start + Number(10, 0)));
3596 BEAST_EXPECT(
3597 env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(90, 0)));
3598 BEAST_EXPECT(
3599 env.balance(d.vaultAccount, d.shares) == STAmount(d.share, Number(-900, 0)));
3600 }
3601
3602 {
3603 testcase("Scale withdraw insignificant amount");
3604 auto tx = d.vault.withdraw(
3605 {.depositor = d.depositor,
3606 .id = d.keylet.key,
3607 .amount = STAmount(d.asset, Number(4, -2))});
3608 env(tx, Ter{tecPRECISION_LOSS});
3609 }
3610
3611 {
3612 testcase("Scale withdraw with rounding assets");
3613 // assetsToSharesWithdraw:
3614 // shares = sharesTotal * (assets / assetsTotal)
3615 // shares = 900 * 2.5 / 90 = 900 * 0.02777... = 25
3616 // sharesToAssetsWithdraw:
3617 // assets = assetsTotal * (shares / sharesTotal)
3618 // assets = 90 * 25 / 900 = 90 * 0.02777... = 2.5
3619
3620 auto const start = env.balance(d.depositor, d.assets).number();
3621 d.peek([](SLE& vault, auto&) -> bool {
3622 vault[sfAssetsAvailable] = Number(1);
3623 return true;
3624 });
3625
3626 // Note, this transaction fails first (because of above change
3627 // in the open ledger) but then succeeds when the ledger is
3628 // closed (because a modification like above is not persistent),
3629 // which is why the checks below are expected to pass.
3630 auto tx = d.vault.withdraw(
3631 {.depositor = d.depositor,
3632 .id = d.keylet.key,
3633 .amount = STAmount(d.asset, Number(25, -1))});
3634 env(tx, Ter{tecINSUFFICIENT_FUNDS});
3635 env.close();
3636 BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(900 - 25));
3637 BEAST_EXPECT(
3638 env.balance(d.depositor, d.assets) ==
3639 STAmount(d.asset, start + Number(25, -1)));
3640 BEAST_EXPECT(
3641 env.balance(d.vaultAccount, d.assets) ==
3642 STAmount(d.asset, Number(900 - 25, -1)));
3643 BEAST_EXPECT(
3644 env.balance(d.vaultAccount, d.shares) ==
3645 STAmount(d.share, -Number(900 - 25, 0)));
3646 }
3647
3648 {
3649 testcase("Scale withdraw with rounding shares up");
3650 // assetsToSharesWithdraw:
3651 // shares = sharesTotal * (assets / assetsTotal)
3652 // shares = 875 * 3.75 / 87.5 = 875 * 0.042857... = 37.5
3653 // sharesToAssetsWithdraw:
3654 // assets = assetsTotal * (shares / sharesTotal)
3655 // assets = 87.5 * 38 / 875 = 87.5 * 0.043428... = 3.8
3656
3657 auto const start = env.balance(d.depositor, d.assets).number();
3658 auto tx = d.vault.withdraw(
3659 {.depositor = d.depositor,
3660 .id = d.keylet.key,
3661 .amount = STAmount(d.asset, Number(375, -2))});
3662 env(tx);
3663 env.close();
3664 BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(875 - 38));
3665 BEAST_EXPECT(
3666 env.balance(d.depositor, d.assets) ==
3667 STAmount(d.asset, start + Number(38, -1)));
3668 BEAST_EXPECT(
3669 env.balance(d.vaultAccount, d.assets) ==
3670 STAmount(d.asset, Number(875 - 38, -1)));
3671 BEAST_EXPECT(
3672 env.balance(d.vaultAccount, d.shares) ==
3673 STAmount(d.share, -Number(875 - 38, 0)));
3674 }
3675
3676 {
3677 testcase("Scale withdraw with rounding shares down");
3678 // assetsToSharesWithdraw:
3679 // shares = sharesTotal * (assets / assetsTotal)
3680 // shares = 837 * 3.72 / 83.7 = 837 * 0.04444... = 37.2
3681 // sharesToAssetsWithdraw:
3682 // assets = assetsTotal * (shares / sharesTotal)
3683 // assets = 83.7 * 37 / 837 = 83.7 * 0.044205... = 3.7
3684
3685 auto const start = env.balance(d.depositor, d.assets).number();
3686 auto tx = d.vault.withdraw(
3687 {.depositor = d.depositor,
3688 .id = d.keylet.key,
3689 .amount = STAmount(d.asset, Number(372, -2))});
3690 env(tx);
3691 env.close();
3692 BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(837 - 37));
3693 BEAST_EXPECT(
3694 env.balance(d.depositor, d.assets) ==
3695 STAmount(d.asset, start + Number(37, -1)));
3696 BEAST_EXPECT(
3697 env.balance(d.vaultAccount, d.assets) ==
3698 STAmount(d.asset, Number(837 - 37, -1)));
3699 BEAST_EXPECT(
3700 env.balance(d.vaultAccount, d.shares) ==
3701 STAmount(d.share, -Number(837 - 37, 0)));
3702 }
3703
3704 {
3705 testcase("Scale withdraw tiny amount");
3706
3707 auto const start = env.balance(d.depositor, d.assets).number();
3708 auto tx = d.vault.withdraw(
3709 {.depositor = d.depositor,
3710 .id = d.keylet.key,
3711 .amount = STAmount(d.asset, Number(9, -2))});
3712 env(tx);
3713 env.close();
3714 BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(800 - 1));
3715 BEAST_EXPECT(
3716 env.balance(d.depositor, d.assets) == STAmount(d.asset, start + Number(1, -1)));
3717 BEAST_EXPECT(
3718 env.balance(d.vaultAccount, d.assets) ==
3719 STAmount(d.asset, Number(800 - 1, -1)));
3720 BEAST_EXPECT(
3721 env.balance(d.vaultAccount, d.shares) ==
3722 STAmount(d.share, -Number(800 - 1, 0)));
3723 }
3724
3725 {
3726 testcase("Scale withdraw rest");
3727 auto const rest = env.balance(d.vaultAccount, d.assets).number();
3728
3729 tx = d.vault.withdraw(
3730 {.depositor = d.depositor,
3731 .id = d.keylet.key,
3732 .amount = STAmount(d.asset, rest)});
3733 env(tx);
3734 env.close();
3735 BEAST_EXPECT(env.balance(d.depositor, d.shares).number() == 0);
3736 BEAST_EXPECT(env.balance(d.vaultAccount, d.assets).number() == 0);
3737 BEAST_EXPECT(env.balance(d.vaultAccount, d.shares).number() == 0);
3738 }
3739 });
3740
3741 testCase(18, [&, this](Env& env, Data d) {
3742 testcase("Scale clawback overflow");
3743
3744 {
3745 auto tx = d.vault.deposit(
3746 {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(5)});
3747 env(tx);
3748 env.close();
3749 }
3750
3751 {
3752 auto tx = d.vault.clawback(
3753 {.issuer = d.issuer,
3754 .id = d.keylet.key,
3755 .holder = d.depositor,
3756 .amount = STAmount(d.asset, Number(10, 0))});
3757 env(tx, Ter{tecPATH_DRY});
3758 env.close();
3759 }
3760 });
3761
3762 testCase(1, [&, this](Env& env, Data d) {
3763 // initial setup: deposit 100 IOU, receive 1000 shares
3764 auto const start = env.balance(d.depositor, d.assets).number();
3765 auto tx = d.vault.deposit(
3766 {.depositor = d.depositor,
3767 .id = d.keylet.key,
3768 .amount = STAmount(d.asset, Number(100, 0))});
3769 env(tx);
3770 env.close();
3771 BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(1000));
3772 BEAST_EXPECT(
3773 env.balance(d.depositor, d.assets) == STAmount(d.asset, start - Number(100, 0)));
3774 BEAST_EXPECT(
3775 env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(100, 0)));
3776 BEAST_EXPECT(
3777 env.balance(d.vaultAccount, d.shares) == STAmount(d.share, -Number(1000, 0)));
3778 {
3779 testcase("Scale clawback exact");
3780 // assetsToSharesWithdraw:
3781 // shares = sharesTotal * (assets / assetsTotal)
3782 // shares = 1000 * 10 / 100 = 1000 * 0.1 = 100
3783 // sharesToAssetsWithdraw:
3784 // assets = assetsTotal * (shares / sharesTotal)
3785 // assets = 100 * 100 / 1000 = 100 * 0.1 = 10
3786
3787 auto const start = env.balance(d.depositor, d.assets).number();
3788 auto tx = d.vault.clawback(
3789 {.issuer = d.issuer,
3790 .id = d.keylet.key,
3791 .holder = d.depositor,
3792 .amount = STAmount(d.asset, Number(10, 0))});
3793 env(tx);
3794 env.close();
3795 BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(900));
3796 BEAST_EXPECT(env.balance(d.depositor, d.assets) == STAmount(d.asset, start));
3797 BEAST_EXPECT(
3798 env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(90, 0)));
3799 BEAST_EXPECT(
3800 env.balance(d.vaultAccount, d.shares) == STAmount(d.share, -Number(900, 0)));
3801 }
3802
3803 {
3804 testcase("Scale clawback insignificant amount");
3805 auto tx = d.vault.clawback(
3806 {.issuer = d.issuer,
3807 .id = d.keylet.key,
3808 .holder = d.depositor,
3809 .amount = STAmount(d.asset, Number(4, -2))});
3810 env(tx, Ter{tecPRECISION_LOSS});
3811 }
3812
3813 {
3814 testcase("Scale clawback with rounding assets");
3815 // assetsToSharesWithdraw:
3816 // shares = sharesTotal * (assets / assetsTotal)
3817 // shares = 900 * 2.5 / 90 = 900 * 0.02777... = 25
3818 // sharesToAssetsWithdraw:
3819 // assets = assetsTotal * (shares / sharesTotal)
3820 // assets = 90 * 25 / 900 = 90 * 0.02777... = 2.5
3821
3822 auto const start = env.balance(d.depositor, d.assets).number();
3823 auto tx = d.vault.clawback(
3824 {.issuer = d.issuer,
3825 .id = d.keylet.key,
3826 .holder = d.depositor,
3827 .amount = STAmount(d.asset, Number(25, -1))});
3828 env(tx);
3829 env.close();
3830 BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(900 - 25));
3831 BEAST_EXPECT(env.balance(d.depositor, d.assets) == STAmount(d.asset, start));
3832 BEAST_EXPECT(
3833 env.balance(d.vaultAccount, d.assets) ==
3834 STAmount(d.asset, Number(900 - 25, -1)));
3835 BEAST_EXPECT(
3836 env.balance(d.vaultAccount, d.shares) ==
3837 STAmount(d.share, -Number(900 - 25, 0)));
3838 }
3839
3840 {
3841 testcase("Scale clawback with rounding shares up");
3842 // assetsToSharesWithdraw:
3843 // shares = sharesTotal * (assets / assetsTotal)
3844 // shares = 875 * 3.75 / 87.5 = 875 * 0.042857... = 37.5
3845 // sharesToAssetsWithdraw:
3846 // assets = assetsTotal * (shares / sharesTotal)
3847 // assets = 87.5 * 38 / 875 = 87.5 * 0.043428... = 3.8
3848
3849 auto const start = env.balance(d.depositor, d.assets).number();
3850 auto tx = d.vault.clawback(
3851 {.issuer = d.issuer,
3852 .id = d.keylet.key,
3853 .holder = d.depositor,
3854 .amount = STAmount(d.asset, Number(375, -2))});
3855 env(tx);
3856 env.close();
3857 BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(875 - 38));
3858 BEAST_EXPECT(env.balance(d.depositor, d.assets) == STAmount(d.asset, start));
3859 BEAST_EXPECT(
3860 env.balance(d.vaultAccount, d.assets) ==
3861 STAmount(d.asset, Number(875 - 38, -1)));
3862 BEAST_EXPECT(
3863 env.balance(d.vaultAccount, d.shares) ==
3864 STAmount(d.share, -Number(875 - 38, 0)));
3865 }
3866
3867 {
3868 testcase("Scale clawback with rounding shares down");
3869 // assetsToSharesWithdraw:
3870 // shares = sharesTotal * (assets / assetsTotal)
3871 // shares = 837 * 3.72 / 83.7 = 837 * 0.04444... = 37.2
3872 // sharesToAssetsWithdraw:
3873 // assets = assetsTotal * (shares / sharesTotal)
3874 // assets = 83.7 * 37 / 837 = 83.7 * 0.044205... = 3.7
3875
3876 auto const start = env.balance(d.depositor, d.assets).number();
3877 auto tx = d.vault.clawback(
3878 {.issuer = d.issuer,
3879 .id = d.keylet.key,
3880 .holder = d.depositor,
3881 .amount = STAmount(d.asset, Number(372, -2))});
3882 env(tx);
3883 env.close();
3884 BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(837 - 37));
3885 BEAST_EXPECT(env.balance(d.depositor, d.assets) == STAmount(d.asset, start));
3886 BEAST_EXPECT(
3887 env.balance(d.vaultAccount, d.assets) ==
3888 STAmount(d.asset, Number(837 - 37, -1)));
3889 BEAST_EXPECT(
3890 env.balance(d.vaultAccount, d.shares) ==
3891 STAmount(d.share, -Number(837 - 37, 0)));
3892 }
3893
3894 {
3895 testcase("Scale clawback tiny amount");
3896
3897 auto const start = env.balance(d.depositor, d.assets).number();
3898 auto tx = d.vault.clawback(
3899 {.issuer = d.issuer,
3900 .id = d.keylet.key,
3901 .holder = d.depositor,
3902 .amount = STAmount(d.asset, Number(9, -2))});
3903 env(tx);
3904 env.close();
3905 BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(800 - 1));
3906 BEAST_EXPECT(env.balance(d.depositor, d.assets) == STAmount(d.asset, start));
3907 BEAST_EXPECT(
3908 env.balance(d.vaultAccount, d.assets) ==
3909 STAmount(d.asset, Number(800 - 1, -1)));
3910 BEAST_EXPECT(
3911 env.balance(d.vaultAccount, d.shares) ==
3912 STAmount(d.share, -Number(800 - 1, 0)));
3913 }
3914
3915 {
3916 testcase("Scale clawback rest");
3917 auto const rest = env.balance(d.vaultAccount, d.assets).number();
3918 d.peek([](SLE& vault, auto&) -> bool {
3919 vault[sfAssetsAvailable] = Number(5);
3920 return true;
3921 });
3922
3923 // Note, this transaction yields two different results:
3924 // * in the open ledger, with AssetsAvailable = 5
3925 // * when the ledger is closed with unmodified AssetsAvailable
3926 // because a modification like above is not persistent.
3927 tx = d.vault.clawback(
3928 {.issuer = d.issuer,
3929 .id = d.keylet.key,
3930 .holder = d.depositor,
3931 .amount = STAmount(d.asset, rest)});
3932 env(tx);
3933 env.close();
3934 BEAST_EXPECT(env.balance(d.depositor, d.shares).number() == 0);
3935 BEAST_EXPECT(env.balance(d.vaultAccount, d.assets).number() == 0);
3936 BEAST_EXPECT(env.balance(d.vaultAccount, d.shares).number() == 0);
3937 }
3938 });
3939
3940 // Non-1:1 ratio (scale=1, 10:1 shares:assets) with an outstanding loan.
3941 // Deposit 100 IOU → 1000 shares. Borrow 40 → assetsAvailable=60.
3942 // Clawback 80 IOU → clamped to 60, then share math uses truncation.
3943 testCase(1, [&, this](Env& env, Data d) {
3944 using namespace loanBroker;
3945 using namespace loan;
3946
3947 testcase("Scale clawback clamped with outstanding loan");
3948
3949 auto tx = d.vault.deposit(
3950 {.depositor = d.depositor,
3951 .id = d.keylet.key,
3952 .amount = STAmount(d.asset, Number(100, 0))});
3953 env(tx);
3954 env.close();
3955 BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(1000));
3956
3957 // Create a loan broker backed by this vault
3958 auto const brokerKeylet = keylet::loanBroker(d.owner.id(), env.seq(d.owner));
3959 env(set(d.owner, d.keylet.key));
3960 env.close();
3961
3962 // Borrow 40: assetsAvailable=60, assetsTotal=100
3963 env(set(d.depositor, brokerKeylet.key, STAmount(d.asset, Number(40, 0))),
3964 loan::kInterestRate(TenthBips32(0)),
3965 kGracePeriod(60),
3966 kPaymentInterval(120),
3967 kPaymentTotal(10),
3968 Sig(sfCounterpartySignature, d.owner),
3969 Fee(env.current()->fees().base * 2),
3970 Ter(tesSUCCESS));
3971 env.close();
3972
3973 {
3974 auto const sle = env.le(d.keylet);
3975 BEAST_EXPECT(sle->at(sfAssetsAvailable) == STAmount(d.asset, Number(60, 0)));
3976 BEAST_EXPECT(sle->at(sfAssetsTotal) == STAmount(d.asset, Number(100, 0)));
3977 }
3978
3979 // Request 80 IOU clawback — clamped to assetsAvailable (60)
3980 // With scale=1 (10:1), 60 assets = 600 shares destroyed
3981 tx = d.vault.clawback(
3982 {.issuer = d.issuer,
3983 .id = d.keylet.key,
3984 .holder = d.depositor,
3985 .amount = STAmount(d.asset, Number(80, 0))});
3986 env(tx, Ter(tesSUCCESS));
3987 env.close();
3988
3989 {
3990 auto const sle = env.le(d.keylet);
3991 BEAST_EXPECT(sle != nullptr);
3992 BEAST_EXPECT(sle->at(sfAssetsAvailable) == STAmount(d.asset, Number(0, 0)));
3993 BEAST_EXPECT(sle->at(sfAssetsTotal) == STAmount(d.asset, Number(40, 0)));
3994
3995 // 600 of 1000 shares destroyed, 400 remain
3996 BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(400));
3997 }
3998 });
3999 }
4000
4001 void
4003 {
4004 using namespace test::jtx;
4005
4006 testcase("RPC");
4007 Env env{*this, testableAmendments()};
4008 Account const owner{"owner"};
4009 Account const issuer{"issuer"};
4010 Vault const vault{env};
4011 env.fund(XRP(1000), issuer, owner);
4012 env.close();
4013
4014 PrettyAsset const asset = issuer["IOU"];
4015 env.trust(asset(1000), owner);
4016 env(pay(issuer, owner, asset(200)));
4017 env.close();
4018
4019 auto const sequence = env.seq(owner);
4020 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
4021 env(tx);
4022 env.close();
4023
4024 // Set some fields
4025 {
4026 auto tx1 = vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(50)});
4027 env(tx1);
4028
4029 auto tx2 = vault.set({.owner = owner, .id = keylet.key});
4030 tx2[sfAssetsMaximum] = asset(1000).number();
4031 env(tx2);
4032 env.close();
4033 }
4034
4035 auto const sleVault = [&env, keylet = keylet, this]() {
4036 auto const vault = env.le(keylet);
4037 BEAST_EXPECT(vault != nullptr);
4038 return vault;
4039 }();
4040
4041 auto const check = [&, keylet = keylet, sle = sleVault, this](
4042 json::Value const& vault,
4043 json::Value const& issuance = json::ValueType::Null) {
4044 BEAST_EXPECT(vault.isObject());
4045
4046 static constexpr auto kCheckString =
4047 [](auto& node, SField const& field, std::string v) -> bool {
4048 return node.isMember(field.fieldName) && node[field.fieldName].isString() &&
4049 node[field.fieldName] == v;
4050 };
4051 static constexpr auto kCheckObject =
4052 [](auto& node, SField const& field, json::Value v) -> bool {
4053 return node.isMember(field.fieldName) && node[field.fieldName].isObject() &&
4054 node[field.fieldName] == v;
4055 };
4056 static constexpr auto kCheckInt = [](auto& node, SField const& field, int v) -> bool {
4057 return node.isMember(field.fieldName) &&
4058 ((node[field.fieldName].isInt() && node[field.fieldName] == json::Int(v)) ||
4059 (node[field.fieldName].isUInt() && node[field.fieldName] == json::UInt(v)));
4060 };
4061
4062 BEAST_EXPECT(vault["LedgerEntryType"].asString() == "Vault");
4063 BEAST_EXPECT(vault[jss::index].asString() == strHex(keylet.key));
4064 BEAST_EXPECT(kCheckInt(vault, sfFlags, 0));
4065 // Ignore all other standard fields, this test doesn't care
4066
4067 BEAST_EXPECT(kCheckString(vault, sfAccount, toBase58(sle->at(sfAccount))));
4068 BEAST_EXPECT(kCheckObject(vault, sfAsset, toJson(sle->at(sfAsset))));
4069 BEAST_EXPECT(kCheckString(vault, sfAssetsAvailable, "50"));
4070 BEAST_EXPECT(kCheckString(vault, sfAssetsMaximum, "1000"));
4071 BEAST_EXPECT(kCheckString(vault, sfAssetsTotal, "50"));
4072 BEAST_EXPECT(!vault.isMember(sfLossUnrealized.getJsonName()));
4073
4074 auto const strShareID = strHex(sle->at(sfShareMPTID));
4075 BEAST_EXPECT(kCheckString(vault, sfShareMPTID, strShareID));
4076 BEAST_EXPECT(kCheckString(vault, sfOwner, toBase58(owner.id())));
4077 BEAST_EXPECT(kCheckInt(vault, sfSequence, sequence));
4078 BEAST_EXPECT(kCheckInt(vault, sfWithdrawalPolicy, kVaultStrategyFirstComeFirstServe));
4079
4080 if (issuance.isObject())
4081 {
4082 BEAST_EXPECT(issuance["LedgerEntryType"].asString() == "MPTokenIssuance");
4083 BEAST_EXPECT(issuance[jss::mpt_issuance_id].asString() == strShareID);
4084 BEAST_EXPECT(kCheckInt(issuance, sfSequence, 1));
4085 BEAST_EXPECT(kCheckInt(
4086 issuance, sfFlags, int(lsfMPTCanEscrow | lsfMPTCanTrade | lsfMPTCanTransfer)));
4087 BEAST_EXPECT(kCheckString(issuance, sfOutstandingAmount, "50000000"));
4088 }
4089 };
4090
4091 {
4092 testcase("RPC ledger_entry selected by key");
4093 json::Value jvParams;
4094 jvParams[jss::ledger_index] = jss::validated;
4095 jvParams[jss::vault] = strHex(keylet.key);
4096 auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
4097
4098 BEAST_EXPECT(!jvVault[jss::result].isMember(jss::error));
4099 BEAST_EXPECT(jvVault[jss::result].isMember(jss::node));
4100 check(jvVault[jss::result][jss::node]);
4101 }
4102
4103 {
4104 testcase("RPC ledger_entry selected by owner and seq");
4105 json::Value jvParams;
4106 jvParams[jss::ledger_index] = jss::validated;
4107 jvParams[jss::vault][jss::owner] = owner.human();
4108 jvParams[jss::vault][jss::seq] = sequence;
4109 auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
4110
4111 BEAST_EXPECT(!jvVault[jss::result].isMember(jss::error));
4112 BEAST_EXPECT(jvVault[jss::result].isMember(jss::node));
4113 check(jvVault[jss::result][jss::node]);
4114 }
4115
4116 {
4117 testcase("RPC ledger_entry cannot find vault by key");
4118 json::Value jvParams;
4119 jvParams[jss::ledger_index] = jss::validated;
4120 jvParams[jss::vault] = to_string(uint256(42));
4121 auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
4122 BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "entryNotFound");
4123 }
4124
4125 {
4126 testcase("RPC ledger_entry cannot find vault by owner and seq");
4127 json::Value jvParams;
4128 jvParams[jss::ledger_index] = jss::validated;
4129 jvParams[jss::vault][jss::owner] = issuer.human();
4130 jvParams[jss::vault][jss::seq] = 1'000'000;
4131 auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
4132 BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "entryNotFound");
4133 }
4134
4135 {
4136 testcase("RPC ledger_entry malformed key");
4137 json::Value jvParams;
4138 jvParams[jss::ledger_index] = jss::validated;
4139 jvParams[jss::vault] = 42;
4140 auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
4141 BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "malformedRequest");
4142 }
4143
4144 {
4145 testcase("RPC ledger_entry malformed owner");
4146 json::Value jvParams;
4147 jvParams[jss::ledger_index] = jss::validated;
4148 jvParams[jss::vault][jss::owner] = 42;
4149 jvParams[jss::vault][jss::seq] = sequence;
4150 auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
4151 BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "malformedOwner");
4152 }
4153
4154 {
4155 testcase("RPC ledger_entry malformed seq");
4156 json::Value jvParams;
4157 jvParams[jss::ledger_index] = jss::validated;
4158 jvParams[jss::vault][jss::owner] = issuer.human();
4159 jvParams[jss::vault][jss::seq] = "foo";
4160 auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
4161 BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "malformedRequest");
4162 }
4163
4164 {
4165 testcase("RPC ledger_entry negative seq");
4166 json::Value jvParams;
4167 jvParams[jss::ledger_index] = jss::validated;
4168 jvParams[jss::vault][jss::owner] = issuer.human();
4169 jvParams[jss::vault][jss::seq] = -1;
4170 auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
4171 BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "malformedRequest");
4172 }
4173
4174 {
4175 testcase("RPC ledger_entry oversized seq");
4176 json::Value jvParams;
4177 jvParams[jss::ledger_index] = jss::validated;
4178 jvParams[jss::vault][jss::owner] = issuer.human();
4179 jvParams[jss::vault][jss::seq] = 1e20;
4180 auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
4181 BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "malformedRequest");
4182 }
4183
4184 {
4185 testcase("RPC ledger_entry bool seq");
4186 json::Value jvParams;
4187 jvParams[jss::ledger_index] = jss::validated;
4188 jvParams[jss::vault][jss::owner] = issuer.human();
4189 jvParams[jss::vault][jss::seq] = true;
4190 auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
4191 BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "malformedRequest");
4192 }
4193
4194 {
4195 testcase("RPC account_objects");
4196
4197 json::Value jvParams;
4198 jvParams[jss::account] = owner.human();
4199 jvParams[jss::type] = jss::vault;
4200 auto jv = env.rpc("json", "account_objects", to_string(jvParams))[jss::result];
4201
4202 BEAST_EXPECT(jv[jss::account_objects].size() == 1);
4203 check(jv[jss::account_objects][0u]);
4204 }
4205
4206 {
4207 testcase("RPC ledger_data");
4208
4209 json::Value jvParams;
4210 jvParams[jss::ledger_index] = jss::validated;
4211 jvParams[jss::binary] = false;
4212 jvParams[jss::type] = jss::vault;
4213 json::Value jv = env.rpc("json", "ledger_data", to_string(jvParams));
4214 BEAST_EXPECT(jv[jss::result][jss::state].size() == 1);
4215 check(jv[jss::result][jss::state][0u]);
4216 }
4217
4218 {
4219 testcase("RPC vault_info command line");
4220 json::Value jv = env.rpc("vault_info", strHex(keylet.key), "validated");
4221
4222 BEAST_EXPECT(!jv[jss::result].isMember(jss::error));
4223 BEAST_EXPECT(jv[jss::result].isMember(jss::vault));
4224 check(jv[jss::result][jss::vault], jv[jss::result][jss::vault][jss::shares]);
4225 }
4226
4227 {
4228 testcase("RPC vault_info json");
4229 json::Value jvParams;
4230 jvParams[jss::ledger_index] = jss::validated;
4231 jvParams[jss::vault_id] = strHex(keylet.key);
4232 auto jv = env.rpc("json", "vault_info", to_string(jvParams));
4233
4234 BEAST_EXPECT(!jv[jss::result].isMember(jss::error));
4235 BEAST_EXPECT(jv[jss::result].isMember(jss::vault));
4236 check(jv[jss::result][jss::vault], jv[jss::result][jss::vault][jss::shares]);
4237 }
4238
4239 {
4240 testcase("RPC vault_info invalid vault_id");
4241 json::Value jvParams;
4242 jvParams[jss::ledger_index] = jss::validated;
4243 jvParams[jss::vault_id] = "foobar";
4244 auto jv = env.rpc("json", "vault_info", to_string(jvParams));
4245 BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
4246 }
4247
4248 {
4249 testcase("RPC vault_info json invalid index");
4250 json::Value jvParams;
4251 jvParams[jss::ledger_index] = jss::validated;
4252 jvParams[jss::vault_id] = 0;
4253 auto jv = env.rpc("json", "vault_info", to_string(jvParams));
4254 BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
4255 }
4256
4257 {
4258 testcase("RPC vault_info json by owner and sequence");
4259 json::Value jvParams;
4260 jvParams[jss::ledger_index] = jss::validated;
4261 jvParams[jss::owner] = owner.human();
4262 jvParams[jss::seq] = sequence;
4263 auto jv = env.rpc("json", "vault_info", to_string(jvParams));
4264
4265 BEAST_EXPECT(!jv[jss::result].isMember(jss::error));
4266 BEAST_EXPECT(jv[jss::result].isMember(jss::vault));
4267 check(jv[jss::result][jss::vault], jv[jss::result][jss::vault][jss::shares]);
4268 }
4269
4270 {
4271 testcase("RPC vault_info json malformed sequence");
4272 json::Value jvParams;
4273 jvParams[jss::ledger_index] = jss::validated;
4274 jvParams[jss::owner] = owner.human();
4275 jvParams[jss::seq] = "foobar";
4276 auto jv = env.rpc("json", "vault_info", to_string(jvParams));
4277 BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
4278 }
4279
4280 {
4281 testcase("RPC vault_info json invalid sequence");
4282 json::Value jvParams;
4283 jvParams[jss::ledger_index] = jss::validated;
4284 jvParams[jss::owner] = owner.human();
4285 jvParams[jss::seq] = 0;
4286 auto jv = env.rpc("json", "vault_info", to_string(jvParams));
4287 BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
4288 }
4289
4290 {
4291 testcase("RPC vault_info json negative sequence");
4292 json::Value jvParams;
4293 jvParams[jss::ledger_index] = jss::validated;
4294 jvParams[jss::owner] = owner.human();
4295 jvParams[jss::seq] = -1;
4296 auto jv = env.rpc("json", "vault_info", to_string(jvParams));
4297 BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
4298 }
4299
4300 {
4301 testcase("RPC vault_info json oversized sequence");
4302 json::Value jvParams;
4303 jvParams[jss::ledger_index] = jss::validated;
4304 jvParams[jss::owner] = owner.human();
4305 jvParams[jss::seq] = 1e20;
4306 auto jv = env.rpc("json", "vault_info", to_string(jvParams));
4307 BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
4308 }
4309
4310 {
4311 testcase("RPC vault_info json bool sequence");
4312 json::Value jvParams;
4313 jvParams[jss::ledger_index] = jss::validated;
4314 jvParams[jss::owner] = owner.human();
4315 jvParams[jss::seq] = true;
4316 auto jv = env.rpc("json", "vault_info", to_string(jvParams));
4317 BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
4318 }
4319
4320 {
4321 testcase("RPC vault_info json malformed owner");
4322 json::Value jvParams;
4323 jvParams[jss::ledger_index] = jss::validated;
4324 jvParams[jss::owner] = "foobar";
4325 jvParams[jss::seq] = sequence;
4326 auto jv = env.rpc("json", "vault_info", to_string(jvParams));
4327 BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
4328 }
4329
4330 {
4331 testcase("RPC vault_info json invalid combination only owner");
4332 json::Value jvParams;
4333 jvParams[jss::ledger_index] = jss::validated;
4334 jvParams[jss::owner] = owner.human();
4335 auto jv = env.rpc("json", "vault_info", to_string(jvParams));
4336 BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
4337 }
4338
4339 {
4340 testcase("RPC vault_info json invalid combination only seq");
4341 json::Value jvParams;
4342 jvParams[jss::ledger_index] = jss::validated;
4343 jvParams[jss::seq] = sequence;
4344 auto jv = env.rpc("json", "vault_info", to_string(jvParams));
4345 BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
4346 }
4347
4348 {
4349 testcase("RPC vault_info json invalid combination seq vault_id");
4350 json::Value jvParams;
4351 jvParams[jss::ledger_index] = jss::validated;
4352 jvParams[jss::vault_id] = strHex(keylet.key);
4353 jvParams[jss::seq] = sequence;
4354 auto jv = env.rpc("json", "vault_info", to_string(jvParams));
4355 BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
4356 }
4357
4358 {
4359 testcase("RPC vault_info json invalid combination owner vault_id");
4360 json::Value jvParams;
4361 jvParams[jss::ledger_index] = jss::validated;
4362 jvParams[jss::vault_id] = strHex(keylet.key);
4363 jvParams[jss::owner] = owner.human();
4364 auto jv = env.rpc("json", "vault_info", to_string(jvParams));
4365 BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
4366 }
4367
4368 {
4369 testcase(
4370 "RPC vault_info json invalid combination owner seq "
4371 "vault_id");
4372 json::Value jvParams;
4373 jvParams[jss::ledger_index] = jss::validated;
4374 jvParams[jss::vault_id] = strHex(keylet.key);
4375 jvParams[jss::seq] = sequence;
4376 jvParams[jss::owner] = owner.human();
4377 auto jv = env.rpc("json", "vault_info", to_string(jvParams));
4378 BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
4379 }
4380
4381 {
4382 testcase("RPC vault_info json no input");
4383 json::Value jvParams;
4384 jvParams[jss::ledger_index] = jss::validated;
4385 auto jv = env.rpc("json", "vault_info", to_string(jvParams));
4386 BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
4387 }
4388
4389 {
4390 testcase("RPC vault_info command line invalid index");
4391 json::Value jv = env.rpc("vault_info", "foobar", "validated");
4392 BEAST_EXPECT(jv[jss::error].asString() == "invalidParams");
4393 }
4394
4395 {
4396 testcase("RPC vault_info command line invalid index");
4397 json::Value jv = env.rpc("vault_info", "0", "validated");
4398 BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
4399 }
4400
4401 {
4402 testcase("RPC vault_info command line invalid index");
4403 json::Value jv = env.rpc("vault_info", strHex(uint256(42)), "validated");
4404 BEAST_EXPECT(jv[jss::result][jss::error].asString() == "entryNotFound");
4405 }
4406
4407 {
4408 testcase("RPC vault_info command line invalid ledger");
4409 json::Value jv = env.rpc("vault_info", strHex(keylet.key), "0");
4410 BEAST_EXPECT(jv[jss::result][jss::error].asString() == "lgrNotFound");
4411 }
4412 }
4413
4414 void
4416 {
4417 using namespace test::jtx;
4418 using namespace loanBroker;
4419 using namespace loan;
4420 Env env(*this, beast::Severity::Warning);
4421
4422 auto const vaultAssetBalance = [&](Keylet const& vaultKeylet) {
4423 auto const sleVault = env.le(vaultKeylet);
4424 BEAST_EXPECT(sleVault != nullptr);
4425
4426 return std::make_pair(sleVault->at(sfAssetsAvailable), sleVault->at(sfAssetsTotal));
4427 };
4428
4429 auto const vaultShareBalance = [&](Keylet const& vaultKeylet) {
4430 auto const sleVault = env.le(vaultKeylet);
4431 BEAST_EXPECT(sleVault != nullptr);
4432
4433 auto const sleIssuance = env.le(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
4434 BEAST_EXPECT(sleIssuance != nullptr);
4435
4436 return sleIssuance->at(sfOutstandingAmount);
4437 };
4438
4439 auto const setupVault = [&](PrettyAsset const& asset,
4440 Account const& owner,
4441 Account const& depositor) -> std::pair<Vault, Keylet> {
4442 Vault const vault{env};
4443
4444 auto const& [tx, vaultKeylet] = vault.create({.owner = owner, .asset = asset});
4445 env(tx, Ter(tesSUCCESS));
4446 env.close();
4447
4448 auto const& vaultSle = env.le(vaultKeylet);
4449 BEAST_EXPECT(vaultSle != nullptr);
4450
4451 Asset const share = vaultSle->at(sfShareMPTID);
4452
4453 env(vault.deposit(
4454 {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}),
4455 Ter(tesSUCCESS));
4456 env.close();
4457
4458 auto const& [availablePreDefault, totalPreDefault] = vaultAssetBalance(vaultKeylet);
4459 BEAST_EXPECT(availablePreDefault == totalPreDefault);
4460 BEAST_EXPECT(availablePreDefault == asset(100).value());
4461
4462 // attempt to clawback shares while there are assets fails
4463 env(vault.clawback(
4464 {.issuer = owner,
4465 .id = vaultKeylet.key,
4466 .holder = depositor,
4467 .amount = share(0).value()}),
4468 Ter(tecNO_PERMISSION));
4469 env.close();
4470
4471 auto const& sharesAvailable = vaultShareBalance(vaultKeylet);
4472 auto const& brokerKeylet = keylet::loanBroker(owner.id(), env.seq(owner));
4473
4474 env(set(owner, vaultKeylet.key));
4475 env.close();
4476
4477 auto const& loanKeylet = keylet::loan(brokerKeylet.key, 1);
4478
4479 // Create a simple Loan for the full amount of Vault assets
4480 env(set(depositor, brokerKeylet.key, asset(100).value()),
4481 loan::kInterestRate(TenthBips32(0)),
4482 kGracePeriod(60),
4483 kPaymentInterval(120),
4484 kPaymentTotal(10),
4485 Sig(sfCounterpartySignature, owner),
4486 Fee(env.current()->fees().base * 2),
4487 Ter(tesSUCCESS));
4488 env.close();
4489
4490 // attempt to clawback shares while there assetsAvailable == 0 and
4491 // assetsTotal > 0 fails
4492 env(vault.clawback(
4493 {.issuer = owner,
4494 .id = vaultKeylet.key,
4495 .holder = depositor,
4496 .amount = share(0).value()}),
4497 Ter(tecNO_PERMISSION));
4498 env.close();
4499
4500 env.close(std::chrono::seconds{120 + 60});
4501
4502 env(manage(owner, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
4503
4504 auto const& [availablePostDefault, totalPostDefault] = vaultAssetBalance(vaultKeylet);
4505
4506 BEAST_EXPECT(availablePostDefault == totalPostDefault);
4507 BEAST_EXPECT(availablePostDefault == asset(0).value());
4508 BEAST_EXPECT(vaultShareBalance(vaultKeylet) == sharesAvailable);
4509
4510 return std::make_pair(vault, vaultKeylet);
4511 };
4512
4513 auto const testCase = [&](PrettyAsset const& asset,
4514 std::string const& prefix,
4515 Account const& owner,
4516 Account const& depositor) {
4517 {
4518 testcase("VaultClawback (share) - " + prefix + " owner asset clawback fails");
4519 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor);
4520 // when asset is XRP or owner is not issuer clawback fail
4521 // when owner is issuer precision loss occurs as vault is
4522 // empty
4523 auto const expectedTer = [&]() {
4524 if (asset.native())
4525 return Ter(temMALFORMED);
4526 if (asset.raw().getIssuer() != owner.id())
4527 return Ter(tecNO_PERMISSION);
4528 return Ter(tecPRECISION_LOSS);
4529 }();
4530 env(vault.clawback({
4531 .issuer = owner,
4532 .id = vaultKeylet.key,
4533 .holder = depositor,
4534 .amount = asset(100).value(),
4535 }),
4536 expectedTer);
4537 env.close();
4538 }
4539
4540 {
4541 testcase(
4542 "VaultClawback (share) - " + prefix + " owner incomplete share clawback fails");
4543 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor);
4544 auto const& vaultSle = env.le(vaultKeylet);
4545 if (!BEAST_EXPECT(vaultSle))
4546 return;
4547 Asset const share = vaultSle->at(sfShareMPTID);
4548 env(vault.clawback({
4549 .issuer = owner,
4550 .id = vaultKeylet.key,
4551 .holder = depositor,
4552 .amount = share(1).value(),
4553 }),
4554 Ter(tecLIMIT_EXCEEDED));
4555 env.close();
4556 }
4557
4558 {
4559 testcase(
4560 "VaultClawback (share) - " + prefix +
4561 " owner implicit complete share clawback");
4562 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor);
4563 env(vault.clawback({
4564 .issuer = owner,
4565 .id = vaultKeylet.key,
4566 .holder = depositor,
4567 }),
4568 // when owner is issuer implicit clawback fails
4569 asset.native() || asset.raw().getIssuer() != owner.id() ? Ter(tesSUCCESS)
4570 : Ter(tecWRONG_ASSET));
4571 env.close();
4572 }
4573
4574 {
4575 testcase(
4576 "VaultClawback (share) - " + prefix +
4577 " owner explicit complete share clawback succeeds");
4578 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor);
4579 auto const& vaultSle = env.le(vaultKeylet);
4580 if (!BEAST_EXPECT(vaultSle))
4581 return;
4582 Asset const share = vaultSle->at(sfShareMPTID);
4583 env(vault.clawback({
4584 .issuer = owner,
4585 .id = vaultKeylet.key,
4586 .holder = depositor,
4587 .amount = share(vaultShareBalance(vaultKeylet)).value(),
4588 }),
4589 Ter(tesSUCCESS));
4590 env.close();
4591 }
4592 {
4593 testcase("VaultClawback (share) - " + prefix + " owner can clawback own shares");
4594 auto [vault, vaultKeylet] = setupVault(asset, owner, owner);
4595 auto const& vaultSle = env.le(vaultKeylet);
4596 if (!BEAST_EXPECT(vaultSle))
4597 return;
4598 Asset const share = vaultSle->at(sfShareMPTID);
4599 env(vault.clawback({
4600 .issuer = owner,
4601 .id = vaultKeylet.key,
4602 .holder = owner,
4603 .amount = share(vaultShareBalance(vaultKeylet)).value(),
4604 }),
4605 Ter(tesSUCCESS));
4606 env.close();
4607 }
4608
4609 {
4610 testcase("VaultClawback (share) - " + prefix + " empty vault share clawback fails");
4611 auto [vault, vaultKeylet] = setupVault(asset, owner, owner);
4612 auto const& vaultSle = env.le(vaultKeylet);
4613 if (!BEAST_EXPECT(vaultSle))
4614 return;
4615 Asset const share = vaultSle->at(sfShareMPTID);
4616 env(vault.clawback({
4617 .issuer = owner,
4618 .id = vaultKeylet.key,
4619 .holder = owner,
4620 .amount = share(vaultShareBalance(vaultKeylet)).value(),
4621 }),
4622 Ter(tesSUCCESS));
4623
4624 // Now the vault is empty, clawback again fails
4625 env(vault.clawback({
4626 .issuer = owner,
4627 .id = vaultKeylet.key,
4628 .holder = owner,
4629 .amount = share(vaultShareBalance(vaultKeylet)).value(),
4630 }),
4631 Ter(tecNO_PERMISSION));
4632 env.close();
4633 }
4634 };
4635
4636 Account const owner{"alice"};
4637 Account const depositor{"bob"};
4638 Account const issuer{"issuer"};
4639
4640 env.fund(XRP(10000), issuer, owner, depositor);
4641 env.close();
4642
4643 // Test XRP
4644 PrettyAsset const xrp = xrpIssue();
4645 testCase(xrp, "XRP", owner, depositor);
4646 testCase(xrp, "XRP (depositor is owner)", owner, owner);
4647
4648 // Test IOU
4649 PrettyAsset const iou = issuer["IOU"];
4650 env(fset(issuer, asfAllowTrustLineClawback));
4651 env.close();
4652
4653 env.trust(iou(1000), owner);
4654 env.trust(iou(1000), depositor);
4655 env(pay(issuer, owner, iou(100)));
4656 env(pay(issuer, depositor, iou(100)));
4657 env.close();
4658 testCase(iou, "IOU", owner, depositor);
4659 testCase(iou, "IOU (owner is issuer)", issuer, depositor);
4660
4661 // Test MPT
4662 MPTTester mptt{env, issuer, kMptInitNoFund};
4663 mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
4664 PrettyAsset const mpt = mptt.issuanceID();
4665 mptt.authorize({.account = owner});
4666 mptt.authorize({.account = depositor});
4667 env(pay(issuer, owner, mpt(1000)));
4668 env(pay(issuer, depositor, mpt(1000)));
4669 env.close();
4670 testCase(mpt, "MPT", owner, depositor);
4671 testCase(mpt, "MPT (owner is issuer)", issuer, depositor);
4672 }
4673
4674 void
4676 {
4677 using namespace test::jtx;
4678 using namespace loanBroker;
4679 using namespace loan;
4680 Env env(*this);
4681 env.enableFeature(fixCleanup3_1_3);
4682
4683 auto const setupVault = [&](PrettyAsset const& asset,
4684 Account const& owner,
4685 Account const& depositor,
4686 Account const& issuer) -> std::pair<Vault, Keylet> {
4687 Vault const vault{env};
4688
4689 auto const& [tx, vaultKeylet] = vault.create({.owner = owner, .asset = asset});
4690 env(tx, Ter(tesSUCCESS));
4691 env.close();
4692
4693 auto const& vaultSle = env.le(vaultKeylet);
4694 BEAST_EXPECT(vaultSle != nullptr);
4695 env.memoize(Account("vault", vaultSle->at(sfAccount)));
4696 env(vault.deposit(
4697 {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}),
4698 Ter(tesSUCCESS));
4699 env.close();
4700
4701 return std::make_pair(vault, vaultKeylet);
4702 };
4703
4704 auto const testCase = [&](PrettyAsset const& asset,
4705 std::string const& prefix,
4706 Account const& owner,
4707 Account const& depositor,
4708 Account const& issuer) {
4709 if (asset.native())
4710 {
4711 testcase("VaultClawback (asset) - " + prefix + " issuer XRP clawback fails");
4712 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
4713 // If the asset is XRP, clawback with amount fails as malformed
4714 // when asset is specified.
4715 env(vault.clawback({
4716 .issuer = issuer,
4717 .id = vaultKeylet.key,
4718 .holder = issuer,
4719 .amount = asset(1).value(),
4720 }),
4721 Ter(temMALFORMED));
4722 // When asset is implicit, clawback fails as no permission.
4723 env(vault.clawback({
4724 .issuer = issuer,
4725 .id = vaultKeylet.key,
4726 .holder = issuer,
4727 }),
4728 Ter(tecNO_PERMISSION));
4729 return;
4730 }
4731
4732 {
4733 testcase(
4734 "VaultClawback (asset) - " + prefix + " clawback for different asset fails");
4735 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
4736
4737 Account const issuer2{"issuer2"};
4738 PrettyAsset const asset2 = issuer2["FOO"];
4739 env(vault.clawback({
4740 .issuer = issuer,
4741 .id = vaultKeylet.key,
4742 .holder = depositor,
4743 .amount = asset2(1).value(),
4744 }),
4745 Ter(tecWRONG_ASSET));
4746 }
4747
4748 {
4749 testcase(
4750 "VaultClawback (asset) - " + prefix +
4751 " ambiguous owner/issuer asset clawback fails");
4752 auto [vault, vaultKeylet] = setupVault(asset, issuer, depositor, issuer);
4753 env(vault.clawback({
4754 .issuer = issuer,
4755 .id = vaultKeylet.key,
4756 .holder = issuer,
4757 }),
4758 Ter(tecWRONG_ASSET));
4759 }
4760
4761 {
4762 testcase("VaultClawback (asset) - " + prefix + " non-issuer asset clawback fails");
4763 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
4764
4765 env(vault.clawback({
4766 .issuer = owner,
4767 .id = vaultKeylet.key,
4768 .holder = depositor,
4769 }),
4770 Ter(tecNO_PERMISSION));
4771
4772 env(vault.clawback({
4773 .issuer = owner,
4774 .id = vaultKeylet.key,
4775 .holder = depositor,
4776 .amount = asset(1).value(),
4777 }),
4778 Ter(tecNO_PERMISSION));
4779 }
4780
4781 {
4782 testcase("VaultClawback (asset) - " + prefix + " issuer clawback from self fails");
4783 auto [vault, vaultKeylet] = setupVault(asset, owner, issuer, issuer);
4784 env(vault.clawback({
4785 .issuer = issuer,
4786 .id = vaultKeylet.key,
4787 .holder = issuer,
4788 }),
4789 Ter(tecNO_PERMISSION));
4790 }
4791
4792 {
4793 testcase("VaultClawback (asset) - " + prefix + " issuer share clawback fails");
4794 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
4795 auto const& vaultSle = env.le(vaultKeylet);
4796 if (!BEAST_EXPECT(vaultSle))
4797 return;
4798 Asset const share = vaultSle->at(sfShareMPTID);
4799
4800 env(vault.clawback({
4801 .issuer = issuer,
4802 .id = vaultKeylet.key,
4803 .holder = depositor,
4804 .amount = share(1).value(),
4805 }),
4806 Ter(tecNO_PERMISSION));
4807 }
4808
4809 {
4810 testcase(
4811 "VaultClawback (asset) - " + prefix +
4812 " partial issuer asset clawback succeeds");
4813 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
4814
4815 env(vault.clawback({
4816 .issuer = issuer,
4817 .id = vaultKeylet.key,
4818 .holder = depositor,
4819 .amount = asset(1).value(),
4820 }),
4821 Ter(tesSUCCESS));
4822 }
4823
4824 {
4825 testcase(
4826 "VaultClawback (asset) - " + prefix + " full issuer asset clawback succeeds");
4827 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
4828
4829 env(vault.clawback({
4830 .issuer = issuer,
4831 .id = vaultKeylet.key,
4832 .holder = depositor,
4833 .amount = asset(100).value(),
4834 }),
4835 Ter(tesSUCCESS));
4836 }
4837
4838 {
4839 testcase(
4840 "VaultClawback (asset) - " + prefix +
4841 " implicit full issuer asset clawback succeeds");
4842 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
4843
4844 env(vault.clawback({
4845 .issuer = issuer,
4846 .id = vaultKeylet.key,
4847 .holder = depositor,
4848 }),
4849 Ter(tesSUCCESS));
4850 }
4851
4852 {
4853 testcase(
4854 "VaultClawback (asset) - " + prefix +
4855 " zero-amount clawback clamped with outstanding loan");
4856 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
4857
4858 auto const vaultSle = env.le(vaultKeylet);
4859 if (!BEAST_EXPECT(vaultSle))
4860 return;
4861
4862 PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
4863
4864 // Create a loan broker backed by this vault
4865 auto const brokerKeylet = keylet::loanBroker(owner.id(), env.seq(owner));
4866 env(set(owner, vaultKeylet.key));
4867 env.close();
4868
4869 // Depositor borrows 40 units, reducing assetsAvailable to 60
4870 // while assetsTotal stays at 100
4871 env(set(depositor, brokerKeylet.key, asset(40).value()),
4872 loan::kInterestRate(TenthBips32(0)),
4873 kGracePeriod(60),
4874 kPaymentInterval(120),
4875 kPaymentTotal(10),
4876 Sig(sfCounterpartySignature, owner),
4877 Fee(env.current()->fees().base * 2),
4878 Ter(tesSUCCESS));
4879 env.close();
4880
4881 {
4882 auto const sle = env.le(vaultKeylet);
4883 BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(60).value());
4884 BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(100).value());
4885 }
4886
4887 // Zero-amount clawback (= "clawback all") should succeed,
4888 // clamped to assetsAvailable (60) rather than the full
4889 // share value (100).
4890 env(vault.clawback({
4891 .issuer = issuer,
4892 .id = vaultKeylet.key,
4893 .holder = depositor,
4894 }),
4895 Ter(tesSUCCESS));
4896 env.close();
4897
4898 // Only 60 assets clawed back; loan's 40 still outstanding
4899 {
4900 auto const sle = env.le(vaultKeylet);
4901 BEAST_EXPECT(sle != nullptr);
4902 BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(0).value());
4903 BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(40).value());
4904
4905 // 60 of 100 shares destroyed (1:1 ratio), 40 remain
4906 auto const sharesAfter = env.balance(depositor, shares);
4907 BEAST_EXPECT(sharesAfter == shares(Number{4, sle->at(sfScale) + 1}));
4908 }
4909 }
4910
4911 {
4912 testcase(
4913 "VaultClawback (asset) - " + prefix +
4914 " non-zero clawback clamped with outstanding loan");
4915 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
4916
4917 auto const vaultSle = env.le(vaultKeylet);
4918 if (!BEAST_EXPECT(vaultSle))
4919 return;
4920 PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
4921
4922 // Create a loan broker backed by this vault
4923 auto const brokerKeylet = keylet::loanBroker(owner.id(), env.seq(owner));
4924 env(set(owner, vaultKeylet.key));
4925 env.close();
4926
4927 // Depositor borrows 40 units
4928 env(set(depositor, brokerKeylet.key, asset(40).value()),
4929 loan::kInterestRate(TenthBips32(0)),
4930 kGracePeriod(60),
4931 kPaymentInterval(120),
4932 kPaymentTotal(10),
4933 Sig(sfCounterpartySignature, owner),
4934 Fee(env.current()->fees().base * 2),
4935 Ter(tesSUCCESS));
4936 env.close();
4937
4938 {
4939 auto const sle = env.le(vaultKeylet);
4940 BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(60).value());
4941 BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(100).value());
4942 }
4943
4944 // Request 100 but only 60 available — clamped to 60
4945 env(vault.clawback({
4946 .issuer = issuer,
4947 .id = vaultKeylet.key,
4948 .holder = depositor,
4949 .amount = asset(100).value(),
4950 }),
4951 Ter(tesSUCCESS));
4952 env.close();
4953
4954 {
4955 auto const sle = env.le(vaultKeylet);
4956 BEAST_EXPECT(sle != nullptr);
4957 BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(0).value());
4958 BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(40).value());
4959
4960 // 60 of 100 shares destroyed (1:1 ratio), 40 remain
4961 auto const sharesAfter = env.balance(depositor, shares);
4962 BEAST_EXPECT(sharesAfter == shares(Number{4, sle->at(sfScale) + 1}));
4963 }
4964 }
4965
4966 {
4967 testcase(
4968 "VaultClawback (asset) - " + prefix +
4969 " partial clawback below available with outstanding loan");
4970 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
4971
4972 auto const vaultSle = env.le(vaultKeylet);
4973 if (!BEAST_EXPECT(vaultSle))
4974 return;
4975 PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
4976
4977 // Create a loan broker backed by this vault
4978 auto const brokerKeylet = keylet::loanBroker(owner.id(), env.seq(owner));
4979 env(set(owner, vaultKeylet.key));
4980 env.close();
4981
4982 // Depositor borrows 40 units: assetsAvailable=60, assetsTotal=100
4983 env(set(depositor, brokerKeylet.key, asset(40).value()),
4984 loan::kInterestRate(TenthBips32(0)),
4985 kGracePeriod(60),
4986 kPaymentInterval(120),
4987 kPaymentTotal(10),
4988 Sig(sfCounterpartySignature, owner),
4989 Fee(env.current()->fees().base * 2),
4990 Ter(tesSUCCESS));
4991 env.close();
4992
4993 {
4994 auto const sle = env.le(vaultKeylet);
4995 BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(60).value());
4996 BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(100).value());
4997 }
4998
4999 // Clawback 30 — well under available (60), no clamping needed
5000 env(vault.clawback({
5001 .issuer = issuer,
5002 .id = vaultKeylet.key,
5003 .holder = depositor,
5004 .amount = asset(30).value(),
5005 }),
5006 Ter(tesSUCCESS));
5007 env.close();
5008
5009 {
5010 auto const sle = env.le(vaultKeylet);
5011 BEAST_EXPECT(sle != nullptr);
5012 BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(30).value());
5013 BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(70).value());
5014
5015 // 30 of 100 shares destroyed (1:1 ratio), 70 remain
5016 auto const sharesAfter = env.balance(depositor, shares);
5017 BEAST_EXPECT(sharesAfter == shares(Number{7, sle->at(sfScale) + 1}));
5018 }
5019 }
5020
5021 {
5022 testcase(
5023 "VaultClawback (asset) - " + prefix +
5024 " clawback exactly equal to available with outstanding loan");
5025 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
5026
5027 auto const vaultSle = env.le(vaultKeylet);
5028 if (!BEAST_EXPECT(vaultSle))
5029 return;
5030 PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
5031
5032 auto const brokerKeylet = keylet::loanBroker(owner.id(), env.seq(owner));
5033 env(set(owner, vaultKeylet.key));
5034 env.close();
5035
5036 // Depositor borrows 40 units: assetsAvailable=60, assetsTotal=100
5037 env(set(depositor, brokerKeylet.key, asset(40).value()),
5038 loan::kInterestRate(TenthBips32(0)),
5039 kGracePeriod(60),
5040 kPaymentInterval(120),
5041 kPaymentTotal(10),
5042 Sig(sfCounterpartySignature, owner),
5043 Fee(env.current()->fees().base * 2),
5044 Ter(tesSUCCESS));
5045 env.close();
5046
5047 // Clawback exactly 60 — at the boundary, no clamping needed
5048 env(vault.clawback({
5049 .issuer = issuer,
5050 .id = vaultKeylet.key,
5051 .holder = depositor,
5052 .amount = asset(60).value(),
5053 }),
5054 Ter(tesSUCCESS));
5055 env.close();
5056
5057 {
5058 auto const sle = env.le(vaultKeylet);
5059 BEAST_EXPECT(sle != nullptr);
5060 BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(0).value());
5061 BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(40).value());
5062
5063 // 60 of 100 shares destroyed (1:1 ratio), 40 remain
5064 auto const sharesAfter = env.balance(depositor, shares);
5065 BEAST_EXPECT(sharesAfter == shares(Number{4, sle->at(sfScale) + 1}));
5066 }
5067 }
5068
5069 {
5070 testcase(
5071 "VaultClawback (asset) - " + prefix +
5072 " clawback with zero available (fully borrowed)");
5073 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
5074
5075 auto const vaultSle = env.le(vaultKeylet);
5076 if (!BEAST_EXPECT(vaultSle))
5077 return;
5078 PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
5079
5080 auto const brokerKeylet = keylet::loanBroker(owner.id(), env.seq(owner));
5081 env(set(owner, vaultKeylet.key));
5082 env.close();
5083
5084 // Depositor borrows all 100 units: assetsAvailable=0, assetsTotal=100
5085 env(set(depositor, brokerKeylet.key, asset(100).value()),
5086 loan::kInterestRate(TenthBips32(0)),
5087 kGracePeriod(60),
5088 kPaymentInterval(120),
5089 kPaymentTotal(10),
5090 Sig(sfCounterpartySignature, owner),
5091 Fee(env.current()->fees().base * 2),
5092 Ter(tesSUCCESS));
5093 env.close();
5094
5095 {
5096 auto const sle = env.le(vaultKeylet);
5097 BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(0).value());
5098 BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(100).value());
5099 }
5100
5101 auto const sharesBefore = env.balance(depositor, shares);
5102
5103 // Zero-amount clawback — nothing available, clamped to 0,
5104 // resulting in zero shares destroyed → tecPRECISION_LOSS
5105 env(vault.clawback({
5106 .issuer = issuer,
5107 .id = vaultKeylet.key,
5108 .holder = depositor,
5109 }),
5110 Ter(tecPRECISION_LOSS));
5111 env.close();
5112
5113 // Explicit amount clawback — also nothing available
5114 env(vault.clawback({
5115 .issuer = issuer,
5116 .id = vaultKeylet.key,
5117 .holder = depositor,
5118 .amount = asset(50).value(),
5119 }),
5120 Ter(tecPRECISION_LOSS));
5121 env.close();
5122
5123 {
5124 // Nothing changed — vault and shares unchanged
5125 auto const sle = env.le(vaultKeylet);
5126 BEAST_EXPECT(sle != nullptr);
5127 BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(0).value());
5128 BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(100).value());
5129 auto const sharesAfter = env.balance(depositor, shares);
5130 BEAST_EXPECT(sharesAfter == sharesBefore);
5131 }
5132 }
5133 };
5134
5135 Account const owner{"alice"};
5136 Account const depositor{"bob"};
5137 Account const issuer{"issuer"};
5138
5139 env.fund(XRP(10000), issuer, owner, depositor);
5140 env.close();
5141
5142 // Test XRP
5143 PrettyAsset const xrp = xrpIssue();
5144 testCase(xrp, "XRP", owner, depositor, issuer);
5145
5146 // Test IOU
5147 PrettyAsset const iou = issuer["IOU"];
5148 env(fset(issuer, asfAllowTrustLineClawback));
5149 env.close();
5150 env.trust(iou(2000), owner);
5151 env.trust(iou(2000), depositor);
5152 env(pay(issuer, owner, iou(2000)));
5153 env(pay(issuer, depositor, iou(2000)));
5154 env.close();
5155 testCase(iou, "IOU", owner, depositor, issuer);
5156
5157 // Test MPT
5158 MPTTester mptt{env, issuer, kMptInitNoFund};
5159 mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
5160
5161 PrettyAsset const mpt = mptt.issuanceID();
5162 mptt.authorize({.account = owner});
5163 mptt.authorize({.account = depositor});
5164 env(pay(issuer, depositor, mpt(2000)));
5165 env.close();
5166 testCase(mpt, "MPT", owner, depositor, issuer);
5167
5168 // Test pre-fixCleanup3_1_3 legacy path: zero-amount clawback
5169 // returns early without clamping to assetsAvailable.
5170 {
5171 testcase(
5172 "VaultClawback (asset) - IOU pre-fixCleanup3_1_3"
5173 " zero-amount clawback unclamped with outstanding loan");
5174
5175 env.disableFeature(fixCleanup3_1_3);
5176
5177 auto [vault, vaultKeylet] = setupVault(iou, owner, depositor, issuer);
5178
5179 auto const vaultSle = env.le(vaultKeylet);
5180 BEAST_EXPECT(vaultSle != nullptr);
5181 if (!vaultSle)
5182 return;
5183
5184 PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
5185
5186 // Create a loan broker backed by this vault
5187 auto const brokerKeylet = keylet::loanBroker(owner.id(), env.seq(owner));
5188 env(set(owner, vaultKeylet.key));
5189 env.close();
5190
5191 // Depositor borrows 40 units, reducing assetsAvailable to 60
5192 // while assetsTotal stays at 100
5193 env(set(depositor, brokerKeylet.key, iou(40).value()),
5194 loan::kInterestRate(TenthBips32(0)),
5195 kGracePeriod(60),
5196 kPaymentInterval(120),
5197 kPaymentTotal(10),
5198 Sig(sfCounterpartySignature, owner),
5199 Fee(env.current()->fees().base * 2),
5200 Ter(tesSUCCESS));
5201 env.close();
5202
5203 {
5204 auto const sle = env.le(vaultKeylet);
5205 BEAST_EXPECT(sle->at(sfAssetsAvailable) == iou(60).value());
5206 BEAST_EXPECT(sle->at(sfAssetsTotal) == iou(100).value());
5207 }
5208
5209 auto const sharesBefore = env.balance(depositor, shares);
5210
5211 // Legacy: zero-amount clawback tries to recover the full
5212 // share value (100) without clamping to assetsAvailable (60).
5213 // This causes the vault balance to go negative, triggering
5214 // the sanity check in doApply → tefINTERNAL.
5215 env(vault.clawback({
5216 .issuer = issuer,
5217 .id = vaultKeylet.key,
5218 .holder = depositor,
5219 }),
5220 Ter(tefINTERNAL));
5221 env.close();
5222
5223 {
5224 // Transaction rolled back — vault and shares unchanged
5225 auto const sle = env.le(vaultKeylet);
5226 BEAST_EXPECT(sle != nullptr);
5227 BEAST_EXPECT(sle->at(sfAssetsAvailable) == iou(60).value());
5228 BEAST_EXPECT(sle->at(sfAssetsTotal) == iou(100).value());
5229 auto const sharesAfter = env.balance(depositor, shares);
5230 BEAST_EXPECT(sharesAfter == sharesBefore);
5231 }
5232
5233 env.enableFeature(fixCleanup3_1_3);
5234 }
5235 }
5236
5237 void
5239 {
5240 testcase("Assets Maximum");
5241
5242 using namespace test::jtx;
5243
5244 Env env{*this, testableAmendments()};
5245 Account const owner{"owner"};
5246 Account const issuer{"issuer"};
5247
5248 Vault const vault{env};
5249 env.fund(XRP(1'000'000), issuer, owner);
5250 env.close();
5251
5253 BEAST_EXPECT(maxInt64 == "9223372036854775807");
5254
5255 // Naming things is hard
5256 auto const maxInt64Plus1 = std::to_string(
5258 BEAST_EXPECT(maxInt64Plus1 == "9223372036854775808");
5259
5260 auto const initialXRP = to_string(kInitialXrp);
5261 BEAST_EXPECT(initialXRP == "100000000000000000");
5262
5263 auto const initialXRPPlus1 = to_string(kInitialXrp + 1);
5264 BEAST_EXPECT(initialXRPPlus1 == "100000000000000001");
5265
5266 {
5267 testcase("Assets Maximum: XRP");
5268
5269 PrettyAsset const xrpAsset = xrpIssue();
5270
5271 auto [tx, keylet] = vault.create({.owner = owner, .asset = xrpAsset});
5272 tx[sfData] = "4D65746144617461";
5273
5274 tx[sfAssetsMaximum] = maxInt64;
5275 env(tx, Ter(tefEXCEPTION));
5276 env.close();
5277
5278 tx[sfAssetsMaximum] = initialXRPPlus1;
5279 env(tx, Ter(tefEXCEPTION));
5280 env.close();
5281
5282 tx[sfAssetsMaximum] = initialXRP;
5283 env(tx);
5284 env.close();
5285
5286 tx[sfAssetsMaximum] = maxInt64Plus1;
5287 env(tx, Ter(tefEXCEPTION));
5288 env.close();
5289
5290 // This value will be rounded
5291 auto const insertAt = maxInt64Plus1.size() - 3;
5292 auto const decimalTest = maxInt64Plus1.substr(0, insertAt) + "." +
5293 maxInt64Plus1.substr(insertAt); // (max int64+1) / 1000
5294 BEAST_EXPECT(decimalTest == "9223372036854775.808");
5295 tx[sfAssetsMaximum] = decimalTest;
5296 auto const newKeylet = keylet::vault(owner.id(), env.seq(owner));
5297 env(tx);
5298 env.close();
5299
5300 auto const vaultSle = env.le(newKeylet);
5301 if (!BEAST_EXPECT(vaultSle))
5302 return;
5303
5304 BEAST_EXPECT(vaultSle->at(sfAssetsMaximum) == 9223372036854776);
5305 }
5306
5307 {
5308 testcase("Assets Maximum: MPT");
5309
5310 PrettyAsset const mptAsset = [&]() {
5311 MPTTester mptt{env, issuer, kMptInitNoFund};
5312 mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
5313 env.close();
5314 PrettyAsset const mptAsset = mptt["MPT"];
5315 mptt.authorize({.account = owner});
5316 env.close();
5317 return mptAsset;
5318 }();
5319
5320 env(pay(issuer, owner, mptAsset(100'000)));
5321 env.close();
5322
5323 auto [tx, keylet] = vault.create({.owner = owner, .asset = mptAsset});
5324 tx[sfData] = "4D65746144617461";
5325
5326 tx[sfAssetsMaximum] = maxInt64;
5327 env(tx);
5328 env.close();
5329
5330 tx[sfAssetsMaximum] = initialXRPPlus1;
5331 env(tx);
5332 env.close();
5333
5334 tx[sfAssetsMaximum] = initialXRP;
5335 env(tx);
5336 env.close();
5337
5338 tx[sfAssetsMaximum] = maxInt64Plus1;
5339 env(tx, Ter(tefEXCEPTION));
5340 env.close();
5341
5342 // This value will be rounded
5343 auto const insertAt = maxInt64Plus1.size() - 1;
5344 auto const decimalTest = maxInt64Plus1.substr(0, insertAt) + "." +
5345 maxInt64Plus1.substr(insertAt); // (max int64+1) / 10
5346 BEAST_EXPECT(decimalTest == "922337203685477580.8");
5347 tx[sfAssetsMaximum] = decimalTest;
5348 auto const newKeylet = keylet::vault(owner.id(), env.seq(owner));
5349 env(tx);
5350 env.close();
5351
5352 auto const vaultSle = env.le(newKeylet);
5353 if (!BEAST_EXPECT(vaultSle))
5354 return;
5355
5356 BEAST_EXPECT(vaultSle->at(sfAssetsMaximum) == 922337203685477581);
5357 }
5358
5359 {
5360 testcase("Assets Maximum: IOU");
5361
5362 // Almost anything goes with IOUs
5363 PrettyAsset const iouAsset = issuer["IOU"];
5364 env.trust(iouAsset(1000), owner);
5365 env(pay(issuer, owner, iouAsset(200)));
5366 env.close();
5367
5368 auto [tx, keylet] = vault.create({.owner = owner, .asset = iouAsset});
5369 tx[sfData] = "4D65746144617461";
5370
5371 tx[sfAssetsMaximum] = maxInt64;
5372 env(tx);
5373 env.close();
5374
5375 tx[sfAssetsMaximum] = initialXRPPlus1;
5376 env(tx);
5377 env.close();
5378
5379 tx[sfAssetsMaximum] = initialXRP;
5380 env(tx);
5381 env.close();
5382
5383 tx[sfAssetsMaximum] = maxInt64Plus1;
5384 env(tx);
5385 env.close();
5386
5387 tx[sfAssetsMaximum] = "1000000000000000e80";
5388 env.close();
5389
5390 tx[sfAssetsMaximum] = "1000000000000000e-96";
5391 env.close();
5392
5393 // These values will be rounded to 15 significant digits
5394 {
5395 auto const insertAt = maxInt64Plus1.size() - 1;
5396 auto const decimalTest = maxInt64Plus1.substr(0, insertAt) + "." +
5397 maxInt64Plus1.substr(insertAt); // (max int64+1) / 10
5398 BEAST_EXPECT(decimalTest == "922337203685477580.8");
5399 tx[sfAssetsMaximum] = decimalTest;
5400 auto const newKeylet = keylet::vault(owner.id(), env.seq(owner));
5401 env(tx);
5402 env.close();
5403
5404 auto const vaultSle = env.le(newKeylet);
5405 if (!BEAST_EXPECT(vaultSle))
5406 return;
5407
5408 BEAST_EXPECT(
5409 (vaultSle->at(sfAssetsMaximum) ==
5410 Number{9223372036854776, 2, Number::Normalized{}}));
5411 }
5412 {
5413 tx[sfAssetsMaximum] = "9223372036854775807e40"; // max int64 * 10^40
5414 auto const newKeylet = keylet::vault(owner.id(), env.seq(owner));
5415 env(tx);
5416 env.close();
5417
5418 auto const vaultSle = env.le(newKeylet);
5419 if (!BEAST_EXPECT(vaultSle))
5420 return;
5421
5422 BEAST_EXPECT(
5423 (vaultSle->at(sfAssetsMaximum) ==
5424 Number{9223372036854776, 43, Number::Normalized{}}));
5425 }
5426 {
5427 tx[sfAssetsMaximum] = "9223372036854775807e-40"; // max int64 * 10^-40
5428 auto const newKeylet = keylet::vault(owner.id(), env.seq(owner));
5429 env(tx);
5430 env.close();
5431
5432 auto const vaultSle = env.le(newKeylet);
5433 if (!BEAST_EXPECT(vaultSle))
5434 return;
5435
5436 BEAST_EXPECT(
5437 (vaultSle->at(sfAssetsMaximum) ==
5438 Number{9223372036854776, -37, Number::Normalized{}}));
5439 }
5440 {
5441 tx[sfAssetsMaximum] = "9223372036854775807e-100"; // max int64 * 10^-100
5442 auto const newKeylet = keylet::vault(owner.id(), env.seq(owner));
5443 env(tx);
5444 env.close();
5445
5446 // Field 'AssetsMaximum' may not be explicitly set to default.
5447 auto const vaultSle = env.le(newKeylet);
5448 if (!BEAST_EXPECT(vaultSle))
5449 return;
5450
5451 BEAST_EXPECT(vaultSle->at(sfAssetsMaximum) == kNumZero);
5452 }
5453
5454 // What _can't_ IOUs do?
5455 // 1. Exceed maximum exponent / offset
5456 tx[sfAssetsMaximum] = "1000000000000000e81";
5457 env(tx, Ter(tefEXCEPTION));
5458 env.close();
5459
5460 // 2. Mantissa larger than uint64 max
5461 env.setParseFailureExpected(true);
5462 try
5463 {
5464 tx[sfAssetsMaximum] = "18446744073709551617e5"; // uint64 max + 1
5465 env(tx);
5466 BEAST_EXPECTS(false, "Expected parse_error for mantissa larger than uint64 max");
5467 }
5468 catch (ParseError const& e)
5469 {
5470 using namespace std::string_literals;
5471 BEAST_EXPECT(
5472 e.what() == "invalidParamsField 'tx_json.AssetsMaximum' has invalid data."s);
5473 }
5474 env.setParseFailureExpected(false);
5475 }
5476 }
5477
5478 void
5480 {
5481 using namespace test::jtx;
5482 using namespace std::literals;
5483
5484 // Verify vault deposit/withdraw/clawback respect sfLockedAmount.
5485 // When MPT tokens are escrowed, sfMPTAmount is reduced and
5486 // sfLockedAmount is increased. Vault operations go through
5487 // accountSend/accountHolds which read sfMPTAmount, so escrowed
5488 // tokens are naturally excluded.
5489
5490 {
5491 testcase("Vault deposit fails when MPT asset is escrowed");
5492
5493 Env env{*this, testableAmendments()};
5494 auto const baseFee = env.current()->fees().base;
5495 Account const owner{"owner"};
5496 Account const depositor{"depositor"};
5497 Account const issuer{"issuer"};
5498 Account const bob{"bob"};
5499
5500 env.fund(XRP(10000), issuer, owner, depositor, bob);
5501 env.close();
5502
5503 MPTTester mptt{env, issuer, kMptInitNoFund};
5504 mptt.create(
5505 {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTCanEscrow});
5506 mptt.authorize({.account = owner});
5507 mptt.authorize({.account = depositor});
5508 mptt.authorize({.account = bob});
5509 PrettyAsset const asset = mptt.issuanceID();
5510 env(pay(issuer, depositor, asset(100)));
5511 env.close();
5512
5513 // Escrow 60 of 100 MPT tokens: sfMPTAmount drops to 40
5514 auto const escrowSeq = env.seq(depositor);
5515 env(escrow::create(depositor, bob, asset(60)),
5516 escrow::kCondition(escrow::kCb1),
5517 escrow::kFinishTime(env.now() + 1s),
5518 Fee(baseFee * 150),
5519 Ter(tesSUCCESS));
5520 env.close();
5521
5522 Vault const vault{env};
5523 auto [tx, vaultKeylet] = vault.create({.owner = owner, .asset = asset});
5524 env(tx, Ter(tesSUCCESS));
5525 env.close();
5526
5527 // Deposit 100 should fail — only 40 spendable
5528 env(vault.deposit(
5529 {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}),
5531 env.close();
5532
5533 // Deposit 40 (the unlocked balance) should succeed
5534 env(vault.deposit({.depositor = depositor, .id = vaultKeylet.key, .amount = asset(40)}),
5535 Ter(tesSUCCESS));
5536 env.close();
5537
5538 {
5539 auto const sle = env.le(vaultKeylet);
5540 BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(40).value());
5541 }
5542
5543 // Clean up escrow
5544 env(escrow::finish(bob, depositor, escrowSeq),
5545 escrow::kCondition(escrow::kCb1),
5546 escrow::kFulfillment(escrow::kFb1),
5547 Fee(baseFee * 150),
5548 Ter(tesSUCCESS));
5549 env.close();
5550 }
5551
5552 {
5553 testcase("Vault withdraw respects escrowed shares");
5554
5555 Env env{*this, testableAmendments()};
5556 auto const baseFee = env.current()->fees().base;
5557 Account const owner{"owner"};
5558 Account const depositor{"depositor"};
5559 Account const issuer{"issuer"};
5560 Account const bob{"bob"};
5561
5562 env.fund(XRP(10000), issuer, owner, depositor, bob);
5563 env.close();
5564
5565 MPTTester mptt{env, issuer, kMptInitNoFund};
5566 mptt.create(
5567 {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTCanEscrow});
5568 mptt.authorize({.account = owner});
5569 mptt.authorize({.account = depositor});
5570 PrettyAsset const asset = mptt.issuanceID();
5571 env(pay(issuer, depositor, asset(100)));
5572 env.close();
5573
5574 Vault const vault{env};
5575 auto [tx, vaultKeylet] = vault.create({.owner = owner, .asset = asset});
5576 env(tx, Ter(tesSUCCESS));
5577 env.close();
5578
5579 // Deposit 100 → get shares
5580 env(vault.deposit(
5581 {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}),
5582 Ter(tesSUCCESS));
5583 env.close();
5584
5585 auto const vaultSle = env.le(vaultKeylet);
5586 if (!BEAST_EXPECT(vaultSle))
5587 return;
5588 env.memoize(Account("vault", vaultSle->at(sfAccount)));
5589 PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
5590
5591 // Authorize bob for share MPT so he can receive escrowed shares
5592 auto const shareMPTID = vaultSle->at(sfShareMPTID);
5593 {
5594 json::Value jv;
5595 jv[jss::Account] = bob.human();
5596 jv[sfMPTokenIssuanceID] = to_string(shareMPTID);
5597 jv[jss::TransactionType] = jss::MPTokenAuthorize;
5598 env(jv, Ter(tesSUCCESS));
5599 env.close();
5600 }
5601
5602 // Escrow 60% of shares
5603 auto const escrowAmount = shares(Number{6, vaultSle->at(sfScale) + 1});
5604 env(escrow::create(depositor, bob, escrowAmount),
5605 escrow::kCondition(escrow::kCb1),
5606 escrow::kFinishTime(env.now() + 1s),
5607 Fee(baseFee * 150),
5608 Ter(tesSUCCESS));
5609 env.close();
5610
5611 // Withdraw all 100 should fail — only 40% of shares are unlocked
5612 env(vault.withdraw(
5613 {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}),
5615 env.close();
5616
5617 // Withdraw 40 (matching unlocked shares) should succeed
5618 env(vault.withdraw(
5619 {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(40)}),
5620 Ter(tesSUCCESS));
5621 env.close();
5622
5623 {
5624 auto const sle = env.le(vaultKeylet);
5625 BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(60).value());
5626 }
5627 }
5628
5629 {
5630 testcase("Vault clawback only recovers unlocked shares");
5631
5632 Env env{*this, testableAmendments() | fixCleanup3_1_3};
5633 auto const baseFee = env.current()->fees().base;
5634 Account const owner{"owner"};
5635 Account const depositor{"depositor"};
5636 Account const issuer{"issuer"};
5637 Account const bob{"bob"};
5638
5639 env.fund(XRP(10000), issuer, owner, depositor, bob);
5640 env.close();
5641
5642 MPTTester mptt{env, issuer, kMptInitNoFund};
5643 mptt.create(
5644 {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTCanEscrow});
5645 mptt.authorize({.account = owner});
5646 mptt.authorize({.account = depositor});
5647 PrettyAsset const asset = mptt.issuanceID();
5648 env(pay(issuer, depositor, asset(100)));
5649 env.close();
5650
5651 Vault const vault{env};
5652 auto [tx, vaultKeylet] = vault.create({.owner = owner, .asset = asset});
5653 env(tx, Ter(tesSUCCESS));
5654 env.close();
5655
5656 // Deposit 100 → get shares
5657 env(vault.deposit(
5658 {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}),
5659 Ter(tesSUCCESS));
5660 env.close();
5661
5662 auto const vaultSle = env.le(vaultKeylet);
5663 if (!BEAST_EXPECT(vaultSle))
5664 return;
5665 env.memoize(Account("vault", vaultSle->at(sfAccount)));
5666 PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
5667
5668 // Authorize bob for share MPT so he can receive escrowed shares
5669 auto const shareMPTID = vaultSle->at(sfShareMPTID);
5670 {
5671 json::Value jv;
5672 jv[jss::Account] = bob.human();
5673 jv[sfMPTokenIssuanceID] = to_string(shareMPTID);
5674 jv[jss::TransactionType] = jss::MPTokenAuthorize;
5675 env(jv, Ter(tesSUCCESS));
5676 env.close();
5677 }
5678
5679 // Escrow 60% of shares
5680 auto const escrowAmount = shares(Number{6, vaultSle->at(sfScale) + 1});
5681 env(escrow::create(depositor, bob, escrowAmount),
5682 escrow::kCondition(escrow::kCb1),
5683 escrow::kFinishTime(env.now() + 1s),
5684 Fee(baseFee * 150),
5685 Ter(tesSUCCESS));
5686 env.close();
5687
5688 // Zero-amount clawback ("all") — should only recover assets
5689 // corresponding to unlocked shares (40%)
5690 env(vault.clawback({
5691 .issuer = issuer,
5692 .id = vaultKeylet.key,
5693 .holder = depositor,
5694 }),
5695 Ter(tesSUCCESS));
5696 env.close();
5697
5698 {
5699 auto const sle = env.le(vaultKeylet);
5700 BEAST_EXPECT(sle != nullptr);
5701 // Only 40 of 100 assets recovered (matching 40% unlocked shares)
5702 BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(60).value());
5703 BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(60).value());
5704
5705 // Depositor's unlocked shares are now 0
5706 auto const sharesAfter = env.balance(depositor, shares);
5707 BEAST_EXPECT(sharesAfter == shares(0));
5708 }
5709 }
5710 }
5711
5712 // Reproduction: canWithdraw IOU limit check bypassed when
5713 // withdrawal amount is specified in shares (MPT) rather than in assets.
5714 void
5716 {
5717 using namespace test::jtx;
5718 testcase("Bug6 - limit bypass with share-denominated withdrawal");
5719
5720 auto const allAmendments = testableAmendments() | featureSingleAssetVault;
5721
5722 for (auto const& features : {allAmendments, allAmendments - fixCleanup3_1_3})
5723 {
5724 bool const withFix = features[fixCleanup3_1_3];
5725
5726 Env env{*this, features};
5727 Account const owner{"owner"};
5728 Account const issuer{"issuer"};
5729 Account const depositor{"depositor"};
5730 Account const charlie{"charlie"};
5731 Vault const vault{env};
5732
5733 env.fund(XRP(1000), issuer, owner, depositor, charlie);
5734 env(fset(issuer, asfAllowTrustLineClawback));
5735 env.close();
5736
5737 PrettyAsset const asset = issuer["IOU"];
5738 env.trust(asset(1000), owner);
5739 env.trust(asset(1000), depositor);
5740 env(pay(issuer, owner, asset(200)));
5741 env(pay(issuer, depositor, asset(200)));
5742 env.close();
5743
5744 // Charlie gets a LOW trustline limit of 5
5745 env.trust(asset(5), charlie);
5746 env.close();
5747
5748 auto const [tx, keylet] = vault.create({.owner = owner, .asset = asset});
5749 env(tx);
5750 env.close();
5751
5752 auto const depositTx =
5753 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
5754 env(depositTx);
5755 env.close();
5756
5757 // Get the share MPT info
5758 auto const vaultSle = env.le(keylet);
5759 if (!BEAST_EXPECT(vaultSle))
5760 return;
5761 auto const mptIssuanceID = vaultSle->at(sfShareMPTID);
5762 MPTIssue const shares(mptIssuanceID);
5763 PrettyAsset const share(shares);
5764
5765 // CONTROL: Withdraw 10 IOU (asset-denominated) to charlie.
5766 // Charlie's limit is 5, so this should be rejected with tecNO_LINE
5767 // regardless of the amendment.
5768 {
5769 auto withdrawTx =
5770 vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(10)});
5771 withdrawTx[sfDestination] = charlie.human();
5772 env(withdrawTx, Ter{tecNO_LINE});
5773 env.close();
5774 }
5775 auto const charlieBalanceBefore = env.balance(charlie, asset.raw().get<Issue>());
5776
5777 // Withdraw the equivalent amount in shares to charlie.
5778 // Post-fix: rejected (tecNO_LINE) because the share amount is
5779 // converted to assets and the trustline limit is checked.
5780 // Pre-fix: succeeds (tesSUCCESS) because the limit check was
5781 // skipped for share-denominated withdrawals.
5782 {
5783 auto withdrawTx = vault.withdraw(
5784 {.depositor = depositor,
5785 .id = keylet.key,
5786 .amount = STAmount(share, 10'000'000)});
5787 withdrawTx[sfDestination] = charlie.human();
5788 env(withdrawTx, Ter{withFix ? TER{tecNO_LINE} : TER{tesSUCCESS}});
5789 env.close();
5790
5791 auto const charlieBalanceAfter = env.balance(charlie, asset.raw().get<Issue>());
5792 if (withFix)
5793 {
5794 // Post-fix: charlie's balance is unchanged — the withdrawal
5795 // was correctly rejected despite being share-denominated.
5796 BEAST_EXPECT(charlieBalanceAfter == charlieBalanceBefore);
5797 }
5798 else
5799 {
5800 // Pre-fix: charlie received the assets, bypassing the
5801 // trustline limit.
5802 BEAST_EXPECT(charlieBalanceAfter > charlieBalanceBefore);
5803 }
5804 }
5805 }
5806 }
5807
5808 void
5810 {
5811 testcase("removeEmptyHolding deletes MPToken with sfLockedAmount");
5812 using namespace test::jtx;
5813 using namespace std::literals;
5814
5815 auto const amendments = testableAmendments();
5816 auto runTest = [&](FeatureBitset f) {
5817 Env env{*this, f};
5818 auto const baseFee = env.current()->fees().base;
5819
5820 Account const issuer{"issuer"};
5821 Account const owner{"owner"};
5822 Account const depositor{"depositor"};
5823 Account const bob{"bob"};
5824
5825 env.fund(XRP(100000), issuer, owner, depositor, bob);
5826 env.close();
5827
5828 Vault const vault{env};
5829
5830 // Create an MPT asset for the vault
5831 MPTTester mptt{env, issuer, kMptInitNoFund};
5832 mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
5833 PrettyAsset const asset = mptt.issuanceID();
5834 mptt.authorize({.account = owner});
5835 mptt.authorize({.account = depositor});
5836 env(pay(issuer, depositor, asset(1000)));
5837 env.close();
5838
5839 // Create vault
5840 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
5841 env(tx);
5842 env.close();
5843
5844 auto const vaultSle = env.le(keylet);
5845 BEAST_EXPECT(vaultSle != nullptr);
5846 auto const shareMptID = vaultSle->at(sfShareMPTID);
5847 MPTIssue const shareIssue{shareMptID};
5848
5849 // Depositor deposits 1000 asset units into vault, receiving shares
5850 env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(1000)}));
5851 env.close();
5852
5853 // Check depositor has shares
5854 {
5855 auto const sleMpt = env.le(keylet::mptoken(shareMptID, depositor));
5856 BEAST_EXPECT(sleMpt != nullptr);
5857 BEAST_EXPECT(sleMpt->at(sfMPTAmount) == 1000);
5858 }
5859
5860 // Escrow 500 of those shares
5861 env(escrow::create(depositor, bob, STAmount{shareIssue, 500}),
5862 escrow::kCondition(escrow::kCb1),
5863 escrow::kFinishTime(env.now() + 1s),
5864 Fee(baseFee * 150),
5865 Ter(tesSUCCESS));
5866 env.close();
5867
5868 // Verify: sfMPTAmount=500, sfLockedAmount=500
5869 {
5870 auto const sleMpt = env.le(keylet::mptoken(shareMptID, depositor));
5871 BEAST_EXPECT(sleMpt != nullptr);
5872 BEAST_EXPECT(sleMpt->at(sfLockedAmount) == 500);
5873 BEAST_EXPECT(sleMpt->at(sfMPTAmount) == 500);
5874 }
5875
5876 // Withdraw remaining spendable shares — triggers removeEmptyHolding
5877 env(vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(500)}),
5878 Ter(tesSUCCESS));
5879 env.close();
5880
5881 auto const sleMptAfter = env.le(keylet::mptoken(shareMptID, depositor));
5882 if (!f[fixCleanup3_1_3])
5883 {
5884 // Without the fix, removeEmptyHolding deletes the MPToken
5885 // even though sfLockedAmount > 0, leaving the escrow's locked
5886 // amount untracked.
5887 BEAST_EXPECT(sleMptAfter == nullptr);
5888 }
5889 else
5890 {
5891 // With the fix, MPToken must still exist with sfLockedAmount > 0
5892 // and sfMPTAmount == 0 (all spendable shares withdrawn).
5893 BEAST_EXPECT(sleMptAfter != nullptr);
5894 if (sleMptAfter)
5895 {
5896 BEAST_EXPECT(sleMptAfter->at(sfLockedAmount) == 500);
5897 BEAST_EXPECT(sleMptAfter->at(sfMPTAmount) == 0);
5898 }
5899 }
5900 };
5901
5902 runTest(amendments - fixCleanup3_1_3);
5903 runTest(amendments);
5904 }
5905
5906 void
5908 {
5909 testcase("removeEmptyHolding keeps MPToken with confidential balances");
5910 using namespace test::jtx;
5911
5912 Env env{*this, testableAmendments()};
5913
5914 Account const issuer{"issuer"};
5915 Account const holder{"holder"};
5916 MPTTester mpt{env, issuer, {.holders = {holder}}};
5917 mpt.create({.authorize = MPTCreate::allHolders});
5918
5919 auto const tokenKeylet = keylet::mptoken(mpt.issuanceID(), holder.id());
5920 auto const encryptedBalanceFields = {
5921 &sfConfidentialBalanceInbox,
5922 &sfConfidentialBalanceSpending,
5923 &sfIssuerEncryptedBalance,
5924 &sfAuditorEncryptedBalance};
5925
5926 env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
5927 for (auto const field : encryptedBalanceFields)
5928 {
5929 Sandbox sb(&view, TapNone);
5930 auto const token = sb.peek(tokenKeylet);
5931 if (!BEAST_EXPECT(token))
5932 return false;
5933
5934 token->setFieldVL(*field, gMakeZeroBuffer(kEcGamalEncryptedTotalLength));
5935 sb.update(token);
5936
5937 BEAST_EXPECT(
5938 removeEmptyHolding(sb, holder.id(), MPTIssue(mpt.issuanceID()), j) ==
5940 BEAST_EXPECT(sb.peek(tokenKeylet) != nullptr);
5941 }
5942 return true;
5943 });
5944 }
5945
5946 // -----------------------------------------------------------------------
5947 // Helpers and tests: sole-shareholder / stuck-depositor (XLS-0065 +
5948 // fixCleanup3_2_0). The vault-level withdraw behavior is tested here;
5949 // the loan-protocol setup is incidental.
5950 // -----------------------------------------------------------------------
5951
5954
5955 // design doc:
5956 // AssetsAvailable ≈ 3,333.50
5957 // AssetsTotal ≈ 6,666.50 (3,333.50 cash + 3,333 receivable)
5958 // LossUnrealized = 3,333
5959 // OutstandingShares = sharesLender (5e9 at IOU scale 1e6)
5973
5974 static constexpr std::int64_t kStuckFunding = 1'000'000;
5975 static constexpr std::int64_t kStuckDepositorIOU = 1'000'000;
5976 static constexpr std::int64_t kStuckBorrowerIOU = 100'000;
5977 static constexpr std::int64_t kStuckDeposit = 5'000;
5978 static constexpr std::int64_t kStuckPrincipal = 3'333;
5979 static constexpr std::uint32_t kStuckPayInterval = 600;
5980 static constexpr std::uint32_t kStuckPayTotal = 2;
5981
5982 [[nodiscard]] StuckDepositorFixture
5984 {
5985 using namespace test::jtx;
5986
5988 f.asset = f.issuer[iouCurrency_];
5989
5990 env.fund(XRP(kStuckFunding), f.issuer, f.lender, f.bob, f.borrower);
5991 env.close();
5992
5993 env(trust(f.lender, (*f.asset)(10'000'000)));
5994 env(trust(f.bob, (*f.asset)(10'000'000)));
5995 env(trust(f.borrower, (*f.asset)(10'000'000)));
5996 env.close();
5997
5998 env(pay(f.issuer, f.lender, (*f.asset)(kStuckDepositorIOU)));
5999 env(pay(f.issuer, f.bob, (*f.asset)(kStuckDepositorIOU)));
6000 env(pay(f.issuer, f.borrower, (*f.asset)(kStuckBorrowerIOU)));
6001 env.close();
6002
6003 // Vault: Lender creates and seeds it; Bob matches the deposit for a
6004 // clean 50/50 split.
6005 Vault const v{env};
6006 auto [createTx, vaultKeylet] = v.create({.owner = f.lender, .asset = *f.asset});
6007 env(createTx);
6008 env.close();
6009 if (!BEAST_EXPECT(env.le(vaultKeylet)))
6010 return f;
6011 f.vaultKeylet = vaultKeylet;
6012
6013 env(v.deposit({
6014 .depositor = f.lender,
6015 .id = vaultKeylet.key,
6016 .amount = (*f.asset)(kStuckDeposit),
6017 }),
6018 Ter(tesSUCCESS));
6019 env(v.deposit({
6020 .depositor = f.bob,
6021 .id = vaultKeylet.key,
6022 .amount = (*f.asset)(kStuckDeposit),
6023 }),
6024 Ter(tesSUCCESS));
6025 env.close();
6026
6027 // Loan broker: no cover, no management fee, debt cap 10x principal.
6028 f.brokerID = keylet::loanBroker(f.lender.id(), env.seq(f.lender)).key;
6029 {
6030 using namespace loanBroker;
6031 env(set(f.lender, vaultKeylet.key),
6032 kDebtMaximum((*f.asset)(kStuckPrincipal * 10).value()));
6033 env.close();
6034 }
6035
6036 // Loan: 3,333 USD principal, impaired immediately.
6037 auto const sleBroker = env.le(keylet::loanBroker(f.brokerID));
6038 if (!BEAST_EXPECT(sleBroker))
6039 return f;
6040 f.loanKeylet = keylet::loan(f.brokerID, sleBroker->at(sfLoanSequence));
6041
6042 {
6043 using namespace loan;
6045 Sig(sfCounterpartySignature, f.lender),
6046 kPaymentTotal(kStuckPayTotal),
6047 kPaymentInterval(kStuckPayInterval),
6048 Fee(env.current()->fees().base * 2),
6049 Ter(tesSUCCESS));
6050 env.close();
6051 env(manage(f.lender, f.loanKeylet->key, tfLoanImpair), Ter(tesSUCCESS));
6052 env.close();
6053 }
6054
6055 auto const vaultSle = env.le(vaultKeylet);
6056 if (!BEAST_EXPECT(vaultSle))
6057 return f;
6058 BEAST_EXPECT(vaultSle->at(sfLossUnrealized) == (*f.asset)(kStuckPrincipal).value());
6059
6060 f.shareAsset = vaultSle->at(sfShareMPTID);
6061
6062 auto const tokenBob = env.le(keylet::mptoken(f.shareAsset, f.bob.id()));
6063 if (!BEAST_EXPECT(tokenBob))
6064 return f;
6065 std::uint64_t const sharesBob = tokenBob->getFieldU64(sfMPTAmount);
6066
6067 // Bob (non-sole) exits at the discounted rate. Always succeeds.
6068 STAmount const bobShareAmt{MPTIssue{f.shareAsset}, Number(sharesBob)};
6069 env(v.withdraw({
6070 .depositor = f.bob,
6071 .id = vaultKeylet.key,
6072 .amount = bobShareAmt,
6073 }),
6074 Ter(tesSUCCESS));
6075 env.close();
6076
6077 auto const tokenLender = env.le(keylet::mptoken(f.shareAsset, f.lender.id()));
6078 if (!BEAST_EXPECT(tokenLender))
6079 return f;
6080 f.sharesLender = tokenLender->getFieldU64(sfMPTAmount);
6081
6082 auto const sleIssuance = env.le(keylet::mptokenIssuance(f.shareAsset));
6083 if (!BEAST_EXPECT(sleIssuance))
6084 return f;
6085 BEAST_EXPECT(sleIssuance->getFieldU64(sfOutstandingAmount) == f.sharesLender);
6086
6087 auto const vaultAfterBob = env.le(vaultKeylet);
6088 if (!BEAST_EXPECT(vaultAfterBob))
6089 return f;
6090 // After Bob's exit: loss is unchanged (3,333 receivable), and the
6091 // gap between assetsTotal and assetsAvailable equals exactly that
6092 // receivable.
6093 BEAST_EXPECT(vaultAfterBob->at(sfLossUnrealized) == (*f.asset)(kStuckPrincipal).value());
6094 BEAST_EXPECT(
6095 vaultAfterBob->at(sfAssetsTotal) - vaultAfterBob->at(sfAssetsAvailable) ==
6096 vaultAfterBob->at(sfLossUnrealized));
6097
6098 return f;
6099 }
6100
6101 // Reproduces the worked example from the XLS-0065 design doc. The sole
6102 // remaining shareholder asks (via fixed-asset input) for the vault's
6103 // entire AssetsAvailable. Pre-fix this fails with the zero-sized-vault
6104 // invariant violation. Post-fix the full-price exchange rate burns
6105 // only a portion of the shares, the depositor receives all of
6106 // AssetsAvailable, and the residual shares remain backed by the
6107 // impaired-loan receivable.
6108 void
6110 {
6111 using namespace test::jtx;
6112
6113 bool const withFix = features[fixCleanup3_2_0];
6114 testcase(
6115 std::string{"Vault withdraw: sole shareholder exits via "
6116 "fixed-asset amount with impaired loan"} +
6117 (withFix ? " (fixCleanup3_2_0)" : " (pre-fix)"));
6118
6119 std::string logs;
6120 Env env(*this, features, std::make_unique<test::CaptureLogs>(&logs));
6121 auto const f = setupStuckDepositor(env);
6122 if (!f.vaultKeylet || !f.asset || f.sharesLender == 0)
6123 {
6124 BEAST_EXPECT(false);
6125 return;
6126 }
6127 Keylet const& vaultKey = *f.vaultKeylet;
6128 PrettyAsset const& asset = *f.asset;
6129
6130 auto const vaultBefore = env.le(vaultKey);
6131 if (!BEAST_EXPECT(vaultBefore))
6132 return;
6133 Number const availableBefore = vaultBefore->at(sfAssetsAvailable);
6134 Number const totalBefore = vaultBefore->at(sfAssetsTotal);
6135 Number const lossBefore = vaultBefore->at(sfLossUnrealized);
6136
6137 STAmount const lenderBalanceBefore = env.balance(f.lender, asset);
6138
6139 // The requested amount differs between feature regimes because
6140 // the two regimes are testing different behaviors:
6141 //
6142 // - Pre-fix: request the full AssetsAvailable (3,333.50). Under
6143 // the discounted formula this would burn every outstanding
6144 // share, hitting the zero-sized-vault invariant. The
6145 // transaction is rejected with tecINVARIANT_FAILED — the
6146 // stuck-depositor bug.
6147 //
6148 // - Post-fix: request a strictly smaller amount (1,000 USD).
6149 // The full-price formula burns only ~30% of the outstanding
6150 // shares; the vault retains the rest, backed by the impaired
6151 // receivable. Requesting *exactly* AssetsAvailable post-fix
6152 // would currently fail with tecINSUFFICIENT_FUNDS due to the
6153 // round-to-nearest used by assetsToSharesWithdraw (the
6154 // recomputed payout can overshoot the request by a few ULPs).
6155 // The "force payout to AssetsAvailable" branch in doApply
6156 // only triggers when every share is burned, which is covered
6157 // by the loan-repayment test.
6158 STAmount const requestAssets =
6159 withFix ? asset(1000).value() : STAmount{asset.raw(), availableBefore};
6160 Vault const v{env};
6161 env(v.withdraw({
6162 .depositor = f.lender,
6163 .id = vaultKey.key,
6164 .amount = requestAssets,
6165 }),
6166 Ter(withFix ? TER{tesSUCCESS} : TER{tecINVARIANT_FAILED}));
6167 env.close();
6168
6169 auto const vaultAfter = env.le(vaultKey);
6170 if (!BEAST_EXPECT(vaultAfter))
6171 return;
6172 auto const issuanceAfter = env.le(keylet::mptokenIssuance(f.shareAsset));
6173 if (!BEAST_EXPECT(issuanceAfter))
6174 return;
6175
6176 std::uint64_t const sharesAfter = issuanceAfter->getFieldU64(sfOutstandingAmount);
6177 Number const availableAfter = vaultAfter->at(sfAssetsAvailable);
6178 Number const totalAfter = vaultAfter->at(sfAssetsTotal);
6179 Number const lossAfter = vaultAfter->at(sfLossUnrealized);
6180
6181 if (!withFix)
6182 {
6183 // Pre-fix: rejected — vault state unchanged.
6184 BEAST_EXPECT(sharesAfter == f.sharesLender);
6185 BEAST_EXPECT(availableAfter == availableBefore);
6186 BEAST_EXPECT(totalAfter == totalBefore);
6187 BEAST_EXPECT(lossAfter == lossBefore);
6188 return;
6189 }
6190
6191 // Post-fix exact-value derivation (fixture: sharesLender=5e9,
6192 // totalBefore=6666.5, request=1000):
6193 // sharesRedeemed = round(sharesLender * request / totalBefore)
6194 // = round(750,018,750.469) = 750,018,750
6195 // received = totalBefore * sharesRedeemed / sharesLender
6196 // = 999.999999375 (slightly under 1,000 due to
6197 // integer-share rounding)
6198 constexpr std::uint64_t kExpectedSharesRedeemed = 750'018'750;
6199 Number const expectedReceived =
6200 totalBefore * Number(kExpectedSharesRedeemed) / Number(f.sharesLender);
6201
6202 BEAST_EXPECT(sharesAfter == f.sharesLender - kExpectedSharesRedeemed);
6203
6204 // LossUnrealized is unchanged: the loan-protocol side is untouched.
6205 BEAST_EXPECT(lossAfter == lossBefore);
6206
6207 // The entire (total - available) gap is the impaired receivable,
6208 // i.e. equal to lossUnrealized.
6209 BEAST_EXPECT(totalAfter - availableAfter == lossAfter);
6210
6211 STAmount const lenderBalanceAfter = env.balance(f.lender, asset);
6212 Number const received{lenderBalanceAfter - lenderBalanceBefore};
6213 BEAST_EXPECT(received == expectedReceived);
6214
6215 // Conservation: assets removed from the vault equal what the
6216 // depositor received.
6217 BEAST_EXPECT(totalBefore - totalAfter == received);
6218 BEAST_EXPECT(availableBefore - availableAfter == received);
6219 }
6220
6221 // Sole shareholder attempts to burn ALL outstanding shares via
6222 // fixed-shares input while the vault still holds an impaired
6223 // receivable. Pre-fix this fails with the zero-sized-vault invariant
6224 // violation. Post-fix the full-price rate causes assetsWithdrawn to
6225 // equal assetsTotal, which exceeds assetsAvailable, so the transaction
6226 // is rejected with tecINSUFFICIENT_FUNDS.
6227 void
6229 {
6230 using namespace test::jtx;
6231
6232 bool const withFix = features[fixCleanup3_2_0];
6233 testcase(
6234 std::string{"Vault withdraw: sole shareholder full-shares "
6235 "burn is rejected while loss outstanding"} +
6236 (withFix ? " (fixCleanup3_2_0)" : " (pre-fix)"));
6237
6238 std::string logs;
6239 Env env(*this, features, std::make_unique<test::CaptureLogs>(&logs));
6240 auto const f = setupStuckDepositor(env);
6241 if (!f.vaultKeylet || f.sharesLender == 0)
6242 {
6243 BEAST_EXPECT(false);
6244 return;
6245 }
6246 Keylet const& vaultKey = *f.vaultKeylet;
6247
6248 auto const vaultBefore = env.le(vaultKey);
6249 if (!BEAST_EXPECT(vaultBefore))
6250 return;
6251 Number const availableBefore = vaultBefore->at(sfAssetsAvailable);
6252 Number const totalBefore = vaultBefore->at(sfAssetsTotal);
6253 Number const lossBefore = vaultBefore->at(sfLossUnrealized);
6254
6255 // Fixed-shares input: ask for ALL outstanding shares.
6256 STAmount const shareAmt{MPTIssue{f.shareAsset}, Number(f.sharesLender)};
6257 Vault const v{env};
6258 env(v.withdraw({
6259 .depositor = f.lender,
6260 .id = vaultKey.key,
6261 .amount = shareAmt,
6262 }),
6264 env.close();
6265
6266 // Either way the transaction was rejected; vault state unchanged.
6267 auto const vaultAfter = env.le(vaultKey);
6268 if (!BEAST_EXPECT(vaultAfter))
6269 return;
6270 auto const issuanceAfter = env.le(keylet::mptokenIssuance(f.shareAsset));
6271 if (!BEAST_EXPECT(issuanceAfter))
6272 return;
6273 BEAST_EXPECT(issuanceAfter->getFieldU64(sfOutstandingAmount) == f.sharesLender);
6274 BEAST_EXPECT(vaultAfter->at(sfAssetsAvailable) == availableBefore);
6275 BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == totalBefore);
6276 BEAST_EXPECT(vaultAfter->at(sfLossUnrealized) == lossBefore);
6277 }
6278
6279 // Post-fix end-to-end resolution: after the sole-shareholder partial
6280 // exit, the loan is repaid in full. With unrealized loss cleared and
6281 // all assets back as cash, the depositor can burn all remaining
6282 // shares and fully exit the vault. The final withdrawal hits the
6283 // "force payout to assetsAvailable" branch in doApply.
6284 void
6286 {
6287 using namespace test::jtx;
6288 using namespace loan;
6289
6290 testcase(
6291 "Vault withdraw: sole shareholder fully exits after impaired "
6292 "loan is repaid (fixCleanup3_2_0)");
6293
6294 Env env(*this, all_ | fixCleanup3_2_0);
6295 auto const f = setupStuckDepositor(env);
6296 if (!f.vaultKeylet || !f.asset || !f.loanKeylet || f.sharesLender == 0)
6297 {
6298 BEAST_EXPECT(false);
6299 return;
6300 }
6301 Keylet const& vaultKey = *f.vaultKeylet;
6302 Keylet const& loanKey = *f.loanKeylet;
6303 PrettyAsset const& asset = *f.asset;
6304
6305 Vault const v{env};
6306
6307 // Sole-shareholder partial exit (see comment in
6308 // testWithdrawSoleShareholderFixedAssetExit for why we request
6309 // less than full AssetsAvailable).
6310 {
6311 STAmount const requestAssets = asset(1000).value();
6312 env(v.withdraw({
6313 .depositor = f.lender,
6314 .id = vaultKey.key,
6315 .amount = requestAssets,
6316 }),
6317 Ter(tesSUCCESS));
6318 env.close();
6319 }
6320
6321 // Confirm the "dormant-but-alive" state from the design doc. The
6322 // partial exit burned exactly 750,018,750 shares (see derivation
6323 // in testWithdrawSoleShareholderFixedAssetExit).
6324 auto const tokenAfterExit = env.le(keylet::mptoken(f.shareAsset, f.lender.id()));
6325 if (!BEAST_EXPECT(tokenAfterExit))
6326 return;
6327 std::uint64_t const retainedShares = tokenAfterExit->getFieldU64(sfMPTAmount);
6328 BEAST_EXPECT(retainedShares == f.sharesLender - 750'018'750);
6329
6330 // Borrower repays the loan in full (pays more than the outstanding
6331 // total; the loan transactor caps the receivable).
6332 env(pay(f.borrower, loanKey.key, asset(kStuckPrincipal * 2)), Ter(tesSUCCESS));
6333 env.close();
6334
6335 auto const vaultAfterRepay = env.le(vaultKey);
6336 if (!BEAST_EXPECT(vaultAfterRepay))
6337 return;
6338 // Repayment converts the 3,333 receivable back to cash; assetsTotal
6339 // is unchanged but assetsAvailable jumps by exactly the same amount,
6340 // and lossUnrealized clears to zero.
6341 BEAST_EXPECT(vaultAfterRepay->at(sfLossUnrealized) == beast::kZero);
6342 BEAST_EXPECT(vaultAfterRepay->at(sfAssetsAvailable) == vaultAfterRepay->at(sfAssetsTotal));
6343
6344 STAmount const lenderBalanceBeforeFinal = env.balance(f.lender, asset);
6345 Number const availableBeforeFinal = vaultAfterRepay->at(sfAssetsAvailable);
6346
6347 // Burn all remaining shares — the clean-state preconditions of
6348 // the "final withdrawal" guard are now satisfied.
6349 STAmount const allShares{MPTIssue{f.shareAsset}, Number(retainedShares)};
6350 env(v.withdraw({
6351 .depositor = f.lender,
6352 .id = vaultKey.key,
6353 .amount = allShares,
6354 }),
6355 Ter(tesSUCCESS));
6356 env.close();
6357
6358 auto const vaultFinal = env.le(vaultKey);
6359 if (!BEAST_EXPECT(vaultFinal))
6360 return;
6361 auto const issuanceFinal = env.le(keylet::mptokenIssuance(f.shareAsset));
6362 if (!BEAST_EXPECT(issuanceFinal))
6363 return;
6364
6365 // Zero-sized vault invariant satisfied: 0 shares, 0 assets.
6366 BEAST_EXPECT(issuanceFinal->getFieldU64(sfOutstandingAmount) == 0);
6367 BEAST_EXPECT(vaultFinal->at(sfAssetsTotal) == beast::kZero);
6368 BEAST_EXPECT(vaultFinal->at(sfAssetsAvailable) == beast::kZero);
6369 BEAST_EXPECT(vaultFinal->at(sfLossUnrealized) == beast::kZero);
6370
6371 // The final payout equals exactly the AssetsAvailable that
6372 // existed before the call (the "force payout" branch).
6373 STAmount const lenderBalanceAfter = env.balance(f.lender, asset);
6374 Number const finalReceived{lenderBalanceAfter - lenderBalanceBeforeFinal};
6375 BEAST_EXPECT(finalReceived == availableBeforeFinal);
6376 }
6377
6378 // Clean-state regression: with no impaired loan, a sole shareholder
6379 // burning all their shares fully empties the vault under both the
6380 // pre-fix and post-fix code paths. Confirms the new logic doesn't
6381 // break the existing happy-path close-out.
6382 void
6384 {
6385 using namespace test::jtx;
6386
6387 bool const withFix = features[fixCleanup3_2_0];
6388 testcase(
6389 std::string{"Vault withdraw: sole shareholder clean-state "
6390 "close-out unchanged"} +
6391 (withFix ? " (fixCleanup3_2_0)" : " (pre-fix)"));
6392
6393 Env env(*this, features);
6394
6395 Account const issuer{"issuer"};
6396 Account const lender{"lender"};
6397
6398 env.fund(XRP(kStuckFunding), issuer, lender);
6399 env.close();
6400
6401 PrettyAsset const asset = issuer[iouCurrency_];
6402 env(trust(lender, asset(10'000'000)));
6403 env.close();
6404 env(pay(issuer, lender, asset(kStuckDepositorIOU)));
6405 env.close();
6406
6407 // Sole shareholder of a clean vault — no loan broker needed.
6408 Vault const v{env};
6409 auto [createTx, vaultKeylet] = v.create({.owner = lender, .asset = asset});
6410 env(createTx);
6411 env.close();
6412
6413 env(v.deposit({
6414 .depositor = lender,
6415 .id = vaultKeylet.key,
6416 .amount = asset(kStuckDeposit),
6417 }),
6418 Ter(tesSUCCESS));
6419 env.close();
6420
6421 auto const vaultBefore = env.le(vaultKeylet);
6422 if (!BEAST_EXPECT(vaultBefore))
6423 return;
6424 auto const shareAsset = vaultBefore->at(sfShareMPTID);
6425 auto const tokenLender = env.le(keylet::mptoken(shareAsset, lender.id()));
6426 if (!BEAST_EXPECT(tokenLender))
6427 return;
6428 std::uint64_t const sharesLender = tokenLender->getFieldU64(sfMPTAmount);
6429
6430 // Sole shareholder, no loans, no loss. Burn everything.
6431 STAmount const allShares{MPTIssue{shareAsset}, Number(sharesLender)};
6432 env(v.withdraw({
6433 .depositor = lender,
6434 .id = vaultKeylet.key,
6435 .amount = allShares,
6436 }),
6437 Ter(tesSUCCESS));
6438 env.close();
6439
6440 auto const vaultFinal = env.le(vaultKeylet);
6441 if (!BEAST_EXPECT(vaultFinal))
6442 return;
6443 auto const issuanceFinal = env.le(keylet::mptokenIssuance(shareAsset));
6444 if (!BEAST_EXPECT(issuanceFinal))
6445 return;
6446 BEAST_EXPECT(issuanceFinal->getFieldU64(sfOutstandingAmount) == 0);
6447 BEAST_EXPECT(vaultFinal->at(sfAssetsTotal) == beast::kZero);
6448 BEAST_EXPECT(vaultFinal->at(sfAssetsAvailable) == beast::kZero);
6449 BEAST_EXPECT(vaultFinal->at(sfLossUnrealized) == beast::kZero);
6450
6451 // (Pre-fix path takes the regular code path; post-fix path enters
6452 // the new final-withdrawal guard, which forces payout to exactly
6453 // assetsAvailable. Either way the result is identical for a clean
6454 // vault.)
6455 (void)withFix;
6456 }
6457
6458 // Sole shareholder in an impaired vault redeems a *partial* count of
6459 // shares via fixed-shares input. Pre-fix the discounted formula is
6460 // used; post-fix the full-price formula is used (waiveUnrealizedLoss
6461 // = Yes). The relative payout therefore differs, and post-fix the
6462 // depositor recovers proportionally more of the residual cash for
6463 // the shares burned. In both cases the vault is left in a valid
6464 // (non-empty) state.
6465 void
6467 {
6468 using namespace test::jtx;
6469
6470 testcase(
6471 "Vault withdraw: sole-shareholder partial fixed-shares uses "
6472 "full-price rate (fixCleanup3_2_0)");
6473
6474 Env env(*this, all_ | fixCleanup3_2_0);
6475 auto const f = setupStuckDepositor(env);
6476 if (!f.vaultKeylet || !f.asset || f.sharesLender == 0)
6477 {
6478 BEAST_EXPECT(false);
6479 return;
6480 }
6481 Keylet const& vaultKey = *f.vaultKeylet;
6482 PrettyAsset const& asset = *f.asset;
6483
6484 auto const vaultBefore = env.le(vaultKey);
6485 if (!BEAST_EXPECT(vaultBefore))
6486 return;
6487 Number const totalBefore = vaultBefore->at(sfAssetsTotal);
6488 Number const availableBefore = vaultBefore->at(sfAssetsAvailable);
6489 Number const lossBefore = vaultBefore->at(sfLossUnrealized);
6490
6491 // Burn exactly half of the outstanding shares.
6492 std::uint64_t const halfShares = f.sharesLender / 2;
6493 STAmount const halfAmt{MPTIssue{f.shareAsset}, Number(halfShares)};
6494
6495 STAmount const lenderBalanceBefore = env.balance(f.lender, asset);
6496
6497 Vault const v{env};
6498 env(v.withdraw({
6499 .depositor = f.lender,
6500 .id = vaultKey.key,
6501 .amount = halfAmt,
6502 }),
6503 Ter(tesSUCCESS));
6504 env.close();
6505
6506 // Expected payout under the full-price formula:
6507 // assets = totalBefore * halfShares / sharesLender
6508 // which (with halfShares == sharesLender/2) is roughly
6509 // totalBefore / 2.
6510 STAmount const lenderBalanceAfter = env.balance(f.lender, asset);
6511 Number const received{lenderBalanceAfter - lenderBalanceBefore};
6512 Number const expected = totalBefore * Number(halfShares) / Number(f.sharesLender);
6513 BEAST_EXPECT(received == expected);
6514
6515 // The full-price payout exceeds the discounted formula by exactly
6516 // lossBefore * halfShares / sharesLender — that's the whole point
6517 // of the waive.
6518 Number const discounted =
6519 (totalBefore - lossBefore) * Number(halfShares) / Number(f.sharesLender);
6520 Number const expectedDelta = lossBefore * Number(halfShares) / Number(f.sharesLender);
6521 BEAST_EXPECT(received - discounted == expectedDelta);
6522
6523 auto const vaultAfter = env.le(vaultKey);
6524 if (!BEAST_EXPECT(vaultAfter))
6525 return;
6526 auto const issuanceAfter = env.le(keylet::mptokenIssuance(f.shareAsset));
6527 if (!BEAST_EXPECT(issuanceAfter))
6528 return;
6529
6530 // Vault remains valid: half the shares remain, lossUnrealized
6531 // is untouched, and the entire (total - available) gap is still
6532 // the impaired receivable.
6533 BEAST_EXPECT(
6534 issuanceAfter->getFieldU64(sfOutstandingAmount) == f.sharesLender - halfShares);
6535 BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == totalBefore - received);
6536 BEAST_EXPECT(vaultAfter->at(sfLossUnrealized) == lossBefore);
6537 BEAST_EXPECT(
6538 vaultAfter->at(sfAssetsTotal) - vaultAfter->at(sfAssetsAvailable) ==
6539 vaultAfter->at(sfLossUnrealized));
6540
6541 // Conservation: vault delta matches the depositor's gain.
6542 BEAST_EXPECT(totalBefore - vaultAfter->at(sfAssetsTotal) == received);
6543 BEAST_EXPECT(availableBefore - vaultAfter->at(sfAssetsAvailable) == received);
6544 }
6545
6546 // Bug: DeltaInfo::makeDelta uses max(scale(after), scale(before)) for the
6547 // sfAssetsTotal and sfAssetsAvailable deltas, and visitEntry applies the
6548 // same max() for the vault pseudo-account RippleState. When
6549 // sfAssetsTotal sits exactly at 1e16 (IOU exponent 1, ULP = 10) and a
6550 // withdrawal of 5 USD brings it to 9.999...995e15 (IOU exponent 0,
6551 // ULP = 1), all three computations pick the anterior coarser scale 1.
6552 // roundToAsset(-5, scale=1) collapses to 0, so the invariant check
6553 // vaultPseudoDeltaAssets >= kZero fires even though the state change is
6554 // valid and fully consistent at IOU precision.
6555 //
6556 // Fix (fixCleanup3_2_0): finalize compares the vault pseudo-account and
6557 // sfAssetsTotal/Available deltas directly in Number space, bypassing
6558 // scale-coarsened rounding.
6559 void
6561 {
6562 using namespace test::jtx;
6563
6564 auto runScenario = [this](FeatureBitset features, TER expected) {
6565 std::string logs;
6566 Env env(*this, features, std::make_unique<test::CaptureLogs>(&logs));
6567
6568 Account const issuer{"issuer"};
6569 Account const alice{"alice"};
6570
6571 env.fund(XRP(100'000), issuer, alice);
6572 env.close();
6573 env(fset(issuer, asfDefaultRipple));
6574 env.close();
6575
6576 PrettyAsset const usd{issuer["USD"]};
6577 // Trust limit of 2e16, fund exactly 1e16 so deposit lands at the
6578 // IOU scale-1 boundary (exponent 1, ULP = 10).
6579 STAmount const fundAndDeposit{usd.raw(), Number{1, 16}};
6580
6581 env(trust(alice, STAmount{usd.raw(), 2, 16}));
6582 env.close();
6583 env(pay(issuer, alice, fundAndDeposit));
6584 env.close();
6585
6586 Vault const vault{env};
6587 auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = usd});
6588 vaultTx[sfScale] = 0;
6589 env(vaultTx);
6590 env.close();
6591
6592 // sfAssetsTotal = sfAssetsAvailable = 1e16 (exponent 1, ULP = 10).
6593 env(vault.deposit(
6594 {.depositor = alice, .id = vaultKeylet.key, .amount = fundAndDeposit}));
6595 env.close();
6596
6597 // Withdraw 5 USD: -5 is sub-ULP at the anterior scale (ULP = 10)
6598 // but exact at the posterior scale (ULP = 1). The state change is
6599 // consistent; only the invariant's scale selection is wrong.
6600 env(vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = usd(5)}),
6601 Ter(expected));
6602 env.close();
6603 };
6604
6605 {
6606 testcase(
6607 "bug: VaultWithdraw across IOU scale boundary fires invariant "
6608 "(pre-fixCleanup3_2_0)");
6609 runScenario(testableAmendments() - fixCleanup3_2_0, tecINVARIANT_FAILED);
6610 }
6611 {
6612 testcase(
6613 "bug: VaultWithdraw across IOU scale boundary succeeds "
6614 "(post-fixCleanup3_2_0)");
6615 runScenario(testableAmendments(), tesSUCCESS);
6616 }
6617 }
6618
6619 // Bug: DeltaInfo::makeDelta uses max(scale(after), scale(before)) for
6620 // sfAssetsTotal/Available deltas. This is symmetric to
6621 // testBugMakeDeltaAnteriorScale but in the opposite direction: a deposit
6622 // pushes assetsTotal from just below 1e16 (IOU exponent 0, ULP = 1) to just
6623 // above it (exponent 1, ULP = 10). makeDelta picks the coarser *posterior*
6624 // scale 1. The trust line balance rounds from atEdge + 2 = 10,000,000,000,000,001
6625 // → 1e16, so the pseudo-account delta is only +1 in IOU space.
6626 // roundToAsset(+1, scale=1) = 0 fires "deposit must increase vault balance"
6627 // even though the state change is consistent at every precision boundary.
6628 //
6629 // Fix (fixCleanup3_2_0): computeVaultMinScale uses the posterior Number-space
6630 // scale of sfAssetsTotal (which retains the full value 10,000,000,000,000,001,
6631 // exponent 0), giving minScale = 0. roundToAsset(+1, scale=0) = 1 > 0 and
6632 // the invariant passes. However the transactor's own precision guard fires
6633 // first (bob pays 2 USD, vault receives only 1 due to IOU rounding), so the
6634 // post-amendment result is tecPRECISION_LOSS rather than tesSUCCESS —
6635 // the depositor is protected from silently losing 1 USD to rounding.
6636 void
6638 {
6639 using namespace test::jtx;
6640
6641 auto runScenario = [this](FeatureBitset features, TER expected) {
6642 std::string logs;
6643 Env env(*this, features, std::make_unique<test::CaptureLogs>(&logs));
6644
6645 Account const issuer{"issuer"};
6646 Account const alice{"alice"};
6647 Account const bob{"bob"};
6648
6649 env.fund(XRP(100'000), issuer, alice, bob);
6650 env.close();
6651 env(fset(issuer, asfDefaultRipple));
6652 env.close();
6653
6654 PrettyAsset const usd{issuer["USD"]};
6655 // atEdge is the largest IOU value with exponent 0 (ULP = 1).
6656 // A deposit of 2 USD brings assetsTotal to 10,000,000,000,000,001
6657 // in Number space, crossing the 1e16 boundary in IOU space.
6658 STAmount const atEdge{usd.raw(), Number{9'999'999'999'999'999LL}};
6659
6660 env(trust(alice, STAmount{usd.raw(), 2, 16}));
6661 env(trust(bob, usd(100)));
6662 env.close();
6663 env(pay(issuer, alice, atEdge));
6664 env(pay(issuer, bob, usd(2)));
6665 env.close();
6666
6667 Vault const vault{env};
6668 auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = usd});
6669 vaultTx[sfScale] = 0;
6670 env(vaultTx);
6671 env.close();
6672
6673 // sfAssetsTotal = sfAssetsAvailable = atEdge (exponent 0, ULP = 1)
6674 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = atEdge}));
6675 env.close();
6676
6677 // Deposit 2 USD: +2 is sub-ULP at the posterior IOU scale (ULP = 10)
6678 // but exact at the Number scale retained by sfAssetsTotal.
6679 env(vault.deposit({.depositor = bob, .id = vaultKeylet.key, .amount = usd(2)}),
6680 Ter(expected));
6681 env.close();
6682 };
6683
6684 {
6685 testcase(
6686 "bug: VaultDeposit across IOU scale boundary fires invariant "
6687 "(pre-fixCleanup3_2_0)");
6688 runScenario(testableAmendments() - fixCleanup3_2_0, tecINVARIANT_FAILED);
6689 }
6690 {
6691 testcase(
6692 "bug: VaultDeposit across IOU scale boundary succeeds "
6693 "(post-fixCleanup3_2_0)");
6694 runScenario(testableAmendments(), tecPRECISION_LOSS);
6695 }
6696 }
6697
6698 // Bug: ValidVault::visitEntry computes destinationDelta.scale as
6699 // max(before_exponent, after_exponent) for RippleState entries. When a
6700 // withdrawal credits a destination whose IOU balance sits just below a
6701 // power-of-10 boundary (atEdge = 9'999'999'999'999'999), the post-credit
6702 // STAmount rounds up one exponent (exponent 0 → 1), making
6703 // destinationDelta.scale = 1. The invariant then calls
6704 // roundToAsset(+2 USD, scale=1) = 0 and incorrectly fires
6705 // "withdrawal must increase destination balance".
6706 //
6707 // Fix (fixCleanup3_2_0): finalize compares destination delta directly in
6708 // Number space, bypassing scale-coarsened rounding. The transaction
6709 // itself succeeds because the effective IOU credit is non-trivial at
6710 // Number precision even though the STAmount exponent shifted.
6711 void
6713 {
6714 using namespace test::jtx;
6715
6716 enum class DestKind : bool { ThirdParty = false, Self = true };
6717
6718 auto runScenario = [this](FeatureBitset features, DestKind destKind, TER expected) {
6719 std::string logs;
6720 Env env(*this, features, std::make_unique<test::CaptureLogs>(&logs));
6721
6722 Account const issuer{"issuer"};
6723 Account const alice{"alice"};
6724 Account const bob{"bob"};
6725
6726 env.fund(XRP(100'000), issuer, alice, bob);
6727 env.close();
6728 env(fset(issuer, asfDefaultRipple));
6729 env.close();
6730
6731 PrettyAsset const usd{issuer["USD"]};
6732 STAmount const aliceLimit{usd.raw(), 2, 16};
6733 STAmount const bobLimit{usd.raw(), 2, 16};
6734 STAmount const atEdge{usd.raw(), Number{9'999'999'999'999'999LL}};
6735
6736 env(trust(alice, aliceLimit));
6737 if (destKind == DestKind::ThirdParty)
6738 env(trust(bob, bobLimit));
6739 env.close();
6740
6741 env(pay(issuer, alice, usd(1'000)));
6742 if (destKind == DestKind::ThirdParty)
6743 env(pay(issuer, bob, atEdge));
6744 env.close();
6745
6746 Vault const vault{env};
6747 auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = usd});
6748 vaultTx[sfScale] = 0;
6749 env(vaultTx);
6750 env.close();
6751
6752 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = usd(1'000)}));
6753 env.close();
6754
6755 // For the self-destination case, push alice's own trust line to
6756 // the IOU edge so the next withdraw inflow crosses the boundary.
6757 if (destKind == DestKind::Self)
6758 {
6759 env(pay(issuer, alice, atEdge));
6760 env.close();
6761 }
6762
6763 auto tx = vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = usd(2)});
6764 if (destKind == DestKind::ThirdParty)
6765 tx[sfDestination] = bob.human();
6766 env(tx, Ter(expected));
6767 env.close();
6768 };
6769
6770 {
6771 testcase(
6772 "bug: VaultWithdraw to third-party at IOU edge fires invariant "
6773 "(pre-fixCleanup3_2_0)");
6774 runScenario(
6775 testableAmendments() - fixCleanup3_2_0, DestKind::ThirdParty, tecINVARIANT_FAILED);
6776 }
6777 {
6778 testcase(
6779 "bug: VaultWithdraw to third-party at IOU edge succeeds "
6780 "(post-fixCleanup3_2_0)");
6781 runScenario(testableAmendments(), DestKind::ThirdParty, tesSUCCESS);
6782 }
6783 {
6784 testcase(
6785 "bug: VaultWithdraw to self at IOU edge fires invariant "
6786 "(pre-fixCleanup3_2_0)");
6787 runScenario(
6788 testableAmendments() - fixCleanup3_2_0, DestKind::Self, tecINVARIANT_FAILED);
6789 }
6790 {
6791 testcase(
6792 "bug: VaultWithdraw to self at IOU edge succeeds "
6793 "(post-fixCleanup3_2_0)");
6794 runScenario(testableAmendments(), DestKind::Self, tesSUCCESS);
6795 }
6796 }
6797
6798 // Bug: the equality check (vault outflow == destination inflow) was
6799 // skipped whenever the destination delta rounded to zero at localMinScale,
6800 // including cases where the vault outflow rounded to a non-zero value and
6801 // a representable amount of value was genuinely destroyed.
6802 //
6803 // Scenario: Bob's IOU balance sits 5 units below the 10^16 STAmount
6804 // precision boundary (atEdge2 = 9,999,999,999,999,995). A withdrawal of
6805 // 6 USD shifts his balance across that boundary: the exponent increments
6806 // (0 → 1), so his effective inflow in Number space is only +5 — 1 USD is
6807 // consumed by the precision-boundary rounding and cannot be credited.
6808 //
6809 // The destroyed amount (1 USD) is sub-ULP at destinationScale=1 (step=10),
6810 // so the check treats it as an unavoidable IOU-precision artefact and
6811 // lets the transaction succeed.
6812 //
6813 // Contrast: if 15 USD were destroyed at the same scale (destroyed ≥ step),
6814 // floor(15/10)=1 ≠ 0 and the invariant would fire — that discrepancy IS
6815 // representable and indicates a real accounting bug.
6816 //
6817 // Pre-fixCleanup3_2_0: the "must increase destination balance" check fires
6818 // because roundedDestinationDelta = 0 ≤ 0.
6819 void
6821 {
6822 using namespace test::jtx;
6823
6824 auto runScenario = [this](FeatureBitset features, TER expected) {
6825 std::string logs;
6826 Env env(*this, features, std::make_unique<test::CaptureLogs>(&logs));
6827
6828 Account const issuer{"issuer"};
6829 Account const alice{"alice"};
6830 Account const bob{"bob"};
6831
6832 env.fund(XRP(100'000), issuer, alice, bob);
6833 env.close();
6834 env(fset(issuer, asfDefaultRipple));
6835 env.close();
6836
6837 PrettyAsset const usd{issuer["USD"]};
6838 STAmount const aliceLimit{usd.raw(), 2, 16};
6839 STAmount const bobLimit{usd.raw(), 2, 16};
6840 // Bob's balance sits 5 units below the 10^16 STAmount precision
6841 // boundary. Receiving 6 USD shifts his exponent 0 → 1; the
6842 // STAmount records +5, not +6 (1 USD is lost to rounding).
6843 STAmount const atEdge2{usd.raw(), Number{9'999'999'999'999'995LL}};
6844
6845 env(trust(alice, aliceLimit));
6846 env(trust(bob, bobLimit));
6847 env.close();
6848
6849 env(pay(issuer, alice, usd(1'000)));
6850 env(pay(issuer, bob, atEdge2));
6851 env.close();
6852
6853 Vault const vault{env};
6854 auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = usd});
6855 vaultTx[sfScale] = 0;
6856 env(vaultTx);
6857 env.close();
6858
6859 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = usd(1'000)}));
6860 env.close();
6861
6862 // Withdraw 6 USD to Bob: vault loses 6, Bob gains only 5.
6863 // Destroyed amount = 1 USD, which is sub-ULP at destinationScale=1.
6864 auto tx = vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = usd(6)});
6865 tx[sfDestination] = bob.human();
6866 env(tx, Ter(expected));
6867 env.close();
6868 };
6869
6870 {
6871 testcase(
6872 "bug: VaultWithdraw to destination at IOU precision boundary fires "
6873 "invariant (pre-fixCleanup3_2_0)");
6874 runScenario(testableAmendments() - fixCleanup3_2_0, tecINVARIANT_FAILED);
6875 }
6876 {
6877 testcase(
6878 "bug: VaultWithdraw to destination at IOU precision boundary succeeds "
6879 "when destroyed amount is sub-ULP (post-fixCleanup3_2_0)");
6880 runScenario(testableAmendments(), tesSUCCESS);
6881 }
6882 }
6883
6884 // Bug: when a depositor's IOU trustline balance is very large (e.g.
6885 // ~1e17), adding a small deposit (e.g. 1 USD) leaves sfAssetsTotal
6886 // unchanged at IOU precision because the increment is sub-ULP at the
6887 // vault's current asset scale. The vault records the deposit, mints
6888 // shares, and decrements the depositor's trustline, but sfAssetsTotal
6889 // does not change — the conservation invariant fires because the rail
6890 // delta is zero.
6891 //
6892 // Two sub-cases are exercised:
6893 // 1. First-ever deposit into an empty vault: the depositor's own
6894 // trustline has a large balance so 1 USD canonicalizes to zero
6895 // when written back through the IOU rail.
6896 // 2. Subsequent deposit after the vault already holds a large
6897 // sfAssetsTotal: a different depositor (bob, with a small balance)
6898 // sends 1 USD, which again rounds to zero at the vault's coarse
6899 // asset scale.
6900 //
6901 // Fix (fixCleanup3_2_0): the deposit transactor checks whether
6902 // roundToAsset(amount, vault_scale) == 0 and rejects early with
6903 // tecPRECISION_LOSS before any state is modified.
6904 void
6906 {
6907 using namespace test::jtx;
6908 auto runScenario = [this](FeatureBitset features, TER expected) {
6909 std::string logs;
6910 Env env(*this, features, std::make_unique<test::CaptureLogs>(&logs));
6911
6912 Account const issuer{"issuer"};
6913 Account const alice{"alice"};
6914 Account const bob{"bob"};
6915
6916 env.fund(XRP(100'000), issuer, alice, bob);
6917 env.close();
6918
6919 env(fset(issuer, asfDefaultRipple));
6920 env.close();
6921
6922 PrettyAsset const usd{issuer["USD"]};
6923
6924 STAmount const trustLimit{usd.raw(), Number{99'999'999'999'999'999LL}};
6925 STAmount const aliceFund{usd.raw(), Number{99'999'999'999'999'999LL}};
6926
6927 env(trust(alice, trustLimit));
6928 env(trust(bob, trustLimit));
6929 env.close();
6930
6931 env(pay(issuer, alice, aliceFund));
6932 env(pay(issuer, bob, usd(1000)));
6933 env.close();
6934
6935 Vault const vault{env};
6936
6937 // Scale=0 so sfAssetsTotal stores whole USD
6938 auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = usd});
6939 vaultTx[sfScale] = 0;
6940 env(vaultTx);
6941 env.close();
6942
6943 // Alice's deposit canonicalizes to zero at her own trustline scale
6944 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = usd(1)}),
6945 Ter(expected));
6946
6947 // Increase vault-scale
6948 env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = aliceFund}));
6949 env.close();
6950
6951 env(vault.deposit({.depositor = bob, .id = vaultKeylet.key, .amount = usd(1)}),
6952 Ter(expected));
6953 env.close();
6954 };
6955
6956 {
6957 testcase(
6958 "bug: VaultDeposit below Vault precision canonicalized to zero "
6959 "(pre-fixCleanup3_2_0)");
6960 runScenario(testableAmendments() - fixCleanup3_2_0, tecINVARIANT_FAILED);
6961 }
6962 {
6963 testcase(
6964 "bug: VaultDeposit below Vault precision canonicalized to zero "
6965 "(post-fixCleanup3_2_0)");
6966 runScenario(testableAmendments(), tecPRECISION_LOSS);
6967 }
6968 }
6969
6970 // VaultDeposit by issuer with the vault parked at the IOU 16-digit
6971 // edge (9.999e15). Issuer mints 2 more USD; the vault trust line
6972 // goes 9.999e15 → 10^16, gaining 1 unit instead of 2 (canonicalization).
6973 //
6974 // Pre-fixCleanup3_2_0: the proactive check is absent; the deposit
6975 // applies, then VaultInvariant's "deposit must increase vault
6976 // balance" assertion fires at finalize time on the rounded vault
6977 // delta of zero, returning tecINVARIANT_FAILED.
6978 // Post-amendment: reject deposit that is not representable at Vault scale.
6979 void
6981 {
6982 using namespace test::jtx;
6983
6984 auto runScenario = [this](FeatureBitset features, TER expected) {
6985 std::string logs;
6986 Env env(*this, features, std::make_unique<test::CaptureLogs>(&logs));
6987
6988 Account const issuer{"issuer"};
6989 Account const owner{"owner"};
6990
6991 env.fund(XRP(100'000), issuer, owner);
6992 env.close();
6993 env(fset(issuer, asfDefaultRipple));
6994 env.close();
6995
6996 PrettyAsset const usd{issuer["USD"]};
6997 STAmount const trustLimit{usd.raw(), 2, 16};
6998 STAmount const ownerFund{usd.raw(), Number{9'999'999'999'999'999LL}};
6999
7000 env(trust(owner, trustLimit));
7001 env.close();
7002 env(pay(issuer, owner, ownerFund));
7003 env.close();
7004
7005 Vault const vault{env};
7006 auto [vaultTx, vaultKeylet] = vault.create({.owner = owner, .asset = usd});
7007 vaultTx[sfScale] = 0;
7008 env(vaultTx);
7009 env.close();
7010 env(vault.deposit({.depositor = owner, .id = vaultKeylet.key, .amount = ownerFund}));
7011 env.close();
7012
7013 // Vault pseudo-account is now at 9.999e15. Issuer mints 2
7014 // more USD. Pre: tecINVARIANT_FAILED at finalize. Post:
7015 // tecPRECISION_LOSS proactively. Either way, no value moves.
7016 env(vault.deposit({.depositor = issuer, .id = vaultKeylet.key, .amount = usd(2)}),
7017 Ter(expected));
7018 env.close();
7019 };
7020
7021 {
7022 testcase(
7023 "bug: VaultDeposit by issuer at IOU edge fires "
7024 "tecINVARIANT_FAILED at finalize (pre-fixCleanup3_2_0)");
7025 runScenario(testableAmendments() - fixCleanup3_2_0, tecINVARIANT_FAILED);
7026 }
7027 {
7028 testcase(
7029 "bug: VaultDeposit by issuer at IOU edge rejects with "
7030 "tecPRECISION_LOSS proactively (post-fixCleanup3_2_0)");
7031 runScenario(testableAmendments(), tecPRECISION_LOSS);
7032 }
7033 }
7034
7035 void
7037 {
7038 using namespace test::jtx;
7039
7040 auto readReferenceHolding = [&](Env const& env,
7041 Keylet const& vaultKeylet) -> std::optional<uint256> {
7042 auto const sleVault = env.le(vaultKeylet);
7043 if (!sleVault)
7044 return std::nullopt;
7045 auto const sleIssuance = env.le(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
7046 if (!sleIssuance || !sleIssuance->isFieldPresent(sfReferenceHolding))
7047 return std::nullopt;
7048 return sleIssuance->getFieldH256(sfReferenceHolding);
7049 };
7050
7051 // Post-fixCleanup3_2_0: vault share carries sfReferenceHolding
7052 // pointing to the vault pseudo's MPToken (for MPT-backed vaults)
7053 // or RippleState (for IOU-backed vaults).
7054 {
7055 testcase("sfReferenceHolding: MPT-backed vault, post-amendment");
7056 Env env{*this, testableAmendments()};
7057 Account const issuer{"issuer"};
7058 Account const owner{"owner"};
7059 env.fund(XRP(10'000), issuer, owner);
7060 env.close();
7061
7062 MPTTester mptt{env, issuer, kMptInitNoFund};
7063 mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
7064 PrettyAsset const asset = mptt.issuanceID();
7065 mptt.authorize({.account = owner});
7066
7067 Vault const vault{env};
7068 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
7069 env(tx);
7070 env.close();
7071
7072 auto const sleVault = env.le(keylet);
7073 BEAST_EXPECT(sleVault != nullptr);
7074 auto const pseudoId = sleVault->at(sfAccount);
7075 auto const expected = keylet::mptoken(mptt.issuanceID(), pseudoId).key;
7076
7077 auto const stored = readReferenceHolding(env, keylet);
7078 BEAST_EXPECT(stored.has_value());
7079 BEAST_EXPECT(stored && *stored == expected);
7080 // The pointed-to MPToken must actually exist.
7081 BEAST_EXPECT(env.le(keylet::mptoken(mptt.issuanceID(), pseudoId)) != nullptr);
7082 }
7083
7084 {
7085 testcase("sfReferenceHolding: IOU-backed vault, post-amendment");
7086 Env env{*this, testableAmendments()};
7087 Account const issuer{"issuer"};
7088 Account const owner{"owner"};
7089 env.fund(XRP(10'000), issuer, owner);
7090 env(fset(issuer, asfDefaultRipple));
7091 env.close();
7092
7093 PrettyAsset const asset = issuer["IOU"];
7094 env.trust(asset(1'000'000), owner);
7095 env.close();
7096
7097 Vault const vault{env};
7098 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
7099 env(tx);
7100 env.close();
7101
7102 auto const sleVault = env.le(keylet);
7103 BEAST_EXPECT(sleVault != nullptr);
7104 auto const pseudoId = sleVault->at(sfAccount);
7105 auto const expected = keylet::trustLine(pseudoId, asset.raw().get<Issue>()).key;
7106
7107 auto const stored = readReferenceHolding(env, keylet);
7108 BEAST_EXPECT(stored.has_value());
7109 BEAST_EXPECT(stored && *stored == expected);
7110 // The pointed-to RippleState must actually exist.
7111 BEAST_EXPECT(env.le(keylet::trustLine(pseudoId, asset.raw().get<Issue>())) != nullptr);
7112 }
7113
7114 // XRP-backed vaults leave the field absent: XRP has no separate
7115 // holding ledger entry and no transferability concept to inherit.
7116 {
7117 testcase("sfReferenceHolding: XRP-backed vault, field absent");
7118 Env env{*this, testableAmendments()};
7119 Account const owner{"owner"};
7120 env.fund(XRP(10'000), owner);
7121 env.close();
7122
7123 PrettyAsset const asset{xrpIssue(), 1'000'000};
7124 Vault const vault{env};
7125 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
7126 env(tx);
7127 env.close();
7128
7129 BEAST_EXPECT(!readReferenceHolding(env, keylet).has_value());
7130 }
7131
7132 // Pre-fixCleanup3_2_0: vault share has the field absent regardless
7133 // of underlying type.
7134 {
7135 testcase("sfReferenceHolding: vault share, pre-amendment");
7136 Env env{*this, testableAmendments() - fixCleanup3_2_0};
7137 Account const issuer{"issuer"};
7138 Account const owner{"owner"};
7139 env.fund(XRP(10'000), issuer, owner);
7140 env.close();
7141
7142 MPTTester mptt{env, issuer, kMptInitNoFund};
7143 mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
7144 PrettyAsset const asset = mptt.issuanceID();
7145 mptt.authorize({.account = owner});
7146
7147 Vault const vault{env};
7148 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
7149 env(tx);
7150 env.close();
7151
7152 BEAST_EXPECT(!readReferenceHolding(env, keylet).has_value());
7153 }
7154
7155 // Plain MPTokenIssuanceCreate (not a vault share) must never
7156 // populate the field. Only the post-amendment case is
7157 // interesting; pre-amendment nothing writes the field at all.
7158 {
7159 testcase("sfReferenceHolding: plain MPT issuance never set");
7160 Env env{*this, testableAmendments()};
7161 Account const issuer{"issuer"};
7162 env.fund(XRP(10'000), issuer);
7163 env.close();
7164
7165 MPTTester mptt{env, issuer, kMptInitNoFund};
7166 mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
7167 env.close();
7168
7169 auto const sleIssuance = env.le(keylet::mptokenIssuance(mptt.issuanceID()));
7170 if (BEAST_EXPECT(sleIssuance))
7171 BEAST_EXPECT(!sleIssuance->isFieldPresent(sfReferenceHolding));
7172 }
7173 }
7174
7175 // Probe every transactor surface that might delete the vault pseudo-
7176 // account's underlying holding (the MPToken or RippleState pointed to
7177 // by sfReferenceHolding). Each scenario asserts either that the
7178 // existing pseudo-account guards stop the deletion at preclaim, or
7179 // that the ledger leaves the holding intact afterwards. This is a
7180 // regression guard: if any of these guards regresses, the share's
7181 // sfReferenceHolding pointer would dangle and the new ValidMPTIssuance
7182 // invariant would catch it - but we want to fail much earlier, at
7183 // the transactor's preclaim / doApply, not at invariant time.
7184 void
7186 {
7187 using namespace test::jtx;
7188
7189 // Helper: read the share's referenced holding and confirm the
7190 // pointed-to SLE still exists after the probe.
7191 auto referencedHoldingExists = [&](Env const& env, Keylet const& vaultKeylet) -> bool {
7192 auto const sleVault = env.le(vaultKeylet);
7193 if (!sleVault)
7194 return false;
7195 auto const sleIssuance = env.le(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
7196 if (!sleIssuance || !sleIssuance->isFieldPresent(sfReferenceHolding))
7197 return false;
7198 auto const holdingKey = sleIssuance->getFieldH256(sfReferenceHolding);
7199 return env.le(keylet::unchecked(holdingKey)) != nullptr;
7200 };
7201
7202 // ---- MPT-backed vault ----------------------------------------
7203 {
7204 testcase("vault pseudo MPToken: Clawback blocked by tecPSEUDO_ACCOUNT");
7205 Env env{*this, testableAmendments()};
7206 Account const issuer{"issuer"};
7207 Account const owner{"owner"};
7208 Account const depositor{"depositor"};
7209 env.fund(XRP(10'000), issuer, owner, depositor);
7210 env.close();
7211
7212 MPTTester mptt{env, issuer, kMptInitNoFund};
7213 mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanClawback});
7214 PrettyAsset const asset = mptt.issuanceID();
7215 mptt.authorize({.account = owner});
7216 mptt.authorize({.account = depositor});
7217 env(pay(issuer, depositor, asset(1'000)));
7218 env.close();
7219
7220 Vault const vault{env};
7221 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
7222 env(tx);
7223 env.close();
7224
7225 env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(500)}));
7226 env.close();
7227
7228 BEAST_EXPECT(referencedHoldingExists(env, keylet));
7229
7230 Account const pseudoAccount{"vault-pseudo", env.le(keylet)->at(sfAccount)};
7231 // Issuer attempts to claw back the FULL underlying balance
7232 // (500) directly from the vault pseudo-account. With the
7233 // full amount, the doApply path would drain the pseudo's
7234 // MPToken to zero and removeEmptyHolding would erase it -
7235 // if doApply ever ran. SAV's pseudo-account guard at
7236 // Clawback.cpp:201 refuses at preclaim with
7237 // tecPSEUDO_ACCOUNT before any state change.
7238 env(claw(issuer, asset(500), pseudoAccount), Ter{tecPSEUDO_ACCOUNT});
7239 env.close();
7240 BEAST_EXPECT(referencedHoldingExists(env, keylet));
7241 // Sanity: pseudo's full balance is intact.
7242 BEAST_EXPECT(env.balance(pseudoAccount, asset).number() == 500);
7243 }
7244
7245 {
7246 testcase("vault pseudo MPToken: Issuer cannot Unauthorize pseudo");
7247 Env env{*this, testableAmendments()};
7248 Account const issuer{"issuer"};
7249 Account const owner{"owner"};
7250 env.fund(XRP(10'000), issuer, owner);
7251 env.close();
7252
7253 MPTTester mptt{env, issuer, kMptInitNoFund};
7254 mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth});
7255 PrettyAsset const asset = mptt.issuanceID();
7256 mptt.authorize({.account = owner});
7257 mptt.authorize({.account = issuer, .holder = owner});
7258 env.close();
7259
7260 Vault const vault{env};
7261 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
7262 env(tx);
7263 env.close();
7264
7265 BEAST_EXPECT(referencedHoldingExists(env, keylet));
7266
7267 auto const pseudoId = env.le(keylet)->at(sfAccount);
7268 // Issuer attempts MPTokenAuthorize against the pseudo with
7269 // tfMPTUnauthorize. MPTokenAuthorize.cpp blocks pseudo
7270 // accounts via isPseudoAccount; the pseudo's MPToken is
7271 // preserved. Construct the tx manually since the pseudo
7272 // lacks a signing key, and the issuer-driven flavour is
7273 // expressed via sfHolder.
7274 json::Value jv;
7275 jv[sfAccount] = issuer.human();
7276 jv[sfHolder] = toBase58(pseudoId);
7277 jv[sfMPTokenIssuanceID] = to_string(mptt.issuanceID());
7278 jv[sfFlags] = tfMPTUnauthorize;
7279 jv[sfTransactionType] = jss::MPTokenAuthorize;
7280 env(jv, Ter{tecNO_PERMISSION});
7281 env.close();
7282 BEAST_EXPECT(referencedHoldingExists(env, keylet));
7283 }
7284
7285 {
7286 testcase("vault pseudo MPToken: MPTokenIssuanceDestroy blocked while vault holds");
7287 Env env{*this, testableAmendments()};
7288 Account const issuer{"issuer"};
7289 Account const owner{"owner"};
7290 Account const depositor{"depositor"};
7291 env.fund(XRP(10'000), issuer, owner, depositor);
7292 env.close();
7293
7294 MPTTester mptt{env, issuer, kMptInitNoFund};
7295 mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
7296 PrettyAsset const asset = mptt.issuanceID();
7297 mptt.authorize({.account = owner});
7298 mptt.authorize({.account = depositor});
7299 env(pay(issuer, depositor, asset(1'000)));
7300 env.close();
7301
7302 Vault const vault{env};
7303 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
7304 env(tx);
7305 env.close();
7306
7307 env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(500)}));
7308 env.close();
7309
7310 BEAST_EXPECT(referencedHoldingExists(env, keylet));
7311
7312 // While the vault holds outstanding underlying, the issuer
7313 // cannot destroy the issuance. tecHAS_OBLIGATIONS confirms
7314 // the protection - and as a side effect, the share's
7315 // sfReferenceHolding pointer cannot be left pointing at a
7316 // ghost issuance.
7317 mptt.destroy({.id = mptt.issuanceID(), .err = tecHAS_OBLIGATIONS});
7318 env.close();
7319 BEAST_EXPECT(referencedHoldingExists(env, keylet));
7320 }
7321
7322 // ---- IOU-backed vault ----------------------------------------
7323 {
7324 testcase("vault pseudo trust line: Clawback blocked by tecPSEUDO_ACCOUNT");
7325 Env env{*this, testableAmendments()};
7326 Account const issuer{"issuer"};
7327 Account const owner{"owner"};
7328 env.fund(XRP(10'000), issuer, owner);
7329 env(fset(issuer, asfAllowTrustLineClawback));
7330 env.close();
7331
7332 PrettyAsset const asset = issuer["IOU"];
7333 env.trust(asset(1'000'000), owner);
7334 env(pay(issuer, owner, asset(1'000)));
7335 env.close();
7336
7337 Vault const vault{env};
7338 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
7339 env(tx);
7340 env.close();
7341
7342 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(500)}));
7343 env.close();
7344
7345 BEAST_EXPECT(referencedHoldingExists(env, keylet));
7346
7347 Account const pseudoAccount{"vault-pseudo", env.le(keylet)->at(sfAccount)};
7348 // Issuer attempts to claw back the FULL IOU balance (500)
7349 // directly from the vault pseudo. With the full amount, the
7350 // doApply path would drain the trust line to zero and (if
7351 // both reserve flags clear) trustDelete would erase it - if
7352 // doApply ever ran. The same SAV pseudo-account guard
7353 // refuses at preclaim with tecPSEUDO_ACCOUNT. The amount's
7354 // STAmount issuer field is the holder, per IOU clawback
7355 // convention.
7356 env(claw(issuer, pseudoAccount["IOU"](500)), Ter{tecPSEUDO_ACCOUNT});
7357 env.close();
7358 BEAST_EXPECT(referencedHoldingExists(env, keylet));
7359 // Sanity: pseudo's full balance is intact.
7360 BEAST_EXPECT(env.balance(pseudoAccount, asset).number() == 500);
7361 }
7362
7363 {
7364 testcase("vault pseudo trust line: TrustSet limit=0 from issuer preserves line");
7365 Env env{*this, testableAmendments()};
7366 Account const issuer{"issuer"};
7367 Account const owner{"owner"};
7368 env.fund(XRP(10'000), issuer, owner);
7369 env(fset(issuer, asfDefaultRipple));
7370 env.close();
7371
7372 PrettyAsset const asset = issuer["IOU"];
7373 env.trust(asset(1'000'000), owner);
7374 env(pay(issuer, owner, asset(1'000)));
7375 env.close();
7376
7377 Vault const vault{env};
7378 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
7379 env(tx);
7380 env.close();
7381
7382 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(500)}));
7383 env.close();
7384
7385 BEAST_EXPECT(referencedHoldingExists(env, keylet));
7386
7387 // Issuer submits TrustSet with limit=0 against the vault
7388 // pseudo. The pseudo's side of the line still has the
7389 // original (non-zero) limit and a non-zero balance, so the
7390 // line is preserved - even though the issuer cleared its
7391 // own side. trustDelete only fires when both limits clear
7392 // and the balance is zero.
7393 Account const pseudoAccount{"vault-pseudo", env.le(keylet)->at(sfAccount)};
7394 env(trust(issuer, pseudoAccount["IOU"](0)));
7395 env.close();
7396 BEAST_EXPECT(referencedHoldingExists(env, keylet));
7397 }
7398
7399 // ---- Positive control: VaultDelete is the only legitimate path
7400 {
7401 testcase("vault pseudo holding: VaultDelete is the legitimate cleanup path");
7402 Env env{*this, testableAmendments()};
7403 Account const issuer{"issuer"};
7404 Account const owner{"owner"};
7405 env.fund(XRP(10'000), issuer, owner);
7406 env.close();
7407
7408 MPTTester mptt{env, issuer, kMptInitNoFund};
7409 mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
7410 PrettyAsset const asset = mptt.issuanceID();
7411 mptt.authorize({.account = owner});
7412
7413 Vault const vault{env};
7414 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
7415 env(tx);
7416 env.close();
7417
7418 BEAST_EXPECT(referencedHoldingExists(env, keylet));
7419 auto const pseudoId = env.le(keylet)->at(sfAccount);
7420 auto const sharedMptId = env.le(keylet)->at(sfShareMPTID);
7421 auto const holdingKeylet = keylet::mptoken(mptt.issuanceID(), pseudoId);
7422
7423 // VaultDelete tears down the vault pseudo's holding, the
7424 // share issuance, and the pseudo-account itself. Invariant
7425 // permits this because the tx is ttVAULT_DELETE.
7426 env(vault.del({.owner = owner, .id = keylet.key}));
7427 env.close();
7428
7429 BEAST_EXPECT(env.le(keylet) == nullptr);
7430 BEAST_EXPECT(env.le(holdingKeylet) == nullptr);
7431 BEAST_EXPECT(env.le(keylet::mptokenIssuance(sharedMptId)) == nullptr);
7432 }
7433 }
7434
7435 // VaultDeposit::preclaim uses accountHolds(..., SpendableHandling::
7436 // shFULL_BALANCE), which for an IOU asset adds the counterparty's
7437 // LowLimit/HighLimit to the depositor's raw balance (TokenHelpers.cpp:
7438 // getTrustLineBalance with includeOppositeLimit=true). When the
7439 // depositor's raw balance < deposit amount but raw + opposite limit >=
7440 // amount, preclaim is satisfied. doApply then calls
7441 // directSendNoFeeIOU, which unconditionally subtracts saAmount from
7442 // saBalance — driving the trust line negative — and returns tesSUCCESS.
7443 // The post-send sanity check uses the default shSIMPLE_BALANCE (no
7444 // opposite-limit add), sees a negative balance, and returns tefINTERNAL.
7445 void
7447 {
7448 auto runTest = [&](FeatureBitset f, TER expected) {
7449 using namespace test::jtx;
7450 using namespace std::literals;
7451
7452 Env env{*this, f};
7453 Account const gw{"gateway"};
7454 Account const owner{"owner"};
7455 Account const depositor{"depositor"};
7456
7457 env.fund(XRP(10000), gw, owner, depositor);
7458 env.close();
7459
7460 // Gateway with DefaultRipple so vault creation on its IOU works.
7461 env(fset(gw, asfDefaultRipple));
7462 env.close();
7463
7464 // Depositor opens a trust line to gateway and receives a small
7465 // balance.
7466 PrettyAsset const usd = gw["USD"];
7467 env.trust(usd(1000), depositor);
7468 env(pay(gw, depositor, usd(100))); // raw trust-line balance: 100
7469 env.close();
7470
7471 // Key precondition: gateway sets a non-zero limit on the same
7472 // RippleState — the "opposite field" from depositor's perspective.
7473 // This is what inflates shFULL_BALANCE in preclaim above the raw
7474 // balance.
7475 env(trust(gw, depositor["USD"](1000)));
7476 env.close();
7477
7478 // Create the IOU vault.
7479 Vault const vault{env};
7480 auto [vaultTx, keylet] = vault.create({.owner = owner, .asset = usd});
7481 env(vaultTx);
7482 env.close();
7483
7484 // Submit a deposit of 500 USD:
7485 // - raw balance: 100 USD
7486 // - opposite limit (gw's side): 1000 USD
7487 // - preclaim sees 100 + 1000 = 1100, passes (>= 500)
7488 // - doApply transfers 500, depositor's trust-line balance
7489 // becomes -400
7490 // - sanity check at VaultDeposit.cpp:256 fires
7491 // - tx returns tefINTERNAL (BUG — should be tesSUCCESS.
7492 auto depositTx =
7493 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = usd(500)});
7494 env(depositTx, Ter(expected));
7495 env.close();
7496 };
7497
7498 {
7499 testcase(
7500 "IOU vault deposit exceeding depositor's balance but "
7501 "within counterparty's trust limit, pre-fixCleanup3_2_0 "
7502 "(tefINTERNAL)");
7503 runTest(test::jtx::testableAmendments() - fixCleanup3_2_0, tefINTERNAL);
7504 }
7505 {
7506 testcase(
7507 "IOU vault deposit exceeding depositor's balance but "
7508 "within counterparty's trust limit, post-fixCleanup3_2_0 "
7509 "(tesSUCCESS)");
7511 }
7512 }
7513
7514 void
7516 {
7517 using namespace test::jtx;
7518
7519 Account const issuer{"issuer"};
7520 Account const owner{"owner"};
7521
7522 // === IOU ===
7523 {
7524 testcase("VaultDeposit IOU freeze checks");
7525 Env env{*this};
7526 Vault vault{env};
7527
7528 env.fund(XRP(100'000), issuer, owner);
7529 env(fset(issuer, asfAllowTrustLineClawback));
7530 env.close();
7531 PrettyAsset const asset = issuer["IOU"];
7532 env.trust(asset(1'000'000), owner);
7533 env(pay(issuer, owner, asset(100'000)));
7534 env.close();
7535
7536 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
7537 env(tx);
7538 env.close();
7539 auto const vaultAcct = Account("vault", env.le(keylet)->at(sfAccount));
7540
7541 // Initial deposit so the vault pseudo-account has a trustline
7542 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
7543 env.close();
7544
7545 auto runTests = [&]() {
7546 auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0);
7547
7548 // Global freeze
7549 env(fset(issuer, asfGlobalFreeze));
7550 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
7551 Ter(tecFROZEN));
7552 env(fclear(issuer, asfGlobalFreeze));
7553
7554 // Depositor regular freeze
7555 env(trust(issuer, asset(0), owner, tfSetFreeze));
7556 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
7557 Ter(tecFROZEN));
7558 env(trust(issuer, asset(0), owner, tfClearFreeze));
7559
7560 // Depositor deep freeze
7561 env(trust(issuer, asset(0), owner, tfSetFreeze | tfSetDeepFreeze));
7562 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
7563 Ter(tecFROZEN));
7564 env(trust(issuer, asset(0), owner, tfClearFreeze | tfClearDeepFreeze));
7565
7566 // Vault-account regular freeze
7567 // Post-fix: checkDepositFreeze catches it → tecFROZEN
7568 // Pre-fix: not checked directly, but the transitive share
7569 // check triggers → tecLOCKED
7570 {
7571 auto trustSet = [&]() {
7572 json::Value jv;
7573 jv[jss::Account] = issuer.human();
7574 {
7575 auto& ja = jv[jss::LimitAmount] =
7576 asset(0).value().getJson(JsonOptions::Values::None);
7577 ja[jss::issuer] = toBase58(vaultAcct.id());
7578 }
7579 jv[jss::TransactionType] = jss::TrustSet;
7580 return jv;
7581 }();
7582
7583 trustSet[jss::Flags] = tfSetFreeze;
7584 env(trustSet);
7585 env.close();
7586
7587 TER const expected = fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED);
7588 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
7589 Ter(expected));
7590
7591 trustSet[jss::Flags] = tfClearFreeze;
7592 env(trustSet);
7593 env.close();
7594 }
7595
7596 // Vault-account deep freeze
7597 {
7598 auto trustSet = [&]() {
7599 json::Value jv;
7600 jv[jss::Account] = issuer.human();
7601 {
7602 auto& ja = jv[jss::LimitAmount] =
7603 asset(0).value().getJson(JsonOptions::Values::None);
7604 ja[jss::issuer] = toBase58(vaultAcct.id());
7605 }
7606 jv[jss::TransactionType] = jss::TrustSet;
7607 return jv;
7608 }();
7609
7610 trustSet[jss::Flags] = tfSetFreeze | tfSetDeepFreeze;
7611 env(trustSet);
7612 env.close();
7613
7614 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
7615 Ter(fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED)));
7616
7617 trustSet[jss::Flags] = tfClearFreeze | tfClearDeepFreeze;
7618 env(trustSet);
7619 env.close();
7620 }
7621
7622 // Clawback works while frozen
7623 env(fset(issuer, asfGlobalFreeze));
7624 env(vault.clawback(
7625 {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(1)}));
7626 env(fclear(issuer, asfGlobalFreeze));
7627 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
7628 env.close();
7629 };
7630
7631 runTests();
7632 env.disableFeature(fixCleanup3_3_0);
7633 runTests();
7634 env.enableFeature(fixCleanup3_3_0);
7635 }
7636
7637 // === MPT ===
7638 {
7639 testcase("VaultDeposit MPT lock checks");
7640 Env env{*this};
7641 Vault vault{env};
7642
7643 env.fund(XRP(100'000), issuer, owner);
7644 env.close();
7645
7646 MPTTester mptt{env, issuer, kMptInitNoFund};
7647 mptt.create(
7648 {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth});
7649 PrettyAsset const mpt{mptt.issuanceID()};
7650
7651 mptt.authorize({.account = owner});
7652 mptt.authorize({.account = issuer, .holder = owner});
7653 env.close();
7654 env(pay(issuer, owner, mpt(100'000)));
7655 env.close();
7656
7657 auto [tx, keylet] = vault.create({.owner = owner, .asset = mpt});
7658 env(tx);
7659 env.close();
7660 auto const vaultAcctID = env.le(keylet)->at(sfAccount);
7661 Account const vaultAcct("vault", vaultAcctID);
7662
7663 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(100)}));
7664 env.close();
7665
7666 // For MPT isDeepFrozen == isFrozen, so all locks block in
7667 // both pre- and post-fix.
7668 auto runTests = [&]() {
7669 // Global lock
7670 mptt.set({.flags = tfMPTLock});
7671 env.close();
7672 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
7673 Ter(tecLOCKED));
7674 mptt.set({.flags = tfMPTUnlock});
7675 env.close();
7676
7677 // Depositor individual lock
7678 mptt.set({.holder = owner, .flags = tfMPTLock});
7679 env.close();
7680 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
7681 Ter(tecLOCKED));
7682 mptt.set({.holder = owner, .flags = tfMPTUnlock});
7683 env.close();
7684
7685 // Vault pseudo-account individual lock
7686 mptt.set({.holder = vaultAcct, .flags = tfMPTLock});
7687 env.close();
7688 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
7689 Ter(tecLOCKED));
7690 mptt.set({.holder = vaultAcct, .flags = tfMPTUnlock});
7691 env.close();
7692
7693 // Clawback works while locked
7694 mptt.set({.flags = tfMPTLock});
7695 env.close();
7696 env(vault.clawback(
7697 {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = mpt(1)}));
7698 mptt.set({.flags = tfMPTUnlock});
7699 env.close();
7700 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}));
7701 env.close();
7702 };
7703
7704 runTests();
7705 env.disableFeature(fixCleanup3_3_0);
7706 runTests();
7707 env.enableFeature(fixCleanup3_3_0);
7708 }
7709 }
7710
7711 // Focused demonstration: a depositor under a regular individual IOU freeze
7712 // can still withdraw to themselves (self-withdrawal), but is blocked from
7713 // withdrawing to a third party.
7714 //
7715 // Pre-fixCleanup3_3_0: both the self-withdrawal AND the third-party
7716 // withdrawal were blocked because the old code checked checkFrozen on the
7717 // destination regardless of whether it was the submitter.
7718 // Post-fixCleanup3_3_0: checkWithdrawFreeze skips the submitter freeze
7719 // check when submitter == destination, so self-withdrawal succeeds.
7720 void
7722 {
7723 testcase("VaultWithdraw IOU self-withdrawal while individually frozen");
7724
7725 using namespace test::jtx;
7726
7727 Account const issuer{"issuer"};
7728 Account const owner{"owner"};
7729 Account const charlie{"charlie"};
7730 Env env{*this};
7731 Vault vault{env};
7732
7733 env.fund(XRP(100'000), issuer, owner, charlie);
7734 env(fset(issuer, asfAllowTrustLineClawback));
7735 env.close();
7736
7737 PrettyAsset const asset = issuer["IOU"];
7738 env.trust(asset(1'000'000), owner);
7739 env.trust(asset(1'000'000), charlie);
7740 env(pay(issuer, owner, asset(100'000)));
7741 env.close();
7742
7743 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
7744 env(tx);
7745 env.close();
7746
7747 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(10)}));
7748 env.close();
7749
7750 auto runTests = [&]() {
7751 auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0);
7752
7753 // Set a regular individual freeze on the owner's IOU trustline.
7754 env(trust(issuer, asset(0), owner, tfSetFreeze));
7755 env.close();
7756
7757 // Self-withdrawal: submitter == destination, so the submitter
7758 // freeze check is skipped.
7759 // Post-fix: tesSUCCESS. Pre-fix: tecFROZEN.
7760 env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
7761 Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecFROZEN)));
7762
7763 // Withdrawal to a third party is blocked: submitter != destination
7764 // so the submitter freeze check applies.
7765 {
7766 auto withdrawToCharlie =
7767 vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
7768 withdrawToCharlie[sfDestination] = charlie.human();
7769 // Post-fix: tecFROZEN (checkIndividualFrozen on submitter).
7770 // Pre-fix: tecLOCKED (isFrozen on the vault share).
7771 env(withdrawToCharlie, Ter(fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED)));
7772 }
7773
7774 env(trust(issuer, asset(0), owner, tfClearFreeze));
7775 env.close();
7776 };
7777
7778 runTests();
7779 env.disableFeature(fixCleanup3_3_0);
7780 runTests();
7781 env.enableFeature(fixCleanup3_3_0);
7782 }
7783
7784 void
7786 {
7787 using namespace test::jtx;
7788
7789 Account const issuer{"issuer"};
7790 Account const owner{"owner"};
7791
7792 // === IOU ===
7793 {
7794 testcase("VaultWithdraw IOU freeze checks");
7795 Env env{*this};
7796 Vault vault{env};
7797
7798 env.fund(XRP(100'000), issuer, owner);
7799 env(fset(issuer, asfAllowTrustLineClawback));
7800 env.close();
7801 PrettyAsset const asset = issuer["IOU"];
7802 env.trust(asset(1'000'000), owner);
7803 env(pay(issuer, owner, asset(100'000)));
7804 env.close();
7805
7806 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
7807 env(tx);
7808 env.close();
7809 auto const vaultAcct = Account("vault", env.le(keylet)->at(sfAccount));
7810
7811 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
7812 env.close();
7813
7814 Account const charlie{"charlie"};
7815 env.fund(XRP(10'000), charlie);
7816 env.trust(asset(1'000'000), charlie);
7817 env.close();
7818
7819 auto runTests = [&]() {
7820 auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0);
7821 // Post-fix: submitter freeze blocks withdraw to 3rd party
7822 // Pre-fix: submitter's IOU freeze not checked, but
7823 // checkFrozen(depositor, share) may trigger tecLOCKED
7824 TER const submitterTo3rd = fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED);
7825
7826 // Global freeze → self-withdraw
7827 env(fset(issuer, asfGlobalFreeze));
7828 env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
7829 Ter(tecFROZEN));
7830 // Global freeze → withdraw to 3rd party
7831 {
7832 auto withdrawToCharlie =
7833 vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
7834 withdrawToCharlie[sfDestination] = charlie.human();
7835 env(withdrawToCharlie, Ter(tecFROZEN));
7836 }
7837 env(fclear(issuer, asfGlobalFreeze));
7838
7839 // Vault-account regular freeze
7840 {
7841 auto trustSet = [&]() {
7842 json::Value jv;
7843 jv[jss::Account] = issuer.human();
7844 {
7845 auto& ja = jv[jss::LimitAmount] =
7846 asset(0).value().getJson(JsonOptions::Values::None);
7847 ja[jss::issuer] = toBase58(vaultAcct.id());
7848 }
7849 jv[jss::TransactionType] = jss::TrustSet;
7850 return jv;
7851 }();
7852
7853 trustSet[jss::Flags] = tfSetFreeze;
7854 env(trustSet);
7855 env.close();
7856
7857 TER const vaultAcctFreeze = fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED);
7858
7859 // Self-withdraw
7860 env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
7861 Ter(vaultAcctFreeze));
7862 // Withdraw to 3rd party
7863 {
7864 auto withdrawToCharlie = vault.withdraw(
7865 {.depositor = owner, .id = keylet.key, .amount = asset(1)});
7866 withdrawToCharlie[sfDestination] = charlie.human();
7867 env(withdrawToCharlie, Ter(vaultAcctFreeze));
7868 }
7869
7870 trustSet[jss::Flags] = tfClearFreeze;
7871 env(trustSet);
7872 env.close();
7873 }
7874
7875 // Depositor regular freeze → self-withdraw
7876 env(trust(issuer, asset(0), owner, tfSetFreeze));
7877 // Post-fix: self-withdraw allowed (submitter==dst skip)
7878 // Pre-fix: isFrozen(depositor, iou) catches it
7879 env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
7880 Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecFROZEN)));
7881
7882 // Depositor regular freeze → withdraw to 3rd party
7883 {
7884 auto withdrawTo3rd =
7885 vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
7886 withdrawTo3rd[sfDestination] = charlie.human();
7887 env(withdrawTo3rd, Ter(submitterTo3rd));
7888 }
7889 env(trust(issuer, asset(0), owner, tfClearFreeze));
7890 // Replenish what was withdrawn
7891 if (fix330Enabled)
7892 {
7893 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
7894 }
7895 env.close();
7896
7897 // Depositor deep freeze → self-withdraw blocked
7898 env(trust(issuer, asset(0), owner, tfSetFreeze | tfSetDeepFreeze));
7899 // TODO: branches are identical - confirm the intended pre/post-fix330
7900 // expectations and replace with the correct values (one branch may be wrong).
7901 env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
7902 // NOLINTNEXTLINE(bugprone-branch-clone)
7903 Ter(fix330Enabled ? TER(tecFROZEN) : TER(tecFROZEN)));
7904 env(trust(issuer, asset(0), owner, tfClearFreeze | tfClearDeepFreeze));
7905
7906 // Destination regular freeze → withdraw to 3rd party
7907 env(trust(issuer, asset(0), charlie, tfSetFreeze));
7908 // Self-withdraw unaffected by charlie's freeze
7909 env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
7910 {
7911 auto withdrawToCharlie =
7912 vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
7913 withdrawToCharlie[sfDestination] = charlie.human();
7914 // Post-fix: regular freeze on dst allowed
7915 // Pre-fix: checkFrozen(dst, iou) catches it
7916 env(withdrawToCharlie, Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecFROZEN)));
7917 }
7918 env(trust(issuer, asset(0), charlie, tfClearFreeze));
7919 // Replenish: 1 for self-withdraw + 1 if charlie withdraw succeeded
7920 env(vault.deposit(
7921 {.depositor = owner,
7922 .id = keylet.key,
7923 .amount = asset(fix330Enabled ? 2 : 1)}));
7924 env.close();
7925
7926 // Destination deep freeze → withdraw to 3rd party blocked
7927 env(trust(issuer, asset(0), charlie, tfSetFreeze | tfSetDeepFreeze));
7928 {
7929 auto withdrawToCharlie =
7930 vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
7931 withdrawToCharlie[sfDestination] = charlie.human();
7932 env(withdrawToCharlie, Ter(tecFROZEN));
7933 }
7934 // Destination deep freeze → self-withdraw unaffected
7935 env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
7936 env(trust(issuer, asset(0), charlie, tfClearFreeze | tfClearDeepFreeze));
7937 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
7938 env.close();
7939
7940 // Clawback works while frozen
7941 env(fset(issuer, asfGlobalFreeze));
7942 env(vault.clawback(
7943 {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(1)}));
7944 env(fclear(issuer, asfGlobalFreeze));
7945 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
7946 env.close();
7947 };
7948
7949 runTests();
7950 env.disableFeature(fixCleanup3_3_0);
7951 runTests();
7952 env.enableFeature(fixCleanup3_3_0);
7953 }
7954
7955 // === MPT ===
7956 {
7957 testcase("VaultWithdraw MPT lock checks");
7958 Env env{*this};
7959 Vault vault{env};
7960
7961 env.fund(XRP(100'000), issuer, owner);
7962 env.close();
7963
7964 MPTTester mptt{env, issuer, kMptInitNoFund};
7965 mptt.create(
7966 {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth});
7967 PrettyAsset const mpt{mptt.issuanceID()};
7968
7969 mptt.authorize({.account = owner});
7970 mptt.authorize({.account = issuer, .holder = owner});
7971 env.close();
7972 env(pay(issuer, owner, mpt(100'000)));
7973 env.close();
7974
7975 auto [tx, keylet] = vault.create({.owner = owner, .asset = mpt});
7976 env(tx);
7977 env.close();
7978 Account const vaultAcct("vault", env.le(keylet)->at(sfAccount));
7979
7980 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(100)}));
7981 env.close();
7982
7983 Account const charlie{"charlie"};
7984 env.fund(XRP(10'000), charlie);
7985 env.close();
7986 mptt.authorize({.account = charlie});
7987 mptt.authorize({.account = issuer, .holder = charlie});
7988 env.close();
7989
7990 auto runTests = [&]() {
7991 auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0);
7992
7993 // Global lock
7994 mptt.set({.flags = tfMPTLock});
7995 env.close();
7996 env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
7997 Ter(tecLOCKED));
7998
7999 // Global lock → withdraw to issuer
8000 // Post-fix: bypasses freeze checks, but accountHolds
8001 // on the pseudo returns 0 under global lock
8002 // Pre-fix: checkFrozen(dst=issuer) catches global lock
8003 {
8004 auto withdrawToIssuer =
8005 vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)});
8006 withdrawToIssuer[sfDestination] = issuer.human();
8007 env(withdrawToIssuer, Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecLOCKED)));
8008 }
8009 mptt.set({.flags = tfMPTUnlock});
8010 env.close();
8011 if (fix330Enabled)
8012 {
8013 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}));
8014 }
8015 env.close();
8016
8017 // Vault pseudo-account individual lock
8018 mptt.set({.holder = vaultAcct, .flags = tfMPTLock});
8019 env.close();
8020 env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
8021 Ter(tecLOCKED));
8022 mptt.set({.holder = vaultAcct, .flags = tfMPTUnlock});
8023 env.close();
8024
8025 // Depositor individual lock → self-withdraw blocked
8026 // (isDeepFrozen == isFrozen for MPT)
8027 mptt.set({.holder = owner, .flags = tfMPTLock});
8028 env.close();
8029 env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
8030 Ter(tecLOCKED));
8031 // Depositor lock → withdraw to 3rd party also blocked
8032 {
8033 auto withdrawToCharlie =
8034 vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)});
8035 withdrawToCharlie[sfDestination] = charlie.human();
8036 env(withdrawToCharlie, Ter(tecLOCKED));
8037 }
8038
8039 // Depositor lock → withdraw to issuer
8040 // Post-fix: issuer bypass in checkWithdrawFreezes
8041 // Pre-fix: checkFrozen(depositor, share) blocks transitively
8042 {
8043 auto withdrawToIssuer =
8044 vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)});
8045 withdrawToIssuer[sfDestination] = issuer.human();
8046 env(withdrawToIssuer, Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecLOCKED)));
8047 }
8048 mptt.set({.holder = owner, .flags = tfMPTUnlock});
8049 env.close();
8050 if (fix330Enabled)
8051 {
8052 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}));
8053 }
8054 env.close();
8055
8056 // 3rd party destination lock → withdraw to 3rd party blocked
8057 mptt.set({.holder = charlie, .flags = tfMPTLock});
8058 env.close();
8059 {
8060 auto withdrawToCharlie =
8061 vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)});
8062 withdrawToCharlie[sfDestination] = charlie.human();
8063 env(withdrawToCharlie, Ter{tecLOCKED});
8064 }
8065 // 3rd party lock → self-withdraw unaffected
8066 env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}));
8067 mptt.set({.holder = charlie, .flags = tfMPTUnlock});
8068 env.close();
8069 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}));
8070 env.close();
8071
8072 // Clawback works while locked
8073 mptt.set({.flags = tfMPTLock});
8074 env.close();
8075 env(vault.clawback(
8076 {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = mpt(1)}));
8077 mptt.set({.flags = tfMPTUnlock});
8078 env.close();
8079 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}));
8080 env.close();
8081 };
8082
8083 runTests();
8084 env.disableFeature(fixCleanup3_3_0);
8085 runTests();
8086 env.enableFeature(fixCleanup3_3_0);
8087 }
8088 }
8089
8090public:
8091 void
8092 run() override
8093 {
8101 testSequences();
8102 testPreflight();
8106 testWithMPT();
8107 testWithIOU();
8112 testScaleIOU();
8113 testRPC();
8121
8130
8134
8137 }
8138};
8139
8141
8142} // namespace xrpl
A generic endpoint for log messages.
Definition Journal.h:38
A testsuite class.
Definition suite.h:50
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:149
Represents a JSON value.
Definition json_value.h:130
constexpr TIss const & get() const
constexpr bool native() const
Definition Asset.h:115
AccountID const & getIssuer() const
Definition Asset.cpp:21
Integers of any length that is a multiple of 32-bits.
Definition base_uint.h:71
A currency issued by an account.
Definition Issue.h:13
constexpr MPTID const & getMptID() const
Definition MPTIssue.h:33
Number is a floating point type that can represent a wide range of values.
Definition Number.h:306
Writable ledger view that accumulates state and tx changes.
Definition OpenView.h:45
Identifies fields.
Definition SField.h:130
Discardable, editable view to a ledger.
Definition Sandbox.h:15
void apply(RawView &to)
Definition Sandbox.h:35
void testVaultEscrowedMPT()
StuckDepositorFixture setupStuckDepositor(test::jtx::Env &env)
void testVaultDepositNegativeBalanceFromOppositeLimit()
void testVaultClawbackAssets()
void testBugIssuerVaultDepositAtEdge()
void testVaultClawbackBurnShares()
void testVaultWithdrawEqualityEnforced()
void testHoldingDeletionBlocked()
static constexpr std::uint32_t kStuckPayInterval
void testRemoveEmptyHoldingConfidentialBalances()
static constexpr std::int64_t kStuckPrincipal
void testVaultWithdrawFreeze()
void testBug6LimitBypassWithShares()
void testRemoveEmptyHoldingLockedAmount()
void testVaultWithdrawCanonicalizeToZero()
void run() override
Runs the suite.
void testWithdrawSoleShareholderPartialFixedSharesUsesFullPrice()
void testWithdrawSoleShareholderLoanRepaymentExit()
static constexpr auto kNegativeAmount
static constexpr std::uint32_t kStuckPayTotal
void testVaultDepositCanonicalizeToZero()
static constexpr std::int64_t kStuckDepositorIOU
void testWithDomainChecXRP()
void testBugMakeDeltaPosteriorScale()
void testFailedPseudoAccount()
void testBugMakeDeltaAnteriorScale()
xrpl::test::jtx::PrettyAmount PrettyAmount
void testNonTransferableShares()
static constexpr std::int64_t kStuckFunding
void testReferenceHolding()
static constexpr std::int64_t kStuckDeposit
void testWithdrawSoleShareholderFixedAssetExit(FeatureBitset features)
std::string const iouCurrency_
void testWithdrawSoleShareholderFullSharesRejected(FeatureBitset features)
void testWithdrawSoleShareholderCleanVaultUnaffected(FeatureBitset features)
void testVaultSelfWithdrawWhileFrozen()
void testVaultDepositFreeze()
FeatureBitset const all_
static constexpr std::int64_t kStuckBorrowerIOU
xrpl::test::jtx::PrettyAsset PrettyAsset
SLE::pointer peek(Keylet const &k) override
Prepare to modify the SLE associated with key.
void update(SLE::ref sle) override
Indicate changes to a peeked SLE.
Immutable cryptographic account descriptor.
Definition jtx/Account.h:17
AccountID id() const
Returns the Account ID.
Definition jtx/Account.h:85
A transaction testing environment.
Definition Env.h:143
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
SLE::const_pointer le(Account const &account) const
Return an account root.
Definition Env.cpp:284
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:296
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition Env.cpp:275
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:353
T make_pair(T... args)
T make_unique(T... args)
T max(T... args)
int Int
unsigned int UInt
@ Object
object value (collection of name/value pairs).
Definition json_value.h:26
@ Null
'null' value
Definition json_value.h:19
Keylet computation functions.
Definition Indexes.h:34
Keylet const & skip() noexcept
The index of the "short" skip list.
Definition Indexes.cpp:198
Keylet loanBroker(AccountID const &owner, std::uint32_t seq) noexcept
Definition Indexes.cpp:557
Keylet unchecked(uint256 const &key) noexcept
Any ledger entry.
Definition Indexes.cpp:351
Keylet mptokenIssuance(std::uint32_t seq, AccountID const &issuer) noexcept
Definition Indexes.cpp:521
Keylet loan(uint256 const &loanBrokerID, std::uint32_t loanSeq) noexcept
Definition Indexes.cpp:563
Keylet vault(AccountID const &owner, std::uint32_t seq) noexcept
Definition Indexes.cpp:551
Keylet mptoken(MPTID const &issuanceID, AccountID const &holder) noexcept
Definition Indexes.cpp:533
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:186
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:241
FeatureBitset testableAmendments()
Definition Env.h:76
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
static constexpr Number kNumZero
Definition Number.h:612
constexpr FlagValue tmfMPTCanEnableCanTransfer
Definition TxFlags.h:349
@ telINSUF_FEE_P
Definition TER.h:41
@ terNO_RIPPLE
Definition TER.h:216
@ terADDRESS_COLLISION
Definition TER.h:220
@ terNO_ACCOUNT
Definition TER.h:209
bool set(T &target, std::string const &name, Section const &section)
Set a value from a configuration Section If the named value is not found or doesn't parse as a T,...
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Definition Issue.h:97
AccountID pseudoAccountAddress(ReadView const &view, uint256 const &pseudoOwnerKey)
Generate a pseudo-account address from a pseudo owner key.
std::optional< AccountID > parseBase58(std::string const &s)
Parse AccountID from checked, base58 string.
std::string strHex(FwdIt begin, FwdIt end)
Definition strHex.h:10
constexpr XRPAmount kDropsPerXrp
Number of drops per 1 XRP.
Definition XRPAmount.h:240
int scale(Number const &number, Asset const &asset)
Get the scale of a Number for a given asset.
Definition STAmount.h:779
@ tefINTERNAL
Definition TER.h:163
@ tefEXCEPTION
Definition TER.h:162
std::string toBase58(AccountID const &v)
Convert AccountID to base58 checked string.
Definition AccountID.cpp:93
constexpr FlagValue tmfMPTSetCanTrade
Definition TxFlags.h:368
constexpr std::size_t kEcGamalEncryptedTotalLength
EC ElGamal ciphertext length: two compressed EC points concatenated.
Definition Protocol.h:324
TenthBips< std::uint32_t > TenthBips32
Definition Units.h:439
BEAST_DEFINE_TESTSUITE_PRIO(AccountSet, app, xrpl, 1)
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:633
STLedgerEntry SLE
json::Value toJson(Asset const &asset)
Definition Asset.h:157
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:44
@ TapNone
Definition ApplyView.h:13
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:28
std::map< std::string, AmendmentSupport > const & allAmendments()
All amendments libxrpl knows about.
@ temBAD_FEE
Definition TER.h:78
@ temINVALID_FLAG
Definition TER.h:97
@ temMALFORMED
Definition TER.h:73
@ temDISABLED
Definition TER.h:100
@ temBAD_AMOUNT
Definition TER.h:75
constexpr std::uint8_t kVaultStrategyFirstComeFirstServe
Vault withdrawal policies.
Definition Protocol.h:245
TERSubset< CanCvtToTER > TER
Definition TER.h:634
constexpr FlagValue tmfMPTCanEnableCanTrade
Definition TxFlags.h:348
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:358
@ tecPSEUDO_ACCOUNT
Definition TER.h:360
@ tecLOCKED
Definition TER.h:356
@ tecNO_LINE_INSUF_RESERVE
Definition TER.h:290
@ tecPATH_PARTIAL
Definition TER.h:280
@ tecNO_ENTRY
Definition TER.h:304
@ tecPATH_DRY
Definition TER.h:292
@ tecOBJECT_NOT_FOUND
Definition TER.h:324
@ tecNO_AUTH
Definition TER.h:298
@ tecINVARIANT_FAILED
Definition TER.h:311
@ tecFROZEN
Definition TER.h:301
@ tecINSUFFICIENT_FUNDS
Definition TER.h:323
@ tecEXPIRED
Definition TER.h:312
@ tecNO_LINE
Definition TER.h:299
@ tecPRECISION_LOSS
Definition TER.h:361
@ tecINSUFFICIENT_RESERVE
Definition TER.h:305
@ tecLIMIT_EXCEEDED
Definition TER.h:359
@ tecNO_PERMISSION
Definition TER.h:303
@ tecDST_TAG_NEEDED
Definition TER.h:307
@ tecHAS_OBLIGATIONS
Definition TER.h:315
constexpr XRPAmount kInitialXrp
Configure the native currency.
TER removeEmptyHolding(ApplyView &view, AccountID const &accountID, MPTIssue const &mptIssue, beast::Journal journal)
BaseUInt< 256 > uint256
Definition base_uint.h:562
MPTID makeMptID(std::uint32_t sequence, AccountID const &account)
Definition Indexes.cpp:172
@ tesSUCCESS
Definition TER.h:240
A pair of SHAMap key and LedgerEntryType.
Definition Keylet.h:19
uint256 key
Definition Keylet.h:20
std::optional< PrettyAsset > asset
Represents an XRP, IOU, or MPT quantity This customizes the string conversion and supports XRP conver...
T to_string(T... args)