xrpld
Loading...
Searching...
No Matches
VaultShares_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/noop.h>
11#include <test/jtx/pay.h>
12#include <test/jtx/seq.h>
13#include <test/jtx/sig.h>
14#include <test/jtx/tags.h>
15#include <test/jtx/ter.h>
16#include <test/jtx/trust.h>
17#include <test/jtx/vault.h>
18
19#include <xrpl/basics/Number.h>
20#include <xrpl/basics/base_uint.h>
21#include <xrpl/basics/strHex.h>
22#include <xrpl/beast/unit_test/suite.h>
23#include <xrpl/beast/utility/Journal.h>
24#include <xrpl/core/ServiceRegistry.h>
25#include <xrpl/json/json_forwards.h>
26#include <xrpl/json/json_value.h>
27#include <xrpl/ledger/ApplyView.h>
28#include <xrpl/ledger/OpenView.h>
29#include <xrpl/ledger/Sandbox.h>
30#include <xrpl/ledger/helpers/AccountRootHelpers.h>
31#include <xrpl/ledger/helpers/MPTokenHelpers.h>
32#include <xrpl/ledger/helpers/VaultHelpers.h>
33#include <xrpl/protocol/AccountID.h>
34#include <xrpl/protocol/Asset.h>
35#include <xrpl/protocol/Feature.h>
36#include <xrpl/protocol/Indexes.h>
37#include <xrpl/protocol/Issue.h>
38#include <xrpl/protocol/Keylet.h>
39#include <xrpl/protocol/MPTIssue.h>
40#include <xrpl/protocol/Protocol.h>
41#include <xrpl/protocol/SField.h>
42#include <xrpl/protocol/STAmount.h>
43#include <xrpl/protocol/SeqProxy.h>
44#include <xrpl/protocol/TER.h>
45#include <xrpl/protocol/TxFlags.h>
46#include <xrpl/protocol/XRPAmount.h>
47#include <xrpl/protocol/jss.h>
48
49#include <optional>
50#include <string>
51#include <tuple>
52#include <utility>
53
54namespace xrpl {
55
57{
58private:
59 void
61 {
62 using namespace test::jtx;
63
64 Env env{*this, testableAmendments()};
65 Account const issuer{"issuer"};
66 Account const owner{"owner"};
67 Account const depositor{"depositor"};
68 env.fund(XRP(1000), issuer, owner, depositor);
69 env.close();
70
71 Vault const vault{env};
72 PrettyAsset const asset = issuer["IOU"];
73 env.trust(asset(1000), owner);
74 env(pay(issuer, owner, asset(100)));
75 env.trust(asset(1000), depositor);
76 env(pay(issuer, depositor, asset(100)));
77 env.close();
78
79 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
80 tx[sfFlags] = tfVaultShareNonTransferable;
81 env(tx);
82 env.close();
83
84 {
85 testcase("nontransferable deposits");
86 auto tx1 =
87 vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(40)});
88 env(tx1);
89
90 auto tx2 = vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(60)});
91 env(tx2);
92 env.close();
93 }
94
95 auto const vaultAccount = //
96 [&env, key = keylet.key, this]() -> AccountID {
97 auto jvVault = env.rpc("vault_info", strHex(key));
98
99 BEAST_EXPECT(jvVault[jss::result][jss::vault][sfAssetsTotal] == "100");
100 BEAST_EXPECT(
101 jvVault[jss::result][jss::vault][jss::shares][sfOutstandingAmount] == "100000000");
102
103 // Vault pseudo-account
104 return parseBase58<AccountID>(jvVault[jss::result][jss::vault][jss::Account].asString())
105 .value();
106 }();
107
108 auto const mptId = makeMptID(1, vaultAccount);
109 Asset const shares = mptId;
110
111 {
112 testcase("nontransferable shares cannot be moved");
113 env(pay(owner, depositor, shares(10)), Ter{tecNO_AUTH});
114 env(pay(depositor, owner, shares(10)), Ter{tecNO_AUTH});
115 }
116
117 {
118 testcase("nontransferable shares can be used to withdraw");
119 auto tx1 =
120 vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(20)});
121 env(tx1);
122
123 auto tx2 = vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(30)});
124 env(tx2);
125 env.close();
126 }
127
128 {
129 testcase("nontransferable shares balance check");
130 auto jvVault = env.rpc("vault_info", strHex(keylet.key));
131 BEAST_EXPECT(jvVault[jss::result][jss::vault][sfAssetsTotal] == "50");
132 BEAST_EXPECT(
133 jvVault[jss::result][jss::vault][jss::shares][sfOutstandingAmount] == "50000000");
134 }
135
136 {
137 testcase("nontransferable shares withdraw rest");
138 auto tx1 =
139 vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(20)});
140 env(tx1);
141
142 auto tx2 = vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(30)});
143 env(tx2);
144 env.close();
145 }
146
147 {
148 testcase("nontransferable shares delete empty vault");
149 auto tx = vault.del({.owner = owner, .id = keylet.key});
150 env(tx);
151 BEAST_EXPECT(!env.le(keylet));
152 }
153 }
154
155 void
157 {
158 using namespace test::jtx;
159
160 testcase("fail pseudo-account allocation");
161 Env env{*this, testableAmendments()};
162 Account const owner{"owner"};
163 Vault const vault{env};
164 env.fund(XRP(1000), owner);
165
166 auto const keylet = keylet::vault(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
167 for (int i = 0; i < 256; ++i)
168 {
169 AccountID const accountId = xrpl::pseudoAccountAddress(*env.current(), keylet.key);
170
171 env(pay(env.master.id(), accountId, XRP(1000)),
172 Seq(kAutofill),
173 Fee(kAutofill),
174 Sig(kAutofill));
175 }
176
177 auto [tx, keylet1] = vault.create({.owner = owner, .asset = xrpIssue()});
178 BEAST_EXPECT(keylet.key == keylet1.key);
179 env(tx, Ter{terADDRESS_COLLISION});
180 }
181
182 void
184 {
185 testcase("removeEmptyHolding deletes MPToken with sfLockedAmount");
186 using namespace test::jtx;
187 using namespace std::literals;
188
189 auto const amendments = testableAmendments();
190 auto runTest = [&](FeatureBitset f) {
191 Env env{*this, f};
192 auto const baseFee = env.current()->fees().base;
193
194 Account const issuer{"issuer"};
195 Account const owner{"owner"};
196 Account const depositor{"depositor"};
197 Account const bob{"bob"};
198
199 env.fund(XRP(100000), issuer, owner, depositor, bob);
200 env.close();
201
202 Vault const vault{env};
203
204 // Create an MPT asset for the vault
205 MPTTester mptt{env, issuer, kMptInitNoFund};
206 mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
207 PrettyAsset const asset = mptt.issuanceID();
208 mptt.authorize({.account = owner});
209 mptt.authorize({.account = depositor});
210 env(pay(issuer, depositor, asset(1000)));
211 env.close();
212
213 // Create vault
214 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
215 env(tx);
216 env.close();
217
218 auto const vaultSle = env.le(keylet);
219 BEAST_EXPECT(vaultSle != nullptr);
220 auto const shareMptID = vaultSle->at(sfShareMPTID);
221 MPTIssue const shareIssue{shareMptID};
222
223 // Depositor deposits 1000 asset units into vault, receiving shares
224 env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(1000)}));
225 env.close();
226
227 // Check depositor has shares
228 {
229 auto const sleMpt = env.le(keylet::mptoken(shareMptID, depositor));
230 BEAST_EXPECT(sleMpt != nullptr);
231 BEAST_EXPECT(sleMpt->at(sfMPTAmount) == 1000);
232 }
233
234 // Escrow 500 of those shares
235 env(escrow::create(depositor, bob, STAmount{shareIssue, 500}),
236 escrow::kCondition(escrow::kCb1),
237 escrow::kFinishTime(env.now() + 1s),
238 Fee(baseFee * 150),
239 Ter(tesSUCCESS));
240 env.close();
241
242 // Verify: sfMPTAmount=500, sfLockedAmount=500
243 {
244 auto const sleMpt = env.le(keylet::mptoken(shareMptID, depositor));
245 BEAST_EXPECT(sleMpt != nullptr);
246 BEAST_EXPECT(sleMpt->at(sfLockedAmount) == 500);
247 BEAST_EXPECT(sleMpt->at(sfMPTAmount) == 500);
248 }
249
250 // Withdraw remaining spendable shares — triggers removeEmptyHolding
251 env(vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(500)}),
252 Ter(tesSUCCESS));
253 env.close();
254
255 auto const sleMptAfter = env.le(keylet::mptoken(shareMptID, depositor));
256 if (!f[fixCleanup3_1_3])
257 {
258 // Without the fix, removeEmptyHolding deletes the MPToken
259 // even though sfLockedAmount > 0, leaving the escrow's locked
260 // amount untracked.
261 BEAST_EXPECT(sleMptAfter == nullptr);
262 }
263 else
264 {
265 // With the fix, MPToken must still exist with sfLockedAmount > 0
266 // and sfMPTAmount == 0 (all spendable shares withdrawn).
267 BEAST_EXPECT(sleMptAfter != nullptr);
268 if (sleMptAfter)
269 {
270 BEAST_EXPECT(sleMptAfter->at(sfLockedAmount) == 500);
271 BEAST_EXPECT(sleMptAfter->at(sfMPTAmount) == 0);
272 }
273 }
274 };
275
276 runTest(amendments - fixCleanup3_1_3);
277 runTest(amendments);
278 }
279
280 void
282 {
283 testcase("removeEmptyHolding keeps MPToken with confidential balances");
284 using namespace test::jtx;
285
286 Env env{*this, testableAmendments()};
287
288 Account const issuer{"issuer"};
289 Account const holder{"holder"};
290 MPTTester mpt{env, issuer, {.holders = {holder}}};
291 mpt.create({.authorize = MPTCreate::allHolders});
292
293 auto const tokenKeylet = keylet::mptoken(mpt.issuanceID(), holder.id());
294 auto const encryptedBalanceFields = {
295 &sfConfidentialBalanceInbox,
296 &sfConfidentialBalanceSpending,
297 &sfIssuerEncryptedBalance,
298 &sfAuditorEncryptedBalance};
299
300 env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
301 for (auto const field : encryptedBalanceFields)
302 {
303 Sandbox sb(&view, TapNone);
304 auto const token = sb.peek(tokenKeylet);
305 if (!BEAST_EXPECT(token))
306 return false;
307
308 token->setFieldVL(*field, gMakeZeroBuffer(kEcGamalEncryptedTotalLength));
309 sb.update(token);
310
311 auto const dummyTx = *env.jt(noop(holder)).stx;
312 BEAST_EXPECT(
313 removeEmptyHolding({sb, dummyTx}, holder.id(), MPTIssue(mpt.issuanceID()), j) ==
315 BEAST_EXPECT(sb.peek(tokenKeylet) != nullptr);
316 }
317 return true;
318 });
319 }
320
321 void
323 {
324 using namespace test::jtx;
325
326 auto readReferenceHolding = [&](Env const& env,
327 Keylet const& vaultKeylet) -> std::optional<UInt256> {
328 auto const sleVault = env.le(vaultKeylet);
329 if (!sleVault)
330 return std::nullopt;
331 auto const sleIssuance = env.le(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
332 if (!sleIssuance || !sleIssuance->isFieldPresent(sfReferenceHolding))
333 return std::nullopt;
334 return sleIssuance->getFieldH256(sfReferenceHolding);
335 };
336
337 // Post-fixCleanup3_2_0: vault share carries sfReferenceHolding
338 // pointing to the vault pseudo's MPToken (for MPT-backed vaults)
339 // or RippleState (for IOU-backed vaults).
340 {
341 testcase("sfReferenceHolding: MPT-backed vault, post-amendment");
342 Env env{*this, testableAmendments()};
343 Account const issuer{"issuer"};
344 Account const owner{"owner"};
345 env.fund(XRP(10'000), issuer, owner);
346 env.close();
347
348 MPTTester mptt{env, issuer, kMptInitNoFund};
349 mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
350 PrettyAsset const asset = mptt.issuanceID();
351 mptt.authorize({.account = owner});
352
353 Vault const vault{env};
354 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
355 env(tx);
356 env.close();
357
358 auto const sleVault = env.le(keylet);
359 BEAST_EXPECT(sleVault != nullptr);
360 auto const pseudoId = sleVault->at(sfAccount);
361 auto const expected = keylet::mptoken(mptt.issuanceID(), pseudoId).key;
362
363 auto const stored = readReferenceHolding(env, keylet);
364 BEAST_EXPECT(stored.has_value());
365 BEAST_EXPECT(stored && *stored == expected);
366 // The pointed-to MPToken must actually exist.
367 BEAST_EXPECT(env.le(keylet::mptoken(mptt.issuanceID(), pseudoId)) != nullptr);
368 }
369
370 {
371 testcase("sfReferenceHolding: IOU-backed vault, post-amendment");
372 Env env{*this, testableAmendments()};
373 Account const issuer{"issuer"};
374 Account const owner{"owner"};
375 env.fund(XRP(10'000), issuer, owner);
376 env(fset(issuer, asfDefaultRipple));
377 env.close();
378
379 PrettyAsset const asset = issuer["IOU"];
380 env.trust(asset(1'000'000), owner);
381 env.close();
382
383 Vault const vault{env};
384 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
385 env(tx);
386 env.close();
387
388 auto const sleVault = env.le(keylet);
389 BEAST_EXPECT(sleVault != nullptr);
390 auto const pseudoId = sleVault->at(sfAccount);
391 auto const expected = keylet::trustLine(pseudoId, asset.raw().get<Issue>()).key;
392
393 auto const stored = readReferenceHolding(env, keylet);
394 BEAST_EXPECT(stored.has_value());
395 BEAST_EXPECT(stored && *stored == expected);
396 // The pointed-to RippleState must actually exist.
397 BEAST_EXPECT(env.le(keylet::trustLine(pseudoId, asset.raw().get<Issue>())) != nullptr);
398 }
399
400 // XRP-backed vaults leave the field absent: XRP has no separate
401 // holding ledger entry and no transferability concept to inherit.
402 {
403 testcase("sfReferenceHolding: XRP-backed vault, field absent");
404 Env env{*this, testableAmendments()};
405 Account const owner{"owner"};
406 env.fund(XRP(10'000), owner);
407 env.close();
408
409 PrettyAsset const asset{xrpIssue(), 1'000'000};
410 Vault const vault{env};
411 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
412 env(tx);
413 env.close();
414
415 BEAST_EXPECT(!readReferenceHolding(env, keylet).has_value());
416 }
417
418 // Pre-fixCleanup3_2_0: vault share has the field absent regardless
419 // of underlying type.
420 {
421 testcase("sfReferenceHolding: vault share, pre-amendment");
422 Env env{*this, testableAmendments() - fixCleanup3_2_0};
423 Account const issuer{"issuer"};
424 Account const owner{"owner"};
425 env.fund(XRP(10'000), issuer, owner);
426 env.close();
427
428 MPTTester mptt{env, issuer, kMptInitNoFund};
429 mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
430 PrettyAsset const asset = mptt.issuanceID();
431 mptt.authorize({.account = owner});
432
433 Vault const vault{env};
434 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
435 env(tx);
436 env.close();
437
438 BEAST_EXPECT(!readReferenceHolding(env, keylet).has_value());
439 }
440
441 // Plain MPTokenIssuanceCreate (not a vault share) must never
442 // populate the field. Only the post-amendment case is
443 // interesting; pre-amendment nothing writes the field at all.
444 {
445 testcase("sfReferenceHolding: plain MPT issuance never set");
446 Env env{*this, testableAmendments()};
447 Account const issuer{"issuer"};
448 env.fund(XRP(10'000), issuer);
449 env.close();
450
451 MPTTester mptt{env, issuer, kMptInitNoFund};
452 mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
453 env.close();
454
455 auto const sleIssuance = env.le(keylet::mptokenIssuance(mptt.issuanceID()));
456 if (BEAST_EXPECT(sleIssuance))
457 BEAST_EXPECT(!sleIssuance->isFieldPresent(sfReferenceHolding));
458 }
459 }
460
461 // Probe every transactor surface that might delete the vault pseudo-
462 // account's underlying holding (the MPToken or RippleState pointed to
463 // by sfReferenceHolding). Each scenario asserts either that the
464 // existing pseudo-account guards stop the deletion at preclaim, or
465 // that the ledger leaves the holding intact afterwards. This is a
466 // regression guard: if any of these guards regresses, the share's
467 // sfReferenceHolding pointer would dangle and the new ValidMPTIssuance
468 // invariant would catch it - but we want to fail much earlier, at
469 // the transactor's preclaim / doApply, not at invariant time.
470 void
472 {
473 using namespace test::jtx;
474
475 // Helper: read the share's referenced holding and confirm the
476 // pointed-to SLE still exists after the probe.
477 auto referencedHoldingExists = [&](Env const& env, Keylet const& vaultKeylet) -> bool {
478 auto const sleVault = env.le(vaultKeylet);
479 if (!sleVault)
480 return false;
481 auto const sleIssuance = env.le(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
482 if (!sleIssuance || !sleIssuance->isFieldPresent(sfReferenceHolding))
483 return false;
484 auto const holdingKey = sleIssuance->getFieldH256(sfReferenceHolding);
485 return env.le(keylet::unchecked(holdingKey)) != nullptr;
486 };
487
488 // ---- MPT-backed vault ----------------------------------------
489 {
490 testcase("vault pseudo MPToken: Clawback blocked by tecPSEUDO_ACCOUNT");
491 Env env{*this, testableAmendments()};
492 Account const issuer{"issuer"};
493 Account const owner{"owner"};
494 Account const depositor{"depositor"};
495 env.fund(XRP(10'000), issuer, owner, depositor);
496 env.close();
497
498 MPTTester mptt{env, issuer, kMptInitNoFund};
499 mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanClawback});
500 PrettyAsset const asset = mptt.issuanceID();
501 mptt.authorize({.account = owner});
502 mptt.authorize({.account = depositor});
503 env(pay(issuer, depositor, asset(1'000)));
504 env.close();
505
506 Vault const vault{env};
507 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
508 env(tx);
509 env.close();
510
511 env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(500)}));
512 env.close();
513
514 BEAST_EXPECT(referencedHoldingExists(env, keylet));
515
516 Account const pseudoAccount{"vault-pseudo", env.le(keylet)->at(sfAccount)};
517 // Issuer attempts to claw back the FULL underlying balance
518 // (500) directly from the vault pseudo-account. With the
519 // full amount, the doApply path would drain the pseudo's
520 // MPToken to zero and removeEmptyHolding would erase it -
521 // if doApply ever ran. SAV's pseudo-account guard at
522 // Clawback.cpp:201 refuses at preclaim with
523 // tecPSEUDO_ACCOUNT before any state change.
524 env(claw(issuer, asset(500), pseudoAccount), Ter{tecPSEUDO_ACCOUNT});
525 env.close();
526 BEAST_EXPECT(referencedHoldingExists(env, keylet));
527 // Sanity: pseudo's full balance is intact.
528 BEAST_EXPECT(env.balance(pseudoAccount, asset).number() == 500);
529 }
530
531 {
532 testcase("vault pseudo MPToken: Issuer cannot Unauthorize pseudo");
533 Env env{*this, testableAmendments()};
534 Account const issuer{"issuer"};
535 Account const owner{"owner"};
536 env.fund(XRP(10'000), issuer, owner);
537 env.close();
538
539 MPTTester mptt{env, issuer, kMptInitNoFund};
540 mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth});
541 PrettyAsset const asset = mptt.issuanceID();
542 mptt.authorize({.account = owner});
543 mptt.authorize({.account = issuer, .holder = owner});
544 env.close();
545
546 Vault const vault{env};
547 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
548 env(tx);
549 env.close();
550
551 BEAST_EXPECT(referencedHoldingExists(env, keylet));
552
553 auto const pseudoId = env.le(keylet)->at(sfAccount);
554 // Issuer attempts MPTokenAuthorize against the pseudo with
555 // tfMPTUnauthorize. MPTokenAuthorize.cpp blocks pseudo
556 // accounts via isPseudoAccount; the pseudo's MPToken is
557 // preserved. Construct the tx manually since the pseudo
558 // lacks a signing key, and the issuer-driven flavour is
559 // expressed via sfHolder.
560 json::Value jv;
561 jv[sfAccount] = issuer.human();
562 jv[sfHolder] = toBase58(pseudoId);
563 jv[sfMPTokenIssuanceID] = to_string(mptt.issuanceID());
564 jv[sfFlags] = tfMPTUnauthorize;
565 jv[sfTransactionType] = jss::MPTokenAuthorize;
566 env(jv, Ter{tecNO_PERMISSION});
567 env.close();
568 BEAST_EXPECT(referencedHoldingExists(env, keylet));
569 }
570
571 {
572 testcase("vault pseudo MPToken: MPTokenIssuanceDestroy blocked while vault holds");
573 Env env{*this, testableAmendments()};
574 Account const issuer{"issuer"};
575 Account const owner{"owner"};
576 Account const depositor{"depositor"};
577 env.fund(XRP(10'000), issuer, owner, depositor);
578 env.close();
579
580 MPTTester mptt{env, issuer, kMptInitNoFund};
581 mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
582 PrettyAsset const asset = mptt.issuanceID();
583 mptt.authorize({.account = owner});
584 mptt.authorize({.account = depositor});
585 env(pay(issuer, depositor, asset(1'000)));
586 env.close();
587
588 Vault const vault{env};
589 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
590 env(tx);
591 env.close();
592
593 env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(500)}));
594 env.close();
595
596 BEAST_EXPECT(referencedHoldingExists(env, keylet));
597
598 // While the vault holds outstanding underlying, the issuer
599 // cannot destroy the issuance. tecHAS_OBLIGATIONS confirms
600 // the protection - and as a side effect, the share's
601 // sfReferenceHolding pointer cannot be left pointing at a
602 // ghost issuance.
603 mptt.destroy({.id = mptt.issuanceID(), .err = tecHAS_OBLIGATIONS});
604 env.close();
605 BEAST_EXPECT(referencedHoldingExists(env, keylet));
606 }
607
608 // ---- IOU-backed vault ----------------------------------------
609 {
610 testcase("vault pseudo trust line: Clawback blocked by tecPSEUDO_ACCOUNT");
611 Env env{*this, testableAmendments()};
612 Account const issuer{"issuer"};
613 Account const owner{"owner"};
614 env.fund(XRP(10'000), issuer, owner);
615 env(fset(issuer, asfAllowTrustLineClawback));
616 env.close();
617
618 PrettyAsset const asset = issuer["IOU"];
619 env.trust(asset(1'000'000), owner);
620 env(pay(issuer, owner, asset(1'000)));
621 env.close();
622
623 Vault const vault{env};
624 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
625 env(tx);
626 env.close();
627
628 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(500)}));
629 env.close();
630
631 BEAST_EXPECT(referencedHoldingExists(env, keylet));
632
633 Account const pseudoAccount{"vault-pseudo", env.le(keylet)->at(sfAccount)};
634 // Issuer attempts to claw back the FULL IOU balance (500)
635 // directly from the vault pseudo. With the full amount, the
636 // doApply path would drain the trust line to zero and (if
637 // both reserve flags clear) trustDelete would erase it - if
638 // doApply ever ran. The same SAV pseudo-account guard
639 // refuses at preclaim with tecPSEUDO_ACCOUNT. The amount's
640 // STAmount issuer field is the holder, per IOU clawback
641 // convention.
642 env(claw(issuer, pseudoAccount["IOU"](500)), Ter{tecPSEUDO_ACCOUNT});
643 env.close();
644 BEAST_EXPECT(referencedHoldingExists(env, keylet));
645 // Sanity: pseudo's full balance is intact.
646 BEAST_EXPECT(env.balance(pseudoAccount, asset).number() == 500);
647 }
648
649 {
650 testcase("vault pseudo trust line: TrustSet limit=0 from issuer preserves line");
651 Env env{*this, testableAmendments()};
652 Account const issuer{"issuer"};
653 Account const owner{"owner"};
654 env.fund(XRP(10'000), issuer, owner);
655 env(fset(issuer, asfDefaultRipple));
656 env.close();
657
658 PrettyAsset const asset = issuer["IOU"];
659 env.trust(asset(1'000'000), owner);
660 env(pay(issuer, owner, asset(1'000)));
661 env.close();
662
663 Vault const vault{env};
664 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
665 env(tx);
666 env.close();
667
668 env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(500)}));
669 env.close();
670
671 BEAST_EXPECT(referencedHoldingExists(env, keylet));
672
673 // Issuer submits TrustSet with limit=0 against the vault
674 // pseudo. The pseudo's side of the line still has the
675 // original (non-zero) limit and a non-zero balance, so the
676 // line is preserved - even though the issuer cleared its
677 // own side. trustDelete only fires when both limits clear
678 // and the balance is zero.
679 Account const pseudoAccount{"vault-pseudo", env.le(keylet)->at(sfAccount)};
680 env(trust(issuer, pseudoAccount["IOU"](0)));
681 env.close();
682 BEAST_EXPECT(referencedHoldingExists(env, keylet));
683 }
684
685 // ---- Positive control: VaultDelete is the only legitimate path
686 {
687 testcase("vault pseudo holding: VaultDelete is the legitimate cleanup path");
688 Env env{*this, testableAmendments()};
689 Account const issuer{"issuer"};
690 Account const owner{"owner"};
691 env.fund(XRP(10'000), issuer, owner);
692 env.close();
693
694 MPTTester mptt{env, issuer, kMptInitNoFund};
695 mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
696 PrettyAsset const asset = mptt.issuanceID();
697 mptt.authorize({.account = owner});
698
699 Vault const vault{env};
700 auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
701 env(tx);
702 env.close();
703
704 BEAST_EXPECT(referencedHoldingExists(env, keylet));
705 auto const pseudoId = env.le(keylet)->at(sfAccount);
706 auto const sharedMptId = env.le(keylet)->at(sfShareMPTID);
707 auto const holdingKeylet = keylet::mptoken(mptt.issuanceID(), pseudoId);
708
709 // VaultDelete tears down the vault pseudo's holding, the
710 // share issuance, and the pseudo-account itself. Invariant
711 // permits this because the tx is ttVAULT_DELETE.
712 env(vault.del({.owner = owner, .id = keylet.key}));
713 env.close();
714
715 BEAST_EXPECT(env.le(keylet) == nullptr);
716 BEAST_EXPECT(env.le(holdingKeylet) == nullptr);
717 BEAST_EXPECT(env.le(keylet::mptokenIssuance(sharedMptId)) == nullptr);
718 }
719 }
720
721public:
722 void
732};
733
734BEAST_DEFINE_TESTSUITE(VaultShares, app, xrpl);
735
736} // namespace xrpl
A generic endpoint for log messages.
Definition Journal.h:44
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
A currency issued by an account.
Definition Issue.h:18
Writable ledger view that accumulates state and tx changes.
Definition OpenView.h:59
Discardable, editable view to a ledger.
Definition Sandbox.h:18
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.
void testRemoveEmptyHoldingConfidentialBalances()
Shared base for the Vault*_test family under src/test/app/vault/.
test::jtx::PrettyAsset PrettyAsset
SLE::pointer peek(Keylet const &k) override
Prepare to modify the SLE associated with key.
void update(SLE::Ref sle) override
Indicate changes to a peeked SLE.
Keylet computation functions.
Definition Indexes.h:40
Keylet unchecked(UInt256 const &key) noexcept
Any ledger entry.
Definition Indexes.cpp:397
Keylet vault(AccountID const &owner, SeqProxy const &seq) noexcept
Definition Indexes.cpp:591
Keylet mptoken(MPTID const &issuanceID, AccountID const &holder) noexcept
Definition Indexes.cpp:573
Keylet mptokenIssuance(MPTID const &issuanceID) noexcept
Definition Indexes.cpp:567
Keylet trustLine(AccountID const &id0, AccountID const &id1, Currency const &currency) noexcept
The index of a trust line for a given currency.
Definition Indexes.cpp:275
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
@ terADDRESS_COLLISION
Definition TER.h:229
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Definition Issue.h:108
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
TER removeEmptyHolding(ApplyViewContext ctx, AccountID const &accountID, MPTIssue const &mptIssue, beast::Journal journal)
std::string toBase58(AccountID const &v)
Convert AccountID to base58 checked string.
Definition AccountID.cpp:95
constexpr std::size_t kEcGamalEncryptedTotalLength
EC ElGamal ciphertext length: two compressed EC points concatenated.
Definition Protocol.h:480
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:657
AccountID pseudoAccountAddress(ReadView const &view, UInt256 const &pseudoOwnerKey)
Generate a pseudo-account address from a pseudo owner key.
MPTID makeMptID(std::uint32_t const sequence, AccountID const &account)
Definition Indexes.cpp:206
@ TapNone
Definition ApplyView.h:28
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:34
@ tecPSEUDO_ACCOUNT
Definition TER.h:370
@ tecNO_AUTH
Definition TER.h:308
@ tecNO_PERMISSION
Definition TER.h:313
@ tecHAS_OBLIGATIONS
Definition TER.h:325
BEAST_DEFINE_TESTSUITE(AccountTxPaging, app, xrpl)
@ tesSUCCESS
Definition TER.h:250
A pair of SHAMap key and LedgerEntryType.
Definition Keylet.h:20
UInt256 key
Definition Keylet.h:21