Files
rippled/src/test/app/MPToken_test.cpp
2026-04-20 21:25:52 +00:00

6818 lines
255 KiB
C++

#include <test/jtx/AMM.h>
#include <test/jtx/AMMTest.h>
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/amount.h>
#include <test/jtx/balance.h> // IWYU pragma: keep
#include <test/jtx/check.h>
#include <test/jtx/credentials.h>
#include <test/jtx/delivermin.h>
#include <test/jtx/deposit.h>
#include <test/jtx/domain.h>
#include <test/jtx/envconfig.h>
#include <test/jtx/escrow.h>
#include <test/jtx/fee.h>
#include <test/jtx/flags.h>
#include <test/jtx/mpt.h>
#include <test/jtx/multisign.h>
#include <test/jtx/offer.h>
#include <test/jtx/paths.h>
#include <test/jtx/pay.h>
#include <test/jtx/permissioned_dex.h>
#include <test/jtx/permissioned_domains.h>
#include <test/jtx/sendmax.h>
#include <test/jtx/ter.h>
#include <test/jtx/trust.h>
#include <test/jtx/txflags.h>
#include <test/jtx/xchain_bridge.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/json/to_string.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ApplyViewImpl.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/IOUAmount.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/MPTAmount.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/SOTemplate.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STPathSet.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol/jss.h>
#include <cstdint>
#include <functional>
#include <initializer_list>
#include <optional>
#include <set>
#include <stdexcept>
#include <string>
#include <tuple>
#include <utility>
#include <vector>
namespace xrpl {
namespace test {
class MPToken_test : public beast::unit_test::suite
{
void
testCreateValidation(FeatureBitset features)
{
testcase("Create Validate");
using namespace test::jtx;
Account const alice("alice");
// test preflight of MPTokenIssuanceCreate
{
// If the MPT amendment is not enabled, you should not be able to
// create MPTokenIssuance
Env env{*this, features - featureMPTokensV1};
MPTTester mptAlice(env, alice);
mptAlice.create({.ownerCount = 0, .err = temDISABLED});
}
// test preflight of MPTokenIssuanceCreate
{
Env env{*this, features};
MPTTester mptAlice(env, alice);
mptAlice.create({.flags = 0x00000001, .err = temINVALID_FLAG});
// tries to set a txfee while not enabling in the flag
mptAlice.create(
{.maxAmt = 100,
.assetScale = 0,
.transferFee = 1,
.metadata = "test",
.err = temMALFORMED});
if (!features[featureSingleAssetVault])
{
// tries to set DomainID when SAV is disabled
mptAlice.create(
{.maxAmt = 100,
.assetScale = 0,
.metadata = "test",
.flags = tfMPTRequireAuth,
.domainID = uint256(42),
.err = temDISABLED});
}
else if (!features[featurePermissionedDomains])
{
// tries to set DomainID when PD is disabled
mptAlice.create(
{.maxAmt = 100,
.assetScale = 0,
.metadata = "test",
.flags = tfMPTRequireAuth,
.domainID = uint256(42),
.err = temDISABLED});
}
else
{
// tries to set DomainID when RequireAuth is not set
mptAlice.create(
{.maxAmt = 100,
.assetScale = 0,
.metadata = "test",
.domainID = uint256(42),
.err = temMALFORMED});
// tries to set zero DomainID
mptAlice.create(
{.maxAmt = 100,
.assetScale = 0,
.metadata = "test",
.flags = tfMPTRequireAuth,
.domainID = beast::zero,
.err = temMALFORMED});
}
// tries to set a txfee greater than max
mptAlice.create(
{.maxAmt = 100,
.assetScale = 0,
.transferFee = maxTransferFee + 1,
.metadata = "test",
.flags = tfMPTCanTransfer,
.err = temBAD_TRANSFER_FEE});
// tries to set a txfee while not enabling transfer
mptAlice.create(
{.maxAmt = 100,
.assetScale = 0,
.transferFee = maxTransferFee,
.metadata = "test",
.err = temMALFORMED});
// empty metadata returns error
mptAlice.create(
{.maxAmt = 100,
.assetScale = 0,
.transferFee = 0,
.metadata = "",
.err = temMALFORMED});
// MaximumAmount of 0 returns error
mptAlice.create(
{.maxAmt = 0,
.assetScale = 1,
.transferFee = 1,
.metadata = "test",
.err = temMALFORMED});
// MaximumAmount larger than 63 bit returns error
mptAlice.create(
{.maxAmt = 0xFFFF'FFFF'FFFF'FFF0, // 18'446'744'073'709'551'600
.assetScale = 0,
.transferFee = 0,
.metadata = "test",
.err = temMALFORMED});
mptAlice.create(
{.maxAmt = maxMPTokenAmount + 1, // 9'223'372'036'854'775'808
.assetScale = 0,
.transferFee = 0,
.metadata = "test",
.err = temMALFORMED});
}
}
void
testCreateEnabled(FeatureBitset features)
{
testcase("Create Enabled");
using namespace test::jtx;
Account const alice("alice");
{
// If the MPT amendment IS enabled, you should be able to create
// MPTokenIssuances
Env env{*this, features};
MPTTester mptAlice(env, alice);
mptAlice.create(
{.maxAmt = maxMPTokenAmount, // 9'223'372'036'854'775'807
.assetScale = 1,
.transferFee = 10,
.metadata = "123",
.ownerCount = 1,
.flags = tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow | tfMPTCanTrade |
tfMPTCanTransfer | tfMPTCanClawback});
// Get the hash for the most recent transaction.
std::string const txHash{env.tx()->getJson(JsonOptions::none)[jss::hash].asString()};
Json::Value const result = env.rpc("tx", txHash)[jss::result];
BEAST_EXPECT(result[sfMaximumAmount.getJsonName()] == "9223372036854775807");
}
if (features[featureSingleAssetVault])
{
// Add permissioned domain
Account const credIssuer1{"credIssuer1"};
std::string const credType = "credential";
pdomain::Credentials const credentials1{{.issuer = credIssuer1, .credType = credType}};
{
Env env{*this, features};
env.fund(XRP(1000), credIssuer1);
env(pdomain::setTx(credIssuer1, credentials1));
auto const domainId1 = [&]() {
auto tx = env.tx()->getJson(JsonOptions::none);
return pdomain::getNewDomain(env.meta());
}();
MPTTester mptAlice(env, alice);
mptAlice.create({
.maxAmt = maxMPTokenAmount, // 9'223'372'036'854'775'807
.assetScale = 1,
.transferFee = 10,
.metadata = "123",
.ownerCount = 1,
.flags = tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow | tfMPTCanTrade |
tfMPTCanTransfer | tfMPTCanClawback,
.domainID = domainId1,
});
// Get the hash for the most recent transaction.
std::string const txHash{
env.tx()->getJson(JsonOptions::none)[jss::hash].asString()};
Json::Value const result = env.rpc("tx", txHash)[jss::result];
BEAST_EXPECT(result[sfMaximumAmount.getJsonName()] == "9223372036854775807");
}
}
}
void
testDestroyValidation(FeatureBitset features)
{
testcase("Destroy Validate");
using namespace test::jtx;
Account const alice("alice");
Account const bob("bob");
// MPTokenIssuanceDestroy (preflight)
{
Env env{*this, features - featureMPTokensV1};
MPTTester mptAlice(env, alice);
auto const id = makeMptID(env.seq(alice), alice);
mptAlice.destroy({.id = id, .ownerCount = 0, .err = temDISABLED});
env.enableFeature(featureMPTokensV1);
mptAlice.destroy({.id = id, .flags = 0x00000001, .err = temINVALID_FLAG});
}
// MPTokenIssuanceDestroy (preclaim)
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.destroy(
{.id = makeMptID(env.seq(alice), alice),
.ownerCount = 0,
.err = tecOBJECT_NOT_FOUND});
mptAlice.create({.ownerCount = 1});
// a non-issuer tries to destroy a mptissuance they didn't issue
mptAlice.destroy({.issuer = bob, .err = tecNO_PERMISSION});
// Make sure that issuer can't delete issuance when it still has
// outstanding balance
{
// bob now holds a mptoken object
mptAlice.authorize({.account = bob, .holderCount = 1});
// alice pays bob 100 tokens
mptAlice.pay(alice, bob, 100);
mptAlice.destroy({.err = tecHAS_OBLIGATIONS});
}
}
}
void
testDestroyEnabled(FeatureBitset features)
{
testcase("Destroy Enabled");
using namespace test::jtx;
Account const alice("alice");
// If the MPT amendment IS enabled, you should be able to destroy
// MPTokenIssuances
Env env{*this, features};
MPTTester mptAlice(env, alice);
mptAlice.create({.ownerCount = 1});
mptAlice.destroy({.ownerCount = 0});
}
void
testAuthorizeValidation(FeatureBitset features)
{
testcase("Validate authorize transaction");
using namespace test::jtx;
Account const alice("alice");
Account const bob("bob");
Account const cindy("cindy");
// Validate amendment enable in MPTokenAuthorize (preflight)
{
Env env{*this, features - featureMPTokensV1};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.authorize(
{.account = bob, .id = makeMptID(env.seq(alice), alice), .err = temDISABLED});
}
// Validate fields in MPTokenAuthorize (preflight)
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({.ownerCount = 1});
// The only valid MPTokenAuthorize flag is tfMPTUnauthorize, which
// has a value of 1
mptAlice.authorize({.account = bob, .flags = 0x00000002, .err = temINVALID_FLAG});
mptAlice.authorize({.account = bob, .holder = bob, .err = temMALFORMED});
mptAlice.authorize({.holder = alice, .err = temMALFORMED});
}
// Try authorizing when MPTokenIssuance doesn't exist in
// MPTokenAuthorize (preclaim)
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
auto const id = makeMptID(env.seq(alice), alice);
mptAlice.authorize({.holder = bob, .id = id, .err = tecOBJECT_NOT_FOUND});
mptAlice.authorize({.account = bob, .id = id, .err = tecOBJECT_NOT_FOUND});
}
// Test bad scenarios without allowlisting in MPTokenAuthorize
// (preclaim)
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({.ownerCount = 1});
// bob submits a tx with a holder field
mptAlice.authorize({.account = bob, .holder = alice, .err = tecNO_PERMISSION});
// alice tries to hold onto her own token
mptAlice.authorize({.account = alice, .err = tecNO_PERMISSION});
// the mpt does not enable allowlisting
mptAlice.authorize({.holder = bob, .err = tecNO_AUTH});
// bob now holds a mptoken object
mptAlice.authorize({.account = bob, .holderCount = 1});
// bob cannot create the mptoken the second time
mptAlice.authorize({.account = bob, .err = tecDUPLICATE});
// Check that bob cannot delete MPToken when his balance is
// non-zero
{
// alice pays bob 100 tokens
mptAlice.pay(alice, bob, 100);
// bob tries to delete his MPToken, but fails since he still
// holds tokens
mptAlice.authorize(
{.account = bob, .flags = tfMPTUnauthorize, .err = tecHAS_OBLIGATIONS});
// bob pays back alice 100 tokens
mptAlice.pay(bob, alice, 100);
}
// bob deletes/unauthorizes his MPToken
mptAlice.authorize({.account = bob, .flags = tfMPTUnauthorize});
// bob receives error when he tries to delete his MPToken that has
// already been deleted
mptAlice.authorize(
{.account = bob,
.holderCount = 0,
.flags = tfMPTUnauthorize,
.err = tecOBJECT_NOT_FOUND});
}
// Test bad scenarios with allow-listing in MPTokenAuthorize (preclaim)
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({.ownerCount = 1, .flags = tfMPTRequireAuth});
// alice submits a tx without specifying a holder's account
mptAlice.authorize({.err = tecNO_PERMISSION});
// alice submits a tx to authorize a holder that hasn't created
// a mptoken yet
mptAlice.authorize({.holder = bob, .err = tecOBJECT_NOT_FOUND});
// alice specifies a holder acct that doesn't exist
mptAlice.authorize({.holder = cindy, .err = tecNO_DST});
// bob now holds a mptoken object
mptAlice.authorize({.account = bob, .holderCount = 1});
// alice tries to unauthorize bob.
// although tx is successful,
// but nothing happens because bob hasn't been authorized yet
mptAlice.authorize({.holder = bob, .flags = tfMPTUnauthorize});
// alice authorizes bob
// make sure bob's mptoken has set lsfMPTAuthorized
mptAlice.authorize({.holder = bob});
// alice tries authorizes bob again.
// tx is successful, but bob is already authorized,
// so no changes
mptAlice.authorize({.holder = bob});
// bob deletes his mptoken
mptAlice.authorize({.account = bob, .holderCount = 0, .flags = tfMPTUnauthorize});
}
// Test mptoken reserve requirement - first two mpts free (doApply)
{
Env env{*this, features};
auto const acctReserve = env.current()->fees().reserve;
auto const incReserve = env.current()->fees().increment;
// 1 drop
BEAST_EXPECT(incReserve > XRPAmount(1));
MPTTester mptAlice1(
env, alice, {.holders = {bob}, .xrpHolders = acctReserve + (incReserve - 1)});
mptAlice1.create();
MPTTester mptAlice2(env, alice, {.fund = false});
mptAlice2.create();
MPTTester mptAlice3(env, alice, {.fund = false});
mptAlice3.create({.ownerCount = 3});
// first mpt for free
mptAlice1.authorize({.account = bob, .holderCount = 1});
// second mpt free
mptAlice2.authorize({.account = bob, .holderCount = 2});
mptAlice3.authorize({.account = bob, .err = tecINSUFFICIENT_RESERVE});
env(pay(env.master, bob, drops(incReserve + incReserve + incReserve)));
env.close();
mptAlice3.authorize({.account = bob, .holderCount = 3});
}
}
void
testAuthorizeEnabled(FeatureBitset features)
{
testcase("Authorize Enabled");
using namespace test::jtx;
Account const alice("alice");
Account const bob("bob");
// Basic authorization without allowlisting
{
Env env{*this, features};
// alice create mptissuance without allowisting
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({.ownerCount = 1});
// bob creates a mptoken
mptAlice.authorize({.account = bob, .holderCount = 1});
mptAlice.authorize({.account = bob, .holderCount = 1, .err = tecDUPLICATE});
// bob deletes his mptoken
mptAlice.authorize({.account = bob, .holderCount = 0, .flags = tfMPTUnauthorize});
}
// With allowlisting
{
Env env{*this, features};
// alice creates a mptokenissuance that requires authorization
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({.ownerCount = 1, .flags = tfMPTRequireAuth});
// bob creates a mptoken
mptAlice.authorize({.account = bob, .holderCount = 1});
// alice authorizes bob
mptAlice.authorize({.account = alice, .holder = bob});
// Unauthorize bob's mptoken
mptAlice.authorize(
{.account = alice, .holder = bob, .holderCount = 1, .flags = tfMPTUnauthorize});
mptAlice.authorize({.account = bob, .holderCount = 0, .flags = tfMPTUnauthorize});
}
// Holder can have dangling MPToken even if issuance has been destroyed.
// Make sure they can still delete/unauthorize the MPToken
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({.ownerCount = 1});
// bob creates a mptoken
mptAlice.authorize({.account = bob, .holderCount = 1});
// alice deletes her issuance
mptAlice.destroy({.ownerCount = 0});
// bob can delete his mptoken even though issuance is no longer
// existent
mptAlice.authorize({.account = bob, .holderCount = 0, .flags = tfMPTUnauthorize});
}
}
void
testSetValidation(FeatureBitset features)
{
testcase("Validate set transaction");
using namespace test::jtx;
Account const alice("alice"); // issuer
Account const bob("bob"); // holder
Account const cindy("cindy");
// Validate fields in MPTokenIssuanceSet (preflight)
{
Env env{*this, features - featureMPTokensV1};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.set(
{.account = bob, .id = makeMptID(env.seq(alice), alice), .err = temDISABLED});
env.enableFeature(featureMPTokensV1);
mptAlice.create({.ownerCount = 1, .holderCount = 0});
mptAlice.authorize({.account = bob, .holderCount = 1});
// test invalid flag - only valid flags are tfMPTLock (1) and Unlock
// (2)
mptAlice.set({.account = alice, .flags = 0x00000008, .err = temINVALID_FLAG});
if (!features[featureSingleAssetVault] && !features[featureDynamicMPT])
{
// test invalid flags - nothing is being changed
mptAlice.set({.account = alice, .flags = 0x00000000, .err = tecNO_PERMISSION});
mptAlice.set(
{.account = alice,
.holder = bob,
.flags = 0x00000000,
.err = tecNO_PERMISSION});
// cannot set DomainID since SAV is not enabled
mptAlice.set({.account = alice, .domainID = uint256(42), .err = temDISABLED});
}
else
{
// test invalid flags - nothing is being changed
mptAlice.set({.account = alice, .flags = 0x00000000, .err = temMALFORMED});
mptAlice.set(
{.account = alice, .holder = bob, .flags = 0x00000000, .err = temMALFORMED});
if (!features[featurePermissionedDomains] || !features[featureSingleAssetVault])
{
// cannot set DomainID since PD is not enabled
mptAlice.set({.account = alice, .domainID = uint256(42), .err = temDISABLED});
}
else if (features[featureSingleAssetVault])
{
// cannot set DomainID since Holder is set
mptAlice.set(
{.account = alice,
.holder = bob,
.domainID = uint256(42),
.err = temMALFORMED});
}
}
// set both lock and unlock flags at the same time will fail
mptAlice.set(
{.account = alice, .flags = tfMPTLock | tfMPTUnlock, .err = temINVALID_FLAG});
// if the holder is the same as the acct that submitted the tx,
// tx fails
mptAlice.set(
{.account = alice, .holder = alice, .flags = tfMPTLock, .err = temMALFORMED});
}
// Validate fields in MPTokenIssuanceSet (preclaim)
// test when a mptokenissuance has disabled locking
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({.ownerCount = 1});
// alice tries to lock a mptissuance that has disabled locking
mptAlice.set({.account = alice, .flags = tfMPTLock, .err = tecNO_PERMISSION});
// alice tries to unlock mptissuance that has disabled locking
mptAlice.set({.account = alice, .flags = tfMPTUnlock, .err = tecNO_PERMISSION});
// issuer tries to lock a bob's mptoken that has disabled
// locking
mptAlice.set(
{.account = alice, .holder = bob, .flags = tfMPTLock, .err = tecNO_PERMISSION});
// issuer tries to unlock a bob's mptoken that has disabled
// locking
mptAlice.set(
{.account = alice, .holder = bob, .flags = tfMPTUnlock, .err = tecNO_PERMISSION});
}
// Validate fields in MPTokenIssuanceSet (preclaim)
// test when mptokenissuance has enabled locking
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
// alice trying to set when the mptissuance doesn't exist yet
mptAlice.set(
{.id = makeMptID(env.seq(alice), alice),
.flags = tfMPTLock,
.err = tecOBJECT_NOT_FOUND});
// create a mptokenissuance with locking
mptAlice.create({.ownerCount = 1, .flags = tfMPTCanLock});
// a non-issuer acct tries to set the mptissuance
mptAlice.set({.account = bob, .flags = tfMPTLock, .err = tecNO_PERMISSION});
// trying to set a holder who doesn't have a mptoken
mptAlice.set({.holder = bob, .flags = tfMPTLock, .err = tecOBJECT_NOT_FOUND});
// trying to set a holder who doesn't exist
mptAlice.set({.holder = cindy, .flags = tfMPTLock, .err = tecNO_DST});
}
if (features[featureSingleAssetVault] && features[featurePermissionedDomains])
{
// Add permissioned domain
Account const credIssuer1{"credIssuer1"};
std::string const credType = "credential";
pdomain::Credentials const credentials1{{.issuer = credIssuer1, .credType = credType}};
{
Env env{*this, features};
MPTTester mptAlice(env, alice);
mptAlice.create({});
// Trying to set DomainID on a public MPTokenIssuance
mptAlice.set({.domainID = uint256(42), .err = tecNO_PERMISSION});
mptAlice.set({.domainID = beast::zero, .err = tecNO_PERMISSION});
}
{
Env env{*this, features};
MPTTester mptAlice(env, alice);
mptAlice.create({.flags = tfMPTRequireAuth});
// Trying to set non-existing DomainID
mptAlice.set({.domainID = uint256(42), .err = tecOBJECT_NOT_FOUND});
// Trying to lock but locking is disabled
mptAlice.set(
{.flags = tfMPTUnlock, .domainID = uint256(42), .err = tecNO_PERMISSION});
mptAlice.set(
{.flags = tfMPTUnlock, .domainID = beast::zero, .err = tecNO_PERMISSION});
}
}
}
void
testSetEnabled(FeatureBitset features)
{
testcase("Enabled set transaction");
using namespace test::jtx;
Account const alice("alice"); // issuer
Account const bob("bob"); // holder
{
// Test locking and unlocking
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
// create a mptokenissuance with locking
mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanLock});
mptAlice.authorize({.account = bob, .holderCount = 1});
// locks bob's mptoken
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
// trying to lock bob's mptoken again will still succeed
// but no changes to the objects
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
// alice locks the mptissuance
mptAlice.set({.account = alice, .flags = tfMPTLock});
// alice tries to lock up both mptissuance and mptoken again
// it will not change the flags and both will remain locked.
mptAlice.set({.account = alice, .flags = tfMPTLock});
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
// alice unlocks bob's mptoken
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTUnlock});
// locks up bob's mptoken again
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
if (!features[featureSingleAssetVault])
{
// Delete bob's mptoken even though it is locked
mptAlice.authorize({.account = bob, .flags = tfMPTUnauthorize});
mptAlice.set(
{.account = alice,
.holder = bob,
.flags = tfMPTUnlock,
.err = tecOBJECT_NOT_FOUND});
return;
}
// Cannot delete locked MPToken
mptAlice.authorize(
{.account = bob, .flags = tfMPTUnauthorize, .err = tecNO_PERMISSION});
// alice unlocks mptissuance
mptAlice.set({.account = alice, .flags = tfMPTUnlock});
// alice unlocks bob's mptoken
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTUnlock});
// alice unlocks mptissuance and bob's mptoken again despite that
// they are already unlocked. Make sure this will not change the
// flags
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTUnlock});
mptAlice.set({.account = alice, .flags = tfMPTUnlock});
}
if (features[featureSingleAssetVault])
{
// Add permissioned domain
std::string const credType = "credential";
// Test setting and resetting domain ID
Env env{*this, features};
auto const domainId1 = [&]() {
Account const credIssuer1{"credIssuer1"};
env.fund(XRP(1000), credIssuer1);
pdomain::Credentials const credentials1{
{.issuer = credIssuer1, .credType = credType}};
env(pdomain::setTx(credIssuer1, credentials1));
return [&]() {
auto tx = env.tx()->getJson(JsonOptions::none);
return pdomain::getNewDomain(env.meta());
}();
}();
auto const domainId2 = [&]() {
Account const credIssuer2{"credIssuer2"};
env.fund(XRP(1000), credIssuer2);
pdomain::Credentials const credentials2{
{.issuer = credIssuer2, .credType = credType}};
env(pdomain::setTx(credIssuer2, credentials2));
return [&]() {
auto tx = env.tx()->getJson(JsonOptions::none);
return pdomain::getNewDomain(env.meta());
}();
}();
MPTTester mptAlice(env, alice, {.holders = {bob}});
// create a mptokenissuance with auth.
mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTRequireAuth});
BEAST_EXPECT(mptAlice.checkDomainID(std::nullopt));
// reset "domain not set" to "domain not set", i.e. no change
mptAlice.set({.domainID = beast::zero});
BEAST_EXPECT(mptAlice.checkDomainID(std::nullopt));
// reset "domain not set" to domain1
mptAlice.set({.domainID = domainId1});
BEAST_EXPECT(mptAlice.checkDomainID(domainId1));
// reset domain1 to domain2
mptAlice.set({.domainID = domainId2});
BEAST_EXPECT(mptAlice.checkDomainID(domainId2));
// reset domain to "domain not set"
mptAlice.set({.domainID = beast::zero});
BEAST_EXPECT(mptAlice.checkDomainID(std::nullopt));
}
}
void
testPayment(FeatureBitset features)
{
testcase("Payment");
using namespace test::jtx;
Account const alice("alice"); // issuer
Account const bob("bob"); // holder
Account const carol("carol"); // holder
auto const MPTokensV2 = features[featureMPTokensV2];
// preflight validation
// MPT is disabled
{
Env env{*this, features - featureMPTokensV1};
Account const alice("alice");
Account const bob("bob");
env.fund(XRP(1'000), alice);
env.fund(XRP(1'000), bob);
STAmount const mpt{MPTIssue{makeMptID(1, alice)}, UINT64_C(100)};
env(pay(alice, bob, mpt), ter(temDISABLED));
}
// MPT is disabled, unsigned request
{
Env env{*this, features - featureMPTokensV1};
Account const alice("alice"); // issuer
Account const carol("carol");
auto const USD = alice["USD"];
env.fund(XRP(1'000), alice);
env.fund(XRP(1'000), carol);
STAmount const mpt{MPTIssue{makeMptID(1, alice)}, UINT64_C(100)};
Json::Value jv;
jv[jss::secret] = alice.name();
jv[jss::tx_json] = pay(alice, carol, mpt);
jv[jss::tx_json][jss::Fee] = to_string(env.current()->fees().base);
auto const jrr = env.rpc("json", "submit", to_string(jv));
BEAST_EXPECT(jrr[jss::result][jss::engine_result] == "temDISABLED");
}
// Invalid flag
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({.ownerCount = 1, .holderCount = 0});
auto const MPT = mptAlice["MPT"];
mptAlice.authorize({.account = bob});
auto err = !features[featureMPTokensV2] ? ter(temINVALID_FLAG) : ter(temRIPPLE_EMPTY);
env(pay(alice, bob, MPT(10)), txflags(tfNoRippleDirect), err);
err = !features[featureMPTokensV2] ? ter(temINVALID_FLAG) : ter(tesSUCCESS);
env(pay(alice, bob, MPT(10)), txflags(tfLimitQuality), err);
}
// Invalid combination of send, sendMax, deliverMin, paths
{
Env env{*this, features};
Account const alice("alice");
Account const carol("carol");
MPTTester mptAlice(env, alice, {.holders = {carol}});
mptAlice.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
mptAlice.authorize({.account = carol});
// sendMax and DeliverMin are valid XRP amount,
// but is invalid combination with MPT amount
auto const MPT = mptAlice["MPT"];
auto err = !MPTokensV2 ? ter(temMALFORMED) : ter(tecPATH_PARTIAL);
env(pay(alice, carol, MPT(100)), sendmax(XRP(100)), err);
env(pay(alice, carol, MPT(100)), deliver_min(XRP(100)), ter(temBAD_AMOUNT));
// sendMax MPT is invalid with IOU or XRP
auto const USD = alice["USD"];
err = !MPTokensV2 ? ter(temMALFORMED) : ter(tecPATH_DRY);
env(pay(alice, carol, USD(100)), sendmax(MPT(100)), err);
err = !MPTokensV2 ? ter(temMALFORMED) : ter(tecPATH_PARTIAL);
env(pay(alice, carol, XRP(100)), sendmax(MPT(100)), err);
env(pay(alice, carol, USD(100)), deliver_min(MPT(100)), ter(temBAD_AMOUNT));
env(pay(alice, carol, XRP(100)), deliver_min(MPT(100)), ter(temBAD_AMOUNT));
// sendmax and amount are different MPT issue
test::jtx::MPT const MPT1("MPT", makeMptID(env.seq(alice) + 10, alice));
err = !MPTokensV2 ? ter(temMALFORMED) : ter(tecOBJECT_NOT_FOUND);
env(pay(alice, carol, MPT1(100)), sendmax(MPT(100)), err);
// "paths" is invalid in V1
err = !MPTokensV2 ? ter(temMALFORMED) : ter(tesSUCCESS);
env(pay(alice, carol, MPT(100)), path(~USD), err);
}
// build_path is invalid if MPT
{
Env env{*this, features - featureMPTokensV2};
Account const alice("alice");
Account const carol("carol");
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create({.ownerCount = 1, .holderCount = 0});
auto const MPT = mptAlice["MPT"];
mptAlice.authorize({.account = carol});
Json::Value payment;
payment[jss::secret] = alice.name();
payment[jss::tx_json] = pay(alice, carol, MPT(100));
payment[jss::build_path] = true;
auto jrr = env.rpc("json", "submit", to_string(payment));
BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");
BEAST_EXPECT(
jrr[jss::result][jss::error_message] ==
"Field 'build_path' not allowed in this context.");
}
// Can't pay negative amount
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create({.ownerCount = 1, .holderCount = 0});
auto const MPT = mptAlice["MPT"];
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
mptAlice.pay(alice, bob, -1, temBAD_AMOUNT);
mptAlice.pay(bob, carol, -1, temBAD_AMOUNT);
mptAlice.pay(bob, alice, -1, temBAD_AMOUNT);
env(pay(alice, bob, MPT(10)), sendmax(MPT(-1)), ter(temBAD_AMOUNT));
}
// Pay to self
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({.ownerCount = 1, .holderCount = 0});
mptAlice.authorize({.account = bob});
mptAlice.pay(bob, bob, 10, temREDUNDANT);
}
// preclaim validation
// Destination doesn't exist
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({.ownerCount = 1, .holderCount = 0});
mptAlice.authorize({.account = bob});
Account const bad{"bad"};
env.memoize(bad);
mptAlice.pay(bob, bad, 10, tecNO_DST);
}
// apply validation
// If RequireAuth is enabled, Payment fails if the receiver is not
// authorized
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTRequireAuth | tfMPTCanTransfer});
mptAlice.authorize({.account = bob});
mptAlice.pay(alice, bob, 100, tecNO_AUTH);
}
// If RequireAuth is enabled, Payment fails if the sender is not
// authorized
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTRequireAuth | tfMPTCanTransfer});
// bob creates an empty MPToken
mptAlice.authorize({.account = bob});
// alice authorizes bob to hold funds
mptAlice.authorize({.account = alice, .holder = bob});
// alice sends 100 MPT to bob
mptAlice.pay(alice, bob, 100);
// alice UNAUTHORIZES bob
mptAlice.authorize({.account = alice, .holder = bob, .flags = tfMPTUnauthorize});
// bob fails to send back to alice because he is no longer
// authorize to move his funds!
mptAlice.pay(bob, alice, 100, tecNO_AUTH);
}
if (features[featureSingleAssetVault] && features[featurePermissionedDomains])
{
// If RequireAuth is enabled and domain is a match, payment succeeds
{
Env env{*this, features};
std::string const credType = "credential";
Account const credIssuer1{"credIssuer1"};
env.fund(XRP(1000), credIssuer1, bob);
auto const domainId1 = [&]() {
pdomain::Credentials const credentials1{
{.issuer = credIssuer1, .credType = credType}};
env(pdomain::setTx(credIssuer1, credentials1));
return [&]() {
auto tx = env.tx()->getJson(JsonOptions::none);
return pdomain::getNewDomain(env.meta());
}();
}();
// bob is authorized via domain
env(credentials::create(bob, credIssuer1, credType));
env(credentials::accept(bob, credIssuer1, credType));
env.close();
MPTTester mptAlice(env, alice, MPTInit{});
env.close();
mptAlice.create({
.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTRequireAuth | tfMPTCanTransfer,
.domainID = domainId1,
});
mptAlice.authorize({.account = bob});
env.close();
// bob is authorized via domain
mptAlice.pay(alice, bob, 100);
mptAlice.set({.domainID = beast::zero});
// bob is no longer authorized
mptAlice.pay(alice, bob, 100, tecNO_AUTH);
}
{
Env env{*this, features};
std::string const credType = "credential";
Account const credIssuer1{"credIssuer1"};
env.fund(XRP(1000), credIssuer1, bob);
auto const domainId1 = [&]() {
pdomain::Credentials const credentials1{
{.issuer = credIssuer1, .credType = credType}};
env(pdomain::setTx(credIssuer1, credentials1));
return [&]() {
auto tx = env.tx()->getJson(JsonOptions::none);
return pdomain::getNewDomain(env.meta());
}();
}();
// bob is authorized via domain
env(credentials::create(bob, credIssuer1, credType));
env(credentials::accept(bob, credIssuer1, credType));
env.close();
MPTTester mptAlice(env, alice, MPTInit{});
env.close();
mptAlice.create({
.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTRequireAuth | tfMPTCanTransfer,
.domainID = domainId1,
});
// bob creates an empty MPToken
mptAlice.authorize({.account = bob});
// alice authorizes bob to hold funds
mptAlice.authorize({.account = alice, .holder = bob});
// alice sends 100 MPT to bob
mptAlice.pay(alice, bob, 100);
// alice UNAUTHORIZES bob
mptAlice.authorize({.account = alice, .holder = bob, .flags = tfMPTUnauthorize});
// bob is still authorized, via domain
mptAlice.pay(bob, alice, 10);
mptAlice.set({.domainID = beast::zero});
// bob fails to send back to alice because he is no longer
// authorize to move his funds!
mptAlice.pay(bob, alice, 10, tecNO_AUTH);
}
{
Env env{*this, features};
std::string const credType = "credential";
// credIssuer1 is the owner of domainId1 and a credential issuer
Account const credIssuer1{"credIssuer1"};
// credIssuer2 is the owner of domainId2 and a credential issuer
// Note, domainId2 also lists credentials issued by credIssuer1
Account const credIssuer2{"credIssuer2"};
env.fund(XRP(1000), credIssuer1, credIssuer2, bob, carol);
auto const domainId1 = [&]() {
pdomain::Credentials const credentials{
{.issuer = credIssuer1, .credType = credType}};
env(pdomain::setTx(credIssuer1, credentials));
return [&]() {
auto tx = env.tx()->getJson(JsonOptions::none);
return pdomain::getNewDomain(env.meta());
}();
}();
auto const domainId2 = [&]() {
pdomain::Credentials const credentials{
{.issuer = credIssuer1, .credType = credType},
{.issuer = credIssuer2, .credType = credType}};
env(pdomain::setTx(credIssuer2, credentials));
return [&]() {
auto tx = env.tx()->getJson(JsonOptions::none);
return pdomain::getNewDomain(env.meta());
}();
}();
// bob is authorized via credIssuer1 which is recognized by both
// domainId1 and domainId2
env(credentials::create(bob, credIssuer1, credType));
env(credentials::accept(bob, credIssuer1, credType));
env.close();
// carol is authorized via credIssuer2, only recognized by
// domainId2
env(credentials::create(carol, credIssuer2, credType));
env(credentials::accept(carol, credIssuer2, credType));
env.close();
MPTTester mptAlice(env, alice, MPTInit{});
env.close();
mptAlice.create({
.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTRequireAuth | tfMPTCanTransfer,
.domainID = domainId1,
});
// bob and carol create an empty MPToken
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
env.close();
// alice sends 50 MPT to bob but cannot send to carol
mptAlice.pay(alice, bob, 50);
mptAlice.pay(alice, carol, 50, tecNO_AUTH);
env.close();
// bob cannot send to carol because they are not on the same
// domain (since credIssuer2 is not recognized by domainId1)
mptAlice.pay(bob, carol, 10, tecNO_AUTH);
env.close();
// alice updates domainID to domainId2 which recognizes both
// credIssuer1 and credIssuer2
mptAlice.set({.domainID = domainId2});
// alice can now send to carol
mptAlice.pay(alice, carol, 10);
env.close();
// bob can now send to carol because both are in the same
// domain
mptAlice.pay(bob, carol, 10);
env.close();
// bob loses his authorization and can no longer send MPT
env(credentials::deleteCred(credIssuer1, bob, credIssuer1, credType));
env.close();
mptAlice.pay(bob, carol, 10, tecNO_AUTH);
mptAlice.pay(bob, alice, 10, tecNO_AUTH);
}
}
// Non-issuer cannot send to each other if MPTCanTransfer isn't set
{
Env env(*this, features);
Account const alice{"alice"};
Account const bob{"bob"};
Account const cindy{"cindy"};
MPTTester mptAlice(env, alice, {.holders = {bob, cindy}});
// alice creates issuance without MPTCanTransfer
mptAlice.create({.ownerCount = 1, .holderCount = 0});
// bob creates a MPToken
mptAlice.authorize({.account = bob});
// cindy creates a MPToken
mptAlice.authorize({.account = cindy});
// alice pays bob 100 tokens
mptAlice.pay(alice, bob, 100);
// bob tries to send cindy 10 tokens, but fails because canTransfer
// is off
mptAlice.pay(bob, cindy, 10, tecNO_AUTH);
// bob can send back to alice(issuer) just fine
mptAlice.pay(bob, alice, 10);
}
// Holder is not authorized
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});
// issuer to holder
mptAlice.pay(alice, bob, 100, tecNO_AUTH);
// holder to issuer
mptAlice.pay(bob, alice, 100, tecNO_AUTH);
// holder to holder
mptAlice.pay(bob, carol, 50, tecNO_AUTH);
}
// Payer doesn't have enough funds
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create({.ownerCount = 1, .flags = tfMPTCanTransfer});
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
mptAlice.pay(alice, bob, 100);
// Pay to another holder
mptAlice.pay(bob, carol, 101, tecPATH_PARTIAL);
// Pay to the issuer
mptAlice.pay(bob, alice, 101, tecPATH_PARTIAL);
}
// MPT is locked
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create({.ownerCount = 1, .flags = tfMPTCanLock | tfMPTCanTransfer});
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
mptAlice.pay(alice, bob, 100);
mptAlice.pay(alice, carol, 100);
auto const err =
env.current()->rules().enabled(featureMPTokensV2) ? tecPATH_DRY : tecLOCKED;
// Global lock
mptAlice.set({.account = alice, .flags = tfMPTLock});
// Can't send between holders
mptAlice.pay(bob, carol, 1, err);
mptAlice.pay(carol, bob, 2, err);
// Issuer can send
mptAlice.pay(alice, bob, 3);
// Holder can send back to issuer
mptAlice.pay(bob, alice, 4);
// Global unlock
mptAlice.set({.account = alice, .flags = tfMPTUnlock});
// Individual lock
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
// Can't send between holders
mptAlice.pay(bob, carol, 5, err);
mptAlice.pay(carol, bob, 6, err);
// Issuer can send
mptAlice.pay(alice, bob, 7);
// Holder can send back to issuer
mptAlice.pay(bob, alice, 8);
}
// Transfer fee
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
// Transfer fee is 10%
mptAlice.create(
{.transferFee = 10'000,
.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanTransfer});
// Holders create MPToken
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
// Payment between the issuer and the holder, no transfer fee.
mptAlice.pay(alice, bob, 2'000);
// Payment between the holder and the issuer, no transfer fee.
mptAlice.pay(bob, alice, 1'000);
BEAST_EXPECT(mptAlice.checkMPTokenAmount(bob, 1'000));
// Payment between the holders. The sender doesn't have
// enough funds to cover the transfer fee.
mptAlice.pay(bob, carol, 1'000, tecPATH_PARTIAL);
// Payment between the holders. The sender has enough funds
// but SendMax is not included.
mptAlice.pay(bob, carol, 100, tecPATH_PARTIAL);
auto const MPT = mptAlice["MPT"];
// SendMax doesn't cover the fee
env(pay(bob, carol, MPT(100)), sendmax(MPT(109)), ter(tecPATH_PARTIAL));
// Payment succeeds if sufficient SendMax is included.
// 100 to carol, 10 to issuer
env(pay(bob, carol, MPT(100)), sendmax(MPT(110)));
// 100 to carol, 10 to issuer
env(pay(bob, carol, MPT(100)), sendmax(MPT(115)));
BEAST_EXPECT(mptAlice.checkMPTokenAmount(bob, 780));
BEAST_EXPECT(mptAlice.checkMPTokenAmount(carol, 200));
// Payment succeeds if partial payment even if
// SendMax is less than deliver amount
env(pay(bob, carol, MPT(100)), sendmax(MPT(90)), txflags(tfPartialPayment));
// 82 to carol, 8 to issuer (90 / 1.1 ~ 81.81 (rounded to nearest in
// v1) = 82)
BEAST_EXPECT(mptAlice.checkMPTokenAmount(bob, 690));
// In V2 the payments are executed via the payment engine and
// the rounding results in a higher quality trade
BEAST_EXPECT(
mptAlice.checkMPTokenAmount(carol, !features[featureMPTokensV2] ? 282 : 281));
}
// Insufficient SendMax with no transfer fee
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});
// Holders create MPToken
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
mptAlice.pay(alice, bob, 1'000);
auto const MPT = mptAlice["MPT"];
// SendMax is less than the amount
env(pay(bob, carol, MPT(100)), sendmax(MPT(99)), ter(tecPATH_PARTIAL));
env(pay(bob, alice, MPT(100)), sendmax(MPT(99)), ter(tecPATH_PARTIAL));
// Payment succeeds if sufficient SendMax is included.
env(pay(bob, carol, MPT(100)), sendmax(MPT(100)));
BEAST_EXPECT(mptAlice.checkMPTokenAmount(carol, 100));
// Payment succeeds if partial payment
env(pay(bob, carol, MPT(100)), sendmax(MPT(99)), txflags(tfPartialPayment));
BEAST_EXPECT(mptAlice.checkMPTokenAmount(carol, 199));
}
// DeliverMin
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});
// Holders create MPToken
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
mptAlice.pay(alice, bob, 1'000);
auto const MPT = mptAlice["MPT"];
// Fails even with the partial payment because
// deliver amount < deliverMin
env(pay(bob, alice, MPT(100)),
sendmax(MPT(99)),
deliver_min(MPT(100)),
txflags(tfPartialPayment),
ter(tecPATH_PARTIAL));
// Payment succeeds if deliver amount >= deliverMin
env(pay(bob, alice, MPT(100)),
sendmax(MPT(99)),
deliver_min(MPT(99)),
txflags(tfPartialPayment));
}
// Issuer fails trying to send more than the maximum amount allowed
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.maxAmt = 100, .ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});
mptAlice.authorize({.account = bob});
// issuer sends holder the max amount allowed
mptAlice.pay(alice, bob, 100);
// issuer tries to exceed max amount
auto const err = MPTokensV2 ? tecPATH_DRY : tecPATH_PARTIAL;
mptAlice.pay(alice, bob, 1, err);
}
// Issuer fails trying to send more than the default maximum
// amount allowed
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({.ownerCount = 1, .holderCount = 0});
mptAlice.authorize({.account = bob});
// issuer sends holder the default max amount allowed
mptAlice.pay(alice, bob, maxMPTokenAmount);
// issuer tries to exceed max amount
auto const err = MPTokensV2 ? tecPATH_DRY : tecPATH_PARTIAL;
mptAlice.pay(alice, bob, 1, err);
}
// Pay more than max amount fails in the json parser before
// transactor is called
{
Env env{*this, features};
env.fund(XRP(1'000), alice, bob);
STAmount const mpt{MPTIssue{makeMptID(1, alice)}, UINT64_C(100)};
Json::Value jv;
jv[jss::secret] = alice.name();
jv[jss::tx_json] = pay(alice, bob, mpt);
jv[jss::tx_json][jss::Amount][jss::value] = std::to_string(maxMPTokenAmount + 1);
auto const jrr = env.rpc("json", "submit", to_string(jv));
BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");
}
// Pay maximum amount with the transfer fee, SendMax, and
// partial payment
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create(
{.maxAmt = 10'000,
.transferFee = 100,
.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanTransfer});
auto const MPT = mptAlice["MPT"];
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
// issuer sends holder the max amount allowed
mptAlice.pay(alice, bob, 10'000);
// payment between the holders
env(pay(bob, carol, MPT(10'000)), sendmax(MPT(10'000)), txflags(tfPartialPayment));
// Verify the metadata
auto const meta = env.meta()->getJson(JsonOptions::none)[sfAffectedNodes.fieldName];
// Issuer got 10 in the transfer fees
BEAST_EXPECT(
meta[0u][sfModifiedNode.fieldName][sfFinalFields.fieldName]
[sfOutstandingAmount.fieldName] == "9990");
// Destination account got 9'990
BEAST_EXPECT(
meta[1u][sfModifiedNode.fieldName][sfFinalFields.fieldName]
[sfMPTAmount.fieldName] == "9990");
// Source account spent 10'000
BEAST_EXPECT(
meta[2u][sfModifiedNode.fieldName][sfPreviousFields.fieldName]
[sfMPTAmount.fieldName] == "10000");
BEAST_EXPECT(!meta[2u][sfModifiedNode.fieldName][sfFinalFields.fieldName].isMember(
sfMPTAmount.fieldName));
// payment between the holders fails without
// partial payment
auto const err = MPTokensV2 ? tecPATH_DRY : tecPATH_PARTIAL;
env(pay(bob, carol, MPT(10'000)), sendmax(MPT(10'000)), ter(err));
}
// Pay maximum allowed amount
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create(
{.maxAmt = maxMPTokenAmount,
.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanTransfer});
auto const MPT = mptAlice["MPT"];
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
// issuer sends holder the max amount allowed
mptAlice.pay(alice, bob, maxMPTokenAmount);
BEAST_EXPECT(mptAlice.checkMPTokenOutstandingAmount(maxMPTokenAmount));
// payment between the holders
mptAlice.pay(bob, carol, maxMPTokenAmount);
BEAST_EXPECT(mptAlice.checkMPTokenOutstandingAmount(maxMPTokenAmount));
// holder pays back to the issuer
mptAlice.pay(carol, alice, maxMPTokenAmount);
BEAST_EXPECT(mptAlice.checkMPTokenOutstandingAmount(0));
}
// Issuer fails trying to send fund after issuance was destroyed
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({.ownerCount = 1, .holderCount = 0});
mptAlice.authorize({.account = bob});
// alice destroys issuance
mptAlice.destroy({.ownerCount = 0});
// alice tries to send bob fund after issuance is destroyed, should
// fail.
mptAlice.pay(alice, bob, 100, tecOBJECT_NOT_FOUND);
}
// Non-existent issuance
{
Env env{*this, features};
env.fund(XRP(1'000), alice, bob);
STAmount const mpt{MPTID{0}, 100};
auto const err =
!features[featureMPTokensV2] ? ter(tecOBJECT_NOT_FOUND) : ter(temBAD_CURRENCY);
env(pay(alice, bob, mpt), err);
}
// Issuer fails trying to send to an account, which doesn't own MPT for
// an issuance that was destroyed
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({.ownerCount = 1, .holderCount = 0});
// alice destroys issuance
mptAlice.destroy({.ownerCount = 0});
// alice tries to send bob who doesn't own the MPT after issuance is
// destroyed, it should fail
mptAlice.pay(alice, bob, 100, tecOBJECT_NOT_FOUND);
}
// Issuers issues maximum amount of MPT to a holder, the holder should
// be able to transfer the max amount to someone else
{
Env env{*this, features};
Account const alice("alice");
Account const carol("bob");
Account const bob("carol");
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create({.maxAmt = 100, .ownerCount = 1, .flags = tfMPTCanTransfer});
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
mptAlice.pay(alice, bob, 100);
// transfer max amount to another holder
mptAlice.pay(bob, carol, 100);
}
// Simple payment
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
// issuer to holder
mptAlice.pay(alice, bob, 100);
// holder to issuer
mptAlice.pay(bob, alice, 100);
// holder to holder
mptAlice.pay(alice, bob, 100);
mptAlice.pay(bob, carol, 50);
}
}
void
testDepositPreauth(FeatureBitset features)
{
using namespace test::jtx;
Account const alice("alice"); // issuer
Account const bob("bob"); // holder
Account const diana("diana");
Account const dpIssuer("dpIssuer"); // holder
char const credType[] = "abcde";
if (features[featureCredentials])
{
testcase("DepositPreauth");
Env env(*this, features);
env.fund(XRP(50000), diana, dpIssuer);
env.close();
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTRequireAuth | tfMPTCanTransfer});
env(pay(diana, bob, XRP(500)));
env.close();
// bob creates an empty MPToken
mptAlice.authorize({.account = bob});
// alice authorizes bob to hold funds
mptAlice.authorize({.account = alice, .holder = bob});
// Bob require pre-authorization
env(fset(bob, asfDepositAuth));
env.close();
// alice try to send 100 MPT to bob, not authorized
mptAlice.pay(alice, bob, 100, tecNO_PERMISSION);
env.close();
// Bob authorize alice
env(deposit::auth(bob, alice));
env.close();
// alice sends 100 MPT to bob
mptAlice.pay(alice, bob, 100);
env.close();
// Create credentials
env(credentials::create(alice, dpIssuer, credType));
env.close();
env(credentials::accept(alice, dpIssuer, credType));
env.close();
auto const jv = credentials::ledgerEntry(env, alice, dpIssuer, credType);
std::string const credIdx = jv[jss::result][jss::index].asString();
// alice sends 100 MPT to bob with credentials which aren't required
mptAlice.pay(alice, bob, 100, tesSUCCESS, {{credIdx}});
env.close();
// Bob revoke authorization
env(deposit::unauth(bob, alice));
env.close();
// alice try to send 100 MPT to bob, not authorized
mptAlice.pay(alice, bob, 100, tecNO_PERMISSION);
env.close();
// alice sends 100 MPT to bob with credentials, not authorized
mptAlice.pay(alice, bob, 100, tecNO_PERMISSION, {{credIdx}});
env.close();
// Bob authorize credentials
env(deposit::authCredentials(bob, {{dpIssuer, credType}}));
env.close();
// alice try to send 100 MPT to bob, not authorized
mptAlice.pay(alice, bob, 100, tecNO_PERMISSION);
env.close();
// alice sends 100 MPT to bob with credentials
mptAlice.pay(alice, bob, 100, tesSUCCESS, {{credIdx}});
env.close();
}
testcase("DepositPreauth disabled featureCredentials");
{
Env env(*this, testable_amendments() - featureCredentials);
std::string const credIdx =
"D007AE4B6E1274B4AF872588267B810C2F82716726351D1C7D38D3E5499FC6"
"E2";
env.fund(XRP(50000), diana, dpIssuer);
env.close();
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTRequireAuth | tfMPTCanTransfer});
env(pay(diana, bob, XRP(500)));
env.close();
// bob creates an empty MPToken
mptAlice.authorize({.account = bob});
// alice authorizes bob to hold funds
mptAlice.authorize({.account = alice, .holder = bob});
// Bob require pre-authorization
env(fset(bob, asfDepositAuth));
env.close();
// alice try to send 100 MPT to bob, not authorized
mptAlice.pay(alice, bob, 100, tecNO_PERMISSION);
env.close();
// alice try to send 100 MPT to bob with credentials, amendment
// disabled
mptAlice.pay(alice, bob, 100, temDISABLED, {{credIdx}});
env.close();
// Bob authorize alice
env(deposit::auth(bob, alice));
env.close();
// alice sends 100 MPT to bob
mptAlice.pay(alice, bob, 100);
env.close();
// alice sends 100 MPT to bob with credentials, amendment disabled
mptAlice.pay(alice, bob, 100, temDISABLED, {{credIdx}});
env.close();
// Bob revoke authorization
env(deposit::unauth(bob, alice));
env.close();
// alice try to send 100 MPT to bob
mptAlice.pay(alice, bob, 100, tecNO_PERMISSION);
env.close();
// alice sends 100 MPT to bob with credentials, amendment disabled
mptAlice.pay(alice, bob, 100, temDISABLED, {{credIdx}});
env.close();
}
}
void
testMPTInvalidInTx(FeatureBitset features)
{
testcase("MPT Issue Invalid in Transaction");
using namespace test::jtx;
// Validate that every transaction with an amount/issue field,
// which doesn't support MPT, fails.
// keyed by transaction + amount/issue field
std::set<std::string> txWithAmounts;
for (auto const& format : TxFormats::getInstance())
{
for (auto const& e : format.getSOTemplate())
{
// Transaction has amount/issue fields.
// Exclude pseudo-transaction SetFee. Don't consider
// the Fee field since it's included in every transaction.
if (e.supportMPT() == soeMPTNotSupported && e.sField().getName() != jss::Fee &&
format.getName() != jss::SetFee)
{
txWithAmounts.insert(format.getName() + e.sField().fieldName);
break;
}
}
}
Account const alice("alice");
auto const USD = alice["USD"];
Account const carol("carol");
MPTIssue const issue(makeMptID(1, alice));
STAmount mpt{issue, UINT64_C(100)};
auto const jvb = bridge(alice, USD, alice, USD);
for (auto const& feature : {features, features - featureMPTokensV1})
{
Env env{*this, feature};
env.fund(XRP(1'000), alice);
env.fund(XRP(1'000), carol);
auto test = [&](Json::Value const& jv, std::string const& mptField) {
txWithAmounts.erase(jv[jss::TransactionType].asString() + mptField);
// tx is signed
auto jtx = env.jt(jv);
Serializer s;
jtx.stx->add(s);
auto jrr = env.rpc("submit", strHex(s.slice()));
BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidTransaction");
// tx is unsigned
Json::Value jv1;
jv1[jss::secret] = alice.name();
jv1[jss::tx_json] = jv;
jrr = env.rpc("json", "submit", to_string(jv1));
BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");
jrr = env.rpc("json", "sign", to_string(jv1));
BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");
};
auto setMPTFields = [&](SField const& field, Json::Value& jv, bool withAmount = true) {
jv[jss::Asset] = to_json(xrpIssue());
jv[jss::Asset2] = to_json(USD.issue());
if (withAmount)
jv[field.fieldName] = USD(10).value().getJson(JsonOptions::none);
if (field == sfAsset)
{
jv[jss::Asset] = to_json(mpt.get<MPTIssue>());
}
else if (field == sfAsset2)
{
jv[jss::Asset2] = to_json(mpt.get<MPTIssue>());
}
else
{
jv[field.fieldName] = mpt.getJson(JsonOptions::none);
}
};
// All transactions with sfAmount, which don't support MPT.
// Transactions with amount fields, which can't be MPT.
// Transactions with issue fields, which can't be MPT.
// AMMDeposit
auto ammDeposit = [&](SField const& field) {
Json::Value jv;
jv[jss::TransactionType] = jss::AMMDeposit;
jv[jss::Account] = alice.human();
jv[jss::Flags] = tfSingleAsset;
setMPTFields(field, jv);
test(jv, field.fieldName);
};
for (SField const& field : {std::ref(sfEPrice), std::ref(sfLPTokenOut)})
ammDeposit(field);
// AMMWithdraw
auto ammWithdraw = [&](SField const& field) {
Json::Value jv;
jv[jss::TransactionType] = jss::AMMWithdraw;
jv[jss::Account] = alice.human();
jv[jss::Flags] = tfSingleAsset;
setMPTFields(field, jv);
test(jv, field.fieldName);
};
for (SField const& field : {std::ref(sfEPrice), std::ref(sfLPTokenIn)})
ammWithdraw(field);
// AMMBid
auto ammBid = [&](SField const& field) {
Json::Value jv;
jv[jss::TransactionType] = jss::AMMBid;
jv[jss::Account] = alice.human();
setMPTFields(field, jv);
test(jv, field.fieldName);
};
ammBid(sfBidMin);
ammBid(sfBidMax);
// PaymentChannelCreate
{
Json::Value jv;
jv[jss::TransactionType] = jss::PaymentChannelCreate;
jv[jss::Account] = alice.human();
jv[jss::Destination] = carol.human();
jv[jss::SettleDelay] = 1;
jv[sfPublicKey.fieldName] = strHex(alice.pk().slice());
jv[jss::Amount] = mpt.getJson(JsonOptions::none);
test(jv, jss::Amount.c_str());
}
// PaymentChannelFund
{
Json::Value jv;
jv[jss::TransactionType] = jss::PaymentChannelFund;
jv[jss::Account] = alice.human();
jv[sfChannel.fieldName] = to_string(uint256{1});
jv[jss::Amount] = mpt.getJson(JsonOptions::none);
test(jv, jss::Amount.c_str());
}
// PaymentChannelClaim
{
Json::Value jv;
jv[jss::TransactionType] = jss::PaymentChannelClaim;
jv[jss::Account] = alice.human();
jv[sfChannel.fieldName] = to_string(uint256{1});
jv[jss::Amount] = mpt.getJson(JsonOptions::none);
test(jv, jss::Amount.c_str());
}
// NFTokenCreateOffer
{
Json::Value jv;
jv[jss::TransactionType] = jss::NFTokenCreateOffer;
jv[jss::Account] = alice.human();
jv[sfNFTokenID.fieldName] = to_string(uint256{1});
jv[jss::Amount] = mpt.getJson(JsonOptions::none);
test(jv, jss::Amount.c_str());
}
// NFTokenAcceptOffer
{
Json::Value jv;
jv[jss::TransactionType] = jss::NFTokenAcceptOffer;
jv[jss::Account] = alice.human();
jv[sfNFTokenBrokerFee.fieldName] = mpt.getJson(JsonOptions::none);
test(jv, sfNFTokenBrokerFee.fieldName);
}
// NFTokenMint
{
Json::Value jv;
jv[jss::TransactionType] = jss::NFTokenMint;
jv[jss::Account] = alice.human();
jv[sfNFTokenTaxon.fieldName] = 1;
jv[jss::Amount] = mpt.getJson(JsonOptions::none);
test(jv, jss::Amount.c_str());
}
// TrustSet
auto trustSet = [&](SField const& field) {
Json::Value jv;
jv[jss::TransactionType] = jss::TrustSet;
jv[jss::Account] = alice.human();
jv[jss::Flags] = 0;
jv[field.fieldName] = mpt.getJson(JsonOptions::none);
test(jv, field.fieldName);
};
trustSet(sfLimitAmount);
trustSet(sfFee);
// XChainCommit
{
Json::Value const jv = xchain_commit(alice, jvb, 1, mpt);
test(jv, jss::Amount.c_str());
}
// XChainClaim
{
Json::Value const jv = xchain_claim(alice, jvb, 1, mpt, alice);
test(jv, jss::Amount.c_str());
}
// XChainCreateClaimID
{
Json::Value const jv = xchain_create_claim_id(alice, jvb, mpt, alice);
test(jv, sfSignatureReward.fieldName);
}
// XChainAddClaimAttestation
{
Json::Value const jv =
claim_attestation(alice, jvb, alice, mpt, alice, true, 1, alice, signer(alice));
test(jv, jss::Amount.c_str());
}
// XChainAddAccountCreateAttestation
{
Json::Value jv = create_account_attestation(
alice, jvb, alice, mpt, XRP(10), alice, false, 1, alice, signer(alice));
for (auto const& field : {sfAmount.fieldName, sfSignatureReward.fieldName})
{
jv[field] = mpt.getJson(JsonOptions::none);
test(jv, field);
}
}
// XChainAccountCreateCommit
{
Json::Value jv = sidechain_xchain_account_create(alice, jvb, alice, mpt, XRP(10));
for (auto const& field : {sfAmount.fieldName, sfSignatureReward.fieldName})
{
jv[field] = mpt.getJson(JsonOptions::none);
test(jv, field);
}
}
// XChain[Create|Modify]Bridge
auto bridgeTx = [&](Json::StaticString const& tt,
STAmount const& rewardAmount,
STAmount const& minAccountAmount,
std::string const& field) {
Json::Value jv;
jv[jss::TransactionType] = tt;
jv[jss::Account] = alice.human();
jv[sfXChainBridge.fieldName] = jvb;
jv[sfSignatureReward.fieldName] = rewardAmount.getJson(JsonOptions::none);
jv[sfMinAccountCreateAmount.fieldName] =
minAccountAmount.getJson(JsonOptions::none);
test(jv, field);
};
auto reward = STAmount{sfSignatureReward, mpt};
auto minAmount = STAmount{sfMinAccountCreateAmount, USD(10)};
for (SField const& field :
{std::ref(sfSignatureReward), std::ref(sfMinAccountCreateAmount)})
{
bridgeTx(jss::XChainCreateBridge, reward, minAmount, field.fieldName);
bridgeTx(jss::XChainModifyBridge, reward, minAmount, field.fieldName);
reward = STAmount{sfSignatureReward, USD(10)};
minAmount = STAmount{sfMinAccountCreateAmount, mpt};
}
}
BEAST_EXPECT(txWithAmounts.empty());
}
void
testTxJsonMetaFields(FeatureBitset features)
{
// checks synthetically injected mptissuanceid from `tx` response
testcase("Test synthetic fields from tx response");
using namespace test::jtx;
Account const alice{"alice"};
auto cfg = envconfig();
cfg->FEES.reference_fee = 10;
Env env{*this, std::move(cfg), features};
MPTTester mptAlice(env, alice);
mptAlice.create();
std::string const txHash{env.tx()->getJson(JsonOptions::none)[jss::hash].asString()};
BEAST_EXPECTS(
txHash ==
"E11F0E0CA14219922B7881F060B9CEE67CFBC87E4049A441ED2AE348FF8FAC"
"0E",
txHash);
Json::Value const meta = env.rpc("tx", txHash)[jss::result][jss::meta];
auto const id = meta[jss::mpt_issuance_id].asString();
// Expect mpt_issuance_id field
BEAST_EXPECT(meta.isMember(jss::mpt_issuance_id));
BEAST_EXPECT(id == to_string(mptAlice.issuanceID()));
BEAST_EXPECTS(id == "00000004AE123A8556F3CF91154711376AFB0F894F832B3D", id);
}
void
testClawbackValidation(FeatureBitset features)
{
testcase("MPT clawback validations");
using namespace test::jtx;
// Make sure clawback cannot work when featureMPTokensV1 is disabled
{
Env env(*this, features - featureMPTokensV1);
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(1000), alice, bob);
env.close();
auto const USD = alice["USD"];
auto const mpt = xrpl::test::jtx::MPT(alice.name(), makeMptID(env.seq(alice), alice));
env(claw(alice, bob["USD"](5), bob), ter(temMALFORMED));
env.close();
env(claw(alice, mpt(5)), ter(temDISABLED));
env.close();
env(claw(alice, mpt(5), bob), ter(temDISABLED));
env.close();
}
// Test preflight
{
Env env(*this, features);
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(1000), alice, bob);
env.close();
auto const USD = alice["USD"];
auto const mpt = xrpl::test::jtx::MPT(alice.name(), makeMptID(env.seq(alice), alice));
// clawing back IOU from a MPT holder fails
env(claw(alice, bob["USD"](5), bob), ter(temMALFORMED));
env.close();
// clawing back MPT without specifying a holder fails
env(claw(alice, mpt(5)), ter(temMALFORMED));
env.close();
// clawing back zero amount fails
env(claw(alice, mpt(0), bob), ter(temBAD_AMOUNT));
env.close();
// alice can't claw back from herself
env(claw(alice, mpt(5), alice), ter(temMALFORMED));
env.close();
// can't clawback negative amount
env(claw(alice, mpt(-1), bob), ter(temBAD_AMOUNT));
env.close();
}
// Preclaim - clawback fails when MPTCanClawback is disabled on issuance
{
Env env(*this, features);
Account const alice{"alice"};
Account const bob{"bob"};
MPTTester mptAlice(env, alice, {.holders = {bob}});
// enable asfAllowTrustLineClawback for alice
env(fset(alice, asfAllowTrustLineClawback));
env.close();
env.require(flags(alice, asfAllowTrustLineClawback));
// Create issuance without enabling clawback
mptAlice.create({.ownerCount = 1, .holderCount = 0});
mptAlice.authorize({.account = bob});
mptAlice.pay(alice, bob, 100);
// alice cannot clawback before she didn't enable MPTCanClawback
// asfAllowTrustLineClawback has no effect
mptAlice.claw(alice, bob, 1, tecNO_PERMISSION);
}
// Preclaim - test various scenarios
{
Env env(*this, features);
Account const alice{"alice"};
Account const bob{"bob"};
Account const carol{"carol"};
env.fund(XRP(1000), carol);
env.close();
MPTTester mptAlice(env, alice, {.holders = {bob}});
auto const fakeMpt =
xrpl::test::jtx::MPT(alice.name(), makeMptID(env.seq(alice), alice));
// issuer tries to clawback MPT where issuance doesn't exist
env(claw(alice, fakeMpt(5), bob), ter(tecOBJECT_NOT_FOUND));
env.close();
// alice creates issuance
mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanClawback});
// alice tries to clawback from someone who doesn't have MPToken
mptAlice.claw(alice, bob, 1, tecOBJECT_NOT_FOUND);
// bob creates a MPToken
mptAlice.authorize({.account = bob});
// clawback fails because bob currently has a balance of zero
mptAlice.claw(alice, bob, 1, tecINSUFFICIENT_FUNDS);
// alice pays bob 100 tokens
mptAlice.pay(alice, bob, 100);
// carol fails tries to clawback from bob because he is not the
// issuer
mptAlice.claw(carol, bob, 1, tecNO_PERMISSION);
}
// clawback more than max amount
// fails in the json parser before
// transactor is called
{
Env env(*this, features);
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(1000), alice, bob);
env.close();
auto const mpt = xrpl::test::jtx::MPT(alice.name(), makeMptID(env.seq(alice), alice));
Json::Value jv = claw(alice, mpt(1), bob);
jv[jss::Amount][jss::value] = std::to_string(maxMPTokenAmount + 1);
Json::Value jv1;
jv1[jss::secret] = alice.name();
jv1[jss::tx_json] = jv;
auto const jrr = env.rpc("json", "submit", to_string(jv1));
BEAST_EXPECT(jrr[jss::result][jss::error] == "invalidParams");
}
}
void
testClawback(FeatureBitset features)
{
testcase("MPT Clawback");
using namespace test::jtx;
{
Env env(*this, features);
Account const alice{"alice"};
Account const bob{"bob"};
MPTTester mptAlice(env, alice, {.holders = {bob}});
// alice creates issuance
mptAlice.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanClawback});
// bob creates a MPToken
mptAlice.authorize({.account = bob});
// alice pays bob 100 tokens
mptAlice.pay(alice, bob, 100);
mptAlice.claw(alice, bob, 1);
mptAlice.claw(alice, bob, 1000);
// clawback fails because bob currently has a balance of zero
mptAlice.claw(alice, bob, 1, tecINSUFFICIENT_FUNDS);
}
// Test that globally locked funds can be clawed
{
Env env(*this, features);
Account const alice{"alice"};
Account const bob{"bob"};
MPTTester mptAlice(env, alice, {.holders = {bob}});
// alice creates issuance
mptAlice.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanLock | tfMPTCanClawback});
// bob creates a MPToken
mptAlice.authorize({.account = bob});
// alice pays bob 100 tokens
mptAlice.pay(alice, bob, 100);
mptAlice.set({.account = alice, .flags = tfMPTLock});
mptAlice.claw(alice, bob, 100);
}
// Test that individually locked funds can be clawed
{
Env env(*this, features);
Account const alice{"alice"};
Account const bob{"bob"};
MPTTester mptAlice(env, alice, {.holders = {bob}});
// alice creates issuance
mptAlice.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanLock | tfMPTCanClawback});
// bob creates a MPToken
mptAlice.authorize({.account = bob});
// alice pays bob 100 tokens
mptAlice.pay(alice, bob, 100);
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
mptAlice.claw(alice, bob, 100);
}
// Test that unauthorized funds can be clawed back
{
Env env(*this, features);
Account const alice{"alice"};
Account const bob{"bob"};
MPTTester mptAlice(env, alice, {.holders = {bob}});
// alice creates issuance
mptAlice.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanClawback | tfMPTRequireAuth});
// bob creates a MPToken
mptAlice.authorize({.account = bob});
// alice authorizes bob
mptAlice.authorize({.account = alice, .holder = bob});
// alice pays bob 100 tokens
mptAlice.pay(alice, bob, 100);
// alice unauthorizes bob
mptAlice.authorize({.account = alice, .holder = bob, .flags = tfMPTUnauthorize});
mptAlice.claw(alice, bob, 100);
}
}
void
testTokensEquality()
{
using namespace test::jtx;
testcase("Tokens Equality");
Currency const cur1{to_currency("CU1")};
Currency const cur2{to_currency("CU2")};
Account const gw1{"gw1"};
Account const gw2{"gw2"};
MPTID const mpt1 = makeMptID(1, gw1);
MPTID const mpt1a = makeMptID(1, gw1);
MPTID const mpt2 = makeMptID(1, gw2);
MPTID const mpt3 = makeMptID(2, gw2);
Asset const assetCur1Gw1{Issue{cur1, gw1}};
Asset const assetCur1Gw1a{Issue{cur1, gw1}};
Asset const assetCur2Gw1{Issue{cur2, gw1}};
Asset const assetCur2Gw2{Issue{cur2, gw2}};
Asset const assetMpt1Gw1{mpt1};
Asset const assetMpt1Gw1a{mpt1a};
Asset const assetMpt1Gw2{mpt2};
Asset const assetMpt2Gw2{mpt3};
// Assets holding Issue
// Currencies are equal regardless of the issuer
BEAST_EXPECT(equalTokens(assetCur1Gw1, assetCur1Gw1a));
BEAST_EXPECT(equalTokens(assetCur2Gw1, assetCur2Gw2));
// Currencies are different regardless of whether the issuers
// are the same or not
BEAST_EXPECT(!equalTokens(assetCur1Gw1, assetCur2Gw1));
BEAST_EXPECT(!equalTokens(assetCur1Gw1, assetCur2Gw2));
// Assets holding MPTIssue
// MPTIDs are the same if the sequence and the issuer are the same
BEAST_EXPECT(equalTokens(assetMpt1Gw1, assetMpt1Gw1a));
// MPTIDs are different if sequence and the issuer don't match
BEAST_EXPECT(!equalTokens(assetMpt1Gw1, assetMpt1Gw2));
BEAST_EXPECT(!equalTokens(assetMpt1Gw2, assetMpt2Gw2));
// Assets holding Issue and MPTIssue
BEAST_EXPECT(!equalTokens(assetCur1Gw1, assetMpt1Gw1));
BEAST_EXPECT(!equalTokens(assetMpt2Gw2, assetCur2Gw2));
}
void
testHelperFunctions()
{
using namespace test::jtx;
Account const gw{"gw"};
Asset const asset1{makeMptID(1, gw)};
Asset const asset2{makeMptID(2, gw)};
Asset const asset3{makeMptID(3, gw)};
STAmount const amt1{asset1, 100};
STAmount const amt2{asset2, 100};
STAmount const amt3{asset3, 10'000};
{
testcase("Test STAmount MPT arithmetic");
using namespace std::string_literals;
STAmount res = multiply(amt1, amt2, asset3);
BEAST_EXPECT(res == amt3);
res = mulRound(amt1, amt2, asset3, true);
BEAST_EXPECT(res == amt3);
res = mulRoundStrict(amt1, amt2, asset3, true);
BEAST_EXPECT(res == amt3);
// overflow, any value > 3037000499ull
STAmount mptOverflow{asset2, UINT64_C(3037000500)};
try
{
res = multiply(mptOverflow, mptOverflow, asset3);
fail("should throw runtime exception 1");
}
catch (std::runtime_error const& e)
{
BEAST_EXPECTS(e.what() == "MPT value overflow"s, e.what());
}
// overflow, (v1 >> 32) * v2 > 2147483648ull
mptOverflow = STAmount{asset2, UINT64_C(2147483648)};
uint64_t const mantissa = (2ull << 32) + 2;
try
{
res = multiply(STAmount{asset1, mantissa}, mptOverflow, asset3);
fail("should throw runtime exception 2");
}
catch (std::runtime_error const& e)
{
BEAST_EXPECTS(e.what() == "MPT value overflow"s, e.what());
}
}
{
testcase("Test MPTAmount arithmetic");
MPTAmount mptAmt1{100};
MPTAmount const mptAmt2{100};
BEAST_EXPECT((mptAmt1 += mptAmt2) == MPTAmount{200});
BEAST_EXPECT(mptAmt1 == 200);
BEAST_EXPECT((mptAmt1 -= mptAmt2) == mptAmt1);
BEAST_EXPECT(mptAmt1 == mptAmt2);
BEAST_EXPECT(mptAmt1 == 100);
BEAST_EXPECT(MPTAmount::minPositiveAmount() == MPTAmount{1});
}
{
testcase("Test MPTIssue from/to Json");
MPTIssue const issue1{asset1.get<MPTIssue>()};
Json::Value const jv = to_json(issue1);
BEAST_EXPECT(jv[jss::mpt_issuance_id] == to_string(asset1.get<MPTIssue>()));
BEAST_EXPECT(issue1 == mptIssueFromJson(jv));
}
{
testcase("Test Asset from/to Json");
Json::Value const jv = to_json(asset1);
BEAST_EXPECT(jv[jss::mpt_issuance_id] == to_string(asset1.get<MPTIssue>()));
BEAST_EXPECT(
to_string(jv) ==
"{\"mpt_issuance_id\":"
"\"00000001A407AF5856CCF3C42619DAA925813FC955C72983\"}");
BEAST_EXPECT(asset1 == assetFromJson(jv));
}
}
void
testInvalidCreateDynamic(FeatureBitset features)
{
testcase("invalid MPTokenIssuanceCreate for DynamicMPT");
using namespace test::jtx;
Account const alice("alice");
// Can not provide MutableFlags when DynamicMPT amendment is not enabled
{
Env env{*this, features - featureDynamicMPT};
MPTTester mptAlice(env, alice);
mptAlice.create({.ownerCount = 0, .mutableFlags = 2, .err = temDISABLED});
mptAlice.create({.ownerCount = 0, .mutableFlags = 0, .err = temDISABLED});
}
// MutableFlags contains invalid values
{
Env env{*this, features};
MPTTester mptAlice(env, alice);
// Value 1 is reserved for MPT lock.
mptAlice.create({.ownerCount = 0, .mutableFlags = 1, .err = temINVALID_FLAG});
mptAlice.create({.ownerCount = 0, .mutableFlags = 17, .err = temINVALID_FLAG});
mptAlice.create({.ownerCount = 0, .mutableFlags = 65535, .err = temINVALID_FLAG});
// MutableFlags can not be 0
mptAlice.create({.ownerCount = 0, .mutableFlags = 0, .err = temINVALID_FLAG});
}
}
void
testInvalidSetDynamic(FeatureBitset features)
{
testcase("invalid MPTokenIssuanceSet for DynamicMPT");
using namespace test::jtx;
Account const alice("alice");
Account const bob("bob");
// Can not provide MutableFlags, MPTokenMetadata or TransferFee when
// DynamicMPT amendment is not enabled
{
Env env{*this, features - featureDynamicMPT};
MPTTester mptAlice(env, alice, {.holders = {bob}});
auto const mptID = makeMptID(env.seq(alice), alice);
// MutableFlags is not allowed when DynamicMPT is not enabled
mptAlice.set({.account = alice, .id = mptID, .mutableFlags = 2, .err = temDISABLED});
mptAlice.set({.account = alice, .id = mptID, .mutableFlags = 0, .err = temDISABLED});
// MPTokenMetadata is not allowed when DynamicMPT is not enabled
mptAlice.set({.account = alice, .id = mptID, .metadata = "test", .err = temDISABLED});
mptAlice.set({.account = alice, .id = mptID, .metadata = "", .err = temDISABLED});
// TransferFee is not allowed when DynamicMPT is not enabled
mptAlice.set({.account = alice, .id = mptID, .transferFee = 100, .err = temDISABLED});
mptAlice.set({.account = alice, .id = mptID, .transferFee = 0, .err = temDISABLED});
}
// Can not provide holder when MutableFlags, MPTokenMetadata or
// TransferFee is present
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
auto const mptID = makeMptID(env.seq(alice), alice);
// Holder is not allowed when MutableFlags is present
mptAlice.set(
{.account = alice,
.holder = bob,
.id = mptID,
.mutableFlags = 2,
.err = temMALFORMED});
// Holder is not allowed when MPTokenMetadata is present
mptAlice.set(
{.account = alice,
.holder = bob,
.id = mptID,
.metadata = "test",
.err = temMALFORMED});
// Holder is not allowed when TransferFee is present
mptAlice.set(
{.account = alice,
.holder = bob,
.id = mptID,
.transferFee = 100,
.err = temMALFORMED});
}
// Can not set Flags when MutableFlags, MPTokenMetadata or
// TransferFee is present
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.ownerCount = 1,
.mutableFlags = tmfMPTCanMutateMetadata | tmfMPTCanMutateCanLock |
tmfMPTCanMutateTransferFee});
// Setting flags is not allowed when MutableFlags is present
mptAlice.set(
{.account = alice, .flags = tfMPTCanLock, .mutableFlags = 2, .err = temMALFORMED});
// Setting flags is not allowed when MPTokenMetadata is present
mptAlice.set(
{.account = alice, .flags = tfMPTCanLock, .metadata = "test", .err = temMALFORMED});
// setting flags is not allowed when TransferFee is present
mptAlice.set(
{.account = alice, .flags = tfMPTCanLock, .transferFee = 100, .err = temMALFORMED});
}
// Flags being 0 or tfFullyCanonicalSig is fine
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.transferFee = 10,
.ownerCount = 1,
.flags = tfMPTCanTransfer,
.mutableFlags = tmfMPTCanMutateTransferFee | tmfMPTCanMutateMetadata});
mptAlice.set({.account = alice, .flags = 0, .transferFee = 100, .metadata = "test"});
mptAlice.set(
{.account = alice,
.flags = tfFullyCanonicalSig,
.transferFee = 200,
.metadata = "test2"});
}
// Invalid MutableFlags
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
auto const mptID = makeMptID(env.seq(alice), alice);
for (auto const flags : {10000, 0, 5000})
{
mptAlice.set(
{.account = alice, .id = mptID, .mutableFlags = flags, .err = temINVALID_FLAG});
}
}
// Can not set and clear the same mutable flag
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
auto const mptID = makeMptID(env.seq(alice), alice);
auto const flagCombinations = {
tmfMPTSetCanLock | tmfMPTClearCanLock,
tmfMPTSetRequireAuth | tmfMPTClearRequireAuth,
tmfMPTSetCanEscrow | tmfMPTClearCanEscrow,
tmfMPTSetCanTrade | tmfMPTClearCanTrade,
tmfMPTSetCanTransfer | tmfMPTClearCanTransfer,
tmfMPTSetCanClawback | tmfMPTClearCanClawback,
tmfMPTSetCanLock | tmfMPTClearCanLock | tmfMPTClearCanTrade,
tmfMPTSetCanTransfer | tmfMPTClearCanTransfer | tmfMPTSetCanEscrow |
tmfMPTClearCanClawback};
for (auto const& mutableFlags : flagCombinations)
{
mptAlice.set(
{.account = alice,
.id = mptID,
.mutableFlags = mutableFlags,
.err = temINVALID_FLAG});
}
}
// Can not mutate flag which is not mutable
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({.ownerCount = 1});
auto const mutableFlags = {
tmfMPTSetCanLock,
tmfMPTClearCanLock,
tmfMPTSetRequireAuth,
tmfMPTClearRequireAuth,
tmfMPTSetCanEscrow,
tmfMPTClearCanEscrow,
tmfMPTSetCanTrade,
tmfMPTClearCanTrade,
tmfMPTSetCanTransfer,
tmfMPTClearCanTransfer,
tmfMPTSetCanClawback,
tmfMPTClearCanClawback};
for (auto const& mutableFlag : mutableFlags)
{
mptAlice.set(
{.account = alice, .mutableFlags = mutableFlag, .err = tecNO_PERMISSION});
}
}
// Metadata exceeding max length
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({.ownerCount = 1, .mutableFlags = tmfMPTCanMutateMetadata});
std::string metadata(maxMPTokenMetadataLength + 1, 'a');
mptAlice.set({.account = alice, .metadata = metadata, .err = temMALFORMED});
}
// Can not mutate metadata when it is not mutable
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({.ownerCount = 1});
mptAlice.set({.account = alice, .metadata = "test", .err = tecNO_PERMISSION});
}
// Transfer fee exceeding the max value
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
auto const mptID = makeMptID(env.seq(alice), alice);
mptAlice.create({.ownerCount = 1, .mutableFlags = tmfMPTCanMutateTransferFee});
mptAlice.set(
{.account = alice,
.id = mptID,
.transferFee = maxTransferFee + 1,
.err = temBAD_TRANSFER_FEE});
}
// Test setting non-zero transfer fee and clearing MPTCanTransfer at the
// same time
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.transferFee = 100,
.ownerCount = 1,
.flags = tfMPTCanTransfer,
.mutableFlags = tmfMPTCanMutateTransferFee | tmfMPTCanMutateCanTransfer});
// Can not set non-zero transfer fee and clear MPTCanTransfer at the
// same time
mptAlice.set(
{.account = alice,
.mutableFlags = tmfMPTClearCanTransfer,
.transferFee = 1,
.err = temMALFORMED});
// Can set transfer fee to zero and clear MPTCanTransfer at the same
// time. tfMPTCanTransfer will be cleared and TransferFee field will
// be removed.
mptAlice.set(
{.account = alice, .mutableFlags = tmfMPTClearCanTransfer, .transferFee = 0});
BEAST_EXPECT(!mptAlice.isTransferFeePresent());
}
// Can not set non-zero transfer fee when MPTCanTransfer is not set
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.ownerCount = 1,
.mutableFlags = tmfMPTCanMutateTransferFee | tmfMPTCanMutateCanTransfer});
mptAlice.set({.account = alice, .transferFee = 100, .err = tecNO_PERMISSION});
// Can not set transfer fee even when trying to set MPTCanTransfer
// at the same time. MPTCanTransfer must be set first, then transfer
// fee can be set in a separate transaction.
mptAlice.set(
{.account = alice,
.mutableFlags = tmfMPTSetCanTransfer,
.transferFee = 100,
.err = tecNO_PERMISSION});
}
// Can not mutate transfer fee when it is not mutable
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({.transferFee = 10, .ownerCount = 1, .flags = tfMPTCanTransfer});
mptAlice.set({.account = alice, .transferFee = 100, .err = tecNO_PERMISSION});
mptAlice.set({.account = alice, .transferFee = 0, .err = tecNO_PERMISSION});
}
// Set some flags mutable. Can not mutate the others
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.ownerCount = 1,
.mutableFlags = tmfMPTCanMutateCanTrade | tmfMPTCanMutateCanTransfer |
tmfMPTCanMutateMetadata});
// Can not mutate transfer fee
mptAlice.set({.account = alice, .transferFee = 100, .err = tecNO_PERMISSION});
auto const invalidFlags = {
tmfMPTSetCanLock,
tmfMPTClearCanLock,
tmfMPTSetRequireAuth,
tmfMPTClearRequireAuth,
tmfMPTSetCanEscrow,
tmfMPTClearCanEscrow,
tmfMPTSetCanClawback,
tmfMPTClearCanClawback};
// Can not mutate flags which are not mutable
for (auto const& mutableFlag : invalidFlags)
{
mptAlice.set(
{.account = alice, .mutableFlags = mutableFlag, .err = tecNO_PERMISSION});
}
// Can mutate MPTCanTrade
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanTrade});
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanTrade});
// Can mutate MPTCanTransfer
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanTransfer});
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanTransfer});
// Can mutate metadata
mptAlice.set({.account = alice, .metadata = "test"});
mptAlice.set({.account = alice, .metadata = ""});
}
}
void
testMutateMPT(FeatureBitset features)
{
testcase("Mutate MPT");
using namespace test::jtx;
Account const alice("alice");
// Mutate metadata
{
Env env{*this, features};
MPTTester mptAlice(env, alice);
mptAlice.create(
{.metadata = "test", .ownerCount = 1, .mutableFlags = tmfMPTCanMutateMetadata});
std::vector<std::string> const metadatas = {
"mutate metadata",
"mutate metadata 2",
"mutate metadata 3",
"mutate metadata 3",
"test",
"mutate metadata"};
for (auto const& metadata : metadatas)
{
mptAlice.set({.account = alice, .metadata = metadata});
BEAST_EXPECT(mptAlice.checkMetadata(metadata));
}
// Metadata being empty will remove the field
mptAlice.set({.account = alice, .metadata = ""});
BEAST_EXPECT(!mptAlice.isMetadataPresent());
}
// Mutate transfer fee
{
Env env{*this, features};
MPTTester mptAlice(env, alice);
mptAlice.create(
{.transferFee = 100,
.metadata = "test",
.ownerCount = 1,
.flags = tfMPTCanTransfer,
.mutableFlags = tmfMPTCanMutateTransferFee});
for (std::uint16_t const fee :
std::initializer_list<std::uint16_t>{1, 10, 100, 200, 500, 1000, maxTransferFee})
{
mptAlice.set({.account = alice, .transferFee = fee});
BEAST_EXPECT(mptAlice.checkTransferFee(fee));
}
// Setting TransferFee to zero will remove the field
mptAlice.set({.account = alice, .transferFee = 0});
BEAST_EXPECT(!mptAlice.isTransferFeePresent());
// Set transfer fee again
mptAlice.set({.account = alice, .transferFee = 10});
BEAST_EXPECT(mptAlice.checkTransferFee(10));
}
// Test flag toggling
{
auto testFlagToggle = [&](std::uint32_t createFlags,
std::uint32_t setFlags,
std::uint32_t clearFlags) {
Env env{*this, features};
MPTTester mptAlice(env, alice);
// Create the MPT object with the specified initial flags
mptAlice.create({.metadata = "test", .ownerCount = 1, .mutableFlags = createFlags});
// Set and clear the flag multiple times
mptAlice.set({.account = alice, .mutableFlags = setFlags});
mptAlice.set({.account = alice, .mutableFlags = clearFlags});
mptAlice.set({.account = alice, .mutableFlags = clearFlags});
mptAlice.set({.account = alice, .mutableFlags = setFlags});
mptAlice.set({.account = alice, .mutableFlags = setFlags});
mptAlice.set({.account = alice, .mutableFlags = clearFlags});
mptAlice.set({.account = alice, .mutableFlags = setFlags});
mptAlice.set({.account = alice, .mutableFlags = clearFlags});
};
testFlagToggle(tmfMPTCanMutateCanLock, tfMPTCanLock, tmfMPTClearCanLock);
testFlagToggle(
tmfMPTCanMutateRequireAuth, tmfMPTSetRequireAuth, tmfMPTClearRequireAuth);
testFlagToggle(tmfMPTCanMutateCanEscrow, tmfMPTSetCanEscrow, tmfMPTClearCanEscrow);
testFlagToggle(tmfMPTCanMutateCanTrade, tmfMPTSetCanTrade, tmfMPTClearCanTrade);
testFlagToggle(
tmfMPTCanMutateCanTransfer, tmfMPTSetCanTransfer, tmfMPTClearCanTransfer);
testFlagToggle(
tmfMPTCanMutateCanClawback, tmfMPTSetCanClawback, tmfMPTClearCanClawback);
}
}
void
testMutateCanLock(FeatureBitset features)
{
testcase("Mutate MPTCanLock");
using namespace test::jtx;
Account const alice("alice");
Account const bob("bob");
// Individual lock
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanLock | tfMPTCanTransfer,
.mutableFlags = tmfMPTCanMutateCanLock | tmfMPTCanMutateCanTrade |
tmfMPTCanMutateTransferFee});
mptAlice.authorize({.account = bob, .holderCount = 1});
// Lock bob's mptoken
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
// Can mutate the mutable flags and fields
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanLock});
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanLock});
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanLock});
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanTrade});
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanTrade});
mptAlice.set({.account = alice, .transferFee = 200});
}
// Global lock
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanLock,
.mutableFlags = tmfMPTCanMutateCanLock | tmfMPTCanMutateCanClawback |
tmfMPTCanMutateMetadata});
mptAlice.authorize({.account = bob, .holderCount = 1});
// Lock issuance
mptAlice.set({.account = alice, .flags = tfMPTLock});
// Can mutate the mutable flags and fields
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanLock});
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanLock});
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanLock});
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanClawback});
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanClawback});
mptAlice.set({.account = alice, .metadata = "mutate"});
}
// Test lock and unlock after mutating MPTCanLock
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanLock,
.mutableFlags = tmfMPTCanMutateCanLock | tmfMPTCanMutateCanClawback |
tmfMPTCanMutateMetadata});
mptAlice.authorize({.account = bob, .holderCount = 1});
// Can lock and unlock
mptAlice.set({.account = alice, .flags = tfMPTLock});
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
mptAlice.set({.account = alice, .flags = tfMPTUnlock});
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTUnlock});
// Clear lsfMPTCanLock
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanLock});
// Can not lock or unlock
mptAlice.set({.account = alice, .flags = tfMPTLock, .err = tecNO_PERMISSION});
mptAlice.set({.account = alice, .flags = tfMPTUnlock, .err = tecNO_PERMISSION});
mptAlice.set(
{.account = alice, .holder = bob, .flags = tfMPTLock, .err = tecNO_PERMISSION});
mptAlice.set(
{.account = alice, .holder = bob, .flags = tfMPTUnlock, .err = tecNO_PERMISSION});
// Set MPTCanLock again
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanLock});
// Can lock and unlock again
mptAlice.set({.account = alice, .flags = tfMPTLock});
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
mptAlice.set({.account = alice, .flags = tfMPTUnlock});
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTUnlock});
}
}
void
testMutateRequireAuth(FeatureBitset features)
{
testcase("Mutate MPTRequireAuth");
using namespace test::jtx;
// test mutating RequireAuth flag on the issuance and its effect on payment authorization
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.ownerCount = 1,
.flags = tfMPTRequireAuth,
.mutableFlags = tmfMPTCanMutateRequireAuth});
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = alice, .holder = bob});
// Pay to bob
mptAlice.pay(alice, bob, 1000);
// Unauthorize bob
mptAlice.authorize({.account = alice, .holder = bob, .flags = tfMPTUnauthorize});
// Can not pay to bob
mptAlice.pay(bob, alice, 100, tecNO_AUTH);
// Clear RequireAuth
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearRequireAuth});
// Can pay to bob
mptAlice.pay(alice, bob, 1000);
// Set RequireAuth again
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetRequireAuth});
// Can not pay to bob since he is not authorized
mptAlice.pay(bob, alice, 100, tecNO_AUTH);
// Authorize bob again
mptAlice.authorize({.account = alice, .holder = bob});
// Can pay to bob again
mptAlice.pay(alice, bob, 100);
}
// Cannot clear RequireAuth when a DomainID is set on the issuance
{
Account const alice{"alice"};
Account const bob{"bob"};
Account const credIssuer{"credIssuer"};
pdomain::Credentials const credentials{
{.issuer = credIssuer, .credType = "credential"}};
Env env{*this, features};
env.fund(XRP(1000), credIssuer);
env.close();
env(pdomain::setTx(credIssuer, credentials));
env.close();
auto const domainId = pdomain::getNewDomain(env.meta());
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({
.ownerCount = 1,
.flags = tfMPTRequireAuth,
.mutableFlags = tmfMPTCanMutateRequireAuth,
.domainID = domainId,
});
// Clearing RequireAuth while a DomainID is present must be rejected,
mptAlice.set({
.account = alice,
.mutableFlags = tmfMPTClearRequireAuth,
.err = tecNO_PERMISSION,
});
// Setting RequireAuth (already set) is still allowed, though it has no effect.
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetRequireAuth});
}
}
void
testMutateCanEscrow(FeatureBitset features)
{
testcase("Mutate MPTCanEscrow");
using namespace test::jtx;
using namespace std::literals;
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
MPTTester mptAlice(env, alice, {.holders = {carol, bob}});
mptAlice.create(
{.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanTransfer,
.mutableFlags = tmfMPTCanMutateCanEscrow});
mptAlice.authorize({.account = carol});
mptAlice.authorize({.account = bob});
auto const MPT = mptAlice["MPT"];
env(pay(alice, carol, MPT(10'000)));
env(pay(alice, bob, MPT(10'000)));
env.close();
// MPTCanEscrow is not enabled
env(escrow::create(carol, bob, MPT(3)),
escrow::condition(escrow::cb1),
escrow::finish_time(env.now() + 1s),
fee(baseFee * 150),
ter(tecNO_PERMISSION));
// MPTCanEscrow is enabled now
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanEscrow});
env(escrow::create(carol, bob, MPT(3)),
escrow::condition(escrow::cb1),
escrow::finish_time(env.now() + 1s),
fee(baseFee * 150));
// Clear MPTCanEscrow
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanEscrow});
env(escrow::create(carol, bob, MPT(3)),
escrow::condition(escrow::cb1),
escrow::finish_time(env.now() + 1s),
fee(baseFee * 150),
ter(tecNO_PERMISSION));
}
void
testMutateCanTransfer(FeatureBitset features)
{
testcase("Mutate MPTCanTransfer");
using namespace test::jtx;
Account const alice("alice");
Account const bob("bob");
Account const carol("carol");
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create(
{.ownerCount = 1,
.mutableFlags = tmfMPTCanMutateCanTransfer | tmfMPTCanMutateTransferFee});
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = carol});
// Pay to bob
mptAlice.pay(alice, bob, 1000);
// Bob can not pay carol since MPTCanTransfer is not set
mptAlice.pay(bob, carol, 50, tecNO_AUTH);
// Can not set non-zero transfer fee when MPTCanTransfer is not set
mptAlice.set({.account = alice, .transferFee = 100, .err = tecNO_PERMISSION});
// Can not set non-zero transfer fee even when trying to set
// MPTCanTransfer at the same time
mptAlice.set(
{.account = alice,
.mutableFlags = tmfMPTSetCanTransfer,
.transferFee = 100,
.err = tecNO_PERMISSION});
// Alice sets MPTCanTransfer
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanTransfer});
// Can set transfer fee now
BEAST_EXPECT(!mptAlice.isTransferFeePresent());
mptAlice.set({.account = alice, .transferFee = 100});
BEAST_EXPECT(mptAlice.isTransferFeePresent());
// Bob can pay carol
MPT const MPTC = mptAlice;
if (!features[featureMPTokensV2])
{
mptAlice.pay(bob, carol, 50);
BEAST_EXPECT(env.balance(carol, MPTC) == MPTC(50));
}
else
{
// The difference is due to the rounding in MPT/DEX.
// 1 MPTC is the transfer fee paid by bob to the issuer.
env(pay(bob, carol, mptAlice(50)), txflags(tfPartialPayment));
BEAST_EXPECT(env.balance(carol, MPTC) == MPTC(49));
}
// Alice clears MPTCanTransfer
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanTransfer});
// TransferFee field is removed when MPTCanTransfer is cleared
BEAST_EXPECT(!mptAlice.isTransferFeePresent());
// Bob can not pay
mptAlice.pay(bob, carol, 50, tecNO_AUTH);
}
// Can set transfer fee to zero when MPTCanTransfer is not set, but
// tmfMPTCanMutateTransferFee is set.
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
mptAlice.create(
{.transferFee = 100,
.ownerCount = 1,
.flags = tfMPTCanTransfer,
.mutableFlags = tmfMPTCanMutateTransferFee | tmfMPTCanMutateCanTransfer});
BEAST_EXPECT(mptAlice.checkTransferFee(100));
// Clear MPTCanTransfer and transfer fee is removed
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanTransfer});
BEAST_EXPECT(!mptAlice.isTransferFeePresent());
// Can still set transfer fee to zero, although it is already zero
mptAlice.set({.account = alice, .transferFee = 0});
// TransferFee field is still not present
BEAST_EXPECT(!mptAlice.isTransferFeePresent());
}
}
void
testMutateCanClawback(FeatureBitset features)
{
testcase("Mutate MPTCanClawback");
using namespace test::jtx;
Env env(*this, features);
Account const alice{"alice"};
Account const bob{"bob"};
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.ownerCount = 1, .holderCount = 0, .mutableFlags = tmfMPTCanMutateCanClawback});
// Bob creates an MPToken
mptAlice.authorize({.account = bob});
// Alice pays bob 100 tokens
mptAlice.pay(alice, bob, 100);
// MPTCanClawback is not enabled
mptAlice.claw(alice, bob, 1, tecNO_PERMISSION);
// Enable MPTCanClawback
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanClawback});
// Can clawback now
mptAlice.claw(alice, bob, 1);
// Clear MPTCanClawback
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearCanClawback});
// Can not clawback
mptAlice.claw(alice, bob, 1, tecNO_PERMISSION);
}
void
testMultiSendMaximumAmount(FeatureBitset features)
{
// Verify that directSendNoLimitMultiMPT correctly enforces MaximumAmount
// when the issuer sends to multiple receivers. Pre-fixSecurity3_1_3,
// a stale view.read() snapshot caused per-iteration checks to miss
// aggregate overflows. Post-fix, a running total is used instead.
testcase("Multi-send MaximumAmount enforcement");
using namespace test::jtx;
Account const issuer("issuer");
Account const alice("alice");
Account const bob("bob");
std::uint64_t constexpr maxAmt = 150;
Env env{*this, features};
MPTTester mptt(env, issuer, {.holders = {alice, bob}});
mptt.create({.maxAmt = maxAmt, .ownerCount = 1, .flags = tfMPTCanTransfer});
mptt.authorize({.account = alice});
mptt.authorize({.account = bob});
Asset const asset{MPTIssue{mptt.issuanceID()}};
// Each test case creates a fresh ApplyView and calls
// accountSendMulti from the issuer to the given receivers.
auto const runTest = [&](MultiplePaymentDestinations const& receivers,
TER expectedTer,
std::optional<std::uint64_t> expectedOutstanding,
std::string const& label) {
ApplyViewImpl av(&*env.current(), tapNONE);
auto const ter =
accountSendMulti(av, issuer.id(), asset, receivers, env.app().getJournal("View"));
BEAST_EXPECTS(ter == expectedTer, label);
// Only verify OutstandingAmount on success — on error the
// view may contain partial state and must be discarded.
if (expectedOutstanding)
{
auto const sle = av.peek(keylet::mptIssuance(mptt.issuanceID()));
if (!BEAST_EXPECT(sle))
return;
BEAST_EXPECTS(sle->getFieldU64(sfOutstandingAmount) == *expectedOutstanding, label);
}
};
using R = MultiplePaymentDestinations;
// Post-amendment: aggregate check with running total
runTest(
R{{alice.id(), 100}, {bob.id(), 100}},
tecPATH_DRY,
std::nullopt,
"aggregate exceeds max");
runTest(R{{alice.id(), 75}, {bob.id(), 75}}, tesSUCCESS, maxAmt, "aggregate at boundary");
runTest(R{{alice.id(), 50}, {bob.id(), 50}}, tesSUCCESS, 100, "aggregate within limit");
runTest(
R{{alice.id(), 150}, {bob.id(), 0}},
tesSUCCESS,
maxAmt,
"one receiver at max, other zero");
runTest(
R{{alice.id(), 151}, {bob.id(), 0}},
tecPATH_DRY,
std::nullopt,
"one receiver exceeds max, other zero");
// Issue 50 tokens so outstandingAmount is nonzero, then verify
// the third condition: outstandingAmount > maximumAmount - sendAmount - totalSendAmount
mptt.pay(issuer, alice, 50);
env.close();
// maxAmt=150, outstanding=50, so 100 more available
runTest(
R{{alice.id(), 50}, {bob.id(), 50}},
tesSUCCESS,
maxAmt,
"nonzero outstanding, aggregate at boundary");
runTest(
R{{alice.id(), 50}, {bob.id(), 51}},
tecPATH_DRY,
std::nullopt,
"nonzero outstanding, aggregate exceeds max");
runTest(
R{{alice.id(), 100}, {bob.id(), 0}},
tesSUCCESS,
maxAmt,
"nonzero outstanding, single send at remaining capacity");
runTest(
R{{alice.id(), 101}, {bob.id(), 0}},
tecPATH_DRY,
std::nullopt,
"nonzero outstanding, single send exceeds remaining capacity");
// Pre-amendment: the stale per-iteration check allows each
// individual send (100 <= 150) even though the aggregate (200)
// exceeds MaximumAmount. Preserved for ledger replay.
{
// KNOWN BUG (pre-fixSecurity3_1_3): preserved for ledger replay only
env.disableFeature(fixSecurity3_1_3);
runTest(
R{{alice.id(), 100}, {bob.id(), 100}},
tesSUCCESS,
250,
"pre-amendment allows over-send");
env.enableFeature(fixSecurity3_1_3);
}
}
void
testOfferCrossing(FeatureBitset features)
{
testcase("Offer Crossing");
using namespace test::jtx;
Account const gw = Account("gw");
Account const alice = Account("alice");
Account const carol = Account("carol");
auto const USD = gw["USD"];
// Blocking flags
for (auto flags :
{tfMPTCanTrade | tfMPTCanLock | tfMPTCanClawback, // global lock - holder, issuer fail
tfMPTCanTrade | tfMPTRequireAuth, // not authorized - holder fails
tfMPTCanTrade, // holder, issuer succeed
tfMPTCanTrade | tfMPTCanLock, // local lock - holder fails
tfMPTCanTransfer}) // can't trade - holder, issuer fail
{
Env env{*this, features};
env.fund(XRP(1'000), gw, alice);
env.close();
// Use CanClawback flag to distinguish global from local lock
bool const lockMPToken = (flags & (tfMPTCanLock | tfMPTCanClawback)) == tfMPTCanLock;
bool const lockMPTIssue =
(flags & (tfMPTCanLock | tfMPTCanClawback)) == (tfMPTCanLock | tfMPTCanClawback);
bool const requireAuth = (flags & tfMPTRequireAuth) != 0u;
auto mpt = MPTTester(
{.env = env,
.issuer = gw,
.holders = {alice},
.pay = 1'000,
.flags = flags,
.authHolder = true});
MPT const BTC = mpt;
if (requireAuth)
mpt.authorize({.account = gw, .holder = alice, .flags = tfMPTUnauthorize});
if (lockMPToken)
{
mpt.set({.holder = alice, .flags = tfMPTLock});
}
else if (lockMPTIssue)
{
mpt.set({.flags = tfMPTLock});
}
auto testOffer =
[&](Account const& account, auto const& buy, auto const& sell, bool buyUSD) {
auto error = [&](auto const err) -> TER {
if (account == gw)
return tesSUCCESS;
return err;
};
auto const [errBuy, errSell] = [&]() -> std::pair<TER, TER> {
// Global lock
if (lockMPTIssue)
return std::make_pair(tecFROZEN, tecFROZEN);
// Local lock
if (lockMPToken)
return std::make_pair(tesSUCCESS, error(tecUNFUNDED_OFFER));
// MPToken doesn't exist
if (requireAuth)
return std::make_pair(error(tecNO_AUTH), error(tecUNFUNDED_OFFER));
if (flags & tfMPTCanTransfer)
return std::make_pair(tecNO_PERMISSION, tecNO_PERMISSION);
return std::make_pair(tesSUCCESS, tesSUCCESS);
}();
auto const err = buyUSD ? errBuy : errSell;
auto seq(env.seq(account));
env(offer(account, buy(10), sell(10)), ter(err));
env(offer_cancel(account, seq));
env.close();
};
auto testOffers = [&](Account const& account) {
testOffer(account, XRP, BTC, false);
testOffer(account, BTC, XRP, true);
};
testOffers(alice);
testOffers(gw);
}
// MPTokenV2 is disabled
{
Env env{*this, features - featureMPTokensV2};
MPTTester mpt(env, gw, {.holders = {alice}});
mpt.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});
mpt.authorize({.account = alice});
mpt.pay(gw, alice, 200);
env(offer(alice, XRP(100), mpt.mpt(101)), ter(temDISABLED));
env.close();
}
// MPTokenIssuance object doesn't exist
{
Env env(*this);
env.fund(XRP(1'000), gw, alice);
env.close();
MPT const BTC = MPTTester({.env = env, .issuer = gw, .holders = {alice}, .pay = 100});
MPT const ETH = MPT(gw, 1);
env(offer(alice, ETH(10), BTC(10)), ter(tecOBJECT_NOT_FOUND));
env(offer(alice, BTC(10), ETH(10)), ter(tecUNFUNDED_OFFER));
}
// MPToken object doesn't exist and the account is not the issuer of MPT
{
Env env(*this);
env.fund(XRP(1'000), gw, alice);
MPTTester const BTC({.env = env, .issuer = gw, .holders = {alice}, .pay = 100});
MPTTester const ETH({.env = env, .issuer = gw});
env(offer(alice, ETH(10), BTC(10)));
env(offer(alice, BTC(10), ETH(10)), ter(tecUNFUNDED_OFFER));
}
// MPTLock flag is set and the account is not the issuer of MPT
{
Account const bob = Account("bob");
Account const dan = Account("dan");
Env env(*this);
env.fund(XRP(1'000), gw, alice, carol, bob, dan);
MPTTester BTC(
{.env = env,
.issuer = gw,
.holders = {alice, carol, bob, dan},
.pay = 100,
.flags = tfMPTCanLock | MPTDEXFlags});
MPTTester ETH(
{.env = env,
.issuer = gw,
.holders = {alice, carol, bob, dan},
.pay = 100,
.flags = tfMPTCanLock | MPTDEXFlags});
env(offer(bob, ETH(10), BTC(10)), txflags(tfPassive));
env(offer(dan, BTC(10), ETH(10)), txflags(tfPassive));
auto test = [&](auto const& flag, bool gwOwner = false) {
BTC.set({.holder = carol, .flags = flag});
BTC.set({.holder = alice, .flags = flag});
if (gwOwner)
{
// Succeeds if the account is the issuer
env(offer(gw, ETH(1), BTC(1)));
env(offer(gw, BTC(1), ETH(1)));
}
else
{
auto const err = flag == tfMPTLock ? ter(tecUNFUNDED_OFFER) : ter(tesSUCCESS);
env(offer(alice, ETH(1), BTC(1)), err);
// Offer created by not crossed
env(offer(carol, BTC(1), ETH(1)));
BEAST_EXPECT(expectOffers(env, carol, 1, {{BTC(1), ETH(1)}}));
}
};
test(tfMPTLock);
test(tfMPTLock, true);
test(tfMPTUnlock);
}
// MPTRequireAuth flag is set and the account is not authorized
{
Env env(*this);
env.fund(XRP(1'000), gw, alice);
MPTTester BTC(
{.env = env,
.issuer = gw,
.holders = {alice},
.pay = 100,
.flags = tfMPTRequireAuth | MPTDEXFlags,
.authHolder = true});
MPTTester const ETH(
{.env = env,
.issuer = gw,
.holders = {alice},
.pay = 100,
.flags = tfMPTRequireAuth | MPTDEXFlags,
.authHolder = true});
BTC.authorize({.account = gw, .holder = alice, .flags = tfMPTUnauthorize});
env(offer(alice, ETH(10), BTC(10)), ter(tecUNFUNDED_OFFER));
// issuer can create
env(offer(gw, ETH(10), BTC(10)));
env.close();
}
// MPTCanTransfer is not set and the account is not the issuer of MPT
{
Env env(*this);
env.fund(XRP(1'000), gw, alice, carol);
MPTTester BTC(
{.env = env,
.issuer = gw,
.holders = {alice, carol},
.pay = 100,
.flags = tfMPTCanTrade,
.mutableFlags = tmfMPTCanMutateCanTransfer});
MPTTester ETH(
{.env = env,
.issuer = gw,
.holders = {alice, carol},
.pay = 100,
.flags = tfMPTCanTrade | tfMPTCanTransfer,
.mutableFlags = tmfMPTCanMutateCanTransfer});
// Can create
env(offer(alice, ETH(10), BTC(10)), txflags(tfPassive));
BTC.set({.mutableFlags = tmfMPTSetCanTransfer});
ETH.set({.mutableFlags = tmfMPTClearCanTransfer});
env(offer(alice, ETH(10), BTC(10)), txflags(tfPassive));
BEAST_EXPECT(getAccountOffers(env, alice)[jss::offers].size() == 2);
// issuer can create
env(offer(gw, ETH(10), BTC(10)), txflags(tfPassive));
env.close();
// can cross issuer's offer, other offers are removed
env(offer(carol, BTC(10), ETH(10)));
BEAST_EXPECT(expectOffers(env, alice, 0));
BEAST_EXPECT(expectOffers(env, gw, 0));
BEAST_EXPECT(expectOffers(env, carol, 0));
// can't cross holder's offer, holder's offer is removed
env(offer(alice, ETH(10), BTC(10)), txflags(tfPassive));
env(offer(carol, BTC(10), ETH(10)));
BEAST_EXPECT(expectOffers(env, alice, 0));
BEAST_EXPECT(expectOffers(env, carol, 1));
}
// MPTCanTrade is disabled
{
Env env(*this);
env.fund(XRP(1'000), gw, alice, carol);
MPTTester const BTC(
{.env = env,
.issuer = gw,
.holders = {alice, carol},
.pay = 100,
.flags = tfMPTCanTransfer,
.mutableFlags = tmfMPTCanMutateCanTrade});
MPTTester const ETH(
{.env = env,
.issuer = gw,
.holders = {alice, carol},
.pay = 100,
.flags = tfMPTCanTrade,
.mutableFlags = tmfMPTCanMutateCanTrade});
// Can't create
env(offer(gw, ETH(10), BTC(10)), ter(tecNO_PERMISSION));
env.close();
}
// XRP/MPT
{
Env env{*this, features};
MPTTester mpt(env, gw, {.holders = {alice, carol}});
mpt.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT = mpt["MPT"];
mpt.authorize({.account = alice});
mpt.pay(gw, alice, 200);
mpt.authorize({.account = carol});
mpt.pay(gw, carol, 200);
env(offer(alice, XRP(100), MPT(101)));
env.close();
BEAST_EXPECT(expectOffers(env, alice, 1, {{Amounts{XRP(100), MPT(101)}}}));
env(offer(carol, MPT(101), XRP(100)));
env.close();
BEAST_EXPECT(expectOffers(env, alice, 0));
BEAST_EXPECT(expectOffers(env, carol, 0));
BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(400));
BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 99));
BEAST_EXPECT(mpt.checkMPTokenAmount(carol, 301));
}
// IOU/MPT
{
Env env{*this, features};
MPTTester mpt(env, gw, {.holders = {alice, carol}});
mpt.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT = mpt["MPT"];
env(trust(alice, USD(2'000)));
env(pay(gw, alice, USD(1'000)));
env.close();
env(trust(carol, USD(2'000)));
env(pay(gw, carol, USD(1'000)));
env.close();
mpt.authorize({.account = alice});
mpt.pay(gw, alice, 200);
mpt.authorize({.account = carol});
mpt.pay(gw, carol, 200);
env(offer(alice, USD(100), MPT(101)));
env.close();
BEAST_EXPECT(expectOffers(env, alice, 1, {{Amounts{USD(100), MPT(101)}}}));
env(offer(carol, MPT(101), USD(100)));
env.close();
BEAST_EXPECT(env.balance(alice, USD) == USD(1'100));
BEAST_EXPECT(env.balance(carol, USD) == USD(900));
BEAST_EXPECT(expectOffers(env, alice, 0));
BEAST_EXPECT(expectOffers(env, carol, 0));
BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(400));
BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 99));
BEAST_EXPECT(mpt.checkMPTokenAmount(carol, 301));
}
// MPT/MPT
{
Env env{*this, features};
MPTTester mpt1(env, gw, {.holders = {alice, carol}});
mpt1.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT1 = mpt1["MPT1"];
MPTTester mpt2(env, gw, {.holders = {alice, carol}, .fund = false});
mpt2.create(
{.ownerCount = 2, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT2 = mpt2["MPT2"];
mpt1.authorize({.account = alice});
mpt1.authorize({.account = carol});
mpt1.pay(gw, alice, 200);
mpt1.pay(gw, carol, 200);
mpt2.authorize({.account = alice});
mpt2.authorize({.account = carol});
mpt2.pay(gw, alice, 200);
mpt2.pay(gw, carol, 200);
env(offer(alice, MPT2(100), MPT1(101)));
env.close();
BEAST_EXPECT(expectOffers(env, alice, 1, {{Amounts{MPT2(100), MPT1(101)}}}));
env(offer(carol, MPT1(101), MPT2(100)));
env.close();
BEAST_EXPECT(expectOffers(env, alice, 0));
BEAST_EXPECT(expectOffers(env, carol, 0));
BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(400));
BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 99));
BEAST_EXPECT(mpt1.checkMPTokenAmount(carol, 301));
BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(400));
BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 300));
BEAST_EXPECT(mpt2.checkMPTokenAmount(carol, 100));
}
}
void
testCrossAssetPayment(FeatureBitset features)
{
testcase("Cross Asset Payment");
using namespace test::jtx;
Account const gw = Account("gw");
Account const alice = Account("alice");
Account const carol = Account("carol");
Account const bob = Account("bob");
auto const USD = gw["USD"];
// Loop
{
Env env{*this, features};
MPTTester mpt(env, gw, {.holders = {carol, bob}});
mpt.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT = mpt["MPT"];
mpt.authorize({.account = carol});
mpt.pay(gw, carol, 200);
mpt.authorize({.account = bob});
// holder to holder
env(pay(carol, bob, MPT(1)),
test::jtx::path(~MPT, ~USD, ~MPT),
sendmax(XRP(1)),
txflags(tfPartialPayment),
ter(temBAD_PATH_LOOP));
env.close();
// issuer to holder
env(pay(gw, bob, MPT(1)),
test::jtx::path(~MPT, ~USD, ~MPT),
sendmax(XRP(1)),
txflags(tfPartialPayment),
ter(temBAD_PATH_LOOP));
env.close();
// holder to issuer
env(pay(bob, gw, MPT(1)),
test::jtx::path(~MPT, ~USD, ~MPT),
sendmax(XRP(1)),
txflags(tfPartialPayment),
ter(temBAD_PATH_LOOP));
env.close();
}
// Rippling
{
Env env{*this, features};
MPTTester mpt(env, gw, {.holders = {carol, bob}});
mpt.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT = mpt["MPT"];
mpt.authorize({.account = carol});
mpt.pay(gw, carol, 200);
mpt.authorize({.account = bob});
// holder to holder
env(pay(carol, bob, MPT(1)),
test::jtx::path(~MPT, gw),
sendmax(XRP(1)),
txflags(tfPartialPayment),
ter(temBAD_PATH));
env.close();
// issuer to holder
env(pay(gw, bob, MPT(1)),
test::jtx::path(~MPT, carol),
sendmax(XRP(1)),
txflags(tfPartialPayment),
ter(temBAD_PATH));
env.close();
// holder to issuer
env(pay(bob, gw, MPT(1)),
test::jtx::path(~MPT, carol),
sendmax(XRP(1)),
txflags(tfPartialPayment),
ter(temBAD_PATH));
env.close();
}
// MPTokenV2 is disabled
{
Env env{*this, features - featureMPTokensV2};
MPTTester mpt(env, gw, {.holders = {alice}});
mpt.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT = mpt["MPT"];
mpt.authorize({.account = alice});
env(pay(gw, alice, MPT(101)),
test::jtx::path(~MPT),
sendmax(XRP(100)),
txflags(tfPartialPayment),
ter(temMALFORMED));
}
{
auto const ed = Account{"ed"};
Env env{*this, features};
env.fund(XRP(1'000), gw, alice, carol, bob, ed);
MPTTester BTC(
{.env = env,
.issuer = gw,
.holders = {alice, carol, bob},
.pay = 1'000,
.flags = tfMPTCanLock | MPTDEXFlags,
.mutableFlags = tmfMPTCanMutateRequireAuth | tmfMPTCanMutateCanTrade |
tmfMPTCanMutateCanTransfer});
MPTTester ETH(
{.env = env,
.issuer = gw,
.holders = {alice, carol, bob},
.pay = 1'000,
.flags = tfMPTCanLock | MPTDEXFlags,
.mutableFlags = tmfMPTCanMutateCanTransfer});
MPTTester const USD(
{.env = env,
.issuer = gw,
.holders = {alice, carol, bob},
.pay = 1'000,
.flags = MPTDEXFlags | tfMPTCanLock,
.mutableFlags = tmfMPTCanMutateCanTransfer});
MPTTester const CAD(
{.env = env,
.issuer = gw,
.holders = {alice, carol, bob},
.pay = 1'000,
.flags = MPTDEXFlags | tfMPTCanLock,
.mutableFlags = tmfMPTCanMutateCanTransfer});
env(offer(bob, ETH(1'000), BTC(1'000)), txflags(tfPassive));
env.close();
env(offer(bob, BTC(1'000), ETH(1'000)), txflags(tfPassive));
env.close();
// MPTokenIssuance doesn't exist
env(pay(alice, carol, MPT(gw, 1'000)(10)), sendmax(ETH(10)), ter(tecOBJECT_NOT_FOUND));
env.close();
env(pay(alice, carol, ETH(10)), sendmax(MPT(gw)(10)), ter(tecOBJECT_NOT_FOUND));
env.close();
// MPToken object doesn't exist
// holder and issuer fail
env(pay(ed, carol, BTC(10)), sendmax(ETH(10)), ter(tecNO_AUTH));
env(pay(carol, ed, BTC(10)), sendmax(ETH(10)), ter(tecNO_AUTH));
env(pay(ed, gw, BTC(10)), sendmax(ETH(10)), ter(tecNO_AUTH));
env(pay(gw, ed, BTC(10)), sendmax(ETH(10)), ter(tecNO_AUTH));
env.close();
// MPTRequireAuth is set
BTC.authorize({.account = ed});
ETH.authorize({.account = ed});
env(pay(gw, ed, ETH(100)));
env(pay(gw, ed, BTC(100)));
env.close();
BTC.set({.mutableFlags = tmfMPTSetRequireAuth});
// authorize bob to enable the offers trading
BTC.authorize({.account = gw, .holder = bob});
env.close();
env(pay(ed, carol, BTC(10)), path(~BTC), sendmax(ETH(10)), ter(tecNO_AUTH));
env(pay(carol, ed, BTC(10)), path(~BTC), sendmax(ETH(10)), ter(tecNO_AUTH));
// BTC is transferred from bob to ed, ed is not authorized
env(pay(gw, ed, BTC(10)), path(~BTC), sendmax(ETH(10)), ter(tecNO_AUTH));
// BTC is transferred from bob to issuer
env(pay(ed, gw, BTC(10)), path(~BTC), sendmax(ETH(10)));
// BTC is transferred from issuer to bob
env(pay(gw, ed, ETH(10)), path(~ETH), sendmax(BTC(10)));
// BTC is transferred from ed to bob, ed is not authorized
env(pay(ed, gw, ETH(10)), path(~ETH), sendmax(BTC(10)), ter(tecNO_AUTH));
env.close();
BTC.set({.mutableFlags = tmfMPTClearRequireAuth});
// MPTCanTransfer is not set
// Fail regardless if source/destination is the issuer or
// not since the offer is owned by a holder.
BTC.set({.mutableFlags = tmfMPTClearCanTransfer});
env(pay(ed, carol, BTC(10)), path(~BTC), sendmax(ETH(10)), ter(tecPATH_PARTIAL));
env(pay(carol, ed, BTC(10)), path(~BTC), sendmax(ETH(10)), ter(tecPATH_PARTIAL));
env(pay(ed, carol, ETH(10)), path(~ETH), sendmax(BTC(10)), ter(tecPATH_PARTIAL));
env(pay(carol, ed, ETH(10)), path(~ETH), sendmax(BTC(10)), ter(tecPATH_PARTIAL));
// Fail because BTC, which has CanTransfer disabled, is sent to
// bob
env(pay(ed, gw, ETH(10)), path(~ETH), sendmax(BTC(10)), ter(tecPATH_PARTIAL));
env(pay(ed, gw, BTC(10)), path(~BTC), sendmax(ETH(10)), ter(tesSUCCESS));
env(pay(gw, ed, ETH(10)), path(~ETH), sendmax(BTC(10)), ter(tesSUCCESS));
// Fail because BTC, which has CanTransfer disabled, is sent to
// ed
env(pay(gw, ed, BTC(10)), path(~BTC), sendmax(ETH(10)), ter(tecPATH_PARTIAL));
env.close();
env(offer(gw, ETH(100), BTC(100)), txflags(tfPassive));
env.close();
env(offer(gw, BTC(100), ETH(100)), txflags(tfPassive));
env.close();
BEAST_EXPECT(expectOffers(env, bob, 2));
env(pay(ed, carol, BTC(10)), path(~BTC), sendmax(ETH(10)), ter(tesSUCCESS));
env(pay(ed, carol, ETH(10)), path(~ETH), sendmax(BTC(10)), ter(tesSUCCESS));
env(pay(gw, carol, BTC(10)), path(~BTC), sendmax(ETH(10)), ter(tesSUCCESS));
env.close();
env(pay(ed, gw, BTC(10)), path(~BTC), sendmax(ETH(10)));
env.close();
}
// Multiple steps: CAD/USD, USD/BTC, BTC/ETH
{
auto const ed = Account{"ed"};
Env env{*this, features};
env.fund(XRP(1'000), gw, alice, carol, bob, ed);
env.close();
MPTTester BTC(
{.env = env,
.issuer = gw,
.holders = {alice, carol, bob},
.pay = 1'000,
.flags = tfMPTCanLock | MPTDEXFlags,
.mutableFlags = tmfMPTCanMutateCanTransfer});
MPTTester ETH(
{.env = env,
.issuer = gw,
.holders = {alice, carol, bob},
.pay = 1'000,
.flags = tfMPTCanLock | MPTDEXFlags,
.mutableFlags = tmfMPTCanMutateCanTransfer});
MPTTester USD(
{.env = env,
.issuer = gw,
.holders = {alice, carol, bob},
.pay = 1'000,
.flags = MPTDEXFlags | tfMPTCanLock,
.mutableFlags = tmfMPTCanMutateCanTransfer});
MPTTester CAD(
{.env = env,
.issuer = gw,
.holders = {alice, carol, bob},
.pay = 1'000,
.flags = MPTDEXFlags | tfMPTCanLock,
.mutableFlags = tmfMPTCanMutateCanTransfer});
// takerGets can transfer if:
// - CanTransfer is set
// - The offer's owner is the issuer
// - BookStep is the last step, which means strand's destination is
// the issuer
// takerPays can transfer if
// - BookStep is the first step, which means strand's source is
// the issuer
// - The offer's owner is the issuer
// - Previous step is BookStep, which transfers per above
// - CanTransfer is set
env(offer(bob, CAD(100), USD(100)), txflags(tfPassive));
env(offer(bob, USD(100), BTC(100)), txflags(tfPassive));
env(offer(bob, BTC(100), ETH(100)), txflags(tfPassive));
env.close();
BEAST_EXPECT(expectOffers(env, bob, 3));
BTC.set({.mutableFlags = tmfMPTSetCanTransfer});
USD.set({.mutableFlags = tmfMPTClearCanTransfer});
// TakerGets
// fail - CAD/USD is owned by bob
env(pay(alice, carol, ETH(1)),
path(~USD, ~BTC, ~ETH),
sendmax(CAD(1)),
ter(tecPATH_PARTIAL));
auto seq(env.seq(gw));
env(offer(gw, USD(1), BTC(1)), txflags(tfPassive));
env.close();
// fail - CAD/USD is owned by bob
env(pay(alice, carol, ETH(1)),
path(~USD, ~BTC, ~ETH),
sendmax(CAD(1)),
ter(tecPATH_PARTIAL));
env.close();
env(offer_cancel(gw, seq));
env(offer(gw, CAD(1), USD(1)), txflags(tfPassive));
env.close();
BEAST_EXPECT(expectOffers(env, bob, 3));
// succeed - CAD/USD is owned by issuer
env(pay(alice, carol, ETH(1)), path(~USD, ~BTC, ~ETH), sendmax(CAD(1)));
env.close();
// bob's CAD/USD is deleted
BEAST_EXPECT(expectOffers(env, bob, 2));
env(offer(bob, CAD(100), USD(100)), txflags(tfPassive));
BEAST_EXPECT(expectOffers(env, gw, 0));
USD.set({.mutableFlags = tmfMPTSetCanTransfer});
ETH.set({.mutableFlags = tmfMPTClearCanTransfer});
// fail - BTC/ETH is owned by bob, destination is carol
env(pay(alice, carol, ETH(1)),
path(~USD, ~BTC, ~ETH),
sendmax(CAD(1)),
ter(tecPATH_PARTIAL));
env.close();
BEAST_EXPECT(expectOffers(env, bob, 3));
// succeed - destination is an issuer
env(pay(alice, gw, ETH(1)), path(~USD, ~BTC, ~ETH), sendmax(CAD(1)));
env.close();
BEAST_EXPECT(expectOffers(env, bob, 3));
// TakerPays
ETH.set({.mutableFlags = tmfMPTSetCanTransfer});
CAD.set({.mutableFlags = tmfMPTClearCanTransfer});
// fail - CAD/USD is owned by bob, source is alice
env(pay(alice, carol, ETH(1)),
path(~USD, ~BTC, ~ETH),
sendmax(CAD(1)),
ter(tecPATH_PARTIAL));
// succeed - source is the issuer
env(pay(gw, carol, ETH(1)), path(~USD, ~BTC, ~ETH), sendmax(CAD(1)));
env.close();
env(offer(gw, CAD(1), USD(1)), txflags(tfPassive));
env.close();
// succeed - CAD/USD is owned by issuer
env(pay(alice, carol, ETH(1)), path(~USD, ~BTC, ~ETH), sendmax(CAD(1)));
env.close();
BEAST_EXPECT(expectOffers(env, gw, 0));
BEAST_EXPECT(expectOffers(env, bob, 2));
CAD.set({.mutableFlags = tmfMPTSetCanTransfer});
BTC.set({.mutableFlags = tmfMPTClearCanTransfer});
env(offer(bob, CAD(1), USD(1)), txflags(tfPassive));
env(offer(gw, USD(1), BTC(1)), txflags(tfPassive));
env.close();
// succeed - USD/BTC is owned by issuer
env(pay(alice, carol, ETH(1)), path(~USD, ~BTC, ~ETH), sendmax(CAD(1)));
env.close();
BEAST_EXPECT(expectOffers(env, gw, 0));
}
// MPTCanTrade is not set
{
Env env{*this, features};
env.fund(XRP(1'000), gw, alice, carol, bob);
env.close();
MPTTester BTC(
{.env = env,
.issuer = gw,
.holders = {alice, carol, bob},
.pay = 1'000,
.flags = tfMPTCanTransfer,
.mutableFlags = tmfMPTCanMutateCanTrade});
MPTTester const ETH(
{.env = env,
.issuer = gw,
.holders = {alice, carol, bob},
.pay = 1'000,
.flags = tfMPTCanTransfer | tfMPTCanTrade,
.mutableFlags = tmfMPTCanMutateCanTrade});
MPTTester const USD(
{.env = env,
.issuer = gw,
.holders = {alice, carol, bob},
.pay = 1'000,
.flags = tfMPTCanTransfer | tfMPTCanTrade,
.mutableFlags = tmfMPTCanMutateCanTrade});
env(pay(alice, carol, ETH(1)), path(~ETH), sendmax(BTC(1)), ter(tecNO_PERMISSION));
env(pay(alice, carol, BTC(1)), path(~BTC), sendmax(ETH(1)), ter(tecNO_PERMISSION));
env.close();
BTC.set({.mutableFlags = tmfMPTSetCanTrade});
env(offer(bob, XRP(1), BTC(1)));
env(offer(bob, BTC(1), ETH(1)));
env(offer(bob, ETH(1), USD(1)));
env.close();
BTC.set({.mutableFlags = tmfMPTClearCanTrade});
env(pay(gw, carol, USD(1)),
path(~BTC, ~ETH, ~USD),
sendmax(XRP(1)),
ter(tecPATH_PARTIAL));
env.close();
BEAST_EXPECT(expectOffers(env, bob, 3));
}
// Holders are locked
{
enum LockType { Global, Individual, None };
struct TestArg
{
Account src;
Account dst;
Account offerOwner;
LockType srcFlag = None;
LockType dstFlag = None;
LockType offerFlagBuy = None;
LockType offerFlagSell = None;
LockType globalFlagBuy = None;
LockType globalFlagSell = None;
TER err = tesSUCCESS;
std::optional<TER> errIOU = std::nullopt;
};
auto getErr = [&]<typename Token>(Token const&, TestArg const& arg) {
if constexpr (std::is_same_v<Token, IOU>)
{
return arg.errIOU.value_or(arg.err);
}
else if constexpr (std::is_same_v<Token, MPTTester>)
{
return arg.err;
}
};
auto getMPT = [&](Env& env) {
MPTTester const BTC(
{.env = env,
.issuer = gw,
.holders = {alice, carol, bob},
.pay = 100,
.flags = tfMPTCanLock | MPTDEXFlags});
MPTTester const ETH(
{.env = env,
.issuer = gw,
.holders = {alice, carol, bob},
.pay = 100,
.flags = tfMPTCanLock | MPTDEXFlags});
return std::make_pair(BTC, ETH);
};
auto getIOU = [&](Env& env) {
for (auto const& iou : {gw["BTC"], gw["ETH"]})
{
for (auto const& a : {alice, carol, bob})
{
env(fset(a, asfDefaultRipple));
env.close();
env(trust(a, iou(200)));
env(pay(gw, a, iou(100)));
env.close();
}
}
return std::make_pair(gw["BTC"], gw["ETH"]);
};
auto lock = [&]<typename Token>(
Env& env, Account const& account, Token& token, LockType lock) {
if (lock == None)
return;
if constexpr (std::is_same_v<Token, IOU>)
{
if (lock == Global)
{
env(fset(gw, asfGlobalFreeze));
}
else
{
IOU const iou{account, token.currency};
env(trust(gw, iou(0), tfSetFreeze));
}
}
else if constexpr (std::is_same_v<Token, MPTTester>)
{
if (lock == Global)
{
token.set({.flags = tfMPTLock});
}
else if (token.issuer() != account)
{
token.set({.holder = account, .flags = tfMPTLock});
}
}
};
auto test = [&](auto&& getTokens, TestArg const& arg) {
Env env(*this);
env.fund(XRP(1'000), gw, alice, carol, bob);
auto [BTC, ETH] = getTokens(env);
env(offer(arg.offerOwner, ETH(10), BTC(10)), txflags(tfPassive));
env.close();
if (arg.globalFlagBuy != LockType::None)
{
lock(env, gw, ETH, LockType::Global);
}
else
{
lock(env, arg.offerOwner, ETH, arg.offerFlagBuy);
lock(env, arg.src, ETH, arg.srcFlag);
}
if (arg.globalFlagSell != LockType::None)
{
lock(env, gw, BTC, LockType::Global);
}
else
{
lock(env, arg.offerOwner, BTC, arg.offerFlagSell);
lock(env, arg.dst, BTC, arg.dstFlag);
}
auto const err = getErr(ETH, arg);
env(pay(arg.src, arg.dst, BTC(1)),
path(~BTC),
txflags(tfNoRippleDirect),
sendmax(ETH(1)),
ter(err));
env.close();
};
// clang-format off
std::vector<TestArg> const tests = {
// src, dst, offer's owner are a holder
{.src = alice, .dst = carol, .offerOwner = bob, .srcFlag = Individual, .err = tecPATH_DRY},
// dst can receive IOU even if the account is frozen
{.src = alice, .dst = carol, .offerOwner = bob, .dstFlag = Individual, .err = tecPATH_DRY, .errIOU = tesSUCCESS},
{.src = alice, .dst = carol, .offerOwner = bob, .globalFlagBuy = Global, .err = tecPATH_DRY},
{.src = alice, .dst = carol, .offerOwner = bob, .globalFlagSell = Global, .err = tecPATH_DRY},
// offer's owner can receive IOU even if the account is frozen
{.src = alice, .dst = carol, .offerOwner = bob, .offerFlagBuy = Individual, .err =
tecPATH_PARTIAL, .errIOU = tesSUCCESS},
{.src = alice, .dst = carol, .offerOwner = bob, .offerFlagSell = Individual, .err = tecPATH_PARTIAL},
// src, dst are a holder, offer's owner is an issuer
{.src = alice, .dst = carol, .offerOwner = gw, .srcFlag = Individual, .err = tecPATH_DRY},
// dst can receive IOU even if the account is frozen
{.src = alice, .dst = carol, .offerOwner = gw, .dstFlag = Individual, .err = tecPATH_DRY, .errIOU = tesSUCCESS},
{.src = alice, .dst = carol, .offerOwner = gw, .globalFlagBuy = Global, .err = tecPATH_DRY},
{.src = alice, .dst = carol, .offerOwner = gw, .globalFlagSell = Global, .err = tecPATH_DRY},
// src is issuer, dst and offer's owner are a holder
// dst can receive IOU even if the account is frozen
{.src = gw, .dst = carol, .offerOwner = bob, .dstFlag = Individual, .err = tecPATH_DRY, .errIOU = tesSUCCESS},
// offer's owner can receive IOU from an issuer even if takerBuys is frozen, MPT offer is unfunded in this case
{.src = gw, .dst = carol, .offerOwner = bob, .offerFlagBuy = Individual, .err = tecPATH_PARTIAL, .errIOU = tesSUCCESS},
{.src = gw, .dst = carol, .offerOwner = bob, .offerFlagSell = Individual, .err = tecPATH_PARTIAL},
// dst is issuer, src and offer's owner are a holder
{.src = alice, .dst = gw, .offerOwner = bob, .srcFlag = Individual, .err = tecPATH_DRY},
// offer's owner can receive IOU even if the account is frozen
{.src = alice, .dst = gw, .offerOwner = bob, .offerFlagBuy = Individual, .err = tecPATH_PARTIAL,
.errIOU = tesSUCCESS},
{.src = alice, .dst = gw, .offerOwner = bob, .offerFlagSell = Individual, .err = tecPATH_PARTIAL},
};
// clang-format on
for (auto const& t : tests)
{
test(getMPT, t);
test(getIOU, t);
}
}
{
Env env(*this);
auto const USD = gw["USD"];
env.fund(XRP(1'000), gw, alice, carol, bob);
MPTTester BTC(
{.env = env,
.issuer = gw,
.holders = {alice, carol, bob},
.pay = 100,
.flags = tfMPTCanLock | MPTDEXFlags});
MPTTester ETH(
{.env = env,
.issuer = gw,
.holders = {alice, carol, bob},
.pay = 100,
.flags = tfMPTCanLock | MPTDEXFlags});
env(trust(alice, USD(100)));
env(pay(gw, alice, USD(100)));
env(trust(carol, USD(100)));
env(offer(alice, XRP(10), ETH(10)));
env(offer(bob, ETH(10), BTC(10)));
env(offer(alice, BTC(10), USD(10)));
env.close();
BTC.set({.holder = bob, .flags = tfMPTLock});
// Bob's offer is unfunded
env(pay(alice, carol, USD(1)),
path(~(MPT)ETH, ~(MPT)BTC, ~USD),
txflags(tfNoRippleDirect | tfPartialPayment),
sendmax(XRP(1)),
ter(tecPATH_DRY));
env.close();
BTC.set({.holder = bob, .flags = tfMPTUnlock});
ETH.set({.holder = bob, .flags = tfMPTLock});
env(pay(alice, carol, USD(1)),
path(~(MPT)ETH, ~(MPT)BTC, ~USD),
txflags(tfNoRippleDirect | tfPartialPayment),
sendmax(XRP(1)),
ter(tecPATH_DRY));
}
// A domain payment should only consume a USD/MPT offer with a domain.
// It must not consume a regular USD/MPT offer.
{
Env env(*this, features);
Account const domainOwner("DomainOwner");
env.fund(XRP(1'000), gw, alice, carol, bob);
auto const domainID =
setupDomain(env, {alice, bob, carol, gw}, domainOwner, "permdex-cred");
MPTTester BTC({.env = env, .issuer = gw, .holders = {alice, carol, bob}, .pay = 100});
MPTTester ETH({.env = env, .issuer = gw, .holders = {alice, carol, bob}, .pay = 100});
auto test = [&](bool withDomain) {
if (withDomain)
{
env(offer(bob, ETH(1), BTC(1)), domain(domainID));
}
else
{
env(offer(bob, ETH(1), BTC(1)));
}
auto const err = withDomain ? ter(tesSUCCESS) : ter(tecPATH_DRY);
env(pay(alice, carol, BTC(1)),
path(~(MPT)BTC),
txflags(tfPartialPayment),
sendmax(ETH(1)),
domain(domainID),
err);
};
test(true);
test(false);
}
// A hybrid USD/MPT domain offer should still be consumable by
// a regular payment.
{
Env env(*this, features);
Account const domainOwner("DomainOwner");
env.fund(XRP(1'000), gw, alice, carol, bob);
auto const domainID =
setupDomain(env, {alice, bob, carol, gw}, domainOwner, "permdex-cred");
MPTTester BTC({.env = env, .issuer = gw, .holders = {alice, carol, bob}, .pay = 100});
MPTTester ETH({.env = env, .issuer = gw, .holders = {alice, carol, bob}, .pay = 100});
auto test = [&](bool isHybrid) {
auto const flags = isHybrid ? tfHybrid : 0;
env(offer(bob, ETH(1), BTC(1)), txflags(flags), domain(domainID));
auto const err = isHybrid ? ter(tesSUCCESS) : ter(tecPATH_DRY);
env(pay(alice, carol, BTC(1)),
path(~(MPT)BTC),
txflags(tfPartialPayment),
sendmax(ETH(1)),
err);
};
test(true);
test(false);
}
// MPT/XRP
{
Env env{*this, features};
MPTTester mpt(env, gw, {.holders = {alice, carol, bob}});
mpt.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT = mpt["MPT"];
mpt.authorize({.account = alice});
mpt.pay(gw, alice, 200);
mpt.authorize({.account = carol});
mpt.pay(gw, carol, 200);
mpt.authorize({.account = bob});
env(offer(alice, XRP(100), MPT(101)));
env.close();
BEAST_EXPECT(expectOffers(env, alice, 1, {{Amounts{XRP(100), MPT(101)}}}));
env(pay(carol, bob, MPT(101)),
test::jtx::path(~MPT),
sendmax(XRP(100)),
txflags(tfPartialPayment));
env.close();
BEAST_EXPECT(expectOffers(env, alice, 0));
BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(400));
BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 99));
BEAST_EXPECT(mpt.checkMPTokenAmount(bob, 101));
}
// MPT/IOU
{
Env env{*this, features};
MPTTester mpt(env, gw, {.holders = {alice, carol, bob}});
mpt.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT = mpt["MPT"];
env(trust(alice, USD(2'000)));
env(pay(gw, alice, USD(1'000)));
env(trust(bob, USD(2'000)));
env(pay(gw, bob, USD(1'000)));
env(trust(carol, USD(2'000)));
env(pay(gw, carol, USD(1'000)));
env.close();
mpt.authorize({.account = alice});
mpt.pay(gw, alice, 200);
mpt.authorize({.account = carol});
mpt.pay(gw, carol, 200);
mpt.authorize({.account = bob});
env(offer(alice, USD(100), MPT(101)));
env.close();
BEAST_EXPECT(expectOffers(env, alice, 1, {{Amounts{USD(100), MPT(101)}}}));
env(pay(carol, bob, MPT(101)),
test::jtx::path(~MPT),
sendmax(USD(100)),
txflags(tfPartialPayment));
env.close();
BEAST_EXPECT(expectOffers(env, alice, 0));
BEAST_EXPECT(env.balance(carol, USD) == USD(900));
BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(400));
BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 99));
BEAST_EXPECT(mpt.checkMPTokenAmount(bob, 101));
}
// IOU/MPT
{
Env env{*this, features};
MPTTester mpt(env, gw, {.holders = {alice, carol, bob}});
mpt.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT = mpt["MPT"];
env(trust(alice, USD(2'000)), txflags(tfClearNoRipple));
env(pay(gw, alice, USD(1'000)));
env(trust(bob, USD(2'000)), txflags(tfClearNoRipple));
env.close();
mpt.authorize({.account = alice});
env(pay(gw, alice, MPT(200)));
mpt.authorize({.account = carol});
env(pay(gw, carol, MPT(200)));
env(offer(alice, MPT(101), USD(100)));
env.close();
BEAST_EXPECT(expectOffers(env, alice, 1, {{Amounts{MPT(101), USD(100)}}}));
env(pay(carol, bob, USD(100)),
test::jtx::path(~USD),
sendmax(MPT(101)),
txflags(tfPartialPayment | tfNoRippleDirect));
env.close();
BEAST_EXPECT(expectOffers(env, alice, 0));
BEAST_EXPECT(env.balance(alice, USD) == USD(900));
BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 301));
BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(400));
BEAST_EXPECT(mpt.checkMPTokenAmount(carol, 99));
BEAST_EXPECT(env.balance(bob, USD) == USD(100));
}
// MPT/MPT
{
Env env{*this, features};
MPTTester mpt1(env, gw, {.holders = {alice, carol, bob}});
mpt1.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT1 = mpt1["MPT1"];
MPTTester mpt2(env, gw, {.holders = {alice, carol, bob}, .fund = false});
mpt2.create(
{.ownerCount = 2, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT2 = mpt2["MPT2"];
mpt1.authorize({.account = alice});
mpt1.pay(gw, alice, 200);
mpt2.authorize({.account = alice});
mpt2.authorize({.account = carol});
mpt2.pay(gw, carol, 200);
mpt1.authorize({.account = bob});
mpt2.authorize({.account = bob});
mpt2.pay(gw, bob, 200);
env(offer(alice, MPT2(100), MPT1(100)));
env.close();
BEAST_EXPECT(expectOffers(env, alice, 1, {{Amounts{MPT2(100), MPT1(100)}}}));
// holder to holder
env(pay(carol, bob, MPT1(10)),
test::jtx::path(~MPT1),
sendmax(MPT2(10)),
txflags(tfPartialPayment));
env.close();
BEAST_EXPECT(expectOffers(env, alice, 1));
BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 190));
BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 10));
BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(200));
BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(400));
BEAST_EXPECT(mpt2.checkMPTokenAmount(carol, 190));
BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 10));
// issuer to holder
env(pay(gw, bob, MPT1(20)),
test::jtx::path(~MPT1),
sendmax(MPT2(20)),
txflags(tfPartialPayment));
env.close();
BEAST_EXPECT(expectOffers(env, alice, 1));
BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 170));
BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 30));
BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(200));
BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(420));
BEAST_EXPECT(mpt2.checkMPTokenAmount(carol, 190));
BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 30));
// holder to issuer
env(pay(bob, gw, MPT1(70)),
test::jtx::path(~MPT1),
sendmax(MPT2(70)),
txflags(tfPartialPayment));
env.close();
BEAST_EXPECT(expectOffers(env, alice, 0));
BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 100));
BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 100));
BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(130));
BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(420));
BEAST_EXPECT(mpt2.checkMPTokenAmount(carol, 190));
BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 30));
BEAST_EXPECT(mpt2.checkMPTokenAmount(bob, 130));
}
// MPT/MPT, issuer owns the offer
{
Env env{*this, features};
MPTTester mpt1(env, gw, {.holders = {carol, bob}});
mpt1.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT1 = mpt1["MPT1"];
MPTTester mpt2(env, gw, {.holders = {carol, bob}, .fund = false});
mpt2.create(
{.ownerCount = 2, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT2 = mpt2["MPT2"];
mpt2.authorize({.account = carol});
mpt2.pay(gw, carol, 200);
mpt1.authorize({.account = bob});
mpt2.authorize({.account = bob});
mpt2.pay(gw, bob, 200);
env(offer(gw, MPT2(100), MPT1(100)));
env.close();
BEAST_EXPECT(expectOffers(env, gw, 1, {{Amounts{MPT2(100), MPT1(100)}}}));
// holder to holder
env(pay(carol, bob, MPT1(10)),
test::jtx::path(~MPT1),
sendmax(MPT2(10)),
txflags(tfPartialPayment));
env.close();
BEAST_EXPECT(expectOffers(env, gw, 1));
BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(10));
BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(390));
BEAST_EXPECT(mpt2.checkMPTokenAmount(carol, 190));
BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 10));
// issuer to holder
env(pay(gw, bob, MPT1(20)),
test::jtx::path(~MPT1),
sendmax(MPT2(20)),
txflags(tfPartialPayment));
env.close();
BEAST_EXPECT(expectOffers(env, gw, 1));
BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(30));
BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(390));
BEAST_EXPECT(mpt2.checkMPTokenAmount(carol, 190));
BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 30));
// holder to issuer
env(pay(bob, gw, MPT1(70)),
test::jtx::path(~MPT1),
sendmax(MPT2(70)),
txflags(tfPartialPayment));
env.close();
BEAST_EXPECT(expectOffers(env, gw, 0));
BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(30));
BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(320));
BEAST_EXPECT(mpt2.checkMPTokenAmount(carol, 190));
BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 30));
BEAST_EXPECT(mpt2.checkMPTokenAmount(bob, 130));
}
// MPT/MPT, different issuer
{
Env env{*this, features};
Account const gw1{"gw1"};
MPTTester mpt1(env, gw, {.holders = {alice, carol, bob}});
mpt1.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT1 = mpt1["MPT1"];
env.fund(XRP(1'000), gw1);
MPTTester mpt2(env, gw1, {.holders = {alice, carol, bob}, .fund = false});
mpt2.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT2 = mpt2["MPT2"];
mpt1.authorize({.account = alice});
mpt1.pay(gw, alice, 200);
mpt2.authorize({.account = alice});
mpt2.authorize({.account = carol});
mpt2.pay(gw1, carol, 200);
mpt1.authorize({.account = bob});
mpt1.pay(gw, bob, 200);
mpt2.authorize({.account = bob});
mpt2.pay(gw1, bob, 200);
mpt1.authorize({.account = gw1});
mpt1.pay(gw, gw1, 200);
mpt2.authorize({.account = gw});
mpt2.pay(gw1, gw, 200);
env(offer(alice, MPT2(100), MPT1(100)));
env.close();
BEAST_EXPECT(expectOffers(env, alice, 1, {{Amounts{MPT2(100), MPT1(100)}}}));
env(pay(carol, bob, MPT1(10)),
test::jtx::path(~MPT1),
sendmax(MPT2(10)),
txflags(tfPartialPayment));
env.close();
BEAST_EXPECT(expectOffers(env, alice, 1));
BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(600));
BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(600));
BEAST_EXPECT(mpt1.checkMPTokenAmount(gw1, 200));
BEAST_EXPECT(mpt2.checkMPTokenAmount(gw, 200));
BEAST_EXPECT(mpt2.checkMPTokenAmount(carol, 190));
BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 210));
BEAST_EXPECT(mpt2.checkMPTokenAmount(bob, 200));
BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 190));
BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 10));
env(pay(bob, gw, MPT1(10)),
test::jtx::path(~MPT1),
sendmax(MPT2(10)),
txflags(tfPartialPayment));
env.close();
BEAST_EXPECT(expectOffers(env, alice, 1));
BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(590));
BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(600));
BEAST_EXPECT(mpt1.checkMPTokenAmount(gw1, 200));
BEAST_EXPECT(mpt2.checkMPTokenAmount(gw, 200));
BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 210));
BEAST_EXPECT(mpt2.checkMPTokenAmount(bob, 190));
BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 180));
BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 20));
env(pay(gw, bob, MPT1(10)),
test::jtx::path(~MPT1),
sendmax(MPT2(10)),
txflags(tfPartialPayment));
env.close();
BEAST_EXPECT(expectOffers(env, alice, 1));
BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(590));
BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(600));
BEAST_EXPECT(mpt1.checkMPTokenAmount(gw1, 200));
BEAST_EXPECT(mpt2.checkMPTokenAmount(gw, 190));
BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 220));
BEAST_EXPECT(mpt2.checkMPTokenAmount(bob, 190));
BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 170));
BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 30));
env(pay(bob, gw1, MPT1(10)),
test::jtx::path(~MPT1),
sendmax(MPT2(10)),
txflags(tfPartialPayment));
env.close();
BEAST_EXPECT(expectOffers(env, alice, 1));
BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(590));
BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(600));
BEAST_EXPECT(mpt1.checkMPTokenAmount(gw1, 210));
BEAST_EXPECT(mpt2.checkMPTokenAmount(gw, 190));
BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 220));
BEAST_EXPECT(mpt2.checkMPTokenAmount(bob, 180));
BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 160));
BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 40));
env(pay(gw1, bob, MPT1(10)),
test::jtx::path(~MPT1),
sendmax(MPT2(10)),
txflags(tfPartialPayment));
env.close();
BEAST_EXPECT(expectOffers(env, alice, 1));
BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(590));
BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(610));
BEAST_EXPECT(mpt1.checkMPTokenAmount(gw1, 210));
BEAST_EXPECT(mpt2.checkMPTokenAmount(gw, 190));
BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 230));
BEAST_EXPECT(mpt2.checkMPTokenAmount(bob, 180));
BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 150));
BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 50));
env(pay(gw, gw1, MPT1(10)),
test::jtx::path(~MPT1),
sendmax(MPT2(10)),
txflags(tfPartialPayment));
env.close();
BEAST_EXPECT(expectOffers(env, alice, 1));
BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(590));
BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(610));
BEAST_EXPECT(mpt1.checkMPTokenAmount(gw1, 220));
BEAST_EXPECT(mpt2.checkMPTokenAmount(gw, 180));
BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 140));
BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 60));
env(pay(gw1, gw, MPT1(40)),
test::jtx::path(~MPT1),
sendmax(MPT2(40)),
txflags(tfPartialPayment));
env.close();
BEAST_EXPECT(expectOffers(env, alice, 0));
BEAST_EXPECT(mpt1.checkMPTokenOutstandingAmount(550));
BEAST_EXPECT(mpt2.checkMPTokenOutstandingAmount(650));
BEAST_EXPECT(mpt1.checkMPTokenAmount(gw1, 220));
BEAST_EXPECT(mpt2.checkMPTokenAmount(gw, 180));
BEAST_EXPECT(mpt1.checkMPTokenAmount(alice, 100));
BEAST_EXPECT(mpt2.checkMPTokenAmount(alice, 100));
}
// MPT/IOU IOU/MPT1
{
Env env = pathTestEnv(*this);
Account const gw1{"gw1"};
Account const gw2{"gw2"};
Account const dan{"dan"};
env.fund(XRP(1'000), gw2);
auto const USD = gw2["USD"];
MPTTester mpt(env, gw, {.holders = {alice, carol}});
mpt.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT = mpt["MPT"];
mpt.authorize({.account = alice});
mpt.authorize({.account = carol});
mpt.pay(gw, carol, 200);
MPTTester mpt1(env, gw1, {.holders = {bob, dan}});
mpt1.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT1 = mpt1["MPT1"];
mpt1.authorize({.account = bob});
mpt1.pay(gw1, bob, 200);
mpt1.authorize({.account = dan});
env(trust(alice, USD(400)));
env(pay(gw2, alice, USD(200)));
env(trust(bob, USD(400)));
env(offer(alice, MPT(100), USD(100)));
env(offer(bob, USD(100), MPT1(100)));
env.close();
env(pay(carol, dan, MPT1(100)),
sendmax(MPT(100)),
path(~USD, ~MPT1),
txflags(tfPartialPayment | tfNoRippleDirect));
env.close();
BEAST_EXPECT(expectOffers(env, alice, 0));
BEAST_EXPECT(expectOffers(env, bob, 0));
BEAST_EXPECT(mpt.checkMPTokenAmount(carol, 100));
BEAST_EXPECT(mpt1.checkMPTokenAmount(dan, 100));
}
// XRP/MPT AMM
{
Env env{*this, features};
fund(env, gw, {alice, carol, bob}, XRP(11'000), {USD(20'000)});
MPTTester mpt(env, gw, {.fund = false});
mpt.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT = mpt["MPT"];
mpt.authorize({.account = alice});
mpt.authorize({.account = bob});
mpt.pay(gw, alice, 10'100);
AMM const amm(env, alice, XRP(10'000), MPT(10'100));
env(pay(carol, bob, MPT(100)),
test::jtx::path(~MPT),
sendmax(XRP(100)),
txflags(tfPartialPayment | tfNoRippleDirect));
env.close();
BEAST_EXPECT(amm.expectBalances(XRP(10'100), MPT(10'000), amm.tokens()));
BEAST_EXPECT(mpt.checkMPTokenAmount(bob, 100));
}
// IOU/MPT AMM
{
Env env{*this, features};
fund(env, gw, {alice, carol, bob}, XRP(11'000), {USD(20'000)});
MPTTester mpt(env, gw, {.fund = false});
mpt.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT = mpt["MPT"];
mpt.authorize({.account = alice});
mpt.authorize({.account = bob});
mpt.pay(gw, alice, 10'100);
AMM const amm(env, alice, USD(10'000), MPT(10'100));
env(pay(carol, bob, MPT(100)),
test::jtx::path(~MPT),
sendmax(USD(100)),
txflags(tfPartialPayment | tfNoRippleDirect));
env.close();
BEAST_EXPECT(amm.expectBalances(USD(10'100), MPT(10'000), amm.tokens()));
BEAST_EXPECT(mpt.checkMPTokenAmount(bob, 100));
}
// MPT/MPT AMM cross-asset payment
{
Env env{*this, features};
env.fund(XRP(20'000), gw, alice, carol, bob);
env.close();
MPTTester mpt1(env, gw, {.fund = false});
mpt1.create({.flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT1 = mpt1["MPT1"];
mpt1.authorize({.account = alice});
mpt1.authorize({.account = bob});
mpt1.pay(gw, alice, 10'100);
MPTTester mpt2(env, gw, {.fund = false});
mpt2.create({.flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT2 = mpt2["MPT1"];
mpt2.authorize({.account = alice});
mpt2.authorize({.account = bob});
mpt2.authorize({.account = carol});
mpt2.pay(gw, alice, 10'100);
mpt2.pay(gw, carol, 100);
AMM const amm(env, alice, MPT2(10'000), MPT1(10'100));
env(pay(carol, bob, MPT1(100)),
test::jtx::path(~MPT1),
sendmax(MPT2(100)),
txflags(tfPartialPayment | tfNoRippleDirect));
env.close();
BEAST_EXPECT(amm.expectBalances(MPT2(10'100), MPT1(10'000), amm.tokens()));
BEAST_EXPECT(mpt1.checkMPTokenAmount(bob, 100));
}
// Multi-steps with AMM
// EUR/MPT1 MPT1/MPT2 MPT2/USD USD/CRN AMM:CRN/MPT MPT/YAN
{
Env env{*this, features};
auto const USD = gw["USD"];
auto const EUR = gw["EUR"];
auto const CRN = gw["CRN"];
auto const YAN = gw["YAN"];
fund(
env,
gw,
{alice, carol, bob},
XRP(1'000),
{USD(1'000), EUR(1'000), CRN(2'000), YAN(1'000)});
auto createMPT = [&]() -> std::pair<MPTTester, MPT> {
MPTTester mpt(env, gw, {.fund = false});
mpt.create({.flags = tfMPTCanTransfer | tfMPTCanTrade});
mpt.authorize({.account = alice});
mpt.pay(gw, alice, 2'000);
return {mpt, mpt["MPT"]};
};
auto const [mpt1, MPT1] = createMPT();
auto const [mpt2, MPT2] = createMPT();
auto const [mpt3, MPT3] = createMPT();
env(offer(alice, EUR(100), MPT1(101)));
env(offer(alice, MPT1(101), MPT2(102)));
env(offer(alice, MPT2(102), USD(103)));
env(offer(alice, USD(103), CRN(104)));
env.close();
AMM const amm(env, alice, CRN(1'000), MPT3(1'104));
env(offer(alice, MPT3(104), YAN(100)));
env(pay(carol, bob, YAN(100)),
test::jtx::path(~MPT1, ~MPT2, ~USD, ~CRN, ~MPT3, ~YAN),
sendmax(EUR(100)),
txflags(tfPartialPayment | tfNoRippleDirect));
env.close();
BEAST_EXPECT(env.balance(carol, EUR) == EUR(900));
BEAST_EXPECT(env.balance(bob, YAN) == YAN(1'100));
BEAST_EXPECT(amm.expectBalances(CRN(1'104), MPT3(1'000), amm.tokens()));
BEAST_EXPECT(expectOffers(env, alice, 0));
}
// Multi-steps with AMM and MPT endpoints
// MPT1/EUR EUR/MPT2 MPT2/USD USD/CRN AMM:CRN/MPT3 MPT3/MPT4
{
Env env{*this, features};
auto const USD = gw["USD"];
auto const EUR = gw["EUR"];
auto const CRN = gw["CRN"];
fund(env, gw, {alice, carol, bob}, XRP(1'000), {USD(1'000), EUR(1'000), CRN(2'000)});
auto createMPT = [&]() -> std::pair<MPTTester, MPT> {
MPTTester mpt(env, gw, {.fund = false});
mpt.create({.flags = tfMPTCanTransfer | tfMPTCanTrade});
mpt.authorize({.account = alice});
mpt.pay(gw, alice, 2'000);
return {mpt, mpt["MPT"]};
};
auto const [mpt1, MPT1] = createMPT();
auto const [mpt2, MPT2] = createMPT();
auto const [mpt3, MPT3] = createMPT();
auto [mpt4, MPT4] = createMPT();
mpt4.authorize({.account = bob});
env(offer(alice, EUR(100), MPT1(101)));
env(offer(alice, MPT1(101), MPT2(102)));
env(offer(alice, MPT2(102), USD(103)));
env(offer(alice, USD(103), CRN(104)));
env.close();
AMM const amm(env, alice, CRN(1'000), MPT3(1'104));
env(offer(alice, MPT3(104), MPT4(100)));
env(pay(carol, bob, MPT4(100)),
test::jtx::path(~MPT1, ~MPT2, ~USD, ~CRN, ~MPT3, ~MPT4),
sendmax(EUR(100)),
txflags(tfPartialPayment | tfNoRippleDirect));
env.close();
BEAST_EXPECT(env.balance(carol, EUR) == EUR(900));
BEAST_EXPECT(mpt4.checkMPTokenAmount(bob, 100));
BEAST_EXPECT(amm.expectBalances(CRN(1'104), MPT3(1'000), amm.tokens()));
BEAST_EXPECT(expectOffers(env, alice, 0));
}
// Check that limiting step reduces maximumAmount returned by
// MPTEndpointStep::maxPaymentFlow()
{
Env env(*this, features);
env.fund(XRP(1'000), gw, alice, carol, bob);
MPTTester usd(env, gw, {.holders = {alice, carol, bob}, .fund = false});
usd.create(
{.maxAmt = 1'000,
.authorize = MPTCreate::AllHolders,
.pay = {{{alice}, 1'000}},
.flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const USD = usd["USD"];
MPTTester eur(env, gw, {.holders = {alice, carol, bob}, .fund = false});
eur.create(
{.maxAmt = 1'000,
.authorize = {{alice, carol}},
.pay = {{{carol}, 100}},
.flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const EUR = eur["EUR"];
env(offer(alice, EUR(10), USD(10)));
env(pay(carol, bob, USD(10)),
sendmax(EUR(10)),
path(~USD),
txflags(tfNoRippleDirect | tfPartialPayment));
}
{
Env env(*this, features);
env.fund(XRP(1'000), gw, alice, carol, bob);
auto MUSD = MPTTester(
{.env = env, .issuer = gw, .holders = {alice, carol, bob}, .maxAmt = 1'000});
MPT const USD = MUSD;
env(pay(gw, alice, USD(800)));
env(offer(gw, XRP(300), USD(300)));
env(pay(carol, bob, USD(300)),
sendmax(XRP(300)),
path(~USD),
txflags(tfPartialPayment));
BEAST_EXPECT(MUSD.checkMPTokenAmount(bob, 200));
BEAST_EXPECT(MUSD.checkMPTokenOutstandingAmount(1'000));
// initial + offer - fees
BEAST_EXPECT(env.balance(gw) == (XRP(1'000) + XRP(200) - txfee(env, 3)));
}
{
Env env(*this, features);
auto const EUR = gw["EUR"];
env.fund(XRP(1'000), gw, alice, carol, bob);
env.close();
env(trust(alice, EUR(1'000)));
env(pay(gw, alice, EUR(300)));
env(trust(bob, EUR(1'000)));
auto MUSD = MPTTester(
{.env = env, .issuer = gw, .holders = {alice, carol, bob}, .maxAmt = 1'000});
MPT const USD = MUSD;
env(pay(gw, alice, USD(800)));
env(offer(gw, XRP(300), USD(300)));
env(offer(alice, USD(300), EUR(300)));
env(pay(carol, bob, EUR(300)),
sendmax(XRP(300)),
path(~USD, ~EUR),
txflags(tfPartialPayment));
BEAST_EXPECT(MUSD.checkMPTokenAmount(alice, 1'000));
BEAST_EXPECT(MUSD.checkMPTokenOutstandingAmount(1'000));
// initial + offer - fees
BEAST_EXPECT(env.balance(gw) == (XRP(1'000) + XRP(200) - txfee(env, 4)));
BEAST_EXPECT(env.balance(bob, EUR) == EUR(200));
}
}
void
testPath(FeatureBitset features)
{
testcase("Path");
using namespace test::jtx;
Account const gw{"gw"};
Account const gw1{"gw1"};
Account const alice{"alice"};
Account const carol{"carol"};
Account const bob{"bob"};
Account const dan{"dan"};
auto const USD = gw["USD"];
auto const EUR = gw1["EUR"];
// MPT can be a mpt end point step or a book-step
// Direct MPT payment
{
Env env = pathTestEnv(*this);
MPTTester mpt(env, gw, {.holders = {dan, carol}});
mpt.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT = mpt["MPT"];
mpt.authorize({.account = dan});
mpt.authorize({.account = carol});
mpt.pay(gw, carol, 200);
auto const [pathSet, srcAmt, dstAmt] = find_paths(env, carol, dan, MPT(-1));
BEAST_EXPECT(srcAmt == MPT(200));
BEAST_EXPECT(dstAmt == MPT(200));
// Direct payment, no path
BEAST_EXPECT(pathSet.empty());
}
// Cross-asset payment via XRP/MPT offer (one step)
{
Env env = pathTestEnv(*this);
env.fund(XRP(1'000), carol);
MPTTester mpt(env, gw, {.holders = {alice, dan}});
mpt.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT = mpt["MPT"];
mpt.authorize({.account = alice});
mpt.authorize({.account = dan});
mpt.pay(gw, alice, 200);
env(offer(alice, XRP(100), MPT(100)));
env.close();
auto const [pathSet, srcAmt, dstAmt] = find_paths(env, carol, dan, MPT(-1));
BEAST_EXPECT(srcAmt == XRP(100));
BEAST_EXPECT(dstAmt == MPT(100));
if (BEAST_EXPECT(same(pathSet, stpath(IPE(mpt.issuanceID())))))
{
// validate a payment works with the path
env(pay(carol, dan, MPT(10)),
path(~MPT),
sendmax(XRP(10)),
txflags(tfNoRippleDirect | tfPartialPayment));
}
}
// Cross-asset payment via IOU/MPT offer (one step)
{
Env env = pathTestEnv(*this);
env.fund(XRP(1'000), carol);
env.fund(XRP(1'000), gw);
MPTTester mpt(env, gw1, {.holders = {alice, dan}});
mpt.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT = mpt["MPT"];
mpt.authorize({.account = alice});
mpt.authorize({.account = dan});
mpt.pay(gw1, alice, 200);
env(trust(alice, USD(400)));
env(trust(carol, USD(400)));
env(pay(gw, carol, USD(200)));
env(offer(alice, USD(100), MPT(100)));
env.close();
// No sendMax
STPathSet pathSet;
STAmount srcAmt;
STAmount dstAmt;
std::tie(pathSet, srcAmt, dstAmt) = find_paths(env, carol, dan, MPT(-1));
BEAST_EXPECT(srcAmt == USD(100));
BEAST_EXPECT(dstAmt == MPT(100));
if (BEAST_EXPECT(
pathSet.size() == 1 && same(pathSet, stpath(gw, IPE(mpt.issuanceID())))))
{
// Validate the payment works with the path
env(pay(carol, dan, MPT(10)),
path(pathSet[0]),
sendmax(USD(10)),
txflags(tfNoRippleDirect | tfPartialPayment));
}
// Include sendMax
std::tie(pathSet, srcAmt, dstAmt) = find_paths(env, carol, dan, MPT(-1), USD(-1));
BEAST_EXPECT(srcAmt == USD(90));
BEAST_EXPECT(dstAmt == MPT(90));
if (BEAST_EXPECT(pathSet.size() == 1 && same(pathSet, stpath(IPE(mpt.issuanceID())))))
{
// validate a payment works with the path
env(pay(carol, dan, MPT(10)),
path(pathSet[0]),
sendmax(USD(10)),
txflags(tfNoRippleDirect | tfPartialPayment));
}
// Include source token
std::tie(pathSet, srcAmt, dstAmt) =
find_paths(env, carol, dan, MPT(-1), std::nullopt, USD.currency);
BEAST_EXPECT(srcAmt == USD(80));
BEAST_EXPECT(dstAmt == MPT(80));
if (BEAST_EXPECT(
pathSet.size() == 1 && same(pathSet, stpath(gw, IPE(mpt.issuanceID())))))
{
// validate a payment works with the path
env(pay(carol, dan, MPT(10)),
path(pathSet[0]),
sendmax(USD(10)),
txflags(tfNoRippleDirect | tfPartialPayment));
}
}
// Cross-asset payment via MPT/IOU offer (one step)
{
Env env = pathTestEnv(*this);
env.fund(XRP(1'000), dan);
env.fund(XRP(1'000), gw);
MPTTester mpt(env, gw1, {.holders = {carol, alice}});
mpt.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT = mpt["MPT"];
mpt.authorize({.account = carol});
mpt.authorize({.account = alice});
mpt.pay(gw1, carol, 200);
env(trust(dan, USD(400)));
env(trust(alice, USD(400)));
env(pay(gw, alice, USD(200)));
env(offer(alice, MPT(100), USD(100)));
env.close();
// No sendMax
STPathSet pathSet;
STAmount srcAmt;
STAmount dstAmt;
std::tie(pathSet, srcAmt, dstAmt) = find_paths(env, carol, dan, USD(-1));
BEAST_EXPECT(srcAmt == MPT(100));
BEAST_EXPECT(dstAmt == USD(100));
if (BEAST_EXPECT(pathSet.size() == 1 && same(pathSet, stpath(IPE(USD)))))
{
// Validate the payment works with the path
env(pay(carol, dan, USD(10)),
path(pathSet[0]),
sendmax(MPT(10)),
txflags(tfNoRippleDirect | tfPartialPayment));
}
// Include sendMax
std::tie(pathSet, srcAmt, dstAmt) = find_paths(env, carol, dan, USD(-1), MPT(-1));
BEAST_EXPECT(srcAmt == MPT(90));
BEAST_EXPECT(dstAmt == USD(90));
if (BEAST_EXPECT(pathSet.size() == 1 && same(pathSet, stpath(IPE(USD)))))
{
// validate a payment works with the path
env(pay(carol, dan, USD(10)),
path(pathSet[0]),
sendmax(MPT(10)),
txflags(tfNoRippleDirect | tfPartialPayment));
}
// Include source token
std::tie(pathSet, srcAmt, dstAmt) =
find_paths(env, carol, dan, USD(-1), std::nullopt, MPT.mpt());
BEAST_EXPECT(srcAmt == MPT(80));
BEAST_EXPECT(dstAmt == USD(80));
if (BEAST_EXPECT(pathSet.size() == 1 && same(pathSet, stpath(IPE(USD)))))
{
// validate a payment works with the path
env(pay(carol, dan, USD(10)),
path(pathSet[0]),
sendmax(MPT(10)),
txflags(tfNoRippleDirect | tfPartialPayment));
}
}
// Cross-asset payment via MPT1/MPT offer (one step)
{
Env env = pathTestEnv(*this);
MPTTester mpt(env, gw, {.holders = {alice, dan}});
MPTTester mpt1(env, gw1, {.holders = {carol}});
mpt.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT = mpt["MPT"];
mpt1.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT1 = mpt1["MPT1"];
mpt.authorize({.account = alice});
mpt.authorize({.account = dan});
mpt.pay(gw, alice, 200);
mpt1.authorize({.account = carol});
mpt1.authorize({.account = alice});
mpt1.pay(gw1, carol, 200);
env(offer(alice, MPT1(100), MPT(100)));
env.close();
// No sendMax
STPathSet pathSet;
STAmount srcAmt;
STAmount dstAmt;
std::tie(pathSet, srcAmt, dstAmt) = find_paths(env, carol, dan, MPT(-1));
BEAST_EXPECT(srcAmt == MPT1(100));
BEAST_EXPECT(dstAmt == MPT(100));
if (BEAST_EXPECT(pathSet.size() == 1 && same(pathSet, stpath(IPE(mpt.issuanceID())))))
{
// validate a payment works with the path
env(pay(carol, dan, MPT(10)),
path(pathSet[0]),
sendmax(MPT1(10)),
txflags(tfNoRippleDirect | tfPartialPayment));
}
// Include sendMax
std::tie(pathSet, srcAmt, dstAmt) = find_paths(env, carol, dan, MPT(-1), MPT1(-1));
BEAST_EXPECT(srcAmt == MPT1(90));
BEAST_EXPECT(dstAmt == MPT(90));
if (BEAST_EXPECT(pathSet.size() == 1 && same(pathSet, stpath(IPE(mpt.issuanceID())))))
{
// validate a payment works with the path
env(pay(carol, dan, MPT(10)),
path(pathSet[0]),
sendmax(MPT1(10)),
txflags(tfNoRippleDirect | tfPartialPayment));
}
// Include source token
std::tie(pathSet, srcAmt, dstAmt) =
find_paths(env, carol, dan, MPT(-1), std::nullopt, MPT1.mpt());
BEAST_EXPECT(srcAmt == MPT1(80));
BEAST_EXPECT(dstAmt == MPT(80));
if (BEAST_EXPECT(pathSet.size() == 1 && same(pathSet, stpath(IPE(mpt.issuanceID())))))
{
// validate a payment works with the path
env(pay(carol, dan, MPT(10)),
path(pathSet[0]),
sendmax(MPT1(10)),
txflags(tfNoRippleDirect | tfPartialPayment));
}
}
// Cross-asset payment via offers (two steps)
{
Env env = pathTestEnv(*this);
env.fund(XRP(1'000), carol);
env.fund(XRP(1'000), dan);
MPTTester mpt(env, gw, {.holders = {alice, bob}});
mpt.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT = mpt["MPT"];
mpt.authorize({.account = alice});
mpt.authorize({.account = bob});
mpt.pay(gw, alice, 200);
mpt.pay(gw, bob, 200);
env(trust(bob, USD(200)));
env(pay(gw, bob, USD(100)));
env(trust(dan, USD(200)));
env(trust(alice, USD(200)));
env(offer(alice, XRP(100), MPT(100)));
env(offer(bob, MPT(100), USD(100)));
env.close();
// No sendMax
STPathSet pathSet;
STAmount srcAmt;
STAmount dstAmt;
std::tie(pathSet, srcAmt, dstAmt) = find_paths(env, carol, dan, USD(-1));
BEAST_EXPECT(srcAmt == XRP(100));
BEAST_EXPECT(dstAmt == USD(100));
if (BEAST_EXPECT(
pathSet.size() == 1 && same(pathSet, stpath(IPE(mpt.issuanceID()), IPE(USD)))))
{
// validate a payment works with the path
env(pay(carol, dan, USD(10)),
path(pathSet[0]),
sendmax(XRP(10)),
txflags(tfNoRippleDirect | tfPartialPayment));
}
// Include sendMax
std::tie(pathSet, srcAmt, dstAmt) = find_paths(env, carol, dan, USD(-1), XRP(100));
BEAST_EXPECT(srcAmt == XRP(90));
BEAST_EXPECT(dstAmt == USD(90));
if (BEAST_EXPECT(
pathSet.size() == 1 && same(pathSet, stpath(IPE(mpt.issuanceID()), IPE(USD)))))
{
// validate a payment works with the path
env(pay(carol, dan, USD(10)),
path(pathSet[0]),
sendmax(XRP(10)),
txflags(tfNoRippleDirect | tfPartialPayment));
}
}
// Cross-asset payment via offers (two steps)
// Start/End with mpt/mp1 and book steps in the middle
{
Env env = pathTestEnv(*this);
Account const gw2{"gw2"};
env.fund(XRP(1'000), gw2);
auto const USD2 = gw2["USD"];
MPTTester mpt(env, gw, {.holders = {alice, carol}});
mpt.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT = mpt["MPT"];
mpt.authorize({.account = alice});
mpt.authorize({.account = carol});
mpt.pay(gw, carol, 200);
MPTTester mpt1(env, gw1, {.holders = {bob, dan}});
mpt1.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT1 = mpt1["MPT1"];
mpt1.authorize({.account = bob});
mpt1.pay(gw1, bob, 200);
mpt1.authorize({.account = dan});
env(trust(alice, USD2(400)));
env(pay(gw2, alice, USD2(200)));
env(trust(bob, USD2(400)));
env(offer(alice, MPT(100), USD2(100)));
env(offer(bob, USD2(100), MPT1(100)));
env.close();
// No sendMax
STPathSet pathSet;
STAmount srcAmt;
STAmount dstAmt;
std::tie(pathSet, srcAmt, dstAmt) = find_paths(env, carol, dan, MPT1(-1));
BEAST_EXPECT(srcAmt == MPT(100));
BEAST_EXPECT(dstAmt == MPT1(100));
if (BEAST_EXPECT(
pathSet.size() == 1 &&
same(pathSet, stpath(IPE(USD2), IPE(mpt1.issuanceID())))))
{
// validate a payment works with the path
env(pay(carol, dan, MPT1(10)),
path(pathSet[0]),
sendmax(MPT(10)),
txflags(tfNoRippleDirect | tfPartialPayment));
}
// Include sendMax
std::tie(pathSet, srcAmt, dstAmt) = find_paths(env, carol, dan, MPT1(-1), MPT(-1));
BEAST_EXPECT(srcAmt == MPT(90));
BEAST_EXPECT(dstAmt == MPT1(90));
if (BEAST_EXPECT(
pathSet.size() == 1 &&
same(pathSet, stpath(IPE(USD2), IPE(mpt1.issuanceID())))))
{
// validate a payment works with the path
env(pay(carol, dan, MPT1(10)),
path(pathSet[0]),
sendmax(MPT(10)),
txflags(tfNoRippleDirect | tfPartialPayment));
}
// Include source token
std::tie(pathSet, srcAmt, dstAmt) =
find_paths(env, carol, dan, MPT1(-1), std::nullopt, MPT.mpt());
BEAST_EXPECT(srcAmt == MPT(80));
BEAST_EXPECT(dstAmt == MPT1(80));
if (BEAST_EXPECT(
pathSet.size() == 1 &&
same(pathSet, stpath(IPE(USD2), IPE(mpt1.issuanceID())))))
{
// validate a payment works with the path
env(pay(carol, dan, MPT1(10)),
path(pathSet[0]),
sendmax(MPT(10)),
txflags(tfNoRippleDirect | tfPartialPayment));
}
}
// Cross-asset payment via offers (two steps)
// Start/End with mpt/mp2 and book steps in the middle
// offers are MPT/MPT
{
Env env = pathTestEnv(*this);
Account const gw2{"gw2"};
env.fund(XRP(1'000), gw, gw1, gw2, alice, bob, carol, dan);
MPTTester mpt(env, gw, {.holders = {alice, carol}, .fund = false});
mpt.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT = mpt["MPT"];
mpt.authorize({.account = alice});
mpt.authorize({.account = carol});
mpt.pay(gw, carol, 200);
MPTTester mpt1(env, gw1, {.holders = {bob, alice}, .fund = false});
mpt1.create({.ownerCount = 1, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT1 = mpt1["MPT1"];
mpt1.authorize({.account = alice});
mpt1.pay(gw1, alice, 200);
mpt1.authorize({.account = bob});
MPTTester mpt2(env, gw2, {.holders = {bob, dan}, .fund = false});
mpt2.create({.ownerCount = 1, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT2 = mpt2["MPT2"];
mpt2.authorize({.account = bob});
mpt2.pay(gw2, bob, 200);
mpt2.authorize({.account = dan});
env(offer(alice, MPT(100), MPT1(100)));
env(offer(bob, MPT1(100), MPT2(100)));
env.close();
// No sendMax
STPathSet pathSet;
STAmount srcAmt;
STAmount dstAmt;
std::tie(pathSet, srcAmt, dstAmt) = find_paths(env, carol, dan, MPT2(-1));
BEAST_EXPECT(srcAmt == MPT(100));
BEAST_EXPECT(dstAmt == MPT2(100));
if (BEAST_EXPECT(
pathSet.size() == 1 &&
same(pathSet, stpath(IPE(mpt1.issuanceID()), IPE(mpt2.issuanceID())))))
{
// validate a payment works with the path
env(pay(carol, dan, MPT2(10)),
path(pathSet[0]),
sendmax(MPT(10)),
txflags(tfNoRippleDirect | tfPartialPayment));
}
// Include sendMax
std::tie(pathSet, srcAmt, dstAmt) = find_paths(env, carol, dan, MPT2(-1), MPT(-1));
BEAST_EXPECT(srcAmt == MPT(90));
BEAST_EXPECT(dstAmt == MPT2(90));
if (BEAST_EXPECT(
pathSet.size() == 1 &&
same(pathSet, stpath(IPE(mpt1.issuanceID()), IPE(mpt2.issuanceID())))))
{
// validate a payment works with the path
env(pay(carol, dan, MPT2(10)),
path(pathSet[0]),
sendmax(MPT(10)),
txflags(tfNoRippleDirect | tfPartialPayment));
}
// Include source token
std::tie(pathSet, srcAmt, dstAmt) =
find_paths(env, carol, dan, MPT2(-1), std::nullopt, MPT.mpt());
BEAST_EXPECT(srcAmt == MPT(80));
BEAST_EXPECT(dstAmt == MPT2(80));
if (BEAST_EXPECT(
pathSet.size() == 1 &&
same(pathSet, stpath(IPE(mpt1.issuanceID()), IPE(mpt2.issuanceID())))))
{
// validate a payment works with the path
env(pay(carol, dan, MPT2(10)),
path(pathSet[0]),
sendmax(MPT(10)),
txflags(tfNoRippleDirect | tfPartialPayment));
}
}
// verify no MPT rippling
{
Env env = pathTestEnv(*this);
Account const gw{"gw"};
Account const gw1{"gw1"};
Account const carol{"carol"};
Account const bob{"bob"};
Account const dan{"dan"};
Account const john{"john"};
Account const sean{"sean"};
env.fund(XRP(1'000'000), gw);
env.fund(XRP(1'000'000), gw1);
env.fund(XRP(1'000'000), carol);
env.fund(XRP(1'000'000), dan);
env.fund(XRP(1'000'000), bob);
env.fund(XRP(1'000'000), john);
env.fund(XRP(1'000'000), sean);
env.close();
MPTTester usd(env, gw, {.holders = {carol, dan}, .fund = false});
usd.create(
{.authorize = MPTCreate::AllHolders,
.pay = {{MPTCreate::AllHolders, 100}},
.flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const USD = usd["USD"];
env(offer(carol, XRP(100), USD(100)));
MPTTester gbp(env, gw, {.holders = {bob, sean}, .fund = false});
gbp.create(
{.authorize = MPTCreate::AllHolders,
.pay = {{{bob}, 100}},
.flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const GBP = gbp["GBP"];
MPTTester usd1(env, gw1, {.holders = {bob, dan}, .fund = false});
usd1.create(
{.authorize = MPTCreate::AllHolders,
.pay = {{{dan}, 100}},
.flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const USD1 = usd1["USD1"];
env(offer(bob, USD1(100), GBP(100)));
// dan has USD/gw and USD1/gw. Had USD been IOU, it would have
// been able to ripple through dan's account.
auto const [pathSet, srcAmt, dstAmt] = find_paths(env, john, sean, GBP(-1), XRP(-1));
BEAST_EXPECT(pathSet.empty());
env(pay(john, sean, GBP(10)),
sendmax(XRP(20)),
path(~USD, dan, gw1, ~GBP),
txflags(tfNoRippleDirect | tfPartialPayment),
ter(temBAD_PATH));
}
}
void
testCheck(FeatureBitset features)
{
testcase("Check Create/Cash");
using namespace test::jtx;
Account const gw{"gw"};
Account const alice{"alice"};
Account const carol{"carol"};
// MPTokensV2 is disabled
{
Env env{*this, features - featureMPTokensV2};
MPTTester mpt(env, gw, {.holders = {alice}});
mpt.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT = mpt["MPT"];
mpt.authorize({.account = alice});
uint256 const checkId{keylet::check(gw, env.seq(gw)).key};
env(check::create(gw, alice, MPT(100)), ter(temDISABLED));
env.close();
env(check::cash(alice, checkId, MPT(100)), ter(temDISABLED));
env.close();
}
// Insufficient funds
{
Env env{*this, features};
Account const carol{"carol"};
MPTTester mpt(env, gw, {.holders = {alice, carol}});
mpt.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT = mpt["MPT"];
mpt.authorize({.account = alice});
mpt.pay(gw, alice, 50);
uint256 const checkId{keylet::check(alice, env.seq(alice)).key};
// can create
env(check::create(alice, carol, MPT(100)));
env.close();
// can't cash since alice only has 50 of MPT
env(check::cash(carol, checkId, MPT(100)), ter(tecPATH_PARTIAL));
env.close();
// can cash if DeliverMin is set
// carol is not authorized, MPToken is authorized by CheckCash
env(check::cash(carol, checkId, check::DeliverMin(MPT(50))));
env.close();
BEAST_EXPECT(mpt.checkMPTokenAmount(carol, 50));
BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(50));
}
// Exceed max amount
{
Env env{*this, features};
MPTTester mpt(env, gw, {.holders = {alice}});
mpt.create(
{.maxAmt = 100,
.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT = mpt["MPT"];
uint256 const checkId{keylet::check(gw, env.seq(gw)).key};
// can create
env(check::create(gw, alice, MPT(200)));
env.close();
// can't cash since the outstanding amount exceeds max amount
env(check::cash(alice, checkId, MPT(200)), ter(tecPATH_PARTIAL));
env.close();
// can cash if DeliverMin is set
env(check::cash(alice, checkId, check::DeliverMin(MPT(100))));
env.close();
BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 100));
BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(100));
}
// MPTokenIssuance object doesn't exist
{
Env env{*this, features};
env.fund(XRP(1'000), gw, alice, carol);
env(check::create(alice, carol, MPT(gw)(50)), ter(tecOBJECT_NOT_FOUND));
env.close();
auto BTC = MPTTester({.env = env, .issuer = gw});
uint256 const chkId{getCheckIndex(gw, env.seq(gw))};
env(check::cash(carol, chkId, MPT(gw)(1)), ter(tecNO_ENTRY));
env.close();
}
// MPToken doesn't exist - can create check since MPToken will be
// automatically created on cash check
{
Env env{*this, features};
env.fund(XRP(1'000), gw, alice, carol);
auto BTC = MPTTester({.env = env, .issuer = gw});
uint256 const chkId{getCheckIndex(alice, env.seq(alice))};
env(check::create(alice, carol, BTC(50)));
env.close();
// But cashing fails if alice doesn't have MPToken
env(check::cash(carol, chkId, BTC(1)), ter(tecPATH_PARTIAL));
env.close();
}
// MPTLock is set
{
Env env{*this, features};
env.fund(XRP(1'000), gw, alice, carol);
env.close();
auto mpt = MPTTester(
{.env = env,
.issuer = gw,
.holders = {alice, carol},
.pay = 100,
.flags = MPTDEXFlags | tfMPTCanLock});
mpt.set({.flags = tfMPTLock});
// Create Check fails, holder or issuer as destination
env(check::create(alice, carol, mpt(10)), ter(tecLOCKED));
env.close();
env(check::create(gw, carol, mpt(10)), ter(tecLOCKED));
env.close();
mpt.set({.flags = tfMPTUnlock});
// Create Check succeeds, holder or issuer as destination
uint256 const chkIdAlice{getCheckIndex(alice, env.seq(alice))};
env(check::create(alice, carol, mpt(10)));
env.close();
uint256 const chkIdGw{getCheckIndex(gw, env.seq(gw))};
env(check::create(gw, carol, mpt(10)));
env.close();
mpt.set({.flags = tfMPTLock});
// Cash Check fails, holder and issuer env(check::cash(carol,
// chkIdAlice, mpt(1)), ter(tecPATH_PARTIAL)); // tec is different
// if the source is the issuer (this is consistent with IOU)
env(check::cash(carol, chkIdGw, mpt(2)), ter(tecLOCKED));
env.close();
mpt.set({.flags = tfMPTUnlock});
// Cash Check succeeds, holder and issuer.
env(check::cash(carol, chkIdAlice, mpt(1)));
env(check::cash(carol, chkIdGw, mpt(2)));
// Individual lock
mpt.set({.holder = alice, .flags = tfMPTLock});
env(check::create(alice, carol, mpt(10)), ter(tecLOCKED));
env.close();
env(check::create(carol, alice, mpt(10)), ter(tecLOCKED));
env.close();
mpt.set({.holder = alice, .flags = tfMPTUnlock});
uint256 const chkId1{getCheckIndex(alice, env.seq(alice))};
env(check::create(alice, carol, mpt(10)));
env.close();
uint256 const chkId2{getCheckIndex(gw, env.seq(gw))};
env(check::create(gw, alice, mpt(10)));
env.close();
uint256 const chkId3{getCheckIndex(alice, env.seq(alice))};
env(check::create(alice, gw, mpt(10)));
env.close();
uint256 const chkId4{getCheckIndex(gw, env.seq(gw))};
env(check::create(gw, alice, mpt(10)));
env.close();
mpt.set({.holder = alice, .flags = tfMPTLock});
env(check::cash(carol, chkId1, mpt(1)), ter(tecPATH_PARTIAL));
env(check::cash(alice, chkId2, mpt(1)), ter(tecLOCKED));
env(check::cash(gw, chkId3, mpt(1)), ter(tecPATH_PARTIAL));
env(check::cash(alice, chkId4, mpt(1)), ter(tecLOCKED));
}
// MPTRequireAuth flag is set and the account is not authorized.
// Can create check, which is consistent with the trustlines.
// It should fail on cash check.
{
Env env{*this, features};
env.fund(XRP(1'000), gw, alice, carol);
auto BTC = MPTTester(
{.env = env,
.issuer = gw,
.holders = {alice, carol},
.flags = tfMPTRequireAuth | MPTDEXFlags});
uint256 const chkId{getCheckIndex(alice, env.seq(alice))};
env(check::create(alice, carol, BTC(50)));
env.close();
// Authorize alice
BTC.authorize({.account = gw, .holder = alice});
env(pay(gw, alice, BTC(100)));
// carol is still not authorized
env(check::cash(carol, chkId, BTC(10)), ter(tecNO_AUTH));
env.close();
// authorize carol, can cash now
BTC.authorize({.account = gw, .holder = carol});
env(check::cash(carol, chkId, BTC(10)));
env.close();
}
// MPTCanTransfer disabled
{
Env env{*this, features};
env.fund(XRP(1'000), gw, alice, carol);
env.close();
MPTTester mpt(
{.env = env,
.issuer = gw,
.holders = {alice, carol},
.flags = tfMPTCanTrade,
.mutableFlags = tmfMPTCanMutateCanTransfer});
// src is issuer
uint256 checkId{keylet::check(gw, env.seq(gw)).key};
// can create
env(check::create(gw, alice, mpt(100)));
env.close();
// can cash since source is issuer
env(check::cash(alice, checkId, mpt(100)));
env.close();
BEAST_EXPECT(env.balance(alice, mpt) == mpt(100));
BEAST_EXPECT(env.balance(gw, mpt) == mpt(-100));
// dst is issuer
checkId = keylet::check(alice, env.seq(alice)).key;
// can create
env(check::create(alice, gw, mpt(100)));
env.close();
// can cash since source is issuer
env(check::cash(gw, checkId, mpt(100)));
env.close();
BEAST_EXPECT(env.balance(alice, mpt) == mpt(0));
BEAST_EXPECT(env.balance(gw, mpt) == mpt(0));
// neither src nor dst is issuer, can still create
checkId = keylet::check(alice, env.seq(alice)).key;
env(check::create(alice, carol, mpt(100)));
env.close();
// can't cash
env(check::cash(carol, checkId, mpt(10)), ter(tecPATH_PARTIAL));
env.close();
// can cash now
mpt.set({.account = gw, .mutableFlags = tmfMPTSetCanTransfer});
env(pay(gw, alice, mpt(10)));
env.close();
env(check::cash(carol, checkId, mpt(10)));
env.close();
}
// MPTCanTrade disabled
{
Env env{*this, features};
env.fund(XRP(1'000), gw, alice, carol);
env.close();
MPTTester mpt(
{.env = env,
.issuer = gw,
.holders = {alice, carol},
.flags = tfMPTCanTransfer,
.mutableFlags = tmfMPTCanMutateCanTrade});
uint256 checkId{keylet::check(gw, env.seq(gw)).key};
// can't create
env(check::create(gw, alice, mpt(100)), ter(tecNO_PERMISSION));
env.close();
mpt.set({.account = gw, .mutableFlags = tmfMPTSetCanTrade});
// can't cash
checkId = keylet::check(gw, env.seq(gw)).key;
env(check::create(gw, carol, mpt(100)));
env.close();
mpt.set({.account = gw, .mutableFlags = tmfMPTClearCanTrade});
env(check::cash(carol, checkId, mpt(10)), ter(tecNO_PERMISSION));
env.close();
}
// MPTokenIssuance object doesn't exist
{
Env env{*this, features};
env.fund(XRP(1'000), gw, alice, carol);
auto USD = MPTTester({.env = env, .issuer = gw, .holders = {alice}});
uint256 const chkId{getCheckIndex(alice, env.seq(alice))};
env(check::create(alice, carol, USD(1)));
env.close();
// temMALFORMED because MPT is not USD. It doesn't matter if it
// exists or not
env(check::cash(carol, chkId, MPT(alice)(1)), ter(temMALFORMED));
env.close();
}
// MPToken object doesn't exist and the account is not the issuer of MPT
{
Env env{*this, features};
env.fund(XRP(1'000), gw, alice, carol);
auto BTC = MPTTester({.env = env, .issuer = gw, .holders = {alice}, .pay = 1'000});
uint256 const chkId{getCheckIndex(alice, env.seq(alice))};
env(check::create(alice, carol, BTC(1)));
env.close();
// MPToken is automatically created
env(check::cash(carol, chkId, BTC(1)));
env.close();
}
// MPTRequireAuth flag is set and the account is not authorized.
{
Env env{*this, features};
env.fund(XRP(1'000), gw, alice, carol);
auto BTC = MPTTester(
{.env = env,
.issuer = gw,
.holders = {alice},
.flags = tfMPTRequireAuth | MPTDEXFlags,
.authHolder = true});
uint256 const chkId{getCheckIndex(alice, env.seq(alice))};
env(check::create(alice, carol, BTC(1)));
env.close();
env(check::cash(carol, chkId, BTC(1)), ter(tecPATH_PARTIAL));
env.close();
}
// MPTCanTransfer is not set and the account is not the issuer of MPT
{
Env env{*this, features};
env.fund(XRP(1'000), gw, alice, carol);
auto EUR = MPTTester(
{.env = env, .issuer = gw, .holders = {alice, carol}, .flags = tfMPTCanTrade});
uint256 const chkId{getCheckIndex(alice, env.seq(alice))};
// alice can create
env(check::create(alice, carol, EUR(1)));
env.close();
// carol can't cash
env(check::cash(carol, chkId, EUR(1)), ter(tecPATH_PARTIAL));
env.close();
// if issuer creates a check then carol can cash since
// it's a transfer from the issuer
uint256 const chkId1{getCheckIndex(gw, env.seq(gw))};
// alice can't create since CanTransfer is not set
env(check::create(gw, carol, EUR(1)));
env.close();
env(check::cash(carol, chkId1, EUR(1)));
env.close();
}
// Can create check if src/dst don't own MPT
{
Env env{*this, features};
MPTTester mpt(env, gw);
mpt.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT = mpt["MPT"];
env.fund(XRP(1'000), alice, carol);
// src is issuer
uint256 const checkId{keylet::check(alice, env.seq(alice)).key};
// can create
env(check::create(alice, carol, MPT(100)));
env.close();
// authorize/fund alice
mpt.authorize({.account = alice});
mpt.pay(gw, alice, 100);
// carol can cash the check. MPToken is created automatically
env(check::cash(carol, checkId, MPT(100)));
env.close();
BEAST_EXPECT(mpt.checkMPTokenAmount(carol, 100));
BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(100));
}
// Normal create/cash
{
Env env{*this, features};
MPTTester mpt(env, gw, {.holders = {alice}});
mpt.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT = mpt["MPT"];
mpt.authorize({.account = alice});
uint256 const checkId{keylet::check(gw, env.seq(gw)).key};
env(check::create(gw, alice, MPT(100)));
env.close();
env(check::cash(alice, checkId, MPT(100)));
env.close();
BEAST_EXPECT(mpt.checkMPTokenAmount(alice, 100));
BEAST_EXPECT(mpt.checkMPTokenOutstandingAmount(100));
}
}
void
testAMMClawback(FeatureBitset features)
{
using namespace jtx;
testcase("AMMClawback");
Account const gw{"gw"};
Account const alice{"alice"};
auto const USD = gw["USD"];
// MPTokenIssuance object doesn't exist
{
Env env(*this, features);
env.fund(XRP(1'000), gw, alice);
MPTTester const BTC({.env = env, .issuer = gw});
AMM const amm(env, gw, BTC(100), USD(100));
env(amm::ammClawback(gw, alice, USD, MPT(alice), std::nullopt), ter(terNO_AMM));
env(amm::ammClawback(gw, alice, USD, BTC, MPT(alice)(100)), ter(temBAD_AMOUNT));
}
// MPTLock flag is set and the account is not the issuer of MPT -
// can still clawback since the issuer clawbacks
{
Env env(*this, features);
env.fund(XRP(100'000), gw, alice);
env.close();
env(fset(gw, asfAllowTrustLineClawback));
env.close();
auto BTC = MPTTester(
{.env = env,
.issuer = gw,
.holders = {alice},
.pay = 40'000,
.flags = tfMPTCanLock | tfMPTCanClawback | MPTDEXFlags});
env.trust(USD(10'000), alice);
env(pay(gw, alice, USD(10'000)));
env.close();
AMM amm(env, gw, BTC(100), USD(100));
env.close();
amm.deposit(alice, 1'000);
env.close();
BTC.set({.flags = tfMPTLock});
env(amm::ammClawback(gw, alice, BTC, USD, std::nullopt));
}
// MPTRequireAuth flag is set and the account is not authorized -
// can still clawback since the issuer clawbacks
{
Env env(*this, features);
env.fund(XRP(100'000), gw, alice);
env.close();
env(fset(gw, asfAllowTrustLineClawback));
env.close();
auto BTC = MPTTester(
{.env = env,
.issuer = gw,
.holders = {alice},
.pay = 40'000,
.flags = tfMPTRequireAuth | tfMPTCanClawback | MPTDEXFlags,
.authHolder = true});
env.trust(USD(10'000), alice);
env(pay(gw, alice, USD(10'000)));
env.close();
AMM amm(env, gw, BTC(100), USD(100));
env.close();
amm.deposit(alice, 1'000);
env.close();
BTC.authorize({.account = gw, .holder = alice, .flags = tfMPTUnauthorize});
env(amm::ammClawback(gw, alice, BTC, USD, std::nullopt));
}
// MPTCanTransfer is not set and the account is not the issuer of MPT -
// can't clawback since a holder can't deposit
{
Env env(*this, features);
env.fund(XRP(100'000), gw, alice);
env.close();
env(fset(gw, asfAllowTrustLineClawback));
env.close();
auto BTC = MPTTester(
{.env = env,
.issuer = gw,
.holders = {alice},
.pay = 40'000,
.flags = tfMPTCanClawback | tfMPTCanTrade,
.authHolder = true});
env.trust(USD(10'000), alice);
env(pay(gw, alice, USD(10'000)));
env.close();
AMM amm(env, gw, BTC(100), USD(100));
env.close();
// alice can't deposit since MPTCanTransfer is not set
amm.deposit(
DepositArg{.account = alice, .tokens = 1'000, .err = ter(tecNO_PERMISSION)});
env.close();
// can't clawback since alice is not an LP
env(amm::ammClawback(gw, alice, BTC, USD, std::nullopt), ter(tecAMM_BALANCE));
}
{
Env env(*this, features);
fund(env, gw, {alice}, XRP(1'000), {USD(1'000)});
MPTTester mpt(env, gw, {.fund = false});
mpt.create({.flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT = mpt["MPT"];
AMM amm(env, gw, MPT(100), XRP(100));
amm.deposit(DepositArg{.account = alice, .asset1In = XRP(10)});
amm::ammClawback(gw, alice, MPTIssue(mpt.issuanceID()), xrpIssue(), MPT(10));
}
{
Env env(*this, features);
fund(env, gw, {alice}, XRP(1'000), {USD(1'000)});
MPTTester mpt(env, gw, {.fund = false});
mpt.create({.flags = tfMPTCanTransfer | tfMPTCanTrade});
mpt.authorize({.account = alice});
mpt.pay(gw, alice, 1'000);
auto const MPT = mpt["MPT"];
AMM amm(env, gw, MPT(100), XRP(100));
amm.deposit(DepositArg{.account = alice, .tokens = 10'000});
amm::ammClawback(gw, alice, MPTIssue(mpt.issuanceID()), xrpIssue(), MPT(10));
}
// clawback one asset from MPT/MPT AMM. MPToken for another asset
// is created for the Liquidity Provider
{
Env env(*this, features);
env.fund(XRP(1'000), gw, alice);
auto USD = MPTTester(
{.env = env,
.issuer = gw,
.holders = {alice},
.pay = 10'000,
.flags = tfMPTCanClawback | MPTDEXFlags});
auto EUR =
MPTTester({.env = env, .issuer = gw, .flags = tfMPTCanClawback | MPTDEXFlags});
AMM amm(env, gw, USD(1'000), EUR(1'000));
amm.deposit({.account = alice, .asset1In = USD(1'000)});
// MPToken doesn't exist
BEAST_EXPECT(env.le(keylet::mptoken(EUR.issuanceID(), alice)) == nullptr);
env(amm::ammClawback(gw, alice, USD, EUR, USD(100)));
// MPToken is created
BEAST_EXPECT(env.le(keylet::mptoken(EUR.issuanceID(), alice)));
}
}
void
testBasicAMM(FeatureBitset features)
{
testcase("Basic AMM");
using namespace jtx;
Account const gw{"gw"};
Account const alice{"alice"};
Account const carol{"carol"};
Account const bob{"bob"};
auto const USD = gw["USD"];
// Create/deposit/withdraw
{
Env env{*this};
fund(env, gw, {alice, carol, bob}, XRP(1'000), {USD(1'000)});
MPTTester mpt(env, gw, {.fund = false});
mpt.create({.flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT = mpt["MPT"];
mpt.authorize({.account = alice});
mpt.authorize({.account = carol});
mpt.pay(gw, alice, 1'000);
mpt.pay(gw, carol, 1'000);
MPTTester mpt1(env, gw, {.fund = false});
mpt1.create({.flags = tfMPTCanTransfer | tfMPTCanTrade});
auto const MPT1 = mpt1["MPT1"];
mpt1.authorize({.account = alice});
mpt1.authorize({.account = carol});
mpt1.pay(gw, alice, 1'000);
mpt1.pay(gw, carol, 1'000);
std::vector<std::tuple<PrettyAmount, PrettyAmount, IOUAmount>> pools = {
{XRP(100), MPT(100), IOUAmount{100'000}},
{USD(100), MPT(100), IOUAmount{100}},
{MPT(100), MPT1(100), IOUAmount{100}}};
for (auto& pool : pools)
{
AMM amm(env, gw, std::get<0>(pool), std::get<1>(pool));
amm.deposit(alice, std::get<2>(pool));
amm.deposit(carol, std::get<2>(pool));
// bob doesn't own MPT
amm.deposit(
DepositArg{
.account = bob, .tokens = std::get<2>(pool), .err = ter(tecNO_AUTH)});
amm.withdrawAll(alice);
amm.withdrawAll(carol);
amm.withdrawAll(gw);
BEAST_EXPECT(!amm.ammExists());
}
}
// Payment, one step
{
Env env(*this);
env.fund(XRP(1'000), gw, alice, carol);
env.close();
MPT const USD = MPTTester({.env = env, .issuer = gw, .holders = {alice, carol}});
MPT const EUR = MPTTester({.env = env, .issuer = gw, .holders = {alice, carol}});
env(pay(gw, alice, EUR(100)));
AMM const amm(env, gw, USD(1'100), EUR(1'000));
env(pay(alice, carol, USD(100)), sendmax(EUR(100)));
BEAST_EXPECT(amm.expectBalances(USD(1'000), EUR(1'100), amm.tokens()));
BEAST_EXPECT(env.balance(carol, USD) == USD(100));
BEAST_EXPECT(env.balance(alice, EUR) == EUR(0));
}
// Payment, two steps
{
Env env(*this);
env.fund(XRP(1'000), gw, alice, carol);
env.close();
MPT const USD = MPTTester({.env = env, .issuer = gw, .holders = {alice, carol}});
MPT const EUR = MPTTester({.env = env, .issuer = gw, .holders = {alice, carol}});
MPT const BTC = MPTTester({.env = env, .issuer = gw, .holders = {alice, carol}});
env(pay(gw, alice, EUR(100)));
AMM const ammEUR_USD(env, gw, EUR(1'000), USD(1'100));
AMM const ammUSD_BTC(env, gw, USD(1'000), BTC(1'100));
env(pay(alice, carol, BTC(100)),
sendmax(EUR(100)),
path(~USD, ~BTC),
txflags(tfNoRippleDirect));
BEAST_EXPECT(ammEUR_USD.expectBalances(USD(1'000), EUR(1'100), ammEUR_USD.tokens()));
BEAST_EXPECT(ammUSD_BTC.expectBalances(USD(1'100), BTC(1'000), ammUSD_BTC.tokens()));
BEAST_EXPECT(env.balance(carol, BTC) == BTC(100));
BEAST_EXPECT(env.balance(alice, EUR) == EUR(0));
}
// Offer crossing
{
Env env(*this);
env.fund(XRP(1'000), gw, alice);
env.close();
MPT const USD = MPTTester({.env = env, .issuer = gw, .holders = {alice}});
MPT const EUR = MPTTester({.env = env, .issuer = gw, .holders = {alice}});
env(pay(gw, alice, EUR(1'000)));
AMM const amm(env, gw, EUR(1'000'000), USD(1'001'000));
env(offer(alice, USD(1'000), EUR(1'000)));
BEAST_EXPECT(amm.expectBalances(USD(1'000'000), EUR(1'001'000), amm.tokens()));
BEAST_EXPECT(env.balance(alice, USD) == USD(1'000));
BEAST_EXPECT(env.balance(alice, EUR) == EUR(0));
}
{
Env env(*this);
env.fund(XRP(1'000'000), gw, alice, carol);
auto USD = MPTTester(
{.env = env,
.issuer = gw,
.flags = tfMPTCanLock | MPTDEXFlags,
.mutableFlags = tmfMPTCanMutateRequireAuth | tmfMPTCanMutateCanTransfer |
tmfMPTCanMutateCanClawback | tmfMPTCanMutateCanTrade});
auto EUR = MPTTester({.env = env, .issuer = gw, .holders = {alice}, .pay = 1'000'000});
auto const increment = env.current()->fees().increment;
auto const txfee = fee(drops(increment));
auto const badMPT = MPT(gw, 1'000);
auto createDeleteAMM = [&](Account const& lp) {
AMM amm(
env,
lp,
USD(1'000),
EUR(1'000),
CreateArg{.fee = static_cast<std::uint32_t>(increment.value())});
amm.withdrawAll(lp);
BEAST_EXPECT(!amm.ammExists());
};
//
// AMMCreate
//
auto createJv = AMM::createJv(alice, badMPT(1'000), EUR(1'000), 0);
auto createFail = [&](Account const& account, auto const& err) {
createJv[sfAccount] = account.human();
env(createJv, txfee, ter(err));
env.close();
};
// MPTokenIssuance doesn't exist
createFail(alice, tecOBJECT_NOT_FOUND);
// MPToken doesn't exist
createJv[sfAmount] = STAmount{USD(1'000)}.getJson();
createFail(alice, tecNO_AUTH);
// alice authorizes MPToken, can create
USD.authorize({.account = alice});
env(pay(gw, alice, USD(1'000'000)), txfee);
env.close();
createDeleteAMM(alice);
// MPTLock is set
// alice and issuer can't create
USD.set({.flags = tfMPTLock});
createFail(alice, tecFROZEN);
createFail(gw, tecFROZEN);
// MPTRequireAuth is set
// alice is not authorized
USD.set({.flags = tfMPTUnlock});
USD.set({.mutableFlags = tmfMPTSetRequireAuth});
createFail(alice, tecNO_AUTH);
// issuer can create
createDeleteAMM(gw);
// alice is authorized, can create
USD.authorize({.account = gw, .holder = alice});
createDeleteAMM(alice);
// MPTCanTransfer is not set
USD.set({.mutableFlags = tmfMPTClearRequireAuth});
USD.set({.mutableFlags = tmfMPTClearCanTransfer});
// alice can't create
createFail(alice, tecNO_PERMISSION);
// issuer can create
createDeleteAMM(gw);
USD.set({.mutableFlags = tmfMPTSetCanTransfer});
// alice can create
createDeleteAMM(alice);
// MPTCanTrade is not set
USD.set({.mutableFlags = tmfMPTSetCanTransfer});
USD.set({.mutableFlags = tmfMPTClearCanTrade});
// alice and issuer can't create
createFail(alice, tecNO_PERMISSION);
createFail(gw, tecNO_PERMISSION);
USD.set({.mutableFlags = tmfMPTSetCanTrade});
//
// AMMDeposit
//
AMM amm(env, gw, USD(1'000), EUR(1'000));
// MPTokenIssuance doesn't exist
amm.deposit(
{.account = alice,
.asset1In = badMPT(1),
.asset2In = EUR(1),
.assets = std::make_pair(badMPT, EUR),
.err = ter(terNO_AMM)});
// MPToken doesn't exist
amm.deposit(
{.account = carol, .asset1In = USD(1), .asset2In = EUR(1), .err = ter(tecNO_AUTH)});
// MPTLock is set
USD.set({.flags = tfMPTLock});
// alice and issuer can't deposit
for (auto const& account : {carol, gw})
{
amm.deposit(
{.account = account,
.asset1In = USD(1),
.asset2In = EUR(1),
.err = ter(tecFROZEN)});
amm.deposit(
{.account = account,
.asset1In = EUR(1),
.assets = std::make_pair(EUR, USD),
.err = ter(tecFROZEN)});
}
USD.set({.flags = tfMPTUnlock});
// MPTRequireAuth is set
// carol authorizes MPToken but is not authorized by the issuer
USD.authorize({.account = carol});
env(pay(gw, carol, USD(1'000'000)));
// carol authorizes EUR
EUR.authorize({.account = carol});
env(pay(gw, carol, EUR(1'000'000)));
USD.set({.mutableFlags = tmfMPTSetRequireAuth});
// have to authorize amm account
USD.authorize({.account = gw, .holder = Account{"amm", amm.ammAccount()}});
env.close();
amm.deposit(
{.account = carol, .asset1In = USD(1), .asset2In = EUR(1), .err = ter(tecNO_AUTH)});
amm.deposit(
{.account = carol,
.asset1In = EUR(1),
.assets = std::make_pair(EUR, USD),
.err = ter(tecNO_AUTH)});
// issuer can deposit
amm.deposit({.account = gw, .tokens = 1'000});
// carol is authorized, can deposit
USD.authorize({.account = gw, .holder = carol});
amm.deposit({.account = carol, .tokens = 1'000});
// MPTCanTransfer is not set
USD.set({.mutableFlags = tmfMPTClearRequireAuth});
USD.set({.mutableFlags = tmfMPTClearCanTransfer});
// carol can't deposit
amm.deposit(
{.account = carol,
.asset1In = USD(1),
.asset2In = EUR(1),
.err = ter(tecNO_PERMISSION)});
amm.deposit(
{.account = carol,
.asset1In = EUR(1),
.assets = std::make_pair(EUR, USD),
.err = ter(tecNO_PERMISSION)});
// issuer can deposit
amm.deposit({.account = gw, .tokens = 1'000});
// carol can deposit
USD.set({.mutableFlags = tmfMPTSetCanTransfer});
amm.deposit({.account = carol, .tokens = 1'000});
// MPTCanTrade is not set
USD.set({.mutableFlags = tmfMPTSetCanTransfer});
USD.set({.mutableFlags = tmfMPTClearCanTrade});
amm.deposit({.account = gw, .tokens = 1'000, .err = ter(tecNO_PERMISSION)});
amm.deposit({.account = carol, .tokens = 1'000, .err = ter(tecNO_PERMISSION)});
USD.set({.mutableFlags = tmfMPTSetCanTrade});
//
// AMMWithdraw
//
// MPTokenIssuance doesn't exist
amm.withdraw(
WithdrawArg{
.account = carol,
.asset1Out = badMPT(1),
.asset2Out = EUR(1),
.assets = std::make_pair(badMPT, EUR),
.err = ter(terNO_AMM)});
// MPToken doesn't exist - doesn't apply since MPToken is created
// on withdraw in this case
// MPTLock is set
USD.set({.flags = tfMPTLock});
// carol and issuer can't withdraw
for (auto const& account : {carol, gw})
{
amm.withdraw(
{.account = account,
.asset1Out = USD(1),
.asset2Out = EUR(1),
.err = ter(tecFROZEN)});
amm.withdraw({.account = account, .tokens = 1'000, .err = ter(tecFROZEN)});
// can single withdraw another asset
amm.withdraw(
{.account = account, .asset1Out = EUR(1), .assets = std::make_pair(EUR, USD)});
}
USD.set({.flags = tfMPTUnlock});
// MPTRequireAuth is set
USD.set({.mutableFlags = tmfMPTSetRequireAuth});
USD.authorize({.account = gw, .holder = carol, .flags = tfMPTUnauthorize});
// carol can't withdraw
amm.withdraw(
{.account = carol,
.asset1Out = USD(1),
.asset2Out = EUR(1),
.err = ter(tecNO_AUTH)});
// can withdraw another asset
amm.withdraw(
{.account = carol, .asset1Out = EUR(1), .assets = std::make_pair(EUR, USD)});
// issuer can withdraw
amm.withdraw({.account = gw, .asset1Out = USD(1), .asset2Out = EUR(1)});
// carol is authorized, can withdraw
USD.authorize({.account = gw, .holder = carol});
amm.withdraw({.account = carol, .asset1Out = USD(1), .asset2Out = EUR(1)});
// MPTCanTransfer is set
USD.set({.mutableFlags = tmfMPTClearRequireAuth});
USD.set({.mutableFlags = tmfMPTClearCanTransfer});
// carol can't withdraw
amm.withdraw(
{.account = carol,
.asset1Out = USD(1),
.asset2Out = EUR(1),
.err = ter(tecNO_PERMISSION)});
// can withdraw another asset
amm.withdraw(
{.account = carol, .asset1Out = EUR(1), .assets = std::make_pair(EUR, USD)});
// issuer can withdraw
amm.withdraw({.account = gw, .asset1Out = USD(1), .asset2Out = EUR(1)});
// carol can withdraw
USD.set({.mutableFlags = tmfMPTSetCanTransfer});
amm.withdraw({.account = carol, .asset1Out = USD(1), .asset2Out = EUR(1)});
USD.set({.mutableFlags = tmfMPTSetCanTransfer});
USD.set({.mutableFlags = tmfMPTClearCanTrade});
amm.withdraw({.account = gw, .tokens = 1'000, .err = ter(tecNO_PERMISSION)});
amm.withdraw({.account = carol, .tokens = 1'000, .err = ter(tecNO_PERMISSION)});
USD.set({.mutableFlags = tmfMPTSetCanTrade});
// MPToken created on withdraw
// redeem all carol's USD and unauthorize USD
amm.withdrawAll(carol);
env(pay(carol, gw, env.balance(carol, USD)));
USD.authorize({.account = carol, .flags = tfMPTUnauthorize});
BEAST_EXPECT(env.le(keylet::mptoken(USD.issuanceID(), carol)) == nullptr);
// single-deposit EUR
amm.deposit(
{.account = carol, .asset1In = EUR(1'000), .assets = std::make_pair(EUR, USD)});
// withdraw in USD to create MPToken
amm.withdraw({.account = carol, .asset1Out = USD(100)});
BEAST_EXPECT(env.le(keylet::mptoken(USD.issuanceID(), carol)));
}
}
public:
void
run() override
{
using namespace test::jtx;
FeatureBitset const all{testable_amendments()};
testMultiSendMaximumAmount(all);
// MPTokenIssuanceCreate
testCreateValidation(all - featureSingleAssetVault);
testCreateValidation(all - featurePermissionedDomains);
testCreateValidation(all);
testCreateEnabled(all - featureSingleAssetVault);
testCreateEnabled(all);
// MPTokenIssuanceDestroy
testDestroyValidation(all - featureSingleAssetVault);
testDestroyValidation(all - featureSingleAssetVault - featureMPTokensV2);
testDestroyValidation((all | featureSingleAssetVault) - featureMPTokensV2);
testDestroyValidation(all - featureMPTokensV2);
testDestroyValidation(all | featureSingleAssetVault);
testDestroyEnabled(all - featureSingleAssetVault);
testDestroyEnabled(all - featureSingleAssetVault - featureMPTokensV2);
testDestroyEnabled((all | featureSingleAssetVault) - featureMPTokensV2);
testDestroyEnabled(all - featureMPTokensV2);
testDestroyEnabled(all | featureSingleAssetVault);
// MPTokenAuthorize
testAuthorizeValidation(all - featureSingleAssetVault);
testAuthorizeValidation(all - featureSingleAssetVault - featureMPTokensV2);
testAuthorizeValidation((all | featureSingleAssetVault) - featureMPTokensV2);
testAuthorizeValidation(all - featureMPTokensV2);
testAuthorizeValidation(all | featureSingleAssetVault);
testAuthorizeEnabled(all - featureSingleAssetVault);
testAuthorizeEnabled(all - featureSingleAssetVault - featureMPTokensV2);
testAuthorizeEnabled((all | featureSingleAssetVault) - featureMPTokensV2);
testAuthorizeEnabled(all - featureMPTokensV2);
testAuthorizeEnabled(all | featureSingleAssetVault);
// MPTokenIssuanceSet
testSetValidation(all - featureSingleAssetVault - featureDynamicMPT);
testSetValidation(all - featureSingleAssetVault);
testSetValidation(all - featureDynamicMPT);
testSetValidation(all - featurePermissionedDomains);
testSetValidation(all);
testSetEnabled(all - featureSingleAssetVault);
testSetEnabled(all);
// MPT clawback
testClawbackValidation(all);
testClawbackValidation(all - featureMPTokensV2);
testClawback(all);
testClawback(all - featureMPTokensV2);
// Test Direct Payment
testPayment(all);
testPayment(all | featureSingleAssetVault);
testPayment((all | featureSingleAssetVault) - featureMPTokensV2);
testPayment(all - featureMPTokensV2);
testDepositPreauth(all);
testDepositPreauth(all - featureCredentials);
testDepositPreauth(all - featureMPTokensV2);
testDepositPreauth(all - featureCredentials - featureMPTokensV2);
// Test MPT Amount is invalid in Tx, which don't support MPT
testMPTInvalidInTx(all);
// Test parsed MPTokenIssuanceID in API response metadata
testTxJsonMetaFields(all);
// Test tokens equality
testTokensEquality();
// Test helpers
testHelperFunctions();
// Dynamic MPT
testInvalidCreateDynamic(all);
testInvalidSetDynamic(all);
testMutateMPT(all);
testMutateCanLock(all);
testMutateRequireAuth(all);
testMutateCanEscrow(all);
testMutateCanTransfer(all);
testMutateCanTransfer(all - featureMPTokensV2);
testMutateCanClawback(all);
// Test offer crossing
testOfferCrossing(all);
// Test cross asset payment
testCrossAssetPayment(all);
// Test path finding
testPath(all);
// Test checks
testCheck(all);
// Add AMMClawback
testAMMClawback(all);
// Test AMM
testBasicAMM(all);
}
};
BEAST_DEFINE_TESTSUITE_PRIO(MPToken, app, xrpl, 2);
} // namespace test
} // namespace xrpl