mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-19 10:35:50 +00:00
#5224 added (among other things) a `VaultWithdraw` transaction that allows setting the recipient of the withdrawn funds in the `Destination` transaction field. This technically turns this transaction into a payment, and in some respect the implementation does follow payment rules, e.g. enforcement of `lsfRequireDestTag` or `lsfDepositAuth`, or that MPT transfer has destination `MPToken`. However for IOUs, it missed verification that the destination account has a trust line to the asset issuer. Since the default behavior of `accountSendIOU` is to create this trust line (if missing), this is what `VaultWithdraw` currently does. This is incorrect, since the `Destination` might not be interested in holding the asset in question; this basically enables spammy transfers. This change, therefore, removes automatic creation of a trust line to the `Destination` account in `VaultWithdraw`.
3348 lines
115 KiB
C++
3348 lines
115 KiB
C++
//------------------------------------------------------------------------------
|
|
/*
|
|
This file is part of rippled: https://github.com/ripple/rippled
|
|
Copyright (c) 2024 Ripple Labs Inc.
|
|
|
|
Permission to use, copy, modify, and/or distribute this software for any
|
|
purpose with or without fee is hereby granted, provided that the above
|
|
copyright notice and this permission notice appear in all copies.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
//==============================================================================
|
|
|
|
#include <test/jtx.h>
|
|
#include <test/jtx/AMMTest.h>
|
|
|
|
#include <xrpld/ledger/View.h>
|
|
|
|
#include <xrpl/basics/base_uint.h>
|
|
#include <xrpl/beast/unit_test/suite.h>
|
|
#include <xrpl/json/json_forwards.h>
|
|
#include <xrpl/json/json_value.h>
|
|
#include <xrpl/protocol/AccountID.h>
|
|
#include <xrpl/protocol/Asset.h>
|
|
#include <xrpl/protocol/Feature.h>
|
|
#include <xrpl/protocol/Indexes.h>
|
|
#include <xrpl/protocol/Issue.h>
|
|
#include <xrpl/protocol/MPTIssue.h>
|
|
#include <xrpl/protocol/Protocol.h>
|
|
#include <xrpl/protocol/SField.h>
|
|
#include <xrpl/protocol/STAmount.h>
|
|
#include <xrpl/protocol/STNumber.h>
|
|
#include <xrpl/protocol/TER.h>
|
|
#include <xrpl/protocol/TxFlags.h>
|
|
#include <xrpl/protocol/XRPAmount.h>
|
|
#include <xrpl/protocol/jss.h>
|
|
|
|
namespace ripple {
|
|
|
|
class Vault_test : public beast::unit_test::suite
|
|
{
|
|
using PrettyAsset = ripple::test::jtx::PrettyAsset;
|
|
using PrettyAmount = ripple::test::jtx::PrettyAmount;
|
|
|
|
static auto constexpr negativeAmount =
|
|
[](PrettyAsset const& asset) -> PrettyAmount {
|
|
return {STAmount{asset.raw(), 1ul, 0, true, STAmount::unchecked{}}, ""};
|
|
};
|
|
|
|
void
|
|
testSequences()
|
|
{
|
|
using namespace test::jtx;
|
|
|
|
auto const testSequence = [this](
|
|
std::string const& prefix,
|
|
Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Account const& depositor,
|
|
Account const& charlie,
|
|
Vault& vault,
|
|
PrettyAsset const& asset) {
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
tx[sfData] = "AFEED00E";
|
|
tx[sfAssetsMaximum] = asset(100).number();
|
|
env(tx);
|
|
env.close();
|
|
BEAST_EXPECT(env.le(keylet));
|
|
|
|
auto const share = [&env, keylet = keylet, this]() -> PrettyAsset {
|
|
auto const vault = env.le(keylet);
|
|
BEAST_EXPECT(vault != nullptr);
|
|
return MPTIssue(vault->at(sfShareMPTID));
|
|
}();
|
|
|
|
// Several 3rd party accounts which cannot receive funds
|
|
Account alice{"alice"};
|
|
Account dave{"dave"};
|
|
Account erin{"erin"}; // not authorized by issuer
|
|
env.fund(XRP(1000), alice, dave, erin);
|
|
env(fset(alice, asfDepositAuth));
|
|
env(fset(dave, asfRequireDest));
|
|
env.close();
|
|
|
|
{
|
|
testcase(prefix + " fail to deposit more than assets held");
|
|
auto tx = vault.deposit(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(10000)});
|
|
env(tx, ter(tecINSUFFICIENT_FUNDS));
|
|
}
|
|
|
|
{
|
|
testcase(prefix + " deposit non-zero amount");
|
|
auto tx = vault.deposit(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(50)});
|
|
env(tx);
|
|
}
|
|
|
|
{
|
|
testcase(prefix + " deposit non-zero amount again");
|
|
auto tx = vault.deposit(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(50)});
|
|
env(tx);
|
|
}
|
|
|
|
{
|
|
testcase(prefix + " fail to delete non-empty vault");
|
|
auto tx = vault.del({.owner = owner, .id = keylet.key});
|
|
env(tx, ter(tecHAS_OBLIGATIONS));
|
|
}
|
|
|
|
{
|
|
testcase(prefix + " fail to update because wrong owner");
|
|
auto tx = vault.set({.owner = issuer, .id = keylet.key});
|
|
tx[sfAssetsMaximum] = asset(50).number();
|
|
env(tx, ter(tecNO_PERMISSION));
|
|
}
|
|
|
|
{
|
|
testcase(
|
|
prefix + " fail to set maximum lower than current amount");
|
|
auto tx = vault.set({.owner = owner, .id = keylet.key});
|
|
tx[sfAssetsMaximum] = asset(50).number();
|
|
env(tx, ter(tecLIMIT_EXCEEDED));
|
|
}
|
|
|
|
{
|
|
testcase(prefix + " set maximum higher than current amount");
|
|
auto tx = vault.set({.owner = owner, .id = keylet.key});
|
|
tx[sfAssetsMaximum] = asset(150).number();
|
|
env(tx);
|
|
}
|
|
|
|
{
|
|
testcase(prefix + " set data");
|
|
auto tx = vault.set({.owner = owner, .id = keylet.key});
|
|
tx[sfData] = "0";
|
|
env(tx);
|
|
}
|
|
|
|
{
|
|
testcase(prefix + " fail to set domain on public vault");
|
|
auto tx = vault.set({.owner = owner, .id = keylet.key});
|
|
tx[sfDomainID] = to_string(base_uint<256>(42ul));
|
|
env(tx, ter{tecNO_PERMISSION});
|
|
}
|
|
|
|
{
|
|
testcase(prefix + " fail to deposit more than maximum");
|
|
auto tx = vault.deposit(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(100)});
|
|
env(tx, ter(tecLIMIT_EXCEEDED));
|
|
}
|
|
|
|
{
|
|
testcase(prefix + " reset maximum to zero i.e. not enforced");
|
|
auto tx = vault.set({.owner = owner, .id = keylet.key});
|
|
tx[sfAssetsMaximum] = asset(0).number();
|
|
env(tx);
|
|
}
|
|
|
|
{
|
|
testcase(prefix + " fail to withdraw more than assets held");
|
|
auto tx = vault.withdraw(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(1000)});
|
|
env(tx, ter(tecINSUFFICIENT_FUNDS));
|
|
}
|
|
|
|
{
|
|
testcase(prefix + " deposit some more");
|
|
auto tx = vault.deposit(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(100)});
|
|
env(tx);
|
|
}
|
|
|
|
{
|
|
testcase(prefix + " clawback some");
|
|
auto code =
|
|
asset.raw().native() ? ter(temMALFORMED) : ter(tesSUCCESS);
|
|
auto tx = vault.clawback(
|
|
{.issuer = issuer,
|
|
.id = keylet.key,
|
|
.holder = depositor,
|
|
.amount = asset(10)});
|
|
env(tx, code);
|
|
}
|
|
|
|
{
|
|
testcase(prefix + " clawback all");
|
|
auto code = asset.raw().native() ? ter(tecNO_PERMISSION)
|
|
: ter(tesSUCCESS);
|
|
auto tx = vault.clawback(
|
|
{.issuer = issuer, .id = keylet.key, .holder = depositor});
|
|
env(tx, code);
|
|
}
|
|
|
|
if (!asset.raw().native())
|
|
{
|
|
testcase(prefix + " deposit again");
|
|
auto tx = vault.deposit(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(200)});
|
|
env(tx);
|
|
}
|
|
|
|
{
|
|
testcase(
|
|
prefix + " fail to withdraw to 3rd party lsfDepositAuth");
|
|
auto tx = vault.withdraw(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(100)});
|
|
tx[sfDestination] = alice.human();
|
|
env(tx, ter{tecNO_PERMISSION});
|
|
}
|
|
|
|
{
|
|
testcase(prefix + " fail to withdraw to zero destination");
|
|
auto tx = vault.withdraw(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(1000)});
|
|
tx[sfDestination] = "0";
|
|
env(tx, ter(temMALFORMED));
|
|
}
|
|
|
|
{
|
|
testcase(
|
|
prefix +
|
|
" fail to withdraw with tag but without destination");
|
|
auto tx = vault.withdraw(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(1000)});
|
|
tx[sfDestinationTag] = "0";
|
|
env(tx, ter(temMALFORMED));
|
|
}
|
|
|
|
if (!asset.raw().native())
|
|
{
|
|
testcase(
|
|
prefix + " fail to withdraw to 3rd party no authorization");
|
|
auto tx = vault.withdraw(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(100)});
|
|
tx[sfDestination] = erin.human();
|
|
env(tx,
|
|
ter{asset.raw().holds<Issue>() ? tecNO_LINE : tecNO_AUTH});
|
|
}
|
|
|
|
if (!asset.raw().native() && asset.raw().holds<Issue>())
|
|
{
|
|
testcase(prefix + " temporary authorization for 3rd party");
|
|
env(trust(erin, asset(1000)));
|
|
env(trust(issuer, asset(0), erin, tfSetfAuth));
|
|
env(pay(issuer, erin, asset(10)));
|
|
|
|
// Erin deposits all in vault, then sends shares to depositor
|
|
auto tx = vault.deposit(
|
|
{.depositor = erin, .id = keylet.key, .amount = asset(10)});
|
|
env(tx);
|
|
env(pay(erin, depositor, share(10)));
|
|
|
|
testcase(prefix + " withdraw to authorized 3rd party");
|
|
// Depositor withdraws shares, destined to Erin
|
|
tx = vault.withdraw(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(10)});
|
|
tx[sfDestination] = erin.human();
|
|
env(tx);
|
|
// Erin returns assets to issuer
|
|
env(pay(erin, issuer, asset(10)));
|
|
|
|
testcase(prefix + " fail to pay to unauthorized 3rd party");
|
|
env(trust(erin, asset(0)));
|
|
// Erin has MPToken but is no longer authorized to hold assets
|
|
env(pay(depositor, erin, share(1)), ter{tecNO_LINE});
|
|
}
|
|
|
|
{
|
|
testcase(
|
|
prefix +
|
|
" fail to withdraw to 3rd party lsfRequireDestTag");
|
|
auto tx = vault.withdraw(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(100)});
|
|
tx[sfDestination] = dave.human();
|
|
env(tx, ter{tecDST_TAG_NEEDED});
|
|
}
|
|
|
|
{
|
|
testcase(prefix + " withdraw to authorized 3rd party");
|
|
auto tx = vault.withdraw(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(100)});
|
|
tx[sfDestination] = charlie.human();
|
|
env(tx);
|
|
}
|
|
|
|
{
|
|
testcase(prefix + " withdraw to issuer");
|
|
auto tx = vault.withdraw(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(50)});
|
|
tx[sfDestination] = issuer.human();
|
|
env(tx);
|
|
}
|
|
|
|
{
|
|
testcase(prefix + " withdraw remaining assets");
|
|
auto tx = vault.withdraw(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(50)});
|
|
env(tx);
|
|
}
|
|
|
|
{
|
|
testcase(prefix + " fail to delete because wrong owner");
|
|
auto tx = vault.del({.owner = issuer, .id = keylet.key});
|
|
env(tx, ter(tecNO_PERMISSION));
|
|
}
|
|
|
|
{
|
|
testcase(prefix + " delete empty vault");
|
|
auto tx = vault.del({.owner = owner, .id = keylet.key});
|
|
env(tx);
|
|
BEAST_EXPECT(!env.le(keylet));
|
|
}
|
|
};
|
|
|
|
auto testCases = [this, &testSequence](
|
|
std::string prefix,
|
|
std::function<PrettyAsset(
|
|
Env & env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Account const& depositor,
|
|
Account const& charlie)> setup) {
|
|
Env env{*this, testable_amendments() | featureSingleAssetVault};
|
|
Account issuer{"issuer"};
|
|
Account owner{"owner"};
|
|
Account depositor{"depositor"};
|
|
Account charlie{"charlie"}; // authorized 3rd party
|
|
Vault vault{env};
|
|
env.fund(XRP(1000), issuer, owner, depositor, charlie);
|
|
env.close();
|
|
env(fset(issuer, asfAllowTrustLineClawback));
|
|
env(fset(issuer, asfRequireAuth));
|
|
env.close();
|
|
env.require(flags(issuer, asfAllowTrustLineClawback));
|
|
env.require(flags(issuer, asfRequireAuth));
|
|
|
|
PrettyAsset asset = setup(env, issuer, owner, depositor, charlie);
|
|
testSequence(
|
|
prefix, env, issuer, owner, depositor, charlie, vault, asset);
|
|
};
|
|
|
|
testCases(
|
|
"XRP",
|
|
[](Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Account const& depositor,
|
|
Account const& charlie) -> PrettyAsset {
|
|
return {xrpIssue(), 1'000'000};
|
|
});
|
|
|
|
testCases(
|
|
"IOU",
|
|
[](Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Account const& depositor,
|
|
Account const& charlie) -> Asset {
|
|
PrettyAsset asset = issuer["IOU"];
|
|
env(trust(owner, asset(1000)));
|
|
env(trust(depositor, asset(1000)));
|
|
env(trust(charlie, asset(1000)));
|
|
env(trust(issuer, asset(0), owner, tfSetfAuth));
|
|
env(trust(issuer, asset(0), depositor, tfSetfAuth));
|
|
env(trust(issuer, asset(0), charlie, tfSetfAuth));
|
|
env(pay(issuer, depositor, asset(1000)));
|
|
env.close();
|
|
return asset;
|
|
});
|
|
|
|
testCases(
|
|
"MPT",
|
|
[](Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Account const& depositor,
|
|
Account const& charlie) -> Asset {
|
|
MPTTester mptt{env, issuer, mptInitNoFund};
|
|
mptt.create(
|
|
{.flags =
|
|
tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
|
|
PrettyAsset asset = mptt.issuanceID();
|
|
mptt.authorize({.account = depositor});
|
|
mptt.authorize({.account = charlie});
|
|
env(pay(issuer, depositor, asset(1000)));
|
|
env.close();
|
|
return asset;
|
|
});
|
|
}
|
|
|
|
void
|
|
testPreflight()
|
|
{
|
|
using namespace test::jtx;
|
|
|
|
struct CaseArgs
|
|
{
|
|
FeatureBitset features =
|
|
testable_amendments() | featureSingleAssetVault;
|
|
};
|
|
|
|
auto testCase = [&, this](
|
|
std::function<void(
|
|
Env & env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Asset const& asset,
|
|
Vault& vault)> test,
|
|
CaseArgs args = {}) {
|
|
Env env{*this, args.features};
|
|
Account issuer{"issuer"};
|
|
Account owner{"owner"};
|
|
Vault vault{env};
|
|
env.fund(XRP(1000), issuer, owner);
|
|
env.close();
|
|
|
|
env(fset(issuer, asfAllowTrustLineClawback));
|
|
env(fset(issuer, asfRequireAuth));
|
|
env.close();
|
|
|
|
PrettyAsset asset = issuer["IOU"];
|
|
env(trust(owner, asset(1000)));
|
|
env(trust(issuer, asset(0), owner, tfSetfAuth));
|
|
env(pay(issuer, owner, asset(1000)));
|
|
env.close();
|
|
|
|
test(env, issuer, owner, asset, vault);
|
|
};
|
|
|
|
testCase(
|
|
[&](Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Asset const& asset,
|
|
Vault& vault) {
|
|
testcase("disabled single asset vault");
|
|
|
|
auto [tx, keylet] =
|
|
vault.create({.owner = owner, .asset = asset});
|
|
env(tx, ter{temDISABLED});
|
|
|
|
{
|
|
auto tx = vault.set({.owner = owner, .id = keylet.key});
|
|
env(tx, ter{temDISABLED});
|
|
}
|
|
|
|
{
|
|
auto tx = vault.deposit(
|
|
{.depositor = owner,
|
|
.id = keylet.key,
|
|
.amount = asset(10)});
|
|
env(tx, ter{temDISABLED});
|
|
}
|
|
|
|
{
|
|
auto tx = vault.withdraw(
|
|
{.depositor = owner,
|
|
.id = keylet.key,
|
|
.amount = asset(10)});
|
|
env(tx, ter{temDISABLED});
|
|
}
|
|
|
|
{
|
|
auto tx = vault.clawback(
|
|
{.issuer = issuer,
|
|
.id = keylet.key,
|
|
.holder = owner,
|
|
.amount = asset(10)});
|
|
env(tx, ter{temDISABLED});
|
|
}
|
|
|
|
{
|
|
auto tx = vault.del({.owner = owner, .id = keylet.key});
|
|
env(tx, ter{temDISABLED});
|
|
}
|
|
},
|
|
{.features = testable_amendments() - featureSingleAssetVault});
|
|
|
|
testCase([&](Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Asset const& asset,
|
|
Vault& vault) {
|
|
testcase("invalid flags");
|
|
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
tx[sfFlags] = tfClearDeepFreeze;
|
|
env(tx, ter{temINVALID_FLAG});
|
|
|
|
{
|
|
auto tx = vault.set({.owner = owner, .id = keylet.key});
|
|
tx[sfFlags] = tfClearDeepFreeze;
|
|
env(tx, ter{temINVALID_FLAG});
|
|
}
|
|
|
|
{
|
|
auto tx = vault.deposit(
|
|
{.depositor = owner,
|
|
.id = keylet.key,
|
|
.amount = asset(10)});
|
|
tx[sfFlags] = tfClearDeepFreeze;
|
|
env(tx, ter{temINVALID_FLAG});
|
|
}
|
|
|
|
{
|
|
auto tx = vault.withdraw(
|
|
{.depositor = owner,
|
|
.id = keylet.key,
|
|
.amount = asset(10)});
|
|
tx[sfFlags] = tfClearDeepFreeze;
|
|
env(tx, ter{temINVALID_FLAG});
|
|
}
|
|
|
|
{
|
|
auto tx = vault.clawback(
|
|
{.issuer = issuer,
|
|
.id = keylet.key,
|
|
.holder = owner,
|
|
.amount = asset(10)});
|
|
tx[sfFlags] = tfClearDeepFreeze;
|
|
env(tx, ter{temINVALID_FLAG});
|
|
}
|
|
|
|
{
|
|
auto tx = vault.del({.owner = owner, .id = keylet.key});
|
|
tx[sfFlags] = tfClearDeepFreeze;
|
|
env(tx, ter{temINVALID_FLAG});
|
|
}
|
|
});
|
|
|
|
testCase([&](Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Asset const& asset,
|
|
Vault& vault) {
|
|
testcase("invalid fee");
|
|
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
tx[jss::Fee] = "-1";
|
|
env(tx, ter{temBAD_FEE});
|
|
|
|
{
|
|
auto tx = vault.set({.owner = owner, .id = keylet.key});
|
|
tx[jss::Fee] = "-1";
|
|
env(tx, ter{temBAD_FEE});
|
|
}
|
|
|
|
{
|
|
auto tx = vault.deposit(
|
|
{.depositor = owner,
|
|
.id = keylet.key,
|
|
.amount = asset(10)});
|
|
tx[jss::Fee] = "-1";
|
|
env(tx, ter{temBAD_FEE});
|
|
}
|
|
|
|
{
|
|
auto tx = vault.withdraw(
|
|
{.depositor = owner,
|
|
.id = keylet.key,
|
|
.amount = asset(10)});
|
|
tx[jss::Fee] = "-1";
|
|
env(tx, ter{temBAD_FEE});
|
|
}
|
|
|
|
{
|
|
auto tx = vault.clawback(
|
|
{.issuer = issuer,
|
|
.id = keylet.key,
|
|
.holder = owner,
|
|
.amount = asset(10)});
|
|
tx[jss::Fee] = "-1";
|
|
env(tx, ter{temBAD_FEE});
|
|
}
|
|
|
|
{
|
|
auto tx = vault.del({.owner = owner, .id = keylet.key});
|
|
tx[jss::Fee] = "-1";
|
|
env(tx, ter{temBAD_FEE});
|
|
}
|
|
});
|
|
|
|
testCase(
|
|
[&](Env& env,
|
|
Account const&,
|
|
Account const& owner,
|
|
Asset const&,
|
|
Vault& vault) {
|
|
testcase("disabled permissioned domain");
|
|
|
|
auto [tx, keylet] =
|
|
vault.create({.owner = owner, .asset = xrpIssue()});
|
|
tx[sfDomainID] = to_string(base_uint<256>(42ul));
|
|
env(tx, ter{temDISABLED});
|
|
|
|
{
|
|
auto tx = vault.set({.owner = owner, .id = keylet.key});
|
|
tx[sfDomainID] = to_string(base_uint<256>(42ul));
|
|
env(tx, ter{temDISABLED});
|
|
}
|
|
|
|
{
|
|
auto tx = vault.set({.owner = owner, .id = keylet.key});
|
|
tx[sfDomainID] = "0";
|
|
env(tx, ter{temDISABLED});
|
|
}
|
|
},
|
|
{.features = (testable_amendments() | featureSingleAssetVault) -
|
|
featurePermissionedDomains});
|
|
|
|
testCase([&](Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Asset const& asset,
|
|
Vault& vault) {
|
|
testcase("use zero vault");
|
|
|
|
auto [tx, keylet] =
|
|
vault.create({.owner = owner, .asset = xrpIssue()});
|
|
|
|
{
|
|
auto tx = vault.set({
|
|
.owner = owner,
|
|
.id = beast::zero,
|
|
});
|
|
env(tx, ter{temMALFORMED});
|
|
}
|
|
|
|
{
|
|
auto tx = vault.deposit(
|
|
{.depositor = owner,
|
|
.id = beast::zero,
|
|
.amount = asset(10)});
|
|
env(tx, ter(temMALFORMED));
|
|
}
|
|
|
|
{
|
|
auto tx = vault.withdraw(
|
|
{.depositor = owner,
|
|
.id = beast::zero,
|
|
.amount = asset(10)});
|
|
env(tx, ter{temMALFORMED});
|
|
}
|
|
|
|
{
|
|
auto tx = vault.clawback(
|
|
{.issuer = issuer,
|
|
.id = beast::zero,
|
|
.holder = owner,
|
|
.amount = asset(10)});
|
|
env(tx, ter{temMALFORMED});
|
|
}
|
|
|
|
{
|
|
auto tx = vault.del({
|
|
.owner = owner,
|
|
.id = beast::zero,
|
|
});
|
|
env(tx, ter{temMALFORMED});
|
|
}
|
|
});
|
|
|
|
testCase([&](Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Asset const& asset,
|
|
Vault& vault) {
|
|
testcase("clawback from self");
|
|
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
|
|
{
|
|
auto tx = vault.clawback(
|
|
{.issuer = issuer,
|
|
.id = keylet.key,
|
|
.holder = issuer,
|
|
.amount = asset(10)});
|
|
env(tx, ter{temMALFORMED});
|
|
}
|
|
});
|
|
|
|
testCase([&](Env& env,
|
|
Account const&,
|
|
Account const& owner,
|
|
Asset const& asset,
|
|
Vault& vault) {
|
|
testcase("withdraw to bad destination");
|
|
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
|
|
{
|
|
auto tx = vault.withdraw(
|
|
{.depositor = owner,
|
|
.id = keylet.key,
|
|
.amount = asset(10)});
|
|
tx[jss::Destination] = "0";
|
|
env(tx, ter{temMALFORMED});
|
|
}
|
|
});
|
|
|
|
testCase([&](Env& env,
|
|
Account const&,
|
|
Account const& owner,
|
|
Asset const& asset,
|
|
Vault& vault) {
|
|
testcase("create or set invalid data");
|
|
|
|
auto [tx1, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
|
|
{
|
|
auto tx = tx1;
|
|
tx[sfData] = "";
|
|
env(tx, ter(temMALFORMED));
|
|
}
|
|
|
|
{
|
|
auto tx = tx1;
|
|
// A hexadecimal string of 257 bytes.
|
|
tx[sfData] = std::string(514, 'A');
|
|
env(tx, ter(temMALFORMED));
|
|
}
|
|
|
|
{
|
|
auto tx = vault.set({.owner = owner, .id = keylet.key});
|
|
tx[sfData] = "";
|
|
env(tx, ter{temMALFORMED});
|
|
}
|
|
|
|
{
|
|
auto tx = vault.set({.owner = owner, .id = keylet.key});
|
|
// A hexadecimal string of 257 bytes.
|
|
tx[sfData] = std::string(514, 'A');
|
|
env(tx, ter{temMALFORMED});
|
|
}
|
|
});
|
|
|
|
testCase([&](Env& env,
|
|
Account const&,
|
|
Account const& owner,
|
|
Asset const& asset,
|
|
Vault& vault) {
|
|
testcase("set nothing updated");
|
|
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
|
|
{
|
|
auto tx = vault.set({.owner = owner, .id = keylet.key});
|
|
env(tx, ter{temMALFORMED});
|
|
}
|
|
});
|
|
|
|
testCase([&](Env& env,
|
|
Account const&,
|
|
Account const& owner,
|
|
Asset const& asset,
|
|
Vault& vault) {
|
|
testcase("create with invalid metadata");
|
|
|
|
auto [tx1, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
|
|
{
|
|
auto tx = tx1;
|
|
tx[sfMPTokenMetadata] = "";
|
|
env(tx, ter(temMALFORMED));
|
|
}
|
|
|
|
{
|
|
auto tx = tx1;
|
|
// This metadata is for the share token.
|
|
// A hexadecimal string of 1025 bytes.
|
|
tx[sfMPTokenMetadata] = std::string(2050, 'B');
|
|
env(tx, ter(temMALFORMED));
|
|
}
|
|
});
|
|
|
|
testCase([&](Env& env,
|
|
Account const&,
|
|
Account const& owner,
|
|
Asset const& asset,
|
|
Vault& vault) {
|
|
testcase("set negative maximum");
|
|
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
|
|
{
|
|
auto tx = vault.set({.owner = owner, .id = keylet.key});
|
|
tx[sfAssetsMaximum] = negativeAmount(asset).number();
|
|
env(tx, ter{temMALFORMED});
|
|
}
|
|
});
|
|
|
|
testCase([&](Env& env,
|
|
Account const&,
|
|
Account const& owner,
|
|
Asset const& asset,
|
|
Vault& vault) {
|
|
testcase("invalid deposit amount");
|
|
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
|
|
{
|
|
auto tx = vault.deposit(
|
|
{.depositor = owner,
|
|
.id = keylet.key,
|
|
.amount = negativeAmount(asset)});
|
|
env(tx, ter(temBAD_AMOUNT));
|
|
}
|
|
|
|
{
|
|
auto tx = vault.deposit(
|
|
{.depositor = owner, .id = keylet.key, .amount = asset(0)});
|
|
env(tx, ter(temBAD_AMOUNT));
|
|
}
|
|
});
|
|
|
|
testCase([&](Env& env,
|
|
Account const&,
|
|
Account const& owner,
|
|
Asset const& asset,
|
|
Vault& vault) {
|
|
testcase("invalid set immutable flag");
|
|
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
|
|
{
|
|
auto tx = vault.set({.owner = owner, .id = keylet.key});
|
|
tx[sfFlags] = tfVaultPrivate;
|
|
env(tx, ter(temINVALID_FLAG));
|
|
}
|
|
});
|
|
|
|
testCase([&](Env& env,
|
|
Account const&,
|
|
Account const& owner,
|
|
Asset const& asset,
|
|
Vault& vault) {
|
|
testcase("invalid withdraw amount");
|
|
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
|
|
{
|
|
auto tx = vault.withdraw(
|
|
{.depositor = owner,
|
|
.id = keylet.key,
|
|
.amount = negativeAmount(asset)});
|
|
env(tx, ter(temBAD_AMOUNT));
|
|
}
|
|
|
|
{
|
|
auto tx = vault.withdraw(
|
|
{.depositor = owner, .id = keylet.key, .amount = asset(0)});
|
|
env(tx, ter(temBAD_AMOUNT));
|
|
}
|
|
});
|
|
|
|
testCase([&](Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Asset const& asset,
|
|
Vault& vault) {
|
|
testcase("invalid clawback");
|
|
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
|
|
{
|
|
auto tx = vault.clawback(
|
|
{.issuer = owner,
|
|
.id = keylet.key,
|
|
.holder = issuer,
|
|
.amount = asset(50)});
|
|
env(tx, ter(temMALFORMED));
|
|
}
|
|
|
|
{
|
|
auto tx = vault.clawback(
|
|
{.issuer = issuer,
|
|
.id = keylet.key,
|
|
.holder = owner,
|
|
.amount = negativeAmount(asset)});
|
|
env(tx, ter(temBAD_AMOUNT));
|
|
}
|
|
});
|
|
|
|
testCase([&](Env& env,
|
|
Account const&,
|
|
Account const& owner,
|
|
Asset const& asset,
|
|
Vault& vault) {
|
|
testcase("invalid create");
|
|
|
|
auto [tx1, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
|
|
{
|
|
auto tx = tx1;
|
|
tx[sfWithdrawalPolicy] = 0;
|
|
env(tx, ter(temMALFORMED));
|
|
}
|
|
|
|
{
|
|
auto tx = tx1;
|
|
tx[sfDomainID] = to_string(base_uint<256>(42ul));
|
|
env(tx, ter{temMALFORMED});
|
|
}
|
|
|
|
{
|
|
auto tx = tx1;
|
|
tx[sfAssetsMaximum] = negativeAmount(asset).number();
|
|
env(tx, ter{temMALFORMED});
|
|
}
|
|
|
|
{
|
|
auto tx = tx1;
|
|
tx[sfFlags] = tfVaultPrivate;
|
|
tx[sfDomainID] = "0";
|
|
env(tx, ter{temMALFORMED});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Test for non-asset specific behaviors.
|
|
void
|
|
testCreateFailXRP()
|
|
{
|
|
using namespace test::jtx;
|
|
|
|
auto testCase = [this](std::function<void(
|
|
Env & env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Account const& depositor,
|
|
Asset const& asset,
|
|
Vault& vault)> test) {
|
|
Env env{*this, testable_amendments() | featureSingleAssetVault};
|
|
Account issuer{"issuer"};
|
|
Account owner{"owner"};
|
|
Account depositor{"depositor"};
|
|
env.fund(XRP(1000), issuer, owner, depositor);
|
|
env.close();
|
|
Vault vault{env};
|
|
Asset asset = xrpIssue();
|
|
|
|
test(env, issuer, owner, depositor, asset, vault);
|
|
};
|
|
|
|
testCase([this](
|
|
Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Account const& depositor,
|
|
PrettyAsset const& asset,
|
|
Vault& vault) {
|
|
testcase("nothing to set");
|
|
auto tx = vault.set({.owner = owner, .id = keylet::skip().key});
|
|
tx[sfAssetsMaximum] = asset(0).number();
|
|
env(tx, ter(tecNO_ENTRY));
|
|
});
|
|
|
|
testCase([this](
|
|
Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Account const& depositor,
|
|
PrettyAsset const& asset,
|
|
Vault& vault) {
|
|
testcase("nothing to deposit to");
|
|
auto tx = vault.deposit(
|
|
{.depositor = depositor,
|
|
.id = keylet::skip().key,
|
|
.amount = asset(10)});
|
|
env(tx, ter(tecNO_ENTRY));
|
|
});
|
|
|
|
testCase([this](
|
|
Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Account const& depositor,
|
|
PrettyAsset const& asset,
|
|
Vault& vault) {
|
|
testcase("nothing to withdraw from");
|
|
auto tx = vault.withdraw(
|
|
{.depositor = depositor,
|
|
.id = keylet::skip().key,
|
|
.amount = asset(10)});
|
|
env(tx, ter(tecNO_ENTRY));
|
|
});
|
|
|
|
testCase([this](
|
|
Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Account const& depositor,
|
|
Asset const& asset,
|
|
Vault& vault) {
|
|
testcase("nothing to delete");
|
|
auto tx = vault.del({.owner = owner, .id = keylet::skip().key});
|
|
env(tx, ter(tecNO_ENTRY));
|
|
});
|
|
|
|
testCase([this](
|
|
Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Account const& depositor,
|
|
Asset const& asset,
|
|
Vault& vault) {
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
testcase("transaction is good");
|
|
env(tx);
|
|
});
|
|
|
|
testCase([this](
|
|
Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Account const& depositor,
|
|
Asset const& asset,
|
|
Vault& vault) {
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
tx[sfWithdrawalPolicy] = 1;
|
|
testcase("explicitly select withdrawal policy");
|
|
env(tx);
|
|
});
|
|
|
|
testCase([this](
|
|
Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Account const& depositor,
|
|
Asset const& asset,
|
|
Vault& vault) {
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
testcase("insufficient fee");
|
|
env(tx, fee(env.current()->fees().base), ter(telINSUF_FEE_P));
|
|
});
|
|
|
|
testCase([this](
|
|
Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Account const& depositor,
|
|
Asset const& asset,
|
|
Vault& vault) {
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
testcase("insufficient reserve");
|
|
// It is possible to construct a complicated mathematical
|
|
// expression for this amount, but it is sadly not easy.
|
|
env(pay(owner, issuer, XRP(775)));
|
|
env.close();
|
|
env(tx, ter(tecINSUFFICIENT_RESERVE));
|
|
});
|
|
|
|
testCase([this](
|
|
Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Account const& depositor,
|
|
Asset const& asset,
|
|
Vault& vault) {
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
tx[sfFlags] = tfVaultPrivate;
|
|
tx[sfDomainID] = to_string(base_uint<256>(42ul));
|
|
testcase("non-existing domain");
|
|
env(tx, ter{tecOBJECT_NOT_FOUND});
|
|
});
|
|
}
|
|
|
|
void
|
|
testCreateFailIOU()
|
|
{
|
|
using namespace test::jtx;
|
|
{
|
|
{
|
|
testcase("IOU fail create frozen");
|
|
Env env{*this, testable_amendments() | featureSingleAssetVault};
|
|
Account issuer{"issuer"};
|
|
Account owner{"owner"};
|
|
env.fund(XRP(1000), issuer, owner);
|
|
env.close();
|
|
env(fset(issuer, asfGlobalFreeze));
|
|
env.close();
|
|
|
|
Vault vault{env};
|
|
Asset asset = issuer["IOU"];
|
|
auto [tx, keylet] =
|
|
vault.create({.owner = owner, .asset = asset});
|
|
|
|
env(tx, ter(tecFROZEN));
|
|
env.close();
|
|
}
|
|
|
|
{
|
|
testcase("IOU fail create no ripling");
|
|
Env env{*this, testable_amendments() | featureSingleAssetVault};
|
|
Account issuer{"issuer"};
|
|
Account owner{"owner"};
|
|
env.fund(XRP(1000), issuer, owner);
|
|
env.close();
|
|
env(fclear(issuer, asfDefaultRipple));
|
|
env.close();
|
|
|
|
Vault vault{env};
|
|
Asset asset = issuer["IOU"];
|
|
auto [tx, keylet] =
|
|
vault.create({.owner = owner, .asset = asset});
|
|
env(tx, ter(terNO_RIPPLE));
|
|
env.close();
|
|
}
|
|
|
|
{
|
|
testcase("IOU no issuer");
|
|
Env env{*this, testable_amendments() | featureSingleAssetVault};
|
|
Account issuer{"issuer"};
|
|
Account owner{"owner"};
|
|
env.fund(XRP(1000), owner);
|
|
env.close();
|
|
|
|
Vault vault{env};
|
|
Asset asset = issuer["IOU"];
|
|
{
|
|
auto [tx, keylet] =
|
|
vault.create({.owner = owner, .asset = asset});
|
|
env(tx, ter(terNO_ACCOUNT));
|
|
env.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
testcase("IOU fail create vault for AMM LPToken");
|
|
Env env{*this, testable_amendments() | featureSingleAssetVault};
|
|
Account const gw("gateway");
|
|
Account const alice("alice");
|
|
Account const carol("carol");
|
|
IOU const USD = gw["USD"];
|
|
|
|
auto const [asset1, asset2] =
|
|
std::pair<STAmount, STAmount>(XRP(10000), USD(10000));
|
|
auto tofund = [&](STAmount const& a) -> STAmount {
|
|
if (a.native())
|
|
{
|
|
auto const defXRP = XRP(30000);
|
|
if (a <= defXRP)
|
|
return defXRP;
|
|
return a + XRP(1000);
|
|
}
|
|
auto const defIOU = STAmount{a.issue(), 30000};
|
|
if (a <= defIOU)
|
|
return defIOU;
|
|
return a + STAmount{a.issue(), 1000};
|
|
};
|
|
auto const toFund1 = tofund(asset1);
|
|
auto const toFund2 = tofund(asset2);
|
|
BEAST_EXPECT(asset1 <= toFund1 && asset2 <= toFund2);
|
|
|
|
if (!asset1.native() && !asset2.native())
|
|
fund(env, gw, {alice, carol}, {toFund1, toFund2}, Fund::All);
|
|
else if (asset1.native())
|
|
fund(env, gw, {alice, carol}, toFund1, {toFund2}, Fund::All);
|
|
else if (asset2.native())
|
|
fund(env, gw, {alice, carol}, toFund2, {toFund1}, Fund::All);
|
|
|
|
AMM ammAlice(
|
|
env, alice, asset1, asset2, CreateArg{.log = false, .tfee = 0});
|
|
|
|
Account const owner{"owner"};
|
|
env.fund(XRP(1000000), owner);
|
|
|
|
Vault vault{env};
|
|
auto [tx, k] =
|
|
vault.create({.owner = owner, .asset = ammAlice.lptIssue()});
|
|
env(tx, ter{tecWRONG_ASSET});
|
|
env.close();
|
|
}
|
|
}
|
|
|
|
void
|
|
testCreateFailMPT()
|
|
{
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, testable_amendments() | featureSingleAssetVault};
|
|
Account issuer{"issuer"};
|
|
Account owner{"owner"};
|
|
Account depositor{"depositor"};
|
|
env.fund(XRP(1000), issuer, owner, depositor);
|
|
env.close();
|
|
Vault vault{env};
|
|
|
|
MPTTester mptt{env, issuer, mptInitNoFund};
|
|
|
|
// Locked because that is the default flag.
|
|
mptt.create();
|
|
Asset asset = mptt.issuanceID();
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
env(tx, ter(tecNO_AUTH));
|
|
}
|
|
|
|
void
|
|
testNonTransferableShares()
|
|
{
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, testable_amendments() | featureSingleAssetVault};
|
|
Account issuer{"issuer"};
|
|
Account owner{"owner"};
|
|
Account depositor{"depositor"};
|
|
env.fund(XRP(1000), issuer, owner, depositor);
|
|
env.close();
|
|
|
|
Vault vault{env};
|
|
PrettyAsset asset = issuer["IOU"];
|
|
env.trust(asset(1000), owner);
|
|
env(pay(issuer, owner, asset(100)));
|
|
env.trust(asset(1000), depositor);
|
|
env(pay(issuer, depositor, asset(100)));
|
|
env.close();
|
|
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
tx[sfFlags] = tfVaultShareNonTransferable;
|
|
env(tx);
|
|
env.close();
|
|
|
|
{
|
|
testcase("nontransferable deposits");
|
|
auto tx1 = vault.deposit(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(40)});
|
|
env(tx1);
|
|
|
|
auto tx2 = vault.deposit(
|
|
{.depositor = owner, .id = keylet.key, .amount = asset(60)});
|
|
env(tx2);
|
|
env.close();
|
|
}
|
|
|
|
auto const vaultAccount = //
|
|
[&env, key = keylet.key, this]() -> AccountID {
|
|
auto jvVault = env.rpc("vault_info", strHex(key));
|
|
|
|
BEAST_EXPECT(
|
|
jvVault[jss::result][jss::vault][sfAssetsTotal] == "100");
|
|
BEAST_EXPECT(
|
|
jvVault[jss::result][jss::vault][jss::shares]
|
|
[sfOutstandingAmount] == "100");
|
|
|
|
// Vault pseudo-account
|
|
return parseBase58<AccountID>(
|
|
jvVault[jss::result][jss::vault][jss::Account]
|
|
.asString())
|
|
.value();
|
|
}();
|
|
|
|
auto const MptID = makeMptID(1, vaultAccount);
|
|
Asset shares = MptID;
|
|
|
|
{
|
|
testcase("nontransferable shares cannot be moved");
|
|
env(pay(owner, depositor, shares(10)), ter{tecNO_AUTH});
|
|
env(pay(depositor, owner, shares(10)), ter{tecNO_AUTH});
|
|
}
|
|
|
|
{
|
|
testcase("nontransferable shares can be used to withdraw");
|
|
auto tx1 = vault.withdraw(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(20)});
|
|
env(tx1);
|
|
|
|
auto tx2 = vault.withdraw(
|
|
{.depositor = owner, .id = keylet.key, .amount = asset(30)});
|
|
env(tx2);
|
|
env.close();
|
|
}
|
|
|
|
{
|
|
testcase("nontransferable shares balance check");
|
|
auto jvVault = env.rpc("vault_info", strHex(keylet.key));
|
|
BEAST_EXPECT(
|
|
jvVault[jss::result][jss::vault][sfAssetsTotal] == "50");
|
|
BEAST_EXPECT(
|
|
jvVault[jss::result][jss::vault][jss::shares]
|
|
[sfOutstandingAmount] == "50");
|
|
}
|
|
|
|
{
|
|
testcase("nontransferable shares withdraw rest");
|
|
auto tx1 = vault.withdraw(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(20)});
|
|
env(tx1);
|
|
|
|
auto tx2 = vault.withdraw(
|
|
{.depositor = owner, .id = keylet.key, .amount = asset(30)});
|
|
env(tx2);
|
|
env.close();
|
|
}
|
|
|
|
{
|
|
testcase("nontransferable shares delete empty vault");
|
|
auto tx = vault.del({.owner = owner, .id = keylet.key});
|
|
env(tx);
|
|
BEAST_EXPECT(!env.le(keylet));
|
|
}
|
|
}
|
|
|
|
void
|
|
testWithMPT()
|
|
{
|
|
using namespace test::jtx;
|
|
|
|
struct CaseArgs
|
|
{
|
|
bool enableClawback = true;
|
|
bool requireAuth = true;
|
|
};
|
|
|
|
auto testCase = [this](
|
|
std::function<void(
|
|
Env & env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Account const& depositor,
|
|
Asset const& asset,
|
|
Vault& vault,
|
|
MPTTester& mptt)> test,
|
|
CaseArgs args = {}) {
|
|
Env env{*this, testable_amendments() | featureSingleAssetVault};
|
|
Account issuer{"issuer"};
|
|
Account owner{"owner"};
|
|
Account depositor{"depositor"};
|
|
env.fund(XRP(1000), issuer, owner, depositor);
|
|
env.close();
|
|
Vault vault{env};
|
|
|
|
MPTTester mptt{env, issuer, mptInitNoFund};
|
|
auto const none = LedgerSpecificFlags(0);
|
|
mptt.create(
|
|
{.flags = tfMPTCanTransfer | tfMPTCanLock |
|
|
(args.enableClawback ? tfMPTCanClawback : none) |
|
|
(args.requireAuth ? tfMPTRequireAuth : none)});
|
|
PrettyAsset asset = mptt.issuanceID();
|
|
mptt.authorize({.account = owner});
|
|
mptt.authorize({.account = depositor});
|
|
if (args.requireAuth)
|
|
{
|
|
mptt.authorize({.account = issuer, .holder = owner});
|
|
mptt.authorize({.account = issuer, .holder = depositor});
|
|
}
|
|
|
|
env(pay(issuer, depositor, asset(1000)));
|
|
env.close();
|
|
|
|
test(env, issuer, owner, depositor, asset, vault, mptt);
|
|
};
|
|
|
|
testCase([this](
|
|
Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Account const& depositor,
|
|
PrettyAsset const& asset,
|
|
Vault& vault,
|
|
MPTTester& mptt) {
|
|
testcase("MPT nothing to clawback from");
|
|
auto tx = vault.clawback(
|
|
{.issuer = issuer,
|
|
.id = keylet::skip().key,
|
|
.holder = depositor,
|
|
.amount = asset(10)});
|
|
env(tx, ter(tecNO_ENTRY));
|
|
});
|
|
|
|
testCase([this](
|
|
Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Account const& depositor,
|
|
Asset const& asset,
|
|
Vault& vault,
|
|
MPTTester& mptt) {
|
|
testcase("MPT global lock blocks create");
|
|
mptt.set({.account = issuer, .flags = tfMPTLock});
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
env(tx, ter(tecLOCKED));
|
|
});
|
|
|
|
testCase([this](
|
|
Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Account const& depositor,
|
|
Asset const& asset,
|
|
Vault& vault,
|
|
MPTTester& mptt) {
|
|
testcase("MPT global lock blocks deposit");
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
env(tx);
|
|
env.close();
|
|
|
|
mptt.set({.account = issuer, .flags = tfMPTLock});
|
|
env.close();
|
|
|
|
tx = vault.deposit(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(100)});
|
|
env(tx, ter{tecLOCKED});
|
|
env.close();
|
|
|
|
// Can delete empty vault, even if global lock
|
|
tx = vault.del({.owner = owner, .id = keylet.key});
|
|
env(tx);
|
|
});
|
|
|
|
testCase([this](
|
|
Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Account const& depositor,
|
|
Asset const& asset,
|
|
Vault& vault,
|
|
MPTTester& mptt) {
|
|
testcase("MPT global lock blocks withdrawal");
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
env(tx);
|
|
env.close();
|
|
tx = vault.deposit(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(100)});
|
|
env(tx);
|
|
env.close();
|
|
|
|
// Check that the OutstandingAmount field of MPTIssuance
|
|
// accounts for the issued shares.
|
|
auto v = env.le(keylet);
|
|
BEAST_EXPECT(v);
|
|
MPTID share = (*v)[sfShareMPTID];
|
|
auto issuance = env.le(keylet::mptIssuance(share));
|
|
BEAST_EXPECT(issuance);
|
|
Number outstandingShares = issuance->at(sfOutstandingAmount);
|
|
BEAST_EXPECT(outstandingShares == 100);
|
|
|
|
mptt.set({.account = issuer, .flags = tfMPTLock});
|
|
env.close();
|
|
|
|
tx = vault.withdraw(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(100)});
|
|
env(tx, ter(tecLOCKED));
|
|
|
|
tx[sfDestination] = issuer.human();
|
|
env(tx, ter(tecLOCKED));
|
|
|
|
// Clawback is still permitted, even with global lock
|
|
tx = vault.clawback(
|
|
{.issuer = issuer,
|
|
.id = keylet.key,
|
|
.holder = depositor,
|
|
.amount = asset(0)});
|
|
env(tx);
|
|
env.close();
|
|
|
|
// Can delete empty vault, even if global lock
|
|
tx = vault.del({.owner = owner, .id = keylet.key});
|
|
env(tx);
|
|
});
|
|
|
|
testCase([this](
|
|
Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Account const& depositor,
|
|
PrettyAsset const& asset,
|
|
Vault& vault,
|
|
MPTTester& mptt) {
|
|
testcase("MPT only issuer can clawback");
|
|
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
env(tx);
|
|
env.close();
|
|
|
|
tx = vault.deposit(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(100)});
|
|
env(tx);
|
|
env.close();
|
|
|
|
{
|
|
auto tx = vault.clawback(
|
|
{.issuer = owner, .id = keylet.key, .holder = depositor});
|
|
env(tx, ter(tecNO_PERMISSION));
|
|
}
|
|
});
|
|
|
|
testCase(
|
|
[this](
|
|
Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Account const& depositor,
|
|
PrettyAsset const& asset,
|
|
Vault& vault,
|
|
MPTTester& mptt) {
|
|
testcase(
|
|
"MPT 3rd party without MPToken cannot be withdrawal "
|
|
"destination");
|
|
|
|
auto [tx, keylet] =
|
|
vault.create({.owner = owner, .asset = asset});
|
|
env(tx);
|
|
env.close();
|
|
|
|
tx = vault.deposit(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(100)});
|
|
env(tx);
|
|
env.close();
|
|
|
|
{
|
|
// Set destination to 3rd party without MPToken
|
|
Account charlie{"charlie"};
|
|
env.fund(XRP(1000), charlie);
|
|
env.close();
|
|
|
|
tx = vault.withdraw(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(100)});
|
|
tx[sfDestination] = charlie.human();
|
|
env(tx, ter(tecNO_AUTH));
|
|
}
|
|
},
|
|
{.requireAuth = false});
|
|
|
|
testCase(
|
|
[this](
|
|
Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Account const& depositor,
|
|
PrettyAsset const& asset,
|
|
Vault& vault,
|
|
MPTTester& mptt) {
|
|
testcase("MPT depositor without MPToken cannot withdraw");
|
|
|
|
auto [tx, keylet] =
|
|
vault.create({.owner = owner, .asset = asset});
|
|
env(tx);
|
|
env.close();
|
|
|
|
tx = vault.deposit(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(1000)});
|
|
env(tx);
|
|
env.close();
|
|
|
|
{
|
|
// Remove depositor's MPToken and withdraw will fail
|
|
mptt.authorize(
|
|
{.account = depositor, .flags = tfMPTUnauthorize});
|
|
env.close();
|
|
auto const mptoken =
|
|
env.le(keylet::mptoken(mptt.issuanceID(), depositor));
|
|
BEAST_EXPECT(mptoken == nullptr);
|
|
|
|
tx = vault.withdraw(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(100)});
|
|
env(tx, ter(tecNO_AUTH));
|
|
}
|
|
|
|
{
|
|
// Restore depositor's MPToken and withdraw will succeed
|
|
mptt.authorize({.account = depositor});
|
|
env.close();
|
|
|
|
tx = vault.withdraw(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(100)});
|
|
env(tx);
|
|
}
|
|
},
|
|
{.requireAuth = false});
|
|
|
|
testCase([this](
|
|
Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Account const& depositor,
|
|
PrettyAsset const& asset,
|
|
Vault& vault,
|
|
MPTTester& mptt) {
|
|
testcase("MPT issuance deleted");
|
|
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
env(tx);
|
|
env.close();
|
|
|
|
tx = vault.deposit(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(1000)});
|
|
env(tx);
|
|
env.close();
|
|
|
|
{
|
|
auto tx = vault.clawback(
|
|
{.issuer = issuer,
|
|
.id = keylet.key,
|
|
.holder = depositor,
|
|
.amount = asset(0)});
|
|
env(tx);
|
|
}
|
|
|
|
mptt.destroy({.issuer = issuer, .id = mptt.issuanceID()});
|
|
env.close();
|
|
|
|
{
|
|
auto [tx, keylet] =
|
|
vault.create({.owner = depositor, .asset = asset});
|
|
env(tx, ter{tecOBJECT_NOT_FOUND});
|
|
}
|
|
|
|
{
|
|
auto tx = vault.deposit(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(10)});
|
|
env(tx, ter{tecOBJECT_NOT_FOUND});
|
|
}
|
|
|
|
{
|
|
auto tx = vault.withdraw(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(10)});
|
|
env(tx, ter{tecOBJECT_NOT_FOUND});
|
|
}
|
|
|
|
{
|
|
auto tx = vault.clawback(
|
|
{.issuer = issuer,
|
|
.id = keylet.key,
|
|
.holder = depositor,
|
|
.amount = asset(0)});
|
|
env(tx, ter{tecOBJECT_NOT_FOUND});
|
|
}
|
|
|
|
env(vault.del({.owner = owner, .id = keylet.key}));
|
|
});
|
|
|
|
testCase(
|
|
[this](
|
|
Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Account const& depositor,
|
|
PrettyAsset const& asset,
|
|
Vault& vault,
|
|
MPTTester& mptt) {
|
|
testcase("MPT clawback disabled");
|
|
|
|
auto [tx, keylet] =
|
|
vault.create({.owner = owner, .asset = asset});
|
|
env(tx);
|
|
env.close();
|
|
|
|
tx = vault.deposit(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(1000)});
|
|
env(tx);
|
|
env.close();
|
|
|
|
{
|
|
auto tx = vault.clawback(
|
|
{.issuer = issuer,
|
|
.id = keylet.key,
|
|
.holder = depositor,
|
|
.amount = asset(0)});
|
|
env(tx, ter{tecNO_PERMISSION});
|
|
}
|
|
},
|
|
{.enableClawback = false});
|
|
|
|
testCase([this](
|
|
Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Account const& depositor,
|
|
Asset const& asset,
|
|
Vault& vault,
|
|
MPTTester& mptt) {
|
|
testcase("MPT un-authorization");
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
env(tx);
|
|
env.close();
|
|
tx = vault.deposit(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(1000)});
|
|
env(tx);
|
|
env.close();
|
|
|
|
mptt.authorize(
|
|
{.account = issuer,
|
|
.holder = depositor,
|
|
.flags = tfMPTUnauthorize});
|
|
env.close();
|
|
|
|
{
|
|
auto tx = vault.withdraw(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(100)});
|
|
env(tx, ter(tecNO_AUTH));
|
|
|
|
// Withdrawal to other (authorized) accounts works
|
|
tx[sfDestination] = issuer.human();
|
|
env(tx);
|
|
tx[sfDestination] = owner.human();
|
|
env(tx);
|
|
env.close();
|
|
}
|
|
|
|
{
|
|
// Cannot deposit some more
|
|
auto tx = vault.deposit(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(100)});
|
|
env(tx, ter(tecNO_AUTH));
|
|
}
|
|
|
|
// Clawback works
|
|
tx = vault.clawback(
|
|
{.issuer = issuer,
|
|
.id = keylet.key,
|
|
.holder = depositor,
|
|
.amount = asset(800)});
|
|
env(tx);
|
|
|
|
env(vault.del({.owner = owner, .id = keylet.key}));
|
|
});
|
|
|
|
testCase([this](
|
|
Env& env,
|
|
Account const& issuer,
|
|
Account const& owner,
|
|
Account const& depositor,
|
|
Asset const& asset,
|
|
Vault& vault,
|
|
MPTTester& mptt) {
|
|
testcase("MPT lock of vault pseudo-account");
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
env(tx);
|
|
env.close();
|
|
|
|
auto const vaultAccount =
|
|
[&env, keylet = keylet, this]() -> AccountID {
|
|
auto const vault = env.le(keylet);
|
|
BEAST_EXPECT(vault != nullptr);
|
|
return vault->at(sfAccount);
|
|
}();
|
|
|
|
tx = vault.deposit(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(100)});
|
|
env(tx);
|
|
env.close();
|
|
|
|
tx = [&]() {
|
|
Json::Value jv;
|
|
jv[jss::Account] = issuer.human();
|
|
jv[sfMPTokenIssuanceID] =
|
|
to_string(asset.get<MPTIssue>().getMptID());
|
|
jv[jss::Holder] = toBase58(vaultAccount);
|
|
jv[jss::TransactionType] = jss::MPTokenIssuanceSet;
|
|
jv[jss::Flags] = tfMPTLock;
|
|
return jv;
|
|
}();
|
|
env(tx);
|
|
env.close();
|
|
|
|
tx = vault.deposit(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(100)});
|
|
env(tx, ter(tecLOCKED));
|
|
|
|
tx = vault.withdraw(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(100)});
|
|
env(tx, ter(tecLOCKED));
|
|
|
|
// Clawback works, even when locked
|
|
tx = vault.clawback(
|
|
{.issuer = issuer,
|
|
.id = keylet.key,
|
|
.holder = depositor,
|
|
.amount = asset(100)});
|
|
env(tx);
|
|
|
|
// Can delete an empty vault even when asset is locked.
|
|
tx = vault.del({.owner = owner, .id = keylet.key});
|
|
env(tx);
|
|
});
|
|
|
|
{
|
|
testcase("MPT shares to a vault");
|
|
|
|
Env env{*this, testable_amendments() | featureSingleAssetVault};
|
|
Account owner{"owner"};
|
|
Account issuer{"issuer"};
|
|
env.fund(XRP(1000000), owner, issuer);
|
|
env.close();
|
|
Vault vault{env};
|
|
|
|
MPTTester mptt{env, issuer, mptInitNoFund};
|
|
mptt.create(
|
|
{.flags = tfMPTCanTransfer | tfMPTCanLock | lsfMPTCanClawback |
|
|
tfMPTRequireAuth});
|
|
mptt.authorize({.account = owner});
|
|
mptt.authorize({.account = issuer, .holder = owner});
|
|
PrettyAsset asset = mptt.issuanceID();
|
|
env(pay(issuer, owner, asset(100)));
|
|
auto [tx1, k1] = vault.create({.owner = owner, .asset = asset});
|
|
env(tx1);
|
|
env.close();
|
|
|
|
auto const shares = [&env, keylet = k1, this]() -> Asset {
|
|
auto const vault = env.le(keylet);
|
|
BEAST_EXPECT(vault != nullptr);
|
|
return MPTIssue(vault->at(sfShareMPTID));
|
|
}();
|
|
|
|
auto [tx2, k2] = vault.create({.owner = owner, .asset = shares});
|
|
env(tx2, ter{tecWRONG_ASSET});
|
|
env.close();
|
|
}
|
|
}
|
|
|
|
void
|
|
testWithIOU()
|
|
{
|
|
using namespace test::jtx;
|
|
|
|
auto testCase =
|
|
[&, this](
|
|
std::function<void(
|
|
Env & env,
|
|
Account const& owner,
|
|
Account const& issuer,
|
|
Account const& charlie,
|
|
std::function<AccountID(ripple::Keylet)> vaultAccount,
|
|
Vault& vault,
|
|
PrettyAsset const& asset,
|
|
std::function<MPTID(ripple::Keylet)> issuanceId,
|
|
std::function<PrettyAmount(ripple::Keylet)> vaultBalance)>
|
|
test) {
|
|
Env env{*this, testable_amendments() | featureSingleAssetVault};
|
|
Account const owner{"owner"};
|
|
Account const issuer{"issuer"};
|
|
Account const charlie{"charlie"};
|
|
Vault vault{env};
|
|
env.fund(XRP(1000), issuer, owner, charlie);
|
|
env(fset(issuer, asfAllowTrustLineClawback));
|
|
env.close();
|
|
|
|
PrettyAsset const asset = issuer["IOU"];
|
|
env.trust(asset(1000), owner);
|
|
env.trust(asset(1000), charlie);
|
|
env(pay(issuer, owner, asset(200)));
|
|
env(rate(issuer, 1.25));
|
|
env.close();
|
|
|
|
auto const [tx, keylet] =
|
|
vault.create({.owner = owner, .asset = asset});
|
|
env(tx);
|
|
env.close();
|
|
|
|
auto const vaultAccount =
|
|
[&env](ripple::Keylet keylet) -> AccountID {
|
|
return env.le(keylet)->at(sfAccount);
|
|
};
|
|
auto const issuanceId = [&env](ripple::Keylet keylet) -> MPTID {
|
|
return env.le(keylet)->at(sfShareMPTID);
|
|
};
|
|
auto const vaultBalance = //
|
|
[&env, &vaultAccount, issue = asset.raw().get<Issue>()](
|
|
ripple::Keylet keylet) -> PrettyAmount {
|
|
auto const account = vaultAccount(keylet);
|
|
auto const sle = env.le(keylet::line(account, issue));
|
|
if (sle == nullptr)
|
|
return {
|
|
STAmount(issue, 0),
|
|
env.lookup(issue.account).name()};
|
|
auto amount = sle->getFieldAmount(sfBalance);
|
|
amount.setIssuer(issue.account);
|
|
if (account > issue.account)
|
|
amount.negate();
|
|
return {amount, env.lookup(issue.account).name()};
|
|
};
|
|
|
|
test(
|
|
env,
|
|
owner,
|
|
issuer,
|
|
charlie,
|
|
vaultAccount,
|
|
vault,
|
|
asset,
|
|
issuanceId,
|
|
vaultBalance);
|
|
};
|
|
|
|
testCase([&, this](
|
|
Env& env,
|
|
Account const& owner,
|
|
Account const& issuer,
|
|
Account const&,
|
|
auto vaultAccount,
|
|
Vault& vault,
|
|
PrettyAsset const& asset,
|
|
auto&&...) {
|
|
testcase("IOU cannot use different asset");
|
|
PrettyAsset const foo = issuer["FOO"];
|
|
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
env(tx);
|
|
env.close();
|
|
|
|
{
|
|
// Cannot create new trustline to a vault
|
|
auto tx = [&, account = vaultAccount(keylet)]() {
|
|
Json::Value jv;
|
|
jv[jss::Account] = issuer.human();
|
|
{
|
|
auto& ja = jv[jss::LimitAmount] =
|
|
foo(0).value().getJson(JsonOptions::none);
|
|
ja[jss::issuer] = toBase58(account);
|
|
}
|
|
jv[jss::TransactionType] = jss::TrustSet;
|
|
jv[jss::Flags] = tfSetFreeze;
|
|
return jv;
|
|
}();
|
|
env(tx, ter{tecNO_PERMISSION});
|
|
env.close();
|
|
}
|
|
|
|
{
|
|
auto tx = vault.deposit(
|
|
{.depositor = issuer, .id = keylet.key, .amount = foo(20)});
|
|
env(tx, ter{tecWRONG_ASSET});
|
|
env.close();
|
|
}
|
|
|
|
{
|
|
auto tx = vault.withdraw(
|
|
{.depositor = issuer, .id = keylet.key, .amount = foo(20)});
|
|
env(tx, ter{tecWRONG_ASSET});
|
|
env.close();
|
|
}
|
|
|
|
env(vault.del({.owner = owner, .id = keylet.key}));
|
|
env.close();
|
|
});
|
|
|
|
testCase([&, this](
|
|
Env& env,
|
|
Account const& owner,
|
|
Account const& issuer,
|
|
Account const& charlie,
|
|
auto vaultAccount,
|
|
Vault& vault,
|
|
PrettyAsset const& asset,
|
|
auto issuanceId,
|
|
auto) {
|
|
testcase("IOU frozen trust line to vault account");
|
|
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
env(tx);
|
|
env.close();
|
|
|
|
env(vault.deposit(
|
|
{.depositor = owner, .id = keylet.key, .amount = asset(100)}));
|
|
env.close();
|
|
|
|
Asset const share = Asset(issuanceId(keylet));
|
|
|
|
// Freeze the trustline to the vault
|
|
auto trustSet = [&, account = vaultAccount(keylet)]() {
|
|
Json::Value jv;
|
|
jv[jss::Account] = issuer.human();
|
|
{
|
|
auto& ja = jv[jss::LimitAmount] =
|
|
asset(0).value().getJson(JsonOptions::none);
|
|
ja[jss::issuer] = toBase58(account);
|
|
}
|
|
jv[jss::TransactionType] = jss::TrustSet;
|
|
jv[jss::Flags] = tfSetFreeze;
|
|
return jv;
|
|
}();
|
|
env(trustSet);
|
|
env.close();
|
|
|
|
{
|
|
// Note, the "frozen" state of the trust line to vault account
|
|
// is reported as "locked" state of the vault shares, because
|
|
// this state is attached to shares by means of the transitive
|
|
// isFrozen.
|
|
auto tx = vault.deposit(
|
|
{.depositor = owner,
|
|
.id = keylet.key,
|
|
.amount = asset(80)});
|
|
env(tx, ter{tecLOCKED});
|
|
}
|
|
|
|
{
|
|
auto tx = vault.withdraw(
|
|
{.depositor = owner,
|
|
.id = keylet.key,
|
|
.amount = asset(100)});
|
|
env(tx, ter{tecLOCKED});
|
|
|
|
// also when trying to withdraw to a 3rd party
|
|
tx[sfDestination] = charlie.human();
|
|
env(tx, ter{tecLOCKED});
|
|
env.close();
|
|
}
|
|
|
|
{
|
|
// Clawback works, even when locked
|
|
auto tx = vault.clawback(
|
|
{.issuer = issuer,
|
|
.id = keylet.key,
|
|
.holder = owner,
|
|
.amount = asset(50)});
|
|
env(tx);
|
|
env.close();
|
|
}
|
|
|
|
// Clear the frozen state
|
|
trustSet[jss::Flags] = tfClearFreeze;
|
|
env(trustSet);
|
|
env.close();
|
|
|
|
env(vault.withdraw(
|
|
{.depositor = owner, .id = keylet.key, .amount = share(50)}));
|
|
|
|
env(vault.del({.owner = owner, .id = keylet.key}));
|
|
env.close();
|
|
});
|
|
|
|
testCase([&, this](
|
|
Env& env,
|
|
Account const& owner,
|
|
Account const& issuer,
|
|
Account const& charlie,
|
|
auto,
|
|
Vault& vault,
|
|
PrettyAsset const& asset,
|
|
auto issuanceId,
|
|
auto vaultBalance) {
|
|
testcase("IOU transfer fees not applied");
|
|
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
env(tx);
|
|
env.close();
|
|
|
|
env(vault.deposit(
|
|
{.depositor = owner, .id = keylet.key, .amount = asset(100)}));
|
|
env.close();
|
|
|
|
auto const issue = asset.raw().get<Issue>();
|
|
Asset const share = Asset(issuanceId(keylet));
|
|
|
|
// transfer fees ignored on deposit
|
|
BEAST_EXPECT(env.balance(owner, issue) == asset(100));
|
|
BEAST_EXPECT(vaultBalance(keylet) == asset(100));
|
|
|
|
{
|
|
auto tx = vault.clawback(
|
|
{.issuer = issuer,
|
|
.id = keylet.key,
|
|
.holder = owner,
|
|
.amount = asset(50)});
|
|
env(tx);
|
|
env.close();
|
|
}
|
|
|
|
// transfer fees ignored on clawback
|
|
BEAST_EXPECT(env.balance(owner, issue) == asset(100));
|
|
BEAST_EXPECT(vaultBalance(keylet) == asset(50));
|
|
|
|
env(vault.withdraw(
|
|
{.depositor = owner, .id = keylet.key, .amount = share(20)}));
|
|
|
|
// transfer fees ignored on withdraw
|
|
BEAST_EXPECT(env.balance(owner, issue) == asset(120));
|
|
BEAST_EXPECT(vaultBalance(keylet) == asset(30));
|
|
|
|
{
|
|
auto tx = vault.withdraw(
|
|
{.depositor = owner,
|
|
.id = keylet.key,
|
|
.amount = share(30)});
|
|
tx[sfDestination] = charlie.human();
|
|
env(tx);
|
|
}
|
|
|
|
// transfer fees ignored on withdraw to 3rd party
|
|
BEAST_EXPECT(env.balance(owner, issue) == asset(120));
|
|
BEAST_EXPECT(env.balance(charlie, issue) == asset(30));
|
|
BEAST_EXPECT(vaultBalance(keylet) == asset(0));
|
|
|
|
env(vault.del({.owner = owner, .id = keylet.key}));
|
|
env.close();
|
|
});
|
|
|
|
testCase([&, this](
|
|
Env& env,
|
|
Account const& owner,
|
|
Account const& issuer,
|
|
Account const& charlie,
|
|
auto,
|
|
Vault& vault,
|
|
PrettyAsset const& asset,
|
|
auto&&...) {
|
|
testcase("IOU frozen trust line to depositor");
|
|
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
env(tx);
|
|
env.close();
|
|
|
|
env(vault.deposit(
|
|
{.depositor = owner, .id = keylet.key, .amount = asset(100)}));
|
|
env.close();
|
|
|
|
// Withdraw to 3rd party works
|
|
auto const withdrawToCharlie = [&](ripple::Keylet keylet) {
|
|
auto tx = vault.withdraw(
|
|
{.depositor = owner,
|
|
.id = keylet.key,
|
|
.amount = asset(10)});
|
|
tx[sfDestination] = charlie.human();
|
|
return tx;
|
|
}(keylet);
|
|
env(withdrawToCharlie);
|
|
|
|
// Freeze the owner
|
|
env(trust(issuer, asset(0), owner, tfSetFreeze));
|
|
env.close();
|
|
|
|
// Cannot withdraw
|
|
auto const withdraw = vault.withdraw(
|
|
{.depositor = owner, .id = keylet.key, .amount = asset(10)});
|
|
env(withdraw, ter{tecFROZEN});
|
|
|
|
// Cannot withdraw to 3rd party
|
|
env(withdrawToCharlie, ter{tecLOCKED});
|
|
env.close();
|
|
|
|
{
|
|
// Cannot deposit some more
|
|
auto tx = vault.deposit(
|
|
{.depositor = owner,
|
|
.id = keylet.key,
|
|
.amount = asset(10)});
|
|
env(tx, ter{tecFROZEN});
|
|
}
|
|
|
|
{
|
|
// Clawback still works
|
|
auto tx = vault.clawback(
|
|
{.issuer = issuer,
|
|
.id = keylet.key,
|
|
.holder = owner,
|
|
.amount = asset(0)});
|
|
env(tx);
|
|
env.close();
|
|
}
|
|
|
|
env(vault.del({.owner = owner, .id = keylet.key}));
|
|
env.close();
|
|
});
|
|
|
|
testCase([&, this](
|
|
Env& env,
|
|
Account const& owner,
|
|
Account const& issuer,
|
|
Account const& charlie,
|
|
auto,
|
|
Vault& vault,
|
|
PrettyAsset const& asset,
|
|
auto&&...) {
|
|
testcase("IOU no trust line to 3rd party");
|
|
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
env(tx);
|
|
env.close();
|
|
|
|
env(vault.deposit(
|
|
{.depositor = owner, .id = keylet.key, .amount = asset(100)}));
|
|
env.close();
|
|
|
|
Account const erin{"erin"};
|
|
env.fund(XRP(1000), erin);
|
|
env.close();
|
|
|
|
// Withdraw to 3rd party without trust line
|
|
auto const tx1 = [&](ripple::Keylet keylet) {
|
|
auto tx = vault.withdraw(
|
|
{.depositor = owner,
|
|
.id = keylet.key,
|
|
.amount = asset(10)});
|
|
tx[sfDestination] = erin.human();
|
|
return tx;
|
|
}(keylet);
|
|
env(tx1, ter{tecNO_LINE});
|
|
});
|
|
|
|
testCase([&, this](
|
|
Env& env,
|
|
Account const& owner,
|
|
Account const& issuer,
|
|
Account const& charlie,
|
|
auto,
|
|
Vault& vault,
|
|
PrettyAsset const& asset,
|
|
auto&&...) {
|
|
testcase("IOU no trust line to depositor");
|
|
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
env(tx);
|
|
env.close();
|
|
|
|
// reset limit, so deposit of all funds will delete the trust line
|
|
env.trust(asset(0), owner);
|
|
env.close();
|
|
|
|
env(vault.deposit(
|
|
{.depositor = owner, .id = keylet.key, .amount = asset(200)}));
|
|
env.close();
|
|
|
|
auto trustline =
|
|
env.le(keylet::line(owner, asset.raw().get<Issue>()));
|
|
BEAST_EXPECT(trustline == nullptr);
|
|
|
|
// Withdraw without trust line, will succeed
|
|
auto const tx1 = [&](ripple::Keylet keylet) {
|
|
auto tx = vault.withdraw(
|
|
{.depositor = owner,
|
|
.id = keylet.key,
|
|
.amount = asset(10)});
|
|
return tx;
|
|
}(keylet);
|
|
env(tx1);
|
|
});
|
|
|
|
testCase([&, this](
|
|
Env& env,
|
|
Account const& owner,
|
|
Account const& issuer,
|
|
Account const& charlie,
|
|
auto,
|
|
Vault& vault,
|
|
PrettyAsset const& asset,
|
|
auto&&...) {
|
|
testcase("IOU frozen trust line to 3rd party");
|
|
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
env(tx);
|
|
env.close();
|
|
|
|
env(vault.deposit(
|
|
{.depositor = owner, .id = keylet.key, .amount = asset(100)}));
|
|
env.close();
|
|
|
|
// Withdraw to 3rd party works
|
|
auto const withdrawToCharlie = [&](ripple::Keylet keylet) {
|
|
auto tx = vault.withdraw(
|
|
{.depositor = owner,
|
|
.id = keylet.key,
|
|
.amount = asset(10)});
|
|
tx[sfDestination] = charlie.human();
|
|
return tx;
|
|
}(keylet);
|
|
env(withdrawToCharlie);
|
|
|
|
// Freeze the 3rd party
|
|
env(trust(issuer, asset(0), charlie, tfSetFreeze));
|
|
env.close();
|
|
|
|
// Can withdraw
|
|
auto const withdraw = vault.withdraw(
|
|
{.depositor = owner, .id = keylet.key, .amount = asset(10)});
|
|
env(withdraw);
|
|
env.close();
|
|
|
|
// Cannot withdraw to 3rd party
|
|
env(withdrawToCharlie, ter{tecFROZEN});
|
|
env.close();
|
|
|
|
env(vault.clawback(
|
|
{.issuer = issuer,
|
|
.id = keylet.key,
|
|
.holder = owner,
|
|
.amount = asset(0)}));
|
|
env.close();
|
|
|
|
env(vault.del({.owner = owner, .id = keylet.key}));
|
|
env.close();
|
|
});
|
|
|
|
testCase([&, this](
|
|
Env& env,
|
|
Account const& owner,
|
|
Account const& issuer,
|
|
Account const& charlie,
|
|
auto,
|
|
Vault& vault,
|
|
PrettyAsset const& asset,
|
|
auto&&...) {
|
|
testcase("IOU global freeze");
|
|
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
env(tx);
|
|
env.close();
|
|
|
|
env(vault.deposit(
|
|
{.depositor = owner, .id = keylet.key, .amount = asset(100)}));
|
|
env.close();
|
|
|
|
env(fset(issuer, asfGlobalFreeze));
|
|
env.close();
|
|
|
|
{
|
|
// Cannot withdraw
|
|
auto tx = vault.withdraw(
|
|
{.depositor = owner,
|
|
.id = keylet.key,
|
|
.amount = asset(10)});
|
|
env(tx, ter{tecFROZEN});
|
|
|
|
// Cannot withdraw to 3rd party
|
|
tx[sfDestination] = charlie.human();
|
|
env(tx, ter{tecFROZEN});
|
|
env.close();
|
|
|
|
// Cannot deposit some more
|
|
tx = vault.deposit(
|
|
{.depositor = owner,
|
|
.id = keylet.key,
|
|
.amount = asset(10)});
|
|
|
|
env(tx, ter{tecFROZEN});
|
|
}
|
|
|
|
// Clawback is permitted
|
|
env(vault.clawback(
|
|
{.issuer = issuer,
|
|
.id = keylet.key,
|
|
.holder = owner,
|
|
.amount = asset(0)}));
|
|
env.close();
|
|
|
|
env(vault.del({.owner = owner, .id = keylet.key}));
|
|
env.close();
|
|
});
|
|
}
|
|
|
|
void
|
|
testWithDomainCheck()
|
|
{
|
|
using namespace test::jtx;
|
|
|
|
testcase("private vault");
|
|
|
|
Env env{*this, testable_amendments() | featureSingleAssetVault};
|
|
Account issuer{"issuer"};
|
|
Account owner{"owner"};
|
|
Account depositor{"depositor"};
|
|
Account charlie{"charlie"};
|
|
Account pdOwner{"pdOwner"};
|
|
Account credIssuer1{"credIssuer1"};
|
|
Account credIssuer2{"credIssuer2"};
|
|
std::string const credType = "credential";
|
|
Vault vault{env};
|
|
env.fund(
|
|
XRP(1000),
|
|
issuer,
|
|
owner,
|
|
depositor,
|
|
charlie,
|
|
pdOwner,
|
|
credIssuer1,
|
|
credIssuer2);
|
|
env.close();
|
|
env(fset(issuer, asfAllowTrustLineClawback));
|
|
env.close();
|
|
env.require(flags(issuer, asfAllowTrustLineClawback));
|
|
|
|
PrettyAsset asset = issuer["IOU"];
|
|
env.trust(asset(1000), owner);
|
|
env(pay(issuer, owner, asset(500)));
|
|
env.trust(asset(1000), depositor);
|
|
env(pay(issuer, depositor, asset(500)));
|
|
env.trust(asset(1000), charlie);
|
|
env(pay(issuer, charlie, asset(5)));
|
|
env.close();
|
|
|
|
auto [tx, keylet] = vault.create(
|
|
{.owner = owner, .asset = asset, .flags = tfVaultPrivate});
|
|
env(tx);
|
|
env.close();
|
|
BEAST_EXPECT(env.le(keylet));
|
|
|
|
{
|
|
testcase("private vault owner can deposit");
|
|
auto tx = vault.deposit(
|
|
{.depositor = owner, .id = keylet.key, .amount = asset(50)});
|
|
env(tx);
|
|
}
|
|
|
|
{
|
|
testcase("private vault depositor not authorized yet");
|
|
auto tx = vault.deposit(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(50)});
|
|
env(tx, ter{tecNO_AUTH});
|
|
}
|
|
|
|
{
|
|
testcase("private vault cannot set non-existing domain");
|
|
auto tx = vault.set({.owner = owner, .id = keylet.key});
|
|
tx[sfDomainID] = to_string(base_uint<256>(42ul));
|
|
env(tx, ter{tecOBJECT_NOT_FOUND});
|
|
}
|
|
|
|
{
|
|
testcase("private vault set domainId");
|
|
|
|
{
|
|
pdomain::Credentials const credentials1{
|
|
{.issuer = credIssuer1, .credType = credType}};
|
|
|
|
env(pdomain::setTx(pdOwner, credentials1));
|
|
auto const domainId1 = [&]() {
|
|
auto tx = env.tx()->getJson(JsonOptions::none);
|
|
return pdomain::getNewDomain(env.meta());
|
|
}();
|
|
|
|
auto tx = vault.set({.owner = owner, .id = keylet.key});
|
|
tx[sfDomainID] = to_string(domainId1);
|
|
env(tx);
|
|
env.close();
|
|
|
|
// Update domain second time, should be harmless
|
|
env(tx);
|
|
env.close();
|
|
}
|
|
|
|
{
|
|
pdomain::Credentials const credentials{
|
|
{.issuer = credIssuer1, .credType = credType},
|
|
{.issuer = credIssuer2, .credType = credType}};
|
|
|
|
env(pdomain::setTx(pdOwner, credentials));
|
|
auto const domainId = [&]() {
|
|
auto tx = env.tx()->getJson(JsonOptions::none);
|
|
return pdomain::getNewDomain(env.meta());
|
|
}();
|
|
|
|
auto tx = vault.set({.owner = owner, .id = keylet.key});
|
|
tx[sfDomainID] = to_string(domainId);
|
|
env(tx);
|
|
env.close();
|
|
}
|
|
}
|
|
|
|
{
|
|
testcase("private vault depositor still not authorized");
|
|
auto tx = vault.deposit(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(50)});
|
|
env(tx, ter{tecNO_AUTH});
|
|
env.close();
|
|
}
|
|
|
|
auto const credKeylet =
|
|
credentials::keylet(depositor, credIssuer1, credType);
|
|
{
|
|
testcase("private vault depositor now authorized");
|
|
env(credentials::create(depositor, credIssuer1, credType));
|
|
env(credentials::accept(depositor, credIssuer1, credType));
|
|
env(credentials::create(charlie, credIssuer1, credType));
|
|
// charlie's credential not accepted
|
|
env.close();
|
|
auto credSle = env.le(credKeylet);
|
|
BEAST_EXPECT(credSle != nullptr);
|
|
|
|
auto tx = vault.deposit(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(50)});
|
|
env(tx);
|
|
env.close();
|
|
|
|
tx = vault.deposit(
|
|
{.depositor = charlie, .id = keylet.key, .amount = asset(50)});
|
|
env(tx, ter{tecNO_AUTH});
|
|
env.close();
|
|
}
|
|
|
|
{
|
|
testcase("private vault depositor lost authorization");
|
|
env(credentials::deleteCred(
|
|
credIssuer1, depositor, credIssuer1, credType));
|
|
env(credentials::deleteCred(
|
|
credIssuer1, charlie, credIssuer1, credType));
|
|
env.close();
|
|
auto credSle = env.le(credKeylet);
|
|
BEAST_EXPECT(credSle == nullptr);
|
|
|
|
auto tx = vault.deposit(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(50)});
|
|
env(tx, ter{tecNO_AUTH});
|
|
env.close();
|
|
}
|
|
|
|
auto const shares = [&env, keylet = keylet, this]() -> Asset {
|
|
auto const vault = env.le(keylet);
|
|
BEAST_EXPECT(vault != nullptr);
|
|
return MPTIssue(vault->at(sfShareMPTID));
|
|
}();
|
|
|
|
{
|
|
testcase("private vault expired authorization");
|
|
uint32_t const closeTime = env.current()
|
|
->info()
|
|
.parentCloseTime.time_since_epoch()
|
|
.count();
|
|
{
|
|
auto tx0 =
|
|
credentials::create(depositor, credIssuer2, credType);
|
|
tx0[sfExpiration] = closeTime + 20;
|
|
env(tx0);
|
|
tx0 = credentials::create(charlie, credIssuer2, credType);
|
|
tx0[sfExpiration] = closeTime + 20;
|
|
env(tx0);
|
|
env.close();
|
|
|
|
env(credentials::accept(depositor, credIssuer2, credType));
|
|
env(credentials::accept(charlie, credIssuer2, credType));
|
|
env.close();
|
|
}
|
|
|
|
{
|
|
auto tx1 = vault.deposit(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(50)});
|
|
env(tx1);
|
|
env.close();
|
|
|
|
auto const tokenKeylet = keylet::mptoken(
|
|
shares.get<MPTIssue>().getMptID(), depositor.id());
|
|
BEAST_EXPECT(env.le(tokenKeylet) != nullptr);
|
|
}
|
|
|
|
{
|
|
// time advance
|
|
env.close();
|
|
env.close();
|
|
env.close();
|
|
|
|
auto const credsKeylet =
|
|
credentials::keylet(depositor, credIssuer2, credType);
|
|
BEAST_EXPECT(env.le(credsKeylet) != nullptr);
|
|
|
|
auto tx2 = vault.deposit(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(1)});
|
|
env(tx2, ter{tecEXPIRED});
|
|
env.close();
|
|
|
|
BEAST_EXPECT(env.le(credsKeylet) == nullptr);
|
|
}
|
|
|
|
{
|
|
auto const credsKeylet =
|
|
credentials::keylet(charlie, credIssuer2, credType);
|
|
BEAST_EXPECT(env.le(credsKeylet) != nullptr);
|
|
auto const tokenKeylet = keylet::mptoken(
|
|
shares.get<MPTIssue>().getMptID(), charlie.id());
|
|
BEAST_EXPECT(env.le(tokenKeylet) == nullptr);
|
|
|
|
auto tx3 = vault.deposit(
|
|
{.depositor = charlie,
|
|
.id = keylet.key,
|
|
.amount = asset(2)});
|
|
env(tx3, ter{tecEXPIRED});
|
|
|
|
env.close();
|
|
BEAST_EXPECT(env.le(credsKeylet) == nullptr);
|
|
BEAST_EXPECT(env.le(tokenKeylet) == nullptr);
|
|
}
|
|
}
|
|
|
|
{
|
|
testcase("private vault reset domainId");
|
|
auto tx = vault.set({.owner = owner, .id = keylet.key});
|
|
tx[sfDomainID] = "0";
|
|
env(tx);
|
|
env.close();
|
|
|
|
tx = vault.deposit(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(50)});
|
|
env(tx, ter{tecNO_AUTH});
|
|
env.close();
|
|
|
|
tx = vault.withdraw(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(50)});
|
|
env(tx);
|
|
|
|
tx = vault.clawback(
|
|
{.issuer = issuer,
|
|
.id = keylet.key,
|
|
.holder = depositor,
|
|
.amount = asset(0)});
|
|
env(tx);
|
|
|
|
tx = vault.clawback(
|
|
{.issuer = issuer,
|
|
.id = keylet.key,
|
|
.holder = owner,
|
|
.amount = asset(0)});
|
|
env(tx);
|
|
|
|
tx = vault.del({
|
|
.owner = owner,
|
|
.id = keylet.key,
|
|
});
|
|
env(tx);
|
|
}
|
|
}
|
|
|
|
void
|
|
testWithDomainCheckXRP()
|
|
{
|
|
using namespace test::jtx;
|
|
|
|
testcase("private XRP vault");
|
|
|
|
Env env{*this, testable_amendments() | featureSingleAssetVault};
|
|
Account owner{"owner"};
|
|
Account depositor{"depositor"};
|
|
Account alice{"charlie"};
|
|
std::string const credType = "credential";
|
|
Vault vault{env};
|
|
env.fund(XRP(100000), owner, depositor, alice);
|
|
env.close();
|
|
|
|
PrettyAsset asset = xrpIssue();
|
|
auto [tx, keylet] = vault.create(
|
|
{.owner = owner, .asset = asset, .flags = tfVaultPrivate});
|
|
env(tx);
|
|
env.close();
|
|
|
|
auto const [vaultAccount, issuanceId] =
|
|
[&env, keylet = keylet, this]() -> std::tuple<AccountID, uint192> {
|
|
auto const vault = env.le(keylet);
|
|
BEAST_EXPECT(vault != nullptr);
|
|
return {vault->at(sfAccount), vault->at(sfShareMPTID)};
|
|
}();
|
|
BEAST_EXPECT(env.le(keylet::account(vaultAccount)));
|
|
BEAST_EXPECT(env.le(keylet::mptIssuance(issuanceId)));
|
|
PrettyAsset shares{issuanceId};
|
|
|
|
{
|
|
testcase("private XRP vault owner can deposit");
|
|
auto tx = vault.deposit(
|
|
{.depositor = owner, .id = keylet.key, .amount = asset(50)});
|
|
env(tx);
|
|
}
|
|
|
|
{
|
|
testcase("private XRP vault cannot pay shares to depositor yet");
|
|
env(pay(owner, depositor, shares(1)), ter{tecNO_AUTH});
|
|
}
|
|
|
|
{
|
|
testcase("private XRP vault depositor not authorized yet");
|
|
auto tx = vault.deposit(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(50)});
|
|
env(tx, ter{tecNO_AUTH});
|
|
}
|
|
|
|
{
|
|
testcase("private XRP vault set DomainID");
|
|
pdomain::Credentials const credentials{
|
|
{.issuer = owner, .credType = credType}};
|
|
|
|
env(pdomain::setTx(owner, credentials));
|
|
auto const domainId = [&]() {
|
|
auto tx = env.tx()->getJson(JsonOptions::none);
|
|
return pdomain::getNewDomain(env.meta());
|
|
}();
|
|
|
|
auto tx = vault.set({.owner = owner, .id = keylet.key});
|
|
tx[sfDomainID] = to_string(domainId);
|
|
env(tx);
|
|
env.close();
|
|
}
|
|
|
|
auto const credKeylet = credentials::keylet(depositor, owner, credType);
|
|
{
|
|
testcase("private XRP vault depositor now authorized");
|
|
env(credentials::create(depositor, owner, credType));
|
|
env(credentials::accept(depositor, owner, credType));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(env.le(credKeylet));
|
|
auto tx = vault.deposit(
|
|
{.depositor = depositor,
|
|
.id = keylet.key,
|
|
.amount = asset(50)});
|
|
env(tx);
|
|
env.close();
|
|
}
|
|
|
|
{
|
|
testcase("private XRP vault can pay shares to depositor");
|
|
env(pay(owner, depositor, shares(1)));
|
|
}
|
|
|
|
{
|
|
testcase("private XRP vault cannot pay shares to 3rd party");
|
|
Json::Value jv;
|
|
jv[sfAccount] = alice.human();
|
|
jv[sfTransactionType] = jss::MPTokenAuthorize;
|
|
jv[sfMPTokenIssuanceID] = to_string(issuanceId);
|
|
env(jv);
|
|
env.close();
|
|
|
|
env(pay(owner, alice, shares(1)), ter{tecNO_AUTH});
|
|
}
|
|
}
|
|
|
|
void
|
|
testFailedPseudoAccount()
|
|
{
|
|
using namespace test::jtx;
|
|
|
|
testcase("failed pseudo-account allocation");
|
|
Env env{*this, testable_amendments() | featureSingleAssetVault};
|
|
Account const owner{"owner"};
|
|
Vault vault{env};
|
|
env.fund(XRP(1000), owner);
|
|
|
|
auto const keylet = keylet::vault(owner.id(), env.seq(owner));
|
|
for (int i = 0; i < 256; ++i)
|
|
{
|
|
AccountID const accountId =
|
|
ripple::pseudoAccountAddress(*env.current(), keylet.key);
|
|
|
|
env(pay(env.master.id(), accountId, XRP(1000)),
|
|
seq(autofill),
|
|
fee(autofill),
|
|
sig(autofill));
|
|
}
|
|
|
|
auto [tx, keylet1] =
|
|
vault.create({.owner = owner, .asset = xrpIssue()});
|
|
BEAST_EXPECT(keylet.key == keylet1.key);
|
|
env(tx, ter{terADDRESS_COLLISION});
|
|
}
|
|
|
|
void
|
|
testRPC()
|
|
{
|
|
using namespace test::jtx;
|
|
|
|
testcase("RPC");
|
|
Env env{*this, testable_amendments() | featureSingleAssetVault};
|
|
Account const owner{"owner"};
|
|
Account const issuer{"issuer"};
|
|
Vault vault{env};
|
|
env.fund(XRP(1000), issuer, owner);
|
|
env.close();
|
|
|
|
PrettyAsset asset = issuer["IOU"];
|
|
env.trust(asset(1000), owner);
|
|
env(pay(issuer, owner, asset(200)));
|
|
env.close();
|
|
|
|
auto const sequence = env.seq(owner);
|
|
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
|
env(tx);
|
|
env.close();
|
|
|
|
// Set some fields
|
|
{
|
|
auto tx1 = vault.deposit(
|
|
{.depositor = owner, .id = keylet.key, .amount = asset(50)});
|
|
env(tx1);
|
|
|
|
auto tx2 = vault.set({.owner = owner, .id = keylet.key});
|
|
tx2[sfAssetsMaximum] = asset(1000).number();
|
|
env(tx2);
|
|
env.close();
|
|
}
|
|
|
|
auto const sleVault = [&env, keylet = keylet, this]() {
|
|
auto const vault = env.le(keylet);
|
|
BEAST_EXPECT(vault != nullptr);
|
|
return vault;
|
|
}();
|
|
|
|
auto const check = [&, keylet = keylet, sle = sleVault, this](
|
|
Json::Value const& vault,
|
|
Json::Value const& issuance = Json::nullValue) {
|
|
BEAST_EXPECT(vault.isObject());
|
|
|
|
constexpr auto checkString =
|
|
[](auto& node, SField const& field, std::string v) -> bool {
|
|
return node.isMember(field.fieldName) &&
|
|
node[field.fieldName].isString() &&
|
|
node[field.fieldName] == v;
|
|
};
|
|
constexpr auto checkObject =
|
|
[](auto& node, SField const& field, Json::Value v) -> bool {
|
|
return node.isMember(field.fieldName) &&
|
|
node[field.fieldName].isObject() &&
|
|
node[field.fieldName] == v;
|
|
};
|
|
constexpr auto checkInt =
|
|
[](auto& node, SField const& field, int v) -> bool {
|
|
return node.isMember(field.fieldName) &&
|
|
((node[field.fieldName].isInt() &&
|
|
node[field.fieldName] == Json::Int(v)) ||
|
|
(node[field.fieldName].isUInt() &&
|
|
node[field.fieldName] == Json::UInt(v)));
|
|
};
|
|
|
|
BEAST_EXPECT(vault["LedgerEntryType"].asString() == "Vault");
|
|
BEAST_EXPECT(vault[jss::index].asString() == strHex(keylet.key));
|
|
BEAST_EXPECT(checkInt(vault, sfFlags, 0));
|
|
// Ignore all other standard fields, this test doesn't care
|
|
|
|
BEAST_EXPECT(
|
|
checkString(vault, sfAccount, toBase58(sle->at(sfAccount))));
|
|
BEAST_EXPECT(
|
|
checkObject(vault, sfAsset, to_json(sle->at(sfAsset))));
|
|
BEAST_EXPECT(checkString(vault, sfAssetsAvailable, "50"));
|
|
BEAST_EXPECT(checkString(vault, sfAssetsMaximum, "1000"));
|
|
BEAST_EXPECT(checkString(vault, sfAssetsTotal, "50"));
|
|
BEAST_EXPECT(checkString(vault, sfLossUnrealized, "0"));
|
|
|
|
auto const strShareID = strHex(sle->at(sfShareMPTID));
|
|
BEAST_EXPECT(checkString(vault, sfShareMPTID, strShareID));
|
|
BEAST_EXPECT(checkString(vault, sfOwner, toBase58(owner.id())));
|
|
BEAST_EXPECT(checkInt(vault, sfSequence, sequence));
|
|
BEAST_EXPECT(checkInt(
|
|
vault, sfWithdrawalPolicy, vaultStrategyFirstComeFirstServe));
|
|
|
|
if (issuance.isObject())
|
|
{
|
|
BEAST_EXPECT(
|
|
issuance["LedgerEntryType"].asString() ==
|
|
"MPTokenIssuance");
|
|
BEAST_EXPECT(
|
|
issuance[jss::mpt_issuance_id].asString() == strShareID);
|
|
BEAST_EXPECT(checkInt(issuance, sfSequence, 1));
|
|
BEAST_EXPECT(checkInt(
|
|
issuance,
|
|
sfFlags,
|
|
int(lsfMPTCanEscrow | lsfMPTCanTrade | lsfMPTCanTransfer)));
|
|
BEAST_EXPECT(checkString(issuance, sfOutstandingAmount, "50"));
|
|
}
|
|
};
|
|
|
|
{
|
|
testcase("RPC ledger_entry selected by key");
|
|
Json::Value jvParams;
|
|
jvParams[jss::ledger_index] = jss::validated;
|
|
jvParams[jss::vault] = strHex(keylet.key);
|
|
auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
|
|
|
|
BEAST_EXPECT(!jvVault[jss::result].isMember(jss::error));
|
|
BEAST_EXPECT(jvVault[jss::result].isMember(jss::node));
|
|
check(jvVault[jss::result][jss::node]);
|
|
}
|
|
|
|
{
|
|
testcase("RPC ledger_entry selected by owner and seq");
|
|
Json::Value jvParams;
|
|
jvParams[jss::ledger_index] = jss::validated;
|
|
jvParams[jss::vault][jss::owner] = owner.human();
|
|
jvParams[jss::vault][jss::seq] = sequence;
|
|
auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
|
|
|
|
BEAST_EXPECT(!jvVault[jss::result].isMember(jss::error));
|
|
BEAST_EXPECT(jvVault[jss::result].isMember(jss::node));
|
|
check(jvVault[jss::result][jss::node]);
|
|
}
|
|
|
|
{
|
|
testcase("RPC ledger_entry cannot find vault by key");
|
|
Json::Value jvParams;
|
|
jvParams[jss::ledger_index] = jss::validated;
|
|
jvParams[jss::vault] = to_string(uint256(42));
|
|
auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
|
|
BEAST_EXPECT(
|
|
jvVault[jss::result][jss::error].asString() == "entryNotFound");
|
|
}
|
|
|
|
{
|
|
testcase("RPC ledger_entry cannot find vault by owner and seq");
|
|
Json::Value jvParams;
|
|
jvParams[jss::ledger_index] = jss::validated;
|
|
jvParams[jss::vault][jss::owner] = issuer.human();
|
|
jvParams[jss::vault][jss::seq] = 1'000'000;
|
|
auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
|
|
BEAST_EXPECT(
|
|
jvVault[jss::result][jss::error].asString() == "entryNotFound");
|
|
}
|
|
|
|
{
|
|
testcase("RPC ledger_entry malformed key");
|
|
Json::Value jvParams;
|
|
jvParams[jss::ledger_index] = jss::validated;
|
|
jvParams[jss::vault] = 42;
|
|
auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
|
|
BEAST_EXPECT(
|
|
jvVault[jss::result][jss::error].asString() ==
|
|
"malformedRequest");
|
|
}
|
|
|
|
{
|
|
testcase("RPC ledger_entry malformed owner");
|
|
Json::Value jvParams;
|
|
jvParams[jss::ledger_index] = jss::validated;
|
|
jvParams[jss::vault][jss::owner] = 42;
|
|
jvParams[jss::vault][jss::seq] = sequence;
|
|
auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
|
|
BEAST_EXPECT(
|
|
jvVault[jss::result][jss::error].asString() ==
|
|
"malformedOwner");
|
|
}
|
|
|
|
{
|
|
testcase("RPC ledger_entry malformed seq");
|
|
Json::Value jvParams;
|
|
jvParams[jss::ledger_index] = jss::validated;
|
|
jvParams[jss::vault][jss::owner] = issuer.human();
|
|
jvParams[jss::vault][jss::seq] = "foo";
|
|
auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
|
|
BEAST_EXPECT(
|
|
jvVault[jss::result][jss::error].asString() ==
|
|
"malformedRequest");
|
|
}
|
|
|
|
{
|
|
testcase("RPC ledger_entry zero seq");
|
|
Json::Value jvParams;
|
|
jvParams[jss::ledger_index] = jss::validated;
|
|
jvParams[jss::vault][jss::owner] = issuer.human();
|
|
jvParams[jss::vault][jss::seq] = 0;
|
|
auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
|
|
BEAST_EXPECT(
|
|
jvVault[jss::result][jss::error].asString() ==
|
|
"malformedRequest");
|
|
}
|
|
|
|
{
|
|
testcase("RPC ledger_entry negative seq");
|
|
Json::Value jvParams;
|
|
jvParams[jss::ledger_index] = jss::validated;
|
|
jvParams[jss::vault][jss::owner] = issuer.human();
|
|
jvParams[jss::vault][jss::seq] = -1;
|
|
auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
|
|
BEAST_EXPECT(
|
|
jvVault[jss::result][jss::error].asString() ==
|
|
"malformedRequest");
|
|
}
|
|
|
|
{
|
|
testcase("RPC ledger_entry oversized seq");
|
|
Json::Value jvParams;
|
|
jvParams[jss::ledger_index] = jss::validated;
|
|
jvParams[jss::vault][jss::owner] = issuer.human();
|
|
jvParams[jss::vault][jss::seq] = 1e20;
|
|
auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
|
|
BEAST_EXPECT(
|
|
jvVault[jss::result][jss::error].asString() ==
|
|
"malformedRequest");
|
|
}
|
|
|
|
{
|
|
testcase("RPC ledger_entry bool seq");
|
|
Json::Value jvParams;
|
|
jvParams[jss::ledger_index] = jss::validated;
|
|
jvParams[jss::vault][jss::owner] = issuer.human();
|
|
jvParams[jss::vault][jss::seq] = true;
|
|
auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
|
|
BEAST_EXPECT(
|
|
jvVault[jss::result][jss::error].asString() ==
|
|
"malformedRequest");
|
|
}
|
|
|
|
{
|
|
testcase("RPC account_objects");
|
|
|
|
Json::Value jvParams;
|
|
jvParams[jss::account] = owner.human();
|
|
jvParams[jss::type] = jss::vault;
|
|
auto jv = env.rpc(
|
|
"json", "account_objects", to_string(jvParams))[jss::result];
|
|
|
|
BEAST_EXPECT(jv[jss::account_objects].size() == 1);
|
|
check(jv[jss::account_objects][0u]);
|
|
}
|
|
|
|
{
|
|
testcase("RPC ledger_data");
|
|
|
|
Json::Value jvParams;
|
|
jvParams[jss::ledger_index] = jss::validated;
|
|
jvParams[jss::binary] = false;
|
|
jvParams[jss::type] = jss::vault;
|
|
Json::Value jv =
|
|
env.rpc("json", "ledger_data", to_string(jvParams));
|
|
BEAST_EXPECT(jv[jss::result][jss::state].size() == 1);
|
|
check(jv[jss::result][jss::state][0u]);
|
|
}
|
|
|
|
{
|
|
testcase("RPC vault_info command line");
|
|
Json::Value jv =
|
|
env.rpc("vault_info", strHex(keylet.key), "validated");
|
|
|
|
BEAST_EXPECT(!jv[jss::result].isMember(jss::error));
|
|
BEAST_EXPECT(jv[jss::result].isMember(jss::vault));
|
|
check(
|
|
jv[jss::result][jss::vault],
|
|
jv[jss::result][jss::vault][jss::shares]);
|
|
}
|
|
|
|
{
|
|
testcase("RPC vault_info json");
|
|
Json::Value jvParams;
|
|
jvParams[jss::ledger_index] = jss::validated;
|
|
jvParams[jss::vault_id] = strHex(keylet.key);
|
|
auto jv = env.rpc("json", "vault_info", to_string(jvParams));
|
|
|
|
BEAST_EXPECT(!jv[jss::result].isMember(jss::error));
|
|
BEAST_EXPECT(jv[jss::result].isMember(jss::vault));
|
|
check(
|
|
jv[jss::result][jss::vault],
|
|
jv[jss::result][jss::vault][jss::shares]);
|
|
}
|
|
|
|
{
|
|
testcase("RPC vault_info invalid vault_id");
|
|
Json::Value jvParams;
|
|
jvParams[jss::ledger_index] = jss::validated;
|
|
jvParams[jss::vault_id] = "foobar";
|
|
auto jv = env.rpc("json", "vault_info", to_string(jvParams));
|
|
BEAST_EXPECT(
|
|
jv[jss::result][jss::error].asString() == "malformedRequest");
|
|
}
|
|
|
|
{
|
|
testcase("RPC vault_info json invalid index");
|
|
Json::Value jvParams;
|
|
jvParams[jss::ledger_index] = jss::validated;
|
|
jvParams[jss::vault_id] = 0;
|
|
auto jv = env.rpc("json", "vault_info", to_string(jvParams));
|
|
BEAST_EXPECT(
|
|
jv[jss::result][jss::error].asString() == "malformedRequest");
|
|
}
|
|
|
|
{
|
|
testcase("RPC vault_info json by owner and sequence");
|
|
Json::Value jvParams;
|
|
jvParams[jss::ledger_index] = jss::validated;
|
|
jvParams[jss::owner] = owner.human();
|
|
jvParams[jss::seq] = sequence;
|
|
auto jv = env.rpc("json", "vault_info", to_string(jvParams));
|
|
|
|
BEAST_EXPECT(!jv[jss::result].isMember(jss::error));
|
|
BEAST_EXPECT(jv[jss::result].isMember(jss::vault));
|
|
check(
|
|
jv[jss::result][jss::vault],
|
|
jv[jss::result][jss::vault][jss::shares]);
|
|
}
|
|
|
|
{
|
|
testcase("RPC vault_info json malformed sequence");
|
|
Json::Value jvParams;
|
|
jvParams[jss::ledger_index] = jss::validated;
|
|
jvParams[jss::owner] = owner.human();
|
|
jvParams[jss::seq] = "foobar";
|
|
auto jv = env.rpc("json", "vault_info", to_string(jvParams));
|
|
BEAST_EXPECT(
|
|
jv[jss::result][jss::error].asString() == "malformedRequest");
|
|
}
|
|
|
|
{
|
|
testcase("RPC vault_info json invalid sequence");
|
|
Json::Value jvParams;
|
|
jvParams[jss::ledger_index] = jss::validated;
|
|
jvParams[jss::owner] = owner.human();
|
|
jvParams[jss::seq] = 0;
|
|
auto jv = env.rpc("json", "vault_info", to_string(jvParams));
|
|
BEAST_EXPECT(
|
|
jv[jss::result][jss::error].asString() == "malformedRequest");
|
|
}
|
|
|
|
{
|
|
testcase("RPC vault_info json negative sequence");
|
|
Json::Value jvParams;
|
|
jvParams[jss::ledger_index] = jss::validated;
|
|
jvParams[jss::owner] = owner.human();
|
|
jvParams[jss::seq] = -1;
|
|
auto jv = env.rpc("json", "vault_info", to_string(jvParams));
|
|
BEAST_EXPECT(
|
|
jv[jss::result][jss::error].asString() == "malformedRequest");
|
|
}
|
|
|
|
{
|
|
testcase("RPC vault_info json oversized sequence");
|
|
Json::Value jvParams;
|
|
jvParams[jss::ledger_index] = jss::validated;
|
|
jvParams[jss::owner] = owner.human();
|
|
jvParams[jss::seq] = 1e20;
|
|
auto jv = env.rpc("json", "vault_info", to_string(jvParams));
|
|
BEAST_EXPECT(
|
|
jv[jss::result][jss::error].asString() == "malformedRequest");
|
|
}
|
|
|
|
{
|
|
testcase("RPC vault_info json bool sequence");
|
|
Json::Value jvParams;
|
|
jvParams[jss::ledger_index] = jss::validated;
|
|
jvParams[jss::owner] = owner.human();
|
|
jvParams[jss::seq] = true;
|
|
auto jv = env.rpc("json", "vault_info", to_string(jvParams));
|
|
BEAST_EXPECT(
|
|
jv[jss::result][jss::error].asString() == "malformedRequest");
|
|
}
|
|
|
|
{
|
|
testcase("RPC vault_info json malformed owner");
|
|
Json::Value jvParams;
|
|
jvParams[jss::ledger_index] = jss::validated;
|
|
jvParams[jss::owner] = "foobar";
|
|
jvParams[jss::seq] = sequence;
|
|
auto jv = env.rpc("json", "vault_info", to_string(jvParams));
|
|
BEAST_EXPECT(
|
|
jv[jss::result][jss::error].asString() == "malformedRequest");
|
|
}
|
|
|
|
{
|
|
testcase("RPC vault_info json invalid combination only owner");
|
|
Json::Value jvParams;
|
|
jvParams[jss::ledger_index] = jss::validated;
|
|
jvParams[jss::owner] = owner.human();
|
|
auto jv = env.rpc("json", "vault_info", to_string(jvParams));
|
|
BEAST_EXPECT(
|
|
jv[jss::result][jss::error].asString() == "malformedRequest");
|
|
}
|
|
|
|
{
|
|
testcase("RPC vault_info json invalid combination only seq");
|
|
Json::Value jvParams;
|
|
jvParams[jss::ledger_index] = jss::validated;
|
|
jvParams[jss::seq] = sequence;
|
|
auto jv = env.rpc("json", "vault_info", to_string(jvParams));
|
|
BEAST_EXPECT(
|
|
jv[jss::result][jss::error].asString() == "malformedRequest");
|
|
}
|
|
|
|
{
|
|
testcase("RPC vault_info json invalid combination seq vault_id");
|
|
Json::Value jvParams;
|
|
jvParams[jss::ledger_index] = jss::validated;
|
|
jvParams[jss::vault_id] = strHex(keylet.key);
|
|
jvParams[jss::seq] = sequence;
|
|
auto jv = env.rpc("json", "vault_info", to_string(jvParams));
|
|
BEAST_EXPECT(
|
|
jv[jss::result][jss::error].asString() == "malformedRequest");
|
|
}
|
|
|
|
{
|
|
testcase("RPC vault_info json invalid combination owner vault_id");
|
|
Json::Value jvParams;
|
|
jvParams[jss::ledger_index] = jss::validated;
|
|
jvParams[jss::vault_id] = strHex(keylet.key);
|
|
jvParams[jss::owner] = owner.human();
|
|
auto jv = env.rpc("json", "vault_info", to_string(jvParams));
|
|
BEAST_EXPECT(
|
|
jv[jss::result][jss::error].asString() == "malformedRequest");
|
|
}
|
|
|
|
{
|
|
testcase(
|
|
"RPC vault_info json invalid combination owner seq "
|
|
"vault_id");
|
|
Json::Value jvParams;
|
|
jvParams[jss::ledger_index] = jss::validated;
|
|
jvParams[jss::vault_id] = strHex(keylet.key);
|
|
jvParams[jss::seq] = sequence;
|
|
jvParams[jss::owner] = owner.human();
|
|
auto jv = env.rpc("json", "vault_info", to_string(jvParams));
|
|
BEAST_EXPECT(
|
|
jv[jss::result][jss::error].asString() == "malformedRequest");
|
|
}
|
|
|
|
{
|
|
testcase("RPC vault_info json no input");
|
|
Json::Value jvParams;
|
|
jvParams[jss::ledger_index] = jss::validated;
|
|
auto jv = env.rpc("json", "vault_info", to_string(jvParams));
|
|
BEAST_EXPECT(
|
|
jv[jss::result][jss::error].asString() == "malformedRequest");
|
|
}
|
|
|
|
{
|
|
testcase("RPC vault_info command line invalid index");
|
|
Json::Value jv = env.rpc("vault_info", "foobar", "validated");
|
|
BEAST_EXPECT(jv[jss::error].asString() == "invalidParams");
|
|
}
|
|
|
|
{
|
|
testcase("RPC vault_info command line invalid index");
|
|
Json::Value jv = env.rpc("vault_info", "0", "validated");
|
|
BEAST_EXPECT(
|
|
jv[jss::result][jss::error].asString() == "malformedRequest");
|
|
}
|
|
|
|
{
|
|
testcase("RPC vault_info command line invalid index");
|
|
Json::Value jv =
|
|
env.rpc("vault_info", strHex(uint256(42)), "validated");
|
|
BEAST_EXPECT(
|
|
jv[jss::result][jss::error].asString() == "entryNotFound");
|
|
}
|
|
|
|
{
|
|
testcase("RPC vault_info command line invalid ledger");
|
|
Json::Value jv = env.rpc("vault_info", strHex(keylet.key), "0");
|
|
BEAST_EXPECT(
|
|
jv[jss::result][jss::error].asString() == "lgrNotFound");
|
|
}
|
|
}
|
|
|
|
public:
|
|
void
|
|
run() override
|
|
{
|
|
testSequences();
|
|
testPreflight();
|
|
testCreateFailXRP();
|
|
testCreateFailIOU();
|
|
testCreateFailMPT();
|
|
testWithMPT();
|
|
testWithIOU();
|
|
testWithDomainCheck();
|
|
testWithDomainCheckXRP();
|
|
testNonTransferableShares();
|
|
testFailedPseudoAccount();
|
|
testRPC();
|
|
}
|
|
};
|
|
|
|
BEAST_DEFINE_TESTSUITE_PRIO(Vault, tx, ripple, 1);
|
|
|
|
} // namespace ripple
|