xrpld
Loading...
Searching...
No Matches
MPToken_test.cpp
1#include <test/jtx/AMM.h>
2#include <test/jtx/AMMTest.h>
3#include <test/jtx/Account.h>
4#include <test/jtx/Env.h>
5#include <test/jtx/TestHelpers.h>
6#include <test/jtx/amount.h>
7#include <test/jtx/balance.h> // IWYU pragma: keep
8#include <test/jtx/check.h>
9#include <test/jtx/credentials.h>
10#include <test/jtx/delivermin.h>
11#include <test/jtx/deposit.h>
12#include <test/jtx/domain.h>
13#include <test/jtx/envconfig.h>
14#include <test/jtx/escrow.h>
15#include <test/jtx/fee.h>
16#include <test/jtx/flags.h>
17#include <test/jtx/mpt.h>
18#include <test/jtx/multisign.h>
19#include <test/jtx/offer.h>
20#include <test/jtx/paths.h>
21#include <test/jtx/pay.h>
22#include <test/jtx/permissioned_dex.h>
23#include <test/jtx/permissioned_domains.h>
24#include <test/jtx/sendmax.h>
25#include <test/jtx/ter.h>
26#include <test/jtx/trust.h>
27#include <test/jtx/txflags.h>
28#include <test/jtx/vault.h>
29#include <test/jtx/xchain_bridge.h>
30
31#include <xrpl/basics/base_uint.h>
32#include <xrpl/basics/strHex.h>
33#include <xrpl/beast/unit_test/suite.h>
34#include <xrpl/beast/utility/Journal.h>
35#include <xrpl/json/json_value.h>
36#include <xrpl/json/to_string.h>
37#include <xrpl/ledger/ApplyView.h>
38#include <xrpl/ledger/ApplyViewImpl.h>
39#include <xrpl/ledger/helpers/MPTokenHelpers.h>
40#include <xrpl/ledger/helpers/TokenHelpers.h>
41#include <xrpl/protocol/Asset.h>
42#include <xrpl/protocol/Feature.h>
43#include <xrpl/protocol/IOUAmount.h>
44#include <xrpl/protocol/Indexes.h>
45#include <xrpl/protocol/Issue.h>
46#include <xrpl/protocol/MPTAmount.h>
47#include <xrpl/protocol/MPTIssue.h>
48#include <xrpl/protocol/Protocol.h>
49#include <xrpl/protocol/Quality.h>
50#include <xrpl/protocol/SField.h>
51#include <xrpl/protocol/SOTemplate.h>
52#include <xrpl/protocol/STAmount.h>
53#include <xrpl/protocol/STPathSet.h>
54#include <xrpl/protocol/STTx.h>
55#include <xrpl/protocol/Serializer.h>
56#include <xrpl/protocol/TER.h>
57#include <xrpl/protocol/TxFlags.h>
58#include <xrpl/protocol/TxFormats.h>
59#include <xrpl/protocol/UintTypes.h>
60#include <xrpl/protocol/XRPAmount.h>
61#include <xrpl/protocol/jss.h>
62
63#include <array>
64#include <cstdint>
65#include <functional>
66#include <initializer_list>
67#include <limits>
68#include <memory>
69#include <optional>
70#include <set>
71#include <stdexcept>
72#include <string>
73#include <string_view>
74#include <tuple>
75#include <utility>
76#include <vector>
77
78namespace xrpl::test {
79
81{
82 void
84 {
85 testcase("Create Validate");
86 using namespace test::jtx;
87 Account const alice("alice");
88
89 // test preflight of MPTokenIssuanceCreate
90 {
91 // If the MPT amendment is not enabled, you should not be able to
92 // create MPTokenIssuance
93 Env env{*this, features - featureMPTokensV1};
94 MPTTester mptAlice(env, alice);
95
96 mptAlice.create({.ownerCount = 0, .err = temDISABLED});
97 }
98
99 // test preflight of MPTokenIssuanceCreate
100 {
101 Env env{*this, features};
102 MPTTester mptAlice(env, alice);
103
104 mptAlice.create({.flags = 0x00000001, .err = temINVALID_FLAG});
105
106 // tries to set a txFee while not enabling in the flag
107 mptAlice.create(
108 {.maxAmt = 100,
109 .assetScale = 0,
110 .transferFee = 1,
111 .metadata = "test",
112 .err = temMALFORMED});
113
114 if (!features[featureSingleAssetVault] || !features[featurePermissionedDomains])
115 {
116 // tries to set DomainID when SAV or PD is disabled
117 mptAlice.create(
118 {.maxAmt = 100,
119 .assetScale = 0,
120 .metadata = "test",
121 .flags = tfMPTRequireAuth,
122 .domainID = uint256(42),
123 .err = temDISABLED});
124 }
125 else
126 {
127 // tries to set DomainID when RequireAuth is not set
128 mptAlice.create(
129 {.maxAmt = 100,
130 .assetScale = 0,
131 .metadata = "test",
132 .domainID = uint256(42),
133 .err = temMALFORMED});
134
135 // tries to set zero DomainID
136 mptAlice.create(
137 {.maxAmt = 100,
138 .assetScale = 0,
139 .metadata = "test",
140 .flags = tfMPTRequireAuth,
141 .domainID = uint256{},
142 .err = temMALFORMED});
143 }
144
145 // tries to set a txFee greater than max
146 mptAlice.create(
147 {.maxAmt = 100,
148 .assetScale = 0,
149 .transferFee = kMaxTransferFee + 1,
150 .metadata = "test",
151 .flags = tfMPTCanTransfer,
152 .err = temBAD_TRANSFER_FEE});
153
154 // tries to set a txFee while not enabling transfer
155 mptAlice.create(
156 {.maxAmt = 100,
157 .assetScale = 0,
158 .transferFee = kMaxTransferFee,
159 .metadata = "test",
160 .err = temMALFORMED});
161
162 // empty metadata returns error
163 mptAlice.create(
164 {.maxAmt = 100,
165 .assetScale = 0,
166 .transferFee = 0,
167 .metadata = "",
168 .err = temMALFORMED});
169
170 // MaximumAmount of 0 returns error
171 mptAlice.create(
172 {.maxAmt = 0,
173 .assetScale = 1,
174 .transferFee = 1,
175 .metadata = "test",
176 .err = temMALFORMED});
177
178 // MaximumAmount larger than 63 bit returns error
179 mptAlice.create(
180 {.maxAmt = 0xFFFF'FFFF'FFFF'FFF0, // 18'446'744'073'709'551'600
181 .assetScale = 0,
182 .transferFee = 0,
183 .metadata = "test",
184 .err = temMALFORMED});
185 mptAlice.create(
186 {.maxAmt = kMaxMpTokenAmount + 1, // 9'223'372'036'854'775'808
187 .assetScale = 0,
188 .transferFee = 0,
189 .metadata = "test",
190 .err = temMALFORMED});
191 }
192
193 // sfReferenceHolding is populated internally only by VaultCreate.
194 // A user-submitted MPTokenIssuanceCreate carrying the field must be
195 // rejected at preflight under fixCleanup3_2_0.
196 if (features[fixCleanup3_2_0])
197 {
198 Env env{*this, features};
199 env.fund(XRP(1'000), alice);
200 env.close();
201
202 json::Value jv;
203 jv[sfAccount] = alice.human();
204 jv[sfTransactionType] = jss::MPTokenIssuanceCreate;
205 jv[sfReferenceHolding] = to_string(uint256{1});
206 env(jv, Ter(temMALFORMED));
207 }
208 }
209
210 void
212 {
213 testcase("Create Enabled");
214
215 using namespace test::jtx;
216 Account const alice("alice");
217
218 {
219 // If the MPT amendment IS enabled, you should be able to create
220 // MPTokenIssuances
221 Env env{*this, features};
222 MPTTester mptAlice(env, alice);
223 mptAlice.create(
224 {.maxAmt = kMaxMpTokenAmount, // 9'223'372'036'854'775'807
225 .assetScale = 1,
226 .transferFee = 10,
227 .metadata = "123",
228 .ownerCount = 1,
229 .flags = tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow | tfMPTCanTrade |
230 tfMPTCanTransfer | tfMPTCanClawback});
231
232 // Get the hash for the most recent transaction.
233 std::string const txHash{
234 env.tx()->getJson(JsonOptions::Values::None)[jss::hash].asString()};
235
236 json::Value const result = env.rpc("tx", txHash)[jss::result];
237 BEAST_EXPECT(result[sfMaximumAmount.getJsonName()] == "9223372036854775807");
238 }
239
240 if (features[featureSingleAssetVault])
241 {
242 // Add permissioned domain
243 Account const credIssuer1{"credIssuer1"};
244 std::string const credType = "credential";
245
246 pdomain::Credentials const credentials1{{.issuer = credIssuer1, .credType = credType}};
247
248 {
249 Env env{*this, features};
250 env.fund(XRP(1000), credIssuer1);
251
252 env(pdomain::setTx(credIssuer1, credentials1));
253 auto const domainId1 = [&]() {
254 auto tx = env.tx()->getJson(JsonOptions::Values::None);
255 return pdomain::getNewDomain(env.meta());
256 }();
257
258 MPTTester mptAlice(env, alice);
259 mptAlice.create({
260 .maxAmt = kMaxMpTokenAmount, // 9'223'372'036'854'775'807
261 .assetScale = 1,
262 .transferFee = 10,
263 .metadata = "123",
264 .ownerCount = 1,
265 .flags = tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow | tfMPTCanTrade |
266 tfMPTCanTransfer | tfMPTCanClawback,
267 .domainID = domainId1,
268 });
269
270 // Get the hash for the most recent transaction.
271 std::string const txHash{
272 env.tx()->getJson(JsonOptions::Values::None)[jss::hash].asString()};
273
274 json::Value const result = env.rpc("tx", txHash)[jss::result];
275 BEAST_EXPECT(result[sfMaximumAmount.getJsonName()] == "9223372036854775807");
276 }
277 }
278 }
279
280 void
282 {
283 testcase("Destroy Validate");
284
285 using namespace test::jtx;
286 Account const alice("alice");
287 Account const bob("bob");
288 // MPTokenIssuanceDestroy (preflight)
289 {
290 Env env{*this, features - featureMPTokensV1};
291 MPTTester mptAlice(env, alice);
292 auto const id = makeMptID(env.seq(alice), alice);
293 mptAlice.destroy({.id = id, .ownerCount = 0, .err = temDISABLED});
294
295 env.enableFeature(featureMPTokensV1);
296
297 mptAlice.destroy({.id = id, .flags = 0x00000001, .err = temINVALID_FLAG});
298 }
299
300 // MPTokenIssuanceDestroy (preclaim)
301 {
302 Env env{*this, features};
303 MPTTester mptAlice(env, alice, {.holders = {bob}});
304
305 mptAlice.destroy(
306 {.id = makeMptID(env.seq(alice), alice),
307 .ownerCount = 0,
308 .err = tecOBJECT_NOT_FOUND});
309
310 mptAlice.create({.ownerCount = 1});
311
312 // a non-issuer tries to destroy a mptissuance they didn't issue
313 mptAlice.destroy({.issuer = bob, .err = tecNO_PERMISSION});
314
315 // Make sure that issuer can't delete issuance when it still has
316 // outstanding balance
317 {
318 // bob now holds a mptoken object
319 mptAlice.authorize({.account = bob, .holderCount = 1});
320
321 // alice pays bob 100 tokens
322 mptAlice.pay(alice, bob, 100);
323
324 mptAlice.destroy({.err = tecHAS_OBLIGATIONS});
325 }
326 }
327 }
328
329 void
331 {
332 testcase("Destroy Enabled");
333
334 using namespace test::jtx;
335 Account const alice("alice");
336
337 // If the MPT amendment IS enabled, you should be able to destroy
338 // MPTokenIssuances
339 Env env{*this, features};
340 MPTTester mptAlice(env, alice);
341
342 mptAlice.create({.ownerCount = 1});
343
344 mptAlice.destroy({.ownerCount = 0});
345 }
346
347 void
349 {
350 testcase("Validate authorize transaction");
351
352 using namespace test::jtx;
353 Account const alice("alice");
354 Account const bob("bob");
355 Account const cindy("cindy");
356 // Validate amendment enable in MPTokenAuthorize (preflight)
357 {
358 Env env{*this, features - featureMPTokensV1};
359 MPTTester mptAlice(env, alice, {.holders = {bob}});
360
361 mptAlice.authorize(
362 {.account = bob, .id = makeMptID(env.seq(alice), alice), .err = temDISABLED});
363 }
364
365 // Validate fields in MPTokenAuthorize (preflight)
366 {
367 Env env{*this, features};
368 MPTTester mptAlice(env, alice, {.holders = {bob}});
369
370 mptAlice.create({.ownerCount = 1});
371
372 // The only valid MPTokenAuthorize flag is tfMPTUnauthorize, which
373 // has a value of 1
374 mptAlice.authorize({.account = bob, .flags = 0x00000002, .err = temINVALID_FLAG});
375
376 mptAlice.authorize({.account = bob, .holder = bob, .err = temMALFORMED});
377
378 mptAlice.authorize({.holder = alice, .err = temMALFORMED});
379 }
380
381 // Try authorizing when MPTokenIssuance doesn't exist in
382 // MPTokenAuthorize (preclaim)
383 {
384 Env env{*this, features};
385 MPTTester mptAlice(env, alice, {.holders = {bob}});
386 auto const id = makeMptID(env.seq(alice), alice);
387
388 mptAlice.authorize({.holder = bob, .id = id, .err = tecOBJECT_NOT_FOUND});
389
390 mptAlice.authorize({.account = bob, .id = id, .err = tecOBJECT_NOT_FOUND});
391 }
392
393 // Test bad scenarios without allowlisting in MPTokenAuthorize
394 // (preclaim)
395 {
396 Env env{*this, features};
397 MPTTester mptAlice(env, alice, {.holders = {bob}});
398
399 mptAlice.create({.ownerCount = 1});
400
401 // bob submits a tx with a holder field
402 mptAlice.authorize({.account = bob, .holder = alice, .err = tecNO_PERMISSION});
403
404 // alice tries to hold onto her own token
405 mptAlice.authorize({.account = alice, .err = tecNO_PERMISSION});
406
407 // the mpt does not enable allowlisting
408 mptAlice.authorize({.holder = bob, .err = tecNO_AUTH});
409
410 // bob now holds a mptoken object
411 mptAlice.authorize({.account = bob, .holderCount = 1});
412
413 // bob cannot create the mptoken the second time
414 mptAlice.authorize({.account = bob, .err = tecDUPLICATE});
415
416 // Check that bob cannot delete MPToken when his balance is
417 // non-zero
418 {
419 // alice pays bob 100 tokens
420 mptAlice.pay(alice, bob, 100);
421
422 // bob tries to delete his MPToken, but fails since he still
423 // holds tokens
424 mptAlice.authorize(
425 {.account = bob, .flags = tfMPTUnauthorize, .err = tecHAS_OBLIGATIONS});
426
427 // bob pays back alice 100 tokens
428 mptAlice.pay(bob, alice, 100);
429 }
430
431 // bob deletes/unauthorizes his MPToken
432 mptAlice.authorize({.account = bob, .flags = tfMPTUnauthorize});
433
434 // bob receives error when he tries to delete his MPToken that has
435 // already been deleted
436 mptAlice.authorize(
437 {.account = bob,
438 .holderCount = 0,
439 .flags = tfMPTUnauthorize,
440 .err = tecOBJECT_NOT_FOUND});
441 }
442
443 // Test bad scenarios with allow-listing in MPTokenAuthorize (preclaim)
444 {
445 Env env{*this, features};
446 MPTTester mptAlice(env, alice, {.holders = {bob}});
447
448 mptAlice.create({.ownerCount = 1, .flags = tfMPTRequireAuth});
449
450 // alice submits a tx without specifying a holder's account
451 mptAlice.authorize({.err = tecNO_PERMISSION});
452
453 // alice submits a tx to authorize a holder that hasn't created
454 // a mptoken yet
455 mptAlice.authorize({.holder = bob, .err = tecOBJECT_NOT_FOUND});
456
457 // alice specifies a holder acct that doesn't exist
458 mptAlice.authorize({.holder = cindy, .err = tecNO_DST});
459
460 // bob now holds a mptoken object
461 mptAlice.authorize({.account = bob, .holderCount = 1});
462
463 // alice tries to unauthorize bob.
464 // although tx is successful,
465 // but nothing happens because bob hasn't been authorized yet
466 mptAlice.authorize({.holder = bob, .flags = tfMPTUnauthorize});
467
468 // alice authorizes bob
469 // make sure bob's mptoken has set lsfMPTAuthorized
470 mptAlice.authorize({.holder = bob});
471
472 // alice tries authorizes bob again.
473 // tx is successful, but bob is already authorized,
474 // so no changes
475 mptAlice.authorize({.holder = bob});
476
477 // bob deletes his mptoken
478 mptAlice.authorize({.account = bob, .holderCount = 0, .flags = tfMPTUnauthorize});
479 }
480
481 // Test mptoken reserve requirement - first two mpts free (doApply)
482 {
483 Env env{*this, features};
484 auto const acctReserve = env.current()->fees().reserve;
485 auto const incReserve = env.current()->fees().increment;
486
487 // 1 drop
488 BEAST_EXPECT(incReserve > XRPAmount(1));
489 MPTTester mptAlice1(
490 env, alice, {.holders = {bob}, .xrpHolders = acctReserve + (incReserve - 1)});
491 mptAlice1.create();
492
493 MPTTester mptAlice2(env, alice, {.fund = false});
494 mptAlice2.create();
495
496 MPTTester mptAlice3(env, alice, {.fund = false});
497 mptAlice3.create({.ownerCount = 3});
498
499 // first mpt for free
500 mptAlice1.authorize({.account = bob, .holderCount = 1});
501
502 // second mpt free
503 mptAlice2.authorize({.account = bob, .holderCount = 2});
504
505 mptAlice3.authorize({.account = bob, .err = tecINSUFFICIENT_RESERVE});
506
507 env(pay(env.master, bob, drops(incReserve + incReserve + incReserve)));
508 env.close();
509
510 mptAlice3.authorize({.account = bob, .holderCount = 3});
511 }
512 }
513
514 void
516 {
517 testcase("Authorize Enabled");
518
519 using namespace test::jtx;
520 Account const alice("alice");
521 Account const bob("bob");
522 // Basic authorization without allowlisting
523 {
524 Env env{*this, features};
525
526 // alice create mptissuance without allowisting
527 MPTTester mptAlice(env, alice, {.holders = {bob}});
528
529 mptAlice.create({.ownerCount = 1});
530
531 // bob creates a mptoken
532 mptAlice.authorize({.account = bob, .holderCount = 1});
533
534 mptAlice.authorize({.account = bob, .holderCount = 1, .err = tecDUPLICATE});
535
536 // bob deletes his mptoken
537 mptAlice.authorize({.account = bob, .holderCount = 0, .flags = tfMPTUnauthorize});
538 }
539
540 // With allowlisting
541 {
542 Env env{*this, features};
543
544 // alice creates a mptokenissuance that requires authorization
545 MPTTester mptAlice(env, alice, {.holders = {bob}});
546
547 mptAlice.create({.ownerCount = 1, .flags = tfMPTRequireAuth});
548
549 // bob creates a mptoken
550 mptAlice.authorize({.account = bob, .holderCount = 1});
551
552 // alice authorizes bob
553 mptAlice.authorize({.account = alice, .holder = bob});
554
555 // Unauthorize bob's mptoken
556 mptAlice.authorize(
557 {.account = alice, .holder = bob, .holderCount = 1, .flags = tfMPTUnauthorize});
558
559 mptAlice.authorize({.account = bob, .holderCount = 0, .flags = tfMPTUnauthorize});
560 }
561
562 // Holder can have dangling MPToken even if issuance has been destroyed.
563 // Make sure they can still delete/unauthorize the MPToken
564 {
565 Env env{*this, features};
566 MPTTester mptAlice(env, alice, {.holders = {bob}});
567
568 mptAlice.create({.ownerCount = 1});
569
570 // bob creates a mptoken
571 mptAlice.authorize({.account = bob, .holderCount = 1});
572
573 // alice deletes her issuance
574 mptAlice.destroy({.ownerCount = 0});
575
576 // bob can delete his mptoken even though issuance is no longer
577 // existent
578 mptAlice.authorize({.account = bob, .holderCount = 0, .flags = tfMPTUnauthorize});
579 }
580 }
581
582 void
584 {
585 testcase("Validate set transaction");
586
587 using namespace test::jtx;
588 Account const alice("alice"); // issuer
589 Account const bob("bob"); // holder
590 Account const cindy("cindy");
591 // Validate fields in MPTokenIssuanceSet (preflight)
592 {
593 Env env{*this, features - featureMPTokensV1};
594 MPTTester mptAlice(env, alice, {.holders = {bob}});
595
596 mptAlice.set(
597 {.account = bob, .id = makeMptID(env.seq(alice), alice), .err = temDISABLED});
598
599 env.enableFeature(featureMPTokensV1);
600
601 mptAlice.create({.ownerCount = 1, .holderCount = 0});
602
603 mptAlice.authorize({.account = bob, .holderCount = 1});
604
605 // test invalid flag - only valid flags are tfMPTLock (1) and Unlock
606 // (2)
607 mptAlice.set({.account = alice, .flags = 0x00000008, .err = temINVALID_FLAG});
608
609 if (!features[featureSingleAssetVault] && !features[featureDynamicMPT] &&
610 !features[featureConfidentialTransfer])
611 {
612 // test invalid flags - nothing is being changed
613 mptAlice.set({.account = alice, .flags = 0x00000000, .err = tecNO_PERMISSION});
614
615 mptAlice.set(
616 {.account = alice,
617 .holder = bob,
618 .flags = 0x00000000,
619 .err = tecNO_PERMISSION});
620
621 // cannot set DomainID since SAV is not enabled
622 mptAlice.set({.account = alice, .domainID = uint256(42), .err = temDISABLED});
623 }
624 else
625 {
626 // test invalid flags - nothing is being changed
627 mptAlice.set({.account = alice, .flags = 0x00000000, .err = temMALFORMED});
628
629 mptAlice.set(
630 {.account = alice, .holder = bob, .flags = 0x00000000, .err = temMALFORMED});
631
632 if (!features[featurePermissionedDomains] || !features[featureSingleAssetVault])
633 {
634 // cannot set DomainID since PD is not enabled
635 mptAlice.set({.account = alice, .domainID = uint256(42), .err = temDISABLED});
636 }
637 else if (features[featureSingleAssetVault])
638 {
639 // cannot set DomainID since Holder is set
640 mptAlice.set(
641 {.account = alice,
642 .holder = bob,
643 .domainID = uint256(42),
644 .err = temMALFORMED});
645 }
646 }
647
648 // set both lock and unlock flags at the same time will fail
649 mptAlice.set(
650 {.account = alice, .flags = tfMPTLock | tfMPTUnlock, .err = temINVALID_FLAG});
651
652 // if the holder is the same as the acct that submitted the tx,
653 // tx fails
654 mptAlice.set(
655 {.account = alice, .holder = alice, .flags = tfMPTLock, .err = temMALFORMED});
656 }
657
658 // Validate fields in MPTokenIssuanceSet (preclaim)
659 // test when a mptokenissuance has disabled locking
660 {
661 Env env{*this, features};
662
663 MPTTester mptAlice(env, alice, {.holders = {bob}});
664
665 mptAlice.create({.ownerCount = 1});
666
667 // alice tries to lock a mptissuance that has disabled locking
668 mptAlice.set({.account = alice, .flags = tfMPTLock, .err = tecNO_PERMISSION});
669
670 // alice tries to unlock mptissuance that has disabled locking
671 mptAlice.set({.account = alice, .flags = tfMPTUnlock, .err = tecNO_PERMISSION});
672
673 // issuer tries to lock a bob's mptoken that has disabled
674 // locking
675 mptAlice.set(
676 {.account = alice, .holder = bob, .flags = tfMPTLock, .err = tecNO_PERMISSION});
677
678 // issuer tries to unlock a bob's mptoken that has disabled
679 // locking
680 mptAlice.set(
681 {.account = alice, .holder = bob, .flags = tfMPTUnlock, .err = tecNO_PERMISSION});
682 }
683
684 // Validate fields in MPTokenIssuanceSet (preclaim)
685 // test when mptokenissuance has enabled locking
686 {
687 Env env{*this, features};
688
689 MPTTester mptAlice(env, alice, {.holders = {bob}});
690
691 // alice trying to set when the mptissuance doesn't exist yet
692 mptAlice.set(
693 {.id = makeMptID(env.seq(alice), alice),
694 .flags = tfMPTLock,
695 .err = tecOBJECT_NOT_FOUND});
696
697 // create a mptokenissuance with locking
698 mptAlice.create({.ownerCount = 1, .flags = tfMPTCanLock});
699
700 // a non-issuer acct tries to set the mptissuance
701 mptAlice.set({.account = bob, .flags = tfMPTLock, .err = tecNO_PERMISSION});
702
703 // trying to set a holder who doesn't have a mptoken
704 mptAlice.set({.holder = bob, .flags = tfMPTLock, .err = tecOBJECT_NOT_FOUND});
705
706 // trying to set a holder who doesn't exist
707 mptAlice.set({.holder = cindy, .flags = tfMPTLock, .err = tecNO_DST});
708 }
709
710 if (features[featureSingleAssetVault] && features[featurePermissionedDomains])
711 {
712 // Add permissioned domain
713 Account const credIssuer1{"credIssuer1"};
714 std::string const credType = "credential";
715
716 pdomain::Credentials const credentials1{{.issuer = credIssuer1, .credType = credType}};
717
718 {
719 Env env{*this, features};
720
721 MPTTester mptAlice(env, alice);
722 mptAlice.create({});
723
724 // Trying to set DomainID on a public MPTokenIssuance
725 mptAlice.set({.domainID = uint256(42), .err = tecNO_PERMISSION});
726
727 mptAlice.set({.domainID = uint256{}, .err = tecNO_PERMISSION});
728 }
729
730 {
731 Env env{*this, features};
732
733 MPTTester mptAlice(env, alice);
734 mptAlice.create({.flags = tfMPTRequireAuth});
735
736 // Trying to set non-existing DomainID
737 mptAlice.set({.domainID = uint256(42), .err = tecOBJECT_NOT_FOUND});
738
739 // Trying to lock but locking is disabled
740 mptAlice.set(
741 {.flags = tfMPTUnlock, .domainID = uint256(42), .err = tecNO_PERMISSION});
742
743 mptAlice.set(
744 {.flags = tfMPTUnlock, .domainID = uint256{}, .err = tecNO_PERMISSION});
745 }
746 }
747 }
748
749 void
751 {
752 testcase("Enabled set transaction");
753
754 using namespace test::jtx;
755 Account const alice("alice"); // issuer
756 Account const bob("bob"); // holder
757
758 {
759 // Test locking and unlocking
760 Env env{*this, features};
761
762 MPTTester mptAlice(env, alice, {.holders = {bob}});
763
764 // create a mptokenissuance with locking
765 mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanLock});
766
767 mptAlice.authorize({.account = bob, .holderCount = 1});
768
769 // locks bob's mptoken
770 mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
771
772 // trying to lock bob's mptoken again will still succeed
773 // but no changes to the objects
774 mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
775
776 // alice locks the mptissuance
777 mptAlice.set({.account = alice, .flags = tfMPTLock});
778
779 // alice tries to lock up both mptissuance and mptoken again
780 // it will not change the flags and both will remain locked.
781 mptAlice.set({.account = alice, .flags = tfMPTLock});
782 mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
783
784 // alice unlocks bob's mptoken
785 mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTUnlock});
786
787 // locks up bob's mptoken again
788 mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
789 if (!features[featureSingleAssetVault])
790 {
791 // Delete bob's mptoken even though it is locked
792 mptAlice.authorize({.account = bob, .flags = tfMPTUnauthorize});
793
794 mptAlice.set(
795 {.account = alice,
796 .holder = bob,
797 .flags = tfMPTUnlock,
798 .err = tecOBJECT_NOT_FOUND});
799
800 return;
801 }
802
803 // Cannot delete locked MPToken
804 mptAlice.authorize(
805 {.account = bob, .flags = tfMPTUnauthorize, .err = tecNO_PERMISSION});
806
807 // alice unlocks mptissuance
808 mptAlice.set({.account = alice, .flags = tfMPTUnlock});
809
810 // alice unlocks bob's mptoken
811 mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTUnlock});
812
813 // alice unlocks mptissuance and bob's mptoken again despite that
814 // they are already unlocked. Make sure this will not change the
815 // flags
816 mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTUnlock});
817 mptAlice.set({.account = alice, .flags = tfMPTUnlock});
818 }
819
820 if (features[featureSingleAssetVault])
821 {
822 // Add permissioned domain
823 std::string const credType = "credential";
824
825 // Test setting and resetting domain ID
826 Env env{*this, features};
827
828 auto const domainId1 = [&]() {
829 Account const credIssuer1{"credIssuer1"};
830 env.fund(XRP(1000), credIssuer1);
831
832 pdomain::Credentials const credentials1{
833 {.issuer = credIssuer1, .credType = credType}};
834
835 env(pdomain::setTx(credIssuer1, credentials1));
836 return [&]() {
837 auto tx = env.tx()->getJson(JsonOptions::Values::None);
838 return pdomain::getNewDomain(env.meta());
839 }();
840 }();
841
842 auto const domainId2 = [&]() {
843 Account const credIssuer2{"credIssuer2"};
844 env.fund(XRP(1000), credIssuer2);
845
846 pdomain::Credentials const credentials2{
847 {.issuer = credIssuer2, .credType = credType}};
848
849 env(pdomain::setTx(credIssuer2, credentials2));
850 return [&]() {
851 auto tx = env.tx()->getJson(JsonOptions::Values::None);
852 return pdomain::getNewDomain(env.meta());
853 }();
854 }();
855
856 MPTTester mptAlice(env, alice, {.holders = {bob}});
857
858 // create a mptokenissuance with auth.
859 mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTRequireAuth});
860 BEAST_EXPECT(mptAlice.checkDomainID(std::nullopt));
861
862 // reset "domain not set" to "domain not set", i.e. no change
863 mptAlice.set({.domainID = uint256{}});
864 BEAST_EXPECT(mptAlice.checkDomainID(std::nullopt));
865
866 // reset "domain not set" to domain1
867 mptAlice.set({.domainID = domainId1});
868 BEAST_EXPECT(mptAlice.checkDomainID(domainId1));
869
870 // reset domain1 to domain2
871 mptAlice.set({.domainID = domainId2});
872 BEAST_EXPECT(mptAlice.checkDomainID(domainId2));
873
874 // reset domain to "domain not set"
875 mptAlice.set({.domainID = uint256{}});
876 BEAST_EXPECT(mptAlice.checkDomainID(std::nullopt));
877 }
878 }
879
880 void
882 {
883 testcase("Payment");
884
885 using namespace test::jtx;
886 Account const alice("alice"); // issuer
887 Account const bob("bob"); // holder
888 Account const carol("carol"); // holder
889 auto const mpTokensV2 = features[featureMPTokensV2];
890
891 // preflight validation
892
893 // MPT is disabled
894 {
895 Env env{*this, features - featureMPTokensV1}; // NOLINT TODO
896 Account const alice("alice");
897 Account const bob("bob");
898
899 env.fund(XRP(1'000), alice);
900 env.fund(XRP(1'000), bob);
901 STAmount const mpt{MPTIssue{makeMptID(1, alice)}, UINT64_C(100)};
902
903 env(pay(alice, bob, mpt), Ter(temDISABLED));
904 }
905
906 // MPT is disabled, unsigned request
907 {
908 Env env{*this, features - featureMPTokensV1};
909 Account const alice("alice"); // issuer
910 Account const carol("carol");
911 auto const usd = alice["USD"];
912
913 env.fund(XRP(1'000), alice);
914 env.fund(XRP(1'000), carol);
915 STAmount const mpt{MPTIssue{makeMptID(1, alice)}, UINT64_C(100)};
916
917 json::Value jv;
918 jv[jss::secret] = alice.name();
919 jv[jss::tx_json] = pay(alice, carol, mpt);
920 jv[jss::tx_json][jss::Fee] = to_string(env.current()->fees().base);
921 auto const jrr = env.rpc("json", "submit", to_string(jv));
922 BEAST_EXPECT(jrr[jss::result][jss::engine_result] == "temDISABLED");
923 }
924
925 // Invalid flag
926 {
927 Env env{*this, features};
928
929 MPTTester mptAlice(env, alice, {.holders = {bob}});
930
931 mptAlice.create({.ownerCount = 1, .holderCount = 0});
932 auto const mpt = mptAlice["MPT"];
933
934 mptAlice.authorize({.account = bob});
935
936 auto err = !features[featureMPTokensV2] ? Ter(temINVALID_FLAG) : Ter(temRIPPLE_EMPTY);
937 env(pay(alice, bob, mpt(10)), Txflags(tfNoRippleDirect), err);
938 err = !features[featureMPTokensV2] ? Ter(temINVALID_FLAG) : Ter(tesSUCCESS);
939 env(pay(alice, bob, mpt(10)), Txflags(tfLimitQuality), err);
940 }
941
942 // Invalid combination of send, sendMax, deliverMin, paths
943 {
944 Env env{*this, features};
945 Account const alice("alice");
946 Account const carol("carol");
947
948 MPTTester mptAlice(env, alice, {.holders = {carol}});
949
950 mptAlice.create(
951 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
952
953 mptAlice.authorize({.account = carol});
954
955 // sendMax and DeliverMin are valid XRP amount,
956 // but is invalid combination with MPT amount
957 auto const mpt = mptAlice["MPT"];
958 auto err = !mpTokensV2 ? Ter(temMALFORMED) : Ter(tecPATH_PARTIAL);
959 env(pay(alice, carol, mpt(100)), Sendmax(XRP(100)), err);
960 env(pay(alice, carol, mpt(100)), DeliverMin(XRP(100)), Ter(temBAD_AMOUNT));
961 // sendMax MPT is invalid with IOU or XRP
962 auto const usd = alice["USD"];
963 err = !mpTokensV2 ? Ter(temMALFORMED) : Ter(tecPATH_DRY);
964 env(pay(alice, carol, usd(100)), Sendmax(mpt(100)), err);
965 err = !mpTokensV2 ? Ter(temMALFORMED) : Ter(tecPATH_PARTIAL);
966 env(pay(alice, carol, XRP(100)), Sendmax(mpt(100)), err);
967 env(pay(alice, carol, usd(100)), DeliverMin(mpt(100)), Ter(temBAD_AMOUNT));
968 env(pay(alice, carol, XRP(100)), DeliverMin(mpt(100)), Ter(temBAD_AMOUNT));
969 // sendmax and amount are different MPT issue
970 test::jtx::MPT const mpT1("MPT", makeMptID(env.seq(alice) + 10, alice));
971 err = !mpTokensV2 ? Ter(temMALFORMED) : Ter(tecOBJECT_NOT_FOUND);
972 env(pay(alice, carol, mpT1(100)), Sendmax(mpt(100)), err);
973 // "paths" is invalid in V1
974 err = !mpTokensV2 ? Ter(temMALFORMED) : Ter(tesSUCCESS);
975 env(pay(alice, carol, mpt(100)), Path(~usd), err);
976 }
977
978 // build_path is invalid if MPT
979 {
980 Env env{*this, features - featureMPTokensV2};
981 Account const alice("alice");
982 Account const carol("carol");
983
984 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
985
986 mptAlice.create({.ownerCount = 1, .holderCount = 0});
987 auto const mpt = mptAlice["MPT"];
988
989 mptAlice.authorize({.account = carol});
990
991 json::Value payment;
992 payment[jss::secret] = alice.name();
993 payment[jss::tx_json] = pay(alice, carol, mpt(100));
994
995 payment[jss::build_path] = true;
996 auto jrr = env.rpc("json", "submit", to_string(payment));
997 BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");
998 BEAST_EXPECT(
999 jrr[jss::result][jss::error_message] ==
1000 "Field 'build_path' not allowed in this context.");
1001 }
1002
1003 // Can't pay negative amount
1004 {
1005 Env env{*this, features};
1006
1007 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
1008
1009 mptAlice.create({.ownerCount = 1, .holderCount = 0});
1010 auto const mpt = mptAlice["MPT"];
1011
1012 mptAlice.authorize({.account = bob});
1013 mptAlice.authorize({.account = carol});
1014
1015 mptAlice.pay(alice, bob, -1, temBAD_AMOUNT);
1016
1017 mptAlice.pay(bob, carol, -1, temBAD_AMOUNT);
1018
1019 mptAlice.pay(bob, alice, -1, temBAD_AMOUNT);
1020
1021 env(pay(alice, bob, mpt(10)), Sendmax(mpt(-1)), Ter(temBAD_AMOUNT));
1022 }
1023
1024 // Pay to self
1025 {
1026 Env env{*this, features};
1027
1028 MPTTester mptAlice(env, alice, {.holders = {bob}});
1029
1030 mptAlice.create({.ownerCount = 1, .holderCount = 0});
1031
1032 mptAlice.authorize({.account = bob});
1033
1034 mptAlice.pay(bob, bob, 10, temREDUNDANT);
1035 }
1036
1037 // preclaim validation
1038
1039 // Destination doesn't exist
1040 {
1041 Env env{*this, features};
1042
1043 MPTTester mptAlice(env, alice, {.holders = {bob}});
1044
1045 mptAlice.create({.ownerCount = 1, .holderCount = 0});
1046
1047 mptAlice.authorize({.account = bob});
1048
1049 Account const bad{"bad"};
1050 env.memoize(bad);
1051
1052 mptAlice.pay(bob, bad, 10, tecNO_DST);
1053 }
1054
1055 // apply validation
1056
1057 // If RequireAuth is enabled, Payment fails if the receiver is not
1058 // authorized
1059 {
1060 Env env{*this, features};
1061
1062 MPTTester mptAlice(env, alice, {.holders = {bob}});
1063
1064 mptAlice.create(
1065 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTRequireAuth | tfMPTCanTransfer});
1066
1067 mptAlice.authorize({.account = bob});
1068
1069 mptAlice.pay(alice, bob, 100, tecNO_AUTH);
1070 }
1071
1072 // If RequireAuth is enabled, Payment fails if the sender is not
1073 // authorized
1074 {
1075 Env env{*this, features};
1076
1077 MPTTester mptAlice(env, alice, {.holders = {bob}});
1078
1079 mptAlice.create(
1080 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTRequireAuth | tfMPTCanTransfer});
1081
1082 // bob creates an empty MPToken
1083 mptAlice.authorize({.account = bob});
1084
1085 // alice authorizes bob to hold funds
1086 mptAlice.authorize({.account = alice, .holder = bob});
1087
1088 // alice sends 100 MPT to bob
1089 mptAlice.pay(alice, bob, 100);
1090
1091 // alice UNAUTHORIZES bob
1092 mptAlice.authorize({.account = alice, .holder = bob, .flags = tfMPTUnauthorize});
1093
1094 // bob fails to send back to alice because he is no longer
1095 // authorize to move his funds!
1096 mptAlice.pay(bob, alice, 100, tecNO_AUTH);
1097 }
1098
1099 if (features[featureSingleAssetVault] && features[featurePermissionedDomains])
1100 {
1101 // If RequireAuth is enabled and domain is a match, payment succeeds
1102 {
1103 Env env{*this, features};
1104 std::string const credType = "credential";
1105 Account const credIssuer1{"credIssuer1"};
1106 env.fund(XRP(1000), credIssuer1, bob);
1107
1108 auto const domainId1 = [&]() {
1109 pdomain::Credentials const credentials1{
1110 {.issuer = credIssuer1, .credType = credType}};
1111
1112 env(pdomain::setTx(credIssuer1, credentials1));
1113 return [&]() {
1114 auto tx = env.tx()->getJson(JsonOptions::Values::None);
1115 return pdomain::getNewDomain(env.meta());
1116 }();
1117 }();
1118 // bob is authorized via domain
1119 env(credentials::create(bob, credIssuer1, credType));
1120 env(credentials::accept(bob, credIssuer1, credType));
1121 env.close();
1122
1123 MPTTester mptAlice(env, alice, MPTInit{});
1124 env.close();
1125
1126 mptAlice.create({
1127 .ownerCount = 1,
1128 .holderCount = 0,
1129 .flags = tfMPTRequireAuth | tfMPTCanTransfer,
1130 .domainID = domainId1,
1131 });
1132
1133 mptAlice.authorize({.account = bob});
1134 env.close();
1135
1136 // bob is authorized via domain
1137 mptAlice.pay(alice, bob, 100);
1138 mptAlice.set({.domainID = uint256{}});
1139
1140 // bob is no longer authorized
1141 mptAlice.pay(alice, bob, 100, tecNO_AUTH);
1142 }
1143
1144 {
1145 Env env{*this, features};
1146 std::string const credType = "credential";
1147 Account const credIssuer1{"credIssuer1"};
1148 env.fund(XRP(1000), credIssuer1, bob);
1149
1150 auto const domainId1 = [&]() {
1151 pdomain::Credentials const credentials1{
1152 {.issuer = credIssuer1, .credType = credType}};
1153
1154 env(pdomain::setTx(credIssuer1, credentials1));
1155 return [&]() {
1156 auto tx = env.tx()->getJson(JsonOptions::Values::None);
1157 return pdomain::getNewDomain(env.meta());
1158 }();
1159 }();
1160 // bob is authorized via domain
1161 env(credentials::create(bob, credIssuer1, credType));
1162 env(credentials::accept(bob, credIssuer1, credType));
1163 env.close();
1164
1165 MPTTester mptAlice(env, alice, MPTInit{});
1166 env.close();
1167
1168 mptAlice.create({
1169 .ownerCount = 1,
1170 .holderCount = 0,
1171 .flags = tfMPTRequireAuth | tfMPTCanTransfer,
1172 .domainID = domainId1,
1173 });
1174
1175 // bob creates an empty MPToken
1176 mptAlice.authorize({.account = bob});
1177
1178 // alice authorizes bob to hold funds
1179 mptAlice.authorize({.account = alice, .holder = bob});
1180
1181 // alice sends 100 MPT to bob
1182 mptAlice.pay(alice, bob, 100);
1183
1184 // alice UNAUTHORIZES bob
1185 mptAlice.authorize({.account = alice, .holder = bob, .flags = tfMPTUnauthorize});
1186
1187 // bob is still authorized, via domain
1188 mptAlice.pay(bob, alice, 10);
1189
1190 mptAlice.set({.domainID = uint256{}});
1191
1192 // bob fails to send back to alice because he is no longer
1193 // authorize to move his funds!
1194 mptAlice.pay(bob, alice, 10, tecNO_AUTH);
1195 }
1196
1197 {
1198 Env env{*this, features};
1199 std::string const credType = "credential";
1200 // credIssuer1 is the owner of domainId1 and a credential issuer
1201 Account const credIssuer1{"credIssuer1"};
1202 // credIssuer2 is the owner of domainId2 and a credential issuer
1203 // Note, domainId2 also lists credentials issued by credIssuer1
1204 Account const credIssuer2{"credIssuer2"};
1205 env.fund(XRP(1000), credIssuer1, credIssuer2, bob, carol);
1206
1207 auto const domainId1 = [&]() {
1209 {.issuer = credIssuer1, .credType = credType}};
1210
1211 env(pdomain::setTx(credIssuer1, credentials));
1212 return [&]() {
1213 auto tx = env.tx()->getJson(JsonOptions::Values::None);
1214 return pdomain::getNewDomain(env.meta());
1215 }();
1216 }();
1217
1218 auto const domainId2 = [&]() {
1220 {.issuer = credIssuer1, .credType = credType},
1221 {.issuer = credIssuer2, .credType = credType}};
1222
1223 env(pdomain::setTx(credIssuer2, credentials));
1224 return [&]() {
1225 auto tx = env.tx()->getJson(JsonOptions::Values::None);
1226 return pdomain::getNewDomain(env.meta());
1227 }();
1228 }();
1229
1230 // bob is authorized via credIssuer1 which is recognized by both
1231 // domainId1 and domainId2
1232 env(credentials::create(bob, credIssuer1, credType));
1233 env(credentials::accept(bob, credIssuer1, credType));
1234 env.close();
1235
1236 // carol is authorized via credIssuer2, only recognized by
1237 // domainId2
1238 env(credentials::create(carol, credIssuer2, credType));
1239 env(credentials::accept(carol, credIssuer2, credType));
1240 env.close();
1241
1242 MPTTester mptAlice(env, alice, MPTInit{});
1243 env.close();
1244
1245 mptAlice.create({
1246 .ownerCount = 1,
1247 .holderCount = 0,
1248 .flags = tfMPTRequireAuth | tfMPTCanTransfer,
1249 .domainID = domainId1,
1250 });
1251
1252 // bob and carol create an empty MPToken
1253 mptAlice.authorize({.account = bob});
1254 mptAlice.authorize({.account = carol});
1255 env.close();
1256
1257 // alice sends 50 MPT to bob but cannot send to carol
1258 mptAlice.pay(alice, bob, 50);
1259 mptAlice.pay(alice, carol, 50, tecNO_AUTH);
1260 env.close();
1261
1262 // bob cannot send to carol because they are not on the same
1263 // domain (since credIssuer2 is not recognized by domainId1)
1264 mptAlice.pay(bob, carol, 10, tecNO_AUTH);
1265 env.close();
1266
1267 // alice updates domainID to domainId2 which recognizes both
1268 // credIssuer1 and credIssuer2
1269 mptAlice.set({.domainID = domainId2});
1270 // alice can now send to carol
1271 mptAlice.pay(alice, carol, 10);
1272 env.close();
1273
1274 // bob can now send to carol because both are in the same
1275 // domain
1276 mptAlice.pay(bob, carol, 10);
1277 env.close();
1278
1279 // bob loses his authorization and can no longer send MPT
1280 env(credentials::deleteCred(credIssuer1, bob, credIssuer1, credType));
1281 env.close();
1282
1283 mptAlice.pay(bob, carol, 10, tecNO_AUTH);
1284 mptAlice.pay(bob, alice, 10, tecNO_AUTH);
1285 }
1286 }
1287
1288 // Non-issuer cannot send to each other if MPTCanTransfer isn't set
1289 {
1290 Env env(*this, features);
1291 Account const alice{"alice"};
1292 Account const bob{"bob"};
1293 Account const cindy{"cindy"};
1294
1295 MPTTester mptAlice(env, alice, {.holders = {bob, cindy}});
1296
1297 // alice creates issuance without MPTCanTransfer
1298 mptAlice.create({.ownerCount = 1, .holderCount = 0});
1299
1300 // bob creates a MPToken
1301 mptAlice.authorize({.account = bob});
1302
1303 // cindy creates a MPToken
1304 mptAlice.authorize({.account = cindy});
1305
1306 // alice pays bob 100 tokens
1307 mptAlice.pay(alice, bob, 100);
1308
1309 // bob tries to send cindy 10 tokens, but fails because canTransfer
1310 // is off
1311 mptAlice.pay(bob, cindy, 10, tecNO_AUTH);
1312
1313 // bob can send back to alice(issuer) just fine
1314 mptAlice.pay(bob, alice, 10);
1315 }
1316
1317 // Holder is not authorized
1318 {
1319 Env env{*this, features};
1320
1321 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
1322
1323 mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});
1324
1325 // issuer to holder
1326 mptAlice.pay(alice, bob, 100, tecNO_AUTH);
1327
1328 // holder to issuer
1329 mptAlice.pay(bob, alice, 100, tecNO_AUTH);
1330
1331 // holder to holder
1332 mptAlice.pay(bob, carol, 50, tecNO_AUTH);
1333 }
1334
1335 // Payer doesn't have enough funds
1336 {
1337 Env env{*this, features};
1338
1339 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
1340
1341 mptAlice.create({.ownerCount = 1, .flags = tfMPTCanTransfer});
1342
1343 mptAlice.authorize({.account = bob});
1344 mptAlice.authorize({.account = carol});
1345
1346 mptAlice.pay(alice, bob, 100);
1347
1348 // Pay to another holder
1349 mptAlice.pay(bob, carol, 101, tecPATH_PARTIAL);
1350
1351 // Pay to the issuer
1352 mptAlice.pay(bob, alice, 101, tecPATH_PARTIAL);
1353 }
1354
1355 // MPT is locked
1356 {
1357 Env env{*this, features};
1358
1359 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
1360
1361 mptAlice.create({.ownerCount = 1, .flags = tfMPTCanLock | tfMPTCanTransfer});
1362
1363 mptAlice.authorize({.account = bob});
1364 mptAlice.authorize({.account = carol});
1365
1366 mptAlice.pay(alice, bob, 100);
1367 mptAlice.pay(alice, carol, 100);
1368
1369 auto const err =
1370 env.current()->rules().enabled(featureMPTokensV2) ? tecPATH_DRY : tecLOCKED;
1371
1372 // Global lock
1373 mptAlice.set({.account = alice, .flags = tfMPTLock});
1374 // Can't send between holders
1375 mptAlice.pay(bob, carol, 1, err);
1376 mptAlice.pay(carol, bob, 2, err);
1377 // Issuer can send
1378 mptAlice.pay(alice, bob, 3);
1379 // Holder can send back to issuer
1380 mptAlice.pay(bob, alice, 4);
1381
1382 // Global unlock
1383 mptAlice.set({.account = alice, .flags = tfMPTUnlock});
1384 // Individual lock
1385 mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
1386 // Can't send between holders
1387 mptAlice.pay(bob, carol, 5, err);
1388 mptAlice.pay(carol, bob, 6, err);
1389 // Issuer can send
1390 mptAlice.pay(alice, bob, 7);
1391 // Holder can send back to issuer
1392 mptAlice.pay(bob, alice, 8);
1393 }
1394
1395 // Transfer fee
1396 {
1397 Env env{*this, features};
1398
1399 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
1400
1401 // Transfer fee is 10%
1402 mptAlice.create(
1403 {.transferFee = 10'000,
1404 .ownerCount = 1,
1405 .holderCount = 0,
1406 .flags = tfMPTCanTransfer});
1407
1408 // Holders create MPToken
1409 mptAlice.authorize({.account = bob});
1410 mptAlice.authorize({.account = carol});
1411
1412 // Payment between the issuer and the holder, no transfer fee.
1413 mptAlice.pay(alice, bob, 2'000);
1414
1415 // Payment between the holder and the issuer, no transfer fee.
1416 mptAlice.pay(bob, alice, 1'000);
1417 BEAST_EXPECT(mptAlice.checkMPTokenAmount(bob, 1'000));
1418
1419 // Payment between the holders. The sender doesn't have
1420 // enough funds to cover the transfer fee.
1421 mptAlice.pay(bob, carol, 1'000, tecPATH_PARTIAL);
1422
1423 // Payment between the holders. The sender has enough funds
1424 // but SendMax is not included.
1425 mptAlice.pay(bob, carol, 100, tecPATH_PARTIAL);
1426
1427 auto const mpt = mptAlice["MPT"];
1428 // SendMax doesn't cover the fee
1429 env(pay(bob, carol, mpt(100)), Sendmax(mpt(109)), Ter(tecPATH_PARTIAL));
1430
1431 // Payment succeeds if sufficient SendMax is included.
1432 // 100 to carol, 10 to issuer
1433 env(pay(bob, carol, mpt(100)), Sendmax(mpt(110)));
1434 // 100 to carol, 10 to issuer
1435 env(pay(bob, carol, mpt(100)), Sendmax(mpt(115)));
1436 BEAST_EXPECT(mptAlice.checkMPTokenAmount(bob, 780));
1437 BEAST_EXPECT(mptAlice.checkMPTokenAmount(carol, 200));
1438 // Payment succeeds if partial payment even if
1439 // SendMax is less than deliver amount
1440 env(pay(bob, carol, mpt(100)), Sendmax(mpt(90)), Txflags(tfPartialPayment));
1441 // 82 to carol, 8 to issuer (90 / 1.1 ~ 81.81 (rounded to nearest) =
1442 // 82)
1443 BEAST_EXPECT(mptAlice.checkMPTokenAmount(bob, 690));
1444 // In V2 the payments are executed via the payment engine and
1445 // the rounding results in a higher quality trade
1446 BEAST_EXPECT(
1447 mptAlice.checkMPTokenAmount(carol, !features[featureMPTokensV2] ? 282 : 281));
1448 }
1449
1450 // Insufficient SendMax with no transfer fee
1451 {
1452 Env env{*this, features};
1453
1454 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
1455
1456 mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});
1457
1458 // Holders create MPToken
1459 mptAlice.authorize({.account = bob});
1460 mptAlice.authorize({.account = carol});
1461 mptAlice.pay(alice, bob, 1'000);
1462
1463 auto const mpt = mptAlice["MPT"];
1464 // SendMax is less than the amount
1465 env(pay(bob, carol, mpt(100)), Sendmax(mpt(99)), Ter(tecPATH_PARTIAL));
1466 env(pay(bob, alice, mpt(100)), Sendmax(mpt(99)), Ter(tecPATH_PARTIAL));
1467
1468 // Payment succeeds if sufficient SendMax is included.
1469 env(pay(bob, carol, mpt(100)), Sendmax(mpt(100)));
1470 BEAST_EXPECT(mptAlice.checkMPTokenAmount(carol, 100));
1471 // Payment succeeds if partial payment
1472 env(pay(bob, carol, mpt(100)), Sendmax(mpt(99)), Txflags(tfPartialPayment));
1473 BEAST_EXPECT(mptAlice.checkMPTokenAmount(carol, 199));
1474 }
1475
1476 // DeliverMin
1477 {
1478 Env env{*this, features};
1479
1480 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
1481
1482 mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});
1483
1484 // Holders create MPToken
1485 mptAlice.authorize({.account = bob});
1486 mptAlice.authorize({.account = carol});
1487 mptAlice.pay(alice, bob, 1'000);
1488
1489 auto const mpt = mptAlice["MPT"];
1490 // Fails even with the partial payment because
1491 // deliver amount < deliverMin
1492 env(pay(bob, alice, mpt(100)),
1493 Sendmax(mpt(99)),
1494 DeliverMin(mpt(100)),
1495 Txflags(tfPartialPayment),
1497 // Payment succeeds if deliver amount >= deliverMin
1498 env(pay(bob, alice, mpt(100)),
1499 Sendmax(mpt(99)),
1500 DeliverMin(mpt(99)),
1501 Txflags(tfPartialPayment));
1502 }
1503
1504 // Issuer fails trying to send more than the maximum amount allowed
1505 {
1506 Env env{*this, features};
1507
1508 MPTTester mptAlice(env, alice, {.holders = {bob}});
1509
1510 mptAlice.create(
1511 {.maxAmt = 100, .ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});
1512
1513 mptAlice.authorize({.account = bob});
1514
1515 // issuer sends holder the max amount allowed
1516 mptAlice.pay(alice, bob, 100);
1517
1518 // issuer tries to exceed max amount
1519 auto const err = mpTokensV2 ? tecPATH_DRY : tecPATH_PARTIAL;
1520 mptAlice.pay(alice, bob, 1, err);
1521 }
1522
1523 // Issuer fails trying to send more than the default maximum
1524 // amount allowed
1525 {
1526 Env env{*this, features};
1527
1528 MPTTester mptAlice(env, alice, {.holders = {bob}});
1529
1530 mptAlice.create({.ownerCount = 1, .holderCount = 0});
1531
1532 mptAlice.authorize({.account = bob});
1533
1534 // issuer sends holder the default max amount allowed
1535 mptAlice.pay(alice, bob, kMaxMpTokenAmount);
1536
1537 // issuer tries to exceed max amount
1538 auto const err = mpTokensV2 ? tecPATH_DRY : tecPATH_PARTIAL;
1539 mptAlice.pay(alice, bob, 1, err);
1540 }
1541
1542 // Pay more than max amount fails in the json parser before
1543 // transactor is called
1544 {
1545 Env env{*this, features};
1546 env.fund(XRP(1'000), alice, bob);
1547 STAmount const mpt{MPTIssue{makeMptID(1, alice)}, UINT64_C(100)};
1548 json::Value jv;
1549 jv[jss::secret] = alice.name();
1550 jv[jss::tx_json] = pay(alice, bob, mpt);
1551 jv[jss::tx_json][jss::Amount][jss::value] = std::to_string(kMaxMpTokenAmount + 1);
1552 auto const jrr = env.rpc("json", "submit", to_string(jv));
1553 BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");
1554 }
1555
1556 // Pay maximum amount with the transfer fee, SendMax, and
1557 // partial payment
1558 {
1559 Env env{*this, features};
1560
1561 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
1562
1563 mptAlice.create(
1564 {.maxAmt = 10'000,
1565 .transferFee = 100,
1566 .ownerCount = 1,
1567 .holderCount = 0,
1568 .flags = tfMPTCanTransfer});
1569 auto const mpt = mptAlice["MPT"];
1570
1571 mptAlice.authorize({.account = bob});
1572 mptAlice.authorize({.account = carol});
1573
1574 // issuer sends holder the max amount allowed
1575 mptAlice.pay(alice, bob, 10'000);
1576
1577 // payment between the holders
1578 env(pay(bob, carol, mpt(10'000)), Sendmax(mpt(10'000)), Txflags(tfPartialPayment));
1579 // Verify the metadata
1580 auto const meta =
1581 env.meta()->getJson(JsonOptions::Values::None)[sfAffectedNodes.fieldName];
1582 // Issuer got 10 in the transfer fees
1583 BEAST_EXPECT(
1584 meta[0u][sfModifiedNode.fieldName][sfFinalFields.fieldName]
1585 [sfOutstandingAmount.fieldName] == "9990");
1586 // Destination account got 9'990
1587 BEAST_EXPECT(
1588 meta[1u][sfModifiedNode.fieldName][sfFinalFields.fieldName]
1589 [sfMPTAmount.fieldName] == "9990");
1590 // Source account spent 10'000
1591 BEAST_EXPECT(
1592 meta[2u][sfModifiedNode.fieldName][sfPreviousFields.fieldName]
1593 [sfMPTAmount.fieldName] == "10000");
1594 BEAST_EXPECT(!meta[2u][sfModifiedNode.fieldName][sfFinalFields.fieldName].isMember(
1595 sfMPTAmount.fieldName));
1596
1597 // payment between the holders fails without
1598 // partial payment
1599 auto const err = mpTokensV2 ? tecPATH_DRY : tecPATH_PARTIAL;
1600 env(pay(bob, carol, mpt(10'000)), Sendmax(mpt(10'000)), Ter(err));
1601 }
1602
1603 // Pay maximum allowed amount
1604 {
1605 Env env{*this, features};
1606
1607 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
1608
1609 mptAlice.create(
1610 {.maxAmt = kMaxMpTokenAmount,
1611 .ownerCount = 1,
1612 .holderCount = 0,
1613 .flags = tfMPTCanTransfer});
1614 auto const mpt = mptAlice["MPT"];
1615
1616 mptAlice.authorize({.account = bob});
1617 mptAlice.authorize({.account = carol});
1618
1619 // issuer sends holder the max amount allowed
1620 mptAlice.pay(alice, bob, kMaxMpTokenAmount);
1621 BEAST_EXPECT(mptAlice.checkMPTokenOutstandingAmount(kMaxMpTokenAmount));
1622
1623 // payment between the holders
1624 mptAlice.pay(bob, carol, kMaxMpTokenAmount);
1625 BEAST_EXPECT(mptAlice.checkMPTokenOutstandingAmount(kMaxMpTokenAmount));
1626 // holder pays back to the issuer
1627 mptAlice.pay(carol, alice, kMaxMpTokenAmount);
1628 BEAST_EXPECT(mptAlice.checkMPTokenOutstandingAmount(0));
1629 }
1630
1631 // Issuer fails trying to send fund after issuance was destroyed
1632 {
1633 Env env{*this, features};
1634
1635 MPTTester mptAlice(env, alice, {.holders = {bob}});
1636
1637 mptAlice.create({.ownerCount = 1, .holderCount = 0});
1638
1639 mptAlice.authorize({.account = bob});
1640
1641 // alice destroys issuance
1642 mptAlice.destroy({.ownerCount = 0});
1643
1644 // alice tries to send bob fund after issuance is destroyed, should
1645 // fail.
1646 mptAlice.pay(alice, bob, 100, tecOBJECT_NOT_FOUND);
1647 }
1648
1649 // Non-existent issuance
1650 {
1651 Env env{*this, features}; // NOLINT TODO
1652
1653 env.fund(XRP(1'000), alice, bob);
1654
1655 STAmount const mpt{MPTID{0}, 100};
1656 auto const err =
1657 !features[featureMPTokensV2] ? Ter(tecOBJECT_NOT_FOUND) : Ter(temBAD_CURRENCY);
1658 env(pay(alice, bob, mpt), err);
1659 }
1660
1661 // Issuer fails trying to send to an account, which doesn't own MPT for
1662 // an issuance that was destroyed
1663 {
1664 Env env{*this, features};
1665
1666 MPTTester mptAlice(env, alice, {.holders = {bob}});
1667
1668 mptAlice.create({.ownerCount = 1, .holderCount = 0});
1669
1670 // alice destroys issuance
1671 mptAlice.destroy({.ownerCount = 0});
1672
1673 // alice tries to send bob who doesn't own the MPT after issuance is
1674 // destroyed, it should fail
1675 mptAlice.pay(alice, bob, 100, tecOBJECT_NOT_FOUND);
1676 }
1677
1678 // Issuers issues maximum amount of MPT to a holder, the holder should
1679 // be able to transfer the max amount to someone else
1680 {
1681 Env env{*this, features};
1682 Account const alice("alice");
1683 Account const carol("bob");
1684 Account const bob("carol");
1685
1686 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
1687
1688 mptAlice.create({.maxAmt = 100, .ownerCount = 1, .flags = tfMPTCanTransfer});
1689
1690 mptAlice.authorize({.account = bob});
1691 mptAlice.authorize({.account = carol});
1692
1693 mptAlice.pay(alice, bob, 100);
1694
1695 // transfer max amount to another holder
1696 mptAlice.pay(bob, carol, 100);
1697 }
1698
1699 // Simple payment
1700 {
1701 Env env{*this, features};
1702
1703 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
1704
1705 mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});
1706
1707 mptAlice.authorize({.account = bob});
1708 mptAlice.authorize({.account = carol});
1709
1710 // issuer to holder
1711 mptAlice.pay(alice, bob, 100);
1712
1713 // holder to issuer
1714 mptAlice.pay(bob, alice, 100);
1715
1716 // holder to holder
1717 mptAlice.pay(alice, bob, 100);
1718 mptAlice.pay(bob, carol, 50);
1719 }
1720 }
1721
1722 void
1724 {
1725 using namespace test::jtx;
1726 Account const alice("alice"); // issuer
1727 Account const bob("bob"); // holder
1728 Account const diana("diana");
1729 Account const dpIssuer("dpIssuer"); // holder
1730
1731 char const credType[] = "abcde";
1732
1733 if (features[featureCredentials])
1734 {
1735 testcase("DepositPreauth");
1736
1737 Env env(*this, features);
1738
1739 env.fund(XRP(50000), diana, dpIssuer);
1740 env.close();
1741
1742 MPTTester mptAlice(env, alice, {.holders = {bob}});
1743 mptAlice.create(
1744 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTRequireAuth | tfMPTCanTransfer});
1745
1746 env(pay(diana, bob, XRP(500)));
1747 env.close();
1748
1749 // bob creates an empty MPToken
1750 mptAlice.authorize({.account = bob});
1751 // alice authorizes bob to hold funds
1752 mptAlice.authorize({.account = alice, .holder = bob});
1753
1754 // Bob require pre-authorization
1755 env(fset(bob, asfDepositAuth));
1756 env.close();
1757
1758 // alice try to send 100 MPT to bob, not authorized
1759 mptAlice.pay(alice, bob, 100, tecNO_PERMISSION);
1760 env.close();
1761
1762 // Bob authorize alice
1763 env(deposit::auth(bob, alice));
1764 env.close();
1765
1766 // alice sends 100 MPT to bob
1767 mptAlice.pay(alice, bob, 100);
1768 env.close();
1769
1770 // Create credentials
1771 env(credentials::create(alice, dpIssuer, credType));
1772 env.close();
1773 env(credentials::accept(alice, dpIssuer, credType));
1774 env.close();
1775 auto const jv = credentials::ledgerEntry(env, alice, dpIssuer, credType);
1776 std::string const credIdx = jv[jss::result][jss::index].asString();
1777
1778 // alice sends 100 MPT to bob with credentials which aren't required
1779 mptAlice.pay(alice, bob, 100, tesSUCCESS, {{credIdx}});
1780 env.close();
1781
1782 // Bob revoke authorization
1783 env(deposit::unauth(bob, alice));
1784 env.close();
1785
1786 // alice try to send 100 MPT to bob, not authorized
1787 mptAlice.pay(alice, bob, 100, tecNO_PERMISSION);
1788 env.close();
1789
1790 // alice sends 100 MPT to bob with credentials, not authorized
1791 mptAlice.pay(alice, bob, 100, tecNO_PERMISSION, {{credIdx}});
1792 env.close();
1793
1794 // Bob authorize credentials
1795 env(deposit::authCredentials(bob, {{.issuer = dpIssuer, .credType = credType}}));
1796 env.close();
1797
1798 // alice try to send 100 MPT to bob, not authorized
1799 mptAlice.pay(alice, bob, 100, tecNO_PERMISSION);
1800 env.close();
1801
1802 // alice sends 100 MPT to bob with credentials
1803 mptAlice.pay(alice, bob, 100, tesSUCCESS, {{credIdx}});
1804 env.close();
1805 }
1806
1807 testcase("DepositPreauth disabled featureCredentials");
1808 {
1809 Env env(*this, testableAmendments() - featureCredentials);
1810
1811 std::string const credIdx =
1812 "D007AE4B6E1274B4AF872588267B810C2F82716726351D1C7D38D3E5499FC6"
1813 "E2";
1814
1815 env.fund(XRP(50000), diana, dpIssuer);
1816 env.close();
1817
1818 MPTTester mptAlice(env, alice, {.holders = {bob}});
1819 mptAlice.create(
1820 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTRequireAuth | tfMPTCanTransfer});
1821
1822 env(pay(diana, bob, XRP(500)));
1823 env.close();
1824
1825 // bob creates an empty MPToken
1826 mptAlice.authorize({.account = bob});
1827 // alice authorizes bob to hold funds
1828 mptAlice.authorize({.account = alice, .holder = bob});
1829
1830 // Bob require pre-authorization
1831 env(fset(bob, asfDepositAuth));
1832 env.close();
1833
1834 // alice try to send 100 MPT to bob, not authorized
1835 mptAlice.pay(alice, bob, 100, tecNO_PERMISSION);
1836 env.close();
1837
1838 // alice try to send 100 MPT to bob with credentials, amendment
1839 // disabled
1840 mptAlice.pay(alice, bob, 100, temDISABLED, {{credIdx}});
1841 env.close();
1842
1843 // Bob authorize alice
1844 env(deposit::auth(bob, alice));
1845 env.close();
1846
1847 // alice sends 100 MPT to bob
1848 mptAlice.pay(alice, bob, 100);
1849 env.close();
1850
1851 // alice sends 100 MPT to bob with credentials, amendment disabled
1852 mptAlice.pay(alice, bob, 100, temDISABLED, {{credIdx}});
1853 env.close();
1854
1855 // Bob revoke authorization
1856 env(deposit::unauth(bob, alice));
1857 env.close();
1858
1859 // alice try to send 100 MPT to bob
1860 mptAlice.pay(alice, bob, 100, tecNO_PERMISSION);
1861 env.close();
1862
1863 // alice sends 100 MPT to bob with credentials, amendment disabled
1864 mptAlice.pay(alice, bob, 100, temDISABLED, {{credIdx}});
1865 env.close();
1866 }
1867 }
1868
1869 void
1871 {
1872 testcase("MPT Issue Invalid in Transaction");
1873 using namespace test::jtx;
1874
1875 // Validate that every transaction with an amount/issue field,
1876 // which doesn't support MPT, fails.
1877
1878 // keyed by transaction + amount/issue field
1879 std::set<std::string> txWithAmounts;
1880 for (auto const& format : TxFormats::getInstance())
1881 {
1882 for (auto const& e : format.getSOTemplate())
1883 {
1884 // Transaction has amount/issue fields.
1885 // Exclude pseudo-transaction SetFee. Don't consider
1886 // the Fee field since it's included in every transaction.
1887 if (e.supportMPT() == SoeMptNotSupported && e.sField().getName() != jss::Fee &&
1888 format.getName() != std::string("SetFee"))
1889 {
1890 txWithAmounts.insert(format.getName() + e.sField().fieldName);
1891 break;
1892 }
1893 }
1894 }
1895
1896 Account const alice("alice");
1897 auto const usd = alice["USD"];
1898 Account const carol("carol");
1899 MPTIssue const issue(makeMptID(1, alice));
1900 STAmount mpt{issue, UINT64_C(100)};
1901 auto const jvb = bridge(alice, usd, alice, usd);
1902 for (auto const& feature : {features, features - featureMPTokensV1})
1903 {
1904 Env env{*this, feature};
1905 env.fund(XRP(1'000), alice);
1906 env.fund(XRP(1'000), carol);
1907 auto test = [&](json::Value const& jv, std::string const& mptField) {
1908 txWithAmounts.erase(jv[jss::TransactionType].asString() + mptField);
1909
1910 // tx is signed
1911 auto jtx = env.jt(jv);
1912 Serializer s;
1913 jtx.stx->add(s);
1914 auto jrr = env.rpc("submit", strHex(s.slice()));
1915 BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidTransaction");
1916
1917 // tx is unsigned
1918 json::Value jv1;
1919 jv1[jss::secret] = alice.name();
1920 jv1[jss::tx_json] = jv;
1921 jrr = env.rpc("json", "submit", to_string(jv1));
1922 BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");
1923
1924 jrr = env.rpc("json", "sign", to_string(jv1));
1925 BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");
1926 };
1927 auto setMPTFields = [&](SField const& field, json::Value& jv, bool withAmount = true) {
1928 jv[jss::Asset] = toJson(xrpIssue());
1929 jv[jss::Asset2] = toJson(usd.issue());
1930 if (withAmount)
1931 jv[field.fieldName] = usd(10).value().getJson(JsonOptions::Values::None);
1932 if (field == sfAsset)
1933 {
1934 jv[jss::Asset] = toJson(mpt.get<MPTIssue>());
1935 }
1936 else if (field == sfAsset2)
1937 {
1938 jv[jss::Asset2] = toJson(mpt.get<MPTIssue>());
1939 }
1940 else
1941 {
1942 jv[field.fieldName] = mpt.getJson(JsonOptions::Values::None);
1943 }
1944 };
1945 // All transactions with sfAmount, which don't support MPT.
1946 // Transactions with amount fields, which can't be MPT.
1947 // Transactions with issue fields, which can't be MPT.
1948
1949 // AMMDeposit
1950 auto ammDeposit = [&](SField const& field) {
1951 json::Value jv;
1952 jv[jss::TransactionType] = jss::AMMDeposit;
1953 jv[jss::Account] = alice.human();
1954 jv[jss::Flags] = tfSingleAsset;
1955 setMPTFields(field, jv);
1956 test(jv, field.fieldName);
1957 };
1958 for (SField const& field : {std::ref(sfEPrice), std::ref(sfLPTokenOut)})
1959 ammDeposit(field);
1960 // AMMWithdraw
1961 auto ammWithdraw = [&](SField const& field) {
1962 json::Value jv;
1963 jv[jss::TransactionType] = jss::AMMWithdraw;
1964 jv[jss::Account] = alice.human();
1965 jv[jss::Flags] = tfSingleAsset;
1966 setMPTFields(field, jv);
1967 test(jv, field.fieldName);
1968 };
1969 for (SField const& field : {std::ref(sfEPrice), std::ref(sfLPTokenIn)})
1970 ammWithdraw(field);
1971 // AMMBid
1972 auto ammBid = [&](SField const& field) {
1973 json::Value jv;
1974 jv[jss::TransactionType] = jss::AMMBid;
1975 jv[jss::Account] = alice.human();
1976 setMPTFields(field, jv);
1977 test(jv, field.fieldName);
1978 };
1979 ammBid(sfBidMin);
1980 ammBid(sfBidMax);
1981 // PaymentChannelCreate
1982 {
1983 json::Value jv;
1984 jv[jss::TransactionType] = jss::PaymentChannelCreate;
1985 jv[jss::Account] = alice.human();
1986 jv[jss::Destination] = carol.human();
1987 jv[jss::SettleDelay] = 1;
1988 jv[sfPublicKey.fieldName] = strHex(alice.pk().slice());
1989 jv[jss::Amount] = mpt.getJson(JsonOptions::Values::None);
1990 test(jv, jss::Amount.cStr());
1991 }
1992 // PaymentChannelFund
1993 {
1994 json::Value jv;
1995 jv[jss::TransactionType] = jss::PaymentChannelFund;
1996 jv[jss::Account] = alice.human();
1997 jv[sfChannel.fieldName] = to_string(uint256{1});
1998 jv[jss::Amount] = mpt.getJson(JsonOptions::Values::None);
1999 test(jv, jss::Amount.cStr());
2000 }
2001 // PaymentChannelClaim
2002 {
2003 json::Value jv;
2004 jv[jss::TransactionType] = jss::PaymentChannelClaim;
2005 jv[jss::Account] = alice.human();
2006 jv[sfChannel.fieldName] = to_string(uint256{1});
2007 jv[jss::Amount] = mpt.getJson(JsonOptions::Values::None);
2008 test(jv, jss::Amount.cStr());
2009 }
2010 // NFTokenCreateOffer
2011 {
2012 json::Value jv;
2013 jv[jss::TransactionType] = jss::NFTokenCreateOffer;
2014 jv[jss::Account] = alice.human();
2015 jv[sfNFTokenID.fieldName] = to_string(uint256{1});
2016 jv[jss::Amount] = mpt.getJson(JsonOptions::Values::None);
2017 test(jv, jss::Amount.cStr());
2018 }
2019 // NFTokenAcceptOffer
2020 {
2021 json::Value jv;
2022 jv[jss::TransactionType] = jss::NFTokenAcceptOffer;
2023 jv[jss::Account] = alice.human();
2024 jv[sfNFTokenBrokerFee.fieldName] = mpt.getJson(JsonOptions::Values::None);
2025 test(jv, sfNFTokenBrokerFee.fieldName);
2026 }
2027 // NFTokenMint
2028 {
2029 json::Value jv;
2030 jv[jss::TransactionType] = jss::NFTokenMint;
2031 jv[jss::Account] = alice.human();
2032 jv[sfNFTokenTaxon.fieldName] = 1;
2033 jv[jss::Amount] = mpt.getJson(JsonOptions::Values::None);
2034 test(jv, jss::Amount.cStr());
2035 }
2036 // TrustSet
2037 auto trustSet = [&](SField const& field) {
2038 json::Value jv;
2039 jv[jss::TransactionType] = jss::TrustSet;
2040 jv[jss::Account] = alice.human();
2041 jv[jss::Flags] = 0;
2042 jv[field.fieldName] = mpt.getJson(JsonOptions::Values::None);
2043 test(jv, field.fieldName);
2044 };
2045 trustSet(sfLimitAmount);
2046 trustSet(sfFee);
2047 // XChainCommit
2048 {
2049 json::Value const jv = xchainCommit(alice, jvb, 1, mpt);
2050 test(jv, jss::Amount.cStr());
2051 }
2052 // XChainClaim
2053 {
2054 json::Value const jv = xchainClaim(alice, jvb, 1, mpt, alice);
2055 test(jv, jss::Amount.cStr());
2056 }
2057 // XChainCreateClaimID
2058 {
2059 json::Value const jv = xchainCreateClaimId(alice, jvb, mpt, alice);
2060 test(jv, sfSignatureReward.fieldName);
2061 }
2062 // XChainAddClaimAttestation
2063 {
2064 json::Value const jv =
2065 claimAttestation(alice, jvb, alice, mpt, alice, true, 1, alice, Signer(alice));
2066 test(jv, jss::Amount.cStr());
2067 }
2068 // XChainAddAccountCreateAttestation
2069 {
2071 alice, jvb, alice, mpt, XRP(10), alice, false, 1, alice, Signer(alice));
2072 for (auto const& field : {sfAmount.fieldName, sfSignatureReward.fieldName})
2073 {
2074 jv[field] = mpt.getJson(JsonOptions::Values::None);
2075 test(jv, field);
2076 }
2077 }
2078 // XChainAccountCreateCommit
2079 {
2080 json::Value jv = sidechainXchainAccountCreate(alice, jvb, alice, mpt, XRP(10));
2081 for (auto const& field : {sfAmount.fieldName, sfSignatureReward.fieldName})
2082 {
2083 jv[field] = mpt.getJson(JsonOptions::Values::None);
2084 test(jv, field);
2085 }
2086 }
2087 // XChain[Create|Modify]Bridge
2088 auto bridgeTx = [&](json::StaticString const& tt,
2089 STAmount const& rewardAmount,
2090 STAmount const& minAccountAmount,
2091 std::string const& field) {
2092 json::Value jv;
2093 jv[jss::TransactionType] = tt;
2094 jv[jss::Account] = alice.human();
2095 jv[sfXChainBridge.fieldName] = jvb;
2096 jv[sfSignatureReward.fieldName] = rewardAmount.getJson(JsonOptions::Values::None);
2097 jv[sfMinAccountCreateAmount.fieldName] =
2098 minAccountAmount.getJson(JsonOptions::Values::None);
2099 test(jv, field);
2100 };
2101 auto reward = STAmount{sfSignatureReward, mpt};
2102 auto minAmount = STAmount{sfMinAccountCreateAmount, usd(10)};
2103 for (SField const& field :
2104 {std::ref(sfSignatureReward), std::ref(sfMinAccountCreateAmount)})
2105 {
2106 bridgeTx(jss::XChainCreateBridge, reward, minAmount, field.fieldName);
2107 bridgeTx(jss::XChainModifyBridge, reward, minAmount, field.fieldName);
2108 reward = STAmount{sfSignatureReward, usd(10)};
2109 minAmount = STAmount{sfMinAccountCreateAmount, mpt};
2110 }
2111 }
2112 BEAST_EXPECT(txWithAmounts.empty());
2113 }
2114
2115 void
2117 {
2118 using namespace test::jtx;
2119 using namespace std::literals;
2120 FeatureBitset const withoutFix = features - fixCleanup3_2_0;
2121 FeatureBitset const withFix = features | fixCleanup3_2_0;
2122 FeatureBitset const withoutFixAndV2 = withoutFix - featureMPTokensV2;
2123 FeatureBitset const withFixAndWithoutV2 = withFix - featureMPTokensV2;
2124
2125 Account const alice{"alice"};
2126 Account const bob{"bob"};
2127 Account const gw{"gw"};
2128
2129 using MPTValue = MPTAmount::value_type;
2130 MPTValue const mptMin = std::numeric_limits<MPTValue>::min();
2131 MPTValue const mptMax = std::numeric_limits<MPTValue>::max();
2133 std::uint64_t const firstInvalidMPTMantissa = static_cast<std::uint64_t>(mptMax) + 1;
2134 MPTValue const alice0 = 10'000;
2135 MPTValue const gw0 = -20'000;
2136 TER const success = tesSUCCESS;
2137 TER const invariantFailed = tecINVARIANT_FAILED;
2138 TER const pathPartial = tecPATH_PARTIAL;
2139 TER const badAmountTer = temBAD_AMOUNT;
2140
2141 struct BadMPTAmount
2142 {
2143 std::string_view name;
2144 std::uint64_t mantissa;
2145 bool negative;
2146 MPTValue mptValue;
2147 TER issuerToHolderPreFixTer;
2148 TER holderSourcePreFixTer;
2149 MPTValue issuerToHolderAliceAfterPreFix;
2150 MPTValue issuerToHolderIssuerAfterPreFix;
2151 MPTValue issuerToHolderAliceAfterPostFix;
2152 MPTValue issuerToHolderIssuerAfterPostFix;
2153 };
2154 // clang-format off
2155 std::array<BadMPTAmount, 7> const badMPTAmounts = {{
2156 { .name="INT64_MAX + 1", .mantissa=firstInvalidMPTMantissa, .negative=false, .mptValue=mptMin, .issuerToHolderPreFixTer=invariantFailed, .holderSourcePreFixTer=pathPartial, .issuerToHolderAliceAfterPreFix=alice0, .issuerToHolderIssuerAfterPreFix=gw0, .issuerToHolderAliceAfterPostFix=alice0 - 1, .issuerToHolderIssuerAfterPostFix=gw0 + 1},
2157 { .name="INT64_MAX + 10", .mantissa=firstInvalidMPTMantissa + 9, .negative=false, .mptValue=mptMin + 9, .issuerToHolderPreFixTer=invariantFailed, .holderSourcePreFixTer=pathPartial, .issuerToHolderAliceAfterPreFix=alice0, .issuerToHolderIssuerAfterPreFix=gw0, .issuerToHolderAliceAfterPostFix=alice0 - 1, .issuerToHolderIssuerAfterPostFix=gw0 + 1},
2158 { .name="UINT64_MAX - 9998", .mantissa=u64Max - 9'998, .negative=false, .mptValue=MPTValue{-9'999}, .issuerToHolderPreFixTer=success, .holderSourcePreFixTer=pathPartial, .issuerToHolderAliceAfterPreFix=alice0 - 9'999, .issuerToHolderIssuerAfterPreFix=gw0 + 9'999, .issuerToHolderAliceAfterPostFix=alice0 - 10'000, .issuerToHolderIssuerAfterPostFix=gw0 + 10'000},
2159 { .name="UINT64_MAX - 9", .mantissa=u64Max - 9, .negative=false, .mptValue=MPTValue{-10}, .issuerToHolderPreFixTer=success, .holderSourcePreFixTer=pathPartial, .issuerToHolderAliceAfterPreFix=alice0 - 10, .issuerToHolderIssuerAfterPreFix=gw0 + 10, .issuerToHolderAliceAfterPostFix=alice0 - 11, .issuerToHolderIssuerAfterPostFix=gw0 + 11},
2160 { .name="UINT64_MAX - 1", .mantissa=u64Max - 1, .negative=false, .mptValue=MPTValue{-2}, .issuerToHolderPreFixTer=success, .holderSourcePreFixTer=pathPartial, .issuerToHolderAliceAfterPreFix=alice0 - 2, .issuerToHolderIssuerAfterPreFix=gw0 + 2, .issuerToHolderAliceAfterPostFix=alice0 - 3, .issuerToHolderIssuerAfterPostFix=gw0 + 3},
2161 { .name="UINT64_MAX", .mantissa=u64Max, .negative=false, .mptValue=MPTValue{-1}, .issuerToHolderPreFixTer=success, .holderSourcePreFixTer=pathPartial, .issuerToHolderAliceAfterPreFix=alice0 - 1, .issuerToHolderIssuerAfterPreFix=gw0 + 1, .issuerToHolderAliceAfterPostFix=alice0 - 2, .issuerToHolderIssuerAfterPostFix=gw0 + 2},
2162 { .name="-2", .mantissa=std::uint64_t{2}, .negative=true, .mptValue=MPTValue{-2}, .issuerToHolderPreFixTer=badAmountTer, .holderSourcePreFixTer=badAmountTer, .issuerToHolderAliceAfterPreFix=alice0, .issuerToHolderIssuerAfterPreFix=gw0, .issuerToHolderAliceAfterPostFix=alice0 - 1, .issuerToHolderIssuerAfterPostFix=gw0 + 1}
2163 }};
2164 // clang-format on
2165 auto const badMPTAmount = [&](MPTIssue const& issue, BadMPTAmount const& bad) {
2166 return STAmount{issue, bad.mantissa, 0, bad.negative, STAmount::Unchecked{}};
2167 };
2168 auto const makeIssue = [&](Env& env) {
2169 MPTTester const mpt{
2170 {.env = env,
2171 .issuer = gw,
2172 .holders = {alice, bob},
2173 .pay = 10'000,
2174 .flags = tfMPTCanTransfer | tfMPTCanTrade | tfMPTCanEscrow | tfMPTCanClawback}};
2175 return MPTIssue{mpt.issuanceID()};
2176 };
2177 auto const withNonCanonicalMPTAmount =
2178 [](JTx jt, SField const& field, STAmount const& amount, Account const& signer) {
2179 STTx tx{*jt.stx};
2180 tx.setFieldAmount(field, amount);
2181 tx.sign(signer.pk(), signer.sk());
2183 return jt;
2184 };
2185 auto const roundTrip = [](STTx const& tx) {
2186 Serializer s;
2187 tx.add(s);
2188 SerialIter sit{s.slice()};
2189 return STTx{sit};
2190 };
2191 auto const expectRoundTripBadMPT =
2192 [&](JTx const& jt, SField const& field, BadMPTAmount const& bad) {
2193 auto const roundTripped = roundTrip(*jt.stx);
2194 auto const persisted = roundTripped.getFieldAmount(field);
2195 BEAST_EXPECT(persisted.holds<MPTIssue>());
2196 BEAST_EXPECT(persisted.mantissa() == bad.mantissa);
2197 BEAST_EXPECT(persisted.exponent() == 0);
2198 BEAST_EXPECT(persisted.negative() == bad.negative);
2199 BEAST_EXPECT(persisted.mpt().value() == bad.mptValue);
2200 if (!bad.negative)
2201 BEAST_EXPECT(persisted.mantissa() > kMaxMpTokenAmount);
2202 };
2203
2204 for (auto const& bad : badMPTAmounts)
2205 {
2206 testcase("fixCleanup3_2_0 rejects non-canonical MPT Payment amounts");
2207 {
2208 Env env{*this, withoutFixAndV2};
2209 env.fund(XRP(100'000), alice, bob, gw);
2210 env.close();
2211 auto const issue = makeIssue(env);
2212
2213 auto const badAmount = badMPTAmount(issue, bad);
2214 auto malformedHolderToHolder = withNonCanonicalMPTAmount(
2215 env.jt(pay(alice, bob, STAmount{issue, std::uint64_t{1}})),
2216 sfAmount,
2217 badAmount,
2218 alice);
2219 expectRoundTripBadMPT(malformedHolderToHolder, sfAmount, bad);
2220 malformedHolderToHolder.ter = bad.holderSourcePreFixTer;
2221 env.submit(malformedHolderToHolder);
2222 env.close();
2223 BEAST_EXPECT(
2224 (env.balance(alice, issue).value() == STAmount{MPTAmount{10'000}, issue}));
2225 BEAST_EXPECT(
2226 (env.balance(bob, issue).value() == STAmount{MPTAmount{10'000}, issue}));
2227 BEAST_EXPECT(
2228 (env.balance(gw, issue).value() == STAmount{MPTAmount{-20'000}, issue}));
2229
2230 env.enableFeature(fixCleanup3_2_0);
2231 env.close();
2232 env(env.jt(pay(bob, alice, STAmount{issue, std::uint64_t{1}})), Ter{tesSUCCESS});
2233 env.close();
2234 BEAST_EXPECT(
2235 (env.balance(alice, issue).value() == STAmount{MPTAmount{10'001}, issue}));
2236 BEAST_EXPECT(
2237 (env.balance(bob, issue).value() == STAmount{MPTAmount{9'999}, issue}));
2238 BEAST_EXPECT(
2239 (env.balance(gw, issue).value() == STAmount{MPTAmount{-20'000}, issue}));
2240 }
2241 {
2242 Env env{*this, envconfig(), withoutFixAndV2, nullptr, beast::Severity::Disabled};
2243 env.fund(XRP(100'000), alice, bob, gw);
2244 env.close();
2245 auto const issue = makeIssue(env);
2246
2247 auto const badAmount = badMPTAmount(issue, bad);
2248 auto malformedIssuerToHolder = withNonCanonicalMPTAmount(
2249 env.jt(pay(gw, alice, STAmount{issue, std::uint64_t{1}})),
2250 sfAmount,
2251 badAmount,
2252 gw);
2253 expectRoundTripBadMPT(malformedIssuerToHolder, sfAmount, bad);
2254 malformedIssuerToHolder.ter = bad.issuerToHolderPreFixTer;
2255 env.submit(malformedIssuerToHolder);
2256 env.close();
2257 BEAST_EXPECT(
2258 (env.balance(alice, issue).value() ==
2259 STAmount{MPTAmount{bad.issuerToHolderAliceAfterPreFix}, issue}));
2260 BEAST_EXPECT(
2261 (env.balance(bob, issue).value() == STAmount{MPTAmount{10'000}, issue}));
2262 BEAST_EXPECT(
2263 (env.balance(gw, issue).value() ==
2264 STAmount{MPTAmount{bad.issuerToHolderIssuerAfterPreFix}, issue}));
2265
2266 env.enableFeature(fixCleanup3_2_0);
2267 env.close();
2268 env(env.jt(pay(alice, gw, STAmount{issue, std::uint64_t{1}})), Ter{tesSUCCESS});
2269 env.close();
2270 BEAST_EXPECT(
2271 (env.balance(alice, issue).value() ==
2272 STAmount{MPTAmount{bad.issuerToHolderAliceAfterPostFix}, issue}));
2273 BEAST_EXPECT(
2274 (env.balance(bob, issue).value() == STAmount{MPTAmount{10'000}, issue}));
2275 BEAST_EXPECT(
2276 (env.balance(gw, issue).value() ==
2277 STAmount{MPTAmount{bad.issuerToHolderIssuerAfterPostFix}, issue}));
2278 }
2279 {
2280 Env env{*this, withoutFixAndV2};
2281 env.fund(XRP(100'000), alice, bob, gw);
2282 env.close();
2283 auto const issue = makeIssue(env);
2284
2285 auto const badAmount = badMPTAmount(issue, bad);
2286 auto malformedHolderToIssuer = withNonCanonicalMPTAmount(
2287 env.jt(pay(alice, gw, STAmount{issue, std::uint64_t{1}})),
2288 sfAmount,
2289 badAmount,
2290 alice);
2291 expectRoundTripBadMPT(malformedHolderToIssuer, sfAmount, bad);
2292 malformedHolderToIssuer.ter = bad.holderSourcePreFixTer;
2293 env.submit(malformedHolderToIssuer);
2294 env.close();
2295 BEAST_EXPECT(
2296 (env.balance(alice, issue).value() == STAmount{MPTAmount{10'000}, issue}));
2297 BEAST_EXPECT(
2298 (env.balance(bob, issue).value() == STAmount{MPTAmount{10'000}, issue}));
2299 BEAST_EXPECT(
2300 (env.balance(gw, issue).value() == STAmount{MPTAmount{-20'000}, issue}));
2301
2302 env.enableFeature(fixCleanup3_2_0);
2303 env.close();
2304 env(env.jt(pay(gw, alice, STAmount{issue, std::uint64_t{1}})), Ter{tesSUCCESS});
2305 env.close();
2306 BEAST_EXPECT(
2307 (env.balance(alice, issue).value() == STAmount{MPTAmount{10'001}, issue}));
2308 BEAST_EXPECT(
2309 (env.balance(bob, issue).value() == STAmount{MPTAmount{10'000}, issue}));
2310 BEAST_EXPECT(
2311 (env.balance(gw, issue).value() == STAmount{MPTAmount{-20'001}, issue}));
2312 }
2313 {
2314 Env env{*this, withFixAndWithoutV2};
2315 env.fund(XRP(100'000), alice, bob, gw);
2316 env.close();
2317 auto const issue = makeIssue(env);
2318
2319 auto const badAmount = badMPTAmount(issue, bad);
2320 auto tx = withNonCanonicalMPTAmount(
2321 env.jt(pay(alice, bob, STAmount{issue, std::uint64_t{1}})),
2322 sfAmount,
2323 badAmount,
2324 alice);
2325 tx.ter = temBAD_AMOUNT;
2326 env.submit(tx);
2327 }
2328 {
2329 Env env{*this, withFixAndWithoutV2};
2330 env.fund(XRP(100'000), alice, bob, gw);
2331 env.close();
2332 auto const issue = makeIssue(env);
2333
2334 auto const badAmount = badMPTAmount(issue, bad);
2335 auto tx = withNonCanonicalMPTAmount(
2336 env.jt(pay(gw, alice, STAmount{issue, std::uint64_t{1}})),
2337 sfAmount,
2338 badAmount,
2339 gw);
2340 tx.ter = temBAD_AMOUNT;
2341 env.submit(tx);
2342 }
2343 {
2344 Env env{*this, withFixAndWithoutV2};
2345 env.fund(XRP(100'000), alice, bob, gw);
2346 env.close();
2347 auto const issue = makeIssue(env);
2348
2349 auto const badAmount = badMPTAmount(issue, bad);
2350 auto tx = withNonCanonicalMPTAmount(
2351 env.jt(pay(alice, gw, STAmount{issue, std::uint64_t{1}})),
2352 sfAmount,
2353 badAmount,
2354 alice);
2355 tx.ter = temBAD_AMOUNT;
2356 env.submit(tx);
2357 }
2358
2359 testcase("fixCleanup3_2_0 rejects non-canonical MPT Check amounts");
2360 {
2361 Env env{*this, envconfig(), withoutFix, nullptr, beast::Severity::Disabled};
2362 env.fund(XRP(100'000), alice, bob, gw);
2363 env.close();
2364 auto const issue = makeIssue(env);
2365
2366 auto const badSendMax = badMPTAmount(issue, bad);
2367 auto const checkSeq = env.seq(alice);
2368 auto tx = withNonCanonicalMPTAmount(
2369 env.jt(check::create(alice, bob, STAmount{issue, std::uint64_t{10}})),
2370 sfSendMax,
2371 badSendMax,
2372 alice);
2373 tx.ter = bad.negative ? TER{temBAD_AMOUNT} : TER{tesSUCCESS};
2374 env.submit(tx);
2375 env.close();
2376
2377 auto const checkKeylet = keylet::check(alice.id(), checkSeq);
2378 auto const sleCheck = env.le(checkKeylet);
2379 BEAST_EXPECT((sleCheck != nullptr) == !bad.negative);
2380 if (sleCheck && !bad.negative)
2381 {
2382 auto const persisted = sleCheck->getFieldAmount(sfSendMax);
2383 BEAST_EXPECT(persisted.holds<MPTIssue>());
2384 BEAST_EXPECT(persisted.mantissa() == bad.mantissa);
2385 BEAST_EXPECT(persisted.negative() == bad.negative);
2386 }
2387 }
2388 {
2389 Env env{*this, envconfig(), withoutFix, nullptr, beast::Severity::Disabled};
2390 env.fund(XRP(100'000), alice, bob, gw);
2391 env.close();
2392 auto const issue = makeIssue(env);
2393
2394 auto const badSendMax = badMPTAmount(issue, bad);
2395 auto const checkSeq = env.seq(alice);
2396 auto tx = withNonCanonicalMPTAmount(
2397 env.jt(check::create(alice, bob, STAmount{issue, std::uint64_t{10}})),
2398 sfSendMax,
2399 badSendMax,
2400 alice);
2401 tx.ter = bad.negative ? TER{temBAD_AMOUNT} : TER{tesSUCCESS};
2402 env.submit(tx);
2403 env.close();
2404
2405 auto const checkKeylet = keylet::check(alice.id(), checkSeq);
2406 BEAST_EXPECT((env.le(checkKeylet) != nullptr) == !bad.negative);
2407 if (!bad.negative)
2408 {
2409 // CheckCancel has no amount fields, but it must be able to
2410 // remove a malformed legacy Check while the fix is disabled.
2411 env(env.jt(check::cancel(alice, checkKeylet.key)), Ter{tesSUCCESS});
2412 env.close();
2413 BEAST_EXPECT(env.le(checkKeylet) == nullptr);
2414 }
2415 }
2416 {
2417 Env env{*this, envconfig(), withoutFix, nullptr, beast::Severity::Disabled};
2418 env.fund(XRP(100'000), alice, bob, gw);
2419 env.close();
2420 auto const issue = makeIssue(env);
2421
2422 auto const badSendMax = badMPTAmount(issue, bad);
2423 auto const checkSeq = env.seq(alice);
2424 auto tx = withNonCanonicalMPTAmount(
2425 env.jt(check::create(alice, bob, STAmount{issue, std::uint64_t{10}})),
2426 sfSendMax,
2427 badSendMax,
2428 alice);
2429 tx.ter = bad.negative ? TER{temBAD_AMOUNT} : TER{tesSUCCESS};
2430 env.submit(tx);
2431 env.close();
2432
2433 auto const checkKeylet = keylet::check(alice.id(), checkSeq);
2434 BEAST_EXPECT((env.le(checkKeylet) != nullptr) == !bad.negative);
2435 if (!bad.negative)
2436 {
2437 env.enableFeature(fixCleanup3_2_0);
2438 env.close();
2439
2440 // Once the fix is enabled, CheckCancel should still remove
2441 // a legacy Check because it does not consume the bad amount.
2442 env(env.jt(check::cancel(alice, checkKeylet.key)), Ter{tesSUCCESS});
2443 env.close();
2444 BEAST_EXPECT(env.le(checkKeylet) == nullptr);
2445 }
2446 }
2447 {
2448 Env env{*this, envconfig(), withoutFix, nullptr, beast::Severity::Disabled};
2449 env.fund(XRP(100'000), alice, bob, gw);
2450 env.close();
2451 auto const issue = makeIssue(env);
2452
2453 auto const badSendMax = badMPTAmount(issue, bad);
2454 auto const checkSeq = env.seq(alice);
2455 auto tx = withNonCanonicalMPTAmount(
2456 env.jt(check::create(alice, bob, STAmount{issue, std::uint64_t{10}})),
2457 sfSendMax,
2458 badSendMax,
2459 alice);
2460 tx.ter = bad.negative ? TER{temBAD_AMOUNT} : TER{tesSUCCESS};
2461 env.submit(tx);
2462 env.close();
2463
2464 auto const checkKeylet = keylet::check(alice.id(), checkSeq);
2465 BEAST_EXPECT((env.le(checkKeylet) != nullptr) == !bad.negative);
2466 if (!bad.negative)
2467 {
2468 env.enableFeature(fixCleanup3_2_0);
2469 env.close();
2470
2471 auto const cashAmount = STAmount{sfAmount, issue, std::uint64_t{1}, 0, false};
2472 env(env.jt(check::cash(bob, checkKeylet.key, cashAmount)), Ter{tefBAD_LEDGER});
2473 env.close();
2474 BEAST_EXPECT(env.le(checkKeylet) != nullptr);
2475 }
2476 }
2477 {
2478 Env env{*this, withoutFix};
2479 env.fund(XRP(100'000), alice, bob, gw);
2480 env.close();
2481 auto const issue = makeIssue(env);
2482
2483 auto const sendMax = STAmount{sfSendMax, issue, std::uint64_t{10}, 0, false};
2484 auto const checkSeq = env.seq(alice);
2485 env(env.jt(check::create(alice, bob, sendMax)), Ter{tesSUCCESS});
2486 env.close();
2487
2488 auto const badCashAmount = badMPTAmount(issue, bad);
2489 auto tx = withNonCanonicalMPTAmount(
2490 env.jt(
2491 check::cash(
2492 bob,
2493 keylet::check(alice.id(), checkSeq).key,
2494 STAmount{issue, std::uint64_t{1}})),
2495 sfAmount,
2496 badCashAmount,
2497 bob);
2498 expectRoundTripBadMPT(tx, sfAmount, bad);
2499 tx.ter = bad.holderSourcePreFixTer;
2500 env.submit(tx);
2501 env.close();
2502 BEAST_EXPECT(env.le(keylet::check(alice.id(), checkSeq)) != nullptr);
2503 BEAST_EXPECT(
2504 (env.balance(alice, issue).value() == STAmount{MPTAmount{10'000}, issue}));
2505 BEAST_EXPECT(
2506 (env.balance(bob, issue).value() == STAmount{MPTAmount{10'000}, issue}));
2507 BEAST_EXPECT(
2508 (env.balance(gw, issue).value() == STAmount{MPTAmount{-20'000}, issue}));
2509 }
2510 {
2511 Env env{*this, withFix};
2512 env.fund(XRP(100'000), alice, bob, gw);
2513 env.close();
2514 auto const issue = makeIssue(env);
2515
2516 auto const sendMax = STAmount{sfSendMax, issue, std::uint64_t{10}, 0, false};
2517 auto const checkSeq = env.seq(alice);
2518 env(env.jt(check::create(alice, bob, sendMax)), Ter{tesSUCCESS});
2519 env.close();
2520
2521 auto const badCashAmount = badMPTAmount(issue, bad);
2522 auto tx = withNonCanonicalMPTAmount(
2523 env.jt(
2524 check::cash(
2525 bob,
2526 keylet::check(alice.id(), checkSeq).key,
2527 STAmount{issue, std::uint64_t{1}})),
2528 sfAmount,
2529 badCashAmount,
2530 bob);
2531 tx.ter = temBAD_AMOUNT;
2532 env.submit(tx);
2533 }
2534
2535 testcase("fixCleanup3_2_0 rejects non-canonical MPT Escrow amounts");
2536 {
2537 Env env{*this, withoutFix};
2538 env.fund(XRP(100'000), alice, bob, gw);
2539 env.close();
2540 auto const issue = makeIssue(env);
2541
2542 auto const escrowSeq = env.seq(alice);
2543 auto const badAmount = badMPTAmount(issue, bad);
2544 auto tx = withNonCanonicalMPTAmount(
2545 env.jt(
2546 escrow::create(alice, bob, STAmount{issue, std::uint64_t{1}}),
2547 escrow::kFinishTime(env.now() + 1s)),
2548 sfAmount,
2549 badAmount,
2550 alice);
2551 tx.ter = bad.negative ? TER{temBAD_AMOUNT} : TER{tecINSUFFICIENT_FUNDS};
2552 env.submit(tx);
2553 env.close();
2554 BEAST_EXPECT(env.le(keylet::escrow(alice.id(), escrowSeq)) == nullptr);
2555 }
2556 {
2557 Env env{*this, withFix};
2558 env.fund(XRP(100'000), alice, bob, gw);
2559 env.close();
2560 auto const issue = makeIssue(env);
2561
2562 auto const badAmount = badMPTAmount(issue, bad);
2563 auto tx = withNonCanonicalMPTAmount(
2564 env.jt(
2565 escrow::create(alice, bob, STAmount{issue, std::uint64_t{1}}),
2566 escrow::kFinishTime(env.now() + 1s)),
2567 sfAmount,
2568 badAmount,
2569 alice);
2570 tx.ter = temBAD_AMOUNT;
2571 env.submit(tx);
2572 }
2573
2574 testcase("fixCleanup3_2_0 rejects non-canonical MPT Clawback amounts");
2575 {
2576 Env env{*this, withoutFix};
2577 env.fund(XRP(100'000), alice, bob, gw);
2578 env.close();
2579 auto const issue = makeIssue(env);
2580
2581 auto const badAmount = badMPTAmount(issue, bad);
2582 auto tx = withNonCanonicalMPTAmount(
2583 env.jt(claw(gw, STAmount{issue, std::uint64_t{1}}, bob)),
2584 sfAmount,
2585 badAmount,
2586 gw);
2587 expectRoundTripBadMPT(tx, sfAmount, bad);
2588 tx.ter = bad.negative ? TER{temBAD_AMOUNT} : TER{tesSUCCESS};
2589 env.submit(tx);
2590 env.close();
2591
2592 MPTValue const bobAfter = bad.negative ? MPTValue{10'000} : MPTValue{0};
2593 MPTValue const gwAfter = bad.negative ? MPTValue{-20'000} : MPTValue{-10'000};
2594 BEAST_EXPECT(
2595 (env.balance(alice, issue).value() == STAmount{MPTAmount{10'000}, issue}));
2596 BEAST_EXPECT(
2597 (env.balance(bob, issue).value() == STAmount{MPTAmount{bobAfter}, issue}));
2598 BEAST_EXPECT(
2599 (env.balance(gw, issue).value() == STAmount{MPTAmount{gwAfter}, issue}));
2600 }
2601 {
2602 Env env{*this, withFix};
2603 env.fund(XRP(100'000), alice, bob, gw);
2604 env.close();
2605 auto const issue = makeIssue(env);
2606
2607 auto const badAmount = badMPTAmount(issue, bad);
2608 auto tx = withNonCanonicalMPTAmount(
2609 env.jt(claw(gw, STAmount{issue, std::uint64_t{1}}, bob)),
2610 sfAmount,
2611 badAmount,
2612 gw);
2613 tx.ter = temBAD_AMOUNT;
2614 env.submit(tx);
2615 env.close();
2616
2617 BEAST_EXPECT(
2618 (env.balance(alice, issue).value() == STAmount{MPTAmount{10'000}, issue}));
2619 BEAST_EXPECT(
2620 (env.balance(bob, issue).value() == STAmount{MPTAmount{10'000}, issue}));
2621 BEAST_EXPECT(
2622 (env.balance(gw, issue).value() == STAmount{MPTAmount{-20'000}, issue}));
2623 }
2624
2625 testcase("featureMPTokensV2 disabled rejects MPT OfferCreate amounts");
2626 {
2627 Env env{*this, withoutFixAndV2};
2628 env.fund(XRP(100'000), alice, bob, gw);
2629 env.close();
2630 auto const issue = makeIssue(env);
2631
2632 auto const badTakerPays = badMPTAmount(issue, bad);
2633 auto tx = withNonCanonicalMPTAmount(
2634 env.jt(offer(alice, STAmount{issue, std::uint64_t{1}}, XRP(10))),
2635 sfTakerPays,
2636 badTakerPays,
2637 alice);
2638 expectRoundTripBadMPT(tx, sfTakerPays, bad);
2639 tx.ter = temDISABLED;
2640 env.submit(tx);
2641 }
2642 {
2643 Env env{*this, withFixAndWithoutV2};
2644 env.fund(XRP(100'000), alice, bob, gw);
2645 env.close();
2646 auto const issue = makeIssue(env);
2647
2648 auto const badTakerPays = badMPTAmount(issue, bad);
2649 auto tx = withNonCanonicalMPTAmount(
2650 env.jt(offer(alice, STAmount{issue, std::uint64_t{1}}, XRP(10))),
2651 sfTakerPays,
2652 badTakerPays,
2653 alice);
2654 tx.ter = temDISABLED;
2655 env.submit(tx);
2656 }
2657 {
2658 // sfTakerPays is MPT: both amendments active. Negative offers
2659 // fail in OfferCreate::preflight() before the universal check;
2660 // positive non-canonical amounts fail in the universal check.
2661 Env env{*this, withFix};
2662 env.fund(XRP(100'000), alice, bob, gw);
2663 env.close();
2664 auto const issue = makeIssue(env);
2665
2666 auto const badTakerPays = badMPTAmount(issue, bad);
2667 auto tx = withNonCanonicalMPTAmount(
2668 env.jt(offer(alice, STAmount{issue, std::uint64_t{1}}, XRP(10))),
2669 sfTakerPays,
2670 badTakerPays,
2671 alice);
2672 tx.ter = TER{temBAD_AMOUNT};
2673 env.submit(tx);
2674 }
2675 {
2676 // sfTakerGets is MPT: both amendments active. Negative offers
2677 // fail in OfferCreate::preflight() before the universal check;
2678 // positive non-canonical amounts fail in the universal check.
2679 Env env{*this, withFix};
2680 env.fund(XRP(100'000), alice, bob, gw);
2681 env.close();
2682 auto const issue = makeIssue(env);
2683
2684 auto const badTakerGets = badMPTAmount(issue, bad);
2685 auto tx = withNonCanonicalMPTAmount(
2686 env.jt(offer(alice, XRP(10), STAmount{issue, std::uint64_t{1}})),
2687 sfTakerGets,
2688 badTakerGets,
2689 alice);
2690 tx.ter = TER{temBAD_AMOUNT};
2691 env.submit(tx);
2692 }
2693
2694 testcase("featureMPTokensV2 disabled rejects MPT AMMCreate amounts");
2695 {
2696 Env env{*this, withoutFixAndV2};
2697 env.fund(XRP(100'000), alice, bob, gw);
2698 env.close();
2699 auto const issue = makeIssue(env);
2700
2701 auto const badAmount = badMPTAmount(issue, bad);
2702 auto tx = withNonCanonicalMPTAmount(
2703 env.jt(
2704 AMM::createJv(alice.id(), STAmount{issue, std::uint64_t{1}}, XRP(1), 0),
2705 Fee(static_cast<std::uint64_t>(env.current()->fees().increment.drops()))),
2706 sfAmount,
2707 badAmount,
2708 alice);
2709 expectRoundTripBadMPT(tx, sfAmount, bad);
2710 tx.ter = temDISABLED;
2711 env.submit(tx);
2712 }
2713 {
2714 Env env{*this, withFixAndWithoutV2};
2715 env.fund(XRP(100'000), alice, bob, gw);
2716 env.close();
2717 auto const issue = makeIssue(env);
2718
2719 auto const badAmount = badMPTAmount(issue, bad);
2720 auto tx = withNonCanonicalMPTAmount(
2721 env.jt(
2722 AMM::createJv(alice.id(), STAmount{issue, std::uint64_t{1}}, XRP(1), 0),
2723 Fee(static_cast<std::uint64_t>(env.current()->fees().increment.drops()))),
2724 sfAmount,
2725 badAmount,
2726 alice);
2727 tx.ter = temDISABLED;
2728 env.submit(tx);
2729 }
2730 {
2731 // sfAmount is MPT: both amendments active, expect temBAD_AMOUNT
2732 Env env{*this, withFix};
2733 env.fund(XRP(100'000), alice, bob, gw);
2734 env.close();
2735 auto const issue = makeIssue(env);
2736
2737 auto const badAmount = badMPTAmount(issue, bad);
2738 auto tx = withNonCanonicalMPTAmount(
2739 env.jt(
2740 AMM::createJv(alice.id(), STAmount{issue, std::uint64_t{1}}, XRP(1), 0),
2741 Fee(static_cast<std::uint64_t>(env.current()->fees().increment.drops()))),
2742 sfAmount,
2743 badAmount,
2744 alice);
2745 tx.ter = temBAD_AMOUNT;
2746 env.submit(tx);
2747 }
2748
2749 testcase("featureMPTokensV2 disabled rejects MPT AMMDeposit amounts");
2750 {
2751 Env env{*this, withoutFixAndV2};
2752 env.fund(XRP(100'000), alice, bob, gw);
2753 env.close();
2754 auto const issue = makeIssue(env);
2755
2756 auto const badAmount = badMPTAmount(issue, bad);
2757 auto tx = withNonCanonicalMPTAmount(
2758 env.jt(
2759 AMM::depositJv(
2760 {.account = alice,
2761 .asset1In = STAmount{issue, std::uint64_t{1}},
2762 .assets = std::make_pair(Asset{issue}, Asset{xrpIssue()})}),
2763 Fee(static_cast<std::uint64_t>(env.current()->fees().increment.drops()))),
2764 sfAmount,
2765 badAmount,
2766 alice);
2767 expectRoundTripBadMPT(tx, sfAmount, bad);
2768 tx.ter = temDISABLED;
2769 env.submit(tx);
2770 }
2771 {
2772 Env env{*this, withFixAndWithoutV2};
2773 env.fund(XRP(100'000), alice, bob, gw);
2774 env.close();
2775 auto const issue = makeIssue(env);
2776
2777 auto const badAmount = badMPTAmount(issue, bad);
2778 auto tx = withNonCanonicalMPTAmount(
2779 env.jt(
2780 AMM::depositJv(
2781 {.account = alice,
2782 .asset1In = STAmount{issue, std::uint64_t{1}},
2783 .assets = std::make_pair(Asset{issue}, Asset{xrpIssue()})}),
2784 Fee(static_cast<std::uint64_t>(env.current()->fees().increment.drops()))),
2785 sfAmount,
2786 badAmount,
2787 alice);
2788 tx.ter = temDISABLED;
2789 env.submit(tx);
2790 }
2791 {
2792 // sfAmount is MPT: both amendments active, expect temBAD_AMOUNT
2793 Env env{*this, withFix};
2794 env.fund(XRP(100'000), alice, bob, gw);
2795 env.close();
2796 auto const issue = makeIssue(env);
2797
2798 auto const badAmount = badMPTAmount(issue, bad);
2799 auto tx = withNonCanonicalMPTAmount(
2800 env.jt(
2801 AMM::depositJv(
2802 {.account = alice,
2803 .asset1In = STAmount{issue, std::uint64_t{1}},
2804 .assets = std::make_pair(Asset{issue}, Asset{xrpIssue()})}),
2805 Fee(static_cast<std::uint64_t>(env.current()->fees().increment.drops()))),
2806 sfAmount,
2807 badAmount,
2808 alice);
2809 tx.ter = temBAD_AMOUNT;
2810 env.submit(tx);
2811 }
2812
2813 testcase("featureMPTokensV2 disabled rejects MPT AMMWithdraw amounts");
2814 {
2815 Env env{*this, withoutFixAndV2};
2816 env.fund(XRP(100'000), alice, bob, gw);
2817 env.close();
2818 auto const issue = makeIssue(env);
2819
2820 auto const badAmount = badMPTAmount(issue, bad);
2821 auto tx = withNonCanonicalMPTAmount(
2822 env.jt(
2823 AMM::withdrawJv(
2824 {.account = alice,
2825 .asset1Out = STAmount{issue, std::uint64_t{1}},
2826 .assets = std::make_pair(Asset{issue}, Asset{xrpIssue()})}),
2827 Fee(static_cast<std::uint64_t>(env.current()->fees().increment.drops()))),
2828 sfAmount,
2829 badAmount,
2830 alice);
2831 expectRoundTripBadMPT(tx, sfAmount, bad);
2832 tx.ter = temDISABLED;
2833 env.submit(tx);
2834 }
2835 {
2836 Env env{*this, withFixAndWithoutV2};
2837 env.fund(XRP(100'000), alice, bob, gw);
2838 env.close();
2839 auto const issue = makeIssue(env);
2840
2841 auto const badAmount = badMPTAmount(issue, bad);
2842 auto tx = withNonCanonicalMPTAmount(
2843 env.jt(
2844 AMM::withdrawJv(
2845 {.account = alice,
2846 .asset1Out = STAmount{issue, std::uint64_t{1}},
2847 .assets = std::make_pair(Asset{issue}, Asset{xrpIssue()})}),
2848 Fee(static_cast<std::uint64_t>(env.current()->fees().increment.drops()))),
2849 sfAmount,
2850 badAmount,
2851 alice);
2852 tx.ter = temDISABLED;
2853 env.submit(tx);
2854 }
2855 {
2856 // sfAmount is MPT: both amendments active, expect temBAD_AMOUNT
2857 Env env{*this, withFix};
2858 env.fund(XRP(100'000), alice, bob, gw);
2859 env.close();
2860 auto const issue = makeIssue(env);
2861
2862 auto const badAmount = badMPTAmount(issue, bad);
2863 auto tx = withNonCanonicalMPTAmount(
2864 env.jt(
2865 AMM::withdrawJv(
2866 {.account = alice,
2867 .asset1Out = STAmount{issue, std::uint64_t{1}},
2868 .assets = std::make_pair(Asset{issue}, Asset{xrpIssue()})}),
2869 Fee(static_cast<std::uint64_t>(env.current()->fees().increment.drops()))),
2870 sfAmount,
2871 badAmount,
2872 alice);
2873 tx.ter = temBAD_AMOUNT;
2874 env.submit(tx);
2875 }
2876
2877 testcase("featureMPTokensV2 disabled rejects MPT AMMClawback amounts");
2878 {
2879 Env env{*this, withoutFixAndV2};
2880 env.fund(XRP(100'000), alice, bob, gw);
2881 env.close();
2882 auto const issue = makeIssue(env);
2883
2884 auto const badAmount = badMPTAmount(issue, bad);
2885 auto tx = withNonCanonicalMPTAmount(
2886 env.jt(
2887 amm::ammClawback(
2888 gw,
2889 alice,
2890 Asset{issue},
2891 Asset{xrpIssue()},
2892 std::make_optional(STAmount{issue, std::uint64_t{1}})),
2893 Fee(static_cast<std::uint64_t>(env.current()->fees().increment.drops()))),
2894 sfAmount,
2895 badAmount,
2896 gw);
2897 expectRoundTripBadMPT(tx, sfAmount, bad);
2898 tx.ter = temDISABLED;
2899 env.submit(tx);
2900 }
2901 {
2902 Env env{*this, withFixAndWithoutV2};
2903 env.fund(XRP(100'000), alice, bob, gw);
2904 env.close();
2905 auto const issue = makeIssue(env);
2906
2907 auto const badAmount = badMPTAmount(issue, bad);
2908 auto tx = withNonCanonicalMPTAmount(
2909 env.jt(
2910 amm::ammClawback(
2911 gw,
2912 alice,
2913 Asset{issue},
2914 Asset{xrpIssue()},
2915 std::make_optional(STAmount{issue, std::uint64_t{1}})),
2916 Fee(static_cast<std::uint64_t>(env.current()->fees().increment.drops()))),
2917 sfAmount,
2918 badAmount,
2919 gw);
2920 tx.ter = temDISABLED;
2921 env.submit(tx);
2922 }
2923 {
2924 // sfAmount is MPT: both amendments active, expect temBAD_AMOUNT
2925 Env env{*this, withFix};
2926 env.fund(XRP(100'000), alice, bob, gw);
2927 env.close();
2928 auto const issue = makeIssue(env);
2929
2930 auto const badAmount = badMPTAmount(issue, bad);
2931 auto tx = withNonCanonicalMPTAmount(
2932 env.jt(
2933 amm::ammClawback(
2934 gw,
2935 alice,
2936 Asset{issue},
2937 Asset{xrpIssue()},
2938 std::make_optional(STAmount{issue, std::uint64_t{1}})),
2939 Fee(static_cast<std::uint64_t>(env.current()->fees().increment.drops()))),
2940 sfAmount,
2941 badAmount,
2942 gw);
2943 tx.ter = temBAD_AMOUNT;
2944 env.submit(tx);
2945 }
2946
2947 testcase("fixCleanup3_2_0 rejects non-canonical MPT VaultClawback amounts");
2948 {
2949 Env env{*this, withFix};
2950 env.fund(XRP(100'000), alice, bob, gw);
2951 env.close();
2952 auto const issue = makeIssue(env);
2953
2954 auto const badAmount = badMPTAmount(issue, bad);
2955 uint256 const fakeVaultId = keylet::vault(gw.id(), 1).key;
2956 auto tx = withNonCanonicalMPTAmount(
2957 env.jt(
2958 Vault::clawback(
2959 {.issuer = gw,
2960 .id = fakeVaultId,
2961 .holder = alice,
2962 .amount = STAmount{issue, std::uint64_t{1}}}),
2963 Fee(static_cast<std::uint64_t>(env.current()->fees().increment.drops()))),
2964 sfAmount,
2965 badAmount,
2966 gw);
2967 tx.ter = temBAD_AMOUNT;
2968 env.submit(tx);
2969 }
2970 }
2971 }
2972
2973 void
2975 {
2976 // checks synthetically injected mptissuanceid from `tx` response
2977 testcase("Test synthetic fields from tx response");
2978
2979 using namespace test::jtx;
2980
2981 Account const alice{"alice"};
2982
2983 auto cfg = envconfig();
2984 cfg->fees.referenceFee = 10;
2985 Env env{*this, std::move(cfg), features};
2986 MPTTester mptAlice(env, alice);
2987
2988 mptAlice.create();
2989
2990 std::string const txHash{
2991 env.tx()->getJson(JsonOptions::Values::None)[jss::hash].asString()};
2992 BEAST_EXPECTS(
2993 txHash ==
2994 "E11F0E0CA14219922B7881F060B9CEE67CFBC87E4049A441ED2AE348FF8FAC"
2995 "0E",
2996 txHash);
2997 json::Value const meta = env.rpc("tx", txHash)[jss::result][jss::meta];
2998 auto const id = meta[jss::mpt_issuance_id].asString();
2999 // Expect mpt_issuance_id field
3000 BEAST_EXPECT(meta.isMember(jss::mpt_issuance_id));
3001 BEAST_EXPECT(id == to_string(mptAlice.issuanceID()));
3002 BEAST_EXPECTS(id == "00000004AE123A8556F3CF91154711376AFB0F894F832B3D", id);
3003 }
3004
3005 void
3007 {
3008 testcase("MPT clawback validations");
3009 using namespace test::jtx;
3010
3011 // Make sure clawback cannot work when featureMPTokensV1 is disabled
3012 {
3013 Env env(*this, features - featureMPTokensV1);
3014 Account const alice{"alice"};
3015 Account const bob{"bob"};
3016
3017 env.fund(XRP(1000), alice, bob);
3018 env.close();
3019
3020 auto const usd = alice["USD"];
3021 auto const mpt = xrpl::test::jtx::MPT(alice.name(), makeMptID(env.seq(alice), alice));
3022
3023 env(claw(alice, bob["USD"](5), bob), Ter(temMALFORMED));
3024 env.close();
3025
3026 env(claw(alice, mpt(5)), Ter(temDISABLED));
3027 env.close();
3028
3029 env(claw(alice, mpt(5), bob), Ter(temDISABLED));
3030 env.close();
3031 }
3032
3033 // Test preflight
3034 {
3035 Env env(*this, features);
3036 Account const alice{"alice"};
3037 Account const bob{"bob"};
3038
3039 env.fund(XRP(1000), alice, bob);
3040 env.close();
3041
3042 auto const usd = alice["USD"];
3043 auto const mpt = xrpl::test::jtx::MPT(alice.name(), makeMptID(env.seq(alice), alice));
3044
3045 // clawing back IOU from a MPT holder fails
3046 env(claw(alice, bob["USD"](5), bob), Ter(temMALFORMED));
3047 env.close();
3048
3049 // clawing back MPT without specifying a holder fails
3050 env(claw(alice, mpt(5)), Ter(temMALFORMED));
3051 env.close();
3052
3053 // clawing back zero amount fails
3054 env(claw(alice, mpt(0), bob), Ter(temBAD_AMOUNT));
3055 env.close();
3056
3057 // alice can't claw back from herself
3058 env(claw(alice, mpt(5), alice), Ter(temMALFORMED));
3059 env.close();
3060
3061 // can't clawback negative amount
3062 env(claw(alice, mpt(-1), bob), Ter(temBAD_AMOUNT));
3063 env.close();
3064 }
3065
3066 // Preclaim - clawback fails when MPTCanClawback is disabled on issuance
3067 {
3068 Env env(*this, features);
3069 Account const alice{"alice"};
3070 Account const bob{"bob"};
3071
3072 MPTTester mptAlice(env, alice, {.holders = {bob}});
3073
3074 // enable asfAllowTrustLineClawback for alice
3075 env(fset(alice, asfAllowTrustLineClawback));
3076 env.close();
3077 env.require(Flags(alice, asfAllowTrustLineClawback));
3078
3079 // Create issuance without enabling clawback
3080 mptAlice.create({.ownerCount = 1, .holderCount = 0});
3081
3082 mptAlice.authorize({.account = bob});
3083
3084 mptAlice.pay(alice, bob, 100);
3085
3086 // alice cannot clawback before she didn't enable MPTCanClawback
3087 // asfAllowTrustLineClawback has no effect
3088 mptAlice.claw(alice, bob, 1, tecNO_PERMISSION);
3089 }
3090
3091 // Preclaim - test various scenarios
3092 {
3093 Env env(*this, features);
3094 Account const alice{"alice"};
3095 Account const bob{"bob"};
3096 Account const carol{"carol"};
3097 env.fund(XRP(1000), carol);
3098 env.close();
3099 MPTTester mptAlice(env, alice, {.holders = {bob}});
3100
3101 auto const fakeMpt =
3102 xrpl::test::jtx::MPT(alice.name(), makeMptID(env.seq(alice), alice));
3103
3104 // issuer tries to clawback MPT where issuance doesn't exist
3105 env(claw(alice, fakeMpt(5), bob), Ter(tecOBJECT_NOT_FOUND));
3106 env.close();
3107
3108 // alice creates issuance
3109 mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanClawback});
3110
3111 // alice tries to clawback from someone who doesn't have MPToken
3112 mptAlice.claw(alice, bob, 1, tecOBJECT_NOT_FOUND);
3113
3114 // bob creates a MPToken
3115 mptAlice.authorize({.account = bob});
3116
3117 // clawback fails because bob currently has a balance of zero
3118 mptAlice.claw(alice, bob, 1, tecINSUFFICIENT_FUNDS);
3119
3120 // alice pays bob 100 tokens
3121 mptAlice.pay(alice, bob, 100);
3122
3123 // carol fails tries to clawback from bob because he is not the
3124 // issuer
3125 mptAlice.claw(carol, bob, 1, tecNO_PERMISSION);
3126 }
3127
3128 // clawback more than max amount
3129 // fails in the json parser before
3130 // transactor is called
3131 {
3132 Env env(*this, features);
3133 Account const alice{"alice"};
3134 Account const bob{"bob"};
3135
3136 env.fund(XRP(1000), alice, bob);
3137 env.close();
3138
3139 auto const mpt = xrpl::test::jtx::MPT(alice.name(), makeMptID(env.seq(alice), alice));
3140
3141 json::Value jv = claw(alice, mpt(1), bob);
3142 jv[jss::Amount][jss::value] = std::to_string(kMaxMpTokenAmount + 1);
3143 json::Value jv1;
3144 jv1[jss::secret] = alice.name();
3145 jv1[jss::tx_json] = jv;
3146 auto const jrr = env.rpc("json", "submit", to_string(jv1));
3147 BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");
3148 }
3149 }
3150
3151 void
3153 {
3154 testcase("MPT Clawback");
3155 using namespace test::jtx;
3156
3157 {
3158 Env env(*this, features);
3159 Account const alice{"alice"};
3160 Account const bob{"bob"};
3161
3162 MPTTester mptAlice(env, alice, {.holders = {bob}});
3163
3164 // alice creates issuance
3165 mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanClawback});
3166
3167 // bob creates a MPToken
3168 mptAlice.authorize({.account = bob});
3169
3170 // alice pays bob 100 tokens
3171 mptAlice.pay(alice, bob, 100);
3172
3173 mptAlice.claw(alice, bob, 1);
3174
3175 mptAlice.claw(alice, bob, 1000);
3176
3177 // clawback fails because bob currently has a balance of zero
3178 mptAlice.claw(alice, bob, 1, tecINSUFFICIENT_FUNDS);
3179 }
3180
3181 // Test that globally locked funds can be clawed
3182 {
3183 Env env(*this, features);
3184 Account const alice{"alice"};
3185 Account const bob{"bob"};
3186
3187 MPTTester mptAlice(env, alice, {.holders = {bob}});
3188
3189 // alice creates issuance
3190 mptAlice.create(
3191 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanLock | tfMPTCanClawback});
3192
3193 // bob creates a MPToken
3194 mptAlice.authorize({.account = bob});
3195
3196 // alice pays bob 100 tokens
3197 mptAlice.pay(alice, bob, 100);
3198
3199 mptAlice.set({.account = alice, .flags = tfMPTLock});
3200
3201 mptAlice.claw(alice, bob, 100);
3202 }
3203
3204 // Test that individually locked funds can be clawed
3205 {
3206 Env env(*this, features);
3207 Account const alice{"alice"};
3208 Account const bob{"bob"};
3209
3210 MPTTester mptAlice(env, alice, {.holders = {bob}});
3211
3212 // alice creates issuance
3213 mptAlice.create(
3214 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanLock | tfMPTCanClawback});
3215
3216 // bob creates a MPToken
3217 mptAlice.authorize({.account = bob});
3218
3219 // alice pays bob 100 tokens
3220 mptAlice.pay(alice, bob, 100);
3221
3222 mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
3223
3224 mptAlice.claw(alice, bob, 100);
3225 }
3226
3227 // Test that unauthorized funds can be clawed back
3228 {
3229 Env env(*this, features);
3230 Account const alice{"alice"};
3231 Account const bob{"bob"};
3232
3233 MPTTester mptAlice(env, alice, {.holders = {bob}});
3234
3235 // alice creates issuance
3236 mptAlice.create(
3237 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanClawback | tfMPTRequireAuth});
3238
3239 // bob creates a MPToken
3240 mptAlice.authorize({.account = bob});
3241
3242 // alice authorizes bob
3243 mptAlice.authorize({.account = alice, .holder = bob});
3244
3245 // alice pays bob 100 tokens
3246 mptAlice.pay(alice, bob, 100);
3247
3248 // alice unauthorizes bob
3249 mptAlice.authorize({.account = alice, .holder = bob, .flags = tfMPTUnauthorize});
3250
3251 mptAlice.claw(alice, bob, 100);
3252 }
3253 }
3254
3255 void
3257 {
3258 using namespace test::jtx;
3259 testcase("Tokens Equality");
3260 Currency const cur1{toCurrency("CU1")};
3261 Currency const cur2{toCurrency("CU2")};
3262 Account const gw1{"gw1"};
3263 Account const gw2{"gw2"};
3264 MPTID const mpt1 = makeMptID(1, gw1);
3265 MPTID const mpt1a = makeMptID(1, gw1);
3266 MPTID const mpt2 = makeMptID(1, gw2);
3267 MPTID const mpt3 = makeMptID(2, gw2);
3268 Asset const assetCur1Gw1{Issue{cur1, gw1}};
3269 Asset const assetCur1Gw1a{Issue{cur1, gw1}};
3270 Asset const assetCur2Gw1{Issue{cur2, gw1}};
3271 Asset const assetCur2Gw2{Issue{cur2, gw2}};
3272 Asset const assetMpt1Gw1{mpt1};
3273 Asset const assetMpt1Gw1a{mpt1a};
3274 Asset const assetMpt1Gw2{mpt2};
3275 Asset const assetMpt2Gw2{mpt3};
3276
3277 // Assets holding Issue
3278 // Currencies are equal regardless of the issuer
3279 BEAST_EXPECT(equalTokens(assetCur1Gw1, assetCur1Gw1a));
3280 BEAST_EXPECT(equalTokens(assetCur2Gw1, assetCur2Gw2));
3281 // Currencies are different regardless of whether the issuers
3282 // are the same or not
3283 BEAST_EXPECT(!equalTokens(assetCur1Gw1, assetCur2Gw1));
3284 BEAST_EXPECT(!equalTokens(assetCur1Gw1, assetCur2Gw2));
3285
3286 // Assets holding MPTIssue
3287 // MPTIDs are the same if the sequence and the issuer are the same
3288 BEAST_EXPECT(equalTokens(assetMpt1Gw1, assetMpt1Gw1a));
3289 // MPTIDs are different if sequence and the issuer don't match
3290 BEAST_EXPECT(!equalTokens(assetMpt1Gw1, assetMpt1Gw2));
3291 BEAST_EXPECT(!equalTokens(assetMpt1Gw2, assetMpt2Gw2));
3292
3293 // Assets holding Issue and MPTIssue
3294 BEAST_EXPECT(!equalTokens(assetCur1Gw1, assetMpt1Gw1));
3295 BEAST_EXPECT(!equalTokens(assetMpt2Gw2, assetCur2Gw2));
3296 }
3297
3298 void
3300 {
3301 using namespace test::jtx;
3302 Account const gw{"gw"};
3303 Asset const asset1{makeMptID(1, gw)};
3304 Asset const asset2{makeMptID(2, gw)};
3305 Asset const asset3{makeMptID(3, gw)};
3306 STAmount const amt1{asset1, 100};
3307 STAmount const amt2{asset2, 100};
3308 STAmount const amt3{asset3, 10'000};
3309
3310 {
3311 testcase("Test STAmount MPT arithmetic");
3312 using namespace std::string_literals;
3313 STAmount res = multiply(amt1, amt2, asset3);
3314 BEAST_EXPECT(res == amt3);
3315
3316 res = mulRound(amt1, amt2, asset3, true);
3317 BEAST_EXPECT(res == amt3);
3318
3319 res = mulRoundStrict(amt1, amt2, asset3, true);
3320 BEAST_EXPECT(res == amt3);
3321
3322 // overflow, any value > 3037000499ull
3323 STAmount mptOverflow{asset2, UINT64_C(3037000500)};
3324 try
3325 {
3326 res = multiply(mptOverflow, mptOverflow, asset3);
3327 fail("should throw runtime exception 1");
3328 }
3329 catch (std::runtime_error const& e)
3330 {
3331 BEAST_EXPECTS(e.what() == "MPT value overflow"s, e.what());
3332 }
3333 // overflow, (v1 >> 32) * v2 > 2147483648ull
3334 mptOverflow = STAmount{asset2, UINT64_C(2147483648)};
3335 uint64_t const mantissa = (2ull << 32) + 2;
3336 try
3337 {
3338 res = multiply(STAmount{asset1, mantissa}, mptOverflow, asset3);
3339 fail("should throw runtime exception 2");
3340 }
3341 catch (std::runtime_error const& e)
3342 {
3343 BEAST_EXPECTS(e.what() == "MPT value overflow"s, e.what());
3344 }
3345 }
3346
3347 {
3348 testcase("Test MPTAmount arithmetic");
3349 MPTAmount mptAmt1{100};
3350 MPTAmount const mptAmt2{100};
3351 BEAST_EXPECT((mptAmt1 += mptAmt2) == MPTAmount{200});
3352 BEAST_EXPECT(mptAmt1 == 200);
3353 BEAST_EXPECT((mptAmt1 -= mptAmt2) == mptAmt1);
3354 BEAST_EXPECT(mptAmt1 == mptAmt2);
3355 BEAST_EXPECT(mptAmt1 == 100);
3356 BEAST_EXPECT(MPTAmount::minPositiveAmount() == MPTAmount{1});
3357 }
3358
3359 {
3360 testcase("Test MPTIssue from/to Json");
3361 MPTIssue const issue1{asset1.get<MPTIssue>()};
3362 json::Value const jv = toJson(issue1);
3363 BEAST_EXPECT(jv[jss::mpt_issuance_id] == to_string(asset1.get<MPTIssue>()));
3364 BEAST_EXPECT(issue1 == mptIssueFromJson(jv));
3365 }
3366
3367 {
3368 testcase("Test Asset from/to Json");
3369 json::Value const jv = toJson(asset1);
3370 BEAST_EXPECT(jv[jss::mpt_issuance_id] == to_string(asset1.get<MPTIssue>()));
3371 BEAST_EXPECT(
3372 to_string(jv) ==
3373 "{\"mpt_issuance_id\":"
3374 "\"00000001A407AF5856CCF3C42619DAA925813FC955C72983\"}");
3375 BEAST_EXPECT(asset1 == assetFromJson(jv));
3376 }
3377 }
3378
3379 void
3381 {
3382 testcase("invalid MPTokenIssuanceCreate for DynamicMPT");
3383
3384 using namespace test::jtx;
3385 Account const alice("alice");
3386
3387 // Can not provide MutableFlags when DynamicMPT amendment is not enabled
3388 {
3389 Env env{*this, features - featureDynamicMPT};
3390 MPTTester mptAlice(env, alice);
3391 mptAlice.create({.ownerCount = 0, .mutableFlags = 2, .err = temDISABLED});
3392 mptAlice.create({.ownerCount = 0, .mutableFlags = 0, .err = temDISABLED});
3393 }
3394
3395 // MutableFlags contains invalid values
3396 {
3397 Env env{*this, features};
3398 MPTTester mptAlice(env, alice);
3399
3400 // Value 1 is reserved for MPT lock.
3401 mptAlice.create({.ownerCount = 0, .mutableFlags = 1, .err = temINVALID_FLAG});
3402 mptAlice.create({.ownerCount = 0, .mutableFlags = 17, .err = temINVALID_FLAG});
3403 mptAlice.create({.ownerCount = 0, .mutableFlags = 65535, .err = temINVALID_FLAG});
3404
3405 // MutableFlags can not be 0
3406 mptAlice.create({.ownerCount = 0, .mutableFlags = 0, .err = temINVALID_FLAG});
3407 }
3408 }
3409
3410 void
3412 {
3413 testcase("invalid MPTokenIssuanceSet for DynamicMPT");
3414
3415 using namespace test::jtx;
3416 Account const alice("alice");
3417 Account const bob("bob");
3418
3419 // Can not provide MutableFlags, MPTokenMetadata or TransferFee when
3420 // DynamicMPT amendment is not enabled
3421 {
3422 Env env{*this, features - featureDynamicMPT};
3423 MPTTester mptAlice(env, alice, {.holders = {bob}});
3424 auto const mptID = makeMptID(env.seq(alice), alice);
3425
3426 // MutableFlags is not allowed when DynamicMPT is not enabled
3427 mptAlice.set({.account = alice, .id = mptID, .mutableFlags = 2, .err = temDISABLED});
3428 mptAlice.set({.account = alice, .id = mptID, .mutableFlags = 0, .err = temDISABLED});
3429
3430 // MPTokenMetadata is not allowed when DynamicMPT is not enabled
3431 mptAlice.set({.account = alice, .id = mptID, .metadata = "test", .err = temDISABLED});
3432 mptAlice.set({.account = alice, .id = mptID, .metadata = "", .err = temDISABLED});
3433
3434 // TransferFee is not allowed when DynamicMPT is not enabled
3435 mptAlice.set({.account = alice, .id = mptID, .transferFee = 100, .err = temDISABLED});
3436 mptAlice.set({.account = alice, .id = mptID, .transferFee = 0, .err = temDISABLED});
3437 }
3438
3439 // Can not provide holder when MutableFlags, MPTokenMetadata or
3440 // TransferFee is present
3441 {
3442 Env env{*this, features};
3443 MPTTester mptAlice(env, alice, {.holders = {bob}});
3444 auto const mptID = makeMptID(env.seq(alice), alice);
3445
3446 // Holder is not allowed when MutableFlags is present
3447 mptAlice.set(
3448 {.account = alice,
3449 .holder = bob,
3450 .id = mptID,
3451 .mutableFlags = 2,
3452 .err = temMALFORMED});
3453
3454 // Holder is not allowed when MPTokenMetadata is present
3455 mptAlice.set(
3456 {.account = alice,
3457 .holder = bob,
3458 .id = mptID,
3459 .metadata = "test",
3460 .err = temMALFORMED});
3461
3462 // Holder is not allowed when TransferFee is present
3463 mptAlice.set(
3464 {.account = alice,
3465 .holder = bob,
3466 .id = mptID,
3467 .transferFee = 100,
3468 .err = temMALFORMED});
3469 }
3470
3471 // Can not set Flags when MutableFlags, MPTokenMetadata or
3472 // TransferFee is present
3473 {
3474 Env env{*this, features};
3475 MPTTester mptAlice(env, alice, {.holders = {bob}});
3476 mptAlice.create(
3477 {.ownerCount = 1,
3480
3481 // Setting flags is not allowed when MutableFlags is present
3482 mptAlice.set(
3483 {.account = alice, .flags = tfMPTCanLock, .mutableFlags = 2, .err = temMALFORMED});
3484
3485 // Setting flags is not allowed when MPTokenMetadata is present
3486 mptAlice.set(
3487 {.account = alice, .flags = tfMPTCanLock, .metadata = "test", .err = temMALFORMED});
3488
3489 // setting flags is not allowed when TransferFee is present
3490 mptAlice.set(
3491 {.account = alice, .flags = tfMPTCanLock, .transferFee = 100, .err = temMALFORMED});
3492 }
3493
3494 // Flags being 0 or tfFullyCanonicalSig is fine
3495 {
3496 Env env{*this, features};
3497 MPTTester mptAlice(env, alice, {.holders = {bob}});
3498
3499 mptAlice.create(
3500 {.transferFee = 10,
3501 .ownerCount = 1,
3502 .flags = tfMPTCanTransfer,
3504
3505 mptAlice.set({.account = alice, .flags = 0, .transferFee = 100, .metadata = "test"});
3506 mptAlice.set(
3507 {.account = alice,
3508 .flags = tfFullyCanonicalSig,
3509 .transferFee = 200,
3510 .metadata = "test2"});
3511 }
3512
3513 // Invalid MutableFlags
3514 {
3515 Env env{*this, features};
3516 MPTTester mptAlice(env, alice, {.holders = {bob}});
3517 auto const mptID = makeMptID(env.seq(alice), alice);
3518
3519 for (auto const flags : {10000, 0, 5000})
3520 {
3521 mptAlice.set(
3522 {.account = alice, .id = mptID, .mutableFlags = flags, .err = temINVALID_FLAG});
3523 }
3524 }
3525
3526 // Can not mutate flag which is not mutable
3527 {
3528 Env env{*this, features};
3529 MPTTester mptAlice(env, alice, {.holders = {bob}});
3530
3531 mptAlice.create({.ownerCount = 1});
3532
3533 auto const mutableFlags = {
3540
3541 for (auto const& mutableFlag : mutableFlags)
3542 {
3543 mptAlice.set(
3544 {.account = alice, .mutableFlags = mutableFlag, .err = tecNO_PERMISSION});
3545 }
3546 }
3547
3548 // Metadata exceeding max length
3549 {
3550 Env env{*this, features};
3551 MPTTester mptAlice(env, alice, {.holders = {bob}});
3552
3553 mptAlice.create({.ownerCount = 1, .mutableFlags = tmfMPTCanMutateMetadata});
3554
3555 std::string const metadata(kMaxMpTokenMetadataLength + 1, 'a');
3556 mptAlice.set({.account = alice, .metadata = metadata, .err = temMALFORMED});
3557 }
3558
3559 // Can not mutate metadata when it is not mutable
3560 {
3561 Env env{*this, features};
3562 MPTTester mptAlice(env, alice, {.holders = {bob}});
3563
3564 mptAlice.create({.ownerCount = 1});
3565 mptAlice.set({.account = alice, .metadata = "test", .err = tecNO_PERMISSION});
3566 }
3567
3568 // Transfer fee exceeding the max value
3569 {
3570 Env env{*this, features};
3571 MPTTester mptAlice(env, alice, {.holders = {bob}});
3572 auto const mptID = makeMptID(env.seq(alice), alice);
3573
3574 mptAlice.create({.ownerCount = 1, .mutableFlags = tmfMPTCanMutateTransferFee});
3575
3576 mptAlice.set(
3577 {.account = alice,
3578 .id = mptID,
3579 .transferFee = kMaxTransferFee + 1,
3580 .err = temBAD_TRANSFER_FEE});
3581 }
3582
3583 // Can not set non-zero transfer fee when MPTCanTransfer is not set
3584 {
3585 Env env{*this, features};
3586 MPTTester mptAlice(env, alice, {.holders = {bob}});
3587
3588 mptAlice.create(
3589 {.ownerCount = 1,
3591
3592 mptAlice.set({.account = alice, .transferFee = 100, .err = tecNO_PERMISSION});
3593
3594 // Can not set transfer fee even when trying to set MPTCanTransfer
3595 // at the same time. MPTCanTransfer must be set first, then transfer
3596 // fee can be set in a separate transaction.
3597 mptAlice.set(
3598 {.account = alice,
3599 .mutableFlags = tmfMPTSetCanTransfer,
3600 .transferFee = 100,
3601 .err = tecNO_PERMISSION});
3602 }
3603
3604 // Can not mutate transfer fee when it is not mutable
3605 {
3606 Env env{*this, features};
3607 MPTTester mptAlice(env, alice, {.holders = {bob}});
3608
3609 mptAlice.create({.transferFee = 10, .ownerCount = 1, .flags = tfMPTCanTransfer});
3610
3611 mptAlice.set({.account = alice, .transferFee = 100, .err = tecNO_PERMISSION});
3612
3613 mptAlice.set({.account = alice, .transferFee = 0, .err = tecNO_PERMISSION});
3614 }
3615
3616 // Set some flags mutable. Can not mutate the others
3617 {
3618 Env env{*this, features};
3619 MPTTester mptAlice(env, alice, {.holders = {bob}});
3620
3621 mptAlice.create(
3622 {.ownerCount = 1,
3625
3626 // Can not mutate transfer fee
3627 mptAlice.set({.account = alice, .transferFee = 100, .err = tecNO_PERMISSION});
3628
3629 auto const invalidFlags = {
3631
3632 // Can not mutate flags which are not mutable
3633 for (auto const& mutableFlag : invalidFlags)
3634 {
3635 mptAlice.set(
3636 {.account = alice, .mutableFlags = mutableFlag, .err = tecNO_PERMISSION});
3637 }
3638
3639 // Can mutate MPTCanTrade
3640 mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanTrade});
3641
3642 // Can mutate MPTCanTransfer
3643 mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanTransfer});
3644
3645 // Can mutate metadata
3646 mptAlice.set({.account = alice, .metadata = "test"});
3647 mptAlice.set({.account = alice, .metadata = ""});
3648 }
3649 }
3650
3651 void
3653 {
3654 testcase("Mutate MPT");
3655 using namespace test::jtx;
3656
3657 Account const alice("alice");
3658
3659 // Mutate metadata
3660 {
3661 Env env{*this, features};
3662 MPTTester mptAlice(env, alice);
3663 mptAlice.create(
3664 {.metadata = "test", .ownerCount = 1, .mutableFlags = tmfMPTCanMutateMetadata});
3665
3666 std::vector<std::string> const metadatas = {
3667 "mutate metadata",
3668 "mutate metadata 2",
3669 "mutate metadata 3",
3670 "mutate metadata 3",
3671 "test",
3672 "mutate metadata"};
3673
3674 for (auto const& metadata : metadatas)
3675 {
3676 mptAlice.set({.account = alice, .metadata = metadata});
3677 BEAST_EXPECT(mptAlice.checkMetadata(metadata));
3678 }
3679
3680 // Metadata being empty will remove the field
3681 mptAlice.set({.account = alice, .metadata = ""});
3682 BEAST_EXPECT(!mptAlice.isMetadataPresent());
3683 }
3684
3685 // Mutate transfer fee
3686 {
3687 Env env{*this, features};
3688 MPTTester mptAlice(env, alice);
3689 mptAlice.create(
3690 {.transferFee = 100,
3691 .metadata = "test",
3692 .ownerCount = 1,
3693 .flags = tfMPTCanTransfer,
3694 .mutableFlags = tmfMPTCanMutateTransferFee});
3695
3696 for (std::uint16_t const fee :
3697 std::initializer_list<std::uint16_t>{1, 10, 100, 200, 500, 1000, kMaxTransferFee})
3698 {
3699 mptAlice.set({.account = alice, .transferFee = fee});
3700 BEAST_EXPECT(mptAlice.checkTransferFee(fee));
3701 }
3702
3703 // Setting TransferFee to zero will remove the field
3704 mptAlice.set({.account = alice, .transferFee = 0});
3705 BEAST_EXPECT(!mptAlice.isTransferFeePresent());
3706
3707 // Set transfer fee again
3708 mptAlice.set({.account = alice, .transferFee = 10});
3709 BEAST_EXPECT(mptAlice.checkTransferFee(10));
3710 }
3711
3712 // Test mutable flag enablement
3713 {
3714 auto testFlagSet = [&](std::uint32_t createFlags, std::uint32_t setFlags) {
3715 Env env{*this, features};
3716 MPTTester mptAlice(env, alice);
3717
3718 // Create the MPT object with the specified initial flags
3719 mptAlice.create({.metadata = "test", .ownerCount = 1, .mutableFlags = createFlags});
3720
3721 // Setting the same mutable capability more than once is harmless.
3722 mptAlice.set({.account = alice, .mutableFlags = setFlags});
3723 mptAlice.set({.account = alice, .mutableFlags = setFlags});
3724 };
3725
3732 }
3733 }
3734
3735 void
3737 {
3738 testcase("Mutate MPTCanLock");
3739 using namespace test::jtx;
3740
3741 Account const alice("alice");
3742 Account const bob("bob");
3743
3744 // Individual lock
3745 {
3746 Env env{*this, features};
3747 MPTTester mptAlice(env, alice, {.holders = {bob}});
3748 mptAlice.create(
3749 {.ownerCount = 1,
3750 .holderCount = 0,
3751 .flags = tfMPTCanLock | tfMPTCanTransfer,
3754 mptAlice.authorize({.account = bob, .holderCount = 1});
3755
3756 // Lock bob's mptoken
3757 mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
3758
3759 // Can mutate the mutable flags and fields
3760 mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanLock});
3761 mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanTrade});
3762 mptAlice.set({.account = alice, .transferFee = 200});
3763 }
3764
3765 // Global lock
3766 {
3767 Env env{*this, features};
3768 MPTTester mptAlice(env, alice, {.holders = {bob}});
3769 mptAlice.create(
3770 {.ownerCount = 1,
3771 .holderCount = 0,
3772 .flags = tfMPTCanLock,
3775 mptAlice.authorize({.account = bob, .holderCount = 1});
3776
3777 // Lock issuance
3778 mptAlice.set({.account = alice, .flags = tfMPTLock});
3779
3780 // Can mutate the mutable flags and fields
3781 mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanLock});
3782 mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanClawback});
3783 mptAlice.set({.account = alice, .metadata = "mutate"});
3784 }
3785
3786 // Test lock and unlock after enabling MPTCanLock
3787 {
3788 Env env{*this, features};
3789 MPTTester mptAlice(env, alice, {.holders = {bob}});
3790 mptAlice.create(
3791 {.ownerCount = 1,
3792 .holderCount = 0,
3795 mptAlice.authorize({.account = bob, .holderCount = 1});
3796
3797 // Can not lock or unlock before MPTCanLock is enabled
3798 mptAlice.set({.account = alice, .flags = tfMPTLock, .err = tecNO_PERMISSION});
3799 mptAlice.set({.account = alice, .flags = tfMPTUnlock, .err = tecNO_PERMISSION});
3800 mptAlice.set(
3801 {.account = alice, .holder = bob, .flags = tfMPTLock, .err = tecNO_PERMISSION});
3802 mptAlice.set(
3803 {.account = alice, .holder = bob, .flags = tfMPTUnlock, .err = tecNO_PERMISSION});
3804
3805 // Set MPTCanLock
3806 mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanLock});
3807
3808 // Can lock and unlock
3809 mptAlice.set({.account = alice, .flags = tfMPTLock});
3810 mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
3811 mptAlice.set({.account = alice, .flags = tfMPTUnlock});
3812 mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTUnlock});
3813 }
3814 }
3815
3816 void
3818 {
3819 testcase("Mutate MPTRequireAuth");
3820 using namespace test::jtx;
3821
3822 // test enabling RequireAuth flag on the issuance and its effect on payment
3823 // authorization
3824 Env env{*this, features};
3825 Account const alice("alice");
3826 Account const bob("bob");
3827
3828 MPTTester mptAlice(env, alice, {.holders = {bob}});
3829 mptAlice.create(
3830 {.ownerCount = 1,
3831 .flags = tfMPTCanTransfer,
3832 .mutableFlags = tmfMPTCanEnableRequireAuth});
3833
3834 mptAlice.authorize({.account = bob});
3835 mptAlice.pay(alice, bob, 1000);
3836
3837 // Set RequireAuth because it is mutable.
3838 mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetRequireAuth});
3839
3840 // This should fail because bob is not authorized yet.
3841 mptAlice.pay(alice, bob, 1000, tecNO_AUTH);
3842
3843 // Issuer authorizes bob and pay should succeed.
3844 mptAlice.authorize({.account = alice, .holder = bob});
3845 mptAlice.pay(alice, bob, 1000);
3846 }
3847
3848 void
3850 {
3851 testcase("Mutate MPTCanEscrow");
3852 using namespace test::jtx;
3853 using namespace std::literals;
3854
3855 Env env{*this, features};
3856 auto const baseFee = env.current()->fees().base;
3857 auto const alice = Account("alice");
3858 auto const bob = Account("bob");
3859 auto const carol = Account("carol");
3860
3861 MPTTester mptAlice(env, alice, {.holders = {carol, bob}});
3862 mptAlice.create(
3863 {.ownerCount = 1,
3864 .holderCount = 0,
3865 .flags = tfMPTCanTransfer,
3866 .mutableFlags = tmfMPTCanEnableCanEscrow});
3867 mptAlice.authorize({.account = carol});
3868 mptAlice.authorize({.account = bob});
3869
3870 auto const mpt = mptAlice["MPT"];
3871 env(pay(alice, carol, mpt(10'000)));
3872 env(pay(alice, bob, mpt(10'000)));
3873 env.close();
3874
3875 // MPTCanEscrow is not enabled
3876 env(escrow::create(carol, bob, mpt(3)),
3878 escrow::kFinishTime(env.now() + 1s),
3879 Fee(baseFee * 150),
3881
3882 // MPTCanEscrow is enabled now
3883 mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanEscrow});
3884 env(escrow::create(carol, bob, mpt(3)),
3886 escrow::kFinishTime(env.now() + 1s),
3887 Fee(baseFee * 150));
3888 }
3889
3890 void
3892 {
3893 testcase("Mutate MPTCanTransfer");
3894
3895 using namespace test::jtx;
3896 Account const alice("alice");
3897 Account const bob("bob");
3898 Account const carol("carol");
3899
3900 {
3901 Env env{*this, features};
3902
3903 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
3904 mptAlice.create(
3905 {.ownerCount = 1,
3907
3908 mptAlice.authorize({.account = bob});
3909 mptAlice.authorize({.account = carol});
3910
3911 // Pay to bob
3912 mptAlice.pay(alice, bob, 1000);
3913
3914 // Bob can not pay carol since MPTCanTransfer is not set
3915 mptAlice.pay(bob, carol, 50, tecNO_AUTH);
3916
3917 // Can not set non-zero transfer fee when MPTCanTransfer is not set
3918 mptAlice.set({.account = alice, .transferFee = 100, .err = tecNO_PERMISSION});
3919
3920 // Can not set non-zero transfer fee even when trying to set
3921 // MPTCanTransfer at the same time
3922 mptAlice.set(
3923 {.account = alice,
3924 .mutableFlags = tmfMPTSetCanTransfer,
3925 .transferFee = 100,
3926 .err = tecNO_PERMISSION});
3927
3928 // Alice sets MPTCanTransfer
3929 mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanTransfer});
3930
3931 // Can set transfer fee now
3932 BEAST_EXPECT(!mptAlice.isTransferFeePresent());
3933 mptAlice.set({.account = alice, .transferFee = 100});
3934 BEAST_EXPECT(mptAlice.isTransferFeePresent());
3935
3936 // Bob can pay carol
3937 MPT const mptc = mptAlice;
3938 if (!features[featureMPTokensV2])
3939 {
3940 mptAlice.pay(bob, carol, 50);
3941 BEAST_EXPECT(env.balance(carol, mptc) == mptc(50));
3942 }
3943 else
3944 {
3945 // The difference is due to the rounding in MPT/DEX.
3946 // 1 MPTC is the transfer fee paid by bob to the issuer.
3947 env(pay(bob, carol, mptAlice(50)), Txflags(tfPartialPayment));
3948 BEAST_EXPECT(env.balance(carol, mptc) == mptc(49));
3949 }
3950 }
3951
3952 // Can set transfer fee to zero when tmfMPTCanMutateTransferFee is set.
3953 {
3954 Env env{*this, features};
3955
3956 MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
3957 mptAlice.create(
3958 {.transferFee = 100,
3959 .ownerCount = 1,
3960 .flags = tfMPTCanTransfer,
3961 .mutableFlags = tmfMPTCanMutateTransferFee});
3962
3963 BEAST_EXPECT(mptAlice.checkTransferFee(100));
3964
3965 // Setting transfer fee to zero removes the field.
3966 mptAlice.set({.account = alice, .transferFee = 0});
3967 BEAST_EXPECT(!mptAlice.isTransferFeePresent());
3968 }
3969 }
3970
3971 void
3973 {
3974 testcase("Mutate MPTCanClawback");
3975
3976 using namespace test::jtx;
3977 Env env(*this, features);
3978 Account const alice{"alice"};
3979 Account const bob{"bob"};
3980
3981 MPTTester mptAlice(env, alice, {.holders = {bob}});
3982
3983 mptAlice.create(
3984 {.ownerCount = 1, .holderCount = 0, .mutableFlags = tmfMPTCanEnableCanClawback});
3985
3986 // Bob creates an MPToken
3987 mptAlice.authorize({.account = bob});
3988
3989 // Alice pays bob 100 tokens
3990 mptAlice.pay(alice, bob, 100);
3991
3992 // MPTCanClawback is not enabled
3993 mptAlice.claw(alice, bob, 1, tecNO_PERMISSION);
3994
3995 // Enable MPTCanClawback
3996 mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanClawback});
3997
3998 // Can clawback now
3999 mptAlice.claw(alice, bob, 1);
4000 }
4001
4002 void
4004 {
4005 // Verify that directSendNoLimitMultiMPT correctly enforces MaximumAmount
4006 // when the issuer sends to multiple receivers. Pre-fixCleanup3_1_3,
4007 // a stale view.read() snapshot caused per-iteration checks to miss
4008 // aggregate overflows. Post-fix, a running total is used instead.
4009 testcase("Multi-send MaximumAmount enforcement");
4010
4011 using namespace test::jtx;
4012
4013 Account const issuer("issuer");
4014 Account const alice("alice");
4015 Account const bob("bob");
4016
4017 static constexpr std::uint64_t kMaxAmt = 150;
4018 Env env{*this, features};
4019
4020 MPTTester mptTester(env, issuer, {.holders = {alice, bob}});
4021 mptTester.create({.maxAmt = kMaxAmt, .ownerCount = 1, .flags = tfMPTCanTransfer});
4022 mptTester.authorize({.account = alice});
4023 mptTester.authorize({.account = bob});
4024
4025 Asset const asset{MPTIssue{mptTester.issuanceID()}};
4026
4027 // Each test case creates a fresh ApplyView and calls
4028 // accountSendMulti from the issuer to the given receivers.
4029 auto const runTest = [&](MultiplePaymentDestinations const& receivers,
4030 TER expectedTer,
4031 std::optional<std::uint64_t> expectedOutstanding,
4032 std::string const& label) {
4033 ApplyViewImpl av(&*env.current(), TapNone);
4034 auto const ter =
4035 accountSendMulti(av, issuer.id(), asset, receivers, env.app().getJournal("View"));
4036 BEAST_EXPECTS(ter == expectedTer, label);
4037
4038 // Only verify OutstandingAmount on success — on error the
4039 // view may contain partial state and must be discarded.
4040 if (expectedOutstanding)
4041 {
4042 auto const sle = av.peek(keylet::mptokenIssuance(mptTester.issuanceID()));
4043 if (!BEAST_EXPECT(sle))
4044 return;
4045 BEAST_EXPECTS(sle->getFieldU64(sfOutstandingAmount) == *expectedOutstanding, label);
4046 }
4047 };
4048
4050
4051 // Post-amendment: aggregate check with running total
4052 runTest(
4053 R{{alice.id(), 100}, {bob.id(), 100}},
4055 std::nullopt,
4056 "aggregate exceeds max");
4057
4058 runTest(R{{alice.id(), 75}, {bob.id(), 75}}, tesSUCCESS, kMaxAmt, "aggregate at boundary");
4059
4060 runTest(R{{alice.id(), 50}, {bob.id(), 50}}, tesSUCCESS, 100, "aggregate within limit");
4061
4062 runTest(
4063 R{{alice.id(), 150}, {bob.id(), 0}},
4064 tesSUCCESS,
4065 kMaxAmt,
4066 "one receiver at max, other zero");
4067
4068 runTest(
4069 R{{alice.id(), 151}, {bob.id(), 0}},
4071 std::nullopt,
4072 "one receiver exceeds max, other zero");
4073
4074 // Issue 50 tokens so outstandingAmount is nonzero, then verify
4075 // the third condition: outstandingAmount > maximumAmount - sendAmount - totalSendAmount
4076 mptTester.pay(issuer, alice, 50);
4077 env.close();
4078
4079 // maxAmt=150, outstanding=50, so 100 more available
4080 runTest(
4081 R{{alice.id(), 50}, {bob.id(), 50}},
4082 tesSUCCESS,
4083 kMaxAmt,
4084 "nonzero outstanding, aggregate at boundary");
4085
4086 runTest(
4087 R{{alice.id(), 50}, {bob.id(), 51}},
4089 std::nullopt,
4090 "nonzero outstanding, aggregate exceeds max");
4091
4092 runTest(
4093 R{{alice.id(), 100}, {bob.id(), 0}},
4094 tesSUCCESS,
4095 kMaxAmt,
4096 "nonzero outstanding, single send at remaining capacity");
4097
4098 runTest(
4099 R{{alice.id(), 101}, {bob.id(), 0}},
4101 std::nullopt,
4102 "nonzero outstanding, single send exceeds remaining capacity");
4103
4104 // Pre-amendment: the stale per-iteration check allows each
4105 // individual send (100 <= 150) even though the aggregate (200)
4106 // exceeds MaximumAmount. Preserved for ledger replay.
4107 {
4108 // KNOWN BUG (pre-fixCleanup3_1_3): preserved for ledger replay only
4109 env.disableFeature(fixCleanup3_1_3);
4110 runTest(
4111 R{{alice.id(), 100}, {bob.id(), 100}},
4112 tesSUCCESS,
4113 250,
4114 "pre-amendment allows over-send");
4115 env.enableFeature(fixCleanup3_1_3);
4116 }
4117 }
4118
4119 void
4121 {
4122 testcase("Offer Crossing");
4123 using namespace test::jtx;
4124 Account const gw = Account("gw");
4125 Account const alice = Account("alice");
4126 Account const carol = Account("carol");
4127 auto const usd = gw["USD"];
4128
4129 // Blocking flags
4130 for (auto flags :
4131 {tfMPTCanTrade | tfMPTCanLock | tfMPTCanClawback, // global lock - holder, issuer fail
4132 tfMPTCanTrade | tfMPTRequireAuth, // not authorized - holder fails
4133 tfMPTCanTrade, // holder, issuer succeed
4134 tfMPTCanTrade | tfMPTCanLock, // local lock - holder fails
4135 tfMPTCanTransfer}) // can't trade - holder, issuer fail
4136 {
4137 Env env{*this, features};
4138 env.fund(XRP(1'000), gw, alice);
4139 env.close();
4140
4141 // Use CanClawback flag to distinguish global from local lock
4142 bool const lockMPToken = (flags & (tfMPTCanLock | tfMPTCanClawback)) == tfMPTCanLock;
4143 bool const lockMPTIssue =
4144 (flags & (tfMPTCanLock | tfMPTCanClawback)) == (tfMPTCanLock | tfMPTCanClawback);
4145 bool const requireAuth = (flags & tfMPTRequireAuth) != 0u;
4146
4147 auto mptTester = MPTTester(
4148 {.env = env,
4149 .issuer = gw,
4150 .holders = {alice},
4151 .pay = 1'000,
4152 .flags = flags,
4153 .authHolder = true});
4154 MPT const btc = mptTester;
4155
4156 if (requireAuth)
4157 mptTester.authorize({.account = gw, .holder = alice, .flags = tfMPTUnauthorize});
4158 if (lockMPToken)
4159 {
4160 mptTester.set({.holder = alice, .flags = tfMPTLock});
4161 }
4162 else if (lockMPTIssue)
4163 {
4164 mptTester.set({.flags = tfMPTLock});
4165 }
4166
4167 auto testOffer =
4168 [&](Account const& account, auto const& buy, auto const& sell, bool buyUSD) {
4169 auto error = [&](auto const err) -> TER {
4170 if (account == gw)
4171 return tesSUCCESS;
4172 return err;
4173 };
4174 auto const [errBuy, errSell] = [&]() -> std::pair<TER, TER> {
4175 // Global lock
4176 if (lockMPTIssue)
4178 // Local lock
4179 if (lockMPToken)
4180 return std::make_pair(error(tecLOCKED), error(tecUNFUNDED_OFFER));
4181 // MPToken doesn't exist
4182 if (requireAuth)
4183 return std::make_pair(error(tecNO_AUTH), error(tecUNFUNDED_OFFER));
4184 if (flags & tfMPTCanTransfer)
4187 }();
4188
4189 auto const err = buyUSD ? errBuy : errSell;
4190
4191 auto seq(env.seq(account));
4192 env(offer(account, buy(10), sell(10)), Ter(err));
4193 env(offerCancel(account, seq));
4194 env.close();
4195 };
4196
4197 auto testOffers = [&](Account const& account) {
4198 testOffer(account, XRP, btc, false);
4199 testOffer(account, btc, XRP, true);
4200 };
4201 testOffers(alice);
4202 testOffers(gw);
4203 }
4204
4205 // MPTokenV2 is disabled
4206 {
4207 Env env{*this, features - featureMPTokensV2};
4208
4209 MPTTester mptTester(env, gw, {.holders = {alice}});
4210
4211 mptTester.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});
4212
4213 mptTester.authorize({.account = alice});
4214 mptTester.pay(gw, alice, 200);
4215
4216 env(offer(alice, XRP(100), mptTester.mpt(101)), Ter(temDISABLED));
4217 env.close();
4218 }
4219
4220 // MPTokenIssuance object doesn't exist
4221 {
4222 Env env(*this);
4223 env.fund(XRP(1'000), gw, alice);
4224 env.close();
4225 MPT const btc = MPTTester({.env = env, .issuer = gw, .holders = {alice}, .pay = 100});
4226 MPT const eth = MPT(gw, 1);
4227
4228 env(offer(alice, eth(10), btc(10)), Ter(tecOBJECT_NOT_FOUND));
4229 env(offer(alice, btc(10), eth(10)), Ter(tecUNFUNDED_OFFER));
4230 }
4231
4232 // MPToken object doesn't exist and the account is not the issuer of MPT
4233 {
4234 Env env(*this);
4235 env.fund(XRP(1'000), gw, alice);
4236 MPTTester const btc({.env = env, .issuer = gw, .holders = {alice}, .pay = 100});
4237 MPTTester const eth({.env = env, .issuer = gw});
4238
4239 env(offer(alice, eth(10), btc(10)));
4240 env(offer(alice, btc(10), eth(10)), Ter(tecUNFUNDED_OFFER));
4241 }
4242
4243 // MPTLock flag is set: MPT/MPT offer crossing with independent issuers.
4244 // gw2 issues BTC and gw issues ETH so each asset can be frozen independently.
4245 // Passive setup: bob sells BTC (offer(bob, ETH, BTC)); dan sells ETH (offer(dan, BTC,
4246 // ETH)).
4247 {
4248 Account const gw2 = Account("gw2");
4249 Account const bob = Account("bob");
4250 Account const dan = Account("dan");
4251 Env env(*this);
4252 env.fund(XRP(1'000), gw, gw2, alice, carol, bob, dan);
4253 MPTTester btc(
4254 {.env = env,
4255 .issuer = gw2,
4256 .holders = {alice, carol, bob, dan, gw},
4257 .pay = 100,
4258 .flags = tfMPTCanLock | kMptDexFlags});
4259 MPTTester eth(
4260 {.env = env,
4261 .issuer = gw,
4262 .holders = {alice, carol, bob, dan, gw2},
4263 .pay = 100,
4264 .flags = tfMPTCanLock | kMptDexFlags});
4265
4266 // bob sells BTC (takerGets=BTC); dan sells ETH (takerGets=ETH)
4267 env(offer(bob, eth(10), btc(10)), Txflags(tfPassive));
4268 env(offer(dan, btc(10), eth(10)), Txflags(tfPassive));
4269 env.close();
4270
4271 // --- Individual lock on BTC ---
4272 // alice sells locked BTC (takerGets): balance zeroed, offer unfunded
4273 btc.set({.holder = alice, .flags = tfMPTLock});
4274 env(offer(alice, eth(1), btc(1)), Ter(tecUNFUNDED_OFFER));
4275 btc.set({.holder = alice, .flags = tfMPTUnlock});
4276 // carol buys locked BTC (takerPays): locked MPToken cannot receive
4277 btc.set({.holder = carol, .flags = tfMPTLock});
4278 env(offer(carol, btc(1), eth(1)), Ter(tecLOCKED));
4279 btc.set({.holder = carol, .flags = tfMPTUnlock});
4280 // gw2 is BTC issuer: individual lock on holders does not affect issuer
4281 env(offer(gw2, eth(1), btc(1)));
4282
4283 // --- Individual lock on ETH ---
4284 // alice sells locked ETH (takerGets): balance zeroed, offer unfunded
4285 eth.set({.holder = alice, .flags = tfMPTLock});
4286 env(offer(alice, btc(1), eth(1)), Ter(tecUNFUNDED_OFFER));
4287 eth.set({.holder = alice, .flags = tfMPTUnlock});
4288 // carol buys locked ETH (takerPays): locked MPToken cannot receive
4289 eth.set({.holder = carol, .flags = tfMPTLock});
4290 env(offer(carol, eth(1), btc(1)), Ter(tecLOCKED));
4291 eth.set({.holder = carol, .flags = tfMPTUnlock});
4292 // gw is ETH issuer: individual lock on holders does not affect issuer
4293 env(offer(gw, btc(1), eth(1)));
4294
4295 // --- Global lock on BTC ---
4296 // All accounts fail regardless of role: global lock is checked in OfferCreate
4297 // before offer crossing, so it applies even to the issuer.
4298 btc.set({.flags = tfMPTLock});
4299 env(offer(alice, eth(1), btc(1)), Ter(tecLOCKED)); // alice sells BTC
4300 env(offer(alice, btc(1), eth(1)), Ter(tecLOCKED)); // alice buys BTC
4301 env(offer(gw2, eth(1), btc(1)), Ter(tecLOCKED)); // gw2 is BTC issuer, still fails
4302 btc.set({.flags = tfMPTUnlock});
4303
4304 // --- Global lock on ETH ---
4305 eth.set({.flags = tfMPTLock});
4306 env(offer(alice, btc(1), eth(1)), Ter(tecLOCKED)); // alice sells ETH
4307 env(offer(alice, eth(1), btc(1)), Ter(tecLOCKED)); // alice buys ETH
4308 env(offer(gw, btc(1), eth(1)), Ter(tecLOCKED)); // gw is ETH issuer, still fails
4309 eth.set({.flags = tfMPTUnlock});
4310
4311 // --- After all locks cleared: normal crossing succeeds ---
4312 env(offer(alice, eth(1), btc(1)));
4313 env(offer(carol, btc(1), eth(1)));
4314 }
4315
4316 // MPTRequireAuth flag is set and the account is not authorized
4317 {
4318 Env env(*this);
4319 env.fund(XRP(1'000), gw, alice);
4320 MPTTester btc(
4321 {.env = env,
4322 .issuer = gw,
4323 .holders = {alice},
4324 .pay = 100,
4325 .flags = tfMPTRequireAuth | kMptDexFlags,
4326 .authHolder = true});
4327 MPTTester const eth(
4328 {.env = env,
4329 .issuer = gw,
4330 .holders = {alice},
4331 .pay = 100,
4332 .flags = tfMPTRequireAuth | kMptDexFlags,
4333 .authHolder = true});
4334
4335 btc.authorize({.account = gw, .holder = alice, .flags = tfMPTUnauthorize});
4336
4337 env(offer(alice, eth(10), btc(10)), Ter(tecUNFUNDED_OFFER));
4338
4339 // issuer can create
4340
4341 env(offer(gw, eth(10), btc(10)));
4342 env.close();
4343 }
4344
4345 // MPTCanTransfer is not set and the account is not the issuer of MPT
4346 {
4347 Env env(*this);
4348 env.fund(XRP(1'000), gw, alice, carol);
4349 MPTTester const btc(
4350 {.env = env,
4351 .issuer = gw,
4352 .holders = {alice, carol},
4353 .pay = 100,
4354 .flags = tfMPTCanTrade | tfMPTCanTransfer});
4355 MPTTester const eth(
4356 {.env = env,
4357 .issuer = gw,
4358 .holders = {alice, carol},
4359 .pay = 100,
4360 .flags = tfMPTCanTrade});
4361
4362 // Can create
4363 env(offer(alice, eth(10), btc(10)), Txflags(tfPassive));
4364 BEAST_EXPECT(getAccountOffers(env, alice)[jss::offers].size() == 1);
4365
4366 // issuer can create
4367 env(offer(gw, eth(10), btc(10)), Txflags(tfPassive));
4368 env.close();
4369
4370 // can cross issuer's offer, other offers are removed
4371 env(offer(carol, btc(10), eth(10)));
4372 BEAST_EXPECT(expectOffers(env, alice, 0));
4373 BEAST_EXPECT(expectOffers(env, gw, 0));
4374 BEAST_EXPECT(expectOffers(env, carol, 0));
4375 // can't cross holder's offer, holder's offer is removed
4376 env(offer(alice, eth(10), btc(10)), Txflags(tfPassive));
4377 env(offer(carol, btc(10), eth(10)));
4378 BEAST_EXPECT(expectOffers(env, alice, 0));
4379 BEAST_EXPECT(expectOffers(env, carol, 1));
4380 }
4381
4382 // MPTCanTrade is disabled
4383 {
4384 Env env(*this);
4385 env.fund(XRP(1'000), gw, alice, carol);
4386 MPTTester const btc(
4387 {.env = env,
4388 .issuer = gw,
4389 .holders = {alice, carol},
4390 .pay = 100,
4391 .flags = tfMPTCanTransfer,
4392 .mutableFlags = tmfMPTCanEnableCanTrade});
4393 MPTTester const eth(
4394 {.env = env,
4395 .issuer = gw,
4396 .holders = {alice, carol},
4397 .pay = 100,
4398 .flags = tfMPTCanTrade,
4399 .mutableFlags = tmfMPTCanEnableCanTrade});
4400
4401 // Can't create
4402 env(offer(gw, eth(10), btc(10)), Ter(tecNO_PERMISSION));
4403 env.close();
4404 }
4405
4406 // XRP/MPT
4407 {
4408 Env env{*this, features};
4409
4410 MPTTester mptTester(env, gw, {.holders = {alice, carol}});
4411
4412 mptTester.create(
4413 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
4414 auto const mpt = mptTester["MPT"];
4415
4416 mptTester.authorize({.account = alice});
4417 mptTester.pay(gw, alice, 200);
4418
4419 mptTester.authorize({.account = carol});
4420 mptTester.pay(gw, carol, 200);
4421
4422 env(offer(alice, XRP(100), mpt(101)));
4423 env.close();
4424 BEAST_EXPECT(expectOffers(env, alice, 1, {{Amounts{XRP(100), mpt(101)}}}));
4425
4426 env(offer(carol, mpt(101), XRP(100)));
4427 env.close();
4428 BEAST_EXPECT(expectOffers(env, alice, 0));
4429 BEAST_EXPECT(expectOffers(env, carol, 0));
4430 BEAST_EXPECT(mptTester.checkMPTokenOutstandingAmount(400));
4431 BEAST_EXPECT(mptTester.checkMPTokenAmount(alice, 99));
4432 BEAST_EXPECT(mptTester.checkMPTokenAmount(carol, 301));
4433 }
4434
4435 // IOU/MPT
4436 {
4437 Env env{*this, features};
4438
4439 MPTTester mptTester(env, gw, {.holders = {alice, carol}});
4440
4441 mptTester.create(
4442 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
4443 auto const mpt = mptTester["MPT"];
4444
4445 env(trust(alice, usd(2'000)));
4446 env(pay(gw, alice, usd(1'000)));
4447 env.close();
4448
4449 env(trust(carol, usd(2'000)));
4450 env(pay(gw, carol, usd(1'000)));
4451 env.close();
4452
4453 mptTester.authorize({.account = alice});
4454 mptTester.pay(gw, alice, 200);
4455
4456 mptTester.authorize({.account = carol});
4457 mptTester.pay(gw, carol, 200);
4458
4459 env(offer(alice, usd(100), mpt(101)));
4460 env.close();
4461 BEAST_EXPECT(expectOffers(env, alice, 1, {{Amounts{usd(100), mpt(101)}}}));
4462
4463 env(offer(carol, mpt(101), usd(100)));
4464 env.close();
4465
4466 BEAST_EXPECT(env.balance(alice, usd) == usd(1'100));
4467 BEAST_EXPECT(env.balance(carol, usd) == usd(900));
4468 BEAST_EXPECT(expectOffers(env, alice, 0));
4469 BEAST_EXPECT(expectOffers(env, carol, 0));
4470 BEAST_EXPECT(mptTester.checkMPTokenOutstandingAmount(400));
4471 BEAST_EXPECT(mptTester.checkMPTokenAmount(alice, 99));
4472 BEAST_EXPECT(mptTester.checkMPTokenAmount(carol, 301));
4473 }
4474
4475 // MPT/MPT
4476 {
4477 Env env{*this, features};
4478
4479 MPTTester mptTester1(env, gw, {.holders = {alice, carol}});
4480 mptTester1.create(
4481 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
4482 auto const mpt1 = mptTester1["MPT1"];
4483
4484 MPTTester mptTester2(env, gw, {.holders = {alice, carol}, .fund = false});
4485 mptTester2.create(
4486 {.ownerCount = 2, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
4487 auto const mpt2 = mptTester2["MPT2"];
4488
4489 mptTester1.authorize({.account = alice});
4490 mptTester1.authorize({.account = carol});
4491 mptTester1.pay(gw, alice, 200);
4492 mptTester1.pay(gw, carol, 200);
4493
4494 mptTester2.authorize({.account = alice});
4495 mptTester2.authorize({.account = carol});
4496 mptTester2.pay(gw, alice, 200);
4497 mptTester2.pay(gw, carol, 200);
4498
4499 env(offer(alice, mpt2(100), mpt1(101)));
4500 env.close();
4501 BEAST_EXPECT(expectOffers(env, alice, 1, {{Amounts{mpt2(100), mpt1(101)}}}));
4502
4503 env(offer(carol, mpt1(101), mpt2(100)));
4504 env.close();
4505
4506 BEAST_EXPECT(expectOffers(env, alice, 0));
4507 BEAST_EXPECT(expectOffers(env, carol, 0));
4508 BEAST_EXPECT(mptTester1.checkMPTokenOutstandingAmount(400));
4509 BEAST_EXPECT(mptTester1.checkMPTokenAmount(alice, 99));
4510 BEAST_EXPECT(mptTester1.checkMPTokenAmount(carol, 301));
4511 BEAST_EXPECT(mptTester2.checkMPTokenOutstandingAmount(400));
4512 BEAST_EXPECT(mptTester2.checkMPTokenAmount(alice, 300));
4513 BEAST_EXPECT(mptTester2.checkMPTokenAmount(carol, 100));
4514 }
4515 }
4516
4517 void
4519 {
4520 testcase("Cross Asset Payment");
4521 using namespace test::jtx;
4522 Account const gw = Account("gw");
4523 Account const gw2 = Account("gw2");
4524 Account const alice = Account("alice");
4525 Account const carol = Account("carol");
4526 Account const bob = Account("bob");
4527 auto const usd = gw["USD"];
4528
4529 // Loop
4530 {
4531 Env env{*this, features};
4532 MPTTester mptTester(env, gw, {.holders = {carol, bob}});
4533
4534 mptTester.create(
4535 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
4536 auto const mpt = mptTester["MPT"];
4537
4538 mptTester.authorize({.account = carol});
4539 mptTester.pay(gw, carol, 200);
4540
4541 mptTester.authorize({.account = bob});
4542
4543 // holder to holder
4544 env(pay(carol, bob, mpt(1)),
4545 test::jtx::Path(~mpt, ~usd, ~mpt),
4546 Sendmax(XRP(1)),
4547 Txflags(tfPartialPayment),
4549 env.close();
4550
4551 // issuer to holder
4552 env(pay(gw, bob, mpt(1)),
4553 test::jtx::Path(~mpt, ~usd, ~mpt),
4554 Sendmax(XRP(1)),
4555 Txflags(tfPartialPayment),
4557 env.close();
4558
4559 // holder to issuer
4560 env(pay(bob, gw, mpt(1)),
4561 test::jtx::Path(~mpt, ~usd, ~mpt),
4562 Sendmax(XRP(1)),
4563 Txflags(tfPartialPayment),
4565 env.close();
4566 }
4567
4568 // Rippling
4569 {
4570 Env env{*this, features};
4571 MPTTester mptTester(env, gw, {.holders = {carol, bob}});
4572
4573 mptTester.create(
4574 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
4575 auto const mpt = mptTester["MPT"];
4576
4577 mptTester.authorize({.account = carol});
4578 mptTester.pay(gw, carol, 200);
4579
4580 mptTester.authorize({.account = bob});
4581
4582 // holder to holder
4583 env(pay(carol, bob, mpt(1)),
4584 test::jtx::Path(~mpt, gw),
4585 Sendmax(XRP(1)),
4586 Txflags(tfPartialPayment),
4587 Ter(temBAD_PATH));
4588 env.close();
4589
4590 // issuer to holder
4591 env(pay(gw, bob, mpt(1)),
4592 test::jtx::Path(~mpt, carol),
4593 Sendmax(XRP(1)),
4594 Txflags(tfPartialPayment),
4595 Ter(temBAD_PATH));
4596 env.close();
4597
4598 // holder to issuer
4599 env(pay(bob, gw, mpt(1)),
4600 test::jtx::Path(~mpt, carol),
4601 Sendmax(XRP(1)),
4602 Txflags(tfPartialPayment),
4603 Ter(temBAD_PATH));
4604 env.close();
4605 }
4606
4607 // MPTokenV2 is disabled
4608 {
4609 Env env{*this, features - featureMPTokensV2};
4610
4611 MPTTester mptTester(env, gw, {.holders = {alice}});
4612
4613 mptTester.create(
4614 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
4615 auto const mpt = mptTester["MPT"];
4616
4617 mptTester.authorize({.account = alice});
4618
4619 env(pay(gw, alice, mpt(101)),
4620 test::jtx::Path(~mpt),
4621 Sendmax(XRP(100)),
4622 Txflags(tfPartialPayment),
4623 Ter(temMALFORMED));
4624 }
4625
4626 {
4627 auto const ed = Account{"ed"};
4628 Env env{*this, features};
4629 env.fund(XRP(1'000), gw, alice, carol, bob, ed);
4630 MPTTester btc(
4631 {.env = env,
4632 .issuer = gw,
4633 .holders = {alice, carol, bob},
4634 .pay = 1'000,
4635 .flags = tfMPTCanLock | kMptDexFlags,
4638 MPTTester eth(
4639 {.env = env,
4640 .issuer = gw,
4641 .holders = {alice, carol, bob},
4642 .pay = 1'000,
4643 .flags = tfMPTCanLock | kMptDexFlags,
4644 .mutableFlags = tmfMPTCanEnableCanTransfer});
4645 MPTTester const usd(
4646 {.env = env,
4647 .issuer = gw,
4648 .holders = {alice, carol, bob},
4649 .pay = 1'000,
4650 .flags = kMptDexFlags | tfMPTCanLock,
4651 .mutableFlags = tmfMPTCanEnableCanTransfer});
4652 MPTTester const cad(
4653 {.env = env,
4654 .issuer = gw,
4655 .holders = {alice, carol, bob},
4656 .pay = 1'000,
4657 .flags = kMptDexFlags | tfMPTCanLock,
4658 .mutableFlags = tmfMPTCanEnableCanTransfer});
4659
4660 env(offer(bob, eth(1'000), btc(1'000)), Txflags(tfPassive));
4661 env.close();
4662 env(offer(bob, btc(1'000), eth(1'000)), Txflags(tfPassive));
4663 env.close();
4664
4665 // MPTokenIssuance doesn't exist
4666
4667 env(pay(alice, carol, MPT(gw, 1'000)(10)), Sendmax(eth(10)), Ter(tecOBJECT_NOT_FOUND));
4668 env.close();
4669 env(pay(alice, carol, eth(10)), Sendmax(MPT(gw)(10)), Ter(tecOBJECT_NOT_FOUND));
4670 env.close();
4671
4672 // MPToken object doesn't exist
4673
4674 // holder and issuer fail
4675 env(pay(ed, carol, btc(10)), Sendmax(eth(10)), Ter(tecNO_AUTH));
4676 env(pay(carol, ed, btc(10)), Sendmax(eth(10)), Ter(tecNO_AUTH));
4677 env(pay(ed, gw, btc(10)), Sendmax(eth(10)), Ter(tecNO_AUTH));
4678 env(pay(gw, ed, btc(10)), Sendmax(eth(10)), Ter(tecNO_AUTH));
4679 env.close();
4680
4681 // MPTRequireAuth is set
4682
4683 btc.authorize({.account = ed});
4684 eth.authorize({.account = ed});
4685 env(pay(gw, ed, eth(100)));
4686 env(pay(gw, ed, btc(100)));
4687 env.close();
4688 btc.set({.mutableFlags = tmfMPTSetRequireAuth});
4689 // authorize bob to enable the offers trading
4690 btc.authorize({.account = gw, .holder = bob});
4691 env.close();
4692 env(pay(ed, carol, btc(10)), Path(~btc), Sendmax(eth(10)), Ter(tecNO_AUTH));
4693 env(pay(carol, ed, btc(10)), Path(~btc), Sendmax(eth(10)), Ter(tecNO_AUTH));
4694 // BTC is transferred from bob to ed, ed is not authorized
4695 env(pay(gw, ed, btc(10)), Path(~btc), Sendmax(eth(10)), Ter(tecNO_AUTH));
4696 // BTC is transferred from bob to issuer
4697 env(pay(ed, gw, btc(10)), Path(~btc), Sendmax(eth(10)));
4698 // BTC is transferred from issuer to bob
4699 env(pay(gw, ed, eth(10)), Path(~eth), Sendmax(btc(10)));
4700 //
4701 // BTC is transferred from ed to bob, ed is not authorized
4702 env(pay(ed, gw, eth(10)), Path(~eth), Sendmax(btc(10)), Ter(tecNO_AUTH));
4703 env.close();
4704 }
4705
4706 // MPTCanTransfer is not set.
4707 {
4708 auto const ed = Account{"ed"};
4709 Env env{*this, features};
4710 env.fund(XRP(1'000), gw, alice, carol, bob, ed);
4711 MPTTester const btc(
4712 {.env = env,
4713 .issuer = gw,
4714 .holders = {alice, carol, bob, ed},
4715 .pay = 1'000,
4716 .flags = tfMPTCanTrade});
4717 MPTTester const eth(
4718 {.env = env,
4719 .issuer = gw,
4720 .holders = {alice, carol, bob, ed},
4721 .pay = 1'000,
4722 .flags = kMptDexFlags});
4723
4724 env(offer(bob, eth(1'000), btc(1'000)), Txflags(tfPassive));
4725 env.close();
4726 env(offer(bob, btc(1'000), eth(1'000)), Txflags(tfPassive));
4727 env.close();
4728
4729 // Fail regardless if source/destination is the issuer or
4730 // not since the offer is owned by a holder.
4731 env(pay(ed, carol, btc(10)), Path(~btc), Sendmax(eth(10)), Ter(tecPATH_PARTIAL));
4732 env(pay(carol, ed, btc(10)), Path(~btc), Sendmax(eth(10)), Ter(tecPATH_PARTIAL));
4733 env(pay(ed, carol, eth(10)), Path(~eth), Sendmax(btc(10)), Ter(tecPATH_PARTIAL));
4734 env(pay(carol, ed, eth(10)), Path(~eth), Sendmax(btc(10)), Ter(tecPATH_PARTIAL));
4735 // Fail because BTC, which has CanTransfer disabled, is sent to bob
4736 env(pay(ed, gw, eth(10)), Path(~eth), Sendmax(btc(10)), Ter(tecPATH_PARTIAL));
4737 env(pay(ed, gw, btc(10)), Path(~btc), Sendmax(eth(10)), Ter(tesSUCCESS));
4738 env(pay(gw, ed, eth(10)), Path(~eth), Sendmax(btc(10)), Ter(tesSUCCESS));
4739 // Fail because BTC, which has CanTransfer disabled, is sent to ed
4740 env(pay(gw, ed, btc(10)), Path(~btc), Sendmax(eth(10)), Ter(tecPATH_PARTIAL));
4741 env.close();
4742 env(offer(gw, eth(100), btc(100)), Txflags(tfPassive));
4743 env.close();
4744 env(offer(gw, btc(100), eth(100)), Txflags(tfPassive));
4745 env.close();
4746 BEAST_EXPECT(expectOffers(env, bob, 2));
4747 env(pay(ed, carol, btc(10)), Path(~btc), Sendmax(eth(10)), Ter(tesSUCCESS));
4748 env(pay(ed, carol, eth(10)), Path(~eth), Sendmax(btc(10)), Ter(tesSUCCESS));
4749 env(pay(gw, carol, btc(10)), Path(~btc), Sendmax(eth(10)), Ter(tesSUCCESS));
4750 env.close();
4751 env(pay(ed, gw, btc(10)), Path(~btc), Sendmax(eth(10)));
4752 env.close();
4753 }
4754
4755 // Multiple steps: CAD/USD, USD/BTC, BTC/ETH.
4756 // takerGets can transfer if:
4757 // - CanTransfer is set
4758 // - The offer's owner is the issuer
4759 // - BookStep is the last step, which means strand's destination is
4760 // the issuer
4761 // takerPays can transfer if
4762 // - BookStep is the first step, which means strand's source is
4763 // the issuer
4764 // - The offer's owner is the issuer
4765 // - Previous step is BookStep, which transfers per above
4766 // - CanTransfer is set
4767 {
4768 // enum to indicate which MPT doesn't set CanTransfer flag.
4769 enum class NoTransferMPT { BTC, ETH, USD, CAD };
4770
4771 // Lambda to test multi-step payment with one of the MPTs not setting CanTransfer flag.
4772 auto const testMultiStepMPTCanTransfer = [&](NoTransferMPT const noTransferMPT,
4773 auto const& test) {
4774 auto const getFlags = [&](NoTransferMPT const mpt) {
4775 return mpt == noTransferMPT ? tfMPTCanTrade : kMptDexFlags;
4776 };
4777
4778 Env env{*this, features};
4779 env.fund(XRP(1'000), gw, alice, carol, bob);
4780 env.close();
4781 MPTTester const btc(
4782 {.env = env,
4783 .issuer = gw,
4784 .holders = {alice, carol, bob},
4785 .pay = 1'000,
4786 .flags = getFlags(NoTransferMPT::BTC)});
4787 MPTTester const eth(
4788 {.env = env,
4789 .issuer = gw,
4790 .holders = {alice, carol, bob},
4791 .pay = 1'000,
4792 .flags = getFlags(NoTransferMPT::ETH)});
4793 MPTTester const usd(
4794 {.env = env,
4795 .issuer = gw,
4796 .holders = {alice, carol, bob},
4797 .pay = 1'000,
4798 .flags = getFlags(NoTransferMPT::USD)});
4799 MPTTester const cad(
4800 {.env = env,
4801 .issuer = gw,
4802 .holders = {alice, carol, bob},
4803 .pay = 1'000,
4804 .flags = getFlags(NoTransferMPT::CAD)});
4805
4806 env(offer(bob, cad(100), usd(100)), Txflags(tfPassive));
4807 env(offer(bob, usd(100), btc(100)), Txflags(tfPassive));
4808 env(offer(bob, btc(100), eth(100)), Txflags(tfPassive));
4809 env.close();
4810
4811 test(env, btc, eth, usd, cad);
4812 };
4813
4814 // USD starts without MPTCanTransfer.
4815 testMultiStepMPTCanTransfer(
4816 NoTransferMPT::USD,
4817 [&](Env& env,
4818 MPTTester const& btc,
4819 MPTTester const& eth,
4820 MPTTester const& usd,
4821 MPTTester const& cad) {
4822 BEAST_EXPECT(expectOffers(env, bob, 3));
4823
4824 // fail - CAD/USD is owned by bob
4825 env(pay(alice, carol, eth(1)),
4826 Path(~usd, ~btc, ~eth),
4827 Sendmax(cad(1)),
4829
4830 auto seq(env.seq(gw));
4831 env(offer(gw, usd(1), btc(1)), Txflags(tfPassive));
4832 env.close();
4833 // fail - CAD/USD is owned by bob
4834 env(pay(alice, carol, eth(1)),
4835 Path(~usd, ~btc, ~eth),
4836 Sendmax(cad(1)),
4838 env.close();
4839 env(offerCancel(gw, seq));
4840 env(offer(gw, cad(1), usd(1)), Txflags(tfPassive));
4841 env.close();
4842 BEAST_EXPECT(expectOffers(env, bob, 3));
4843 // succeed - CAD/USD is owned by issuer
4844 env(pay(alice, carol, eth(1)), Path(~usd, ~btc, ~eth), Sendmax(cad(1)));
4845 env.close();
4846 // bob's CAD/USD is deleted.
4847 BEAST_EXPECT(expectOffers(env, bob, 2));
4848 env(offer(bob, cad(100), usd(100)), Txflags(tfPassive));
4849 BEAST_EXPECT(expectOffers(env, gw, 0));
4850 });
4851
4852 // ETH starts without MPTCanTransfer.
4853 testMultiStepMPTCanTransfer(
4854 NoTransferMPT::ETH,
4855 [&](Env& env,
4856 MPTTester const& btc,
4857 MPTTester const& eth,
4858 MPTTester const& usd,
4859 MPTTester const& cad) {
4860 // fail - BTC/ETH is owned by bob, destination is carol
4861 env(pay(alice, carol, eth(1)),
4862 Path(~usd, ~btc, ~eth),
4863 Sendmax(cad(1)),
4865 env.close();
4866 BEAST_EXPECT(expectOffers(env, bob, 3));
4867
4868 // succeed - destination is an issuer
4869 env(pay(alice, gw, eth(1)), Path(~usd, ~btc, ~eth), Sendmax(cad(1)));
4870 env.close();
4871 BEAST_EXPECT(expectOffers(env, bob, 3));
4872 });
4873
4874 // CAD starts without MPTCanTransfer.
4875 testMultiStepMPTCanTransfer(
4876 NoTransferMPT::CAD,
4877 [&](Env& env,
4878 MPTTester const& btc,
4879 MPTTester const& eth,
4880 MPTTester const& usd,
4881 MPTTester const& cad) {
4882 // fail - CAD/USD is owned by bob, source is alice
4883 env(pay(alice, carol, eth(1)),
4884 Path(~usd, ~btc, ~eth),
4885 Sendmax(cad(1)),
4887 // succeed - source is the issuer
4888 env(pay(gw, carol, eth(1)), Path(~usd, ~btc, ~eth), Sendmax(cad(1)));
4889 env.close();
4890 env(offer(gw, cad(1), usd(1)), Txflags(tfPassive));
4891 env.close();
4892 // succeed - CAD/USD is owned by issuer
4893 env(pay(alice, carol, eth(1)), Path(~usd, ~btc, ~eth), Sendmax(cad(1)));
4894 env.close();
4895 BEAST_EXPECT(expectOffers(env, gw, 0));
4896 BEAST_EXPECT(expectOffers(env, bob, 2));
4897 });
4898
4899 // BTC starts without MPTCanTransfer.
4900 testMultiStepMPTCanTransfer(
4901 NoTransferMPT::BTC,
4902 [&](Env& env,
4903 MPTTester const& btc,
4904 MPTTester const& eth,
4905 MPTTester const& usd,
4906 MPTTester const& cad) {
4907 env(offer(gw, usd(1), btc(1)), Txflags(tfPassive));
4908 env.close();
4909 // succeed - USD/BTC is owned by issuer
4910 env(pay(alice, carol, eth(1)), Path(~usd, ~btc, ~eth), Sendmax(cad(1)));
4911 env.close();
4912 BEAST_EXPECT(expectOffers(env, gw, 0));
4913 });
4914 }
4915
4916 // MPTCanTrade is not set
4917 {
4918 Env env{*this, features};
4919 env.fund(XRP(1'000), gw, alice, carol, bob);
4920 env.close();
4921 MPTTester btc(
4922 {.env = env,
4923 .issuer = gw,
4924 .holders = {alice, carol, bob},
4925 .pay = 1'000,
4926 .flags = tfMPTCanTransfer,
4927 .mutableFlags = tmfMPTCanEnableCanTrade});
4928 MPTTester const eth(
4929 {.env = env,
4930 .issuer = gw,
4931 .holders = {alice, carol, bob},
4932 .pay = 1'000,
4933 .flags = kMptDexFlags});
4934 MPTTester const usd(
4935 {.env = env,
4936 .issuer = gw,
4937 .holders = {alice, carol, bob},
4938 .pay = 1'000,
4939 .flags = kMptDexFlags});
4940
4941 env(pay(alice, carol, eth(1)), Path(~eth), Sendmax(btc(1)), Ter(tecNO_PERMISSION));
4942 env(pay(alice, carol, btc(1)), Path(~btc), Sendmax(eth(1)), Ter(tecNO_PERMISSION));
4943 env.close();
4944
4945 // Enable MPTCanTrade so BTC can be crossed through offers.
4946 btc.set({.mutableFlags = tmfMPTSetCanTrade});
4947 env(offer(bob, XRP(1), btc(1)));
4948 env(offer(bob, btc(1), eth(1)));
4949 env(offer(bob, eth(1), usd(1)));
4950 env.close();
4951 BEAST_EXPECT(expectOffers(env, bob, 3));
4952
4953 env(pay(gw, carol, usd(1)),
4954 Path(~btc, ~eth, ~usd),
4955 Sendmax(XRP(1)),
4956 Txflags(tfPartialPayment | tfNoRippleDirect));
4957 env.close();
4958 BEAST_EXPECT(expectOffers(env, bob, 0));
4959 }
4960
4961 // Holders are locked
4962 {
4963 enum class LockType { Global, Individual, None };
4964 struct TestArg
4965 {
4966 Account src;
4967 Account dst;
4968 Account offerOwner;
4969 LockType srcFlag = LockType::None;
4970 LockType dstFlag = LockType::None;
4971 LockType offerFlagBuy = LockType::None;
4972 LockType offerFlagSell = LockType::None;
4973 LockType globalFlagBuy = LockType::None;
4974 LockType globalFlagSell = LockType::None;
4975 TER err = tesSUCCESS;
4976 std::optional<TER> errIOU = std::nullopt;
4977 };
4978 auto getErr = [&]<typename Token>(Token const&, TestArg const& arg) {
4979 if constexpr (std::is_same_v<Token, IOU>)
4980 {
4981 return arg.errIOU.value_or(arg.err);
4982 }
4983 else if constexpr (std::is_same_v<Token, MPTTester>)
4984 {
4985 return arg.err;
4986 }
4987 };
4988 auto getMPT = [&](Env& env) {
4989 MPTTester const btc(
4990 {.env = env,
4991 .issuer = gw2,
4992 .holders = {alice, carol, bob, gw},
4993 .pay = 100,
4994 .flags = tfMPTCanLock | kMptDexFlags});
4995 MPTTester const eth(
4996 {.env = env,
4997 .issuer = gw,
4998 .holders = {alice, carol, bob, gw2},
4999 .pay = 100,
5000 .flags = tfMPTCanLock | kMptDexFlags});
5001 return std::make_pair(btc, eth);
5002 };
5003 auto getIOU = [&](Env& env) {
5004 for (auto const& a : {alice, carol, bob})
5005 {
5006 env(fset(a, asfDefaultRipple));
5007 env.close();
5008 env(trust(a, gw["ETH"](200)));
5009 env(pay(gw, a, gw["ETH"](100)));
5010 env(trust(a, gw2["BTC"](200)));
5011 env(pay(gw2, a, gw2["BTC"](100)));
5012 env.close();
5013 }
5014 // gw2 needs an ETH trust line to receive ETH when its offers fill
5015 // gw needs BTC to sell BTC
5016 env(trust(gw2, gw["ETH"](200)));
5017 env(trust(gw, gw2["BTC"](200)));
5018 env(pay(gw2, gw, gw2["BTC"](100)));
5019 env.close();
5020 return std::make_pair(gw2["BTC"], gw["ETH"]);
5021 };
5022 auto lock = [&]<typename Token>(
5023 Env& env, Account const& account, Token& token, LockType lock) {
5024 if (lock == LockType::None)
5025 return;
5026 if constexpr (std::is_same_v<Token, IOU>)
5027 {
5028 if (lock == LockType::Global)
5029 {
5030 env(fset(token.account, asfGlobalFreeze));
5031 }
5032 else
5033 {
5034 IOU const iou{account, token.currency};
5035 env(trust(token.account, iou(0), tfSetFreeze));
5036 }
5037 }
5038 else if constexpr (std::is_same_v<Token, MPTTester>)
5039 {
5040 if (lock == LockType::Global)
5041 {
5042 token.set({.flags = tfMPTLock});
5043 }
5044 else if (token.issuer() != account)
5045 {
5046 token.set({.holder = account, .flags = tfMPTLock});
5047 }
5048 }
5049 };
5050 auto test = [&](auto&& getTokens, TestArg const& arg) {
5051 Env env(*this);
5052 env.fund(XRP(1'000), gw, gw2, alice, carol, bob);
5053
5054 auto [btc, eth] = getTokens(env);
5055
5056 env(offer(arg.offerOwner, eth(10), btc(10)), Txflags(tfPassive));
5057 env.close();
5058
5059 if (arg.globalFlagBuy != LockType::None)
5060 {
5061 lock(env, gw, eth, LockType::Global);
5062 }
5063 else
5064 {
5065 lock(env, arg.offerOwner, eth, arg.offerFlagBuy);
5066 lock(env, arg.src, eth, arg.srcFlag);
5067 }
5068 if (arg.globalFlagSell != LockType::None)
5069 {
5070 lock(env, gw2, btc, LockType::Global);
5071 }
5072 else
5073 {
5074 lock(env, arg.offerOwner, btc, arg.offerFlagSell);
5075 lock(env, arg.dst, btc, arg.dstFlag);
5076 }
5077
5078 auto const err = getErr(eth, arg);
5079 env(pay(arg.src, arg.dst, btc(1)),
5080 Path(~btc),
5081 Txflags(tfNoRippleDirect),
5082 Sendmax(eth(1)),
5083 Ter(err));
5084 env.close();
5085 };
5086 // clang-format off
5087 std::vector<TestArg> const tests = {
5088 // ----- src=alice (holder), dst=carol (holder), offerOwner=bob (holder) -----
5089 // alice's ETH locked: caught in check()
5090 {.src = alice, .dst = carol, .offerOwner = bob, .srcFlag = LockType::Individual, .err = tecPATH_DRY},
5091 // carol's BTC locked: caught in MPT check(); IOU dst can still receive when frozen
5092 {.src = alice, .dst = carol, .offerOwner = bob, .dstFlag = LockType::Individual, .err = tecPATH_DRY, .errIOU = tesSUCCESS},
5093 // ETH globally locked: caught in check()
5094 {.src = alice, .dst = carol, .offerOwner = bob, .globalFlagBuy = LockType::Global, .err = tecPATH_DRY},
5095 // BTC globally locked: bob's offer unfunded in OfferStream
5096 {.src = alice, .dst = carol, .offerOwner = bob, .globalFlagSell = LockType::Global, .err = tecPATH_PARTIAL},
5097 // bob's ETH (takerPays) locked: MPT offer unfunded in OfferStream (locked holder cannot receive); IOU can still receive
5098 {.src = alice, .dst = carol, .offerOwner = bob, .offerFlagBuy = LockType::Individual, .err = tecPATH_PARTIAL, .errIOU = tesSUCCESS},
5099 // bob's BTC (takerGets) locked: offer unfunded in OfferStream
5100 {.src = alice, .dst = carol, .offerOwner = bob, .offerFlagSell = LockType::Individual, .err = tecPATH_PARTIAL},
5101 // ----- src=alice (holder), dst=carol (holder), offerOwner=gw2 (BTC issuer) -----
5102 // alice's ETH locked: caught in check()
5103 {.src = alice, .dst = carol, .offerOwner = gw2, .srcFlag = LockType::Individual, .err = tecPATH_DRY},
5104 // carol's BTC locked: caught in MPT check(); IOU dst can still receive when frozen
5105 {.src = alice, .dst = carol, .offerOwner = gw2, .dstFlag = LockType::Individual, .err = tecPATH_DRY, .errIOU = tesSUCCESS},
5106 // ETH globally locked: caught in check()
5107 {.src = alice, .dst = carol, .offerOwner = gw2, .globalFlagBuy = LockType::Global, .err = tecPATH_DRY},
5108 // BTC globally locked: gw2 is the BTC issuer, offer always permitted regardless of global freeze
5109 {.src = alice, .dst = carol, .offerOwner = gw2, .globalFlagSell = LockType::Global, .err = tesSUCCESS},
5110 // ----- src=alice (holder), dst=carol (holder), offerOwner=gw (ETH issuer, BTC holder) -----
5111 // alice's ETH locked: caught in check()
5112 {.src = alice, .dst = carol, .offerOwner = gw, .srcFlag = LockType::Individual, .err = tecPATH_DRY},
5113 // carol's BTC locked: caught in MPT check(); IOU dst can still receive when frozen
5114 {.src = alice, .dst = carol, .offerOwner = gw, .dstFlag = LockType::Individual, .err = tecPATH_DRY, .errIOU = tesSUCCESS},
5115 // ETH globally locked: caught in check()
5116 {.src = alice, .dst = carol, .offerOwner = gw, .globalFlagBuy = LockType::Global, .err = tecPATH_DRY},
5117 // BTC globally locked: gw holds BTC as a holder (not BTC issuer), offer unfunded in OfferStream
5118 {.src = alice, .dst = carol, .offerOwner = gw, .globalFlagSell = LockType::Global, .err = tecPATH_PARTIAL},
5119 // ----- src=gw (ETH issuer), dst=carol (holder), offerOwner=bob (holder) -----
5120 // ETH globally locked, src is ETH issuer: no first MPTEndpointStep so check() passes;
5121 // MPT offer unfunded in OfferStream (globally-locked ETH cannot flow to holder via DEX); IOU issuer can still issue
5122 {.src = gw, .dst = carol, .offerOwner = bob, .srcFlag = LockType::Global, .err = tecPATH_PARTIAL, .errIOU = tesSUCCESS},
5123 // BTC globally locked: last MPTEndpointStep only checks individual freeze, check() passes; offer unfunded in OfferStream
5124 {.src = gw, .dst = carol, .offerOwner = bob, .dstFlag = LockType::Global, .err = tecPATH_PARTIAL},
5125 // carol's BTC locked: caught in MPT check(); IOU dst can still receive when frozen
5126 {.src = gw, .dst = carol, .offerOwner = bob, .dstFlag = LockType::Individual, .err = tecPATH_DRY, .errIOU = tesSUCCESS},
5127 // bob's ETH (takerPays) locked: MPT offer unfunded in OfferStream (locked holder cannot receive); IOU can still receive
5128 {.src = gw, .dst = carol, .offerOwner = bob, .offerFlagBuy = LockType::Individual, .err = tecPATH_PARTIAL, .errIOU = tesSUCCESS},
5129 // bob's BTC (takerGets) locked: offer unfunded in OfferStream
5130 {.src = gw, .dst = carol, .offerOwner = bob, .offerFlagSell = LockType::Individual, .err = tecPATH_PARTIAL},
5131 // ----- src=alice (holder), dst=gw2 (BTC issuer), offerOwner=bob (holder) -----
5132 // alice's ETH locked: caught in check()
5133 {.src = alice, .dst = gw2, .offerOwner = bob, .srcFlag = LockType::Individual, .err = tecPATH_DRY},
5134 // BTC globally locked, dst is BTC issuer: no last MPTEndpointStep so check() passes; offer unfunded in OfferStream
5135 {.src = alice, .dst = gw2, .offerOwner = bob, .dstFlag = LockType::Global, .err = tecPATH_PARTIAL},
5136 // bob's ETH (takerPays) locked: MPT offer unfunded in OfferStream (locked holder cannot receive); IOU can still receive
5137 {.src = alice, .dst = gw2, .offerOwner = bob, .offerFlagBuy = LockType::Individual, .err = tecPATH_PARTIAL, .errIOU = tesSUCCESS},
5138 // bob's BTC (takerGets) locked: offer unfunded in OfferStream
5139 {.src = alice, .dst = gw2, .offerOwner = bob, .offerFlagSell = LockType::Individual, .err = tecPATH_PARTIAL},
5140 };
5141 // clang-format on
5142
5143 for (auto const& t : tests)
5144 {
5145 test(getMPT, t);
5146 test(getIOU, t);
5147 }
5148 }
5149 {
5150 Env env(*this);
5151 auto const usd = gw["USD"];
5152 env.fund(XRP(1'000), gw, alice, carol, bob);
5153 MPTTester btc(
5154 {.env = env,
5155 .issuer = gw,
5156 .holders = {alice, carol, bob},
5157 .pay = 100,
5158 .flags = tfMPTCanLock | kMptDexFlags});
5159 MPTTester eth(
5160 {.env = env,
5161 .issuer = gw,
5162 .holders = {alice, carol, bob},
5163 .pay = 100,
5164 .flags = tfMPTCanLock | kMptDexFlags});
5165
5166 env(trust(alice, usd(100)));
5167 env(pay(gw, alice, usd(100)));
5168 env(trust(carol, usd(100)));
5169
5170 env(offer(alice, XRP(10), eth(10)));
5171 env(offer(bob, eth(10), btc(10)));
5172 env(offer(alice, btc(10), usd(10)));
5173 env.close();
5174
5175 btc.set({.holder = bob, .flags = tfMPTLock});
5176
5177 // Bob's offer is unfunded
5178 env(pay(alice, carol, usd(1)),
5179 Path(~(MPT)eth, ~(MPT)btc, ~usd),
5180 Txflags(tfNoRippleDirect | tfPartialPayment),
5181 Sendmax(XRP(1)),
5182 Ter(tecPATH_DRY));
5183 env.close();
5184
5185 btc.set({.holder = bob, .flags = tfMPTUnlock});
5186 eth.set({.holder = bob, .flags = tfMPTLock});
5187
5188 env(pay(alice, carol, usd(1)),
5189 Path(~(MPT)eth, ~(MPT)btc, ~usd),
5190 Txflags(tfNoRippleDirect | tfPartialPayment),
5191 Sendmax(XRP(1)),
5192 Ter(tecPATH_DRY));
5193 }
5194
5195 // A domain payment should only consume a USD/MPT offer with a domain.
5196 // It must not consume a regular USD/MPT offer.
5197 {
5198 Env env(*this, features);
5199 Account const domainOwner("DomainOwner");
5200 env.fund(XRP(1'000), gw, alice, carol, bob);
5201 auto const domainID =
5202 setupDomain(env, {alice, bob, carol, gw}, domainOwner, "permdex-cred");
5203
5204 MPTTester btc({.env = env, .issuer = gw, .holders = {alice, carol, bob}, .pay = 100});
5205 MPTTester eth({.env = env, .issuer = gw, .holders = {alice, carol, bob}, .pay = 100});
5206
5207 auto test = [&](bool withDomain) {
5208 if (withDomain)
5209 {
5210 env(offer(bob, eth(1), btc(1)), Domain(domainID));
5211 }
5212 else
5213 {
5214 env(offer(bob, eth(1), btc(1)));
5215 }
5216
5217 auto const err = withDomain ? Ter(tesSUCCESS) : Ter(tecPATH_DRY);
5218 env(pay(alice, carol, btc(1)),
5219 Path(~(MPT)btc),
5220 Txflags(tfPartialPayment),
5221 Sendmax(eth(1)),
5222 Domain(domainID),
5223 err);
5224 };
5225 test(true);
5226 test(false);
5227 }
5228
5229 // A hybrid USD/MPT domain offer should still be consumable by
5230 // a regular payment.
5231 {
5232 Env env(*this, features);
5233 Account const domainOwner("DomainOwner");
5234 env.fund(XRP(1'000), gw, alice, carol, bob);
5235 auto const domainID =
5236 setupDomain(env, {alice, bob, carol, gw}, domainOwner, "permdex-cred");
5237
5238 MPTTester btc({.env = env, .issuer = gw, .holders = {alice, carol, bob}, .pay = 100});
5239 MPTTester eth({.env = env, .issuer = gw, .holders = {alice, carol, bob}, .pay = 100});
5240
5241 auto test = [&](bool isHybrid) {
5242 auto const flags = isHybrid ? tfHybrid : 0;
5243 env(offer(bob, eth(1), btc(1)), Txflags(flags), Domain(domainID));
5244
5245 auto const err = isHybrid ? Ter(tesSUCCESS) : Ter(tecPATH_DRY);
5246 env(pay(alice, carol, btc(1)),
5247 Path(~(MPT)btc),
5248 Txflags(tfPartialPayment),
5249 Sendmax(eth(1)),
5250 err);
5251 };
5252 test(true);
5253 test(false);
5254 }
5255
5256 // MPT/XRP
5257 {
5258 Env env{*this, features};
5259 MPTTester mptTester(env, gw, {.holders = {alice, carol, bob}});
5260
5261 mptTester.create(
5262 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
5263 auto const mpt = mptTester["MPT"];
5264
5265 mptTester.authorize({.account = alice});
5266 mptTester.pay(gw, alice, 200);
5267
5268 mptTester.authorize({.account = carol});
5269 mptTester.pay(gw, carol, 200);
5270
5271 mptTester.authorize({.account = bob});
5272
5273 env(offer(alice, XRP(100), mpt(101)));
5274 env.close();
5275 BEAST_EXPECT(expectOffers(env, alice, 1, {{Amounts{XRP(100), mpt(101)}}}));
5276
5277 env(pay(carol, bob, mpt(101)),
5278 test::jtx::Path(~mpt),
5279 Sendmax(XRP(100)),
5280 Txflags(tfPartialPayment));
5281 env.close();
5282
5283 BEAST_EXPECT(expectOffers(env, alice, 0));
5284 BEAST_EXPECT(mptTester.checkMPTokenOutstandingAmount(400));
5285 BEAST_EXPECT(mptTester.checkMPTokenAmount(alice, 99));
5286 BEAST_EXPECT(mptTester.checkMPTokenAmount(bob, 101));
5287 }
5288
5289 // MPT/IOU
5290 {
5291 Env env{*this, features};
5292
5293 MPTTester mptTester(env, gw, {.holders = {alice, carol, bob}});
5294
5295 mptTester.create(
5296 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
5297 auto const mpt = mptTester["MPT"];
5298
5299 env(trust(alice, usd(2'000)));
5300 env(pay(gw, alice, usd(1'000)));
5301 env(trust(bob, usd(2'000)));
5302 env(pay(gw, bob, usd(1'000)));
5303 env(trust(carol, usd(2'000)));
5304 env(pay(gw, carol, usd(1'000)));
5305 env.close();
5306
5307 mptTester.authorize({.account = alice});
5308 mptTester.pay(gw, alice, 200);
5309
5310 mptTester.authorize({.account = carol});
5311 mptTester.pay(gw, carol, 200);
5312
5313 mptTester.authorize({.account = bob});
5314
5315 env(offer(alice, usd(100), mpt(101)));
5316 env.close();
5317 BEAST_EXPECT(expectOffers(env, alice, 1, {{Amounts{usd(100), mpt(101)}}}));
5318
5319 env(pay(carol, bob, mpt(101)),
5320 test::jtx::Path(~mpt),
5321 Sendmax(usd(100)),
5322 Txflags(tfPartialPayment));
5323 env.close();
5324
5325 BEAST_EXPECT(expectOffers(env, alice, 0));
5326 BEAST_EXPECT(env.balance(carol, usd) == usd(900));
5327 BEAST_EXPECT(mptTester.checkMPTokenOutstandingAmount(400));
5328 BEAST_EXPECT(mptTester.checkMPTokenAmount(alice, 99));
5329 BEAST_EXPECT(mptTester.checkMPTokenAmount(bob, 101));
5330 }
5331
5332 // IOU/MPT
5333 {
5334 Env env{*this, features};
5335
5336 MPTTester mptTester(env, gw, {.holders = {alice, carol, bob}});
5337
5338 mptTester.create(
5339 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
5340 auto const mpt = mptTester["MPT"];
5341
5342 env(trust(alice, usd(2'000)), Txflags(tfClearNoRipple));
5343 env(pay(gw, alice, usd(1'000)));
5344 env(trust(bob, usd(2'000)), Txflags(tfClearNoRipple));
5345 env.close();
5346
5347 mptTester.authorize({.account = alice});
5348 env(pay(gw, alice, mpt(200)));
5349
5350 mptTester.authorize({.account = carol});
5351 env(pay(gw, carol, mpt(200)));
5352
5353 env(offer(alice, mpt(101), usd(100)));
5354 env.close();
5355 BEAST_EXPECT(expectOffers(env, alice, 1, {{Amounts{mpt(101), usd(100)}}}));
5356
5357 env(pay(carol, bob, usd(100)),
5358 test::jtx::Path(~usd),
5359 Sendmax(mpt(101)),
5360 Txflags(tfPartialPayment | tfNoRippleDirect));
5361 env.close();
5362
5363 BEAST_EXPECT(expectOffers(env, alice, 0));
5364 BEAST_EXPECT(env.balance(alice, usd) == usd(900));
5365 BEAST_EXPECT(mptTester.checkMPTokenAmount(alice, 301));
5366 BEAST_EXPECT(mptTester.checkMPTokenOutstandingAmount(400));
5367 BEAST_EXPECT(mptTester.checkMPTokenAmount(carol, 99));
5368 BEAST_EXPECT(env.balance(bob, usd) == usd(100));
5369 }
5370
5371 // MPT/MPT
5372 {
5373 Env env{*this, features};
5374
5375 MPTTester mptTester1(env, gw, {.holders = {alice, carol, bob}});
5376 mptTester1.create(
5377 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
5378 auto const mpt1 = mptTester1["MPT1"];
5379
5380 MPTTester mptTester2(env, gw, {.holders = {alice, carol, bob}, .fund = false});
5381 mptTester2.create(
5382 {.ownerCount = 2, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
5383 auto const mpt2 = mptTester2["MPT2"];
5384
5385 mptTester1.authorize({.account = alice});
5386 mptTester1.pay(gw, alice, 200);
5387 mptTester2.authorize({.account = alice});
5388
5389 mptTester2.authorize({.account = carol});
5390 mptTester2.pay(gw, carol, 200);
5391
5392 mptTester1.authorize({.account = bob});
5393 mptTester2.authorize({.account = bob});
5394 mptTester2.pay(gw, bob, 200);
5395
5396 env(offer(alice, mpt2(100), mpt1(100)));
5397 env.close();
5398 BEAST_EXPECT(
5399 expectOffers(env, alice, 1, {{Amounts{mptTester2(100), mptTester1(100)}}}));
5400
5401 // holder to holder
5402 env(pay(carol, bob, mpt1(10)),
5403 test::jtx::Path(~mpt1),
5404 Sendmax(mpt2(10)),
5405 Txflags(tfPartialPayment));
5406 env.close();
5407
5408 BEAST_EXPECT(expectOffers(env, alice, 1));
5409 BEAST_EXPECT(mptTester1.checkMPTokenAmount(alice, 190));
5410 BEAST_EXPECT(mptTester2.checkMPTokenAmount(alice, 10));
5411 BEAST_EXPECT(mptTester1.checkMPTokenOutstandingAmount(200));
5412 BEAST_EXPECT(mptTester2.checkMPTokenOutstandingAmount(400));
5413 BEAST_EXPECT(mptTester2.checkMPTokenAmount(carol, 190));
5414 BEAST_EXPECT(mptTester1.checkMPTokenAmount(bob, 10));
5415
5416 // issuer to holder
5417 env(pay(gw, bob, mpt1(20)),
5418 test::jtx::Path(~mpt1),
5419 Sendmax(mpt2(20)),
5420 Txflags(tfPartialPayment));
5421 env.close();
5422
5423 BEAST_EXPECT(expectOffers(env, alice, 1));
5424 BEAST_EXPECT(mptTester1.checkMPTokenAmount(alice, 170));
5425 BEAST_EXPECT(mptTester2.checkMPTokenAmount(alice, 30));
5426 BEAST_EXPECT(mptTester1.checkMPTokenOutstandingAmount(200));
5427 BEAST_EXPECT(mptTester2.checkMPTokenOutstandingAmount(420));
5428 BEAST_EXPECT(mptTester2.checkMPTokenAmount(carol, 190));
5429 BEAST_EXPECT(mptTester1.checkMPTokenAmount(bob, 30));
5430
5431 // holder to issuer
5432 env(pay(bob, gw, mpt1(70)),
5433 test::jtx::Path(~mpt1),
5434 Sendmax(mpt2(70)),
5435 Txflags(tfPartialPayment));
5436 env.close();
5437
5438 BEAST_EXPECT(expectOffers(env, alice, 0));
5439 BEAST_EXPECT(mptTester1.checkMPTokenAmount(alice, 100));
5440 BEAST_EXPECT(mptTester2.checkMPTokenAmount(alice, 100));
5441 BEAST_EXPECT(mptTester1.checkMPTokenOutstandingAmount(130));
5442 BEAST_EXPECT(mptTester2.checkMPTokenOutstandingAmount(420));
5443 BEAST_EXPECT(mptTester2.checkMPTokenAmount(carol, 190));
5444 BEAST_EXPECT(mptTester1.checkMPTokenAmount(bob, 30));
5445 BEAST_EXPECT(mptTester2.checkMPTokenAmount(bob, 130));
5446 }
5447
5448 // MPT/MPT, issuer owns the offer
5449 {
5450 Env env{*this, features};
5451
5452 MPTTester mptTester1(env, gw, {.holders = {carol, bob}});
5453 mptTester1.create(
5454 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
5455 auto const mpt1 = mptTester1["MPT1"];
5456
5457 MPTTester mptTester2(env, gw, {.holders = {carol, bob}, .fund = false});
5458 mptTester2.create(
5459 {.ownerCount = 2, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
5460 auto const mpt2 = mptTester2["MPT2"];
5461
5462 mptTester2.authorize({.account = carol});
5463 mptTester2.pay(gw, carol, 200);
5464
5465 mptTester1.authorize({.account = bob});
5466 mptTester2.authorize({.account = bob});
5467 mptTester2.pay(gw, bob, 200);
5468
5469 env(offer(gw, mpt2(100), mpt1(100)));
5470 env.close();
5471 BEAST_EXPECT(expectOffers(env, gw, 1, {{Amounts{mpt2(100), mpt1(100)}}}));
5472
5473 // holder to holder
5474 env(pay(carol, bob, mpt1(10)),
5475 test::jtx::Path(~mpt1),
5476 Sendmax(mpt2(10)),
5477 Txflags(tfPartialPayment));
5478 env.close();
5479
5480 BEAST_EXPECT(expectOffers(env, gw, 1));
5481 BEAST_EXPECT(mptTester1.checkMPTokenOutstandingAmount(10));
5482 BEAST_EXPECT(mptTester2.checkMPTokenOutstandingAmount(390));
5483 BEAST_EXPECT(mptTester2.checkMPTokenAmount(carol, 190));
5484 BEAST_EXPECT(mptTester1.checkMPTokenAmount(bob, 10));
5485
5486 // issuer to holder
5487 env(pay(gw, bob, mpt1(20)),
5488 test::jtx::Path(~mpt1),
5489 Sendmax(mpt2(20)),
5490 Txflags(tfPartialPayment));
5491 env.close();
5492
5493 BEAST_EXPECT(expectOffers(env, gw, 1));
5494 BEAST_EXPECT(mptTester1.checkMPTokenOutstandingAmount(30));
5495 BEAST_EXPECT(mptTester2.checkMPTokenOutstandingAmount(390));
5496 BEAST_EXPECT(mptTester2.checkMPTokenAmount(carol, 190));
5497 BEAST_EXPECT(mptTester1.checkMPTokenAmount(bob, 30));
5498
5499 // holder to issuer
5500 env(pay(bob, gw, mpt1(70)),
5501 test::jtx::Path(~mpt1),
5502 Sendmax(mpt2(70)),
5503 Txflags(tfPartialPayment));
5504 env.close();
5505
5506 BEAST_EXPECT(expectOffers(env, gw, 0));
5507 BEAST_EXPECT(mptTester1.checkMPTokenOutstandingAmount(30));
5508 BEAST_EXPECT(mptTester2.checkMPTokenOutstandingAmount(320));
5509 BEAST_EXPECT(mptTester2.checkMPTokenAmount(carol, 190));
5510 BEAST_EXPECT(mptTester1.checkMPTokenAmount(bob, 30));
5511 BEAST_EXPECT(mptTester2.checkMPTokenAmount(bob, 130));
5512 }
5513
5514 // MPT/MPT, different issuer
5515 {
5516 Env env{*this, features};
5517 Account const gw1{"gw1"};
5518
5519 MPTTester mptTester1(env, gw, {.holders = {alice, carol, bob}});
5520 mptTester1.create(
5521 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
5522 auto const mpt1 = mptTester1["MPT1"];
5523
5524 env.fund(XRP(1'000), gw1);
5525 MPTTester mptTester2(env, gw1, {.holders = {alice, carol, bob}, .fund = false});
5526 mptTester2.create(
5527 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
5528 auto const mpt2 = mptTester2["MPT2"];
5529
5530 mptTester1.authorize({.account = alice});
5531 mptTester1.pay(gw, alice, 200);
5532 mptTester2.authorize({.account = alice});
5533
5534 mptTester2.authorize({.account = carol});
5535 mptTester2.pay(gw1, carol, 200);
5536
5537 mptTester1.authorize({.account = bob});
5538 mptTester1.pay(gw, bob, 200);
5539 mptTester2.authorize({.account = bob});
5540 mptTester2.pay(gw1, bob, 200);
5541
5542 mptTester1.authorize({.account = gw1});
5543 mptTester1.pay(gw, gw1, 200);
5544
5545 mptTester2.authorize({.account = gw});
5546 mptTester2.pay(gw1, gw, 200);
5547
5548 env(offer(alice, mpt2(100), mpt1(100)));
5549 env.close();
5550 BEAST_EXPECT(expectOffers(env, alice, 1, {{Amounts{mpt2(100), mpt1(100)}}}));
5551
5552 env(pay(carol, bob, mpt1(10)),
5553 test::jtx::Path(~mpt1),
5554 Sendmax(mpt2(10)),
5555 Txflags(tfPartialPayment));
5556 env.close();
5557 BEAST_EXPECT(expectOffers(env, alice, 1));
5558 BEAST_EXPECT(mptTester1.checkMPTokenOutstandingAmount(600));
5559 BEAST_EXPECT(mptTester2.checkMPTokenOutstandingAmount(600));
5560 BEAST_EXPECT(mptTester1.checkMPTokenAmount(gw1, 200));
5561 BEAST_EXPECT(mptTester2.checkMPTokenAmount(gw, 200));
5562 BEAST_EXPECT(mptTester2.checkMPTokenAmount(carol, 190));
5563 BEAST_EXPECT(mptTester1.checkMPTokenAmount(bob, 210));
5564 BEAST_EXPECT(mptTester2.checkMPTokenAmount(bob, 200));
5565 BEAST_EXPECT(mptTester1.checkMPTokenAmount(alice, 190));
5566 BEAST_EXPECT(mptTester2.checkMPTokenAmount(alice, 10));
5567
5568 env(pay(bob, gw, mpt1(10)),
5569 test::jtx::Path(~mpt1),
5570 Sendmax(mpt2(10)),
5571 Txflags(tfPartialPayment));
5572 env.close();
5573 BEAST_EXPECT(expectOffers(env, alice, 1));
5574 BEAST_EXPECT(mptTester1.checkMPTokenOutstandingAmount(590));
5575 BEAST_EXPECT(mptTester2.checkMPTokenOutstandingAmount(600));
5576 BEAST_EXPECT(mptTester1.checkMPTokenAmount(gw1, 200));
5577 BEAST_EXPECT(mptTester2.checkMPTokenAmount(gw, 200));
5578 BEAST_EXPECT(mptTester1.checkMPTokenAmount(bob, 210));
5579 BEAST_EXPECT(mptTester2.checkMPTokenAmount(bob, 190));
5580 BEAST_EXPECT(mptTester1.checkMPTokenAmount(alice, 180));
5581 BEAST_EXPECT(mptTester2.checkMPTokenAmount(alice, 20));
5582
5583 env(pay(gw, bob, mpt1(10)),
5584 test::jtx::Path(~mpt1),
5585 Sendmax(mpt2(10)),
5586 Txflags(tfPartialPayment));
5587 env.close();
5588 BEAST_EXPECT(expectOffers(env, alice, 1));
5589 BEAST_EXPECT(mptTester1.checkMPTokenOutstandingAmount(590));
5590 BEAST_EXPECT(mptTester2.checkMPTokenOutstandingAmount(600));
5591 BEAST_EXPECT(mptTester1.checkMPTokenAmount(gw1, 200));
5592 BEAST_EXPECT(mptTester2.checkMPTokenAmount(gw, 190));
5593 BEAST_EXPECT(mptTester1.checkMPTokenAmount(bob, 220));
5594 BEAST_EXPECT(mptTester2.checkMPTokenAmount(bob, 190));
5595 BEAST_EXPECT(mptTester1.checkMPTokenAmount(alice, 170));
5596 BEAST_EXPECT(mptTester2.checkMPTokenAmount(alice, 30));
5597
5598 env(pay(bob, gw1, mpt1(10)),
5599 test::jtx::Path(~mpt1),
5600 Sendmax(mpt2(10)),
5601 Txflags(tfPartialPayment));
5602 env.close();
5603 BEAST_EXPECT(expectOffers(env, alice, 1));
5604 BEAST_EXPECT(mptTester1.checkMPTokenOutstandingAmount(590));
5605 BEAST_EXPECT(mptTester2.checkMPTokenOutstandingAmount(600));
5606 BEAST_EXPECT(mptTester1.checkMPTokenAmount(gw1, 210));
5607 BEAST_EXPECT(mptTester2.checkMPTokenAmount(gw, 190));
5608 BEAST_EXPECT(mptTester1.checkMPTokenAmount(bob, 220));
5609 BEAST_EXPECT(mptTester2.checkMPTokenAmount(bob, 180));
5610 BEAST_EXPECT(mptTester1.checkMPTokenAmount(alice, 160));
5611 BEAST_EXPECT(mptTester2.checkMPTokenAmount(alice, 40));
5612
5613 env(pay(gw1, bob, mpt1(10)),
5614 test::jtx::Path(~mpt1),
5615 Sendmax(mpt2(10)),
5616 Txflags(tfPartialPayment));
5617 env.close();
5618 BEAST_EXPECT(expectOffers(env, alice, 1));
5619 BEAST_EXPECT(mptTester1.checkMPTokenOutstandingAmount(590));
5620 BEAST_EXPECT(mptTester2.checkMPTokenOutstandingAmount(610));
5621 BEAST_EXPECT(mptTester1.checkMPTokenAmount(gw1, 210));
5622 BEAST_EXPECT(mptTester2.checkMPTokenAmount(gw, 190));
5623 BEAST_EXPECT(mptTester1.checkMPTokenAmount(bob, 230));
5624 BEAST_EXPECT(mptTester2.checkMPTokenAmount(bob, 180));
5625 BEAST_EXPECT(mptTester1.checkMPTokenAmount(alice, 150));
5626 BEAST_EXPECT(mptTester2.checkMPTokenAmount(alice, 50));
5627
5628 env(pay(gw, gw1, mpt1(10)),
5629 test::jtx::Path(~mpt1),
5630 Sendmax(mpt2(10)),
5631 Txflags(tfPartialPayment));
5632 env.close();
5633 BEAST_EXPECT(expectOffers(env, alice, 1));
5634 BEAST_EXPECT(mptTester1.checkMPTokenOutstandingAmount(590));
5635 BEAST_EXPECT(mptTester2.checkMPTokenOutstandingAmount(610));
5636 BEAST_EXPECT(mptTester1.checkMPTokenAmount(gw1, 220));
5637 BEAST_EXPECT(mptTester2.checkMPTokenAmount(gw, 180));
5638 BEAST_EXPECT(mptTester1.checkMPTokenAmount(alice, 140));
5639 BEAST_EXPECT(mptTester2.checkMPTokenAmount(alice, 60));
5640
5641 env(pay(gw1, gw, mpt1(40)),
5642 test::jtx::Path(~mpt1),
5643 Sendmax(mpt2(40)),
5644 Txflags(tfPartialPayment));
5645 env.close();
5646 BEAST_EXPECT(expectOffers(env, alice, 0));
5647 BEAST_EXPECT(mptTester1.checkMPTokenOutstandingAmount(550));
5648 BEAST_EXPECT(mptTester2.checkMPTokenOutstandingAmount(650));
5649 BEAST_EXPECT(mptTester1.checkMPTokenAmount(gw1, 220));
5650 BEAST_EXPECT(mptTester2.checkMPTokenAmount(gw, 180));
5651 BEAST_EXPECT(mptTester1.checkMPTokenAmount(alice, 100));
5652 BEAST_EXPECT(mptTester2.checkMPTokenAmount(alice, 100));
5653 }
5654
5655 // MPT/IOU IOU/mpt1
5656 {
5657 Env env = pathTestEnv(*this);
5658 Account const gw1{"gw1"};
5659 Account const gw2{"gw2"};
5660 Account const dan{"dan"};
5661 env.fund(XRP(1'000), gw2);
5662 auto const usd = gw2["USD"];
5663
5664 MPTTester mptTester(env, gw, {.holders = {alice, carol}});
5665 mptTester.create(
5666 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
5667 auto const mpt = mptTester["MPT"];
5668 mptTester.authorize({.account = alice});
5669 mptTester.authorize({.account = carol});
5670 mptTester.pay(gw, carol, 200);
5671
5672 MPTTester mptTester1(env, gw1, {.holders = {bob, dan}});
5673 mptTester1.create(
5674 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
5675 auto const mpt1 = mptTester1["MPT1"];
5676 mptTester1.authorize({.account = bob});
5677 mptTester1.pay(gw1, bob, 200);
5678 mptTester1.authorize({.account = dan});
5679
5680 env(trust(alice, usd(400)));
5681 env(pay(gw2, alice, usd(200)));
5682 env(trust(bob, usd(400)));
5683
5684 env(offer(alice, mpt(100), usd(100)));
5685 env(offer(bob, usd(100), mpt1(100)));
5686 env.close();
5687
5688 env(pay(carol, dan, mpt1(100)),
5689 Sendmax(mpt(100)),
5690 Path(~usd, ~mpt1),
5691 Txflags(tfPartialPayment | tfNoRippleDirect));
5692 env.close();
5693 BEAST_EXPECT(expectOffers(env, alice, 0));
5694 BEAST_EXPECT(expectOffers(env, bob, 0));
5695 BEAST_EXPECT(mptTester.checkMPTokenAmount(carol, 100));
5696 BEAST_EXPECT(mptTester1.checkMPTokenAmount(dan, 100));
5697 }
5698
5699 // XRP/MPT AMM
5700 {
5701 Env env{*this, features};
5702
5703 fund(env, gw, {alice, carol, bob}, XRP(11'000), {usd(20'000)});
5704
5705 MPTTester mptTester(env, gw, {.fund = false});
5706
5707 mptTester.create(
5708 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
5709 auto const mpt = mptTester["MPT"];
5710
5711 mptTester.authorize({.account = alice});
5712 mptTester.authorize({.account = bob});
5713 mptTester.pay(gw, alice, 10'100);
5714
5715 AMM const amm(env, alice, XRP(10'000), mpt(10'100));
5716
5717 env(pay(carol, bob, mpt(100)),
5718 test::jtx::Path(~mpt),
5719 Sendmax(XRP(100)),
5720 Txflags(tfPartialPayment | tfNoRippleDirect));
5721 env.close();
5722
5723 BEAST_EXPECT(amm.expectBalances(XRP(10'100), mpt(10'000), amm.tokens()));
5724 BEAST_EXPECT(mptTester.checkMPTokenAmount(bob, 100));
5725 }
5726
5727 // IOU/MPT AMM
5728 {
5729 Env env{*this, features};
5730
5731 fund(env, gw, {alice, carol, bob}, XRP(11'000), {usd(20'000)});
5732
5733 MPTTester mptTester(env, gw, {.fund = false});
5734
5735 mptTester.create(
5736 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
5737 auto const mpt = mptTester["MPT"];
5738
5739 mptTester.authorize({.account = alice});
5740 mptTester.authorize({.account = bob});
5741 mptTester.pay(gw, alice, 10'100);
5742
5743 AMM const amm(env, alice, usd(10'000), mpt(10'100));
5744
5745 env(pay(carol, bob, mpt(100)),
5746 test::jtx::Path(~mpt),
5747 Sendmax(usd(100)),
5748 Txflags(tfPartialPayment | tfNoRippleDirect));
5749 env.close();
5750
5751 BEAST_EXPECT(amm.expectBalances(usd(10'100), mpt(10'000), amm.tokens()));
5752 BEAST_EXPECT(mptTester.checkMPTokenAmount(bob, 100));
5753 }
5754
5755 // MPT/MPT AMM cross-asset payment
5756 {
5757 Env env{*this, features};
5758 env.fund(XRP(20'000), gw, alice, carol, bob);
5759 env.close();
5760
5761 MPTTester mptTester1(env, gw, {.fund = false});
5762 mptTester1.create({.flags = tfMPTCanTransfer | tfMPTCanTrade});
5763 auto const mpt1 = mptTester1["MPT1"];
5764 mptTester1.authorize({.account = alice});
5765 mptTester1.authorize({.account = bob});
5766 mptTester1.pay(gw, alice, 10'100);
5767
5768 MPTTester mptTester2(env, gw, {.fund = false});
5769 mptTester2.create({.flags = tfMPTCanTransfer | tfMPTCanTrade});
5770 auto const mpt2 = mptTester2["MPT1"];
5771 mptTester2.authorize({.account = alice});
5772 mptTester2.authorize({.account = bob});
5773 mptTester2.authorize({.account = carol});
5774 mptTester2.pay(gw, alice, 10'100);
5775 mptTester2.pay(gw, carol, 100);
5776
5777 AMM const amm(env, alice, mpt2(10'000), mpt1(10'100));
5778
5779 env(pay(carol, bob, mpt1(100)),
5780 test::jtx::Path(~mpt1),
5781 Sendmax(mpt2(100)),
5782 Txflags(tfPartialPayment | tfNoRippleDirect));
5783 env.close();
5784
5785 BEAST_EXPECT(amm.expectBalances(mpt2(10'100), mpt1(10'000), amm.tokens()));
5786 BEAST_EXPECT(mptTester1.checkMPTokenAmount(bob, 100));
5787 }
5788
5789 // Multi-steps with AMM
5790 // EUR/MPT1 MPT1/MPT2 MPT2/USD USD/CRN AMM:CRN/MPT MPT/YAN
5791 {
5792 Env env{*this, features};
5793 auto const usd = gw["USD"];
5794 auto const eur = gw["EUR"];
5795 auto const crn = gw["CRN"];
5796 auto const yan = gw["YAN"];
5797
5798 fund(
5799 env,
5800 gw,
5801 {alice, carol, bob},
5802 XRP(1'000),
5803 {usd(1'000), eur(1'000), crn(2'000), yan(1'000)});
5804
5805 auto createMPT = [&]() -> std::pair<MPTTester, MPT> {
5806 MPTTester mptTester(env, gw, {.fund = false});
5807 mptTester.create({.flags = tfMPTCanTransfer | tfMPTCanTrade});
5808 mptTester.authorize({.account = alice});
5809 mptTester.pay(gw, alice, 2'000);
5810 return {mptTester, mptTester["MPT"]};
5811 };
5812
5813 auto const [mptTester1, mpt1] = createMPT();
5814 auto const [mptTester2, mpt2] = createMPT();
5815 auto const [mptTester3, mpt3] = createMPT();
5816
5817 env(offer(alice, eur(100), mpt1(101)));
5818 env(offer(alice, mpt1(101), mpt2(102)));
5819 env(offer(alice, mpt2(102), usd(103)));
5820 env(offer(alice, usd(103), crn(104)));
5821 env.close();
5822 AMM const amm(env, alice, crn(1'000), mpt3(1'104));
5823 env(offer(alice, mpt3(104), yan(100)));
5824
5825 env(pay(carol, bob, yan(100)),
5826 test::jtx::Path(~mpt1, ~mpt2, ~usd, ~crn, ~mpt3, ~yan),
5827 Sendmax(eur(100)),
5828 Txflags(tfPartialPayment | tfNoRippleDirect));
5829 env.close();
5830
5831 BEAST_EXPECT(env.balance(carol, eur) == eur(900));
5832 BEAST_EXPECT(env.balance(bob, yan) == yan(1'100));
5833 BEAST_EXPECT(amm.expectBalances(crn(1'104), mpt3(1'000), amm.tokens()));
5834 BEAST_EXPECT(expectOffers(env, alice, 0));
5835 }
5836
5837 // Multi-steps with AMM and MPT endpoints
5838 // mpt1/EUR EUR/mpt2 mpt2/USD USD/CRN AMM:CRN/MPT3 MPT3/MPT4
5839 {
5840 Env env{*this, features};
5841 auto const usd = gw["USD"];
5842 auto const eur = gw["EUR"];
5843 auto const crn = gw["CRN"];
5844
5845 fund(env, gw, {alice, carol, bob}, XRP(1'000), {usd(1'000), eur(1'000), crn(2'000)});
5846
5847 auto createMPT = [&]() -> std::pair<MPTTester, MPT> {
5848 MPTTester mptTester(env, gw, {.fund = false});
5849 mptTester.create({.flags = tfMPTCanTransfer | tfMPTCanTrade});
5850 mptTester.authorize({.account = alice});
5851 mptTester.pay(gw, alice, 2'000);
5852 return {mptTester, mptTester["MPT"]};
5853 };
5854
5855 auto const [mptTester1, mpt1] = createMPT();
5856 auto const [mptTester2, mpt2] = createMPT();
5857 auto const [mptTester3, mpt3] = createMPT();
5858 auto [mptTester4, mpt4] = createMPT();
5859 mptTester4.authorize({.account = bob});
5860
5861 env(offer(alice, eur(100), mpt1(101)));
5862 env(offer(alice, mpt1(101), mpt2(102)));
5863 env(offer(alice, mpt2(102), usd(103)));
5864 env(offer(alice, usd(103), crn(104)));
5865 env.close();
5866 AMM const amm(env, alice, crn(1'000), mpt3(1'104));
5867 env(offer(alice, mpt3(104), mpt4(100)));
5868
5869 env(pay(carol, bob, mpt4(100)),
5870 test::jtx::Path(~mpt1, ~mpt2, ~usd, ~crn, ~mpt3, ~mpt4),
5871 Sendmax(eur(100)),
5872 Txflags(tfPartialPayment | tfNoRippleDirect));
5873 env.close();
5874
5875 BEAST_EXPECT(env.balance(carol, eur) == eur(900));
5876 BEAST_EXPECT(mptTester4.checkMPTokenAmount(bob, 100));
5877 BEAST_EXPECT(amm.expectBalances(crn(1'104), mpt3(1'000), amm.tokens()));
5878 BEAST_EXPECT(expectOffers(env, alice, 0));
5879 }
5880
5881 // Check that limiting step reduces maximumAmount returned by
5882 // MPTEndpointStep::maxPaymentFlow()
5883 {
5884 Env env(*this, features);
5885
5886 env.fund(XRP(1'000), gw, alice, carol, bob);
5887
5888 MPTTester usdTester(env, gw, {.holders = {alice, carol, bob}, .fund = false});
5889 usdTester.create(
5890 {.maxAmt = 1'000,
5891 .authorize = MPTCreate::allHolders,
5892 .pay = {{{alice}, 1'000}},
5893 .flags = tfMPTCanTransfer | tfMPTCanTrade});
5894 auto const usd = usdTester["USD"];
5895
5896 MPTTester eurTester(env, gw, {.holders = {alice, carol, bob}, .fund = false});
5897 eurTester.create(
5898 {.maxAmt = 1'000,
5899 .authorize = {{alice, carol}},
5900 .pay = {{{carol}, 100}},
5901 .flags = tfMPTCanTransfer | tfMPTCanTrade});
5902 auto const eur = eurTester["EUR"];
5903
5904 env(offer(alice, eur(10), usd(10)));
5905
5906 env(pay(carol, bob, usd(10)),
5907 Sendmax(eur(10)),
5908 Path(~usd),
5909 Txflags(tfNoRippleDirect | tfPartialPayment));
5910 }
5911 {
5912 Env env(*this, features); // NOLINT TODO
5913 env.fund(XRP(1'000), gw, alice, carol, bob);
5914
5915 auto musd = MPTTester(
5916 {.env = env, .issuer = gw, .holders = {alice, carol, bob}, .maxAmt = 1'000});
5917 MPT const usd = musd;
5918 env(pay(gw, alice, usd(800)));
5919 env(offer(gw, XRP(300), usd(300)));
5920 env(pay(carol, bob, usd(300)),
5921 Sendmax(XRP(300)),
5922 Path(~usd),
5923 Txflags(tfPartialPayment));
5924 BEAST_EXPECT(musd.checkMPTokenAmount(bob, 200));
5925 BEAST_EXPECT(musd.checkMPTokenOutstandingAmount(1'000));
5926 // initial + offer - fees
5927 BEAST_EXPECT(env.balance(gw) == (XRP(1'000) + XRP(200) - txFee(env, 3)));
5928 }
5929 {
5930 Env env(*this, features);
5931 auto const eur = gw["EUR"];
5932 env.fund(XRP(1'000), gw, alice, carol, bob);
5933 env.close();
5934
5935 env(trust(alice, eur(1'000)));
5936 env(pay(gw, alice, eur(300)));
5937 env(trust(bob, eur(1'000)));
5938
5939 auto musd = MPTTester(
5940 {.env = env, .issuer = gw, .holders = {alice, carol, bob}, .maxAmt = 1'000});
5941 MPT const usd = musd;
5942
5943 env(pay(gw, alice, usd(800)));
5944 env(offer(gw, XRP(300), usd(300)));
5945 env(offer(alice, usd(300), eur(300)));
5946 env(pay(carol, bob, eur(300)),
5947 Sendmax(XRP(300)),
5948 Path(~usd, ~eur),
5949 Txflags(tfPartialPayment));
5950 BEAST_EXPECT(musd.checkMPTokenAmount(alice, 1'000));
5951 BEAST_EXPECT(musd.checkMPTokenOutstandingAmount(1'000));
5952 // initial + offer - fees
5953 BEAST_EXPECT(env.balance(gw) == (XRP(1'000) + XRP(200) - txFee(env, 4)));
5954 BEAST_EXPECT(env.balance(bob, eur) == eur(200));
5955 }
5956 }
5957
5958 void
5960 {
5961 testcase("Path");
5962 using namespace test::jtx;
5963 Account const gw{"gw"};
5964 Account const gw1{"gw1"};
5965 Account const alice{"alice"};
5966 Account const carol{"carol"};
5967 Account const bob{"bob"};
5968 Account const dan{"dan"};
5969 auto const usd = gw["USD"];
5970 auto const eur = gw1["EUR"];
5971
5972 // MPT can be a mpt end point step or a book-step
5973
5974 // Direct MPT payment
5975 {
5976 Env env = pathTestEnv(*this);
5977
5978 MPTTester mptTester(env, gw, {.holders = {dan, carol}});
5979 mptTester.create(
5980 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
5981 auto const mpt = mptTester["MPT"];
5982 mptTester.authorize({.account = dan});
5983 mptTester.authorize({.account = carol});
5984 mptTester.pay(gw, carol, 200);
5985
5986 auto const [pathSet, srcAmt, dstAmt] = findPaths(env, carol, dan, mpt(-1));
5987 BEAST_EXPECT(srcAmt == mpt(200));
5988 BEAST_EXPECT(dstAmt == mpt(200));
5989 // Direct payment, no path
5990 BEAST_EXPECT(pathSet.empty());
5991 }
5992
5993 // Cross-asset payment via XRP/MPT offer (one step)
5994 {
5995 Env env = pathTestEnv(*this);
5996
5997 env.fund(XRP(1'000), carol);
5998
5999 MPTTester mptTester(env, gw, {.holders = {alice, dan}});
6000
6001 mptTester.create(
6002 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
6003 auto const mpt = mptTester["MPT"];
6004
6005 mptTester.authorize({.account = alice});
6006 mptTester.authorize({.account = dan});
6007 mptTester.pay(gw, alice, 200);
6008
6009 env(offer(alice, XRP(100), mpt(100)));
6010 env.close();
6011
6012 auto const [pathSet, srcAmt, dstAmt] = findPaths(env, carol, dan, mpt(-1));
6013 BEAST_EXPECT(srcAmt == XRP(100));
6014 BEAST_EXPECT(dstAmt == mpt(100));
6015 if (BEAST_EXPECT(same(pathSet, stpath(ipe(mptTester.issuanceID())))))
6016 {
6017 // validate a payment works with the path
6018 env(pay(carol, dan, mpt(10)),
6019 Path(~mpt),
6020 Sendmax(XRP(10)),
6021 Txflags(tfNoRippleDirect | tfPartialPayment));
6022 }
6023 }
6024
6025 // Cross-asset payment via IOU/MPT offer (one step)
6026 {
6027 Env env = pathTestEnv(*this);
6028
6029 env.fund(XRP(1'000), carol);
6030 env.fund(XRP(1'000), gw);
6031
6032 MPTTester mptTester(env, gw1, {.holders = {alice, dan}});
6033
6034 mptTester.create(
6035 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
6036 auto const mpt = mptTester["MPT"];
6037
6038 mptTester.authorize({.account = alice});
6039 mptTester.authorize({.account = dan});
6040 mptTester.pay(gw1, alice, 200);
6041
6042 env(trust(alice, usd(400)));
6043 env(trust(carol, usd(400)));
6044 env(pay(gw, carol, usd(200)));
6045
6046 env(offer(alice, usd(100), mpt(100)));
6047 env.close();
6048
6049 // No sendMax
6050 STPathSet pathSet;
6051 STAmount srcAmt;
6052 STAmount dstAmt;
6053 std::tie(pathSet, srcAmt, dstAmt) = findPaths(env, carol, dan, mpt(-1));
6054 BEAST_EXPECT(srcAmt == usd(100));
6055 BEAST_EXPECT(dstAmt == mpt(100));
6056 if (BEAST_EXPECT(
6057 pathSet.size() == 1 && same(pathSet, stpath(gw, ipe(mptTester.issuanceID())))))
6058 {
6059 // Validate the payment works with the path
6060 env(pay(carol, dan, mpt(10)),
6061 Path(pathSet[0]),
6062 Sendmax(usd(10)),
6063 Txflags(tfNoRippleDirect | tfPartialPayment));
6064 }
6065
6066 // Include sendMax
6067 std::tie(pathSet, srcAmt, dstAmt) = findPaths(env, carol, dan, mpt(-1), usd(-1));
6068 BEAST_EXPECT(srcAmt == usd(90));
6069 BEAST_EXPECT(dstAmt == mpt(90));
6070 if (BEAST_EXPECT(
6071 pathSet.size() == 1 && same(pathSet, stpath(ipe(mptTester.issuanceID())))))
6072 {
6073 // validate a payment works with the path
6074 env(pay(carol, dan, mpt(10)),
6075 Path(pathSet[0]),
6076 Sendmax(usd(10)),
6077 Txflags(tfNoRippleDirect | tfPartialPayment));
6078 }
6079
6080 // Include source token
6081 std::tie(pathSet, srcAmt, dstAmt) =
6082 findPaths(env, carol, dan, mpt(-1), std::nullopt, usd.currency);
6083 BEAST_EXPECT(srcAmt == usd(80));
6084 BEAST_EXPECT(dstAmt == mpt(80));
6085 if (BEAST_EXPECT(
6086 pathSet.size() == 1 && same(pathSet, stpath(gw, ipe(mptTester.issuanceID())))))
6087 {
6088 // validate a payment works with the path
6089 env(pay(carol, dan, mpt(10)),
6090 Path(pathSet[0]),
6091 Sendmax(usd(10)),
6092 Txflags(tfNoRippleDirect | tfPartialPayment));
6093 }
6094 }
6095
6096 // Cross-asset payment via MPT/IOU offer (one step)
6097 {
6098 Env env = pathTestEnv(*this);
6099
6100 env.fund(XRP(1'000), dan);
6101 env.fund(XRP(1'000), gw);
6102
6103 MPTTester mptTester(env, gw1, {.holders = {carol, alice}});
6104
6105 mptTester.create(
6106 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
6107 auto const mpt = mptTester["MPT"];
6108
6109 mptTester.authorize({.account = carol});
6110 mptTester.authorize({.account = alice});
6111 mptTester.pay(gw1, carol, 200);
6112
6113 env(trust(dan, usd(400)));
6114 env(trust(alice, usd(400)));
6115 env(pay(gw, alice, usd(200)));
6116
6117 env(offer(alice, mpt(100), usd(100)));
6118 env.close();
6119
6120 // No sendMax
6121 STPathSet pathSet;
6122 STAmount srcAmt;
6123 STAmount dstAmt;
6124 std::tie(pathSet, srcAmt, dstAmt) = findPaths(env, carol, dan, usd(-1));
6125 BEAST_EXPECT(srcAmt == mpt(100));
6126 BEAST_EXPECT(dstAmt == usd(100));
6127 if (BEAST_EXPECT(pathSet.size() == 1 && same(pathSet, stpath(ipe(usd)))))
6128 {
6129 // Validate the payment works with the path
6130 env(pay(carol, dan, usd(10)),
6131 Path(pathSet[0]),
6132 Sendmax(mpt(10)),
6133 Txflags(tfNoRippleDirect | tfPartialPayment));
6134 }
6135
6136 // Include sendMax
6137 std::tie(pathSet, srcAmt, dstAmt) = findPaths(env, carol, dan, usd(-1), mpt(-1));
6138 BEAST_EXPECT(srcAmt == mpt(90));
6139 BEAST_EXPECT(dstAmt == usd(90));
6140 if (BEAST_EXPECT(pathSet.size() == 1 && same(pathSet, stpath(ipe(usd)))))
6141 {
6142 // validate a payment works with the path
6143 env(pay(carol, dan, usd(10)),
6144 Path(pathSet[0]),
6145 Sendmax(mpt(10)),
6146 Txflags(tfNoRippleDirect | tfPartialPayment));
6147 }
6148
6149 // Include source token
6150 std::tie(pathSet, srcAmt, dstAmt) =
6151 findPaths(env, carol, dan, usd(-1), std::nullopt, mpt.mpt());
6152 BEAST_EXPECT(srcAmt == mpt(80));
6153 BEAST_EXPECT(dstAmt == usd(80));
6154 if (BEAST_EXPECT(pathSet.size() == 1 && same(pathSet, stpath(ipe(usd)))))
6155 {
6156 // validate a payment works with the path
6157 env(pay(carol, dan, usd(10)),
6158 Path(pathSet[0]),
6159 Sendmax(mpt(10)),
6160 Txflags(tfNoRippleDirect | tfPartialPayment));
6161 }
6162 }
6163
6164 // Cross-asset payment via mpt1/MPT offer (one step)
6165 {
6166 Env env = pathTestEnv(*this);
6167
6168 MPTTester mptTester(env, gw, {.holders = {alice, dan}});
6169 MPTTester mptTester1(env, gw1, {.holders = {carol}});
6170
6171 mptTester.create(
6172 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
6173 auto const mpt = mptTester["MPT"];
6174 mptTester1.create(
6175 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
6176 auto const mpt1 = mptTester1["MPT1"];
6177
6178 mptTester.authorize({.account = alice});
6179 mptTester.authorize({.account = dan});
6180 mptTester.pay(gw, alice, 200);
6181
6182 mptTester1.authorize({.account = carol});
6183 mptTester1.authorize({.account = alice});
6184 mptTester1.pay(gw1, carol, 200);
6185
6186 env(offer(alice, mpt1(100), mpt(100)));
6187 env.close();
6188
6189 // No sendMax
6190 STPathSet pathSet;
6191 STAmount srcAmt;
6192 STAmount dstAmt;
6193 std::tie(pathSet, srcAmt, dstAmt) = findPaths(env, carol, dan, mpt(-1));
6194 BEAST_EXPECT(srcAmt == mpt1(100));
6195 BEAST_EXPECT(dstAmt == mpt(100));
6196 if (BEAST_EXPECT(
6197 pathSet.size() == 1 && same(pathSet, stpath(ipe(mptTester.issuanceID())))))
6198 {
6199 // validate a payment works with the path
6200 env(pay(carol, dan, mpt(10)),
6201 Path(pathSet[0]),
6202 Sendmax(mpt1(10)),
6203 Txflags(tfNoRippleDirect | tfPartialPayment));
6204 }
6205
6206 // Include sendMax
6207 std::tie(pathSet, srcAmt, dstAmt) = findPaths(env, carol, dan, mpt(-1), mpt1(-1));
6208 BEAST_EXPECT(srcAmt == mpt1(90));
6209 BEAST_EXPECT(dstAmt == mpt(90));
6210 if (BEAST_EXPECT(
6211 pathSet.size() == 1 && same(pathSet, stpath(ipe(mptTester.issuanceID())))))
6212 {
6213 // validate a payment works with the path
6214 env(pay(carol, dan, mpt(10)),
6215 Path(pathSet[0]),
6216 Sendmax(mpt1(10)),
6217 Txflags(tfNoRippleDirect | tfPartialPayment));
6218 }
6219
6220 // Include source token
6221 std::tie(pathSet, srcAmt, dstAmt) =
6222 findPaths(env, carol, dan, mpt(-1), std::nullopt, mpt1.mpt());
6223 BEAST_EXPECT(srcAmt == mpt1(80));
6224 BEAST_EXPECT(dstAmt == mpt(80));
6225 if (BEAST_EXPECT(
6226 pathSet.size() == 1 && same(pathSet, stpath(ipe(mptTester.issuanceID())))))
6227 {
6228 // validate a payment works with the path
6229 env(pay(carol, dan, mpt(10)),
6230 Path(pathSet[0]),
6231 Sendmax(mpt1(10)),
6232 Txflags(tfNoRippleDirect | tfPartialPayment));
6233 }
6234 }
6235
6236 // Cross-asset payment via offers (two steps)
6237 {
6238 Env env = pathTestEnv(*this);
6239
6240 env.fund(XRP(1'000), carol);
6241 env.fund(XRP(1'000), dan);
6242
6243 MPTTester mptTester(env, gw, {.holders = {alice, bob}});
6244
6245 mptTester.create(
6246 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
6247 auto const mpt = mptTester["MPT"];
6248
6249 mptTester.authorize({.account = alice});
6250 mptTester.authorize({.account = bob});
6251 mptTester.pay(gw, alice, 200);
6252 mptTester.pay(gw, bob, 200);
6253
6254 env(trust(bob, usd(200)));
6255 env(pay(gw, bob, usd(100)));
6256 env(trust(dan, usd(200)));
6257 env(trust(alice, usd(200)));
6258
6259 env(offer(alice, XRP(100), mpt(100)));
6260 env(offer(bob, mpt(100), usd(100)));
6261 env.close();
6262
6263 // No sendMax
6264 STPathSet pathSet;
6265 STAmount srcAmt;
6266 STAmount dstAmt;
6267 std::tie(pathSet, srcAmt, dstAmt) = findPaths(env, carol, dan, usd(-1));
6268 BEAST_EXPECT(srcAmt == XRP(100));
6269 BEAST_EXPECT(dstAmt == usd(100));
6270 if (BEAST_EXPECT(
6271 pathSet.size() == 1 &&
6272 same(pathSet, stpath(ipe(mptTester.issuanceID()), ipe(usd)))))
6273 {
6274 // validate a payment works with the path
6275 env(pay(carol, dan, usd(10)),
6276 Path(pathSet[0]),
6277 Sendmax(XRP(10)),
6278 Txflags(tfNoRippleDirect | tfPartialPayment));
6279 }
6280
6281 // Include sendMax
6282 std::tie(pathSet, srcAmt, dstAmt) = findPaths(env, carol, dan, usd(-1), XRP(100));
6283 BEAST_EXPECT(srcAmt == XRP(90));
6284 BEAST_EXPECT(dstAmt == usd(90));
6285 if (BEAST_EXPECT(
6286 pathSet.size() == 1 &&
6287 same(pathSet, stpath(ipe(mptTester.issuanceID()), ipe(usd)))))
6288 {
6289 // validate a payment works with the path
6290 env(pay(carol, dan, usd(10)),
6291 Path(pathSet[0]),
6292 Sendmax(XRP(10)),
6293 Txflags(tfNoRippleDirect | tfPartialPayment));
6294 }
6295 }
6296
6297 // Cross-asset payment via offers (two steps)
6298 // Start/End with mpt/mp1 and book steps in the middle
6299 {
6300 Env env = pathTestEnv(*this);
6301 Account const gw2{"gw2"};
6302 env.fund(XRP(1'000), gw2);
6303 auto const usd2 = gw2["USD"];
6304
6305 MPTTester mptTester(env, gw, {.holders = {alice, carol}});
6306 mptTester.create(
6307 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
6308 auto const mpt = mptTester["MPT"];
6309 mptTester.authorize({.account = alice});
6310 mptTester.authorize({.account = carol});
6311 mptTester.pay(gw, carol, 200);
6312
6313 MPTTester mptTester1(env, gw1, {.holders = {bob, dan}});
6314 mptTester1.create(
6315 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
6316 auto const mpt1 = mptTester1["MPT1"];
6317 mptTester1.authorize({.account = bob});
6318 mptTester1.pay(gw1, bob, 200);
6319 mptTester1.authorize({.account = dan});
6320
6321 env(trust(alice, usd2(400)));
6322 env(pay(gw2, alice, usd2(200)));
6323 env(trust(bob, usd2(400)));
6324
6325 env(offer(alice, mpt(100), usd2(100)));
6326 env(offer(bob, usd2(100), mpt1(100)));
6327 env.close();
6328
6329 // No sendMax
6330 STPathSet pathSet;
6331 STAmount srcAmt;
6332 STAmount dstAmt;
6333 std::tie(pathSet, srcAmt, dstAmt) = findPaths(env, carol, dan, mpt1(-1));
6334 BEAST_EXPECT(srcAmt == mpt(100));
6335 BEAST_EXPECT(dstAmt == mpt1(100));
6336 if (BEAST_EXPECT(
6337 pathSet.size() == 1 &&
6338 same(pathSet, stpath(ipe(usd2), ipe(mptTester1.issuanceID())))))
6339 {
6340 // validate a payment works with the path
6341 env(pay(carol, dan, mpt1(10)),
6342 Path(pathSet[0]),
6343 Sendmax(mpt(10)),
6344 Txflags(tfNoRippleDirect | tfPartialPayment));
6345 }
6346
6347 // Include sendMax
6348 std::tie(pathSet, srcAmt, dstAmt) = findPaths(env, carol, dan, mpt1(-1), mpt(-1));
6349 BEAST_EXPECT(srcAmt == mpt(90));
6350 BEAST_EXPECT(dstAmt == mpt1(90));
6351 if (BEAST_EXPECT(
6352 pathSet.size() == 1 &&
6353 same(pathSet, stpath(ipe(usd2), ipe(mptTester1.issuanceID())))))
6354 {
6355 // validate a payment works with the path
6356 env(pay(carol, dan, mpt1(10)),
6357 Path(pathSet[0]),
6358 Sendmax(mpt(10)),
6359 Txflags(tfNoRippleDirect | tfPartialPayment));
6360 }
6361
6362 // Include source token
6363 std::tie(pathSet, srcAmt, dstAmt) =
6364 findPaths(env, carol, dan, mpt1(-1), std::nullopt, mpt.mpt());
6365 BEAST_EXPECT(srcAmt == mpt(80));
6366 BEAST_EXPECT(dstAmt == mpt1(80));
6367 if (BEAST_EXPECT(
6368 pathSet.size() == 1 &&
6369 same(pathSet, stpath(ipe(usd2), ipe(mptTester1.issuanceID())))))
6370 {
6371 // validate a payment works with the path
6372 env(pay(carol, dan, mpt1(10)),
6373 Path(pathSet[0]),
6374 Sendmax(mpt(10)),
6375 Txflags(tfNoRippleDirect | tfPartialPayment));
6376 }
6377 }
6378
6379 // Cross-asset payment via offers (two steps)
6380 // Start/End with mpt/mp2 and book steps in the middle
6381 // offers are MPT/MPT
6382 {
6383 Env env = pathTestEnv(*this);
6384 Account const gw2{"gw2"};
6385 env.fund(XRP(1'000), gw, gw1, gw2, alice, bob, carol, dan);
6386
6387 MPTTester mptTester(env, gw, {.holders = {alice, carol}, .fund = false});
6388 mptTester.create(
6389 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
6390 auto const mpt = mptTester["MPT"];
6391 mptTester.authorize({.account = alice});
6392 mptTester.authorize({.account = carol});
6393 mptTester.pay(gw, carol, 200);
6394
6395 MPTTester mptTester1(env, gw1, {.holders = {bob, alice}, .fund = false});
6396 mptTester1.create({.ownerCount = 1, .flags = tfMPTCanTransfer | tfMPTCanTrade});
6397 auto const mpt1 = mptTester1["MPT1"];
6398 mptTester1.authorize({.account = alice});
6399 mptTester1.pay(gw1, alice, 200);
6400 mptTester1.authorize({.account = bob});
6401
6402 MPTTester mptTester2(env, gw2, {.holders = {bob, dan}, .fund = false});
6403 mptTester2.create({.ownerCount = 1, .flags = tfMPTCanTransfer | tfMPTCanTrade});
6404 auto const mpt2 = mptTester2["MPT2"];
6405 mptTester2.authorize({.account = bob});
6406 mptTester2.pay(gw2, bob, 200);
6407 mptTester2.authorize({.account = dan});
6408
6409 env(offer(alice, mpt(100), mpt1(100)));
6410 env(offer(bob, mpt1(100), mpt2(100)));
6411 env.close();
6412
6413 // No sendMax
6414 STPathSet pathSet;
6415 STAmount srcAmt;
6416 STAmount dstAmt;
6417 std::tie(pathSet, srcAmt, dstAmt) = findPaths(env, carol, dan, mpt2(-1));
6418 BEAST_EXPECT(srcAmt == mpt(100));
6419 BEAST_EXPECT(dstAmt == mpt2(100));
6420 if (BEAST_EXPECT(
6421 pathSet.size() == 1 &&
6422 same(
6423 pathSet,
6424 stpath(ipe(mptTester1.issuanceID()), ipe(mptTester2.issuanceID())))))
6425 {
6426 // validate a payment works with the path
6427 env(pay(carol, dan, mpt2(10)),
6428 Path(pathSet[0]),
6429 Sendmax(mpt(10)),
6430 Txflags(tfNoRippleDirect | tfPartialPayment));
6431 }
6432
6433 // Include sendMax
6434 std::tie(pathSet, srcAmt, dstAmt) = findPaths(env, carol, dan, mpt2(-1), mpt(-1));
6435 BEAST_EXPECT(srcAmt == mpt(90));
6436 BEAST_EXPECT(dstAmt == mpt2(90));
6437 if (BEAST_EXPECT(
6438 pathSet.size() == 1 &&
6439 same(
6440 pathSet,
6441 stpath(ipe(mptTester1.issuanceID()), ipe(mptTester2.issuanceID())))))
6442 {
6443 // validate a payment works with the path
6444 env(pay(carol, dan, mpt2(10)),
6445 Path(pathSet[0]),
6446 Sendmax(mpt(10)),
6447 Txflags(tfNoRippleDirect | tfPartialPayment));
6448 }
6449
6450 // Include source token
6451 std::tie(pathSet, srcAmt, dstAmt) =
6452 findPaths(env, carol, dan, mpt2(-1), std::nullopt, mpt.mpt());
6453 BEAST_EXPECT(srcAmt == mpt(80));
6454 BEAST_EXPECT(dstAmt == mpt2(80));
6455 if (BEAST_EXPECT(
6456 pathSet.size() == 1 &&
6457 same(
6458 pathSet,
6459 stpath(ipe(mptTester1.issuanceID()), ipe(mptTester2.issuanceID())))))
6460 {
6461 // validate a payment works with the path
6462 env(pay(carol, dan, mpt2(10)),
6463 Path(pathSet[0]),
6464 Sendmax(mpt(10)),
6465 Txflags(tfNoRippleDirect | tfPartialPayment));
6466 }
6467 }
6468
6469 // verify no MPT rippling
6470 {
6471 Env env = pathTestEnv(*this);
6472 Account const gw{"gw"};
6473 Account const gw1{"gw1"};
6474 Account const carol{"carol"};
6475 Account const bob{"bob"};
6476 Account const dan{"dan"};
6477 Account const john{"john"};
6478 Account const sean{"sean"};
6479
6480 env.fund(XRP(1'000'000), gw);
6481 env.fund(XRP(1'000'000), gw1);
6482 env.fund(XRP(1'000'000), carol);
6483 env.fund(XRP(1'000'000), dan);
6484 env.fund(XRP(1'000'000), bob);
6485 env.fund(XRP(1'000'000), john);
6486 env.fund(XRP(1'000'000), sean);
6487 env.close();
6488
6489 MPTTester usdTester(env, gw, {.holders = {carol, dan}, .fund = false});
6490 usdTester.create(
6491 {.authorize = MPTCreate::allHolders,
6492 .pay = {{MPTCreate::allHolders, 100}},
6493 .flags = tfMPTCanTransfer | tfMPTCanTrade});
6494 auto const usd = usdTester["USD"];
6495 env(offer(carol, XRP(100), usd(100)));
6496
6497 MPTTester gbpTester(env, gw, {.holders = {bob, sean}, .fund = false});
6498 gbpTester.create(
6499 {.authorize = MPTCreate::allHolders,
6500 .pay = {{{bob}, 100}},
6501 .flags = tfMPTCanTransfer | tfMPTCanTrade});
6502 auto const gbp = gbpTester["GBP"];
6503
6504 MPTTester usd1(env, gw1, {.holders = {bob, dan}, .fund = false});
6505 usd1.create(
6506 {.authorize = MPTCreate::allHolders,
6507 .pay = {{{dan}, 100}},
6508 .flags = tfMPTCanTransfer | tfMPTCanTrade});
6509 auto const usD1 = usd1["USD1"];
6510 env(offer(bob, usd1(100), gbp(100)));
6511
6512 // dan has USD/gw and USD1/gw. Had USD been IOU, it would have
6513 // been able to ripple through dan's account.
6514 auto const [pathSet, srcAmt, dstAmt] = findPaths(env, john, sean, gbp(-1), XRP(-1));
6515 BEAST_EXPECT(pathSet.empty());
6516
6517 env(pay(john, sean, gbp(10)),
6518 Sendmax(XRP(20)),
6519 Path(~usd, dan, gw1, ~gbp),
6520 Txflags(tfNoRippleDirect | tfPartialPayment),
6521 Ter(temBAD_PATH));
6522 }
6523 }
6524
6525 void
6527 {
6528 testcase("Check Create/Cash");
6529
6530 using namespace test::jtx;
6531 Account const gw{"gw"};
6532 Account const alice{"alice"};
6533 Account const carol{"carol"};
6534
6535 // MPTokensV2 is disabled
6536 {
6537 Env env{*this, features - featureMPTokensV2};
6538
6539 MPTTester mptTester(env, gw, {.holders = {alice}});
6540 mptTester.create(
6541 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
6542 auto const mpt = mptTester["MPT"];
6543 mptTester.authorize({.account = alice});
6544
6545 uint256 const checkId{keylet::check(gw, env.seq(gw)).key};
6546
6547 env(check::create(gw, alice, mpt(100)), Ter(temDISABLED));
6548 env.close();
6549
6550 env(check::cash(alice, checkId, mpt(100)), Ter(temDISABLED));
6551 env.close();
6552 }
6553
6554 // Insufficient funds
6555 {
6556 Env env{*this, features};
6557 Account const carol{"carol"};
6558
6559 MPTTester mptTester(env, gw, {.holders = {alice, carol}});
6560 mptTester.create(
6561 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
6562 auto const mpt = mptTester["MPT"];
6563 mptTester.authorize({.account = alice});
6564 mptTester.pay(gw, alice, 50);
6565
6566 uint256 const checkId{keylet::check(alice, env.seq(alice)).key};
6567
6568 // can create
6569 env(check::create(alice, carol, mpt(100)));
6570 env.close();
6571
6572 // can't cash since alice only has 50 of MPT
6573 env(check::cash(carol, checkId, mpt(100)), Ter(tecPATH_PARTIAL));
6574 env.close();
6575
6576 // can cash if DeliverMin is set
6577 // carol is not authorized, MPToken is authorized by CheckCash
6578 env(check::cash(carol, checkId, check::DeliverMin(mpt(50))));
6579 env.close();
6580 BEAST_EXPECT(mptTester.checkMPTokenAmount(carol, 50));
6581 BEAST_EXPECT(mptTester.checkMPTokenOutstandingAmount(50));
6582 }
6583
6584 // Exceed max amount
6585 {
6586 Env env{*this, features};
6587
6588 MPTTester mptTester(env, gw, {.holders = {alice}});
6589 mptTester.create(
6590 {.maxAmt = 100,
6591 .ownerCount = 1,
6592 .holderCount = 0,
6593 .flags = tfMPTCanTransfer | tfMPTCanTrade});
6594 auto const mpt = mptTester["MPT"];
6595
6596 uint256 const checkId{keylet::check(gw, env.seq(gw)).key};
6597
6598 // can create
6599 env(check::create(gw, alice, mpt(200)));
6600 env.close();
6601
6602 // can't cash since the outstanding amount exceeds max amount
6603 env(check::cash(alice, checkId, mpt(200)), Ter(tecPATH_PARTIAL));
6604 env.close();
6605
6606 // can cash if DeliverMin is set
6607 env(check::cash(alice, checkId, check::DeliverMin(mpt(100))));
6608 env.close();
6609 BEAST_EXPECT(mptTester.checkMPTokenAmount(alice, 100));
6610 BEAST_EXPECT(mptTester.checkMPTokenOutstandingAmount(100));
6611 }
6612
6613 // MPTokenIssuance object doesn't exist
6614 {
6615 Env env{*this, features};
6616 env.fund(XRP(1'000), gw, alice, carol);
6617 env(check::create(alice, carol, MPT(gw)(50)), Ter(tecOBJECT_NOT_FOUND));
6618 env.close();
6619 auto btc = MPTTester({.env = env, .issuer = gw});
6620 uint256 const chkId{getCheckIndex(gw, env.seq(gw))};
6621 env(check::cash(carol, chkId, MPT(gw)(1)), Ter(tecNO_ENTRY));
6622 env.close();
6623 }
6624
6625 // MPToken doesn't exist - can create check since MPToken will be
6626 // automatically created on cash check
6627 {
6628 Env env{*this, features};
6629 env.fund(XRP(1'000), gw, alice, carol);
6630 auto btc = MPTTester({.env = env, .issuer = gw});
6631 uint256 const chkId{getCheckIndex(alice, env.seq(alice))};
6632 env(check::create(alice, carol, btc(50)));
6633 env.close();
6634
6635 // But cashing fails if alice doesn't have MPToken
6636 env(check::cash(carol, chkId, btc(1)), Ter(tecPATH_PARTIAL));
6637 env.close();
6638 }
6639
6640 // MPTLock is set
6641 {
6642 Env env{*this, features};
6643 env.fund(XRP(1'000), gw, alice, carol);
6644 env.close();
6645 auto mpt = MPTTester(
6646 {.env = env,
6647 .issuer = gw,
6648 .holders = {alice, carol},
6649 .pay = 100,
6650 .flags = kMptDexFlags | tfMPTCanLock});
6651
6652 mpt.set({.flags = tfMPTLock});
6653
6654 // Create Check fails, holder or issuer as destination
6655 env(check::create(alice, carol, mpt(10)), Ter(tecLOCKED));
6656 env.close();
6657 env(check::create(gw, carol, mpt(10)), Ter(tecLOCKED));
6658 env.close();
6659
6660 mpt.set({.flags = tfMPTUnlock});
6661
6662 // Create Check succeeds, holder or issuer as destination
6663 uint256 const chkIdAlice{getCheckIndex(alice, env.seq(alice))};
6664 env(check::create(alice, carol, mpt(10)));
6665 env.close();
6666 uint256 const chkIdGw{getCheckIndex(gw, env.seq(gw))};
6667 env(check::create(gw, carol, mpt(10)));
6668 env.close();
6669
6670 mpt.set({.flags = tfMPTLock});
6671
6672 // Cash Check fails, holder and issuer env(check::cash(carol,
6673 // chkIdAlice, mpt(1)), ter(tecPATH_PARTIAL)); // tec is different
6674 // if the source is the issuer (this is consistent with IOU)
6675 env(check::cash(carol, chkIdGw, mpt(2)), Ter(tecLOCKED));
6676 env.close();
6677
6678 mpt.set({.flags = tfMPTUnlock});
6679
6680 // Cash Check succeeds, holder and issuer.
6681 env(check::cash(carol, chkIdAlice, mpt(1)));
6682 env(check::cash(carol, chkIdGw, mpt(2)));
6683
6684 // Individual lock
6685 mpt.set({.holder = alice, .flags = tfMPTLock});
6686 env(check::create(alice, carol, mpt(10)), Ter(tecLOCKED));
6687 env.close();
6688 env(check::create(carol, alice, mpt(10)), Ter(tecLOCKED));
6689 env.close();
6690
6691 mpt.set({.holder = alice, .flags = tfMPTUnlock});
6692 uint256 const chkId1{getCheckIndex(alice, env.seq(alice))};
6693 env(check::create(alice, carol, mpt(10)));
6694 env.close();
6695 uint256 const chkId2{getCheckIndex(gw, env.seq(gw))};
6696 env(check::create(gw, alice, mpt(10)));
6697 env.close();
6698 uint256 const chkId3{getCheckIndex(alice, env.seq(alice))};
6699 env(check::create(alice, gw, mpt(10)));
6700 env.close();
6701 uint256 const chkId4{getCheckIndex(gw, env.seq(gw))};
6702 env(check::create(gw, alice, mpt(10)));
6703 env.close();
6704 mpt.set({.holder = alice, .flags = tfMPTLock});
6705 env(check::cash(carol, chkId1, mpt(1)), Ter(tecPATH_PARTIAL));
6706 env(check::cash(alice, chkId2, mpt(1)), Ter(tecLOCKED));
6707 env(check::cash(gw, chkId3, mpt(1)), Ter(tecPATH_PARTIAL));
6708 env(check::cash(alice, chkId4, mpt(1)), Ter(tecLOCKED));
6709 }
6710
6711 // MPTRequireAuth flag is set and the account is not authorized.
6712 // Can create check, which is consistent with the trustlines.
6713 // It should fail on cash check.
6714 {
6715 Env env{*this, features};
6716 env.fund(XRP(1'000), gw, alice, carol);
6717 auto btc = MPTTester(
6718 {.env = env,
6719 .issuer = gw,
6720 .holders = {alice, carol},
6721 .flags = tfMPTRequireAuth | kMptDexFlags});
6722 uint256 const chkId{getCheckIndex(alice, env.seq(alice))};
6723 env(check::create(alice, carol, btc(50)));
6724 env.close();
6725
6726 // Authorize alice
6727 btc.authorize({.account = gw, .holder = alice});
6728 env(pay(gw, alice, btc(100)));
6729
6730 // carol is still not authorized
6731 env(check::cash(carol, chkId, btc(10)), Ter(tecNO_AUTH));
6732 env.close();
6733
6734 // authorize carol, can cash now
6735 btc.authorize({.account = gw, .holder = carol});
6736 env(check::cash(carol, chkId, btc(10)));
6737 env.close();
6738 }
6739
6740 // MPTCanTransfer is disabled
6741 {
6742 Env env{*this, features};
6743 env.fund(XRP(1'000), gw, alice, carol);
6744 env.close();
6745
6746 MPTTester mpt(
6747 {.env = env,
6748 .issuer = gw,
6749 .holders = {alice, carol},
6750 .flags = tfMPTCanTrade,
6751 .mutableFlags = tmfMPTCanEnableCanTransfer});
6752
6753 // src is issuer
6754 uint256 checkId{keylet::check(gw, env.seq(gw)).key};
6755
6756 // can create
6757 env(check::create(gw, alice, mpt(100)));
6758 env.close();
6759
6760 // can cash since source is issuer
6761 env(check::cash(alice, checkId, mpt(100)));
6762 env.close();
6763
6764 BEAST_EXPECT(env.balance(alice, mpt) == mpt(100));
6765 BEAST_EXPECT(env.balance(gw, mpt) == mpt(-100));
6766
6767 // dst is issuer
6768 checkId = keylet::check(alice, env.seq(alice)).key;
6769
6770 // can create
6771 env(check::create(alice, gw, mpt(100)));
6772 env.close();
6773
6774 // can cash since source is issuer
6775 env(check::cash(gw, checkId, mpt(100)));
6776 env.close();
6777
6778 BEAST_EXPECT(env.balance(alice, mpt) == mpt(0));
6779 BEAST_EXPECT(env.balance(gw, mpt) == mpt(0));
6780
6781 // neither src nor dst is issuer, can't create
6782 checkId = keylet::check(alice, env.seq(alice)).key;
6783 env(check::create(alice, carol, mpt(100)), Ter(tecNO_AUTH));
6784 env.close();
6785
6786 // can create now
6787 mpt.set({.account = gw, .mutableFlags = tmfMPTSetCanTransfer});
6788 checkId = keylet::check(alice, env.seq(alice)).key;
6789 env(check::create(alice, carol, mpt(100)));
6790 env.close();
6791 env(pay(gw, alice, mpt(10)));
6792 env.close();
6793
6794 // can cash since MPTCanTransfer is enabled
6795 env(check::cash(carol, checkId, mpt(10)));
6796 env.close();
6797 }
6798
6799 // MPTCanTrade is disabled
6800 {
6801 Env env{*this, features};
6802 env.fund(XRP(1'000), gw, alice, carol);
6803 env.close();
6804
6805 MPT const mpt = MPTTester(
6806 {.env = env,
6807 .issuer = gw,
6808 .holders = {alice, carol},
6809 .pay = 10,
6810 .flags = tfMPTCanTransfer});
6811
6812 uint256 const checkId{keylet::check(alice, env.seq(alice)).key};
6813
6814 // can create
6815 env(check::create(alice, carol, mpt(100)));
6816 env.close();
6817
6818 // can cash
6819 env(check::cash(carol, checkId, mpt(10)));
6820 env.close();
6821 }
6822
6823 // MPTokenIssuance object doesn't exist
6824 {
6825 Env env{*this, features};
6826 env.fund(XRP(1'000), gw, alice, carol);
6827 auto usd = MPTTester({.env = env, .issuer = gw, .holders = {alice}});
6828 uint256 const chkId{getCheckIndex(alice, env.seq(alice))};
6829 env(check::create(alice, carol, usd(1)));
6830 env.close();
6831
6832 // temMALFORMED because MPT is not USD. It doesn't matter if it
6833 // exists or not
6834 env(check::cash(carol, chkId, MPT(alice)(1)), Ter(temMALFORMED));
6835 env.close();
6836 }
6837
6838 // MPToken object doesn't exist and the account is not the issuer of MPT
6839 {
6840 Env env{*this, features};
6841 env.fund(XRP(1'000), gw, alice, carol);
6842
6843 auto btc = MPTTester({.env = env, .issuer = gw, .holders = {alice}, .pay = 1'000});
6844
6845 uint256 const chkId{getCheckIndex(alice, env.seq(alice))};
6846
6847 env(check::create(alice, carol, btc(1)));
6848 env.close();
6849
6850 // MPToken is automatically created
6851 env(check::cash(carol, chkId, btc(1)));
6852 env.close();
6853 }
6854
6855 // MPTRequireAuth flag is set and the account is not authorized.
6856 {
6857 Env env{*this, features};
6858 env.fund(XRP(1'000), gw, alice, carol);
6859
6860 auto btc = MPTTester(
6861 {.env = env,
6862 .issuer = gw,
6863 .holders = {alice},
6864 .flags = tfMPTRequireAuth | kMptDexFlags,
6865 .authHolder = true});
6866 uint256 const chkId{getCheckIndex(alice, env.seq(alice))};
6867 env(check::create(alice, carol, btc(1)));
6868 env.close();
6869
6870 env(check::cash(carol, chkId, btc(1)), Ter(tecPATH_PARTIAL));
6871 env.close();
6872 }
6873
6874 // Can create check if src/dst don't own MPT
6875 {
6876 Env env{*this, features};
6877
6878 MPTTester mptTester(env, gw);
6879 mptTester.create(
6880 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
6881 auto const mpt = mptTester["MPT"];
6882
6883 env.fund(XRP(1'000), alice, carol);
6884
6885 // src is issuer
6886 uint256 const checkId{keylet::check(alice, env.seq(alice)).key};
6887
6888 // can create
6889 env(check::create(alice, carol, mpt(100)));
6890 env.close();
6891
6892 // authorize/fund alice
6893 mptTester.authorize({.account = alice});
6894 mptTester.pay(gw, alice, 100);
6895
6896 // carol can cash the check. MPToken is created automatically
6897 env(check::cash(carol, checkId, mpt(100)));
6898 env.close();
6899
6900 BEAST_EXPECT(mptTester.checkMPTokenAmount(carol, 100));
6901 BEAST_EXPECT(mptTester.checkMPTokenOutstandingAmount(100));
6902 }
6903
6904 // Normal create/cash
6905 {
6906 Env env{*this, features};
6907
6908 MPTTester mptTester(env, gw, {.holders = {alice}});
6909 mptTester.create(
6910 {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
6911 auto const mpt = mptTester["MPT"];
6912 mptTester.authorize({.account = alice});
6913
6914 uint256 const checkId{keylet::check(gw, env.seq(gw)).key};
6915
6916 env(check::create(gw, alice, mpt(100)));
6917 env.close();
6918
6919 env(check::cash(alice, checkId, mpt(100)));
6920 env.close();
6921
6922 BEAST_EXPECT(mptTester.checkMPTokenAmount(alice, 100));
6923 BEAST_EXPECT(mptTester.checkMPTokenOutstandingAmount(100));
6924 }
6925 }
6926
6927 void
6929 {
6930 using namespace jtx;
6931 testcase("AMMClawback");
6932 Account const gw{"gw"};
6933 Account const alice{"alice"};
6934 auto const usd = gw["USD"];
6935
6936 // MPTokenIssuance object doesn't exist
6937 {
6938 Env env(*this, features);
6939 env.fund(XRP(1'000), gw, alice);
6940 MPTTester const btc({.env = env, .issuer = gw});
6941 AMM const amm(env, gw, btc(100), usd(100));
6942 env(amm::ammClawback(gw, alice, usd, MPT(alice), std::nullopt), Ter(terNO_AMM));
6943 env(amm::ammClawback(gw, alice, usd, btc, MPT(alice)(100)), Ter(temBAD_AMOUNT));
6944 }
6945
6946 // MPTLock flag is set and the account is not the issuer of MPT -
6947 // can still clawback since the issuer clawbacks
6948 {
6949 Env env(*this, features);
6950 env.fund(XRP(100'000), gw, alice);
6951 env.close();
6952
6953 env(fset(gw, asfAllowTrustLineClawback));
6954 env.close();
6955
6956 auto btc = MPTTester(
6957 {.env = env,
6958 .issuer = gw,
6959 .holders = {alice},
6960 .pay = 40'000,
6961 .flags = tfMPTCanLock | tfMPTCanClawback | kMptDexFlags});
6962
6963 env.trust(usd(10'000), alice);
6964 env(pay(gw, alice, usd(10'000)));
6965 env.close();
6966
6967 AMM amm(env, gw, btc(100), usd(100));
6968 env.close();
6969 amm.deposit(alice, 1'000);
6970 env.close();
6971
6972 btc.set({.flags = tfMPTLock});
6973
6974 env(amm::ammClawback(gw, alice, btc, usd, std::nullopt));
6975 }
6976
6977 // MPTRequireAuth flag is set and the account is not authorized -
6978 // can still clawback since the issuer clawbacks
6979 {
6980 Env env(*this, features);
6981 env.fund(XRP(100'000), gw, alice);
6982 env.close();
6983
6984 env(fset(gw, asfAllowTrustLineClawback));
6985 env.close();
6986
6987 auto btc = MPTTester(
6988 {.env = env,
6989 .issuer = gw,
6990 .holders = {alice},
6991 .pay = 40'000,
6992 .flags = tfMPTRequireAuth | tfMPTCanClawback | kMptDexFlags,
6993 .authHolder = true});
6994
6995 env.trust(usd(10'000), alice);
6996 env(pay(gw, alice, usd(10'000)));
6997 env.close();
6998
6999 AMM amm(env, gw, btc(100), usd(100));
7000 env.close();
7001 amm.deposit(alice, 1'000);
7002 env.close();
7003
7004 btc.authorize({.account = gw, .holder = alice, .flags = tfMPTUnauthorize});
7005
7006 env(amm::ammClawback(gw, alice, btc, usd, std::nullopt));
7007 }
7008
7009 // MPTCanTransfer is not set and the account is not the issuer of MPT -
7010 // can't clawback since a holder can't deposit
7011 {
7012 Env env(*this, features);
7013 env.fund(XRP(100'000), gw, alice);
7014 env.close();
7015
7016 env(fset(gw, asfAllowTrustLineClawback));
7017 env.close();
7018
7019 auto btc = MPTTester(
7020 {.env = env,
7021 .issuer = gw,
7022 .holders = {alice},
7023 .pay = 40'000,
7024 .flags = tfMPTCanClawback | tfMPTCanTrade,
7025 .authHolder = true});
7026
7027 env.trust(usd(10'000), alice);
7028 env(pay(gw, alice, usd(10'000)));
7029 env.close();
7030
7031 AMM amm(env, gw, btc(100), usd(100));
7032 env.close();
7033 // alice can't deposit since MPTCanTransfer is not set
7034 amm.deposit(DepositArg{.account = alice, .tokens = 1'000, .err = Ter(tecNO_AUTH)});
7035 env.close();
7036
7037 // can't clawback since alice is not an LP
7038 env(amm::ammClawback(gw, alice, btc, usd, std::nullopt), Ter(tecAMM_BALANCE));
7039 }
7040
7041 {
7042 Env env(*this, features);
7043 fund(env, gw, {alice}, XRP(1'000), {usd(1'000)});
7044 MPTTester mptTester(env, gw, {.fund = false});
7045 mptTester.create({.flags = tfMPTCanTransfer | tfMPTCanTrade});
7046 auto const mpt = mptTester["MPT"];
7047 AMM amm(env, gw, mpt(100), XRP(100));
7048 amm.deposit(DepositArg{.account = alice, .asset1In = XRP(10)});
7049 amm::ammClawback(gw, alice, MPTIssue(mptTester.issuanceID()), xrpIssue(), mpt(10));
7050 }
7051
7052 {
7053 Env env(*this, features);
7054 fund(env, gw, {alice}, XRP(1'000), {usd(1'000)});
7055 MPTTester mptTester(env, gw, {.fund = false});
7056 mptTester.create({.flags = tfMPTCanTransfer | tfMPTCanTrade});
7057 mptTester.authorize({.account = alice});
7058 mptTester.pay(gw, alice, 1'000);
7059 auto const mpt = mptTester["MPT"];
7060 AMM amm(env, gw, mpt(100), XRP(100));
7061 amm.deposit(DepositArg{.account = alice, .tokens = 10'000});
7062 amm::ammClawback(gw, alice, MPTIssue(mptTester.issuanceID()), xrpIssue(), mpt(10));
7063 }
7064
7065 // clawback one asset from MPT/MPT AMM. MPToken for another asset
7066 // is created for the Liquidity Provider
7067 {
7068 Env env(*this, features);
7069 env.fund(XRP(1'000), gw, alice);
7070 auto usd = MPTTester(
7071 {.env = env,
7072 .issuer = gw,
7073 .holders = {alice},
7074 .pay = 10'000,
7075 .flags = tfMPTCanClawback | kMptDexFlags});
7076 auto eur =
7077 MPTTester({.env = env, .issuer = gw, .flags = tfMPTCanClawback | kMptDexFlags});
7078 AMM amm(env, gw, usd(1'000), eur(1'000));
7079 amm.deposit({.account = alice, .asset1In = usd(1'000)});
7080 // MPToken doesn't exist
7081 BEAST_EXPECT(env.le(keylet::mptoken(eur.issuanceID(), alice)) == nullptr);
7082 env(amm::ammClawback(gw, alice, usd, eur, usd(100)));
7083 // MPToken is created
7084 BEAST_EXPECT(env.le(keylet::mptoken(eur.issuanceID(), alice)));
7085 }
7086 }
7087
7088 void
7090 {
7091 testcase("Basic AMM");
7092 using namespace jtx;
7093 Account const gw{"gw"};
7094 Account const alice{"alice"};
7095 Account const carol{"carol"};
7096 Account const bob{"bob"};
7097 auto const usd = gw["USD"];
7098
7099 // Create/deposit/withdraw
7100 {
7101 Env env{*this};
7102
7103 fund(env, gw, {alice, carol, bob}, XRP(1'000), {usd(1'000)});
7104
7105 MPTTester mptTester(env, gw, {.fund = false});
7106 mptTester.create({.flags = tfMPTCanTransfer | tfMPTCanTrade});
7107 auto const mpt = mptTester["MPT"];
7108 mptTester.authorize({.account = alice});
7109 mptTester.authorize({.account = carol});
7110 mptTester.pay(gw, alice, 1'000);
7111 mptTester.pay(gw, carol, 1'000);
7112
7113 MPTTester mptTester1(env, gw, {.fund = false});
7114 mptTester1.create({.flags = tfMPTCanTransfer | tfMPTCanTrade});
7115 auto const mpt1 = mptTester1["MPT1"];
7116 mptTester1.authorize({.account = alice});
7117 mptTester1.authorize({.account = carol});
7118 mptTester1.pay(gw, alice, 1'000);
7119 mptTester1.pay(gw, carol, 1'000);
7120
7122 {XRP(100), mpt(100), IOUAmount{100'000}},
7123 {usd(100), mpt(100), IOUAmount{100}},
7124 {mpt(100), mpt1(100), IOUAmount{100}}};
7125 for (auto& pool : pools)
7126 {
7127 AMM amm(env, gw, std::get<0>(pool), std::get<1>(pool));
7128 amm.deposit(alice, std::get<2>(pool));
7129 amm.deposit(carol, std::get<2>(pool));
7130 // bob doesn't own MPT
7131 amm.deposit(
7132 DepositArg{
7133 .account = bob, .tokens = std::get<2>(pool), .err = Ter(tecNO_AUTH)});
7134 amm.withdrawAll(alice);
7135 amm.withdrawAll(carol);
7136 amm.withdrawAll(gw);
7137 BEAST_EXPECT(!amm.ammExists());
7138 }
7139 }
7140
7141 // Payment, one step
7142 {
7143 Env env(*this);
7144
7145 env.fund(XRP(1'000), gw, alice, carol);
7146 env.close();
7147
7148 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice, carol}});
7149 MPT const eur = MPTTester({.env = env, .issuer = gw, .holders = {alice, carol}});
7150
7151 env(pay(gw, alice, eur(100)));
7152
7153 AMM const amm(env, gw, usd(1'100), eur(1'000));
7154
7155 env(pay(alice, carol, usd(100)), Sendmax(eur(100)));
7156
7157 BEAST_EXPECT(amm.expectBalances(usd(1'000), eur(1'100), amm.tokens()));
7158 BEAST_EXPECT(env.balance(carol, usd) == usd(100));
7159 BEAST_EXPECT(env.balance(alice, eur) == eur(0));
7160 }
7161
7162 // Payment, two steps
7163 {
7164 Env env(*this);
7165
7166 env.fund(XRP(1'000), gw, alice, carol);
7167 env.close();
7168
7169 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice, carol}});
7170 MPT const eur = MPTTester({.env = env, .issuer = gw, .holders = {alice, carol}});
7171 MPT const btc = MPTTester({.env = env, .issuer = gw, .holders = {alice, carol}});
7172 env(pay(gw, alice, eur(100)));
7173
7174 AMM const ammEurUsd(env, gw, eur(1'000), usd(1'100));
7175 AMM const ammUsdBtc(env, gw, usd(1'000), btc(1'100));
7176
7177 env(pay(alice, carol, btc(100)),
7178 Sendmax(eur(100)),
7179 Path(~usd, ~btc),
7180 Txflags(tfNoRippleDirect));
7181
7182 BEAST_EXPECT(ammEurUsd.expectBalances(usd(1'000), eur(1'100), ammEurUsd.tokens()));
7183 BEAST_EXPECT(ammUsdBtc.expectBalances(usd(1'100), btc(1'000), ammUsdBtc.tokens()));
7184 BEAST_EXPECT(env.balance(carol, btc) == btc(100));
7185 BEAST_EXPECT(env.balance(alice, eur) == eur(0));
7186 }
7187
7188 // Offer crossing
7189 {
7190 Env env(*this);
7191
7192 env.fund(XRP(1'000), gw, alice);
7193 env.close();
7194
7195 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice}});
7196 MPT const eur = MPTTester({.env = env, .issuer = gw, .holders = {alice}});
7197
7198 env(pay(gw, alice, eur(1'000)));
7199
7200 AMM const amm(env, gw, eur(1'000'000), usd(1'001'000));
7201
7202 env(offer(alice, usd(1'000), eur(1'000)));
7203
7204 BEAST_EXPECT(amm.expectBalances(usd(1'000'000), eur(1'001'000), amm.tokens()));
7205 BEAST_EXPECT(env.balance(alice, usd) == usd(1'000));
7206 BEAST_EXPECT(env.balance(alice, eur) == eur(0));
7207 }
7208
7209 {
7210 Env env(*this);
7211 env.fund(XRP(1'000'000), gw, alice, carol);
7212
7213 auto const increment = env.current()->fees().increment;
7214 auto const txfee = Fee(drops(increment));
7215 auto const badMPT = MPT(gw, 1'000);
7216
7217 auto const makeMPT = [&](std::uint32_t const flags,
7218 Holders holders = {},
7219 std::uint64_t const pay = 0,
7220 std::optional<std::uint32_t> const mutableFlags =
7221 std::nullopt) {
7222 return MPTTester(
7223 {.env = env,
7224 .issuer = gw,
7225 .holders = holders,
7226 .pay = pay ? std::optional<std::uint64_t>{pay} : std::nullopt,
7227 .flags = flags,
7228 .mutableFlags = mutableFlags});
7229 };
7230
7231 auto const makeDexMPT = [&](Holders holders = {}, std::uint64_t const pay = 0) {
7232 return makeMPT(
7233 tfMPTCanLock | kMptDexFlags,
7234 holders,
7235 pay,
7238 };
7239
7240 auto const makeNoTransferMPT = [&](Holders holders = {}, std::uint64_t const pay = 0) {
7241 return makeMPT(
7242 tfMPTCanLock | tfMPTCanTrade, holders, pay, tmfMPTCanEnableCanTransfer);
7243 };
7244
7245 auto const makeNoTradeMPT = [&](Holders holders = {}, std::uint64_t const pay = 0) {
7246 return makeMPT(
7247 tfMPTCanLock | tfMPTCanTransfer, holders, pay, tmfMPTCanEnableCanTrade);
7248 };
7249
7250 // AMMCreate
7251 {
7252 auto usd = makeDexMPT();
7253 auto eur = makeDexMPT({alice}, 1'000'000);
7254
7255 auto createDeleteAMM = [&](auto const& asset, Account const& lp) {
7256 AMM amm(
7257 env,
7258 lp,
7259 asset(1'000),
7260 eur(1'000),
7261 CreateArg{.fee = static_cast<std::uint32_t>(increment.value())});
7262 amm.withdrawAll(lp);
7263 BEAST_EXPECT(!amm.ammExists());
7264 };
7265
7266 auto createFail = [&](auto const& asset, Account const& account, auto const& err) {
7267 auto const createJv = AMM::createJv(account, asset(1'000), eur(1'000), 0);
7268 env(createJv, txfee, Ter(err));
7269 env.close();
7270 };
7271
7272 // MPTokenIssuance doesn't exist
7273 createFail(badMPT, alice, tecOBJECT_NOT_FOUND);
7274
7275 // MPToken doesn't exist
7276 createFail(usd, alice, tecNO_AUTH);
7277
7278 // alice authorizes MPToken, can create
7279 usd.authorize({.account = alice});
7280 env(pay(gw, alice, usd(1'000'000)), txfee);
7281 env.close();
7282 createDeleteAMM(usd, alice);
7283
7284 // MPTLock is set
7285 // alice and issuer can't create
7286 usd.set({.flags = tfMPTLock});
7287 createFail(usd, alice, tecLOCKED);
7288 createFail(usd, gw, tecLOCKED);
7289
7290 // MPTRequireAuth is set
7291 // alice is not authorized
7292 usd.set({.flags = tfMPTUnlock});
7293 usd.set({.mutableFlags = tmfMPTSetRequireAuth});
7294 createFail(usd, alice, tecNO_AUTH);
7295 // issuer can create
7296 createDeleteAMM(usd, gw);
7297
7298 // alice is authorized, can create
7299 usd.authorize({.account = gw, .holder = alice});
7300 createDeleteAMM(usd, alice);
7301
7302 // MPTCanTransfer is not set
7303 {
7304 auto usd2 = makeNoTransferMPT({alice}, 1'000'000);
7305
7306 // alice can't create
7307 createFail(usd2, alice, tecNO_AUTH);
7308 // issuer can create
7309 createDeleteAMM(usd2, gw);
7310 usd2.set({.mutableFlags = tmfMPTSetCanTransfer});
7311 // alice can create
7312 createDeleteAMM(usd2, alice);
7313 }
7314
7315 // MPTCanTrade is not set
7316 {
7317 auto usd3 = makeNoTradeMPT({alice}, 1'000'000);
7318
7319 // alice and issuer can't create
7320 createFail(usd3, alice, tecNO_PERMISSION);
7321 createFail(usd3, gw, tecNO_PERMISSION);
7322 usd3.set({.mutableFlags = tmfMPTSetCanTrade});
7323 // alice can create
7324 createDeleteAMM(usd3, alice);
7325 }
7326 }
7327
7328 // AMMDeposit
7329 {
7330 auto usd = makeDexMPT();
7331 auto eur = makeDexMPT({alice}, 1'000'000);
7332 AMM amm(env, gw, usd(1'000), eur(1'000));
7333
7334 // MPTokenIssuance doesn't exist
7335 amm.deposit(
7336 {.account = alice,
7337 .asset1In = badMPT(1),
7338 .asset2In = eur(1),
7339 .assets = std::make_pair(badMPT, eur),
7340 .err = Ter(terNO_AMM)});
7341
7342 // MPToken doesn't exist
7343 amm.deposit(
7344 {.account = carol,
7345 .asset1In = usd(1),
7346 .asset2In = eur(1),
7347 .err = Ter(tecNO_AUTH)});
7348
7349 // Fund carol for the AMMDeposit checks.
7350 usd.authorize({.account = carol});
7351 env(pay(gw, carol, usd(1'000'000)));
7352 eur.authorize({.account = carol});
7353 env(pay(gw, carol, eur(1'000'000)));
7354 env.close();
7355
7356 // MPTLock is set
7357 usd.set({.flags = tfMPTLock});
7358
7359 // alice and issuer can't deposit
7360 for (auto const& account : {carol, gw})
7361 {
7362 amm.deposit(
7363 {.account = account,
7364 .asset1In = usd(1),
7365 .asset2In = eur(1),
7366 .err = Ter(tecLOCKED)});
7367 amm.deposit(
7368 {.account = account,
7369 .asset1In = eur(1),
7370 .assets = std::make_pair(eur, usd),
7371 .err = Ter(tecLOCKED)});
7372 }
7373 usd.set({.flags = tfMPTUnlock});
7374
7375 // MPTRequireAuth is set
7376 // carol is not authorized by the issuer
7377 usd.set({.mutableFlags = tmfMPTSetRequireAuth});
7378 env.close();
7379 amm.deposit(
7380 {.account = carol,
7381 .asset1In = usd(1),
7382 .asset2In = eur(1),
7383 .err = Ter(tecNO_AUTH)});
7384 amm.deposit(
7385 {.account = carol,
7386 .asset1In = eur(1),
7387 .assets = std::make_pair(eur, usd),
7388 .err = Ter(tecNO_AUTH)});
7389 // issuer can deposit
7390 amm.deposit({.account = gw, .tokens = 1'000});
7391 // carol is authorized, can deposit
7392 usd.authorize({.account = gw, .holder = carol});
7393 amm.deposit({.account = carol, .tokens = 1'000});
7394 // Can't authorize or unauthorize AMM pseudo-account
7395 usd.authorize(
7396 {.account = gw,
7397 .holder = Account{"amm", amm.ammAccount()},
7398 .err = tecNO_PERMISSION});
7399 usd.authorize(
7400 {.account = gw,
7401 .holder = Account{"amm", amm.ammAccount()},
7402 .flags = tfMPTUnauthorize,
7403 .err = tecNO_PERMISSION});
7404
7405 // MPTCanTransfer is not set
7406 {
7407 auto usd2 = makeNoTransferMPT({carol}, 1'000'000);
7408 AMM amm2(env, gw, usd2(1'000), eur(1'000));
7409
7410 // carol can't deposit
7411 amm2.deposit(
7412 {.account = carol,
7413 .asset1In = usd2(1),
7414 .asset2In = eur(1),
7415 .err = Ter(tecNO_AUTH)});
7416 amm2.deposit(
7417 {.account = carol,
7418 .asset1In = eur(1),
7419 .assets = std::make_pair(eur, usd2),
7420 .err = Ter(tecNO_AUTH)});
7421 // issuer can deposit
7422 amm2.deposit({.account = gw, .tokens = 1'000});
7423 usd2.set({.mutableFlags = tmfMPTSetCanTransfer});
7424 // carol can deposit
7425 amm2.deposit({.account = carol, .tokens = 1'000});
7426 }
7427 }
7428
7429 // AMMWithdraw
7430 {
7431 auto usd = makeDexMPT();
7432 auto eur = makeDexMPT({carol}, 1'000'000);
7433 AMM amm(env, gw, usd(1'000), eur(1'000));
7434
7435 usd.authorize({.account = carol});
7436 env(pay(gw, carol, usd(1'000'000)));
7437 env.close();
7438 amm.deposit({.account = carol, .tokens = 1'000});
7439
7440 // MPTokenIssuance doesn't exist
7441 amm.withdraw(
7443 .account = carol,
7444 .asset1Out = badMPT(1),
7445 .asset2Out = eur(1),
7446 .assets = std::make_pair(badMPT, eur),
7447 .err = Ter(terNO_AMM)});
7448
7449 // MPToken doesn't exist - doesn't apply since MPToken is created
7450 // on withdraw in this case
7451
7452 // MPTLock is set
7453 usd.set({.flags = tfMPTLock});
7454 auto const fix330 = env.current()->rules().enabled(fixCleanup3_3_0);
7455
7456 // carol can't withdraw the locked token (any withdrawal type)
7457 amm.withdraw(
7458 {.account = carol,
7459 .asset1Out = usd(1),
7460 .asset2Out = eur(1),
7461 .err = Ter(tecLOCKED)});
7462 amm.withdraw({.account = carol, .tokens = 1'000, .err = Ter(tecLOCKED)});
7463 // can single withdraw the non-locked asset
7464 amm.withdraw(
7465 {.account = carol, .asset1Out = eur(1), .assets = std::make_pair(eur, usd)});
7466
7467 // post-fixCleanup3_3_0 the issuer can redeem even when locked.
7468 // Each successful withdrawal burns LP tokens, so replenish between
7469 // each type to keep gw's LP balance stable.
7470 auto const gwLockErr = fix330 ? Ter(tesSUCCESS) : Ter(tecLOCKED);
7471 auto const replenish = [&](IOUAmount tokens) {
7472 usd.set({.flags = tfMPTUnlock});
7473 amm.deposit({.account = gw, .tokens = tokens});
7474 usd.set({.flags = tfMPTLock});
7475 };
7476
7477 amm.withdraw(
7478 {.account = gw, .asset1Out = usd(1), .asset2Out = eur(1), .err = gwLockErr});
7479 if (fix330)
7480 replenish(IOUAmount{1});
7481
7482 amm.withdraw({.account = gw, .tokens = 1'000, .err = gwLockErr});
7483 if (fix330)
7484 replenish(IOUAmount{1'000});
7485
7486 // can single withdraw the non-locked asset
7487 amm.withdraw(
7488 {.account = gw, .asset1Out = eur(1), .assets = std::make_pair(eur, usd)});
7489
7490 usd.set({.flags = tfMPTUnlock});
7491
7492 // MPTRequireAuth is set
7493 usd.set({.mutableFlags = tmfMPTSetRequireAuth});
7494 usd.authorize({.account = gw, .holder = carol, .flags = tfMPTUnauthorize});
7495 // carol can't withdraw
7496 amm.withdraw(
7497 {.account = carol,
7498 .asset1Out = usd(1),
7499 .asset2Out = eur(1),
7500 .err = Ter(tecNO_AUTH)});
7501 // can withdraw another asset
7502 amm.withdraw(
7503 {.account = carol, .asset1Out = eur(1), .assets = std::make_pair(eur, usd)});
7504 // issuer can withdraw
7505 amm.withdraw({.account = gw, .asset1Out = usd(1), .asset2Out = eur(1)});
7506 // carol is authorized, can withdraw
7507 usd.authorize({.account = gw, .holder = carol});
7508 amm.withdraw({.account = carol, .asset1Out = usd(1), .asset2Out = eur(1)});
7509
7510 // MPTCanTransfer is not set, allow to withdraw
7511 {
7512 auto usd2 = makeNoTransferMPT({carol}, 1'000'000);
7513 AMM amm2(env, gw, usd2(1'000), eur(1'000));
7514
7515 // carol cannot deposit usd2 without MPTCanTransfer, so give her
7516 // LP tokens directly to test the withdraw path.
7517 env.trust(STAmount{amm2.lptIssue(), 1'000}, carol);
7518 env(pay(gw, carol, STAmount{amm2.lptIssue(), 100}));
7519 env.close();
7520
7521 // carol can withdraw
7522 amm2.withdraw({.account = carol, .asset1Out = usd2(1), .asset2Out = eur(1)});
7523 // can withdraw another asset
7524 amm2.withdraw(
7525 {.account = carol,
7526 .asset1Out = eur(1),
7527 .assets = std::make_pair(eur, usd2)});
7528 // issuer can withdraw
7529 amm2.withdraw({.account = gw, .asset1Out = usd2(1), .asset2Out = eur(1)});
7530 // Holder can't transfer to another holder
7531 env.fund(XRP(1'000), bob);
7532 usd2.authorize({.account = bob});
7533 env(pay(carol, bob, usd2(1)), Ter(tecNO_AUTH));
7534 usd2.authorize({.account = bob, .flags = tfMPTUnauthorize});
7535 // Can redeem
7536 env(pay(carol, gw, usd2(1)));
7537 usd2.set({.mutableFlags = tmfMPTSetCanTransfer});
7538 // carol can withdraw
7539 amm2.withdraw({.account = carol, .asset1Out = usd2(1), .asset2Out = eur(1)});
7540 }
7541
7542 // MPToken created on withdraw
7543 {
7544 auto usd3 = makeDexMPT();
7545 auto eur3 = makeDexMPT({carol}, 1'000'000);
7546 AMM amm3(env, gw, usd3(1'000), eur3(1'000));
7547
7548 BEAST_EXPECT(env.le(keylet::mptoken(usd3.issuanceID(), carol)) == nullptr);
7549 // single-deposit EUR
7550 amm3.deposit(
7551 {.account = carol,
7552 .asset1In = eur3(1'000),
7553 .assets = std::make_pair(eur3, usd3)});
7554 BEAST_EXPECT(env.le(keylet::mptoken(usd3.issuanceID(), carol)) == nullptr);
7555 // withdraw in USD to create MPToken
7556 amm3.withdraw({.account = carol, .asset1Out = usd3(100)});
7557 BEAST_EXPECT(env.le(keylet::mptoken(usd3.issuanceID(), carol)));
7558 }
7559 }
7560 }
7561 }
7562
7563 void
7565 {
7566 testcase("Fix Double adjustOwnerCount in AMMWithdraw");
7567
7568 using namespace jtx;
7569
7570 // Carol deposits XRP into an XRP/MPT pool, then withdraws MPT.
7571 // Carol has no MPToken before the withdrawal. If the bug exists,
7572 // her ownerCount will be inflated by +1 extra.
7573 Account const gw{"gw"};
7574 Account const alice{"alice"};
7575 Account const carol{"carol"};
7576 Env env(*this, all);
7577 env.fund(XRP(30'000), gw, alice, carol);
7578 env.close();
7579
7580 // Create MPT with DEX flags. Only alice is a holder initially.
7581 MPT const btc = MPTTester(
7582 {.env = env, .issuer = gw, .holders = {alice}, .pay = 20'000, .flags = kMptDexFlags});
7583
7584 // Alice creates XRP/MPT AMM pool
7585 AMM amm(env, alice, XRP(10'000), btc(10'000));
7586
7587 // Carol deposits XRP (single asset) into the pool.
7588 // Carol gets LP tokens but does NOT have an MPToken yet.
7589 auto const carolOwnersBefore = ownerCount(env, carol);
7590 amm.deposit(carol, XRP(1'000), std::nullopt, std::nullopt, tfSingleAsset);
7591 auto const carolOwnersAfterDeposit = ownerCount(env, carol);
7592 // Carol should have +1 for LP token trustline
7593 BEAST_EXPECT(carolOwnersAfterDeposit == carolOwnersBefore + 1);
7594
7595 auto const carolOwnersBeforeWithdraw = ownerCount(env, carol);
7596 // Carol withdraws single MPT asset. She doesn't have an MPToken,
7597 // so one must be created. Bug: ownerCount incremented twice.
7598 amm.withdraw({.account = carol, .asset1Out = btc(100), .flags = tfSingleAsset});
7599 auto const carolOwnersAfterWithdraw = ownerCount(env, carol);
7600
7601 // Expected: +1 for the new MPToken (so total increase = 1)
7602 BEAST_EXPECT(carolOwnersAfterWithdraw == carolOwnersBeforeWithdraw + 1);
7603 }
7604
7605 void
7607 {
7608 using namespace jtx;
7609 testcase("Trade and Transfer");
7610
7611 // Verify canMPTTradeAndTransfer validates the flags when from == to and from != to
7612
7613 Account const gw{"gw"};
7614 Account const alice{"alice"};
7615 Account const carol{"carol"};
7616 Env env(*this);
7617 env.fund(XRP(1'000), gw, alice, carol);
7618
7619 auto const checkCanTradeCanTransfer = [&](std::uint32_t const flags,
7620 TER const gwToGw,
7621 TER const gwToAlice,
7622 TER const aliceToAlice,
7623 TER const aliceToCarol) {
7624 MPTTester const mpt(
7625 {.env = env, .issuer = gw, .holders = {alice, carol}, .pay = 100, .flags = flags});
7626
7627 BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, gw, gw) == gwToGw);
7628 BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, gw, alice) == gwToAlice);
7629 BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, alice, alice) == aliceToAlice);
7630 BEAST_EXPECT(canMPTTradeAndTransfer(*env.current(), mpt, alice, carol) == aliceToCarol);
7631 };
7632
7633 // Both flags are enabled
7634 checkCanTradeCanTransfer(kMptDexFlags, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS);
7635
7636 // MPTCanTrade is disabled
7637 checkCanTradeCanTransfer(
7638 tfMPTCanTransfer,
7643
7644 // MPTCanTransfer is disabled
7645 checkCanTradeCanTransfer(tfMPTCanTrade, tesSUCCESS, tesSUCCESS, tecNO_AUTH, tecNO_AUTH);
7646
7647 // Both flags are disabled
7648 checkCanTradeCanTransfer(
7650 }
7651
7652public:
7653 void
7654 run() override
7655 {
7656 using namespace test::jtx;
7657 FeatureBitset const all{testableAmendments()};
7658
7660 // MPTokenIssuanceCreate
7661 testCreateValidation(all - featureSingleAssetVault);
7662 testCreateValidation(all - featurePermissionedDomains);
7664 testCreateEnabled(all - featureSingleAssetVault);
7665 testCreateEnabled(all);
7666
7667 // MPTokenIssuanceDestroy
7668 testDestroyValidation(all - featureSingleAssetVault);
7669 testDestroyValidation(all - featureSingleAssetVault - featureMPTokensV2);
7670 testDestroyValidation((all | featureSingleAssetVault) - featureMPTokensV2);
7671 testDestroyValidation(all - featureMPTokensV2);
7672 testDestroyValidation(all | featureSingleAssetVault);
7673 testDestroyEnabled(all - featureSingleAssetVault);
7674 testDestroyEnabled(all - featureSingleAssetVault - featureMPTokensV2);
7675 testDestroyEnabled((all | featureSingleAssetVault) - featureMPTokensV2);
7676 testDestroyEnabled(all - featureMPTokensV2);
7677 testDestroyEnabled(all | featureSingleAssetVault);
7678
7679 // MPTokenAuthorize
7680 testAuthorizeValidation(all - featureSingleAssetVault);
7681 testAuthorizeValidation(all - featureSingleAssetVault - featureMPTokensV2);
7682 testAuthorizeValidation((all | featureSingleAssetVault) - featureMPTokensV2);
7683 testAuthorizeValidation(all - featureMPTokensV2);
7684 testAuthorizeValidation(all | featureSingleAssetVault);
7685 testAuthorizeEnabled(all - featureSingleAssetVault);
7686 testAuthorizeEnabled(all - featureSingleAssetVault - featureMPTokensV2);
7687 testAuthorizeEnabled((all | featureSingleAssetVault) - featureMPTokensV2);
7688 testAuthorizeEnabled(all - featureMPTokensV2);
7689 testAuthorizeEnabled(all | featureSingleAssetVault);
7690
7691 // MPTokenIssuanceSet
7692 testSetValidation(all - featureSingleAssetVault - featureDynamicMPT);
7693 testSetValidation(all - featureSingleAssetVault);
7694 testSetValidation(all - featureDynamicMPT);
7695 testSetValidation(all - featurePermissionedDomains);
7696 testSetValidation(all);
7697
7698 testSetEnabled(all - featureSingleAssetVault);
7699 testSetEnabled(all);
7700
7701 // MPT clawback
7703 testClawbackValidation(all - featureMPTokensV2);
7704 testClawback(all);
7705 testClawback(all - featureMPTokensV2);
7706
7707 // Test Direct Payment
7708 testPayment(all);
7709 testPayment(all | featureSingleAssetVault);
7710 testPayment((all | featureSingleAssetVault) - featureMPTokensV2);
7711 testPayment(all - featureMPTokensV2);
7712
7713 testDepositPreauth(all);
7714 testDepositPreauth(all - featureCredentials);
7715 testDepositPreauth(all - featureMPTokensV2);
7716 testDepositPreauth(all - featureCredentials - featureMPTokensV2);
7717
7718 // Test MPT Amount is invalid in Tx, which don't support MPT
7719 testMPTInvalidInTx(all);
7721 // Test parsed MPTokenIssuanceID in API response metadata
7723
7724 // Test tokens equality
7726
7727 // Test helpers
7729
7730 // Dynamic MPT
7733 testMutateMPT(all);
7734 testMutateCanLock(all);
7738 testMutateCanTransfer(all - featureMPTokensV2);
7740
7741 // Test offer crossing
7742 testOfferCrossing(all);
7743
7744 // Test cross asset payment
7746
7747 // Test path finding
7748 testPath(all);
7749
7750 // Test checks
7751 testCheck(all);
7752
7753 // Add AMMClawback
7754 testAMMClawback(all);
7755
7756 // Test AMM
7757 testBasicAMM(all);
7758
7759 // Test Trade/Transfer
7761
7762 // Fixes
7764 }
7765};
7766
7768
7769} // namespace xrpl::test
A testsuite class.
Definition suite.h:50
void fail(String const &reason, char const *file, int line)
Record a failure.
Definition suite.h:522
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:149
std::string const & arg() const
Return the argument associated with the runner.
Definition suite.h:278
Lightweight wrapper to tag static string.
Definition json_value.h:44
Represents a JSON value.
Definition json_value.h:130
std::string asString() const
Returns the unquoted string value.
bool isMember(char const *key) const
Return true if the object has a member named key.
Editable, discardable view that can build metadata for one tx.
constexpr TIss const & get() const
Floating point representation of amounts with high dynamic range.
Definition IOUAmount.h:24
A currency issued by an account.
Definition Issue.h:13
std::int64_t value_type
Definition MPTAmount.h:22
static MPTAmount minPositiveAmount()
Definition MPTAmount.cpp:44
Slice slice() const noexcept
Definition PublicKey.h:103
Identifies fields.
Definition SField.h:130
constexpr TIss const & get() const
json::Value getJson(JsonOptions=JsonOptions::Values::None) const override
Definition STAmount.cpp:734
void setFieldAmount(SField const &field, STAmount const &)
Definition STObject.cpp:793
std::vector< STPath >::size_type size() const
Definition STPathSet.h:528
void sign(PublicKey const &publicKey, SecretKey const &secretKey, std::optional< std::reference_wrapper< SField const > > signatureTarget={})
Definition STTx.cpp:230
Slice slice() const noexcept
Definition Serializer.h:44
virtual beast::Journal getJournal(std::string const &name)=0
static TxFormats const & getInstance()
Definition TxFormats.cpp:57
SLE::pointer peek(Keylet const &k) override
Prepare to modify the SLE associated with key.
void testSetEnabled(FeatureBitset features)
void testMutateCanLock(FeatureBitset features)
void testClawback(FeatureBitset features)
void testMutateCanClawback(FeatureBitset features)
void testDepositPreauth(FeatureBitset features)
void testCheck(FeatureBitset features)
void testOfferCrossing(FeatureBitset features)
void testDestroyEnabled(FeatureBitset features)
void testPayment(FeatureBitset features)
void testAMMClawback(FeatureBitset features)
void testTxJsonMetaFields(FeatureBitset features)
void testMutateCanTransfer(FeatureBitset features)
void run() override
Runs the suite.
void testNonCanonicalMPTAmountCleanup(FeatureBitset features)
void testClawbackValidation(FeatureBitset features)
void testMutateRequireAuth(FeatureBitset features)
void testCrossAssetPayment(FeatureBitset features)
void testMultiSendMaximumAmount(FeatureBitset features)
void testDestroyValidation(FeatureBitset features)
void testInvalidCreateDynamic(FeatureBitset features)
void testMPTInvalidInTx(FeatureBitset features)
void testAuthorizeValidation(FeatureBitset features)
void testSetValidation(FeatureBitset features)
void testMutateMPT(FeatureBitset features)
void testBasicAMM(FeatureBitset features)
void testCreateEnabled(FeatureBitset features)
void testPath(FeatureBitset features)
void testCreateValidation(FeatureBitset features)
void testMutateCanEscrow(FeatureBitset features)
void testFixDoubleOwnerCount(FeatureBitset all)
void testInvalidSetDynamic(FeatureBitset features)
void testAuthorizeEnabled(FeatureBitset features)
Convenience class to test AMM functionality.
IOUAmount tokens() const
static json::Value createJv(AccountID const &account, STAmount const &asset1, STAmount const &asset2, std::uint16_t const &tfee)
Definition AMM.cpp:136
bool expectBalances(STAmount const &asset1, STAmount const &asset2, IOUAmount const &lpt, std::optional< AccountID > const &account=std::nullopt) const
Verify the AMM balances.
Definition AMM.cpp:269
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
std::string const & name() const
Return the name.
Definition jtx/Account.h:61
PublicKey const & pk() const
Return the public key.
Definition jtx/Account.h:68
AccountID id() const
Returns the Account ID.
Definition jtx/Account.h:85
Sets the DeliverMin on a JTx.
Definition delivermin.h:13
Set the domain on a JTx.
Definition domain.h:9
A transaction testing environment.
Definition Env.h:143
Application & app()
Definition Env.h:280
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
virtual void submit(JTx const &jt, std::source_location const &loc=std::source_location::current())
Submit an existing JTx.
Definition Env.cpp:387
void enableFeature(uint256 const feature)
Definition Env.cpp:682
void disableFeature(uint256 const feature)
Definition Env.cpp:690
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition Env.cpp:275
Account const & master
Definition Env.h:147
json::Value rpc(unsigned apiVersion, std::unordered_map< std::string, std::string > const &headers, std::string const &cmd, Args &&... args)
Execute an RPC command.
Definition Env.h:864
JTx jt(JsonValue &&jv, FN const &... fN)
Create a JTx from parameters.
Definition Env.h:566
PrettyAmount balance(Account const &account) const
Returns the XRP balance on an account.
Definition Env.cpp:201
void trust(STAmount const &amount, Account const &account)
Establish trust lines.
Definition Env.cpp:327
std::shared_ptr< STObject const > meta()
Return metadata for the last JTx.
Definition Env.cpp:511
std::shared_ptr< STTx const > tx() const
Return the tx data for the last JTx.
Definition Env.cpp:533
void memoize(Account const &account)
Associate AccountID with account.
Definition Env.cpp:174
void require(Args const &... args)
Check a set of requirements.
Definition Env.h:605
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:353
NetClock::time_point now()
Returns the current network time.
Definition Env.h:305
Set the fee on a JTx.
Definition fee.h:15
Match set account flags.
Definition flags.h:109
Converts to IOU Issue or STAmount.
Test helper for creating, mutating, and asserting MPT and confidential MPT ledger state.
Definition mpt.h:385
void set(MPTSet const &set={})
Definition mpt.cpp:482
bool checkMPTokenAmount(Account const &holder, std::int64_t expectedAmount) const
Definition mpt.cpp:607
void pay(Account const &src, Account const &dest, std::int64_t amount, std::optional< TER > err=std::nullopt, std::optional< std::vector< std::string > > credentials=std::nullopt)
Definition mpt.cpp:668
void create(MPTCreate const &arg=MPTCreate{})
Definition mpt.cpp:256
bool isTransferFeePresent() const
Definition mpt.cpp:662
bool checkMetadata(std::string const &metadata) const
Definition mpt.cpp:635
bool isMetadataPresent() const
Definition mpt.cpp:645
void authorize(MPTAuthorize const &arg=MPTAuthorize{})
Definition mpt.cpp:368
MPTID const & issuanceID() const
Definition mpt.h:576
void destroy(MPTDestroy const &arg=MPTDestroy{})
Definition mpt.cpp:334
bool checkTransferFee(std::uint16_t transferFee) const
Definition mpt.cpp:652
bool checkMPTokenOutstandingAmount(std::int64_t expectedAmount) const
Definition mpt.cpp:614
Converts to MPT Issue or STAmount.
Add a path.
Definition paths.h:39
Sets the SendMax on a JTx.
Definition sendmax.h:13
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition ter.h:13
Set the flags on a JTx.
Definition txflags.h:9
T empty(T... args)
T erase(T... args)
T insert(T... args)
T is_same_v
T lock(T... args)
T make_optional(T... args)
T make_pair(T... args)
T make_shared(T... args)
T max(T... args)
T min(T... args)
Keylet mptokenIssuance(std::uint32_t seq, AccountID const &issuer) noexcept
Definition Indexes.cpp:521
Keylet check(AccountID const &id, std::uint32_t seq) noexcept
A Check.
Definition Indexes.cpp:322
Keylet offer(AccountID const &id, std::uint32_t seq) noexcept
An offer from an account.
Definition Indexes.cpp:264
Keylet mptoken(MPTID const &issuanceID, AccountID const &holder) noexcept
Definition Indexes.cpp:533
json::Value ammClawback(Account const &issuer, Account const &holder, Asset const &asset, Asset const &asset2, std::optional< STAmount > const &amount)
Definition AMM.cpp:920
json::Value create(A const &account, A const &dest, STAmount const &sendMax)
Create a check.
json::Value cash(jtx::Account const &dest, uint256 const &checkId, STAmount const &amount)
Cash a check requiring that a specific amount be delivered.
Definition check.cpp:15
json::Value deleteCred(jtx::Account const &acc, jtx::Account const &subject, jtx::Account const &issuer, std::string_view credType)
Definition creds.cpp:40
json::Value accept(jtx::Account const &subject, jtx::Account const &issuer, std::string_view credType)
Definition creds.cpp:29
json::Value create(jtx::Account const &subject, jtx::Account const &issuer, std::string_view credType)
Definition creds.cpp:16
json::Value ledgerEntry(jtx::Env &env, jtx::Account const &subject, jtx::Account const &issuer, std::string_view credType)
Definition creds.cpp:56
json::Value authCredentials(jtx::Account const &account, std::vector< AuthorizeCredentials > const &auth)
Definition deposit.cpp:38
json::Value auth(Account const &account, Account const &auth)
Preauthorize for deposit.
Definition deposit.cpp:16
json::Value unauth(Account const &account, Account const &unauth)
Remove pre-authorization for deposit.
Definition deposit.cpp:27
json::Value create(AccountID const &account, AccountID const &to, STAmount const &amount)
Definition escrow.cpp:21
auto const kCondition
Definition escrow.h:74
auto const kFinishTime
Set the "FinishAfter" time tag on a JTx.
Definition escrow.h:69
std::array< std::uint8_t, 39 > const kCb1
Definition escrow.h:47
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)
jtx::Env pathTestEnv(beast::unit_test::Suite &suite)
auto const kMptDexFlags
Definition mpt.h:25
json::Value claw(Account const &account, STAmount const &amount, std::optional< Account > const &mptHolder)
Definition trust.cpp:51
json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:14
std::vector< STAmount > fund(jtx::Env &env, jtx::Account const &gw, std::vector< jtx::Account > const &accounts, std::vector< STAmount > const &amts, Fund how)
Definition AMMTest.cpp:34
json::Value sidechainXchainAccountCreate(Account const &acc, json::Value const &bridge, Account const &dst, AnyAmount const &amt, AnyAmount const &reward)
bool expectOffers(Env &env, AccountID const &account, std::uint16_t size, std::vector< Amounts > const &toMatch)
json::Value offerCancel(Account const &account, std::uint32_t offerSeq)
Cancel an offer.
Definition offer.cpp:31
XrpT const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
std::vector< Account > Holders
Definition mpt.h:102
json::Value xchainCommit(Account const &acc, json::Value const &bridge, std::uint32_t claimID, AnyAmount const &amt, std::optional< Account > const &dst)
bool same(STPathSet const &st1, Args const &... args)
std::uint32_t ownerCount(Env const &env, Account const &account)
XRPAmount txFee(Env const &env, std::uint16_t n)
std::tuple< STPathSet, STAmount, STAmount > findPaths(jtx::Env &env, jtx::Account const &src, jtx::Account const &dst, STAmount const &saDstAmount, std::optional< STAmount > const &saSendMax, std::optional< PathAsset > const &srcAsset, std::optional< AccountID > const &srcIssuer, std::optional< uint256 > const &domain)
FeatureBitset testableAmendments()
Definition Env.h:76
json::Value bridge(Account const &lockingChainDoor, Issue const &lockingChainIssue, Account const &issuingChainDoor, Issue const &issuingChainIssue)
uint256 setupDomain(jtx::Env &env, std::vector< jtx::Account > const &accounts, jtx::Account const &domainOwner, std::string const &credType)
STPathElement ipe(Asset const &asset)
json::Value claimAttestation(jtx::Account const &submittingAccount, json::Value const &jvBridge, jtx::Account const &sendingAccount, jtx::AnyAmount const &sendingAmount, jtx::Account const &rewardAccount, bool wasLockingChainSend, std::uint64_t claimID, std::optional< jtx::Account > const &dst, jtx::Signer const &signer)
json::Value offer(Account const &account, STAmount const &takerPays, STAmount const &takerGets, std::uint32_t flags)
Create an offer.
Definition offer.cpp:14
std::unique_ptr< Config > envconfig()
creates and initializes a default configuration for jtx::Env
Definition envconfig.h:28
json::Value createAccountAttestation(jtx::Account const &submittingAccount, json::Value const &jvBridge, jtx::Account const &sendingAccount, jtx::AnyAmount const &sendingAmount, jtx::AnyAmount const &rewardAmount, jtx::Account const &rewardAccount, bool wasLockingChainSend, std::uint64_t createCount, jtx::Account const &dst, jtx::Signer const &signer)
json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition trust.cpp:18
json::Value xchainClaim(Account const &acc, json::Value const &bridge, std::uint32_t claimID, AnyAmount const &amt, Account const &dst)
STPath stpath(Args const &... args)
PrettyAmount drops(Integer i)
Returns an XRP PrettyAmount, which is trivially convertible to STAmount.
json::Value xchainCreateClaimId(Account const &acc, json::Value const &bridge, STAmount const &reward, Account const &otherChainSource)
json::Value getAccountOffers(Env &env, AccountID const &acct, bool current)
json::Value fset(Account const &account, std::uint32_t on, std::uint32_t off=0)
Add and/or remove flag.
Definition flags.cpp:15
uint256 getCheckIndex(AccountID const &account, std::uint32_t uSequence)
constexpr XRPAmount
Convert XRP to drops (integral types).
Definition TxTest.h:48
BEAST_DEFINE_TESTSUITE_PRIO(AccountDelete, app, xrpl, 2)
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
constexpr FlagValue tmfMPTCanEnableCanClawback
Definition TxFlags.h:350
constexpr FlagValue tmfMPTCanEnableCanTransfer
Definition TxFlags.h:349
@ terNO_AMM
Definition TER.h:219
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Definition Issue.h:97
constexpr FlagValue tmfMPTSetCanLock
Definition TxFlags.h:365
constexpr FlagValue tmfMPTSetCanClawback
Definition TxFlags.h:370
std::string strHex(FwdIt begin, FwdIt end)
Definition strHex.h:10
TAmounts< STAmount, STAmount > Amounts
Definition Quality.h:64
STAmount mulRoundStrict(STAmount const &v1, STAmount const &v2, Asset const &asset, bool roundUp)
BaseUInt< 160, detail::CurrencyTag > Currency
Currency is a hash representing a specific currency.
Definition UintTypes.h:36
constexpr FlagValue tmfMPTCanEnableCanLock
Definition TxFlags.h:345
constexpr FlagValue tmfMPTSetCanTrade
Definition TxFlags.h:368
TER canMPTTradeAndTransfer(ReadView const &v, Asset const &asset, AccountID const &from, AccountID const &to)
Convenience to combine canTrade/Transfer.
MPTIssue mptIssueFromJson(json::Value const &jv)
Definition MPTIssue.cpp:67
bool toCurrency(Currency &, std::string const &)
Tries to convert a string to a Currency, returns true on success.
Definition UintTypes.cpp:65
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:633
json::Value toJson(Asset const &asset)
Definition Asset.h:157
constexpr FlagValue tmfMPTSetCanTransfer
Definition TxFlags.h:369
constexpr FlagValue tmfMPTSetCanEscrow
Definition TxFlags.h:367
constexpr std::size_t kMaxMpTokenMetadataLength
The maximum length of MPTokenMetadata.
Definition Protocol.h:235
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
constexpr FlagValue tmfMPTCanMutateMetadata
Definition TxFlags.h:351
constexpr FlagValue tmfMPTCanMutateTransferFee
Definition TxFlags.h:352
@ TapNone
Definition ApplyView.h:13
constexpr FlagValue tmfMPTCanEnableRequireAuth
Definition TxFlags.h:346
constexpr std::uint16_t kMaxTransferFee
The maximum token transfer fee allowed.
Definition Protocol.h:70
STAmount mulRound(STAmount const &v1, STAmount const &v2, Asset const &asset, bool roundUp)
constexpr FlagValue tmfMPTCanEnableCanEscrow
Definition TxFlags.h:347
@ temBAD_PATH
Definition TER.h:82
@ temBAD_CURRENCY
Definition TER.h:76
@ temINVALID_FLAG
Definition TER.h:97
@ temMALFORMED
Definition TER.h:73
@ temBAD_PATH_LOOP
Definition TER.h:83
@ temDISABLED
Definition TER.h:100
@ temBAD_AMOUNT
Definition TER.h:75
@ temBAD_TRANSFER_FEE
Definition TER.h:128
@ temRIPPLE_EMPTY
Definition TER.h:99
@ temREDUNDANT
Definition TER.h:98
TERSubset< CanCvtToTER > TER
Definition TER.h:634
constexpr FlagValue tmfMPTCanEnableCanTrade
Definition TxFlags.h:348
TER requireAuth(ReadView const &view, MPTIssue const &mptIssue, AccountID const &account, AuthType authType=AuthType::Legacy, std::uint8_t depth=0)
Check if the account lacks required authorization for MPT.
MPTID badMPT()
Definition MPTIssue.h:110
@ tecLOCKED
Definition TER.h:356
@ tecPATH_PARTIAL
Definition TER.h:280
@ tecNO_ENTRY
Definition TER.h:304
@ tecPATH_DRY
Definition TER.h:292
@ tecOBJECT_NOT_FOUND
Definition TER.h:324
@ tecNO_AUTH
Definition TER.h:298
@ tecAMM_BALANCE
Definition TER.h:327
@ tecINVARIANT_FAILED
Definition TER.h:311
@ tecINSUFFICIENT_FUNDS
Definition TER.h:323
@ tecUNFUNDED_OFFER
Definition TER.h:282
@ tecINSUFFICIENT_RESERVE
Definition TER.h:305
@ tecNO_PERMISSION
Definition TER.h:303
@ tecDUPLICATE
Definition TER.h:313
@ tecHAS_OBLIGATIONS
Definition TER.h:315
@ tecNO_DST
Definition TER.h:288
STAmount multiply(STAmount const &amount, Number const &frac, Number::RoundingMode rm)
Asset assetFromJson(json::Value const &jv)
Definition Asset.cpp:59
std::vector< std::pair< AccountID, Number > > MultiplePaymentDestinations
TER accountSendMulti(ApplyView &view, AccountID const &senderID, Asset const &asset, MultiplePaymentDestinations const &receivers, beast::Journal j, WaiveTransferFee waiveFee=WaiveTransferFee::No)
Like accountSend, except one account is sending multiple payments (with the same asset!...
constexpr std::uint64_t kMaxMpTokenAmount
The maximum amount of MPTokenIssuance.
Definition Protocol.h:238
@ SoeMptNotSupported
Definition SOTemplate.h:28
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
constexpr bool equalTokens(Asset const &lhs, Asset const &rhs)
Definition Asset.h:275
constexpr FlagValue tfFullyCanonicalSig
Definition TxFlags.h:42
constexpr FlagValue tmfMPTSetRequireAuth
Definition TxFlags.h:366
T ref(T... args)
T size(T... args)
Execution context for applying a JSON transaction.
Definition JTx.h:23
std::shared_ptr< STTx const > stx
Definition JTx.h:33
static std::vector< Account > allHolders
Definition mpt.h:107
Arguments for initializing funded MPT test accounts and issuance.
Definition mpt.h:130
STAmount const & value() const
A signer in a SignerList.
Definition multisign.h:17
Type used to specify DeliverMin for cashing a check.
Definition check.h:20
T tie(T... args)
T to_string(T... args)
T what(T... args)