Files
rippled/src/test/app/EscrowToken_test.cpp
T
Nik Bougalis 55b265a8c2 Improve BaseUInt:
This commit reworks the BaseUInt class, which was originally taken from
Bitcoin and has since been heavily modified.

The internal limb type now widens from 32 to 64 bits when the requested
width is a multiple of 64. This halves the iteration count when working
on limbs; coupled with the use of add-with-carry intrinsics, the result
is better code generation and optimized use of processor resources.

The constructors have been reshuffled, constraining how an instance can
be initialized. As a result, many common initialization errors will now
fail at compile time instead of run time.

A `BaseUInt` can now be initialized with:

 - A byte sequence of fixed size, i.e an `std::array`, a C array, or a
   fixed-extent `std::span` of `unsigned char` or `std::byte`.
 - A string literal, formatted as a hexadecimal string with no leading
   0x. Malformed or improperly sized inputs are a compile-time error.
 - An unsigned integer. This constructor is mostly used for tests, and
   must execute at compile time.
 - A byte sequence whose length is not known at compile time, via the
   fromRaw() function which returns a seated `std::optional` only if
   the input is byte sequence is properly sized.

The runtime raw-pointer and `std::uint64_t` constructors are gone, as
are the container assignment operator and the the `fromVoid` and
`fromVoidChecked` functions.

Other changes:

 - The SFINAE-based container trait is replaced by the ByteCopySource
   and FixedByteRange concepts.
 - operator== and operator<=> are now hidden friends. The workaround
   in the old operator<=> is gone.
 - Efficient tag conversion is now possible.
 - `BaseUInt` is now `noexcept` and (almost) fully `constexpr`: every
   operation except byte access, hashing and text output is usable in
   constant evaluation.

Cleanups:

 - Most invocations of `stringIsUInt256Sized` in PeerImp are now gone
   and leverage `fromRaw`.
2026-10-05 19:42:52 -07:00

4354 lines
165 KiB
C++

