xrpld
Loading...
Searching...
No Matches
VaultClawback_test.cpp
1#include <test/app/vault/VaultTestBase.h>
2#include <test/jtx/Account.h>
3#include <test/jtx/Env.h>
4#include <test/jtx/TestHelpers.h>
5#include <test/jtx/amount.h>
6#include <test/jtx/escrow.h>
7#include <test/jtx/fee.h>
8#include <test/jtx/flags.h>
9#include <test/jtx/mpt.h>
10#include <test/jtx/pay.h>
11#include <test/jtx/sig.h>
12#include <test/jtx/ter.h>
13#include <test/jtx/vault.h>
14
15#include <xrpl/basics/Number.h>
16#include <xrpl/basics/base_uint.h>
17#include <xrpl/beast/unit_test/suite.h>
18#include <xrpl/beast/utility/Journal.h>
19#include <xrpl/json/json_forwards.h>
20#include <xrpl/json/json_value.h>
21#include <xrpl/protocol/Asset.h>
22#include <xrpl/protocol/Feature.h>
23#include <xrpl/protocol/Indexes.h>
24#include <xrpl/protocol/Issue.h>
25#include <xrpl/protocol/Keylet.h>
26#include <xrpl/protocol/MPTIssue.h>
27#include <xrpl/protocol/SField.h>
28#include <xrpl/protocol/STAmount.h>
29#include <xrpl/protocol/SeqProxy.h>
30#include <xrpl/protocol/TER.h>
31#include <xrpl/protocol/TxFlags.h>
32#include <xrpl/protocol/Units.h>
33#include <xrpl/protocol/XRPAmount.h>
34#include <xrpl/protocol/jss.h>
35
36#include <chrono>
37#include <string>
38#include <tuple>
39#include <utility>
40
41namespace xrpl {
42
44{
45private:
46 void
48 {
49 using namespace test::jtx;
50 using namespace loan_broker;
51 using namespace loan;
52 Env env(*this, beast::Severity::Warning);
53
54 auto const vaultAssetBalance = [&](Keylet const& vaultKeylet) {
55 auto const sleVault = env.le(vaultKeylet);
56 BEAST_EXPECT(sleVault != nullptr);
57
58 return std::make_pair(sleVault->at(sfAssetsAvailable), sleVault->at(sfAssetsTotal));
59 };
60
61 auto const vaultShareBalance = [&](Keylet const& vaultKeylet) {
62 auto const sleVault = env.le(vaultKeylet);
63 BEAST_EXPECT(sleVault != nullptr);
64
65 auto const sleIssuance = env.le(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
66 BEAST_EXPECT(sleIssuance != nullptr);
67
68 return sleIssuance->at(sfOutstandingAmount);
69 };
70
71 // Under featureLendingProtocolV1_1 LoanBrokerSet::preclaim only
72 // accepts closed-ended vaults, so build vaults in this suite as
73 // closed-ended and advance past SubscriptionDate before creating
74 // brokers/loans. VaultClawback itself is not phase-gated. The
75 // subscription offset must be large enough that the deposit
76 // ledger close does not accidentally push us past SubscriptionDate
77 // (which would land the deposit in Investment phase and fail).
78 auto const setupVault = [&](PrettyAsset const& asset,
79 Account const& owner,
80 Account const& depositor) -> std::pair<Vault, Keylet> {
81 Vault const vault{env};
82
83 auto const& [tx, vaultKeylet, subscriptionDate] = vault.createClosedEnded(
84 {.owner = owner, .asset = asset, .subscriptionOffset = std::chrono::seconds{60}});
85 env(tx, Ter(tesSUCCESS));
86 env.close();
87
88 auto const& vaultSle = env.le(vaultKeylet);
89 BEAST_EXPECT(vaultSle != nullptr);
90
91 Asset const share = vaultSle->at(sfShareMPTID);
92
93 env(vault.deposit(
94 {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}),
95 Ter(tesSUCCESS));
96 env.close();
97
98 // Move past SubscriptionDate so LoanBrokerSet/LoanSet run in
99 // the Investment phase.
100 vault.closePastSubscription(subscriptionDate);
101
102 auto const& [availablePreDefault, totalPreDefault] = vaultAssetBalance(vaultKeylet);
103 BEAST_EXPECT(availablePreDefault == totalPreDefault);
104 BEAST_EXPECT(availablePreDefault == asset(100).value());
105
106 // attempt to clawback shares while there are assets fails
107 env(vault.clawback(
108 {.issuer = owner,
109 .id = vaultKeylet.key,
110 .holder = depositor,
111 .amount = share(0).value()}),
112 Ter(tecNO_PERMISSION));
113 env.close();
114
115 auto const& sharesAvailable = vaultShareBalance(vaultKeylet);
116 auto const& brokerKeylet =
117 keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
118
119 env(set(owner, vaultKeylet.key));
120 env.close();
121
122 auto const& loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
123
124 // Create a simple Loan for the full amount of Vault assets
125 env(set(depositor, brokerKeylet.key, asset(100).value()),
126 loan::kInterestRate(TenthBips32(0)),
127 kGracePeriod(60),
128 kPaymentInterval(120),
129 kPaymentTotal(10),
130 Sig(sfCounterpartySignature, owner),
131 Fee(env.current()->fees().base * 2),
132 Ter(tesSUCCESS));
133 env.close();
134
135 // attempt to clawback shares while there assetsAvailable == 0 and
136 // assetsTotal > 0 fails
137 env(vault.clawback(
138 {.issuer = owner,
139 .id = vaultKeylet.key,
140 .holder = depositor,
141 .amount = share(0).value()}),
142 Ter(tecNO_PERMISSION));
143 env.close();
144
145 env.close(std::chrono::seconds{120 + 60});
146
147 env(manage(owner, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
148
149 auto const& [availablePostDefault, totalPostDefault] = vaultAssetBalance(vaultKeylet);
150
151 BEAST_EXPECT(availablePostDefault == totalPostDefault);
152 BEAST_EXPECT(availablePostDefault == asset(0).value());
153 BEAST_EXPECT(vaultShareBalance(vaultKeylet) == sharesAvailable);
154
155 return std::make_pair(vault, vaultKeylet);
156 };
157
158 auto const testCase = [&](PrettyAsset const& asset,
159 std::string const& prefix,
160 Account const& owner,
161 Account const& depositor) {
162 {
163 testcase("VaultClawback (share) - " + prefix + " owner asset clawback fails");
164 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor);
165 // when asset is XRP or owner is not issuer clawback fail
166 // when owner is issuer precision loss occurs as vault is
167 // empty
168 auto const expectedTer = [&]() {
169 if (asset.native())
170 return Ter(temMALFORMED);
171 if (asset.raw().getIssuer() != owner.id())
172 return Ter(tecNO_PERMISSION);
173 return Ter(tecPRECISION_LOSS);
174 }();
175 env(vault.clawback({
176 .issuer = owner,
177 .id = vaultKeylet.key,
178 .holder = depositor,
179 .amount = asset(100).value(),
180 }),
181 expectedTer);
182 env.close();
183 }
184
185 {
186 testcase(
187 "VaultClawback (share) - " + prefix + " owner incomplete share clawback fails");
188 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor);
189 auto const& vaultSle = env.le(vaultKeylet);
190 if (!BEAST_EXPECT(vaultSle))
191 return;
192 Asset const share = vaultSle->at(sfShareMPTID);
193 env(vault.clawback({
194 .issuer = owner,
195 .id = vaultKeylet.key,
196 .holder = depositor,
197 .amount = share(1).value(),
198 }),
199 Ter(tecLIMIT_EXCEEDED));
200 env.close();
201 }
202
203 {
204 testcase(
205 "VaultClawback (share) - " + prefix +
206 " owner implicit complete share clawback");
207 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor);
208 env(vault.clawback({
209 .issuer = owner,
210 .id = vaultKeylet.key,
211 .holder = depositor,
212 }),
213 // when owner is issuer implicit clawback fails
214 asset.native() || asset.raw().getIssuer() != owner.id() ? Ter(tesSUCCESS)
215 : Ter(tecWRONG_ASSET));
216 env.close();
217 }
218
219 {
220 testcase(
221 "VaultClawback (share) - " + prefix +
222 " owner explicit complete share clawback succeeds");
223 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor);
224 auto const& vaultSle = env.le(vaultKeylet);
225 if (!BEAST_EXPECT(vaultSle))
226 return;
227 Asset const share = vaultSle->at(sfShareMPTID);
228 env(vault.clawback({
229 .issuer = owner,
230 .id = vaultKeylet.key,
231 .holder = depositor,
232 .amount = share(vaultShareBalance(vaultKeylet)).value(),
233 }),
234 Ter(tesSUCCESS));
235 env.close();
236 }
237 {
238 testcase("VaultClawback (share) - " + prefix + " owner can clawback own shares");
239 auto [vault, vaultKeylet] = setupVault(asset, owner, owner);
240 auto const& vaultSle = env.le(vaultKeylet);
241 if (!BEAST_EXPECT(vaultSle))
242 return;
243 Asset const share = vaultSle->at(sfShareMPTID);
244 env(vault.clawback({
245 .issuer = owner,
246 .id = vaultKeylet.key,
247 .holder = owner,
248 .amount = share(vaultShareBalance(vaultKeylet)).value(),
249 }),
250 Ter(tesSUCCESS));
251 env.close();
252 }
253
254 {
255 testcase("VaultClawback (share) - " + prefix + " empty vault share clawback fails");
256 auto [vault, vaultKeylet] = setupVault(asset, owner, owner);
257 auto const& vaultSle = env.le(vaultKeylet);
258 if (!BEAST_EXPECT(vaultSle))
259 return;
260 Asset const share = vaultSle->at(sfShareMPTID);
261 env(vault.clawback({
262 .issuer = owner,
263 .id = vaultKeylet.key,
264 .holder = owner,
265 .amount = share(vaultShareBalance(vaultKeylet)).value(),
266 }),
267 Ter(tesSUCCESS));
268
269 // Now the vault is empty, clawback again fails
270 env(vault.clawback({
271 .issuer = owner,
272 .id = vaultKeylet.key,
273 .holder = owner,
274 .amount = share(vaultShareBalance(vaultKeylet)).value(),
275 }),
276 Ter(tecNO_PERMISSION));
277 env.close();
278 }
279 };
280
281 Account const owner{"alice"};
282 Account const depositor{"bob"};
283 Account const issuer{"issuer"};
284
285 env.fund(XRP(10000), issuer, owner, depositor);
286 env.close();
287
288 // Test XRP
289 PrettyAsset const xrp = xrpIssue();
290 testCase(xrp, "XRP", owner, depositor);
291 testCase(xrp, "XRP (depositor is owner)", owner, owner);
292
293 // Test IOU
294 PrettyAsset const iou = issuer["IOU"];
295 env(fset(issuer, asfAllowTrustLineClawback));
296 env.close();
297
298 env.trust(iou(1000), owner);
299 env.trust(iou(1000), depositor);
300 env(pay(issuer, owner, iou(100)));
301 env(pay(issuer, depositor, iou(100)));
302 env.close();
303 testCase(iou, "IOU", owner, depositor);
304 testCase(iou, "IOU (owner is issuer)", issuer, depositor);
305
306 // Test MPT
307 MPTTester mptt{env, issuer, kMptInitNoFund};
308 mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
309 PrettyAsset const mpt = mptt.issuanceID();
310 mptt.authorize({.account = owner});
311 mptt.authorize({.account = depositor});
312 env(pay(issuer, owner, mpt(1000)));
313 env(pay(issuer, depositor, mpt(1000)));
314 env.close();
315 testCase(mpt, "MPT", owner, depositor);
316 testCase(mpt, "MPT (owner is issuer)", issuer, depositor);
317 }
318
319 void
321 {
322 using namespace test::jtx;
323 using namespace loan_broker;
324 using namespace loan;
325 Env env(*this);
326 env.enableFeature(fixCleanup3_1_3);
327
328 // Under featureLendingProtocolV1_1 LoanBrokerSet::preclaim only
329 // accepts closed-ended vaults; some tests using this helper later
330 // attach loan brokers to the vault. Build it as closed-ended and
331 // advance past SubscriptionDate so subsequent broker/loan setup
332 // runs in the Investment phase. VaultClawback itself is not
333 // phase-gated. See the other setupVault (share tests) for why the
334 // subscription offset must be generous.
335 auto const setupVault = [&](PrettyAsset const& asset,
336 Account const& owner,
337 Account const& depositor,
338 Account const& issuer) -> std::pair<Vault, Keylet> {
339 Vault const vault{env};
340
341 auto const& [tx, vaultKeylet, subscriptionDate] = vault.createClosedEnded(
342 {.owner = owner, .asset = asset, .subscriptionOffset = std::chrono::seconds{60}});
343 env(tx, Ter(tesSUCCESS));
344 env.close();
345
346 auto const& vaultSle = env.le(vaultKeylet);
347 BEAST_EXPECT(vaultSle != nullptr);
348 env.memoize(Account("vault", vaultSle->at(sfAccount)));
349 env(vault.deposit(
350 {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}),
351 Ter(tesSUCCESS));
352 env.close();
353
354 vault.closePastSubscription(subscriptionDate);
355
356 return std::make_pair(vault, vaultKeylet);
357 };
358
359 auto const testCase = [&](PrettyAsset const& asset,
360 std::string const& prefix,
361 Account const& owner,
362 Account const& depositor,
363 Account const& issuer) {
364 if (asset.native())
365 {
366 testcase("VaultClawback (asset) - " + prefix + " issuer XRP clawback fails");
367 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
368 // If the asset is XRP, clawback with amount fails as malformed
369 // when asset is specified.
370 env(vault.clawback({
371 .issuer = issuer,
372 .id = vaultKeylet.key,
373 .holder = issuer,
374 .amount = asset(1).value(),
375 }),
376 Ter(temMALFORMED));
377 // When asset is implicit, clawback fails as no permission.
378 env(vault.clawback({
379 .issuer = issuer,
380 .id = vaultKeylet.key,
381 .holder = issuer,
382 }),
383 Ter(tecNO_PERMISSION));
384 return;
385 }
386
387 {
388 testcase(
389 "VaultClawback (asset) - " + prefix + " clawback for different asset fails");
390 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
391
392 Account const issuer2{"issuer2"};
393 PrettyAsset const asset2 = issuer2["FOO"];
394 env(vault.clawback({
395 .issuer = issuer,
396 .id = vaultKeylet.key,
397 .holder = depositor,
398 .amount = asset2(1).value(),
399 }),
400 Ter(tecWRONG_ASSET));
401 }
402
403 {
404 testcase(
405 "VaultClawback (asset) - " + prefix +
406 " ambiguous owner/issuer asset clawback fails");
407 auto [vault, vaultKeylet] = setupVault(asset, issuer, depositor, issuer);
408 env(vault.clawback({
409 .issuer = issuer,
410 .id = vaultKeylet.key,
411 .holder = issuer,
412 }),
413 Ter(tecWRONG_ASSET));
414 }
415
416 {
417 testcase("VaultClawback (asset) - " + prefix + " non-issuer asset clawback fails");
418 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
419
420 env(vault.clawback({
421 .issuer = owner,
422 .id = vaultKeylet.key,
423 .holder = depositor,
424 }),
425 Ter(tecNO_PERMISSION));
426
427 env(vault.clawback({
428 .issuer = owner,
429 .id = vaultKeylet.key,
430 .holder = depositor,
431 .amount = asset(1).value(),
432 }),
433 Ter(tecNO_PERMISSION));
434 }
435
436 {
437 testcase("VaultClawback (asset) - " + prefix + " issuer clawback from self fails");
438 auto [vault, vaultKeylet] = setupVault(asset, owner, issuer, issuer);
439 env(vault.clawback({
440 .issuer = issuer,
441 .id = vaultKeylet.key,
442 .holder = issuer,
443 }),
444 Ter(tecNO_PERMISSION));
445 }
446
447 {
448 testcase("VaultClawback (asset) - " + prefix + " issuer share clawback fails");
449 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
450 auto const& vaultSle = env.le(vaultKeylet);
451 if (!BEAST_EXPECT(vaultSle))
452 return;
453 Asset const share = vaultSle->at(sfShareMPTID);
454
455 env(vault.clawback({
456 .issuer = issuer,
457 .id = vaultKeylet.key,
458 .holder = depositor,
459 .amount = share(1).value(),
460 }),
461 Ter(tecNO_PERMISSION));
462 }
463
464 {
465 testcase(
466 "VaultClawback (asset) - " + prefix +
467 " partial issuer asset clawback succeeds");
468 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
469
470 env(vault.clawback({
471 .issuer = issuer,
472 .id = vaultKeylet.key,
473 .holder = depositor,
474 .amount = asset(1).value(),
475 }),
476 Ter(tesSUCCESS));
477 }
478
479 {
480 testcase(
481 "VaultClawback (asset) - " + prefix + " full issuer asset clawback succeeds");
482 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
483
484 env(vault.clawback({
485 .issuer = issuer,
486 .id = vaultKeylet.key,
487 .holder = depositor,
488 .amount = asset(100).value(),
489 }),
490 Ter(tesSUCCESS));
491 }
492
493 {
494 testcase(
495 "VaultClawback (asset) - " + prefix +
496 " implicit full issuer asset clawback succeeds");
497 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
498
499 env(vault.clawback({
500 .issuer = issuer,
501 .id = vaultKeylet.key,
502 .holder = depositor,
503 }),
504 Ter(tesSUCCESS));
505 }
506
507 {
508 testcase(
509 "VaultClawback (asset) - " + prefix +
510 " zero-amount clawback clamped with outstanding loan");
511 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
512
513 auto const vaultSle = env.le(vaultKeylet);
514 if (!BEAST_EXPECT(vaultSle))
515 return;
516
517 PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
518
519 // Create a loan broker backed by this vault
520 auto const brokerKeylet =
521 keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
522 env(set(owner, vaultKeylet.key));
523 env.close();
524
525 // Depositor borrows 40 units, reducing assetsAvailable to 60
526 // while assetsTotal stays at 100
527 env(set(depositor, brokerKeylet.key, asset(40).value()),
528 loan::kInterestRate(TenthBips32(0)),
529 kGracePeriod(60),
530 kPaymentInterval(120),
531 kPaymentTotal(10),
532 Sig(sfCounterpartySignature, owner),
533 Fee(env.current()->fees().base * 2),
534 Ter(tesSUCCESS));
535 env.close();
536
537 {
538 auto const sle = env.le(vaultKeylet);
539 BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(60).value());
540 BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(100).value());
541 }
542
543 // Zero-amount clawback (= "clawback all") should succeed,
544 // clamped to assetsAvailable (60) rather than the full
545 // share value (100).
546 env(vault.clawback({
547 .issuer = issuer,
548 .id = vaultKeylet.key,
549 .holder = depositor,
550 }),
551 Ter(tesSUCCESS));
552 env.close();
553
554 // Only 60 assets clawed back; loan's 40 still outstanding
555 {
556 auto const sle = env.le(vaultKeylet);
557 BEAST_EXPECT(sle != nullptr);
558 BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(0).value());
559 BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(40).value());
560
561 // 60 of 100 shares destroyed (1:1 ratio), 40 remain
562 auto const sharesAfter = env.balance(depositor, shares);
563 BEAST_EXPECT(sharesAfter == shares(Number{4, sle->at(sfScale) + 1}));
564 }
565 }
566
567 {
568 testcase(
569 "VaultClawback (asset) - " + prefix +
570 " non-zero clawback clamped with outstanding loan");
571 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
572
573 auto const vaultSle = env.le(vaultKeylet);
574 if (!BEAST_EXPECT(vaultSle))
575 return;
576 PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
577
578 // Create a loan broker backed by this vault
579 auto const brokerKeylet =
580 keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
581 env(set(owner, vaultKeylet.key));
582 env.close();
583
584 // Depositor borrows 40 units
585 env(set(depositor, brokerKeylet.key, asset(40).value()),
586 loan::kInterestRate(TenthBips32(0)),
587 kGracePeriod(60),
588 kPaymentInterval(120),
589 kPaymentTotal(10),
590 Sig(sfCounterpartySignature, owner),
591 Fee(env.current()->fees().base * 2),
592 Ter(tesSUCCESS));
593 env.close();
594
595 {
596 auto const sle = env.le(vaultKeylet);
597 BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(60).value());
598 BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(100).value());
599 }
600
601 // Request 100 but only 60 available — clamped to 60
602 env(vault.clawback({
603 .issuer = issuer,
604 .id = vaultKeylet.key,
605 .holder = depositor,
606 .amount = asset(100).value(),
607 }),
608 Ter(tesSUCCESS));
609 env.close();
610
611 {
612 auto const sle = env.le(vaultKeylet);
613 BEAST_EXPECT(sle != nullptr);
614 BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(0).value());
615 BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(40).value());
616
617 // 60 of 100 shares destroyed (1:1 ratio), 40 remain
618 auto const sharesAfter = env.balance(depositor, shares);
619 BEAST_EXPECT(sharesAfter == shares(Number{4, sle->at(sfScale) + 1}));
620 }
621 }
622
623 {
624 testcase(
625 "VaultClawback (asset) - " + prefix +
626 " partial clawback below available with outstanding loan");
627 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
628
629 auto const vaultSle = env.le(vaultKeylet);
630 if (!BEAST_EXPECT(vaultSle))
631 return;
632 PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
633
634 // Create a loan broker backed by this vault
635 auto const brokerKeylet =
636 keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
637 env(set(owner, vaultKeylet.key));
638 env.close();
639
640 // Depositor borrows 40 units: assetsAvailable=60, assetsTotal=100
641 env(set(depositor, brokerKeylet.key, asset(40).value()),
642 loan::kInterestRate(TenthBips32(0)),
643 kGracePeriod(60),
644 kPaymentInterval(120),
645 kPaymentTotal(10),
646 Sig(sfCounterpartySignature, owner),
647 Fee(env.current()->fees().base * 2),
648 Ter(tesSUCCESS));
649 env.close();
650
651 {
652 auto const sle = env.le(vaultKeylet);
653 BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(60).value());
654 BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(100).value());
655 }
656
657 // Clawback 30 — well under available (60), no clamping needed
658 env(vault.clawback({
659 .issuer = issuer,
660 .id = vaultKeylet.key,
661 .holder = depositor,
662 .amount = asset(30).value(),
663 }),
664 Ter(tesSUCCESS));
665 env.close();
666
667 {
668 auto const sle = env.le(vaultKeylet);
669 BEAST_EXPECT(sle != nullptr);
670 BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(30).value());
671 BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(70).value());
672
673 // 30 of 100 shares destroyed (1:1 ratio), 70 remain
674 auto const sharesAfter = env.balance(depositor, shares);
675 BEAST_EXPECT(sharesAfter == shares(Number{7, sle->at(sfScale) + 1}));
676 }
677 }
678
679 {
680 testcase(
681 "VaultClawback (asset) - " + prefix +
682 " clawback exactly equal to available with outstanding loan");
683 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
684
685 auto const vaultSle = env.le(vaultKeylet);
686 if (!BEAST_EXPECT(vaultSle))
687 return;
688 PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
689
690 auto const brokerKeylet =
691 keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
692 env(set(owner, vaultKeylet.key));
693 env.close();
694
695 // Depositor borrows 40 units: assetsAvailable=60, assetsTotal=100
696 env(set(depositor, brokerKeylet.key, asset(40).value()),
697 loan::kInterestRate(TenthBips32(0)),
698 kGracePeriod(60),
699 kPaymentInterval(120),
700 kPaymentTotal(10),
701 Sig(sfCounterpartySignature, owner),
702 Fee(env.current()->fees().base * 2),
703 Ter(tesSUCCESS));
704 env.close();
705
706 // Clawback exactly 60 — at the boundary, no clamping needed
707 env(vault.clawback({
708 .issuer = issuer,
709 .id = vaultKeylet.key,
710 .holder = depositor,
711 .amount = asset(60).value(),
712 }),
713 Ter(tesSUCCESS));
714 env.close();
715
716 {
717 auto const sle = env.le(vaultKeylet);
718 BEAST_EXPECT(sle != nullptr);
719 BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(0).value());
720 BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(40).value());
721
722 // 60 of 100 shares destroyed (1:1 ratio), 40 remain
723 auto const sharesAfter = env.balance(depositor, shares);
724 BEAST_EXPECT(sharesAfter == shares(Number{4, sle->at(sfScale) + 1}));
725 }
726 }
727
728 {
729 testcase(
730 "VaultClawback (asset) - " + prefix +
731 " clawback with zero available (fully borrowed)");
732 auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
733
734 auto const vaultSle = env.le(vaultKeylet);
735 if (!BEAST_EXPECT(vaultSle))
736 return;
737 PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
738
739 auto const brokerKeylet =
740 keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
741 env(set(owner, vaultKeylet.key));
742 env.close();
743
744 // Depositor borrows all 100 units: assetsAvailable=0, assetsTotal=100
745 env(set(depositor, brokerKeylet.key, asset(100).value()),
746 loan::kInterestRate(TenthBips32(0)),
747 kGracePeriod(60),
748 kPaymentInterval(120),
749 kPaymentTotal(10),
750 Sig(sfCounterpartySignature, owner),
751 Fee(env.current()->fees().base * 2),
752 Ter(tesSUCCESS));
753 env.close();
754
755 {
756 auto const sle = env.le(vaultKeylet);
757 BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(0).value());
758 BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(100).value());
759 }
760
761 auto const sharesBefore = env.balance(depositor, shares);
762
763 // Zero-amount clawback — nothing available, clamped to 0,
764 // resulting in zero shares destroyed → tecPRECISION_LOSS
765 env(vault.clawback({
766 .issuer = issuer,
767 .id = vaultKeylet.key,
768 .holder = depositor,
769 }),
770 Ter(tecPRECISION_LOSS));
771 env.close();
772
773 // Explicit amount clawback — also nothing available
774 env(vault.clawback({
775 .issuer = issuer,
776 .id = vaultKeylet.key,
777 .holder = depositor,
778 .amount = asset(50).value(),
779 }),
780 Ter(tecPRECISION_LOSS));
781 env.close();
782
783 {
784 // Nothing changed — vault and shares unchanged
785 auto const sle = env.le(vaultKeylet);
786 BEAST_EXPECT(sle != nullptr);
787 BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(0).value());
788 BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(100).value());
789 auto const sharesAfter = env.balance(depositor, shares);
790 BEAST_EXPECT(sharesAfter == sharesBefore);
791 }
792 }
793 };
794
795 Account const owner{"alice"};
796 Account const depositor{"bob"};
797 Account const issuer{"issuer"};
798
799 env.fund(XRP(10000), issuer, owner, depositor);
800 env.close();
801
802 // Test XRP
803 PrettyAsset const xrp = xrpIssue();
804 testCase(xrp, "XRP", owner, depositor, issuer);
805
806 // Test IOU
807 PrettyAsset const iou = issuer["IOU"];
808 env(fset(issuer, asfAllowTrustLineClawback));
809 env.close();
810 env.trust(iou(2000), owner);
811 env.trust(iou(2000), depositor);
812 env(pay(issuer, owner, iou(2000)));
813 env(pay(issuer, depositor, iou(2000)));
814 env.close();
815 testCase(iou, "IOU", owner, depositor, issuer);
816
817 // Test MPT
818 MPTTester mptt{env, issuer, kMptInitNoFund};
819 mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
820
821 PrettyAsset const mpt = mptt.issuanceID();
822 mptt.authorize({.account = owner});
823 mptt.authorize({.account = depositor});
824 env(pay(issuer, depositor, mpt(2000)));
825 env.close();
826 testCase(mpt, "MPT", owner, depositor, issuer);
827
828 // Test pre-fixCleanup3_1_3 legacy path: zero-amount clawback
829 // returns early without clamping to assetsAvailable.
830 {
831 testcase(
832 "VaultClawback (asset) - IOU pre-fixCleanup3_1_3"
833 " zero-amount clawback unclamped with outstanding loan");
834
835 env.disableFeature(fixCleanup3_1_3);
836
837 auto [vault, vaultKeylet] = setupVault(iou, owner, depositor, issuer);
838
839 auto const vaultSle = env.le(vaultKeylet);
840 BEAST_EXPECT(vaultSle != nullptr);
841 if (!vaultSle)
842 return;
843
844 PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
845
846 // Create a loan broker backed by this vault
847 auto const brokerKeylet =
848 keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
849 env(set(owner, vaultKeylet.key));
850 env.close();
851
852 // Depositor borrows 40 units, reducing assetsAvailable to 60
853 // while assetsTotal stays at 100
854 env(set(depositor, brokerKeylet.key, iou(40).value()),
855 loan::kInterestRate(TenthBips32(0)),
856 kGracePeriod(60),
857 kPaymentInterval(120),
858 kPaymentTotal(10),
859 Sig(sfCounterpartySignature, owner),
860 Fee(env.current()->fees().base * 2),
861 Ter(tesSUCCESS));
862 env.close();
863
864 {
865 auto const sle = env.le(vaultKeylet);
866 BEAST_EXPECT(sle->at(sfAssetsAvailable) == iou(60).value());
867 BEAST_EXPECT(sle->at(sfAssetsTotal) == iou(100).value());
868 }
869
870 auto const sharesBefore = env.balance(depositor, shares);
871
872 // Legacy: zero-amount clawback tries to recover the full
873 // share value (100) without clamping to assetsAvailable (60).
874 // This causes the vault balance to go negative, triggering
875 // the sanity check in doApply → tefINTERNAL.
876 env(vault.clawback({
877 .issuer = issuer,
878 .id = vaultKeylet.key,
879 .holder = depositor,
880 }),
881 Ter(tefINTERNAL));
882 env.close();
883
884 {
885 // Transaction rolled back — vault and shares unchanged
886 auto const sle = env.le(vaultKeylet);
887 BEAST_EXPECT(sle != nullptr);
888 BEAST_EXPECT(sle->at(sfAssetsAvailable) == iou(60).value());
889 BEAST_EXPECT(sle->at(sfAssetsTotal) == iou(100).value());
890 auto const sharesAfter = env.balance(depositor, shares);
891 BEAST_EXPECT(sharesAfter == sharesBefore);
892 }
893
894 env.enableFeature(fixCleanup3_1_3);
895 }
896 }
897
898 void
900 {
901 using namespace test::jtx;
902 using namespace std::literals;
903
904 // Verify vault deposit/withdraw/clawback respect sfLockedAmount.
905 // When MPT tokens are escrowed, sfMPTAmount is reduced and
906 // sfLockedAmount is increased. Vault operations go through
907 // accountSend/accountHolds which read sfMPTAmount, so escrowed
908 // tokens are naturally excluded.
909
910 {
911 testcase("Vault deposit fails when MPT asset is escrowed");
912
913 Env env{*this, testableAmendments()};
914 auto const baseFee = env.current()->fees().base;
915 Account const owner{"owner"};
916 Account const depositor{"depositor"};
917 Account const issuer{"issuer"};
918 Account const bob{"bob"};
919
920 env.fund(XRP(10000), issuer, owner, depositor, bob);
921 env.close();
922
923 MPTTester mptt{env, issuer, kMptInitNoFund};
924 mptt.create(
925 {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTCanEscrow});
926 mptt.authorize({.account = owner});
927 mptt.authorize({.account = depositor});
928 mptt.authorize({.account = bob});
929 PrettyAsset const asset = mptt.issuanceID();
930 env(pay(issuer, depositor, asset(100)));
931 env.close();
932
933 // Escrow 60 of 100 MPT tokens: sfMPTAmount drops to 40
934 auto const escrowSeq = env.seq(depositor);
935 env(escrow::create(depositor, bob, asset(60)),
936 escrow::kCondition(escrow::kCb1),
937 escrow::kFinishTime(env.now() + 1s),
938 Fee(baseFee * 150),
939 Ter(tesSUCCESS));
940 env.close();
941
942 Vault const vault{env};
943 auto [tx, vaultKeylet] = vault.create({.owner = owner, .asset = asset});
944 env(tx, Ter(tesSUCCESS));
945 env.close();
946
947 // Deposit 100 should fail — only 40 spendable
948 env(vault.deposit(
949 {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}),
951 env.close();
952
953 // Deposit 40 (the unlocked balance) should succeed
954 env(vault.deposit({.depositor = depositor, .id = vaultKeylet.key, .amount = asset(40)}),
955 Ter(tesSUCCESS));
956 env.close();
957
958 {
959 auto const sle = env.le(vaultKeylet);
960 BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(40).value());
961 }
962
963 // Clean up escrow
964 env(escrow::finish(bob, depositor, escrowSeq),
965 escrow::kCondition(escrow::kCb1),
966 escrow::kFulfillment(escrow::kFb1),
967 Fee(baseFee * 150),
968 Ter(tesSUCCESS));
969 env.close();
970 }
971
972 {
973 testcase("Vault withdraw respects escrowed shares");
974
975 Env env{*this, testableAmendments()};
976 auto const baseFee = env.current()->fees().base;
977 Account const owner{"owner"};
978 Account const depositor{"depositor"};
979 Account const issuer{"issuer"};
980 Account const bob{"bob"};
981
982 env.fund(XRP(10000), issuer, owner, depositor, bob);
983 env.close();
984
985 MPTTester mptt{env, issuer, kMptInitNoFund};
986 mptt.create(
987 {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTCanEscrow});
988 mptt.authorize({.account = owner});
989 mptt.authorize({.account = depositor});
990 PrettyAsset const asset = mptt.issuanceID();
991 env(pay(issuer, depositor, asset(100)));
992 env.close();
993
994 Vault const vault{env};
995 auto [tx, vaultKeylet] = vault.create({.owner = owner, .asset = asset});
996 env(tx, Ter(tesSUCCESS));
997 env.close();
998
999 // Deposit 100 → get shares
1000 env(vault.deposit(
1001 {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}),
1002 Ter(tesSUCCESS));
1003 env.close();
1004
1005 auto const vaultSle = env.le(vaultKeylet);
1006 if (!BEAST_EXPECT(vaultSle))
1007 return;
1008 env.memoize(Account("vault", vaultSle->at(sfAccount)));
1009 PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
1010
1011 // Authorize bob for share MPT so he can receive escrowed shares
1012 auto const shareMPTID = vaultSle->at(sfShareMPTID);
1013 {
1014 json::Value jv;
1015 jv[jss::Account] = bob.human();
1016 jv[sfMPTokenIssuanceID] = to_string(shareMPTID);
1017 jv[jss::TransactionType] = jss::MPTokenAuthorize;
1018 env(jv, Ter(tesSUCCESS));
1019 env.close();
1020 }
1021
1022 // Escrow 60% of shares
1023 auto const escrowAmount = shares(Number{6, vaultSle->at(sfScale) + 1});
1024 env(escrow::create(depositor, bob, escrowAmount),
1025 escrow::kCondition(escrow::kCb1),
1026 escrow::kFinishTime(env.now() + 1s),
1027 Fee(baseFee * 150),
1028 Ter(tesSUCCESS));
1029 env.close();
1030
1031 // Withdraw all 100 should fail — only 40% of shares are unlocked
1032 env(vault.withdraw(
1033 {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}),
1035 env.close();
1036
1037 // Withdraw 40 (matching unlocked shares) should succeed
1038 env(vault.withdraw(
1039 {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(40)}),
1040 Ter(tesSUCCESS));
1041 env.close();
1042
1043 {
1044 auto const sle = env.le(vaultKeylet);
1045 BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(60).value());
1046 }
1047 }
1048
1049 {
1050 testcase("Vault clawback only recovers unlocked shares");
1051
1052 Env env{*this, testableAmendments() | fixCleanup3_1_3};
1053 auto const baseFee = env.current()->fees().base;
1054 Account const owner{"owner"};
1055 Account const depositor{"depositor"};
1056 Account const issuer{"issuer"};
1057 Account const bob{"bob"};
1058
1059 env.fund(XRP(10000), issuer, owner, depositor, bob);
1060 env.close();
1061
1062 MPTTester mptt{env, issuer, kMptInitNoFund};
1063 mptt.create(
1064 {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTCanEscrow});
1065 mptt.authorize({.account = owner});
1066 mptt.authorize({.account = depositor});
1067 PrettyAsset const asset = mptt.issuanceID();
1068 env(pay(issuer, depositor, asset(100)));
1069 env.close();
1070
1071 Vault const vault{env};
1072 auto [tx, vaultKeylet] = vault.create({.owner = owner, .asset = asset});
1073 env(tx, Ter(tesSUCCESS));
1074 env.close();
1075
1076 // Deposit 100 → get shares
1077 env(vault.deposit(
1078 {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}),
1079 Ter(tesSUCCESS));
1080 env.close();
1081
1082 auto const vaultSle = env.le(vaultKeylet);
1083 if (!BEAST_EXPECT(vaultSle))
1084 return;
1085 env.memoize(Account("vault", vaultSle->at(sfAccount)));
1086 PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
1087
1088 // Authorize bob for share MPT so he can receive escrowed shares
1089 auto const shareMPTID = vaultSle->at(sfShareMPTID);
1090 {
1091 json::Value jv;
1092 jv[jss::Account] = bob.human();
1093 jv[sfMPTokenIssuanceID] = to_string(shareMPTID);
1094 jv[jss::TransactionType] = jss::MPTokenAuthorize;
1095 env(jv, Ter(tesSUCCESS));
1096 env.close();
1097 }
1098
1099 // Escrow 60% of shares
1100 auto const escrowAmount = shares(Number{6, vaultSle->at(sfScale) + 1});
1101 env(escrow::create(depositor, bob, escrowAmount),
1102 escrow::kCondition(escrow::kCb1),
1103 escrow::kFinishTime(env.now() + 1s),
1104 Fee(baseFee * 150),
1105 Ter(tesSUCCESS));
1106 env.close();
1107
1108 // Zero-amount clawback ("all") — should only recover assets
1109 // corresponding to unlocked shares (40%)
1110 env(vault.clawback({
1111 .issuer = issuer,
1112 .id = vaultKeylet.key,
1113 .holder = depositor,
1114 }),
1115 Ter(tesSUCCESS));
1116 env.close();
1117
1118 {
1119 auto const sle = env.le(vaultKeylet);
1120 BEAST_EXPECT(sle != nullptr);
1121 // Only 40 of 100 assets recovered (matching 40% unlocked shares)
1122 BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(60).value());
1123 BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(60).value());
1124
1125 // Depositor's unlocked shares are now 0
1126 auto const sharesAfter = env.balance(depositor, shares);
1127 BEAST_EXPECT(sharesAfter == shares(0));
1128 }
1129 }
1130 }
1131
1132 // The vault's pseudo-account issues the shares, so it never holds any, and naming it as Holder
1133 // asks for a clawback that cannot move anything. Before the rule an implicit amount resolved to
1134 // zero shares and ended in tecPRECISION_LOSS, while an explicit one debited the vault first and
1135 // was caught by the invariant that shares must move.
1136 void
1138 {
1139 using namespace test::jtx;
1140
1141 auto const runScenario = [this](FeatureBitset features, std::string const& prefix) {
1142 bool const guarded = features[fixCleanup3_4_0];
1143 Env env{*this, features};
1144
1145 Account const owner{"owner"};
1146 Account const depositor{"depositor"};
1147 Account const issuer{"issuer"};
1148
1149 env.fund(XRP(1'000), owner, depositor, issuer);
1150 env.close();
1151
1152 env(fset(issuer, asfAllowTrustLineClawback));
1153 env.close();
1154
1155 PrettyAsset const asset = issuer["IOU"];
1156 env.trust(asset(1'000), owner);
1157 env.trust(asset(1'000), depositor);
1158 env(pay(issuer, depositor, asset(200)));
1159 env.close();
1160
1161 Vault const vault{env};
1162 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
1163 env(tx);
1164 env.close();
1165
1166 auto const vaultSle = env.le(keylet);
1167 if (!BEAST_EXPECT(vaultSle))
1168 return;
1169 Account const pseudo{"vault pseudo-account", vaultSle->at(sfAccount)};
1170 env.memoize(pseudo);
1171
1172 env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)}));
1173 env.close();
1174
1175 auto const assetsBefore = [&]() -> Number {
1176 auto const sle = env.le(keylet);
1177 if (!BEAST_EXPECT(sle))
1178 return Number{};
1179 return sle->at(sfAssetsTotal);
1180 }();
1181
1182 {
1183 testcase("VaultClawback - " + prefix + " pseudo-account holder, implicit amount");
1184 env(vault.clawback({
1185 .issuer = issuer,
1186 .id = keylet.key,
1187 .holder = pseudo,
1188 }),
1189 Ter(guarded ? TER{tecPSEUDO_ACCOUNT} : TER{tecPRECISION_LOSS}));
1190 env.close();
1191 }
1192
1193 {
1194 testcase("VaultClawback - " + prefix + " pseudo-account holder, explicit amount");
1195 env(vault.clawback({
1196 .issuer = issuer,
1197 .id = keylet.key,
1198 .holder = pseudo,
1199 .amount = asset(10).value(),
1200 }),
1201 Ter(guarded ? TER{tecPSEUDO_ACCOUNT} : TER{tecINVARIANT_FAILED}));
1202 env.close();
1203 }
1204
1205 // Neither attempt may touch the vault, whichever way it was refused.
1206 auto const sleAfter = env.le(keylet);
1207 BEAST_EXPECT(sleAfter && sleAfter->at(sfAssetsTotal) == assetsBefore);
1208 };
1209
1210 runScenario(all_, "post-rule");
1211 runScenario(all_ - fixCleanup3_4_0, "pre-rule");
1212 }
1213
1214public:
1215 void
1223};
1224
1226
1227} // namespace xrpl
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
Represents a JSON value.
Definition json_value.h:117
AccountID const & getIssuer() const
Definition Asset.cpp:21
Number is a floating point type that can represent a wide range of values.
Definition Number.h:351
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
Definition SeqProxy.h:62
void run() override
Runs the suite.
Shared base for the Vault*_test family under src/test/app/vault/.
FeatureBitset const all_
test::jtx::PrettyAsset PrettyAsset
T make_pair(T... args)
Keylet computation functions.
Definition Indexes.h:40
Keylet loanBroker(AccountID const &owner, SeqProxy const &seq) noexcept
Definition Indexes.cpp:597
Keylet loan(UInt256 const &loanBrokerID, SeqProxy const &loanSeq) noexcept
Definition Indexes.cpp:603
Keylet mptokenIssuance(MPTID const &issuanceID) noexcept
Definition Indexes.cpp:567
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
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
@ tefINTERNAL
Definition TER.h:168
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:657
@ temMALFORMED
Definition TER.h:75
TERSubset< CanCvtToTER > TER
Definition TER.h:654
@ tecWRONG_ASSET
Definition TER.h:368
@ tecPSEUDO_ACCOUNT
Definition TER.h:370
@ tecINVARIANT_FAILED
Definition TER.h:321
@ tecINSUFFICIENT_FUNDS
Definition TER.h:333
@ tecPRECISION_LOSS
Definition TER.h:371
@ tecLIMIT_EXCEEDED
Definition TER.h:369
@ tecNO_PERMISSION
Definition TER.h:313
@ tesSUCCESS
Definition TER.h:250
A pair of SHAMap key and LedgerEntryType.
Definition Keylet.h:20