xrpld
Loading...
Searching...
No Matches
InvariantsVault_test.cpp
1#include <test/app/invariants/InvariantsBase.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/fee.h>
7#include <test/jtx/pay.h>
8#include <test/jtx/sig.h>
9#include <test/jtx/trust.h>
10#include <test/jtx/vault.h>
11#include <test/unit_test/SuiteJournal.h>
12
13#include <xrpl/basics/Number.h>
14#include <xrpl/basics/base_uint.h>
15#include <xrpl/basics/chrono.h>
16#include <xrpl/beast/unit_test/suite.h>
17#include <xrpl/beast/utility/Journal.h>
18#include <xrpl/beast/utility/Zero.h>
19#include <xrpl/ledger/ApplyView.h>
20#include <xrpl/ledger/OpenView.h>
21#include <xrpl/ledger/helpers/AccountRootHelpers.h>
22#include <xrpl/ledger/helpers/DirectoryHelpers.h>
23#include <xrpl/protocol/AccountID.h>
24#include <xrpl/protocol/Feature.h>
25#include <xrpl/protocol/Indexes.h>
26#include <xrpl/protocol/Issue.h>
27#include <xrpl/protocol/Keylet.h>
28#include <xrpl/protocol/LedgerFormats.h>
29#include <xrpl/protocol/MPTIssue.h>
30#include <xrpl/protocol/Protocol.h>
31#include <xrpl/protocol/Rules.h>
32#include <xrpl/protocol/SField.h>
33#include <xrpl/protocol/STAmount.h>
34#include <xrpl/protocol/STLedgerEntry.h>
35#include <xrpl/protocol/STObject.h>
36#include <xrpl/protocol/STTx.h>
37#include <xrpl/protocol/SeqProxy.h>
38#include <xrpl/protocol/TER.h>
39#include <xrpl/protocol/TxFlags.h>
40#include <xrpl/protocol/TxFormats.h>
41#include <xrpl/protocol/UintTypes.h>
42#include <xrpl/protocol/XRPAmount.h>
43#include <xrpl/protocol/jss.h>
44#include <xrpl/tx/ApplyContext.h>
45#include <xrpl/tx/Transactor.h>
46#include <xrpl/tx/applySteps.h>
47#include <xrpl/tx/invariants/VaultInvariant.h>
48
49#include <array>
50#include <chrono>
51#include <cstdint>
52#include <functional>
53#include <initializer_list>
54#include <memory>
55#include <optional>
56#include <string>
57#include <tuple>
58#include <utility>
59#include <vector>
60
61namespace xrpl::test {
62
64{
66
67 void
68 testVault() // NOLINT(readability-function-size)
69 {
70 using namespace test::jtx;
71
72 struct AccountAmount
73 {
74 AccountID account;
75 int amount;
76 };
77 // Parameters for a synthetic loan object created alongside a vault
78 // adjustment. The interest due booked to the vault is
79 // totalValueOutstanding - principalOutstanding - managementFeeOutstanding.
80 struct LoanParams
81 {
82 int principalOutstanding = 0;
83 int totalValueOutstanding = 0;
84 int managementFeeOutstanding = 0;
85 AccountID borrower = beast::kZero;
86 // Broker the created loan references. Left unset when the test does
87 // not depend on the broker resolving to a real ledger entry.
88 UInt256 brokerKey = beast::kZero;
89 };
90 struct Adjustments
91 {
92 // NOLINTBEGIN(readability-redundant-member-init)
93 std::optional<int> assetsTotal = std::nullopt;
94 std::optional<int> assetsAvailable = std::nullopt;
95 std::optional<int> lossUnrealized = std::nullopt;
96 std::optional<int> assetsMaximum = std::nullopt;
97 std::optional<int> sharesTotal = std::nullopt;
98 std::optional<int> vaultAssets = std::nullopt;
99 std::optional<AccountAmount> accountAssets = std::nullopt;
100 std::optional<AccountAmount> accountShares = std::nullopt;
101 std::optional<LoanParams> createLoan = std::nullopt;
102 // Number of loan objects to create (only used when createLoan is
103 // set); a valid loan set creates exactly one.
104 int loanCount = 1;
105 // NOLINTEND(readability-redundant-member-init)
106 };
107 constexpr auto kAdjust = [&](ApplyView& ac, xrpl::Keylet keylet, Adjustments args) {
108 // Avoid uint64 + negative-int wrap (flagged by UBSan
109 // unsigned-integer-overflow) when adjusting UINT64 fields.
110 auto const addSigned = [](std::uint64_t current, int adj) -> std::uint64_t {
111 return adj >= 0 //
112 ? current + static_cast<std::uint64_t>(adj)
113 : current - static_cast<std::uint64_t>(-adj);
114 };
115 auto sleVault = ac.peek(keylet);
116 if (!sleVault)
117 return false;
118
119 auto const mptIssuanceID = (*sleVault)[sfShareMPTID];
120 auto sleShares = ac.peek(keylet::mptokenIssuance(mptIssuanceID));
121 if (!sleShares)
122 return false;
123
124 // These two fields are adjusted in absolute terms
125 if (args.lossUnrealized)
126 (*sleVault)[sfLossUnrealized] = *args.lossUnrealized;
127 if (args.assetsMaximum)
128 (*sleVault)[sfAssetsMaximum] = *args.assetsMaximum;
129
130 // Remaining fields are adjusted in terms of difference
131 if (args.assetsTotal)
132 (*sleVault)[sfAssetsTotal] = *(*sleVault)[sfAssetsTotal] + *args.assetsTotal;
133 if (args.assetsAvailable)
134 {
135 (*sleVault)[sfAssetsAvailable] =
136 *(*sleVault)[sfAssetsAvailable] + *args.assetsAvailable;
137 }
138 ac.update(sleVault);
139
140 if (args.sharesTotal)
141 {
142 (*sleShares)[sfOutstandingAmount] =
143 addSigned(*(*sleShares)[sfOutstandingAmount], *args.sharesTotal);
144 ac.update(sleShares);
145 }
146
147 auto const assets = *(*sleVault)[sfAsset];
148 auto const pseudoId = *(*sleVault)[sfAccount];
149 if (args.vaultAssets)
150 {
151 if (assets.native())
152 {
153 auto slePseudoAccount = ac.peek(keylet::account(pseudoId));
154 if (!slePseudoAccount)
155 return false;
156 (*slePseudoAccount)[sfBalance] =
157 *(*slePseudoAccount)[sfBalance] + *args.vaultAssets;
158 ac.update(slePseudoAccount);
159 }
160 else if (assets.holds<MPTIssue>())
161 {
162 auto const mptId = assets.get<MPTIssue>().getMptID();
163 auto sleMPToken = ac.peek(keylet::mptoken(mptId, pseudoId));
164 if (!sleMPToken)
165 return false;
166 (*sleMPToken)[sfMPTAmount] =
167 addSigned(*(*sleMPToken)[sfMPTAmount], *args.vaultAssets);
168 ac.update(sleMPToken);
169 }
170 else
171 {
172 return false; // Not supporting testing with IOU
173 }
174 }
175
176 if (args.accountAssets)
177 {
178 auto const& pair = *args.accountAssets;
179 if (assets.native())
180 {
181 auto sleAccount = ac.peek(keylet::account(pair.account));
182 if (!sleAccount)
183 return false;
184 (*sleAccount)[sfBalance] = *(*sleAccount)[sfBalance] + pair.amount;
185 ac.update(sleAccount);
186 }
187 else if (assets.holds<MPTIssue>())
188 {
189 auto const mptID = assets.get<MPTIssue>().getMptID();
190 auto sleMPToken = ac.peek(keylet::mptoken(mptID, pair.account));
191 if (!sleMPToken)
192 return false;
193 (*sleMPToken)[sfMPTAmount] =
194 addSigned(*(*sleMPToken)[sfMPTAmount], pair.amount);
195 ac.update(sleMPToken);
196 }
197 else
198 {
199 return false; // Not supporting testing with IOU
200 }
201 }
202
203 if (args.accountShares)
204 {
205 auto const& pair = *args.accountShares;
206 auto sleMPToken = ac.peek(keylet::mptoken(mptIssuanceID, pair.account));
207 if (!sleMPToken)
208 return false;
209 (*sleMPToken)[sfMPTAmount] = addSigned(*(*sleMPToken)[sfMPTAmount], pair.amount);
210 ac.update(sleMPToken);
211 }
212
213 if (args.createLoan)
214 {
215 auto const& lp = *args.createLoan;
216 bool const anyOutstanding = lp.principalOutstanding != 0 ||
217 lp.totalValueOutstanding != 0 || lp.managementFeeOutstanding != 0;
218 // The vault key stands in for an unset broker: it keeps the loan
219 // keylet distinct per vault while resolving to no broker.
220 UInt256 const brokerKey = lp.brokerKey != beast::kZero ? lp.brokerKey : keylet.key;
221 for (std::uint32_t seq = 1; seq <= static_cast<std::uint32_t>(args.loanCount);
222 ++seq)
223 {
224 auto sleLoan = makeLoanSle(brokerKey, seq, lp.borrower);
225 sleLoan->at(sfPrincipalOutstanding) = Number(lp.principalOutstanding);
226 sleLoan->at(sfTotalValueOutstanding) = Number(lp.totalValueOutstanding);
227 sleLoan->at(sfManagementFeeOutstanding) = Number(lp.managementFeeOutstanding);
228 sleLoan->setFieldU32(sfPaymentRemaining, anyOutstanding ? 1 : 0);
229 ac.insert(sleLoan);
230 }
231 }
232 return true;
233 };
234
235 static constexpr auto kArgs = [](AccountID id, int adjustment, auto fn) -> Adjustments {
236 Adjustments sample = {
237 .assetsTotal = adjustment,
238 .assetsAvailable = adjustment,
239 .lossUnrealized = 0,
240 .sharesTotal = adjustment,
241 .vaultAssets = adjustment,
242 .accountAssets = //
243 AccountAmount{.account = id, .amount = -adjustment},
244 .accountShares = //
245 AccountAmount{.account = id, .amount = adjustment}};
246 fn(sample);
247 return sample;
248 };
249
250 Account const a3{"A3"};
251 Account const a4{"A4"};
252 auto const precloseXrp = [&](Account const& a1,
253 Account const& a2,
254 Env& env,
255 VaultVersion version = VaultVersion::CashBasis) -> bool {
256 env.fund(XRP(1000), a3, a4);
257 Vault const vault{env};
258 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
259 env(tx);
260 env(vault.deposit({.depositor = a1, .id = keylet.key, .amount = XRP(10)}));
261 env(vault.deposit({.depositor = a2, .id = keylet.key, .amount = XRP(10)}));
262 env(vault.deposit({.depositor = a3, .id = keylet.key, .amount = XRP(10)}));
263 return true;
264 };
265
266 auto const createClosedXrpBroker =
267 [&](Account const& owner, Env& env) -> std::optional<std::pair<Keylet, Keylet>> {
268 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
269 auto const brokerKeylet = createLoanBroker(owner, env, xrpAsset);
270 auto const sleBroker = env.le(brokerKeylet);
271 if (!BEAST_EXPECT(sleBroker))
272 return std::nullopt;
273 auto const vaultKeylet = keylet::vault(sleBroker->at(sfVaultID));
274 env.close(std::chrono::seconds{61});
275 return std::pair{vaultKeylet, brokerKeylet};
276 };
277
278 testcase << "Vault general checks";
280 {"vault deletion succeeded without deleting a vault"},
281 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
282 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
283 auto sleVault = ac.view().peek(keylet);
284 if (!sleVault)
285 return false;
286 ac.view().update(sleVault);
287 return true;
288 },
289 XRPAmount{},
290 STTx{ttVAULT_DELETE, [](STObject&) {}},
292 [&](Account const& a1, Account const& a2, Env& env) {
293 Vault const vault{env};
294 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
295 env(tx);
296 return true;
297 });
298
300 {"vault updated by a wrong transaction type",
301 "deleted Vault without deleting its pseudo-account"},
302 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
303 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
304 auto sleVault = ac.view().peek(keylet);
305 if (!sleVault)
306 return false;
307 ac.view().erase(sleVault);
308 return true;
309 },
310 XRPAmount{},
311 STTx{ttPAYMENT, [](STObject&) {}},
313 [&](Account const& a1, Account const& a2, Env& env) {
314 Vault const vault{env};
315 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
316 env(tx);
317 return true;
318 });
319
321 {"vault updated by a wrong transaction type"},
322 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
323 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
324 auto sleVault = ac.view().peek(keylet);
325 if (!sleVault)
326 return false;
327 ac.view().update(sleVault);
328 return true;
329 },
330 XRPAmount{},
331 STTx{ttPAYMENT, [](STObject&) {}},
333 [&](Account const& a1, Account const& a2, Env& env) {
334 Vault const vault{env};
335 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
336 env(tx);
337 return true;
338 });
339
341 {"vault updated by a wrong transaction type"},
342 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
343 auto const sequence = ac.view().seq();
344 auto const vaultKeylet = keylet::vault(a1.id(), SeqProxy::rawSequence(sequence));
345 auto sleVault = std::make_shared<SLE>(vaultKeylet);
346 auto const vaultPage = ac.view().dirInsert(
347 keylet::ownerDir(a1.id()), sleVault->key(), describeOwnerDir(a1.id()));
348 sleVault->setFieldU64(sfOwnerNode, *vaultPage);
349 sleVault->setAccountID(sfAccount, a1.id());
350 ac.view().insert(sleVault);
351 return true;
352 },
353 XRPAmount{},
354 STTx{ttPAYMENT, [](STObject&) {}},
356
358 {"vault deleted by a wrong transaction type",
359 "deleted Vault without deleting its pseudo-account"},
360 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
361 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
362 auto sleVault = ac.view().peek(keylet);
363 if (!sleVault)
364 return false;
365 ac.view().erase(sleVault);
366 return true;
367 },
368 XRPAmount{},
369 STTx{ttVAULT_SET, [](STObject&) {}},
371 [&](Account const& a1, Account const& a2, Env& env) {
372 Vault const vault{env};
373 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
374 env(tx);
375 return true;
376 });
377
379 {"vault operation updated more than single vault",
380 "deleted Vault without deleting its pseudo-account"},
381 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
382 {
383 auto const keylet =
384 keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
385 auto sleVault = ac.view().peek(keylet);
386 if (!sleVault)
387 return false;
388 ac.view().erase(sleVault);
389 }
390 {
391 auto const keylet =
392 keylet::vault(a2.id(), SeqProxy::rawSequence(ac.view().seq()));
393 auto sleVault = ac.view().peek(keylet);
394 if (!sleVault)
395 return false;
396 ac.view().erase(sleVault);
397 }
398 return true;
399 },
400 XRPAmount{},
401 STTx{ttVAULT_DELETE, [](STObject&) {}},
403 [&](Account const& a1, Account const& a2, Env& env) {
404 Vault const vault{env};
405 {
406 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
407 env(tx);
408 }
409 {
410 auto [tx, _] = vault.create({.owner = a2, .asset = xrpIssue()});
411 env(tx);
412 }
413 return true;
414 });
415
417 {"vault operation updated more than single vault"},
418 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
419 auto const sequence = ac.view().seq();
420 auto const insertVault = [&](Account const a) {
421 auto const vaultKeylet = keylet::vault(a.id(), SeqProxy::rawSequence(sequence));
422 auto sleVault = std::make_shared<SLE>(vaultKeylet);
423 auto const vaultPage = ac.view().dirInsert(
424 keylet::ownerDir(a.id()), sleVault->key(), describeOwnerDir(a.id()));
425 sleVault->setFieldU64(sfOwnerNode, *vaultPage);
426 sleVault->setAccountID(sfAccount, a.id());
427 ac.view().insert(sleVault);
428 };
429 insertVault(a1);
430 insertVault(a2);
431 return true;
432 },
433 XRPAmount{},
434 STTx{ttVAULT_CREATE, [](STObject&) {}},
436
438 {"deleted vault must also delete shares",
439 "deleted Vault without deleting its pseudo-account"},
440 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
441 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
442 auto sleVault = ac.view().peek(keylet);
443 if (!sleVault)
444 return false;
445 ac.view().erase(sleVault);
446 return true;
447 },
448 XRPAmount{},
449 STTx{ttVAULT_DELETE, [](STObject&) {}},
451 [&](Account const& a1, Account const& a2, Env& env) {
452 Vault const vault{env};
453 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
454 env(tx);
455 return true;
456 });
457
459 {"deleted vault must have no shares outstanding",
460 "deleted vault must have no assets outstanding",
461 "deleted vault must have no assets available"},
462 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
463 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
464 auto sleVault = ac.view().peek(keylet);
465 if (!sleVault)
466 return false;
467 auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
468 if (!sleShares)
469 return false;
470 ac.view().erase(sleVault);
471 ac.view().erase(sleShares);
472 return true;
473 },
474 XRPAmount{},
475 STTx{ttVAULT_DELETE, [](STObject&) {}},
477 [&](Account const& a1, Account const& a2, Env& env) {
478 Vault const vault{env};
479 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
480 env(tx);
481 env(vault.deposit({.depositor = a1, .id = keylet.key, .amount = XRP(10)}));
482 return true;
483 });
484
486 {"vault operation succeeded without modifying a vault"},
487 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
488 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
489 auto sleVault = ac.view().peek(keylet);
490 if (!sleVault)
491 return false;
492 auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
493 if (!sleShares)
494 return false;
495 // Note, such an "orphaned" update of MPT issuance attached to a
496 // vault is invalid; ttVAULT_SET must also update Vault object.
497 sleShares->setFieldH256(sfDomainID, UInt256(13));
498 ac.view().update(sleShares);
499 return true;
500 },
501 XRPAmount{},
502 STTx{ttVAULT_SET, [](STObject& tx) {}},
504 precloseXrp,
506
508 {"vault operation succeeded without modifying a vault"},
509 [&](Account const& a1, Account const& a2, ApplyContext& ac) { return true; },
510 XRPAmount{},
511 STTx{ttVAULT_CREATE, [](STObject&) {}},
513 [&](Account const& a1, Account const& a2, Env& env) {
514 Vault const vault{env};
515 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
516 env(tx);
517 return true;
518 });
519
521 {"vault operation succeeded without modifying a vault"},
522 [&](Account const& a1, Account const& a2, ApplyContext& ac) { return true; },
523 XRPAmount{},
524 STTx{ttVAULT_DEPOSIT, [](STObject&) {}},
526 [&](Account const& a1, Account const& a2, Env& env) {
527 Vault const vault{env};
528 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
529 env(tx);
530 return true;
531 });
532
534 {"vault operation succeeded without modifying a vault"},
535 [&](Account const& a1, Account const& a2, ApplyContext& ac) { return true; },
536 XRPAmount{},
537 STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
539 [&](Account const& a1, Account const& a2, Env& env) {
540 Vault const vault{env};
541 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
542 env(tx);
543 return true;
544 });
545
547 {"vault operation succeeded without modifying a vault"},
548 [&](Account const& a1, Account const& a2, ApplyContext& ac) { return true; },
549 XRPAmount{},
550 STTx{ttVAULT_CLAWBACK, [](STObject&) {}},
552 [&](Account const& a1, Account const& a2, Env& env) {
553 Vault const vault{env};
554 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
555 env(tx);
556 return true;
557 });
558
560 {"vault operation succeeded without modifying a vault"},
561 [&](Account const& a1, Account const& a2, ApplyContext& ac) { return true; },
562 XRPAmount{},
563 STTx{ttVAULT_DELETE, [](STObject&) {}},
565 [&](Account const& a1, Account const& a2, Env& env) {
566 Vault const vault{env};
567 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
568 env(tx);
569 return true;
570 });
571
573 {"updated vault must have shares"},
574 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
575 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
576 auto sleVault = ac.view().peek(keylet);
577 if (!sleVault)
578 return false;
579 (*sleVault)[sfAssetsMaximum] = 200;
580 ac.view().update(sleVault);
581
582 auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
583 if (!sleShares)
584 return false;
585 ac.view().erase(sleShares);
586 return true;
587 },
588 XRPAmount{},
589 STTx{ttVAULT_SET, [](STObject&) {}},
591 [&](Account const& a1, Account const& a2, Env& env) {
592 Vault const vault{env};
593 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
594 env(tx);
595 return true;
596 });
597
599 {"vault operation succeeded without updating shares",
600 "assets available must not be greater than assets outstanding"},
601 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
602 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
603 auto sleVault = ac.view().peek(keylet);
604 if (!sleVault)
605 return false;
606 (*sleVault)[sfAssetsTotal] = 9;
607 ac.view().update(sleVault);
608 return true;
609 },
610 XRPAmount{},
611 STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
613 [&](Account const& a1, Account const& a2, Env& env) {
614 Vault const vault{env};
615 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
616 env(tx);
617 env(vault.deposit({.depositor = a1, .id = keylet.key, .amount = XRP(10)}));
618 return true;
619 });
620
622 {"set must not change assets outstanding",
623 "set must not change assets available",
624 "set must not change shares outstanding",
625 "set must not change vault balance",
626 "assets available must not be negative",
627 "assets available must not be greater than assets outstanding",
628 "assets outstanding must not be negative"},
629 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
630 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
631 auto sleVault = ac.view().peek(keylet);
632 if (!sleVault)
633 return false;
634 auto slePseudoAccount = ac.view().peek(keylet::account(*(*sleVault)[sfAccount]));
635 if (!slePseudoAccount)
636 return false;
637 (*slePseudoAccount)[sfBalance] = *(*slePseudoAccount)[sfBalance] - 10;
638 ac.view().update(slePseudoAccount);
639
640 // Move 10 drops to A4 to enforce total XRP balance
641 auto sleA4 = ac.view().peek(keylet::account(a4.id()));
642 if (!sleA4)
643 return false;
644 (*sleA4)[sfBalance] = *(*sleA4)[sfBalance] + 10;
645 ac.view().update(sleA4);
646
647 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
648 sample.assetsAvailable = (kDropsPerXrp * -100).value();
649 sample.assetsTotal = (kDropsPerXrp * -200).value();
650 sample.sharesTotal = -1;
651 }));
652 },
653 XRPAmount{},
654 STTx{ttVAULT_SET, [](STObject& tx) {}},
656 precloseXrp,
658
659 // Under featureLendingProtocolV1_1 the immutability of sfAsset, sfAccount,
660 // sfShareMPTID and sfLEVersion is enforced by NoModifiedUnmodifiableFields.
662 {"changed an unchangeable field"},
663 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
664 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
665 auto sleVault = ac.view().peek(keylet);
666 if (!sleVault)
667 return false;
668 sleVault->setFieldIssue(sfAsset, STIssue{sfAsset, MPTIssue(MPTID(42))});
669 ac.view().update(sleVault);
670 return true;
671 },
672 XRPAmount{},
673 STTx{ttVAULT_SET, [](STObject& tx) {}},
675 precloseXrp);
676
678 {"changed an unchangeable field"},
679 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
680 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
681 auto sleVault = ac.view().peek(keylet);
682 if (!sleVault)
683 return false;
684 sleVault->setAccountID(sfAccount, a2.id());
685 ac.view().update(sleVault);
686 return true;
687 },
688 XRPAmount{},
689 STTx{ttVAULT_SET, [](STObject& tx) {}},
691 precloseXrp);
692
694 {"changed an unchangeable field"},
695 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
696 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
697 auto sleVault = ac.view().peek(keylet);
698 if (!sleVault)
699 return false;
700 (*sleVault)[sfShareMPTID] = MPTID(42);
701 ac.view().update(sleVault);
702 return true;
703 },
704 XRPAmount{},
705 STTx{ttVAULT_SET, [](STObject& tx) {}},
707 precloseXrp);
708
710 {"changed an unchangeable field"},
711 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
712 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
713 auto sleVault = ac.view().peek(keylet);
714 if (!sleVault)
715 return false;
716 (*sleVault)[sfLEVersion] = std::to_underlying(VaultVersion::Legacy);
717 ac.view().update(sleVault);
718 return true;
719 },
720 XRPAmount{},
721 STTx{ttVAULT_SET, [](STObject& tx) {}},
723 [&precloseXrp](Account const& a1, Account const& a2, Env& env) {
724 return precloseXrp(a1, a2, env, VaultVersion::CashBasis);
725 });
726
727 // Pre-featureLendingProtocolV1_1 sfAsset, sfAccount and sfShareMPTID are
728 // guarded by ValidVault instead, so both paths need coverage. ValidVault
729 // returns early once the result is already tec, hence no escalation to
730 // tef on the second pass.
731 auto const preLendingV11Amendments = all_ - featureLendingProtocolV1_1;
733 makeEnv(preLendingV11Amendments),
734 {"violation of vault immutable data"},
735 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
736 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
737 auto sleVault = ac.view().peek(keylet);
738 if (!sleVault)
739 return false;
740 sleVault->setFieldIssue(sfAsset, STIssue{sfAsset, MPTIssue(MPTID(42))});
741 ac.view().update(sleVault);
742 return true;
743 },
744 XRPAmount{},
745 STTx{ttVAULT_SET, [](STObject& tx) {}},
747 precloseXrp);
748
750 makeEnv(preLendingV11Amendments),
751 {"violation of vault immutable data"},
752 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
753 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
754 auto sleVault = ac.view().peek(keylet);
755 if (!sleVault)
756 return false;
757 sleVault->setAccountID(sfAccount, a2.id());
758 ac.view().update(sleVault);
759 return true;
760 },
761 XRPAmount{},
762 STTx{ttVAULT_SET, [](STObject& tx) {}},
764 precloseXrp);
765
767 makeEnv(preLendingV11Amendments),
768 {"violation of vault immutable data"},
769 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
770 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
771 auto sleVault = ac.view().peek(keylet);
772 if (!sleVault)
773 return false;
774 (*sleVault)[sfShareMPTID] = MPTID(42);
775 ac.view().update(sleVault);
776 return true;
777 },
778 XRPAmount{},
779 STTx{ttVAULT_SET, [](STObject& tx) {}},
781 precloseXrp);
782
784 {"vault transaction must not change loss unrealized",
785 "set must not change assets outstanding"},
786 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
787 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
788 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
789 sample.lossUnrealized = 13;
790 sample.assetsTotal = 20;
791 }));
792 },
793 XRPAmount{},
794 STTx{ttVAULT_SET, [](STObject& tx) {}},
796 precloseXrp,
798
800 {"loss unrealized must not exceed the difference "
801 "between assets outstanding and available",
802 "vault transaction must not change loss unrealized"},
803 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
804 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
805 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 100, [&](Adjustments& sample) {
806 sample.lossUnrealized = 13;
807 }));
808 },
809 XRPAmount{},
810 STTx{
811 ttVAULT_DEPOSIT, [](STObject& tx) { tx.setFieldAmount(sfAmount, XRPAmount(200)); }},
813 precloseXrp,
815
816 // A negative loss unrealized must trip the invariant. ttLOAN_MANAGE is
817 // allowed to change loss unrealized, so it isolates this check from the
818 // "must not change loss unrealized" invariant. Gated behind
819 // fixCleanup3_4_0 (see below).
821 {"loss unrealized must not be negative"},
822 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
823 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
824 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
825 sample.lossUnrealized = -1;
826 }));
827 },
828 XRPAmount{},
829 STTx{ttLOAN_MANAGE, [](STObject& tx) {}},
831 precloseXrp,
833
834 // Without fixCleanup3_4_0 the same state must NOT trip the invariant,
835 // preserving pre-amendment behavior (no fork risk). Also remove
836 // featureLendingProtocolV1_1 so finalizeLoanManage's stricter checks
837 // (exactly one loan touched) do not fire from a bare vault mutation
838 // that does not touch a loan.
840 makeEnv(all_ - fixCleanup3_4_0 - featureLendingProtocolV1_1),
841 {},
842 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
843 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
844 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
845 sample.lossUnrealized = -1;
846 }));
847 },
848 XRPAmount{},
849 STTx{ttLOAN_MANAGE, [](STObject& tx) {}},
851 precloseXrp,
853
855 {"set assets outstanding must not exceed assets maximum"},
856 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
857 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
858 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
859 sample.assetsMaximum = 1;
860 }));
861 },
862 XRPAmount{},
863 STTx{ttVAULT_SET, [](STObject& tx) {}},
865 precloseXrp,
867
868 // The cap check has two post-fixCleanup3_4_0 triggers: the transaction
869 // supplied sfAssetsMaximum, or the cap changed. The case above covers
870 // the cap-changed one (its ttVAULT_SET carries no fields). This covers
871 // the other: the cap is left alone at 30 XRP and the transaction
872 // carries sfAssetsMaximum, so only the isFieldPresent disjunct can
873 // fire. AssetsTotal is pushed past the cap here rather than in
874 // preclose because VaultSet::doApply refuses to set a cap below
875 // AssetsTotal, so the over-cap state is only reachable by fabrication.
876 // Raising AssetsTotal also trips the "must not change assets
877 // outstanding" check, hence two expected messages.
878 Number const vaultCap = XRP(30).number();
880 {"set must not change assets outstanding",
881 "set assets outstanding must not exceed assets maximum"},
882 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
883 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
884 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
885 sample.assetsTotal = XRP(1).value().xrp().drops();
886 }));
887 },
888 XRPAmount{},
889 STTx{ttVAULT_SET, [&](STObject& tx) { tx[sfAssetsMaximum] = vaultCap; }},
891 [&](Account const& a1, Account const& a2, Env& env) -> bool {
892 env.fund(XRP(1000), a3, a4);
893 Vault const vault{env};
894 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
895 tx[sfAssetsMaximum] = vaultCap;
896 env(tx);
897 env(vault.deposit({.depositor = a1, .id = keylet.key, .amount = XRP(10)}));
898 env(vault.deposit({.depositor = a2, .id = keylet.key, .amount = XRP(10)}));
899 env(vault.deposit({.depositor = a3, .id = keylet.key, .amount = XRP(10)}));
900 return true;
901 },
903
905 {"assets maximum must not be negative"},
906 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
907 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
908 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
909 sample.assetsMaximum = -1;
910 }));
911 },
912 XRPAmount{},
913 STTx{ttVAULT_SET, [](STObject& tx) {}},
915 precloseXrp,
917
919 {"set must not change shares outstanding",
920 "updated zero sized vault must have no assets outstanding",
921 "updated zero sized vault must have no assets available"},
922 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
923 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
924 auto sleVault = ac.view().peek(keylet);
925 if (!sleVault)
926 return false;
927 ac.view().update(sleVault);
928 auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
929 if (!sleShares)
930 return false;
931 (*sleShares)[sfOutstandingAmount] = 0;
932 ac.view().update(sleShares);
933 return true;
934 },
935 XRPAmount{},
936 STTx{ttVAULT_SET, [](STObject& tx) {}},
938 precloseXrp,
940
942 {"updated shares must not exceed maximum"},
943 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
944 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
945 auto sleVault = ac.view().peek(keylet);
946 if (!sleVault)
947 return false;
948 auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
949 if (!sleShares)
950 return false;
951 (*sleShares)[sfMaximumAmount] = 10;
952 ac.view().update(sleShares);
953
954 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [](Adjustments&) {}));
955 },
956 XRPAmount{},
957 STTx{ttVAULT_DEPOSIT, [](STObject&) {}},
959 precloseXrp,
961
963 {"updated shares must not exceed maximum"},
964 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
965 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
966 kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [](Adjustments&) {}));
967
968 auto sleVault = ac.view().peek(keylet);
969 if (!sleVault)
970 return false;
971 auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
972 if (!sleShares)
973 return false;
974 (*sleShares)[sfOutstandingAmount] = kMaxMpTokenAmount + 1;
975 ac.view().update(sleShares);
976 return true;
977 },
978 XRPAmount{},
979 STTx{ttVAULT_DEPOSIT, [](STObject&) {}},
981 precloseXrp,
983
984 // ttLOAN_SET pre-featureLendingProtocolV1_1: finalizeLoanSet short-
985 // circuits and returns success without inspecting the loan or the
986 // vault. The same state that trips the principal-outstanding check
987 // under V1_1 must be silently accepted here.
989 makeEnv(all_ - featureLendingProtocolV1_1),
990 {},
991 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
992 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
993 return kAdjust(
994 ac.view(),
995 keylet,
996 Adjustments{
997 .assetsAvailable = -200,
998 .vaultAssets = -200,
999 .accountAssets = AccountAmount{.account = a2.id(), .amount = 200},
1000 .createLoan = LoanParams{
1001 .principalOutstanding = 300,
1002 .totalValueOutstanding = 300,
1003 .borrower = a1.id(),
1004 }});
1005 },
1006 XRPAmount{},
1007 STTx{ttLOAN_SET, [](STObject& tx) { tx.at(sfPrincipalRequested) = Number(200); }},
1009 precloseXrp);
1010
1011 // ttLOAN_MANAGE: a loan is created rather than modified. This object-
1012 // existence rule applies on both invariant passes.
1014 {"Loan created by a transaction other than LoanSet"},
1015 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1016 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
1017 return kAdjust(
1018 ac.view(),
1019 keylet,
1020 Adjustments{
1021 .createLoan = LoanParams{
1022 .principalOutstanding = 100,
1023 .totalValueOutstanding = 100,
1024 .borrower = a1.id(),
1025 }});
1026 },
1027 XRPAmount{},
1028 STTx{ttLOAN_MANAGE, [](STObject& tx) { tx.setFieldU32(sfFlags, tfLoanImpair); }},
1030 precloseXrp);
1031
1032 // ttLOAN_MANAGE: loss unrealized driven negative
1033 doInvariantCheck(
1034 {"loss unrealized must not be negative"},
1035 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1036 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
1037 return kAdjust(ac.view(), keylet, Adjustments{.lossUnrealized = -1});
1038 },
1039 XRPAmount{},
1040 STTx{ttLOAN_MANAGE, [](STObject&) {}},
1042 precloseXrp);
1043
1044 // Loan flags may only change under the transaction types that own
1045 // those transitions.
1046 {
1047 struct Case
1048 {
1049 std::uint32_t before;
1050 std::uint32_t after;
1051 std::string expected;
1052 };
1053 auto const cases = std::to_array<Case>({
1054 {.before = 0,
1055 .after = lsfLoanImpaired,
1056 .expected = "lsfLoanImpaired changed outside LoanManage or LoanPay"},
1057 {.before = lsfLoanImpaired,
1058 .after = 0,
1059 .expected = "lsfLoanImpaired changed outside LoanManage or LoanPay"},
1060 {.before = 0,
1061 .after = lsfLoanDefault,
1062 .expected = "lsfLoanDefault changed outside LoanManage"},
1063 });
1064
1065 for (auto const& c : cases)
1066 {
1067 Env env{*this, all_};
1068 Account const a1{"A1"};
1069 Account const a2{"A2"};
1070 env.fund(XRP(1000), a1, a2);
1071 auto const keys = createClosedXrpBroker(a1, env);
1072 if (!keys)
1073 continue;
1074 auto const& brokerKeylet = keys->second;
1075
1076 OpenView ov{*env.current()};
1077 auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
1078 {
1079 auto sleLoan = makeLoanSle(brokerKeylet.key, 1, a1.id());
1080 sleLoan->at(sfPrincipalOutstanding) = Number(100);
1081 sleLoan->at(sfTotalValueOutstanding) = Number(150);
1082 sleLoan->setFieldU32(sfPaymentRemaining, 1);
1083 sleLoan->setFieldU32(sfFlags, c.before);
1084 ov.rawInsert(sleLoan);
1085 }
1086
1087 STTx const tx{ttACCOUNT_SET, [](STObject&) {}};
1088 test::StreamSink sink{beast::Severity::Warning};
1089 beast::Journal const jlog{sink};
1090 ApplyContext ac{
1091 env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
1092 CurrentTransactionRulesGuard const rulesGuard(ov.rules());
1093
1094 auto sleLoan = ac.view().peek(loanKeylet);
1095 if (!BEAST_EXPECT(sleLoan))
1096 continue;
1097 sleLoan->setFieldU32(sfFlags, c.after);
1098 ac.view().update(sleLoan);
1099
1100 auto transactor = makeTransactor(ac);
1101 if (!BEAST_EXPECT(transactor))
1102 continue;
1103 TER const result = transactor->checkInvariants(
1105 BEAST_EXPECT(result == tecINVARIANT_FAILED);
1106 BEAST_EXPECT(sink.messages().str().contains(c.expected));
1107 }
1108 }
1109
1110 // ttLOAN_MANAGE (default): a defaulted loan atomically enters a
1111 // terminal state, which drops sfNextPaymentDueDate from the ledger
1112 // entry. Seed a loan that already carries lsfLoanDefault so the
1113 // "must newly set" check passes, then leave sfNextPaymentDueDate
1114 // present and non-zero on the after-image; the residual due-date
1115 // check must then fire.
1116 {
1117 Env env{*this, all_};
1118 Account const a1{"A1"};
1119 Account const a2{"A2"};
1120 env.fund(XRP(1000), a1, a2);
1121 BEAST_EXPECT(precloseXrp(a1, a2, env));
1122 env.close();
1123
1124 OpenView ov{*env.current()};
1125
1126 auto const brokerKeylet = keylet::loanBroker(a1.id(), SeqProxy::rawSequence(1));
1127 auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
1128 // Pre-insert a loan that is not yet defaulted but has a
1129 // NextPaymentDueDate set; the apply-view mutation below flips
1130 // lsfLoanDefault (so the "must newly set" check passes) while
1131 // leaving the due date behind.
1132 {
1133 auto sleLoan = makeLoanSle(brokerKeylet.key, 1, a2.id());
1134 sleLoan->setFieldU32(sfNextPaymentDueDate, 123);
1135 ov.rawInsert(sleLoan);
1136 }
1137
1138 STTx const tx{
1139 ttLOAN_MANAGE, [](STObject& t) { t.setFieldU32(sfFlags, tfLoanDefault); }};
1140 test::StreamSink sink{beast::Severity::Warning};
1141 beast::Journal const jlog{sink};
1142 ApplyContext ac{
1143 env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
1144 CurrentTransactionRulesGuard const rulesGuard(ov.rules());
1145
1146 auto sleLoan = ac.view().peek(loanKeylet);
1147 if (!BEAST_EXPECT(sleLoan))
1148 return;
1149 sleLoan->setFieldU32(sfFlags, lsfLoanDefault);
1150 ac.view().update(sleLoan);
1151
1152 auto transactor = makeTransactor(ac);
1153 if (!BEAST_EXPECT(transactor))
1154 return;
1155 TER const result = transactor->checkInvariants(
1157 BEAST_EXPECT(result == tecINVARIANT_FAILED);
1158 BEAST_EXPECT(sink.messages().str().contains(
1159 "Loan with zero payments must have zero next payment due date"));
1160 }
1161
1162 // ttLOAN_PAY pre-featureLendingProtocolV1_1: finalizeLoanPay short-
1163 // circuits and returns success. The same "no vault balance change"
1164 // state that trips the check under V1_1 must be silently accepted
1165 // here.
1166 doInvariantCheck(
1167 makeEnv(all_ - featureLendingProtocolV1_1),
1168 {},
1169 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1170 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
1171 return kAdjust(ac.view(), keylet, Adjustments{});
1172 },
1173 XRPAmount{},
1174 STTx{ttLOAN_PAY, [](STObject& tx) { tx.setFieldAmount(sfAmount, XRPAmount(200)); }},
1176 precloseXrp);
1177
1178 // ttLOAN_PAY: cash is credited to the vault and a loan is created
1179 // rather than modified. This object-existence rule applies on both
1180 // invariant passes.
1181 doInvariantCheck(
1182 {"Loan created by a transaction other than LoanSet"},
1183 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1184 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
1185 return kAdjust(
1186 ac.view(),
1187 keylet,
1188 Adjustments{
1189 .assetsTotal = 50,
1190 .assetsAvailable = 50,
1191 .vaultAssets = 50,
1192 .accountAssets = AccountAmount{.account = a2.id(), .amount = -50},
1193 .createLoan = LoanParams{
1194 .principalOutstanding = 100,
1195 .totalValueOutstanding = 100,
1196 .borrower = a1.id(),
1197 }});
1198 },
1199 XRPAmount{},
1200 STTx{ttLOAN_PAY, [](STObject& tx) { tx.setFieldAmount(sfAmount, XRPAmount(50)); }},
1202 precloseXrp);
1203
1204 // ttLOAN_PAY: loss unrealized driven negative. The cash inflow is
1205 // valid, but loss unrealized is set below zero.
1206 doInvariantCheck(
1207 {"loss unrealized must not be negative"},
1208 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1209 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
1210 return kAdjust(
1211 ac.view(),
1212 keylet,
1213 Adjustments{
1214 .assetsTotal = 100,
1215 .assetsAvailable = 100,
1216 .lossUnrealized = -1,
1217 .vaultAssets = 100,
1218 .accountAssets = AccountAmount{.account = a2.id(), .amount = -100}});
1219 },
1220 XRPAmount{},
1221 STTx{ttLOAN_PAY, [](STObject& tx) { tx.setFieldAmount(sfAmount, XRPAmount(200)); }},
1223 precloseXrp);
1224
1225 // ttLOAN_PAY success post-conditions. A loan left with payments still
1226 // remaining after a successful payment must show that payment in its
1227 // balance and schedule: neither PrincipalOutstanding nor
1228 // TotalValueOutstanding may increase, at least one of them must
1229 // strictly decrease, PaymentRemaining must strictly decrease, and
1230 // NextPaymentDueDate must advance by a positive multiple of
1231 // PaymentInterval. Each failing case seeds the same loan, then applies
1232 // an after-image that breaks exactly one of those conditions.
1233 {
1234 struct Case
1235 {
1236 Number principal;
1237 Number totalValue;
1238 std::uint32_t remaining;
1239 std::uint32_t dueDate;
1240 std::string expected;
1241 };
1242 auto const cases = std::to_array<Case>({
1243 {.principal = Number(100),
1244 .totalValue = Number(150),
1245 .remaining = 1,
1246 .dueDate = 110,
1247 .expected = "loan pay must decrease PrincipalOutstanding or "
1248 "TotalValueOutstanding"},
1249 {.principal = Number(110),
1250 .totalValue = Number(150),
1251 .remaining = 1,
1252 .dueDate = 110,
1253 .expected = "loan pay must not increase PrincipalOutstanding"},
1254 {.principal = Number(50),
1255 .totalValue = Number(160),
1256 .remaining = 1,
1257 .dueDate = 110,
1258 .expected = "loan pay must not increase TotalValueOutstanding"},
1259 {.principal = Number(50),
1260 .totalValue = Number(150),
1261 .remaining = 2,
1262 .dueDate = 110,
1263 .expected = "loan pay must decrease PaymentRemaining"},
1264 {.principal = Number(50),
1265 .totalValue = Number(150),
1266 .remaining = 1,
1267 .dueDate = 100,
1268 .expected = "loan pay must advance NextPaymentDueDate"},
1269 // Advanced, but not by a whole number of payment intervals.
1270 {.principal = Number(50),
1271 .totalValue = Number(150),
1272 .remaining = 1,
1273 .dueDate = 105,
1274 .expected = "loan pay must advance NextPaymentDueDate"},
1275 });
1276
1277 for (auto const& c : cases)
1278 {
1279 Env env{*this, all_};
1280 Account const a1{"A1"};
1281 Account const a2{"A2"};
1282 env.fund(XRP(1000), a1, a2);
1283 auto const keys = createClosedXrpBroker(a1, env);
1284 if (!keys)
1285 continue;
1286 auto const& brokerKeylet = keys->second;
1287
1288 OpenView ov{*env.current()};
1289 auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
1290 {
1291 auto sleLoan = makeLoanSle(brokerKeylet.key, 1, a2.id());
1292 sleLoan->at(sfPrincipalOutstanding) = Number(100);
1293 sleLoan->at(sfTotalValueOutstanding) = Number(150);
1294 sleLoan->at(sfPaymentInterval) = 10u;
1295 sleLoan->setFieldU32(sfPaymentRemaining, 2);
1296 sleLoan->setFieldU32(sfNextPaymentDueDate, 100);
1297 ov.rawInsert(sleLoan);
1298 }
1299
1300 STTx const tx{
1301 ttLOAN_PAY, [](STObject& t) { t.setFieldAmount(sfAmount, XRPAmount(50)); }};
1302 test::StreamSink sink{beast::Severity::Warning};
1303 beast::Journal const jlog{sink};
1304 ApplyContext ac{
1305 env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
1306 CurrentTransactionRulesGuard const rulesGuard(ov.rules());
1307
1308 auto sleLoan = ac.view().peek(loanKeylet);
1309 if (!BEAST_EXPECT(sleLoan))
1310 continue;
1311 sleLoan->at(sfPrincipalOutstanding) = c.principal;
1312 sleLoan->at(sfTotalValueOutstanding) = c.totalValue;
1313 sleLoan->setFieldU32(sfPaymentRemaining, c.remaining);
1314 sleLoan->setFieldU32(sfNextPaymentDueDate, c.dueDate);
1315 ac.view().update(sleLoan);
1316
1317 auto transactor = makeTransactor(ac);
1318 if (!BEAST_EXPECT(transactor))
1319 continue;
1320 TER const result = transactor->checkInvariants(
1321 tesSUCCESS, XRPAmount{}, Transactor::InvariantScope::Full);
1322 BEAST_EXPECT(result == tecINVARIANT_FAILED);
1323 BEAST_EXPECT(sink.messages().str().contains(c.expected));
1324 }
1325
1326 // Principal may stick while TotalValueOutstanding falls. This
1327 // after-image is only a Loan mutation, so other (vault) invariants
1328 // still fail under Full scope; ValidLoan itself must not.
1329 {
1330 Env env{*this, all_};
1331 Account const a1{"A1"};
1332 Account const a2{"A2"};
1333 env.fund(XRP(1000), a1, a2);
1334 auto const keys = createClosedXrpBroker(a1, env);
1335 if (!keys)
1336 {
1337 fail();
1338 }
1339 else
1340 {
1341 auto const& brokerKeylet = keys->second;
1342 OpenView ov{*env.current()};
1343 auto const loanKeylet =
1344 keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
1345 {
1346 auto sleLoan = makeLoanSle(brokerKeylet.key, 1, a2.id());
1347 sleLoan->at(sfPrincipalOutstanding) = Number(100);
1348 sleLoan->at(sfTotalValueOutstanding) = Number(150);
1349 sleLoan->at(sfPaymentInterval) = 10u;
1350 sleLoan->setFieldU32(sfPaymentRemaining, 2);
1351 sleLoan->setFieldU32(sfNextPaymentDueDate, 100);
1352 ov.rawInsert(sleLoan);
1353 }
1354
1355 STTx const tx{
1356 ttLOAN_PAY, [](STObject& t) { t.setFieldAmount(sfAmount, XRPAmount(50)); }};
1357 test::StreamSink sink{beast::Severity::Warning};
1358 beast::Journal const jlog{sink};
1359 ApplyContext ac{
1360 env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
1361 CurrentTransactionRulesGuard const rulesGuard(ov.rules());
1362
1363 auto sleLoan = ac.view().peek(loanKeylet);
1364 if (BEAST_EXPECT(sleLoan))
1365 {
1366 sleLoan->at(sfPrincipalOutstanding) = Number(100);
1367 sleLoan->at(sfTotalValueOutstanding) = Number(140);
1368 sleLoan->setFieldU32(sfPaymentRemaining, 1);
1369 sleLoan->setFieldU32(sfNextPaymentDueDate, 110);
1370 ac.view().update(sleLoan);
1371
1372 auto transactor = makeTransactor(ac);
1373 if (BEAST_EXPECT(transactor))
1374 {
1375 std::ignore = transactor->checkInvariants(
1376 tesSUCCESS, XRPAmount{}, Transactor::InvariantScope::Full);
1377 auto const logs = sink.messages().str();
1378 BEAST_EXPECT(!logs.contains("Invariant failed: Loan"));
1379 BEAST_EXPECT(!logs.contains("loan pay"));
1380 }
1381 }
1382 }
1383 }
1384 }
1385
1386 // ttLOAN_MANAGE (default): the write-off is rounded downward at the
1387 // pre-default AssetsTotal scale. A near-total IOU default can leave
1388 // valid positive dust while moving the posterior AssetsTotal to a much
1389 // finer scale. The dust must be bounded by the former scale rather than
1390 // compared with one unit at the posterior scale.
1391 {
1392 Env env{*this, all_ | featureLendingProtocolV1_1};
1393 Account const issuer{"issuer"};
1394 Account const owner{"owner"};
1395 Account const borrower{"borrower"};
1396 env.fund(XRP(1000), issuer, owner, borrower);
1397 env.close();
1398
1399 PrettyAsset const iouAsset{issuer["IOU"]};
1400 auto const brokerKeylet = createLoanBroker(owner, env, iouAsset);
1401 auto const sleBrokerBase = env.le(brokerKeylet);
1402 if (!BEAST_EXPECT(sleBrokerBase))
1403 return;
1404 auto const vaultKeylet = keylet::vault(sleBrokerBase->at(sfVaultID));
1405 env.close();
1406
1407 Number const assetsTotalBefore{1, 1};
1408 Number const loanOwed{9'999'999'999'999'999LL, -15};
1409 Number const assetsTotalAfter{1, -14};
1410 auto const beforeScale = scale(assetsTotalBefore, iouAsset);
1411 auto const afterScale = scale(assetsTotalAfter, iouAsset);
1412 Number const residual = (assetsTotalAfter - assetsTotalBefore) - (-loanOwed);
1413 Number const beforeTolerance{1, beforeScale};
1414 Number const afterTolerance{1, afterScale};
1415
1416 BEAST_EXPECT(afterScale < beforeScale);
1417 BEAST_EXPECT(residual > beast::kZero && residual < beforeTolerance);
1418 BEAST_EXPECT(residual > afterTolerance);
1419
1420 OpenView ov{*env.current()};
1421 {
1422 auto const sleVaultRead = ov.read(vaultKeylet);
1423 if (!BEAST_EXPECT(sleVaultRead))
1424 return;
1425 auto sleVault = std::make_shared<SLE>(*sleVaultRead);
1426 sleVault->at(sfAssetsTotal) = assetsTotalBefore;
1427 sleVault->at(sfAssetsAvailable) = Number(0);
1428 ov.rawReplace(sleVault);
1429
1430 auto const sharesKeylet = keylet::mptokenIssuance(sleVaultRead->at(sfShareMPTID));
1431 auto const sleSharesRead = ov.read(sharesKeylet);
1432 if (!BEAST_EXPECT(sleSharesRead))
1433 return;
1434 auto sleShares = std::make_shared<SLE>(*sleSharesRead);
1435 sleShares->at(sfOutstandingAmount) = 1;
1436 ov.rawReplace(sleShares);
1437 }
1438 {
1439 auto const sleBrokerRead = ov.read(brokerKeylet);
1440 if (!BEAST_EXPECT(sleBrokerRead))
1441 return;
1442 auto sleBroker = std::make_shared<SLE>(*sleBrokerRead);
1443 sleBroker->at(sfDebtTotal) = loanOwed;
1444 ov.rawReplace(sleBroker);
1445 }
1446 auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
1447 {
1448 auto sleLoan = makeLoanSle(brokerKeylet.key, 1, borrower.id());
1449 sleLoan->at(sfPrincipalOutstanding) = loanOwed;
1450 sleLoan->at(sfTotalValueOutstanding) = loanOwed;
1451 sleLoan->setFieldU32(sfPaymentRemaining, 1);
1452 ov.rawInsert(sleLoan);
1453 }
1454
1455 STTx const tx{
1456 ttLOAN_MANAGE, [](STObject& t) { t.setFieldU32(sfFlags, tfLoanDefault); }};
1457 test::StreamSink sink{beast::Severity::Warning};
1458 beast::Journal const jlog{sink};
1459 ApplyContext ac{
1460 env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
1461 CurrentTransactionRulesGuard const rulesGuard(ov.rules());
1462
1463 {
1464 auto sleVault = ac.view().peek(vaultKeylet);
1465 if (!BEAST_EXPECT(sleVault))
1466 return;
1467 sleVault->at(sfAssetsTotal) = assetsTotalAfter;
1468 ac.view().update(sleVault);
1469 }
1470 {
1471 auto sleBroker = ac.view().peek(brokerKeylet);
1472 if (!BEAST_EXPECT(sleBroker))
1473 return;
1474 sleBroker->at(sfDebtTotal) = Number(0);
1475 ac.view().update(sleBroker);
1476 }
1477 {
1478 auto sleLoan = ac.view().peek(loanKeylet);
1479 if (!BEAST_EXPECT(sleLoan))
1480 return;
1481 sleLoan->at(sfPrincipalOutstanding) = Number(0);
1482 sleLoan->at(sfTotalValueOutstanding) = Number(0);
1483 sleLoan->setFieldU32(sfPaymentRemaining, 0);
1484 sleLoan->setFieldU32(sfFlags, lsfLoanDefault);
1485 ac.view().update(sleLoan);
1486 }
1487
1488 auto transactor = makeTransactor(ac);
1489 if (!BEAST_EXPECT(transactor))
1490 return;
1491 TER const result = transactor->checkInvariants(
1492 tesSUCCESS, XRPAmount{}, Transactor::InvariantScope::Full);
1493 BEAST_EXPECT(result == tesSUCCESS);
1494 }
1495
1496 // A loan may only be deleted by a LoanDelete transaction, and only once
1497 // it is fully paid off. Both branches are exercised by creating a real
1498 // loan in the Preclose (so it exists in the base ledger with outstanding
1499 // principal) and then erasing it in the Precheck.
1500 {
1501 Keylet loanKeylet = keylet::amendments();
1502 auto const precloseLoan = [&loanKeylet, this](
1503 Account const& a1, Account const& a2, Env& env) -> bool {
1504 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
1505 auto const brokerKeylet = createLoanBroker(a1, env, xrpAsset);
1506 auto const brokerSle = env.le(brokerKeylet);
1507 if (!BEAST_EXPECT(brokerSle))
1508 return false;
1509 auto const vaultKeylet = keylet::vault(brokerSle->at(sfVaultID));
1510 Vault const vault{env};
1511 env(vault.deposit(
1512 {.depositor = a1, .id = vaultKeylet.key, .amount = xrpAsset(100)}));
1513 env.close(std::chrono::seconds{61});
1514
1515 loanKeylet = keylet::loan(
1516 brokerKeylet.key, SeqProxy::rawSequence(brokerSle->at(sfLoanSequence)));
1517 env(loan::set(a2, brokerKeylet.key, xrpAsset(50).value()),
1518 loan::kCounterparty(a1),
1519 Sig(sfCounterpartySignature, a1),
1520 loan::kPaymentInterval(60),
1521 loan::kPaymentTotal(1),
1522 Fee(env.current()->fees().base * 2));
1523 env.close();
1524 return BEAST_EXPECT(env.le(loanKeylet));
1525 };
1526
1527 auto const eraseLoan = [&loanKeylet](Account const&, Account const&, ApplyContext& ac) {
1528 auto sle = ac.view().peek(loanKeylet);
1529 if (!sle)
1530 return false;
1531 ac.view().erase(sle);
1532 return true;
1533 };
1534
1535 // Deleting the loan under any transaction type other than LoanDelete
1536 // (here the neutral ttACCOUNT_SET) is a violation, even while the
1537 // loan still has outstanding obligations: the transaction-type check
1538 // fires before the not-fully-paid-off check.
1539 doInvariantCheck(
1540 {"Loan deleted by a transaction other than LoanDelete"},
1541 eraseLoan,
1542 XRPAmount{},
1543 STTx{ttACCOUNT_SET, [](STObject&) {}},
1545 precloseLoan);
1546 }
1547
1548 STTx const loanSetTx{
1549 ttLOAN_SET, [](STObject& tx) { tx.at(sfPrincipalRequested) = Number(0); }};
1550
1551 // Loan interest due (total value less principal and management fee) must
1552 // never be negative. The loan below carries a total value short of its
1553 // principal, while every individual field stays non-negative. A real
1554 // broker over an XRP vault is created in the preclose, both so the
1555 // earlier broker-existence checks pass and so the deficit is measured
1556 // in an integral asset domain, where no rounding tolerance applies.
1557 {
1558 Keylet brokerKeylet = keylet::amendments();
1559 auto const precloseBroker = [&brokerKeylet, this](
1560 Account const& a1, Account const&, Env& env) -> bool {
1561 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
1562 brokerKeylet = this->createLoanBroker(a1, env, xrpAsset);
1563 env.close();
1564 return BEAST_EXPECT(env.le(brokerKeylet));
1565 };
1566
1567 doInvariantCheck(
1568 {"Loan interest due is negative"},
1569 [&](Account const&, Account const& a2, ApplyContext& ac) {
1570 auto sleLoan = makeLoanSle(brokerKeylet.key, 1, a2.id());
1571 sleLoan->at(sfPrincipalOutstanding) = Number(100);
1572 sleLoan->at(sfTotalValueOutstanding) = Number(90);
1573 sleLoan->setFieldU32(sfPaymentRemaining, 1);
1574 ac.view().insert(sleLoan);
1575 return true;
1576 },
1577 XRPAmount{},
1578 loanSetTx,
1580 precloseBroker);
1581 }
1582
1583 // Each of these loan STNumber fields must never be negative. The loan
1584 // is created directly with a single field set negative while the
1585 // paid-off bookkeeping is kept consistent, so that only the "<field>
1586 // is negative" check trips.
1587 for (auto const field : {
1588 &sfLoanServiceFee,
1589 &sfLatePaymentFee,
1590 &sfClosePaymentFee,
1591 &sfPrincipalOutstanding,
1592 &sfTotalValueOutstanding,
1593 &sfManagementFeeOutstanding,
1594 })
1595 {
1596 // The outstanding-balance fields also feed the paid-off checks, so
1597 // a loan carrying one must still have payments remaining; a loan
1598 // with only a negative fee stays fully paid off (zero remaining).
1599 bool const isOutstanding = *field == sfPrincipalOutstanding ||
1600 *field == sfTotalValueOutstanding || *field == sfManagementFeeOutstanding;
1601 doInvariantCheck(
1602 {field->getName() + " is negative"},
1603 [&, field](Account const& a1, Account const& a2, ApplyContext& ac) {
1604 auto const brokerKeylet = keylet::loanBroker(a1.id(), SeqProxy::rawSequence(1));
1605 auto sleLoan = makeLoanSle(brokerKeylet.key, 1, a2.id());
1606 sleLoan->at(*field) = Number(-10);
1607 sleLoan->setFieldU32(sfPaymentRemaining, isOutstanding ? 1 : 0);
1608 ac.view().insert(sleLoan);
1609 return true;
1610 },
1611 XRPAmount{},
1612 loanSetTx);
1613 }
1614
1615 // Mirror of the loop above for the strictly-positive constraint: a
1616 // loan's sfPeriodicPayment must always be > 0. Cover both boundary
1617 // failure modes (zero and negative).
1618 for (Number const& badValue : {Number(0), Number(-1)})
1619 {
1620 doInvariantCheck(
1621 {std::string{sfPeriodicPayment.getName()} + " is zero or negative"},
1622 [&, badValue](Account const& a1, Account const& a2, ApplyContext& ac) {
1623 auto const brokerKeylet = keylet::loanBroker(a1.id(), SeqProxy::rawSequence(1));
1624 auto sleLoan = makeLoanSle(brokerKeylet.key, 1, a2.id());
1625 sleLoan->at(sfPeriodicPayment) = badValue;
1626 ac.view().insert(sleLoan);
1627 return true;
1628 },
1629 XRPAmount{},
1630 loanSetTx);
1631 }
1632
1633 // A loan with sfPaymentRemaining == 0 must be fully paid off in every
1634 // outstanding-balance dimension. Insert a bare loan that reports zero
1635 // payments remaining but still carries a non-zero principal owed; the
1636 // paid-off invariant must reject it before the later broker-existence
1637 // check has a chance to run.
1638 doInvariantCheck(
1639 {"Loan with zero payments remaining has not been paid off"},
1640 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1641 auto const brokerKeylet = keylet::loanBroker(a1.id(), SeqProxy::rawSequence(1));
1642 auto sleLoan = makeLoanSle(brokerKeylet.key, 1, a2.id());
1643 sleLoan->at(sfPrincipalOutstanding) = Number(100);
1644 sleLoan->at(sfTotalValueOutstanding) = Number(100);
1645 sleLoan->at(sfPeriodicPayment) = Number(1);
1646 sleLoan->setFieldU32(sfPaymentRemaining, 0);
1647 ac.view().insert(sleLoan);
1648 return true;
1649 },
1650 XRPAmount{},
1651 loanSetTx);
1652
1653 // Converse: a loan whose outstanding balances are all zero has been
1654 // fully paid off and must carry zero payments remaining. Insert a
1655 // fully-zeroed loan with sfPaymentRemaining = 1 to trip the check.
1656 doInvariantCheck(
1657 {"Fully paid off Loan still has payments remaining"},
1658 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1659 auto const brokerKeylet = keylet::loanBroker(a1.id(), SeqProxy::rawSequence(1));
1660 auto sleLoan = makeLoanSle(brokerKeylet.key, 1, a2.id());
1661 sleLoan->setFieldU32(sfPaymentRemaining, 1);
1662 ac.view().insert(sleLoan);
1663 return true;
1664 },
1665 XRPAmount{},
1666 loanSetTx);
1667
1668 // A loan must reference a live loan broker. A bare loan SLE is
1669 // inserted with every other loan-level field kept consistent so the
1670 // earlier ValidLoan checks pass; sfLoanBrokerID defaults to zero,
1671 // which resolves to no broker, and the broker-existence check trips.
1672 doInvariantCheck(
1673 {"Loan broker does not exist"},
1674 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1675 auto sleLoan = makeLoanSle(UInt256{}, 1, a2.id());
1676 ac.view().insert(sleLoan);
1677 return true;
1678 },
1679 XRPAmount{},
1680 loanSetTx);
1681
1682 // A loan's broker must in turn reference a live vault. A real broker
1683 // is created in the preclose so its sfVaultID points at an existing
1684 // vault; the precheck then erases that vault and inserts a loan
1685 // referencing the broker, so the broker-existence check passes and
1686 // the broker-vault-existence check trips.
1687 {
1688 Keylet brokerKeylet = keylet::amendments();
1689 auto const precloseBroker = [&brokerKeylet, this](
1690 Account const& a1, Account const&, Env& env) -> bool {
1691 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
1692 brokerKeylet = this->createLoanBroker(a1, env, xrpAsset);
1693 env.close();
1694 return BEAST_EXPECT(env.le(brokerKeylet));
1695 };
1696
1697 doInvariantCheck(
1698 {"Loan broker vault does not exist"},
1699 [&brokerKeylet](Account const&, Account const&, ApplyContext& ac) {
1700 auto sleBroker = ac.view().peek(brokerKeylet);
1701 if (!sleBroker)
1702 return false;
1703 auto sleVault = ac.view().peek(keylet::vault(sleBroker->at(sfVaultID)));
1704 if (!sleVault)
1705 return false;
1706 ac.view().erase(sleVault);
1707
1708 auto const loanKeylet =
1709 keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
1710 auto sleLoan = std::make_shared<SLE>(loanKeylet);
1711 sleLoan->at(sfLoanBrokerID) = brokerKeylet.key;
1712 sleLoan->at(sfPrincipalOutstanding) = Number(0);
1713 sleLoan->at(sfTotalValueOutstanding) = Number(0);
1714 sleLoan->at(sfManagementFeeOutstanding) = Number(0);
1715 sleLoan->at(sfPeriodicPayment) = Number(1);
1716 sleLoan->setFieldU32(sfPaymentRemaining, 0);
1717 ac.view().insert(sleLoan);
1718 return true;
1719 },
1720 XRPAmount{},
1721 loanSetTx,
1723 precloseBroker);
1724 }
1725
1726 // ttVAULT_SET: owner is immutable (enforced by
1727 // NoModifiedUnmodifiableFields under featureLendingProtocolV1_1.
1728 doInvariantCheck(
1729 {"changed an unchangeable field"},
1730 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1731 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
1732 auto sleVault = ac.view().peek(keylet);
1733 if (!sleVault)
1734 return false;
1735 sleVault->setAccountID(sfOwner, a2.id());
1736 ac.view().update(sleVault);
1737 return true;
1738 },
1739 XRPAmount{},
1740 STTx{ttVAULT_SET, [](STObject& tx) {}},
1742 precloseXrp);
1743
1744 // ttVAULT_SET: withdrawal policy is immutable
1745 doInvariantCheck(
1746 {"changed an unchangeable field"},
1747 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1748 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
1749 auto sleVault = ac.view().peek(keylet);
1750 if (!sleVault)
1751 return false;
1752 sleVault->setFieldU8(
1753 sfWithdrawalPolicy,
1754 static_cast<std::uint8_t>(sleVault->getFieldU8(sfWithdrawalPolicy) + 1));
1755 ac.view().update(sleVault);
1756 return true;
1757 },
1758 XRPAmount{},
1759 STTx{ttVAULT_SET, [](STObject& tx) {}},
1761 precloseXrp);
1762
1763 // ttVAULT_SET: scale is immutable
1764 doInvariantCheck(
1765 {"changed an unchangeable field"},
1766 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1767 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
1768 auto sleVault = ac.view().peek(keylet);
1769 if (!sleVault)
1770 return false;
1771 sleVault->setFieldU8(
1772 sfScale, static_cast<std::uint8_t>(sleVault->getFieldU8(sfScale) + 1));
1773 ac.view().update(sleVault);
1774 return true;
1775 },
1776 XRPAmount{},
1777 STTx{ttVAULT_SET, [](STObject& tx) {}},
1779 precloseXrp);
1780
1781 // featureLendingProtocolV1_1 moves the vault immutability checks from VaultInvariant to
1782 // InvariantCheck.
1783 doInvariantCheck(
1784 makeEnv(all_),
1785 {"changed an unchangeable field"},
1786 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1787 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
1788 auto sleVault = ac.view().peek(keylet);
1789 if (!sleVault)
1790 return false;
1791 sleVault->setFieldU8(
1792 sfWithdrawalPolicy,
1793 static_cast<std::uint8_t>(sleVault->getFieldU8(sfWithdrawalPolicy) + 1));
1794 ac.view().update(sleVault);
1795 return true;
1796 },
1797 XRPAmount{},
1798 STTx{ttVAULT_SET, [](STObject& tx) {}},
1800 precloseXrp);
1801
1802 testcase << "Vault create";
1803 doInvariantCheck(
1804 {
1805 "created vault must be empty",
1806 "updated zero sized vault must have no assets outstanding",
1807 "create operation must not have updated a vault",
1808 },
1809 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1810 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
1811 auto sleVault = ac.view().peek(keylet);
1812 if (!sleVault)
1813 return false;
1814 (*sleVault)[sfAssetsTotal] = 9;
1815 ac.view().update(sleVault);
1816 return true;
1817 },
1818 XRPAmount{},
1819 STTx{ttVAULT_CREATE, [](STObject&) {}},
1821 [&](Account const& a1, Account const& a2, Env& env) {
1822 Vault const vault{env};
1823 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
1824 env(tx);
1825 return true;
1826 });
1827
1828 doInvariantCheck(
1829 {
1830 "created vault must be empty",
1831 "updated zero sized vault must have no assets available",
1832 "assets available must not be greater than assets outstanding",
1833 "create operation must not have updated a vault",
1834 },
1835 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1836 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
1837 auto sleVault = ac.view().peek(keylet);
1838 if (!sleVault)
1839 return false;
1840 (*sleVault)[sfAssetsAvailable] = 9;
1841 ac.view().update(sleVault);
1842 return true;
1843 },
1844 XRPAmount{},
1845 STTx{ttVAULT_CREATE, [](STObject&) {}},
1847 [&](Account const& a1, Account const& a2, Env& env) {
1848 Vault const vault{env};
1849 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
1850 env(tx);
1851 return true;
1852 });
1853
1854 doInvariantCheck(
1855 {
1856 "created vault must be empty",
1857 "loss unrealized must not exceed the difference between assets "
1858 "outstanding and available",
1859 "vault transaction must not change loss unrealized",
1860 "create operation must not have updated a vault",
1861 },
1862 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1863 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
1864 auto sleVault = ac.view().peek(keylet);
1865 if (!sleVault)
1866 return false;
1867 (*sleVault)[sfLossUnrealized] = 1;
1868 ac.view().update(sleVault);
1869 return true;
1870 },
1871 XRPAmount{},
1872 STTx{ttVAULT_CREATE, [](STObject&) {}},
1874 [&](Account const& a1, Account const& a2, Env& env) {
1875 Vault const vault{env};
1876 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
1877 env(tx);
1878 return true;
1879 });
1880
1881 doInvariantCheck(
1882 {
1883 "created vault must be empty",
1884 "create operation must not have updated a vault",
1885 "invalid OutstandingAmount balance 0 9 0",
1886 },
1887 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1888 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
1889 auto sleVault = ac.view().peek(keylet);
1890 if (!sleVault)
1891 return false;
1892 auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
1893 if (!sleShares)
1894 return false;
1895 ac.view().update(sleVault);
1896 (*sleShares)[sfOutstandingAmount] = 9;
1897 ac.view().update(sleShares);
1898 return true;
1899 },
1900 XRPAmount{},
1901 STTx{ttVAULT_CREATE, [](STObject&) {}},
1903 [&](Account const& a1, Account const& a2, Env& env) {
1904 Vault const vault{env};
1905 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
1906 env(tx);
1907 return true;
1908 });
1909
1910 doInvariantCheck(
1911 {
1912 "assets maximum must not be negative",
1913 "create operation must not have updated a vault",
1914 },
1915 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1916 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
1917 auto sleVault = ac.view().peek(keylet);
1918 if (!sleVault)
1919 return false;
1920 (*sleVault)[sfAssetsMaximum] = Number(-1);
1921 ac.view().update(sleVault);
1922 return true;
1923 },
1924 XRPAmount{},
1925 STTx{ttVAULT_CREATE, [](STObject&) {}},
1927 [&](Account const& a1, Account const& a2, Env& env) {
1928 Vault const vault{env};
1929 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
1930 env(tx);
1931 return true;
1932 });
1933
1934 doInvariantCheck(
1935 {"create operation must not have updated a vault",
1936 "shares issuer and vault pseudo-account must be the same",
1937 "shares issuer must be a pseudo-account",
1938 "shares issuer pseudo-account must point back to the vault"},
1939 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1940 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
1941 auto sleVault = ac.view().peek(keylet);
1942 if (!sleVault)
1943 return false;
1944 auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
1945 if (!sleShares)
1946 return false;
1947 ac.view().update(sleVault);
1948 (*sleShares)[sfIssuer] = a1.id();
1949 ac.view().update(sleShares);
1950 return true;
1951 },
1952 XRPAmount{},
1953 STTx{ttVAULT_CREATE, [](STObject&) {}},
1955 [&](Account const& a1, Account const& a2, Env& env) {
1956 Vault const vault{env};
1957 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
1958 env(tx);
1959 return true;
1960 });
1961
1962 doInvariantCheck(
1963 {"vault created by a wrong transaction type", "account root created illegally"},
1964 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1965 // The code below will create a valid vault with (almost) all
1966 // the invariants holding. Except one: it is created by the
1967 // wrong transaction type.
1968 auto const sequence = ac.view().seq();
1969 auto const vaultKeylet = keylet::vault(a1.id(), SeqProxy::rawSequence(sequence));
1970 auto sleVault = std::make_shared<SLE>(vaultKeylet);
1971 auto const vaultPage = ac.view().dirInsert(
1972 keylet::ownerDir(a1.id()), sleVault->key(), describeOwnerDir(a1.id()));
1973 sleVault->setFieldU64(sfOwnerNode, *vaultPage);
1974
1975 auto pseudoId = pseudoAccountAddress(ac.view(), vaultKeylet.key);
1976 // Create pseudo-account.
1977 auto sleAccount = std::make_shared<SLE>(keylet::account(pseudoId));
1978 sleAccount->setAccountID(sfAccount, pseudoId);
1979 sleAccount->setFieldAmount(sfBalance, STAmount{});
1980 std::uint32_t const seqno = //
1981 ac.view().rules().enabled(featureSingleAssetVault) //
1982 ? 0 //
1983 : sequence;
1984 sleAccount->setFieldU32(sfSequence, seqno);
1985 sleAccount->setFieldU32(
1986 sfFlags, lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth);
1987 sleAccount->setFieldH256(sfVaultID, vaultKeylet.key);
1988 ac.view().insert(sleAccount);
1989
1990 auto const sharesMptId = makeMptID(sequence, pseudoId);
1991 auto const sharesKeylet = keylet::mptokenIssuance(sharesMptId);
1992 auto sleShares = std::make_shared<SLE>(sharesKeylet);
1993 auto const sharesPage = ac.view().dirInsert(
1994 keylet::ownerDir(pseudoId), sharesKeylet, describeOwnerDir(pseudoId));
1995 sleShares->setFieldU64(sfOwnerNode, *sharesPage);
1996
1997 sleShares->at(sfFlags) = 0;
1998 sleShares->at(sfIssuer) = pseudoId;
1999 sleShares->at(sfOutstandingAmount) = 0;
2000 sleShares->at(sfSequence) = sequence;
2001
2002 sleVault->at(sfAccount) = pseudoId;
2003 sleVault->at(sfFlags) = 0;
2004 sleVault->at(sfSequence) = sequence;
2005 sleVault->at(sfOwner) = a1.id();
2006 sleVault->at(sfAssetsTotal) = Number(0);
2007 sleVault->at(sfAssetsAvailable) = Number(0);
2008 sleVault->at(sfLossUnrealized) = Number(0);
2009 sleVault->at(sfShareMPTID) = sharesMptId;
2010 sleVault->at(sfWithdrawalPolicy) = kVaultStrategyFirstComeFirstServe;
2011
2012 ac.view().insert(sleVault);
2013 ac.view().insert(sleShares);
2014 return true;
2015 },
2016 XRPAmount{},
2017 STTx{ttVAULT_SET, [](STObject&) {}},
2019
2020 doInvariantCheck(
2021 {"shares issuer and vault pseudo-account must be the same",
2022 "shares issuer pseudo-account must point back to the vault"},
2023 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2024 auto const sequence = ac.view().seq();
2025 auto const vaultKeylet = keylet::vault(a1.id(), SeqProxy::rawSequence(sequence));
2026 auto sleVault = std::make_shared<SLE>(vaultKeylet);
2027 auto const vaultPage = ac.view().dirInsert(
2028 keylet::ownerDir(a1.id()), sleVault->key(), describeOwnerDir(a1.id()));
2029 sleVault->setFieldU64(sfOwnerNode, *vaultPage);
2030
2031 auto pseudoId = pseudoAccountAddress(ac.view(), vaultKeylet.key);
2032 // Create pseudo-account.
2033 auto sleAccount = std::make_shared<SLE>(keylet::account(pseudoId));
2034 sleAccount->setAccountID(sfAccount, pseudoId);
2035 sleAccount->setFieldAmount(sfBalance, STAmount{});
2036 std::uint32_t const seqno = //
2037 ac.view().rules().enabled(featureSingleAssetVault) //
2038 ? 0 //
2039 : sequence;
2040 sleAccount->setFieldU32(sfSequence, seqno);
2041 sleAccount->setFieldU32(
2042 sfFlags, lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth);
2043 // sleAccount->setFieldH256(sfVaultID, vaultKeylet.key);
2044 // Setting wrong vault key
2045 sleAccount->setFieldH256(sfVaultID, UInt256(42));
2046 ac.view().insert(sleAccount);
2047
2048 auto const sharesMptId = makeMptID(sequence, pseudoId);
2049 auto const sharesKeylet = keylet::mptokenIssuance(sharesMptId);
2050 auto sleShares = std::make_shared<SLE>(sharesKeylet);
2051 auto const sharesPage = ac.view().dirInsert(
2052 keylet::ownerDir(pseudoId), sharesKeylet, describeOwnerDir(pseudoId));
2053 sleShares->setFieldU64(sfOwnerNode, *sharesPage);
2054
2055 sleShares->at(sfFlags) = 0;
2056 sleShares->at(sfIssuer) = pseudoId;
2057 sleShares->at(sfOutstandingAmount) = 0;
2058 sleShares->at(sfSequence) = sequence;
2059
2060 // sleVault->at(sfAccount) = pseudoId;
2061 // Setting wrong pseudo account ID
2062 sleVault->at(sfAccount) = a2.id();
2063 sleVault->at(sfFlags) = 0;
2064 sleVault->at(sfSequence) = sequence;
2065 sleVault->at(sfOwner) = a1.id();
2066 sleVault->at(sfAssetsTotal) = Number(0);
2067 sleVault->at(sfAssetsAvailable) = Number(0);
2068 sleVault->at(sfLossUnrealized) = Number(0);
2069 sleVault->at(sfShareMPTID) = sharesMptId;
2070 sleVault->at(sfWithdrawalPolicy) = kVaultStrategyFirstComeFirstServe;
2071
2072 ac.view().insert(sleVault);
2073 ac.view().insert(sleShares);
2074 return true;
2075 },
2076 XRPAmount{},
2077 STTx{ttVAULT_CREATE, [](STObject&) {}},
2079
2080 doInvariantCheck(
2081 {"shares issuer and vault pseudo-account must be the same", "shares issuer must exist"},
2082 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2083 auto const sequence = ac.view().seq();
2084 auto const vaultKeylet = keylet::vault(a1.id(), SeqProxy::rawSequence(sequence));
2085 auto sleVault = std::make_shared<SLE>(vaultKeylet);
2086 auto const vaultPage = ac.view().dirInsert(
2087 keylet::ownerDir(a1.id()), sleVault->key(), describeOwnerDir(a1.id()));
2088 sleVault->setFieldU64(sfOwnerNode, *vaultPage);
2089
2090 auto const sharesMptId = makeMptID(sequence, a2.id());
2091 auto const sharesKeylet = keylet::mptokenIssuance(sharesMptId);
2092 auto sleShares = std::make_shared<SLE>(sharesKeylet);
2093 auto const sharesPage = ac.view().dirInsert(
2094 keylet::ownerDir(a2.id()), sharesKeylet, describeOwnerDir(a2.id()));
2095 sleShares->setFieldU64(sfOwnerNode, *sharesPage);
2096
2097 sleShares->at(sfFlags) = 0;
2098 // Setting wrong pseudo account ID
2099 sleShares->at(sfIssuer) = AccountID(42);
2100 sleShares->at(sfOutstandingAmount) = 0;
2101 sleShares->at(sfSequence) = sequence;
2102
2103 sleVault->at(sfAccount) = a2.id();
2104 sleVault->at(sfFlags) = 0;
2105 sleVault->at(sfSequence) = sequence;
2106 sleVault->at(sfOwner) = a1.id();
2107 sleVault->at(sfAssetsTotal) = Number(0);
2108 sleVault->at(sfAssetsAvailable) = Number(0);
2109 sleVault->at(sfLossUnrealized) = Number(0);
2110 sleVault->at(sfShareMPTID) = sharesMptId;
2111 sleVault->at(sfWithdrawalPolicy) = kVaultStrategyFirstComeFirstServe;
2112
2113 ac.view().insert(sleVault);
2114 ac.view().insert(sleShares);
2115 return true;
2116 },
2117 XRPAmount{},
2118 STTx{ttVAULT_CREATE, [](STObject&) {}},
2120
2121 testcase << "Vault deposit";
2122 doInvariantCheck(
2123 {"deposit must change vault balance"},
2124 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2125 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
2126 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [](Adjustments& sample) {
2127 sample.vaultAssets.reset();
2128 }));
2129 },
2130 XRPAmount{},
2131 STTx{ttVAULT_DEPOSIT, [](STObject&) {}},
2133 precloseXrp);
2134
2135 doInvariantCheck(
2136 {"deposit assets outstanding must not exceed assets maximum"},
2137 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2138 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
2139 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 200, [&](Adjustments& sample) {
2140 sample.assetsMaximum = 1;
2141 }));
2142 },
2143 XRPAmount{},
2144 STTx{
2145 ttVAULT_DEPOSIT, [](STObject& tx) { tx.setFieldAmount(sfAmount, XRPAmount(200)); }},
2147 precloseXrp,
2148 TxAccount::A2);
2149
2150 // This really convoluted unit tests makes the zero balance on the
2151 // depositor, by sending them the same amount as the transaction fee.
2152 // The operation makes no sense, but the defensive check in
2153 // ValidVault::finalize is otherwise impossible to trigger.
2154 doInvariantCheck(
2155 {"deposit must increase vault balance", "deposit must change depositor balance"},
2156 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2157 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
2158
2159 // Move 10 drops to A4 to enforce total XRP balance
2160 auto sleA4 = ac.view().peek(keylet::account(a4.id()));
2161 if (!sleA4)
2162 return false;
2163 (*sleA4)[sfBalance] = *(*sleA4)[sfBalance] + 10;
2164 ac.view().update(sleA4);
2165
2166 return kAdjust(ac.view(), keylet, kArgs(a3.id(), -10, [&](Adjustments& sample) {
2167 sample.accountAssets->amount = -100;
2168 }));
2169 },
2170 XRPAmount{100},
2171 STTx{
2172 ttVAULT_DEPOSIT,
2173 [&](STObject& tx) {
2174 tx[sfFee] = XRPAmount(100);
2175 tx[sfAccount] = a3.id();
2176 }},
2178 precloseXrp);
2179
2180 doInvariantCheck(
2181 {"deposit must increase vault balance",
2182 "deposit must decrease depositor balance",
2183 "deposit must change vault and depositor balance by equal amount",
2184 "deposit and assets outstanding must add up",
2185 "deposit and assets available must add up"},
2186 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2187 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
2188
2189 // Move 10 drops from A2 to A3 to enforce total XRP balance
2190 auto sleA3 = ac.view().peek(keylet::account(a3.id()));
2191 if (!sleA3)
2192 return false;
2193 (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] + 10;
2194 ac.view().update(sleA3);
2195
2196 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
2197 sample.vaultAssets = -20;
2198 sample.accountAssets->amount = 10;
2199 }));
2200 },
2201 XRPAmount{},
2202 STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
2204 precloseXrp,
2205 TxAccount::A2);
2206
2207 doInvariantCheck(
2208 {"deposit must change depositor balance"},
2209 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2210 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
2211
2212 // Move 10 drops from A3 to vault to enforce total XRP balance
2213 auto sleA3 = ac.view().peek(keylet::account(a3.id()));
2214 if (!sleA3)
2215 return false;
2216 (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] - 10;
2217 ac.view().update(sleA3);
2218
2219 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
2220 sample.accountAssets->amount = 0;
2221 }));
2222 },
2223 XRPAmount{},
2224 STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
2226 precloseXrp,
2227 TxAccount::A2);
2228
2229 doInvariantCheck(
2230 {"deposit must change depositor shares"},
2231 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2232 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
2233 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
2234 sample.accountShares.reset();
2235 }));
2236 },
2237 XRPAmount{},
2238 STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
2240 precloseXrp,
2241 TxAccount::A2);
2242
2243 doInvariantCheck(
2244 {"deposit must change vault shares"},
2245 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2246 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
2247
2248 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [](Adjustments& sample) {
2249 sample.sharesTotal = 0;
2250 }));
2251 },
2252 XRPAmount{},
2253 STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
2255 precloseXrp,
2256 TxAccount::A2);
2257
2258 doInvariantCheck(
2259 {"deposit must increase depositor shares",
2260 "deposit must change depositor and vault shares by equal amount",
2261 "deposit must not change vault balance by more than deposited "
2262 "amount"},
2263 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2264 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
2265 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
2266 sample.accountShares->amount = -5;
2267 sample.sharesTotal = -10;
2268 }));
2269 },
2270 XRPAmount{},
2271 STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(5); }},
2273 precloseXrp,
2274 TxAccount::A2);
2275
2276 doInvariantCheck(
2277 {"deposit and assets outstanding must add up"},
2278 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2279 auto sleA3 = ac.view().peek(keylet::account(a3.id()));
2280 (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] - 2000;
2281 ac.view().update(sleA3);
2282
2283 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
2284 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
2285 sample.assetsTotal = 11;
2286 }));
2287 },
2288 XRPAmount{2000},
2289 STTx{
2290 ttVAULT_DEPOSIT,
2291 [&](STObject& tx) {
2292 tx[sfAmount] = XRPAmount(10);
2293 tx[sfDelegate] = a3.id();
2294 tx[sfFee] = XRPAmount(2000);
2295 }},
2297 precloseXrp,
2298 TxAccount::A2);
2299
2300 doInvariantCheck(
2301 {"deposit and assets outstanding must add up",
2302 "deposit and assets available must add up"},
2303 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2304 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
2305 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
2306 sample.assetsTotal = 7;
2307 sample.assetsAvailable = 7;
2308 }));
2309 },
2310 XRPAmount{},
2311 STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
2313 precloseXrp,
2314 TxAccount::A2);
2315
2316 testcase << "Vault withdrawal";
2317 doInvariantCheck(
2318 {"withdrawal must change vault balance"},
2319 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2320 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
2321 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [](Adjustments& sample) {
2322 sample.vaultAssets.reset();
2323 }));
2324 },
2325 XRPAmount{},
2326 STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
2328 precloseXrp);
2329
2330 // Almost identical to the really convoluted test for deposit, where the
2331 // depositor spends only the transaction fee. In case of withdrawal,
2332 // this test is almost the same as normal withdrawal where the
2333 // sfDestination would have been A4, but has been omitted.
2334 doInvariantCheck(
2335 {"withdrawal must change one destination balance"},
2336 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2337 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
2338
2339 // Move 10 drops to A4 to enforce total XRP balance
2340 auto sleA4 = ac.view().peek(keylet::account(a4.id()));
2341 if (!sleA4)
2342 return false;
2343 (*sleA4)[sfBalance] = *(*sleA4)[sfBalance] + 10;
2344 ac.view().update(sleA4);
2345
2346 return kAdjust(ac.view(), keylet, kArgs(a3.id(), -10, [&](Adjustments& sample) {
2347 sample.accountAssets->amount = -100;
2348 }));
2349 },
2350 XRPAmount{100},
2351 STTx{
2352 ttVAULT_WITHDRAW,
2353 [&](STObject& tx) {
2354 tx[sfFee] = XRPAmount(100);
2355 tx[sfAccount] = a3.id();
2356 // This commented out line causes the invariant violation.
2357 // tx[sfDestination] = A4.id();
2358 }},
2360 precloseXrp);
2361
2362 doInvariantCheck(
2363 {
2364 "withdrawal must change vault and destination balance by equal amount",
2365 "withdrawal must decrease vault balance",
2366 "withdrawal must increase destination balance",
2367 "withdrawal and assets outstanding must add up",
2368 "withdrawal and assets available must add up",
2369 },
2370 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2371 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
2372
2373 // Move 10 drops from A2 to A3 to enforce total XRP balance
2374 auto sleA3 = ac.view().peek(keylet::account(a3.id()));
2375 if (!sleA3)
2376 return false;
2377 (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] + 10;
2378 ac.view().update(sleA3);
2379
2380 return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
2381 sample.vaultAssets = 10;
2382 sample.accountAssets->amount = -20;
2383 }));
2384 },
2385 XRPAmount{},
2386 STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
2388 precloseXrp,
2389 TxAccount::A2);
2390
2391 doInvariantCheck(
2392 {"withdrawal must change one destination balance"},
2393 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2394 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
2395 if (!kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
2396 *sample.vaultAssets -= 5;
2397 })))
2398 return false;
2399 auto sleA3 = ac.view().peek(keylet::account(a3.id()));
2400 if (!sleA3)
2401 return false;
2402 (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] + 5;
2403 ac.view().update(sleA3);
2404 return true;
2405 },
2406 XRPAmount{},
2407 STTx{ttVAULT_WITHDRAW, [&](STObject& tx) { tx.setAccountID(sfDestination, a3.id()); }},
2409 precloseXrp,
2410 TxAccount::A2);
2411
2412 doInvariantCheck(
2413 {"withdrawal must change depositor shares"},
2414 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2415 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
2416 return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
2417 sample.accountShares.reset();
2418 }));
2419 },
2420 XRPAmount{},
2421 STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
2423 precloseXrp,
2424 TxAccount::A2);
2425
2426 doInvariantCheck(
2427 {"withdrawal must change vault shares"},
2428 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2429 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
2430 return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [](Adjustments& sample) {
2431 sample.sharesTotal = 0;
2432 }));
2433 },
2434 XRPAmount{},
2435 STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
2437 precloseXrp,
2438 TxAccount::A2);
2439
2440 doInvariantCheck(
2441 {"withdrawal must decrease depositor shares",
2442 "withdrawal must change depositor and vault shares by equal "
2443 "amount"},
2444 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2445 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
2446 return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
2447 sample.accountShares->amount = 5;
2448 sample.sharesTotal = 10;
2449 }));
2450 },
2451 XRPAmount{},
2452 STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
2454 precloseXrp,
2455 TxAccount::A2);
2456
2457 doInvariantCheck(
2458 {"withdrawal and assets outstanding must add up",
2459 "withdrawal and assets available must add up"},
2460 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2461 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
2462 return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
2463 sample.assetsTotal = -15;
2464 sample.assetsAvailable = -15;
2465 }));
2466 },
2467 XRPAmount{},
2468 STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
2470 precloseXrp,
2471 TxAccount::A2);
2472
2473 doInvariantCheck(
2474 {"withdrawal and assets outstanding must add up"},
2475 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2476 auto sleA3 = ac.view().peek(keylet::account(a3.id()));
2477 (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] - 2000;
2478 ac.view().update(sleA3);
2479
2480 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
2481 return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
2482 sample.assetsTotal = -7;
2483 }));
2484 },
2485 XRPAmount{2000},
2486 STTx{
2487 ttVAULT_WITHDRAW,
2488 [&](STObject& tx) {
2489 tx[sfAmount] = XRPAmount(10);
2490 tx[sfDelegate] = a3.id();
2491 tx[sfFee] = XRPAmount(2000);
2492 }},
2494 precloseXrp,
2495 TxAccount::A2);
2496
2497 auto const precloseMpt = [&](Account const& a1, Account const& a2, Env& env) -> bool {
2498 env.fund(XRP(1000), a3, a4);
2499
2500 // Create MPT asset
2501 {
2502 json::Value jv;
2503 jv[sfAccount] = a3.human();
2504 jv[sfTransactionType] = jss::MPTokenIssuanceCreate;
2505 jv[sfFlags] = tfMPTCanTransfer;
2506 env(jv);
2507 env.close();
2508 }
2509
2510 auto const mptID = makeMptID(env.seq(a3) - 1, a3);
2511 Asset const asset = MPTIssue(mptID);
2512 // Authorize A1 A2 A4
2513 {
2514 json::Value jv;
2515 jv[sfAccount] = a1.human();
2516 jv[sfTransactionType] = jss::MPTokenAuthorize;
2517 jv[sfMPTokenIssuanceID] = to_string(mptID);
2518 env(jv);
2519 jv[sfAccount] = a2.human();
2520 env(jv);
2521 jv[sfAccount] = a4.human();
2522 env(jv);
2523
2524 env.close();
2525 }
2526 // Send tokens to A1 A2 A4
2527 {
2528 env(pay(a3, a1, asset(1000)));
2529 env(pay(a3, a2, asset(1000)));
2530 env(pay(a3, a4, asset(1000)));
2531 env.close();
2532 }
2533
2534 Vault const vault{env};
2535 auto [tx, keylet] = vault.create({.owner = a1, .asset = asset});
2536 env(tx);
2537 env(vault.deposit({.depositor = a1, .id = keylet.key, .amount = asset(10)}));
2538 env(vault.deposit({.depositor = a2, .id = keylet.key, .amount = asset(10)}));
2539 env(vault.deposit({.depositor = a4, .id = keylet.key, .amount = asset(10)}));
2540 return true;
2541 };
2542
2543 doInvariantCheck(
2544 {"withdrawal must decrease depositor shares",
2545 "withdrawal must change depositor and vault shares by equal "
2546 "amount"},
2547 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2548 auto const keylet =
2549 keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2));
2550 return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
2551 sample.accountShares->amount = 5;
2552 }));
2553 },
2554 XRPAmount{},
2555 STTx{ttVAULT_WITHDRAW, [&](STObject& tx) { tx[sfAccount] = a3.id(); }},
2557 precloseMpt,
2558 TxAccount::A2);
2559
2560 testcase << "Vault clawback";
2561 doInvariantCheck(
2562 {"clawback must change vault balance"},
2563 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2564 auto const keylet =
2565 keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2));
2566 return kAdjust(ac.view(), keylet, kArgs(a2.id(), -1, [&](Adjustments& sample) {
2567 sample.vaultAssets.reset();
2568 }));
2569 },
2570 XRPAmount{},
2571 STTx{ttVAULT_CLAWBACK, [&](STObject& tx) { tx[sfAccount] = a3.id(); }},
2573 precloseMpt);
2574
2575 // Not the same as below check: attempt to clawback XRP
2576 doInvariantCheck(
2577 {"clawback may only be performed by the asset issuer"},
2578 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2579 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
2580 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {}));
2581 },
2582 XRPAmount{},
2583 STTx{ttVAULT_CLAWBACK, [](STObject&) {}},
2585 precloseXrp);
2586
2587 // Not the same as above check: attempt to clawback MPT by bad account
2588 doInvariantCheck(
2589 {"clawback may only be performed by the asset issuer"},
2590 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2591 auto const keylet =
2592 keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2));
2593 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {}));
2594 },
2595 XRPAmount{},
2596 STTx{ttVAULT_CLAWBACK, [&](STObject& tx) { tx[sfAccount] = a4.id(); }},
2598 precloseMpt);
2599
2600 doInvariantCheck(
2601 {"clawback must decrease vault balance",
2602 "clawback must decrease holder shares",
2603 "clawback must change vault shares"},
2604 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2605 auto const keylet =
2606 keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2));
2607 return kAdjust(ac.view(), keylet, kArgs(a4.id(), 10, [&](Adjustments& sample) {
2608 sample.sharesTotal = 0;
2609 }));
2610 },
2611 XRPAmount{},
2612 STTx{
2613 ttVAULT_CLAWBACK,
2614 [&](STObject& tx) {
2615 tx[sfAccount] = a3.id();
2616 tx[sfHolder] = a4.id();
2617 }},
2619 precloseMpt);
2620
2621 doInvariantCheck(
2622 {"clawback must change holder shares"},
2623 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2624 auto const keylet =
2625 keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2));
2626 return kAdjust(ac.view(), keylet, kArgs(a4.id(), -10, [&](Adjustments& sample) {
2627 sample.accountShares.reset();
2628 }));
2629 },
2630 XRPAmount{},
2631 STTx{
2632 ttVAULT_CLAWBACK,
2633 [&](STObject& tx) {
2634 tx[sfAccount] = a3.id();
2635 tx[sfHolder] = a4.id();
2636 }},
2638 precloseMpt);
2639
2640 doInvariantCheck(
2641 {"clawback must change holder and vault shares by equal amount",
2642 "clawback and assets outstanding must add up",
2643 "clawback and assets available must add up"},
2644 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2645 auto const keylet =
2646 keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2));
2647 return kAdjust(ac.view(), keylet, kArgs(a4.id(), -10, [&](Adjustments& sample) {
2648 sample.accountShares->amount = -8;
2649 sample.assetsTotal = -7;
2650 sample.assetsAvailable = -7;
2651 }));
2652 },
2653 XRPAmount{},
2654 STTx{
2655 ttVAULT_CLAWBACK,
2656 [&](STObject& tx) {
2657 tx[sfAccount] = a3.id();
2658 tx[sfHolder] = a4.id();
2659 }},
2661 precloseMpt);
2662
2663 // ─────────────────────────────────────────────────────────────
2664 // Closed-ended vault invariants added in ValidVault::finalize (create must supply both
2665 // dates and satisfy the redemption-buffer gap), deposit only in Subscription / NoPhase,
2666 // withdraw not in Investment, loan origination only in Investment.
2667
2668 using D = NetClock::duration;
2669 using Tp = NetClock::time_point;
2670
2671 auto const closedEnded = std::to_underlying(VaultKind::ClosedEnded);
2672
2673 // Vault keylet captured by precloseClosedEnded so precheck does not have to rederive it
2674 // from ac.view().seq(), which depends on how many env.close() calls preclose issued.
2675 Keylet closedEndedKeylet = keylet::amendments();
2676
2677 // Preclose that creates a closed-ended vault (in Subscription), optionally seeds it with
2678 // three deposits (so a1/a2/a3 hold a share MPToken that kAdjust can then adjust), and
2679 // optionally advances parent close time past SubscriptionDate. A negative @p advanceBySub
2680 // leaves the vault in Subscription.
2681 auto const precloseClosedEnded = [&](std::int32_t advanceBySub, bool doDeposit) {
2682 return [&, advanceBySub, doDeposit](
2683 Account const& a1, Account const& a2, Env& env) -> bool {
2684 env.fund(XRP(1000), a3, a4);
2685 auto const sub = env.now().time_since_epoch().count() + 60;
2686 auto const red = sub + kMinInvestmentPeriod + 1'000'000;
2687 Vault const vault{env};
2688 auto [tx, keylet] = vault.create(
2689 {.owner = a1,
2690 .asset = xrpIssue(),
2691 .vaultKind = closedEnded,
2692 .subscriptionDate = sub,
2693 .redemptionDate = red});
2694 env(tx);
2695 closedEndedKeylet = keylet;
2696 if (doDeposit)
2697 {
2698 env(vault.deposit({.depositor = a1, .id = keylet.key, .amount = XRP(10)}));
2699 env(vault.deposit({.depositor = a2, .id = keylet.key, .amount = XRP(10)}));
2700 env(vault.deposit({.depositor = a3, .id = keylet.key, .amount = XRP(10)}));
2701 }
2702 if (advanceBySub >= 0)
2703 env.close(Tp{D{sub + advanceBySub}});
2704 return true;
2705 };
2706 };
2707
2708 // Manually insert a bare closed-ended vault (+ pseudo-account + share MPTokenIssuance)
2709 // directly into the view, bypassing the transactor path. Used to synthesize ttVAULT_CREATE
2710 // states no legitimate transactor would produce.
2711 auto const insertBareClosedEndedVault =
2712 [closedEnded](
2713 ApplyContext& ac,
2714 Account const& owner,
2715 std::optional<std::uint32_t> subscriptionDate,
2716 std::optional<std::uint32_t> redemptionDate) -> bool {
2717 auto const sequence = ac.view().seq();
2718 auto const vaultKeylet = keylet::vault(owner.id(), SeqProxy::rawSequence(sequence));
2719 auto sleVault = std::make_shared<SLE>(vaultKeylet);
2720 auto const vaultPage = ac.view().dirInsert(
2721 keylet::ownerDir(owner.id()), sleVault->key(), describeOwnerDir(owner.id()));
2722 if (!vaultPage)
2723 return false;
2724 sleVault->setFieldU64(sfOwnerNode, *vaultPage);
2725
2726 auto const pseudoId = pseudoAccountAddress(ac.view(), vaultKeylet.key);
2727 auto sleAccount = std::make_shared<SLE>(keylet::account(pseudoId));
2728 sleAccount->setAccountID(sfAccount, pseudoId);
2729 sleAccount->setFieldAmount(sfBalance, STAmount{});
2730 sleAccount->setFieldU32(sfSequence, 0);
2731 sleAccount->setFieldU32(sfFlags, lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth);
2732 sleAccount->setFieldH256(sfVaultID, vaultKeylet.key);
2733 ac.view().insert(sleAccount);
2734
2735 auto const sharesMptId = makeMptID(sequence, pseudoId);
2736 auto const sharesKeylet = keylet::mptokenIssuance(sharesMptId);
2737 auto sleShares = std::make_shared<SLE>(sharesKeylet);
2738 auto const sharesPage = ac.view().dirInsert(
2739 keylet::ownerDir(pseudoId), sharesKeylet, describeOwnerDir(pseudoId));
2740 if (!sharesPage)
2741 return false;
2742 sleShares->setFieldU64(sfOwnerNode, *sharesPage);
2743 sleShares->at(sfFlags) = 0;
2744 sleShares->at(sfIssuer) = pseudoId;
2745 sleShares->at(sfOutstandingAmount) = 0;
2746 sleShares->at(sfSequence) = sequence;
2747
2748 sleVault->at(sfAccount) = pseudoId;
2749 sleVault->at(sfFlags) = 0;
2750 sleVault->at(sfSequence) = sequence;
2751 sleVault->at(sfOwner) = owner.id();
2752 sleVault->setFieldIssue(sfAsset, STIssue{sfAsset, Asset{xrpIssue()}});
2753 sleVault->at(sfAssetsTotal) = Number(0);
2754 sleVault->at(sfAssetsAvailable) = Number(0);
2755 sleVault->at(sfLossUnrealized) = Number(0);
2756 sleVault->at(sfShareMPTID) = sharesMptId;
2757 sleVault->at(sfWithdrawalPolicy) = kVaultStrategyFirstComeFirstServe;
2758 sleVault->at(sfVaultKind) = closedEnded;
2759 if (subscriptionDate)
2760 sleVault->at(sfSubscriptionDate) = *subscriptionDate;
2761 if (redemptionDate)
2762 sleVault->at(sfRedemptionDate) = *redemptionDate;
2763
2764 ac.view().insert(sleVault);
2765 ac.view().insert(sleShares);
2766 return true;
2767 };
2768
2769 testcase << "Vault create closed-ended";
2770
2771 // A fresh closed-ended vault must carry both SubscriptionDate and RedemptionDate.
2772 doInvariantCheck(
2773 {"closed-ended vault must have SubscriptionDate and RedemptionDate"},
2774 [&](Account const& a1, Account const&, ApplyContext& ac) {
2775 return insertBareClosedEndedVault(ac, a1, std::nullopt, std::nullopt);
2776 },
2777 XRPAmount{},
2778 STTx{ttVAULT_CREATE, [](STObject&) {}},
2780
2781 // Gap smaller than MIN_INVESTMENT_PERIOD but with RedemptionDate > SubscriptionDate;
2782 // exercises the sub-minimum branch of the gap check.
2783 doInvariantCheck(
2784 {"closed-ended vault RedemptionDate - SubscriptionDate must be "
2785 "within [MIN_INVESTMENT_PERIOD, MAX_INVESTMENT_PERIOD)"},
2786 [&](Account const& a1, Account const&, ApplyContext& ac) {
2787 std::uint32_t const sub = 1'000'000'000;
2788 std::uint32_t const red = sub + kMinInvestmentPeriod - 1;
2789 return insertBareClosedEndedVault(ac, a1, sub, red);
2790 },
2791 XRPAmount{},
2792 STTx{ttVAULT_CREATE, [](STObject&) {}},
2794
2795 // RedemptionDate strictly before SubscriptionDate; the signed int64 gap is negative and
2796 // is caught by the sub-minimum branch of the gap check.
2797 doInvariantCheck(
2798 {"closed-ended vault RedemptionDate - SubscriptionDate must be "
2799 "within [MIN_INVESTMENT_PERIOD, MAX_INVESTMENT_PERIOD)"},
2800 [&](Account const& a1, Account const&, ApplyContext& ac) {
2801 std::uint32_t const sub = 1'000'000'000;
2802 std::uint32_t const red = sub - 1;
2803 return insertBareClosedEndedVault(ac, a1, sub, red);
2804 },
2805 XRPAmount{},
2806 STTx{ttVAULT_CREATE, [](STObject&) {}},
2808
2809 // Gap exactly MAX_INVESTMENT_PERIOD is out of range (bound is half-open on the right).
2810 doInvariantCheck(
2811 {"closed-ended vault RedemptionDate - SubscriptionDate must be "
2812 "within [MIN_INVESTMENT_PERIOD, MAX_INVESTMENT_PERIOD)"},
2813 [&](Account const& a1, Account const&, ApplyContext& ac) {
2814 std::uint32_t const sub = 1'000'000'000;
2815 std::uint32_t const red = sub + kMaxInvestmentPeriod;
2816 return insertBareClosedEndedVault(ac, a1, sub, red);
2817 },
2818 XRPAmount{},
2819 STTx{ttVAULT_CREATE, [](STObject&) {}},
2821
2822 testcase << "Vault deposit closed-ended";
2823
2824 // A deposit into a closed-ended vault that has advanced past SubscriptionDate. kArgs
2825 // simulates an otherwise valid deposit shape so only the phase invariant fires.
2826 doInvariantCheck(
2827 {"deposit only allowed in Subscription or NoPhase"},
2828 [&](Account const&, Account const& a2, ApplyContext& ac) {
2829 return kAdjust(
2830 ac.view(), closedEndedKeylet, kArgs(a2.id(), 10, [](Adjustments&) {}));
2831 },
2832 XRPAmount{},
2833 STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
2835 precloseClosedEnded(/*advanceBySub=*/1, /*doDeposit=*/true),
2836 TxAccount::A2);
2837
2838 testcase << "Vault withdrawal closed-ended";
2839
2840 // A withdrawal from a closed-ended vault in the Investment phase.
2841 doInvariantCheck(
2842 {"withdrawal not allowed during Investment phase"},
2843 [&](Account const&, Account const& a2, ApplyContext& ac) {
2844 return kAdjust(
2845 ac.view(), closedEndedKeylet, kArgs(a2.id(), -10, [](Adjustments&) {}));
2846 },
2847 XRPAmount{},
2848 STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
2850 precloseClosedEnded(/*advanceBySub=*/1, /*doDeposit=*/true),
2851 TxAccount::A2);
2852
2853 testcase << "Vault loan set";
2854
2855 // ttLOAN_SET against a closed-ended vault that is not in Investment. finalizeLoanSet fires
2856 // on any vault mutation; touching the vault SLE with no field change is sufficient.
2857 doInvariantCheck(
2858 {"loan origination only allowed in Investment phase"},
2859 [&](Account const&, Account const&, ApplyContext& ac) {
2860 auto sleVault = ac.view().peek(closedEndedKeylet);
2861 if (!sleVault)
2862 return false;
2863 ac.view().update(sleVault);
2864 return true;
2865 },
2866 XRPAmount{},
2867 STTx{ttLOAN_SET, [](STObject&) {}},
2869 precloseClosedEnded(/*advanceBySub=*/-1, /*doDeposit=*/false));
2870
2871 testcase << "Vault loan set - closed-ended final payment past "
2872 "RedemptionDate";
2873
2874 // A newly-created loan against a closed-ended vault must satisfy StartDate +
2875 // PaymentInterval * PaymentRemaining + kLoanRedemptionBuffer <= RedemptionDate.
2876 // LoanSet::preclaim enforces the same bound; this test synthesises a loan whose
2877 // final payment is still before RedemptionDate (so the old unbuffered check would
2878 // pass) but inside the buffer zone.
2879 Keylet closedEndedBrokerKeylet = keylet::amendments();
2880 std::uint32_t closedEndedRed = 0;
2881 doInvariantCheck(
2882 {"closed-ended loan final payment must precede RedemptionDate by at least "
2883 "kLoanRedemptionBuffer"},
2884 [&](Account const& a1, Account const&, ApplyContext& ac) {
2885 // Touch the vault so ValidVault::finalizeLoanSet sees an
2886 // entry in afterVault_; the vault is in Investment, so
2887 // finalizeLoanSet itself passes.
2888 auto sleVault = ac.view().peek(closedEndedKeylet);
2889 if (!sleVault)
2890 return false;
2891 ac.view().update(sleVault);
2892
2893 // Read the broker's next loan sequence to build the loan
2894 // keylet the same way LoanSet::doApply would.
2895 auto sleBroker = ac.view().peek(closedEndedBrokerKeylet);
2896 if (!sleBroker)
2897 return false;
2898 std::uint32_t const loanSeq = sleBroker->at(sfLoanSequence);
2899
2900 // Final payment at RedemptionDate - (kLoanRedemptionBuffer - 1): still
2901 // strictly before RedemptionDate, but inside the buffer.
2902 auto sleLoan = makeLoanSle(closedEndedBrokerKeylet.key, loanSeq, a1.id());
2903 sleLoan->at(sfLoanBrokerID) = closedEndedBrokerKeylet.key;
2904 sleLoan->at(sfLoanSequence) = loanSeq;
2905 sleLoan->at(sfBorrower) = a1.id();
2906 sleLoan->at(sfStartDate) = closedEndedRed - kLoanRedemptionBuffer;
2907 sleLoan->at(sfPaymentInterval) = 1;
2908 sleLoan->at(sfPaymentRemaining) = 1;
2909 sleLoan->at(sfTotalValueOutstanding) = Number(100);
2910 sleLoan->at(sfPeriodicPayment) = Number(1);
2911 ac.view().insert(sleLoan);
2912 return true;
2913 },
2914 XRPAmount{},
2915 STTx{ttLOAN_SET, [](STObject&) {}},
2917 [&](Account const& a1, Account const&, Env& env) -> bool {
2918 auto const sub = env.now().time_since_epoch().count() + 60;
2919 auto const red = sub + kMinInvestmentPeriod + 1'000'000;
2920 closedEndedRed = red;
2921
2922 Vault const vault{env};
2923 auto [tx, keylet] = vault.create(
2924 {.owner = a1,
2925 .asset = xrpIssue(),
2926 .vaultKind = closedEnded,
2927 .subscriptionDate = sub,
2928 .redemptionDate = red});
2929 env(tx);
2930 closedEndedKeylet = keylet;
2931
2932 // Create the loan broker; LoanBrokerSet has no phase gate.
2933 closedEndedBrokerKeylet =
2934 keylet::loanBroker(a1.id(), SeqProxy::rawSequence(env.seq(a1)));
2935 env(loan_broker::set(a1, keylet.key));
2936
2937 // Advance parent close time into Investment so
2938 // ValidVault::finalizeLoanSet is satisfied.
2939 env.close(Tp{D{sub + 1}});
2940 return true;
2941 });
2942 }
2943
2944 // Minimal impaired-loan setup for testVaultLossExceedsGap. Kept
2945 // inline here so this file has no dependency on LoanTestBase.
2946 Keylet
2948 test::jtx::Account const& owner,
2949 test::jtx::Account const& borrower,
2950 test::jtx::Account const& issuer,
2951 test::jtx::Env& env)
2952 {
2953 using namespace test::jtx;
2954
2955 env.fund(XRP(1'000'000), issuer, borrower);
2956 env.close();
2957
2958 PrettyAsset const usd = issuer["USD"];
2959 STAmount const trustLimit{usd.raw(), Number{9'999'999'999'999'999LL}};
2960 env(trust(owner, trustLimit));
2961 env(trust(borrower, trustLimit));
2962 env.close();
2963
2964 env(pay(issuer, owner, usd(100'000)));
2965 env(pay(issuer, borrower, usd(1'000)));
2966 env.close();
2967
2968 // Under featureLendingProtocolV1_1 LoanBrokerSet::preclaim only
2969 // accepts closed-ended vaults. The 10-year investment window
2970 // covers this helper's 120 monthly payments so LoanSet's
2971 // RedemptionDate bound is satisfied.
2972 Vault const vault{env};
2973 auto [vaultTx, vaultKeylet, subscriptionDate] = vault.createClosedEnded(
2974 {.owner = owner,
2975 .asset = usd,
2976 .subscriptionOffset = std::chrono::seconds{60},
2977 .investmentWindow = std::chrono::seconds{10ull * 365ull * 24ull * 60ull * 60ull}});
2978 env(vaultTx);
2979 env.close();
2980
2981 env(vault.deposit(
2982 {.depositor = owner, .id = vaultKeylet.key, .amount = usd(1'000).value()}));
2983 env.close();
2984
2985 auto const brokerKeylet =
2986 keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
2987
2988 {
2989 using namespace loan_broker;
2990 env(set(owner, vaultKeylet.key),
2991 kCoverRateMinimum(percentageToTenthBips(1)),
2992 kCoverRateLiquidation(xrpl::lending::kMaxCoverRate),
2993 Fee(env.current()->fees().base * 2));
2994 env.close();
2995
2996 env(coverDeposit(owner, brokerKeylet.key, usd(10'000).value()),
2997 Fee(env.current()->fees().base * 2));
2998 env.close();
2999 }
3000
3001 // LoanSet is gated on Investment; advance out of Subscription.
3002 vault.closePastSubscription(subscriptionDate);
3003
3004 auto const brokerSle = env.le(brokerKeylet);
3005 if (!BEAST_EXPECT(brokerSle))
3006 return vaultKeylet;
3007
3008 auto const loanKeylet =
3009 keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(brokerSle->at(sfLoanSequence)));
3010
3011 {
3012 using namespace loan;
3013 env(set(borrower, brokerKeylet.key, usd(100).value()),
3014 kCounterparty(owner),
3015 kInterestRate(TenthBips32{1000}),
3016 kPaymentTotal(120),
3017 kPaymentInterval(86400u * 30u),
3018 kGracePeriod(86400u * 30u),
3019 Sig(sfCounterpartySignature, owner),
3020 Fee(env.current()->fees().base * 200));
3021 env.close();
3022
3023 // Under fixCleanup3_4_0 impair requires the payment to already
3024 // be late, so advance past the loan's due date first.
3025 if (env.current()->rules().enabled(fixCleanup3_4_0))
3026 {
3027 auto const loanSle = env.le(loanKeylet);
3028 if (!BEAST_EXPECT(loanSle))
3029 return vaultKeylet;
3030 std::uint32_t const dueDate = loanSle->at(sfNextPaymentDueDate);
3031 env.close(
3033 }
3034
3035 env(manage(owner, loanKeylet.key, tfLoanImpair));
3036 env.close();
3037 }
3038
3039 return vaultKeylet;
3040 }
3041
3042 // Regression test for the loss-vs-gap invariant relaxation introduced
3043 // by fixCleanup3_4_0. Even with the one-unit tolerance, a loss value
3044 // exceeding (T - A) by more than one ULP must still fire. Two
3045 // mutations exercise this:
3046 // 1. L = (T - A) * 2 — fires under both amendment settings.
3047 // 2. L = (T - A) + 2 * oneUnit — fires post-amendment, catching
3048 // any accidental widening of the tolerance beyond one unit.
3049 void
3051 {
3052 testcase("vault loss exceeds gap (fixCleanup3_4_0 tolerance)");
3053 using namespace test::jtx;
3054
3055 auto const kExpectedLog = std::vector<std::string>{
3056 "loss unrealized must not exceed the difference between assets "
3057 "outstanding and available"};
3058
3059 for (auto const withFix : {false, true})
3060 {
3061 FeatureBitset amendments = all_;
3062 if (!withFix)
3063 amendments = amendments - fixCleanup3_4_0;
3064
3065 // Variant 1: L = (T - A) * 2. Fires under both settings.
3066 {
3067 Keylet vaultKeylet = keylet::vault(UInt256{});
3068 Account const issuer{"issuer_loss_gap"};
3069 Account const borrower{"borrower_loss_gap"};
3070
3071 auto preclose = [&, this](Account const& owner, Account const&, Env& env) -> bool {
3072 vaultKeylet = this->makeImpairedVault(owner, borrower, issuer, env);
3073 return BEAST_EXPECT(env.le(vaultKeylet));
3074 };
3075
3077 makeEnv(amendments),
3078 kExpectedLog,
3079 [&vaultKeylet](Account const&, Account const&, ApplyContext& ac) -> bool {
3080 auto sle = ac.view().peek(vaultKeylet);
3081 if (!sle)
3082 return false;
3083 Number const total = sle->at(sfAssetsTotal);
3084 Number const available = sle->at(sfAssetsAvailable);
3085 (*sle)[sfLossUnrealized] = (total - available) * 2;
3086 ac.view().update(sle);
3087 return true;
3088 },
3089 XRPAmount{},
3090 STTx{
3091 ttVAULT_DEPOSIT,
3092 [&vaultKeylet](STObject& tx) {
3093 tx.setFieldH256(sfVaultID, vaultKeylet.key);
3094 }},
3096 preclose,
3098 }
3099
3100 // Variant 2: L = (T - A) + 2 * oneUnit at scale(T). Must fire
3101 // post-fix because the tolerance is exactly one unit. A
3102 // regression that widened it to two units would silently accept
3103 // this state.
3104 {
3105 Keylet vaultKeylet = keylet::vault(UInt256{});
3106 Account const issuer{"issuer_loss_gap2"};
3107 Account const borrower{"borrower_loss_gap2"};
3108
3109 auto preclose = [&, this](Account const& owner, Account const&, Env& env) -> bool {
3110 vaultKeylet = this->makeImpairedVault(owner, borrower, issuer, env);
3111 return BEAST_EXPECT(env.le(vaultKeylet));
3112 };
3113
3115 makeEnv(amendments),
3116 kExpectedLog,
3117 [&vaultKeylet](Account const&, Account const&, ApplyContext& ac) -> bool {
3118 auto sle = ac.view().peek(vaultKeylet);
3119 if (!sle)
3120 return false;
3121 Number const total = sle->at(sfAssetsTotal);
3122 Number const available = sle->at(sfAssetsAvailable);
3123 Asset const asset = sle->at(sfAsset);
3124 Number const oneUnit{1, scale(total, asset)};
3125 (*sle)[sfLossUnrealized] = (total - available) + oneUnit * 2;
3126 ac.view().update(sle);
3127 return true;
3128 },
3129 XRPAmount{},
3130 STTx{
3131 ttVAULT_DEPOSIT,
3132 [&vaultKeylet](STObject& tx) {
3133 tx.setFieldH256(sfVaultID, vaultKeylet.key);
3134 }},
3136 preclose,
3138 }
3139 }
3140 }
3141
3142 void
3144 {
3145 using namespace jtx;
3146
3147 Account const issuer{"issuer"};
3148 PrettyAsset const vaultAsset = issuer["IOU"];
3149
3150 struct TestCase
3151 {
3152 std::string name;
3153 std::int32_t expectedMinScale;
3155 };
3156
3157 for (auto const mantissaScale : MantissaRange::getAllScales())
3158 {
3159 if (mantissaScale == MantissaRange::MantissaScale::Small)
3160 continue;
3161 NumberMantissaScaleGuard const g{mantissaScale};
3162
3163 auto makeDelta = [&vaultAsset](Number const& n) -> ValidVault::DeltaInfo {
3164 return {.delta = n, .scale = scale(n, vaultAsset.raw())};
3165 };
3166
3167 auto const testCases = std::vector<TestCase>{
3168 {
3169 .name = "No values",
3170 .expectedMinScale = 0,
3171 .values = {},
3172 },
3173 {
3174 .name = "Mixed integer and Number values",
3175 .expectedMinScale = -15,
3176 .values = {makeDelta(1), makeDelta(-1), makeDelta(Number{10, -1})},
3177 },
3178 {
3179 .name = "Mixed scales",
3180 .expectedMinScale = -17,
3181 .values =
3182 {makeDelta(Number{1, -2}),
3183 makeDelta(Number{5, -3}),
3184 makeDelta(Number{3, -2})},
3185 },
3186 {
3187 .name = "Equal scales",
3188 .expectedMinScale = -16,
3189 .values =
3190 {makeDelta(Number{1, -1}),
3191 makeDelta(Number{5, -1}),
3192 makeDelta(Number{1, -1})},
3193 },
3194 {
3195 .name = "Mixed mantissa sizes",
3196 .expectedMinScale = -12,
3197 .values =
3198 {makeDelta(Number{1}),
3199 makeDelta(Number{1234, -3}),
3200 makeDelta(Number{12345, -6}),
3201 makeDelta(Number{123, 1})},
3202 },
3203 };
3204
3205 for (auto const& tc : testCases)
3206 {
3207 testcase("vault computeCoarsestScale: " + tc.name);
3208
3209 auto const actualScale = ValidVault::computeCoarsestScale(tc.values);
3210
3211 BEAST_EXPECTS(
3212 actualScale == tc.expectedMinScale,
3213 "expected: " + std::to_string(tc.expectedMinScale) +
3214 ", actual: " + std::to_string(actualScale));
3215 for (auto const& num : tc.values)
3216 {
3217 // None of these scales are far enough apart that rounding the
3218 // values would lose information, so check that the rounded
3219 // value matches the original.
3220 auto const actualRounded = roundToAsset(vaultAsset, num.delta, actualScale);
3221 BEAST_EXPECTS(
3222 actualRounded == num.delta,
3223 "number " + to_string(num.delta) + " rounded to scale " +
3224 std::to_string(actualScale) + " is " + to_string(actualRounded));
3225 }
3226 }
3227
3228 auto const testCases2 = std::vector<TestCase>{
3229 {
3230 .name = "False equivalence",
3231 .expectedMinScale = -15,
3232 .values =
3233 {
3234 makeDelta(Number{1234567890123456789, -18}),
3235 makeDelta(Number{12345, -4}),
3236 makeDelta(Number{1}),
3237 },
3238 },
3239 };
3240
3241 // Unlike the first set of test cases, the values in these test could
3242 // look equivalent if using the wrong scale.
3243 for (auto const& tc : testCases2)
3244 {
3245 testcase("vault computeCoarsestScale: " + tc.name);
3246
3247 auto const actualScale = ValidVault::computeCoarsestScale(tc.values);
3248
3249 BEAST_EXPECTS(
3250 actualScale == tc.expectedMinScale,
3251 "expected: " + std::to_string(tc.expectedMinScale) +
3252 ", actual: " + std::to_string(actualScale));
3254 Number firstRounded;
3255 for (auto const& num : tc.values)
3256 {
3257 if (!first)
3258 {
3259 first = num.delta;
3260 firstRounded = roundToAsset(vaultAsset, num.delta, actualScale);
3261 continue;
3262 }
3263 auto const numRounded = roundToAsset(vaultAsset, num.delta, actualScale);
3264 BEAST_EXPECTS(
3265 numRounded != firstRounded,
3266 "at a scale of " + std::to_string(actualScale) + " " +
3267 to_string(num.delta) + " == " + to_string(*first));
3268 }
3269 }
3270 }
3271 }
3272
3273 void
3274 run() override
3275 {
3276 testVault();
3279 }
3280};
3281
3282BEAST_DEFINE_TESTSUITE(InvariantsVault, app, xrpl);
3283
3284} // namespace xrpl::test
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
State information when applying a tx.
ApplyView & view()
Writeable view to a ledger, for applying a transaction.
Definition ApplyView.h:141
virtual SLE::pointer peek(Keylet const &k)=0
Prepare to modify the SLE associated with key.
virtual void insert(SLE::Ref sle)=0
Insert a new state SLE.
std::optional< std::uint64_t > dirInsert(Keylet const &directory, UInt256 const &key, std::function< void(SLE::Ref)> const &describe)
Insert an entry to a directory.
Definition ApplyView.h:373
virtual void erase(SLE::Ref sle)=0
Remove a peeked SLE.
virtual void update(SLE::Ref sle)=0
Indicate changes to a peeked SLE.
std::chrono::time_point< NetClock > time_point
Definition chrono.h:48
std::chrono::duration< rep, period > duration
Definition chrono.h:47
Sets the new scale and restores the old scale when it leaves scope.
Definition Number.h:963
Number is a floating point type that can represent a wide range of values.
Definition Number.h:351
LedgerIndex seq() const
Returns the sequence number of the base ledger.
Definition ReadView.h:115
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
Definition SeqProxy.h:62
static std::int32_t computeCoarsestScale(std::vector< DeltaInfo > const &numbers)
Keylet createLoanBroker(jtx::Account const &a, jtx::Env &env, jtx::PrettyAsset const &asset)
test::jtx::Env makeEnv(FeatureBitset features)
void doInvariantCheck(std::vector< std::string > const &expectLogs, Precheck const &precheck, XRPAmount fee=XRPAmount{}, STTx tx=STTx{ttACCOUNT_SET, [](STObject &) {}}, std::initializer_list< TER > ters={tecINVARIANT_FAILED, tefINVARIANT_FAILED}, Preclose const &preclose={}, TxAccount setTxAccount=TxAccount::None, std::source_location const &loc=std::source_location::current(), TER initialResult=tesSUCCESS)
Run a specific test case to put the ledger into a state that will be detected by an invariant.
static SLE::pointer makeLoanSle(UInt256 const &loanBrokerID, std::uint32_t loanSeq, AccountID const &borrower)
Keylet makeImpairedVault(test::jtx::Account const &owner, test::jtx::Account const &borrower, test::jtx::Account const &issuer, test::jtx::Env &env)
void run() override
Runs the suite.
Immutable cryptographic account descriptor.
Definition jtx/Account.h:21
AccountID id() const
Returns the Account ID.
A transaction testing environment.
Definition Env.h:161
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
SLE::const_pointer le(Account const &account) const
Return an account root.
Definition Env.cpp:311
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:323
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition Env.cpp:302
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:377
Set the fee on a JTx.
Definition fee.h:20
Set the regular signature on a JTx.
Definition sig.h:19
T make_shared(T... args)
constexpr Zero kZero
Definition Zero.h:30
Keylet computation functions.
Definition Indexes.h:40
Keylet ownerDir(AccountID const &id) noexcept
The root page of an account's directory.
Definition Indexes.cpp:403
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 loanBroker(AccountID const &owner, SeqProxy const &seq) noexcept
Definition Indexes.cpp:597
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:220
Keylet loan(UInt256 const &loanBrokerID, SeqProxy const &loanSeq) noexcept
Definition Indexes.cpp:603
Keylet mptokenIssuance(MPTID const &issuanceID) noexcept
Definition Indexes.cpp:567
constexpr TenthBips32 kMaxCoverRate
The maximum coverage rate required of a loan broker in 1/10 bips.
Definition Protocol.h:159
Keylet keylet(test::jtx::Account const &subject, test::jtx::Account const &issuer, std::string_view credType)
Definition credentials.h:21
json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:14
XrpT const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
FeatureBitset testableAmendments()
Definition Env.h:92
json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition trust.cpp:18
BEAST_DEFINE_TESTSUITE(AMMClawback, app, xrpl)
constexpr XRPAmount
Convert XRP to drops (integral types).
Definition TxTest.h:54
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
constexpr TenthBips32 percentageToTenthBips(std::uint32_t percentage)
Definition Protocol.h:127
int scale(Number const &number, Asset const &asset)
Get the scale of a Number for a given asset.
Definition STAmount.h:794
@ tefINVARIANT_FAILED
Definition TER.h:178
constexpr std::uint32_t kMaxInvestmentPeriod
Definition Protocol.h:371
TenthBips< std::uint32_t > TenthBips32
Definition Units.h:454
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:657
BaseUInt< 256 > UInt256
Definition base_uint.h:580
VaultVersion
Vault ledger-entry schema versions.
Definition Protocol.h:327
std::unique_ptr< Transactor > makeTransactor(ApplyContext &ctx)
BaseUInt< 192 > MPTID
MPTID is a 192-bit value representing MPT Issuance ID, which is a concatenation of a 32-bit sequence ...
Definition UintTypes.h:54
bool after(NetClock::time_point now, std::uint32_t mark)
Has the specified time passed?
Definition View.cpp:644
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
void roundToAsset(A const &asset, Number &value)
Round an arbitrary precision Number IN PLACE to the precision of a given Asset.
Definition STAmount.h:735
@ TapNone
Definition ApplyView.h:28
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:34
constexpr std::uint8_t kVaultStrategyFirstComeFirstServe
Vault withdrawal policies.
Definition Protocol.h:308
TERSubset< CanCvtToTER > TER
Definition TER.h:654
@ tecINVARIANT_FAILED
Definition TER.h:321
constexpr std::uint32_t kLoanRedemptionBuffer
Minimum gap between a closed-ended loan's final scheduled payment and the vault's RedemptionDate.
Definition Protocol.h:357
std::function< void(SLE::Ref)> describeOwnerDir(AccountID const &account)
Returns a function that sets the owner on a directory SLE.
constexpr std::uint64_t kMaxMpTokenAmount
The maximum amount of MPTokenIssuance.
Definition Protocol.h:297
constexpr std::uint32_t kMinInvestmentPeriod
Bounds on the length of a closed-ended vault's Investment phase (RedemptionDate - SubscriptionDate).
Definition Protocol.h:369
@ tesSUCCESS
Definition TER.h:250
A pair of SHAMap key and LedgerEntryType.
Definition Keylet.h:20
UInt256 key
Definition Keylet.h:21
static std::set< MantissaScale > const & getAllScales()
Definition Number.h:178
T to_string(T... args)