From 67fa36045db1a38ab4a0134d98b4f161caafa30a Mon Sep 17 00:00:00 2001 From: Denis Angell Date: Wed, 12 Feb 2025 22:38:34 +0100 Subject: [PATCH] featureTokenEscrow --- include/xrpl/protocol/Feature.h | 2 +- include/xrpl/protocol/LedgerFormats.h | 3 +- include/xrpl/protocol/STAmount.h | 42 + include/xrpl/protocol/TER.h | 1 + include/xrpl/protocol/TxFlags.h | 1 + include/xrpl/protocol/detail/features.macro | 1 + .../xrpl/protocol/detail/ledger_entries.macro | 2 + .../xrpl/protocol/detail/transactions.macro | 2 +- src/libxrpl/protocol/TER.cpp | 1 + src/test/app/AccountDelete_test.cpp | 28 + src/test/app/EscrowToken_test.cpp | 3055 +++++++++++++++++ src/test/app/Escrow_test.cpp | 130 +- src/test/ledger/Invariants_test.cpp | 28 +- src/test/rpc/AccountSet_test.cpp | 6 + src/xrpld/app/tx/detail/Escrow.cpp | 636 +++- src/xrpld/app/tx/detail/InvariantCheck.cpp | 24 +- src/xrpld/app/tx/detail/SetAccount.cpp | 9 + src/xrpld/ledger/View.h | 3 + src/xrpld/ledger/detail/View.cpp | 12 + 19 files changed, 3874 insertions(+), 112 deletions(-) create mode 100644 src/test/app/EscrowToken_test.cpp diff --git a/include/xrpl/protocol/Feature.h b/include/xrpl/protocol/Feature.h index 1c476df617f..b6477baa02d 100644 --- a/include/xrpl/protocol/Feature.h +++ b/include/xrpl/protocol/Feature.h @@ -80,7 +80,7 @@ namespace detail { // Feature.cpp. Because it's only used to reserve storage, and determine how // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than // the actual number of amendments. A LogicError on startup will verify this. -static constexpr std::size_t numFeatures = 88; +static constexpr std::size_t numFeatures = 89; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index 5f3cca53ac8..8ca6c8923f3 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -145,7 +145,8 @@ enum LedgerSpecificFlags { 0x10000000, // True, reject new paychans lsfDisallowIncomingTrustline = 0x20000000, // True, reject new trustlines (only if no issued assets) - // 0x40000000 is available + lsfAllowTokenLocking = + 0x40000000, // True, enable token locking lsfAllowTrustLineClawback = 0x80000000, // True, enable clawback diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index 23e4c5e5b59..4addc45c058 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -684,6 +684,48 @@ isXRP(STAmount const& amount) return amount.native(); } +/** returns true if adding or subtracting results in less than or equal to + * 0.01% precision loss **/ +inline bool +isAddable(STAmount const& amt1, STAmount const& amt2) +{ + // special case: adding anything to zero is always fine + if (amt1 == beast::zero || amt2 == beast::zero) + return true; + + // special case: adding two xrp amounts together. + // this is just an overflow check + if (isXRP(amt1) && isXRP(amt2)) + { + XRPAmount A = (amt1.signum() == -1 ? -(amt1.xrp()) : amt1.xrp()); + XRPAmount B = (amt2.signum() == -1 ? -(amt2.xrp()) : amt2.xrp()); + + XRPAmount finalAmt = A + B; + return (finalAmt >= A && finalAmt >= B); + } + + static const STAmount one{IOUAmount{1, 0}, noIssue()}; + static const STAmount maxLoss{IOUAmount{1, -4}, noIssue()}; + + STAmount A = amt1; + STAmount B = amt2; + + if (isXRP(A)) + A = STAmount{IOUAmount{A.xrp().drops(), -6}, noIssue()}; + + if (isXRP(B)) + B = STAmount{IOUAmount{B.xrp().drops(), -6}, noIssue()}; + + A.setIssue(noIssue()); + B.setIssue(noIssue()); + + STAmount lhs = divide((A - B) + B, A, noIssue()) - one; + STAmount rhs = divide((B - A) + A, B, noIssue()) - one; + + return ((rhs.negative() ? -rhs : rhs) + (lhs.negative() ? -lhs : lhs)) <= + maxLoss; +} + // Since `canonicalize` does not have access to a ledger, this is needed to put // the low-level routine stAmountCanonicalize on an amendment switch. Only // transactions need to use this switchover. Outside of a transaction it's safe diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index 317e9c2c978..24a94036107 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -344,6 +344,7 @@ enum TECcodes : TERUnderlyingType { tecARRAY_TOO_LARGE = 191, tecLOCKED = 192, tecBAD_CREDENTIALS = 193, + tecPRECISION_LOSS = 194, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index f0f6c7f223c..d5c358a082e 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -91,6 +91,7 @@ constexpr std::uint32_t asfDisallowIncomingCheck = 13; constexpr std::uint32_t asfDisallowIncomingPayChan = 14; constexpr std::uint32_t asfDisallowIncomingTrustline = 15; constexpr std::uint32_t asfAllowTrustLineClawback = 16; +constexpr std::uint32_t asfAllowTokenLocking = 17; // OfferCreate flags: constexpr std::uint32_t tfPassive = 0x00010000; diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index aa0782b1378..6d65a03a52d 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -29,6 +29,7 @@ // If you add an amendment here, then do not forget to increment `numFeatures` // in include/xrpl/protocol/Feature.h. +XRPL_FEATURE(TokenEscrow, Supported::yes, VoteBehavior::DefaultNo) // Check flags in Credential transactions XRPL_FIX (InvalidTxFlags, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (FrozenLPTokenTransfer, Supported::yes, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 5a652baf4f7..1f276bf3fe5 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -350,6 +350,8 @@ LEDGER_ENTRY(ltESCROW, 0x0075, Escrow, escrow, ({ {sfPreviousTxnID, soeREQUIRED}, {sfPreviousTxnLgrSeq, soeREQUIRED}, {sfDestinationNode, soeOPTIONAL}, + {sfTransferRate, soeOPTIONAL}, + {sfIssuerNode, soeOPTIONAL}, })) /** A ledger object describing a single unidirectional XRP payment channel. diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index dd3ac42325d..34fe9972547 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -43,7 +43,7 @@ TRANSACTION(ttPAYMENT, 0, Payment, ({ /** This transaction type creates an escrow object. */ TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, ({ {sfDestination, soeREQUIRED}, - {sfAmount, soeREQUIRED}, + {sfAmount, soeREQUIRED, soeMPTSupported}, {sfCondition, soeOPTIONAL}, {sfCancelAfter, soeOPTIONAL}, {sfFinishAfter, soeOPTIONAL}, diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index 815b27c0018..cd39e07b5fd 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -117,6 +117,7 @@ transResults() MAKE_ERROR(tecARRAY_TOO_LARGE, "Array is too large."), MAKE_ERROR(tecLOCKED, "Fund is locked."), MAKE_ERROR(tecBAD_CREDENTIALS, "Bad credentials."), + MAKE_ERROR(tecPRECISION_LOSS, "The amounts used by the transaction cannot interact."), MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), diff --git a/src/test/app/AccountDelete_test.cpp b/src/test/app/AccountDelete_test.cpp index f8d3cf4692a..b949a10c2ce 100644 --- a/src/test/app/AccountDelete_test.cpp +++ b/src/test/app/AccountDelete_test.cpp @@ -403,6 +403,34 @@ class AccountDelete_test : public beast::unit_test::suite jv[sfOfferSequence.jsonName] = seq; return jv; }; + + bool const withTokenEscrow = + env.current()->rules().enabled(featureTokenEscrow); + if (withTokenEscrow) + { + Account const carol("carol"); + auto const USD = gw["USD"]; + env.fund(XRP(100000), carol); + env(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), carol); + env.close(); + env(pay(gw, carol, USD(100))); + env.close(); + + std::uint32_t const escrowSeq{env.seq(carol)}; + env(escrowCreate(carol, becky, USD(1), env.now() + 2s)); + env.close(); + + env(acctdelete(gw, becky), + fee(acctDelFee), + ter(tecHAS_OBLIGATIONS)); + env.close(); + + env(escrowCancel(becky, carol, escrowSeq)); + env.close(); + } + env(escrowCancel(becky, alice, escrowSeq)); env.close(); diff --git a/src/test/app/EscrowToken_test.cpp b/src/test/app/EscrowToken_test.cpp new file mode 100644 index 00000000000..d6d5b05aa85 --- /dev/null +++ b/src/test/app/EscrowToken_test.cpp @@ -0,0 +1,3055 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace ripple { +namespace test { + +struct EscrowToken_test : public beast::unit_test::suite +{ + // A PreimageSha256 fulfillments and its associated condition. + std::array const fb1 = {{0xA0, 0x02, 0x80, 0x00}}; + + std::array const cb1 = { + {0xA0, 0x25, 0x80, 0x20, 0xE3, 0xB0, 0xC4, 0x42, 0x98, 0xFC, + 0x1C, 0x14, 0x9A, 0xFB, 0xF4, 0xC8, 0x99, 0x6F, 0xB9, 0x24, + 0x27, 0xAE, 0x41, 0xE4, 0x64, 0x9B, 0x93, 0x4C, 0xA4, 0x95, + 0x99, 0x1B, 0x78, 0x52, 0xB8, 0x55, 0x81, 0x01, 0x00}}; + + // Another PreimageSha256 fulfillments and its associated condition. + std::array const fb2 = { + {0xA0, 0x05, 0x80, 0x03, 0x61, 0x61, 0x61}}; + + std::array const cb2 = { + {0xA0, 0x25, 0x80, 0x20, 0x98, 0x34, 0x87, 0x6D, 0xCF, 0xB0, + 0x5C, 0xB1, 0x67, 0xA5, 0xC2, 0x49, 0x53, 0xEB, 0xA5, 0x8C, + 0x4A, 0xC8, 0x9B, 0x1A, 0xDF, 0x57, 0xF2, 0x8F, 0x2F, 0x9D, + 0x09, 0xAF, 0x10, 0x7E, 0xE8, 0xF0, 0x81, 0x01, 0x03}}; + + // Another PreimageSha256 fulfillment and its associated condition. + std::array const fb3 = { + {0xA0, 0x06, 0x80, 0x04, 0x6E, 0x69, 0x6B, 0x62}}; + + std::array const cb3 = { + {0xA0, 0x25, 0x80, 0x20, 0x6E, 0x4C, 0x71, 0x45, 0x30, 0xC0, + 0xA4, 0x26, 0x8B, 0x3F, 0xA6, 0x3B, 0x1B, 0x60, 0x6F, 0x2D, + 0x26, 0x4A, 0x2D, 0x85, 0x7B, 0xE8, 0xA0, 0x9C, 0x1D, 0xFD, + 0x57, 0x0D, 0x15, 0x85, 0x8B, 0xD4, 0x81, 0x01, 0x04}}; + + /** Set the "FinishAfter" time tag on a JTx */ + struct finish_time + { + private: + NetClock::time_point value_; + + public: + explicit finish_time(NetClock::time_point const& value) : value_(value) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jt) const + { + jt.jv[sfFinishAfter.jsonName] = value_.time_since_epoch().count(); + } + }; + + /** Set the "CancelAfter" time tag on a JTx */ + struct cancel_time + { + private: + NetClock::time_point value_; + + public: + explicit cancel_time(NetClock::time_point const& value) : value_(value) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jt) const + { + jt.jv[sfCancelAfter.jsonName] = value_.time_since_epoch().count(); + } + }; + + struct condition + { + private: + std::string value_; + + public: + explicit condition(Slice cond) : value_(strHex(cond)) + { + } + + template + explicit condition(std::array c) + : condition(makeSlice(c)) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jt) const + { + jt.jv[sfCondition.jsonName] = value_; + } + }; + + struct fulfillment + { + private: + std::string value_; + + public: + explicit fulfillment(Slice condition) : value_(strHex(condition)) + { + } + + template + explicit fulfillment(std::array f) + : fulfillment(makeSlice(f)) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jt) const + { + jt.jv[sfFulfillment.jsonName] = value_; + } + }; + + static Json::Value + escrow( + jtx::Account const& account, + jtx::Account const& to, + STAmount const& amount) + { + using namespace jtx; + Json::Value jv; + jv[jss::TransactionType] = jss::EscrowCreate; + jv[jss::Flags] = tfUniversal; + jv[jss::Account] = account.human(); + jv[jss::Destination] = to.human(); + jv[jss::Amount] = amount.getJson(JsonOptions::none); + return jv; + } + + static Json::Value + finish( + jtx::Account const& account, + jtx::Account const& from, + std::uint32_t seq) + { + Json::Value jv; + jv[jss::TransactionType] = jss::EscrowFinish; + jv[jss::Flags] = tfUniversal; + jv[jss::Account] = account.human(); + jv[sfOwner.jsonName] = from.human(); + jv[sfOfferSequence.jsonName] = seq; + return jv; + } + + static Json::Value + cancel( + jtx::Account const& account, + jtx::Account const& from, + std::uint32_t seq) + { + Json::Value jv; + jv[jss::TransactionType] = jss::EscrowCancel; + jv[jss::Flags] = tfUniversal; + jv[jss::Account] = account.human(); + jv[sfOwner.jsonName] = from.human(); + jv[sfOfferSequence.jsonName] = seq; + return jv; + } + + static Rate + escrowRate( + jtx::Env const& env, + jtx::Account const& account, + uint32_t const& seq) + { + auto const sle = env.le(keylet::escrow(account.id(), seq)); + if (sle->isFieldPresent(sfTransferRate)) + return ripple::Rate((*sle)[sfTransferRate]); + return Rate{0}; + } + + static STAmount + limitAmount( + jtx::Env const& env, + jtx::Account const& account, + jtx::Account const& gw, + jtx::IOU const& iou) + { + auto const aHigh = account.id() > gw.id(); + auto const sle = env.le(keylet::line(account, gw, iou.currency)); + if (sle && sle->isFieldPresent(aHigh ? sfLowLimit : sfHighLimit)) + return (*sle)[aHigh ? sfLowLimit : sfHighLimit]; + return STAmount(iou, 0); + } + + static STAmount + lineBalance( + jtx::Env const& env, + jtx::Account const& account, + jtx::Account const& gw, + jtx::IOU const& iou) + { + auto const sle = env.le(keylet::line(account, gw, iou.currency)); + if (sle && sle->isFieldPresent(sfBalance)) + return (*sle)[sfBalance]; + return STAmount(iou, 0); + } + + 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 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, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), 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(temDISABLED); + auto const finishResult = + withTokenEscrow ? ter(tesSUCCESS) : ter(tecNO_TARGET); + env(escrow(alice, bob, USD(1000)), + finish_time(env.now() + 1s), + createResult); + env.close(); + + auto const seq1 = env.seq(alice); + + env(escrow(alice, bob, USD(1000)), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500), + createResult); + env.close(); + env(finish(bob, alice, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500), + finishResult); + + auto const seq2 = env.seq(alice); + + env(escrow(alice, bob, USD(1000)), + condition(cb2), + finish_time(env.now() + 1s), + cancel_time(env.now() + 2s), + fee(1500), + createResult); + env.close(); + env(cancel(bob, alice, seq2), fee(1500), finishResult); + } + } + + void + testIOUTiming(FeatureBitset features) + { + using namespace jtx; + using namespace std::chrono; + + { + testcase("Timing: Token Finish Only"); + 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(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + // We create an escrow that can be finished in the future + auto const ts = env.now() + 97s; + + auto const seq = env.seq(alice); + env(escrow(alice, bob, USD(1000)), finish_time(ts)); + + // Advance the ledger, verifying that the finish won't complete + // prematurely. + for (; env.now() < ts; env.close()) + env(finish(bob, alice, seq), fee(1500), ter(tecNO_PERMISSION)); + + env(finish(bob, alice, seq), fee(1500)); + } + + { + testcase("Timing: Token Cancel Only"); + 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(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + // We create an escrow that can be cancelled in the future + auto const ts = env.now() + 117s; + + auto const seq = env.seq("alice"); + env(escrow("alice", "bob", USD(1000)), + condition(cb1), + cancel_time(ts)); + + // Advance the ledger, verifying that the cancel won't complete + // prematurely. + for (; env.now() < ts; env.close()) + env(cancel("bob", "alice", seq), + fee(1500), + ter(tecNO_PERMISSION)); + + // Verify that a finish won't work anymore. + env(finish("bob", "alice", seq), + condition(cb1), + fulfillment(fb1), + fee(1500), + ter(tecNO_PERMISSION)); + + // Verify that the cancel will succeed + env(cancel("bob", "alice", seq), fee(1500)); + } + + { + testcase("Timing: Token Finish and Cancel -> Finish"); + 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(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + // We create an escrow that can be cancelled in the future + auto const fts = env.now() + 117s; + auto const cts = env.now() + 192s; + + auto const seq = env.seq("alice"); + env(escrow("alice", "bob", USD(1000)), + finish_time(fts), + cancel_time(cts)); + + // Advance the ledger, verifying that the finish and cancel won't + // complete prematurely. + for (; env.now() < fts; env.close()) + { + env(finish("bob", "alice", seq), + fee(1500), + ter(tecNO_PERMISSION)); + env(cancel("bob", "alice", seq), + fee(1500), + ter(tecNO_PERMISSION)); + } + + // Verify that a cancel still won't work + env(cancel("bob", "alice", seq), fee(1500), ter(tecNO_PERMISSION)); + + // And verify that a finish will + env(finish("bob", "alice", seq), fee(1500)); + } + + { + testcase("Timing: Token Finish and Cancel -> Cancel"); + 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(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + // We create an escrow that can be cancelled in the future + auto const fts = env.now() + 109s; + auto const cts = env.now() + 184s; + + auto const seq = env.seq("alice"); + env(escrow("alice", "bob", USD(1000)), + finish_time(fts), + cancel_time(cts)); + + // Advance the ledger, verifying that the finish and cancel won't + // complete prematurely. + for (; env.now() < fts; env.close()) + { + env(finish("bob", "alice", seq), + fee(1500), + ter(tecNO_PERMISSION)); + env(cancel("bob", "alice", seq), + fee(1500), + ter(tecNO_PERMISSION)); + } + + // Continue advancing, verifying that the cancel won't complete + // prematurely. At this point a finish would succeed. + for (; env.now() < cts; env.close()) + env(cancel("bob", "alice", seq), + fee(1500), + ter(tecNO_PERMISSION)); + + // Verify that finish will no longer work, since we are past the + // cancel activation time. + env(finish("bob", "alice", seq), fee(1500), ter(tecNO_PERMISSION)); + + // And verify that a cancel will succeed. + env(cancel("bob", "alice", seq), fee(1500)); + } + } + + void + testIOUTags(FeatureBitset features) + { + testcase("IOU Tags"); + + using namespace jtx; + using namespace std::chrono; + + 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(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + // Check to make sure that we correctly detect if tags are really + // required: + env(fset(bob, asfRequireDest)); + env(escrow(alice, bob, USD(1000)), + finish_time(env.now() + 1s), + ter(tecDST_TAG_NEEDED)); + + // set source and dest tags + auto const seq = env.seq(alice); + + env(escrow(alice, bob, USD(1000)), + finish_time(env.now() + 1s), + stag(1), + dtag(2)); + + auto const sle = env.le(keylet::escrow(alice.id(), seq)); + BEAST_EXPECT(sle); + BEAST_EXPECT((*sle)[sfSourceTag] == 1); + BEAST_EXPECT((*sle)[sfDestinationTag] == 2); + } + + void + testIOU1571(FeatureBitset features) + { + using namespace jtx; + using namespace std::chrono; + + { + testcase("IOU Implied Finish Time (without fix1571)"); + + Env env(*this, supported_amendments() - fix1571); + 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"]; + env.fund(XRP(5000), alice, bob, carol, gw); + env(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), 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(); + + // Creating an escrow without a finish time and finishing it + // is allowed without fix1571: + auto const seq1 = env.seq(alice); + env(escrow(alice, bob, USD(100)), + cancel_time(env.now() + 1s), + fee(1500)); + env.close(); + env(finish(carol, alice, seq1), fee(1500)); + BEAST_EXPECT(env.balance(bob, USD) == USD(5100)); + env.close(); + + // Creating an escrow without a finish time and a condition is + // also allowed without fix1571: + auto const seq2 = env.seq(alice); + env(escrow(alice, bob, USD(100)), + cancel_time(env.now() + 1s), + condition(cb1), + fee(1500)); + env.close(); + env(finish(carol, alice, seq2), + condition(cb1), + fulfillment(fb1), + fee(1500)); + BEAST_EXPECT(env.balance(bob, USD) == USD(5200)); + } + + { + testcase("IOU Implied Finish Time (with fix1571)"); + + Env env{*this, features}; + 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"]; + env.fund(XRP(5000), alice, bob, carol, gw); + env(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), 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(); + + // Creating an escrow with only a cancel time is not allowed: + env(escrow(alice, bob, USD(100)), + cancel_time(env.now() + 90s), + fee(1500), + ter(temMALFORMED)); + + // Creating an escrow with only a cancel time and a condition is + // allowed: + auto const seq = env.seq(alice); + env(escrow(alice, bob, USD(100)), + cancel_time(env.now() + 90s), + condition(cb1), + fee(1500)); + env.close(); + env(finish(carol, alice, seq), + condition(cb1), + fulfillment(fb1), + fee(1500)); + BEAST_EXPECT(env.balance(bob, USD) == USD(5100)); + } + } + + void + testIOUFails(FeatureBitset features) + { + testcase("IOU Failure Cases"); + + using namespace jtx; + using namespace std::chrono; + + 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(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + // Finish time is in the past + env(escrow(alice, bob, USD(1000)), + finish_time(env.now() - 5s), + ter(tecNO_PERMISSION)); + + // Cancel time is in the past + env(escrow(alice, bob, USD(1000)), + condition(cb1), + cancel_time(env.now() - 5s), + ter(tecNO_PERMISSION)); + + // no destination account + env(escrow(alice, "carol", USD(1000)), + finish_time(env.now() + 1s), + ter(tecNO_DST)); + + auto const carol = Account("carol"); + env.fund(XRP(5000), carol); + env.close(); + env.trust(USD(10000), carol); + env.close(); + env(pay(gw, carol, USD(5000))); + env.close(); + + env(fclear(gw, asfAllowTokenLocking)); + // issuer has not set sallow token locking + env(escrow(alice, carol, USD(1000)), + finish_time(env.now() + 1s), + ter(tecNO_PERMISSION)); + + env(fset(gw, asfAllowTokenLocking)); + + // Sending zero or no XRP: + env(escrow(alice, carol, USD(0)), + finish_time(env.now() + 1s), + ter(temBAD_AMOUNT)); + env(escrow(alice, carol, USD(-1000)), + finish_time(env.now() + 1s), + ter(temBAD_AMOUNT)); + + // Fail if neither CancelAfter nor FinishAfter are specified: + env(escrow(alice, carol, USD(1)), ter(temBAD_EXPIRATION)); + + // Fail if neither a FinishTime nor a condition are attached: + env(escrow(alice, carol, USD(1)), + cancel_time(env.now() + 1s), + ter(temMALFORMED)); + + // Fail if FinishAfter has already passed: + env(escrow(alice, carol, USD(1)), + finish_time(env.now() - 1s), + ter(tecNO_PERMISSION)); + + // If both CancelAfter and FinishAfter are set, then CancelAfter must + // be strictly later than FinishAfter. + env(escrow(alice, carol, USD(1)), + condition(cb1), + finish_time(env.now() + 10s), + cancel_time(env.now() + 10s), + ter(temBAD_EXPIRATION)); + + env(escrow(alice, carol, USD(1)), + condition(cb1), + finish_time(env.now() + 10s), + cancel_time(env.now() + 5s), + ter(temBAD_EXPIRATION)); + + // Carol now requires the use of a destination tag + env(fset(carol, asfRequireDest)); + + // missing destination tag + env(escrow(alice, carol, USD(1)), + condition(cb1), + cancel_time(env.now() + 1s), + ter(tecDST_TAG_NEEDED)); + + // Success! + env(escrow(alice, carol, USD(1)), + condition(cb1), + cancel_time(env.now() + 1s), + dtag(1)); + + { // Fail if the sender wants to send more than he has: + auto const daniel = Account("daniel"); + env.fund(XRP(5000), daniel); + env.close(); + env.trust(USD(100), daniel); + env.close(); + env(pay(gw, daniel, USD(50))); + env.close(); + + env(escrow(daniel, bob, USD(51)), + finish_time(env.now() + 1s), + ter(tecINSUFFICIENT_FUNDS)); + + // Removed 3 Account Reserve/Increment XRP tests + // See line 602 + + env(escrow(daniel, bob, USD(10)), finish_time(env.now() + 1s)); + env.close(); + env(escrow(daniel, bob, USD(51)), + finish_time(env.now() + 1s), + ter(tecINSUFFICIENT_FUNDS)); + } + + { // Specify incorrect sequence number + auto const hannah = Account("hannah"); + env.fund(XRP(5000), hannah); + env.close(); + env.trust(USD(10000), hannah); + env.close(); + env(pay(gw, hannah, USD(5000))); + env.close(); + + auto const seq = env.seq(hannah); + env(escrow(hannah, hannah, USD(5000)), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + env(finish(hannah, hannah, seq + 7), fee(1500), ter(tecNO_TARGET)); + } + + { // Try to specify a condition for a non-conditional payment + auto const ivan = Account("ivan"); + env.fund(XRP(5000), ivan); + env.close(); + env.trust(USD(10000), ivan); + env.close(); + env(pay(gw, ivan, USD(5000))); + env.close(); + + auto const seq = env.seq(ivan); + + env(escrow(ivan, ivan, USD(10)), finish_time(env.now() + 1s)); + env.close(); + env(finish(ivan, ivan, seq), + condition(cb1), + fulfillment(fb1), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + } + } + + void + testIOULockup(FeatureBitset features) + { + testcase("IOU Lockup"); + + using namespace jtx; + using namespace std::chrono; + + { + // Unconditional + 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(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), alice); + env.trust(USD(10000), bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + auto const seq = env.seq(alice); + env(escrow(alice, alice, USD(1000)), finish_time(env.now() + 5s)); + + env.require(balance(alice, XRP(5000) - drops(10))); + env.require(balance(alice, USD(4000))); + + // Not enough time has elapsed for a finish and canceling isn't + // possible. + env(cancel(bob, alice, seq), ter(tecNO_PERMISSION)); + env(finish(bob, alice, seq), ter(tecNO_PERMISSION)); + env.close(); + + // Cancel continues to not be possible + env(cancel(bob, alice, seq), ter(tecNO_PERMISSION)); + + // Finish should succeed. Verify funds. + env(finish(bob, alice, seq)); + env.require(balance(alice, XRP(5000) - drops(10))); + env.require(balance(alice, USD(5000))); + } + { + // Unconditionally pay from Alice to Bob. Zelda (neither source nor + // destination) signs all cancels and finishes. This shows that + // Escrow will make a payment to Bob with no intervention from Bob. + Env env{*this, features}; + 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"]; + env.fund(XRP(5000), alice, bob, carol, gw); + env(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), 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 seq = env.seq(alice); + env(escrow(alice, bob, USD(1000)), finish_time(env.now() + 5s)); + + // Verify amounts + env.require(balance(alice, XRP(5000) - drops(10))); + env.require(balance(alice, USD(4000))); + + // Not enough time has elapsed for a finish and canceling isn't + // possible. + env(cancel(carol, alice, seq), ter(tecNO_PERMISSION)); + env(finish(carol, alice, seq), ter(tecNO_PERMISSION)); + env.close(); + + // Cancel continues to not be possible + env(cancel(carol, alice, seq), ter(tecNO_PERMISSION)); + + // Finish should succeed. Verify funds. + env(finish(carol, alice, seq)); + env.close(); + + env.require(balance(alice, XRP(5000) - drops(10))); + env.require(balance(alice, USD(4000))); + env.require(balance(bob, USD(6000))); + env.require(balance(carol, XRP(5000) - drops(40))); + } + { + // Bob sets DepositAuth so only Bob can finish the escrow. + Env env{*this, features}; + 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"]; + env.fund(XRP(5000), alice, bob, carol, gw); + env(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), 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(); + + env(fset(bob, asfDepositAuth)); + env.close(); + + auto const seq = env.seq(alice); + env(escrow(alice, bob, USD(1000)), finish_time(env.now() + 5s)); + + // Verify amounts + env.require(balance(alice, XRP(5000) - drops(10))); + env.require(balance(alice, USD(4000))); + + // Not enough time has elapsed for a finish and canceling isn't + // possible. + env(cancel(carol, alice, seq), ter(tecNO_PERMISSION)); + env(cancel(alice, alice, seq), ter(tecNO_PERMISSION)); + env(cancel(bob, alice, seq), ter(tecNO_PERMISSION)); + env(finish(carol, alice, seq), ter(tecNO_PERMISSION)); + env(finish(alice, alice, seq), ter(tecNO_PERMISSION)); + env(finish(bob, alice, seq), ter(tecNO_PERMISSION)); + env.close(); + + // Cancel continues to not be possible. Finish will only succeed + // for + // Bob, because of DepositAuth. + env(cancel(carol, alice, seq), ter(tecNO_PERMISSION)); + env(cancel(alice, alice, seq), ter(tecNO_PERMISSION)); + env(cancel(bob, alice, seq), ter(tecNO_PERMISSION)); + env(finish(carol, alice, seq), ter(tecNO_PERMISSION)); + env(finish(alice, alice, seq), ter(tecNO_PERMISSION)); + env(finish(bob, alice, seq)); + env.close(); + + // Verify amounts + env.require(balance(alice, USD(4000))); + env.require(balance(bob, USD(6000))); + auto const baseFee = env.current()->fees().base; + env.require(balance(alice, XRP(5000) - (baseFee * 5))); + env.require(balance(bob, XRP(5000) - (baseFee * 5))); + env.require(balance(carol, XRP(5000) - (baseFee * 4))); + } + { + // Bob sets DepositAuth but preauthorizes Zelda, so Zelda can + // finish the escrow. + Env env{*this, features}; + 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"]; + env.fund(XRP(5000), alice, bob, carol, gw); + env(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), 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(); + + env(fset(bob, asfDepositAuth)); + env.close(); + env(deposit::auth(bob, carol)); + env.close(); + + auto const seq = env.seq(alice); + env(escrow(alice, bob, USD(1000)), finish_time(env.now() + 5s)); + + env.require(balance(alice, XRP(5000) - drops(10))); + env.require(balance(alice, USD(4000))); + + env.close(); + + // DepositPreauth allows Finish to succeed for either Zelda or + // Bob. But Finish won't succeed for Alice since she is not + // preauthorized. + env(finish(alice, alice, seq), ter(tecNO_PERMISSION)); + env(finish(carol, alice, seq)); + env.close(); + + env.require(balance(alice, USD(4000))); + env.require(balance(bob, USD(6000))); + auto const baseFee = env.current()->fees().base; + env.require(balance(alice, XRP(5000) - (baseFee * 2))); + env.require(balance(bob, XRP(5000) - (baseFee * 2))); + env.require(balance(carol, XRP(5000) - (baseFee * 1))); + } + { + // Conditional + 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(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + auto const seq = env.seq(alice); + env(escrow(alice, alice, USD(1000)), + condition(cb2), + finish_time(env.now() + 5s)); + + env.require(balance(alice, XRP(5000) - drops(10))); + env.require(balance(alice, USD(4000))); + + // Not enough time has elapsed for a finish and canceling isn't + // possible. + env(cancel(alice, alice, seq), ter(tecNO_PERMISSION)); + env(cancel(bob, alice, seq), ter(tecNO_PERMISSION)); + env(finish(alice, alice, seq), ter(tecNO_PERMISSION)); + env(finish(alice, alice, seq), + condition(cb2), + fulfillment(fb2), + fee(1500), + ter(tecNO_PERMISSION)); + env(finish(bob, alice, seq), ter(tecNO_PERMISSION)); + env(finish(bob, alice, seq), + condition(cb2), + fulfillment(fb2), + fee(1500), + ter(tecNO_PERMISSION)); + env.close(); + + // Cancel continues to not be possible. Finish is possible but + // requires the fulfillment associated with the escrow. + env(cancel(alice, alice, seq), ter(tecNO_PERMISSION)); + env(cancel(bob, alice, seq), ter(tecNO_PERMISSION)); + env(finish(bob, alice, seq), ter(tecCRYPTOCONDITION_ERROR)); + env(finish(alice, alice, seq), ter(tecCRYPTOCONDITION_ERROR)); + env.close(); + + env(finish(bob, alice, seq), + condition(cb2), + fulfillment(fb2), + fee(1500)); + env.close(); + + env.require(balance(alice, USD(5000))); + } + { + // Self-escrowed conditional with DepositAuth. + 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(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + auto const seq = env.seq(alice); + env(escrow(alice, alice, USD(1000)), + condition(cb3), + finish_time(env.now() + 5s)); + + env.require(balance(alice, XRP(5000) - drops(10))); + env.require(balance(alice, USD(4000))); + + env.close(); + + // Finish is now possible but requires the cryptocondition. + env(finish(bob, alice, seq), ter(tecCRYPTOCONDITION_ERROR)); + env(finish(alice, alice, seq), ter(tecCRYPTOCONDITION_ERROR)); + + // Enable deposit authorization. After this only Alice can finish + // the escrow. + env(fset(alice, asfDepositAuth)); + env.close(); + + env(finish(alice, alice, seq), + condition(cb2), + fulfillment(fb2), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(cb3), + fulfillment(fb3), + fee(1500), + ter(tecNO_PERMISSION)); + env(finish(alice, alice, seq), + condition(cb3), + fulfillment(fb3), + fee(1500)); + env.close(); + + env.require(balance(alice, USD(5000))); + } + { + // Self-escrowed conditional with DepositAuth and DepositPreauth. + Env env{*this, features}; + 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"]; + env.fund(XRP(5000), alice, bob, carol, gw); + env(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), 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 seq = env.seq(alice); + env(escrow(alice, alice, USD(1000)), + condition(cb3), + finish_time(env.now() + 5s)); + + env.require(balance(alice, XRP(5000) - drops(10))); + env.require(balance(alice, USD(4000))); + + env.close(); + + // Alice preauthorizes Zelda for deposit, even though Alice has + // not + // set the lsfDepositAuth flag (yet). + env(deposit::auth(alice, carol)); + env.close(); + + // Finish is now possible but requires the cryptocondition. + env(finish(alice, alice, seq), ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), ter(tecCRYPTOCONDITION_ERROR)); + env(finish(carol, alice, seq), ter(tecCRYPTOCONDITION_ERROR)); + + // Alice enables deposit authorization. After this only Alice or + // Zelda (because Zelda is preauthorized) can finish the escrow. + env(fset(alice, asfDepositAuth)); + env.close(); + + env(finish(alice, alice, seq), + condition(cb2), + fulfillment(fb2), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(cb3), + fulfillment(fb3), + fee(1500), + ter(tecNO_PERMISSION)); + env(finish(carol, alice, seq), + condition(cb3), + fulfillment(fb3), + fee(1500)); + env.close(); + + env.require(balance(alice, USD(5000))); + } + } + + void + testIOUEscrowConditions(FeatureBitset features) + { + testcase("IOU Escrow with CryptoConditions"); + + using namespace jtx; + using namespace std::chrono; + + { // Test cryptoconditions + Env env{*this, features}; + 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"]; + env.fund(XRP(5000), alice, bob, carol, gw); + env(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), 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 seq = env.seq(alice); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 1); + env(escrow(alice, carol, USD(1000)), + condition(cb1), + cancel_time(env.now() + 1s)); + + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 2); + env.require(balance(alice, XRP(5000) - drops(10))); + env.require(balance(alice, USD(4000))); + env.require(balance(carol, USD(5000))); + env(cancel(bob, alice, seq), ter(tecNO_PERMISSION)); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 2); + + // Attempt to finish without a fulfillment + env(finish(bob, alice, seq), ter(tecCRYPTOCONDITION_ERROR)); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 2); + + // Attempt to finish with a condition instead of a fulfillment + env(finish(bob, alice, seq), + condition(cb1), + fulfillment(cb1), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 2); + env(finish(bob, alice, seq), + condition(cb1), + fulfillment(cb2), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 2); + env(finish(bob, alice, seq), + condition(cb1), + fulfillment(cb3), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 2); + + // Attempt to finish with an incorrect condition and various + // combinations of correct and incorrect fulfillments. + env(finish(bob, alice, seq), + condition(cb2), + fulfillment(fb1), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 2); + env(finish(bob, alice, seq), + condition(cb2), + fulfillment(fb2), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 2); + env(finish(bob, alice, seq), + condition(cb2), + fulfillment(fb3), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 2); + + // Attempt to finish with the correct condition & fulfillment + env(finish(bob, alice, seq), + condition(cb1), + fulfillment(fb1), + fee(1500)); + + // SLE removed on finish + BEAST_EXPECT(!env.le(keylet::escrow(Account(alice).id(), seq))); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 1); + env.require(balance(carol, USD(6000))); + env(cancel(bob, alice, seq), ter(tecNO_TARGET)); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 1); + env(cancel(bob, carol, 1), ter(tecNO_TARGET)); + } + { // Test cancel when condition is present + Env env{*this, features}; + 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"]; + env.fund(XRP(5000), alice, bob, carol, gw); + env(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), 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 seq = env.seq(alice); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 1); + env(escrow(alice, carol, USD(1000)), + condition(cb2), + cancel_time(env.now() + 1s)); + env.close(); + env.require(balance(alice, XRP(5000) - drops(10))); + env.require(balance(alice, USD(4000))); + // balance restored on cancel + env(cancel(bob, alice, seq)); + + env.require(balance(alice, XRP(5000) - drops(10))); + env.require(balance(alice, USD(5000))); + // SLE removed on cancel + BEAST_EXPECT(!env.le(keylet::escrow(Account(alice).id(), seq))); + } + { + Env env{*this, features}; + 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"]; + env.fund(XRP(5000), alice, bob, carol, gw); + env(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), 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 seq = env.seq(alice); + env(escrow(alice, carol, USD(1000)), + condition(cb3), + cancel_time(env.now() + 1s)); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 2); + // cancel fails before expiration + env(cancel(bob, alice, seq), ter(tecNO_PERMISSION)); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 2); + env.close(); + // finish fails after expiration + env(finish(bob, alice, seq), + condition(cb3), + fulfillment(fb3), + fee(1500), + ter(tecNO_PERMISSION)); + BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 2); + env.require(balance(carol, USD(5000))); + } + { // Test long & short conditions during creation + Env env{*this, features}; + 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"]; + env.fund(XRP(5000), alice, bob, carol, gw); + env(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), 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(); + + std::vector v; + v.resize(cb1.size() + 2, 0x78); + std::memcpy(v.data() + 1, cb1.data(), cb1.size()); + + auto const p = v.data(); + auto const s = v.size(); + + auto const ts = env.now() + 1s; + + // All these are expected to fail, because the + // condition we pass in is malformed in some way + env(escrow(alice, carol, USD(1000)), + condition(Slice{p, s}), + cancel_time(ts), + ter(temMALFORMED)); + env(escrow(alice, carol, USD(1000)), + condition(Slice{p, s - 1}), + cancel_time(ts), + ter(temMALFORMED)); + env(escrow(alice, carol, USD(1000)), + condition(Slice{p, s - 2}), + cancel_time(ts), + ter(temMALFORMED)); + env(escrow(alice, carol, USD(1000)), + condition(Slice{p + 1, s - 1}), + cancel_time(ts), + ter(temMALFORMED)); + env(escrow(alice, carol, USD(1000)), + condition(Slice{p + 1, s - 3}), + cancel_time(ts), + ter(temMALFORMED)); + env(escrow(alice, carol, USD(1000)), + condition(Slice{p + 2, s - 2}), + cancel_time(ts), + ter(temMALFORMED)); + env(escrow(alice, carol, USD(1000)), + condition(Slice{p + 2, s - 3}), + cancel_time(ts), + ter(temMALFORMED)); + + auto const seq = env.seq(alice); + env(escrow(alice, carol, USD(1000)), + condition(Slice{p + 1, s - 2}), + cancel_time(ts), + fee(100)); + env(finish(bob, alice, seq), + condition(cb1), + fulfillment(fb1), + fee(1500)); + + env.require(balance(alice, XRP(5000) - drops(100))); + env.require(balance(alice, USD(4000))); + env.require(balance(bob, XRP(5000) - drops(1500))); + env.require(balance(bob, USD(5000))); + env.require(balance(carol, XRP(5000))); + env.require(balance(carol, USD(6000))); + } + { // Test long and short conditions & fulfillments during finish + Env env{*this, features}; + 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"]; + env.fund(XRP(5000), alice, bob, carol, gw); + env(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), 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(); + + std::vector cv; + cv.resize(cb2.size() + 2, 0x78); + std::memcpy(cv.data() + 1, cb2.data(), cb2.size()); + + auto const cp = cv.data(); + auto const cs = cv.size(); + + std::vector fv; + fv.resize(fb2.size() + 2, 0x13); + std::memcpy(fv.data() + 1, fb2.data(), fb2.size()); + + auto const fp = fv.data(); + auto const fs = fv.size(); + + auto const ts = env.now() + 1s; + + // All these are expected to fail, because the + // condition we pass in is malformed in some way + env(escrow(alice, carol, USD(1000)), + condition(Slice{cp, cs}), + cancel_time(ts), + ter(temMALFORMED)); + env(escrow(alice, carol, USD(1000)), + condition(Slice{cp, cs - 1}), + cancel_time(ts), + ter(temMALFORMED)); + env(escrow(alice, carol, USD(1000)), + condition(Slice{cp, cs - 2}), + cancel_time(ts), + ter(temMALFORMED)); + env(escrow(alice, carol, USD(1000)), + condition(Slice{cp + 1, cs - 1}), + cancel_time(ts), + ter(temMALFORMED)); + env(escrow(alice, carol, USD(1000)), + condition(Slice{cp + 1, cs - 3}), + cancel_time(ts), + ter(temMALFORMED)); + env(escrow(alice, carol, USD(1000)), + condition(Slice{cp + 2, cs - 2}), + cancel_time(ts), + ter(temMALFORMED)); + env(escrow(alice, carol, USD(1000)), + condition(Slice{cp + 2, cs - 3}), + cancel_time(ts), + ter(temMALFORMED)); + + auto const seq = env.seq(alice); + env(escrow(alice, carol, USD(1000)), + condition(Slice{cp + 1, cs - 2}), + cancel_time(ts), + fee(100)); + + // Now, try to fulfill using the same sequence of + // malformed conditions. + env(finish(bob, alice, seq), + condition(Slice{cp, cs}), + fulfillment(Slice{fp, fs}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(Slice{cp, cs - 1}), + fulfillment(Slice{fp, fs}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(Slice{cp, cs - 2}), + fulfillment(Slice{fp, fs}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(Slice{cp + 1, cs - 1}), + fulfillment(Slice{fp, fs}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(Slice{cp + 1, cs - 3}), + fulfillment(Slice{fp, fs}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(Slice{cp + 2, cs - 2}), + fulfillment(Slice{fp, fs}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(Slice{cp + 2, cs - 3}), + fulfillment(Slice{fp, fs}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + + // Now, using the correct condition, try malformed fulfillments: + env(finish(bob, alice, seq), + condition(Slice{cp + 1, cs - 2}), + fulfillment(Slice{fp, fs}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(Slice{cp + 1, cs - 2}), + fulfillment(Slice{fp, fs - 1}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(Slice{cp + 1, cs - 2}), + fulfillment(Slice{fp, fs - 2}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(Slice{cp + 1, cs - 2}), + fulfillment(Slice{fp + 1, fs - 1}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(Slice{cp + 1, cs - 2}), + fulfillment(Slice{fp + 1, fs - 3}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(Slice{cp + 1, cs - 2}), + fulfillment(Slice{fp + 1, fs - 3}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(Slice{cp + 1, cs - 2}), + fulfillment(Slice{fp + 2, fs - 2}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(Slice{cp + 1, cs - 2}), + fulfillment(Slice{fp + 2, fs - 3}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + + // Now try for the right one + env(finish(bob, alice, seq), + condition(cb2), + fulfillment(fb2), + fee(1500)); + + env.require(balance(alice, XRP(5000) - drops(100))); + env.require(balance(alice, USD(4000))); + env.require(balance(carol, XRP(5000))); + env.require(balance(carol, USD(6000))); + } + { // Test empty condition during creation and + // empty condition & fulfillment during finish + Env env{*this, features}; + 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"]; + env.fund(XRP(5000), alice, bob, carol, gw); + env(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), 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(); + + env(escrow(alice, carol, USD(1000)), + condition(Slice{}), + cancel_time(env.now() + 1s), + ter(temMALFORMED)); + + auto const seq = env.seq(alice); + env(escrow(alice, carol, USD(1000)), + condition(cb3), + cancel_time(env.now() + 1s)); + + env(finish(bob, alice, seq), + condition(Slice{}), + fulfillment(Slice{}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(cb3), + fulfillment(Slice{}), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + env(finish(bob, alice, seq), + condition(Slice{}), + fulfillment(fb3), + fee(1500), + ter(tecCRYPTOCONDITION_ERROR)); + + // Assemble finish that is missing the Condition or the Fulfillment + // since either both must be present, or neither can: + env(finish(bob, alice, seq), condition(cb3), ter(temMALFORMED)); + env(finish(bob, alice, seq), fulfillment(fb3), ter(temMALFORMED)); + + // Now finish it. + env(finish(bob, alice, seq), + condition(cb3), + fulfillment(fb3), + fee(1500)); + + env.require(balance(alice, XRP(5000) - drops(10))); + env.require(balance(alice, USD(4000))); + env.require(balance(carol, XRP(5000))); + env.require(balance(carol, USD(6000))); + } + { // Test a condition other than PreimageSha256, which + // would require a separate amendment + 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(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + std::array cb = { + {0xA2, 0x2B, 0x80, 0x20, 0x42, 0x4A, 0x70, 0x49, 0x49, + 0x52, 0x92, 0x67, 0xB6, 0x21, 0xB3, 0xD7, 0x91, 0x19, + 0xD7, 0x29, 0xB2, 0x38, 0x2C, 0xED, 0x8B, 0x29, 0x6C, + 0x3C, 0x02, 0x8F, 0xA9, 0x7D, 0x35, 0x0F, 0x6D, 0x07, + 0x81, 0x03, 0x06, 0x34, 0xD2, 0x82, 0x02, 0x03, 0xC8}}; + + // FIXME: this transaction should, eventually, return temDISABLED + // instead of temMALFORMED. + env(escrow(alice, bob, USD(1000)), + condition(cb), + cancel_time(env.now() + 1s), + ter(temMALFORMED)); + } + } + + 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, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), 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(alice, alice, USD(1000)), + finish_time(env.now() + 1s), + cancel_time(env.now() + 500s)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + auto const aa = env.le(keylet::escrow(alice.id(), aseq)); + BEAST_EXPECT(aa); + { + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), aa) != aod.end()); + } + + { + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 4); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), aa) != iod.end()); + } + + env(escrow(bob, bob, USD(1000)), + finish_time(env.now() + 1s), + cancel_time(env.now() + 2s)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + auto const bb = env.le(keylet::escrow(bob.id(), bseq)); + BEAST_EXPECT(bb); + + { + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bb) != bod.end()); + } + + { + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 5); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), bb) != iod.end()); + } + + env.close(5s); + env(finish(alice, alice, aseq)); + { + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), aa) == aod.end()); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bb) != bod.end()); + + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 4); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), bb) != iod.end()); + } + + env.close(5s); + env(cancel(bob, bob, bseq)); + { + BEAST_EXPECT(!env.le(keylet::escrow(bob.id(), bseq))); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 1); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bb) == bod.end()); + + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 3); + BEAST_EXPECT( + 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, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), 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(alice, bob, USD(1000)), finish_time(env.now() + 1s)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + env(escrow(bob, carol, USD(1000)), + finish_time(env.now() + 1s), + cancel_time(env.now() + 2s)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + + auto const ab = env.le(keylet::escrow(alice.id(), aseq)); + BEAST_EXPECT(ab); + + auto const bc = env.le(keylet::escrow(bob.id(), bseq)); + BEAST_EXPECT(bc); + + { + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), ab) != aod.end()); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 3); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), ab) != bod.end()); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bc) != bod.end()); + + ripple::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2); + BEAST_EXPECT( + std::find(cod.begin(), cod.end(), bc) != cod.end()); + + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 5); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), ab) != iod.end()); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), bc) != iod.end()); + } + + env.close(5s); + env(finish(alice, alice, aseq)); + { + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); + BEAST_EXPECT(env.le(keylet::escrow(bob.id(), bseq))); + + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), ab) == aod.end()); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 2); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), ab) == bod.end()); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bc) != bod.end()); + + ripple::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2); + + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 4); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), ab) == iod.end()); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), bc) != iod.end()); + } + + env.close(5s); + env(cancel(bob, bob, bseq)); + { + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); + BEAST_EXPECT(!env.le(keylet::escrow(bob.id(), bseq))); + + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), ab) == aod.end()); + + ripple::Dir bod(*env.current(), keylet::ownerDir(bob.id())); + BEAST_EXPECT(std::distance(bod.begin(), bod.end()) == 1); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), ab) == bod.end()); + BEAST_EXPECT( + std::find(bod.begin(), bod.end(), bc) == bod.end()); + + ripple::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 1); + + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 3); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), ab) == iod.end()); + BEAST_EXPECT( + 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, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), alice, carol); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, carol, USD(5000))); + env.close(); + auto const aseq = env.seq(alice); + auto const gseq = env.seq(gw); + + env(escrow(alice, gw, USD(1000)), finish_time(env.now() + 1s)); + + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + env(escrow(gw, carol, USD(1000)), + finish_time(env.now() + 1s), + cancel_time(env.now() + 2s)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + + auto const ag = env.le(keylet::escrow(alice.id(), aseq)); + BEAST_EXPECT(ag); + + auto const gc = env.le(keylet::escrow(gw.id(), gseq)); + BEAST_EXPECT(gc); + + { + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 2); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), ag) != aod.end()); + + ripple::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2); + BEAST_EXPECT( + std::find(cod.begin(), cod.end(), gc) != cod.end()); + + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 4); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), ag) != iod.end()); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), gc) != iod.end()); + } + + env.close(5s); + env(finish(alice, alice, aseq)); + { + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); + BEAST_EXPECT(env.le(keylet::escrow(gw.id(), gseq))); + + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), ag) == aod.end()); + + ripple::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 2); + + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 3); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), ag) == iod.end()); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), gc) != iod.end()); + } + + env.close(5s); + env(cancel(gw, gw, gseq)); + { + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); + BEAST_EXPECT(!env.le(keylet::escrow(gw.id(), gseq))); + + ripple::Dir aod(*env.current(), keylet::ownerDir(alice.id())); + BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); + BEAST_EXPECT( + std::find(aod.begin(), aod.end(), ag) == aod.end()); + + ripple::Dir cod(*env.current(), keylet::ownerDir(carol.id())); + BEAST_EXPECT(std::distance(cod.begin(), cod.end()) == 1); + + ripple::Dir iod(*env.current(), keylet::ownerDir(gw.id())); + BEAST_EXPECT(std::distance(iod.begin(), iod.end()) == 2); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), ag) == iod.end()); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), gc) == iod.end()); + } + } + } + + void + testIOUConsequences(FeatureBitset features) + { + testcase("IOU Consequences"); + + using namespace jtx; + using namespace std::chrono; + Env env{*this, features}; + + 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"]; + env.fund(XRP(5000), alice, bob, carol, gw); + env(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), 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(); + + env.memoize(alice); + env.memoize(bob); + env.memoize(carol); + + { + auto const jtx = env.jt( + escrow(alice, carol, USD(1000)), + finish_time(env.now() + 1s), + seq(1), + fee(10)); + auto const pf = preflight( + env.app(), + env.current()->rules(), + *jtx.stx, + tapNONE, + env.journal); + BEAST_EXPECT(pf.ter == tesSUCCESS); + BEAST_EXPECT(!pf.consequences.isBlocker()); + BEAST_EXPECT(pf.consequences.fee() == drops(10)); + BEAST_EXPECT(pf.consequences.potentialSpend().value() == 0); + } + + { + auto const jtx = env.jt(cancel(bob, alice, 3), seq(1), fee(10)); + auto const pf = preflight( + env.app(), + env.current()->rules(), + *jtx.stx, + tapNONE, + env.journal); + BEAST_EXPECT(pf.ter == tesSUCCESS); + BEAST_EXPECT(!pf.consequences.isBlocker()); + BEAST_EXPECT(pf.consequences.fee() == drops(10)); + BEAST_EXPECT(pf.consequences.potentialSpend().value() == 0); + } + + { + auto const jtx = env.jt(finish(bob, alice, 3), seq(1), fee(10)); + auto const pf = preflight( + env.app(), + env.current()->rules(), + *jtx.stx, + tapNONE, + env.journal); + BEAST_EXPECT(pf.ter == tesSUCCESS); + BEAST_EXPECT(!pf.consequences.isBlocker()); + BEAST_EXPECT(pf.consequences.fee() == drops(10)); + BEAST_EXPECT(pf.consequences.potentialSpend().value() == 0); + } + } + + void + testIOUEscrowWithTickets(FeatureBitset features) + { + testcase("IOU Escrow with tickets"); + + using namespace jtx; + using namespace std::chrono; + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + { + // Create escrow and finish using tickets. + Env env{*this, features}; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + // alice creates a ticket. + std::uint32_t const aliceTicket{env.seq(alice) + 1}; + env(ticket::create(alice, 1)); + + // bob creates a bunch of tickets because he will be burning + // through them with tec transactions. Just because we can + // we'll use them up starting from largest and going smaller. + constexpr static std::uint32_t bobTicketCount{20}; + env(ticket::create(bob, bobTicketCount)); + env.close(); + std::uint32_t bobTicket{env.seq(bob)}; + env.require(tickets(alice, 1)); + env.require(tickets(bob, bobTicketCount)); + + // Note that from here on all transactions use tickets. No account + // root sequences should change. + std::uint32_t const aliceRootSeq{env.seq(alice)}; + std::uint32_t const bobRootSeq{env.seq(bob)}; + + // alice creates an escrow that can be finished in the future + auto const ts = env.now() + 97s; + + std::uint32_t const escrowSeq = aliceTicket; + env(escrow(alice, bob, USD(1000)), + finish_time(ts), + ticket::use(aliceTicket)); + BEAST_EXPECT(env.seq(alice) == aliceRootSeq); + env.require(tickets(alice, 0)); + env.require(tickets(bob, bobTicketCount)); + + // Advance the ledger, verifying that the finish won't complete + // prematurely. Note that each tec consumes one of bob's tickets. + for (; env.now() < ts; env.close()) + { + env(finish(bob, alice, escrowSeq), + fee(1500), + ticket::use(--bobTicket), + ter(tecNO_PERMISSION)); + BEAST_EXPECT(env.seq(bob) == bobRootSeq); + } + + // bob tries to re-use a ticket, which is rejected. + env(finish(bob, alice, escrowSeq), + fee(1500), + ticket::use(bobTicket), + ter(tefNO_TICKET)); + + // bob uses one of his remaining tickets. Success! + env(finish(bob, alice, escrowSeq), + fee(1500), + ticket::use(--bobTicket)); + env.close(); + BEAST_EXPECT(env.seq(bob) == bobRootSeq); + } + + { + // Create escrow and cancel using tickets. + Env env{*this, features}; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + // alice creates a ticket. + std::uint32_t const aliceTicket{env.seq(alice) + 1}; + env(ticket::create(alice, 1)); + + // bob creates a bunch of tickets because he will be burning + // through them with tec transactions. + constexpr std::uint32_t bobTicketCount{20}; + std::uint32_t bobTicket{env.seq(bob) + 1}; + env(ticket::create(bob, bobTicketCount)); + env.close(); + env.require(tickets(alice, 1)); + env.require(tickets(bob, bobTicketCount)); + + // Note that from here on all transactions use tickets. No account + // root sequences should change. + std::uint32_t const aliceRootSeq{env.seq(alice)}; + std::uint32_t const bobRootSeq{env.seq(bob)}; + + // alice creates an escrow that can be finished in the future. + auto const ts = env.now() + 117s; + + std::uint32_t const escrowSeq = aliceTicket; + env(escrow(alice, bob, USD(1000)), + condition(cb1), + cancel_time(ts), + ticket::use(aliceTicket)); + BEAST_EXPECT(env.seq(alice) == aliceRootSeq); + env.require(tickets(alice, 0)); + env.require(tickets(bob, bobTicketCount)); + + // Advance the ledger, verifying that the cancel won't complete + // prematurely. + for (; env.now() < ts; env.close()) + { + env(cancel(bob, alice, escrowSeq), + fee(1500), + ticket::use(bobTicket++), + ter(tecNO_PERMISSION)); + BEAST_EXPECT(env.seq(bob) == bobRootSeq); + } + + // Verify that a finish won't work anymore. + env(finish(bob, alice, escrowSeq), + condition(cb1), + fulfillment(fb1), + fee(1500), + ticket::use(bobTicket++), + ter(tecNO_PERMISSION)); + BEAST_EXPECT(env.seq(bob) == bobRootSeq); + + // Verify that the cancel succeeds. + env(cancel(bob, alice, escrowSeq), + fee(1500), + ticket::use(bobTicket++)); + env.close(); + BEAST_EXPECT(env.seq(bob) == bobRootSeq); + + // Verify that bob actually consumed his tickets. + env.require(tickets(bob, env.seq(bob) - bobTicket)); + } + } + + 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 tests = {{ + // src > dst && src > issuer && dst no trustline + {Account("alice2"), Account("bob0"), Account{"gw0"}, false, true}, + // src < dst && src < issuer && dst no trustline + {Account("carol0"), Account("dan1"), Account{"gw1"}, false, false}, + // // dst > src && dst > issuer && dst no trustline + {Account("dan1"), Account("alice2"), Account{"gw0"}, false, true}, + // // dst < src && dst < issuer && dst no trustline + {Account("bob0"), Account("carol0"), Account{"gw1"}, false, false}, + // // src > dst && src > issuer && dst has trustline + {Account("alice2"), Account("bob0"), Account{"gw0"}, true, true}, + // // src < dst && src < issuer && dst has trustline + {Account("carol0"), Account("dan1"), Account{"gw1"}, true, false}, + // // dst > src && dst > issuer && dst has trustline + {Account("dan1"), Account("alice2"), Account{"gw0"}, true, true}, + // // dst < src && dst < issuer && dst has trustline + {Account("bob0"), Account("carol0"), Account{"gw1"}, true, false}, + }}; + + for (auto const& t : tests) + { + Env env{*this, features}; + auto const USD = t.gw["USD"]; + env.fund(XRP(5000), t.src, t.dst, t.gw); + env(fset(t.gw, asfAllowTokenLocking)); + env.close(); + + if (t.hasTrustline) + env.trust(USD(100000), t.src, t.dst); + else + env.trust(USD(100000), t.src); + env.close(); + + env(pay(t.gw, t.src, USD(10000))); + if (t.hasTrustline) + env(pay(t.gw, t.dst, USD(10000))); + env.close(); + + // src can create escrow + auto const seq1 = env.seq(t.src); + auto const delta = USD(1000); + env(escrow(t.src, t.dst, delta), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + + // dst can finish escrow + auto const preSrc = lineBalance(env, t.src, t.gw, USD); + auto const preDst = lineBalance(env, t.dst, t.gw, USD); + + env(finish(t.dst, t.src, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500)); + env.close(); + + BEAST_EXPECT(lineBalance(env, t.src, t.gw, USD) == preSrc); + BEAST_EXPECT( + lineBalance(env, t.dst, t.gw, USD) == + (t.negative ? (preDst - delta) : (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; + bool negative; + }; + + std::array gwSrcTests = {{ + // src > dst && src > issuer && dst no trustline + {Account("gw0"), Account{"alice2"}, false, true}, + // // src < dst && src < issuer && dst no trustline + {Account("gw1"), Account{"carol0"}, false, false}, + // // // // // dst > src && dst > issuer && dst no trustline + {Account("gw0"), Account{"dan1"}, false, true}, + // // // // // dst < src && dst < issuer && dst no trustline + {Account("gw1"), Account{"bob0"}, false, false}, + // // // // src > dst && src > issuer && dst has trustline + {Account("gw0"), Account{"alice2"}, true, true}, + // // // // src < dst && src < issuer && dst has trustline + {Account("gw1"), Account{"carol0"}, true, false}, + // // // // dst > src && dst > issuer && dst has trustline + {Account("gw0"), Account{"dan1"}, true, true}, + // // // // dst < src && dst < issuer && dst has trustline + {Account("gw1"), Account{"bob0"}, true, false}, + }}; + + for (auto const& t : gwSrcTests) + { + Env env{*this, features}; + auto const USD = t.src["USD"]; + env.fund(XRP(5000), t.dst, t.src); + env(fset(t.src, asfAllowTokenLocking)); + env.close(); + + if (t.hasTrustline) + env.trust(USD(100000), t.dst); + + env.close(); + + if (t.hasTrustline) + env(pay(t.src, t.dst, USD(10000))); + + env.close(); + + // issuer can create escrow + auto const seq1 = env.seq(t.src); + auto const preDst = lineBalance(env, t.dst, t.src, USD); + env(escrow(t.src, t.dst, USD(1000)), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + + // src can finish escrow, no dest trustline + env(finish(t.dst, t.src, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500)); + env.close(); + auto const preAmount = t.hasTrustline ? 10000 : 0; + BEAST_EXPECT( + preDst == (t.negative ? -USD(preAmount) : USD(preAmount))); + auto const postAmount = t.hasTrustline ? 11000 : 1000; + BEAST_EXPECT( + lineBalance(env, t.dst, t.src, USD) == + (t.negative ? -USD(postAmount) : USD(postAmount))); + BEAST_EXPECT(lineBalance(env, t.src, t.src, USD) == USD(0)); + } + + std::array gwDstTests = {{ + // // // // src > dst && src > issuer && dst has trustline + {Account("alice2"), Account{"gw0"}, true, true}, + // // // // src < dst && src < issuer && dst has trustline + {Account("carol0"), Account{"gw1"}, true, false}, + // // // // dst > src && dst > issuer && dst has trustline + {Account("dan1"), Account{"gw0"}, true, true}, + // // // // dst < src && dst < issuer && dst has trustline + {Account("bob0"), Account{"gw1"}, true, false}, + }}; + + for (auto const& t : gwDstTests) + { + Env env{*this, features}; + auto const USD = t.dst["USD"]; + env.fund(XRP(5000), t.dst, t.src); + env(fset(t.dst, asfAllowTokenLocking)); + env.close(); + + env.trust(USD(100000), t.src); + env.close(); + + env(pay(t.dst, t.src, USD(10000))); + env.close(); + + // issuer can receive escrow + auto const seq1 = env.seq(t.src); + auto const preSrc = lineBalance(env, t.src, t.dst, USD); + env(escrow(t.src, t.dst, USD(1000)), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + + // issuer can finish escrow, no dest trustline + env(finish(t.dst, t.src, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500)); + env.close(); + auto const preAmount = 10000; + BEAST_EXPECT( + preSrc == (t.negative ? -USD(preAmount) : USD(preAmount))); + auto const postAmount = 9000; + BEAST_EXPECT( + lineBalance(env, t.src, t.dst, USD) == + (t.negative ? -USD(postAmount) : USD(postAmount))); + BEAST_EXPECT(lineBalance(env, t.dst, t.dst, USD) == USD(0)); + } + + // issuer is source and destination + { + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + Env env{*this, features}; + env.fund(XRP(5000), gw); + env(fset(gw, asfAllowTokenLocking)); + env.close(); + + // issuer can receive escrow + auto const seq1 = env.seq(gw); + env(escrow(gw, gw, USD(1000)), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + + // issuer can finish escrow + env(finish(gw, gw, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500)); + 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}; + env.fund(XRP(10000), alice, bob, gw); + env(fset(gw, asfAllowTokenLocking)); + env(rate(gw, 1.25)); + env.close(); + env.trust(USD(100000), alice); + env.trust(USD(100000), bob); + env.close(); + env(pay(gw, alice, USD(10000))); + env(pay(gw, bob, USD(10000))); + 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(alice, bob, delta), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + auto const transferRate = escrowRate(env, alice, seq1); + BEAST_EXPECT( + transferRate.value == std::uint32_t(1000000000 * 1.25)); + + // bob can finish escrow + env(finish(bob, alice, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == preAlice - delta); + BEAST_EXPECT(env.balance(bob, USD) == USD(10100)); + } + // test rate change - higher + { + Env env{*this, features}; + env.fund(XRP(10000), alice, bob, gw); + env(fset(gw, asfAllowTokenLocking)); + env(rate(gw, 1.25)); + env.close(); + env.trust(USD(100000), alice); + env.trust(USD(100000), bob); + env.close(); + env(pay(gw, alice, USD(10000))); + env(pay(gw, bob, USD(10000))); + 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(alice, bob, delta), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + auto transferRate = escrowRate(env, alice, seq1); + BEAST_EXPECT( + transferRate.value == std::uint32_t(1000000000 * 1.25)); + + // issuer changes rate higher + env(rate(gw, 1.26)); + env.close(); + + // bob can finish escrow - rate unchanged + env(finish(bob, alice, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == preAlice - delta); + BEAST_EXPECT(env.balance(bob, USD) == USD(10100)); + } + // test rate change - lower + { + Env env{*this, features}; + env.fund(XRP(10000), alice, bob, gw); + env(fset(gw, asfAllowTokenLocking)); + env(rate(gw, 1.25)); + env.close(); + env.trust(USD(100000), alice); + env.trust(USD(100000), bob); + env.close(); + env(pay(gw, alice, USD(10000))); + env(pay(gw, bob, USD(10000))); + 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(alice, bob, delta), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + auto transferRate = escrowRate(env, alice, seq1); + BEAST_EXPECT( + transferRate.value == std::uint32_t(1000000000 * 1.25)); + + // issuer changes rate higher + env(rate(gw, 1.00)); + env.close(); + + // bob can finish escrow - rate changed + env(finish(bob, alice, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == preAlice - delta); + BEAST_EXPECT(env.balance(bob, USD) == USD(10125)); + } + // test issuer doesnt pay own rate + { + Env env{*this, features}; + env.fund(XRP(10000), alice, bob, gw); + env(fset(gw, asfAllowTokenLocking)); + env(rate(gw, 1.25)); + env.close(); + env.trust(USD(100000), alice); + env.trust(USD(100000), bob); + env.close(); + env(pay(gw, alice, USD(10000))); + env(pay(gw, bob, USD(10000))); + env.close(); + + // issuer with rate can create escrow + auto const preAlice = env.balance(alice, USD); + auto const seq1 = env.seq(gw); + auto const delta = USD(125); + env(escrow(gw, alice, delta), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + + auto transferRate = escrowRate(env, gw, seq1); + BEAST_EXPECT( + transferRate.value == std::uint32_t(1000000000 * 1.25)); + + // alice can finish escrow - no rate charged + env(finish(alice, gw, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == preAlice + delta); + BEAST_EXPECT(env.balance(alice, USD) == USD(10125)); + } + } + + void + testIOUTLLimitAmount(FeatureBitset features) + { + testcase("IOU Trustline 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}; + env.fund(XRP(1000), alice, bob, gw); + env(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(10000), alice, bob); + env.close(); + env(pay(gw, alice, USD(1000))); + env(pay(gw, bob, USD(1000))); + env.close(); + + // alice can create escrow + auto seq1 = env.seq(alice); + auto const delta = USD(125); + env(escrow(alice, bob, delta), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + + // bob can finish + auto const preBobLimit = limitAmount(env, bob, gw, USD); + env(finish(bob, alice, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500)); + env.close(); + auto const postBobLimit = limitAmount(env, bob, gw, USD); + // bobs limit is NOT changed + BEAST_EXPECT(postBobLimit == preBobLimit); + } + } + + void + testIOUTLRequireAuth(FeatureBitset features) + { + testcase("IOU Trustline 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"]; + + // test asfRequireAuth + { + Env env{*this, features}; + env.fund(XRP(1000), alice, bob, gw); + env(fset(gw, asfAllowTokenLocking)); + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(gw, aliceUSD(10000)), txflags(tfSetfAuth)); + env(trust(alice, USD(10000))); + env(trust(bob, USD(10000))); + env.close(); + env(pay(gw, alice, USD(1000))); + env.close(); + + // alice cannot create escrow - fails without auth + auto seq1 = env.seq(alice); + auto const delta = USD(125); + env(escrow(alice, bob, delta), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500), + ter(tecNO_AUTH)); + env.close(); + + // set auth on bob + env(trust(gw, bobUSD(10000)), txflags(tfSetfAuth)); + env(trust(bob, USD(10000))); + env.close(); + env(pay(gw, bob, USD(1000))); + env.close(); + + // alice can create escrow - bob has auth + seq1 = env.seq(alice); + env(escrow(alice, bob, delta), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + + // bob can finish + env(finish(bob, alice, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500)); + env.close(); + } + } + + void + testIOUTLFreeze(FeatureBitset features) + { + testcase("IOU Trustline 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}; + env.fund(XRP(10000), alice, bob, gw); + env(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(100000), alice); + env.trust(USD(100000), bob); + env.close(); + env(pay(gw, alice, USD(10000))); + env(pay(gw, bob, USD(10000))); + 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(alice, bob, delta), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500), + ter(tecFROZEN)); + env.close(); + + // clear global freeze + env(fclear(gw, asfGlobalFreeze)); + env.close(); + + // create escrow success + seq1 = env.seq(alice); + env(escrow(alice, bob, delta), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + + // set global freeze + env(fset(gw, asfGlobalFreeze)); + env.close(); + + // bob finish escrow success regardless of frozen assets + env(finish(bob, alice, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500)); + env.close(); + } + // test Individual Freeze + { + // Env Setup + Env env{*this, features}; + env.fund(XRP(10000), alice, bob, gw); + env(fset(gw, asfAllowTokenLocking)); + env.close(); + env(trust(alice, USD(100000))); + env(trust(bob, USD(100000))); + env.close(); + env(pay(gw, alice, USD(10000))); + env(pay(gw, bob, USD(10000))); + env.close(); + + // set freeze on alice trustline + env(trust(gw, USD(10000), alice, tfSetFreeze)); + env.close(); + + // setup transaction + auto seq1 = env.seq(alice); + auto const delta = USD(125); + + // create escrow fails - frozen trustline + env(escrow(alice, bob, delta), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500), + ter(tecFROZEN)); + env.close(); + + // clear freeze on alice trustline + env(trust(gw, USD(10000), alice, tfClearFreeze)); + env.close(); + + // create escrow success + seq1 = env.seq(alice); + env(escrow(alice, bob, delta), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + + // set freeze on bob trustline + env(trust(gw, USD(10000), bob, tfSetFreeze)); + env.close(); + + // bob finish escrow success regardless of frozen assets + env(finish(bob, alice, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500)); + env.close(); + } + } + void + testIOUTLINSF(FeatureBitset features) + { + testcase("IOU Trustline Insuficient 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 10000, escrow 1000 then try to pay 10000 + Env env{*this, features}; + env.fund(XRP(10000), alice, bob, gw); + env(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(100000), alice); + env.trust(USD(100000), bob); + env.close(); + env(pay(gw, alice, USD(10000))); + env(pay(gw, bob, USD(10000))); + env.close(); + + // create escrow success + auto const delta = USD(1000); + env(escrow(alice, bob, delta), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + env(pay(alice, gw, USD(10000)), ter(tecPATH_PARTIAL)); + } + { + // test tecINSUFFICIENT_FUNDS + // ie. has 10000 escrow 1000 then try to escrow 10000 + Env env{*this, features}; + env.fund(XRP(10000), alice, bob, gw); + env(fset(gw, asfAllowTokenLocking)); + env.close(); + env.trust(USD(100000), alice); + env.trust(USD(100000), bob); + env.close(); + env(pay(gw, alice, USD(10000))); + env(pay(gw, bob, USD(10000))); + env.close(); + + auto const delta = USD(1000); + env(escrow(alice, bob, delta), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + + env(escrow(alice, bob, USD(10000)), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500), + 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); + env.fund(XRP(10000), alice, bob, gw); + env(fset(gw, asfAllowTokenLocking)); + 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(); + + // alice cannot create escrow for 1/10 iou - precision loss + env(escrow(alice, bob, USD(1)), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500), + ter(tecPRECISION_LOSS)); + env.close(); + + auto const seq1 = env.seq(alice); + // alice can create escrow for 1000 iou + env(escrow(alice, bob, USD(1000)), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500)); + env.close(); + + // bob finish escrow success + env(finish(bob, alice, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500)); + 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 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(temDISABLED); + auto const finishResult = + withTokenEscrow ? ter(tesSUCCESS) : ter(tecNO_TARGET); + env(escrow(alice, bob, MPT(1000)), + finish_time(env.now() + 1s), + createResult); + env.close(); + + auto const seq1 = env.seq(alice); + + env(escrow(alice, bob, MPT(1000)), + condition(cb1), + finish_time(env.now() + 1s), + fee(1500), + createResult); + env.close(); + env(finish(bob, alice, seq1), + condition(cb1), + fulfillment(fb1), + fee(1500), + finishResult); + + auto const seq2 = env.seq(alice); + + env(escrow(alice, bob, MPT(1000)), + condition(cb2), + finish_time(env.now() + 1s), + cancel_time(env.now() + 2s), + fee(1500), + createResult); + env.close(); + env(cancel(bob, alice, seq2), fee(1500), finishResult); + } + } + + void + testIOUWithFeats(FeatureBitset features) + { + testIOUEnablement(features); + // testIOUTiming(features); + // testIOUTags(features); + // testIOU1571(features); + // testIOUFails(features); + // testIOULockup(features); + // testIOUEscrowConditions(features); + // testIOUMetaAndOwnership(features); + // testIOUConsequences(features); + // testIOUEscrowWithTickets(features); + // testIOURippleState(features); + // testIOUGateway(features); + // testIOULockedRate(features); + // testIOUTLLimitAmount(features); + // testIOUTLRequireAuth(features); + // testIOUTLFreeze(features); + // testIOUTLINSF(features); + // testIOUPrecisionLoss(features); + } + + void + testMPTWithFeats(FeatureBitset features) + { + testMPTEnablement(features); + // testIOULockup(features); + // testIOURippleState(features); + // testIOUGateway(features); + // testIOULockedRate(features); + // testIOUTLLimitAmount(features); + // testIOUTLRequireAuth(features); + // testIOUTLFreeze(features); + // testIOUTLINSF(features); + // testIOUPrecisionLoss(features); + } + +public: + void + run() override + { + using namespace test::jtx; + FeatureBitset const all{supported_amendments()}; + testIOUWithFeats(all); + testMPTWithFeats(all); + } +}; + +BEAST_DEFINE_TESTSUITE(EscrowToken, app, ripple); + +} // namespace test +} // namespace ripple \ No newline at end of file diff --git a/src/test/app/Escrow_test.cpp b/src/test/app/Escrow_test.cpp index 714fc7734d9..99545015bb7 100644 --- a/src/test/app/Escrow_test.cpp +++ b/src/test/app/Escrow_test.cpp @@ -63,14 +63,14 @@ struct Escrow_test : public beast::unit_test::suite 0x57, 0x0D, 0x15, 0x85, 0x8B, 0xD4, 0x81, 0x01, 0x04}}; void - testEnablement() + testEnablement(FeatureBitset features) { testcase("Enablement"); using namespace jtx; using namespace std::chrono; - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "alice", "bob"); env(escrow("alice", "bob", XRP(1000)), finish_time(env.now() + 1s)); env.close(); @@ -99,14 +99,14 @@ struct Escrow_test : public beast::unit_test::suite } void - testTiming() + testTiming(FeatureBitset features) { using namespace jtx; using namespace std::chrono; { testcase("Timing: Finish Only"); - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "alice", "bob"); env.close(); @@ -128,7 +128,7 @@ struct Escrow_test : public beast::unit_test::suite { testcase("Timing: Cancel Only"); - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "alice", "bob"); env.close(); @@ -160,7 +160,7 @@ struct Escrow_test : public beast::unit_test::suite { testcase("Timing: Finish and Cancel -> Finish"); - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "alice", "bob"); env.close(); @@ -194,7 +194,7 @@ struct Escrow_test : public beast::unit_test::suite { testcase("Timing: Finish and Cancel -> Cancel"); - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "alice", "bob"); env.close(); @@ -236,14 +236,14 @@ struct Escrow_test : public beast::unit_test::suite } void - testTags() + testTags(FeatureBitset features) { testcase("Tags"); using namespace jtx; using namespace std::chrono; - Env env(*this); + Env env(*this, features); auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -272,7 +272,7 @@ struct Escrow_test : public beast::unit_test::suite } void - testDisallowXRP() + testDisallowXRP(FeatureBitset features) { testcase("Disallow XRP"); @@ -281,7 +281,7 @@ struct Escrow_test : public beast::unit_test::suite { // Respect the "asfDisallowXRP" account flag: - Env env(*this, supported_amendments() - featureDepositAuth); + Env env(*this, features - featureDepositAuth); env.fund(XRP(5000), "bob", "george"); env(fset("george", asfDisallowXRP)); @@ -292,7 +292,7 @@ struct Escrow_test : public beast::unit_test::suite { // Ignore the "asfDisallowXRP" account flag, which we should // have been doing before. - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "bob", "george"); env(fset("george", asfDisallowXRP)); @@ -301,7 +301,7 @@ struct Escrow_test : public beast::unit_test::suite } void - test1571() + test1571(FeatureBitset features) { using namespace jtx; using namespace std::chrono; @@ -309,7 +309,7 @@ struct Escrow_test : public beast::unit_test::suite { testcase("Implied Finish Time (without fix1571)"); - Env env(*this, supported_amendments() - fix1571); + Env env(*this, features - fix1571); env.fund(XRP(5000), "alice", "bob", "carol"); env.close(); @@ -343,7 +343,7 @@ struct Escrow_test : public beast::unit_test::suite { testcase("Implied Finish Time (with fix1571)"); - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "alice", "bob", "carol"); env.close(); @@ -370,14 +370,14 @@ struct Escrow_test : public beast::unit_test::suite } void - testFails() + testFails(FeatureBitset features) { testcase("Failure Cases"); using namespace jtx; using namespace std::chrono; - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "alice", "bob"); env.close(); @@ -400,9 +400,13 @@ struct Escrow_test : public beast::unit_test::suite env.fund(XRP(5000), "carol"); // Using non-XRP: + bool const withTokenEscrow = + env.current()->rules().enabled(featureTokenEscrow); + auto const txResult = + withTokenEscrow ? ter(tecNO_PERMISSION) : ter(temDISABLED); env(escrow("alice", "carol", Account("alice")["USD"](500)), finish_time(env.now() + 1s), - ter(temBAD_AMOUNT)); + txResult); // Sending zero or no XRP: env(escrow("alice", "carol", XRP(0)), @@ -502,7 +506,7 @@ struct Escrow_test : public beast::unit_test::suite } void - testLockup() + testLockup(FeatureBitset features) { testcase("Lockup"); @@ -511,7 +515,7 @@ struct Escrow_test : public beast::unit_test::suite { // Unconditional - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "alice", "bob"); auto const seq = env.seq("alice"); env(escrow("alice", "alice", XRP(1000)), @@ -535,7 +539,7 @@ struct Escrow_test : public beast::unit_test::suite // Unconditionally pay from Alice to Bob. Zelda (neither source nor // destination) signs all cancels and finishes. This shows that // Escrow will make a payment to Bob with no intervention from Bob. - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "alice", "bob", "zelda"); auto const seq = env.seq("alice"); env(escrow("alice", "bob", XRP(1000)), finish_time(env.now() + 5s)); @@ -560,7 +564,7 @@ struct Escrow_test : public beast::unit_test::suite } { // Bob sets DepositAuth so only Bob can finish the escrow. - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "alice", "bob", "zelda"); env(fset("bob", asfDepositAuth)); @@ -598,7 +602,7 @@ struct Escrow_test : public beast::unit_test::suite { // Bob sets DepositAuth but preauthorizes Zelda, so Zelda can // finish the escrow. - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "alice", "bob", "zelda"); env(fset("bob", asfDepositAuth)); @@ -625,7 +629,7 @@ struct Escrow_test : public beast::unit_test::suite } { // Conditional - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "alice", "bob"); auto const seq = env.seq("alice"); env(escrow("alice", "alice", XRP(1000)), @@ -666,7 +670,7 @@ struct Escrow_test : public beast::unit_test::suite } { // Self-escrowed conditional with DepositAuth. - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "alice", "bob"); auto const seq = env.seq("alice"); @@ -702,7 +706,7 @@ struct Escrow_test : public beast::unit_test::suite } { // Self-escrowed conditional with DepositAuth and DepositPreauth. - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "alice", "bob", "zelda"); auto const seq = env.seq("alice"); @@ -745,7 +749,7 @@ struct Escrow_test : public beast::unit_test::suite } void - testEscrowConditions() + testEscrowConditions(FeatureBitset features) { testcase("Escrow with CryptoConditions"); @@ -753,7 +757,7 @@ struct Escrow_test : public beast::unit_test::suite using namespace std::chrono; { // Test cryptoconditions - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "alice", "bob", "carol"); auto const seq = env.seq("alice"); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0); @@ -826,7 +830,7 @@ struct Escrow_test : public beast::unit_test::suite env(cancel("bob", "carol", 1), ter(tecNO_TARGET)); } { // Test cancel when condition is present - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "alice", "bob", "carol"); auto const seq = env.seq("alice"); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0); @@ -842,7 +846,7 @@ struct Escrow_test : public beast::unit_test::suite BEAST_EXPECT(!env.le(keylet::escrow(Account("alice").id(), seq))); } { - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "alice", "bob", "carol"); env.close(); auto const seq = env.seq("alice"); @@ -864,7 +868,7 @@ struct Escrow_test : public beast::unit_test::suite env.require(balance("carol", XRP(5000))); } { // Test long & short conditions during creation - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "alice", "bob", "carol"); std::vector v; @@ -921,7 +925,7 @@ struct Escrow_test : public beast::unit_test::suite env.require(balance("carol", XRP(6000))); } { // Test long and short conditions & fulfillments during finish - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "alice", "bob", "carol"); std::vector cv; @@ -1067,7 +1071,7 @@ struct Escrow_test : public beast::unit_test::suite } { // Test empty condition during creation and // empty condition & fulfillment during finish - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "alice", "bob", "carol"); env(escrow("alice", "carol", XRP(1000)), @@ -1113,7 +1117,7 @@ struct Escrow_test : public beast::unit_test::suite } { // Test a condition other than PreimageSha256, which // would require a separate amendment - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), "alice", "bob"); std::array cb = { @@ -1133,7 +1137,7 @@ struct Escrow_test : public beast::unit_test::suite } void - testMetaAndOwnership() + testMetaAndOwnership(FeatureBitset features) { using namespace jtx; using namespace std::chrono; @@ -1145,7 +1149,7 @@ struct Escrow_test : public beast::unit_test::suite { testcase("Metadata to self"); - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), alice, bruce, carol); auto const aseq = env.seq(alice); auto const bseq = env.seq(bruce); @@ -1220,7 +1224,7 @@ struct Escrow_test : public beast::unit_test::suite { testcase("Metadata to other"); - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), alice, bruce, carol); auto const aseq = env.seq(alice); auto const bseq = env.seq(bruce); @@ -1310,13 +1314,13 @@ struct Escrow_test : public beast::unit_test::suite } void - testConsequences() + testConsequences(FeatureBitset features) { testcase("Consequences"); using namespace jtx; using namespace std::chrono; - Env env(*this); + Env env(*this, features); env.memoize("alice"); env.memoize("bob"); @@ -1370,7 +1374,7 @@ struct Escrow_test : public beast::unit_test::suite } void - testEscrowWithTickets() + testEscrowWithTickets(FeatureBitset features) { testcase("Escrow with tickets"); @@ -1381,7 +1385,7 @@ struct Escrow_test : public beast::unit_test::suite { // Create escrow and finish using tickets. - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), alice, bob); env.close(); @@ -1442,7 +1446,7 @@ struct Escrow_test : public beast::unit_test::suite { // Create escrow and cancel using tickets. - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), alice, bob); env.close(); @@ -1509,7 +1513,7 @@ struct Escrow_test : public beast::unit_test::suite } void - testCredentials() + testCredentials(FeatureBitset features) { testcase("Test with credentials"); @@ -1526,7 +1530,7 @@ struct Escrow_test : public beast::unit_test::suite { // Credentials amendment not enabled - Env env(*this, supported_amendments() - featureCredentials); + Env env(*this, features - featureCredentials); env.fund(XRP(5000), alice, bob); env.close(); @@ -1548,7 +1552,7 @@ struct Escrow_test : public beast::unit_test::suite } { - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), alice, bob, carol, dillon, zelda); env.close(); @@ -1600,7 +1604,7 @@ struct Escrow_test : public beast::unit_test::suite testcase("Escrow with credentials without depositPreauth"); using namespace std::chrono; - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), alice, bob, carol, dillon, zelda); env.close(); @@ -1656,21 +1660,31 @@ struct Escrow_test : public beast::unit_test::suite } } + void + testWithFeats(FeatureBitset features) + { + testEnablement(features); + testTiming(features); + testTags(features); + testDisallowXRP(features); + test1571(features); + testFails(features); + testLockup(features); + testEscrowConditions(features); + testMetaAndOwnership(features); + testConsequences(features); + testEscrowWithTickets(features); + testCredentials(features); + } + +public: void run() override { - testEnablement(); - testTiming(); - testTags(); - testDisallowXRP(); - test1571(); - testFails(); - testLockup(); - testEscrowConditions(); - testMetaAndOwnership(); - testConsequences(); - testEscrowWithTickets(); - testCredentials(); + using namespace test::jtx; + FeatureBitset const all{supported_amendments()}; + testWithFeats(all); + testWithFeats(all - featureTokenEscrow); } }; diff --git a/src/test/ledger/Invariants_test.cpp b/src/test/ledger/Invariants_test.cpp index ecf1c8e3979..d0f529e9c99 100644 --- a/src/test/ledger/Invariants_test.cpp +++ b/src/test/ledger/Invariants_test.cpp @@ -728,20 +728,20 @@ class Invariants_test : public beast::unit_test::suite using namespace test::jtx; testcase << "no zero escrow"; - doInvariantCheck( - {{"Cannot return non-native STAmount as XRPAmount"}}, - [](Account const& A1, Account const& A2, ApplyContext& ac) { - // escrow with nonnative amount - auto const sle = ac.view().peek(keylet::account(A1.id())); - if (!sle) - return false; - auto sleNew = std::make_shared( - keylet::escrow(A1, (*sle)[sfSequence] + 2)); - STAmount nonNative(A2["USD"](51)); - sleNew->setFieldAmount(sfAmount, nonNative); - ac.view().insert(sleNew); - return true; - }); + // doInvariantCheck( + // {{"Cannot return non-native STAmount as XRPAmount"}}, + // [](Account const& A1, Account const& A2, ApplyContext& ac) { + // // escrow with nonnative amount + // auto const sle = ac.view().peek(keylet::account(A1.id())); + // if (!sle) + // return false; + // auto sleNew = std::make_shared( + // keylet::escrow(A1, (*sle)[sfSequence] + 2)); + // STAmount nonNative(A2["USD"](51)); + // sleNew->setFieldAmount(sfAmount, nonNative); + // ac.view().insert(sleNew); + // return true; + // }); doInvariantCheck( {{"XRP net change of -1000000 doesn't match fee 0"}, diff --git a/src/test/rpc/AccountSet_test.cpp b/src/test/rpc/AccountSet_test.cpp index 3c6cad00e28..68ac30fb879 100644 --- a/src/test/rpc/AccountSet_test.cpp +++ b/src/test/rpc/AccountSet_test.cpp @@ -99,6 +99,12 @@ class AccountSet_test : public beast::unit_test::suite // is tested elsewhere. continue; } + if (flag == asfAllowTokenLocking) + { + // These flags are part of the AllowTokenLocking amendment + // and are tested elsewhere + continue; + } if (std::find(goodFlags.begin(), goodFlags.end(), flag) != goodFlags.end()) diff --git a/src/xrpld/app/tx/detail/Escrow.cpp b/src/xrpld/app/tx/detail/Escrow.cpp index 48b9867d3a0..4ec54719f79 100644 --- a/src/xrpld/app/tx/detail/Escrow.cpp +++ b/src/xrpld/app/tx/detail/Escrow.cpp @@ -24,12 +24,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -94,7 +96,40 @@ after(NetClock::time_point now, std::uint32_t mark) TxConsequences EscrowCreate::makeTxConsequences(PreflightContext const& ctx) { - return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; + return TxConsequences{ + ctx.tx, isXRP(ctx.tx[sfAmount]) ? ctx.tx[sfAmount].xrp() : beast::zero}; +} + +template +static NotTEC +escrowCreatePreflightHelper(PreflightContext const& ctx); + +template <> +NotTEC +escrowCreatePreflightHelper(PreflightContext const& ctx) +{ + STAmount const amount = ctx.tx[sfAmount]; + if (!isLegalNet(amount)) + return temBAD_AMOUNT; + + if (badCurrency() == amount.getCurrency()) + return temBAD_CURRENCY; + + return tesSUCCESS; +} + +template <> +NotTEC +escrowCreatePreflightHelper(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureMPTokensV1)) + return temDISABLED; + + auto const amount = ctx.tx[sfAmount]; + if (amount.mpt() > MPTAmount{maxMPTokenAmount} || amount <= beast::zero) + return temBAD_AMOUNT; + + return tesSUCCESS; } NotTEC @@ -106,10 +141,22 @@ EscrowCreate::preflight(PreflightContext const& ctx) if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; - if (!isXRP(ctx.tx[sfAmount])) - return temBAD_AMOUNT; + STAmount const amount{ctx.tx[sfAmount]}; + if (!isXRP(amount)) + { + if (!ctx.rules.enabled(featureTokenEscrow)) + return temDISABLED; - if (ctx.tx[sfAmount] <= beast::zero) + if (auto const ret = std::visit( + [&](T const&) { + return escrowCreatePreflightHelper(ctx); + }, + ctx.tx[sfAmount].asset().value()); + !isTesSuccess(ret)) + return ret; + } + + if (amount <= beast::zero) return temBAD_AMOUNT; // We must specify at least one timeout value @@ -157,15 +204,220 @@ EscrowCreate::preflight(PreflightContext const& ctx) return preflight2(ctx); } +template +static TER +escrowPreclaimHelper( + PreclaimContext const& ctx, + AccountID const& issuer, + AccountID const& account, + AccountID const& dest, + STAmount const& amount); + +template <> +TER +escrowPreclaimHelper( + PreclaimContext const& ctx, + AccountID const& issuer, + AccountID const& account, + AccountID const& dest, + STAmount const& amount) +{ + // If the issuer is the same as the account, return tesSUCCESS + if (issuer == account) + return tesSUCCESS; + + // If the lsfAllowTokenLocking is not enabled, return tecNO_PERMISSION + auto const sleIssuer = ctx.view.read(keylet::account(issuer)); + if (!sleIssuer) + return tecNO_ISSUER; + if (!(sleIssuer->getFlags() & lsfAllowTokenLocking)) + return tecNO_PERMISSION; + + // If the account does not have a trustline to the issuer, return tecNO_LINE + auto const sleRippleState = + ctx.view.read(keylet::line(account, issuer, amount.getCurrency())); + if (!sleRippleState) + return tecNO_LINE; + + STAmount const balance = (*sleRippleState)[sfBalance]; + + // If balance is positive, issuer must have higher address than account + if (balance > beast::zero && issuer < account) + return tecNO_PERMISSION; + + // If balance is negative, issuer must have lower address than account + if (balance < beast::zero && issuer > account) + return tecNO_PERMISSION; + + // If the issuer has no default ripple return tecNO_RIPPLE + if (noDefaultRipple(ctx.view, amount.issue())) + return terNO_RIPPLE; + + // If the issuer has requireAuth set, check if the account is authorized + if (auto const ter = requireAuth(ctx.view, amount.issue(), account); + ter != tesSUCCESS) + return ter; + + // If the issuer has requireAuth set, check if the destination is authorized + if (auto const ter = requireAuth(ctx.view, amount.issue(), dest); + ter != tesSUCCESS) + return ter; + + // If the issuer has frozen the account, return tecFROZEN + if (isFrozen(ctx.view, account, amount.issue())) + return tecFROZEN; + + // If the issuer has frozen the destination, return tecFROZEN + if (isFrozen(ctx.view, dest, amount.issue())) + return tecFROZEN; + + STAmount const spendableAmount = accountHolds( + ctx.view, + account, + amount.getCurrency(), + issuer, + fhIGNORE_FREEZE, + ctx.j); + + // If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS + if (spendableAmount <= beast::zero) + return tecINSUFFICIENT_FUNDS; + + // If the amount is not addable to the balance, return tecPRECISION_LOSS + if (!isAddable(spendableAmount, amount)) + return tecPRECISION_LOSS; + + return tesSUCCESS; +} + +template <> +TER +escrowPreclaimHelper( + PreclaimContext const& ctx, + AccountID const& issuer, + AccountID const& account, + AccountID const& dest, + STAmount const& amount) +{ + // If the issuer is the same as the account, return tesSUCCESS + if (issuer == account) + return tesSUCCESS; + + // If the mpt does not exist, return tecOBJECT_NOT_FOUND + auto const issuanceKey = + keylet::mptIssuance(amount.get().getMptID()); + auto const sleIssuance = ctx.view.read(issuanceKey); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + // If the tfMPTCanEscrow is not enabled, return tecNO_PERMISSION + if (!(sleIssuance->getFieldU32(sfFlags) & tfMPTCanEscrow)) + return tecNO_PERMISSION; + + // If the issuer is not the same as the issuer of the mpt, return + // tecNO_PERMISSION + if (sleIssuance->getAccountID(sfIssuer) != issuer) + return tecNO_PERMISSION; + + // If the account does not have the mpt, return tecOBJECT_NOT_FOUND + if (!ctx.view.exists(keylet::mptoken(issuanceKey.key, account))) + return tecOBJECT_NOT_FOUND; + + auto requireAuth = [](ReadView const& view, + MPTIssue const& mptIssue, + AccountID const& account) -> TER { + auto const mptID = keylet::mptIssuance(mptIssue.getMptID()); + auto const sleIssuance = view.read(mptID); + + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + auto const mptIssuer = sleIssuance->getAccountID(sfIssuer); + + // issuer is always "authorized" + if (mptIssuer == account) + return tesSUCCESS; + + if (sleIssuance->getFieldU32(sfFlags) & lsfMPTRequireAuth) + { + auto const mptokenID = keylet::mptoken(mptID.key, account); + auto const sleToken = view.read(mptokenID); + if (!sleToken) + return tecNO_AUTH; + + if (!(sleToken->getFlags() & lsfMPTAuthorized)) + return tecNO_AUTH; + } + return tesSUCCESS; + }; + + // If the issuer has requireAuth set, check if the account is authorized + auto const& mptIssue = amount.get(); + if (auto const ter = requireAuth(ctx.view, mptIssue, account); + ter != tesSUCCESS) + return ter; + + // If the issuer has requireAuth set, check if the destination is authorized + if (auto const ter = requireAuth(ctx.view, mptIssue, dest); + ter != tesSUCCESS) + return ter; + + // If the issuer has frozen the account, return tecFROZEN + if (isFrozen(ctx.view, account, mptIssue)) + return tecFROZEN; + + // If the issuer has frozen the destination, return tecFROZEN + if (isFrozen(ctx.view, dest, mptIssue)) + return tecFROZEN; + + // If the mpt cannot be transferred, return tecNO_AUTH + if (auto const ter = canTransfer(ctx.view, mptIssue, account, dest); + ter != tesSUCCESS) + return ter; + + STAmount const spendableAmount = accountHolds( + ctx.view, + account, + amount.get(), + fhIGNORE_FREEZE, + ahIGNORE_AUTH, + ctx.j); + + // If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS + if (spendableAmount <= beast::zero) + return tecINSUFFICIENT_FUNDS; + + return tesSUCCESS; +} + TER EscrowCreate::preclaim(PreclaimContext const& ctx) { - auto const sled = ctx.view.read(keylet::account(ctx.tx[sfDestination])); + STAmount const amount{ctx.tx[sfAmount]}; + AccountID const account{ctx.tx[sfAccount]}; + AccountID const dest{ctx.tx[sfDestination]}; + + auto const sled = ctx.view.read(keylet::account(dest)); if (!sled) return tecNO_DST; if (sled->isFieldPresent(sfAMMID)) return tecNO_PERMISSION; + if (!isXRP(amount)) + { + if (!ctx.view.rules().enabled(featureTokenEscrow)) + return temDISABLED; + + AccountID issuer = amount.getIssuer(); + if (auto const ret = std::visit( + [&](T const&) { + return escrowPreclaimHelper( + ctx, issuer, account, dest, amount); + }, + ctx.tx[sfAmount].asset().value()); + !isTesSuccess(ret)) + return ret; + } return tesSUCCESS; } @@ -212,16 +464,22 @@ EscrowCreate::doApply() return tefINTERNAL; // Check reserve and funds availability - { - auto const balance = STAmount((*sle)[sfBalance]).xrp(); - auto const reserve = - ctx_.view().fees().accountReserve((*sle)[sfOwnerCount] + 1); + STAmount const amount{ctx_.tx[sfAmount]}; - if (balance < reserve) - return tecINSUFFICIENT_RESERVE; + auto const balance = STAmount((*sle)[sfBalance]).xrp(); + auto const reserve = + ctx_.view().fees().accountReserve((*sle)[sfOwnerCount] + 1); + AccountID const issuer = amount.getIssuer(); + + if (balance < reserve) + return tecINSUFFICIENT_RESERVE; + // Check reserve and funds availability + if (isXRP(amount)) + { if (balance < reserve + STAmount(ctx_.tx[sfAmount]).xrp()) return tecUNFUNDED; + // pass } // Check destination account @@ -255,6 +513,13 @@ EscrowCreate::doApply() (*slep)[~sfFinishAfter] = ctx_.tx[~sfFinishAfter]; (*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag]; + // TODO: Fix for MPT & IOU + // if (ctx_.view().rules().enabled(featureTokenEscrow)) + // { + // auto const xferRate = transferRate(view(), issuer); + // (*slep)[~sfTransferRate] = xferRate.value; + // } + ctx_.view().insert(slep); // Add escrow to sender's owner directory @@ -267,7 +532,8 @@ EscrowCreate::doApply() } // If it's not a self-send, add escrow to recipient's owner directory. - if (auto const dest = ctx_.tx[sfDestination]; dest != ctx_.tx[sfAccount]) + AccountID const dest = ctx_.tx[sfDestination]; + if (dest != ctx_.tx[sfAccount]) { auto page = ctx_.view().dirInsert( keylet::ownerDir(dest), escrowKeylet, describeOwnerDir(dest)); @@ -276,8 +542,34 @@ EscrowCreate::doApply() (*slep)[sfDestinationNode] = *page; } - // Deduct owner's balance, increment owner count - (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount]; + // If issuer is not source or destination, add escrow to issuers owner + // directory. + if (!isXRP(amount) && issuer != account && issuer != dest) + { + auto page = ctx_.view().dirInsert( + keylet::ownerDir(issuer), escrowKeylet, describeOwnerDir(issuer)); + if (!page) + return tecDIR_FULL; + (*slep)[sfIssuerNode] = *page; + } + + // Deduct owner's balance + if (isXRP(amount)) + (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount]; + else + { + auto const ter = rippleCredit( + ctx_.view(), + amount.getIssuer(), + account, + amount, + amount.holds() ? false : true, + ctx_.journal); + if (ter != tesSUCCESS) + return ter; // LCOV_EXCL_LINE + } + + // increment owner count adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal); ctx_.view().update(sle); @@ -384,20 +676,217 @@ EscrowFinish::preclaim(PreclaimContext const& ctx) return tesSUCCESS; } +template +static TER +escrowFinishApplyHelper( + ApplyView& view, + Rate lockedRate, + std::shared_ptr const& sleDest, + STAmount const& balance, + STAmount const& amount, + AccountID const& issuer, + AccountID const& account, + AccountID const& dest, + bool const& createAsset, + beast::Journal journal); + +template <> +TER +escrowFinishApplyHelper( + ApplyView& view, + Rate lockedRate, + std::shared_ptr const& sleDest, + STAmount const& balance, + STAmount const& amount, + AccountID const& issuer, + AccountID const& account, + AccountID const& dest, + bool const& createAsset, + beast::Journal journal) +{ + Keylet const trustLineKey = keylet::line(dest, amount.issue()); + bool const destLow = issuer > dest; + bool const srcIssuer = issuer == account; + bool const dstIssuer = issuer == dest; + + // Review Note: We could remove this and just say to use batch to auth the + // token first + if (!view.exists(trustLineKey) && createAsset && !dstIssuer) + { + // Can the account cover the trust line's reserve? + if (std::uint32_t const ownerCount = {sleDest->at(sfOwnerCount)}; + balance < view.fees().accountReserve(ownerCount + 1)) + { + JLOG(journal.trace()) << "Trust line does not exist. " + "Insufficent reserve to create line."; + + return tecNO_LINE_INSUF_RESERVE; + } + + Currency const currency = amount.getCurrency(); + STAmount initialBalance(amount.issue()); + initialBalance.setIssuer(noAccount()); + + // clang-format off + if (TER const ter = trustCreate( + view, // payment sandbox + destLow, // is dest low? + issuer, // source + dest, // destination + trustLineKey.key, // ledger index + sleDest, // Account to add to + false, // authorize account + (sleDest->getFlags() & lsfDefaultRipple) == 0, + false, // freeze trust line + false, // deep freeze trust line + initialBalance, // zero initial balance + Issue(currency, dest), // limit of zero + 0, // quality in + 0, // quality out + journal); // journal + !isTesSuccess(ter)) + { + return ter; + } + // clang-format on + + view.update(sleDest); + } + + if (!view.exists(trustLineKey) && !dstIssuer) + return tecNO_LINE; + + auto const xferRate = transferRate(view, issuer); + // update if issuer rate is less than locked rate + if (xferRate < lockedRate) + lockedRate = xferRate; + + // Transfer Rate only applies when: + // 1. Issuer is not involved in the transfer (srcIssuer or dstIssuer) + // 2. The locked rate is different from the parity rate + + auto finalAmt = amount; + if ((!srcIssuer && !dstIssuer) && lockedRate != parityRate) + { + // compute transfer fee, if any + auto const xferFee = amount.value() - + divideRound(amount, lockedRate, amount.issue(), true); + // compute balance to transfer + finalAmt = amount.value() - xferFee; + } + + // If destination is not the issuer then transfer funds + if (!dstIssuer) + { + auto const ter = + rippleCredit(view, issuer, dest, finalAmt, true, journal); + if (ter != tesSUCCESS) + return ter; // LCOV_EXCL_LINE + } + return tesSUCCESS; +} + +template <> +TER +escrowFinishApplyHelper( + ApplyView& view, + Rate lockedRate, + std::shared_ptr const& sleDest, + STAmount const& balance, + STAmount const& amount, + AccountID const& issuer, + AccountID const& account, + AccountID const& dest, + bool const& createAsset, + beast::Journal journal) +{ + bool const srcIssuer = issuer == account; + bool const dstIssuer = issuer == dest; + + // Review Note: We could remove this and just say to use batch to auth the + // token first + auto const issuanceKey = + keylet::mptIssuance(amount.get().getMptID()); + if (!view.exists(keylet::mptoken(issuanceKey.key, dest)) && createAsset && + !dstIssuer) + { + if (std::uint32_t const ownerCount = {sleDest->at(sfOwnerCount)}; + balance < view.fees().accountReserve(ownerCount + 1)) + { + JLOG(journal.trace()) << "MPToken does not exist. " + "Insufficent reserve to create line."; + return tecINSUFFICIENT_RESERVE; + } + + /* + This should have been added to rippleCreditMPT as rippleCreditIOU + creates the trustline + */ + auto const mptokenKey = keylet::mptoken(issuanceKey.key, dest); + auto const ownerNode = view.dirInsert( + keylet::ownerDir(dest), mptokenKey, describeOwnerDir(dest)); + + if (!ownerNode) + return tecDIR_FULL; + + auto mptoken = std::make_shared(mptokenKey); + (*mptoken)[sfAccount] = dest; + (*mptoken)[sfMPTokenIssuanceID] = amount.get().getMptID(); + (*mptoken)[sfFlags] = 0; + (*mptoken)[sfOwnerNode] = *ownerNode; + view.insert(mptoken); + + // Update owner count. + adjustOwnerCount(view, sleDest, 1, journal); + } + + if (!view.exists(keylet::mptoken(issuanceKey.key, dest)) && !dstIssuer) + return tecNO_AUTH; + + auto const xferRate = transferRate(view, issuer); + // update if issuer rate is less than locked rate + if (xferRate < lockedRate) + lockedRate = xferRate; + + // Transfer Rate only applies when: + // 1. Issuer is not involved in the transfer (srcIssuer or dstIssuer) + // 2. The locked rate is different from the parity rate + + auto finalAmt = amount; + if ((!srcIssuer && !dstIssuer) && lockedRate != parityRate) + { + // compute transfer fee, if any + auto const xferFee = amount.value() - + divideRound(amount, lockedRate, amount.issue(), true); + // compute balance to transfer + finalAmt = amount.value() - xferFee; + } + + return rippleCredit( + view, + issuer, + dest, + finalAmt, + /*checkIssuer*/ false, + journal); + return tesSUCCESS; +} + TER EscrowFinish::doApply() { + PaymentSandbox psb(&ctx_.view()); auto const k = keylet::escrow(ctx_.tx[sfOwner], ctx_.tx[sfOfferSequence]); - auto const slep = ctx_.view().peek(k); + auto const slep = psb.peek(k); if (!slep) return tecNO_TARGET; // If a cancel time is present, a finish operation should only succeed prior // to that time. fix1571 corrects a logic error in the check that would make // a finish only succeed strictly after the cancel time. - if (ctx_.view().rules().enabled(fix1571)) + if (psb.rules().enabled(fix1571)) { - auto const now = ctx_.view().info().parentCloseTime; + auto const now = psb.info().parentCloseTime; // Too soon: can't execute before the finish time if ((*slep)[~sfFinishAfter] && !after(now, (*slep)[sfFinishAfter])) @@ -411,13 +900,13 @@ EscrowFinish::doApply() { // Too soon? if ((*slep)[~sfFinishAfter] && - ctx_.view().info().parentCloseTime.time_since_epoch().count() <= + psb.info().parentCloseTime.time_since_epoch().count() <= (*slep)[sfFinishAfter]) return tecNO_PERMISSION; // Too late? if ((*slep)[~sfCancelAfter] && - ctx_.view().info().parentCloseTime.time_since_epoch().count() <= + psb.info().parentCloseTime.time_since_epoch().count() <= (*slep)[sfCancelAfter]) return tecNO_PERMISSION; } @@ -471,11 +960,11 @@ EscrowFinish::doApply() // NOTE: Escrow payments cannot be used to fund accounts. AccountID const destID = (*slep)[sfDestination]; - auto const sled = ctx_.view().peek(keylet::account(destID)); + auto const sled = psb.peek(keylet::account(destID)); if (!sled) return tecNO_DST; - if (ctx_.view().rules().enabled(featureDepositAuth)) + if (psb.rules().enabled(featureDepositAuth)) { if (auto err = verifyDepositPreauth(ctx_, account_, destID, sled); !isTesSuccess(err)) @@ -487,8 +976,7 @@ EscrowFinish::doApply() // Remove escrow from owner directory { auto const page = (*slep)[sfOwnerNode]; - if (!ctx_.view().dirRemove( - keylet::ownerDir(account), page, k.key, true)) + if (!psb.dirRemove(keylet::ownerDir(account), page, k.key, true)) { JLOG(j_.fatal()) << "Unable to delete Escrow from owner."; return tefBAD_LEDGER; @@ -498,26 +986,67 @@ EscrowFinish::doApply() // Remove escrow from recipient's owner directory, if present. if (auto const optPage = (*slep)[~sfDestinationNode]) { - if (!ctx_.view().dirRemove( - keylet::ownerDir(destID), *optPage, k.key, true)) + if (!psb.dirRemove(keylet::ownerDir(destID), *optPage, k.key, true)) { JLOG(j_.fatal()) << "Unable to delete Escrow from recipient."; return tefBAD_LEDGER; } } + auto const amount = slep->getFieldAmount(sfAmount); // Transfer amount to destination - (*sled)[sfBalance] = (*sled)[sfBalance] + (*slep)[sfAmount]; - ctx_.view().update(sled); + if (isXRP(amount)) + (*sled)[sfBalance] = (*sled)[sfBalance] + (*slep)[sfAmount]; + else + { + if (!psb.rules().enabled(featureTokenEscrow)) + return temDISABLED; + + Rate lockedRate = slep->isFieldPresent(sfTransferRate) + ? ripple::Rate(slep->getFieldU32(sfTransferRate)) + : parityRate; + auto const issuer = amount.getIssuer(); + bool const createAsset = destID == account_; + if (auto const ret = std::visit( + [&](T const&) { + return escrowFinishApplyHelper( + psb, + lockedRate, + sled, + mPriorBalance, + amount, + issuer, + account, + destID, + createAsset, + j_); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + + // Remove escrow from issuers owner directory, if present. + if (auto const optPage = (*slep)[~sfIssuerNode]; optPage) + { + if (!psb.dirRemove(keylet::ownerDir(issuer), *optPage, k.key, true)) + { + JLOG(j_.fatal()) << "Unable to delete Escrow from recipient."; + return tefBAD_LEDGER; + } + } + } + + psb.update(sled); // Adjust source owner count - auto const sle = ctx_.view().peek(keylet::account(account)); - adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal); - ctx_.view().update(sle); + auto const sle = psb.peek(keylet::account(account)); + adjustOwnerCount(psb, sle, -1, ctx_.journal); + psb.update(sle); // Remove escrow from ledger - ctx_.view().erase(slep); + psb.erase(slep); + psb.apply(ctx_.rawView()); return tesSUCCESS; } @@ -591,9 +1120,50 @@ EscrowCancel::doApply() } } - // Transfer amount back to owner, decrement owner count auto const sle = ctx_.view().peek(keylet::account(account)); - (*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount]; + auto const amount = slep->getFieldAmount(sfAmount); + bool const srcIssuer = amount.getIssuer() == account; + + // Transfer amount back to the owner + if (isXRP(amount)) + (*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount]; + else + { + if (!ctx_.view().rules().enabled(featureTokenEscrow)) + return temDISABLED; + + // issuer does not need to do anything + if (!srcIssuer) + { + // Review Note: Should we use a helper here? + // Would we also need to add the trustline if it doesnt exist and do + // checks on; requireAuth, frozen, defaultRipple? + auto const ter = rippleCredit( + ctx_.view(), + amount.getIssuer(), + account, + amount, + amount.holds() ? false : true, + ctx_.journal); + if (ter != tesSUCCESS) + return ter; // LCOV_EXCL_LINE + } + + // Remove escrow from issuers owner directory, if present. + if (auto const optPage = (*slep)[~sfIssuerNode]; optPage) + { + if (!ctx_.view().dirRemove( + keylet::ownerDir(amount.getIssuer()), + *optPage, + k.key, + true)) + { + JLOG(j_.fatal()) << "Unable to delete Escrow from recipient."; + return tefBAD_LEDGER; + } + } + } + adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal); ctx_.view().update(sle); diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index d39492c1085..986bdfc54ce 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -107,7 +107,8 @@ XRPNotCreated::visitEntry( ((*before)[sfAmount] - (*before)[sfBalance]).xrp().drops(); break; case ltESCROW: - drops_ -= (*before)[sfAmount].xrp().drops(); + if (isXRP((*before)[sfAmount])) + drops_ -= (*before)[sfAmount].xrp().drops(); break; default: break; @@ -128,7 +129,7 @@ XRPNotCreated::visitEntry( .drops(); break; case ltESCROW: - if (!isDelete) + if (!isDelete && isXRP((*after)[sfAmount])) drops_ += (*after)[sfAmount].xrp().drops(); break; default: @@ -289,12 +290,24 @@ NoZeroEscrow::visitEntry( bool NoZeroEscrow::finalize( - STTx const&, + STTx const& txn, TER const, XRPAmount const, - ReadView const&, + ReadView const& rv, beast::Journal const& j) { + if (bad_ && rv.rules().enabled(featureTokenEscrow) && + txn.isFieldPresent(sfTransactionType)) + { + uint16_t const tt = txn.getFieldU16(sfTransactionType); + if (tt == ttESCROW_CANCEL || tt == ttESCROW_FINISH) + return true; + + if (txn.isFieldPresent(sfAmount) && + !isXRP(txn.getFieldAmount(sfAmount))) + return true; + } + if (bad_) { JLOG(j.fatal()) << "Invariant failed: escrow specifies invalid amount"; @@ -1419,6 +1432,9 @@ ValidMPTIssuance::finalize( return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 && mptokensCreated_ == 0 && mptokensDeleted_ == 0; } + + if (tx.getTxnType() == ttESCROW_FINISH) + return true; } if (mptIssuancesCreated_ != 0) diff --git a/src/xrpld/app/tx/detail/SetAccount.cpp b/src/xrpld/app/tx/detail/SetAccount.cpp index c0e115c2497..f8e57f25632 100644 --- a/src/xrpld/app/tx/detail/SetAccount.cpp +++ b/src/xrpld/app/tx/detail/SetAccount.cpp @@ -593,6 +593,15 @@ SetAccount::doApply() uFlagsOut &= ~lsfDisallowIncomingTrustline; } + // Set or clear flags for disallowing escrow + if (ctx_.view().rules().enabled(featureTokenEscrow)) + { + if (uSetFlag == asfAllowTokenLocking) + uFlagsOut |= lsfAllowTokenLocking; + else if (uClearFlag == asfAllowTokenLocking) + uFlagsOut &= ~lsfAllowTokenLocking; + } + // Set flag for clawback if (ctx_.view().rules().enabled(featureClawback) && uSetFlag == asfAllowTrustLineClawback) diff --git a/src/xrpld/ledger/View.h b/src/xrpld/ledger/View.h index aca3f9fa6d8..241afb45dd4 100644 --- a/src/xrpld/ledger/View.h +++ b/src/xrpld/ledger/View.h @@ -76,6 +76,9 @@ enum class SkipEntry : bool { No = false, Yes }; [[nodiscard]] bool hasExpired(ReadView const& view, std::optional const& exp); +[[nodiscard]] bool +noDefaultRipple(ReadView const& view, Issue const& issue); + /** Controls the treatment of frozen account balances */ enum FreezeHandling { fhIGNORE_FREEZE, fhZERO_IF_FROZEN }; diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index 85abf7fc62c..700e08680c3 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -169,6 +169,18 @@ hasExpired(ReadView const& view, std::optional const& exp) return exp && (view.parentCloseTime() >= tp{d{*exp}}); } +bool +noDefaultRipple(ReadView const& view, Issue const& issue) +{ + if (isXRP(issue)) + return false; + + if (auto const issuerAccount = view.read(keylet::account(issue.account))) + return (issuerAccount->getFlags() & lsfDefaultRipple) == 0; + + return false; +} + bool isGlobalFrozen(ReadView const& view, AccountID const& issuer) {