#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/amount.h>
#include <test/jtx/balance.h> // IWYU pragma: keep
#include <test/jtx/escrow.h>
#include <test/jtx/fee.h>
#include <test/jtx/flags.h>
#include <test/jtx/mpt.h>
#include <test/jtx/pay.h>
#include <test/jtx/rate.h>
#include <test/jtx/ter.h>
#include <test/jtx/trust.h>
#include <test/jtx/txflags.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/to_string.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/Dir.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/ledger/Sandbox.h>
#include <xrpl/protocol/AccountID.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/SeqProxy.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/jss.h>
#include <algorithm>
#include <array>
#include <cstdint>
#include <iterator>
#include <memory>
namespace xrpl::test {
struct EscrowToken_test : public beast::unit_test::Suite
{
static uint64_t
mptEscrowed(jtx::Env const& env, jtx::Account const& account, jtx::MPT const& mpt)
{
auto const sle = env.le(keylet::mptoken(mpt.mpt(), account));
if (sle && sle->isFieldPresent(sfLockedAmount))
return (*sle)[sfLockedAmount];
return 0;
}
static uint64_t
issuerMPTEscrowed(jtx::Env const& env, jtx::MPT const& mpt)
{
auto const sle = env.le(keylet::mptokenIssuance(mpt.mpt()));
if (sle && sle->isFieldPresent(sfLockedAmount))
return (*sle)[sfLockedAmount];
return 0;
}
static jtx::PrettyAmount
issuerBalance(jtx::Env& env, jtx::Account const& account, Issue const& issue)
{
json::Value params;
params[jss::account] = account.human();
auto jrr = env.rpc("json", "gateway_balances", to_string(params));
auto const result = jrr[jss::result];
auto const obligations = result[jss::obligations][to_string(issue.currency)];
if (obligations.isNull())
return {STAmount(issue, 0), account.name()};
STAmount const amount = amountFromString(issue, obligations.asString());
return {amount, account.name()};
}
static jtx::PrettyAmount
issuerEscrowed(jtx::Env& env, jtx::Account const& account, Issue const& issue)
{
json::Value params;
params[jss::account] = account.human();
auto jrr = env.rpc("json", "gateway_balances", to_string(params));
auto const result = jrr[jss::result];
auto const locked = result[jss::locked][to_string(issue.currency)];
if (locked.isNull())
return {STAmount(issue, 0), account.name()};
STAmount const amount = amountFromString(issue, locked.asString());
return {amount, account.name()};
}
void
testIOUEnablement(FeatureBitset features)
{
testcase("IOU Enablement");
using namespace jtx;
using namespace std::chrono;
for (bool const withTokenEscrow : {false, true})
{
auto const amend = withTokenEscrow ? features : features - featureTokenEscrow;
Env env{*this, amend};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
env.fund(XRP(5000), alice, bob, gw);
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env.trust(usd(10'000), alice, bob);
env.close();
env(pay(gw, alice, usd(5000)));
env(pay(gw, bob, usd(5000)));
env.close();
auto const createResult = withTokenEscrow ? Ter(tesSUCCESS) : Ter(temBAD_AMOUNT);
auto const finishResult = withTokenEscrow ? Ter(tesSUCCESS) : Ter(tecNO_TARGET);
auto const seq1 = env.seq(alice);
env(escrow::create(alice, bob, usd(1'000)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
createResult);
env.close();
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150),
finishResult);
env.close();
auto const seq2 = env.seq(alice);
env(escrow::create(alice, bob, usd(1'000)),
escrow::kCondition(escrow::kCb2),
escrow::kFinishTime(env.now() + 1s),
escrow::kCancelTime(env.now() + 2s),
Fee(baseFee * 150),
createResult);
env.close();
env(escrow::cancel(bob, alice, seq2), finishResult);
env.close();
}
for (bool const withTokenEscrow : {false, true})
{
auto const amend = withTokenEscrow ? features : features - featureTokenEscrow;
Env env{*this, amend};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
env.fund(XRP(5000), alice, bob, gw);
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env.trust(usd(10'000), alice, bob);
env.close();
env(pay(gw, alice, usd(5000)));
env(pay(gw, bob, usd(5000)));
env.close();
auto const seq1 = env.seq(alice);
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150),
Ter(tecNO_TARGET));
env.close();
env(escrow::cancel(bob, alice, seq1), Ter(tecNO_TARGET));
env.close();
}
}
void
testIOUAllowLockingFlag(FeatureBitset features)
{
testcase("IOU Allow Locking Flag");
using namespace jtx;
using namespace std::chrono;
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
env.fund(XRP(5000), alice, bob, gw);
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env.trust(usd(10'000), alice, bob);
env.close();
env(pay(gw, alice, usd(5000)));
env(pay(gw, bob, usd(5000)));
env.close();
// Create Escrow #1 & #2
auto const seq1 = env.seq(alice);
env(escrow::create(alice, bob, usd(1'000)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
auto const seq2 = env.seq(alice);
env(escrow::create(alice, bob, usd(1'000)),
escrow::kFinishTime(env.now() + 1s),
escrow::kCancelTime(env.now() + 3s),
Fee(baseFee),
Ter(tesSUCCESS));
env.close();
// Clear the asfAllowTrustLineLocking flag
env(fclear(gw, asfAllowTrustLineLocking));
env.close();
env.require(Nflags(gw, asfAllowTrustLineLocking));
// Cannot Create Escrow without asfAllowTrustLineLocking
env(escrow::create(alice, bob, usd(1'000)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tecNO_PERMISSION));
env.close();
// Can finish the escrow created before the flag was cleared
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
// Can cancel the escrow created before the flag was cleared
env(escrow::cancel(bob, alice, seq2), Ter(tesSUCCESS));
env.close();
}
void
testIOUCreatePreflight(FeatureBitset features)
{
testcase("IOU Create Preflight");
using namespace test::jtx;
using namespace std::literals;
// temBAD_FEE: Exercises invalid preflight1.
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
env.fund(XRP(5000), alice, bob, gw);
env(escrow::create(alice, bob, usd(1)),
escrow::kFinishTime(env.now() + 1s),
Fee(XRP(-1)),
Ter(temBAD_FEE));
env.close();
}
// temBAD_AMOUNT: amount <= 0
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
env.fund(XRP(5000), alice, bob, gw);
env(escrow::create(alice, bob, usd(-1)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(temBAD_AMOUNT));
env.close();
}
// temBAD_CURRENCY: badCurrency() == amount.getCurrency()
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account{"gateway"};
auto const bad = IOU(gw, badCurrency());
env.fund(XRP(5000), alice, bob, gw);
env(escrow::create(alice, bob, bad(1)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(temBAD_CURRENCY));
env.close();
}
}
void
testIOUCreatePreclaim(FeatureBitset features)
{
testcase("IOU Create Preclaim");
using namespace test::jtx;
using namespace std::literals;
// tecNO_PERMISSION: issuer is the same as the account
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
env.fund(XRP(5000), alice, bob, gw);
env(escrow::create(gw, alice, usd(1)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tecNO_PERMISSION));
env.close();
}
// tecNO_ISSUER: Issuer does not exist
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
env.fund(XRP(5000), alice, bob);
env.close();
env.memoize(gw);
env(escrow::create(alice, bob, usd(1)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tecNO_ISSUER));
env.close();
}
// tecNO_PERMISSION: asfAllowTrustLineLocking is not set
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
env.fund(XRP(5000), alice, bob, gw);
env.close();
env.trust(usd(10'000), alice, bob);
env.close();
env(pay(gw, alice, usd(5000)));
env(pay(gw, bob, usd(5000)));
env.close();
env(escrow::create(gw, alice, usd(1)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tecNO_PERMISSION));
env.close();
}
// tecNO_LINE: account does not have a trustline to the issuer
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
env.fund(XRP(5000), alice, bob, gw);
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env(escrow::create(alice, bob, usd(1)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tecNO_LINE));
env.close();
}
// tecNO_PERMISSION: Not testable
// tecNO_PERMISSION: Not testable
// tecNO_AUTH: requireAuth
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
env.fund(XRP(5000), alice, bob, gw);
env(fset(gw, asfAllowTrustLineLocking));
env(fset(gw, asfRequireAuth));
env.close();
env.trust(usd(10'000), alice, bob);
env.close();
env(escrow::create(alice, bob, usd(1)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tecNO_AUTH));
env.close();
}
// tecNO_AUTH: requireAuth
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
auto const aliceUSD = alice["USD"];
env.fund(XRP(5000), alice, bob, gw);
env(fset(gw, asfAllowTrustLineLocking));
env(fset(gw, asfRequireAuth));
env.close();
env(trust(gw, aliceUSD(10'000)), Txflags(tfSetfAuth));
env.trust(usd(10'000), alice, bob);
env.close();
env(escrow::create(alice, bob, usd(1)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tecNO_AUTH));
env.close();
}
// tecFROZEN: account is frozen
{
// Env Setup
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
auto const baseFee = env.current()->fees().base;
env.fund(XRP(10'000), alice, bob, gw);
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env(trust(alice, usd(100'000)));
env(trust(bob, usd(100'000)));
env.close();
env(pay(gw, alice, usd(10'000)));
env(pay(gw, bob, usd(10'000)));
env.close();
// set freeze on alice trustline
env(trust(gw, usd(10'000), alice, tfSetFreeze));
env.close();
env(escrow::create(alice, bob, usd(1)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tecFROZEN));
env.close();
}
// tecFROZEN: dest is frozen
{
// Env Setup
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
auto const baseFee = env.current()->fees().base;
env.fund(XRP(10'000), alice, bob, gw);
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env(trust(alice, usd(100'000)));
env(trust(bob, usd(100'000)));
env.close();
env(pay(gw, alice, usd(10'000)));
env(pay(gw, bob, usd(10'000)));
env.close();
// set freeze on bob trustline
env(trust(gw, usd(10'000), bob, tfSetFreeze));
env.close();
env(escrow::create(alice, bob, usd(1)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tecFROZEN));
env.close();
}
// tecINSUFFICIENT_FUNDS
{
// Env Setup
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
auto const baseFee = env.current()->fees().base;
env.fund(XRP(10'000), alice, bob, gw);
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env(trust(alice, usd(100'000)));
env(trust(bob, usd(100'000)));
env.close();
env(escrow::create(alice, bob, usd(1)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tecINSUFFICIENT_FUNDS));
env.close();
}
// tecINSUFFICIENT_FUNDS
{
// Env Setup
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
auto const baseFee = env.current()->fees().base;
env.fund(XRP(10'000), alice, bob, gw);
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env(trust(alice, usd(100'000)));
env(trust(bob, usd(100'000)));
env.close();
env(pay(gw, alice, usd(10'000)));
env(pay(gw, bob, usd(10'000)));
env.close();
env(escrow::create(alice, bob, usd(10'001)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tecINSUFFICIENT_FUNDS));
env.close();
}
// tecPRECISION_LOSS
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
auto const baseFee = env.current()->fees().base;
env.fund(XRP(10'000), alice, bob, gw);
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env.trust(usd(100000000000000000), alice);
env.trust(usd(100000000000000000), bob);
env.close();
env(pay(gw, alice, usd(10000000000000000)));
env(pay(gw, bob, usd(1)));
env.close();
bool const largeMantissa = features[featureSingleAssetVault] ||
features[featureLendingProtocol] || features[featureMPTokensV2];
// alice cannot create escrow for 1/10 iou - precision loss
env(escrow::create(alice, bob, usd(1)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(largeMantissa ? (TER)tesSUCCESS : (TER)tecPRECISION_LOSS));
env.close();
}
}
void
testIOUFinishPreclaim(FeatureBitset features)
{
testcase("IOU Finish Preclaim");
using namespace test::jtx;
using namespace std::literals;
// tecNO_AUTH: requireAuth set: dest not authorized
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
auto const aliceUSD = alice["USD"];
auto const bobUSD = bob["USD"];
env.fund(XRP(5000), alice, bob, gw);
env(fset(gw, asfAllowTrustLineLocking));
env(fset(gw, asfRequireAuth));
env.close();
env(trust(gw, aliceUSD(10'000)), Txflags(tfSetfAuth));
env(trust(gw, bobUSD(10'000)), Txflags(tfSetfAuth));
env.trust(usd(10'000), alice, bob);
env.close();
env(pay(gw, alice, usd(10'000)));
env(pay(gw, bob, usd(10'000)));
env.close();
auto const seq1 = env.seq(alice);
env(escrow::create(alice, bob, usd(1)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
env(pay(bob, gw, usd(10'000)));
env(trust(gw, bobUSD(0)), Txflags(tfSetfAuth));
env(trust(bob, usd(0)));
env.close();
env.trust(usd(10'000), bob);
env.close();
// bob cannot finish because he is not authorized
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150),
Ter(tecNO_AUTH));
env.close();
}
// tecFROZEN: issuer has deep frozen the dest
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
env.fund(XRP(5000), alice, bob, gw);
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env.trust(usd(10'000), alice, bob);
env.close();
env(pay(gw, alice, usd(10'000)));
env(pay(gw, bob, usd(10'000)));
env.close();
auto const seq1 = env.seq(alice);
env(escrow::create(alice, bob, usd(1)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
// set freeze on bob trustline
env(trust(gw, usd(10'000), bob, tfSetFreeze | tfSetDeepFreeze));
// bob cannot finish because of deep freeze
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150),
Ter(tecFROZEN));
env.close();
}
}
void
testIOUFinishDoApply(FeatureBitset features)
{
testcase("IOU Finish Do Apply");
using namespace test::jtx;
using namespace std::literals;
// tecNO_LINE_INSUF_RESERVE: insufficient reserve to create line
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const acctReserve = env.current()->fees().reserve;
auto const incReserve = env.current()->fees().increment;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
env.fund(XRP(5000), alice, gw);
env.fund(acctReserve + (incReserve - 1), bob);
env.close();
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env.trust(usd(10'000), alice);
env.close();
env(pay(gw, alice, usd(10'000)));
env.close();
auto const seq1 = env.seq(alice);
env(escrow::create(alice, bob, usd(1)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
// bob cannot finish because insufficient reserve to create line
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150),
Ter(tecNO_LINE_INSUF_RESERVE));
env.close();
}
// tecNO_LINE: alice submits; finish IOU not created
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
env.fund(XRP(5000), alice, bob, gw);
env.close();
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env.trust(usd(10'000), alice);
env.close();
env(pay(gw, alice, usd(10'000)));
env.close();
auto const seq1 = env.seq(alice);
env(escrow::create(alice, bob, usd(1)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
// alice cannot finish because bob does not have a trustline
env(escrow::finish(alice, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150),
Ter(tecNO_LINE));
env.close();
}
// tecLIMIT_EXCEEDED: alice submits; IOU Limit < balance + amount
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
env.fund(XRP(5000), alice, bob, gw);
env.close();
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env.trust(usd(1000), alice, bob);
env.close();
env(pay(gw, alice, usd(1000)));
env.close();
auto const seq1 = env.seq(alice);
env(escrow::create(alice, bob, usd(5)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
env.trust(usd(1), bob);
env.close();
// alice cannot finish because bob's limit is too low
env(escrow::finish(alice, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150),
Ter(tecLIMIT_EXCEEDED));
env.close();
}
// tesSUCCESS: bob submits; IOU Limit < balance + amount
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
env.fund(XRP(5000), alice, bob, gw);
env.close();
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env.trust(usd(1000), alice, bob);
env.close();
env(pay(gw, alice, usd(1000)));
env.close();
auto const seq1 = env.seq(alice);
env(escrow::create(alice, bob, usd(5)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
env.trust(usd(1), bob);
env.close();
// bob can finish even if bob's limit is too low
auto const bobPreLimit = env.limit(bob, usd);
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
// bob's limit is not changed
BEAST_EXPECT(env.limit(bob, usd) == bobPreLimit);
}
}
void
testIOUCancelPreclaim(FeatureBitset features)
{
testcase("IOU Cancel Preclaim");
using namespace test::jtx;
using namespace std::literals;
// tecNO_AUTH: requireAuth set: account not authorized
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
auto const aliceUSD = alice["USD"];
auto const bobUSD = bob["USD"];
env.fund(XRP(5000), alice, bob, gw);
env(fset(gw, asfAllowTrustLineLocking));
env(fset(gw, asfRequireAuth));
env.close();
env(trust(gw, aliceUSD(10'000)), Txflags(tfSetfAuth));
env(trust(gw, bobUSD(10'000)), Txflags(tfSetfAuth));
env.trust(usd(10'000), alice, bob);
env.close();
env(pay(gw, alice, usd(10'000)));
env(pay(gw, bob, usd(10'000)));
env.close();
auto const seq1 = env.seq(alice);
env(escrow::create(alice, bob, usd(1)),
escrow::kFinishTime(env.now() + 1s),
escrow::kCancelTime(env.now() + 2s),
Fee(baseFee),
Ter(tesSUCCESS));
env.close();
env(pay(alice, gw, usd(9'999)));
env(trust(gw, aliceUSD(0)), Txflags(tfSetfAuth));
env(trust(alice, usd(0)));
env.close();
env.trust(usd(10'000), alice);
env.close();
// alice cannot cancel because she is not authorized
env(escrow::cancel(bob, alice, seq1), Fee(baseFee), Ter(tecNO_AUTH));
env.close();
}
}
void
testIOUCancelDoApply(FeatureBitset features)
{
testcase("IOU Cancel DoApply");
using namespace 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 gw = Account("gw");
auto const usd = gw["USD"];
env.fund(XRP(10'000), alice, bob, gw);
env.close();
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env.trust(usd(100'000), alice);
env.trust(usd(100'000), bob);
env.close();
env(pay(gw, alice, usd(10'000)));
env.close();
auto const seq = env.seq(alice);
env(escrow::create(alice, bob, usd(1'000)),
escrow::kFinishTime(env.now() + 1s),
escrow::kCancelTime(env.now() + 2s),
Fee(baseFee));
env.close();
BEAST_EXPECT(env.balance(alice, usd) == usd(9'000));
env(pay(alice, gw, usd(9'000)));
env.close();
env(trust(alice, usd(0)));
env.close();
auto const trustLineKey = keylet::trustLine(alice.id(), gw.id(), usd.currency);
BEAST_EXPECT(!env.current()->exists(trustLineKey));
env.close();
env.close();
auto const expectedResult = env.current()->rules().enabled(fixCleanup3_2_0)
? Ter(tesSUCCESS)
: Ter(tefINTERNAL);
env(escrow::cancel(alice, alice, seq), Fee(baseFee), expectedResult);
env.close();
if (env.current()->rules().enabled(fixCleanup3_2_0))
{
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(seq))));
BEAST_EXPECT(env.current()->exists(trustLineKey));
BEAST_EXPECT(env.balance(alice, usd) == usd(1'000));
}
}
}
void
testIOUCancelReserveRecycle(FeatureBitset features)
{
testcase("IOU Cancel Reserve Recycle");
using namespace jtx;
using namespace std::literals;
// Escrowing the whole IOU balance lets the owner delete the now-zero
// trust line, so cancelling has to re-create it: one object destroyed,
// one created, and the reserve requirement unchanged.
Env env{*this, features};
bool const fixEnabled = env.current()->rules().enabled(fixCleanup3_4_0);
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
auto const usd = gw["USD"];
env.fund(XRP(10'000), alice, bob, gw);
env.close();
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env.trust(usd(10'000), alice);
env.close();
env(pay(gw, alice, usd(10'000)));
env.close();
BEAST_EXPECT(env.ownerCount(alice) == 1);
auto const cancelAfter = env.now() + 100s;
auto const seq = env.seq(alice);
env(escrow::create(alice, bob, usd(10'000)),
escrow::kFinishTime(env.now() + 1s),
escrow::kCancelTime(cancelAfter),
Fee(baseFee));
env.close();
BEAST_EXPECT(env.ownerCount(alice) == 2);
auto const trustLineKey = keylet::trustLine(alice.id(), gw.id(), usd.currency);
env(trust(alice, usd(0)));
env.close();
BEAST_EXPECT(!env.current()->exists(trustLineKey));
BEAST_EXPECT(env.ownerCount(alice) == 1);
// Leave alice holding the reserve for exactly one owned object. That
// is the escrow now and the re-created trust line after the cancel.
auto const oneObject = env.current()->fees().accountReserve(1, 1);
auto const twoObjects = env.current()->fees().accountReserve(2, 1);
auto const balance = env.balance(alice).value().xrp();
auto const feeCushion = baseFee.drops() * 20;
env(pay(alice, bob, drops(balance.drops() - oneObject.drops() - feeCushion)));
env.close();
BEAST_EXPECT(env.balance(alice).value().xrp() >= oneObject);
BEAST_EXPECT(env.balance(alice).value().xrp() < twoObjects);
for (; env.now() < cancelAfter; env.close())
{
}
env.close();
env.close();
auto const expectedResult = fixEnabled ? Ter(tesSUCCESS) : Ter(tecNO_LINE_INSUF_RESERVE);
env(escrow::cancel(alice, alice, seq), Fee(baseFee), expectedResult);
env.close();
auto const escrowKey = keylet::escrow(alice.id(), SeqProxy::rawSequence(seq));
if (fixEnabled)
{
BEAST_EXPECT(!env.le(escrowKey));
BEAST_EXPECT(env.current()->exists(trustLineKey));
BEAST_EXPECT(env.balance(alice, usd) == usd(10'000));
BEAST_EXPECT(env.ownerCount(alice) == 1);
}
else
{
// The tec keeps the escrow, so one more owner reserve lets the
// retry through.
BEAST_EXPECT(env.le(escrowKey) != nullptr);
BEAST_EXPECT(!env.current()->exists(trustLineKey));
BEAST_EXPECT(env.ownerCount(alice) == 1);
env(pay(bob, alice, drops(twoObjects.drops() - oneObject.drops())));
env.close();
env(escrow::cancel(alice, alice, seq), Fee(baseFee), Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(!env.le(escrowKey));
BEAST_EXPECT(env.balance(alice, usd) == usd(10'000));
}
}
void
testIOUBalances(FeatureBitset features)
{
testcase("IOU Balances");
using namespace jtx;
using namespace std::chrono;
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
env.fund(XRP(5000), alice, bob, gw);
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env.trust(usd(10'000), alice, bob);
env.close();
env(pay(gw, alice, usd(5'000)));
env(pay(gw, bob, usd(5'000)));
env.close();
auto const outstandingUSD = usd(10'000);
// Create & Finish Escrow
auto const seq1 = env.seq(alice);
{
auto const preAliceUSD = env.balance(alice, usd);
auto const preBobUSD = env.balance(bob, usd);
env(escrow::create(alice, bob, usd(1'000)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(env.balance(alice, usd) == preAliceUSD - usd(1'000));
BEAST_EXPECT(env.balance(bob, usd) == preBobUSD);
BEAST_EXPECT(issuerBalance(env, gw, usd) == outstandingUSD - usd(1'000));
BEAST_EXPECT(issuerEscrowed(env, gw, usd) == usd(1'000));
}
{
auto const preAliceUSD = env.balance(alice, usd);
auto const preBobUSD = env.balance(bob, usd);
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(env.balance(alice, usd) == preAliceUSD);
BEAST_EXPECT(env.balance(bob, usd) == preBobUSD + usd(1'000));
BEAST_EXPECT(issuerBalance(env, gw, usd) == outstandingUSD);
BEAST_EXPECT(issuerEscrowed(env, gw, usd) == usd(0));
}
// Create & Cancel Escrow
auto const seq2 = env.seq(alice);
{
auto const preAliceUSD = env.balance(alice, usd);
auto const preBobUSD = env.balance(bob, usd);
env(escrow::create(alice, bob, usd(1'000)),
escrow::kCondition(escrow::kCb2),
escrow::kFinishTime(env.now() + 1s),
escrow::kCancelTime(env.now() + 2s),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(env.balance(alice, usd) == preAliceUSD - usd(1'000));
BEAST_EXPECT(env.balance(bob, usd) == preBobUSD);
BEAST_EXPECT(issuerBalance(env, gw, usd) == outstandingUSD - usd(1'000));
BEAST_EXPECT(issuerEscrowed(env, gw, usd) == usd(1'000));
}
{
auto const preAliceUSD = env.balance(alice, usd);
auto const preBobUSD = env.balance(bob, usd);
env(escrow::cancel(bob, alice, seq2), Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(env.balance(alice, usd) == preAliceUSD + usd(1'000));
BEAST_EXPECT(env.balance(bob, usd) == preBobUSD);
BEAST_EXPECT(issuerBalance(env, gw, usd) == outstandingUSD);
BEAST_EXPECT(issuerEscrowed(env, gw, usd) == usd(0));
}
}
void
testIOUMetaAndOwnership(FeatureBitset features)
{
using namespace jtx;
using namespace std::chrono;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
{
testcase("IOU Metadata to self");
Env env{*this, features};
env.fund(XRP(5000), alice, bob, carol, gw);
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env.trust(usd(10'000), alice, bob, carol);
env.close();
env(pay(gw, alice, usd(5000)));
env(pay(gw, bob, usd(5000)));
env(pay(gw, carol, usd(5000)));
env.close();
auto const aseq = env.seq(alice);
auto const bseq = env.seq(bob);
env(escrow::create(alice, alice, usd(1'000)),
escrow::kFinishTime(env.now() + 1s),
escrow::kCancelTime(env.now() + 500s));
BEAST_EXPECT(
(*env.meta())[sfTransactionResult] == static_cast<std::uint8_t>(tesSUCCESS));
env.close(5s);
auto const aa = env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(aseq)));
BEAST_EXPECT(aa);
{
xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id()));
BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(aod.begin(), aod.end(), aa) != aod.end());
}
{
xrpl::Dir const iod(*env.current(), keylet::ownerDir(gw.id()));
BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 4);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(iod.begin(), iod.end(), aa) != iod.end());
}
env(escrow::create(bob, bob, usd(1'000)),
escrow::kFinishTime(env.now() + 1s),
escrow::kCancelTime(env.now() + 2s));
BEAST_EXPECT(
(*env.meta())[sfTransactionResult] == static_cast<std::uint8_t>(tesSUCCESS));
env.close(5s);
auto const bb = env.le(keylet::escrow(bob.id(), SeqProxy::rawSequence(bseq)));
BEAST_EXPECT(bb);
{
xrpl::Dir const bod(*env.current(), keylet::ownerDir(bob.id()));
BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(bod.begin(), bod.end(), bb) != bod.end());
}
{
xrpl::Dir const iod(*env.current(), keylet::ownerDir(gw.id()));
BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 5);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(iod.begin(), iod.end(), bb) != iod.end());
}
env.close(5s);
env(escrow::finish(alice, alice, aseq));
{
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(aseq))));
BEAST_EXPECT(
(*env.meta())[sfTransactionResult] == static_cast<std::uint8_t>(tesSUCCESS));
xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id()));
BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(aod.begin(), aod.end(), aa) == aod.end());
xrpl::Dir const bod(*env.current(), keylet::ownerDir(bob.id()));
BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(bod.begin(), bod.end(), bb) != bod.end());
xrpl::Dir const iod(*env.current(), keylet::ownerDir(gw.id()));
BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 4);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(iod.begin(), iod.end(), bb) != iod.end());
}
env.close(5s);
env(escrow::cancel(bob, bob, bseq));
{
BEAST_EXPECT(!env.le(keylet::escrow(bob.id(), SeqProxy::rawSequence(bseq))));
BEAST_EXPECT(
(*env.meta())[sfTransactionResult] == static_cast<std::uint8_t>(tesSUCCESS));
xrpl::Dir const bod(*env.current(), keylet::ownerDir(bob.id()));
BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 1);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(bod.begin(), bod.end(), bb) == bod.end());
xrpl::Dir const iod(*env.current(), keylet::ownerDir(gw.id()));
BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 3);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(iod.begin(), iod.end(), bb) == iod.end());
}
}
{
testcase("IOU Metadata to other");
Env env{*this, features};
env.fund(XRP(5000), alice, bob, carol, gw);
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env.trust(usd(10'000), alice, bob, carol);
env.close();
env(pay(gw, alice, usd(5000)));
env(pay(gw, bob, usd(5000)));
env(pay(gw, carol, usd(5000)));
env.close();
auto const aseq = env.seq(alice);
auto const bseq = env.seq(bob);
env(escrow::create(alice, bob, usd(1'000)), escrow::kFinishTime(env.now() + 1s));
BEAST_EXPECT(
(*env.meta())[sfTransactionResult] == static_cast<std::uint8_t>(tesSUCCESS));
env.close(5s);
env(escrow::create(bob, carol, usd(1'000)),
escrow::kFinishTime(env.now() + 1s),
escrow::kCancelTime(env.now() + 2s));
BEAST_EXPECT(
(*env.meta())[sfTransactionResult] == static_cast<std::uint8_t>(tesSUCCESS));
env.close(5s);
auto const ab = env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(aseq)));
BEAST_EXPECT(ab);
auto const bc = env.le(keylet::escrow(bob.id(), SeqProxy::rawSequence(bseq)));
BEAST_EXPECT(bc);
{
xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id()));
BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(aod.begin(), aod.end(), ab) != aod.end());
xrpl::Dir const bod(*env.current(), keylet::ownerDir(bob.id()));
BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 3);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(bod.begin(), bod.end(), ab) != bod.end());
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(bod.begin(), bod.end(), bc) != bod.end());
xrpl::Dir const cod(*env.current(), keylet::ownerDir(carol.id()));
BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(cod.begin(), cod.end(), bc) != cod.end());
xrpl::Dir const iod(*env.current(), keylet::ownerDir(gw.id()));
BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 5);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(iod.begin(), iod.end(), ab) != iod.end());
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(iod.begin(), iod.end(), bc) != iod.end());
}
env.close(5s);
env(escrow::finish(alice, alice, aseq));
{
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(aseq))));
BEAST_EXPECT(env.le(keylet::escrow(bob.id(), SeqProxy::rawSequence(bseq))));
xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id()));
BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(aod.begin(), aod.end(), ab) == aod.end());
xrpl::Dir const bod(*env.current(), keylet::ownerDir(bob.id()));
BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(bod.begin(), bod.end(), ab) == bod.end());
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(bod.begin(), bod.end(), bc) != bod.end());
xrpl::Dir const cod(*env.current(), keylet::ownerDir(carol.id()));
BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2);
xrpl::Dir const iod(*env.current(), keylet::ownerDir(gw.id()));
BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 4);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(iod.begin(), iod.end(), ab) == iod.end());
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(iod.begin(), iod.end(), bc) != iod.end());
}
env.close(5s);
env(escrow::cancel(bob, bob, bseq));
{
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(aseq))));
BEAST_EXPECT(!env.le(keylet::escrow(bob.id(), SeqProxy::rawSequence(bseq))));
xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id()));
BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(aod.begin(), aod.end(), ab) == aod.end());
xrpl::Dir const bod(*env.current(), keylet::ownerDir(bob.id()));
BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 1);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(bod.begin(), bod.end(), ab) == bod.end());
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(bod.begin(), bod.end(), bc) == bod.end());
xrpl::Dir const cod(*env.current(), keylet::ownerDir(carol.id()));
BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 1);
xrpl::Dir const iod(*env.current(), keylet::ownerDir(gw.id()));
BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 3);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(iod.begin(), iod.end(), ab) == iod.end());
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(iod.begin(), iod.end(), bc) == iod.end());
}
}
{
testcase("IOU Metadata to issuer");
Env env{*this, features};
env.fund(XRP(5000), alice, carol, gw);
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env.trust(usd(10'000), alice, carol);
env.close();
env(pay(gw, alice, usd(5000)));
env(pay(gw, carol, usd(5000)));
env.close();
auto const aseq = env.seq(alice);
env(escrow::create(alice, gw, usd(1'000)), escrow::kFinishTime(env.now() + 1s));
BEAST_EXPECT(
(*env.meta())[sfTransactionResult] == static_cast<std::uint8_t>(tesSUCCESS));
env.close(5s);
env(escrow::create(gw, carol, usd(1'000)),
escrow::kFinishTime(env.now() + 1s),
escrow::kCancelTime(env.now() + 2s),
Ter(tecNO_PERMISSION));
env.close(5s);
auto const ag = env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(aseq)));
BEAST_EXPECT(ag);
{
xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id()));
BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(aod.begin(), aod.end(), ag) != aod.end());
xrpl::Dir const cod(*env.current(), keylet::ownerDir(carol.id()));
BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 1);
xrpl::Dir const iod(*env.current(), keylet::ownerDir(gw.id()));
BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 3);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(iod.begin(), iod.end(), ag) != iod.end());
}
env.close(5s);
env(escrow::finish(alice, alice, aseq));
{
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(aseq))));
xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id()));
BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(aod.begin(), aod.end(), ag) == aod.end());
xrpl::Dir const cod(*env.current(), keylet::ownerDir(carol.id()));
BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 1);
xrpl::Dir const iod(*env.current(), keylet::ownerDir(gw.id()));
BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 2);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(iod.begin(), iod.end(), ag) == iod.end());
}
}
}
void
testIOURippleState(FeatureBitset features)
{
testcase("IOU RippleState");
using namespace test::jtx;
using namespace std::literals;
struct TestAccountData
{
Account src;
Account dst;
Account gw;
bool hasTrustline;
bool negative;
};
std::array<TestAccountData, 8> const tests = {{
// src > dst && src > issuer && dst no trustline
{.src = Account("alice2"),
.dst = Account("bob0"),
.gw = Account{"gw0"},
.hasTrustline = false,
.negative = true},
// src < dst && src < issuer && dst no trustline
{.src = Account("carol0"),
.dst = Account("dan1"),
.gw = Account{"gw1"},
.hasTrustline = false,
.negative = false},
// dst > src && dst > issuer && dst no trustline
{.src = Account("dan1"),
.dst = Account("alice2"),
.gw = Account{"gw0"},
.hasTrustline = false,
.negative = true},
// dst < src && dst < issuer && dst no trustline
{.src = Account("bob0"),
.dst = Account("carol0"),
.gw = Account{"gw1"},
.hasTrustline = false,
.negative = false},
// src > dst && src > issuer && dst has trustline
{.src = Account("alice2"),
.dst = Account("bob0"),
.gw = Account{"gw0"},
.hasTrustline = true,
.negative = true},
// src < dst && src < issuer && dst has trustline
{.src = Account("carol0"),
.dst = Account("dan1"),
.gw = Account{"gw1"},
.hasTrustline = true,
.negative = false},
// dst > src && dst > issuer && dst has trustline
{.src = Account("dan1"),
.dst = Account("alice2"),
.gw = Account{"gw0"},
.hasTrustline = true,
.negative = true},
// dst < src && dst < issuer && dst has trustline
{.src = Account("bob0"),
.dst = Account("carol0"),
.gw = Account{"gw1"},
.hasTrustline = true,
.negative = false},
}};
for (auto const& t : tests)
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const usd = t.gw["USD"];
env.fund(XRP(5000), t.src, t.dst, t.gw);
env(fset(t.gw, asfAllowTrustLineLocking));
env.close();
if (t.hasTrustline)
{
env.trust(usd(100'000), t.src, t.dst);
}
else
{
env.trust(usd(100'000), t.src);
}
env.close();
env(pay(t.gw, t.src, usd(10'000)));
if (t.hasTrustline)
env(pay(t.gw, t.dst, usd(10'000)));
env.close();
// src can create escrow
auto const seq1 = env.seq(t.src);
auto const delta = usd(1'000);
env(escrow::create(t.src, t.dst, delta),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150));
env.close();
// dst can finish escrow
auto const preSrc = env.balance(t.src, usd);
auto const preDst = env.balance(t.dst, usd);
env(escrow::finish(t.dst, t.src, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150));
env.close();
BEAST_EXPECT(env.balance(t.src, usd) == preSrc);
BEAST_EXPECT(env.balance(t.dst, usd) == preDst + delta);
}
}
void
testIOUGateway(FeatureBitset features)
{
testcase("IOU Gateway");
using namespace test::jtx;
using namespace std::literals;
struct TestAccountData
{
Account src;
Account dst;
bool hasTrustline;
};
// issuer is source
{
auto const gw = Account{"gateway"};
auto const alice = Account{"alice"};
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const usd = gw["USD"];
env.fund(XRP(5000), alice, gw);
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env.trust(usd(100'000), alice);
env.close();
env(pay(gw, alice, usd(10'000)));
env.close();
// issuer cannot create escrow
env(escrow::create(gw, alice, usd(1'000)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tecNO_PERMISSION));
env.close();
}
std::array<TestAccountData, 4> const gwDstTests = {{
// src > dst && src > issuer && dst has trustline
{.src = Account("alice2"), .dst = Account{"gw0"}, .hasTrustline = true},
// src < dst && src < issuer && dst has trustline
{.src = Account("carol0"), .dst = Account{"gw1"}, .hasTrustline = true},
// dst > src && dst > issuer && dst has trustline
{.src = Account("dan1"), .dst = Account{"gw0"}, .hasTrustline = true},
// dst < src && dst < issuer && dst has trustline
{.src = Account("bob0"), .dst = Account{"gw1"}, .hasTrustline = true},
}};
// issuer is destination
for (auto const& t : gwDstTests)
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const usd = t.dst["USD"];
env.fund(XRP(5000), t.dst, t.src);
env(fset(t.dst, asfAllowTrustLineLocking));
env.close();
env.trust(usd(100'000), t.src);
env.close();
env(pay(t.dst, t.src, usd(10'000)));
env.close();
// issuer can receive escrow
auto const seq1 = env.seq(t.src);
auto const preSrc = env.balance(t.src, usd);
env(escrow::create(t.src, t.dst, usd(1'000)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150));
env.close();
// issuer can finish escrow, no dest trustline
env(escrow::finish(t.dst, t.src, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150));
env.close();
auto const preAmount = 10'000;
BEAST_EXPECT(preSrc == usd(preAmount));
auto const postAmount = 9000;
BEAST_EXPECT(env.balance(t.src, usd) == usd(postAmount));
BEAST_EXPECT(env.balance(t.dst, usd) == usd(0));
}
// issuer is source and destination
{
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
env.fund(XRP(5000), gw);
env(fset(gw, asfAllowTrustLineLocking));
env.close();
// issuer cannot receive escrow
env(escrow::create(gw, gw, usd(1'000)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tecNO_PERMISSION));
env.close();
}
}
void
testIOULockedRate(FeatureBitset features)
{
testcase("IOU Locked Rate");
using namespace test::jtx;
using namespace std::literals;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
// test locked rate
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
env.fund(XRP(10'000), alice, bob, gw);
env(fset(gw, asfAllowTrustLineLocking));
env(rate(gw, 1.25));
env.close();
env.trust(usd(100'000), alice);
env.trust(usd(100'000), bob);
env.close();
env(pay(gw, alice, usd(10'000)));
env(pay(gw, bob, usd(10'000)));
env.close();
// alice can create escrow w/ xfer rate
auto const preAlice = env.balance(alice, usd);
auto const seq1 = env.seq(alice);
auto const delta = usd(125);
env(escrow::create(alice, bob, delta),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150));
env.close();
auto const transferRate = escrow::rate(env, alice, seq1);
BEAST_EXPECT(transferRate.value == std::uint32_t(1'000'000'000 * 1.25));
// bob can finish escrow
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150));
env.close();
BEAST_EXPECT(env.balance(alice, usd) == preAlice - delta);
BEAST_EXPECT(env.balance(bob, usd) == usd(10'100));
}
// test rate change - higher
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
env.fund(XRP(10'000), alice, bob, gw);
env(fset(gw, asfAllowTrustLineLocking));
env(rate(gw, 1.25));
env.close();
env.trust(usd(100'000), alice);
env.trust(usd(100'000), bob);
env.close();
env(pay(gw, alice, usd(10'000)));
env(pay(gw, bob, usd(10'000)));
env.close();
// alice can create escrow w/ xfer rate
auto const preAlice = env.balance(alice, usd);
auto const seq1 = env.seq(alice);
auto const delta = usd(125);
env(escrow::create(alice, bob, delta),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150));
env.close();
auto transferRate = escrow::rate(env, alice, seq1);
BEAST_EXPECT(transferRate.value == std::uint32_t(1'000'000'000 * 1.25));
// issuer changes rate higher
env(rate(gw, 1.26));
env.close();
// bob can finish escrow - rate unchanged
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150));
env.close();
BEAST_EXPECT(env.balance(alice, usd) == preAlice - delta);
BEAST_EXPECT(env.balance(bob, usd) == usd(10'100));
}
// test rate change - lower
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
env.fund(XRP(10'000), alice, bob, gw);
env(fset(gw, asfAllowTrustLineLocking));
env(rate(gw, 1.25));
env.close();
env.trust(usd(100'000), alice);
env.trust(usd(100'000), bob);
env.close();
env(pay(gw, alice, usd(10'000)));
env(pay(gw, bob, usd(10'000)));
env.close();
// alice can create escrow w/ xfer rate
auto const preAlice = env.balance(alice, usd);
auto const seq1 = env.seq(alice);
auto const delta = usd(125);
env(escrow::create(alice, bob, delta),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150));
env.close();
auto transferRate = escrow::rate(env, alice, seq1);
BEAST_EXPECT(transferRate.value == std::uint32_t(1'000'000'000 * 1.25));
// issuer changes rate lower
env(rate(gw, 1.00));
env.close();
// bob can finish escrow - rate changed
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150));
env.close();
BEAST_EXPECT(env.balance(alice, usd) == preAlice - delta);
BEAST_EXPECT(env.balance(bob, usd) == usd(10125));
}
// test cancel doesn't charge rate
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
env.fund(XRP(10'000), alice, bob, gw);
env(fset(gw, asfAllowTrustLineLocking));
env(rate(gw, 1.25));
env.close();
env.trust(usd(100'000), alice);
env.trust(usd(100'000), bob);
env.close();
env(pay(gw, alice, usd(10'000)));
env(pay(gw, bob, usd(10'000)));
env.close();
// alice can create escrow w/ xfer rate
auto const preAlice = env.balance(alice, usd);
auto const seq1 = env.seq(alice);
auto const delta = usd(125);
env(escrow::create(alice, bob, delta),
escrow::kFinishTime(env.now() + 1s),
escrow::kCancelTime(env.now() + 3s),
Fee(baseFee));
env.close();
auto transferRate = escrow::rate(env, alice, seq1);
BEAST_EXPECT(transferRate.value == std::uint32_t(1'000'000'000 * 1.25));
// issuer changes rate lower
env(rate(gw, 1.00));
env.close();
// alice can cancel escrow - rate is not charged
env(escrow::cancel(alice, alice, seq1), Fee(baseFee));
env.close();
BEAST_EXPECT(env.balance(alice, usd) == preAlice);
BEAST_EXPECT(env.balance(bob, usd) == usd(10000));
}
}
void
testIOULimitAmount(FeatureBitset features)
{
testcase("IOU Limit");
using namespace test::jtx;
using namespace std::literals;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
// test LimitAmount
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
env.fund(XRP(1'000), alice, bob, gw);
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env.trust(usd(10'000), alice, bob);
env.close();
env(pay(gw, alice, usd(1'000)));
env(pay(gw, bob, usd(1'000)));
env.close();
// alice can create escrow
auto seq1 = env.seq(alice);
auto const delta = usd(125);
env(escrow::create(alice, bob, delta),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150));
env.close();
// bob can finish
auto const preBobLimit = env.limit(bob, usd);
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150));
env.close();
auto const postBobLimit = env.limit(bob, usd);
// bob's limit is NOT changed
BEAST_EXPECT(postBobLimit == preBobLimit);
}
}
void
testIOURequireAuth(FeatureBitset features)
{
testcase("IOU Require Auth");
using namespace test::jtx;
using namespace std::literals;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
auto const aliceUSD = alice["USD"];
auto const bobUSD = bob["USD"];
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
env.fund(XRP(1'000), alice, bob, gw);
env(fset(gw, asfAllowTrustLineLocking));
env(fset(gw, asfRequireAuth));
env.close();
env(trust(gw, aliceUSD(10'000)), Txflags(tfSetfAuth));
env(trust(alice, usd(10'000)));
env(trust(bob, usd(10'000)));
env.close();
env(pay(gw, alice, usd(1'000)));
env.close();
// alice cannot create escrow - fails without auth
auto seq1 = env.seq(alice);
auto const delta = usd(125);
env(escrow::create(alice, bob, delta),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tecNO_AUTH));
env.close();
// set auth on bob
env(trust(gw, bobUSD(10'000)), Txflags(tfSetfAuth));
env(trust(bob, usd(10'000)));
env.close();
env(pay(gw, bob, usd(1'000)));
env.close();
// alice can create escrow - bob has auth
seq1 = env.seq(alice);
env(escrow::create(alice, bob, delta),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150));
env.close();
// bob can finish
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150));
env.close();
}
void
testIOUFreeze(FeatureBitset features)
{
testcase("IOU Freeze");
using namespace test::jtx;
using namespace std::literals;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
// test Global Freeze
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
env.fund(XRP(10'000), alice, bob, gw);
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env.trust(usd(100'000), alice);
env.trust(usd(100'000), bob);
env.close();
env(pay(gw, alice, usd(10'000)));
env(pay(gw, bob, usd(10'000)));
env.close();
env(fset(gw, asfGlobalFreeze));
env.close();
// setup transaction
auto seq1 = env.seq(alice);
auto const delta = usd(125);
// create escrow fails - frozen trustline
env(escrow::create(alice, bob, delta),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tecFROZEN));
env.close();
// clear global freeze
env(fclear(gw, asfGlobalFreeze));
env.close();
// create escrow success
seq1 = env.seq(alice);
env(escrow::create(alice, bob, delta),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150));
env.close();
// set global freeze
env(fset(gw, asfGlobalFreeze));
env.close();
// bob finish escrow success regardless of frozen assets
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150));
env.close();
// clear global freeze
env(fclear(gw, asfGlobalFreeze));
env.close();
// create escrow success
seq1 = env.seq(alice);
env(escrow::create(alice, bob, delta),
escrow::kCondition(escrow::kCb1),
escrow::kCancelTime(env.now() + 1s),
Fee(baseFee * 150));
env.close();
// set global freeze
env(fset(gw, asfGlobalFreeze));
env.close();
// bob cancel escrow success regardless of frozen assets
env(escrow::cancel(bob, alice, seq1), Fee(baseFee));
env.close();
}
// test Individual Freeze
{
// Env Setup
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
env.fund(XRP(10'000), alice, bob, gw);
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env(trust(alice, usd(100'000)));
env(trust(bob, usd(100'000)));
env.close();
env(pay(gw, alice, usd(10'000)));
env(pay(gw, bob, usd(10'000)));
env.close();
// set freeze on alice trustline
env(trust(gw, usd(10'000), alice, tfSetFreeze));
env.close();
// setup transaction
auto seq1 = env.seq(alice);
auto const delta = usd(125);
// create escrow fails - frozen trustline
env(escrow::create(alice, bob, delta),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tecFROZEN));
env.close();
// clear freeze on alice trustline
env(trust(gw, usd(10'000), alice, tfClearFreeze));
env.close();
// create escrow success
seq1 = env.seq(alice);
env(escrow::create(alice, bob, delta),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150));
env.close();
// set freeze on bob trustline
env(trust(gw, usd(10'000), bob, tfSetFreeze));
env.close();
// bob finish escrow success regardless of frozen assets
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150));
env.close();
// reset freeze on bob and alice trustline
env(trust(gw, usd(10'000), alice, tfClearFreeze));
env(trust(gw, usd(10'000), bob, tfClearFreeze));
env.close();
// create escrow success
seq1 = env.seq(alice);
env(escrow::create(alice, bob, delta),
escrow::kCondition(escrow::kCb1),
escrow::kCancelTime(env.now() + 1s),
Fee(baseFee * 150));
env.close();
// set freeze on bob trustline
env(trust(gw, usd(10'000), bob, tfSetFreeze));
env.close();
// bob cancel escrow success regardless of frozen assets
env(escrow::cancel(bob, alice, seq1), Fee(baseFee));
env.close();
}
// test Deep Freeze
{
// Env Setup
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
env.fund(XRP(10'000), alice, bob, gw);
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env(trust(alice, usd(100'000)));
env(trust(bob, usd(100'000)));
env.close();
env(pay(gw, alice, usd(10'000)));
env(pay(gw, bob, usd(10'000)));
env.close();
// set freeze on alice trustline
env(trust(gw, usd(10'000), alice, tfSetFreeze | tfSetDeepFreeze));
env.close();
// setup transaction
auto seq1 = env.seq(alice);
auto const delta = usd(125);
// create escrow fails - frozen trustline
env(escrow::create(alice, bob, delta),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tecFROZEN));
env.close();
// clear freeze on alice trustline
env(trust(gw, usd(10'000), alice, tfClearFreeze | tfClearDeepFreeze));
env.close();
// create escrow success
seq1 = env.seq(alice);
env(escrow::create(alice, bob, delta),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150));
env.close();
// set freeze on bob trustline
env(trust(gw, usd(10'000), bob, tfSetFreeze | tfSetDeepFreeze));
env.close();
// bob finish escrow fails because of deep frozen assets
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150),
Ter(tecFROZEN));
env.close();
// reset freeze on alice and bob trustline
env(trust(gw, usd(10'000), alice, tfClearFreeze | tfClearDeepFreeze));
env(trust(gw, usd(10'000), bob, tfClearFreeze | tfClearDeepFreeze));
env.close();
// create escrow success
seq1 = env.seq(alice);
env(escrow::create(alice, bob, delta),
escrow::kCondition(escrow::kCb1),
escrow::kCancelTime(env.now() + 1s),
Fee(baseFee * 150));
env.close();
// set freeze on bob trustline
env(trust(gw, usd(10'000), bob, tfSetFreeze | tfSetDeepFreeze));
env.close();
// bob cancel escrow fails because of deep frozen assets
env(escrow::cancel(bob, alice, seq1), Fee(baseFee), Ter(tesSUCCESS));
env.close();
}
}
void
testIOUInsufficientFunds(FeatureBitset features)
{
testcase("IOU Insufficient Funds");
using namespace test::jtx;
using namespace std::literals;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
{
// test tecPATH_PARTIAL
// ie. has 10'000, escrow 1'000 then try to pay 10'000
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
env.fund(XRP(10'000), alice, bob, gw);
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env.trust(usd(100'000), alice);
env.trust(usd(100'000), bob);
env.close();
env(pay(gw, alice, usd(10'000)));
env(pay(gw, bob, usd(10'000)));
env.close();
// create escrow success
auto const delta = usd(1'000);
env(escrow::create(alice, bob, delta),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150));
env.close();
env(pay(alice, gw, usd(10'000)), Ter(tecPATH_PARTIAL));
}
{
// test tecINSUFFICIENT_FUNDS
// ie. has 10'000 escrow 1'000 then try to escrow 10'000
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
env.fund(XRP(10'000), alice, bob, gw);
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env.trust(usd(100'000), alice);
env.trust(usd(100'000), bob);
env.close();
env(pay(gw, alice, usd(10'000)));
env(pay(gw, bob, usd(10'000)));
env.close();
auto const delta = usd(1'000);
env(escrow::create(alice, bob, delta),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150));
env.close();
env(escrow::create(alice, bob, usd(10'000)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tecINSUFFICIENT_FUNDS));
env.close();
}
}
void
testIOUPrecisionLoss(FeatureBitset features)
{
testcase("IOU Precision Loss");
using namespace test::jtx;
using namespace std::literals;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
// test min create precision loss
{
Env env(*this, features);
auto const baseFee = env.current()->fees().base;
env.fund(XRP(10'000), alice, bob, gw);
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env.trust(usd(100000000000000000), alice);
env.trust(usd(100000000000000000), bob);
env.close();
env(pay(gw, alice, usd(10000000000000000)));
env(pay(gw, bob, usd(1)));
env.close();
bool const largeMantissa = features[featureSingleAssetVault] ||
features[featureLendingProtocol] || features[featureMPTokensV2];
// alice cannot create escrow for 1/10 iou - precision loss
env(escrow::create(alice, bob, usd(1)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(largeMantissa ? (TER)tesSUCCESS : (TER)tecPRECISION_LOSS));
env.close();
auto const seq1 = env.seq(alice);
// alice can create escrow for 1'000 iou
env(escrow::create(alice, bob, usd(1'000)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150));
env.close();
// bob finish escrow success
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150));
env.close();
}
}
void
testMPTEnablement(FeatureBitset features)
{
testcase("MPT Enablement");
using namespace jtx;
using namespace std::chrono;
for (bool const withTokenEscrow : {false, true})
{
auto const amend = withTokenEscrow ? features : features - featureTokenEscrow;
Env env{*this, amend};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
env.fund(XRP(5000), bob);
MPTTester mptGw(env, gw, {.holders = {alice}});
mptGw.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
mptGw.authorize({.account = alice});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(10'000)));
env.close();
auto const createResult = withTokenEscrow ? Ter(tesSUCCESS) : Ter(temBAD_AMOUNT);
auto const finishResult = withTokenEscrow ? Ter(tesSUCCESS) : Ter(tecNO_TARGET);
auto const seq1 = env.seq(alice);
env(escrow::create(alice, bob, mpt(1'000)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
createResult);
env.close();
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150),
finishResult);
env.close();
auto const seq2 = env.seq(alice);
env(escrow::create(alice, bob, mpt(1'000)),
escrow::kCondition(escrow::kCb2),
escrow::kFinishTime(env.now() + 1s),
escrow::kCancelTime(env.now() + 2s),
Fee(baseFee * 150),
createResult);
env.close();
env(escrow::cancel(bob, alice, seq2), finishResult);
env.close();
}
}
void
testMPTCreatePreflight(FeatureBitset features)
{
testcase("MPT Create Preflight");
using namespace test::jtx;
using namespace std::literals;
for (bool const withMPT : {true, false})
{
auto const amend = withMPT ? features : features - featureMPTokensV1;
Env env{*this, amend};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
env.fund(XRP(1'000), alice, bob, gw);
json::Value jv = escrow::create(alice, bob, XRP(1));
jv.removeMember(jss::Amount);
jv[jss::Amount][jss::mpt_issuance_id] =
"00000004A407AF5856CCF3C42619DAA925813FC955C72983";
jv[jss::Amount][jss::value] = "-1";
auto const result = withMPT ? Ter(temBAD_AMOUNT) : Ter(temDISABLED);
env(jv,
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
result);
env.close();
}
// temBAD_AMOUNT: amount < 0
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
mptGw.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
mptGw.authorize({.account = alice});
mptGw.authorize({.account = bob});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(10'000)));
env(pay(gw, bob, mpt(10'000)));
env.close();
env(escrow::create(alice, bob, mpt(-1)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(temBAD_AMOUNT));
env.close();
}
}
void
testMPTCreatePreclaim(FeatureBitset features)
{
testcase("MPT Create Preclaim");
using namespace test::jtx;
using namespace std::literals;
// tecNO_PERMISSION: issuer is the same as the account
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const gw = Account("gw");
MPTTester mptGw(env, gw, {.holders = {alice}});
mptGw.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
mptGw.authorize({.account = alice});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(10'000)));
env.close();
env(escrow::create(gw, alice, mpt(1)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tecNO_PERMISSION));
env.close();
}
// tecOBJECT_NOT_FOUND: mpt does not exist
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
env.fund(XRP(10'000), alice, bob, gw);
env.close();
auto const mpt = xrpl::test::jtx::MPT(alice.name(), makeMptID(env.seq(alice), alice));
json::Value jv = escrow::create(alice, bob, mpt(2));
jv[jss::Amount][jss::mpt_issuance_id] =
"00000004A407AF5856CCF3C42619DAA925813FC955C72983";
env(jv,
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tecOBJECT_NOT_FOUND));
env.close();
}
// tecNO_PERMISSION: tfMPTCanEscrow is not enabled
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
mptGw.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});
mptGw.authorize({.account = alice});
mptGw.authorize({.account = bob});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(10'000)));
env(pay(gw, bob, mpt(10'000)));
env.close();
env(escrow::create(alice, bob, mpt(3)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tecNO_PERMISSION));
env.close();
}
// tecOBJECT_NOT_FOUND: account does not have the mpt
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
mptGw.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
auto const mpt = mptGw["MPT"];
env(escrow::create(alice, bob, mpt(4)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tecOBJECT_NOT_FOUND));
env.close();
}
// tecNO_AUTH: requireAuth set: account not authorized
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
mptGw.create(
{.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth});
mptGw.authorize({.account = alice});
mptGw.authorize({.account = gw, .holder = alice});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(10'000)));
env.close();
// unauthorize account
mptGw.authorize({.account = gw, .holder = alice, .flags = tfMPTUnauthorize});
env(escrow::create(alice, bob, mpt(5)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tecNO_AUTH));
env.close();
}
// tecNO_AUTH: requireAuth set: dest not authorized
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
mptGw.create(
{.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth});
mptGw.authorize({.account = alice});
mptGw.authorize({.account = gw, .holder = alice});
mptGw.authorize({.account = bob});
mptGw.authorize({.account = gw, .holder = bob});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(10'000)));
env(pay(gw, bob, mpt(10'000)));
env.close();
// unauthorize dest
mptGw.authorize({.account = gw, .holder = bob, .flags = tfMPTUnauthorize});
env(escrow::create(alice, bob, mpt(6)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tecNO_AUTH));
env.close();
}
// tecLOCKED: issuer has locked the account
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
mptGw.create(
{.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock});
mptGw.authorize({.account = alice});
mptGw.authorize({.account = bob});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(10'000)));
env(pay(gw, bob, mpt(10'000)));
env.close();
// lock account
mptGw.set({.account = gw, .holder = alice, .flags = tfMPTLock});
env(escrow::create(alice, bob, mpt(7)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tecLOCKED));
env.close();
}
// tecLOCKED: issuer has locked the dest
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
mptGw.create(
{.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock});
mptGw.authorize({.account = alice});
mptGw.authorize({.account = bob});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(10'000)));
env(pay(gw, bob, mpt(10'000)));
env.close();
// lock dest
mptGw.set({.account = gw, .holder = bob, .flags = tfMPTLock});
env(escrow::create(alice, bob, mpt(8)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tecLOCKED));
env.close();
}
// tecNO_AUTH: mpt cannot be transferred
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
mptGw.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow});
mptGw.authorize({.account = alice});
mptGw.authorize({.account = bob});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(10'000)));
env(pay(gw, bob, mpt(10'000)));
env.close();
env(escrow::create(alice, bob, mpt(9)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tecNO_AUTH));
env.close();
}
// tecINSUFFICIENT_FUNDS: spendable amount is zero
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
mptGw.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
mptGw.authorize({.account = alice});
mptGw.authorize({.account = bob});
auto const mpt = mptGw["MPT"];
env(pay(gw, bob, mpt(10)));
env.close();
env(escrow::create(alice, bob, mpt(11)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tecINSUFFICIENT_FUNDS));
env.close();
}
// tecINSUFFICIENT_FUNDS: spendable amount is less than the amount
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
mptGw.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
mptGw.authorize({.account = alice});
mptGw.authorize({.account = bob});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(10)));
env(pay(gw, bob, mpt(10)));
env.close();
env(escrow::create(alice, bob, mpt(11)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tecINSUFFICIENT_FUNDS));
env.close();
}
}
void
testMPTFinishPreclaim(FeatureBitset features)
{
testcase("MPT Finish Preclaim");
using namespace test::jtx;
using namespace std::literals;
// tecNO_AUTH: requireAuth set: dest not authorized
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
mptGw.create(
{.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth});
mptGw.authorize({.account = alice});
mptGw.authorize({.account = gw, .holder = alice});
mptGw.authorize({.account = bob});
mptGw.authorize({.account = gw, .holder = bob});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(10'000)));
env(pay(gw, bob, mpt(10'000)));
env.close();
auto const seq1 = env.seq(alice);
env(escrow::create(alice, bob, mpt(10)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
// unauthorize dest
mptGw.authorize({.account = gw, .holder = bob, .flags = tfMPTUnauthorize});
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150),
Ter(tecNO_AUTH));
env.close();
}
// tecOBJECT_NOT_FOUND: MPT issuance does not exist
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10'000), alice, bob);
env.close();
auto const seq1 = env.seq(alice);
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
Sandbox sb(&view, ApplyFlags::None);
auto sleNew =
std::make_shared<SLE>(keylet::escrow(alice, SeqProxy::rawSequence(seq1)));
MPTIssue const mpt{MPTIssue{makeMptID(1, AccountID{0x4985601})}};
STAmount const amt(mpt, 10);
sleNew->setAccountID(sfDestination, bob);
sleNew->setFieldAmount(sfAmount, amt);
sb.insert(sleNew);
sb.apply(view);
return true;
});
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150),
Ter(tecOBJECT_NOT_FOUND));
env.close();
}
// tecLOCKED: issuer has locked the dest
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
mptGw.create(
{.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock});
mptGw.authorize({.account = alice});
mptGw.authorize({.account = bob});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(10'000)));
env(pay(gw, bob, mpt(10'000)));
env.close();
auto const seq1 = env.seq(alice);
env(escrow::create(alice, bob, mpt(8)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
// lock dest
mptGw.set({.account = gw, .holder = bob, .flags = tfMPTLock});
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150),
Ter(tecLOCKED));
env.close();
}
}
void
testMPTFinishDoApply(FeatureBitset features)
{
testcase("MPT Finish Do Apply");
using namespace test::jtx;
using namespace std::literals;
// tecINSUFFICIENT_RESERVE: insufficient reserve to create MPT
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const acctReserve = env.current()->fees().reserve;
auto const incReserve = env.current()->fees().increment;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
env.fund(acctReserve + (incReserve - 1), bob);
env.close();
MPTTester mptGw(env, gw, {.holders = {alice}});
mptGw.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
mptGw.authorize({.account = alice});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(10'000)));
env.close();
auto const seq1 = env.seq(alice);
env(escrow::create(alice, bob, mpt(10)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150),
Ter(tecINSUFFICIENT_RESERVE));
env.close();
}
// tesSUCCESS: bob submits; finish MPT created
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
env.fund(XRP(10'000), bob);
env.close();
MPTTester mptGw(env, gw, {.holders = {alice}});
mptGw.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
mptGw.authorize({.account = alice});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(10'000)));
env.close();
auto const seq1 = env.seq(alice);
env(escrow::create(alice, bob, mpt(10)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
}
// tecNO_PERMISSION: carol submits; finish MPT not created
{
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");
auto const gw = Account("gw");
env.fund(XRP(10'000), bob, carol);
env.close();
MPTTester mptGw(env, gw, {.holders = {alice}});
mptGw.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
mptGw.authorize({.account = alice});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(10'000)));
env.close();
auto const seq1 = env.seq(alice);
env(escrow::create(alice, bob, mpt(10)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
env(escrow::finish(carol, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150),
Ter(tecNO_PERMISSION));
env.close();
}
}
void
testMPTCancelPreclaim(FeatureBitset features)
{
testcase("MPT Cancel Preclaim");
using namespace test::jtx;
using namespace std::literals;
// tecNO_AUTH: requireAuth set: account not authorized
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
mptGw.create(
{.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth});
mptGw.authorize({.account = alice});
mptGw.authorize({.account = gw, .holder = alice});
mptGw.authorize({.account = bob});
mptGw.authorize({.account = gw, .holder = bob});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(10'000)));
env(pay(gw, bob, mpt(10'000)));
env.close();
auto const seq1 = env.seq(alice);
env(escrow::create(alice, bob, mpt(10)),
escrow::kCancelTime(env.now() + 2s),
escrow::kCondition(escrow::kCb1),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
// unauthorize account
mptGw.authorize({.account = gw, .holder = alice, .flags = tfMPTUnauthorize});
env(escrow::cancel(bob, alice, seq1), Ter(tecNO_AUTH));
env.close();
}
// tecOBJECT_NOT_FOUND: MPT issuance does not exist
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(10'000), alice, bob);
auto const seq1 = env.seq(alice);
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
Sandbox sb(&view, ApplyFlags::None);
auto sleNew =
std::make_shared<SLE>(keylet::escrow(alice, SeqProxy::rawSequence(seq1)));
MPTIssue const mpt{MPTIssue{makeMptID(1, AccountID{0x4985601})}};
STAmount const amt(mpt, 10);
sleNew->setAccountID(sfDestination, bob);
sleNew->setFieldAmount(sfAmount, amt);
sb.insert(sleNew);
sb.apply(view);
return true;
});
env(escrow::cancel(bob, alice, seq1), Fee(baseFee), Ter(tecOBJECT_NOT_FOUND));
env.close();
}
}
void
testMPTBalances(FeatureBitset features)
{
testcase("MPT Balances");
using namespace jtx;
using namespace std::chrono;
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");
auto const gw = Account("gw");
env.fund(XRP(5000), bob);
MPTTester mptGw(env, gw, {.holders = {alice, carol}});
mptGw.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
mptGw.authorize({.account = alice});
mptGw.authorize({.account = carol});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(10'000)));
env(pay(gw, carol, mpt(10'000)));
env.close();
auto outstandingMPT = env.balance(gw, mpt);
// Create & Finish Escrow
auto const seq1 = env.seq(alice);
{
auto const preAliceMPT = env.balance(alice, mpt);
auto const preBobMPT = env.balance(bob, mpt);
env(escrow::create(alice, bob, mpt(1'000)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(env.balance(alice, mpt) == preAliceMPT - mpt(1'000));
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 1'000);
BEAST_EXPECT(env.balance(bob, mpt) == preBobMPT);
BEAST_EXPECT(mptEscrowed(env, bob, mpt) == 0);
BEAST_EXPECT(env.balance(gw, mpt) == outstandingMPT);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 1'000);
}
{
auto const preAliceMPT = env.balance(alice, mpt);
auto const preBobMPT = env.balance(bob, mpt);
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(env.balance(alice, mpt) == preAliceMPT);
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 0);
BEAST_EXPECT(env.balance(bob, mpt) == preBobMPT + mpt(1'000));
BEAST_EXPECT(mptEscrowed(env, bob, mpt) == 0);
BEAST_EXPECT(env.balance(gw, mpt) == outstandingMPT);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 0);
}
// Create & Cancel Escrow
auto const seq2 = env.seq(alice);
{
auto const preAliceMPT = env.balance(alice, mpt);
auto const preBobMPT = env.balance(bob, mpt);
env(escrow::create(alice, bob, mpt(1'000)),
escrow::kCondition(escrow::kCb2),
escrow::kFinishTime(env.now() + 1s),
escrow::kCancelTime(env.now() + 2s),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(env.balance(alice, mpt) == preAliceMPT - mpt(1'000));
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 1'000);
BEAST_EXPECT(env.balance(bob, mpt) == preBobMPT);
BEAST_EXPECT(mptEscrowed(env, bob, mpt) == 0);
BEAST_EXPECT(env.balance(gw, mpt) == outstandingMPT);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 1'000);
}
{
auto const preAliceMPT = env.balance(alice, mpt);
auto const preBobMPT = env.balance(bob, mpt);
env(escrow::cancel(bob, alice, seq2), Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(env.balance(alice, mpt) == preAliceMPT + mpt(1'000));
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 0);
BEAST_EXPECT(env.balance(bob, mpt) == preBobMPT);
BEAST_EXPECT(mptEscrowed(env, bob, mpt) == 0);
BEAST_EXPECT(env.balance(gw, mpt) == outstandingMPT);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 0);
}
// Self Escrow Create & Finish
{
auto const seq = env.seq(alice);
auto const preAliceMPT = env.balance(alice, mpt);
env(escrow::create(alice, alice, mpt(1'000)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(env.balance(alice, mpt) == preAliceMPT - mpt(1'000));
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 1'000);
BEAST_EXPECT(env.balance(gw, mpt) == outstandingMPT);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 1'000);
env(escrow::finish(alice, alice, seq),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(env.balance(alice, mpt) == preAliceMPT);
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 0);
BEAST_EXPECT(env.balance(gw, mpt) == outstandingMPT);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 0);
}
// Self Escrow Create & Cancel
{
auto const seq = env.seq(alice);
auto const preAliceMPT = env.balance(alice, mpt);
env(escrow::create(alice, alice, mpt(1'000)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
escrow::kCancelTime(env.now() + 2s),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(env.balance(alice, mpt) == preAliceMPT - mpt(1'000));
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 1'000);
BEAST_EXPECT(env.balance(gw, mpt) == outstandingMPT);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 1'000);
env(escrow::cancel(alice, alice, seq), Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(env.balance(alice, mpt) == preAliceMPT);
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 0);
BEAST_EXPECT(env.balance(gw, mpt) == outstandingMPT);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 0);
}
// Multiple Escrows
{
auto const preAliceMPT = env.balance(alice, mpt);
auto const preBobMPT = env.balance(bob, mpt);
auto const preCarolMPT = env.balance(carol, mpt);
env(escrow::create(alice, bob, mpt(1'000)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
env(escrow::create(carol, bob, mpt(1'000)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(env.balance(alice, mpt) == preAliceMPT - mpt(1'000));
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 1'000);
BEAST_EXPECT(env.balance(bob, mpt) == preBobMPT);
BEAST_EXPECT(mptEscrowed(env, bob, mpt) == 0);
BEAST_EXPECT(env.balance(carol, mpt) == preCarolMPT - mpt(1'000));
BEAST_EXPECT(mptEscrowed(env, carol, mpt) == 1'000);
BEAST_EXPECT(env.balance(gw, mpt) == outstandingMPT);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 2'000);
}
// Max MPT Amount Issued (Escrow 1 MPT)
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
mptGw.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
mptGw.authorize({.account = alice});
mptGw.authorize({.account = bob});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(kMaxMpTokenAmount)));
env.close();
auto const preAliceMPT = env.balance(alice, mpt);
auto const preBobMPT = env.balance(bob, mpt);
auto const outstandingMPT = env.balance(gw, mpt);
auto const seq1 = env.seq(alice);
env(escrow::create(alice, bob, mpt(1)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150));
env.close();
BEAST_EXPECT(env.balance(alice, mpt) == preAliceMPT - mpt(1));
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 1);
BEAST_EXPECT(env.balance(bob, mpt) == preBobMPT);
BEAST_EXPECT(mptEscrowed(env, bob, mpt) == 0);
BEAST_EXPECT(env.balance(gw, mpt) == outstandingMPT);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 1);
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(env.balance(alice, mpt) == preAliceMPT - mpt(1));
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 0);
BEAST_EXPECT(
!env.le(keylet::mptoken(mpt.mpt(), alice))->isFieldPresent(sfLockedAmount));
BEAST_EXPECT(env.balance(bob, mpt) == preBobMPT + mpt(1));
BEAST_EXPECT(mptEscrowed(env, bob, mpt) == 0);
BEAST_EXPECT(env.balance(gw, mpt) == outstandingMPT);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 0);
BEAST_EXPECT(
!env.le(keylet::mptokenIssuance(mpt.mpt()))->isFieldPresent(sfLockedAmount));
}
// Max MPT Amount Issued (Escrow Max MPT)
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
mptGw.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
mptGw.authorize({.account = alice});
mptGw.authorize({.account = bob});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(kMaxMpTokenAmount)));
env.close();
auto const preAliceMPT = env.balance(alice, mpt);
auto const preBobMPT = env.balance(bob, mpt);
auto const outstandingMPT = env.balance(gw, mpt);
// Escrow Max MPT - 10
auto const seq1 = env.seq(alice);
env(escrow::create(alice, bob, mpt(kMaxMpTokenAmount - 10)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150));
env.close();
// Escrow 10 MPT
auto const seq2 = env.seq(alice);
env(escrow::create(alice, bob, mpt(10)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150));
env.close();
BEAST_EXPECT(env.balance(alice, mpt) == preAliceMPT - mpt(kMaxMpTokenAmount));
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == kMaxMpTokenAmount);
BEAST_EXPECT(env.balance(bob, mpt) == preBobMPT);
BEAST_EXPECT(mptEscrowed(env, bob, mpt) == 0);
BEAST_EXPECT(env.balance(gw, mpt) == outstandingMPT);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == kMaxMpTokenAmount);
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
env(escrow::finish(bob, alice, seq2),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(env.balance(alice, mpt) == preAliceMPT - mpt(kMaxMpTokenAmount));
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 0);
BEAST_EXPECT(env.balance(bob, mpt) == preBobMPT + mpt(kMaxMpTokenAmount));
BEAST_EXPECT(mptEscrowed(env, bob, mpt) == 0);
BEAST_EXPECT(env.balance(gw, mpt) == outstandingMPT);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 0);
}
}
void
testMPTMetaAndOwnership(FeatureBitset features)
{
using namespace jtx;
using namespace std::chrono;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
auto const gw = Account{"gateway"};
{
testcase("MPT Metadata to self");
Env env{*this, features};
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
mptGw.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
mptGw.authorize({.account = alice});
mptGw.authorize({.account = bob});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(10'000)));
env(pay(gw, bob, mpt(10'000)));
env.close();
auto const aseq = env.seq(alice);
auto const bseq = env.seq(bob);
env(escrow::create(alice, alice, mpt(1'000)),
escrow::kFinishTime(env.now() + 1s),
escrow::kCancelTime(env.now() + 500s));
BEAST_EXPECT(
(*env.meta())[sfTransactionResult] == static_cast<std::uint8_t>(tesSUCCESS));
env.close(5s);
auto const aa = env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(aseq)));
BEAST_EXPECT(aa);
{
xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id()));
BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(aod.begin(), aod.end(), aa) != aod.end());
}
{
xrpl::Dir const iod(*env.current(), keylet::ownerDir(gw.id()));
BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 1);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(iod.begin(), iod.end(), aa) == iod.end());
}
env(escrow::create(bob, bob, mpt(1'000)),
escrow::kFinishTime(env.now() + 1s),
escrow::kCancelTime(env.now() + 2s));
BEAST_EXPECT(
(*env.meta())[sfTransactionResult] == static_cast<std::uint8_t>(tesSUCCESS));
env.close(5s);
auto const bb = env.le(keylet::escrow(bob.id(), SeqProxy::rawSequence(bseq)));
BEAST_EXPECT(bb);
{
xrpl::Dir const bod(*env.current(), keylet::ownerDir(bob.id()));
BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(bod.begin(), bod.end(), bb) != bod.end());
}
env.close(5s);
env(escrow::finish(alice, alice, aseq));
{
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(aseq))));
BEAST_EXPECT(
(*env.meta())[sfTransactionResult] == static_cast<std::uint8_t>(tesSUCCESS));
xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id()));
BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(aod.begin(), aod.end(), aa) == aod.end());
xrpl::Dir const bod(*env.current(), keylet::ownerDir(bob.id()));
BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(bod.begin(), bod.end(), bb) != bod.end());
}
env.close(5s);
env(escrow::cancel(bob, bob, bseq));
{
BEAST_EXPECT(!env.le(keylet::escrow(bob.id(), SeqProxy::rawSequence(bseq))));
BEAST_EXPECT(
(*env.meta())[sfTransactionResult] == static_cast<std::uint8_t>(tesSUCCESS));
xrpl::Dir const bod(*env.current(), keylet::ownerDir(bob.id()));
BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 1);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(bod.begin(), bod.end(), bb) == bod.end());
}
}
{
testcase("MPT Metadata to other");
Env env{*this, features};
MPTTester mptGw(env, gw, {.holders = {alice, bob, carol}});
mptGw.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
mptGw.authorize({.account = alice});
mptGw.authorize({.account = bob});
mptGw.authorize({.account = carol});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(10'000)));
env(pay(gw, bob, mpt(10'000)));
env(pay(gw, carol, mpt(10'000)));
env.close();
auto const aseq = env.seq(alice);
auto const bseq = env.seq(bob);
env(escrow::create(alice, bob, mpt(1'000)), escrow::kFinishTime(env.now() + 1s));
BEAST_EXPECT(
(*env.meta())[sfTransactionResult] == static_cast<std::uint8_t>(tesSUCCESS));
env.close(5s);
env(escrow::create(bob, carol, mpt(1'000)),
escrow::kFinishTime(env.now() + 1s),
escrow::kCancelTime(env.now() + 2s));
BEAST_EXPECT(
(*env.meta())[sfTransactionResult] == static_cast<std::uint8_t>(tesSUCCESS));
env.close(5s);
auto const ab = env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(aseq)));
BEAST_EXPECT(ab);
auto const bc = env.le(keylet::escrow(bob.id(), SeqProxy::rawSequence(bseq)));
BEAST_EXPECT(bc);
{
xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id()));
BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(aod.begin(), aod.end(), ab) != aod.end());
xrpl::Dir const bod(*env.current(), keylet::ownerDir(bob.id()));
BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 3);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(bod.begin(), bod.end(), ab) != bod.end());
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(bod.begin(), bod.end(), bc) != bod.end());
xrpl::Dir const cod(*env.current(), keylet::ownerDir(carol.id()));
BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(cod.begin(), cod.end(), bc) != cod.end());
}
env.close(5s);
env(escrow::finish(alice, alice, aseq));
{
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(aseq))));
BEAST_EXPECT(env.le(keylet::escrow(bob.id(), SeqProxy::rawSequence(bseq))));
xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id()));
BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(aod.begin(), aod.end(), ab) == aod.end());
xrpl::Dir const bod(*env.current(), keylet::ownerDir(bob.id()));
BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(bod.begin(), bod.end(), ab) == bod.end());
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(bod.begin(), bod.end(), bc) != bod.end());
xrpl::Dir const cod(*env.current(), keylet::ownerDir(carol.id()));
BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2);
}
env.close(5s);
env(escrow::cancel(bob, bob, bseq));
{
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(aseq))));
BEAST_EXPECT(!env.le(keylet::escrow(bob.id(), SeqProxy::rawSequence(bseq))));
xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id()));
BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(aod.begin(), aod.end(), ab) == aod.end());
xrpl::Dir const bod(*env.current(), keylet::ownerDir(bob.id()));
BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 1);
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(bod.begin(), bod.end(), ab) == bod.end());
BEAST_EXPECT(
// NOLINTNEXTLINE(modernize-use-ranges)
std::find(bod.begin(), bod.end(), bc) == bod.end());
xrpl::Dir const cod(*env.current(), keylet::ownerDir(carol.id()));
BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 1);
}
}
}
void
testMPTGateway(FeatureBitset features)
{
testcase("MPT Gateway Balances");
using namespace test::jtx;
using namespace std::literals;
// issuer is dest; alice w/ authorization
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const gw = Account("gw");
MPTTester mptGw(env, gw, {.holders = {alice}});
mptGw.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
mptGw.authorize({.account = alice});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(10'000)));
env.close();
// issuer can be destination
auto const seq1 = env.seq(alice);
auto const preAliceMPT = env.balance(alice, mpt);
auto const preOutstanding = env.balance(gw, mpt);
auto const preEscrowed = issuerMPTEscrowed(env, mpt);
BEAST_EXPECT(preOutstanding == mpt(-10'000));
BEAST_EXPECT(preEscrowed == 0);
env(escrow::create(alice, gw, mpt(1'000)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150));
env.close();
BEAST_EXPECT(env.balance(alice, mpt) == preAliceMPT - mpt(1'000));
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 1'000);
BEAST_EXPECT(env.balance(gw, mpt) == preOutstanding);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == preEscrowed + 1'000);
// issuer (dest) can finish escrow
env(escrow::finish(gw, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150));
env.close();
BEAST_EXPECT(env.balance(alice, mpt) == preAliceMPT - mpt(1'000));
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 0);
BEAST_EXPECT(env.balance(gw, mpt) == preOutstanding + mpt(1'000));
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == preEscrowed);
}
}
void
testMPTLockedRate(FeatureBitset features)
{
testcase("MPT Locked Rate");
using namespace test::jtx;
using namespace std::literals;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
// test locked rate: finish
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
mptGw.create(
{.transferFee = 25000,
.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanEscrow | tfMPTCanTransfer});
mptGw.authorize({.account = alice});
mptGw.authorize({.account = bob});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(10'000)));
env(pay(gw, bob, mpt(10'000)));
env.close();
// alice can create escrow w/ xfer rate
auto const preAlice = env.balance(alice, mpt);
auto const seq1 = env.seq(alice);
auto const delta = mpt(125);
env(escrow::create(alice, bob, mpt(125)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150));
env.close();
auto const transferRate = escrow::rate(env, alice, seq1);
BEAST_EXPECT(transferRate.value == std::uint32_t(1'000'000'000 * 1.25));
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 125);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 125);
BEAST_EXPECT(env.balance(gw, mpt) == mpt(-20'000));
// bob can finish escrow
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150));
env.close();
BEAST_EXPECT(env.balance(alice, mpt) == preAlice - delta);
BEAST_EXPECT(env.balance(bob, mpt) == mpt(10'100));
auto const escrowedWithFix = env.current()->rules().enabled(fixTokenEscrowV1) ? 0 : 25;
auto const outstandingWithFix =
env.current()->rules().enabled(fixTokenEscrowV1) ? mpt(19'975) : mpt(20'000);
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == escrowedWithFix);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == escrowedWithFix);
BEAST_EXPECT(env.balance(gw, mpt) == -outstandingWithFix);
}
// test locked rate: cancel
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
mptGw.create(
{.transferFee = 25000,
.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanEscrow | tfMPTCanTransfer});
mptGw.authorize({.account = alice});
mptGw.authorize({.account = bob});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(10'000)));
env(pay(gw, bob, mpt(10'000)));
env.close();
// alice can create escrow w/ xfer rate
auto const preAlice = env.balance(alice, mpt);
auto const preBob = env.balance(bob, mpt);
auto const seq1 = env.seq(alice);
auto const delta = mpt(125);
env(escrow::create(alice, bob, mpt(125)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
escrow::kCancelTime(env.now() + 3s),
Fee(baseFee * 150));
env.close();
auto const transferRate = escrow::rate(env, alice, seq1);
BEAST_EXPECT(transferRate.value == std::uint32_t(1'000'000'000 * 1.25));
// alice can cancel escrow
env(escrow::cancel(alice, alice, seq1), Fee(baseFee));
env.close();
BEAST_EXPECT(env.balance(alice, mpt) == preAlice);
BEAST_EXPECT(env.balance(bob, mpt) == preBob);
BEAST_EXPECT(env.balance(gw, mpt) == mpt(-20'000));
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 0);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 0);
}
// test locked rate: issuer is destination
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
mptGw.create(
{.transferFee = 25000,
.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanEscrow | tfMPTCanTransfer});
mptGw.authorize({.account = alice});
mptGw.authorize({.account = bob});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(10'000)));
env(pay(gw, bob, mpt(10'000)));
env.close();
// alice can create escrow w/ xfer rate
auto const preAlice = env.balance(alice, mpt);
auto const seq1 = env.seq(alice);
auto const delta = mpt(125);
env(escrow::create(alice, gw, mpt(125)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150));
env.close();
auto const transferRate = escrow::rate(env, alice, seq1);
BEAST_EXPECT(transferRate.value == std::uint32_t(1'000'000'000 * 1.25));
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 125);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 125);
BEAST_EXPECT(env.balance(gw, mpt) == mpt(-20'000));
// bob can finish escrow
env(escrow::finish(gw, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150));
env.close();
BEAST_EXPECT(env.balance(alice, mpt) == preAlice - delta);
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 0);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 0);
BEAST_EXPECT(env.balance(gw, mpt) == mpt(-19'875));
}
}
void
testMPTSplitEscrowTransferFee(FeatureBitset features)
{
using namespace test::jtx;
using namespace std::literals;
bool const withCleanup340 = features[fixCleanup3_4_0];
testcase(
std::string("MPT Split Escrow Transfer Fee ") +
(withCleanup340 ? "with Cleanup340" : "without Cleanup340"));
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
env.fund(XRP(1'000), alice, bob, gw);
env.close();
MPTTester const mpt({
.env = env,
.issuer = gw,
.holders = {alice, bob},
.transferFee = 1'000,
.flags = tfMPTCanEscrow | tfMPTCanTransfer,
});
env(pay(gw, alice, mpt(10'000)));
env.close();
static constexpr int escrowCount = 10;
static constexpr int splitAmount = 10;
static constexpr int totalLocked = escrowCount * splitAmount;
std::array<std::uint32_t, escrowCount> seqs{};
for (auto& seq : seqs)
{
seq = env.seq(alice);
env(escrow::create(alice, bob, mpt(splitAmount)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150));
env.close();
}
BEAST_EXPECT(env.balance(alice, mpt) == mpt(10'000 - totalLocked));
BEAST_EXPECT(env.balance(bob, mpt) == mpt(0));
BEAST_EXPECT(env.balance(gw, mpt) == mpt(-10'000));
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == totalLocked);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == totalLocked);
for (auto const seq : seqs)
{
env(escrow::finish(bob, alice, seq),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150));
env.close();
}
auto const feeBurned = withCleanup340 ? escrowCount : 0;
BEAST_EXPECT(env.balance(alice, mpt) == mpt(10'000 - totalLocked));
BEAST_EXPECT(env.balance(bob, mpt) == mpt(totalLocked - feeBurned));
BEAST_EXPECT(env.balance(gw, mpt) == mpt(-10'000 + feeBurned));
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 0);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 0);
}
void
testMPTLargeLockedRate(FeatureBitset features)
{
testcase("MPT large locked rate");
using namespace test::jtx;
using namespace std::literals;
auto constexpr escrowAmount = 200'000'000'000'000'000LL;
auto constexpr noOverflowEscrowAmount = 186'000'000'000'000'000LL;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
for (auto const testFeatures :
{features - featureMPTokensV2 - fixCleanup3_4_0,
features - featureMPTokensV2,
(features | featureMPTokensV2) - fixCleanup3_4_0,
features | featureMPTokensV2})
{
bool const mptV2 = testFeatures[featureMPTokensV2];
bool const tokenEscrowV1 = testFeatures[fixTokenEscrowV1];
// The transfer-fee split in EscrowFinish only overflows on the
// legacy divideRound(amount, lockedRate, ...) path, which runs when
// fixCleanup3_4_0 is disabled. With fixCleanup3_4_0 the split uses
// mulRatio (128-bit intermediate), which cannot overflow. Without
// it, this large amount overflows unless the MPTokensV2 Number path
// is active. So the finish succeeds when either amendment is enabled.
bool const cleanup340 = testFeatures[fixCleanup3_4_0];
bool const noOverflow = cleanup340 || mptV2;
auto const expectedErr = noOverflow ? Ter(tesSUCCESS) : Ter(tefEXCEPTION);
// Finish with a large MPT amount and non-zero transfer fee. When the
// computation overflows (legacy divideRound path, no MPTokensV2) the
// finish fails with tefEXCEPTION and the escrow is untouched;
// otherwise it unlocks the escrow.
{
Env env{*this, testFeatures};
env.fund(XRP(1'000), alice, bob, gw);
auto const baseFee = env.current()->fees().base;
MPTTester const mpt(
{.env = env,
.issuer = gw,
.holders = {alice, bob},
.transferFee = 1'000,
.flags = tfMPTCanEscrow | tfMPTCanTransfer});
env(pay(gw, alice, mpt(escrowAmount)));
env.close();
auto const preAlice = env.balance(alice, mpt);
auto const preBob = env.balance(bob, mpt);
auto const seq = env.seq(alice);
env(escrow::create(alice, bob, mpt(escrowAmount)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
escrow::kCancelTime(env.now() + 500s),
Fee(baseFee * 150));
env.close();
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == escrowAmount);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == escrowAmount);
env(escrow::finish(bob, alice, seq),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150),
expectedErr);
env.close();
if (noOverflow)
{
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(seq))));
BEAST_EXPECT(env.balance(alice, mpt) == preAlice - mpt(escrowAmount));
auto const postBob = env.balance(bob, mpt);
BEAST_EXPECT(postBob.value() > preBob.value());
BEAST_EXPECT(postBob.value() < (preBob + mpt(escrowAmount)).value());
auto const xferFee = escrowAmount - (postBob.value() - preBob.value());
auto const expectedEscrow = tokenEscrowV1 ? 0 : xferFee;
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == expectedEscrow);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == expectedEscrow);
}
else
{
BEAST_EXPECT(env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(seq))));
BEAST_EXPECT(env.balance(alice, mpt) == preAlice - mpt(escrowAmount));
BEAST_EXPECT(env.balance(bob, mpt) == preBob);
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == escrowAmount);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == escrowAmount);
}
}
// Control: a still-large amount below the legacy overflow boundary
// finishes successfully in both feature modes.
{
Env env{*this, testFeatures};
env.fund(XRP(1'000), alice, bob, gw);
auto const baseFee = env.current()->fees().base;
MPTTester const mpt(
{.env = env,
.issuer = gw,
.holders = {alice, bob},
.transferFee = 1'000,
.flags = tfMPTCanEscrow | tfMPTCanTransfer});
env(pay(gw, alice, mpt(noOverflowEscrowAmount)));
env.close();
auto const preAlice = env.balance(alice, mpt);
auto const preBob = env.balance(bob, mpt);
auto const seq = env.seq(alice);
env(escrow::create(alice, bob, mpt(noOverflowEscrowAmount)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
escrow::kCancelTime(env.now() + 500s),
Fee(baseFee * 150));
env.close();
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == noOverflowEscrowAmount);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == noOverflowEscrowAmount);
env(escrow::finish(bob, alice, seq),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(seq))));
BEAST_EXPECT(env.balance(alice, mpt) == preAlice - mpt(noOverflowEscrowAmount));
auto const postBob = env.balance(bob, mpt);
BEAST_EXPECT(postBob.value() > preBob.value());
BEAST_EXPECT(postBob.value() < (preBob + mpt(noOverflowEscrowAmount)).value());
auto const xferFee = noOverflowEscrowAmount - (postBob.value() - preBob.value());
auto const expectedEscrow = tokenEscrowV1 ? 0 : xferFee;
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == expectedEscrow);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == expectedEscrow);
}
// Cancel returns the escrow to the owner using parity rate, so it
// does not hit the transfer-rate division in either feature mode.
{
Env env{*this, testFeatures};
env.fund(XRP(1'000), alice, bob, gw);
auto const baseFee = env.current()->fees().base;
MPTTester const mpt(
{.env = env,
.issuer = gw,
.holders = {alice, bob},
.transferFee = 1'000,
.flags = tfMPTCanEscrow | tfMPTCanTransfer});
env(pay(gw, alice, mpt(escrowAmount)));
env.close();
auto const preAlice = env.balance(alice, mpt);
auto const preBob = env.balance(bob, mpt);
auto const seq = env.seq(alice);
env(escrow::create(alice, bob, mpt(escrowAmount)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
escrow::kCancelTime(env.now() + 3s),
Fee(baseFee * 150));
env.close();
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == escrowAmount);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == escrowAmount);
env(escrow::cancel(alice, alice, seq), Fee(baseFee), Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(seq))));
BEAST_EXPECT(env.balance(alice, mpt) == preAlice);
BEAST_EXPECT(env.balance(bob, mpt) == preBob);
BEAST_EXPECT(env.balance(gw, mpt) == -mpt(escrowAmount));
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 0);
BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 0);
}
}
}
void
testMPTRequireAuth(FeatureBitset features)
{
testcase("MPT Require Auth");
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 gw = Account("gw");
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
mptGw.create(
{.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth});
mptGw.authorize({.account = alice});
mptGw.authorize({.account = gw, .holder = alice});
mptGw.authorize({.account = bob});
mptGw.authorize({.account = gw, .holder = bob});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(10'000)));
env.close();
auto seq = env.seq(alice);
auto const delta = mpt(125);
// alice can create escrow - is authorized
env(escrow::create(alice, bob, mpt(100)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150));
env.close();
// bob can finish escrow - is authorized
env(escrow::finish(bob, alice, seq),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150));
env.close();
}
void
testMPTLock(FeatureBitset features)
{
testcase("MPT Lock");
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 gw = Account("gw");
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
mptGw.create(
{.ownerCount = 1,
.holderCount = 0,
.flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock});
mptGw.authorize({.account = alice});
mptGw.authorize({.account = bob});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(10'000)));
env(pay(gw, bob, mpt(10'000)));
env.close();
// alice create escrow
auto seq1 = env.seq(alice);
env(escrow::create(alice, bob, mpt(100)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
escrow::kCancelTime(env.now() + 2s),
Fee(baseFee * 150));
env.close();
// lock account & dest
mptGw.set({.account = gw, .holder = alice, .flags = tfMPTLock});
mptGw.set({.account = gw, .holder = bob, .flags = tfMPTLock});
// bob cannot finish
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150),
Ter(tecLOCKED));
env.close();
// bob can cancel
env(escrow::cancel(bob, alice, seq1));
env.close();
}
void
testMPTCanTransfer(FeatureBitset features)
{
testcase("MPT Can Transfer");
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 gw = Account("gw");
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
mptGw.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow});
mptGw.authorize({.account = alice});
mptGw.authorize({.account = bob});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(10'000)));
env(pay(gw, bob, mpt(10'000)));
env.close();
// alice cannot create escrow to non issuer
env(escrow::create(alice, bob, mpt(100)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
escrow::kCancelTime(env.now() + 2s),
Fee(baseFee * 150),
Ter(tecNO_AUTH));
env.close();
// Escrow Create & Finish
{
// alice an create escrow to issuer
auto seq = env.seq(alice);
env(escrow::create(alice, gw, mpt(100)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150));
env.close();
// gw can finish
env(escrow::finish(gw, alice, seq),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150));
env.close();
}
// Escrow Create & Cancel
{
// alice an create escrow to issuer
auto seq = env.seq(alice);
env(escrow::create(alice, gw, mpt(100)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
escrow::kCancelTime(env.now() + 2s),
Fee(baseFee * 150));
env.close();
// alice can cancel
env(escrow::cancel(alice, alice, seq));
env.close();
}
}
void
testMPTDestroy(FeatureBitset features)
{
testcase("MPT Destroy");
using namespace test::jtx;
using namespace std::literals;
// tecHAS_OBLIGATIONS: issuer cannot destroy issuance
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
MPTTester mptGw(env, gw, {.holders = {alice, bob}});
mptGw.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
mptGw.authorize({.account = alice});
mptGw.authorize({.account = bob});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(10'000)));
env(pay(gw, bob, mpt(10'000)));
env.close();
auto const seq1 = env.seq(alice);
env(escrow::create(alice, bob, mpt(10)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150));
env.close();
env(pay(alice, gw, mpt(10'000)), Ter(tecPATH_PARTIAL));
env(pay(alice, gw, mpt(9'990)));
env(pay(bob, gw, mpt(10'000)));
BEAST_EXPECT(env.balance(alice, mpt) == mpt(0));
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 10);
BEAST_EXPECT(env.balance(bob, mpt) == mpt(0));
BEAST_EXPECT(mptEscrowed(env, bob, mpt) == 0);
BEAST_EXPECT(env.balance(gw, mpt) == mpt(-10));
mptGw.authorize({.account = bob, .flags = tfMPTUnauthorize});
mptGw.destroy({.id = mptGw.issuanceID(), .ownerCount = 1, .err = tecHAS_OBLIGATIONS});
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
env(pay(bob, gw, mpt(10)));
mptGw.destroy({.id = mptGw.issuanceID(), .ownerCount = 0});
}
// tecHAS_OBLIGATIONS: holder cannot destroy mptoken
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
env.fund(XRP(10'000), bob);
env.close();
MPTTester mptGw(env, gw, {.holders = {alice}});
mptGw.create(
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow | tfMPTCanTransfer});
mptGw.authorize({.account = alice});
auto const mpt = mptGw["MPT"];
env(pay(gw, alice, mpt(10'000)));
env.close();
auto const seq1 = env.seq(alice);
env(escrow::create(alice, bob, mpt(10)),
escrow::kCondition(escrow::kCb1),
escrow::kFinishTime(env.now() + 1s),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
env(pay(alice, gw, mpt(9'990)));
env.close();
BEAST_EXPECT(env.balance(alice, mpt) == mpt(0));
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 10);
mptGw.authorize(
{.account = alice, .flags = tfMPTUnauthorize, .err = tecHAS_OBLIGATIONS});
env(escrow::finish(bob, alice, seq1),
escrow::kCondition(escrow::kCb1),
escrow::kFulfillment(escrow::kFb1),
Fee(baseFee * 150),
Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(env.balance(alice, mpt) == mpt(0));
BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 0);
mptGw.authorize({.account = alice, .flags = tfMPTUnauthorize});
BEAST_EXPECT(!env.le(keylet::mptoken(mpt.mpt(), alice)));
}
}
void
testIOUWithFeats(FeatureBitset features)
{
testIOUEnablement(features);
testIOUAllowLockingFlag(features);
testIOUCreatePreflight(features);
testIOUCreatePreclaim(features);
testIOUFinishPreclaim(features);
testIOUFinishDoApply(features);
testIOUCancelPreclaim(features);
testIOUCancelDoApply(features);
testIOUBalances(features);
testIOUMetaAndOwnership(features);
testIOURippleState(features);
testIOUGateway(features);
testIOULockedRate(features);
testIOULimitAmount(features);
testIOURequireAuth(features);
testIOUFreeze(features);
testIOUInsufficientFunds(features);
testIOUPrecisionLoss(features);
}
void
testMPTWithFeats(FeatureBitset features)
{
testMPTEnablement(features);
testMPTCreatePreflight(features);
testMPTCreatePreclaim(features);
testMPTFinishPreclaim(features);
testMPTFinishDoApply(features);
testMPTCancelPreclaim(features);
testMPTBalances(features);
testMPTMetaAndOwnership(features);
testMPTGateway(features);
testMPTLockedRate(features);
testMPTLargeLockedRate(features);
testMPTRequireAuth(features);
testMPTLock(features);
testMPTCanTransfer(features);
testMPTDestroy(features);
}
public:
void
run() override
{
using namespace test::jtx;
FeatureBitset const all{testableAmendments()};
for (FeatureBitset const& feats :
{all - featureSingleAssetVault - featureLendingProtocol - featureMPTokensV2, all})
{
testIOUWithFeats(feats);
testIOUWithFeats(feats - fixCleanup3_2_0);
testMPTWithFeats(feats);
testMPTWithFeats(feats - fixTokenEscrowV1);
}
testMPTSplitEscrowTransferFee(all - fixCleanup3_4_0);
testMPTSplitEscrowTransferFee(all);
testIOUCancelReserveRecycle(all - fixCleanup3_4_0);
testIOUCancelReserveRecycle(all);
}
};
BEAST_DEFINE_TESTSUITE(EscrowToken, app, xrpl);
} // namespace xrpl::test