xrpld
Loading...
Searching...
No Matches
VaultDomain_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/credentials.h>
7#include <test/jtx/deposit.h>
8#include <test/jtx/flags.h>
9#include <test/jtx/offer.h>
10#include <test/jtx/pay.h>
11#include <test/jtx/permissioned_domains.h>
12#include <test/jtx/ter.h>
13#include <test/jtx/vault.h>
14
15#include <xrpl/basics/base_uint.h>
16#include <xrpl/basics/chrono.h>
17#include <xrpl/beast/unit_test/suite.h>
18#include <xrpl/json/json_forwards.h>
19#include <xrpl/json/json_value.h>
20#include <xrpl/protocol/AccountID.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/TER.h>
29#include <xrpl/protocol/TxFlags.h>
30#include <xrpl/protocol/jss.h>
31
32#include <chrono>
33#include <cstdint>
34#include <string>
35#include <tuple>
36#include <utility>
37
38namespace xrpl {
39
41{
42private:
43 void
45 {
46 using namespace test::jtx;
47
48 testcase("private vault");
49
50 Env env{*this, testableAmendments()};
51 Account const issuer{"issuer"};
52 Account const owner{"owner"};
53 Account const depositor{"depositor"};
54 Account const charlie{"charlie"};
55 Account const pdOwner{"pdOwner"};
56 Account const credIssuer1{"credIssuer1"};
57 Account const credIssuer2{"credIssuer2"};
58 std::string const credType = "credential";
59 Vault const vault{env};
60 env.fund(XRP(1000), issuer, owner, depositor, charlie, pdOwner, credIssuer1, credIssuer2);
61 env.close();
62 env(fset(issuer, asfAllowTrustLineClawback));
63 env.close();
64 env.require(Flags(issuer, asfAllowTrustLineClawback));
65
66 PrettyAsset const asset = issuer["IOU"];
67 env.trust(asset(1000), owner);
68 env(pay(issuer, owner, asset(500)));
69 env.trust(asset(1000), depositor);
70 env(pay(issuer, depositor, asset(500)));
71 env.trust(asset(1000), charlie);
72 env(pay(issuer, charlie, asset(5)));
73 env.close();
74
75 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset, .flags = tfVaultPrivate});
76 env(tx);
77 env.close();
78 BEAST_EXPECT(env.le(keylet));
79
80 {
81 testcase("private vault owner can deposit");
82 auto tx = vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(50)});
83 env(tx);
84 }
85
86 {
87 testcase("private vault depositor not authorized yet");
88 auto tx =
89 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
90 env(tx, Ter{tecNO_AUTH});
91 }
92
93 {
94 testcase("private vault cannot set non-existing domain");
95 auto tx = vault.set({.owner = owner, .id = keylet.key});
96 tx[sfDomainID] = to_string(BaseUInt<256>(42ul));
97 env(tx, Ter{tecOBJECT_NOT_FOUND});
98 }
99
100 {
101 testcase("private vault set domainId");
102
103 {
104 pdomain::Credentials const credentials1{
105 {.issuer = credIssuer1, .credType = credType}};
106
107 env(pdomain::setTx(pdOwner, credentials1));
108 auto const domainId1 = [&]() {
109 auto tx = env.tx()->getJson(JsonOptions::Values::None);
110 return pdomain::getNewDomain(env.meta());
111 }();
112
113 auto tx = vault.set({.owner = owner, .id = keylet.key});
114 tx[sfDomainID] = to_string(domainId1);
115 env(tx);
116 env.close();
117
118 // Update domain second time, should be harmless
119 env(tx);
120 env.close();
121 }
122
123 {
124 pdomain::Credentials const credentials{
125 {.issuer = credIssuer1, .credType = credType},
126 {.issuer = credIssuer2, .credType = credType}};
127
128 env(pdomain::setTx(pdOwner, credentials));
129 auto const domainId = [&]() {
130 auto tx = env.tx()->getJson(JsonOptions::Values::None);
131 return pdomain::getNewDomain(env.meta());
132 }();
133
134 auto tx = vault.set({.owner = owner, .id = keylet.key});
135 tx[sfDomainID] = to_string(domainId);
136 env(tx);
137 env.close();
138
139 // Should be idempotent
140 tx = vault.set({.owner = owner, .id = keylet.key});
141 tx[sfDomainID] = to_string(domainId);
142 env(tx);
143 env.close();
144 }
145 }
146
147 {
148 testcase("private vault depositor still not authorized");
149 auto tx =
150 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
151 env(tx, Ter{tecNO_AUTH});
152 env.close();
153 }
154
155 auto const credKeylet = credentials::keylet(depositor, credIssuer1, credType);
156 {
157 testcase("private vault depositor now authorized");
158 env(credentials::create(depositor, credIssuer1, credType));
159 env(credentials::accept(depositor, credIssuer1, credType));
160 env(credentials::create(charlie, credIssuer1, credType));
161 // charlie's credential not accepted
162 env.close();
163 auto credSle = env.le(credKeylet);
164 BEAST_EXPECT(credSle != nullptr);
165
166 auto tx =
167 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
168 env(tx);
169 env.close();
170
171 tx = vault.deposit({.depositor = charlie, .id = keylet.key, .amount = asset(50)});
172 env(tx, Ter{tecNO_AUTH});
173 env.close();
174 }
175
176 {
177 testcase("private vault depositor lost authorization");
178 env(credentials::deleteCred(credIssuer1, depositor, credIssuer1, credType));
179 env(credentials::deleteCred(credIssuer1, charlie, credIssuer1, credType));
180 env.close();
181 auto credSle = env.le(credKeylet);
182 BEAST_EXPECT(credSle == nullptr);
183
184 auto tx =
185 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
186 env(tx, Ter{tecNO_AUTH});
187 env.close();
188 }
189
190 auto const shares = [&env, keylet = keylet, this]() -> Asset {
191 auto const vault = env.le(keylet);
192 BEAST_EXPECT(vault != nullptr);
193 return MPTIssue(vault->at(sfShareMPTID));
194 }();
195
196 {
197 testcase("private vault expired authorization");
198 uint32_t const closeTime =
199 env.current()->header().parentCloseTime.time_since_epoch().count();
200 {
201 auto tx0 = credentials::create(depositor, credIssuer2, credType);
202 tx0[sfExpiration] = closeTime + 20;
203 env(tx0);
204 tx0 = credentials::create(charlie, credIssuer2, credType);
205 tx0[sfExpiration] = closeTime + 20;
206 env(tx0);
207 env.close();
208
209 env(credentials::accept(depositor, credIssuer2, credType));
210 env(credentials::accept(charlie, credIssuer2, credType));
211 env.close();
212 }
213
214 {
215 auto tx1 =
216 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
217 env(tx1);
218 env.close();
219
220 auto const tokenKeylet =
221 keylet::mptoken(shares.get<MPTIssue>().getMptID(), depositor.id());
222 BEAST_EXPECT(env.le(tokenKeylet) != nullptr);
223 }
224
225 {
226 // time advance
227 env.close();
228 env.close();
229 env.close();
230
231 auto const credsKeylet = credentials::keylet(depositor, credIssuer2, credType);
232 BEAST_EXPECT(env.le(credsKeylet) != nullptr);
233
234 auto tx2 =
235 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(1)});
236 env(tx2, Ter{tecEXPIRED});
237 env.close();
238
239 BEAST_EXPECT(env.le(credsKeylet) == nullptr);
240 }
241
242 {
243 auto const credsKeylet = credentials::keylet(charlie, credIssuer2, credType);
244 BEAST_EXPECT(env.le(credsKeylet) != nullptr);
245 auto const tokenKeylet =
246 keylet::mptoken(shares.get<MPTIssue>().getMptID(), charlie.id());
247 BEAST_EXPECT(env.le(tokenKeylet) == nullptr);
248
249 auto tx3 =
250 vault.deposit({.depositor = charlie, .id = keylet.key, .amount = asset(2)});
251 env(tx3, Ter{tecEXPIRED});
252
253 env.close();
254 BEAST_EXPECT(env.le(credsKeylet) == nullptr);
255 BEAST_EXPECT(env.le(tokenKeylet) == nullptr);
256 }
257 }
258
259 {
260 testcase("private vault reset domainId");
261 auto tx = vault.set({.owner = owner, .id = keylet.key});
262 tx[sfDomainID] = "0";
263 env(tx);
264 env.close();
265
266 tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
267 env(tx, Ter{tecNO_AUTH});
268 env.close();
269
270 tx = vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
271 env(tx);
272 env.close();
273
274 tx = vault.clawback(
275 {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(0)});
276 env(tx);
277
278 tx = vault.clawback(
279 {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(0)});
280 env(tx);
281 env.close();
282
283 tx = vault.del({
284 .owner = owner,
285 .id = keylet.key,
286 });
287 env(tx);
288 }
289 }
290
291 void
293 {
294 using namespace test::jtx;
295
296 testcase("private vault share transfer after depositor loses domain");
297
298 // The "Private Vault - Access Control Rules" spec requires that a holder who
299 // loses Layer 2 (Permissioned Domain membership) after acquiring shares be
300 // blocked from sending them onward, by P2P transfer or DEX offer, the same
301 // way a brand-new never-authorized holder is blocked. Only withdrawal to
302 // self is meant to stay open.
303 //
304 // For a domain-gated share MPToken, requireAuth()'s escape hatch for
305 // holders who already have an MPToken (MPTokenHelpers.cpp) only applies to
306 // the classic explicit-issuer-authorization flag, which
307 // enforceMPTokenAuthorization documents as "meaningless" for
308 // domain-authorized holders and never sets. So a stale MPToken does not
309 // carry authorization forward once the account's domain credential is
310 // gone, and both actions below are correctly blocked.
311
312 Env env{*this, testableAmendments()};
313 Account const issuer{"issuer"};
314 Account const owner{"owner"};
315 Account const depositor{"depositor"};
316 Account const bob{"bob"};
317 Account const pdOwner{"pdOwner"};
318 Account const credIssuer{"credIssuer"};
319 std::string const credType = "credential";
320 Vault const vault{env};
321 env.fund(XRP(1000), issuer, owner, depositor, bob, pdOwner, credIssuer);
322 env.close();
323
324 PrettyAsset const asset = issuer["IOU"];
325 env.trust(asset(1000), owner);
326 env(pay(issuer, owner, asset(500)));
327 env.trust(asset(1000), depositor);
328 env(pay(issuer, depositor, asset(500)));
329 env.trust(asset(1000), bob);
330 env(pay(issuer, bob, asset(500)));
331 env.close();
332
333 // Transferable shares (no tfVaultShareNonTransferable): sections 3.3/3.4 of
334 // the spec (DEX trading / P2P transfer) only apply to transferable shares.
335 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset, .flags = tfVaultPrivate});
336 env(tx);
337 env.close();
338
339 pdomain::Credentials const credentials{{.issuer = credIssuer, .credType = credType}};
340 env(pdomain::setTx(pdOwner, credentials));
341 auto const domainId = [&]() {
342 auto tx = env.tx()->getJson(JsonOptions::Values::None);
343 return pdomain::getNewDomain(env.meta());
344 }();
345 {
346 auto domainTx = vault.set({.owner = owner, .id = keylet.key});
347 domainTx[sfDomainID] = to_string(domainId);
348 env(domainTx);
349 env.close();
350 }
351
352 // Both depositor and bob acquire domain membership and deposit, so each
353 // ends up with an authorized share MPToken.
354 env(credentials::create(depositor, credIssuer, credType));
355 env(credentials::accept(depositor, credIssuer, credType));
356 env(credentials::create(bob, credIssuer, credType));
357 env(credentials::accept(bob, credIssuer, credType));
358 env.close();
359
360 env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)}));
361 env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(100)}));
362 env.close();
363
364 auto const shares = [&env, keylet = keylet, this]() -> PrettyAsset {
365 auto const sle = env.le(keylet);
366 BEAST_EXPECT(sle != nullptr);
367 return MPTIssue(sle->at(sfShareMPTID));
368 }();
369
370 // Depositor loses Layer 2: their Permissioned Domain credential is revoked.
371 auto const credKeylet = credentials::keylet(depositor, credIssuer, credType);
372 env(credentials::deleteCred(credIssuer, depositor, credIssuer, credType));
373 env.close();
374 BEAST_EXPECT(env.le(credKeylet) == nullptr);
375
376 // Sanity check, mirrors testWithDomainCheck's "not authorized yet" case: a
377 // brand-new depositor with no MPToken yet is still correctly blocked. The
378 // gap below is specific to holders who already hold shares.
379 {
380 Account const charlie{"charlie"};
381 env.fund(XRP(1000), charlie);
382 env.close();
383 auto depTx =
384 vault.deposit({.depositor = charlie, .id = keylet.key, .amount = asset(1)});
385 env(depTx, Ter{tecNO_AUTH});
386 }
387
388 // P2P transfer: spec section 3.4 requires this blocked once Layer 2 is
389 // lost, and it is.
390 env(pay(depositor, bob, shares(1)), Ter{tecNO_AUTH});
391 env.close();
392
393 // DEX/CLOB: spec section 3.3 requires the seller leg blocked the same way.
394 // The offer can't even be created: preclaim treats the seller as
395 // unfunded once their share balance reads as zero for auth purposes.
396 env(offer(depositor, XRP(1), shares(1)), Ter{tecUNFUNDED_OFFER});
397 env.close();
398 BEAST_EXPECT(expectOffers(env, depositor, 0));
399 }
400
401 void
403 {
404 using namespace test::jtx;
405
406 testcase("private vault share purchase via DEX requires buyer domain membership");
407
408 // The "Private Vault - Access Control Rules" spec requires the buyer leg
409 // of a DEX trade in private-vault shares to hold Layer 1 and Layer 2 as
410 // well, not just the seller.
411
412 Env env{*this, testableAmendments()};
413 Account const issuer{"issuer"};
414 Account const owner{"owner"};
415 Account const bob{"bob"};
416 Account const charlie{"charlie"};
417 Account const pdOwner{"pdOwner"};
418 Account const credIssuer{"credIssuer"};
419 std::string const credType = "credential";
420 Vault const vault{env};
421 env.fund(XRP(1000), issuer, owner, bob, charlie, pdOwner, credIssuer);
422 env.close();
423
424 PrettyAsset const asset = issuer["IOU"];
425 env.trust(asset(1000), owner);
426 env(pay(issuer, owner, asset(500)));
427 env.trust(asset(1000), bob);
428 env(pay(issuer, bob, asset(500)));
429 env.close();
430
431 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset, .flags = tfVaultPrivate});
432 env(tx);
433 env.close();
434
435 pdomain::Credentials const credentials{{.issuer = credIssuer, .credType = credType}};
436 env(pdomain::setTx(pdOwner, credentials));
437 auto const domainId = [&]() {
438 auto tx = env.tx()->getJson(JsonOptions::Values::None);
439 return pdomain::getNewDomain(env.meta());
440 }();
441 {
442 auto domainTx = vault.set({.owner = owner, .id = keylet.key});
443 domainTx[sfDomainID] = to_string(domainId);
444 env(domainTx);
445 env.close();
446 }
447
448 // Only bob joins the domain and deposits; charlie never does.
449 env(credentials::create(bob, credIssuer, credType));
450 env(credentials::accept(bob, credIssuer, credType));
451 env.close();
452 env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(100)}));
453 env.close();
454
455 auto const shares = [&env, keylet = keylet, this]() -> PrettyAsset {
456 auto const sle = env.le(keylet);
457 BEAST_EXPECT(sle != nullptr);
458 return MPTIssue(sle->at(sfShareMPTID));
459 }();
460
461 // Bob (domain member, holds shares) rests a sell offer.
462 env(offer(bob, XRP(1), shares(1)));
463 env.close();
464 BEAST_EXPECT(expectOffers(env, bob, 1));
465
466 // Charlie never held the domain credential. Buying shares via a
467 // crossing offer must be blocked the same way a direct MPTokenAuthorize
468 // + pay attempt already is (see testWithDomainChecXRP's "cannot pay
469 // shares to 3rd party"): checkAcceptAsset() rejects the offer outright
470 // in preclaim, before any funding check is even reached.
471 env(offer(charlie, shares(1), XRP(1)), Ter{tecNO_AUTH});
472 env.close();
473 BEAST_EXPECT(expectOffers(env, bob, 1));
474 BEAST_EXPECT(expectOffers(env, charlie, 0));
475 }
476
477 void
479 {
480 using namespace test::jtx;
481
482 testcase("private XRP vault");
483
484 Env env{*this, testableAmendments()};
485 Account const owner{"owner"};
486 Account const depositor{"depositor"};
487 Account const alice{"charlie"};
488 std::string const credType = "credential";
489 Vault const vault{env};
490 env.fund(XRP(100000), owner, depositor, alice);
491 env.close();
492
493 PrettyAsset const asset = xrpIssue();
494 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset, .flags = tfVaultPrivate});
495 env(tx);
496 env.close();
497
498 auto const [vaultAccount, issuanceId] =
499 [&env, keylet = keylet, this]() -> std::tuple<AccountID, UInt192> {
500 auto const vault = env.le(keylet);
501 BEAST_EXPECT(vault != nullptr);
502 return {vault->at(sfAccount), vault->at(sfShareMPTID)};
503 }();
504 BEAST_EXPECT(env.le(keylet::account(vaultAccount)));
505 BEAST_EXPECT(env.le(keylet::mptokenIssuance(issuanceId)));
506 PrettyAsset const shares{issuanceId};
507
508 {
509 testcase("private XRP vault owner can deposit");
510 auto tx = vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(50)});
511 env(tx);
512 env.close();
513 }
514
515 {
516 testcase("private XRP vault cannot pay shares to depositor yet");
517 env(pay(owner, depositor, shares(1)), Ter{tecNO_AUTH});
518 }
519
520 {
521 testcase("private XRP vault depositor not authorized yet");
522 auto tx =
523 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
524 env(tx, Ter{tecNO_AUTH});
525 }
526
527 {
528 testcase("private XRP vault set DomainID");
529 pdomain::Credentials const credentials{{.issuer = owner, .credType = credType}};
530
531 env(pdomain::setTx(owner, credentials));
532 auto const domainId = [&]() {
533 auto tx = env.tx()->getJson(JsonOptions::Values::None);
534 return pdomain::getNewDomain(env.meta());
535 }();
536
537 auto tx = vault.set({.owner = owner, .id = keylet.key});
538 tx[sfDomainID] = to_string(domainId);
539 env(tx);
540 env.close();
541 }
542
543 auto const credKeylet = credentials::keylet(depositor, owner, credType);
544 {
545 testcase("private XRP vault depositor now authorized");
546 env(credentials::create(depositor, owner, credType));
547 env(credentials::accept(depositor, owner, credType));
548 env.close();
549
550 BEAST_EXPECT(env.le(credKeylet));
551 auto tx =
552 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
553 env(tx);
554 env.close();
555 }
556
557 {
558 testcase("private XRP vault can pay shares to depositor");
559 env(pay(owner, depositor, shares(1)));
560 }
561
562 {
563 testcase("private XRP vault cannot pay shares to 3rd party");
564 json::Value jv;
565 jv[sfAccount] = alice.human();
566 jv[sfTransactionType] = jss::MPTokenAuthorize;
567 jv[sfMPTokenIssuanceID] = to_string(issuanceId);
568 env(jv);
569 env.close();
570
571 env(pay(owner, alice, shares(1)), Ter{tecNO_AUTH});
572 }
573 }
574
575 // Withdrawing out of a private vault to a third party requires both the
576 // submitter and the destination to be members of the vault's permissioned
577 // domain. Withdrawal to self is exempt: revoking vault access must not
578 // trap already deposited funds. The asset issuer is exempt as a
579 // destination, so that frozen assets can always be returned.
580 void
582 {
583 using namespace test::jtx;
584
585 bool const withFix = features[fixCleanup3_4_0];
586 testcase(
587 std::string{"VaultWithdraw private vault destination domain check"} +
588 (withFix ? " (fixCleanup3_4_0)" : " (pre-fix)"));
589
590 Account const issuer{"issuer"};
591 Account const owner{"owner"};
592 Account const depositor{"depositor"};
593 Account const beneficiary{"beneficiary"};
594 Account const outsider{"outsider"};
595 Account const pdOwner{"pdOwner"};
596 Account const credIssuer{"credIssuer"};
597 std::string const credType = "credential";
598
599 Env env{*this, features};
600 Vault const vault{env};
601
602 env.fund(
603 XRP(100'000), issuer, owner, depositor, beneficiary, outsider, pdOwner, credIssuer);
604 env.close();
605
606 PrettyAsset const asset = issuer["IOU"];
607 // Everyone holds Layer 1 (asset) permission, so anything blocked below
608 // is blocked by the Layer 2 (vault) check alone.
609 for (auto const& account : {owner, depositor, beneficiary, outsider})
610 {
611 env.trust(asset(1'000'000), account);
612 env(pay(issuer, account, asset(10'000)));
613 }
614 env.close();
615
616 auto const domainId = [&]() {
617 pdomain::Credentials const credentials{{.issuer = credIssuer, .credType = credType}};
618 env(pdomain::setTx(pdOwner, credentials));
619 env.close();
620 return pdomain::getNewDomain(env.meta());
621 }();
622
623 auto const joinDomain = [&](Account const& account) {
624 env(credentials::create(account, credIssuer, credType));
625 env(credentials::accept(account, credIssuer, credType));
626 env.close();
627 };
628 joinDomain(depositor);
629 joinDomain(beneficiary);
630
631 auto [createTx, keylet] =
632 vault.create({.owner = owner, .asset = asset, .flags = tfVaultPrivate});
633 env(createTx);
634 env.close();
635
636 {
637 auto tx = vault.set({.owner = owner, .id = keylet.key});
638 tx[sfDomainID] = to_string(domainId);
639 env(tx);
640 env.close();
641 }
642
643 env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(1'000)}));
644 env.close();
645
646 auto const withdrawTo = [&, keylet = keylet](Account const& destination) {
647 auto tx =
648 vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(1)});
649 tx[sfDestination] = destination.human();
650 return tx;
651 };
652
653 {
654 // Destination holds both layers of permission.
655 env(withdrawTo(beneficiary));
656 env.close();
657 }
658
659 {
660 // Destination may hold the asset but was never let into the vault.
661 env(withdrawTo(outsider), Ter(withFix ? TER(tecNO_AUTH) : TER(tesSUCCESS)));
662 env.close();
663 }
664
665 {
666 // The asset issuer can always receive, to keep the recovery path
667 // for frozen assets open.
668 env(withdrawTo(issuer));
669 env.close();
670 }
671
672 {
673 // The vault owner gets no special treatment as a destination: it
674 // is a third party like any other and needs domain membership.
675 env(withdrawTo(owner), Ter(withFix ? TER(tecNO_AUTH) : TER(tesSUCCESS)));
676 env.close();
677 }
678
679 {
680 // Withdrawal to self needs no Destination and stays unaffected.
681 env(vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(1)}));
682 env.close();
683 }
684
685 {
686 // Naming yourself as the Destination is still a withdrawal to self.
687 env(withdrawTo(depositor));
688 env.close();
689 }
690
691 {
692 testcase(
693 std::string{"VaultWithdraw private vault submitter lost vault access"} +
694 (withFix ? " (fixCleanup3_4_0)" : " (pre-fix)"));
695
696 env(credentials::deleteCred(credIssuer, depositor, credIssuer, credType));
697 env.close();
698
699 // The exit of last resort: the submitter lost vault access but
700 // must still be able to redeem its own shares.
701 env(vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(1)}));
702 env.close();
703
704 // Moving funds to anyone else is not allowed any more, even to a
705 // destination that is itself a domain member.
706 env(withdrawTo(beneficiary), Ter(withFix ? TER(tecNO_AUTH) : TER(tesSUCCESS)));
707 env.close();
708
709 // Returning assets to the issuer stays open regardless.
710 env(withdrawTo(issuer));
711 env.close();
712 }
713
714 {
715 testcase(
716 std::string{"VaultWithdraw private vault with no domain set"} +
717 (withFix ? " (fixCleanup3_4_0)" : " (pre-fix)"));
718
719 // Give the submitter its vault access back first, so that the
720 // vault having no domain is the only reason left to refuse.
721 env(credentials::create(depositor, credIssuer, credType));
722 env(credentials::accept(depositor, credIssuer, credType));
723 env.close();
724
725 auto tx = vault.set({.owner = owner, .id = keylet.key});
726 tx[sfDomainID] = "0";
727 env(tx);
728 env.close();
729
730 // Clearing the domain leaves the vault with nobody it considers
731 // authorized, so a third-party destination cannot qualify even
732 // though both ends of the payout hold a credential.
733 env(withdrawTo(beneficiary), Ter(withFix ? TER(tecNO_AUTH) : TER(tesSUCCESS)));
734 env.close();
735
736 // The two exempt paths survive the domain going away.
737 env(vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(1)}));
738 env.close();
739
740 env(withdrawTo(issuer));
741 env.close();
742 }
743
744 {
745 testcase(
746 std::string{"VaultWithdraw public vault destination unaffected"} +
747 (withFix ? " (fixCleanup3_4_0)" : " (pre-fix)"));
748
749 auto [publicTx, publicKeylet] = vault.create({.owner = owner, .asset = asset});
750 env(publicTx);
751 env.close();
752
753 env(vault.deposit({.depositor = owner, .id = publicKeylet.key, .amount = asset(100)}));
754 env.close();
755
756 auto tx =
757 vault.withdraw({.depositor = owner, .id = publicKeylet.key, .amount = asset(1)});
758 tx[sfDestination] = outsider.human();
759 env(tx);
760 env.close();
761 }
762 }
763
764 void
766 {
767 testcase(
768 "withdraw with credential-based deposit preauth " +
769 std::string{features[fixCleanup3_4_0] ? "post-fix" : "pre-fix"});
770 using namespace test::jtx;
771 using namespace std::chrono_literals;
772
773 bool const fixEnabled = features[fixCleanup3_4_0];
774
775 Env env{*this, features};
776
777 Account const owner{"owner"};
778 Account const depositor{"depositor"};
779 Account const dest{"dest"};
780 Account const credIssuer{"credIssuer"};
781 char const credType[] = "abcde";
782
783 env.fund(XRP(1000), owner, depositor, dest, credIssuer);
784 env(fset(dest, asfDepositAuth));
785 env.close();
786
787 PrettyAsset const asset{xrpIssue(), 1'000'000};
788 Vault vault{env};
789 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
790 env(tx);
791 env.close();
792
793 env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)}));
794 env.close();
795
796 auto withdrawToDest = [&]() {
797 auto wtx =
798 vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(10)});
799 wtx[sfDestination] = dest.human();
800 return wtx;
801 };
802
803 // Without any preauth, withdraw to dest fails
804 env(withdrawToDest(), Ter{tecNO_PERMISSION});
805 env.close();
806
807 // Issue and accept a credential for the depositor (with expiration)
808 auto jv = credentials::create(depositor, credIssuer, credType);
809 std::uint32_t const expiration =
810 env.current()->header().parentCloseTime.time_since_epoch().count() + 100;
811 jv[sfExpiration.jsonName] = expiration;
812 env(jv);
813 env(credentials::accept(depositor, credIssuer, credType));
814 env.close();
815
816 auto const credKeylet = credentials::keylet(depositor, credIssuer, credType);
817 auto const credIdx =
818 credentials::ledgerEntry(env, depositor, credIssuer, credType)[jss::result][jss::index]
819 .asString();
820
821 // dest authorizes deposits from holders of credentials issued by credIssuer
822 env(deposit::authCredentials(dest, {{.issuer = credIssuer, .credType = credType}}));
823 env.close();
824
825 // Withdraw without supplying credentials still fails
826 env(withdrawToDest(), Ter{tecNO_PERMISSION});
827 env.close();
828
829 if (!fixEnabled)
830 {
831 // Pre-fix: sfCredentialIDs in VaultWithdraw is rejected as disabled
832 env(withdrawToDest(), credentials::Ids({credIdx}), Ter{temDISABLED});
833 env.close();
834 return;
835 }
836
837 // Withdraw with credentials succeeds
838 env(withdrawToDest(), credentials::Ids({credIdx}));
839 env.close();
840
841 // Bad credential id is rejected
842 std::string const invalidIdx =
843 "0E0B04ED60588A758B67E21FBBE95AC5A63598BA951761DC0EC9C08D7E01E034";
844 env(withdrawToDest(), credentials::Ids({invalidIdx}), Ter{tecBAD_CREDENTIALS});
845 env.close();
846
847 // Malformed credential array (duplicates) is rejected by checkFields
848 env(withdrawToDest(), credentials::Ids({credIdx, credIdx}), Ter{temMALFORMED});
849 env.close();
850
851 // Valid credential not authorized by dest hits authorizedDepositPreauth error path
852 char const credType2[] = "fghij";
853 env(credentials::create(depositor, credIssuer, credType2));
854 env(credentials::accept(depositor, credIssuer, credType2));
855 env.close();
856 auto const credIdx2 =
857 credentials::ledgerEntry(env, depositor, credIssuer, credType2)[jss::result][jss::index]
858 .asString();
859 env(withdrawToDest(), credentials::Ids({credIdx2}), Ter{tecNO_PERMISSION});
860 env.close();
861
862 // Advance time past expiration: credentials yield tecEXPIRED and are deleted
863 env.close(150s);
864 BEAST_EXPECT(env.le(credKeylet));
865 env(withdrawToDest(), credentials::Ids({credIdx}), Ter{tecEXPIRED});
866 env.close();
867 BEAST_EXPECT(!env.le(credKeylet));
868 }
869
870public:
871 void
883};
884
885BEAST_DEFINE_TESTSUITE(VaultDomain, app, xrpl);
886
887} // namespace xrpl
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
Represents a JSON value.
Definition json_value.h:117
Integers of any length that is a multiple of 32-bits.
Definition base_uint.h:82
constexpr MPTID const & getMptID() const
Definition MPTIssue.h:43
void testVaultWithdrawPrivateDestinationDomain(FeatureBitset features)
void run() override
Runs the suite.
void testWithdrawCredentialDepositPreauth(FeatureBitset features)
Shared base for the Vault*_test family under src/test/app/vault/.
FeatureBitset const all_
test::jtx::PrettyAsset PrettyAsset
Keylet computation functions.
Definition Indexes.h:40
Keylet mptoken(MPTID const &issuanceID, AccountID const &holder) noexcept
Definition Indexes.cpp:573
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:220
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
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Definition Issue.h:108
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:657
@ temMALFORMED
Definition TER.h:75
@ temDISABLED
Definition TER.h:102
TERSubset< CanCvtToTER > TER
Definition TER.h:654
@ tecOBJECT_NOT_FOUND
Definition TER.h:334
@ tecNO_AUTH
Definition TER.h:308
@ tecBAD_CREDENTIALS
Definition TER.h:367
@ tecUNFUNDED_OFFER
Definition TER.h:292
@ tecEXPIRED
Definition TER.h:322
@ tecNO_PERMISSION
Definition TER.h:313
BEAST_DEFINE_TESTSUITE(AccountTxPaging, app, xrpl)
@ tesSUCCESS
Definition TER.h:250