xrpld
Loading...
Searching...
No Matches
Invariants_test.cpp
1#include <test/jtx/AMM.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/flags.h>
8#include <test/jtx/mpt.h>
9#include <test/jtx/pay.h>
10#include <test/jtx/permissioned_domains.h>
11#include <test/jtx/tags.h>
12#include <test/jtx/token.h>
13#include <test/jtx/trust.h>
14#include <test/jtx/vault.h>
15#include <test/unit_test/SuiteJournal.h>
16
17#include <xrpl/basics/Number.h>
18#include <xrpl/basics/base_uint.h>
19#include <xrpl/beast/unit_test/suite.h>
20#include <xrpl/beast/utility/Journal.h>
21#include <xrpl/beast/utility/Zero.h>
22#include <xrpl/ledger/ApplyView.h>
23#include <xrpl/ledger/OpenView.h>
24#include <xrpl/ledger/helpers/AccountRootHelpers.h>
25#include <xrpl/ledger/helpers/DirectoryHelpers.h>
26#include <xrpl/ledger/helpers/RippleStateHelpers.h>
27#include <xrpl/protocol/AccountID.h>
28#include <xrpl/protocol/Book.h>
29#include <xrpl/protocol/Feature.h>
30#include <xrpl/protocol/Indexes.h>
31#include <xrpl/protocol/InnerObjectFormats.h>
32#include <xrpl/protocol/Issue.h>
33#include <xrpl/protocol/Keylet.h>
34#include <xrpl/protocol/LedgerFormats.h>
35#include <xrpl/protocol/MPTIssue.h>
36#include <xrpl/protocol/Protocol.h>
37#include <xrpl/protocol/Rules.h>
38#include <xrpl/protocol/SField.h>
39#include <xrpl/protocol/SOTemplate.h>
40#include <xrpl/protocol/STAmount.h>
41#include <xrpl/protocol/STArray.h>
42#include <xrpl/protocol/STLedgerEntry.h>
43#include <xrpl/protocol/STObject.h>
44#include <xrpl/protocol/STTx.h>
45#include <xrpl/protocol/SystemParameters.h>
46#include <xrpl/protocol/TER.h>
47#include <xrpl/protocol/TxFlags.h>
48#include <xrpl/protocol/TxFormats.h>
49#include <xrpl/protocol/UintTypes.h>
50#include <xrpl/protocol/XRPAmount.h>
51#include <xrpl/protocol/jss.h>
52#include <xrpl/tx/ApplyContext.h>
53#include <xrpl/tx/Transactor.h>
54#include <xrpl/tx/applySteps.h>
55#include <xrpl/tx/invariants/AMMInvariant.h>
56#include <xrpl/tx/invariants/DirectoryInvariant.h>
57#include <xrpl/tx/invariants/VaultInvariant.h>
58
59#include <algorithm>
60#include <array>
61#include <cstddef>
62#include <cstdint>
63#include <functional>
64#include <initializer_list>
65#include <memory>
66#include <optional>
67#include <string>
68#include <utility>
69#include <vector>
70
71namespace xrpl {
72
73// Test-only factory — not part of the public API.
74// The returned Transactor holds a raw reference to ctx; the caller must ensure
75// the ApplyContext outlives the Transactor. Implemented in applySteps.cpp
76std::unique_ptr<Transactor>
78
79} // namespace xrpl
80
81namespace xrpl::test {
82
84{
85 // The optional Preclose function is used to process additional transactions
86 // on the ledger after creating two accounts, but before closing it, and
87 // before the Precheck function. These should only be valid functions, and
88 // not direct manipulations. Preclose is not commonly used.
90 bool(test::jtx::Account const& a, test::jtx::Account const& b, test::jtx::Env& env)>;
91
92 // this is common setup/method for running a failing invariant check. The
93 // precheck function is used to manipulate the ApplyContext with view
94 // changes that will cause the check to fail.
96 bool(test::jtx::Account const& a, test::jtx::Account const& b, ApplyContext& ac)>;
97
98 static FeatureBitset
100 {
101 return xrpl::test::jtx::testableAmendments() | fixCleanup3_1_3 | fixCleanup3_2_0;
102 }
103
120 enum class TxAccount : int { None = 0, A1, A2 };
121 void
123 std::vector<std::string> const& expectLogs,
124 Precheck const& precheck,
125 XRPAmount fee = XRPAmount{},
126 STTx tx = STTx{ttACCOUNT_SET, [](STObject&) {}},
128 Preclose const& preclose = {},
129 TxAccount setTxAccount = TxAccount::None)
130 {
132 test::jtx::Env(*this, defaultAmendments()),
133 expectLogs,
134 precheck,
135 fee,
136 tx,
137 ters,
138 preclose,
139 setTxAccount);
140 }
141
142 void
144 test::jtx::Env&& env,
145 std::vector<std::string> const& expectLogs,
146 Precheck const& precheck,
147 XRPAmount fee = XRPAmount{},
148 STTx tx = STTx{ttACCOUNT_SET, [](STObject&) {}},
150 Preclose const& preclose = {},
151 TxAccount setTxAccount = TxAccount::None)
152 {
153 using namespace test::jtx;
154
155 Account const a1{"A1"};
156 Account const a2{"A2"};
157 env.fund(XRP(1000), a1, a2);
158 if (preclose)
159 BEAST_EXPECT(preclose(a1, a2, env));
160 env.close();
161
162 if (setTxAccount != TxAccount::None)
163 tx.setAccountID(sfAccount, setTxAccount == TxAccount::A1 ? a1.id() : a2.id());
164
165 doInvariantCheck(std::move(env), a1, a2, expectLogs, precheck, fee, tx, ters);
166 }
167
168 void
170 // NOLINTNEXTLINE(cppcoreguidelines-rvalue-reference-param-not-moved)
171 test::jtx::Env&& env,
172 test::jtx::Account const& a1,
173 test::jtx::Account const& a2,
174 std::vector<std::string> const& expectLogs,
175 Precheck const& precheck,
176 XRPAmount fee = XRPAmount{},
177 STTx tx = STTx{ttACCOUNT_SET, [](STObject&) {}},
179 {
180 using namespace test::jtx;
181
182 OpenView ov{*env.current()};
183 test::StreamSink sink{beast::Severity::Warning};
184 beast::Journal const jlog{sink};
185 ApplyContext ac{env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
186
187 // Invariants normally run in the Transaction's "apply" (operator()) context, and can always
188 // access global Rules.
189 CurrentTransactionRulesGuard const rulesGuard(ov.rules());
190
191 BEAST_EXPECT(precheck(a1, a2, ac));
192
193 auto transactor = makeTransactor(ac);
194 if (!BEAST_EXPECT(transactor))
195 return;
196
197 // invoke check twice to cover tec and tef cases
198 if (!BEAST_EXPECT(ters.size() == 2))
199 return;
200
201 TER terActual = tesSUCCESS;
202 for (TER const& terExpect : ters)
203 {
204 terActual = transactor->checkInvariants(terActual, fee);
205 BEAST_EXPECTS(
206 terExpect == terActual,
207 "expected: " + transToken(terExpect) + " got: " + transToken(terActual));
208 auto const messages = sink.messages().str();
209
210 if (!isTesSuccess(terActual))
211 {
212 BEAST_EXPECTS(
213 messages.starts_with("Invariant failed:") ||
214 messages.starts_with("Transaction caused an exception"),
215 messages);
216 }
217
218 // std::cerr << messages << '\n';
219 for (auto const& m : expectLogs)
220 {
221 BEAST_EXPECTS(messages.contains(m), m);
222 }
223 }
224 }
225
226 void
228 {
229 using namespace test::jtx;
230 testcase << "XRP created";
232 {{"XRP net change was positive: 500"}},
233 [](Account const& a1, Account const&, ApplyContext& ac) {
234 // put a single account in the view and "manufacture" some XRP
235 auto const sle = ac.view().peek(keylet::account(a1.id()));
236 if (!sle)
237 return false;
238 auto amt = sle->getFieldAmount(sfBalance);
239 sle->setFieldAmount(sfBalance, amt + STAmount{500});
240 ac.view().update(sle);
241 return true;
242 });
243 }
244
245 void
247 {
248 using namespace test::jtx;
249 testcase << "account root removed";
250
251 // An account was deleted, but not by an AccountDelete transaction.
253 {{"an account root was deleted"}},
254 [](Account const& a1, Account const&, ApplyContext& ac) {
255 // remove an account from the view
256 auto sle = ac.view().peek(keylet::account(a1.id()));
257 if (!sle)
258 return false;
259 // Clear the balance so the "account deletion left behind a
260 // non-zero balance" check doesn't trip earlier than the desired
261 // check.
262 sle->at(sfBalance) = beast::kZero;
263 ac.view().erase(sle);
264 return true;
265 });
266
267 // Successful AccountDelete transaction that didn't delete an account.
268 //
269 // Note that this is a case where a second invocation of the invariant
270 // checker returns a tecINVARIANT_FAILED, not a tefINVARIANT_FAILED.
271 // After a discussion with the team, we believe that's okay.
273 {{"account deletion succeeded without deleting an account"}},
274 [](Account const&, Account const&, ApplyContext& ac) { return true; },
275 XRPAmount{},
276 STTx{ttACCOUNT_DELETE, [](STObject& tx) {}},
278
279 // Successful AccountDelete that deleted more than one account.
281 {{"account deletion succeeded but deleted multiple accounts"}},
282 [](Account const& a1, Account const& a2, ApplyContext& ac) {
283 // remove two accounts from the view
284 auto sleA1 = ac.view().peek(keylet::account(a1.id()));
285 auto sleA2 = ac.view().peek(keylet::account(a2.id()));
286 if (!sleA1 || !sleA2)
287 return false;
288 // Clear the balance so the "account deletion left behind a
289 // non-zero balance" check doesn't trip earlier than the desired
290 // check.
291 sleA1->at(sfBalance) = beast::kZero;
292 sleA2->at(sfBalance) = beast::kZero;
293 ac.view().erase(sleA1);
294 ac.view().erase(sleA2);
295 return true;
296 },
297 XRPAmount{},
298 STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
299 }
300
301 void
303 {
304 using namespace test::jtx;
305 testcase << "account root deletion left artifact";
306
308 {{"account deletion left behind a non-zero balance"}},
309 // NOLINTNEXTLINE(readability-identifier-naming)
310 [&](Account const& A1, Account const& A2, ApplyContext& ac) {
311 // A1 has a balance. Delete A1
312 auto const a1 = A1.id();
313 auto const sleA1 = ac.view().peek(keylet::account(a1));
314 if (!sleA1)
315 return false;
316 if (!BEAST_EXPECT(*sleA1->at(sfBalance) != beast::kZero))
317 return false;
318
319 ac.view().erase(sleA1);
320
321 return true;
322 },
323 XRPAmount{},
324 STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
325
327 {{"account deletion left behind a non-zero owner count"}},
328 // NOLINTNEXTLINE(readability-identifier-naming)
329 [&](Account const& A1, Account const& A2, ApplyContext& ac) {
330 // Increment A1's owner count, then delete A1
331 auto const a1 = A1.id();
332 auto const sleA1 = ac.view().peek(keylet::account(a1));
333 if (!sleA1)
334 return false;
335 // Clear the balance so the "account deletion left behind a
336 // non-zero balance" check doesn't trip earlier than the desired
337 // check.
338 sleA1->at(sfBalance) = beast::kZero;
339 BEAST_EXPECT(sleA1->at(sfOwnerCount) == 0);
340 adjustOwnerCount(ac.view(), sleA1, 1, ac.journal);
341
342 ac.view().erase(sleA1);
343
344 return true;
345 },
346 XRPAmount{},
347 STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
348
349 for (auto const& keyletInfo : kDirectAccountKeylets)
350 {
351 // TODO: Use structured binding once LLVM 16 is the minimum
352 // supported version. See also:
353 // https://github.com/llvm/llvm-project/issues/48582
354 // https://github.com/llvm/llvm-project/commit/127bf44385424891eb04cff8e52d3f157fc2cb7c
355 if (!keyletInfo.includeInTests)
356 continue;
357 auto const& keyletfunc = keyletInfo.function;
358 auto const& type = keyletInfo.expectedLEName;
359
360 using namespace std::string_literals;
361
363 {{"account deletion left behind a "s + type.cStr() + " object"}},
364 // NOLINTNEXTLINE(readability-identifier-naming)
365 [&](Account const& A1, Account const& A2, ApplyContext& ac) {
366 // Add an object to the ledger for account A1, then delete
367 // A1
368 auto const a1 = A1.id();
369 auto sleA1 = ac.view().peek(keylet::account(a1));
370 if (!sleA1)
371 return false;
372
373 auto const key = std::invoke(keyletfunc, a1);
374 auto const newSLE = std::make_shared<SLE>(key);
375 ac.view().insert(newSLE);
376 // Clear the balance so the "account deletion left behind a
377 // non-zero balance" check doesn't trip earlier than the
378 // desired check.
379 sleA1->at(sfBalance) = beast::kZero;
380 ac.view().erase(sleA1);
381
382 return true;
383 },
384 XRPAmount{},
385 STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
386 }
387
388 // NFT special case
390 {{"account deletion left behind a NFTokenPage object"}},
391 [&](Account const& a1, Account const&, ApplyContext& ac) {
392 // remove an account from the view
393 auto sle = ac.view().peek(keylet::account(a1.id()));
394 if (!sle)
395 return false;
396 // Clear the balance so the "account deletion left behind a
397 // non-zero balance" check doesn't trip earlier than the desired
398 // check.
399 sle->at(sfBalance) = beast::kZero;
400 sle->at(sfOwnerCount) = 0;
401 ac.view().erase(sle);
402 return true;
403 },
404 XRPAmount{},
405 STTx{ttACCOUNT_DELETE, [](STObject& tx) {}},
407 [&](Account const& a1, Account const&, Env& env) {
408 // Preclose callback to mint the NFT which will be deleted in
409 // the Precheck callback above.
410 env(token::mint(a1));
411
412 return true;
413 });
414
415 // AMM special cases
416 AccountID ammAcctID;
417 uint256 ammKey;
418 Issue ammIssue;
420 {{"account deletion left behind a DirectoryNode object"}},
421 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
422 // Delete the AMM account without cleaning up the directory or
423 // deleting the AMM object
424 auto sle = ac.view().peek(keylet::account(ammAcctID));
425 if (!sle)
426 return false;
427
428 BEAST_EXPECT(sle->at(~sfAMMID));
429 BEAST_EXPECT(sle->at(~sfAMMID) == ammKey);
430
431 // Clear the balance so the "account deletion left behind a
432 // non-zero balance" check doesn't trip earlier than the desired
433 // check.
434 sle->at(sfBalance) = beast::kZero;
435 sle->at(sfOwnerCount) = 0;
436 ac.view().erase(sle);
437
438 return true;
439 },
440 XRPAmount{},
441 STTx{ttAMM_WITHDRAW, [](STObject& tx) {}},
443 [&](Account const& a1, Account const& a2, Env& env) {
444 // Preclose callback to create the AMM which will be partially
445 // deleted in the Precheck callback above.
446 AMM const amm(env, a1, XRP(100), a1["USD"](50));
447 ammAcctID = amm.ammAccount();
448 ammKey = amm.ammID();
449 ammIssue = amm.lptIssue();
450 return true;
451 });
453 {{"account deletion left behind a AMM object"}},
454 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
455 // Delete all the AMM's trust lines, remove the AMM from the AMM
456 // account's directory (this deletes the directory), and delete
457 // the AMM account. Do not delete the AMM object.
458 auto sle = ac.view().peek(keylet::account(ammAcctID));
459 if (!sle)
460 return false;
461
462 BEAST_EXPECT(sle->at(~sfAMMID));
463 BEAST_EXPECT(sle->at(~sfAMMID) == ammKey);
464
465 for (auto const& trustKeylet :
466 {keylet::trustLine(ammAcctID, a1["USD"]), keylet::trustLine(a1, ammIssue)})
467 {
468 auto const line = ac.view().peek(trustKeylet);
469 if (!line)
470 {
471 return false;
472 }
473
474 STAmount const lowLimit = line->at(sfLowLimit);
475 STAmount const highLimit = line->at(sfHighLimit);
476 BEAST_EXPECT(
478 ac.view(),
479 line,
480 lowLimit.getIssuer(),
481 highLimit.getIssuer(),
482 ac.journal) == tesSUCCESS);
483 }
484
485 auto const ammSle = ac.view().peek(keylet::amm(ammKey));
486 if (!BEAST_EXPECT(ammSle))
487 return false;
488 auto const ownerDirKeylet = keylet::ownerDir(ammAcctID);
489
490 BEAST_EXPECT(
491 ac.view().dirRemove(ownerDirKeylet, ammSle->at(sfOwnerNode), ammKey, false));
492 BEAST_EXPECT(
493 !ac.view().exists(ownerDirKeylet) || ac.view().emptyDirDelete(ownerDirKeylet));
494
495 // Clear the balance so the "account deletion left behind a
496 // non-zero balance" check doesn't trip earlier than the desired
497 // check.
498 sle->at(sfBalance) = beast::kZero;
499 sle->at(sfOwnerCount) = 0;
500 ac.view().erase(sle);
501
502 return true;
503 },
504 XRPAmount{},
505 STTx{ttAMM_WITHDRAW, [](STObject& tx) {}},
507 [&](Account const& a1, Account const& a2, Env& env) {
508 // Preclose callback to create the AMM which will be partially
509 // deleted in the Precheck callback above.
510 AMM const amm(env, a1, XRP(100), a1["USD"](50));
511 ammAcctID = amm.ammAccount();
512 ammKey = amm.ammID();
513 ammIssue = amm.lptIssue();
514 return true;
515 });
516 }
517
518 void
520 {
521 using namespace test::jtx;
522 testcase << "ledger entry types don't match";
524 {{"ledger entry type mismatch"}, {"XRP net change of -1000000000 doesn't match fee 0"}},
525 [](Account const& a1, Account const&, ApplyContext& ac) {
526 // replace an entry in the table with an SLE of a different type
527 auto const sle = ac.view().peek(keylet::account(a1.id()));
528 if (!sle)
529 return false;
530 auto const sleNew = std::make_shared<SLE>(ltTICKET, sle->key());
531 ac.rawView().rawReplace(sleNew);
532 return true;
533 });
534
536 {{"invalid ledger entry type added"}},
537 [](Account const& a1, Account const&, ApplyContext& ac) {
538 // add an entry in the table with an SLE of an invalid type
539 auto const sle = ac.view().peek(keylet::account(a1.id()));
540 if (!sle)
541 return false;
542
543 // make a dummy escrow ledger entry, then change the type to an
544 // unsupported value so that the valid type invariant check
545 // will fail.
546 auto const sleNew =
547 std::make_shared<SLE>(keylet::escrow(a1, (*sle)[sfSequence] + 2));
548
549 // We don't use ltNICKNAME directly since it's marked deprecated
550 // to prevent accidental use elsewhere.
551 sleNew->type_ = static_cast<LedgerEntryType>('n');
552 ac.view().insert(sleNew);
553 return true;
554 });
555 }
556
557 void
559 {
560 using namespace test::jtx;
561 testcase << "trust lines with XRP not allowed";
563 {{"an XRP trust line was created"}},
564 [](Account const& a1, Account const& a2, ApplyContext& ac) {
565 // create simple trust SLE with xrp currency
566 auto const sleNew =
568 ac.view().insert(sleNew);
569 return true;
570 });
571 }
572
573 void
575 {
576 using namespace test::jtx;
577 testcase << "trust lines with deep freeze flag without freeze "
578 "not allowed";
580 {{"a trust line with deep freeze flag without normal freeze was "
581 "created"}},
582 [](Account const& a1, Account const& a2, ApplyContext& ac) {
583 auto const sleNew =
584 std::make_shared<SLE>(keylet::trustLine(a1, a2, a1["USD"].currency));
585 sleNew->setFieldAmount(sfLowLimit, a1["USD"](0));
586 sleNew->setFieldAmount(sfHighLimit, a1["USD"](0));
587
588 std::uint32_t uFlags = 0u;
589 uFlags |= lsfLowDeepFreeze;
590 sleNew->setFieldU32(sfFlags, uFlags);
591 ac.view().insert(sleNew);
592 return true;
593 });
594
596 {{"a trust line with deep freeze flag without normal freeze was "
597 "created"}},
598 [](Account const& a1, Account const& a2, ApplyContext& ac) {
599 auto const sleNew =
600 std::make_shared<SLE>(keylet::trustLine(a1, a2, a1["USD"].currency));
601 sleNew->setFieldAmount(sfLowLimit, a1["USD"](0));
602 sleNew->setFieldAmount(sfHighLimit, a1["USD"](0));
603 std::uint32_t uFlags = 0u;
604 uFlags |= lsfHighDeepFreeze;
605 sleNew->setFieldU32(sfFlags, uFlags);
606 ac.view().insert(sleNew);
607 return true;
608 });
609
611 {{"a trust line with deep freeze flag without normal freeze was "
612 "created"}},
613 [](Account const& a1, Account const& a2, ApplyContext& ac) {
614 auto const sleNew =
615 std::make_shared<SLE>(keylet::trustLine(a1, a2, a1["USD"].currency));
616 sleNew->setFieldAmount(sfLowLimit, a1["USD"](0));
617 sleNew->setFieldAmount(sfHighLimit, a1["USD"](0));
618 std::uint32_t uFlags = 0u;
619 uFlags |= lsfLowDeepFreeze | lsfHighDeepFreeze;
620 sleNew->setFieldU32(sfFlags, uFlags);
621 ac.view().insert(sleNew);
622 return true;
623 });
624
626 {{"a trust line with deep freeze flag without normal freeze was "
627 "created"}},
628 [](Account const& a1, Account const& a2, ApplyContext& ac) {
629 auto const sleNew =
630 std::make_shared<SLE>(keylet::trustLine(a1, a2, a1["USD"].currency));
631 sleNew->setFieldAmount(sfLowLimit, a1["USD"](0));
632 sleNew->setFieldAmount(sfHighLimit, a1["USD"](0));
633 std::uint32_t uFlags = 0u;
634 uFlags |= lsfLowDeepFreeze | lsfHighFreeze;
635 sleNew->setFieldU32(sfFlags, uFlags);
636 ac.view().insert(sleNew);
637 return true;
638 });
639
641 {{"a trust line with deep freeze flag without normal freeze was "
642 "created"}},
643 [](Account const& a1, Account const& a2, ApplyContext& ac) {
644 auto const sleNew =
645 std::make_shared<SLE>(keylet::trustLine(a1, a2, a1["USD"].currency));
646 sleNew->setFieldAmount(sfLowLimit, a1["USD"](0));
647 sleNew->setFieldAmount(sfHighLimit, a1["USD"](0));
648 std::uint32_t uFlags = 0u;
649 uFlags |= lsfLowFreeze | lsfHighDeepFreeze;
650 sleNew->setFieldU32(sfFlags, uFlags);
651 ac.view().insert(sleNew);
652 return true;
653 });
654 }
655
656 void
658 {
659 using namespace test::jtx;
660 testcase << "transfers when frozen";
661
662 Account const g1{"G1"};
663 // Helper function to establish the trustlines
664 auto const createTrustlines = [&](Account const& a1, Account const& a2, Env& env) {
665 // Preclose callback to establish trust lines with gateway
666 env.fund(XRP(1000), g1);
667
668 env.trust(g1["USD"](10000), a1);
669 env.trust(g1["USD"](10000), a2);
670 env.close();
671
672 env(pay(g1, a1, g1["USD"](1000)));
673 env(pay(g1, a2, g1["USD"](1000)));
674 env.close();
675
676 return true;
677 };
678
679 auto const a1FrozenByIssuer = [&](Account const& a1, Account const& a2, Env& env) {
680 createTrustlines(a1, a2, env);
681 env(trust(g1, a1["USD"](10000), tfSetFreeze));
682 env.close();
683
684 return true;
685 };
686
687 auto const a1DeepFrozenByIssuer = [&](Account const& a1, Account const& a2, Env& env) {
688 a1FrozenByIssuer(a1, a2, env);
689 env(trust(g1, a1["USD"](10000), tfSetDeepFreeze));
690 env.close();
691
692 return true;
693 };
694
695 auto const changeBalances = [&](Account const& a1,
696 Account const& a2,
697 ApplyContext& ac,
698 int a1Balance,
699 int a2Balance) {
700 auto const sleA1 = ac.view().peek(keylet::trustLine(a1, g1["USD"]));
701 auto const sleA2 = ac.view().peek(keylet::trustLine(a2, g1["USD"]));
702
703 sleA1->setFieldAmount(sfBalance, g1["USD"](a1Balance));
704 sleA2->setFieldAmount(sfBalance, g1["USD"](a2Balance));
705
706 ac.view().update(sleA1);
707 ac.view().update(sleA2);
708 };
709
710 // test: imitating frozen A1 making a payment to A2.
712 {{"Attempting to move frozen funds"}},
713 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
714 changeBalances(a1, a2, ac, -900, -1100);
715 return true;
716 },
717 XRPAmount{},
718 STTx{ttPAYMENT, [](STObject& tx) {}},
720 a1FrozenByIssuer);
721
722 // test: imitating deep frozen A1 making a payment to A2.
724 {{"Attempting to move frozen funds"}},
725 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
726 changeBalances(a1, a2, ac, -900, -1100);
727 return true;
728 },
729 XRPAmount{},
730 STTx{ttPAYMENT, [](STObject& tx) {}},
732 a1DeepFrozenByIssuer);
733
734 // test: imitating A2 making a payment to deep frozen A1.
736 {{"Attempting to move frozen funds"}},
737 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
738 changeBalances(a1, a2, ac, -1100, -900);
739 return true;
740 },
741 XRPAmount{},
742 STTx{ttPAYMENT, [](STObject& tx) {}},
744 a1DeepFrozenByIssuer);
745 }
746
747 void
749 {
750 using namespace test::jtx;
751 testcase << "XRP balance checks";
752
754 {{"Cannot return non-native STAmount as XRPAmount"}},
755 [](Account const& a1, Account const& a2, ApplyContext& ac) {
756 // non-native balance
757 auto const sle = ac.view().peek(keylet::account(a1.id()));
758 if (!sle)
759 return false;
760 STAmount const nonNative(a2["USD"](51));
761 sle->setFieldAmount(sfBalance, nonNative);
762 ac.view().update(sle);
763 return true;
764 });
765
767 {{"incorrect account XRP balance"}, {"XRP net change was positive: 99999999000000001"}},
768 [this](Account const& a1, Account const&, ApplyContext& ac) {
769 // balance exceeds genesis amount
770 auto const sle = ac.view().peek(keylet::account(a1.id()));
771 if (!sle)
772 return false;
773 // Use `drops(1)` to bypass a call to STAmount::canonicalize
774 // with an invalid value
775 sle->setFieldAmount(sfBalance, kInitialXrp + drops(1));
776 BEAST_EXPECT(!sle->getFieldAmount(sfBalance).negative());
777 ac.view().update(sle);
778 return true;
779 });
780
782 {{"incorrect account XRP balance"},
783 {"XRP net change of -1000000001 doesn't match fee 0"}},
784 [this](Account const& a1, Account const&, ApplyContext& ac) {
785 // balance is negative
786 auto const sle = ac.view().peek(keylet::account(a1.id()));
787 if (!sle)
788 return false;
789 sle->setFieldAmount(sfBalance, STAmount{1, true});
790 BEAST_EXPECT(sle->getFieldAmount(sfBalance).negative());
791 ac.view().update(sle);
792 return true;
793 });
794 }
795
796 void
798 {
799 using namespace test::jtx;
800 using namespace std::string_literals;
801 testcase << "Transaction fee checks";
802
804 {{"fee paid was negative: -1"}, {"XRP net change of 0 doesn't match fee -1"}},
805 [](Account const&, Account const&, ApplyContext&) { return true; },
806 XRPAmount{-1});
807
809 {{"fee paid exceeds system limit: "s + to_string(kInitialXrp)},
810 {"XRP net change of 0 doesn't match fee "s + to_string(kInitialXrp)}},
811 [](Account const&, Account const&, ApplyContext&) { return true; },
813
815 {{"fee paid is 20 exceeds fee specified in transaction."},
816 {"XRP net change of 0 doesn't match fee 20"}},
817 [](Account const&, Account const&, ApplyContext&) { return true; },
818 XRPAmount{20},
819 STTx{ttACCOUNT_SET, [](STObject& tx) { tx.setFieldAmount(sfFee, XRPAmount{10}); }});
820 }
821
822 void
824 {
825 using namespace test::jtx;
826 testcase << "no bad offers";
827
829 {{"offer with a bad amount"}}, [](Account const& a1, Account const&, ApplyContext& ac) {
830 // offer with negative takerpays
831 auto const sle = ac.view().peek(keylet::account(a1.id()));
832 if (!sle)
833 return false;
834 auto sleNew = std::make_shared<SLE>(keylet::offer(a1.id(), (*sle)[sfSequence]));
835 sleNew->setAccountID(sfAccount, a1.id());
836 sleNew->setFieldU32(sfSequence, (*sle)[sfSequence]);
837 sleNew->setFieldAmount(sfTakerPays, XRP(-1));
838 ac.view().insert(sleNew);
839 return true;
840 });
841
843 {{"offer with a bad amount"}}, [](Account const& a1, Account const&, ApplyContext& ac) {
844 // offer with negative takergets
845 auto const sle = ac.view().peek(keylet::account(a1.id()));
846 if (!sle)
847 return false;
848 auto sleNew = std::make_shared<SLE>(keylet::offer(a1.id(), (*sle)[sfSequence]));
849 sleNew->setAccountID(sfAccount, a1.id());
850 sleNew->setFieldU32(sfSequence, (*sle)[sfSequence]);
851 sleNew->setFieldAmount(sfTakerPays, a1["USD"](10));
852 sleNew->setFieldAmount(sfTakerGets, XRP(-1));
853 ac.view().insert(sleNew);
854 return true;
855 });
856
858 {{"offer with a bad amount"}}, [](Account const& a1, Account const&, ApplyContext& ac) {
859 // offer XRP to XRP
860 auto const sle = ac.view().peek(keylet::account(a1.id()));
861 if (!sle)
862 return false;
863 auto sleNew = std::make_shared<SLE>(keylet::offer(a1.id(), (*sle)[sfSequence]));
864 sleNew->setAccountID(sfAccount, a1.id());
865 sleNew->setFieldU32(sfSequence, (*sle)[sfSequence]);
866 sleNew->setFieldAmount(sfTakerPays, XRP(10));
867 sleNew->setFieldAmount(sfTakerGets, XRP(11));
868 ac.view().insert(sleNew);
869 return true;
870 });
871 }
872
873 void
875 {
876 using namespace test::jtx;
877 testcase << "no zero escrow";
878
880 {{"XRP net change of -1000000 doesn't match fee 0"},
881 {"escrow specifies invalid amount"}},
882 [](Account const& a1, Account const&, ApplyContext& ac) {
883 // escrow with negative amount
884 auto const sle = ac.view().peek(keylet::account(a1.id()));
885 if (!sle)
886 return false;
887 auto sleNew = std::make_shared<SLE>(keylet::escrow(a1, (*sle)[sfSequence] + 2));
888 sleNew->setFieldAmount(sfAmount, XRP(-1));
889 ac.view().insert(sleNew);
890 return true;
891 });
892
894 {{"XRP net change was positive: 100000000000000001"},
895 {"escrow specifies invalid amount"}},
896 [](Account const& a1, Account const&, ApplyContext& ac) {
897 // escrow with too-large amount
898 auto const sle = ac.view().peek(keylet::account(a1.id()));
899 if (!sle)
900 return false;
901 auto sleNew = std::make_shared<SLE>(keylet::escrow(a1, (*sle)[sfSequence] + 2));
902 // Use `drops(1)` to bypass a call to STAmount::canonicalize
903 // with an invalid value
904 sleNew->setFieldAmount(sfAmount, kInitialXrp + drops(1));
905 ac.view().insert(sleNew);
906 return true;
907 });
908
909 // IOU < 0
911 {{"escrow specifies invalid amount"}},
912 [](Account const& a1, Account const&, ApplyContext& ac) {
913 // escrow with too-little iou
914 auto const sle = ac.view().peek(keylet::account(a1.id()));
915 if (!sle)
916 return false;
917 auto sleNew = std::make_shared<SLE>(keylet::escrow(a1, (*sle)[sfSequence] + 2));
918
919 Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)};
920 STAmount const amt(usd, -1);
921 sleNew->setFieldAmount(sfAmount, amt);
922 ac.view().insert(sleNew);
923 return true;
924 });
925
926 // IOU bad currency
928 {{"escrow specifies invalid amount"}},
929 [](Account const& a1, Account const&, ApplyContext& ac) {
930 // escrow with bad iou currency
931 auto const sle = ac.view().peek(keylet::account(a1.id()));
932 if (!sle)
933 return false;
934 auto sleNew = std::make_shared<SLE>(keylet::escrow(a1, (*sle)[sfSequence] + 2));
935
936 Issue const bad{badCurrency(), AccountID(0x4985601)};
937 STAmount const amt(bad, 1);
938 sleNew->setFieldAmount(sfAmount, amt);
939 ac.view().insert(sleNew);
940 return true;
941 });
942
943 // MPT < 0
945 {{"escrow specifies invalid amount"}},
946 [](Account const& a1, Account const&, ApplyContext& ac) {
947 // escrow with too-little mpt
948 auto const sle = ac.view().peek(keylet::account(a1.id()));
949 if (!sle)
950 return false;
951 auto sleNew = std::make_shared<SLE>(keylet::escrow(a1, (*sle)[sfSequence] + 2));
952
953 MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
954 STAmount const amt(mpt, -1);
955 sleNew->setFieldAmount(sfAmount, amt);
956 ac.view().insert(sleNew);
957 return true;
958 });
959
960 // MPT OutstandingAmount < 0
962 {{"escrow specifies invalid amount"}},
963 [](Account const& a1, Account const&, ApplyContext& ac) {
964 // mptissuance outstanding is negative
965 auto const sle = ac.view().peek(keylet::account(a1.id()));
966 if (!sle)
967 return false;
968
969 MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
971 sleNew->setFieldU64(sfOutstandingAmount, -1);
972 ac.view().insert(sleNew);
973 return true;
974 });
975
976 // MPT LockedAmount < 0
978 {{"escrow specifies invalid amount"}},
979 [](Account const& a1, Account const&, ApplyContext& ac) {
980 // mptissuance locked is less than locked
981 auto const sle = ac.view().peek(keylet::account(a1.id()));
982 if (!sle)
983 return false;
984
985 MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
987 sleNew->setFieldU64(sfLockedAmount, -1);
988 ac.view().insert(sleNew);
989 return true;
990 });
991
992 // MPT OutstandingAmount < LockedAmount
994 {{"escrow specifies invalid amount"}},
995 [](Account const& a1, Account const&, ApplyContext& ac) {
996 // mptissuance outstanding is less than locked
997 auto const sle = ac.view().peek(keylet::account(a1.id()));
998 if (!sle)
999 return false;
1000
1001 MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
1003 sleNew->setFieldU64(sfOutstandingAmount, 1);
1004 sleNew->setFieldU64(sfLockedAmount, 10);
1005 ac.view().insert(sleNew);
1006 return true;
1007 });
1008
1009 // MPT MPTAmount < 0
1011 {{"escrow specifies invalid amount"}},
1012 [](Account const& a1, Account const&, ApplyContext& ac) {
1013 // mptoken amount is negative
1014 auto const sle = ac.view().peek(keylet::account(a1.id()));
1015 if (!sle)
1016 return false;
1017
1018 MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
1019 auto sleNew = std::make_shared<SLE>(keylet::mptoken(mpt.getMptID(), a1));
1020 sleNew->setFieldU64(sfMPTAmount, -1);
1021 ac.view().insert(sleNew);
1022 return true;
1023 });
1024
1025 // MPT LockedAmount < 0
1027 {{"escrow specifies invalid amount"}},
1028 [](Account const& a1, Account const&, ApplyContext& ac) {
1029 // mptoken locked amount is negative
1030 auto const sle = ac.view().peek(keylet::account(a1.id()));
1031 if (!sle)
1032 return false;
1033
1034 MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
1035 auto sleNew = std::make_shared<SLE>(keylet::mptoken(mpt.getMptID(), a1));
1036 sleNew->setFieldU64(sfLockedAmount, -1);
1037 ac.view().insert(sleNew);
1038 return true;
1039 });
1040 }
1041
1042 void
1044 {
1045 using namespace test::jtx;
1046 testcase << "valid new account root";
1047
1049 {{"account root created illegally"}},
1050 [](Account const&, Account const&, ApplyContext& ac) {
1051 // Insert a new account root created by a non-payment into
1052 // the view.
1053 Account const a3{"A3"};
1054 Keylet const acctKeylet = keylet::account(a3);
1055 auto const sleNew = std::make_shared<SLE>(acctKeylet);
1056 ac.view().insert(sleNew);
1057 return true;
1058 });
1059
1061 {{"multiple accounts created in a single transaction"}},
1062 [](Account const&, Account const&, ApplyContext& ac) {
1063 // Insert two new account roots into the view.
1064 {
1065 Account const a3{"A3"};
1066 Keylet const acctKeylet = keylet::account(a3);
1067 auto const sleA3 = std::make_shared<SLE>(acctKeylet);
1068 ac.view().insert(sleA3);
1069 }
1070 {
1071 Account const a4{"A4"};
1072 Keylet const acctKeylet = keylet::account(a4);
1073 auto const sleA4 = std::make_shared<SLE>(acctKeylet);
1074 ac.view().insert(sleA4);
1075 }
1076 return true;
1077 });
1078
1080 {{"account created with wrong starting sequence number"}},
1081 [](Account const&, Account const&, ApplyContext& ac) {
1082 // Insert a new account root with the wrong starting sequence.
1083 Account const a3{"A3"};
1084 Keylet const acctKeylet = keylet::account(a3);
1085 auto const sleNew = std::make_shared<SLE>(acctKeylet);
1086 sleNew->setFieldU32(sfSequence, ac.view().seq() + 1);
1087 ac.view().insert(sleNew);
1088 return true;
1089 },
1090 XRPAmount{},
1091 STTx{ttPAYMENT, [](STObject& tx) {}});
1092
1094 {{"pseudo-account created by a wrong transaction type"}},
1095 [](Account const&, Account const&, ApplyContext& ac) {
1096 Account const a3{"A3"};
1097 Keylet const acctKeylet = keylet::account(a3);
1098 auto const sleNew = std::make_shared<SLE>(acctKeylet);
1099 sleNew->setFieldU32(sfSequence, 0);
1100 sleNew->setFieldH256(sfAMMID, uint256(1));
1101 sleNew->setFieldU32(sfFlags, lsfDisableMaster | lsfDefaultRipple);
1102 ac.view().insert(sleNew);
1103 return true;
1104 },
1105 XRPAmount{},
1106 STTx{ttPAYMENT, [](STObject& tx) {}});
1107
1109 {{"account created with wrong starting sequence number"}},
1110 [](Account const&, Account const&, ApplyContext& ac) {
1111 Account const a3{"A3"};
1112 Keylet const acctKeylet = keylet::account(a3);
1113 auto const sleNew = std::make_shared<SLE>(acctKeylet);
1114 sleNew->setFieldU32(sfSequence, ac.view().seq());
1115 sleNew->setFieldH256(sfAMMID, uint256(1));
1116 sleNew->setFieldU32(sfFlags, lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth);
1117 ac.view().insert(sleNew);
1118 return true;
1119 },
1120 XRPAmount{},
1121 STTx{ttAMM_CREATE, [](STObject& tx) {}});
1122
1124 {{"pseudo-account created with wrong flags"}},
1125 [](Account const&, Account const&, ApplyContext& ac) {
1126 Account const a3{"A3"};
1127 Keylet const acctKeylet = keylet::account(a3);
1128 auto const sleNew = std::make_shared<SLE>(acctKeylet);
1129 sleNew->setFieldU32(sfSequence, 0);
1130 sleNew->setFieldH256(sfAMMID, uint256(1));
1131 sleNew->setFieldU32(sfFlags, lsfDisableMaster | lsfDefaultRipple);
1132 ac.view().insert(sleNew);
1133 return true;
1134 },
1135 XRPAmount{},
1136 STTx{ttVAULT_CREATE, [](STObject& tx) {}});
1137
1139 {{"pseudo-account created with wrong flags"}},
1140 [](Account const&, Account const&, ApplyContext& ac) {
1141 Account const a3{"A3"};
1142 Keylet const acctKeylet = keylet::account(a3);
1143 auto const sleNew = std::make_shared<SLE>(acctKeylet);
1144 sleNew->setFieldU32(sfSequence, 0);
1145 sleNew->setFieldH256(sfAMMID, uint256(1));
1146 sleNew->setFieldU32(
1147 sfFlags,
1148 lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth | lsfRequireDestTag);
1149 ac.view().insert(sleNew);
1150 return true;
1151 },
1152 XRPAmount{},
1153 STTx{ttAMM_CREATE, [](STObject& tx) {}});
1154 }
1155
1156 void
1158 {
1159 using namespace test::jtx;
1160 testcase << "NFTokenPage";
1161
1162 // lambda that returns an STArray of NFTokenIDs.
1163 uint256 const firstNFTID(
1164 "0000000000000000000000000000000000000001FFFFFFFFFFFFFFFF00000000");
1165 auto makeNFTokenIDs = [&firstNFTID](unsigned int nftCount) {
1166 SOTemplate const* nfTokenTemplate =
1168
1169 uint256 nftID(firstNFTID);
1170 STArray ret;
1171 for (int i = 0; i < nftCount; ++i)
1172 {
1173 STObject newNFToken(*nfTokenTemplate, sfNFToken, [&nftID](STObject& object) {
1174 object.setFieldH256(sfNFTokenID, nftID);
1175 });
1176 ret.pushBack(std::move(newNFToken));
1177 ++nftID;
1178 }
1179 return ret;
1180 };
1181
1183 {{"NFT page has invalid size"}},
1184 [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
1186 nftPage->setFieldArray(sfNFTokens, makeNFTokenIDs(0));
1187
1188 ac.view().insert(nftPage);
1189 return true;
1190 });
1191
1193 {{"NFT page has invalid size"}},
1194 [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
1196 nftPage->setFieldArray(sfNFTokens, makeNFTokenIDs(33));
1197
1198 ac.view().insert(nftPage);
1199 return true;
1200 });
1201
1203 {{"NFTs on page are not sorted"}},
1204 [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
1205 STArray nfTokens = makeNFTokenIDs(2);
1206 std::iter_swap(nfTokens.begin(), nfTokens.begin() + 1);
1207
1209 nftPage->setFieldArray(sfNFTokens, nfTokens);
1210
1211 ac.view().insert(nftPage);
1212 return true;
1213 });
1214
1216 {{"NFT contains empty URI"}},
1217 [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
1218 STArray nfTokens = makeNFTokenIDs(1);
1219 nfTokens[0].setFieldVL(sfURI, Blob{});
1220
1222 nftPage->setFieldArray(sfNFTokens, nfTokens);
1223
1224 ac.view().insert(nftPage);
1225 return true;
1226 });
1227
1229 {{"NFT page is improperly linked"}},
1230 [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
1232 nftPage->setFieldArray(sfNFTokens, makeNFTokenIDs(1));
1233 nftPage->setFieldH256(sfPreviousPageMin, keylet::nftokenPageMax(a1).key);
1234
1235 ac.view().insert(nftPage);
1236 return true;
1237 });
1238
1240 {{"NFT page is improperly linked"}},
1241 [&makeNFTokenIDs](Account const& a1, Account const& a2, ApplyContext& ac) {
1243 nftPage->setFieldArray(sfNFTokens, makeNFTokenIDs(1));
1244 nftPage->setFieldH256(sfPreviousPageMin, keylet::nftokenPageMin(a2).key);
1245
1246 ac.view().insert(nftPage);
1247 return true;
1248 });
1249
1251 {{"NFT page is improperly linked"}},
1252 [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
1254 nftPage->setFieldArray(sfNFTokens, makeNFTokenIDs(1));
1255 nftPage->setFieldH256(sfNextPageMin, nftPage->key());
1256
1257 ac.view().insert(nftPage);
1258 return true;
1259 });
1260
1262 {{"NFT page is improperly linked"}},
1263 [&makeNFTokenIDs](Account const& a1, Account const& a2, ApplyContext& ac) {
1264 STArray nfTokens = makeNFTokenIDs(1);
1266 keylet::nftokenPageMax(a1), ++(nfTokens[0].getFieldH256(sfNFTokenID))));
1267 nftPage->setFieldArray(sfNFTokens, nfTokens);
1268 nftPage->setFieldH256(sfNextPageMin, keylet::nftokenPageMax(a2).key);
1269
1270 ac.view().insert(nftPage);
1271 return true;
1272 });
1273
1275 {{"NFT found in incorrect page"}},
1276 [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
1277 STArray nfTokens = makeNFTokenIDs(2);
1279 keylet::nftokenPageMax(a1), (nfTokens[1].getFieldH256(sfNFTokenID))));
1280 nftPage->setFieldArray(sfNFTokens, nfTokens);
1281
1282 ac.view().insert(nftPage);
1283 return true;
1284 });
1285 }
1286
1287 void
1289 {
1290 using namespace test::jtx;
1291
1292 bool const enforceAMMDelete = features[fixCleanup3_3_0];
1293 testcase << "AMM delete invariants" + std::string(enforceAMMDelete ? " fix" : "");
1294
1295 Env env(*this, features);
1296 Account const issuer{"issuer"};
1297 Issue const lptIssue{Currency(0x4c50540000000000), issuer.id()};
1298 STAmount const zeroLP{lptIssue, 0};
1299 STAmount const nonZeroLP{lptIssue, 1};
1300
1301 auto const makeAMM = [](STAmount const& lptBalance) {
1302 auto sleAMM = std::make_shared<SLE>(keylet::amm(uint256(1)));
1303 sleAMM->setFieldAmount(sfLPTokenBalance, lptBalance);
1304 return sleAMM;
1305 };
1306
1307 auto const checkInvariant = [&](TxType txType,
1308 TER result,
1309 std::optional<STAmount> const& deletedLPBalance,
1310 bool expected,
1311 std::string const& expectedLog) {
1313 beast::Journal const jlog{sink};
1314 ValidAMM invariant;
1315
1316 if (deletedLPBalance)
1317 invariant.visitEntry(true, makeAMM(*deletedLPBalance), nullptr);
1318
1319 bool const actual = invariant.finalize(
1320 STTx{txType, [](STObject&) {}}, result, XRPAmount{}, *env.current(), jlog);
1321
1322 BEAST_EXPECTS(actual == expected, "unexpected AMM delete invariant result");
1323 auto const messages = sink.messages().str();
1324 auto const expectedLogWhenEnforced = enforceAMMDelete ? expectedLog : "";
1325 if (!expectedLogWhenEnforced.empty())
1326 {
1327 BEAST_EXPECTS(messages.contains(expectedLogWhenEnforced), expectedLogWhenEnforced);
1328 }
1329 else
1330 {
1331 BEAST_EXPECTS(messages.empty(), messages);
1332 }
1333 };
1334
1335 checkInvariant(
1336 ttPAYMENT,
1337 tesSUCCESS,
1338 nonZeroLP,
1339 !enforceAMMDelete,
1340 "Invariant failed: AMM failed, unexpected AMM deletion by");
1341 checkInvariant(
1342 ttAMM_DELETE,
1343 tesSUCCESS,
1344 std::nullopt,
1345 !enforceAMMDelete,
1346 "Invariant failed: AMMDelete failed, AMM object remained on tesSUCCESS");
1347 checkInvariant(
1348 ttAMM_DELETE,
1349 tesSUCCESS,
1350 nonZeroLP,
1351 !enforceAMMDelete,
1352 "Invariant failed: AMMDelete failed, AMM object deleted with non-zero LP balance");
1353 checkInvariant(
1354 ttAMM_DELETE,
1356 zeroLP,
1357 !enforceAMMDelete,
1358 "Invariant failed: AMMDelete failed, AMM object deleted when result is not tesSUCCESS");
1359
1360 checkInvariant(ttAMM_WITHDRAW, tesSUCCESS, nonZeroLP, true, "");
1361 checkInvariant(ttAMM_CLAWBACK, tesSUCCESS, nonZeroLP, true, "");
1362
1363 checkInvariant(ttAMM_DELETE, tesSUCCESS, zeroLP, true, "");
1364 checkInvariant(ttAMM_WITHDRAW, tesSUCCESS, zeroLP, true, "");
1365 checkInvariant(ttAMM_CLAWBACK, tesSUCCESS, zeroLP, true, "");
1366 }
1367
1368 static SLE::pointer
1370 ApplyContext& ac,
1371 test::jtx::Account const& a1,
1372 test::jtx::Account const& a2,
1373 std::uint32_t numCreds = 2,
1374 std::uint32_t seq = 10)
1375 {
1376 Keylet const pdKeylet = keylet::permissionedDomain(a1.id(), seq);
1377 auto sle = std::make_shared<SLE>(pdKeylet);
1378
1379 sle->setAccountID(sfOwner, a1);
1380 sle->setFieldU32(sfSequence, seq);
1381
1382 if (numCreds != 0u)
1383 {
1384 // This array is sorted naturally, but if you are going to change
1385 // this behavior, don't forget to use credentials::makeSorted
1386 STArray credentials(sfAcceptedCredentials, numCreds);
1387 for (std::size_t n = 0; n < numCreds; ++n)
1388 {
1389 auto cred = STObject::makeInnerObject(sfCredential);
1390 cred.setAccountID(sfIssuer, a2);
1391 auto credType = "cred_type" + std::to_string(n);
1392 cred.setFieldVL(sfCredentialType, Slice(credType.c_str(), credType.size()));
1393 credentials.pushBack(std::move(cred));
1394 }
1395 sle->setFieldArray(sfAcceptedCredentials, credentials);
1396 }
1397
1398 ac.view().insert(sle);
1399 return sle;
1400 };
1401
1402 void
1404 {
1405 using namespace test::jtx;
1406
1407 bool const fixEnabled = features[fixCleanup3_1_3];
1410
1411 testcase << "PermissionedDomain" + std::string(fixEnabled ? " fix" : "");
1412
1414 Env(*this, features),
1415 {{"permissioned domain with no rules."}},
1416 [](Account const& a1, Account const& a2, ApplyContext& ac) {
1417 return createPermissionedDomain(ac, a1, a2, 0).get();
1418 },
1419 XRPAmount{},
1420 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
1421 fixEnabled ? failTers : badTers);
1422
1423 testcase << "PermissionedDomain 2";
1424
1425 static constexpr auto kTooBig = kMaxPermissionedDomainCredentialsArraySize + 1;
1427 Env(*this, features),
1428 {{"permissioned domain bad credentials size " + std::to_string(kTooBig)}},
1429 [](Account const& a1, Account const& a2, ApplyContext& ac) {
1430 return !!createPermissionedDomain(ac, a1, a2, kTooBig);
1431 },
1432 XRPAmount{},
1433 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
1434 fixEnabled ? failTers : badTers);
1435
1436 testcase << "PermissionedDomain 3";
1438 Env(*this, features),
1439 {{"permissioned domain credentials aren't sorted"}},
1440 [](Account const& a1, Account const& a2, ApplyContext& ac) {
1441 auto slePd = createPermissionedDomain(ac, a1, a2, 0);
1442
1443 STArray credentials(sfAcceptedCredentials, 2);
1444 for (std::size_t n = 0; n < 2; ++n)
1445 {
1446 auto cred = STObject::makeInnerObject(sfCredential);
1447 cred.setAccountID(sfIssuer, a2);
1448 auto credType = std::string("cred_type") + std::to_string(9 - n);
1449 cred.setFieldVL(sfCredentialType, Slice(credType.c_str(), credType.size()));
1450 credentials.pushBack(std::move(cred));
1451 }
1452 slePd->setFieldArray(sfAcceptedCredentials, credentials);
1453 ac.view().update(slePd);
1454 return true;
1455 },
1456 XRPAmount{},
1457 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
1458 fixEnabled ? failTers : badTers);
1459
1460 testcase << "PermissionedDomain 4";
1462 Env(*this, features),
1463 {{"permissioned domain credentials aren't unique"}},
1464 [](Account const& a1, Account const& a2, ApplyContext& ac) {
1465 auto slePd = createPermissionedDomain(ac, a1, a2, 0);
1466
1467 STArray credentials(sfAcceptedCredentials, 2);
1468 for (std::size_t n = 0; n < 2; ++n)
1469 {
1470 auto cred = STObject::makeInnerObject(sfCredential);
1471 cred.setAccountID(sfIssuer, a2);
1472 cred.setFieldVL(sfCredentialType, Slice("cred_type", 9));
1473 credentials.pushBack(std::move(cred));
1474 }
1475 slePd->setFieldArray(sfAcceptedCredentials, credentials);
1476 ac.view().update(slePd);
1477 return true;
1478 },
1479 XRPAmount{},
1480 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
1481 fixEnabled ? failTers : badTers);
1482
1483 testcase << "PermissionedDomain Set 1";
1485 Env(*this, features),
1486 {{"permissioned domain with no rules."}},
1487 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1488 // create PD
1489 auto slePd = createPermissionedDomain(ac, a1, a2);
1490
1491 // update PD with empty rules
1492 {
1493 STArray const credentials(sfAcceptedCredentials, 2);
1494 slePd->setFieldArray(sfAcceptedCredentials, credentials);
1495 ac.view().update(slePd);
1496 }
1497
1498 return true;
1499 },
1500 XRPAmount{},
1501 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
1502 fixEnabled ? failTers : badTers);
1503
1504 testcase << "PermissionedDomain Set 2";
1506 Env(*this, features),
1507 {{"permissioned domain bad credentials size " + std::to_string(kTooBig)}},
1508 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1509 // create PD
1510 auto slePd = createPermissionedDomain(ac, a1, a2);
1511
1512 // update PD
1513 {
1514 STArray credentials(sfAcceptedCredentials, kTooBig);
1515
1516 for (std::size_t n = 0; n < kTooBig; ++n)
1517 {
1518 auto cred = STObject::makeInnerObject(sfCredential);
1519 cred.setAccountID(sfIssuer, a2);
1520 auto credType = "cred_type2" + std::to_string(n);
1521 cred.setFieldVL(sfCredentialType, Slice(credType.c_str(), credType.size()));
1522 credentials.pushBack(std::move(cred));
1523 }
1524
1525 slePd->setFieldArray(sfAcceptedCredentials, credentials);
1526 ac.view().update(slePd);
1527 }
1528
1529 return true;
1530 },
1531 XRPAmount{},
1532 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
1533 fixEnabled ? failTers : badTers);
1534
1535 testcase << "PermissionedDomain Set 3";
1537 Env(*this, features),
1538 {{"permissioned domain credentials aren't sorted"}},
1539 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1540 // create PD
1541 auto slePd = createPermissionedDomain(ac, a1, a2);
1542
1543 // update PD
1544 {
1545 STArray credentials(sfAcceptedCredentials, 2);
1546 for (std::size_t n = 0; n < 2; ++n)
1547 {
1548 auto cred = STObject::makeInnerObject(sfCredential);
1549 cred.setAccountID(sfIssuer, a2);
1550 auto credType = std::string("cred_type2") + std::to_string(9 - n);
1551 cred.setFieldVL(sfCredentialType, Slice(credType.c_str(), credType.size()));
1552 credentials.pushBack(std::move(cred));
1553 }
1554
1555 slePd->setFieldArray(sfAcceptedCredentials, credentials);
1556 ac.view().update(slePd);
1557 }
1558
1559 return true;
1560 },
1561 XRPAmount{},
1562 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
1563 fixEnabled ? failTers : badTers);
1564
1565 testcase << "PermissionedDomain Set 4";
1567 Env(*this, features),
1568 {{"permissioned domain credentials aren't unique"}},
1569 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1570 // create PD
1571 auto slePd = createPermissionedDomain(ac, a1, a2);
1572
1573 // update PD
1574 {
1575 STArray credentials(sfAcceptedCredentials, 2);
1576 for (std::size_t n = 0; n < 2; ++n)
1577 {
1578 auto cred = STObject::makeInnerObject(sfCredential);
1579 cred.setAccountID(sfIssuer, a2);
1580 cred.setFieldVL(sfCredentialType, Slice("cred_type", 9));
1581 credentials.pushBack(std::move(cred));
1582 }
1583 slePd->setFieldArray(sfAcceptedCredentials, credentials);
1584 ac.view().update(slePd);
1585 }
1586
1587 return true;
1588 },
1589 XRPAmount{},
1590 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
1591 fixEnabled ? failTers : badTers);
1592
1594
1595 std::vector<std::string> const badMoreThan1{
1596 {"transaction affected more than 1 permissioned domain entry."}};
1597 std::vector<std::string> const emptyV;
1598 std::vector<std::string> const badNoDomains{{"no domain objects affected by"}};
1599 std::vector<std::string> const badNotDeleted{
1600 {"domain object modified, but not deleted by "}};
1601 std::vector<std::string> const badDeleted{{"domain object deleted by"}};
1602 std::vector<std::string> const badTx{
1603 {"domain object(s) affected by an unauthorized transaction."}};
1604
1605 {
1606 testcase << "PermissionedDomain set 2 domains ";
1608 Env(*this, features),
1609 fixEnabled ? badMoreThan1 : emptyV,
1610 [](Account const& a1, Account const& a2, ApplyContext& ac) {
1611 createPermissionedDomain(ac, a1, a2);
1612 createPermissionedDomain(ac, a1, a2, 2, 11);
1613 return true;
1614 },
1615 XRPAmount{},
1616 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
1617 fixEnabled ? failTers : goodTers);
1618 }
1619
1620 {
1621 testcase << "PermissionedDomain del 2 domains";
1622
1623 Env env1(*this, features);
1624
1625 Account const a1{"A1"};
1626 Account const a2{"A2"};
1627 env1.fund(XRP(1000), a1, a2);
1628 env1.close();
1629
1630 [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
1631 [[maybe_unused]] auto [seq2, pd2] = createPermissionedDomainEnv(env1, a1, a2);
1632 env1.close();
1633
1635 std::move(env1),
1636 a1,
1637 a2,
1638 fixEnabled ? badMoreThan1 : emptyV,
1639 [&pd1, &pd2](Account const&, Account const&, ApplyContext& ac) {
1640 auto sle1 = ac.view().peek({ltPERMISSIONED_DOMAIN, pd1});
1641 auto sle2 = ac.view().peek({ltPERMISSIONED_DOMAIN, pd2});
1642 ac.view().erase(sle1);
1643 ac.view().erase(sle2);
1644 return true;
1645 },
1646 XRPAmount{},
1647 STTx{ttPERMISSIONED_DOMAIN_DELETE, [](STObject&) {}},
1648 fixEnabled ? failTers : goodTers);
1649 }
1650
1651 {
1652 testcase << "PermissionedDomain set 0 domains ";
1654 Env(*this, features),
1655 fixEnabled ? badNoDomains : emptyV,
1656 [](Account const&, Account const&, ApplyContext&) { return true; },
1657 XRPAmount{},
1658 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
1659 fixEnabled ? badTers : goodTers);
1660 }
1661
1662 {
1663 testcase << "PermissionedDomain del 0 domains";
1664
1665 Env env1(*this, features);
1666
1667 Account const a1{"A1"};
1668 Account const a2{"A2"};
1669 env1.fund(XRP(1000), a1, a2);
1670 env1.close();
1671
1672 [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
1673 [[maybe_unused]] auto [seq2, pd2] = createPermissionedDomainEnv(env1, a1, a2);
1674 env1.close();
1675
1677 Env(*this, features),
1678 a1,
1679 a2,
1680 fixEnabled ? badNoDomains : emptyV,
1681 [](Account const&, Account const&, ApplyContext&) { return true; },
1682 XRPAmount{},
1683 STTx{ttPERMISSIONED_DOMAIN_DELETE, [](STObject&) {}},
1684 fixEnabled ? badTers : goodTers);
1685 }
1686
1687 {
1688 testcase << "PermissionedDomain set, delete domain";
1689
1690 Env env1(*this, features);
1691
1692 Account const a1{"A1"};
1693 Account const a2{"A2"};
1694 env1.fund(XRP(1000), a1, a2);
1695 env1.close();
1696
1697 [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
1698 env1.close();
1699
1701 std::move(env1),
1702 a1,
1703 a2,
1704 fixEnabled ? badDeleted : emptyV,
1705 [&pd1](Account const&, Account const&, ApplyContext& ac) {
1706 auto sle1 = ac.view().peek({ltPERMISSIONED_DOMAIN, pd1});
1707 ac.view().erase(sle1);
1708 return true;
1709 },
1710 XRPAmount{},
1711 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
1712 fixEnabled ? failTers : goodTers);
1713 }
1714
1715 {
1716 testcase << "PermissionedDomain del, create domain ";
1718 Env(*this, features),
1719 fixEnabled ? badNotDeleted : emptyV,
1720 [](Account const& a1, Account const& a2, ApplyContext& ac) {
1721 createPermissionedDomain(ac, a1, a2);
1722 return true;
1723 },
1724 XRPAmount{},
1725 STTx{ttPERMISSIONED_DOMAIN_DELETE, [](STObject&) {}},
1726 fixEnabled ? failTers : goodTers);
1727 }
1728
1729 {
1730 testcase << "PermissionedDomain invalid tx";
1731
1733 fixEnabled ? badTx : emptyV,
1734 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1735 createPermissionedDomain(ac, a1, a2);
1736 return true;
1737 },
1738 XRPAmount{},
1739 STTx{ttPAYMENT, [](STObject&) {}},
1740 failTers);
1741 }
1742 }
1743
1744 void
1746 {
1747 testcase << "valid pseudo accounts";
1748
1749 using namespace jtx;
1750
1751 AccountID pseudoAccountID;
1752 Preclose const createPseudo = [&, this](Account const& a, Account const& b, Env& env) {
1753 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
1754
1755 // Create vault
1756 Vault const vault{env};
1757 auto [tx, vKeylet] = vault.create({.owner = a, .asset = xrpAsset});
1758 env(tx);
1759 env.close();
1760 if (auto const vSle = env.le(vKeylet); BEAST_EXPECT(vSle))
1761 {
1762 pseudoAccountID = vSle->at(sfAccount);
1763 }
1764
1765 return BEAST_EXPECT(env.le(keylet::account(pseudoAccountID)));
1766 };
1767
1768 /* Cases to check
1769 "pseudo-account has 0 pseudo-account fields set"
1770 "pseudo-account has 2 pseudo-account fields set"
1771 "pseudo-account sequence changed"
1772 "pseudo-account flags are not set"
1773 "pseudo-account has a regular key"
1774 */
1775 struct Mod
1776 {
1777 std::string expectedFailure;
1778 std::function<void(SLE::pointer&)> func;
1779 };
1780 auto const mods = std::to_array<Mod>({
1781 {
1782 .expectedFailure = "pseudo-account has 0 pseudo-account fields set",
1783 .func =
1784 [this](SLE::pointer& sle) {
1785 BEAST_EXPECT(sle->at(~sfVaultID));
1786 sle->at(~sfVaultID) = std::nullopt;
1787 },
1788 },
1789 {
1790 .expectedFailure = "pseudo-account sequence changed",
1791 .func = [](SLE::pointer& sle) { sle->at(sfSequence) = 12345; },
1792 },
1793 {
1794 .expectedFailure = "pseudo-account flags are not set",
1795 .func = [](SLE::pointer& sle) { sle->at(sfFlags) = lsfNoFreeze; },
1796 },
1797 {
1798 .expectedFailure = "pseudo-account has a regular key",
1799 .func = [](SLE::pointer& sle) { sle->at(sfRegularKey) = Account("regular").id(); },
1800 },
1801 });
1802
1803 for (auto const& mod : mods)
1804 {
1806 {{mod.expectedFailure}},
1807 [&](Account const& a1, Account const&, ApplyContext& ac) {
1808 auto sle = ac.view().peek(keylet::account(pseudoAccountID));
1809 if (!sle)
1810 return false;
1811 mod.func(sle);
1812 ac.view().update(sle);
1813 return true;
1814 },
1815 XRPAmount{},
1816 STTx{ttACCOUNT_SET, [](STObject& tx) {}},
1818 createPseudo);
1819 }
1820 for (auto const pField : getPseudoAccountFields())
1821 {
1822 // createPseudo creates a vault, so sfVaultID will be set, and
1823 // setting it again will not cause an error
1824 if (pField == &sfVaultID)
1825 continue;
1827 {{"pseudo-account has 2 pseudo-account fields set"}},
1828 [&](Account const& a1, Account const&, ApplyContext& ac) {
1829 auto sle = ac.view().peek(keylet::account(pseudoAccountID));
1830 if (!sle)
1831 return false;
1832
1833 auto const vaultID = ~sle->at(~sfVaultID);
1834 BEAST_EXPECT(vaultID && !sle->isFieldPresent(*pField));
1835 sle->setFieldH256(*pField, *vaultID);
1836
1837 ac.view().update(sle);
1838 return true;
1839 },
1840 XRPAmount{},
1841 STTx{ttACCOUNT_SET, [](STObject& tx) {}},
1843 createPseudo);
1844 }
1845
1846 // Take one of the regular accounts and set the sequence to 0, which
1847 // will make it look like a pseudo-account
1849 {{"pseudo-account has 0 pseudo-account fields set"},
1850 {"pseudo-account sequence changed"},
1851 {"pseudo-account flags are not set"}},
1852 [&](Account const& a1, Account const&, ApplyContext& ac) {
1853 auto sle = ac.view().peek(keylet::account(a1.id()));
1854 if (!sle)
1855 return false;
1856 sle->at(sfSequence) = 0;
1857 ac.view().update(sle);
1858 return true;
1859 });
1860 }
1861
1864 test::jtx::Env& env,
1865 test::jtx::Account const& a1,
1866 test::jtx::Account const& a2,
1867 std::uint32_t numCreds = 2)
1868 {
1869 using namespace test::jtx;
1870
1872
1873 for (std::size_t n = 0; n < numCreds; ++n)
1874 {
1875 auto credType = "cred_type" + std::to_string(n);
1876 credentials.push_back({.issuer = a2, .credType = credType});
1877 }
1878
1879 std::uint32_t const seq = env.seq(a1);
1880 env(pdomain::setTx(a1, credentials));
1881 uint256 const key = pdomain::getNewDomain(env.meta());
1882
1883 // std::cout << "PD, acc: " << A1.id() << ", seq: " << seq << ", k: " <<
1884 // key << std::endl;
1885 return {seq, key};
1886 }
1887
1888 void
1890 {
1891 using namespace test::jtx;
1892
1893 bool const fixEnabled = features[fixCleanup3_1_3];
1894
1895 testcase << "PermissionedDEX" + std::string(fixEnabled ? " fix" : "");
1896
1898 Env(*this, features),
1899 {{"domain doesn't exist"}},
1900 [](Account const& a1, Account const&, ApplyContext& ac) {
1901 Keylet const offerKey = keylet::offer(a1.id(), 10);
1902 auto sleOffer = std::make_shared<SLE>(offerKey);
1903 sleOffer->setAccountID(sfAccount, a1);
1904 sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
1905 sleOffer->setFieldAmount(sfTakerGets, XRP(1));
1906 ac.view().insert(sleOffer);
1907 return true;
1908 },
1909 XRPAmount{},
1910 STTx{
1911 ttOFFER_CREATE,
1912 [](STObject& tx) {
1913 tx.setFieldH256(
1914 sfDomainID,
1915 uint256{"F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E33"
1916 "70F3649CE134E5"});
1917 Account const a1{"A1"};
1918 tx.setFieldAmount(sfTakerPays, a1["USD"](10));
1919 tx.setFieldAmount(sfTakerGets, XRP(1));
1920 }},
1922
1923 // missing domain ID in offer object
1925 Env(*this, features),
1926 {{"hybrid offer is malformed"}},
1927 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1928 Keylet const offerKey = keylet::offer(a2.id(), 10);
1929 auto sleOffer = std::make_shared<SLE>(offerKey);
1930 sleOffer->setAccountID(sfAccount, a2);
1931 sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
1932 sleOffer->setFieldAmount(sfTakerGets, XRP(1));
1933 sleOffer->setFlag(lsfHybrid);
1934
1935 STArray bookArr;
1936 bookArr.pushBack(STObject::makeInnerObject(sfBook));
1937 sleOffer->setFieldArray(sfAdditionalBooks, bookArr);
1938 ac.view().insert(sleOffer);
1939 return true;
1940 },
1941 XRPAmount{},
1942 STTx{ttOFFER_CREATE, [&](STObject&) {}},
1944
1945 // more than one entry in sfAdditionalBooks
1946 {
1947 Env env1(*this, features);
1948
1949 Account const a1{"A1"};
1950 Account const a2{"A2"};
1951 env1.fund(XRP(1000), a1, a2);
1952 env1.close();
1953
1954 [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
1955 env1.close();
1956
1958 std::move(env1),
1959 a1,
1960 a2,
1961 {{"hybrid offer is malformed"}},
1962 [&pd1](Account const& a1, Account const& a2, ApplyContext& ac) {
1963 Keylet const offerKey = keylet::offer(a2.id(), 10);
1964 auto sleOffer = std::make_shared<SLE>(offerKey);
1965 sleOffer->setAccountID(sfAccount, a2);
1966 sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
1967 sleOffer->setFieldAmount(sfTakerGets, XRP(1));
1968 sleOffer->setFlag(lsfHybrid);
1969 sleOffer->setFieldH256(sfDomainID, pd1);
1970
1971 STArray bookArr;
1972 bookArr.pushBack(STObject::makeInnerObject(sfBook));
1973 bookArr.pushBack(STObject::makeInnerObject(sfBook));
1974 sleOffer->setFieldArray(sfAdditionalBooks, bookArr);
1975 ac.view().insert(sleOffer);
1976 return true;
1977 },
1978 XRPAmount{},
1979 STTx{ttOFFER_CREATE, [&](STObject&) {}},
1981 }
1982
1983 // empty sfAdditionalBooks (size 0)
1984 {
1985 Env env1(*this, features);
1986
1987 Account const a1{"A1"};
1988 Account const a2{"A2"};
1989 env1.fund(XRP(1000), a1, a2);
1990 env1.close();
1991
1992 [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
1993 env1.close();
1994
1996 std::move(env1),
1997 a1,
1998 a2,
1999 fixEnabled ? std::vector<std::string>{{"hybrid offer is malformed"}}
2001 [&pd1](Account const& a1, Account const& a2, ApplyContext& ac) {
2002 Keylet const offerKey = keylet::offer(a2.id(), 10);
2003 auto sleOffer = std::make_shared<SLE>(offerKey);
2004 sleOffer->setAccountID(sfAccount, a2);
2005 sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
2006 sleOffer->setFieldAmount(sfTakerGets, XRP(1));
2007 sleOffer->setFlag(lsfHybrid);
2008 sleOffer->setFieldH256(sfDomainID, pd1);
2009
2010 STArray const bookArr; // empty array, size 0
2011 sleOffer->setFieldArray(sfAdditionalBooks, bookArr);
2012 ac.view().insert(sleOffer);
2013 return true;
2014 },
2015 XRPAmount{},
2016 STTx{ttOFFER_CREATE, [&](STObject&) {}},
2019 }
2020
2021 // hybrid offer missing sfAdditionalBooks
2022 {
2023 Env env1(*this, features);
2024
2025 Account const a1{"A1"};
2026 Account const a2{"A2"};
2027 env1.fund(XRP(1000), a1, a2);
2028 env1.close();
2029
2030 [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
2031 env1.close();
2032
2034 std::move(env1),
2035 a1,
2036 a2,
2037 {{"hybrid offer is malformed"}},
2038 [&pd1](Account const& a1, Account const& a2, ApplyContext& ac) {
2039 Keylet const offerKey = keylet::offer(a2.id(), 10);
2040 auto sleOffer = std::make_shared<SLE>(offerKey);
2041 sleOffer->setAccountID(sfAccount, a2);
2042 sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
2043 sleOffer->setFieldAmount(sfTakerGets, XRP(1));
2044 sleOffer->setFlag(lsfHybrid);
2045 sleOffer->setFieldH256(sfDomainID, pd1);
2046 ac.view().insert(sleOffer);
2047 return true;
2048 },
2049 XRPAmount{},
2050 STTx{ttOFFER_CREATE, [&](STObject&) {}},
2052 }
2053
2054 {
2055 Env env1(*this, features);
2056
2057 Account const a1{"A1"};
2058 Account const a2{"A2"};
2059 env1.fund(XRP(1000), a1, a2);
2060 env1.close();
2061
2062 [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
2063 [[maybe_unused]] auto [seq2, pd2] = createPermissionedDomainEnv(env1, a1, a2);
2064 env1.close();
2065
2067 std::move(env1),
2068 a1,
2069 a2,
2070 {{"transaction consumed wrong domains"}},
2071 [&pd1](Account const& a1, Account const& a2, ApplyContext& ac) {
2072 Keylet const offerKey = keylet::offer(a2.id(), 10);
2073 auto sleOffer = std::make_shared<SLE>(offerKey);
2074 sleOffer->setAccountID(sfAccount, a2);
2075 sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
2076 sleOffer->setFieldAmount(sfTakerGets, XRP(1));
2077 sleOffer->setFieldH256(sfDomainID, pd1);
2078 ac.view().insert(sleOffer);
2079 return true;
2080 },
2081 XRPAmount{},
2082 STTx{
2083 ttOFFER_CREATE,
2084 [&pd2, &a1](STObject& tx) {
2085 tx.setFieldH256(sfDomainID, pd2);
2086 tx.setFieldAmount(sfTakerPays, a1["USD"](10));
2087 tx.setFieldAmount(sfTakerGets, XRP(1));
2088 }},
2090 }
2091
2092 {
2093 Env env1(*this, features);
2094
2095 Account const a1{"A1"};
2096 Account const a2{"A2"};
2097 env1.fund(XRP(1000), a1, a2);
2098 env1.close();
2099
2100 [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
2101 env1.close();
2102
2104 std::move(env1),
2105 a1,
2106 a2,
2107 {{"domain transaction affected regular offers"}},
2108 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2109 Keylet const offerKey = keylet::offer(a2.id(), 10);
2110 auto sleOffer = std::make_shared<SLE>(offerKey);
2111 sleOffer->setAccountID(sfAccount, a2);
2112 sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
2113 sleOffer->setFieldAmount(sfTakerGets, XRP(1));
2114 ac.view().insert(sleOffer);
2115 return true;
2116 },
2117 XRPAmount{},
2118 STTx{
2119 ttOFFER_CREATE,
2120 [&](STObject& tx) {
2121 Account const a1{"A1"};
2122 tx.setFieldH256(sfDomainID, pd1);
2123 tx.setFieldAmount(sfTakerPays, a1["USD"](10));
2124 tx.setFieldAmount(sfTakerGets, XRP(1));
2125 }},
2127 }
2128 }
2129
2130 void
2132 {
2133 using namespace test::jtx;
2134 testcase << "book directory exchange rate";
2135
2136 auto const getBookRootKey = [](Account const& account, std::uint64_t quality) {
2137 Book const book{xrpIssue(), account["USD"], std::nullopt};
2138 return keylet::quality(keylet::book(book), quality);
2139 };
2140
2141 // Root book-directory pages carry exchange-rate metadata that must
2142 // match the quality encoded in the directory key.
2143 auto const makeRootPage = [](Keylet const& dir, std::uint64_t exchangeRate) {
2144 auto sleDir = std::make_shared<SLE>(dir);
2145 sleDir->setFieldH256(sfRootIndex, dir.key);
2146 STVector256 indexes;
2147 indexes.pushBack(uint256{1});
2148 sleDir->setFieldV256(sfIndexes, indexes);
2149 sleDir->setFieldU64(sfExchangeRate, exchangeRate);
2150 return sleDir;
2151 };
2152
2153 // Child pages do not carry quality metadata; they only point back to
2154 // the root directory.
2155 auto const makeChildPage = [](Keylet const& rootDir) {
2156 auto sleDir = std::make_shared<SLE>(keylet::page(rootDir, 1));
2157 sleDir->setFieldH256(sfRootIndex, rootDir.key);
2158 STVector256 indexes;
2159 indexes.pushBack(uint256{2});
2160 sleDir->setFieldV256(sfIndexes, indexes);
2161 return sleDir;
2162 };
2163
2164 auto const makeOfferCreateTx = [] {
2165 return STTx{ttOFFER_CREATE, [](STObject& tx) {
2166 Account const account{"A1"};
2167 tx.setFieldAmount(sfTakerPays, XRP(1));
2168 tx.setFieldAmount(sfTakerGets, account["USD"](1));
2169 }};
2170 };
2172
2173 // Creating a root book directory with mismatched exchange-rate
2174 // metadata violates the invariant.
2176 {{"book directory exchange rate does not match directory quality"}},
2177 [&](Account const& a1, Account const&, ApplyContext& ac) {
2178 auto const directoryQuality = STAmount::kURateOne;
2179 auto const dir = getBookRootKey(a1, directoryQuality);
2180 ac.view().insert(makeRootPage(dir, directoryQuality + 1));
2181 return true;
2182 },
2183 XRPAmount{},
2184 makeOfferCreateTx(),
2185 failTers);
2186
2187 // A new child page must point to an existing root page.
2189 {{"book directory root missing"}},
2190 [&](Account const& a1, Account const&, ApplyContext& ac) {
2191 auto const directoryQuality = STAmount::kURateOne;
2192 auto const rootDir = getBookRootKey(a1, directoryQuality);
2193 // Insert only the child page. It points at rootDir, but the
2194 // corresponding root page is intentionally missing.
2195 ac.view().insert(makeChildPage(rootDir));
2196 return true;
2197 },
2198 XRPAmount{},
2199 makeOfferCreateTx(),
2200 failTers);
2201
2202 // Legacy bad-root tolerance:
2203 // - The view contains a pre-existing root page with bad sfExchangeRate
2204 // metadata.
2205 // - The simulated transaction only creates a child page pointing to
2206 // that root.
2207 // - The invariant must pass because this transaction did not create
2208 // the bad root, only adding a child page.
2209 {
2210 Env env{*this, defaultAmendments()};
2211 Account const a1{"A1"};
2212 env.fund(XRP(1000), a1);
2213 env.close();
2214
2215 OpenView view{*env.current()};
2216 auto const directoryQuality = STAmount::kURateOne;
2217 auto const rootDir = getBookRootKey(a1, directoryQuality);
2218 view.rawInsert(makeRootPage(rootDir, directoryQuality + 1));
2219
2220 ValidBookDirectory invariant;
2221 invariant.visitEntry(false, nullptr, makeChildPage(rootDir));
2222
2224 beast::Journal const jlog{sink};
2225 BEAST_EXPECT(
2226 invariant.finalize(makeOfferCreateTx(), tesSUCCESS, XRPAmount{}, view, jlog));
2227 }
2228
2229 // A bad root is rejected when added, ignored when a legacy bad root is
2230 // modified without changing sfRootIndex or deleted, and checked when a
2231 // modified directory changes sfRootIndex.
2232 {
2233 Env env{*this, defaultAmendments()};
2234 Account const a1{"A1"};
2235 env.fund(XRP(1000), a1);
2236 env.close();
2237
2238 OpenView view{*env.current()};
2239 auto const directoryQuality = STAmount::kURateOne;
2240 auto const rootDir = getBookRootKey(a1, directoryQuality);
2241 auto const missingRootDir = getBookRootKey(a1, directoryQuality + 1);
2242 auto const badRoot = makeRootPage(rootDir, directoryQuality + 1);
2243 view.rawInsert(badRoot);
2244
2246 beast::Journal const jlog{sink};
2247
2248 {
2249 // add
2250 ValidBookDirectory invariant;
2251 invariant.visitEntry(false, nullptr, badRoot);
2252
2253 BEAST_EXPECT(
2254 !invariant.finalize(makeOfferCreateTx(), tesSUCCESS, XRPAmount{}, view, jlog));
2255 }
2256 {
2257 // modify (without changing the sfRootIndex)
2258 ValidBookDirectory invariant;
2259 invariant.visitEntry(false, badRoot, badRoot);
2260
2261 BEAST_EXPECT(
2262 invariant.finalize(makeOfferCreateTx(), tesSUCCESS, XRPAmount{}, view, jlog));
2263 }
2264 {
2265 // modify (changing sfRootIndex to a missing root)
2266 auto const childBefore = makeChildPage(rootDir);
2267 auto const childAfter = std::make_shared<SLE>(*childBefore, childBefore->key());
2268 childAfter->setFieldH256(sfRootIndex, missingRootDir.key);
2269
2270 ValidBookDirectory invariant;
2271 invariant.visitEntry(false, childBefore, childAfter);
2272
2274 beast::Journal const missingRootJlog{missingRootSink};
2275 BEAST_EXPECT(!invariant.finalize(
2276 makeOfferCreateTx(), tesSUCCESS, XRPAmount{}, view, missingRootJlog));
2277 BEAST_EXPECT(
2278 missingRootSink.messages().str().contains("book directory root missing"));
2279 }
2280 {
2281 // delete
2282 view.rawErase(badRoot);
2283 BEAST_EXPECT(!view.exists(rootDir));
2284
2285 ValidBookDirectory invariant;
2286 invariant.visitEntry(true, badRoot, badRoot);
2287 BEAST_EXPECT(
2288 invariant.finalize(makeOfferCreateTx(), tesSUCCESS, XRPAmount{}, view, jlog));
2289 }
2290 }
2291 }
2292
2293 Keylet
2295 {
2296 using namespace jtx;
2297
2298 // Create vault
2299 uint256 vaultID;
2300 Vault const vault{env};
2301 auto [tx, vKeylet] = vault.create({.owner = a, .asset = asset});
2302 env(tx);
2303 BEAST_EXPECT(env.le(vKeylet));
2304
2305 vaultID = vKeylet.key;
2306
2307 // Create Loan Broker
2308 using namespace loanBroker;
2309
2310 auto const loanBrokerKeylet = keylet::loanBroker(a.id(), env.seq(a));
2311 // Create a Loan Broker with all default values.
2312 env(set(a, vaultID), Fee(kIncrement));
2313
2314 return loanBrokerKeylet;
2315 };
2316
2317 void
2319 {
2320 testcase("no modified unmodifiable fields");
2321 using namespace jtx;
2322
2323 // Initialize with a placeholder value because there's no default ctor
2324 Keylet loanBrokerKeylet = keylet::amendments();
2325 Preclose const createLoanBroker = [&, this](Account const& a, Account const& b, Env& env) {
2326 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
2327
2328 loanBrokerKeylet = this->createLoanBroker(a, env, xrpAsset);
2329 return BEAST_EXPECT(env.le(loanBrokerKeylet));
2330 };
2331
2332 {
2333 auto const mods = std::to_array<std::function<void(SLE::pointer&)>>({
2334 [](SLE::pointer& sle) { sle->at(sfSequence) += 1; },
2335 [](SLE::pointer& sle) { sle->at(sfOwnerNode) += 1; },
2336 [](SLE::pointer& sle) { sle->at(sfVaultNode) += 1; },
2337 [](SLE::pointer& sle) { sle->at(sfVaultID) = uint256(1u); },
2338 [](SLE::pointer& sle) { sle->at(sfAccount) = sle->at(sfOwner); },
2339 [](SLE::pointer& sle) { sle->at(sfOwner) = sle->at(sfAccount); },
2340 [](SLE::pointer& sle) { sle->at(sfManagementFeeRate) += 1; },
2341 [](SLE::pointer& sle) { sle->at(sfCoverRateMinimum) += 1; },
2342 [](SLE::pointer& sle) { sle->at(sfCoverRateLiquidation) += 1; },
2343 [](SLE::pointer& sle) { sle->at(sfLedgerEntryType) += 1; },
2344 [](SLE::pointer& sle) { sle->at(sfLedgerIndex) = sle->at(sfVaultID).value(); },
2345 });
2346
2347 for (auto const& mod : mods)
2348 {
2350 {{"changed an unchangeable field"}},
2351 [&](Account const& a1, Account const&, ApplyContext& ac) {
2352 auto sle = ac.view().peek(loanBrokerKeylet);
2353 if (!sle)
2354 return false;
2355 mod(sle);
2356 ac.view().update(sle);
2357 return true;
2358 },
2359 XRPAmount{},
2360 STTx{ttACCOUNT_SET, [](STObject& tx) {}},
2363 }
2364 }
2365
2366 // TODO: Loan Object
2367
2368 {
2369 auto const mods = std::to_array<std::function<void(SLE::pointer&)>>({
2370 [](SLE::pointer& sle) { sle->at(sfLedgerEntryType) += 1; },
2371 [](SLE::pointer& sle) { sle->at(sfLedgerIndex) = uint256(1u); },
2372 });
2373
2374 for (auto const& mod : mods)
2375 {
2377 {{"changed an unchangeable field"}},
2378 [&](Account const& a1, Account const&, ApplyContext& ac) {
2379 auto sle = ac.view().peek(keylet::account(a1.id()));
2380 if (!sle)
2381 return false;
2382 mod(sle);
2383 ac.view().update(sle);
2384 return true;
2385 });
2386 }
2387 }
2388 }
2389
2390 void
2392 {
2393 testcase << "valid loan broker";
2394
2395 using namespace jtx;
2396
2397 enum class Asset { XRP, IOU, MPT };
2398 auto const assetTypes = std::to_array({Asset::XRP, Asset::IOU, Asset::MPT});
2399
2400 for (auto const assetType : assetTypes)
2401 {
2402 // Initialize with a placeholder value because there's no default
2403 // ctor
2404 auto const setupAsset =
2405 [&](Account const& alice, Account const& issuer, Env& env) -> PrettyAsset {
2406 switch (assetType)
2407 {
2408 case Asset::IOU: {
2409 PrettyAsset const iouAsset = issuer["IOU"];
2410 env(trust(alice, iouAsset(1000)));
2411 env(pay(issuer, alice, iouAsset(1000)));
2412 env.close();
2413 return iouAsset;
2414 }
2415 case Asset::MPT: {
2416 MPTTester mptt{env, issuer, kMptInitNoFund};
2417 mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
2418 PrettyAsset const mptAsset = mptt.issuanceID();
2419 mptt.authorize({.account = alice});
2420 env(pay(issuer, alice, mptAsset(1000)));
2421 env.close();
2422 return mptAsset;
2423 }
2424 case Asset::XRP:
2425 default:
2426 return PrettyAsset{xrpIssue(), 1'000'000};
2427 }
2428 };
2429
2430 Keylet loanBrokerKeylet = keylet::amendments();
2432 [&, this](Account const& alice, Account const& issuer, Env& env) {
2433 auto const asset = setupAsset(alice, issuer, env);
2434 loanBrokerKeylet = this->createLoanBroker(alice, env, asset);
2435 return BEAST_EXPECT(env.le(loanBrokerKeylet));
2436 };
2437
2438 // Ensure the test scenarios are set up completely. The test cases
2439 // will need to recompute any of these values it needs for itself
2440 // rather than trying to return a bunch of items
2441 auto setupTest = [&, this](Account const& a1, Account const&, ApplyContext& ac)
2443 if (loanBrokerKeylet.type != ltLOAN_BROKER)
2444 return {};
2445 auto sleBroker = ac.view().peek(loanBrokerKeylet);
2446 if (!sleBroker)
2447 return {};
2448 if (!BEAST_EXPECT(sleBroker->at(sfOwnerCount) == 0))
2449 return {};
2450 // Need to touch sleBroker so that it is included in the
2451 // modified entries for the invariant to find
2452 ac.view().update(sleBroker);
2453
2454 // The pseudo-account holds the directory, so get it
2455 auto const pseudoAccountID = sleBroker->at(sfAccount);
2456 auto const pseudoAccountKeylet = keylet::account(pseudoAccountID);
2457 // Strictly speaking, we don't need to load the
2458 // ACCOUNT_ROOT, but check anyway
2459 auto slePseudo = ac.view().peek(pseudoAccountKeylet);
2460 if (!BEAST_EXPECT(slePseudo))
2461 return {};
2462 // Make sure the directory doesn't already exist
2463 auto const dirKeylet = keylet::ownerDir(pseudoAccountID);
2464 auto sleDir = ac.view().peek(dirKeylet);
2465 auto const describe = describeOwnerDir(pseudoAccountID);
2466 if (!sleDir)
2467 {
2468 // Create the directory
2469 BEAST_EXPECT(
2471 ac.view(), dirKeylet, loanBrokerKeylet.key, describe) == 0);
2472
2473 sleDir = ac.view().peek(dirKeylet);
2474 }
2475
2476 return std::make_pair(slePseudo, sleDir);
2477 };
2478
2480 {{"Loan Broker with zero OwnerCount has multiple directory "
2481 "pages"}},
2482 [&setupTest, this](Account const& a1, Account const& a2, ApplyContext& ac) {
2483 auto test = setupTest(a1, a2, ac);
2484 if (!test || !test->first || !test->second)
2485 return false;
2486
2487 auto slePseudo = test->first;
2488 auto sleDir = test->second;
2489 auto const describe = describeOwnerDir(slePseudo->at(sfAccount));
2490
2491 BEAST_EXPECT(
2493 ac.view(),
2494 0,
2495 sleDir,
2496 0,
2497 sleDir,
2498 slePseudo->key(),
2499 keylet::page(sleDir->key(), 0),
2500 describe) == 1);
2501
2502 return true;
2503 },
2504 XRPAmount{},
2505 STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
2508
2510 {{"Loan Broker with zero OwnerCount has multiple indexes in "
2511 "the Directory root"}},
2512 [&setupTest](Account const& a1, Account const& a2, ApplyContext& ac) {
2513 auto test = setupTest(a1, a2, ac);
2514 if (!test || !test->first || !test->second)
2515 return false;
2516
2517 auto slePseudo = test->first;
2518 auto sleDir = test->second;
2519 auto indexes = sleDir->getFieldV256(sfIndexes);
2520
2521 // Put some extra garbage into the directory
2522 for (auto const& key : {slePseudo->key(), sleDir->key()})
2523 {
2524 ::xrpl::directory::insertKey(ac.view(), sleDir, 0, false, indexes, key);
2525 }
2526
2527 return true;
2528 },
2529 XRPAmount{},
2530 STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
2533
2535 {{"Loan Broker directory corrupt"}},
2536 [&setupTest](Account const& a1, Account const& a2, ApplyContext& ac) {
2537 auto test = setupTest(a1, a2, ac);
2538 if (!test || !test->first || !test->second)
2539 return false;
2540
2541 auto slePseudo = test->first;
2542 auto sleDir = test->second;
2543 auto const describe = describeOwnerDir(slePseudo->at(sfAccount));
2544 // Empty vector will overwrite the existing entry for the
2545 // holding, if any, avoiding the "has multiple indexes"
2546 // failure.
2547 STVector256 indexes;
2548
2549 // Put one meaningless key into the directory
2550 auto const key = keylet::account(Account("random").id()).key;
2551 ::xrpl::directory::insertKey(ac.view(), sleDir, 0, false, indexes, key);
2552
2553 return true;
2554 },
2555 XRPAmount{},
2556 STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
2559
2561 {{"Loan Broker with zero OwnerCount has an unexpected entry in "
2562 "the directory"}},
2563 [&setupTest](Account const& a1, Account const& a2, ApplyContext& ac) {
2564 auto test = setupTest(a1, a2, ac);
2565 if (!test || !test->first || !test->second)
2566 return false;
2567
2568 auto slePseudo = test->first;
2569 auto sleDir = test->second;
2570 // Empty vector will overwrite the existing entry for the
2571 // holding, if any, avoiding the "has multiple indexes"
2572 // failure.
2573 STVector256 indexes;
2574
2576 ac.view(), sleDir, 0, false, indexes, slePseudo->key());
2577
2578 return true;
2579 },
2580 XRPAmount{},
2581 STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
2584
2586 {{"Loan Broker sequence number decreased"}},
2587 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2588 if (loanBrokerKeylet.type != ltLOAN_BROKER)
2589 return false;
2590 auto sleBroker = ac.view().peek(loanBrokerKeylet);
2591 if (!sleBroker)
2592 return false;
2593 if (!BEAST_EXPECT(sleBroker->at(sfLoanSequence) > 0))
2594 return false;
2595 // Need to touch sleBroker so that it is included in the
2596 // modified entries for the invariant to find
2597 ac.view().update(sleBroker);
2598
2599 sleBroker->at(sfLoanSequence) -= 1;
2600
2601 return true;
2602 },
2603 XRPAmount{},
2604 STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
2607
2608 // Test: cover available less than pseudo-account asset balance
2609 {
2610 Keylet brokerKeylet = keylet::amendments();
2611 Preclose const createBrokerWithCover =
2612 [&, this](Account const& alice, Account const& issuer, Env& env) {
2613 auto const asset = setupAsset(alice, issuer, env);
2614 brokerKeylet = this->createLoanBroker(alice, env, asset);
2615 if (!BEAST_EXPECT(env.le(brokerKeylet)))
2616 return false;
2617 env(loanBroker::coverDeposit(alice, brokerKeylet.key, asset(10)));
2618 env.close();
2619 return BEAST_EXPECT(env.le(brokerKeylet));
2620 };
2621
2623 {{"Loan Broker cover available is less than pseudo-account asset balance"}},
2624 [&](Account const&, Account const&, ApplyContext& ac) {
2625 auto sle = ac.view().peek(brokerKeylet);
2626 if (!BEAST_EXPECT(sle))
2627 return false;
2628 // Pseudo-account holds 10 units, set cover to 5
2629 sle->at(sfCoverAvailable) = Number(5);
2630 ac.view().update(sle);
2631 return true;
2632 },
2633 XRPAmount{},
2634 STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
2636 createBrokerWithCover);
2637 }
2638
2639 // Test: cover available greater than pseudo-account asset balance
2640 // (requires fixCleanup3_1_3)
2642 {{"Loan Broker cover available is greater than pseudo-account asset balance"}},
2643 [&](Account const&, Account const&, ApplyContext& ac) {
2644 auto sle = ac.view().peek(loanBrokerKeylet);
2645 if (!BEAST_EXPECT(sle))
2646 return false;
2647 // Pseudo-account has no cover deposited; set cover
2648 // higher than any incidental balance
2649 sle->at(sfCoverAvailable) = Number(1'000'000);
2650 ac.view().update(sle);
2651 return true;
2652 },
2653 XRPAmount{},
2654 STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
2657 }
2658 }
2659
2660 void
2662 {
2663 using namespace test::jtx;
2664
2665 struct AccountAmount
2666 {
2667 AccountID account;
2668 int amount;
2669 };
2670 struct Adjustments
2671 {
2672 // NOLINTBEGIN(readability-redundant-member-init)
2673 std::optional<int> assetsTotal = std::nullopt;
2674 std::optional<int> assetsAvailable = std::nullopt;
2675 std::optional<int> lossUnrealized = std::nullopt;
2676 std::optional<int> assetsMaximum = std::nullopt;
2677 std::optional<int> sharesTotal = std::nullopt;
2678 std::optional<int> vaultAssets = std::nullopt;
2679 std::optional<AccountAmount> accountAssets = std::nullopt;
2680 std::optional<AccountAmount> accountShares = std::nullopt;
2681 // NOLINTEND(readability-redundant-member-init)
2682 };
2683 constexpr auto kAdjust = [&](ApplyView& ac, xrpl::Keylet keylet, Adjustments args) {
2684 auto sleVault = ac.peek(keylet);
2685 if (!sleVault)
2686 return false;
2687
2688 auto const mptIssuanceID = (*sleVault)[sfShareMPTID];
2689 auto sleShares = ac.peek(keylet::mptokenIssuance(mptIssuanceID));
2690 if (!sleShares)
2691 return false;
2692
2693 // These two fields are adjusted in absolute terms
2694 if (args.lossUnrealized)
2695 (*sleVault)[sfLossUnrealized] = *args.lossUnrealized;
2696 if (args.assetsMaximum)
2697 (*sleVault)[sfAssetsMaximum] = *args.assetsMaximum;
2698
2699 // Remaining fields are adjusted in terms of difference
2700 if (args.assetsTotal)
2701 (*sleVault)[sfAssetsTotal] = *(*sleVault)[sfAssetsTotal] + *args.assetsTotal;
2702 if (args.assetsAvailable)
2703 {
2704 (*sleVault)[sfAssetsAvailable] =
2705 *(*sleVault)[sfAssetsAvailable] + *args.assetsAvailable;
2706 }
2707 ac.update(sleVault);
2708
2709 if (args.sharesTotal)
2710 {
2711 (*sleShares)[sfOutstandingAmount] =
2712 *(*sleShares)[sfOutstandingAmount] + *args.sharesTotal;
2713 ac.update(sleShares);
2714 }
2715
2716 auto const assets = *(*sleVault)[sfAsset];
2717 auto const pseudoId = *(*sleVault)[sfAccount];
2718 if (args.vaultAssets)
2719 {
2720 if (assets.native())
2721 {
2722 auto slePseudoAccount = ac.peek(keylet::account(pseudoId));
2723 if (!slePseudoAccount)
2724 return false;
2725 (*slePseudoAccount)[sfBalance] =
2726 *(*slePseudoAccount)[sfBalance] + *args.vaultAssets;
2727 ac.update(slePseudoAccount);
2728 }
2729 else if (assets.holds<MPTIssue>())
2730 {
2731 auto const mptId = assets.get<MPTIssue>().getMptID();
2732 auto sleMPToken = ac.peek(keylet::mptoken(mptId, pseudoId));
2733 if (!sleMPToken)
2734 return false;
2735 (*sleMPToken)[sfMPTAmount] = *(*sleMPToken)[sfMPTAmount] + *args.vaultAssets;
2736 ac.update(sleMPToken);
2737 }
2738 else
2739 {
2740 return false; // Not supporting testing with IOU
2741 }
2742 }
2743
2744 if (args.accountAssets)
2745 {
2746 auto const& pair = *args.accountAssets;
2747 if (assets.native())
2748 {
2749 auto sleAccount = ac.peek(keylet::account(pair.account));
2750 if (!sleAccount)
2751 return false;
2752 (*sleAccount)[sfBalance] = *(*sleAccount)[sfBalance] + pair.amount;
2753 ac.update(sleAccount);
2754 }
2755 else if (assets.holds<MPTIssue>())
2756 {
2757 auto const mptID = assets.get<MPTIssue>().getMptID();
2758 auto sleMPToken = ac.peek(keylet::mptoken(mptID, pair.account));
2759 if (!sleMPToken)
2760 return false;
2761 (*sleMPToken)[sfMPTAmount] = *(*sleMPToken)[sfMPTAmount] + pair.amount;
2762 ac.update(sleMPToken);
2763 }
2764 else
2765 {
2766 return false; // Not supporting testing with IOU
2767 }
2768 }
2769
2770 if (args.accountShares)
2771 {
2772 auto const& pair = *args.accountShares;
2773 auto sleMPToken = ac.peek(keylet::mptoken(mptIssuanceID, pair.account));
2774 if (!sleMPToken)
2775 return false;
2776 (*sleMPToken)[sfMPTAmount] = *(*sleMPToken)[sfMPTAmount] + pair.amount;
2777 ac.update(sleMPToken);
2778 }
2779 return true;
2780 };
2781
2782 static constexpr auto kArgs = [](AccountID id, int adjustment, auto fn) -> Adjustments {
2783 Adjustments sample = {
2784 .assetsTotal = adjustment,
2785 .assetsAvailable = adjustment,
2786 .lossUnrealized = 0,
2787 .sharesTotal = adjustment,
2788 .vaultAssets = adjustment,
2789 .accountAssets = //
2790 AccountAmount{.account = id, .amount = -adjustment},
2791 .accountShares = //
2792 AccountAmount{.account = id, .amount = adjustment}};
2793 fn(sample);
2794 return sample;
2795 };
2796
2797 Account const a3{"A3"};
2798 Account const a4{"A4"};
2799 auto const precloseXrp = [&](Account const& a1, Account const& a2, Env& env) -> bool {
2800 env.fund(XRP(1000), a3, a4);
2801 Vault const vault{env};
2802 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
2803 env(tx);
2804 env(vault.deposit({.depositor = a1, .id = keylet.key, .amount = XRP(10)}));
2805 env(vault.deposit({.depositor = a2, .id = keylet.key, .amount = XRP(10)}));
2806 env(vault.deposit({.depositor = a3, .id = keylet.key, .amount = XRP(10)}));
2807 return true;
2808 };
2809
2810 testcase << "Vault general checks";
2812 {"vault deletion succeeded without deleting a vault"},
2813 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2814 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
2815 auto sleVault = ac.view().peek(keylet);
2816 if (!sleVault)
2817 return false;
2818 ac.view().update(sleVault);
2819 return true;
2820 },
2821 XRPAmount{},
2822 STTx{ttVAULT_DELETE, [](STObject&) {}},
2824 [&](Account const& a1, Account const& a2, Env& env) {
2825 Vault const vault{env};
2826 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
2827 env(tx);
2828 return true;
2829 });
2830
2832 {"vault updated by a wrong transaction type"},
2833 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2834 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
2835 auto sleVault = ac.view().peek(keylet);
2836 if (!sleVault)
2837 return false;
2838 ac.view().erase(sleVault);
2839 return true;
2840 },
2841 XRPAmount{},
2842 STTx{ttPAYMENT, [](STObject&) {}},
2844 [&](Account const& a1, Account const& a2, Env& env) {
2845 Vault const vault{env};
2846 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
2847 env(tx);
2848 return true;
2849 });
2850
2852 {"vault updated by a wrong transaction type"},
2853 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2854 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
2855 auto sleVault = ac.view().peek(keylet);
2856 if (!sleVault)
2857 return false;
2858 ac.view().update(sleVault);
2859 return true;
2860 },
2861 XRPAmount{},
2862 STTx{ttPAYMENT, [](STObject&) {}},
2864 [&](Account const& a1, Account const& a2, Env& env) {
2865 Vault const vault{env};
2866 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
2867 env(tx);
2868 return true;
2869 });
2870
2872 {"vault updated by a wrong transaction type"},
2873 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2874 auto const sequence = ac.view().seq();
2875 auto const vaultKeylet = keylet::vault(a1.id(), sequence);
2876 auto sleVault = std::make_shared<SLE>(vaultKeylet);
2877 auto const vaultPage = ac.view().dirInsert(
2878 keylet::ownerDir(a1.id()), sleVault->key(), describeOwnerDir(a1.id()));
2879 sleVault->setFieldU64(sfOwnerNode, *vaultPage);
2880 ac.view().insert(sleVault);
2881 return true;
2882 },
2883 XRPAmount{},
2884 STTx{ttPAYMENT, [](STObject&) {}},
2886
2888 {"vault deleted by a wrong transaction type"},
2889 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2890 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
2891 auto sleVault = ac.view().peek(keylet);
2892 if (!sleVault)
2893 return false;
2894 ac.view().erase(sleVault);
2895 return true;
2896 },
2897 XRPAmount{},
2898 STTx{ttVAULT_SET, [](STObject&) {}},
2900 [&](Account const& a1, Account const& a2, Env& env) {
2901 Vault const vault{env};
2902 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
2903 env(tx);
2904 return true;
2905 });
2906
2908 {"vault operation updated more than single vault"},
2909 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2910 {
2911 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
2912 auto sleVault = ac.view().peek(keylet);
2913 if (!sleVault)
2914 return false;
2915 ac.view().erase(sleVault);
2916 }
2917 {
2918 auto const keylet = keylet::vault(a2.id(), ac.view().seq());
2919 auto sleVault = ac.view().peek(keylet);
2920 if (!sleVault)
2921 return false;
2922 ac.view().erase(sleVault);
2923 }
2924 return true;
2925 },
2926 XRPAmount{},
2927 STTx{ttVAULT_DELETE, [](STObject&) {}},
2929 [&](Account const& a1, Account const& a2, Env& env) {
2930 Vault const vault{env};
2931 {
2932 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
2933 env(tx);
2934 }
2935 {
2936 auto [tx, _] = vault.create({.owner = a2, .asset = xrpIssue()});
2937 env(tx);
2938 }
2939 return true;
2940 });
2941
2943 {"vault operation updated more than single vault"},
2944 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2945 auto const sequence = ac.view().seq();
2946 auto const insertVault = [&](Account const a) {
2947 auto const vaultKeylet = keylet::vault(a.id(), sequence);
2948 auto sleVault = std::make_shared<SLE>(vaultKeylet);
2949 auto const vaultPage = ac.view().dirInsert(
2950 keylet::ownerDir(a.id()), sleVault->key(), describeOwnerDir(a.id()));
2951 sleVault->setFieldU64(sfOwnerNode, *vaultPage);
2952 ac.view().insert(sleVault);
2953 };
2954 insertVault(a1);
2955 insertVault(a2);
2956 return true;
2957 },
2958 XRPAmount{},
2959 STTx{ttVAULT_CREATE, [](STObject&) {}},
2961
2963 {"deleted vault must also delete shares"},
2964 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2965 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
2966 auto sleVault = ac.view().peek(keylet);
2967 if (!sleVault)
2968 return false;
2969 ac.view().erase(sleVault);
2970 return true;
2971 },
2972 XRPAmount{},
2973 STTx{ttVAULT_DELETE, [](STObject&) {}},
2975 [&](Account const& a1, Account const& a2, Env& env) {
2976 Vault const vault{env};
2977 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
2978 env(tx);
2979 return true;
2980 });
2981
2983 {"deleted vault must have no shares outstanding",
2984 "deleted vault must have no assets outstanding",
2985 "deleted vault must have no assets available"},
2986 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2987 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
2988 auto sleVault = ac.view().peek(keylet);
2989 if (!sleVault)
2990 return false;
2991 auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
2992 if (!sleShares)
2993 return false;
2994 ac.view().erase(sleVault);
2995 ac.view().erase(sleShares);
2996 return true;
2997 },
2998 XRPAmount{},
2999 STTx{ttVAULT_DELETE, [](STObject&) {}},
3001 [&](Account const& a1, Account const& a2, Env& env) {
3002 Vault const vault{env};
3003 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
3004 env(tx);
3005 env(vault.deposit({.depositor = a1, .id = keylet.key, .amount = XRP(10)}));
3006 return true;
3007 });
3008
3010 {"vault operation succeeded without modifying a vault"},
3011 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3012 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3013 auto sleVault = ac.view().peek(keylet);
3014 if (!sleVault)
3015 return false;
3016 auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
3017 if (!sleShares)
3018 return false;
3019 // Note, such an "orphaned" update of MPT issuance attached to a
3020 // vault is invalid; ttVAULT_SET must also update Vault object.
3021 sleShares->setFieldH256(sfDomainID, uint256(13));
3022 ac.view().update(sleShares);
3023 return true;
3024 },
3025 XRPAmount{},
3026 STTx{ttVAULT_SET, [](STObject& tx) {}},
3028 precloseXrp,
3030
3032 {"vault operation succeeded without modifying a vault"},
3033 [&](Account const& a1, Account const& a2, ApplyContext& ac) { return true; },
3034 XRPAmount{},
3035 STTx{ttVAULT_CREATE, [](STObject&) {}},
3037 [&](Account const& a1, Account const& a2, Env& env) {
3038 Vault const vault{env};
3039 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
3040 env(tx);
3041 return true;
3042 });
3043
3045 {"vault operation succeeded without modifying a vault"},
3046 [&](Account const& a1, Account const& a2, ApplyContext& ac) { return true; },
3047 XRPAmount{},
3048 STTx{ttVAULT_DEPOSIT, [](STObject&) {}},
3050 [&](Account const& a1, Account const& a2, Env& env) {
3051 Vault const vault{env};
3052 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
3053 env(tx);
3054 return true;
3055 });
3056
3058 {"vault operation succeeded without modifying a vault"},
3059 [&](Account const& a1, Account const& a2, ApplyContext& ac) { return true; },
3060 XRPAmount{},
3061 STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
3063 [&](Account const& a1, Account const& a2, Env& env) {
3064 Vault const vault{env};
3065 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
3066 env(tx);
3067 return true;
3068 });
3069
3071 {"vault operation succeeded without modifying a vault"},
3072 [&](Account const& a1, Account const& a2, ApplyContext& ac) { return true; },
3073 XRPAmount{},
3074 STTx{ttVAULT_CLAWBACK, [](STObject&) {}},
3076 [&](Account const& a1, Account const& a2, Env& env) {
3077 Vault const vault{env};
3078 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
3079 env(tx);
3080 return true;
3081 });
3082
3084 {"vault operation succeeded without modifying a vault"},
3085 [&](Account const& a1, Account const& a2, ApplyContext& ac) { return true; },
3086 XRPAmount{},
3087 STTx{ttVAULT_DELETE, [](STObject&) {}},
3089 [&](Account const& a1, Account const& a2, Env& env) {
3090 Vault const vault{env};
3091 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
3092 env(tx);
3093 return true;
3094 });
3095
3097 {"updated vault must have shares"},
3098 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3099 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3100 auto sleVault = ac.view().peek(keylet);
3101 if (!sleVault)
3102 return false;
3103 (*sleVault)[sfAssetsMaximum] = 200;
3104 ac.view().update(sleVault);
3105
3106 auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
3107 if (!sleShares)
3108 return false;
3109 ac.view().erase(sleShares);
3110 return true;
3111 },
3112 XRPAmount{},
3113 STTx{ttVAULT_SET, [](STObject&) {}},
3115 [&](Account const& a1, Account const& a2, Env& env) {
3116 Vault const vault{env};
3117 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
3118 env(tx);
3119 return true;
3120 });
3121
3123 {"vault operation succeeded without updating shares",
3124 "assets available must not be greater than assets outstanding"},
3125 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3126 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3127 auto sleVault = ac.view().peek(keylet);
3128 if (!sleVault)
3129 return false;
3130 (*sleVault)[sfAssetsTotal] = 9;
3131 ac.view().update(sleVault);
3132 return true;
3133 },
3134 XRPAmount{},
3135 STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
3137 [&](Account const& a1, Account const& a2, Env& env) {
3138 Vault const vault{env};
3139 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
3140 env(tx);
3141 env(vault.deposit({.depositor = a1, .id = keylet.key, .amount = XRP(10)}));
3142 return true;
3143 });
3144
3146 {"set must not change assets outstanding",
3147 "set must not change assets available",
3148 "set must not change shares outstanding",
3149 "set must not change vault balance",
3150 "assets available must be positive",
3151 "assets available must not be greater than assets outstanding",
3152 "assets outstanding must be positive"},
3153 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3154 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3155 auto sleVault = ac.view().peek(keylet);
3156 if (!sleVault)
3157 return false;
3158 auto slePseudoAccount = ac.view().peek(keylet::account(*(*sleVault)[sfAccount]));
3159 if (!slePseudoAccount)
3160 return false;
3161 (*slePseudoAccount)[sfBalance] = *(*slePseudoAccount)[sfBalance] - 10;
3162 ac.view().update(slePseudoAccount);
3163
3164 // Move 10 drops to A4 to enforce total XRP balance
3165 auto sleA4 = ac.view().peek(keylet::account(a4.id()));
3166 if (!sleA4)
3167 return false;
3168 (*sleA4)[sfBalance] = *(*sleA4)[sfBalance] + 10;
3169 ac.view().update(sleA4);
3170
3171 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
3172 sample.assetsAvailable = (kDropsPerXrp * -100).value();
3173 sample.assetsTotal = (kDropsPerXrp * -200).value();
3174 sample.sharesTotal = -1;
3175 }));
3176 },
3177 XRPAmount{},
3178 STTx{ttVAULT_SET, [](STObject& tx) {}},
3180 precloseXrp,
3182
3184 {"violation of vault immutable data"},
3185 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3186 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3187 auto sleVault = ac.view().peek(keylet);
3188 if (!sleVault)
3189 return false;
3190 sleVault->setFieldIssue(sfAsset, STIssue{sfAsset, MPTIssue(MPTID(42))});
3191 ac.view().update(sleVault);
3192 return true;
3193 },
3194 XRPAmount{},
3195 STTx{ttVAULT_SET, [](STObject& tx) {}},
3197 precloseXrp);
3198
3200 {"violation of vault immutable data"},
3201 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3202 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3203 auto sleVault = ac.view().peek(keylet);
3204 if (!sleVault)
3205 return false;
3206 sleVault->setAccountID(sfAccount, a2.id());
3207 ac.view().update(sleVault);
3208 return true;
3209 },
3210 XRPAmount{},
3211 STTx{ttVAULT_SET, [](STObject& tx) {}},
3213 precloseXrp);
3214
3216 {"violation of vault immutable data"},
3217 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3218 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3219 auto sleVault = ac.view().peek(keylet);
3220 if (!sleVault)
3221 return false;
3222 (*sleVault)[sfShareMPTID] = MPTID(42);
3223 ac.view().update(sleVault);
3224 return true;
3225 },
3226 XRPAmount{},
3227 STTx{ttVAULT_SET, [](STObject& tx) {}},
3229 precloseXrp);
3230
3232 {"vault transaction must not change loss unrealized",
3233 "set must not change assets outstanding"},
3234 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3235 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3236 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
3237 sample.lossUnrealized = 13;
3238 sample.assetsTotal = 20;
3239 }));
3240 },
3241 XRPAmount{},
3242 STTx{ttVAULT_SET, [](STObject& tx) {}},
3244 precloseXrp,
3246
3248 {"loss unrealized must not exceed the difference "
3249 "between assets outstanding and available",
3250 "vault transaction must not change loss unrealized"},
3251 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3252 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3253 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 100, [&](Adjustments& sample) {
3254 sample.lossUnrealized = 13;
3255 }));
3256 },
3257 XRPAmount{},
3258 STTx{
3259 ttVAULT_DEPOSIT, [](STObject& tx) { tx.setFieldAmount(sfAmount, XRPAmount(200)); }},
3261 precloseXrp,
3263
3265 {"set assets outstanding must not exceed assets maximum"},
3266 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3267 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3268 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
3269 sample.assetsMaximum = 1;
3270 }));
3271 },
3272 XRPAmount{},
3273 STTx{ttVAULT_SET, [](STObject& tx) {}},
3275 precloseXrp,
3277
3279 {"assets maximum must be positive"},
3280 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3281 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3282 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
3283 sample.assetsMaximum = -1;
3284 }));
3285 },
3286 XRPAmount{},
3287 STTx{ttVAULT_SET, [](STObject& tx) {}},
3289 precloseXrp,
3291
3293 {"set must not change shares outstanding",
3294 "updated zero sized vault must have no assets outstanding",
3295 "updated zero sized vault must have no assets available"},
3296 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3297 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3298 auto sleVault = ac.view().peek(keylet);
3299 if (!sleVault)
3300 return false;
3301 ac.view().update(sleVault);
3302 auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
3303 if (!sleShares)
3304 return false;
3305 (*sleShares)[sfOutstandingAmount] = 0;
3306 ac.view().update(sleShares);
3307 return true;
3308 },
3309 XRPAmount{},
3310 STTx{ttVAULT_SET, [](STObject& tx) {}},
3312 precloseXrp,
3314
3316 {"updated shares must not exceed maximum"},
3317 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3318 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3319 auto sleVault = ac.view().peek(keylet);
3320 if (!sleVault)
3321 return false;
3322 auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
3323 if (!sleShares)
3324 return false;
3325 (*sleShares)[sfMaximumAmount] = 10;
3326 ac.view().update(sleShares);
3327
3328 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [](Adjustments&) {}));
3329 },
3330 XRPAmount{},
3331 STTx{ttVAULT_DEPOSIT, [](STObject&) {}},
3333 precloseXrp,
3335
3337 {"updated shares must not exceed maximum"},
3338 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3339 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3340 kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [](Adjustments&) {}));
3341
3342 auto sleVault = ac.view().peek(keylet);
3343 if (!sleVault)
3344 return false;
3345 auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
3346 if (!sleShares)
3347 return false;
3348 (*sleShares)[sfOutstandingAmount] = kMaxMpTokenAmount + 1;
3349 ac.view().update(sleShares);
3350 return true;
3351 },
3352 XRPAmount{},
3353 STTx{ttVAULT_DEPOSIT, [](STObject&) {}},
3355 precloseXrp,
3357
3358 testcase << "Vault create";
3360 {
3361 "created vault must be empty",
3362 "updated zero sized vault must have no assets outstanding",
3363 "create operation must not have updated a vault",
3364 },
3365 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3366 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3367 auto sleVault = ac.view().peek(keylet);
3368 if (!sleVault)
3369 return false;
3370 (*sleVault)[sfAssetsTotal] = 9;
3371 ac.view().update(sleVault);
3372 return true;
3373 },
3374 XRPAmount{},
3375 STTx{ttVAULT_CREATE, [](STObject&) {}},
3377 [&](Account const& a1, Account const& a2, Env& env) {
3378 Vault const vault{env};
3379 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
3380 env(tx);
3381 return true;
3382 });
3383
3385 {
3386 "created vault must be empty",
3387 "updated zero sized vault must have no assets available",
3388 "assets available must not be greater than assets outstanding",
3389 "create operation must not have updated a vault",
3390 },
3391 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3392 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3393 auto sleVault = ac.view().peek(keylet);
3394 if (!sleVault)
3395 return false;
3396 (*sleVault)[sfAssetsAvailable] = 9;
3397 ac.view().update(sleVault);
3398 return true;
3399 },
3400 XRPAmount{},
3401 STTx{ttVAULT_CREATE, [](STObject&) {}},
3403 [&](Account const& a1, Account const& a2, Env& env) {
3404 Vault const vault{env};
3405 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
3406 env(tx);
3407 return true;
3408 });
3409
3411 {
3412 "created vault must be empty",
3413 "loss unrealized must not exceed the difference between assets "
3414 "outstanding and available",
3415 "vault transaction must not change loss unrealized",
3416 "create operation must not have updated a vault",
3417 },
3418 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3419 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3420 auto sleVault = ac.view().peek(keylet);
3421 if (!sleVault)
3422 return false;
3423 (*sleVault)[sfLossUnrealized] = 1;
3424 ac.view().update(sleVault);
3425 return true;
3426 },
3427 XRPAmount{},
3428 STTx{ttVAULT_CREATE, [](STObject&) {}},
3430 [&](Account const& a1, Account const& a2, Env& env) {
3431 Vault const vault{env};
3432 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
3433 env(tx);
3434 return true;
3435 });
3436
3438 {
3439 "created vault must be empty",
3440 "create operation must not have updated a vault",
3441 },
3442 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3443 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3444 auto sleVault = ac.view().peek(keylet);
3445 if (!sleVault)
3446 return false;
3447 auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
3448 if (!sleShares)
3449 return false;
3450 ac.view().update(sleVault);
3451 (*sleShares)[sfOutstandingAmount] = 9;
3452 ac.view().update(sleShares);
3453 return true;
3454 },
3455 XRPAmount{},
3456 STTx{ttVAULT_CREATE, [](STObject&) {}},
3458 [&](Account const& a1, Account const& a2, Env& env) {
3459 Vault const vault{env};
3460 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
3461 env(tx);
3462 return true;
3463 });
3464
3466 {
3467 "assets maximum must be positive",
3468 "create operation must not have updated a vault",
3469 },
3470 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3471 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3472 auto sleVault = ac.view().peek(keylet);
3473 if (!sleVault)
3474 return false;
3475 (*sleVault)[sfAssetsMaximum] = Number(-1);
3476 ac.view().update(sleVault);
3477 return true;
3478 },
3479 XRPAmount{},
3480 STTx{ttVAULT_CREATE, [](STObject&) {}},
3482 [&](Account const& a1, Account const& a2, Env& env) {
3483 Vault const vault{env};
3484 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
3485 env(tx);
3486 return true;
3487 });
3488
3490 {"create operation must not have updated a vault",
3491 "shares issuer and vault pseudo-account must be the same",
3492 "shares issuer must be a pseudo-account",
3493 "shares issuer pseudo-account must point back to the vault"},
3494 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3495 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3496 auto sleVault = ac.view().peek(keylet);
3497 if (!sleVault)
3498 return false;
3499 auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
3500 if (!sleShares)
3501 return false;
3502 ac.view().update(sleVault);
3503 (*sleShares)[sfIssuer] = a1.id();
3504 ac.view().update(sleShares);
3505 return true;
3506 },
3507 XRPAmount{},
3508 STTx{ttVAULT_CREATE, [](STObject&) {}},
3510 [&](Account const& a1, Account const& a2, Env& env) {
3511 Vault const vault{env};
3512 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
3513 env(tx);
3514 return true;
3515 });
3516
3518 {"vault created by a wrong transaction type", "account root created illegally"},
3519 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3520 // The code below will create a valid vault with (almost) all
3521 // the invariants holding. Except one: it is created by the
3522 // wrong transaction type.
3523 auto const sequence = ac.view().seq();
3524 auto const vaultKeylet = keylet::vault(a1.id(), sequence);
3525 auto sleVault = std::make_shared<SLE>(vaultKeylet);
3526 auto const vaultPage = ac.view().dirInsert(
3527 keylet::ownerDir(a1.id()), sleVault->key(), describeOwnerDir(a1.id()));
3528 sleVault->setFieldU64(sfOwnerNode, *vaultPage);
3529
3530 auto pseudoId = pseudoAccountAddress(ac.view(), vaultKeylet.key);
3531 // Create pseudo-account.
3532 auto sleAccount = std::make_shared<SLE>(keylet::account(pseudoId));
3533 sleAccount->setAccountID(sfAccount, pseudoId);
3534 sleAccount->setFieldAmount(sfBalance, STAmount{});
3535 std::uint32_t const seqno = //
3536 ac.view().rules().enabled(featureSingleAssetVault) //
3537 ? 0 //
3538 : sequence;
3539 sleAccount->setFieldU32(sfSequence, seqno);
3540 sleAccount->setFieldU32(
3541 sfFlags, lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth);
3542 sleAccount->setFieldH256(sfVaultID, vaultKeylet.key);
3543 ac.view().insert(sleAccount);
3544
3545 auto const sharesMptId = makeMptID(sequence, pseudoId);
3546 auto const sharesKeylet = keylet::mptokenIssuance(sharesMptId);
3547 auto sleShares = std::make_shared<SLE>(sharesKeylet);
3548 auto const sharesPage = ac.view().dirInsert(
3549 keylet::ownerDir(pseudoId), sharesKeylet, describeOwnerDir(pseudoId));
3550 sleShares->setFieldU64(sfOwnerNode, *sharesPage);
3551
3552 sleShares->at(sfFlags) = 0;
3553 sleShares->at(sfIssuer) = pseudoId;
3554 sleShares->at(sfOutstandingAmount) = 0;
3555 sleShares->at(sfSequence) = sequence;
3556
3557 sleVault->at(sfAccount) = pseudoId;
3558 sleVault->at(sfFlags) = 0;
3559 sleVault->at(sfSequence) = sequence;
3560 sleVault->at(sfOwner) = a1.id();
3561 sleVault->at(sfAssetsTotal) = Number(0);
3562 sleVault->at(sfAssetsAvailable) = Number(0);
3563 sleVault->at(sfLossUnrealized) = Number(0);
3564 sleVault->at(sfShareMPTID) = sharesMptId;
3565 sleVault->at(sfWithdrawalPolicy) = kVaultStrategyFirstComeFirstServe;
3566
3567 ac.view().insert(sleVault);
3568 ac.view().insert(sleShares);
3569 return true;
3570 },
3571 XRPAmount{},
3572 STTx{ttVAULT_SET, [](STObject&) {}},
3574
3576 {"shares issuer and vault pseudo-account must be the same",
3577 "shares issuer pseudo-account must point back to the vault"},
3578 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3579 auto const sequence = ac.view().seq();
3580 auto const vaultKeylet = keylet::vault(a1.id(), sequence);
3581 auto sleVault = std::make_shared<SLE>(vaultKeylet);
3582 auto const vaultPage = ac.view().dirInsert(
3583 keylet::ownerDir(a1.id()), sleVault->key(), describeOwnerDir(a1.id()));
3584 sleVault->setFieldU64(sfOwnerNode, *vaultPage);
3585
3586 auto pseudoId = pseudoAccountAddress(ac.view(), vaultKeylet.key);
3587 // Create pseudo-account.
3588 auto sleAccount = std::make_shared<SLE>(keylet::account(pseudoId));
3589 sleAccount->setAccountID(sfAccount, pseudoId);
3590 sleAccount->setFieldAmount(sfBalance, STAmount{});
3591 std::uint32_t const seqno = //
3592 ac.view().rules().enabled(featureSingleAssetVault) //
3593 ? 0 //
3594 : sequence;
3595 sleAccount->setFieldU32(sfSequence, seqno);
3596 sleAccount->setFieldU32(
3597 sfFlags, lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth);
3598 // sleAccount->setFieldH256(sfVaultID, vaultKeylet.key);
3599 // Setting wrong vault key
3600 sleAccount->setFieldH256(sfVaultID, uint256(42));
3601 ac.view().insert(sleAccount);
3602
3603 auto const sharesMptId = makeMptID(sequence, pseudoId);
3604 auto const sharesKeylet = keylet::mptokenIssuance(sharesMptId);
3605 auto sleShares = std::make_shared<SLE>(sharesKeylet);
3606 auto const sharesPage = ac.view().dirInsert(
3607 keylet::ownerDir(pseudoId), sharesKeylet, describeOwnerDir(pseudoId));
3608 sleShares->setFieldU64(sfOwnerNode, *sharesPage);
3609
3610 sleShares->at(sfFlags) = 0;
3611 sleShares->at(sfIssuer) = pseudoId;
3612 sleShares->at(sfOutstandingAmount) = 0;
3613 sleShares->at(sfSequence) = sequence;
3614
3615 // sleVault->at(sfAccount) = pseudoId;
3616 // Setting wrong pseudo account ID
3617 sleVault->at(sfAccount) = a2.id();
3618 sleVault->at(sfFlags) = 0;
3619 sleVault->at(sfSequence) = sequence;
3620 sleVault->at(sfOwner) = a1.id();
3621 sleVault->at(sfAssetsTotal) = Number(0);
3622 sleVault->at(sfAssetsAvailable) = Number(0);
3623 sleVault->at(sfLossUnrealized) = Number(0);
3624 sleVault->at(sfShareMPTID) = sharesMptId;
3625 sleVault->at(sfWithdrawalPolicy) = kVaultStrategyFirstComeFirstServe;
3626
3627 ac.view().insert(sleVault);
3628 ac.view().insert(sleShares);
3629 return true;
3630 },
3631 XRPAmount{},
3632 STTx{ttVAULT_CREATE, [](STObject&) {}},
3634
3636 {"shares issuer and vault pseudo-account must be the same", "shares issuer must exist"},
3637 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3638 auto const sequence = ac.view().seq();
3639 auto const vaultKeylet = keylet::vault(a1.id(), sequence);
3640 auto sleVault = std::make_shared<SLE>(vaultKeylet);
3641 auto const vaultPage = ac.view().dirInsert(
3642 keylet::ownerDir(a1.id()), sleVault->key(), describeOwnerDir(a1.id()));
3643 sleVault->setFieldU64(sfOwnerNode, *vaultPage);
3644
3645 auto const sharesMptId = makeMptID(sequence, a2.id());
3646 auto const sharesKeylet = keylet::mptokenIssuance(sharesMptId);
3647 auto sleShares = std::make_shared<SLE>(sharesKeylet);
3648 auto const sharesPage = ac.view().dirInsert(
3649 keylet::ownerDir(a2.id()), sharesKeylet, describeOwnerDir(a2.id()));
3650 sleShares->setFieldU64(sfOwnerNode, *sharesPage);
3651
3652 sleShares->at(sfFlags) = 0;
3653 // Setting wrong pseudo account ID
3654 sleShares->at(sfIssuer) = AccountID(42);
3655 sleShares->at(sfOutstandingAmount) = 0;
3656 sleShares->at(sfSequence) = sequence;
3657
3658 sleVault->at(sfAccount) = a2.id();
3659 sleVault->at(sfFlags) = 0;
3660 sleVault->at(sfSequence) = sequence;
3661 sleVault->at(sfOwner) = a1.id();
3662 sleVault->at(sfAssetsTotal) = Number(0);
3663 sleVault->at(sfAssetsAvailable) = Number(0);
3664 sleVault->at(sfLossUnrealized) = Number(0);
3665 sleVault->at(sfShareMPTID) = sharesMptId;
3666 sleVault->at(sfWithdrawalPolicy) = kVaultStrategyFirstComeFirstServe;
3667
3668 ac.view().insert(sleVault);
3669 ac.view().insert(sleShares);
3670 return true;
3671 },
3672 XRPAmount{},
3673 STTx{ttVAULT_CREATE, [](STObject&) {}},
3675
3676 testcase << "Vault deposit";
3678 {"deposit must change vault balance"},
3679 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3680 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3681 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [](Adjustments& sample) {
3682 sample.vaultAssets.reset();
3683 }));
3684 },
3685 XRPAmount{},
3686 STTx{ttVAULT_DEPOSIT, [](STObject&) {}},
3688 precloseXrp);
3689
3691 {"deposit assets outstanding must not exceed assets maximum"},
3692 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3693 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3694 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 200, [&](Adjustments& sample) {
3695 sample.assetsMaximum = 1;
3696 }));
3697 },
3698 XRPAmount{},
3699 STTx{
3700 ttVAULT_DEPOSIT, [](STObject& tx) { tx.setFieldAmount(sfAmount, XRPAmount(200)); }},
3702 precloseXrp,
3704
3705 // This really convoluted unit tests makes the zero balance on the
3706 // depositor, by sending them the same amount as the transaction fee.
3707 // The operation makes no sense, but the defensive check in
3708 // ValidVault::finalize is otherwise impossible to trigger.
3710 {"deposit must increase vault balance", "deposit must change depositor balance"},
3711 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3712 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3713
3714 // Move 10 drops to A4 to enforce total XRP balance
3715 auto sleA4 = ac.view().peek(keylet::account(a4.id()));
3716 if (!sleA4)
3717 return false;
3718 (*sleA4)[sfBalance] = *(*sleA4)[sfBalance] + 10;
3719 ac.view().update(sleA4);
3720
3721 return kAdjust(ac.view(), keylet, kArgs(a3.id(), -10, [&](Adjustments& sample) {
3722 sample.accountAssets->amount = -100;
3723 }));
3724 },
3725 XRPAmount{100},
3726 STTx{
3727 ttVAULT_DEPOSIT,
3728 [&](STObject& tx) {
3729 tx[sfFee] = XRPAmount(100);
3730 tx[sfAccount] = a3.id();
3731 }},
3733 precloseXrp);
3734
3736 {"deposit must increase vault balance",
3737 "deposit must decrease depositor balance",
3738 "deposit must change vault and depositor balance by equal amount",
3739 "deposit and assets outstanding must add up",
3740 "deposit and assets available must add up"},
3741 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3742 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3743
3744 // Move 10 drops from A2 to A3 to enforce total XRP balance
3745 auto sleA3 = ac.view().peek(keylet::account(a3.id()));
3746 if (!sleA3)
3747 return false;
3748 (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] + 10;
3749 ac.view().update(sleA3);
3750
3751 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
3752 sample.vaultAssets = -20;
3753 sample.accountAssets->amount = 10;
3754 }));
3755 },
3756 XRPAmount{},
3757 STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
3759 precloseXrp,
3761
3763 {"deposit must change depositor balance"},
3764 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3765 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3766
3767 // Move 10 drops from A3 to vault to enforce total XRP balance
3768 auto sleA3 = ac.view().peek(keylet::account(a3.id()));
3769 if (!sleA3)
3770 return false;
3771 (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] - 10;
3772 ac.view().update(sleA3);
3773
3774 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
3775 sample.accountAssets->amount = 0;
3776 }));
3777 },
3778 XRPAmount{},
3779 STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
3781 precloseXrp,
3783
3785 {"deposit must change depositor shares"},
3786 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3787 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3788 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
3789 sample.accountShares.reset();
3790 }));
3791 },
3792 XRPAmount{},
3793 STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
3795 precloseXrp,
3797
3799 {"deposit must change vault shares"},
3800 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3801 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3802
3803 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [](Adjustments& sample) {
3804 sample.sharesTotal = 0;
3805 }));
3806 },
3807 XRPAmount{},
3808 STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
3810 precloseXrp,
3812
3814 {"deposit must increase depositor shares",
3815 "deposit must change depositor and vault shares by equal amount",
3816 "deposit must not change vault balance by more than deposited "
3817 "amount"},
3818 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3819 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3820 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
3821 sample.accountShares->amount = -5;
3822 sample.sharesTotal = -10;
3823 }));
3824 },
3825 XRPAmount{},
3826 STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(5); }},
3828 precloseXrp,
3830
3832 {"deposit and assets outstanding must add up"},
3833 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3834 auto sleA3 = ac.view().peek(keylet::account(a3.id()));
3835 (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] - 2000;
3836 ac.view().update(sleA3);
3837
3838 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3839 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
3840 sample.assetsTotal = 11;
3841 }));
3842 },
3843 XRPAmount{2000},
3844 STTx{
3845 ttVAULT_DEPOSIT,
3846 [&](STObject& tx) {
3847 tx[sfAmount] = XRPAmount(10);
3848 tx[sfDelegate] = a3.id();
3849 tx[sfFee] = XRPAmount(2000);
3850 }},
3852 precloseXrp,
3854
3856 {"deposit and assets outstanding must add up",
3857 "deposit and assets available must add up"},
3858 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3859 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3860 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
3861 sample.assetsTotal = 7;
3862 sample.assetsAvailable = 7;
3863 }));
3864 },
3865 XRPAmount{},
3866 STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
3868 precloseXrp,
3870
3871 testcase << "Vault withdrawal";
3873 {"withdrawal must change vault balance"},
3874 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3875 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3876 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [](Adjustments& sample) {
3877 sample.vaultAssets.reset();
3878 }));
3879 },
3880 XRPAmount{},
3881 STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
3883 precloseXrp);
3884
3885 // Almost identical to the really convoluted test for deposit, where the
3886 // depositor spends only the transaction fee. In case of withdrawal,
3887 // this test is almost the same as normal withdrawal where the
3888 // sfDestination would have been A4, but has been omitted.
3890 {"withdrawal must change one destination balance"},
3891 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3892 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3893
3894 // Move 10 drops to A4 to enforce total XRP balance
3895 auto sleA4 = ac.view().peek(keylet::account(a4.id()));
3896 if (!sleA4)
3897 return false;
3898 (*sleA4)[sfBalance] = *(*sleA4)[sfBalance] + 10;
3899 ac.view().update(sleA4);
3900
3901 return kAdjust(ac.view(), keylet, kArgs(a3.id(), -10, [&](Adjustments& sample) {
3902 sample.accountAssets->amount = -100;
3903 }));
3904 },
3905 XRPAmount{100},
3906 STTx{
3907 ttVAULT_WITHDRAW,
3908 [&](STObject& tx) {
3909 tx[sfFee] = XRPAmount(100);
3910 tx[sfAccount] = a3.id();
3911 // This commented out line causes the invariant violation.
3912 // tx[sfDestination] = A4.id();
3913 }},
3915 precloseXrp);
3916
3918 {
3919 "withdrawal must change vault and destination balance by equal amount",
3920 "withdrawal must decrease vault balance",
3921 "withdrawal must increase destination balance",
3922 "withdrawal and assets outstanding must add up",
3923 "withdrawal and assets available must add up",
3924 },
3925 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3926 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3927
3928 // Move 10 drops from A2 to A3 to enforce total XRP balance
3929 auto sleA3 = ac.view().peek(keylet::account(a3.id()));
3930 if (!sleA3)
3931 return false;
3932 (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] + 10;
3933 ac.view().update(sleA3);
3934
3935 return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
3936 sample.vaultAssets = 10;
3937 sample.accountAssets->amount = -20;
3938 }));
3939 },
3940 XRPAmount{},
3941 STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
3943 precloseXrp,
3945
3947 {"withdrawal must change one destination balance"},
3948 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3949 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3950 if (!kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
3951 *sample.vaultAssets -= 5;
3952 })))
3953 return false;
3954 auto sleA3 = ac.view().peek(keylet::account(a3.id()));
3955 if (!sleA3)
3956 return false;
3957 (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] + 5;
3958 ac.view().update(sleA3);
3959 return true;
3960 },
3961 XRPAmount{},
3962 STTx{ttVAULT_WITHDRAW, [&](STObject& tx) { tx.setAccountID(sfDestination, a3.id()); }},
3964 precloseXrp,
3966
3968 {"withdrawal must change depositor shares"},
3969 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3970 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3971 return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
3972 sample.accountShares.reset();
3973 }));
3974 },
3975 XRPAmount{},
3976 STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
3978 precloseXrp,
3980
3982 {"withdrawal must change vault shares"},
3983 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3984 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
3985 return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [](Adjustments& sample) {
3986 sample.sharesTotal = 0;
3987 }));
3988 },
3989 XRPAmount{},
3990 STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
3992 precloseXrp,
3994
3996 {"withdrawal must decrease depositor shares",
3997 "withdrawal must change depositor and vault shares by equal "
3998 "amount"},
3999 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4000 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
4001 return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
4002 sample.accountShares->amount = 5;
4003 sample.sharesTotal = 10;
4004 }));
4005 },
4006 XRPAmount{},
4007 STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
4009 precloseXrp,
4011
4013 {"withdrawal and assets outstanding must add up",
4014 "withdrawal and assets available must add up"},
4015 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4016 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
4017 return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
4018 sample.assetsTotal = -15;
4019 sample.assetsAvailable = -15;
4020 }));
4021 },
4022 XRPAmount{},
4023 STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
4025 precloseXrp,
4027
4029 {"withdrawal and assets outstanding must add up"},
4030 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4031 auto sleA3 = ac.view().peek(keylet::account(a3.id()));
4032 (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] - 2000;
4033 ac.view().update(sleA3);
4034
4035 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
4036 return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
4037 sample.assetsTotal = -7;
4038 }));
4039 },
4040 XRPAmount{2000},
4041 STTx{
4042 ttVAULT_WITHDRAW,
4043 [&](STObject& tx) {
4044 tx[sfAmount] = XRPAmount(10);
4045 tx[sfDelegate] = a3.id();
4046 tx[sfFee] = XRPAmount(2000);
4047 }},
4049 precloseXrp,
4051
4052 auto const precloseMpt = [&](Account const& a1, Account const& a2, Env& env) -> bool {
4053 env.fund(XRP(1000), a3, a4);
4054
4055 // Create MPT asset
4056 {
4057 json::Value jv;
4058 jv[sfAccount] = a3.human();
4059 jv[sfTransactionType] = jss::MPTokenIssuanceCreate;
4060 jv[sfFlags] = tfMPTCanTransfer;
4061 env(jv);
4062 env.close();
4063 }
4064
4065 auto const mptID = makeMptID(env.seq(a3) - 1, a3);
4066 Asset const asset = MPTIssue(mptID);
4067 // Authorize A1 A2 A4
4068 {
4069 json::Value jv;
4070 jv[sfAccount] = a1.human();
4071 jv[sfTransactionType] = jss::MPTokenAuthorize;
4072 jv[sfMPTokenIssuanceID] = to_string(mptID);
4073 env(jv);
4074 jv[sfAccount] = a2.human();
4075 env(jv);
4076 jv[sfAccount] = a4.human();
4077 env(jv);
4078
4079 env.close();
4080 }
4081 // Send tokens to A1 A2 A4
4082 {
4083 env(pay(a3, a1, asset(1000)));
4084 env(pay(a3, a2, asset(1000)));
4085 env(pay(a3, a4, asset(1000)));
4086 env.close();
4087 }
4088
4089 Vault const vault{env};
4090 auto [tx, keylet] = vault.create({.owner = a1, .asset = asset});
4091 env(tx);
4092 env(vault.deposit({.depositor = a1, .id = keylet.key, .amount = asset(10)}));
4093 env(vault.deposit({.depositor = a2, .id = keylet.key, .amount = asset(10)}));
4094 env(vault.deposit({.depositor = a4, .id = keylet.key, .amount = asset(10)}));
4095 return true;
4096 };
4097
4099 {"withdrawal must decrease depositor shares",
4100 "withdrawal must change depositor and vault shares by equal "
4101 "amount"},
4102 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4103 auto const keylet = keylet::vault(a1.id(), ac.view().seq() - 2);
4104 return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
4105 sample.accountShares->amount = 5;
4106 }));
4107 },
4108 XRPAmount{},
4109 STTx{ttVAULT_WITHDRAW, [&](STObject& tx) { tx[sfAccount] = a3.id(); }},
4111 precloseMpt,
4113
4114 testcase << "Vault clawback";
4116 {"clawback must change vault balance"},
4117 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4118 auto const keylet = keylet::vault(a1.id(), ac.view().seq() - 2);
4119 return kAdjust(ac.view(), keylet, kArgs(a2.id(), -1, [&](Adjustments& sample) {
4120 sample.vaultAssets.reset();
4121 }));
4122 },
4123 XRPAmount{},
4124 STTx{ttVAULT_CLAWBACK, [&](STObject& tx) { tx[sfAccount] = a3.id(); }},
4126 precloseMpt);
4127
4128 // Not the same as below check: attempt to clawback XRP
4130 {"clawback may only be performed by the asset issuer"},
4131 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4132 auto const keylet = keylet::vault(a1.id(), ac.view().seq());
4133 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {}));
4134 },
4135 XRPAmount{},
4136 STTx{ttVAULT_CLAWBACK, [](STObject&) {}},
4138 precloseXrp);
4139
4140 // Not the same as above check: attempt to clawback MPT by bad account
4142 {"clawback may only be performed by the asset issuer"},
4143 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4144 auto const keylet = keylet::vault(a1.id(), ac.view().seq() - 2);
4145 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {}));
4146 },
4147 XRPAmount{},
4148 STTx{ttVAULT_CLAWBACK, [&](STObject& tx) { tx[sfAccount] = a4.id(); }},
4150 precloseMpt);
4151
4153 {"clawback must decrease vault balance",
4154 "clawback must decrease holder shares",
4155 "clawback must change vault shares"},
4156 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4157 auto const keylet = keylet::vault(a1.id(), ac.view().seq() - 2);
4158 return kAdjust(ac.view(), keylet, kArgs(a4.id(), 10, [&](Adjustments& sample) {
4159 sample.sharesTotal = 0;
4160 }));
4161 },
4162 XRPAmount{},
4163 STTx{
4164 ttVAULT_CLAWBACK,
4165 [&](STObject& tx) {
4166 tx[sfAccount] = a3.id();
4167 tx[sfHolder] = a4.id();
4168 }},
4170 precloseMpt);
4171
4173 {"clawback must change holder shares"},
4174 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4175 auto const keylet = keylet::vault(a1.id(), ac.view().seq() - 2);
4176 return kAdjust(ac.view(), keylet, kArgs(a4.id(), -10, [&](Adjustments& sample) {
4177 sample.accountShares.reset();
4178 }));
4179 },
4180 XRPAmount{},
4181 STTx{
4182 ttVAULT_CLAWBACK,
4183 [&](STObject& tx) {
4184 tx[sfAccount] = a3.id();
4185 tx[sfHolder] = a4.id();
4186 }},
4188 precloseMpt);
4189
4191 {"clawback must change holder and vault shares by equal amount",
4192 "clawback and assets outstanding must add up",
4193 "clawback and assets available must add up"},
4194 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4195 auto const keylet = keylet::vault(a1.id(), ac.view().seq() - 2);
4196 return kAdjust(ac.view(), keylet, kArgs(a4.id(), -10, [&](Adjustments& sample) {
4197 sample.accountShares->amount = -8;
4198 sample.assetsTotal = -7;
4199 sample.assetsAvailable = -7;
4200 }));
4201 },
4202 XRPAmount{},
4203 STTx{
4204 ttVAULT_CLAWBACK,
4205 [&](STObject& tx) {
4206 tx[sfAccount] = a3.id();
4207 tx[sfHolder] = a4.id();
4208 }},
4210 precloseMpt);
4211 }
4212
4213 void
4215 {
4216 using namespace test::jtx;
4217 testcase << "MPT";
4218
4219 MPTIssue const nonCanonicalMPTIssue{makeMptID(1, AccountID(0x4985601))};
4220 auto const nonCanonicalMPTAmount = [&](SField const& field) {
4221 return STAmount{
4222 field,
4223 nonCanonicalMPTIssue,
4225 0,
4226 false,
4228 };
4229 auto const negativeMPTAmount = [&](SField const& field) {
4230 return STAmount{field, nonCanonicalMPTIssue, 2, 0, true, STAmount::Unchecked{}};
4231 };
4232 auto const nonCanonicalMPTPayment = [&]() {
4233 return STTx{ttPAYMENT, [&](STObject& tx) {
4234 tx.setFieldAmount(sfAmount, nonCanonicalMPTAmount(sfAmount));
4235 }};
4236 };
4237
4239 Env{*this, defaultAmendments() - fixCleanup3_2_0},
4240 {},
4241 [](Account const&, Account const&, ApplyContext&) { return true; },
4242 XRPAmount{},
4243 nonCanonicalMPTPayment(),
4245
4247 {{"ledger entry contains non-canonical MPT or XRP amount"}},
4248 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4249 auto const sle = ac.view().peek(keylet::account(a1.id()));
4250 if (!sle)
4251 return false;
4252
4253 auto sleNew = std::make_shared<SLE>(keylet::check(a1.id(), (*sle)[sfSequence]));
4254 sleNew->setAccountID(sfAccount, a1.id());
4255 sleNew->setAccountID(sfDestination, a2.id());
4256 sleNew->setFieldAmount(sfSendMax, nonCanonicalMPTAmount(sfSendMax));
4257 ac.view().insert(sleNew);
4258 return true;
4259 });
4260
4262 {{"ledger entry contains non-canonical MPT or XRP amount"}},
4263 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4264 auto const sle = ac.view().peek(keylet::account(a1.id()));
4265 if (!sle)
4266 return false;
4267
4268 auto sleNew = std::make_shared<SLE>(keylet::check(a1.id(), (*sle)[sfSequence]));
4269 sleNew->setAccountID(sfAccount, a1.id());
4270 sleNew->setAccountID(sfDestination, a2.id());
4271 sleNew->setFieldAmount(sfSendMax, negativeMPTAmount(sfSendMax));
4272 ac.view().insert(sleNew);
4273 return true;
4274 });
4275
4276 // MPT OutstandingAmount > MaximumAmount
4278 {{"OutstandingAmount overflow"}},
4279 [](Account const& a1, Account const&, ApplyContext& ac) {
4280 // mptissuance outstanding is negative
4281 auto const sle = ac.view().peek(keylet::account(a1.id()));
4282 if (!sle)
4283 return false;
4284
4285 MPTIssue const mpt{makeMptID(sle->getFieldU32(sfSequence), a1)};
4287 sleNew->setFieldU64(sfOutstandingAmount, 110);
4288 sleNew->setFieldU64(sfMaximumAmount, 100);
4289 ac.view().insert(sleNew);
4290 return true;
4291 });
4292
4293 // MPTToken amount doesn't add up to OutstandingAmount
4295 {{"invalid OutstandingAmount balance"}},
4296 [](Account const& a1, Account const& a2, ApplyContext& ac) {
4297 // mptissuance outstanding is negative
4298 auto const sle = ac.view().peek(keylet::account(a1.id()));
4299 if (!sle)
4300 return false;
4301
4302 MPTIssue const mpt{makeMptID(sle->getFieldU32(sfSequence), a1)};
4304 sleNew->setFieldU64(sfOutstandingAmount, 100);
4305 sleNew->setFieldU64(sfMaximumAmount, 100);
4306 ac.view().insert(sleNew);
4307
4308 sleNew = std::make_shared<SLE>(keylet::mptoken(mpt.getMptID(), a2));
4309 sleNew->setFieldU64(sfMPTAmount, 90);
4310 ac.view().insert(sleNew);
4311
4312 return true;
4313 });
4314
4315 // Overflow/Invalid balance on payment
4316 auto testPayment = [&](std::string const& log, auto&& update) {
4317 MPTID id;
4319 {{log}},
4320 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4321 return update(id, ac, a1);
4322 },
4323 XRPAmount{},
4324 STTx{ttPAYMENT, [](STObject& tx) {}},
4326 [&](Account const& a1, Account const& a2, Env& env) {
4327 Account const gw("gw");
4328 env.fund(XRP(1'000), gw);
4329 MPTTester const mpt(
4330 {.env = env, .issuer = gw, .holders = {a1}, .pay = 100, .maxAmt = 100});
4331 id = mpt.issuanceID();
4332 return true;
4333 });
4334 };
4335 testPayment(
4336 "invalid OutstandingAmount balance",
4337 [&](MPTID const& id, ApplyContext& ac, Account const& a1) {
4338 auto sle = ac.view().peek(keylet::mptoken(id, a1));
4339 if (!sle)
4340 return false;
4341 sle->setFieldU64(sfMPTAmount, 101);
4342 ac.view().update(sle);
4343 return true;
4344 });
4345 testPayment(
4346 "OutstandingAmount overflow", [&](MPTID const& id, ApplyContext& ac, Account const&) {
4347 auto sle = ac.view().peek(keylet::mptokenIssuance(id));
4348 if (!sle)
4349 return false;
4350 sle->setFieldU64(sfOutstandingAmount, 101);
4351 ac.view().update(sle);
4352 return true;
4353 });
4354
4355 // More MPTokens created than expected
4357 std::make_pair(ttAMM_WITHDRAW, 2),
4358 std::make_pair(ttAMM_CLAWBACK, 2),
4359 std::make_pair(ttAMM_CREATE, 3),
4360 std::make_pair(ttCHECK_CASH, 2)};
4361 for (auto const& [tx, nTokens] : tests)
4362 {
4364 {{std::string("MPToken created for the MPT issuer")}},
4365 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4366 auto const sle = ac.view().peek(keylet::account(a1.id()));
4367 if (!sle)
4368 return false;
4369
4370 auto seq = sle->getFieldU32(sfSequence);
4371 for (int i = 0; i < nTokens; ++i)
4372 {
4373 MPTIssue const mpt{makeMptID(seq + i, a1)};
4374 auto sleNew =
4376 ac.view().insert(sleNew);
4377
4378 sleNew = std::make_shared<SLE>(keylet::mptoken(mpt.getMptID(), a2));
4379 ac.view().insert(sleNew);
4380 }
4381
4382 return true;
4383 },
4384 XRPAmount{},
4385 STTx{tx, [](STObject& tx) {}},
4387 }
4388
4389 // More MPTokens deleted than expected
4390 for (auto const& tx : {ttAMM_WITHDRAW, ttAMM_CLAWBACK})
4391 {
4392 MPTID id;
4393 Account const a3("A3");
4395 {{"MPT authorize succeeded but created/deleted bad number of mptokens"}},
4396 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4397 for (auto const& a : {a1, a2, a3})
4398 {
4399 auto sle = ac.view().peek(keylet::mptoken(id, a));
4400 if (!sle)
4401 return false;
4402 ac.view().erase(sle);
4403 }
4404 return true;
4405 },
4406 XRPAmount{},
4407 STTx{tx, [](STObject& tx) {}},
4409 [&](Account const& a1, Account const& a2, Env& env) {
4410 Account const gw("gw");
4411 env.fund(XRP(1'000), gw, a3);
4412 MPTTester const mpt({.env = env, .issuer = gw, .holders = {a1, a2, a3}});
4413 id = mpt.issuanceID();
4414 return true;
4415 });
4416 }
4417
4418 // sfReferenceHolding can only be set on creation by VaultCreate. A
4419 // non-VaultCreate transaction that creates an MPTokenIssuance with
4420 // sfReferenceHolding present must trip the invariant.
4422 {{"sfReferenceHolding set on a new MPTokenIssuance by a "
4423 "non-VaultCreate transaction"}},
4424 [](Account const& a1, Account const&, ApplyContext& ac) {
4425 auto const sleAcct = ac.view().peek(keylet::account(a1.id()));
4426 if (!sleAcct)
4427 return false;
4428 MPTIssue const mpt{makeMptID(sleAcct->getFieldU32(sfSequence), a1)};
4430 sleNew->setFieldH256(sfReferenceHolding, uint256{1});
4431 ac.view().insert(sleNew);
4432 return true;
4433 },
4434 XRPAmount{},
4435 STTx{ttACCOUNT_SET, [](STObject&) {}});
4436
4437 // sfReferenceHolding is immutable: changing the field on an
4438 // existing MPTokenIssuance must trip the invariant. Set up a real
4439 // vault via preclose (so the share issuance carries
4440 // sfReferenceHolding), then mutate it in precheck to produce a
4441 // before/after pair.
4442 {
4443 uint256 vaultKey;
4445 {{"sfReferenceHolding was modified on an existing "
4446 "MPTokenIssuance"}},
4447 [&](Account const&, Account const&, ApplyContext& ac) {
4448 auto const sleVault = ac.view().peek(keylet::vault(vaultKey));
4449 if (!sleVault)
4450 return false;
4451 auto sleIssuance =
4452 ac.view().peek(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
4453 if (!sleIssuance)
4454 return false;
4455 sleIssuance->setFieldH256(sfReferenceHolding, uint256{2});
4456 ac.view().update(sleIssuance);
4457 return true;
4458 },
4459 XRPAmount{},
4460 STTx{ttACCOUNT_SET, [](STObject&) {}},
4462 [&](Account const& a1, Account const&, Env& env) {
4463 Account const issuer{"issuer"};
4464 env.fund(XRP(10'000), issuer);
4465 env.close();
4466 MPTTester mptt{env, issuer, kMptInitNoFund};
4467 mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
4468 PrettyAsset const asset = mptt.issuanceID();
4469 mptt.authorize({.account = a1});
4470 env.close();
4471
4472 Vault const vault{env};
4473 auto [tx, keylet] = vault.create({.owner = a1, .asset = asset});
4474 env(tx);
4475 env.close();
4476 vaultKey = keylet.key;
4477 return true;
4478 });
4479 }
4480
4481 // A vault pseudo-account's MPToken cannot be deleted by anything
4482 // other than a VaultDelete transaction. Set up a vault, then have
4483 // an arbitrary tx erase the pseudo's MPToken in precheck.
4484 {
4485 uint256 vaultKey;
4487 {{"vault pseudo-account holding deleted by a "
4488 "non-VaultDelete transaction"}},
4489 [&](Account const&, Account const&, ApplyContext& ac) {
4490 auto const sleVault = ac.view().peek(keylet::vault(vaultKey));
4491 if (!sleVault)
4492 return false;
4493 auto const sleIssuance =
4494 ac.view().peek(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
4495 if (!sleIssuance || !sleIssuance->isFieldPresent(sfReferenceHolding))
4496 return false;
4497 auto sleHolding = ac.view().peek(
4498 keylet::unchecked(sleIssuance->getFieldH256(sfReferenceHolding)));
4499 if (!sleHolding)
4500 return false;
4501 ac.view().erase(sleHolding);
4502 return true;
4503 },
4504 XRPAmount{},
4505 STTx{ttACCOUNT_SET, [](STObject&) {}},
4507 [&](Account const& a1, Account const&, Env& env) {
4508 Account const issuer{"issuer"};
4509 env.fund(XRP(10'000), issuer);
4510 env.close();
4511 MPTTester mptt{env, issuer, kMptInitNoFund};
4512 mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
4513 PrettyAsset const asset = mptt.issuanceID();
4514 mptt.authorize({.account = a1});
4515 env.close();
4516
4517 Vault const vault{env};
4518 auto [tx, keylet] = vault.create({.owner = a1, .asset = asset});
4519 env(tx);
4520 env.close();
4521 vaultKey = keylet.key;
4522 return true;
4523 });
4524 }
4525
4526 // Invalid transfer
4527 std::array<std::pair<TxType, bool>, 3> const invalidTransferTests = {
4528 std::make_pair(ttAMM_WITHDRAW, false),
4529 std::make_pair(ttPAYMENT, false),
4530 std::make_pair(ttPAYMENT, true)};
4531 for (auto const enabled : {true, false})
4532 {
4533 for (auto const& [tx, crossCurrencyPayment] : invalidTransferTests)
4534 {
4535 for (auto const flag :
4536 {static_cast<std::uint32_t>(lsfMPTLocked),
4537 ~lsfMPTCanTransfer,
4538 ~lsfMPTCanTrade,
4539 0u})
4540 {
4541 MPTID id{};
4542 auto const isSuccess = !enabled || flag == 0 ||
4543 (tx == ttPAYMENT && !crossCurrencyPayment && (flag == ~lsfMPTCanTrade)) ||
4544 (tx == ttAMM_WITHDRAW &&
4545 (flag == ~lsfMPTCanTrade || flag == ~lsfMPTCanTransfer));
4546 std::pair<TER, TER> const error = isSuccess
4550 {{isSuccess ? "" : "invalid MPToken transfer between holders"}},
4551 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4552 auto update = [&](AccountID const& a, std::uint64_t v) {
4553 auto sle = ac.view().peek(keylet::mptoken(id, a));
4554 if (!sle)
4555 return false;
4556 sle->at(sfMPTAmount) = v;
4557 ac.view().update(sle);
4558 return true;
4559 };
4560 auto issuanceSle = ac.view().peek(keylet::mptokenIssuance(id));
4561 if (!issuanceSle)
4562 return false;
4563 auto const flags = issuanceSle->at(sfFlags);
4564 if (flag == lsfMPTLocked)
4565 {
4566 issuanceSle->at(sfFlags) = flags | lsfMPTLocked;
4567 }
4568 else if (flag != 0u)
4569 {
4570 issuanceSle->at(sfFlags) = flags & flag;
4571 }
4572 issuanceSle->at(sfOutstandingAmount) = 200;
4573 ac.view().update(issuanceSle);
4574 return update(a1, 101) && update(a2, 99);
4575 },
4576 XRPAmount{},
4577 STTx{
4578 tx,
4579 [&](STObject& tx) {
4580 if (crossCurrencyPayment)
4581 {
4582 tx.setFieldAmount(
4583 sfSendMax, STAmount(MPTAmount{100}, MPTIssue{id}));
4584 }
4585 }},
4586 {error.first, error.second},
4587 [&](Account const& a1, Account const& a2, Env& env) {
4588 Account const gw("gw");
4589 env.fund(XRP(1'000), gw);
4590 MPTTester const usd(
4591 {.env = env, .issuer = gw, .holders = {a1, a2}, .pay = 100});
4592 id = usd.issuanceID();
4593 if (!enabled)
4594 {
4595 env.disableFeature(featureMPTokensV2);
4596 }
4597 return true;
4598 });
4599 }
4600 }
4601 }
4602
4603 // Vault-share transfer: ValidMPTTransfer gates isVaultPseudoAccountFrozen
4604 // on fixCleanup3_3_0. Pre-amendment, vault-share transfers are allowed
4605 // even when the underlying asset is individually frozen for the sender;
4606 // post-amendment they are blocked.
4607 {
4608 Account const gw{"gw"};
4609 MPTID shareID{};
4610
4611 auto const preclose = [&](Account const& a1, Account const& a2, Env& env) -> bool {
4612 env.fund(XRP(1'000), gw);
4613 env.trust(gw["IOU"](10'000), a1);
4614 env.trust(gw["IOU"](10'000), a2);
4615 env.close();
4616 env(pay(gw, a1, gw["IOU"](500)));
4617 env(pay(gw, a2, gw["IOU"](500)));
4618 env.close();
4619
4620 PrettyAsset const iou = gw["IOU"];
4621 Vault const vault{env};
4622 auto [createTx, vaultKeylet] = vault.create({.owner = a1, .asset = iou});
4623 env(createTx);
4624 env.close();
4625 // Both a1 and a2 deposit IOU, each receiving vault shares.
4626 env(vault.deposit({.depositor = a1, .id = vaultKeylet.key, .amount = iou(100)}));
4627 env(vault.deposit({.depositor = a2, .id = vaultKeylet.key, .amount = iou(100)}));
4628 env.close();
4629
4630 shareID = env.le(vaultKeylet)->at(sfShareMPTID);
4631
4632 // Freeze a2's IOU trustline from the issuer side.
4633 // a2 is the receiver in the simulated AMM withdraw; the
4634 // distinction under test is that pre-fix330 the invariant
4635 // does not apply the transitive vault freeze to receivers.
4636 env(trust(gw, gw["IOU"](0), a2, tfSetFreeze));
4637 env.close();
4638 return true;
4639 };
4640
4641 // Simulate a vault-share transfer: a1 sends 10 shares to a2.
4642 auto const precheck =
4643 [&](Account const& a1, Account const& a2, ApplyContext& ac) -> bool {
4644 auto sle1 = ac.view().peek(keylet::mptoken(shareID, a1.id()));
4645 auto sle2 = ac.view().peek(keylet::mptoken(shareID, a2.id()));
4646 if (!sle1 || !sle2)
4647 return false;
4648 (*sle1)[sfMPTAmount] -= 10;
4649 (*sle2)[sfMPTAmount] += 10;
4650 ac.view().update(sle1);
4651 ac.view().update(sle2);
4652 return true;
4653 };
4654
4655 // post-fixCleanup3_3_0: full isFrozen() applies to all holders;
4656 // isVaultPseudoAccountFrozen finds a2's underlying IOU frozen →
4657 // invalidTransfer → invariant fires.
4659 Env{*this, defaultAmendments()},
4660 {{"invalid MPToken transfer between holders"}},
4661 precheck,
4662 XRPAmount{},
4663 STTx{ttAMM_WITHDRAW, [](STObject&) {}},
4665 preclose);
4666
4667 // pre-fixCleanup3_3_0: legacy AMM withdraw only checked
4668 // checkIndividualFrozen on the destination, not the transitive
4669 // vault freeze; a2 as receiver is exempt → invariant passes.
4671 Env{*this, defaultAmendments() - fixCleanup3_3_0},
4672 {},
4673 precheck,
4674 XRPAmount{},
4675 STTx{ttAMM_WITHDRAW, [](STObject&) {}},
4677 preclose);
4678 }
4679
4680 // Side-specific vault-share AMM_WITHDRAW invariant tests.
4681 // Both cases use a real vault (IOU underlying) and a real AMM whose
4682 // pool includes vault shares. precheck simulates an AMM_WITHDRAW by
4683 // transferring 10 vault shares from the AMM pseudo-account to a2.
4684 {
4685 MPTID shareID{};
4686 AccountID ammAcctID{};
4687 AccountID vaultPseudoID{};
4688 Account const gw{"gw"};
4689
4690 // Simulate AMM_WITHDRAW: AMM pseudo-account sends 10 vault shares
4691 // to a2. The AMM pseudo is the sender (decreasing balance);
4692 // a2 is the receiver (increasing balance).
4693 auto const precheck2 =
4694 [&](Account const& /*a1*/, Account const& a2, ApplyContext& ac) -> bool {
4695 auto sleAMM = ac.view().peek(keylet::mptoken(shareID, ammAcctID));
4696 auto sle2 = ac.view().peek(keylet::mptoken(shareID, a2.id()));
4697 if (!sleAMM || !sle2)
4698 return false;
4699 (*sleAMM)[sfMPTAmount] -= 10;
4700 (*sle2)[sfMPTAmount] += 10;
4701 ac.view().update(sleAMM);
4702 ac.view().update(sle2);
4703 return true;
4704 };
4705
4706 // Shared vault + AMM setup: a1 deposits 500 IOU into a vault and
4707 // creates an AMM with XRP + 100 vault shares, giving the AMM
4708 // pseudo-account a vault-share MPToken balance.
4709 auto const setupVaultAMM = [&](Account const& a1, Account const& a2, Env& env) -> bool {
4710 env.fund(XRP(1'000), gw);
4711 env(fset(gw, asfDefaultRipple));
4712 env.close();
4713
4714 env.trust(gw["IOU"](10'000), a1);
4715 env.trust(gw["IOU"](10'000), a2);
4716 env.close();
4717 env(pay(gw, a1, gw["IOU"](1'000)));
4718 env(pay(gw, a2, gw["IOU"](500)));
4719 env.close();
4720
4721 Vault const vault{env};
4722 auto [createTx, vaultKeylet] = vault.create({.owner = a1, .asset = gw["IOU"]});
4723 env(createTx);
4724 env.close();
4725
4726 env(vault.deposit(
4727 {.depositor = a1, .id = vaultKeylet.key, .amount = gw["IOU"](500)}));
4728 env(vault.deposit(
4729 {.depositor = a2, .id = vaultKeylet.key, .amount = gw["IOU"](200)}));
4730 env.close();
4731
4732 shareID = env.le(vaultKeylet)->at(sfShareMPTID);
4733 vaultPseudoID = env.le(vaultKeylet)->at(sfAccount);
4734
4735 // a1 creates AMM with XRP + 100 vault shares; the AMM
4736 // pseudo-account receives an MPToken record for shareID.
4737 AMM const amm(env, a1, XRP(100), STAmount{MPTIssue{shareID}, 100});
4738 ammAcctID = amm.ammAccount();
4739 return true;
4740 };
4741
4742 // Case 1: freeze the vault pseudo-account's IOU trustline.
4743 // isVaultPseudoAccountFrozen(ammAcct) calls isAnyFrozen({vaultPseudo,
4744 // ammAcct}, IOU); since vaultPseudo is frozen it returns true. The
4745 // AMM sender has a decreasing balance (not a receiver) so it is
4746 // never exempt from the check — invariant fires both pre- and
4747 // post-fixCleanup3_3_0.
4748 auto const preclose3 = [&](Account const& a1, Account const& a2, Env& env) -> bool {
4749 if (!setupVaultAMM(a1, a2, env))
4750 return false;
4751 env(trust(gw, gw["IOU"](0), Account{"vaultPseudo", vaultPseudoID}, tfSetFreeze));
4752 env.close();
4753 return true;
4754 };
4755
4757 Env{*this, defaultAmendments()},
4758 {{"invalid MPToken transfer between holders"}},
4759 precheck2,
4760 XRPAmount{},
4761 STTx{ttAMM_WITHDRAW, [](STObject&) {}},
4763 preclose3);
4764
4766 Env{*this, defaultAmendments() - fixCleanup3_3_0},
4767 {{"invalid MPToken transfer between holders"}},
4768 precheck2,
4769 XRPAmount{},
4770 STTx{ttAMM_WITHDRAW, [](STObject&) {}},
4772 preclose3);
4773
4774 // Case 2: freeze a2's (receiver's) IOU trustline.
4775 // isVaultPseudoAccountFrozen(a2) → isAnyFrozen({vaultPseudo, a2},
4776 // IOU) → true. The AMM sender's check passes (vaultPseudo and
4777 // ammAcct are not frozen). Pre-fix330: receiver is exempt from
4778 // isVaultPseudoAccountFrozen in ttAMM_WITHDRAW → passes.
4779 // Post-fix330: full isFrozen() applied to a2 → fires.
4780 auto const preclose4 = [&](Account const& a1, Account const& a2, Env& env) -> bool {
4781 if (!setupVaultAMM(a1, a2, env))
4782 return false;
4783 env(trust(gw, gw["IOU"](0), a2, tfSetFreeze));
4784 env.close();
4785 return true;
4786 };
4787
4789 Env{*this, defaultAmendments()},
4790 {{"invalid MPToken transfer between holders"}},
4791 precheck2,
4792 XRPAmount{},
4793 STTx{ttAMM_WITHDRAW, [](STObject&) {}},
4795 preclose4);
4796
4798 Env{*this, defaultAmendments() - fixCleanup3_3_0},
4799 {},
4800 precheck2,
4801 XRPAmount{},
4802 STTx{ttAMM_WITHDRAW, [](STObject&) {}},
4804 preclose4);
4805 }
4806 }
4807
4808 void
4810 {
4811 testcase << "AMM";
4812 using namespace jtx;
4813
4814 MPTID mptID{};
4815 uint256 ammID{};
4816 AccountID ammAccountID{};
4817 Account const gw{"gw"};
4818 Issue lptIssue{};
4819 PrettyAsset poolAsset{xrpIssue()};
4820
4821 auto deleteAMMAccount = [&](ApplyContext& ac, bool) {
4822 auto sle = ac.view().peek(keylet::account(ammAccountID));
4823 if (!sle)
4824 return false;
4825 ac.view().erase(sle);
4826 return true;
4827 };
4828
4829 auto updateLPTokensBalance = [&](ApplyContext& ac, std::int64_t amount) {
4830 auto sle = ac.view().peek(keylet::amm(ammID));
4831 if (!sle)
4832 return false;
4833 sle->setFieldAmount(sfLPTokenBalance, STAmount{lptIssue, amount});
4834 ac.view().update(sle);
4835 return true;
4836 };
4837 auto updateLPTokensBadAmount = [&](ApplyContext& ac, bool) {
4838 return updateLPTokensBalance(ac, -1);
4839 };
4840 auto updateLPTokensBadBalance = [&](ApplyContext& ac, bool) {
4841 return updateLPTokensBalance(ac, 200'000'000);
4842 };
4843 auto updateAMM = [&](ApplyContext& ac, bool) { return updateLPTokensBalance(ac, 10); };
4844
4845 auto updateAMMPool = [&](ApplyContext& ac, bool isMPT) {
4846 if (isMPT)
4847 {
4848 auto sle = ac.view().peek(keylet::mptoken(mptID, ammAccountID));
4849 if (!sle)
4850 return false;
4851 sle->setFieldU64(sfMPTAmount, 1);
4852 ac.view().update(sle);
4853 return true;
4854 }
4855 auto sle = ac.view().peek(keylet::account(ammAccountID));
4856 if (!sle)
4857 return false;
4858 sle->setFieldAmount(sfBalance, XRP(1));
4859 ac.view().update(sle);
4860 return true;
4861 };
4862
4863 auto test = [&](auto const txType,
4864 auto&& update,
4865 bool isMPT,
4866 TER error = tecINVARIANT_FAILED) {
4868 {{"AMM"}},
4869 [&](Account const&, Account const&, ApplyContext& ac) { return update(ac, isMPT); },
4870 XRPAmount{},
4871 STTx{txType, [&](STObject& tx) {}},
4872 {tecINVARIANT_FAILED, error},
4873 [&](Account const&, Account const&, Env& env) {
4874 env.fund(XRP(1'000), gw);
4875 poolAsset = [&]() -> PrettyAsset {
4876 if (isMPT)
4877 {
4878 MPT const mpt = MPTTester({.env = env, .issuer = gw});
4879 mptID = mpt.issuanceID;
4880 return mpt;
4881 }
4882 return gw["USD"];
4883 }();
4884 AMM const amm(env, gw, XRP(100), poolAsset(100));
4885 ammAccountID = amm.ammAccount();
4886 ammID = amm.ammID();
4887 lptIssue = amm.lptIssue();
4888 return true;
4889 });
4890 };
4891
4892 for (bool const isMPT : {false, true})
4893 {
4894 auto const error = isMPT ? TER(tecINVARIANT_FAILED) : TER(tefINVARIANT_FAILED);
4895 for (auto txType : {ttAMM_CREATE, ttAMM_DEPOSIT, ttAMM_CLAWBACK, ttAMM_WITHDRAW})
4896 {
4897 test(txType, deleteAMMAccount, isMPT, tefINVARIANT_FAILED);
4898 test(txType, updateLPTokensBadAmount, isMPT);
4899 test(txType, updateLPTokensBadBalance, isMPT);
4900 }
4901 for (auto txType : {ttAMM_BID, ttAMM_VOTE})
4902 {
4903 test(txType, updateAMMPool, isMPT, error);
4904 test(txType, updateLPTokensBadAmount, isMPT);
4905 test(txType, updateLPTokensBadBalance, isMPT);
4906 }
4907 for (auto txType : {ttAMM_DELETE, ttCHECK_CASH, ttOFFER_CREATE, ttPAYMENT})
4908 {
4909 test(txType, updateAMM, isMPT);
4910 }
4911 }
4912 }
4913
4914 // Test the invariant overwrite fix for both pre- and post-amendment
4915 // behavior. With the fix enabled, |= accumulates violations across
4916 // entries so a later valid entry cannot clear an earlier violation.
4917 // Without the fix, = assignment means the last-visited entry wins.
4918 void
4920 {
4921 using namespace test::jtx;
4922 bool const fixEnabled = features[fixCleanup3_1_3];
4925
4926 // Insert two trust line SLEs in hash-sorted order, with the "bad"
4927 // entry at the lower-sorting key so it is visited first by
4928 // ApplyStateTable::visit(). The configurer callables receive the
4929 // SLE and the Issue corresponding to that side's keylet currency.
4930 auto const insertOrderedTrustLinePair = [](ApplyContext& ac,
4931 Account const& a1,
4932 Account const& a2,
4933 Account const& a3,
4934 auto const& badConfig,
4935 auto const& goodConfig) {
4936 char const* const c1 = "USD";
4937 char const* const c2 = "EUR";
4938 auto const k1 = keylet::trustLine(a1, a2, a1[c1].currency);
4939 auto const k2 = keylet::trustLine(a1, a3, a1[c2].currency);
4940
4941 bool const k1First = k1.key < k2.key;
4942 auto const& badKey = k1First ? k1 : k2;
4943 auto const& goodKey = k1First ? k2 : k1;
4944 Issue const badIss{k1First ? a1[c1].currency : a1[c2].currency, a1.id()};
4945 Issue const goodIss{k1First ? a1[c2].currency : a1[c1].currency, a1.id()};
4946
4947 auto const sleBad = std::make_shared<SLE>(badKey);
4948 badConfig(*sleBad, badIss);
4949 ac.view().insert(sleBad);
4950
4951 auto const sleGood = std::make_shared<SLE>(goodKey);
4952 goodConfig(*sleGood, goodIss);
4953 ac.view().insert(sleGood);
4954 };
4955
4956 // Regression: bad XRP trust line followed by a valid trust line.
4957 // With the fix, the invariant catches the violation. Without it,
4958 // the valid entry overwrites the flag to false. The keylet
4959 // currencies are non-XRP (the invariant inspects sfLowLimit /
4960 // sfHighLimit issue, not the keylet currency).
4961 testcase << "overwrite: NoXRPTrustLines" + std::string(fixEnabled ? " fix" : "");
4963 Env(*this, features),
4964 fixEnabled ? std::vector<std::string>{{"an XRP trust line was created"}}
4966 [&insertOrderedTrustLinePair](Account const& a1, Account const& a2, ApplyContext& ac) {
4967 Account const a3{"A3"};
4968 insertOrderedTrustLinePair(
4969 ac,
4970 a1,
4971 a2,
4972 a3,
4973 [](SLE& sle, Issue const& iss) {
4974 // sfLowLimit has xrpIssue, making isXrp = true
4975 sle.setFieldAmount(sfLowLimit, STAmount{xrpIssue(), 0});
4976 sle.setFieldAmount(sfHighLimit, STAmount{iss, 0});
4977 },
4978 [](SLE& sle, Issue const& iss) {
4979 sle.setFieldAmount(sfLowLimit, STAmount{iss, 0});
4980 sle.setFieldAmount(sfHighLimit, STAmount{iss, 0});
4981 });
4982 return true;
4983 },
4984 XRPAmount{},
4985 STTx{ttACCOUNT_SET, [](STObject&) {}},
4986 fixEnabled ? failTers : passTers);
4987
4988 // Regression: bad deep-freeze trust line followed by a valid one.
4989 testcase << "overwrite: NoDeepFreeze" + std::string(fixEnabled ? " fix" : "");
4991 Env(*this, features),
4992 fixEnabled ? std::vector<std::string>{{"a trust line with deep freeze flag without "
4993 "normal freeze was created"}}
4995 [&insertOrderedTrustLinePair](Account const& a1, Account const& a2, ApplyContext& ac) {
4996 Account const a3{"A3"};
4997 insertOrderedTrustLinePair(
4998 ac,
4999 a1,
5000 a2,
5001 a3,
5002 [](SLE& sle, Issue const& iss) {
5003 sle.setFieldAmount(sfLowLimit, STAmount{iss, 0});
5004 sle.setFieldAmount(sfHighLimit, STAmount{iss, 0});
5005 sle.setFieldU32(sfFlags, lsfLowDeepFreeze);
5006 },
5007 [](SLE& sle, Issue const& iss) {
5008 sle.setFieldAmount(sfLowLimit, STAmount{iss, 0});
5009 sle.setFieldAmount(sfHighLimit, STAmount{iss, 0});
5010 sle.setFieldU32(sfFlags, 0u);
5011 });
5012 return true;
5013 },
5014 XRPAmount{},
5015 STTx{ttACCOUNT_SET, [](STObject&) {}},
5016 fixEnabled ? failTers : passTers);
5017
5018 // Regression: MPT OutstandingAmount exceeds max, but locked <=
5019 // outstanding. Plain assignment would overwrite bad_ = true.
5020 // With the fix, NoZeroEscrow catches it.
5021 // Without the fix, NoZeroEscrow passes but ValidMPTIssuance
5022 // still fires ("a MPT issuance was created").
5023 testcase << "overwrite: NoZeroEscrow MPT" + std::string(fixEnabled ? " fix" : "");
5025 Env(*this, features),
5026 fixEnabled ? std::vector<std::string>{{"escrow specifies invalid amount"}}
5027 : std::vector<std::string>{{"a MPT issuance was created"}},
5028 [](Account const& a1, Account const&, ApplyContext& ac) {
5029 auto const sle = ac.view().peek(keylet::account(a1.id()));
5030 if (!sle)
5031 return false;
5032
5033 MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
5035 // outstanding exceeds kMaxMpTokenAmount -> checkAmount sets bad_
5036 sleNew->setFieldU64(sfOutstandingAmount, kMaxMpTokenAmount + 1);
5037 // locked is valid and <= outstanding -> must NOT clear bad_
5038 sleNew->setFieldU64(sfLockedAmount, 10);
5039 ac.view().insert(sleNew);
5040 return true;
5041 },
5042 XRPAmount{},
5043 STTx{ttACCOUNT_SET, [](STObject&) {}},
5044 failTers);
5045 }
5046
5047 void
5049 {
5050 using namespace jtx;
5051
5052 Account const issuer{"issuer"};
5053 PrettyAsset const vaultAsset = issuer["IOU"];
5054
5055 struct TestCase
5056 {
5057 std::string name;
5058 std::int32_t expectedMinScale;
5060 };
5061
5062 for (auto const mantissaScale : {
5065 })
5066 {
5067 NumberMantissaScaleGuard const g{mantissaScale};
5068
5069 auto makeDelta = [&vaultAsset](Number const& n) -> ValidVault::DeltaInfo {
5070 return {.delta = n, .scale = scale(n, vaultAsset.raw())};
5071 };
5072
5073 auto const testCases = std::vector<TestCase>{
5074 {
5075 .name = "No values",
5076 .expectedMinScale = 0,
5077 .values = {},
5078 },
5079 {
5080 .name = "Mixed integer and Number values",
5081 .expectedMinScale = -15,
5082 .values = {makeDelta(1), makeDelta(-1), makeDelta(Number{10, -1})},
5083 },
5084 {
5085 .name = "Mixed scales",
5086 .expectedMinScale = -17,
5087 .values =
5088 {makeDelta(Number{1, -2}),
5089 makeDelta(Number{5, -3}),
5090 makeDelta(Number{3, -2})},
5091 },
5092 {
5093 .name = "Equal scales",
5094 .expectedMinScale = -16,
5095 .values =
5096 {makeDelta(Number{1, -1}),
5097 makeDelta(Number{5, -1}),
5098 makeDelta(Number{1, -1})},
5099 },
5100 {
5101 .name = "Mixed mantissa sizes",
5102 .expectedMinScale = -12,
5103 .values =
5104 {makeDelta(Number{1}),
5105 makeDelta(Number{1234, -3}),
5106 makeDelta(Number{12345, -6}),
5107 makeDelta(Number{123, 1})},
5108 },
5109 };
5110
5111 for (auto const& tc : testCases)
5112 {
5113 testcase("vault computeCoarsestScale: " + tc.name);
5114
5115 auto const actualScale = ValidVault::computeCoarsestScale(tc.values);
5116
5117 BEAST_EXPECTS(
5118 actualScale == tc.expectedMinScale,
5119 "expected: " + std::to_string(tc.expectedMinScale) +
5120 ", actual: " + std::to_string(actualScale));
5121 for (auto const& num : tc.values)
5122 {
5123 // None of these scales are far enough apart that rounding the
5124 // values would lose information, so check that the rounded
5125 // value matches the original.
5126 auto const actualRounded = roundToAsset(vaultAsset, num.delta, actualScale);
5127 BEAST_EXPECTS(
5128 actualRounded == num.delta,
5129 "number " + to_string(num.delta) + " rounded to scale " +
5130 std::to_string(actualScale) + " is " + to_string(actualRounded));
5131 }
5132 }
5133
5134 auto const testCases2 = std::vector<TestCase>{
5135 {
5136 .name = "False equivalence",
5137 .expectedMinScale = -15,
5138 .values =
5139 {
5140 makeDelta(Number{1234567890123456789, -18}),
5141 makeDelta(Number{12345, -4}),
5142 makeDelta(Number{1}),
5143 },
5144 },
5145 };
5146
5147 // Unlike the first set of test cases, the values in these test could
5148 // look equivalent if using the wrong scale.
5149 for (auto const& tc : testCases2)
5150 {
5151 testcase("vault computeCoarsestScale: " + tc.name);
5152
5153 auto const actualScale = ValidVault::computeCoarsestScale(tc.values);
5154
5155 BEAST_EXPECTS(
5156 actualScale == tc.expectedMinScale,
5157 "expected: " + std::to_string(tc.expectedMinScale) +
5158 ", actual: " + std::to_string(actualScale));
5160 Number firstRounded;
5161 for (auto const& num : tc.values)
5162 {
5163 if (!first)
5164 {
5165 first = num.delta;
5166 firstRounded = roundToAsset(vaultAsset, num.delta, actualScale);
5167 continue;
5168 }
5169 auto const numRounded = roundToAsset(vaultAsset, num.delta, actualScale);
5170 BEAST_EXPECTS(
5171 numRounded != firstRounded,
5172 "at a scale of " + std::to_string(actualScale) + " " +
5173 to_string(num.delta) + " == " + to_string(*first));
5174 }
5175 }
5176 }
5177 }
5178
5179 void
5181 {
5182 using namespace test::jtx;
5183 testcase << "ValidConfidentialMPToken";
5184
5185 MPTID mptID;
5186
5187 // Generate an MPT with privacy, issue 100 tokens to A2.
5188 // Perform a confidential conversion to populate encrypted state.
5189 auto const precloseConfidential =
5190 [&mptID](Account const& a1, Account const& a2, Env& env) -> bool {
5191 MPTTester mpt(env, a1, {.holders = {a2}, .fund = false});
5192 mpt.create({.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance});
5193 mptID = mpt.issuanceID();
5194
5195 mpt.authorize({.account = a2});
5196 mpt.pay(a1, a2, 100);
5197
5198 mpt.generateKeyPair(a1);
5199 mpt.set({.account = a1, .issuerPubKey = mpt.getPubKey(a1)});
5200
5201 mpt.generateKeyPair(a2);
5202 mpt.convert({
5203 .account = a2,
5204 .amt = 100,
5205 .holderPubKey = mpt.getPubKey(a2),
5206 });
5207 return true;
5208 };
5209
5210 // badDelete
5212 {"MPToken deleted with encrypted fields while COA > 0"},
5213 [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
5214 auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
5215 if (!sleToken)
5216 return false;
5217 // Force an erase of the object while the COA remains 100
5218 ac.view().erase(sleToken);
5219 return true;
5220 },
5221 XRPAmount{},
5222 STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
5224 precloseConfidential);
5225
5226 // badConsistency
5228 {"MPToken encrypted field existence inconsistency"},
5229 [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
5230 auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
5231 if (!sleToken)
5232 return false;
5233 // Remove one of the required encrypted fields to create a mismatch
5234 sleToken->makeFieldAbsent(sfIssuerEncryptedBalance);
5235 ac.view().update(sleToken);
5236 return true;
5237 },
5238 XRPAmount{},
5239 STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
5241 precloseConfidential);
5242
5244 {"MPToken encrypted field existence inconsistency"},
5245 [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
5246 auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
5247 if (!sleToken)
5248 return false;
5249 sleToken->makeFieldAbsent(sfIssuerEncryptedBalance);
5250 sleToken->makeFieldAbsent(sfConfidentialBalanceInbox);
5251 sleToken->makeFieldAbsent(sfConfidentialBalanceSpending);
5252 sleToken->setFieldVL(sfAuditorEncryptedBalance, Blob{0x00});
5253 ac.view().update(sleToken);
5254 return true;
5255 },
5256 XRPAmount{},
5257 STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
5259 precloseConfidential);
5260
5261 // requiresPrivacyFlag
5262 auto const precloseNoPrivacy = [&mptID](
5263 Account const& a1, Account const& a2, Env& env) -> bool {
5264 MPTTester mpt(env, a1, {.holders = {a2}, .fund = false});
5265 // completely omitted the tfMPTCanHoldConfidentialBalance flag here.
5266 mpt.create({.flags = tfMPTCanTransfer});
5267 mptID = mpt.issuanceID();
5268 mpt.authorize({.account = a2});
5269 mpt.pay(a1, a2, 100);
5270 return true;
5271 };
5272
5274 {"MPToken has encrypted fields but Issuance does not have "
5275 "lsfMPTCanHoldConfidentialBalance "
5276 "set"},
5277 [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
5278 auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
5279 if (!sleToken)
5280 return false;
5281 // Inject all three encrypted fields consistently (inbox+spending+issuer must be
5282 // in sync or badConsistency fires first and masks requiresPrivacyFlag).
5283 sleToken->setFieldVL(sfConfidentialBalanceInbox, Blob{0x00});
5284 sleToken->setFieldVL(sfConfidentialBalanceSpending, Blob{0x00});
5285 sleToken->setFieldVL(sfIssuerEncryptedBalance, Blob{0x00});
5286 ac.view().update(sleToken);
5287 return true;
5288 },
5289 XRPAmount{},
5290 STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
5292 precloseNoPrivacy);
5293
5294 // badCOA
5296 {"Confidential outstanding amount exceeds total outstanding amount"},
5297 [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
5298 auto sleIssuance = ac.view().peek(keylet::mptokenIssuance(mptID));
5299 if (!sleIssuance)
5300 return false;
5301 // Total outstanding is natively 100; bloat the COA over 100
5302 sleIssuance->setFieldU64(sfConfidentialOutstandingAmount, 200);
5303 ac.view().update(sleIssuance);
5304 return true;
5305 },
5306 XRPAmount{},
5307 STTx{ttMPTOKEN_ISSUANCE_SET, [](STObject&) {}},
5309 precloseConfidential);
5310
5311 // Conservation Violation
5313 {"Token conservation violation for MPT"},
5314 [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
5315 auto sleIssuance = ac.view().peek(keylet::mptokenIssuance(mptID));
5316 if (!sleIssuance)
5317 return false;
5318
5319 sleIssuance->setFieldU64(
5320 sfConfidentialOutstandingAmount,
5321 sleIssuance->getFieldU64(sfConfidentialOutstandingAmount) - 10);
5322 ac.view().update(sleIssuance);
5323
5324 return true;
5325 },
5326 XRPAmount{},
5327 STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
5329 precloseConfidential);
5330
5331 // Send/MergeInbox must not change OutstandingAmount (coaDelta == 0)
5333 {"Invariant failed: OutstandingAmount changed "
5334 "by confidential transaction that should not "
5335 "modify it for MPT"},
5336 [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
5337 auto sleIssuance = ac.view().peek(keylet::mptokenIssuance(mptID));
5338 if (!sleIssuance)
5339 return false;
5340 sleIssuance->setFieldU64(
5341 sfOutstandingAmount, sleIssuance->getFieldU64(sfOutstandingAmount) + 1);
5342 ac.view().update(sleIssuance);
5343 return true;
5344 },
5345 XRPAmount{},
5346 STTx{ttCONFIDENTIAL_MPT_SEND, [](STObject&) {}},
5348 precloseConfidential);
5349
5350 // Send/MergeInbox and zero-COA-delta confidential transactions must not
5351 // change public holder MPTAmount.
5353 {"Invariant failed: MPTAmount changed by confidential "
5354 "transaction that should not modify this field."},
5355 [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
5356 auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
5357 if (!sleToken)
5358 return false;
5359 sleToken->setFieldU64(sfMPTAmount, sleToken->getFieldU64(sfMPTAmount) + 1);
5360 ac.view().update(sleToken);
5361 return true;
5362 },
5363 XRPAmount{},
5364 STTx{ttCONFIDENTIAL_MPT_SEND, [](STObject&) {}},
5366 precloseConfidential);
5367
5368 // badVersion
5370 {"MPToken sfConfidentialBalanceVersion not updated when sfConfidentialBalanceSpending "
5371 "changed"},
5372 [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
5373 Blob const kChangedConfidentialSpending = {0xBA, 0xDD};
5374 auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
5375 if (!sleToken)
5376 return false;
5377 sleToken->setFieldVL(sfConfidentialBalanceSpending, kChangedConfidentialSpending);
5378
5379 // DO NOT update sfConfidentialBalanceVersion
5380 ac.view().update(sleToken);
5381 return true;
5382 },
5383 XRPAmount{},
5384 STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
5386 precloseConfidential);
5387
5388 // Skipping Deleted MPTs (Issuance deleted)
5389 auto const precloseOrphan = [&mptID](
5390 Account const& a1, Account const& a2, Env& env) -> bool {
5391 MPTTester mpt(env, a1, {.holders = {a2}, .fund = false});
5392 mpt.create({.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance});
5393 mptID = mpt.issuanceID();
5394 mpt.authorize({.account = a2});
5395
5396 // Generate privacy keys and convert 0 amount so Bob has the encrypted fields
5397 mpt.generateKeyPair(a1);
5398 mpt.set({.account = a1, .issuerPubKey = mpt.getPubKey(a1)});
5399 mpt.generateKeyPair(a2);
5400 mpt.convert({
5401 .account = a2,
5402 .amt = 0,
5403 .holderPubKey = mpt.getPubKey(a2),
5404 });
5405
5406 // Immediately destroy the issuance. A2's empty, encrypted token object lives on.
5407 mpt.destroy();
5408 return true;
5409 };
5410
5412 {},
5413 [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
5414 auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
5415 if (!sleToken)
5416 return false;
5417 // Safely able to erase the deleted token.
5418 ac.view().erase(sleToken);
5419 return true;
5420 },
5421 XRPAmount{},
5422 STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
5424 precloseOrphan);
5425 }
5426
5427public:
5428 void
5462};
5463
5465
5466} // namespace xrpl::test
A generic endpoint for log messages.
Definition Journal.h:38
A testsuite class.
Definition suite.h:50
LogOs< char > log
Logging output stream.
Definition suite.h:146
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:149
Represents a JSON value.
Definition json_value.h:130
State information when applying a tx.
ApplyView & view()
Writeable view to a ledger, for applying a transaction.
Definition ApplyView.h:118
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.
virtual void erase(SLE::ref sle)=0
Remove a peeked 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:340
virtual void update(SLE::ref sle)=0
Indicate changes to a peeked SLE.
Specifies an order book.
Definition Book.h:16
SOTemplate const * findSOTemplateBySField(SField const &sField) const
static InnerObjectFormats const & getInstance()
A currency issued by an account.
Definition Issue.h:13
constexpr MPTID const & getMptID() const
Definition MPTIssue.h:33
Sets the new scale and restores the old scale when it leaves scope.
Definition Number.h:920
Number is a floating point type that can represent a wide range of values.
Definition Number.h:306
Writable ledger view that accumulates state and tx changes.
Definition OpenView.h:45
void rawInsert(SLE::ref sle) override
Unconditionally insert a state item.
Definition OpenView.cpp:237
void rawErase(SLE::ref sle) override
Delete an existing state item.
Definition OpenView.cpp:231
bool exists(Keylet const &k) const override
Determine if a state item exists.
Definition OpenView.cpp:154
virtual Rules const & rules() const =0
Returns the tx processing rules.
LedgerIndex seq() const
Returns the sequence number of the base ledger.
Definition ReadView.h:97
bool enabled(uint256 const &feature) const
Returns true if a feature is enabled.
Definition Rules.cpp:171
Identifies fields.
Definition SField.h:130
Defines the fields and their attributes within a STObject.
Definition SOTemplate.h:96
static std::uint64_t const kURateOne
Definition STAmount.h:64
AccountID const & getIssuer() const
Definition STAmount.h:498
iterator begin()
Definition STArray.h:216
void pushBack(STObject const &object)
Definition STArray.h:204
std::shared_ptr< STLedgerEntry > pointer
void setFieldU32(SField const &field, std::uint32_t)
Definition STObject.cpp:733
void setFieldAmount(SField const &field, STAmount const &)
Definition STObject.cpp:793
static STObject makeInnerObject(SField const &name)
Definition STObject.cpp:74
void setAccountID(SField const &field, AccountID const &)
Definition STObject.cpp:775
void pushBack(uint256 const &v)
An immutable linear range of bytes.
Definition Slice.h:26
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
void visitEntry(bool, SLE::const_ref, SLE::const_ref)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
void visitEntry(bool, std::shared_ptr< SLE const > const &, std::shared_ptr< SLE const > const &)
static std::int32_t computeCoarsestScale(std::vector< DeltaInfo > const &numbers)
void run() override
Runs the suite.
void testInvariantOverwrite(FeatureBitset features)
std::function< bool(test::jtx::Account const &a, test::jtx::Account const &b, test::jtx::Env &env)> Preclose
void doInvariantCheck(test::jtx::Env &&env, test::jtx::Account const &a1, test::jtx::Account const &a2, 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})
Keylet createLoanBroker(jtx::Account const &a, jtx::Env &env, jtx::PrettyAsset const &asset)
void testPermissionedDEX(FeatureBitset features)
static SLE::pointer createPermissionedDomain(ApplyContext &ac, test::jtx::Account const &a1, test::jtx::Account const &a2, std::uint32_t numCreds=2, std::uint32_t seq=10)
TxAccount
Run a specific test case to put the ledger into a state that will be detected by an invariant.
static std::pair< std::uint32_t, uint256 > createPermissionedDomainEnv(test::jtx::Env &env, test::jtx::Account const &a1, test::jtx::Account const &a2, std::uint32_t numCreds=2)
void testPermissionedDomainInvariants(FeatureBitset features)
std::function< bool(test::jtx::Account const &a, test::jtx::Account const &b, ApplyContext &ac)> Precheck
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)
void testAMMDeleteInvariants(FeatureBitset features)
void doInvariantCheck(test::jtx::Env &&env, 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)
static FeatureBitset defaultAmendments()
std::stringstream const & messages() const
Convenience class to test AMM functionality.
Immutable cryptographic account descriptor.
Definition jtx/Account.h:17
std::string const & human() const
Returns the human readable public key.
Definition jtx/Account.h:92
AccountID id() const
Returns the Account ID.
Definition jtx/Account.h:85
A transaction testing environment.
Definition Env.h:143
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:284
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:296
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition Env.cpp:275
std::shared_ptr< STObject const > meta()
Return metadata for the last JTx.
Definition Env.cpp:511
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:353
Set the fee on a JTx.
Definition fee.h:15
Converts to IOU Issue or STAmount.
Test helper for creating, mutating, and asserting MPT and confidential MPT ledger state.
Definition mpt.h:385
void create(MPTCreate const &arg=MPTCreate{})
Definition mpt.cpp:256
void authorize(MPTAuthorize const &arg=MPTAuthorize{})
Definition mpt.cpp:368
MPTID const & issuanceID() const
Definition mpt.h:576
Converts to MPT Issue or STAmount.
T get(T... args)
T invoke(T... args)
T iter_swap(T... args)
T make_pair(T... args)
T make_shared(T... args)
std::uint64_t insertKey(ApplyView &view, SLE::ref node, std::uint64_t page, bool preserveOrder, STVector256 &indexes, uint256 const &key)
Definition ApplyView.cpp:70
std::optional< std::uint64_t > insertPage(ApplyView &view, std::uint64_t page, SLE::pointer node, std::uint64_t nextPage, SLE::ref next, uint256 const &key, Keylet const &directory, std::function< void(SLE::ref)> const &describe)
std::uint64_t createRoot(ApplyView &view, Keylet const &directory, uint256 const &key, std::function< void(SLE::ref)> const &describe)
Helper functions for managing low-level directory operations.
Definition ApplyView.cpp:30
Keylet computation functions.
Definition Indexes.h:34
Keylet quality(Keylet const &k, std::uint64_t q) noexcept
The initial directory page for a specific quality.
Definition Indexes.cpp:270
Keylet loanBroker(AccountID const &owner, std::uint32_t seq) noexcept
Definition Indexes.cpp:557
Keylet unchecked(uint256 const &key) noexcept
Any ledger entry.
Definition Indexes.cpp:351
Keylet mptokenIssuance(std::uint32_t seq, AccountID const &issuer) noexcept
Definition Indexes.cpp:521
Keylet book(Book const &b)
The beginning of an order book.
Definition Indexes.cpp:235
Keylet escrow(AccountID const &src, std::uint32_t seq) noexcept
An escrow entry.
Definition Indexes.cpp:372
Keylet const & amendments() noexcept
The index of the amendment table.
Definition Indexes.cpp:214
Keylet ownerDir(AccountID const &id) noexcept
The root page of an account's directory.
Definition Indexes.cpp:357
Keylet check(AccountID const &id, std::uint32_t seq) noexcept
A Check.
Definition Indexes.cpp:322
Keylet amm(Asset const &issue1, Asset const &issue2) noexcept
AMM entry.
Definition Indexes.cpp:425
Keylet nftokenPageMin(AccountID const &owner)
NFT page keylets.
Definition Indexes.cpp:384
Keylet offer(AccountID const &id, std::uint32_t seq) noexcept
An offer from an account.
Definition Indexes.cpp:264
Keylet nftokenPage(Keylet const &k, uint256 const &token)
Definition Indexes.cpp:400
Keylet vault(AccountID const &owner, std::uint32_t seq) noexcept
Definition Indexes.cpp:551
Keylet nftokenPageMax(AccountID const &owner)
A keylet for the owner's last possible NFT page.
Definition Indexes.cpp:392
Keylet mptoken(MPTID const &issuanceID, AccountID const &holder) noexcept
Definition Indexes.cpp:533
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:186
Keylet permissionedDomain(AccountID const &account, std::uint32_t seq) noexcept
Definition Indexes.cpp:569
Keylet page(uint256 const &root, std::uint64_t index=0) noexcept
A page in a directory.
Definition Indexes.cpp:363
Keylet trustLine(AccountID const &id0, AccountID const &id1, Currency const &currency) noexcept
The index of a trust line for a given currency.
Definition Indexes.cpp:241
json::Value coverDeposit(AccountID const &account, uint256 const &brokerID, STAmount const &amount, uint32_t flags)
std::vector< Credential > Credentials
json::Value setTx(AccountID const &account, Credentials const &credentials, std::optional< uint256 > domain)
uint256 getNewDomain(std::shared_ptr< STObject const > const &meta)
json::Value mint(jtx::Account const &account, std::uint32_t nfTokenTaxon)
Mint an NFToken.
Definition token.cpp:23
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:76
static IncrementT const kIncrement
Definition tags.h:29
json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition trust.cpp:18
PrettyAmount drops(Integer i)
Returns an XRP PrettyAmount, which is trivially convertible to STAmount.
json::Value fset(Account const &account, std::uint32_t on, std::uint32_t off=0)
Add and/or remove flag.
Definition flags.cpp:15
static MPTInit const kMptInitNoFund
Definition mpt.h:142
BEAST_DEFINE_TESTSUITE(AMMClawback, app, xrpl)
STTx createTx(bool disabling, LedgerIndex seq, PublicKey const &txKey)
Create ttUNL_MODIFY Tx.
constexpr XRPAmount
Convert XRP to drops (integral types).
Definition TxTest.h:48
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:97
std::vector< SField const * > const & getPseudoAccountFields()
Returns the list of fields that define an ACCOUNT_ROOT as a pseudo-account if set.
AccountID pseudoAccountAddress(ReadView const &view, uint256 const &pseudoOwnerKey)
Generate a pseudo-account address from a pseudo owner key.
TxType
Transaction type identifiers.
Definition TxFormats.h:41
int scale(Number const &number, Asset const &asset)
Get the scale of a Number for a given asset.
Definition STAmount.h:779
@ tefINVARIANT_FAILED
Definition TER.h:173
std::array< KeyletDesc< AccountID const & >, 6 > const kDirectAccountKeylets
Definition Indexes.h:360
BaseUInt< 160, detail::CurrencyTag > Currency
Currency is a hash representing a specific currency.
Definition UintTypes.h:36
TER trustDelete(ApplyView &view, SLE::ref sleRippleState, AccountID const &uLowAccountID, AccountID const &uHighAccountID, beast::Journal j)
std::string transToken(TER code)
Definition TER.cpp:247
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:633
STLedgerEntry SLE
TER deleteAMMAccount(Sandbox &view, Asset const &asset, Asset const &asset2, beast::Journal j)
Delete trustlines to AMM.
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:44
void adjustOwnerCount(ApplyView &view, SLE::ref sle, std::int32_t amount, beast::Journal j)
Adjust the owner count up or down.
std::function< void(SLE::ref)> describeOwnerDir(AccountID const &account)
Returns a function that sets the owner on a directory SLE.
void roundToAsset(A const &asset, Number &value)
Round an arbitrary precision Number IN PLACE to the precision of a given Asset.
Definition STAmount.h:722
@ TapNone
Definition ApplyView.h:13
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:28
bool isTesSuccess(TER x) noexcept
Definition TER.h:663
constexpr std::uint8_t kVaultStrategyFirstComeFirstServe
Vault withdrawal policies.
Definition Protocol.h:245
TERSubset< CanCvtToTER > TER
Definition TER.h:634
constexpr std::size_t kMaxPermissionedDomainCredentialsArraySize
The maximum number of credentials can be passed in array for permissioned domain.
Definition Protocol.h:232
LedgerEntryType
Identifiers for on-ledger objects.
@ tecINCOMPLETE
Definition TER.h:333
@ tecINVARIANT_FAILED
Definition TER.h:311
std::vector< unsigned char > Blob
Storage for linear binary data.
Definition Blob.h:10
Currency const & badCurrency()
We deliberately disallow the currency that looks like "XRP" because too many people were using it ins...
constexpr std::uint64_t kMaxMpTokenAmount
The maximum amount of MPTokenIssuance.
Definition Protocol.h:238
constexpr XRPAmount kInitialXrp
Configure the native currency.
BaseUInt< 256 > uint256
Definition base_uint.h:562
MPTID makeMptID(std::uint32_t sequence, AccountID const &account)
Definition Indexes.cpp:172
@ tesSUCCESS
Definition TER.h:240
T str(T... args)
A pair of SHAMap key and LedgerEntryType.
Definition Keylet.h:19
uint256 key
Definition Keylet.h:20
LedgerEntryType type
Definition Keylet.h:21
T to_string(T... args)