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