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