diff --git a/conanfile.py b/conanfile.py index a42c116ca2..da8a09611d 100644 --- a/conanfile.py +++ b/conanfile.py @@ -1,4 +1,4 @@ -from conan import ConanFile +from conan import ConanFile, __version__ as conan_version from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout import re @@ -24,13 +24,11 @@ class Xrpl(ConanFile): } requires = [ - 'date/3.0.3', 'grpc/1.50.1', 'libarchive/3.7.6', 'nudb/2.0.8', 'openssl/1.1.1v', 'soci/4.0.3', - 'xxhash/0.8.2', 'zlib/1.3.1', ] @@ -99,7 +97,10 @@ class Xrpl(ConanFile): self.options['boost'].visibility = 'global' def requirements(self): - self.requires('boost/1.83.0', force=True) + # Conan 2 requires transitive headers to be specified + transitive_headers_opt = {'transitive_headers': True} if conan_version.split('.')[0] == '2' else {} + self.requires('boost/1.83.0', force=True, **transitive_headers_opt) + self.requires('date/3.0.3', **transitive_headers_opt) self.requires('lz4/1.10.0', force=True) self.requires('protobuf/3.21.9', force=True) self.requires('sqlite3/3.47.0', force=True) @@ -107,6 +108,7 @@ class Xrpl(ConanFile): self.requires('jemalloc/5.3.0') if self.options.rocksdb: self.requires('rocksdb/9.7.3') + self.requires('xxhash/0.8.2', **transitive_headers_opt) exports_sources = ( 'CMakeLists.txt', diff --git a/include/xrpl/protocol/IOUAmount.h b/include/xrpl/protocol/IOUAmount.h index a27069e37b..93fba4150d 100644 --- a/include/xrpl/protocol/IOUAmount.h +++ b/include/xrpl/protocol/IOUAmount.h @@ -98,6 +98,12 @@ public: static IOUAmount minPositiveAmount(); + + friend std::ostream& + operator<<(std::ostream& os, IOUAmount const& x) + { + return os << to_string(x); + } }; inline IOUAmount::IOUAmount(beast::Zero) diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index 30f6438f02..c138c0ccbb 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 + lsfAllowTrustLineLocking = + 0x40000000, // True, enable trustline locking lsfAllowTrustLineClawback = 0x80000000, // True, enable clawback diff --git a/include/xrpl/protocol/Rules.h b/include/xrpl/protocol/Rules.h index 6b22d01afe..efdaf803fd 100644 --- a/include/xrpl/protocol/Rules.h +++ b/include/xrpl/protocol/Rules.h @@ -28,6 +28,9 @@ namespace ripple { +bool +isFeatureEnabled(uint256 const& feature); + class DigestAwareReadView; /** Rules controlling protocol behavior. */ diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index b7b8966a86..8c455af85e 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -733,6 +733,12 @@ isXRP(STAmount const& amount) return amount.native(); } +bool +canAdd(STAmount const& amt1, STAmount const& amt2); + +bool +canSubtract(STAmount const& amt1, STAmount const& amt2); + // 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 diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index 4483d6251a..9ace6b80f8 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -360,6 +360,8 @@ enum TECcodes : TERUnderlyingType { tecWRONG_ASSET = 194, tecLIMIT_EXCEEDED = 195, tecPSEUDO_ACCOUNT = 196, + tecPRECISION_LOSS = 197, + tecNO_DELEGATE_PERMISSION = 198, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index cf9ccbbbc9..f1082251f9 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -92,6 +92,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 asfAllowTrustLineLocking = 17; // OfferCreate flags: constexpr std::uint32_t tfPassive = 0x00010000; @@ -121,13 +122,7 @@ constexpr std::uint32_t tfClearDeepFreeze = 0x00800000; constexpr std::uint32_t tfTrustSetMask = ~(tfUniversal | tfSetfAuth | tfSetNoRipple | tfClearNoRipple | tfSetFreeze | tfClearFreeze | tfSetDeepFreeze | tfClearDeepFreeze); - -// valid flags for granular permission -constexpr std::uint32_t tfTrustSetGranularMask = tfSetfAuth | tfSetFreeze | tfClearFreeze; - -// bits representing supportedGranularMask are set to 0 and the bits -// representing other flags are set to 1 in tfPermissionMask. -constexpr std::uint32_t tfTrustSetPermissionMask = (~tfTrustSetMask) & (~tfTrustSetGranularMask); +constexpr std::uint32_t tfTrustSetPermissionMask = ~(tfUniversal | tfSetfAuth | tfSetFreeze | tfClearFreeze); // EnableAmendment flags: constexpr std::uint32_t tfGotMajority = 0x00010000; @@ -167,8 +162,7 @@ constexpr std::uint32_t const tfMPTokenAuthorizeMask = ~(tfUniversal | tfMPTUna constexpr std::uint32_t const tfMPTLock = 0x00000001; constexpr std::uint32_t const tfMPTUnlock = 0x00000002; constexpr std::uint32_t const tfMPTokenIssuanceSetMask = ~(tfUniversal | tfMPTLock | tfMPTUnlock); -constexpr std::uint32_t const tfMPTokenIssuanceSetGranularMask = tfMPTLock | tfMPTUnlock; -constexpr std::uint32_t const tfMPTokenIssuanceSetPermissionMask = (~tfMPTokenIssuanceSetMask) & (~tfMPTokenIssuanceSetGranularMask); +constexpr std::uint32_t const tfMPTokenIssuanceSetPermissionMask = ~(tfUniversal | tfMPTLock | tfMPTUnlock); // MPTokenIssuanceDestroy flags: constexpr std::uint32_t const tfMPTokenIssuanceDestroyMask = ~tfUniversal; diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index c04b04d4c1..1470ad6d17 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -33,6 +33,9 @@ // Keep it sorted in reverse chronological order. XRPL_FEATURE(LendingProtocol, Supported::yes, VoteBehavior::DefaultNo) +XRPL_FEATURE(TokenEscrow, Supported::yes, VoteBehavior::DefaultNo) +XRPL_FIX (EnforceNFTokenTrustlineV2, Supported::yes, VoteBehavior::DefaultNo) +XRPL_FIX (AMMv1_3, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(PermissionedDEX, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(Batch, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(SingleAssetVault, Supported::no, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 49bd578f24..377d423dc4 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -355,6 +355,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. @@ -406,6 +408,7 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({ {sfAssetScale, soeDEFAULT}, {sfMaximumAmount, soeOPTIONAL}, {sfOutstandingAmount, soeREQUIRED}, + {sfLockedAmount, soeOPTIONAL}, {sfMPTokenMetadata, soeOPTIONAL}, {sfPreviousTxnID, soeREQUIRED}, {sfPreviousTxnLgrSeq, soeREQUIRED}, @@ -419,6 +422,7 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({ {sfAccount, soeREQUIRED}, {sfMPTokenIssuanceID, soeREQUIRED}, {sfMPTAmount, soeDEFAULT}, + {sfLockedAmount, soeOPTIONAL}, {sfOwnerNode, soeREQUIRED}, {sfPreviousTxnID, soeREQUIRED}, {sfPreviousTxnLgrSeq, soeREQUIRED}, diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 0e4c6d00b3..1863ad0b13 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -162,8 +162,9 @@ TYPED_SFIELD(sfOutstandingAmount, UINT64, 25, SField::sMD_BaseTen|SFie TYPED_SFIELD(sfMPTAmount, UINT64, 26, SField::sMD_BaseTen|SField::sMD_Default) TYPED_SFIELD(sfIssuerNode, UINT64, 27) TYPED_SFIELD(sfSubjectNode, UINT64, 28) -TYPED_SFIELD(sfVaultNode, UINT64, 29) -TYPED_SFIELD(sfLoanBrokerNode, UINT64, 30) +TYPED_SFIELD(sfLockedAmount, UINT64, 29, SField::sMD_BaseTen|SField::sMD_Default) +TYPED_SFIELD(sfVaultNode, UINT64, 30) +TYPED_SFIELD(sfLoanBrokerNode, UINT64, 31) // 128-bit TYPED_SFIELD(sfEmailHash, UINT128, 1) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 6472564081..32d4335c8b 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -82,7 +82,7 @@ TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, Delegation::delegatable, noPriv, ({ {sfDestination, soeREQUIRED}, - {sfAmount, soeREQUIRED}, + {sfAmount, soeREQUIRED, soeMPTSupported}, {sfCondition, soeOPTIONAL}, {sfCancelAfter, soeOPTIONAL}, {sfFinishAfter, soeOPTIONAL}, diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index b71c3c80d6..2adf06d075 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -391,6 +391,7 @@ JSS(load_fee); // out: LoadFeeTrackImp, NetworkOPs JSS(local); // out: resource/Logic.h JSS(local_txs); // out: GetCounts JSS(local_static_keys); // out: ValidatorList +JSS(locked); // out: GatewayBalances JSS(low); // out: BookChanges JSS(lowest_sequence); // out: AccountInfo JSS(lowest_ticket); // out: AccountInfo diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp index 1f061cebdc..b9b583046e 100644 --- a/src/libxrpl/protocol/BuildInfo.cpp +++ b/src/libxrpl/protocol/BuildInfo.cpp @@ -36,7 +36,7 @@ namespace BuildInfo { // and follow the format described at http://semver.org/ //------------------------------------------------------------------------------ // clang-format off -char const* const versionString = "2.5.0-b1" +char const* const versionString = "2.5.0-rc1" // clang-format on #if defined(DEBUG) || defined(SANITIZER) diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp index 3d1c718e65..b472b9b0f1 100644 --- a/src/libxrpl/protocol/Rules.cpp +++ b/src/libxrpl/protocol/Rules.cpp @@ -161,4 +161,12 @@ Rules::operator!=(Rules const& other) const { return !(*this == other); } + +bool +isFeatureEnabled(uint256 const& feature) +{ + auto const& rules = getCurrentTransactionRules(); + return rules && rules->enabled(feature); +} + } // namespace ripple diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 8ba4c5f297..7daf5547df 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -506,6 +506,157 @@ getRate(STAmount const& offerOut, STAmount const& offerIn) return 0; } +/** + * @brief Safely checks if two STAmount values can be added without overflow, + * underflow, or precision loss. + * + * This function determines whether the addition of two STAmount objects is + * safe, depending on their type: + * - For XRP amounts, it checks for integer overflow and underflow. + * - For IOU amounts, it checks for acceptable precision loss. + * - For MPT amounts, it checks for overflow and underflow within 63-bit signed + * integer limits. + * - If either amount is zero, addition is always considered safe. + * - If the amounts are of different currencies or types, addition is not + * allowed. + * + * @param a The first STAmount to add. + * @param b The second STAmount to add. + * @return true if the addition is safe; false otherwise. + */ +bool +canAdd(STAmount const& a, STAmount const& b) +{ + // cannot add different currencies + if (!areComparable(a, b)) + return false; + + // special case: adding anything to zero is always fine + if (a == beast::zero || b == beast::zero) + return true; + + // XRP case (overflow & underflow check) + if (isXRP(a) && isXRP(b)) + { + XRPAmount A = a.xrp(); + XRPAmount B = b.xrp(); + + if ((B > XRPAmount{0} && + A > XRPAmount{std::numeric_limits::max()} - + B) || + (B < XRPAmount{0} && + A < XRPAmount{std::numeric_limits::min()} - + B)) + { + return false; + } + return true; + } + + // IOU case (precision check) + if (a.holds() && b.holds()) + { + static STAmount const one{IOUAmount{1, 0}, noIssue()}; + static STAmount const maxLoss{IOUAmount{1, -4}, 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; + } + + // MPT (overflow & underflow check) + if (a.holds() && b.holds()) + { + MPTAmount A = a.mpt(); + MPTAmount B = b.mpt(); + if ((B > MPTAmount{0} && + A > MPTAmount{std::numeric_limits::max()} - + B) || + (B < MPTAmount{0} && + A < MPTAmount{std::numeric_limits::min()} - + B)) + { + return false; + } + + return true; + } + return false; +} + +/** + * @brief Determines if it is safe to subtract one STAmount from another. + * + * This function checks whether subtracting amount `b` from amount `a` is valid, + * considering currency compatibility and underflow conditions for specific + * types. + * + * - Subtracting zero is always allowed. + * - Subtraction is only allowed between comparable currencies. + * - For XRP amounts, ensures no underflow or overflow occurs. + * - For IOU amounts, subtraction is always allowed (no underflow). + * - For MPT amounts, ensures no underflow or overflow occurs. + * + * @param a The minuend (amount to subtract from). + * @param b The subtrahend (amount to subtract). + * @return true if subtraction is allowed, false otherwise. + */ +bool +canSubtract(STAmount const& a, STAmount const& b) +{ + // Cannot subtract different currencies + if (!areComparable(a, b)) + return false; + + // Special case: subtracting zero is always fine + if (b == beast::zero) + return true; + + // XRP case (underflow & overflow check) + if (isXRP(a) && isXRP(b)) + { + XRPAmount A = a.xrp(); + XRPAmount B = b.xrp(); + // Check for underflow + if (B > XRPAmount{0} && A < B) + return false; + + // Check for overflow + if (B < XRPAmount{0} && + A > XRPAmount{std::numeric_limits::max()} + + B) + return false; + + return true; + } + + // IOU case (no underflow) + if (a.holds() && b.holds()) + { + return true; + } + + // MPT case (underflow & overflow check) + if (a.holds() && b.holds()) + { + MPTAmount A = a.mpt(); + MPTAmount B = b.mpt(); + + // Underflow check + if (B > MPTAmount{0} && A < B) + return false; + + // Overflow check + if (B < MPTAmount{0} && + A > MPTAmount{std::numeric_limits::max()} + + B) + return false; + return true; + } + + return false; +} + void STAmount::setJson(Json::Value& elem) const { diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index 68125fab83..a396949afe 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -126,6 +126,8 @@ transResults() MAKE_ERROR(tecWRONG_ASSET, "Wrong asset given."), MAKE_ERROR(tecLIMIT_EXCEEDED, "Limit exceeded."), MAKE_ERROR(tecPSEUDO_ACCOUNT, "This operation is not allowed against a pseudo-account."), + MAKE_ERROR(tecPRECISION_LOSS, "The amounts used by the transaction cannot interact."), + MAKE_ERROR(tecNO_DELEGATE_PERMISSION, "Delegated account lacks permission to perform this transaction."), 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/AMMClawback_test.cpp b/src/test/app/AMMClawback_test.cpp index 878c570a12..77e908d5fe 100644 --- a/src/test/app/AMMClawback_test.cpp +++ b/src/test/app/AMMClawback_test.cpp @@ -581,8 +581,12 @@ class AMMClawback_test : public jtx::AMMTest AMM amm(env, alice, EUR(5000), USD(4000), ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - USD(4000), EUR(5000), IOUAmount{4472135954999580, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(4000), EUR(5000), IOUAmount{4472135954999580, -12})); + else + BEAST_EXPECT(amm.expectBalances( + USD(4000), EUR(5000), IOUAmount{4472135954999579, -12})); // gw clawback 1000 USD from the AMM pool env(amm::ammClawback(gw, alice, USD, EUR, USD(1000)), @@ -601,12 +605,20 @@ class AMMClawback_test : public jtx::AMMTest // 1000 USD and 1250 EUR was withdrawn from the AMM pool, so the // current balance is 3000 USD and 3750 EUR. - BEAST_EXPECT(amm.expectBalances( - USD(3000), EUR(3750), IOUAmount{3354101966249685, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(3000), EUR(3750), IOUAmount{3354101966249685, -12})); + else + BEAST_EXPECT(amm.expectBalances( + USD(3000), EUR(3750), IOUAmount{3354101966249684, -12})); // Alice has 3/4 of its initial lptokens Left. - BEAST_EXPECT( - amm.expectLPTokens(alice, IOUAmount{3354101966249685, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{3354101966249685, -12})); + else + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{3354101966249684, -12})); // gw clawback another 500 USD from the AMM pool. env(amm::ammClawback(gw, alice, USD, EUR, USD(500)), @@ -617,14 +629,21 @@ class AMMClawback_test : public jtx::AMMTest // AMM pool. env.require(balance(alice, gw["USD"](2000))); - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(2500000000000001), -12}, - STAmount{EUR, UINT64_C(3125000000000001), -12}, - IOUAmount{2795084971874738, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(2500000000000001), -12}, + STAmount{EUR, UINT64_C(3125000000000001), -12}, + IOUAmount{2795084971874738, -12})); + else + BEAST_EXPECT(amm.expectBalances( + USD(2500), EUR(3125), IOUAmount{2795084971874737, -12})); - BEAST_EXPECT( - env.balance(alice, EUR) == - STAmount(EUR, UINT64_C(2874999999999999), -12)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT( + env.balance(alice, EUR) == + STAmount(EUR, UINT64_C(2874999999999999), -12)); + else + BEAST_EXPECT(env.balance(alice, EUR) == EUR(2875)); // gw clawback small amount, 1 USD. env(amm::ammClawback(gw, alice, USD, EUR, USD(1)), ter(tesSUCCESS)); @@ -633,14 +652,21 @@ class AMMClawback_test : public jtx::AMMTest // Another 1 USD / 1.25 EUR was withdrawn. env.require(balance(alice, gw["USD"](2000))); - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(2499000000000002), -12}, - STAmount{EUR, UINT64_C(3123750000000002), -12}, - IOUAmount{2793966937885989, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(2499000000000002), -12}, + STAmount{EUR, UINT64_C(3123750000000002), -12}, + IOUAmount{2793966937885989, -12})); + else + BEAST_EXPECT(amm.expectBalances( + USD(2499), EUR(3123.75), IOUAmount{2793966937885987, -12})); - BEAST_EXPECT( - env.balance(alice, EUR) == - STAmount(EUR, UINT64_C(2876249999999998), -12)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT( + env.balance(alice, EUR) == + STAmount(EUR, UINT64_C(2'876'249999999998), -12)); + else + BEAST_EXPECT(env.balance(alice, EUR) == EUR(2876.25)); // gw clawback 4000 USD, exceeding the current balance. We // will clawback all. @@ -713,14 +739,26 @@ class AMMClawback_test : public jtx::AMMTest // gw2 creates AMM pool of XRP/EUR, alice and bob deposit XRP/EUR. AMM amm2(env, gw2, XRP(3000), EUR(1000), ter(tesSUCCESS)); - BEAST_EXPECT(amm2.expectBalances( - EUR(1000), XRP(3000), IOUAmount{1732050807568878, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm2.expectBalances( + EUR(1000), XRP(3000), IOUAmount{1732050807568878, -9})); + else + BEAST_EXPECT(amm2.expectBalances( + EUR(1000), XRP(3000), IOUAmount{1732050807568877, -9})); amm2.deposit(alice, EUR(1000), XRP(3000)); - BEAST_EXPECT(amm2.expectBalances( - EUR(2000), XRP(6000), IOUAmount{3464101615137756, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm2.expectBalances( + EUR(2000), XRP(6000), IOUAmount{3464101615137756, -9})); + else + BEAST_EXPECT(amm2.expectBalances( + EUR(2000), XRP(6000), IOUAmount{3464101615137754, -9})); amm2.deposit(bob, EUR(1000), XRP(3000)); - BEAST_EXPECT(amm2.expectBalances( - EUR(3000), XRP(9000), IOUAmount{5196152422706634, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm2.expectBalances( + EUR(3000), XRP(9000), IOUAmount{5196152422706634, -9})); + else + BEAST_EXPECT(amm2.expectBalances( + EUR(3000), XRP(9000), IOUAmount{5196152422706631, -9})); env.close(); auto aliceXrpBalance = env.balance(alice, XRP); @@ -743,10 +781,18 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT( expectLedgerEntryRoot(env, alice, aliceXrpBalance + XRP(1000))); - BEAST_EXPECT(amm.expectBalances( - USD(2500), XRP(5000), IOUAmount{3535533905932738, -9})); - BEAST_EXPECT( - amm.expectLPTokens(alice, IOUAmount{7071067811865480, -10})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(2500), XRP(5000), IOUAmount{3535533905932738, -9})); + else + BEAST_EXPECT(amm.expectBalances( + USD(2500), XRP(5000), IOUAmount{3535533905932737, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{7071067811865480, -10})); + else + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{7071067811865474, -10})); BEAST_EXPECT( amm.expectLPTokens(bob, IOUAmount{1414213562373095, -9})); @@ -760,14 +806,26 @@ class AMMClawback_test : public jtx::AMMTest // Bob gets 20 XRP back. BEAST_EXPECT( expectLedgerEntryRoot(env, bob, bobXrpBalance + XRP(20))); - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(2490000000000001), -12}, - XRP(4980), - IOUAmount{3521391770309008, -9})); - BEAST_EXPECT( - amm.expectLPTokens(alice, IOUAmount{7071067811865480, -10})); - BEAST_EXPECT( - amm.expectLPTokens(bob, IOUAmount{1400071426749365, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(2490000000000001), -12}, + XRP(4980), + IOUAmount{3521391770309008, -9})); + else + BEAST_EXPECT(amm.expectBalances( + USD(2'490), XRP(4980), IOUAmount{3521391770309006, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{7071067811865480, -10})); + else + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{7071067811865474, -10})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{1400071426749365, -9})); + else + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{1400071426749364, -9})); // gw2 clawback 200 EUR from amm2. env(amm::ammClawback(gw2, alice, EUR, XRP, EUR(200)), @@ -780,12 +838,24 @@ class AMMClawback_test : public jtx::AMMTest // Alice gets 600 XRP back. BEAST_EXPECT(expectLedgerEntryRoot( env, alice, aliceXrpBalance + XRP(1000) + XRP(600))); - BEAST_EXPECT(amm2.expectBalances( - EUR(2800), XRP(8400), IOUAmount{4849742261192859, -9})); - BEAST_EXPECT( - amm2.expectLPTokens(alice, IOUAmount{1385640646055103, -9})); - BEAST_EXPECT( - amm2.expectLPTokens(bob, IOUAmount{1732050807568878, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm2.expectBalances( + EUR(2800), XRP(8400), IOUAmount{4849742261192859, -9})); + else + BEAST_EXPECT(amm2.expectBalances( + EUR(2800), XRP(8400), IOUAmount{4849742261192856, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm2.expectLPTokens( + alice, IOUAmount{1385640646055103, -9})); + else + BEAST_EXPECT(amm2.expectLPTokens( + alice, IOUAmount{1385640646055102, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT( + amm2.expectLPTokens(bob, IOUAmount{1732050807568878, -9})); + else + BEAST_EXPECT( + amm2.expectLPTokens(bob, IOUAmount{1732050807568877, -9})); // gw claw back 1000 USD from alice in amm, which exceeds alice's // balance. This will clawback all the remaining LP tokens of alice @@ -798,17 +868,34 @@ class AMMClawback_test : public jtx::AMMTest env.require(balance(bob, gw["USD"](4000))); // Alice gets 1000 XRP back. - BEAST_EXPECT(expectLedgerEntryRoot( - env, - alice, - aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000))); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(expectLedgerEntryRoot( + env, + alice, + aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000))); + else + BEAST_EXPECT(expectLedgerEntryRoot( + env, + alice, + aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) - + XRPAmount{1})); BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); - BEAST_EXPECT( - amm.expectLPTokens(bob, IOUAmount{1400071426749365, -9})); - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(1990000000000001), -12}, - XRP(3980), - IOUAmount{2814284989122460, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{1400071426749365, -9})); + else + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{1400071426749364, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(1990000000000001), -12}, + XRP(3980), + IOUAmount{2814284989122460, -9})); + else + BEAST_EXPECT(amm.expectBalances( + USD(1'990), + XRPAmount{3'980'000'001}, + IOUAmount{2814284989122459, -9})); // gw clawback 1000 USD from bob in amm, which also exceeds bob's // balance in amm. All bob's lptoken in amm will be consumed, which @@ -820,10 +907,17 @@ class AMMClawback_test : public jtx::AMMTest env.require(balance(alice, gw["USD"](5000))); env.require(balance(bob, gw["USD"](4000))); - BEAST_EXPECT(expectLedgerEntryRoot( - env, - alice, - aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000))); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(expectLedgerEntryRoot( + env, + alice, + aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000))); + else + BEAST_EXPECT(expectLedgerEntryRoot( + env, + alice, + aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) - + XRPAmount{1})); BEAST_EXPECT(expectLedgerEntryRoot( env, bob, bobXrpBalance + XRP(20) + XRP(1980))); @@ -843,21 +937,32 @@ class AMMClawback_test : public jtx::AMMTest // Alice gets another 2400 XRP back, bob's XRP balance remains the // same. - BEAST_EXPECT(expectLedgerEntryRoot( - env, - alice, - aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) + - XRP(2400))); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(expectLedgerEntryRoot( + env, + alice, + aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) + + XRP(2400))); + else + BEAST_EXPECT(expectLedgerEntryRoot( + env, + alice, + aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) + + XRP(2400) - XRPAmount{1})); BEAST_EXPECT(expectLedgerEntryRoot( env, bob, bobXrpBalance + XRP(20) + XRP(1980))); // Alice now does not have any lptoken in amm2 BEAST_EXPECT(amm2.expectLPTokens(alice, IOUAmount(0))); - BEAST_EXPECT(amm2.expectBalances( - EUR(2000), XRP(6000), IOUAmount{3464101615137756, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm2.expectBalances( + EUR(2000), XRP(6000), IOUAmount{3464101615137756, -9})); + else + BEAST_EXPECT(amm2.expectBalances( + EUR(2000), XRP(6000), IOUAmount{3464101615137754, -9})); - // gw2 claw back 2000 EUR from bib in amm2, which exceeds bob's + // gw2 claw back 2000 EUR from bob in amm2, which exceeds bob's // balance. All bob's lptokens will be consumed, which corresponds // to 1000EUR / 3000 XRP. env(amm::ammClawback(gw2, bob, EUR, XRP, EUR(2000)), @@ -869,11 +974,18 @@ class AMMClawback_test : public jtx::AMMTest // Bob gets another 3000 XRP back. Alice's XRP balance remains the // same. - BEAST_EXPECT(expectLedgerEntryRoot( - env, - alice, - aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) + - XRP(2400))); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(expectLedgerEntryRoot( + env, + alice, + aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) + + XRP(2400))); + else + BEAST_EXPECT(expectLedgerEntryRoot( + env, + alice, + aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) + + XRP(2400) - XRPAmount{1})); BEAST_EXPECT(expectLedgerEntryRoot( env, bob, bobXrpBalance + XRP(20) + XRP(1980) + XRP(3000))); @@ -881,8 +993,12 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(amm2.expectLPTokens(alice, IOUAmount(0))); BEAST_EXPECT(amm2.expectLPTokens(bob, IOUAmount(0))); - BEAST_EXPECT(amm2.expectBalances( - EUR(1000), XRP(3000), IOUAmount{1732050807568878, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm2.expectBalances( + EUR(1000), XRP(3000), IOUAmount{1732050807568878, -9})); + else + BEAST_EXPECT(amm2.expectBalances( + EUR(1000), XRP(3000), IOUAmount{1732050807568877, -9})); } } @@ -940,21 +1056,45 @@ class AMMClawback_test : public jtx::AMMTest AMM amm(env, alice, EUR(5000), USD(4000), ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - USD(4000), EUR(5000), IOUAmount{4472135954999580, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(4000), EUR(5000), IOUAmount{4472135954999580, -12})); + else + BEAST_EXPECT(amm.expectBalances( + USD(4000), EUR(5000), IOUAmount{4472135954999579, -12})); amm.deposit(bob, USD(2000), EUR(2500)); - BEAST_EXPECT(amm.expectBalances( - USD(6000), EUR(7500), IOUAmount{6708203932499370, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(6000), EUR(7500), IOUAmount{6708203932499370, -12})); + else + BEAST_EXPECT(amm.expectBalances( + USD(6000), EUR(7500), IOUAmount{6708203932499368, -12})); amm.deposit(carol, USD(1000), EUR(1250)); - BEAST_EXPECT(amm.expectBalances( - USD(7000), EUR(8750), IOUAmount{7826237921249265, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(7000), EUR(8750), IOUAmount{7826237921249265, -12})); + else + BEAST_EXPECT(amm.expectBalances( + USD(7000), EUR(8750), IOUAmount{7826237921249262, -12})); - BEAST_EXPECT( - amm.expectLPTokens(alice, IOUAmount{4472135954999580, -12})); - BEAST_EXPECT( - amm.expectLPTokens(bob, IOUAmount{2236067977499790, -12})); - BEAST_EXPECT( - amm.expectLPTokens(carol, IOUAmount{1118033988749895, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{4472135954999580, -12})); + else + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{4472135954999579, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{2236067977499790, -12})); + else + BEAST_EXPECT( + amm.expectLPTokens(bob, IOUAmount{2236067977499789, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectLPTokens( + carol, IOUAmount{1118033988749895, -12})); + else + BEAST_EXPECT(amm.expectLPTokens( + carol, IOUAmount{1118033988749894, -12})); env.require(balance(alice, gw["USD"](2000))); env.require(balance(alice, gw2["EUR"](1000))); @@ -968,16 +1108,30 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(4999999999999999), -12}, - STAmount{EUR, UINT64_C(6249999999999999), -12}, - IOUAmount{5590169943749475, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(4999999999999999), -12}, + STAmount{EUR, UINT64_C(6249999999999999), -12}, + IOUAmount{5590169943749475, -12})); + else + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(5000000000000001), -12}, + STAmount{EUR, UINT64_C(6250000000000001), -12}, + IOUAmount{5590169943749473, -12})); - BEAST_EXPECT( - amm.expectLPTokens(alice, IOUAmount{4472135954999580, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{4472135954999580, -12})); + else + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{4472135954999579, -12})); BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount(0))); - BEAST_EXPECT( - amm.expectLPTokens(carol, IOUAmount{1118033988749895, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectLPTokens( + carol, IOUAmount{1118033988749895, -12})); + else + BEAST_EXPECT(amm.expectLPTokens( + carol, IOUAmount{1118033988749894, -12})); // Bob will get 2500 EUR back. env.require(balance(alice, gw["USD"](2000))); @@ -986,9 +1140,14 @@ class AMMClawback_test : public jtx::AMMTest env.balance(bob, USD) == STAmount(USD, UINT64_C(3000000000000000), -12)); - BEAST_EXPECT( - env.balance(bob, EUR) == - STAmount(EUR, UINT64_C(5000000000000001), -12)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT( + env.balance(bob, EUR) == + STAmount(EUR, UINT64_C(5000000000000001), -12)); + else + BEAST_EXPECT( + env.balance(bob, EUR) == + STAmount(EUR, UINT64_C(4999999999999999), -12)); env.require(balance(carol, gw["USD"](3000))); env.require(balance(carol, gw2["EUR"](2750))); @@ -996,13 +1155,23 @@ class AMMClawback_test : public jtx::AMMTest env(amm::ammClawback(gw2, carol, EUR, USD, std::nullopt), ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(3999999999999999), -12}, - STAmount{EUR, UINT64_C(4999999999999999), -12}, - IOUAmount{4472135954999580, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(3999999999999999), -12}, + STAmount{EUR, UINT64_C(4999999999999999), -12}, + IOUAmount{4472135954999580, -12})); + else + BEAST_EXPECT(amm.expectBalances( + STAmount{USD, UINT64_C(4000000000000001), -12}, + STAmount{EUR, UINT64_C(5000000000000002), -12}, + IOUAmount{4472135954999579, -12})); - BEAST_EXPECT( - amm.expectLPTokens(alice, IOUAmount{4472135954999580, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{4472135954999580, -12})); + else + BEAST_EXPECT(amm.expectLPTokens( + alice, IOUAmount{4472135954999579, -12})); BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount(0))); BEAST_EXPECT(amm.expectLPTokens(carol, IOUAmount(0))); @@ -1041,14 +1210,26 @@ class AMMClawback_test : public jtx::AMMTest // gw creates AMM pool of XRP/USD, alice and bob deposit XRP/USD. AMM amm(env, gw, XRP(2000), USD(10000), ter(tesSUCCESS)); - BEAST_EXPECT(amm.expectBalances( - USD(10000), XRP(2000), IOUAmount{4472135954999580, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(10000), XRP(2000), IOUAmount{4472135954999580, -9})); + else + BEAST_EXPECT(amm.expectBalances( + USD(10000), XRP(2000), IOUAmount{4472135954999579, -9})); amm.deposit(alice, USD(1000), XRP(200)); - BEAST_EXPECT(amm.expectBalances( - USD(11000), XRP(2200), IOUAmount{4919349550499538, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(11000), XRP(2200), IOUAmount{4919349550499538, -9})); + else + BEAST_EXPECT(amm.expectBalances( + USD(11000), XRP(2200), IOUAmount{4919349550499536, -9})); amm.deposit(bob, USD(2000), XRP(400)); - BEAST_EXPECT(amm.expectBalances( - USD(13000), XRP(2600), IOUAmount{5813776741499453, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(13000), XRP(2600), IOUAmount{5813776741499453, -9})); + else + BEAST_EXPECT(amm.expectBalances( + USD(13000), XRP(2600), IOUAmount{5813776741499451, -9})); env.close(); auto aliceXrpBalance = env.balance(alice, XRP); @@ -1058,18 +1239,34 @@ class AMMClawback_test : public jtx::AMMTest env(amm::ammClawback(gw, alice, USD, XRP, std::nullopt), ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - USD(12000), XRP(2400), IOUAmount{5366563145999495, -9})); - BEAST_EXPECT( - expectLedgerEntryRoot(env, alice, aliceXrpBalance + XRP(200))); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(12000), XRP(2400), IOUAmount{5366563145999495, -9})); + else + BEAST_EXPECT(amm.expectBalances( + USD(12000), + XRPAmount(2400000001), + IOUAmount{5366563145999494, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(200))); + else + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(200) - XRPAmount{1})); BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); // gw clawback all bob's USD in amm. (2000 USD / 400 XRP) env(amm::ammClawback(gw, bob, USD, XRP, std::nullopt), ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - USD(10000), XRP(2000), IOUAmount{4472135954999580, -9})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(10000), XRP(2000), IOUAmount{4472135954999580, -9})); + else + BEAST_EXPECT(amm.expectBalances( + USD(10000), + XRPAmount(2000000001), + IOUAmount{4472135954999579, -9})); BEAST_EXPECT( expectLedgerEntryRoot(env, bob, bobXrpBalance + XRP(400))); BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); @@ -1125,10 +1322,12 @@ class AMMClawback_test : public jtx::AMMTest amm.deposit(bob, USD(4000), EUR(1000)); BEAST_EXPECT( amm.expectBalances(USD(12000), EUR(3000), IOUAmount(6000))); - amm.deposit(carol, USD(2000), EUR(500)); + if (!features[fixAMMv1_3]) + amm.deposit(carol, USD(2000), EUR(500)); + else + amm.deposit(carol, USD(2000.25), EUR(500)); BEAST_EXPECT( amm.expectBalances(USD(14000), EUR(3500), IOUAmount(7000))); - // gw clawback 1000 USD from carol. env(amm::ammClawback(gw, carol, USD, EUR, USD(1000)), ter(tesSUCCESS)); env.close(); @@ -1142,7 +1341,12 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(alice, EUR) == EUR(8000)); BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + else + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); // 250 EUR goes back to carol. BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); @@ -1164,7 +1368,12 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); // 250 EUR did not go back to bob because tfClawTwoAssets is set. BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + else + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); // gw clawback all USD from alice and set tfClawTwoAssets. @@ -1181,7 +1390,12 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(alice, EUR) == EUR(8000)); BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + else + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); } @@ -1366,12 +1580,21 @@ class AMMClawback_test : public jtx::AMMTest // gw2 claws back 1000 EUR from gw. env(amm::ammClawback(gw2, gw, EUR, USD, EUR(1000)), ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - USD(4500), - STAmount(EUR, UINT64_C(9000000000000001), -12), - IOUAmount{6363961030678928, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(4500), + STAmount(EUR, UINT64_C(9000000000000001), -12), + IOUAmount{6363961030678928, -12})); + else + BEAST_EXPECT(amm.expectBalances( + USD(4500), EUR(9000), IOUAmount{6363961030678928, -12})); - BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{7071067811865480, -13})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT( + amm.expectLPTokens(gw, IOUAmount{7071067811865480, -13})); + else + BEAST_EXPECT( + amm.expectLPTokens(gw, IOUAmount{7071067811865475, -13})); BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{1414213562373095, -12})); BEAST_EXPECT( amm.expectLPTokens(alice, IOUAmount{4242640687119285, -12})); @@ -1384,12 +1607,21 @@ class AMMClawback_test : public jtx::AMMTest // gw2 claws back 4000 EUR from alice. env(amm::ammClawback(gw2, alice, EUR, USD, EUR(4000)), ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - USD(2500), - STAmount(EUR, UINT64_C(5000000000000001), -12), - IOUAmount{3535533905932738, -12})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + USD(2500), + STAmount(EUR, UINT64_C(5000000000000001), -12), + IOUAmount{3535533905932738, -12})); + else + BEAST_EXPECT(amm.expectBalances( + USD(2500), EUR(5000), IOUAmount{3535533905932738, -12})); - BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{7071067811865480, -13})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT( + amm.expectLPTokens(gw, IOUAmount{7071067811865480, -13})); + else + BEAST_EXPECT( + amm.expectLPTokens(gw, IOUAmount{7071067811865475, -13})); BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{1414213562373095, -12})); BEAST_EXPECT( amm.expectLPTokens(alice, IOUAmount{1414213562373095, -12})); @@ -1653,7 +1885,10 @@ class AMMClawback_test : public jtx::AMMTest amm.deposit(bob, USD(4000), EUR(1000)); BEAST_EXPECT( amm.expectBalances(USD(12000), EUR(3000), IOUAmount(6000))); - amm.deposit(carol, USD(2000), EUR(500)); + if (!features[fixAMMv1_3]) + amm.deposit(carol, USD(2000), EUR(500)); + else + amm.deposit(carol, USD(2000.25), EUR(500)); BEAST_EXPECT( amm.expectBalances(USD(14000), EUR(3500), IOUAmount(7000))); @@ -1675,7 +1910,12 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(alice, EUR) == EUR(8000)); BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + else + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); // 250 EUR goes back to carol. BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); @@ -1697,7 +1937,12 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); // 250 EUR did not go back to bob because tfClawTwoAssets is set. BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + else + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); // gw clawback all USD from alice and set tfClawTwoAssets. @@ -1715,7 +1960,12 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(alice, EUR) == EUR(8000)); BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + else + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); } } @@ -1763,13 +2013,23 @@ class AMMClawback_test : public jtx::AMMTest env(amm::ammClawback(gw, alice, USD, XRP, USD(400)), ter(tesSUCCESS)); env.close(); - BEAST_EXPECT(amm.expectBalances( - STAmount(USD, UINT64_C(5656854249492380), -13), - XRP(70.710678), - IOUAmount(200000))); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(amm.expectBalances( + STAmount(USD, UINT64_C(5656854249492380), -13), + XRP(70.710678), + IOUAmount(200000))); + else + BEAST_EXPECT(amm.expectBalances( + STAmount(USD, UINT64_C(565'685424949238), -12), + XRP(70.710679), + IOUAmount(200000))); BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); - BEAST_EXPECT(expectLedgerEntryRoot( - env, alice, aliceXrpBalance + XRP(29.289322))); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(29.289322))); + else + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(29.289321))); } void @@ -1780,13 +2040,18 @@ class AMMClawback_test : public jtx::AMMTest testFeatureDisabled(all - featureAMMClawback); testAMMClawbackSpecificAmount(all); testAMMClawbackExceedBalance(all); + testAMMClawbackExceedBalance(all - fixAMMv1_3); testAMMClawbackAll(all); + testAMMClawbackAll(all - fixAMMv1_3); testAMMClawbackSameIssuerAssets(all); + testAMMClawbackSameIssuerAssets(all - fixAMMv1_3); testAMMClawbackSameCurrency(all); testAMMClawbackIssuesEachOther(all); testNotHoldingLptoken(all); testAssetFrozen(all); + testAssetFrozen(all - fixAMMv1_3); testSingleDepositAndClawback(all); + testSingleDepositAndClawback(all - fixAMMv1_3); } }; BEAST_DEFINE_TESTSUITE(AMMClawback, app, ripple); diff --git a/src/test/app/AMMExtended_test.cpp b/src/test/app/AMMExtended_test.cpp index f9750eaa53..3d959a6a09 100644 --- a/src/test/app/AMMExtended_test.cpp +++ b/src/test/app/AMMExtended_test.cpp @@ -1451,7 +1451,7 @@ private: using namespace jtx; FeatureBitset const all{supported_amendments()}; testRmFundedOffer(all); - testRmFundedOffer(all - fixAMMv1_1); + testRmFundedOffer(all - fixAMMv1_1 - fixAMMv1_3); testEnforceNoRipple(all); testFillModes(all); testOfferCrossWithXRP(all); @@ -1465,7 +1465,7 @@ private: testOfferCreateThenCross(all); testSellFlagExceedLimit(all); testGatewayCrossCurrency(all); - testGatewayCrossCurrency(all - fixAMMv1_1); + testGatewayCrossCurrency(all - fixAMMv1_1 - fixAMMv1_3); testBridgedCross(all); testSellWithFillOrKill(all); testTransferRateOffer(all); @@ -1473,7 +1473,7 @@ private: testBadPathAssert(all); testSellFlagBasic(all); testDirectToDirectPath(all); - testDirectToDirectPath(all - fixAMMv1_1); + testDirectToDirectPath(all - fixAMMv1_1 - fixAMMv1_3); testRequireAuth(all); testMissingAuth(all); } @@ -4063,9 +4063,9 @@ private: testBookStep(all); testBookStep(all | ownerPaysFee); testTransferRate(all | ownerPaysFee); - testTransferRate((all - fixAMMv1_1) | ownerPaysFee); + testTransferRate((all - fixAMMv1_1 - fixAMMv1_3) | ownerPaysFee); testTransferRateNoOwnerFee(all); - testTransferRateNoOwnerFee(all - fixAMMv1_1); + testTransferRateNoOwnerFee(all - fixAMMv1_1 - fixAMMv1_3); testLimitQuality(); testXRPPathLoop(); } @@ -4076,7 +4076,7 @@ private: using namespace jtx; FeatureBitset const all{supported_amendments()}; testStepLimit(all); - testStepLimit(all - fixAMMv1_1); + testStepLimit(all - fixAMMv1_1 - fixAMMv1_3); } void @@ -4085,7 +4085,7 @@ private: using namespace jtx; FeatureBitset const all{supported_amendments()}; test_convert_all_of_an_asset(all); - test_convert_all_of_an_asset(all - fixAMMv1_1); + test_convert_all_of_an_asset(all - fixAMMv1_1 - fixAMMv1_3); } void diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index a07a5eb1a2..286d1ca425 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -27,6 +27,7 @@ #include #include +#include #include #include @@ -836,21 +837,6 @@ private: std::nullopt, ter(tecAMM_FAILED)); - // Tiny deposit - ammAlice.deposit( - carol, - IOUAmount{1, -4}, - std::nullopt, - std::nullopt, - ter(temBAD_AMOUNT)); - ammAlice.deposit( - carol, - STAmount{USD, 1, -12}, - std::nullopt, - std::nullopt, - std::nullopt, - ter(tecAMM_INVALID_TOKENS)); - // Deposit non-empty AMM ammAlice.deposit( carol, @@ -861,6 +847,34 @@ private: ter(tecAMM_NOT_EMPTY)); }); + // Tiny deposit + testAMM( + [&](AMM& ammAlice, Env& env) { + auto const enabledv1_3 = + env.current()->rules().enabled(fixAMMv1_3); + auto const err = + !enabledv1_3 ? ter(temBAD_AMOUNT) : ter(tesSUCCESS); + // Pre-amendment XRP deposit side is rounded to 0 + // and deposit fails. + // Post-amendment XRP deposit side is rounded to 1 + // and deposit succeeds. + ammAlice.deposit( + carol, IOUAmount{1, -4}, std::nullopt, std::nullopt, err); + // Pre/post-amendment LPTokens is rounded to 0 and deposit + // fails with tecAMM_INVALID_TOKENS. + ammAlice.deposit( + carol, + STAmount{USD, 1, -12}, + std::nullopt, + std::nullopt, + std::nullopt, + ter(tecAMM_INVALID_TOKENS)); + }, + std::nullopt, + 0, + std::nullopt, + {features, features - fixAMMv1_3}); + // Invalid AMM testAMM([&](AMM& ammAlice, Env& env) { ammAlice.withdrawAll(alice); @@ -1316,6 +1330,53 @@ private: std::nullopt, ter(tecAMM_FAILED)); }); + + // Equal deposit, tokens rounded to 0 + testAMM([&](AMM& amm, Env& env) { + amm.deposit(DepositArg{ + .tokens = IOUAmount{1, -12}, + .err = ter(tecAMM_INVALID_TOKENS)}); + }); + + // Equal deposit limit, tokens rounded to 0 + testAMM( + [&](AMM& amm, Env& env) { + amm.deposit(DepositArg{ + .asset1In = STAmount{USD, 1, -15}, + .asset2In = XRPAmount{1}, + .err = ter(tecAMM_INVALID_TOKENS)}); + }, + {.pool = {{USD(1'000'000), XRP(1'000'000)}}, + .features = {features - fixAMMv1_3}}); + testAMM([&](AMM& amm, Env& env) { + amm.deposit(DepositArg{ + .asset1In = STAmount{USD, 1, -15}, + .asset2In = XRPAmount{1}, + .err = ter(tecAMM_INVALID_TOKENS)}); + }); + + // Single deposit by asset, tokens rounded to 0 + testAMM([&](AMM& amm, Env& env) { + amm.deposit(DepositArg{ + .asset1In = STAmount{USD, 1, -15}, + .err = ter(tecAMM_INVALID_TOKENS)}); + }); + + // Single deposit by tokens, tokens rounded to 0 + testAMM([&](AMM& amm, Env& env) { + amm.deposit(DepositArg{ + .tokens = IOUAmount{1, -10}, + .asset1In = STAmount{USD, 1, -15}, + .err = ter(tecAMM_INVALID_TOKENS)}); + }); + + // Single deposit with eprice, tokens rounded to 0 + testAMM([&](AMM& amm, Env& env) { + amm.deposit(DepositArg{ + .asset1In = STAmount{USD, 1, -15}, + .maxEP = STAmount{USD, 1, -1}, + .err = ter(tecAMM_INVALID_TOKENS)}); + }); } void @@ -1324,6 +1385,7 @@ private: testcase("Deposit"); using namespace jtx; + auto const all = supported_amendments(); // Equal deposit: 1000000 tokens, 10% of the current pool testAMM([&](AMM& ammAlice, Env& env) { @@ -1529,8 +1591,9 @@ private: }); // Issuer create/deposit + for (auto const& feat : {all, all - fixAMMv1_3}) { - Env env(*this); + Env env(*this, feat); env.fund(XRP(30000), gw); AMM ammGw(env, gw, XRP(10'000), USD(10'000)); BEAST_EXPECT( @@ -1624,6 +1687,7 @@ private: testcase("Invalid Withdraw"); using namespace jtx; + auto const all = supported_amendments(); testAMM( [&](AMM& ammAlice, Env& env) { @@ -1918,16 +1982,6 @@ private: ammAlice.withdraw( carol, 10'000, std::nullopt, std::nullopt, ter(tecAMM_BALANCE)); - // Withdraw entire one side of the pool. - // Equal withdraw but due to XRP precision limit, - // this results in full withdraw of XRP pool only, - // while leaving a tiny amount in USD pool. - ammAlice.withdraw( - alice, - IOUAmount{9'999'999'9999, -4}, - std::nullopt, - std::nullopt, - ter(tecAMM_BALANCE)); // Withdrawing from one side. // XRP by tokens ammAlice.withdraw( @@ -1959,6 +2013,57 @@ private: ter(tecAMM_BALANCE)); }); + testAMM( + [&](AMM& ammAlice, Env& env) { + // Withdraw entire one side of the pool. + // Pre-amendment: + // Equal withdraw but due to XRP rounding + // this results in full withdraw of XRP pool only, + // while leaving a tiny amount in USD pool. + // Post-amendment: + // Most of the pool is withdrawn with remaining tiny amounts + auto err = env.enabled(fixAMMv1_3) ? ter(tesSUCCESS) + : ter(tecAMM_BALANCE); + ammAlice.withdraw( + alice, + IOUAmount{9'999'999'9999, -4}, + std::nullopt, + std::nullopt, + err); + if (env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(1), STAmount{USD, 1, -7}, IOUAmount{1, -4})); + }, + std::nullopt, + 0, + std::nullopt, + {all, all - fixAMMv1_3}); + + testAMM( + [&](AMM& ammAlice, Env& env) { + // Similar to above with even smaller remaining amount + // is it ok that the pool is unbalanced? + // Withdraw entire one side of the pool. + // Equal withdraw but due to XRP precision limit, + // this results in full withdraw of XRP pool only, + // while leaving a tiny amount in USD pool. + auto err = env.enabled(fixAMMv1_3) ? ter(tesSUCCESS) + : ter(tecAMM_BALANCE); + ammAlice.withdraw( + alice, + IOUAmount{9'999'999'999999999, -9}, + std::nullopt, + std::nullopt, + err); + if (env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(1), STAmount{USD, 1, -11}, IOUAmount{1, -8})); + }, + std::nullopt, + 0, + std::nullopt, + {all, all - fixAMMv1_3}); + // Invalid AMM testAMM([&](AMM& ammAlice, Env& env) { ammAlice.withdrawAll(alice); @@ -2022,15 +2127,19 @@ private: // Withdraw with EPrice limit. Fails to withdraw, calculated tokens // to withdraw are 0. - testAMM([&](AMM& ammAlice, Env&) { - ammAlice.deposit(carol, 1'000'000); - ammAlice.withdraw( - carol, - USD(100), - std::nullopt, - IOUAmount{500, 0}, - ter(tecAMM_FAILED)); - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.deposit(carol, 1'000'000); + auto const err = env.enabled(fixAMMv1_3) + ? ter(tecAMM_INVALID_TOKENS) + : ter(tecAMM_FAILED); + ammAlice.withdraw( + carol, USD(100), std::nullopt, IOUAmount{500, 0}, err); + }, + std::nullopt, + 0, + std::nullopt, + {all, all - fixAMMv1_3}); // Withdraw with EPrice limit. Fails to withdraw, calculated tokens // to withdraw are greater than the LP shares. @@ -2095,14 +2204,19 @@ private: // Withdraw close to one side of the pool. Account's LP tokens // are rounded to all LP tokens. - testAMM([&](AMM& ammAlice, Env&) { - ammAlice.withdraw( - alice, - STAmount{USD, UINT64_C(9'999'999999999999), -12}, - std::nullopt, - std::nullopt, - ter(tecAMM_BALANCE)); - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + auto const err = env.enabled(fixAMMv1_3) + ? ter(tecINVARIANT_FAILED) + : ter(tecAMM_BALANCE); + ammAlice.withdraw( + alice, + STAmount{USD, UINT64_C(9'999'999999999999), -12}, + std::nullopt, + std::nullopt, + err); + }, + {.features = {all, all - fixAMMv1_3}, .noLog = true}); // Tiny withdraw testAMM([&](AMM& ammAlice, Env&) { @@ -2133,6 +2247,17 @@ private: XRPAmount{1}, std::nullopt, ter(tecAMM_INVALID_TOKENS)); + ammAlice.withdraw(WithdrawArg{ + .tokens = IOUAmount{1, -10}, + .err = ter(tecAMM_INVALID_TOKENS)}); + ammAlice.withdraw(WithdrawArg{ + .asset1Out = STAmount{USD, 1, -15}, + .asset2Out = XRPAmount{1}, + .err = ter(tecAMM_INVALID_TOKENS)}); + ammAlice.withdraw(WithdrawArg{ + .tokens = IOUAmount{1, -10}, + .asset1Out = STAmount{USD, 1, -15}, + .err = ter(tecAMM_INVALID_TOKENS)}); }); } @@ -2142,6 +2267,7 @@ private: testcase("Withdraw"); using namespace jtx; + auto const all = supported_amendments(); // Equal withdrawal by Carol: 1000000 of tokens, 10% of the current // pool @@ -2196,11 +2322,24 @@ private: }); // Single withdrawal by amount XRP1000 - testAMM([&](AMM& ammAlice, Env&) { - ammAlice.withdraw(alice, XRP(1'000)); - BEAST_EXPECT(ammAlice.expectBalances( - XRP(9'000), USD(10'000), IOUAmount{9'486'832'98050514, -8})); - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.withdraw(alice, XRP(1'000)); + if (!env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(9'000), + USD(10'000), + IOUAmount{9'486'832'98050514, -8})); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{9'000'000'001}, + USD(10'000), + IOUAmount{9'486'832'98050514, -8})); + }, + std::nullopt, + 0, + std::nullopt, + {all, all - fixAMMv1_3}); // Single withdrawal by tokens 10000. testAMM([&](AMM& ammAlice, Env&) { @@ -2251,20 +2390,31 @@ private: }); // Single deposit/withdraw by the same account - testAMM([&](AMM& ammAlice, Env&) { - // Since a smaller amount might be deposited due to - // the lp tokens adjustment, withdrawing by tokens - // is generally preferred to withdrawing by amount. - auto lpTokens = ammAlice.deposit(carol, USD(1'000)); - ammAlice.withdraw(carol, lpTokens, USD(0)); - lpTokens = ammAlice.deposit(carol, STAmount(USD, 1, -6)); - ammAlice.withdraw(carol, lpTokens, USD(0)); - lpTokens = ammAlice.deposit(carol, XRPAmount(1)); - ammAlice.withdraw(carol, lpTokens, XRPAmount(0)); - BEAST_EXPECT(ammAlice.expectBalances( - XRP(10'000), USD(10'000), ammAlice.tokens())); - BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0})); - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + // Since a smaller amount might be deposited due to + // the lp tokens adjustment, withdrawing by tokens + // is generally preferred to withdrawing by amount. + auto lpTokens = ammAlice.deposit(carol, USD(1'000)); + ammAlice.withdraw(carol, lpTokens, USD(0)); + lpTokens = ammAlice.deposit(carol, STAmount(USD, 1, -6)); + ammAlice.withdraw(carol, lpTokens, USD(0)); + lpTokens = ammAlice.deposit(carol, XRPAmount(1)); + ammAlice.withdraw(carol, lpTokens, XRPAmount(0)); + if (!env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10'000), USD(10'000), ammAlice.tokens())); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(10'000'000'001), + USD(10'000), + ammAlice.tokens())); + BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0})); + }, + std::nullopt, + 0, + std::nullopt, + {all, all - fixAMMv1_3}); // Single deposit by different accounts and then withdraw // in reverse. @@ -2307,36 +2457,34 @@ private: IOUAmount{10'000'000, 0})); }); - auto const all = supported_amendments(); // Withdraw with EPrice limit. testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit(carol, 1'000'000); ammAlice.withdraw( carol, USD(100), std::nullopt, IOUAmount{520, 0}); - if (!env.current()->rules().enabled(fixAMMv1_1)) - BEAST_EXPECT( - ammAlice.expectBalances( - XRPAmount(11'000'000'000), - STAmount{USD, UINT64_C(9'372'781065088757), -12}, - IOUAmount{10'153'846'15384616, -8}) && - ammAlice.expectLPTokens( - carol, IOUAmount{153'846'15384616, -8})); - else - BEAST_EXPECT( - ammAlice.expectBalances( - XRPAmount(11'000'000'000), - STAmount{USD, UINT64_C(9'372'781065088769), -12}, - IOUAmount{10'153'846'15384616, -8}) && - ammAlice.expectLPTokens( - carol, IOUAmount{153'846'15384616, -8})); + BEAST_EXPECT(ammAlice.expectLPTokens( + carol, IOUAmount{153'846'15384616, -8})); + if (!env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(11'000'000'000), + STAmount{USD, UINT64_C(9'372'781065088757), -12}, + IOUAmount{10'153'846'15384616, -8})); + else if (env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(11'000'000'000), + STAmount{USD, UINT64_C(9'372'781065088769), -12}, + IOUAmount{10'153'846'15384616, -8})); + else if (env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(11'000'000'000), + STAmount{USD, UINT64_C(9'372'78106508877), -11}, + IOUAmount{10'153'846'15384616, -8})); ammAlice.withdrawAll(carol); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0})); }, - std::nullopt, - 0, - std::nullopt, - {all, all - fixAMMv1_1}); + {.features = {all, all - fixAMMv1_3, all - fixAMMv1_1 - fixAMMv1_3}, + .noLog = true}); // Withdraw with EPrice limit. AssetOut is 0. testAMM( @@ -2344,27 +2492,28 @@ private: ammAlice.deposit(carol, 1'000'000); ammAlice.withdraw( carol, USD(0), std::nullopt, IOUAmount{520, 0}); - if (!env.current()->rules().enabled(fixAMMv1_1)) - BEAST_EXPECT( - ammAlice.expectBalances( - XRPAmount(11'000'000'000), - STAmount{USD, UINT64_C(9'372'781065088757), -12}, - IOUAmount{10'153'846'15384616, -8}) && - ammAlice.expectLPTokens( - carol, IOUAmount{153'846'15384616, -8})); - else - BEAST_EXPECT( - ammAlice.expectBalances( - XRPAmount(11'000'000'000), - STAmount{USD, UINT64_C(9'372'781065088769), -12}, - IOUAmount{10'153'846'15384616, -8}) && - ammAlice.expectLPTokens( - carol, IOUAmount{153'846'15384616, -8})); + BEAST_EXPECT(ammAlice.expectLPTokens( + carol, IOUAmount{153'846'15384616, -8})); + if (!env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(11'000), + STAmount{USD, UINT64_C(9'372'781065088757), -12}, + IOUAmount{10'153'846'15384616, -8})); + else if (env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(11'000), + STAmount{USD, UINT64_C(9'372'781065088769), -12}, + IOUAmount{10'153'846'15384616, -8})); + else if (env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(11'000), + STAmount{USD, UINT64_C(9'372'78106508877), -11}, + IOUAmount{10'153'846'15384616, -8})); }, std::nullopt, 0, std::nullopt, - {all, all - fixAMMv1_1}); + {all, all - fixAMMv1_3, all - fixAMMv1_1 - fixAMMv1_3}); // IOU to IOU + transfer fee { @@ -2403,14 +2552,25 @@ private: STAmount{USD, UINT64_C(9'999'999999), -6}, IOUAmount{9'999'999'999, -3})); }); - testAMM([&](AMM& ammAlice, Env&) { - // Single XRP pool - ammAlice.withdraw(alice, std::nullopt, XRPAmount{1}); - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount{9'999'999'999}, - USD(10'000), - IOUAmount{9'999'999'9995, -4})); - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + // Single XRP pool + ammAlice.withdraw(alice, std::nullopt, XRPAmount{1}); + if (!env.enabled(fixAMMv1_3)) + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{9'999'999'999}, + USD(10'000), + IOUAmount{9'999'999'9995, -4})); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10'000), + USD(10'000), + IOUAmount{9'999'999'9995, -4})); + }, + std::nullopt, + 0, + std::nullopt, + {all, all - fixAMMv1_3}); testAMM([&](AMM& ammAlice, Env&) { // Single USD pool ammAlice.withdraw(alice, std::nullopt, STAmount{USD, 1, -10}); @@ -2528,6 +2688,7 @@ private: { testcase("Fee Vote"); using namespace jtx; + auto const all = supported_amendments(); // One vote sets fee to 1%. testAMM([&](AMM& ammAlice, Env& env) { @@ -2545,6 +2706,12 @@ private: std::uint32_t tokens = 10'000'000, std::vector* accounts = nullptr) { Account a(std::to_string(i)); + // post-amendment the amount to deposit is slightly higher + // in order to ensure AMM invariant sqrt(asset1 * asset2) >= tokens + // fund just one USD higher in this case, which is enough for + // deposit to succeed + if (env.enabled(fixAMMv1_3)) + ++fundUSD; fund(env, gw, {a}, {USD(fundUSD)}, Fund::Acct); ammAlice.deposit(a, tokens); ammAlice.vote(a, 50 * (i + 1)); @@ -2553,11 +2720,16 @@ private: }; // Eight votes fill all voting slots, set fee 0.175%. - testAMM([&](AMM& ammAlice, Env& env) { - for (int i = 0; i < 7; ++i) - vote(ammAlice, env, i, 10'000); - BEAST_EXPECT(ammAlice.expectTradingFee(175)); - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + for (int i = 0; i < 7; ++i) + vote(ammAlice, env, i, 10'000); + BEAST_EXPECT(ammAlice.expectTradingFee(175)); + }, + std::nullopt, + 0, + std::nullopt, + {all}); // Eight votes fill all voting slots, set fee 0.175%. // New vote, same account, sets fee 0.225% @@ -2951,8 +3123,14 @@ private: fund(env, gw, {bob}, {USD(10'000)}, Fund::Acct); ammAlice.deposit(bob, 1'000'000); - BEAST_EXPECT(ammAlice.expectBalances( - XRP(12'000), USD(12'000), IOUAmount{12'000'000, 0})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(12'000), USD(12'000), IOUAmount{12'000'000, 0})); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{12'000'000'001}, + USD(12'000), + IOUAmount{12'000'000, 0})); // Initial state. Pay bidMin. env(ammAlice.bid({.account = carol, .bidMin = 110})).close(); @@ -2984,8 +3162,16 @@ private: BEAST_EXPECT(ammAlice.expectAuctionSlot( 0, std::nullopt, IOUAmount{110})); // ~321.09 tokens burnt on bidding fees. - BEAST_EXPECT(ammAlice.expectBalances( - XRP(12'000), USD(12'000), IOUAmount{11'999'678'91, -2})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(12'000), + USD(12'000), + IOUAmount{11'999'678'91, -2})); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{12'000'000'001}, + USD(12'000), + IOUAmount{11'999'678'91, -2})); }, std::nullopt, 0, @@ -3014,8 +3200,12 @@ private: auto const slotPrice = IOUAmount{5'200}; ammTokens -= slotPrice; BEAST_EXPECT(ammAlice.expectAuctionSlot(100, 0, slotPrice)); - BEAST_EXPECT(ammAlice.expectBalances( - XRP(13'000), USD(13'000), ammTokens)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(13'000), USD(13'000), ammTokens)); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'000'000'003}, USD(13'000), ammTokens)); // Discounted trade for (int i = 0; i < 10; ++i) { @@ -3056,10 +3246,16 @@ private: env.balance(ed, USD) == STAmount(USD, UINT64_C(18'999'0057261184), -10)); // USD pool is slightly higher because of the fees. - BEAST_EXPECT(ammAlice.expectBalances( - XRP(13'000), - STAmount(USD, UINT64_C(13'002'98282151422), -11), - ammTokens)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(13'000), + STAmount(USD, UINT64_C(13'002'98282151422), -11), + ammTokens)); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'000'000'003}, + STAmount(USD, UINT64_C(13'002'98282151422), -11), + ammTokens)); } ammTokens = ammAlice.getLPTokensBalance(); // Trade with the fee @@ -3101,31 +3297,54 @@ private: } else { - BEAST_EXPECT( - env.balance(dan, USD) == - STAmount(USD, UINT64_C(19'490'05672274399), -11)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT( + env.balance(dan, USD) == + STAmount(USD, UINT64_C(19'490'05672274399), -11)); + else + BEAST_EXPECT( + env.balance(dan, USD) == + STAmount(USD, UINT64_C(19'490'05672274398), -11)); // USD pool gains more in dan's fees. - BEAST_EXPECT(ammAlice.expectBalances( - XRP(13'000), - STAmount{USD, UINT64_C(13'012'92609877023), -11}, - ammTokens)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(13'000), + STAmount{USD, UINT64_C(13'012'92609877023), -11}, + ammTokens)); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'000'000'003}, + STAmount{USD, UINT64_C(13'012'92609877024), -11}, + ammTokens)); // Discounted fee payment ammAlice.deposit(carol, USD(100)); ammTokens = ammAlice.getLPTokensBalance(); - BEAST_EXPECT(ammAlice.expectBalances( - XRP(13'000), - STAmount{USD, UINT64_C(13'112'92609877023), -11}, - ammTokens)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(13'000), + STAmount{USD, UINT64_C(13'112'92609877023), -11}, + ammTokens)); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'000'000'003}, + STAmount{USD, UINT64_C(13'112'92609877024), -11}, + ammTokens)); env(pay(carol, bob, USD(100)), path(~USD), sendmax(XRP(110))); env.close(); // carol pays 100000 drops in fees // 99900668XRP swapped in for 100USD - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount{13'100'000'668}, - STAmount{USD, UINT64_C(13'012'92609877023), -11}, - ammTokens)); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'100'000'668}, + STAmount{USD, UINT64_C(13'012'92609877023), -11}, + ammTokens)); + else + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'100'000'671}, + STAmount{USD, UINT64_C(13'012'92609877024), -11}, + ammTokens)); } // Payment with the trading fee env(pay(alice, carol, XRP(100)), path(~XRP), sendmax(USD(110))); @@ -3133,20 +3352,27 @@ private: // alice pays ~1.011USD in fees, which is ~10 times more // than carol's fee // 100.099431529USD swapped in for 100XRP - if (!features[fixAMMv1_1]) + if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{13'000'000'668}, STAmount{USD, UINT64_C(13'114'03663047264), -11}, ammTokens)); } - else + else if (features[fixAMMv1_1] && !features[fixAMMv1_3]) { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount{13'000'000'668}, STAmount{USD, UINT64_C(13'114'03663047269), -11}, ammTokens)); } + else + { + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'000'000'671}, + STAmount{USD, UINT64_C(13'114'03663044937), -11}, + ammTokens)); + } // Auction slot expired, no discounted fee env.close(seconds(TOTAL_TIME_SLOT_SECS + 1)); // clock is parent's based @@ -3155,7 +3381,7 @@ private: BEAST_EXPECT( env.balance(carol, USD) == STAmount(USD, UINT64_C(29'399'00572620545), -11)); - else + else if (!features[fixAMMv1_3]) BEAST_EXPECT( env.balance(carol, USD) == STAmount(USD, UINT64_C(29'399'00572620544), -11)); @@ -3167,7 +3393,7 @@ private: } // carol pays ~9.94USD in fees, which is ~10 times more in // trading fees vs discounted fee. - if (!features[fixAMMv1_1]) + if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) { BEAST_EXPECT( env.balance(carol, USD) == @@ -3177,7 +3403,7 @@ private: STAmount{USD, UINT64_C(13'123'98038490681), -11}, ammTokens)); } - else + else if (features[fixAMMv1_1] && !features[fixAMMv1_3]) { BEAST_EXPECT( env.balance(carol, USD) == @@ -3187,25 +3413,42 @@ private: STAmount{USD, UINT64_C(13'123'98038490689), -11}, ammTokens)); } + else + { + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(29'389'06197177129), -11)); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{13'000'000'671}, + STAmount{USD, UINT64_C(13'123'98038488352), -11}, + ammTokens)); + } env(pay(carol, bob, USD(100)), path(~USD), sendmax(XRP(110))); env.close(); // carol pays ~1.008XRP in trading fee, which is // ~10 times more than the discounted fee. // 99.815876XRP is swapped in for 100USD - if (!features[fixAMMv1_1]) + if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount(13'100'824'790), STAmount{USD, UINT64_C(13'023'98038490681), -11}, ammTokens)); } - else + else if (features[fixAMMv1_1] && !features[fixAMMv1_3]) { BEAST_EXPECT(ammAlice.expectBalances( XRPAmount(13'100'824'790), STAmount{USD, UINT64_C(13'023'98038490689), -11}, ammTokens)); } + else + { + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(13'100'824'793), + STAmount{USD, UINT64_C(13'023'98038488352), -11}, + ammTokens)); + } }, std::nullopt, 1'000, @@ -3408,10 +3651,10 @@ private: // Can't pay into AMM with escrow. testAMM([&](AMM& ammAlice, Env& env) { auto const baseFee = env.current()->fees().base; - env(escrow(carol, ammAlice.ammAccount(), XRP(1)), - condition(cb1), - finish_time(env.now() + 1s), - cancel_time(env.now() + 2s), + env(escrow::create(carol, ammAlice.ammAccount(), XRP(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), fee(baseFee * 150), ter(tecNO_PERMISSION)); }); @@ -4506,7 +4749,7 @@ private: testAMM([&](AMM& ammAlice, Env& env) { auto const baseFee = env.current()->fees().base.drops(); auto const token1 = ammAlice.lptIssue(); - auto priceXRP = withdrawByTokens( + auto priceXRP = ammAssetOut( STAmount{XRPAmount{10'000'000'000}}, STAmount{token1, 10'000'000}, STAmount{token1, 5'000'000}, @@ -4533,7 +4776,7 @@ private: BEAST_EXPECT( accountBalance(env, carol) == std::to_string(22500000000 - 4 * baseFee)); - priceXRP = withdrawByTokens( + priceXRP = ammAssetOut( STAmount{XRPAmount{10'000'000'000}}, STAmount{token1, 9'999'900}, STAmount{token1, 4'999'900}, @@ -4890,9 +5133,12 @@ private: carol, USD(100), std::nullopt, IOUAmount{520, 0}); // carol withdraws ~1,443.44USD auto const balanceAfterWithdraw = [&]() { - if (!features[fixAMMv1_1]) + if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) return STAmount(USD, UINT64_C(30'443'43891402715), -11); - return STAmount(USD, UINT64_C(30'443'43891402714), -11); + else if (features[fixAMMv1_1] && !features[fixAMMv1_3]) + return STAmount(USD, UINT64_C(30'443'43891402714), -11); + else + return STAmount(USD, UINT64_C(30'443'43891402713), -11); }(); BEAST_EXPECT(env.balance(carol, USD) == balanceAfterWithdraw); // Set to original pool size @@ -4902,22 +5148,29 @@ private: ammAlice.vote(alice, 0); BEAST_EXPECT(ammAlice.expectTradingFee(0)); auto const tokensNoFee = ammAlice.withdraw(carol, deposit); - if (!features[fixAMMv1_1]) + if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) BEAST_EXPECT( env.balance(carol, USD) == STAmount(USD, UINT64_C(30'443'43891402717), -11)); - else + else if (features[fixAMMv1_1] && !features[fixAMMv1_3]) BEAST_EXPECT( env.balance(carol, USD) == STAmount(USD, UINT64_C(30'443'43891402716), -11)); - // carol pays ~4008 LPTokens in fees or ~0.5% of the no-fee - // LPTokens - if (!features[fixAMMv1_1]) - BEAST_EXPECT( - tokensNoFee == IOUAmount(746'579'80779913, -8)); else + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(30'443'43891402713), -11)); + // carol pays ~4008 LPTokens in fees or ~0.5% of the no-fee + // LPTokens + if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) + BEAST_EXPECT( + tokensNoFee == IOUAmount(746'579'80779913, -8)); + else if (features[fixAMMv1_1] && !features[fixAMMv1_3]) BEAST_EXPECT( tokensNoFee == IOUAmount(746'579'80779912, -8)); + else + BEAST_EXPECT( + tokensNoFee == IOUAmount(746'579'80779911, -8)); BEAST_EXPECT(tokensFee == IOUAmount(750'588'23529411, -8)); }, std::nullopt, @@ -5214,11 +5467,16 @@ private: // Due to round off some accounts have a tiny gain, while // other have a tiny loss. The last account to withdraw // gets everything in the pool. - if (!features[fixAMMv1_1]) + if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), STAmount{USD, UINT64_C(10'000'0000000013), -10}, IOUAmount{10'000'000})); + else if (features[fixAMMv1_3]) + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10'000), + STAmount{USD, UINT64_C(10'000'0000000003), -10}, + IOUAmount{10'000'000})); else BEAST_EXPECT(ammAlice.expectBalances( XRP(10'000), USD(10'000), IOUAmount{10'000'000})); @@ -5226,25 +5484,29 @@ private: BEAST_EXPECT(expectLine(env, simon, USD(1'500'000))); BEAST_EXPECT(expectLine(env, chris, USD(1'500'000))); BEAST_EXPECT(expectLine(env, dan, USD(1'500'000))); - if (!features[fixAMMv1_1]) + if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) BEAST_EXPECT(expectLine( env, carol, STAmount{USD, UINT64_C(30'000'00000000001), -11})); + else if (features[fixAMMv1_1] && !features[fixAMMv1_3]) + BEAST_EXPECT(expectLine(env, carol, USD(30'000))); else BEAST_EXPECT(expectLine(env, carol, USD(30'000))); BEAST_EXPECT(expectLine(env, ed, USD(1'500'000))); BEAST_EXPECT(expectLine(env, paul, USD(1'500'000))); - if (!features[fixAMMv1_1]) + if (!features[fixAMMv1_1] && !features[fixAMMv1_3]) BEAST_EXPECT(expectLine( env, nataly, STAmount{USD, UINT64_C(1'500'000'000000002), -9})); - else + else if (features[fixAMMv1_1] && !features[fixAMMv1_3]) BEAST_EXPECT(expectLine( env, nataly, STAmount{USD, UINT64_C(1'500'000'000000005), -9})); + else + BEAST_EXPECT(expectLine(env, nataly, USD(1'500'000))); ammAlice.withdrawAll(alice); BEAST_EXPECT(!ammAlice.ammExists()); if (!features[fixAMMv1_1]) @@ -5252,6 +5514,11 @@ private: env, alice, STAmount{USD, UINT64_C(30'000'0000000013), -10})); + else if (features[fixAMMv1_3]) + BEAST_EXPECT(expectLine( + env, + alice, + STAmount{USD, UINT64_C(30'000'0000000003), -10})); else BEAST_EXPECT(expectLine(env, alice, USD(30'000))); // alice XRP balance is 30,000initial - 50 ammcreate fee - @@ -5267,68 +5534,110 @@ private: {features}); // Same as above but deposit/withdraw in XRP - testAMM([&](AMM& ammAlice, Env& env) { - Account const bob("bob"); - Account const ed("ed"); - Account const paul("paul"); - Account const dan("dan"); - Account const chris("chris"); - Account const simon("simon"); - Account const ben("ben"); - Account const nataly("nataly"); - fund( - env, - gw, - {bob, ed, paul, dan, chris, simon, ben, nataly}, - XRP(2'000'000), - {}, - Fund::Acct); - for (int i = 0; i < 10; ++i) - { - ammAlice.deposit(ben, XRPAmount{1}); - ammAlice.withdrawAll(ben, XRP(0)); - ammAlice.deposit(simon, XRPAmount(1'000)); - ammAlice.withdrawAll(simon, XRP(0)); - ammAlice.deposit(chris, XRP(1)); - ammAlice.withdrawAll(chris, XRP(0)); - ammAlice.deposit(dan, XRP(10)); - ammAlice.withdrawAll(dan, XRP(0)); - ammAlice.deposit(bob, XRP(100)); - ammAlice.withdrawAll(bob, XRP(0)); - ammAlice.deposit(carol, XRP(1'000)); - ammAlice.withdrawAll(carol, XRP(0)); - ammAlice.deposit(ed, XRP(10'000)); - ammAlice.withdrawAll(ed, XRP(0)); - ammAlice.deposit(paul, XRP(100'000)); - ammAlice.withdrawAll(paul, XRP(0)); - ammAlice.deposit(nataly, XRP(1'000'000)); - ammAlice.withdrawAll(nataly, XRP(0)); - } - // No round off with XRP in this test - BEAST_EXPECT(ammAlice.expectBalances( - XRP(10'000), USD(10'000), IOUAmount{10'000'000})); - ammAlice.withdrawAll(alice); - BEAST_EXPECT(!ammAlice.ammExists()); - // 20,000 initial - (deposit+withdraw) * 10 - auto const xrpBalance = (XRP(2'000'000) - txfee(env, 20)).getText(); - BEAST_EXPECT(accountBalance(env, ben) == xrpBalance); - BEAST_EXPECT(accountBalance(env, simon) == xrpBalance); - BEAST_EXPECT(accountBalance(env, chris) == xrpBalance); - BEAST_EXPECT(accountBalance(env, dan) == xrpBalance); + testAMM( + [&](AMM& ammAlice, Env& env) { + Account const bob("bob"); + Account const ed("ed"); + Account const paul("paul"); + Account const dan("dan"); + Account const chris("chris"); + Account const simon("simon"); + Account const ben("ben"); + Account const nataly("nataly"); + fund( + env, + gw, + {bob, ed, paul, dan, chris, simon, ben, nataly}, + XRP(2'000'000), + {}, + Fund::Acct); + for (int i = 0; i < 10; ++i) + { + ammAlice.deposit(ben, XRPAmount{1}); + ammAlice.withdrawAll(ben, XRP(0)); + ammAlice.deposit(simon, XRPAmount(1'000)); + ammAlice.withdrawAll(simon, XRP(0)); + ammAlice.deposit(chris, XRP(1)); + ammAlice.withdrawAll(chris, XRP(0)); + ammAlice.deposit(dan, XRP(10)); + ammAlice.withdrawAll(dan, XRP(0)); + ammAlice.deposit(bob, XRP(100)); + ammAlice.withdrawAll(bob, XRP(0)); + ammAlice.deposit(carol, XRP(1'000)); + ammAlice.withdrawAll(carol, XRP(0)); + ammAlice.deposit(ed, XRP(10'000)); + ammAlice.withdrawAll(ed, XRP(0)); + ammAlice.deposit(paul, XRP(100'000)); + ammAlice.withdrawAll(paul, XRP(0)); + ammAlice.deposit(nataly, XRP(1'000'000)); + ammAlice.withdrawAll(nataly, XRP(0)); + } + auto const baseFee = env.current()->fees().base.drops(); + if (!features[fixAMMv1_3]) + { + // No round off with XRP in this test + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10'000), USD(10'000), IOUAmount{10'000'000})); + ammAlice.withdrawAll(alice); + BEAST_EXPECT(!ammAlice.ammExists()); + // 20,000 initial - (deposit+withdraw) * 10 + auto const xrpBalance = + (XRP(2'000'000) - txfee(env, 20)).getText(); + BEAST_EXPECT(accountBalance(env, ben) == xrpBalance); + BEAST_EXPECT(accountBalance(env, simon) == xrpBalance); + BEAST_EXPECT(accountBalance(env, chris) == xrpBalance); + BEAST_EXPECT(accountBalance(env, dan) == xrpBalance); - auto const baseFee = env.current()->fees().base.drops(); - // 30,000 initial - (deposit+withdraw) * 10 - BEAST_EXPECT( - accountBalance(env, carol) == - std::to_string(30000000000 - 20 * baseFee)); - BEAST_EXPECT(accountBalance(env, ed) == xrpBalance); - BEAST_EXPECT(accountBalance(env, paul) == xrpBalance); - BEAST_EXPECT(accountBalance(env, nataly) == xrpBalance); - // 30,000 initial - 50 ammcreate fee - 10drops withdraw fee - BEAST_EXPECT( - accountBalance(env, alice) == - std::to_string(29950000000 - baseFee)); - }); + // 30,000 initial - (deposit+withdraw) * 10 + BEAST_EXPECT( + accountBalance(env, carol) == + std::to_string(30'000'000'000 - 20 * baseFee)); + BEAST_EXPECT(accountBalance(env, ed) == xrpBalance); + BEAST_EXPECT(accountBalance(env, paul) == xrpBalance); + BEAST_EXPECT(accountBalance(env, nataly) == xrpBalance); + // 30,000 initial - 50 ammcreate fee - 10drops withdraw fee + BEAST_EXPECT( + accountBalance(env, alice) == + std::to_string(29'950'000'000 - baseFee)); + } + else + { + // post-amendment the rounding takes place to ensure + // AMM invariant + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(10'000'000'080), + USD(10'000), + IOUAmount{10'000'000})); + ammAlice.withdrawAll(alice); + BEAST_EXPECT(!ammAlice.ammExists()); + auto const xrpBalance = + XRP(2'000'000) - txfee(env, 20) - drops(10); + auto const xrpBalanceText = xrpBalance.getText(); + BEAST_EXPECT(accountBalance(env, ben) == xrpBalanceText); + BEAST_EXPECT(accountBalance(env, simon) == xrpBalanceText); + BEAST_EXPECT(accountBalance(env, chris) == xrpBalanceText); + BEAST_EXPECT(accountBalance(env, dan) == xrpBalanceText); + BEAST_EXPECT( + accountBalance(env, carol) == + std::to_string(30'000'000'000 - 20 * baseFee - 10)); + BEAST_EXPECT( + accountBalance(env, ed) == + (xrpBalance + drops(2)).getText()); + BEAST_EXPECT( + accountBalance(env, paul) == + (xrpBalance + drops(3)).getText()); + BEAST_EXPECT( + accountBalance(env, nataly) == + (xrpBalance + drops(5)).getText()); + BEAST_EXPECT( + accountBalance(env, alice) == + std::to_string(29'950'000'000 - baseFee + 80)); + } + }, + std::nullopt, + 0, + std::nullopt, + {features}); } void @@ -6370,11 +6679,11 @@ private: } void - testFixOverflowOffer(FeatureBitset features) + testFixOverflowOffer(FeatureBitset featuresInitial) { using namespace jtx; using namespace std::chrono; - FeatureBitset const all{features}; + FeatureBitset const all{featuresInitial}; std::string logs; @@ -6401,6 +6710,7 @@ private: STAmount const goodUsdBIT; STAmount const goodUsdBITr; IOUAmount const lpTokenBalance; + std::optional const lpTokenBalanceAlt = {}; double const offer1BtcGH = 0.1; double const offer2BtcGH = 0.1; double const offer2UsdGH = 1; @@ -6426,6 +6736,7 @@ private: .goodUsdBIT{usdBIT, uint64_t(8'464739069120721), -15}, // .goodUsdBITr{usdBIT, uint64_t(8'464739069098152), -15}, // .lpTokenBalance = {28'61817604250837, -14}, // + .lpTokenBalanceAlt = IOUAmount{28'61817604250836, -14}, // .offer1BtcGH = 0.1, // .offer2BtcGH = 0.1, // .offer2UsdGH = 1, // @@ -6604,7 +6915,7 @@ private: { testcase(input.testCase); for (auto const& features : - {all - fixAMMOverflowOffer, all | fixAMMOverflowOffer}) + {all - fixAMMOverflowOffer - fixAMMv1_1 - fixAMMv1_3, all}) { Env env(*this, features, std::make_unique(&logs)); @@ -6658,15 +6969,19 @@ private: features[fixAMMv1_1] ? input.goodUsdGHr : input.goodUsdGH; auto const goodUsdBIT = features[fixAMMv1_1] ? input.goodUsdBITr : input.goodUsdBIT; + auto const lpTokenBalance = + env.enabled(fixAMMv1_3) && input.lpTokenBalanceAlt + ? *input.lpTokenBalanceAlt + : input.lpTokenBalance; if (!features[fixAMMOverflowOffer]) { BEAST_EXPECT(amm.expectBalances( - failUsdGH, failUsdBIT, input.lpTokenBalance)); + failUsdGH, failUsdBIT, lpTokenBalance)); } else { BEAST_EXPECT(amm.expectBalances( - goodUsdGH, goodUsdBIT, input.lpTokenBalance)); + goodUsdGH, goodUsdBIT, lpTokenBalance)); // Invariant: LPToken balance must not change in a // payment or a swap transaction @@ -6862,11 +7177,13 @@ private: void testLPTokenBalance(FeatureBitset features) { + testcase("LPToken Balance"); using namespace jtx; // Last Liquidity Provider is the issuer of one token { - Env env(*this, features); + std::string logs; + Env env(*this, features, std::make_unique(&logs)); fund( env, gw, @@ -6877,7 +7194,9 @@ private: amm.deposit(alice, IOUAmount{1'876123487565916, -15}); amm.deposit(carol, IOUAmount{1'000'000}); amm.withdrawAll(alice); + BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{0})); amm.withdrawAll(carol); + BEAST_EXPECT(amm.expectLPTokens(carol, IOUAmount{0})); auto const lpToken = getAccountLines( env, gw, amm.lptIssue())[jss::lines][0u][jss::balance]; auto const lpTokenBalance = @@ -7199,6 +7518,378 @@ private: supported_amendments() | featureSingleAssetVault); } + void + testDepositAndWithdrawRounding(FeatureBitset features) + { + testcase("Deposit and Withdraw Rounding V2"); + using namespace jtx; + + auto const XPM = gw["XPM"]; + STAmount xrpBalance{XRPAmount(692'614'492'126)}; + STAmount xpmBalance{XPM, UINT64_C(18'610'359'80246901), -8}; + STAmount amount{XPM, UINT64_C(6'566'496939465400), -12}; + std::uint16_t tfee = 941; + + auto test = [&](auto&& cb, std::uint16_t tfee_) { + Env env(*this, features); + env.fund(XRP(1'000'000), gw); + env.fund(XRP(1'000), alice); + env(trust(alice, XPM(7'000))); + env(pay(gw, alice, amount)); + + AMM amm(env, gw, xrpBalance, xpmBalance, CreateArg{.tfee = tfee_}); + // AMM LPToken balance required to replicate single deposit failure + STAmount lptAMMBalance{ + amm.lptIssue(), UINT64_C(3'234'987'266'485968), -6}; + auto const burn = + IOUAmount{amm.getLPTokensBalance() - lptAMMBalance}; + // burn tokens to get to the required AMM state + env(amm.bid(BidArg{.account = gw, .bidMin = burn, .bidMax = burn})); + cb(amm, env); + }; + test( + [&](AMM& amm, Env& env) { + auto const err = env.enabled(fixAMMv1_3) ? ter(tesSUCCESS) + : ter(tecUNFUNDED_AMM); + amm.deposit(DepositArg{ + .account = alice, .asset1In = amount, .err = err}); + }, + tfee); + test( + [&](AMM& amm, Env& env) { + auto const [amount, amount2, lptAMM] = amm.balances(XRP, XPM); + auto const withdraw = STAmount{XPM, 1, -5}; + amm.withdraw(WithdrawArg{.asset1Out = STAmount{XPM, 1, -5}}); + auto const [amount_, amount2_, lptAMM_] = + amm.balances(XRP, XPM); + if (!env.enabled(fixAMMv1_3)) + BEAST_EXPECT((amount2 - amount2_) > withdraw); + else + BEAST_EXPECT((amount2 - amount2_) <= withdraw); + }, + 0); + } + + void + invariant( + jtx::AMM& amm, + jtx::Env& env, + std::string const& msg, + bool shouldFail) + { + auto const [amount, amount2, lptBalance] = amm.balances(GBP, EUR); + + NumberRoundModeGuard g( + env.enabled(fixAMMv1_3) ? Number::upward : Number::getround()); + auto const res = root2(amount * amount2); + + if (shouldFail) + BEAST_EXPECT(res < lptBalance); + else + BEAST_EXPECT(res >= lptBalance); + } + + void + testDepositRounding(FeatureBitset all) + { + testcase("Deposit Rounding"); + using namespace jtx; + + // Single asset deposit + for (auto const& deposit : + {STAmount(EUR, 1, 1), + STAmount(EUR, 1, 2), + STAmount(EUR, 1, 5), + STAmount(EUR, 1, -3), // fail + STAmount(EUR, 1, -6), + STAmount(EUR, 1, -9)}) + { + testAMM( + [&](AMM& ammAlice, Env& env) { + fund( + env, + gw, + {bob}, + XRP(10'000'000), + {GBP(100'000), EUR(100'000)}, + Fund::Acct); + env.close(); + + ammAlice.deposit( + DepositArg{.account = bob, .asset1In = deposit}); + invariant( + ammAlice, + env, + "dep1", + deposit == STAmount{EUR, 1, -3} && + !env.enabled(fixAMMv1_3)); + }, + {{GBP(30'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + } + + // Two-asset proportional deposit (1:1 pool ratio) + testAMM( + [&](AMM& ammAlice, Env& env) { + fund( + env, + gw, + {bob}, + XRP(10'000'000), + {GBP(100'000), EUR(100'000)}, + Fund::Acct); + env.close(); + + STAmount const depositEuro{ + EUR, UINT64_C(10'1234567890123456), -16}; + STAmount const depositGBP{ + GBP, UINT64_C(10'1234567890123456), -16}; + + ammAlice.deposit(DepositArg{ + .account = bob, + .asset1In = depositEuro, + .asset2In = depositGBP}); + invariant(ammAlice, env, "dep2", false); + }, + {{GBP(30'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // Two-asset proportional deposit (1:3 pool ratio) + for (auto const& exponent : {1, 2, 3, 4, -3 /*fail*/, -6, -9}) + { + testAMM( + [&](AMM& ammAlice, Env& env) { + fund( + env, + gw, + {bob}, + XRP(10'000'000), + {GBP(100'000), EUR(100'000)}, + Fund::Acct); + env.close(); + + STAmount const depositEuro{EUR, 1, exponent}; + STAmount const depositGBP{GBP, 1, exponent}; + + ammAlice.deposit(DepositArg{ + .account = bob, + .asset1In = depositEuro, + .asset2In = depositGBP}); + invariant( + ammAlice, + env, + "dep3", + exponent != -3 && !env.enabled(fixAMMv1_3)); + }, + {{GBP(10'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + } + + // tfLPToken deposit + testAMM( + [&](AMM& ammAlice, Env& env) { + fund( + env, + gw, + {bob}, + XRP(10'000'000), + {GBP(100'000), EUR(100'000)}, + Fund::Acct); + env.close(); + + ammAlice.deposit(DepositArg{ + .account = bob, + .tokens = IOUAmount{10'1234567890123456, -16}}); + invariant(ammAlice, env, "dep4", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // tfOneAssetLPToken deposit + for (auto const& tokens : + {IOUAmount{1, -3}, + IOUAmount{1, -2}, + IOUAmount{1, -1}, + IOUAmount{1}, + IOUAmount{10}, + IOUAmount{100}, + IOUAmount{1'000}, + IOUAmount{10'000}}) + { + testAMM( + [&](AMM& ammAlice, Env& env) { + fund( + env, + gw, + {bob}, + XRP(10'000'000), + {GBP(100'000), EUR(1'000'000)}, + Fund::Acct); + env.close(); + + ammAlice.deposit(DepositArg{ + .account = bob, + .tokens = tokens, + .asset1In = STAmount{EUR, 1, 6}}); + invariant(ammAlice, env, "dep5", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + } + + // Single deposit with EP not exceeding specified: + // 1'000 GBP with EP not to exceed 5 (GBP/TokensOut) + testAMM( + [&](AMM& ammAlice, Env& env) { + fund( + env, + gw, + {bob}, + XRP(10'000'000), + {GBP(100'000), EUR(100'000)}, + Fund::Acct); + env.close(); + + ammAlice.deposit( + bob, GBP(1'000), std::nullopt, STAmount{GBP, 5}); + invariant(ammAlice, env, "dep6", false); + }, + {{GBP(30'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + } + + void + testWithdrawRounding(FeatureBitset all) + { + testcase("Withdraw Rounding"); + + using namespace jtx; + + // tfLPToken mode + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.withdraw(alice, 1'000); + invariant(ammAlice, env, "with1", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // tfWithdrawAll mode + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.withdraw( + WithdrawArg{.account = alice, .flags = tfWithdrawAll}); + invariant(ammAlice, env, "with2", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // tfTwoAsset withdraw mode + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.withdraw(WithdrawArg{ + .account = alice, + .asset1Out = STAmount{GBP, 3'500}, + .asset2Out = STAmount{EUR, 15'000}, + .flags = tfTwoAsset}); + invariant(ammAlice, env, "with3", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // tfSingleAsset withdraw mode + // Note: This test fails with 0 trading fees, but doesn't fail if + // trading fees is set to 1'000 -- I suspect the compound operations + // in AMMHelpers.cpp:withdrawByTokens compensate for the rounding + // errors + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.withdraw(WithdrawArg{ + .account = alice, + .asset1Out = STAmount{GBP, 1'234}, + .flags = tfSingleAsset}); + invariant(ammAlice, env, "with4", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // tfOneAssetWithdrawAll mode + testAMM( + [&](AMM& ammAlice, Env& env) { + fund( + env, + gw, + {bob}, + XRP(10'000'000), + {GBP(100'000), EUR(100'000)}, + Fund::Acct); + env.close(); + + ammAlice.deposit(DepositArg{ + .account = bob, .asset1In = STAmount{GBP, 3'456}}); + + ammAlice.withdraw(WithdrawArg{ + .account = bob, + .asset1Out = STAmount{GBP, 1'000}, + .flags = tfOneAssetWithdrawAll}); + invariant(ammAlice, env, "with5", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // tfOneAssetLPToken mode + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.withdraw(WithdrawArg{ + .account = alice, + .tokens = 1'000, + .asset1Out = STAmount{GBP, 100}, + .flags = tfOneAssetLPToken}); + invariant(ammAlice, env, "with6", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // tfLimitLPToken mode + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.withdraw(WithdrawArg{ + .account = alice, + .asset1Out = STAmount{GBP, 100}, + .maxEP = IOUAmount{2}, + .flags = tfLimitLPToken}); + invariant(ammAlice, env, "with7", true); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + } + void run() override { @@ -7214,46 +7905,60 @@ private: testFeeVote(); testInvalidBid(); testBid(all); - testBid(all - fixAMMv1_1); + testBid(all - fixAMMv1_3); + testBid(all - fixAMMv1_1 - fixAMMv1_3); testInvalidAMMPayment(); testBasicPaymentEngine(all); - testBasicPaymentEngine(all - fixAMMv1_1); + testBasicPaymentEngine(all - fixAMMv1_1 - fixAMMv1_3); testBasicPaymentEngine(all - fixReducedOffersV2); - testBasicPaymentEngine(all - fixAMMv1_1 - fixReducedOffersV2); + testBasicPaymentEngine( + all - fixAMMv1_1 - fixAMMv1_3 - fixReducedOffersV2); testAMMTokens(); testAmendment(); testFlags(); testRippling(); testAMMAndCLOB(all); - testAMMAndCLOB(all - fixAMMv1_1); + testAMMAndCLOB(all - fixAMMv1_1 - fixAMMv1_3); testTradingFee(all); - testTradingFee(all - fixAMMv1_1); + testTradingFee(all - fixAMMv1_3); + testTradingFee(all - fixAMMv1_1 - fixAMMv1_3); testAdjustedTokens(all); - testAdjustedTokens(all - fixAMMv1_1); + testAdjustedTokens(all - fixAMMv1_3); + testAdjustedTokens(all - fixAMMv1_1 - fixAMMv1_3); testAutoDelete(); testClawback(); testAMMID(); testSelection(all); - testSelection(all - fixAMMv1_1); + testSelection(all - fixAMMv1_1 - fixAMMv1_3); testFixDefaultInnerObj(); testMalformed(); testFixOverflowOffer(all); - testFixOverflowOffer(all - fixAMMv1_1); + testFixOverflowOffer(all - fixAMMv1_3); + testFixOverflowOffer(all - fixAMMv1_1 - fixAMMv1_3); testSwapRounding(); testFixChangeSpotPriceQuality(all); - testFixChangeSpotPriceQuality(all - fixAMMv1_1); + testFixChangeSpotPriceQuality(all - fixAMMv1_1 - fixAMMv1_3); testFixAMMOfferBlockedByLOB(all); - testFixAMMOfferBlockedByLOB(all - fixAMMv1_1); + testFixAMMOfferBlockedByLOB(all - fixAMMv1_1 - fixAMMv1_3); testLPTokenBalance(all); - testLPTokenBalance(all - fixAMMv1_1); + testLPTokenBalance(all - fixAMMv1_3); + testLPTokenBalance(all - fixAMMv1_1 - fixAMMv1_3); testAMMClawback(all); testAMMClawback(all - featureAMMClawback); - testAMMClawback(all - fixAMMv1_1 - featureAMMClawback); + testAMMClawback(all - fixAMMv1_1 - fixAMMv1_3 - featureAMMClawback); testAMMDepositWithFrozenAssets(all); testAMMDepositWithFrozenAssets(all - featureAMMClawback); testAMMDepositWithFrozenAssets(all - fixAMMv1_1 - featureAMMClawback); + testAMMDepositWithFrozenAssets( + all - fixAMMv1_1 - fixAMMv1_3 - featureAMMClawback); testFixReserveCheckOnWithdrawal(all); testFixReserveCheckOnWithdrawal(all - fixAMMv1_2); + testDepositAndWithdrawRounding(all); + testDepositAndWithdrawRounding(all - fixAMMv1_3); + testDepositRounding(all); + testDepositRounding(all - fixAMMv1_3); + testWithdrawRounding(all); + testWithdrawRounding(all - fixAMMv1_3); testFailedPseudoAccount(); } }; diff --git a/src/test/app/AccountDelete_test.cpp b/src/test/app/AccountDelete_test.cpp index 03283e4611..1ac0256dcb 100644 --- a/src/test/app/AccountDelete_test.cpp +++ b/src/test/app/AccountDelete_test.cpp @@ -335,26 +335,11 @@ public: env(check::cancel(becky, checkId)); env.close(); - // Lambda to create an escrow. - auto escrowCreate = [](jtx::Account const& account, - jtx::Account const& to, - STAmount const& amount, - NetClock::time_point const& cancelAfter) { - Json::Value jv; - jv[jss::TransactionType] = jss::EscrowCreate; - jv[jss::Account] = account.human(); - jv[jss::Destination] = to.human(); - jv[jss::Amount] = amount.getJson(JsonOptions::none); - jv[sfFinishAfter.jsonName] = - cancelAfter.time_since_epoch().count() + 1; - jv[sfCancelAfter.jsonName] = - cancelAfter.time_since_epoch().count() + 2; - return jv; - }; - using namespace std::chrono_literals; std::uint32_t const escrowSeq{env.seq(alice)}; - env(escrowCreate(alice, becky, XRP(333), env.now() + 2s)); + env(escrow::create(alice, becky, XRP(333)), + escrow::finish_time(env.now() + 3s), + escrow::cancel_time(env.now() + 4s)); env.close(); // alice and becky should be unable to delete their accounts because @@ -366,17 +351,39 @@ public: // Now cancel the escrow, but create a payment channel between // alice and becky. - // Lambda to cancel an escrow. - auto escrowCancel = - [](Account const& account, Account const& from, std::uint32_t seq) { - Json::Value jv; - jv[jss::TransactionType] = jss::EscrowCancel; - jv[jss::Account] = account.human(); - jv[sfOwner.jsonName] = from.human(); - jv[sfOfferSequence.jsonName] = seq; - return jv; - }; - env(escrowCancel(becky, alice, escrowSeq)); + bool const withTokenEscrow = + env.current()->rules().enabled(featureTokenEscrow); + if (withTokenEscrow) + { + Account const gw1("gw1"); + Account const carol("carol"); + auto const USD = gw1["USD"]; + env.fund(XRP(100000), carol, gw1); + env(fset(gw1, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10000), carol); + env.close(); + env(pay(gw1, carol, USD(100))); + env.close(); + + std::uint32_t const escrowSeq{env.seq(carol)}; + env(escrow::create(carol, becky, USD(1)), + escrow::finish_time(env.now() + 3s), + escrow::cancel_time(env.now() + 4s)); + env.close(); + + incLgrSeqForAccDel(env, gw1); + + env(acctdelete(gw1, becky), + fee(acctDelFee), + ter(tecHAS_OBLIGATIONS)); + env.close(); + + env(escrow::cancel(becky, carol, escrowSeq)); + env.close(); + } + + env(escrow::cancel(becky, alice, escrowSeq)); env.close(); Keylet const alicePayChanKey{ diff --git a/src/test/app/Delegate_test.cpp b/src/test/app/Delegate_test.cpp index ca173a6993..dc3264d777 100644 --- a/src/test/app/Delegate_test.cpp +++ b/src/test/app/Delegate_test.cpp @@ -209,10 +209,10 @@ class Delegate_test : public beast::unit_test::suite } // when authorizing account which does not exist, should return - // terNO_ACCOUNT + // tecNO_TARGET { env(delegate::set(gw, Account("unknown"), {"Payment"}), - ter(terNO_ACCOUNT)); + ter(tecNO_TARGET)); } // non-delegatable transaction @@ -310,8 +310,9 @@ class Delegate_test : public beast::unit_test::suite { // Fee should be checked before permission check, - // otherwise tecNO_PERMISSION returned when permission check fails - // could cause context reset to pay fee because it is tec error + // otherwise tecNO_DELEGATE_PERMISSION returned when permission + // check fails could cause context reset to pay fee because it is + // tec error auto aliceBalance = env.balance(alice); auto bobBalance = env.balance(bob); auto carolBalance = env.balance(carol); @@ -526,12 +527,12 @@ class Delegate_test : public beast::unit_test::suite // bob does not have permission to create check env(check::create(alice, bob, XRP(10)), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); // carol does not have permission to create check env(check::create(alice, bob, XRP(10)), delegate::as(carol), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); } void @@ -566,7 +567,7 @@ class Delegate_test : public beast::unit_test::suite // delegate ledger object is not created yet env(pay(gw, alice, USD(50)), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env.require(balance(bob, bobBalance - drops(baseFee))); bobBalance = env.balance(bob, XRP); @@ -579,7 +580,7 @@ class Delegate_test : public beast::unit_test::suite // bob sends a payment transaction on behalf of gw env(pay(gw, alice, USD(50)), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env.close(); env.require(balance(bob, bobBalance - drops(baseFee))); bobBalance = env.balance(bob, XRP); @@ -596,7 +597,7 @@ class Delegate_test : public beast::unit_test::suite // can not send XRP env(pay(gw, alice, XRP(50)), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env.close(); env.require(balance(bob, bobBalance - drops(baseFee))); bobBalance = env.balance(bob, XRP); @@ -684,7 +685,7 @@ class Delegate_test : public beast::unit_test::suite // permission env(pay(gw, alice, USD(50)), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env.close(); env.require(balance(bob, bobBalance - drops(baseFee))); bobBalance = env.balance(bob, XRP); @@ -729,7 +730,7 @@ class Delegate_test : public beast::unit_test::suite // has unfreeze permission env(trust(alice, gw["USD"](50)), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env.close(); // alice creates trustline by herself @@ -743,38 +744,38 @@ class Delegate_test : public beast::unit_test::suite // unsupported flags env(trust(alice, gw["USD"](50), tfSetNoRipple), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env(trust(alice, gw["USD"](50), tfClearNoRipple), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env(trust(gw, gw["USD"](0), alice, tfSetDeepFreeze), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env(trust(gw, gw["USD"](0), alice, tfClearDeepFreeze), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env.close(); // supported flags with wrong permission env(trust(gw, gw["USD"](0), alice, tfSetfAuth), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env(trust(gw, gw["USD"](0), alice, tfSetFreeze), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env.close(); env(delegate::set(gw, bob, {"TrustlineAuthorize"})); env.close(); env(trust(gw, gw["USD"](0), alice, tfClearFreeze), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env.close(); // although trustline authorize is granted, bob can not change the // limit number env(trust(gw, gw["USD"](50), alice, tfSetfAuth), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env.close(); // supported flags with correct permission @@ -795,30 +796,30 @@ class Delegate_test : public beast::unit_test::suite // permission env(trust(gw, gw["USD"](0), alice, tfSetFreeze), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); // cannot update LimitAmount with granular permission, both high and // low account env(trust(alice, gw["USD"](100)), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env(trust(gw, alice["USD"](100)), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); // can not set QualityIn or QualityOut auto tx = trust(alice, gw["USD"](50)); tx["QualityIn"] = "1000"; - env(tx, delegate::as(bob), ter(tecNO_PERMISSION)); + env(tx, delegate::as(bob), ter(tecNO_DELEGATE_PERMISSION)); auto tx2 = trust(alice, gw["USD"](50)); tx2["QualityOut"] = "1000"; - env(tx2, delegate::as(bob), ter(tecNO_PERMISSION)); + env(tx2, delegate::as(bob), ter(tecNO_DELEGATE_PERMISSION)); auto tx3 = trust(gw, alice["USD"](50)); tx3["QualityIn"] = "1000"; - env(tx3, delegate::as(bob), ter(tecNO_PERMISSION)); + env(tx3, delegate::as(bob), ter(tecNO_DELEGATE_PERMISSION)); auto tx4 = trust(gw, alice["USD"](50)); tx4["QualityOut"] = "1000"; - env(tx4, delegate::as(bob), ter(tecNO_PERMISSION)); + env(tx4, delegate::as(bob), ter(tecNO_DELEGATE_PERMISSION)); // granting TrustSet can make it work env(delegate::set(gw, bob, {"TrustSet"})); @@ -828,7 +829,7 @@ class Delegate_test : public beast::unit_test::suite env(tx5, delegate::as(bob)); auto tx6 = trust(alice, gw["USD"](50)); tx6["QualityOut"] = "1000"; - env(tx6, delegate::as(bob), ter(tecNO_PERMISSION)); + env(tx6, delegate::as(bob), ter(tecNO_DELEGATE_PERMISSION)); env(delegate::set(alice, bob, {"TrustSet"})); env.close(); env(tx6, delegate::as(bob)); @@ -847,14 +848,14 @@ class Delegate_test : public beast::unit_test::suite // bob does not have permission env(trust(alice, gw["USD"](50)), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env(delegate::set( alice, bob, {"TrustlineUnfreeze", "NFTokenCreateOffer"})); env.close(); // bob still does not have permission env(trust(alice, gw["USD"](50)), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); // add TrustSet permission and some unrelated permission env(delegate::set( @@ -893,6 +894,56 @@ class Delegate_test : public beast::unit_test::suite env(trust(alice, gw["USD"](50), tfClearNoRipple), delegate::as(bob)); } + + // tfFullyCanonicalSig won't block delegated transaction + { + Env env(*this); + Account gw{"gw"}; + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(10000), gw, alice, bob); + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(alice, gw["USD"](50))); + env.close(); + + env(delegate::set(gw, bob, {"TrustlineAuthorize"})); + env.close(); + env(trust( + gw, gw["USD"](0), alice, tfSetfAuth | tfFullyCanonicalSig), + delegate::as(bob)); + } + + // tfInnerBatchTxn won't block delegated transaction + { + Env env(*this); + Account gw{"gw"}; + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(10000), gw, alice, bob); + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(alice, gw["USD"](50))); + env.close(); + + env(delegate::set( + gw, bob, {"TrustlineAuthorize", "TrustlineFreeze"})); + env.close(); + + auto const seq = env.seq(gw); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto jv1 = trust(gw, gw["USD"](0), alice, tfSetfAuth); + jv1[sfDelegate] = bob.human(); + auto jv2 = trust(gw, gw["USD"](0), alice, tfSetFreeze); + jv2[sfDelegate] = bob.human(); + + // batch::inner will set tfInnerBatchTxn, this should not + // block delegated transaction + env(batch::outer(gw, seq, batchFee, tfAllOrNothing), + batch::inner(jv1, seq + 1), + batch::inner(jv2, seq + 2)); + env.close(); + } } void @@ -920,16 +971,15 @@ class Delegate_test : public beast::unit_test::suite // on behalf of alice std::string const domain = "example.com"; auto jt = noop(alice); - jt[sfDomain.fieldName] = strHex(domain); - jt[sfDelegate.fieldName] = bob.human(); - jt[sfFlags.fieldName] = tfFullyCanonicalSig; + jt[sfDomain] = strHex(domain); + jt[sfDelegate] = bob.human(); // add granular permission related to AccountSet but is not the // correct permission for domain set env(delegate::set( alice, bob, {"TrustlineUnfreeze", "AccountEmailHashSet"})); env.close(); - env(jt, ter(tecNO_PERMISSION)); + env(jt, ter(tecNO_DELEGATE_PERMISSION)); // alice give granular permission of AccountDomainSet to bob env(delegate::set(alice, bob, {"AccountDomainSet"})); @@ -940,25 +990,24 @@ class Delegate_test : public beast::unit_test::suite BEAST_EXPECT((*env.le(alice))[sfDomain] == makeSlice(domain)); // bob can reset domain - jt[sfDomain.fieldName] = ""; + jt[sfDomain] = ""; env(jt); BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfDomain)); - // if flag is not equal to tfFullyCanonicalSig, which means bob - // is trying to set the flag at the same time, it will fail + // bob tries to set unauthorized flag, it will fail std::string const failDomain = "fail_domain_update"; - jt[sfFlags.fieldName] = tfRequireAuth; - jt[sfDomain.fieldName] = strHex(failDomain); - env(jt, ter(tecNO_PERMISSION)); + jt[sfFlags] = tfRequireAuth; + jt[sfDomain] = strHex(failDomain); + env(jt, ter(tecNO_DELEGATE_PERMISSION)); // reset flag number - jt[sfFlags.fieldName] = tfFullyCanonicalSig; + jt[sfFlags] = 0; // bob tries to update domain and set email hash, // but he does not have permission to set email hash - jt[sfDomain.fieldName] = strHex(domain); + jt[sfDomain] = strHex(domain); std::string const mh("5F31A79367DC3137FADA860C05742EE6"); - jt[sfEmailHash.fieldName] = mh; - env(jt, ter(tecNO_PERMISSION)); + jt[sfEmailHash] = mh; + env(jt, ter(tecNO_DELEGATE_PERMISSION)); // alice give granular permission of AccountEmailHashSet to bob env(delegate::set( @@ -970,8 +1019,8 @@ class Delegate_test : public beast::unit_test::suite // bob does not have permission to set message key for alice auto const rkp = randomKeyPair(KeyType::ed25519); - jt[sfMessageKey.fieldName] = strHex(rkp.first.slice()); - env(jt, ter(tecNO_PERMISSION)); + jt[sfMessageKey] = strHex(rkp.first.slice()); + env(jt, ter(tecNO_DELEGATE_PERMISSION)); // alice give granular permission of AccountMessageKeySet to bob env(delegate::set( @@ -987,12 +1036,14 @@ class Delegate_test : public beast::unit_test::suite BEAST_EXPECT( strHex((*env.le(alice))[sfMessageKey]) == strHex(rkp.first.slice())); - jt[sfMessageKey.fieldName] = ""; + jt[sfMessageKey] = ""; env(jt); BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfMessageKey)); // bob does not have permission to set transfer rate for alice - env(rate(alice, 2.0), delegate::as(bob), ter(tecNO_PERMISSION)); + env(rate(alice, 2.0), + delegate::as(bob), + ter(tecNO_DELEGATE_PERMISSION)); // alice give granular permission of AccountTransferRateSet to bob env(delegate::set( @@ -1004,14 +1055,13 @@ class Delegate_test : public beast::unit_test::suite "AccountTransferRateSet"})); env.close(); auto jtRate = rate(alice, 2.0); - jtRate[sfDelegate.fieldName] = bob.human(); - jtRate[sfFlags.fieldName] = tfFullyCanonicalSig; + jtRate[sfDelegate] = bob.human(); env(jtRate, delegate::as(bob)); BEAST_EXPECT((*env.le(alice))[sfTransferRate] == 2000000000); // bob does not have permission to set ticksize for alice - jt[sfTickSize.fieldName] = 8; - env(jt, ter(tecNO_PERMISSION)); + jt[sfTickSize] = 8; + env(jt, ter(tecNO_DELEGATE_PERMISSION)); // alice give granular permission of AccountTickSizeSet to bob env(delegate::set( @@ -1029,7 +1079,7 @@ class Delegate_test : public beast::unit_test::suite // can not set asfRequireAuth flag for alice env(fset(alice, asfRequireAuth), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); // reset Delegate will delete the Delegate // object @@ -1038,15 +1088,15 @@ class Delegate_test : public beast::unit_test::suite // alice env(fset(alice, asfRequireAuth), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); // alice can set for herself env(fset(alice, asfRequireAuth)); env.require(flags(alice, asfRequireAuth)); env.close(); // can not update tick size because bob no longer has permission - jt[sfTickSize.fieldName] = 7; - env(jt, ter(tecNO_PERMISSION)); + jt[sfTickSize] = 7; + env(jt, ter(tecNO_DELEGATE_PERMISSION)); env(delegate::set( alice, @@ -1060,12 +1110,11 @@ class Delegate_test : public beast::unit_test::suite std::string const locator = "9633EC8AF54F16B5286DB1D7B519EF49EEFC050C0C8AC4384F1D88ACD1BFDF" "05"; - auto jt2 = noop(alice); - jt2[sfDomain.fieldName] = strHex(domain); - jt2[sfDelegate.fieldName] = bob.human(); - jt2[sfWalletLocator.fieldName] = locator; - jt2[sfFlags.fieldName] = tfFullyCanonicalSig; - env(jt2, ter(tecNO_PERMISSION)); + auto jv2 = noop(alice); + jv2[sfDomain] = strHex(domain); + jv2[sfDelegate] = bob.human(); + jv2[sfWalletLocator] = locator; + env(jv2, ter(tecNO_DELEGATE_PERMISSION)); } // can not set AccountSet flags on behalf of other account @@ -1080,7 +1129,7 @@ class Delegate_test : public beast::unit_test::suite // bob can not set flag on behalf of alice env(fset(alice, flag), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); // alice set by herself env(fset(alice, flag)); env.close(); @@ -1088,7 +1137,7 @@ class Delegate_test : public beast::unit_test::suite // bob can not clear on behalf of alice env(fclear(alice, flag), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); }; // testSetClearFlag(asfNoFreeze); @@ -1117,19 +1166,19 @@ class Delegate_test : public beast::unit_test::suite // bob can not set asfAccountTxnID on behalf of alice env(fset(alice, asfAccountTxnID), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env(fset(alice, asfAccountTxnID)); env.close(); BEAST_EXPECT(env.le(alice)->isFieldPresent(sfAccountTxnID)); env(fclear(alice, asfAccountTxnID), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); // bob can not set asfAuthorizedNFTokenMinter on behalf of alice Json::Value jt = fset(alice, asfAuthorizedNFTokenMinter); - jt[sfDelegate.fieldName] = bob.human(); - jt[sfNFTokenMinter.fieldName] = bob.human(); - env(jt, ter(tecNO_PERMISSION)); + jt[sfDelegate] = bob.human(); + jt[sfNFTokenMinter] = bob.human(); + env(jt, ter(tecNO_DELEGATE_PERMISSION)); // bob gives alice some permissions env(delegate::set( @@ -1145,14 +1194,14 @@ class Delegate_test : public beast::unit_test::suite // behalf of bob. env(fset(alice, asfNoFreeze), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); env(fset(bob, asfNoFreeze)); env.close(); env.require(flags(bob, asfNoFreeze)); // alice can not clear on behalf of bob env(fclear(alice, asfNoFreeze), delegate::as(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); // bob can not set asfDisableMaster on behalf of alice Account const bobKey{"bobKey", KeyType::secp256k1}; @@ -1161,7 +1210,76 @@ class Delegate_test : public beast::unit_test::suite env(fset(alice, asfDisableMaster), delegate::as(bob), sig(bob), - ter(tecNO_PERMISSION)); + ter(tecNO_DELEGATE_PERMISSION)); + } + + // tfFullyCanonicalSig won't block delegated transaction + { + Env env(*this); + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(10000), alice, bob); + env.close(); + + env(delegate::set( + alice, bob, {"AccountDomainSet", "AccountEmailHashSet"})); + env.close(); + + std::string const domain = "example.com"; + auto jt = noop(alice); + jt[sfDomain] = strHex(domain); + jt[sfDelegate] = bob.human(); + jt[sfFlags] = tfFullyCanonicalSig; + + env(jt); + BEAST_EXPECT((*env.le(alice))[sfDomain] == makeSlice(domain)); + } + + // tfInnerBatchTxn won't block delegated transaction + { + Env env(*this); + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(10000), alice, bob); + env.close(); + + env(delegate::set( + alice, bob, {"AccountDomainSet", "AccountEmailHashSet"})); + env.close(); + + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 3); + + auto jv1 = noop(alice); + std::string const domain1 = "example1.com"; + jv1[sfDomain] = strHex(domain1); + jv1[sfDelegate] = bob.human(); + jv1[sfSequence] = seq + 1; + + auto jv2 = noop(alice); + std::string const domain2 = "example2.com"; + jv2[sfDomain] = strHex(domain2); + jv2[sfDelegate] = bob.human(); + jv2[sfSequence] = seq + 2; + + // bob set domain back and add email hash for alice + auto jv3 = noop(alice); + std::string const mh("5F31A79367DC3137FADA860C05742EE6"); + jv3[sfDomain] = strHex(domain1); + jv3[sfEmailHash] = mh; + jv3[sfDelegate] = bob.human(); + jv3[sfSequence] = seq + 3; + + // batch::inner will set tfInnerBatchTxn, this should not + // block delegated transaction + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(jv1, seq + 1), + batch::inner(jv2, seq + 2), + batch::inner(jv3, seq + 3)); + env.close(); + + BEAST_EXPECT((*env.le(alice))[sfDomain] == makeSlice(domain1)); + BEAST_EXPECT(to_string((*env.le(alice))[sfEmailHash]) == mh); } } @@ -1189,7 +1307,7 @@ class Delegate_test : public beast::unit_test::suite {.account = alice, .flags = tfMPTLock, .delegate = bob, - .err = tecNO_PERMISSION}); + .err = tecNO_DELEGATE_PERMISSION}); // alice gives granular permission to bob of MPTokenIssuanceUnlock env(delegate::set(alice, bob, {"MPTokenIssuanceUnlock"})); @@ -1199,7 +1317,7 @@ class Delegate_test : public beast::unit_test::suite {.account = alice, .flags = tfMPTLock, .delegate = bob, - .err = tecNO_PERMISSION}); + .err = tecNO_DELEGATE_PERMISSION}); // bob now has lock permission, but does not have unlock permission env(delegate::set(alice, bob, {"MPTokenIssuanceLock"})); env.close(); @@ -1208,7 +1326,7 @@ class Delegate_test : public beast::unit_test::suite {.account = alice, .flags = tfMPTUnlock, .delegate = bob, - .err = tecNO_PERMISSION}); + .err = tecNO_DELEGATE_PERMISSION}); // now bob can lock and unlock env(delegate::set( @@ -1241,7 +1359,7 @@ class Delegate_test : public beast::unit_test::suite {.account = alice, .flags = tfMPTUnlock, .delegate = bob, - .err = tecNO_PERMISSION}); + .err = tecNO_DELEGATE_PERMISSION}); // alice gives bob some unrelated permission with // MPTokenIssuanceLock @@ -1255,7 +1373,7 @@ class Delegate_test : public beast::unit_test::suite {.account = alice, .flags = tfMPTUnlock, .delegate = bob, - .err = tecNO_PERMISSION}); + .err = tecNO_DELEGATE_PERMISSION}); // alice add MPTokenIssuanceSet to permissions env(delegate::set( @@ -1271,6 +1389,74 @@ class Delegate_test : public beast::unit_test::suite mpt.set({.account = alice, .flags = tfMPTUnlock, .delegate = bob}); mpt.set({.account = alice, .flags = tfMPTLock, .delegate = bob}); } + + // tfFullyCanonicalSig won't block delegated transaction + { + Env env(*this); + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(100000), alice, bob); + env.close(); + + MPTTester mpt(env, alice, {.fund = false}); + env.close(); + mpt.create({.flags = tfMPTCanLock}); + env.close(); + + // alice gives granular permission to bob of MPTokenIssuanceLock + env(delegate::set(alice, bob, {"MPTokenIssuanceLock"})); + env.close(); + mpt.set( + {.account = alice, + .flags = tfMPTLock | tfFullyCanonicalSig, + .delegate = bob}); + } + + // tfInnerBatchTxn won't block delegated transaction + { + Env env(*this); + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(100000), alice, bob); + env.close(); + + auto const mptID = makeMptID(env.seq(alice), alice); + MPTTester mpt(env, alice, {.fund = false}); + env.close(); + mpt.create({.flags = tfMPTCanLock}); + env.close(); + + // alice gives granular permission to bob of MPTokenIssuanceLock + env(delegate::set( + alice, bob, {"MPTokenIssuanceLock", "MPTokenIssuanceUnlock"})); + env.close(); + + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + + Json::Value jv1; + jv1[sfTransactionType] = jss::MPTokenIssuanceSet; + jv1[sfAccount] = alice.human(); + jv1[sfDelegate] = bob.human(); + jv1[sfSequence] = seq + 1; + jv1[sfMPTokenIssuanceID] = to_string(mptID); + jv1[sfFlags] = tfMPTLock; + + Json::Value jv2; + jv2[sfTransactionType] = jss::MPTokenIssuanceSet; + jv2[sfAccount] = alice.human(); + jv2[sfDelegate] = bob.human(); + jv2[sfSequence] = seq + 2; + jv2[sfMPTokenIssuanceID] = to_string(mptID); + jv2[sfFlags] = tfMPTUnlock; + + // batch::inner will set tfInnerBatchTxn, this should not + // block delegated transaction + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(jv1, seq + 1), + batch::inner(jv2, seq + 2)); + env.close(); + } } void diff --git a/src/test/app/DepositAuth_test.cpp b/src/test/app/DepositAuth_test.cpp index c8dc3c00eb..6f314e3a79 100644 --- a/src/test/app/DepositAuth_test.cpp +++ b/src/test/app/DepositAuth_test.cpp @@ -714,12 +714,12 @@ struct DepositPreauth_test : public beast::unit_test::suite if (!supportsPreauth) { auto const seq1 = env.seq(alice); - env(escrow(alice, becky, XRP(100)), - finish_time(env.now() + 1s)); + env(escrow::create(alice, becky, XRP(100)), + escrow::finish_time(env.now() + 1s)); env.close(); // Failed as rule is disabled - env(finish(gw, alice, seq1), + env(escrow::finish(gw, alice, seq1), fee(1500), ter(tecNO_PERMISSION)); env.close(); @@ -1387,12 +1387,13 @@ struct DepositPreauth_test : public beast::unit_test::suite env.close(); auto const seq = env.seq(alice); - env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 1s)); + env(escrow::create(alice, bob, XRP(1000)), + escrow::finish_time(env.now() + 1s)); env.close(); // zelda can't finish escrow with invalid credentials { - env(finish(zelda, alice, seq), + env(escrow::finish(zelda, alice, seq), credentials::ids({}), ter(temMALFORMED)); env.close(); @@ -1404,14 +1405,14 @@ struct DepositPreauth_test : public beast::unit_test::suite "0E0B04ED60588A758B67E21FBBE95AC5A63598BA951761DC0EC9C08D7E" "01E034"; - env(finish(zelda, alice, seq), + env(escrow::finish(zelda, alice, seq), credentials::ids({invalidIdx}), ter(tecBAD_CREDENTIALS)); env.close(); } { // Ledger closed, time increased, zelda can't finish escrow - env(finish(zelda, alice, seq), + env(escrow::finish(zelda, alice, seq), credentials::ids({credIdx}), fee(1500), ter(tecEXPIRED)); diff --git a/src/test/app/EscrowToken_test.cpp b/src/test/app/EscrowToken_test.cpp new file mode 100644 index 0000000000..da9610f0c3 --- /dev/null +++ b/src/test/app/EscrowToken_test.cpp @@ -0,0 +1,3736 @@ +//------------------------------------------------------------------------------ +/* + 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 +{ + static uint64_t + mptEscrowed( + jtx::Env const& env, + jtx::Account const& account, + jtx::MPT const& mpt) + { + auto const sle = env.le(keylet::mptoken(mpt.mpt(), account)); + if (sle && sle->isFieldPresent(sfLockedAmount)) + return (*sle)[sfLockedAmount]; + return 0; + } + + static uint64_t + issuerMPTEscrowed(jtx::Env const& env, jtx::MPT const& mpt) + { + auto const sle = env.le(keylet::mptIssuance(mpt.mpt())); + if (sle && sle->isFieldPresent(sfLockedAmount)) + return (*sle)[sfLockedAmount]; + return 0; + } + + void + issuerIOUEscrowed( + jtx::Env& env, + jtx::Account const& account, + Currency const& currency, + int const& outstanding, + int const& locked) + { + Json::Value params; + params[jss::account] = account.human(); + auto jrr = env.rpc("json", "gateway_balances", to_string(params)); + auto const result = jrr[jss::result]; + auto const actualOutstanding = + result[jss::obligations][to_string(currency)]; + BEAST_EXPECT(actualOutstanding == to_string(outstanding)); + if (locked != 0) + { + auto const actualEscrowed = + result[jss::locked][to_string(currency)]; + BEAST_EXPECT(actualEscrowed == to_string(locked)); + } + } + + void + testIOUEnablement(FeatureBitset features) + { + testcase("IOU Enablement"); + + using namespace jtx; + using namespace std::chrono; + + for (bool const withTokenEscrow : {false, true}) + { + auto const amend = + withTokenEscrow ? features : features - featureTokenEscrow; + Env env{*this, amend}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + auto const createResult = + withTokenEscrow ? ter(tesSUCCESS) : ter(temBAD_AMOUNT); + auto const finishResult = + withTokenEscrow ? ter(tesSUCCESS) : ter(tecNO_TARGET); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, USD(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + createResult); + env.close(); + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + finishResult); + env.close(); + + auto const seq2 = env.seq(alice); + env(escrow::create(alice, bob, USD(1'000)), + escrow::condition(escrow::cb2), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + fee(baseFee * 150), + createResult); + env.close(); + env(escrow::cancel(bob, alice, seq2), finishResult); + env.close(); + } + } + + void + testIOUAllowLockingFlag(FeatureBitset features) + { + testcase("IOU Allow Locking Flag"); + + using namespace jtx; + using namespace std::chrono; + + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + // Create Escrow #1 & #2 + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, USD(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + auto const seq2 = env.seq(alice); + env(escrow::create(alice, bob, USD(1'000)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 3s), + fee(baseFee), + ter(tesSUCCESS)); + env.close(); + + // Clear the asfAllowTrustLineLocking flag + env(fclear(gw, asfAllowTrustLineLocking)); + env.close(); + env.require(nflags(gw, asfAllowTrustLineLocking)); + + // Cannot Create Escrow without asfAllowTrustLineLocking + env(escrow::create(alice, bob, USD(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_PERMISSION)); + env.close(); + + // Can finish the escrow created before the flag was cleared + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + // Can cancel the escrow created before the flag was cleared + env(escrow::cancel(bob, alice, seq2), ter(tesSUCCESS)); + env.close(); + } + + void + testIOUCreatePreflight(FeatureBitset features) + { + testcase("IOU Create Preflight"); + using namespace test::jtx; + using namespace std::literals; + + // temBAD_FEE: Exercises invalid preflight1. + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + + env(escrow::create(alice, bob, USD(1)), + escrow::finish_time(env.now() + 1s), + fee(XRP(-1)), + ter(temBAD_FEE)); + env.close(); + } + + // temBAD_AMOUNT: amount <= 0 + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + + env(escrow::create(alice, bob, USD(-1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(temBAD_AMOUNT)); + env.close(); + } + + // temBAD_CURRENCY: badCurrency() == amount.getCurrency() + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const BAD = IOU(gw, badCurrency()); + env.fund(XRP(5000), alice, bob, gw); + + env(escrow::create(alice, bob, BAD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(temBAD_CURRENCY)); + env.close(); + } + } + + void + testIOUCreatePreclaim(FeatureBitset features) + { + testcase("IOU Create Preclaim"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_PERMISSION: issuer is the same as the account + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + + env(escrow::create(gw, alice, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_PERMISSION)); + env.close(); + } + + // tecNO_ISSUER: Issuer does not exist + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob); + env.close(); + env.memoize(gw); + + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_ISSUER)); + env.close(); + } + + // tecNO_PERMISSION: asfAllowTrustLineLocking is not set + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + env(escrow::create(gw, alice, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_PERMISSION)); + env.close(); + } + + // tecNO_LINE: account does not have a trustline to the issuer + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_LINE)); + env.close(); + } + + // tecNO_PERMISSION: Not testable + // tecNO_PERMISSION: Not testable + // tecNO_AUTH: requireAuth + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(fset(gw, asfRequireAuth)); + env.close(); + env.trust(USD(10'000), alice, bob); + env.close(); + + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_AUTH)); + env.close(); + } + + // tecNO_AUTH: requireAuth + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + auto const aliceUSD = alice["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(gw, aliceUSD(10'000)), txflags(tfSetfAuth)); + env.trust(USD(10'000), alice, bob); + env.close(); + + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_AUTH)); + env.close(); + } + + // tecFROZEN: account is frozen + { + // Env Setup + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, USD(100'000))); + env(trust(bob, USD(100'000))); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // set freeze on alice trustline + env(trust(gw, USD(10'000), alice, tfSetFreeze)); + env.close(); + + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecFROZEN)); + env.close(); + } + + // tecFROZEN: dest is frozen + { + // Env Setup + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, USD(100'000))); + env(trust(bob, USD(100'000))); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // set freeze on bob trustline + env(trust(gw, USD(10'000), bob, tfSetFreeze)); + env.close(); + + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecFROZEN)); + env.close(); + } + + // tecINSUFFICIENT_FUNDS + { + // Env Setup + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, USD(100'000))); + env(trust(bob, USD(100'000))); + env.close(); + + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); + } + + // tecINSUFFICIENT_FUNDS + { + // Env Setup + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, USD(100'000))); + env(trust(bob, USD(100'000))); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + env(escrow::create(alice, bob, USD(10'001)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); + } + + // tecPRECISION_LOSS + { + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100000000000000000), alice); + env.trust(USD(100000000000000000), bob); + env.close(); + env(pay(gw, alice, USD(10000000000000000))); + env(pay(gw, bob, USD(1))); + env.close(); + + // alice cannot create escrow for 1/10 iou - precision loss + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecPRECISION_LOSS)); + env.close(); + } + } + + void + testIOUFinishPreclaim(FeatureBitset features) + { + testcase("IOU Finish Preclaim"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_AUTH: requireAuth set: dest not authorized + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + auto const aliceUSD = alice["USD"]; + auto const bobUSD = bob["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(gw, aliceUSD(10'000)), txflags(tfSetfAuth)); + env(trust(gw, bobUSD(10'000)), txflags(tfSetfAuth)); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env(pay(bob, gw, USD(10'000))); + env(trust(gw, bobUSD(0)), txflags(tfSetfAuth)); + env(trust(bob, USD(0))); + env.close(); + + env.trust(USD(10'000), bob); + env.close(); + + // bob cannot finish because he is not authorized + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecNO_AUTH)); + env.close(); + } + + // tecFROZEN: issuer has deep frozen the dest + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + // set freeze on bob trustline + env(trust(gw, USD(10'000), bob, tfSetFreeze | tfSetDeepFreeze)); + + // bob cannot finish because of deep freeze + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecFROZEN)); + env.close(); + } + } + + void + testIOUFinishDoApply(FeatureBitset features) + { + testcase("IOU Finish Do Apply"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_LINE_INSUF_RESERVE: insufficient reserve to create line + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const acctReserve = env.current()->fees().accountReserve(0); + auto const incReserve = env.current()->fees().increment; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, gw); + env.fund(acctReserve + (incReserve - 1), bob); + env.close(); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice); + env.close(); + env(pay(gw, alice, USD(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + // bob cannot finish because insufficient reserve to create line + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecNO_LINE_INSUF_RESERVE)); + env.close(); + } + + // tecNO_LINE: alice submits; finish IOU not created + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice); + env.close(); + env(pay(gw, alice, USD(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + // alice cannot finish because bob does not have a trustline + env(escrow::finish(alice, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecNO_LINE)); + env.close(); + } + + // tecLIMIT_EXCEEDED: alice submits; IOU Limit < balance + amount + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(1000), alice, bob); + env.close(); + env(pay(gw, alice, USD(1000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, USD(5)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env.trust(USD(1), bob); + env.close(); + + // alice cannot finish because bobs limit is too low + env(escrow::finish(alice, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecLIMIT_EXCEEDED)); + env.close(); + } + + // tesSUCCESS: bob submits; IOU Limit < balance + amount + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(1000), alice, bob); + env.close(); + env(pay(gw, alice, USD(1000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, USD(5)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env.trust(USD(1), bob); + env.close(); + + // bob can finish even if bobs limit is too low + auto const bobPreLimit = env.limit(bob, USD); + + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + // bobs limit is not changed + BEAST_EXPECT(env.limit(bob, USD) == bobPreLimit); + } + } + + void + testIOUCancelPreclaim(FeatureBitset features) + { + testcase("IOU Cancel Preclaim"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_AUTH: requireAuth set: account not authorized + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + auto const aliceUSD = alice["USD"]; + auto const bobUSD = bob["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(gw, aliceUSD(10'000)), txflags(tfSetfAuth)); + env(trust(gw, bobUSD(10'000)), txflags(tfSetfAuth)); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, USD(1)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + fee(baseFee), + ter(tesSUCCESS)); + env.close(); + + env(pay(alice, gw, USD(9'999))); + env(trust(gw, aliceUSD(0)), txflags(tfSetfAuth)); + env(trust(alice, USD(0))); + env.close(); + + env.trust(USD(10'000), alice); + env.close(); + + // alice cannot cancel because she is not authorized + env(escrow::cancel(bob, alice, seq1), + fee(baseFee), + ter(tecNO_AUTH)); + env.close(); + } + } + + void + testIOUBalances(FeatureBitset features) + { + testcase("IOU Balances"); + + using namespace jtx; + using namespace std::chrono; + + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, USD(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + auto const seq2 = env.seq(alice); + env(escrow::create(alice, bob, USD(1'000)), + escrow::condition(escrow::cb2), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + env(escrow::cancel(bob, alice, seq2), ter(tesSUCCESS)); + env.close(); + } + + void + testIOUMetaAndOwnership(FeatureBitset features) + { + using namespace jtx; + using namespace std::chrono; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + { + testcase("IOU Metadata to self"); + + Env env{*this, features}; + env.fund(XRP(5000), alice, bob, carol, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, bob, carol); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env(pay(gw, carol, USD(5000))); + env.close(); + auto const aseq = env.seq(alice); + auto const bseq = env.seq(bob); + + env(escrow::create(alice, alice, USD(1'000)), + escrow::finish_time(env.now() + 1s), + escrow::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::create(bob, bob, USD(1'000)), + escrow::finish_time(env.now() + 1s), + escrow::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(escrow::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(escrow::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, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, bob, carol); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, bob, USD(5000))); + env(pay(gw, carol, USD(5000))); + env.close(); + auto const aseq = env.seq(alice); + auto const bseq = env.seq(bob); + + env(escrow::create(alice, bob, USD(1'000)), + escrow::finish_time(env.now() + 1s)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + env(escrow::create(bob, carol, USD(1'000)), + escrow::finish_time(env.now() + 1s), + escrow::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(escrow::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(escrow::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, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, carol); + env.close(); + env(pay(gw, alice, USD(5000))); + env(pay(gw, carol, USD(5000))); + env.close(); + auto const aseq = env.seq(alice); + + env(escrow::create(alice, gw, USD(1'000)), + escrow::finish_time(env.now() + 1s)); + + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + env(escrow::create(gw, carol, USD(1'000)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + ter(tecNO_PERMISSION)); + env.close(5s); + + auto const ag = env.le(keylet::escrow(alice.id(), aseq)); + BEAST_EXPECT(ag); + + { + 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()) == 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(), ag) != iod.end()); + } + + env.close(5s); + env(escrow::finish(alice, alice, aseq)); + { + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); + + 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()); + } + } + } + + 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 baseFee = env.current()->fees().base; + auto const USD = t.gw["USD"]; + env.fund(XRP(5000), t.src, t.dst, t.gw); + env(fset(t.gw, asfAllowTrustLineLocking)); + env.close(); + + if (t.hasTrustline) + env.trust(USD(100'000), t.src, t.dst); + else + env.trust(USD(100'000), t.src); + env.close(); + + env(pay(t.gw, t.src, USD(10'000))); + if (t.hasTrustline) + env(pay(t.gw, t.dst, USD(10'000))); + env.close(); + + // src can create escrow + auto const seq1 = env.seq(t.src); + auto const delta = USD(1'000); + env(escrow::create(t.src, t.dst, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // dst can finish escrow + auto const preSrc = env.balance(t.src, USD); + auto const preDst = env.balance(t.dst, USD); + + env(escrow::finish(t.dst, t.src, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(env.balance(t.src, USD) == preSrc); + BEAST_EXPECT(env.balance(t.dst, USD) == preDst + delta); + } + } + + void + testIOUGateway(FeatureBitset features) + { + testcase("IOU Gateway"); + using namespace test::jtx; + using namespace std::literals; + + struct TestAccountData + { + Account src; + Account dst; + bool hasTrustline; + }; + + // issuer is source + { + auto const gw = Account{"gateway"}; + auto const alice = Account{"alice"}; + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const USD = gw["USD"]; + env.fund(XRP(5000), alice, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100'000), alice); + env.close(); + + env(pay(gw, alice, USD(10'000))); + env.close(); + + // issuer cannot create escrow + env(escrow::create(gw, alice, USD(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_PERMISSION)); + env.close(); + } + + std::array gwDstTests = {{ + // src > dst && src > issuer && dst has trustline + {Account("alice2"), Account{"gw0"}, true}, + // src < dst && src < issuer && dst has trustline + {Account("carol0"), Account{"gw1"}, true}, + // dst > src && dst > issuer && dst has trustline + {Account("dan1"), Account{"gw0"}, true}, + // dst < src && dst < issuer && dst has trustline + {Account("bob0"), Account{"gw1"}, true}, + }}; + + // issuer is destination + for (auto const& t : gwDstTests) + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const USD = t.dst["USD"]; + env.fund(XRP(5000), t.dst, t.src); + env(fset(t.dst, asfAllowTrustLineLocking)); + env.close(); + + env.trust(USD(100'000), t.src); + env.close(); + + env(pay(t.dst, t.src, USD(10'000))); + env.close(); + + // issuer can receive escrow + auto const seq1 = env.seq(t.src); + auto const preSrc = env.balance(t.src, USD); + env(escrow::create(t.src, t.dst, USD(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // issuer can finish escrow, no dest trustline + env(escrow::finish(t.dst, t.src, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + auto const preAmount = 10'000; + BEAST_EXPECT(preSrc == USD(preAmount)); + auto const postAmount = 9000; + BEAST_EXPECT(env.balance(t.src, USD) == USD(postAmount)); + BEAST_EXPECT(env.balance(t.dst, USD) == USD(0)); + } + + // issuer is source and destination + { + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(5000), gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + + // issuer cannot receive escrow + env(escrow::create(gw, gw, USD(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_PERMISSION)); + env.close(); + } + } + + void + testIOULockedRate(FeatureBitset features) + { + testcase("IOU Locked Rate"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + // test locked rate + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(rate(gw, 1.25)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // alice can create escrow w/ xfer rate + auto const preAlice = env.balance(alice, USD); + auto const seq1 = env.seq(alice); + auto const delta = USD(125); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + auto const transferRate = escrow::rate(env, alice, seq1); + BEAST_EXPECT( + transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // bob can finish escrow + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == preAlice - delta); + BEAST_EXPECT(env.balance(bob, USD) == USD(10'100)); + } + // test rate change - higher + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(rate(gw, 1.25)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // alice can create escrow w/ xfer rate + auto const preAlice = env.balance(alice, USD); + auto const seq1 = env.seq(alice); + auto const delta = USD(125); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + auto transferRate = escrow::rate(env, alice, seq1); + BEAST_EXPECT( + transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // issuer changes rate higher + env(rate(gw, 1.26)); + env.close(); + + // bob can finish escrow - rate unchanged + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == preAlice - delta); + BEAST_EXPECT(env.balance(bob, USD) == USD(10'100)); + } + + // test rate change - lower + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(rate(gw, 1.25)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // alice can create escrow w/ xfer rate + auto const preAlice = env.balance(alice, USD); + auto const seq1 = env.seq(alice); + auto const delta = USD(125); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + auto transferRate = escrow::rate(env, alice, seq1); + BEAST_EXPECT( + transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // issuer changes rate lower + env(rate(gw, 1.00)); + env.close(); + + // bob can finish escrow - rate changed + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == preAlice - delta); + BEAST_EXPECT(env.balance(bob, USD) == USD(10125)); + } + + // test cancel doesnt charge rate + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(rate(gw, 1.25)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // alice can create escrow w/ xfer rate + auto const preAlice = env.balance(alice, USD); + auto const seq1 = env.seq(alice); + auto const delta = USD(125); + env(escrow::create(alice, bob, delta), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 3s), + fee(baseFee)); + env.close(); + auto transferRate = escrow::rate(env, alice, seq1); + BEAST_EXPECT( + transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // issuer changes rate lower + env(rate(gw, 1.00)); + env.close(); + + // alice can cancel escrow - rate is not charged + env(escrow::cancel(alice, alice, seq1), fee(baseFee)); + env.close(); + + BEAST_EXPECT(env.balance(alice, USD) == preAlice); + BEAST_EXPECT(env.balance(bob, USD) == USD(10000)); + } + } + + void + testIOULimitAmount(FeatureBitset features) + { + testcase("IOU Limit"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + // test LimitAmount + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(1'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(10'000), alice, bob); + env.close(); + env(pay(gw, alice, USD(1'000))); + env(pay(gw, bob, USD(1'000))); + env.close(); + + // alice can create escrow + auto seq1 = env.seq(alice); + auto const delta = USD(125); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // bob can finish + auto const preBobLimit = env.limit(bob, USD); + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + auto const postBobLimit = env.limit(bob, USD); + // bobs limit is NOT changed + BEAST_EXPECT(postBobLimit == preBobLimit); + } + } + + void + testIOURequireAuth(FeatureBitset features) + { + testcase("IOU Require Auth"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + auto const aliceUSD = alice["USD"]; + auto const bobUSD = bob["USD"]; + + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(1'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(gw, aliceUSD(10'000)), txflags(tfSetfAuth)); + env(trust(alice, USD(10'000))); + env(trust(bob, USD(10'000))); + env.close(); + env(pay(gw, alice, USD(1'000))); + env.close(); + + // alice cannot create escrow - fails without auth + auto seq1 = env.seq(alice); + auto const delta = USD(125); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_AUTH)); + env.close(); + + // set auth on bob + env(trust(gw, bobUSD(10'000)), txflags(tfSetfAuth)); + env(trust(bob, USD(10'000))); + env.close(); + env(pay(gw, bob, USD(1'000))); + env.close(); + + // alice can create escrow - bob has auth + seq1 = env.seq(alice); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // bob can finish + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + } + + void + testIOUFreeze(FeatureBitset features) + { + testcase("IOU Freeze"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + // test Global Freeze + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + env(fset(gw, asfGlobalFreeze)); + env.close(); + + // setup transaction + auto seq1 = env.seq(alice); + auto const delta = USD(125); + + // create escrow fails - frozen trustline + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecFROZEN)); + env.close(); + + // clear global freeze + env(fclear(gw, asfGlobalFreeze)); + env.close(); + + // create escrow success + seq1 = env.seq(alice); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // set global freeze + env(fset(gw, asfGlobalFreeze)); + env.close(); + + // bob finish escrow success regardless of frozen assets + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + + // clear global freeze + env(fclear(gw, asfGlobalFreeze)); + env.close(); + + // create escrow success + seq1 = env.seq(alice); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::cancel_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // set global freeze + env(fset(gw, asfGlobalFreeze)); + env.close(); + + // bob cancel escrow success regardless of frozen assets + env(escrow::cancel(bob, alice, seq1), fee(baseFee)); + env.close(); + } + + // test Individual Freeze + { + // Env Setup + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, USD(100'000))); + env(trust(bob, USD(100'000))); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // set freeze on alice trustline + env(trust(gw, USD(10'000), alice, tfSetFreeze)); + env.close(); + + // setup transaction + auto seq1 = env.seq(alice); + auto const delta = USD(125); + + // create escrow fails - frozen trustline + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecFROZEN)); + env.close(); + + // clear freeze on alice trustline + env(trust(gw, USD(10'000), alice, tfClearFreeze)); + env.close(); + + // create escrow success + seq1 = env.seq(alice); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // set freeze on bob trustline + env(trust(gw, USD(10'000), bob, tfSetFreeze)); + env.close(); + + // bob finish escrow success regardless of frozen assets + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + + // reset freeze on bob and alice trustline + env(trust(gw, USD(10'000), alice, tfClearFreeze)); + env(trust(gw, USD(10'000), bob, tfClearFreeze)); + env.close(); + + // create escrow success + seq1 = env.seq(alice); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::cancel_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // set freeze on bob trustline + env(trust(gw, USD(10'000), bob, tfSetFreeze)); + env.close(); + + // bob cancel escrow success regardless of frozen assets + env(escrow::cancel(bob, alice, seq1), fee(baseFee)); + env.close(); + } + + // test Deep Freeze + { + // Env Setup + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env(trust(alice, USD(100'000))); + env(trust(bob, USD(100'000))); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // set freeze on alice trustline + env(trust(gw, USD(10'000), alice, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // setup transaction + auto seq1 = env.seq(alice); + auto const delta = USD(125); + + // create escrow fails - frozen trustline + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecFROZEN)); + env.close(); + + // clear freeze on alice trustline + env(trust( + gw, USD(10'000), alice, tfClearFreeze | tfClearDeepFreeze)); + env.close(); + + // create escrow success + seq1 = env.seq(alice); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // set freeze on bob trustline + env(trust(gw, USD(10'000), bob, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // bob finish escrow fails because of deep frozen assets + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecFROZEN)); + env.close(); + + // reset freeze on alice and bob trustline + env(trust( + gw, USD(10'000), alice, tfClearFreeze | tfClearDeepFreeze)); + env(trust(gw, USD(10'000), bob, tfClearFreeze | tfClearDeepFreeze)); + env.close(); + + // create escrow success + seq1 = env.seq(alice); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::cancel_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // set freeze on bob trustline + env(trust(gw, USD(10'000), bob, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // bob cancel escrow fails because of deep frozen assets + env(escrow::cancel(bob, alice, seq1), + fee(baseFee), + ter(tesSUCCESS)); + env.close(); + } + } + void + testIOUINSF(FeatureBitset features) + { + testcase("IOU 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 10'000, escrow 1'000 then try to pay 10'000 + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + // create escrow success + auto const delta = USD(1'000); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + env(pay(alice, gw, USD(10'000)), ter(tecPATH_PARTIAL)); + } + { + // test tecINSUFFICIENT_FUNDS + // ie. has 10'000 escrow 1'000 then try to escrow 10'000 + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100'000), alice); + env.trust(USD(100'000), bob); + env.close(); + env(pay(gw, alice, USD(10'000))); + env(pay(gw, bob, USD(10'000))); + env.close(); + + auto const delta = USD(1'000); + env(escrow::create(alice, bob, delta), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + env(escrow::create(alice, bob, USD(10'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); + } + } + + void + testIOUPrecisionLoss(FeatureBitset features) + { + testcase("IOU Precision Loss"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + // test min create precision loss + { + Env env(*this, features); + auto const baseFee = env.current()->fees().base; + env.fund(XRP(10'000), alice, bob, gw); + env(fset(gw, asfAllowTrustLineLocking)); + env.close(); + env.trust(USD(100000000000000000), alice); + env.trust(USD(100000000000000000), bob); + env.close(); + env(pay(gw, alice, USD(10000000000000000))); + env(pay(gw, bob, USD(1))); + env.close(); + + // alice cannot create escrow for 1/10 iou - precision loss + env(escrow::create(alice, bob, USD(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecPRECISION_LOSS)); + env.close(); + + auto const seq1 = env.seq(alice); + // alice can create escrow for 1'000 iou + env(escrow::create(alice, bob, USD(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // bob finish escrow success + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + } + } + + void + testMPTEnablement(FeatureBitset features) + { + testcase("MPT Enablement"); + + using namespace jtx; + using namespace std::chrono; + + for (bool const withTokenEscrow : {false, true}) + { + auto const amend = + withTokenEscrow ? features : features - featureTokenEscrow; + Env env{*this, amend}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(5000), bob); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + auto const createResult = + withTokenEscrow ? ter(tesSUCCESS) : ter(temBAD_AMOUNT); + auto const finishResult = + withTokenEscrow ? ter(tesSUCCESS) : ter(tecNO_TARGET); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + createResult); + env.close(); + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + finishResult); + env.close(); + auto const seq2 = env.seq(alice); + env(escrow::create(alice, bob, MPT(1'000)), + escrow::condition(escrow::cb2), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + fee(baseFee * 150), + createResult); + env.close(); + env(escrow::cancel(bob, alice, seq2), finishResult); + env.close(); + } + } + + void + testMPTCreatePreflight(FeatureBitset features) + { + testcase("MPT Create Preflight"); + using namespace test::jtx; + using namespace std::literals; + + for (bool const withMPT : {true, false}) + { + auto const amend = + withMPT ? features : features - featureMPTokensV1; + Env env{*this, amend}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(1'000), alice, bob, gw); + + Json::Value jv = escrow::create(alice, bob, XRP(1)); + jv.removeMember(jss::Amount); + jv[jss::Amount][jss::mpt_issuance_id] = + "00000004A407AF5856CCF3C42619DAA925813FC955C72983"; + jv[jss::Amount][jss::value] = "-1"; + + auto const result = withMPT ? ter(temBAD_AMOUNT) : ter(temDISABLED); + env(jv, + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + result); + env.close(); + } + + // temBAD_AMOUNT: amount < 0 + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + env(escrow::create(alice, bob, MPT(-1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(temBAD_AMOUNT)); + env.close(); + } + } + + void + testMPTCreatePreclaim(FeatureBitset features) + { + testcase("MPT Create Preclaim"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_PERMISSION: issuer is the same as the account + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + env(escrow::create(gw, alice, MPT(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_PERMISSION)); + env.close(); + } + + // tecOBJECT_NOT_FOUND: mpt does not exist + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(10'000), alice, bob, gw); + env.close(); + + auto const mpt = ripple::test::jtx::MPT( + alice.name(), makeMptID(env.seq(alice), alice)); + Json::Value jv = escrow::create(alice, bob, mpt(2)); + jv[jss::Amount][jss::mpt_issuance_id] = + "00000004A407AF5856CCF3C42619DAA925813FC955C72983"; + env(jv, + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecOBJECT_NOT_FOUND)); + env.close(); + } + + // tecNO_PERMISSION: tfMPTCanEscrow is not enabled + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + env(escrow::create(alice, bob, MPT(3)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_PERMISSION)); + env.close(); + } + + // tecOBJECT_NOT_FOUND: account does not have the mpt + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + auto const MPT = mptGw["MPT"]; + + env(escrow::create(alice, bob, MPT(4)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecOBJECT_NOT_FOUND)); + env.close(); + } + + // tecNO_AUTH: requireAuth set: account not authorized + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = + tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = gw, .holder = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + // unauthorize account + mptGw.authorize( + {.account = gw, .holder = alice, .flags = tfMPTUnauthorize}); + + env(escrow::create(alice, bob, MPT(5)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_AUTH)); + env.close(); + } + + // tecNO_AUTH: requireAuth set: dest not authorized + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = + tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = gw, .holder = alice}); + mptGw.authorize({.account = bob}); + mptGw.authorize({.account = gw, .holder = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + // unauthorize dest + mptGw.authorize( + {.account = gw, .holder = bob, .flags = tfMPTUnauthorize}); + + env(escrow::create(alice, bob, MPT(6)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_AUTH)); + env.close(); + } + + // tecLOCKED: issuer has locked the account + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + // lock account + mptGw.set({.account = gw, .holder = alice, .flags = tfMPTLock}); + + env(escrow::create(alice, bob, MPT(7)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecLOCKED)); + env.close(); + } + + // tecLOCKED: issuer has locked the dest + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + // lock dest + mptGw.set({.account = gw, .holder = bob, .flags = tfMPTLock}); + + env(escrow::create(alice, bob, MPT(8)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecLOCKED)); + env.close(); + } + + // tecNO_AUTH: mpt cannot be transferred + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + env(escrow::create(alice, bob, MPT(9)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecNO_AUTH)); + env.close(); + } + + // tecINSUFFICIENT_FUNDS: spendable amount is zero + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10))); + env(pay(gw, bob, MPT(10))); + env.close(); + + env(escrow::create(alice, bob, MPT(11)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); + } + + // tecINSUFFICIENT_FUNDS: spendable amount is less than the amount + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10))); + env(pay(gw, bob, MPT(10))); + env.close(); + + env(escrow::create(alice, bob, MPT(11)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); + } + } + + void + testMPTFinishPreclaim(FeatureBitset features) + { + testcase("MPT Finish Preclaim"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_AUTH: requireAuth set: dest not authorized + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = + tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = gw, .holder = alice}); + mptGw.authorize({.account = bob}); + mptGw.authorize({.account = gw, .holder = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(10)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + // unauthorize dest + mptGw.authorize( + {.account = gw, .holder = bob, .flags = tfMPTUnauthorize}); + + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecNO_AUTH)); + env.close(); + } + + // tecLOCKED: issuer has locked the dest + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(8)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + // lock dest + mptGw.set({.account = gw, .holder = bob, .flags = tfMPTLock}); + + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecLOCKED)); + env.close(); + } + } + + void + testMPTFinishDoApply(FeatureBitset features) + { + testcase("MPT Finish Do Apply"); + using namespace test::jtx; + using namespace std::literals; + + // tecINSUFFICIENT_RESERVE: insufficient reserve to create MPT + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const acctReserve = env.current()->fees().accountReserve(0); + auto const incReserve = env.current()->fees().increment; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(acctReserve + (incReserve - 1), bob); + env.close(); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(10)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecINSUFFICIENT_RESERVE)); + env.close(); + } + + // tesSUCCESS: bob submits; finish MPT created + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(10'000), bob); + env.close(); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(10)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + } + + // tecNO_PERMISSION: carol submits; finish MPT not created + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account("gw"); + env.fund(XRP(10'000), bob, carol); + env.close(); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(10)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env(escrow::finish(carol, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecNO_PERMISSION)); + env.close(); + } + } + + void + testMPTCancelPreclaim(FeatureBitset features) + { + testcase("MPT Cancel Preclaim"); + using namespace test::jtx; + using namespace std::literals; + + // tecNO_AUTH: requireAuth set: account not authorized + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = + tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = gw, .holder = alice}); + mptGw.authorize({.account = bob}); + mptGw.authorize({.account = gw, .holder = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(10)), + escrow::cancel_time(env.now() + 2s), + escrow::condition(escrow::cb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + // unauthorize account + mptGw.authorize( + {.account = gw, .holder = alice, .flags = tfMPTUnauthorize}); + + env(escrow::cancel(bob, alice, seq1), ter(tecNO_AUTH)); + env.close(); + } + } + + void + testMPTBalances(FeatureBitset features) + { + testcase("MPT Balances"); + + using namespace jtx; + using namespace std::chrono; + + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account("gw"); + env.fund(XRP(5000), bob); + + MPTTester mptGw(env, gw, {.holders = {alice, carol}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = carol}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, carol, MPT(10'000))); + env.close(); + + auto outstandingMPT = env.balance(gw, MPT); + + // Create & Finish Escrow + auto const seq1 = env.seq(alice); + { + auto const preAliceMPT = env.balance(alice, MPT); + auto const preBobMPT = env.balance(bob, MPT); + env(escrow::create(alice, bob, MPT(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1'000); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 1'000); + } + { + auto const preAliceMPT = env.balance(alice, MPT); + auto const preBobMPT = env.balance(bob, MPT); + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT + MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); + } + + // Create & Cancel Escrow + auto const seq2 = env.seq(alice); + { + auto const preAliceMPT = env.balance(alice, MPT); + auto const preBobMPT = env.balance(bob, MPT); + env(escrow::create(alice, bob, MPT(1'000)), + escrow::condition(escrow::cb2), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1'000); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 1'000); + } + { + auto const preAliceMPT = env.balance(alice, MPT); + auto const preBobMPT = env.balance(bob, MPT); + env(escrow::cancel(bob, alice, seq2), ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT + MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); + } + + // Self Escrow Create & Finish + { + auto const seq = env.seq(alice); + auto const preAliceMPT = env.balance(alice, MPT); + env(escrow::create(alice, alice, MPT(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1'000); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 1'000); + + env(escrow::finish(alice, alice, seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); + } + + // Self Escrow Create & Cancel + { + auto const seq = env.seq(alice); + auto const preAliceMPT = env.balance(alice, MPT); + env(escrow::create(alice, alice, MPT(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1'000); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 1'000); + + env(escrow::cancel(alice, alice, seq), ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); + } + + // Multiple Escrows + { + auto const preAliceMPT = env.balance(alice, MPT); + auto const preBobMPT = env.balance(bob, MPT); + auto const preCarolMPT = env.balance(carol, MPT); + env(escrow::create(alice, bob, MPT(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env(escrow::create(carol, bob, MPT(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1'000); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(carol, MPT) == preCarolMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, carol, MPT) == 1'000); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 2'000); + } + + // Max MPT Amount Issued (Escrow 1 MPT) + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(maxMPTokenAmount))); + env.close(); + + auto const preAliceMPT = env.balance(alice, MPT); + auto const preBobMPT = env.balance(bob, MPT); + auto const outstandingMPT = env.balance(gw, MPT); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 1); + + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + BEAST_EXPECT(!env.le(keylet::mptoken(MPT.mpt(), alice)) + ->isFieldPresent(sfLockedAmount)); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT + MPT(1)); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); + BEAST_EXPECT(!env.le(keylet::mptIssuance(MPT.mpt())) + ->isFieldPresent(sfLockedAmount)); + } + + // Max MPT Amount Issued (Escrow Max MPT) + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(maxMPTokenAmount))); + env.close(); + + auto const preAliceMPT = env.balance(alice, MPT); + auto const preBobMPT = env.balance(bob, MPT); + auto const outstandingMPT = env.balance(gw, MPT); + + // Escrow Max MPT - 10 + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(maxMPTokenAmount - 10)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // Escrow 10 MPT + auto const seq2 = env.seq(alice); + env(escrow::create(alice, bob, MPT(10)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT( + env.balance(alice, MPT) == preAliceMPT - MPT(maxMPTokenAmount)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == maxMPTokenAmount); + BEAST_EXPECT(env.balance(bob, MPT) == preBobMPT); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == maxMPTokenAmount); + + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env(escrow::finish(bob, alice, seq2), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT( + env.balance(alice, MPT) == preAliceMPT - MPT(maxMPTokenAmount)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + BEAST_EXPECT( + env.balance(bob, MPT) == preBobMPT + MPT(maxMPTokenAmount)); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == outstandingMPT); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); + } + } + + void + testMPTMetaAndOwnership(FeatureBitset features) + { + using namespace jtx; + using namespace std::chrono; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + { + testcase("MPT Metadata to self"); + + Env env{*this, features}; + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + auto const aseq = env.seq(alice); + auto const bseq = env.seq(bob); + + env(escrow::create(alice, alice, MPT(1'000)), + escrow::finish_time(env.now() + 1s), + escrow::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()) == 1); + BEAST_EXPECT( + std::find(iod.begin(), iod.end(), aa) == iod.end()); + } + + env(escrow::create(bob, bob, MPT(1'000)), + escrow::finish_time(env.now() + 1s), + escrow::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()); + } + + env.close(5s); + env(escrow::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()); + } + + env.close(5s); + env(escrow::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()); + } + } + + { + testcase("MPT Metadata to other"); + + Env env{*this, features}; + MPTTester mptGw(env, gw, {.holders = {alice, bob, carol}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + mptGw.authorize({.account = carol}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env(pay(gw, carol, MPT(10'000))); + env.close(); + auto const aseq = env.seq(alice); + auto const bseq = env.seq(bob); + + env(escrow::create(alice, bob, MPT(1'000)), + escrow::finish_time(env.now() + 1s)); + BEAST_EXPECT( + (*env.meta())[sfTransactionResult] == + static_cast(tesSUCCESS)); + env.close(5s); + env(escrow::create(bob, carol, MPT(1'000)), + escrow::finish_time(env.now() + 1s), + escrow::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()); + } + + env.close(5s); + env(escrow::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); + } + + env.close(5s); + env(escrow::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); + } + } + } + + void + testMPTGateway(FeatureBitset features) + { + testcase("MPT Gateway Balances"); + using namespace test::jtx; + using namespace std::literals; + + // issuer is dest; alice w/ authorization + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + // issuer can be destination + auto const seq1 = env.seq(alice); + auto const preAliceMPT = env.balance(alice, MPT); + auto const preOutstanding = env.balance(gw, MPT); + auto const preEscrowed = issuerMPTEscrowed(env, MPT); + BEAST_EXPECT(preOutstanding == MPT(10'000)); + BEAST_EXPECT(preEscrowed == 0); + + env(escrow::create(alice, gw, MPT(1'000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 1'000); + BEAST_EXPECT(env.balance(gw, MPT) == preOutstanding); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == preEscrowed + 1'000); + + // issuer (dest) can finish escrow + env(escrow::finish(gw, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAliceMPT - MPT(1'000)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + BEAST_EXPECT(env.balance(gw, MPT) == preOutstanding - MPT(1'000)); + BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == preEscrowed); + } + } + + void + testMPTLockedRate(FeatureBitset features) + { + testcase("MPT Locked Rate"); + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + auto const USD = gw["USD"]; + + // test locked rate: finish + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.transferFee = 25000, + .ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + // alice can create escrow w/ xfer rate + auto const preAlice = env.balance(alice, MPT); + auto const seq1 = env.seq(alice); + auto const delta = MPT(125); + env(escrow::create(alice, bob, MPT(125)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + auto const transferRate = escrow::rate(env, alice, seq1); + BEAST_EXPECT( + transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // bob can finish escrow + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAlice - delta); + BEAST_EXPECT(env.balance(bob, MPT) == MPT(10'100)); + } + + // test locked rate: cancel + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.transferFee = 25000, + .ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + // alice can create escrow w/ xfer rate + auto const preAlice = env.balance(alice, MPT); + auto const preBob = env.balance(bob, MPT); + auto const seq1 = env.seq(alice); + auto const delta = MPT(125); + env(escrow::create(alice, bob, MPT(125)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 3s), + fee(baseFee * 150)); + env.close(); + auto const transferRate = escrow::rate(env, alice, seq1); + BEAST_EXPECT( + transferRate.value == std::uint32_t(1'000'000'000 * 1.25)); + + // alice can cancel escrow + env(escrow::cancel(alice, alice, seq1), fee(baseFee)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == preAlice); + BEAST_EXPECT(env.balance(bob, MPT) == preBob); + } + } + + void + testMPTRequireAuth(FeatureBitset features) + { + testcase("MPT Require Auth"); + using namespace test::jtx; + using namespace std::literals; + + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTRequireAuth}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = gw, .holder = alice}); + mptGw.authorize({.account = bob}); + mptGw.authorize({.account = gw, .holder = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + auto seq = env.seq(alice); + auto const delta = MPT(125); + // alice can create escrow - is authorized + env(escrow::create(alice, bob, MPT(100)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // bob can finish escrow - is authorized + env(escrow::finish(bob, alice, seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + } + + void + testMPTLock(FeatureBitset features) + { + testcase("MPT Lock"); + using namespace test::jtx; + using namespace std::literals; + + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer | tfMPTCanLock}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + // alice create escrow + auto seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(100)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + fee(baseFee * 150)); + env.close(); + + // lock account & dest + mptGw.set({.account = gw, .holder = alice, .flags = tfMPTLock}); + mptGw.set({.account = gw, .holder = bob, .flags = tfMPTLock}); + + // bob cannot finish + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tecLOCKED)); + env.close(); + + // bob can cancel + env(escrow::cancel(bob, alice, seq1)); + env.close(); + } + + void + testMPTCanTransfer(FeatureBitset features) + { + testcase("MPT Can Transfer"); + using namespace test::jtx; + using namespace std::literals; + + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanEscrow}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + // alice cannot create escrow to non issuer + env(escrow::create(alice, bob, MPT(100)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + fee(baseFee * 150), + ter(tecNO_AUTH)); + env.close(); + + // Escrow Create & Finish + { + // alice an create escrow to issuer + auto seq = env.seq(alice); + env(escrow::create(alice, gw, MPT(100)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + // gw can finish + env(escrow::finish(gw, alice, seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150)); + env.close(); + } + + // Escrow Create & Cancel + { + // alice an create escrow to issuer + auto seq = env.seq(alice); + env(escrow::create(alice, gw, MPT(100)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), + fee(baseFee * 150)); + env.close(); + + // alice can cancel + env(escrow::cancel(alice, alice, seq)); + env.close(); + } + } + + void + testMPTDestroy(FeatureBitset features) + { + testcase("MPT Destroy"); + using namespace test::jtx; + using namespace std::literals; + + // tecHAS_OBLIGATIONS: issuer cannot destroy issuance + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + MPTTester mptGw(env, gw, {.holders = {alice, bob}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + mptGw.authorize({.account = bob}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env(pay(gw, bob, MPT(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(10)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150)); + env.close(); + + env(pay(alice, gw, MPT(9'990))); + env(pay(bob, gw, MPT(10'000))); + BEAST_EXPECT(env.balance(alice, MPT) == MPT(0)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 10); + BEAST_EXPECT(env.balance(bob, MPT) == MPT(0)); + BEAST_EXPECT(mptEscrowed(env, bob, MPT) == 0); + mptGw.authorize({.account = bob, .flags = tfMPTUnauthorize}); + mptGw.destroy( + {.id = mptGw.issuanceID(), + .ownerCount = 1, + .err = tecHAS_OBLIGATIONS}); + + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env(pay(bob, gw, MPT(10))); + mptGw.destroy({.id = mptGw.issuanceID(), .ownerCount = 0}); + } + + // tecHAS_OBLIGATIONS: holder cannot destroy mptoken + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(10'000), bob); + env.close(); + + MPTTester mptGw(env, gw, {.holders = {alice}}); + mptGw.create( + {.ownerCount = 1, + .holderCount = 0, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + mptGw.authorize({.account = alice}); + auto const MPT = mptGw["MPT"]; + env(pay(gw, alice, MPT(10'000))); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow::create(alice, bob, MPT(10)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + env(pay(alice, gw, MPT(9'990))); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == MPT(0)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 10); + mptGw.authorize( + {.account = alice, + .flags = tfMPTUnauthorize, + .err = tecHAS_OBLIGATIONS}); + + env(escrow::finish(bob, alice, seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice, MPT) == MPT(0)); + BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); + mptGw.authorize({.account = alice, .flags = tfMPTUnauthorize}); + BEAST_EXPECT(!env.le(keylet::mptoken(MPT.mpt(), alice))); + } + } + + void + testIOUWithFeats(FeatureBitset features) + { + testIOUEnablement(features); + testIOUAllowLockingFlag(features); + testIOUCreatePreflight(features); + testIOUCreatePreclaim(features); + testIOUFinishPreclaim(features); + testIOUFinishDoApply(features); + testIOUCancelPreclaim(features); + testIOUBalances(features); + testIOUMetaAndOwnership(features); + testIOURippleState(features); + testIOUGateway(features); + testIOULockedRate(features); + testIOULimitAmount(features); + testIOURequireAuth(features); + testIOUFreeze(features); + testIOUINSF(features); + testIOUPrecisionLoss(features); + } + + void + testMPTWithFeats(FeatureBitset features) + { + testMPTEnablement(features); + testMPTCreatePreflight(features); + testMPTCreatePreclaim(features); + testMPTFinishPreclaim(features); + testMPTFinishDoApply(features); + testMPTCancelPreclaim(features); + testMPTBalances(features); + testMPTMetaAndOwnership(features); + testMPTGateway(features); + testMPTLockedRate(features); + testMPTRequireAuth(features); + testMPTLock(features); + testMPTCanTransfer(features); + testMPTDestroy(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 diff --git a/src/test/app/Escrow_test.cpp b/src/test/app/Escrow_test.cpp index 1129019aab..aa86ad338e 100644 --- a/src/test/app/Escrow_test.cpp +++ b/src/test/app/Escrow_test.cpp @@ -35,81 +35,53 @@ namespace test { struct Escrow_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}}; - void - testEnablement() + testEnablement(FeatureBitset features) { testcase("Enablement"); using namespace jtx; using namespace std::chrono; - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob"); - env(escrow("alice", "bob", XRP(1000)), finish_time(env.now() + 1s)); + env(escrow::create("alice", "bob", XRP(1000)), + escrow::finish_time(env.now() + 1s)); env.close(); auto const seq1 = env.seq("alice"); - env(escrow("alice", "bob", XRP(1000)), - condition(cb1), - finish_time(env.now() + 1s), + env(escrow::create("alice", "bob", XRP(1000)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), fee(baseFee * 150)); env.close(); - env(finish("bob", "alice", seq1), - condition(cb1), - fulfillment(fb1), + env(escrow::finish("bob", "alice", seq1), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), fee(baseFee * 150)); auto const seq2 = env.seq("alice"); - env(escrow("alice", "bob", XRP(1000)), - condition(cb2), - finish_time(env.now() + 1s), - cancel_time(env.now() + 2s), + env(escrow::create("alice", "bob", XRP(1000)), + escrow::condition(escrow::cb2), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s), fee(baseFee * 150)); env.close(); - env(cancel("bob", "alice", seq2), fee(baseFee * 150)); + env(escrow::cancel("bob", "alice", seq2), fee(baseFee * 150)); } void - testTiming() + testTiming(FeatureBitset features) { using namespace jtx; using namespace std::chrono; { testcase("Timing: Finish Only"); - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob"); env.close(); @@ -118,21 +90,22 @@ struct Escrow_test : public beast::unit_test::suite auto const ts = env.now() + 97s; auto const seq = env.seq("alice"); - env(escrow("alice", "bob", XRP(1000)), finish_time(ts)); + env(escrow::create("alice", "bob", XRP(1000)), + escrow::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), + env(escrow::finish("bob", "alice", seq), fee(baseFee * 150), ter(tecNO_PERMISSION)); - env(finish("bob", "alice", seq), fee(baseFee * 150)); + env(escrow::finish("bob", "alice", seq), fee(baseFee * 150)); } { testcase("Timing: Cancel Only"); - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob"); env.close(); @@ -141,31 +114,31 @@ struct Escrow_test : public beast::unit_test::suite auto const ts = env.now() + 117s; auto const seq = env.seq("alice"); - env(escrow("alice", "bob", XRP(1000)), - condition(cb1), - cancel_time(ts)); + env(escrow::create("alice", "bob", XRP(1000)), + escrow::condition(escrow::cb1), + escrow::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), + env(escrow::cancel("bob", "alice", seq), fee(baseFee * 150), ter(tecNO_PERMISSION)); // Verify that a finish won't work anymore. - env(finish("bob", "alice", seq), - condition(cb1), - fulfillment(fb1), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), fee(baseFee * 150), ter(tecNO_PERMISSION)); // Verify that the cancel will succeed - env(cancel("bob", "alice", seq), fee(baseFee * 150)); + env(escrow::cancel("bob", "alice", seq), fee(baseFee * 150)); } { testcase("Timing: Finish and Cancel -> Finish"); - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob"); env.close(); @@ -175,34 +148,34 @@ struct Escrow_test : public beast::unit_test::suite auto const cts = env.now() + 192s; auto const seq = env.seq("alice"); - env(escrow("alice", "bob", XRP(1000)), - finish_time(fts), - cancel_time(cts)); + env(escrow::create("alice", "bob", XRP(1000)), + escrow::finish_time(fts), + escrow::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), + env(escrow::finish("bob", "alice", seq), fee(baseFee * 150), ter(tecNO_PERMISSION)); - env(cancel("bob", "alice", seq), + env(escrow::cancel("bob", "alice", seq), fee(baseFee * 150), ter(tecNO_PERMISSION)); } // Verify that a cancel still won't work - env(cancel("bob", "alice", seq), + env(escrow::cancel("bob", "alice", seq), fee(baseFee * 150), ter(tecNO_PERMISSION)); // And verify that a finish will - env(finish("bob", "alice", seq), fee(baseFee * 150)); + env(escrow::finish("bob", "alice", seq), fee(baseFee * 150)); } { testcase("Timing: Finish and Cancel -> Cancel"); - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob"); env.close(); @@ -212,18 +185,18 @@ struct Escrow_test : public beast::unit_test::suite auto const cts = env.now() + 184s; auto const seq = env.seq("alice"); - env(escrow("alice", "bob", XRP(1000)), - finish_time(fts), - cancel_time(cts)); + env(escrow::create("alice", "bob", XRP(1000)), + escrow::finish_time(fts), + escrow::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), + env(escrow::finish("bob", "alice", seq), fee(baseFee * 150), ter(tecNO_PERMISSION)); - env(cancel("bob", "alice", seq), + env(escrow::cancel("bob", "alice", seq), fee(baseFee * 150), ter(tecNO_PERMISSION)); } @@ -231,30 +204,30 @@ struct Escrow_test : public beast::unit_test::suite // 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), + env(escrow::cancel("bob", "alice", seq), fee(baseFee * 150), ter(tecNO_PERMISSION)); // Verify that finish will no longer work, since we are past the // cancel activation time. - env(finish("bob", "alice", seq), + env(escrow::finish("bob", "alice", seq), fee(baseFee * 150), ter(tecNO_PERMISSION)); // And verify that a cancel will succeed. - env(cancel("bob", "alice", seq), fee(baseFee * 150)); + env(escrow::cancel("bob", "alice", seq), fee(baseFee * 150)); } } 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"); @@ -264,15 +237,15 @@ struct Escrow_test : public beast::unit_test::suite // Check to make sure that we correctly detect if tags are really // required: env(fset(bob, asfRequireDest)); - env(escrow(alice, bob, XRP(1000)), - finish_time(env.now() + 1s), + env(escrow::create(alice, bob, XRP(1000)), + escrow::finish_time(env.now() + 1s), ter(tecDST_TAG_NEEDED)); // set source and dest tags auto const seq = env.seq(alice); - env(escrow(alice, bob, XRP(1000)), - finish_time(env.now() + 1s), + env(escrow::create(alice, bob, XRP(1000)), + escrow::finish_time(env.now() + 1s), stag(1), dtag(2)); @@ -283,7 +256,7 @@ struct Escrow_test : public beast::unit_test::suite } void - testDisallowXRP() + testDisallowXRP(FeatureBitset features) { testcase("Disallow XRP"); @@ -292,27 +265,28 @@ 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)); - env(escrow("bob", "george", XRP(10)), - finish_time(env.now() + 1s), + env(escrow::create("bob", "george", XRP(10)), + escrow::finish_time(env.now() + 1s), ter(tecNO_TARGET)); } { // 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)); - env(escrow("bob", "george", XRP(10)), finish_time(env.now() + 1s)); + env(escrow::create("bob", "george", XRP(10)), + escrow::finish_time(env.now() + 1s)); } } void - test1571() + test1571(FeatureBitset features) { using namespace jtx; using namespace std::chrono; @@ -328,11 +302,11 @@ struct Escrow_test : public beast::unit_test::suite // Creating an escrow without a finish time and finishing it // is allowed without fix1571: auto const seq1 = env.seq("alice"); - env(escrow("alice", "bob", XRP(100)), - cancel_time(env.now() + 1s), + env(escrow::create("alice", "bob", XRP(100)), + escrow::cancel_time(env.now() + 1s), fee(baseFee * 150)); env.close(); - env(finish("carol", "alice", seq1), fee(baseFee * 150)); + env(escrow::finish("carol", "alice", seq1), fee(baseFee * 150)); BEAST_EXPECT(env.balance("bob") == XRP(5100)); env.close(); @@ -340,14 +314,14 @@ struct Escrow_test : public beast::unit_test::suite // 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", XRP(100)), - cancel_time(env.now() + 1s), - condition(cb1), + env(escrow::create("alice", "bob", XRP(100)), + escrow::cancel_time(env.now() + 1s), + escrow::condition(escrow::cb1), fee(baseFee * 150)); env.close(); - env(finish("carol", "alice", seq2), - condition(cb1), - fulfillment(fb1), + env(escrow::finish("carol", "alice", seq2), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), fee(baseFee * 150)); BEAST_EXPECT(env.balance("bob") == XRP(5200)); } @@ -355,117 +329,125 @@ struct Escrow_test : public beast::unit_test::suite { testcase("Implied Finish Time (with fix1571)"); - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob", "carol"); env.close(); // Creating an escrow with only a cancel time is not allowed: - env(escrow("alice", "bob", XRP(100)), - cancel_time(env.now() + 90s), + env(escrow::create("alice", "bob", XRP(100)), + escrow::cancel_time(env.now() + 90s), fee(baseFee * 150), 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", XRP(100)), - cancel_time(env.now() + 90s), - condition(cb1), + env(escrow::create("alice", "bob", XRP(100)), + escrow::cancel_time(env.now() + 90s), + escrow::condition(escrow::cb1), fee(baseFee * 150)); env.close(); - env(finish("carol", "alice", seq), - condition(cb1), - fulfillment(fb1), + env(escrow::finish("carol", "alice", seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), fee(baseFee * 150)); BEAST_EXPECT(env.balance("bob") == XRP(5100)); } } void - testFails() + testFails(FeatureBitset features) { testcase("Failure Cases"); using namespace jtx; using namespace std::chrono; - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; - env.fund(XRP(5000), "alice", "bob"); + env.fund(XRP(5000), "alice", "bob", "gw"); env.close(); // Finish time is in the past - env(escrow("alice", "bob", XRP(1000)), - finish_time(env.now() - 5s), + env(escrow::create("alice", "bob", XRP(1000)), + escrow::finish_time(env.now() - 5s), ter(tecNO_PERMISSION)); // Cancel time is in the past - env(escrow("alice", "bob", XRP(1000)), - condition(cb1), - cancel_time(env.now() - 5s), + env(escrow::create("alice", "bob", XRP(1000)), + escrow::condition(escrow::cb1), + escrow::cancel_time(env.now() - 5s), ter(tecNO_PERMISSION)); // no destination account - env(escrow("alice", "carol", XRP(1000)), - finish_time(env.now() + 1s), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::finish_time(env.now() + 1s), ter(tecNO_DST)); env.fund(XRP(5000), "carol"); // Using non-XRP: - env(escrow("alice", "carol", Account("alice")["USD"](500)), - finish_time(env.now() + 1s), - ter(temBAD_AMOUNT)); + bool const withTokenEscrow = + env.current()->rules().enabled(featureTokenEscrow); + { + // tecNO_PERMISSION: token escrow is enabled but the issuer did not + // set the asfAllowTrustLineLocking flag + auto const txResult = + withTokenEscrow ? ter(tecNO_PERMISSION) : ter(temBAD_AMOUNT); + env(escrow::create("alice", "carol", Account("alice")["USD"](500)), + escrow::finish_time(env.now() + 5s), + txResult); + } // Sending zero or no XRP: - env(escrow("alice", "carol", XRP(0)), - finish_time(env.now() + 1s), + env(escrow::create("alice", "carol", XRP(0)), + escrow::finish_time(env.now() + 1s), ter(temBAD_AMOUNT)); - env(escrow("alice", "carol", XRP(-1000)), - finish_time(env.now() + 1s), + env(escrow::create("alice", "carol", XRP(-1000)), + escrow::finish_time(env.now() + 1s), ter(temBAD_AMOUNT)); // Fail if neither CancelAfter nor FinishAfter are specified: - env(escrow("alice", "carol", XRP(1)), ter(temBAD_EXPIRATION)); + env(escrow::create("alice", "carol", XRP(1)), ter(temBAD_EXPIRATION)); // Fail if neither a FinishTime nor a condition are attached: - env(escrow("alice", "carol", XRP(1)), - cancel_time(env.now() + 1s), + env(escrow::create("alice", "carol", XRP(1)), + escrow::cancel_time(env.now() + 1s), ter(temMALFORMED)); // Fail if FinishAfter has already passed: - env(escrow("alice", "carol", XRP(1)), - finish_time(env.now() - 1s), + env(escrow::create("alice", "carol", XRP(1)), + escrow::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", XRP(1)), - condition(cb1), - finish_time(env.now() + 10s), - cancel_time(env.now() + 10s), + env(escrow::create("alice", "carol", XRP(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 10s), + escrow::cancel_time(env.now() + 10s), ter(temBAD_EXPIRATION)); - env(escrow("alice", "carol", XRP(1)), - condition(cb1), - finish_time(env.now() + 10s), - cancel_time(env.now() + 5s), + env(escrow::create("alice", "carol", XRP(1)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 10s), + escrow::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", XRP(1)), - condition(cb1), - cancel_time(env.now() + 1s), + env(escrow::create("alice", "carol", XRP(1)), + escrow::condition(escrow::cb1), + escrow::cancel_time(env.now() + 1s), ter(tecDST_TAG_NEEDED)); // Success! - env(escrow("alice", "carol", XRP(1)), - condition(cb1), - cancel_time(env.now() + 1s), + env(escrow::create("alice", "carol", XRP(1)), + escrow::condition(escrow::cb1), + escrow::cancel_time(env.now() + 1s), dtag(1)); { // Fail if the sender wants to send more than he has: @@ -474,29 +456,29 @@ struct Escrow_test : public beast::unit_test::suite drops(env.current()->fees().increment); env.fund(accountReserve + accountIncrement + XRP(50), "daniel"); - env(escrow("daniel", "bob", XRP(51)), - finish_time(env.now() + 1s), + env(escrow::create("daniel", "bob", XRP(51)), + escrow::finish_time(env.now() + 1s), ter(tecUNFUNDED)); env.fund(accountReserve + accountIncrement + XRP(50), "evan"); - env(escrow("evan", "bob", XRP(50)), - finish_time(env.now() + 1s), + env(escrow::create("evan", "bob", XRP(50)), + escrow::finish_time(env.now() + 1s), ter(tecUNFUNDED)); env.fund(accountReserve, "frank"); - env(escrow("frank", "bob", XRP(1)), - finish_time(env.now() + 1s), + env(escrow::create("frank", "bob", XRP(1)), + escrow::finish_time(env.now() + 1s), ter(tecINSUFFICIENT_RESERVE)); } { // Specify incorrect sequence number env.fund(XRP(5000), "hannah"); auto const seq = env.seq("hannah"); - env(escrow("hannah", "hannah", XRP(10)), - finish_time(env.now() + 1s), + env(escrow::create("hannah", "hannah", XRP(10)), + escrow::finish_time(env.now() + 1s), fee(150 * baseFee)); env.close(); - env(finish("hannah", "hannah", seq + 7), + env(escrow::finish("hannah", "hannah", seq + 7), fee(150 * baseFee), ter(tecNO_TARGET)); } @@ -505,18 +487,19 @@ struct Escrow_test : public beast::unit_test::suite env.fund(XRP(5000), "ivan"); auto const seq = env.seq("ivan"); - env(escrow("ivan", "ivan", XRP(10)), finish_time(env.now() + 1s)); + env(escrow::create("ivan", "ivan", XRP(10)), + escrow::finish_time(env.now() + 1s)); env.close(); - env(finish("ivan", "ivan", seq), - condition(cb1), - fulfillment(fb1), + env(escrow::finish("ivan", "ivan", seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); } } void - testLockup() + testLockup(FeatureBitset features) { testcase("Lockup"); @@ -525,49 +508,50 @@ struct Escrow_test : public beast::unit_test::suite { // Unconditional - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob"); auto const seq = env.seq("alice"); - env(escrow("alice", "alice", XRP(1000)), - finish_time(env.now() + 5s)); + env(escrow::create("alice", "alice", XRP(1000)), + escrow::finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(baseFee))); // 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(escrow::cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("bob", "alice", seq), ter(tecNO_PERMISSION)); env.close(); // Cancel continues to not be possible - env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); // Finish should succeed. Verify funds. - env(finish("bob", "alice", seq)); + env(escrow::finish("bob", "alice", seq)); env.require(balance("alice", XRP(5000) - drops(baseFee))); } { // 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); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob", "zelda"); auto const seq = env.seq("alice"); - env(escrow("alice", "bob", XRP(1000)), finish_time(env.now() + 5s)); + env(escrow::create("alice", "bob", XRP(1000)), + escrow::finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(baseFee))); // Not enough time has elapsed for a finish and canceling isn't // possible. - env(cancel("zelda", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("zelda", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("zelda", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("zelda", "alice", seq), ter(tecNO_PERMISSION)); env.close(); // Cancel continues to not be possible - env(cancel("zelda", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("zelda", "alice", seq), ter(tecNO_PERMISSION)); // Finish should succeed. Verify funds. - env(finish("zelda", "alice", seq)); + env(escrow::finish("zelda", "alice", seq)); env.close(); env.require(balance("alice", XRP(4000) - drops(baseFee))); @@ -576,7 +560,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); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob", "zelda"); @@ -584,27 +568,28 @@ struct Escrow_test : public beast::unit_test::suite env.close(); auto const seq = env.seq("alice"); - env(escrow("alice", "bob", XRP(1000)), finish_time(env.now() + 5s)); + env(escrow::create("alice", "bob", XRP(1000)), + escrow::finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(baseFee))); // Not enough time has elapsed for a finish and canceling isn't // possible. - env(cancel("zelda", "alice", seq), ter(tecNO_PERMISSION)); - env(cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); - env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("zelda", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("alice", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("zelda", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("zelda", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::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("zelda", "alice", seq), ter(tecNO_PERMISSION)); - env(cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); - env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("zelda", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("alice", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("bob", "alice", seq)); + env(escrow::cancel("zelda", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("zelda", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("bob", "alice", seq)); env.close(); env.require(balance("alice", XRP(4000) - (baseFee * 5))); @@ -614,7 +599,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); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob", "zelda"); @@ -624,15 +609,16 @@ struct Escrow_test : public beast::unit_test::suite env.close(); auto const seq = env.seq("alice"); - env(escrow("alice", "bob", XRP(1000)), finish_time(env.now() + 5s)); + env(escrow::create("alice", "bob", XRP(1000)), + escrow::finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(baseFee))); 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("zelda", "alice", seq)); + env(escrow::finish("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("zelda", "alice", seq)); env.close(); env.require(balance("alice", XRP(4000) - (baseFee * 2))); @@ -641,93 +627,97 @@ struct Escrow_test : public beast::unit_test::suite } { // Conditional - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob"); auto const seq = env.seq("alice"); - env(escrow("alice", "alice", XRP(1000)), - condition(cb2), - finish_time(env.now() + 5s)); + env(escrow::create("alice", "alice", XRP(1000)), + escrow::condition(escrow::cb2), + escrow::finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(baseFee))); // 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), + env(escrow::cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("alice", "alice", seq), + escrow::condition(escrow::cb2), + escrow::fulfillment(escrow::fb2), fee(150 * baseFee), ter(tecNO_PERMISSION)); - env(finish("bob", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("bob", "alice", seq), - condition(cb2), - fulfillment(fb2), + env(escrow::finish("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb2), + escrow::fulfillment(escrow::fb2), fee(150 * baseFee), 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(escrow::cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::finish("bob", "alice", seq), + ter(tecCRYPTOCONDITION_ERROR)); + env(escrow::finish("alice", "alice", seq), + ter(tecCRYPTOCONDITION_ERROR)); env.close(); - env(finish("bob", "alice", seq), - condition(cb2), - fulfillment(fb2), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb2), + escrow::fulfillment(escrow::fb2), fee(150 * baseFee)); } { // Self-escrowed conditional with DepositAuth. - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob"); auto const seq = env.seq("alice"); - env(escrow("alice", "alice", XRP(1000)), - condition(cb3), - finish_time(env.now() + 5s)); + env(escrow::create("alice", "alice", XRP(1000)), + escrow::condition(escrow::cb3), + escrow::finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(baseFee))); 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)); + env(escrow::finish("bob", "alice", seq), + ter(tecCRYPTOCONDITION_ERROR)); + env(escrow::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), + env(escrow::finish("alice", "alice", seq), + escrow::condition(escrow::cb2), + escrow::fulfillment(escrow::fb2), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(cb3), - fulfillment(fb3), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb3), + escrow::fulfillment(escrow::fb3), fee(150 * baseFee), ter(tecNO_PERMISSION)); - env(finish("alice", "alice", seq), - condition(cb3), - fulfillment(fb3), + env(escrow::finish("alice", "alice", seq), + escrow::condition(escrow::cb3), + escrow::fulfillment(escrow::fb3), fee(150 * baseFee)); } { // Self-escrowed conditional with DepositAuth and DepositPreauth. - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob", "zelda"); auto const seq = env.seq("alice"); - env(escrow("alice", "alice", XRP(1000)), - condition(cb3), - finish_time(env.now() + 5s)); + env(escrow::create("alice", "alice", XRP(1000)), + escrow::condition(escrow::cb3), + escrow::finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(baseFee))); env.close(); @@ -737,34 +727,37 @@ struct Escrow_test : public beast::unit_test::suite 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("zelda", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); + env(escrow::finish("alice", "alice", seq), + ter(tecCRYPTOCONDITION_ERROR)); + env(escrow::finish("bob", "alice", seq), + ter(tecCRYPTOCONDITION_ERROR)); + env(escrow::finish("zelda", "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), + env(escrow::finish("alice", "alice", seq), + escrow::condition(escrow::cb2), + escrow::fulfillment(escrow::fb2), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(cb3), - fulfillment(fb3), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb3), + escrow::fulfillment(escrow::fb3), fee(150 * baseFee), ter(tecNO_PERMISSION)); - env(finish("zelda", "alice", seq), - condition(cb3), - fulfillment(fb3), + env(escrow::finish("zelda", "alice", seq), + escrow::condition(escrow::cb3), + escrow::fulfillment(escrow::fb3), fee(150 * baseFee)); } } void - testEscrowConditions() + testEscrowConditions(FeatureBitset features) { testcase("Escrow with CryptoConditions"); @@ -772,126 +765,127 @@ struct Escrow_test : public beast::unit_test::suite using namespace std::chrono; { // Test cryptoconditions - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob", "carol"); auto const seq = env.seq("alice"); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0); - env(escrow("alice", "carol", XRP(1000)), - condition(cb1), - cancel_time(env.now() + 1s)); + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(escrow::cb1), + escrow::cancel_time(env.now() + 1s)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); env.require(balance("alice", XRP(4000) - drops(baseFee))); env.require(balance("carol", XRP(5000))); - env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); // Attempt to finish without a fulfillment - env(finish("bob", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); + env(escrow::finish("bob", "alice", seq), + ter(tecCRYPTOCONDITION_ERROR)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); // Attempt to finish with a condition instead of a fulfillment - env(finish("bob", "alice", seq), - condition(cb1), - fulfillment(cb1), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::cb1), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); - env(finish("bob", "alice", seq), - condition(cb1), - fulfillment(cb2), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::cb2), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); - env(finish("bob", "alice", seq), - condition(cb1), - fulfillment(cb3), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::cb3), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); // Attempt to finish with an incorrect condition and various // combinations of correct and incorrect fulfillments. - env(finish("bob", "alice", seq), - condition(cb2), - fulfillment(fb1), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb2), + escrow::fulfillment(escrow::fb1), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); - env(finish("bob", "alice", seq), - condition(cb2), - fulfillment(fb2), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb2), + escrow::fulfillment(escrow::fb2), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); - env(finish("bob", "alice", seq), - condition(cb2), - fulfillment(fb3), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb2), + escrow::fulfillment(escrow::fb3), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); // Attempt to finish with the correct condition & fulfillment - env(finish("bob", "alice", seq), - condition(cb1), - fulfillment(fb1), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), fee(150 * baseFee)); // SLE removed on finish BEAST_EXPECT(!env.le(keylet::escrow(Account("alice").id(), seq))); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0); env.require(balance("carol", XRP(6000))); - env(cancel("bob", "alice", seq), ter(tecNO_TARGET)); + env(escrow::cancel("bob", "alice", seq), ter(tecNO_TARGET)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0); - env(cancel("bob", "carol", 1), ter(tecNO_TARGET)); + env(escrow::cancel("bob", "carol", 1), ter(tecNO_TARGET)); } { // Test cancel when condition is present - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob", "carol"); auto const seq = env.seq("alice"); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0); - env(escrow("alice", "carol", XRP(1000)), - condition(cb2), - cancel_time(env.now() + 1s)); + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(escrow::cb2), + escrow::cancel_time(env.now() + 1s)); env.close(); env.require(balance("alice", XRP(4000) - drops(baseFee))); // balance restored on cancel - env(cancel("bob", "alice", seq)); + env(escrow::cancel("bob", "alice", seq)); env.require(balance("alice", XRP(5000) - drops(baseFee))); // SLE removed on cancel BEAST_EXPECT(!env.le(keylet::escrow(Account("alice").id(), seq))); } { - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), "alice", "bob", "carol"); env.close(); auto const seq = env.seq("alice"); - env(escrow("alice", "carol", XRP(1000)), - condition(cb3), - cancel_time(env.now() + 1s)); + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(escrow::cb3), + escrow::cancel_time(env.now() + 1s)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); // cancel fails before expiration - env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(escrow::cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); env.close(); // finish fails after expiration - env(finish("bob", "alice", seq), - condition(cb3), - fulfillment(fb3), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb3), + escrow::fulfillment(escrow::fb3), fee(150 * baseFee), ter(tecNO_PERMISSION)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); 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; - v.resize(cb1.size() + 2, 0x78); - std::memcpy(v.data() + 1, cb1.data(), cb1.size()); + v.resize(escrow::cb1.size() + 2, 0x78); + std::memcpy(v.data() + 1, escrow::cb1.data(), escrow::cb1.size()); auto const p = v.data(); auto const s = v.size(); @@ -900,63 +894,63 @@ struct Escrow_test : public beast::unit_test::suite // All these are expected to fail, because the // condition we pass in is malformed in some way - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{p, s}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{p, s}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{p, s - 1}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{p, s - 1}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{p, s - 2}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{p, s - 2}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{p + 1, s - 1}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{p + 1, s - 1}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{p + 1, s - 3}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{p + 1, s - 3}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{p + 2, s - 2}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{p + 2, s - 2}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{p + 2, s - 3}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{p + 2, s - 3}), + escrow::cancel_time(ts), ter(temMALFORMED)); auto const seq = env.seq("alice"); auto const baseFee = env.current()->fees().base; - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{p + 1, s - 2}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{p + 1, s - 2}), + escrow::cancel_time(ts), fee(10 * baseFee)); - env(finish("bob", "alice", seq), - condition(cb1), - fulfillment(fb1), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), fee(150 * baseFee)); env.require(balance("alice", XRP(4000) - drops(10 * baseFee))); env.require(balance("bob", XRP(5000) - drops(150 * baseFee))); 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; - cv.resize(cb2.size() + 2, 0x78); - std::memcpy(cv.data() + 1, cb2.data(), cb2.size()); + cv.resize(escrow::cb2.size() + 2, 0x78); + std::memcpy(cv.data() + 1, escrow::cb2.data(), escrow::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()); + fv.resize(escrow::fb2.size() + 2, 0x13); + std::memcpy(fv.data() + 1, escrow::fb2.data(), escrow::fb2.size()); auto const fp = fv.data(); auto const fs = fv.size(); @@ -965,180 +959,182 @@ struct Escrow_test : public beast::unit_test::suite // All these are expected to fail, because the // condition we pass in is malformed in some way - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{cp, cs}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{cp, cs}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{cp, cs - 1}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{cp, cs - 1}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{cp, cs - 2}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{cp, cs - 2}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{cp + 1, cs - 1}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{cp + 1, cs - 1}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{cp + 1, cs - 3}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{cp + 1, cs - 3}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{cp + 2, cs - 2}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{cp + 2, cs - 2}), + escrow::cancel_time(ts), ter(temMALFORMED)); - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{cp + 2, cs - 3}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{cp + 2, cs - 3}), + escrow::cancel_time(ts), ter(temMALFORMED)); auto const seq = env.seq("alice"); auto const baseFee = env.current()->fees().base; - env(escrow("alice", "carol", XRP(1000)), - condition(Slice{cp + 1, cs - 2}), - cancel_time(ts), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{cp + 1, cs - 2}), + escrow::cancel_time(ts), fee(10 * baseFee)); // Now, try to fulfill using the same sequence of // malformed conditions. - env(finish("bob", "alice", seq), - condition(Slice{cp, cs}), - fulfillment(Slice{fp, fs}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp, cs}), + escrow::fulfillment(Slice{fp, fs}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp, cs - 1}), - fulfillment(Slice{fp, fs}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp, cs - 1}), + escrow::fulfillment(Slice{fp, fs}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp, cs - 2}), - fulfillment(Slice{fp, fs}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp, cs - 2}), + escrow::fulfillment(Slice{fp, fs}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 1, cs - 1}), - fulfillment(Slice{fp, fs}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 1}), + escrow::fulfillment(Slice{fp, fs}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 1, cs - 3}), - fulfillment(Slice{fp, fs}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 3}), + escrow::fulfillment(Slice{fp, fs}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 2, cs - 2}), - fulfillment(Slice{fp, fs}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 2, cs - 2}), + escrow::fulfillment(Slice{fp, fs}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 2, cs - 3}), - fulfillment(Slice{fp, fs}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 2, cs - 3}), + escrow::fulfillment(Slice{fp, fs}), fee(150 * baseFee), 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}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 2}), + escrow::fulfillment(Slice{fp, fs}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 1, cs - 2}), - fulfillment(Slice{fp, fs - 1}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 2}), + escrow::fulfillment(Slice{fp, fs - 1}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 1, cs - 2}), - fulfillment(Slice{fp, fs - 2}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 2}), + escrow::fulfillment(Slice{fp, fs - 2}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 1, cs - 2}), - fulfillment(Slice{fp + 1, fs - 1}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 2}), + escrow::fulfillment(Slice{fp + 1, fs - 1}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 1, cs - 2}), - fulfillment(Slice{fp + 1, fs - 3}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 2}), + escrow::fulfillment(Slice{fp + 1, fs - 3}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 1, cs - 2}), - fulfillment(Slice{fp + 1, fs - 3}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 2}), + escrow::fulfillment(Slice{fp + 1, fs - 3}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 1, cs - 2}), - fulfillment(Slice{fp + 2, fs - 2}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 2}), + escrow::fulfillment(Slice{fp + 2, fs - 2}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{cp + 1, cs - 2}), - fulfillment(Slice{fp + 2, fs - 3}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{cp + 1, cs - 2}), + escrow::fulfillment(Slice{fp + 2, fs - 3}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); // Now try for the right one - env(finish("bob", "alice", seq), - condition(cb2), - fulfillment(fb2), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb2), + escrow::fulfillment(escrow::fb2), fee(150 * baseFee)); env.require(balance("alice", XRP(4000) - drops(10 * baseFee))); env.require(balance("carol", XRP(6000))); } { // 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)), - condition(Slice{}), - cancel_time(env.now() + 1s), + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(Slice{}), + escrow::cancel_time(env.now() + 1s), ter(temMALFORMED)); auto const seq = env.seq("alice"); auto const baseFee = env.current()->fees().base; - env(escrow("alice", "carol", XRP(1000)), - condition(cb3), - cancel_time(env.now() + 1s)); + env(escrow::create("alice", "carol", XRP(1000)), + escrow::condition(escrow::cb3), + escrow::cancel_time(env.now() + 1s)); - env(finish("bob", "alice", seq), - condition(Slice{}), - fulfillment(Slice{}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{}), + escrow::fulfillment(Slice{}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(cb3), - fulfillment(Slice{}), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb3), + escrow::fulfillment(Slice{}), fee(150 * baseFee), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq), - condition(Slice{}), - fulfillment(fb3), + env(escrow::finish("bob", "alice", seq), + escrow::condition(Slice{}), + escrow::fulfillment(escrow::fb3), fee(150 * baseFee), 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), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb3), + ter(temMALFORMED)); + env(escrow::finish("bob", "alice", seq), + escrow::fulfillment(escrow::fb3), ter(temMALFORMED)); // Now finish it. - env(finish("bob", "alice", seq), - condition(cb3), - fulfillment(fb3), + env(escrow::finish("bob", "alice", seq), + escrow::condition(escrow::cb3), + escrow::fulfillment(escrow::fb3), fee(150 * baseFee)); env.require(balance("carol", XRP(6000))); env.require(balance("alice", XRP(4000) - drops(baseFee))); } { // 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 = { @@ -1150,15 +1146,15 @@ struct Escrow_test : public beast::unit_test::suite // FIXME: this transaction should, eventually, return temDISABLED // instead of temMALFORMED. - env(escrow("alice", "bob", XRP(1000)), - condition(cb), - cancel_time(env.now() + 1s), + env(escrow::create("alice", "bob", XRP(1000)), + escrow::condition(cb), + escrow::cancel_time(env.now() + 1s), ter(temMALFORMED)); } } void - testMetaAndOwnership() + testMetaAndOwnership(FeatureBitset features) { using namespace jtx; using namespace std::chrono; @@ -1170,14 +1166,14 @@ 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); - env(escrow(alice, alice, XRP(1000)), - finish_time(env.now() + 1s), - cancel_time(env.now() + 500s)); + env(escrow::create(alice, alice, XRP(1000)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 500s)); BEAST_EXPECT( (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); @@ -1192,9 +1188,9 @@ struct Escrow_test : public beast::unit_test::suite std::find(aod.begin(), aod.end(), aa) != aod.end()); } - env(escrow(bruce, bruce, XRP(1000)), - finish_time(env.now() + 1s), - cancel_time(env.now() + 2s)); + env(escrow::create(bruce, bruce, XRP(1000)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s)); BEAST_EXPECT( (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); @@ -1210,7 +1206,7 @@ struct Escrow_test : public beast::unit_test::suite } env.close(5s); - env(finish(alice, alice, aseq)); + env(escrow::finish(alice, alice, aseq)); { BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); BEAST_EXPECT( @@ -1229,7 +1225,7 @@ struct Escrow_test : public beast::unit_test::suite } env.close(5s); - env(cancel(bruce, bruce, bseq)); + env(escrow::cancel(bruce, bruce, bseq)); { BEAST_EXPECT(!env.le(keylet::escrow(bruce.id(), bseq))); BEAST_EXPECT( @@ -1245,19 +1241,20 @@ 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); - env(escrow(alice, bruce, XRP(1000)), finish_time(env.now() + 1s)); + env(escrow::create(alice, bruce, XRP(1000)), + escrow::finish_time(env.now() + 1s)); BEAST_EXPECT( (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); env.close(5s); - env(escrow(bruce, carol, XRP(1000)), - finish_time(env.now() + 1s), - cancel_time(env.now() + 2s)); + env(escrow::create(bruce, carol, XRP(1000)), + escrow::finish_time(env.now() + 1s), + escrow::cancel_time(env.now() + 2s)); BEAST_EXPECT( (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); @@ -1289,7 +1286,7 @@ struct Escrow_test : public beast::unit_test::suite } env.close(5s); - env(finish(alice, alice, aseq)); + env(escrow::finish(alice, alice, aseq)); { BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); BEAST_EXPECT(env.le(keylet::escrow(bruce.id(), bseq))); @@ -1311,7 +1308,7 @@ struct Escrow_test : public beast::unit_test::suite } env.close(5s); - env(cancel(bruce, bruce, bseq)); + env(escrow::cancel(bruce, bruce, bseq)); { BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); BEAST_EXPECT(!env.le(keylet::escrow(bruce.id(), bseq))); @@ -1335,13 +1332,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); auto const baseFee = env.current()->fees().base; env.memoize("alice"); @@ -1350,8 +1347,8 @@ struct Escrow_test : public beast::unit_test::suite { auto const jtx = env.jt( - escrow("alice", "carol", XRP(1000)), - finish_time(env.now() + 1s), + escrow::create("alice", "carol", XRP(1000)), + escrow::finish_time(env.now() + 1s), seq(1), fee(baseFee)); auto const pf = preflight( @@ -1368,7 +1365,7 @@ struct Escrow_test : public beast::unit_test::suite { auto const jtx = - env.jt(cancel("bob", "alice", 3), seq(1), fee(baseFee)); + env.jt(escrow::cancel("bob", "alice", 3), seq(1), fee(baseFee)); auto const pf = preflight( env.app(), env.current()->rules(), @@ -1383,7 +1380,7 @@ struct Escrow_test : public beast::unit_test::suite { auto const jtx = - env.jt(finish("bob", "alice", 3), seq(1), fee(baseFee)); + env.jt(escrow::finish("bob", "alice", 3), seq(1), fee(baseFee)); auto const pf = preflight( env.app(), env.current()->rules(), @@ -1398,7 +1395,7 @@ struct Escrow_test : public beast::unit_test::suite } void - testEscrowWithTickets() + testEscrowWithTickets(FeatureBitset features) { testcase("Escrow with tickets"); @@ -1409,7 +1406,7 @@ struct Escrow_test : public beast::unit_test::suite { // Create escrow and finish using tickets. - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), alice, bob); env.close(); @@ -1437,8 +1434,8 @@ struct Escrow_test : public beast::unit_test::suite auto const ts = env.now() + 97s; std::uint32_t const escrowSeq = aliceTicket; - env(escrow(alice, bob, XRP(1000)), - finish_time(ts), + env(escrow::create(alice, bob, XRP(1000)), + escrow::finish_time(ts), ticket::use(aliceTicket)); BEAST_EXPECT(env.seq(alice) == aliceRootSeq); env.require(tickets(alice, 0)); @@ -1448,7 +1445,7 @@ struct Escrow_test : public beast::unit_test::suite // prematurely. Note that each tec consumes one of bob's tickets. for (; env.now() < ts; env.close()) { - env(finish(bob, alice, escrowSeq), + env(escrow::finish(bob, alice, escrowSeq), fee(150 * baseFee), ticket::use(--bobTicket), ter(tecNO_PERMISSION)); @@ -1456,13 +1453,13 @@ struct Escrow_test : public beast::unit_test::suite } // bob tries to re-use a ticket, which is rejected. - env(finish(bob, alice, escrowSeq), + env(escrow::finish(bob, alice, escrowSeq), fee(150 * baseFee), ticket::use(bobTicket), ter(tefNO_TICKET)); // bob uses one of his remaining tickets. Success! - env(finish(bob, alice, escrowSeq), + env(escrow::finish(bob, alice, escrowSeq), fee(150 * baseFee), ticket::use(--bobTicket)); env.close(); @@ -1470,7 +1467,7 @@ struct Escrow_test : public beast::unit_test::suite } { // Create escrow and cancel using tickets. - Env env(*this); + Env env(*this, features); auto const baseFee = env.current()->fees().base; env.fund(XRP(5000), alice, bob); env.close(); @@ -1497,9 +1494,9 @@ struct Escrow_test : public beast::unit_test::suite auto const ts = env.now() + 117s; std::uint32_t const escrowSeq = aliceTicket; - env(escrow(alice, bob, XRP(1000)), - condition(cb1), - cancel_time(ts), + env(escrow::create(alice, bob, XRP(1000)), + escrow::condition(escrow::cb1), + escrow::cancel_time(ts), ticket::use(aliceTicket)); BEAST_EXPECT(env.seq(alice) == aliceRootSeq); env.require(tickets(alice, 0)); @@ -1509,7 +1506,7 @@ struct Escrow_test : public beast::unit_test::suite // prematurely. for (; env.now() < ts; env.close()) { - env(cancel(bob, alice, escrowSeq), + env(escrow::cancel(bob, alice, escrowSeq), fee(150 * baseFee), ticket::use(bobTicket++), ter(tecNO_PERMISSION)); @@ -1517,16 +1514,16 @@ struct Escrow_test : public beast::unit_test::suite } // Verify that a finish won't work anymore. - env(finish(bob, alice, escrowSeq), - condition(cb1), - fulfillment(fb1), + env(escrow::finish(bob, alice, escrowSeq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), fee(150 * baseFee), ticket::use(bobTicket++), ter(tecNO_PERMISSION)); BEAST_EXPECT(env.seq(bob) == bobRootSeq); // Verify that the cancel succeeds. - env(cancel(bob, alice, escrowSeq), + env(escrow::cancel(bob, alice, escrowSeq), fee(150 * baseFee), ticket::use(bobTicket++)); env.close(); @@ -1538,7 +1535,7 @@ struct Escrow_test : public beast::unit_test::suite } void - testCredentials() + testCredentials(FeatureBitset features) { testcase("Test with credentials"); @@ -1555,12 +1552,13 @@ 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(); auto const seq = env.seq(alice); - env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 1s)); + env(escrow::create(alice, bob, XRP(1000)), + escrow::finish_time(env.now() + 1s)); env.close(); env(fset(bob, asfDepositAuth)); @@ -1571,13 +1569,13 @@ struct Escrow_test : public beast::unit_test::suite std::string const credIdx = "48004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6EA288B" "E4"; - env(finish(bob, alice, seq), + env(escrow::finish(bob, alice, seq), credentials::ids({credIdx}), ter(temDISABLED)); } { - Env env(*this); + Env env(*this, features); env.fund(XRP(5000), alice, bob, carol, dillon, zelda); env.close(); @@ -1589,7 +1587,8 @@ struct Escrow_test : public beast::unit_test::suite std::string const credIdx = jv[jss::result][jss::index].asString(); auto const seq = env.seq(alice); - env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 50s)); + env(escrow::create(alice, bob, XRP(1000)), + escrow::finish_time(env.now() + 50s)); env.close(); // Bob require preauthorization @@ -1597,7 +1596,7 @@ struct Escrow_test : public beast::unit_test::suite env.close(); // Fail, credentials not accepted - env(finish(carol, alice, seq), + env(escrow::finish(carol, alice, seq), credentials::ids({credIdx}), ter(tecBAD_CREDENTIALS)); @@ -1607,12 +1606,12 @@ struct Escrow_test : public beast::unit_test::suite env.close(); // Fail, credentials doesn’t belong to root account - env(finish(dillon, alice, seq), + env(escrow::finish(dillon, alice, seq), credentials::ids({credIdx}), ter(tecBAD_CREDENTIALS)); // Fail, no depositPreauth - env(finish(carol, alice, seq), + env(escrow::finish(carol, alice, seq), credentials::ids({credIdx}), ter(tecNO_PERMISSION)); @@ -1621,7 +1620,7 @@ struct Escrow_test : public beast::unit_test::suite // Success env.close(); - env(finish(carol, alice, seq), credentials::ids({credIdx})); + env(escrow::finish(carol, alice, seq), credentials::ids({credIdx})); env.close(); } @@ -1629,7 +1628,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(); @@ -1643,7 +1642,8 @@ struct Escrow_test : public beast::unit_test::suite std::string const credIdx = jv[jss::result][jss::index].asString(); auto const seq = env.seq(alice); - env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 50s)); + env(escrow::create(alice, bob, XRP(1000)), + escrow::finish_time(env.now() + 50s)); // time advance env.close(); env.close(); @@ -1653,7 +1653,7 @@ struct Escrow_test : public beast::unit_test::suite env.close(); // Succeed, Bob doesn't require preauthorization - env(finish(carol, alice, seq), credentials::ids({credIdx})); + env(escrow::finish(carol, alice, seq), credentials::ids({credIdx})); env.close(); { @@ -1669,7 +1669,8 @@ struct Escrow_test : public beast::unit_test::suite .asString(); auto const seq = env.seq(alice); - env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 1s)); + env(escrow::create(alice, bob, XRP(1000)), + escrow::finish_time(env.now() + 1s)); env.close(); // Bob require preauthorization @@ -1679,27 +1680,38 @@ struct Escrow_test : public beast::unit_test::suite env.close(); // Use any valid credentials if account == dst - env(finish(bob, alice, seq), credentials::ids({credIdxBob})); + env(escrow::finish(bob, alice, seq), + credentials::ids({credIdxBob})); env.close(); } } } + 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/app/Freeze_test.cpp b/src/test/app/Freeze_test.cpp index b28e794688..8c2021d657 100644 --- a/src/test/app/Freeze_test.cpp +++ b/src/test/app/Freeze_test.cpp @@ -1885,6 +1885,31 @@ class Freeze_test : public beast::unit_test::suite env.close(); } + // Testing A1 nft buy offer when A2 deep frozen by issuer + if (features[featureDeepFreeze] && + features[fixEnforceNFTokenTrustlineV2]) + { + env(trust(G1, A2["USD"](0), tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + uint256 const nftID{token::getNextID(env, A2, 0u, tfTransferable)}; + env(token::mint(A2, 0), txflags(tfTransferable)); + env.close(); + + auto const buyIdx = keylet::nftoffer(A1, env.seq(A1)).key; + env(token::createOffer(A1, nftID, USD(10)), token::owner(A2)); + env.close(); + + env(token::acceptBuyOffer(A2, buyIdx), ter(tecFROZEN)); + env.close(); + + env(trust(G1, A2["USD"](0), tfClearFreeze | tfClearDeepFreeze)); + env.close(); + + env(token::acceptBuyOffer(A2, buyIdx)); + env.close(); + } + // Testing A2 nft offer sell when A2 frozen by currency holder { auto const sellOfferIndex = createNFTSellOffer(env, A2, USD(10)); @@ -1944,6 +1969,68 @@ class Freeze_test : public beast::unit_test::suite env(trust(A2, limit, tfClearFreeze | tfClearDeepFreeze)); env.close(); } + + // Testing brokered offer acceptance + if (features[featureDeepFreeze] && + features[fixEnforceNFTokenTrustlineV2]) + { + Account broker{"broker"}; + env.fund(XRP(10000), broker); + env.close(); + env(trust(G1, broker["USD"](1000), tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + uint256 const nftID{token::getNextID(env, A2, 0u, tfTransferable)}; + env(token::mint(A2, 0), txflags(tfTransferable)); + env.close(); + + uint256 const sellIdx = keylet::nftoffer(A2, env.seq(A2)).key; + env(token::createOffer(A2, nftID, USD(10)), txflags(tfSellNFToken)); + env.close(); + auto const buyIdx = keylet::nftoffer(A1, env.seq(A1)).key; + env(token::createOffer(A1, nftID, USD(11)), token::owner(A2)); + env.close(); + + env(token::brokerOffers(broker, buyIdx, sellIdx), + token::brokerFee(USD(1)), + ter(tecFROZEN)); + env.close(); + } + + // Testing transfer fee + if (features[featureDeepFreeze] && + features[fixEnforceNFTokenTrustlineV2]) + { + Account minter{"minter"}; + env.fund(XRP(10000), minter); + env.close(); + env(trust(G1, minter["USD"](1000))); + env.close(); + + uint256 const nftID{ + token::getNextID(env, minter, 0u, tfTransferable, 1u)}; + env(token::mint(minter, 0), + token::xferFee(1u), + txflags(tfTransferable)); + env.close(); + + uint256 const minterSellIdx = + keylet::nftoffer(minter, env.seq(minter)).key; + env(token::createOffer(minter, nftID, drops(1)), + txflags(tfSellNFToken)); + env.close(); + env(token::acceptSellOffer(A2, minterSellIdx)); + env.close(); + + uint256 const sellIdx = keylet::nftoffer(A2, env.seq(A2)).key; + env(token::createOffer(A2, nftID, USD(100)), + txflags(tfSellNFToken)); + env.close(); + env(trust(G1, minter["USD"](1000), tfSetFreeze | tfSetDeepFreeze)); + env.close(); + env(token::acceptSellOffer(A1, sellIdx), ter(tecFROZEN)); + env.close(); + } } // Helper function to extract trustline flags from open ledger @@ -2021,10 +2108,16 @@ public: using namespace test::jtx; auto const sa = supported_amendments(); testAll( - sa - featureFlowCross - featureDeepFreeze - featurePermissionedDEX); - testAll(sa - featureFlowCross - featurePermissionedDEX); - testAll(sa - featureDeepFreeze - featurePermissionedDEX); - testAll(sa - featurePermissionedDEX); + sa - featureFlowCross - featureDeepFreeze - featurePermissionedDEX - + fixEnforceNFTokenTrustlineV2); + testAll( + sa - featureFlowCross - featurePermissionedDEX - + fixEnforceNFTokenTrustlineV2); + testAll( + sa - featureDeepFreeze - featurePermissionedDEX - + fixEnforceNFTokenTrustlineV2); + testAll(sa - featurePermissionedDEX - fixEnforceNFTokenTrustlineV2); + testAll(sa - fixEnforceNFTokenTrustlineV2); testAll(sa); } }; diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index a6055d85f6..deee217aa8 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -1694,15 +1694,6 @@ class MPToken_test : public beast::unit_test::suite jv[jss::SendMax] = mpt.getJson(JsonOptions::none); test(jv, jss::SendMax.c_str()); } - // EscrowCreate - { - Json::Value jv; - jv[jss::TransactionType] = jss::EscrowCreate; - jv[jss::Account] = alice.human(); - jv[jss::Destination] = carol.human(); - jv[jss::Amount] = mpt.getJson(JsonOptions::none); - test(jv, jss::Amount.c_str()); - } // OfferCreate { Json::Value jv = offer(alice, USD(100), mpt); diff --git a/src/test/app/NFTokenAuth_test.cpp b/src/test/app/NFTokenAuth_test.cpp new file mode 100644 index 0000000000..9558a03f7a --- /dev/null +++ b/src/test/app/NFTokenAuth_test.cpp @@ -0,0 +1,624 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 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 + +namespace ripple { + +class NFTokenAuth_test : public beast::unit_test::suite +{ + auto + mintAndOfferNFT( + test::jtx::Env& env, + test::jtx::Account const& account, + test::jtx::PrettyAmount const& currency, + uint32_t xfee = 0u) + { + using namespace test::jtx; + auto const nftID{ + token::getNextID(env, account, 0u, tfTransferable, xfee)}; + env(token::mint(account, 0), + token::xferFee(xfee), + txflags(tfTransferable)); + env.close(); + + auto const sellIdx = keylet::nftoffer(account, env.seq(account)).key; + env(token::createOffer(account, nftID, currency), + txflags(tfSellNFToken)); + env.close(); + + return std::make_tuple(nftID, sellIdx); + } + +public: + void + testBuyOffer_UnauthorizedSeller(FeatureBitset features) + { + testcase("Unauthorized seller tries to accept buy offer"); + using namespace test::jtx; + + Env env(*this, features); + Account G1{"G1"}; + Account A1{"A1"}; + Account A2{"A2"}; + auto const USD{G1["USD"]}; + + env.fund(XRP(10000), G1, A1, A2); + env(fset(G1, asfRequireAuth)); + env.close(); + + auto const limit = USD(10000); + + env(trust(A1, limit)); + env(trust(G1, limit, A1, tfSetfAuth)); + env(pay(G1, A1, USD(1000))); + + auto const [nftID, _] = mintAndOfferNFT(env, A2, drops(1)); + auto const buyIdx = keylet::nftoffer(A1, env.seq(A1)).key; + + // It should be possible to create a buy offer even if NFT owner is not + // authorized + env(token::createOffer(A1, nftID, USD(10)), token::owner(A2)); + + if (features[fixEnforceNFTokenTrustlineV2]) + { + // test: G1 requires authorization of A2, no trust line exists + env(token::acceptBuyOffer(A2, buyIdx), ter(tecNO_LINE)); + env.close(); + + // trust line created, but not authorized + env(trust(A2, limit)); + + // test: G1 requires authorization of A2 + env(token::acceptBuyOffer(A2, buyIdx), ter(tecNO_AUTH)); + env.close(); + } + else + { + // Old behavior: it is possible to sell tokens and receive IOUs + // without the authorization + env(token::acceptBuyOffer(A2, buyIdx)); + env.close(); + + BEAST_EXPECT(env.balance(A2, USD) == USD(10)); + } + } + + void + testCreateBuyOffer_UnauthorizedBuyer(FeatureBitset features) + { + testcase("Unauthorized buyer tries to create buy offer"); + using namespace test::jtx; + + Env env(*this, features); + Account G1{"G1"}; + Account A1{"A1"}; + Account A2{"A2"}; + auto const USD{G1["USD"]}; + + env.fund(XRP(10000), G1, A1, A2); + env(fset(G1, asfRequireAuth)); + env.close(); + + auto const [nftID, _] = mintAndOfferNFT(env, A2, drops(1)); + + // test: check that buyer can't make an offer if they're not authorized. + env(token::createOffer(A1, nftID, USD(10)), + token::owner(A2), + ter(tecUNFUNDED_OFFER)); + env.close(); + + // Artificially create an unauthorized trustline with balance. Don't + // close ledger before running the actual tests against this trustline. + // After ledger is closed, the trustline will not exist. + auto const unauthTrustline = [&](OpenView& view, + beast::Journal) -> bool { + auto const sleA1 = + std::make_shared(keylet::line(A1, G1, G1["USD"].currency)); + sleA1->setFieldAmount(sfBalance, A1["USD"](-1000)); + view.rawInsert(sleA1); + return true; + }; + env.app().openLedger().modify(unauthTrustline); + + if (features[fixEnforceNFTokenTrustlineV2]) + { + // test: check that buyer can't make an offer even with balance + env(token::createOffer(A1, nftID, USD(10)), + token::owner(A2), + ter(tecNO_AUTH)); + } + else + { + // old behavior: can create an offer if balance allows, regardless + // ot authorization + env(token::createOffer(A1, nftID, USD(10)), token::owner(A2)); + } + } + + void + testAcceptBuyOffer_UnauthorizedBuyer(FeatureBitset features) + { + testcase("Seller tries to accept buy offer from unauth buyer"); + using namespace test::jtx; + + Env env(*this, features); + Account G1{"G1"}; + Account A1{"A1"}; + Account A2{"A2"}; + auto const USD{G1["USD"]}; + + env.fund(XRP(10000), G1, A1, A2); + env(fset(G1, asfRequireAuth)); + env.close(); + + auto const limit = USD(10000); + + auto const [nftID, _] = mintAndOfferNFT(env, A2, drops(1)); + + // First we authorize buyer and seller so that he can create buy offer + env(trust(A1, limit)); + env(trust(G1, limit, A1, tfSetfAuth)); + env(pay(G1, A1, USD(10))); + env(trust(A2, limit)); + env(trust(G1, limit, A2, tfSetfAuth)); + env(pay(G1, A2, USD(10))); + env.close(); + + auto const buyIdx = keylet::nftoffer(A1, env.seq(A1)).key; + env(token::createOffer(A1, nftID, USD(10)), token::owner(A2)); + env.close(); + + env(pay(A1, G1, USD(10))); + env(trust(A1, USD(0))); + env(trust(G1, A1["USD"](0))); + env.close(); + + // Replace an existing authorized trustline with artificial unauthorized + // trustline with balance. Don't close ledger before running the actual + // tests against this trustline. After ledger is closed, the trustline + // will not exist. + auto const unauthTrustline = [&](OpenView& view, + beast::Journal) -> bool { + auto const sleA1 = + std::make_shared(keylet::line(A1, G1, G1["USD"].currency)); + sleA1->setFieldAmount(sfBalance, A1["USD"](-1000)); + view.rawInsert(sleA1); + return true; + }; + env.app().openLedger().modify(unauthTrustline); + if (features[fixEnforceNFTokenTrustlineV2]) + { + // test: check that offer can't be accepted even with balance + env(token::acceptBuyOffer(A2, buyIdx), ter(tecNO_AUTH)); + } + } + + void + testSellOffer_UnauthorizedSeller(FeatureBitset features) + { + testcase( + "Authorized buyer tries to accept sell offer from unauthorized " + "seller"); + using namespace test::jtx; + + Env env(*this, features); + Account G1{"G1"}; + Account A1{"A1"}; + Account A2{"A2"}; + auto const USD{G1["USD"]}; + + env.fund(XRP(10000), G1, A1, A2); + env(fset(G1, asfRequireAuth)); + env.close(); + + auto const limit = USD(10000); + + env(trust(A1, limit)); + env(trust(G1, limit, A1, tfSetfAuth)); + env(pay(G1, A1, USD(1000))); + + auto const [nftID, _] = mintAndOfferNFT(env, A2, drops(1)); + if (features[fixEnforceNFTokenTrustlineV2]) + { + // test: can't create sell offer if there is no trustline but auth + // required + env(token::createOffer(A2, nftID, USD(10)), + txflags(tfSellNFToken), + ter(tecNO_LINE)); + + env(trust(A2, limit)); + // test: can't create sell offer if not authorized to hold token + env(token::createOffer(A2, nftID, USD(10)), + txflags(tfSellNFToken), + ter(tecNO_AUTH)); + + // Authorizing trustline to make an offer creation possible + env(trust(G1, USD(0), A2, tfSetfAuth)); + env.close(); + auto const sellIdx = keylet::nftoffer(A2, env.seq(A2)).key; + env(token::createOffer(A2, nftID, USD(10)), txflags(tfSellNFToken)); + env.close(); + // + + // Reseting trustline to delete it. This allows to check if + // already existing offers handled correctly + env(trust(A2, USD(0))); + env.close(); + + // test: G1 requires authorization of A1, no trust line exists + env(token::acceptSellOffer(A1, sellIdx), ter(tecNO_LINE)); + env.close(); + + // trust line created, but not authorized + env(trust(A2, limit)); + env.close(); + + // test: G1 requires authorization of A1 + env(token::acceptSellOffer(A1, sellIdx), ter(tecNO_AUTH)); + env.close(); + } + else + { + auto const sellIdx = keylet::nftoffer(A2, env.seq(A2)).key; + + // Old behavior: sell offer can be created without authorization + env(token::createOffer(A2, nftID, USD(10)), txflags(tfSellNFToken)); + env.close(); + + // Old behavior: it is possible to sell NFT and receive IOUs + // without the authorization + env(token::acceptSellOffer(A1, sellIdx)); + env.close(); + + BEAST_EXPECT(env.balance(A2, USD) == USD(10)); + } + } + + void + testSellOffer_UnauthorizedBuyer(FeatureBitset features) + { + testcase("Unauthorized buyer tries to accept sell offer"); + using namespace test::jtx; + + Env env(*this, features); + Account G1{"G1"}; + Account A1{"A1"}; + Account A2{"A2"}; + auto const USD{G1["USD"]}; + + env.fund(XRP(10000), G1, A1, A2); + env(fset(G1, asfRequireAuth)); + env.close(); + + auto const limit = USD(10000); + + env(trust(A2, limit)); + env(trust(G1, limit, A2, tfSetfAuth)); + + auto const [_, sellIdx] = mintAndOfferNFT(env, A2, USD(10)); + + // test: check that buyer can't accept an offer if they're not + // authorized. + env(token::acceptSellOffer(A1, sellIdx), ter(tecINSUFFICIENT_FUNDS)); + env.close(); + + // Creating an artificial unauth trustline + auto const unauthTrustline = [&](OpenView& view, + beast::Journal) -> bool { + auto const sleA1 = + std::make_shared(keylet::line(A1, G1, G1["USD"].currency)); + sleA1->setFieldAmount(sfBalance, A1["USD"](-1000)); + view.rawInsert(sleA1); + return true; + }; + env.app().openLedger().modify(unauthTrustline); + if (features[fixEnforceNFTokenTrustlineV2]) + { + env(token::acceptSellOffer(A1, sellIdx), ter(tecNO_AUTH)); + } + } + + void + testBrokeredAcceptOffer_UnauthorizedBroker(FeatureBitset features) + { + testcase("Unauthorized broker bridges authorized buyer and seller."); + using namespace test::jtx; + + Env env(*this, features); + Account G1{"G1"}; + Account A1{"A1"}; + Account A2{"A2"}; + Account broker{"broker"}; + auto const USD{G1["USD"]}; + + env.fund(XRP(10000), G1, A1, A2, broker); + env(fset(G1, asfRequireAuth)); + env.close(); + + auto const limit = USD(10000); + + env(trust(A1, limit)); + env(trust(G1, limit, A1, tfSetfAuth)); + env(pay(G1, A1, USD(1000))); + env(trust(A2, limit)); + env(trust(G1, limit, A2, tfSetfAuth)); + env(pay(G1, A2, USD(1000))); + env.close(); + + auto const [nftID, sellIdx] = mintAndOfferNFT(env, A2, USD(10)); + auto const buyIdx = keylet::nftoffer(A1, env.seq(A1)).key; + env(token::createOffer(A1, nftID, USD(11)), token::owner(A2)); + env.close(); + + if (features[fixEnforceNFTokenTrustlineV2]) + { + // test: G1 requires authorization of broker, no trust line exists + env(token::brokerOffers(broker, buyIdx, sellIdx), + token::brokerFee(USD(1)), + ter(tecNO_LINE)); + env.close(); + + // trust line created, but not authorized + env(trust(broker, limit)); + env.close(); + + // test: G1 requires authorization of broker + env(token::brokerOffers(broker, buyIdx, sellIdx), + token::brokerFee(USD(1)), + ter(tecNO_AUTH)); + env.close(); + + // test: can still be brokered without broker fee. + env(token::brokerOffers(broker, buyIdx, sellIdx)); + env.close(); + } + else + { + // Old behavior: broker can receive IOUs without the authorization + env(token::brokerOffers(broker, buyIdx, sellIdx), + token::brokerFee(USD(1))); + env.close(); + + BEAST_EXPECT(env.balance(broker, USD) == USD(1)); + } + } + + void + testBrokeredAcceptOffer_UnauthorizedBuyer(FeatureBitset features) + { + testcase( + "Authorized broker tries to bridge offers from unauthorized " + "buyer."); + using namespace test::jtx; + + Env env(*this, features); + Account G1{"G1"}; + Account A1{"A1"}; + Account A2{"A2"}; + Account broker{"broker"}; + auto const USD{G1["USD"]}; + + env.fund(XRP(10000), G1, A1, A2, broker); + env(fset(G1, asfRequireAuth)); + env.close(); + + auto const limit = USD(10000); + + env(trust(A1, limit)); + env(trust(G1, USD(0), A1, tfSetfAuth)); + env(pay(G1, A1, USD(1000))); + env(trust(A2, limit)); + env(trust(G1, USD(0), A2, tfSetfAuth)); + env(pay(G1, A2, USD(1000))); + env(trust(broker, limit)); + env(trust(G1, USD(0), broker, tfSetfAuth)); + env(pay(G1, broker, USD(1000))); + env.close(); + + auto const [nftID, sellIdx] = mintAndOfferNFT(env, A2, USD(10)); + auto const buyIdx = keylet::nftoffer(A1, env.seq(A1)).key; + env(token::createOffer(A1, nftID, USD(11)), token::owner(A2)); + env.close(); + + // Resetting buyer's trust line to delete it + env(pay(A1, G1, USD(1000))); + env(trust(A1, USD(0))); + env.close(); + + auto const unauthTrustline = [&](OpenView& view, + beast::Journal) -> bool { + auto const sleA1 = + std::make_shared(keylet::line(A1, G1, G1["USD"].currency)); + sleA1->setFieldAmount(sfBalance, A1["USD"](-1000)); + view.rawInsert(sleA1); + return true; + }; + env.app().openLedger().modify(unauthTrustline); + + if (features[fixEnforceNFTokenTrustlineV2]) + { + // test: G1 requires authorization of A2 + env(token::brokerOffers(broker, buyIdx, sellIdx), + token::brokerFee(USD(1)), + ter(tecNO_AUTH)); + env.close(); + } + } + + void + testBrokeredAcceptOffer_UnauthorizedSeller(FeatureBitset features) + { + testcase( + "Authorized broker tries to bridge offers from unauthorized " + "seller."); + using namespace test::jtx; + + Env env(*this, features); + Account G1{"G1"}; + Account A1{"A1"}; + Account A2{"A2"}; + Account broker{"broker"}; + auto const USD{G1["USD"]}; + + env.fund(XRP(10000), G1, A1, A2, broker); + env(fset(G1, asfRequireAuth)); + env.close(); + + auto const limit = USD(10000); + + env(trust(A1, limit)); + env(trust(G1, limit, A1, tfSetfAuth)); + env(pay(G1, A1, USD(1000))); + env(trust(broker, limit)); + env(trust(G1, limit, broker, tfSetfAuth)); + env(pay(G1, broker, USD(1000))); + env.close(); + + // Authorizing trustline to make an offer creation possible + env(trust(G1, USD(0), A2, tfSetfAuth)); + env.close(); + + auto const [nftID, sellIdx] = mintAndOfferNFT(env, A2, USD(10)); + auto const buyIdx = keylet::nftoffer(A1, env.seq(A1)).key; + env(token::createOffer(A1, nftID, USD(11)), token::owner(A2)); + env.close(); + + // Reseting trustline to delete it. This allows to check if + // already existing offers handled correctly + env(trust(A2, USD(0))); + env.close(); + + if (features[fixEnforceNFTokenTrustlineV2]) + { + // test: G1 requires authorization of broker, no trust line exists + env(token::brokerOffers(broker, buyIdx, sellIdx), + token::brokerFee(USD(1)), + ter(tecNO_LINE)); + env.close(); + + // trust line created, but not authorized + env(trust(A2, limit)); + env.close(); + + // test: G1 requires authorization of A2 + env(token::brokerOffers(broker, buyIdx, sellIdx), + token::brokerFee(USD(1)), + ter(tecNO_AUTH)); + env.close(); + + // test: cannot be brokered even without broker fee. + env(token::brokerOffers(broker, buyIdx, sellIdx), ter(tecNO_AUTH)); + env.close(); + } + else + { + // Old behavior: broker can receive IOUs without the authorization + env(token::brokerOffers(broker, buyIdx, sellIdx), + token::brokerFee(USD(1))); + env.close(); + + BEAST_EXPECT(env.balance(A2, USD) == USD(10)); + return; + } + } + + void + testTransferFee_UnauthorizedMinter(FeatureBitset features) + { + testcase("Unauthorized minter receives transfer fee."); + using namespace test::jtx; + + Env env(*this, features); + Account G1{"G1"}; + Account minter{"minter"}; + Account A1{"A1"}; + Account A2{"A2"}; + auto const USD{G1["USD"]}; + + env.fund(XRP(10000), G1, minter, A1, A2); + env(fset(G1, asfRequireAuth)); + env.close(); + + auto const limit = USD(10000); + + env(trust(A1, limit)); + env(trust(G1, limit, A1, tfSetfAuth)); + env(pay(G1, A1, USD(1000))); + env(trust(A2, limit)); + env(trust(G1, limit, A2, tfSetfAuth)); + env(pay(G1, A2, USD(1000))); + + env(trust(minter, limit)); + env.close(); + + // We authorized A1 and A2, but not the minter. + // Now mint NFT + auto const [nftID, minterSellIdx] = + mintAndOfferNFT(env, minter, drops(1), 1); + env(token::acceptSellOffer(A1, minterSellIdx)); + + uint256 const sellIdx = keylet::nftoffer(A1, env.seq(A1)).key; + env(token::createOffer(A1, nftID, USD(100)), txflags(tfSellNFToken)); + + if (features[fixEnforceNFTokenTrustlineV2]) + { + // test: G1 requires authorization + env(token::acceptSellOffer(A2, sellIdx), ter(tecNO_AUTH)); + env.close(); + } + else + { + // Old behavior: can sell for USD. Minter can receive tokens + env(token::acceptSellOffer(A2, sellIdx)); + env.close(); + + BEAST_EXPECT(env.balance(minter, USD) == USD(0.001)); + } + } + + void + run() override + { + using namespace test::jtx; + static FeatureBitset const all{supported_amendments()}; + + static std::array const features = { + all - fixEnforceNFTokenTrustlineV2, all}; + + for (auto const feature : features) + { + testBuyOffer_UnauthorizedSeller(feature); + testCreateBuyOffer_UnauthorizedBuyer(feature); + testAcceptBuyOffer_UnauthorizedBuyer(feature); + testSellOffer_UnauthorizedSeller(feature); + testSellOffer_UnauthorizedBuyer(feature); + testBrokeredAcceptOffer_UnauthorizedBroker(feature); + testBrokeredAcceptOffer_UnauthorizedBuyer(feature); + testBrokeredAcceptOffer_UnauthorizedSeller(feature); + testTransferFee_UnauthorizedMinter(feature); + } + } +}; + +BEAST_DEFINE_TESTSUITE_PRIO(NFTokenAuth, tx, ripple, 2); + +} // namespace ripple \ No newline at end of file diff --git a/src/test/jtx.h b/src/test/jtx.h index fa67780cbd..4188910085 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/jtx/AMM.h b/src/test/jtx/AMM.h index 71e2e5f34c..07e60369fe 100644 --- a/src/test/jtx/AMM.h +++ b/src/test/jtx/AMM.h @@ -127,7 +127,6 @@ class AMM STAmount const asset1_; STAmount const asset2_; uint256 const ammID_; - IOUAmount const initialLPTokens_; bool log_; bool doClose_; // Predict next purchase price @@ -140,6 +139,7 @@ class AMM std::uint32_t const fee_; AccountID const ammAccount_; Issue const lptIssue_; + IOUAmount const initialLPTokens_; public: AMM(Env& env, @@ -196,6 +196,12 @@ public: Issue const& issue2, std::optional const& account = std::nullopt) const; + std::tuple + balances(std::optional const& account = std::nullopt) const + { + return balances(asset1_.get(), asset2_.get(), account); + } + [[nodiscard]] bool expectLPTokens(AccountID const& account, IOUAmount const& tokens) const; @@ -430,6 +436,9 @@ private: [[nodiscard]] bool expectAuctionSlot(auto&& cb) const; + + IOUAmount + initialTokens(); }; namespace amm { diff --git a/src/test/jtx/AMMTest.h b/src/test/jtx/AMMTest.h index 5ff2d21a19..28b9affa8f 100644 --- a/src/test/jtx/AMMTest.h +++ b/src/test/jtx/AMMTest.h @@ -35,6 +35,15 @@ class AMM; enum class Fund { All, Acct, Gw, IOUOnly }; +struct TestAMMArg +{ + std::optional> pool = std::nullopt; + std::uint16_t tfee = 0; + std::optional ter = std::nullopt; + std::vector features = {supported_amendments()}; + bool noLog = false; +}; + void fund( jtx::Env& env, @@ -87,6 +96,11 @@ protected: std::uint16_t tfee = 0, std::optional const& ter = std::nullopt, std::vector const& features = {supported_amendments()}); + + void + testAMM( + std::function&& cb, + TestAMMArg const& arg); }; class AMMTest : public jtx::AMMTestBase diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index 452f504374..266a6dea43 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -478,6 +478,12 @@ public: PrettyAmount balance(Account const& account, MPTIssue const& mptIssue) const; + /** Returns the IOU limit on an account. + Returns 0 if the trust line does not exist. + */ + PrettyAmount + limit(Account const& account, Issue const& issue) const; + /** Return the number of objects owned by an account. * Returns 0 if the account does not exist. */ @@ -628,6 +634,12 @@ public: void disableFeature(uint256 const feature); + bool + enabled(uint256 feature) const + { + return current()->rules().enabled(feature); + } + private: void fund(bool setDefaultRipple, STAmount const& amount, Account const& account); diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index 7b1a249a88..f7f7a601fc 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -500,55 +500,6 @@ expectLedgerEntryRoot( Account const& acct, STAmount const& expectedValue); -/* Escrow */ -/******************************************************************************/ - -Json::Value -escrow(AccountID const& account, AccountID const& to, STAmount const& amount); - -inline Json::Value -escrow(Account const& account, Account const& to, STAmount const& amount) -{ - return escrow(account.id(), to.id(), amount); -} - -Json::Value -finish(AccountID const& account, AccountID const& from, std::uint32_t seq); - -inline Json::Value -finish(Account const& account, Account const& from, std::uint32_t seq) -{ - return finish(account.id(), from.id(), seq); -} - -Json::Value -cancel(AccountID const& account, Account const& from, std::uint32_t seq); - -inline Json::Value -cancel(Account const& account, Account const& from, std::uint32_t seq) -{ - return cancel(account.id(), from, seq); -} - -std::array constexpr 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}}; - -// A PreimageSha256 fulfillments and its associated condition. -std::array const fb1 = {{0xA0, 0x02, 0x80, 0x00}}; - -/** Set the "FinishAfter" time tag on a JTx */ -auto const finish_time = JTxFieldWrapper(sfFinishAfter); - -/** Set the "CancelAfter" time tag on a JTx */ -auto const cancel_time = JTxFieldWrapper(sfCancelAfter); - -auto const condition = JTxFieldWrapper(sfCondition); - -auto const fulfillment = JTxFieldWrapper(sfFulfillment); - /* Payment Channel */ /******************************************************************************/ diff --git a/src/test/jtx/escrow.h b/src/test/jtx/escrow.h new file mode 100644 index 0000000000..483db578b0 --- /dev/null +++ b/src/test/jtx/escrow.h @@ -0,0 +1,114 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2019 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. +*/ +//============================================================================== + +#ifndef RIPPLE_TEST_JTX_ESCROW_H_INCLUDED +#define RIPPLE_TEST_JTX_ESCROW_H_INCLUDED + +#include +#include +#include +#include +#include + +#include + +namespace ripple { +namespace test { +namespace jtx { + +/** Escrow operations. */ +namespace escrow { + +Json::Value +create(AccountID const& account, AccountID const& to, STAmount const& amount); + +inline Json::Value +create(Account const& account, Account const& to, STAmount const& amount) +{ + return create(account.id(), to.id(), amount); +} + +Json::Value +finish(AccountID const& account, AccountID const& from, std::uint32_t seq); + +inline Json::Value +finish(Account const& account, Account const& from, std::uint32_t seq) +{ + return finish(account.id(), from.id(), seq); +} + +Json::Value +cancel(AccountID const& account, Account const& from, std::uint32_t seq); + +inline Json::Value +cancel(Account const& account, Account const& from, std::uint32_t seq) +{ + return cancel(account.id(), from, seq); +} + +Rate +rate(Env& env, Account const& account, std::uint32_t const& seq); + +// 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 */ +auto const finish_time = JTxFieldWrapper(sfFinishAfter); + +/** Set the "CancelAfter" time tag on a JTx */ +auto const cancel_time = JTxFieldWrapper(sfCancelAfter); + +auto const condition = JTxFieldWrapper(sfCondition); + +auto const fulfillment = JTxFieldWrapper(sfFulfillment); + +} // namespace escrow + +} // namespace jtx + +} // namespace test +} // namespace ripple + +#endif diff --git a/src/test/jtx/flags.h b/src/test/jtx/flags.h index 4adc75c6a8..8d3fa4f25c 100644 --- a/src/test/jtx/flags.h +++ b/src/test/jtx/flags.h @@ -80,6 +80,9 @@ private: case asfDisallowIncomingTrustline: mask_ |= lsfDisallowIncomingTrustline; break; + case asfAllowTrustLineLocking: + mask_ |= lsfAllowTrustLineLocking; + break; default: Throw("unknown flag"); } diff --git a/src/test/jtx/impl/AMM.cpp b/src/test/jtx/impl/AMM.cpp index 6345253584..ca96401bc4 100644 --- a/src/test/jtx/impl/AMM.cpp +++ b/src/test/jtx/impl/AMM.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -39,12 +40,16 @@ number(STAmount const& a) return a; } -static IOUAmount -initialTokens(STAmount const& asset1, STAmount const& asset2) +IOUAmount +AMM::initialTokens() { - auto const product = number(asset1) * number(asset2); - return (IOUAmount)(product.mantissa() >= 0 ? root2(product) - : root2(-product)); + if (!env_.enabled(fixAMMv1_3)) + { + auto const product = number(asset1_) * number(asset2_); + return (IOUAmount)(product.mantissa() >= 0 ? root2(product) + : root2(-product)); + } + return getLPTokensBalance(); } AMM::AMM( @@ -65,7 +70,6 @@ AMM::AMM( , asset1_(asset1) , asset2_(asset2) , ammID_(keylet::amm(asset1_.issue(), asset2_.issue()).key) - , initialLPTokens_(initialTokens(asset1, asset2)) , log_(log) , doClose_(close) , lastPurchasePrice_(0) @@ -78,6 +82,7 @@ AMM::AMM( asset1_.issue().currency, asset2_.issue().currency, ammAccount_)) + , initialLPTokens_(initialTokens()) { } diff --git a/src/test/jtx/impl/AMMTest.cpp b/src/test/jtx/impl/AMMTest.cpp index 8555be01a9..5bb8f14cbf 100644 --- a/src/test/jtx/impl/AMMTest.cpp +++ b/src/test/jtx/impl/AMMTest.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -105,15 +106,31 @@ AMMTestBase::testAMM( std::uint16_t tfee, std::optional const& ter, std::vector const& vfeatures) +{ + testAMM( + std::move(cb), + TestAMMArg{ + .pool = pool, .tfee = tfee, .ter = ter, .features = vfeatures}); +} + +void +AMMTestBase::testAMM( + std::function&& cb, + TestAMMArg const& arg) { using namespace jtx; - for (auto const& features : vfeatures) + std::string logs; + + for (auto const& features : arg.features) { - Env env{*this, features}; + Env env{ + *this, + features, + arg.noLog ? std::make_unique(&logs) : nullptr}; auto const [asset1, asset2] = - pool ? *pool : std::make_pair(XRP(10000), USD(10000)); + arg.pool ? *arg.pool : std::make_pair(XRP(10000), USD(10000)); auto tofund = [&](STAmount const& a) -> STAmount { if (a.native()) { @@ -143,7 +160,7 @@ AMMTestBase::testAMM( alice, asset1, asset2, - CreateArg{.log = false, .tfee = tfee, .err = ter}); + CreateArg{.log = false, .tfee = arg.tfee, .err = arg.ter}); if (BEAST_EXPECT( ammAlice.expectBalances(asset1, asset2, ammAlice.tokens()))) cb(ammAlice, env); diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index a6b17532dd..6c98a5a20a 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -203,13 +203,31 @@ Env::balance(Account const& account, Issue const& issue) const PrettyAmount Env::balance(Account const& account, MPTIssue const& mptIssue) const { - auto const sle = le(keylet::mptoken(mptIssue.getMptID(), account)); - if (!sle) - { + MPTID const id = mptIssue.getMptID(); + if (!id) return {STAmount(mptIssue, 0), account.name()}; + + AccountID const issuer = mptIssue.getIssuer(); + if (account.id() == issuer) + { + // Issuer balance + auto const sle = le(keylet::mptIssuance(id)); + if (!sle) + return {STAmount(mptIssue, 0), account.name()}; + + STAmount const amount{mptIssue, sle->getFieldU64(sfOutstandingAmount)}; + return {amount, lookup(issuer).name()}; + } + else + { + // Holder balance + auto const sle = le(keylet::mptoken(id, account)); + if (!sle) + return {STAmount(mptIssue, 0), account.name()}; + + STAmount const amount{mptIssue, sle->getFieldU64(sfMPTAmount)}; + return {amount, lookup(issuer).name()}; } - STAmount const amount{mptIssue, sle->getFieldU64(sfMPTAmount)}; - return {amount, lookup(mptIssue.getIssuer()).name()}; } PrettyAmount @@ -220,6 +238,18 @@ Env::balance(Account const& account, Asset const& asset) const asset.value()); } +PrettyAmount +Env::limit(Account const& account, Issue const& issue) const +{ + auto const sle = le(keylet::line(account.id(), issue)); + if (!sle) + return {STAmount(issue, 0), account.name()}; + auto const aHigh = account.id() > issue.account; + if (sle && sle->isFieldPresent(aHigh ? sfLowLimit : sfHighLimit)) + return {(*sle)[aHigh ? sfLowLimit : sfHighLimit], account.name()}; + return {STAmount(issue, 0), account.name()}; +} + std::uint32_t Env::ownerCount(Account const& account) const { diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index 4c0c77dc1c..add8cde25d 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -231,42 +231,6 @@ expectLedgerEntryRoot( return accountBalance(env, acct) == to_string(expectedValue.xrp()); } -/* Escrow */ -/******************************************************************************/ - -Json::Value -escrow(AccountID const& account, AccountID const& to, STAmount const& amount) -{ - Json::Value jv; - jv[jss::TransactionType] = jss::EscrowCreate; - jv[jss::Account] = to_string(account); - jv[jss::Destination] = to_string(to); - jv[jss::Amount] = amount.getJson(JsonOptions::none); - return jv; -} - -Json::Value -finish(AccountID const& account, AccountID const& from, std::uint32_t seq) -{ - Json::Value jv; - jv[jss::TransactionType] = jss::EscrowFinish; - jv[jss::Account] = to_string(account); - jv[sfOwner.jsonName] = to_string(from); - jv[sfOfferSequence.jsonName] = seq; - return jv; -} - -Json::Value -cancel(AccountID const& account, Account const& from, std::uint32_t seq) -{ - Json::Value jv; - jv[jss::TransactionType] = jss::EscrowCancel; - jv[jss::Account] = to_string(account); - jv[sfOwner.jsonName] = from.human(); - jv[sfOfferSequence.jsonName] = seq; - return jv; -} - /* Payment Channel */ /******************************************************************************/ Json::Value diff --git a/src/test/jtx/impl/escrow.cpp b/src/test/jtx/impl/escrow.cpp new file mode 100644 index 0000000000..a1ec6a3c5e --- /dev/null +++ b/src/test/jtx/impl/escrow.cpp @@ -0,0 +1,82 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2019 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 + +namespace ripple { +namespace test { +namespace jtx { + +/** Escrow operations. */ +namespace escrow { + +Json::Value +create(AccountID const& account, AccountID const& to, STAmount const& amount) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::EscrowCreate; + jv[jss::Flags] = tfFullyCanonicalSig; + jv[jss::Account] = to_string(account); + jv[jss::Destination] = to_string(to); + jv[jss::Amount] = amount.getJson(JsonOptions::none); + return jv; +} + +Json::Value +finish(AccountID const& account, AccountID const& from, std::uint32_t seq) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::EscrowFinish; + jv[jss::Flags] = tfFullyCanonicalSig; + jv[jss::Account] = to_string(account); + jv[sfOwner.jsonName] = to_string(from); + jv[sfOfferSequence.jsonName] = seq; + return jv; +} + +Json::Value +cancel(AccountID const& account, Account const& from, std::uint32_t seq) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::EscrowCancel; + jv[jss::Flags] = tfFullyCanonicalSig; + jv[jss::Account] = to_string(account); + jv[sfOwner.jsonName] = from.human(); + jv[sfOfferSequence.jsonName] = seq; + return jv; +} + +Rate +rate(Env& env, Account const& account, std::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}; +} + +} // namespace escrow + +} // namespace jtx + +} // namespace test +} // namespace ripple diff --git a/src/test/ledger/Invariants_test.cpp b/src/test/ledger/Invariants_test.cpp index 567d12ce22..51551f3c2b 100644 --- a/src/test/ledger/Invariants_test.cpp +++ b/src/test/ledger/Invariants_test.cpp @@ -779,21 +779,6 @@ 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( {{"XRP net change of -1000000 doesn't match fee 0"}, {"escrow specifies invalid amount"}}, diff --git a/src/test/protocol/STAmount_test.cpp b/src/test/protocol/STAmount_test.cpp index 712c91000e..d62241f2f4 100644 --- a/src/test/protocol/STAmount_test.cpp +++ b/src/test/protocol/STAmount_test.cpp @@ -17,6 +17,8 @@ */ //============================================================================== +#include + #include #include #include @@ -668,6 +670,366 @@ public: } } + void + testCanAddXRP() + { + testcase("can add xrp"); + + // Adding zero + { + STAmount amt1(XRPAmount(0)); + STAmount amt2(XRPAmount(1000)); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Adding zero + { + STAmount amt1(XRPAmount(1000)); + STAmount amt2(XRPAmount(0)); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Adding two positive XRP amounts + { + STAmount amt1(XRPAmount(500)); + STAmount amt2(XRPAmount(1500)); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Adding two negative XRP amounts + { + STAmount amt1(XRPAmount(-500)); + STAmount amt2(XRPAmount(-1500)); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Adding a positive and a negative XRP amount + { + STAmount amt1(XRPAmount(1000)); + STAmount amt2(XRPAmount(-1000)); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Overflow check for max XRP amounts + { + STAmount amt1(std::numeric_limits::max()); + STAmount amt2(XRPAmount(1)); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + + // Overflow check for min XRP amounts + { + STAmount amt1(std::numeric_limits::max()); + amt1 += XRPAmount(1); + STAmount amt2(XRPAmount(-1)); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + } + + void + testCanAddIOU() + { + testcase("can add iou"); + + Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)}; + Issue const eur{Currency(0x4555520000000000), AccountID(0x4985601)}; + + // Adding two IOU amounts + { + STAmount amt1(usd, 500); + STAmount amt2(usd, 1500); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Adding a positive and a negative IOU amount + { + STAmount amt1(usd, 1000); + STAmount amt2(usd, -1000); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Overflow check for max IOU amounts + { + STAmount amt1(usd, std::numeric_limits::max()); + STAmount amt2(usd, 1); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + + // Overflow check for min IOU amounts + { + STAmount amt1(usd, std::numeric_limits::min()); + STAmount amt2(usd, -1); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + + // Adding XRP and IOU + { + STAmount amt1(XRPAmount(1)); + STAmount amt2(usd, 1); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + + // Adding different IOU issues (non zero) + { + STAmount amt1(usd, 1000); + STAmount amt2(eur, 500); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + + // Adding different IOU issues (zero) + { + STAmount amt1(usd, 0); + STAmount amt2(eur, 500); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + } + + void + testCanAddMPT() + { + testcase("can add mpt"); + + MPTIssue const mpt{MPTIssue{makeMptID(1, AccountID(0x4985601))}}; + MPTIssue const mpt2{MPTIssue{makeMptID(2, AccountID(0x4985601))}}; + + // Adding zero + { + STAmount amt1(mpt, 0); + STAmount amt2(mpt, 1000); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Adding zero + { + STAmount amt1(mpt, 1000); + STAmount amt2(mpt, 0); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Adding two positive MPT amounts + { + STAmount amt1(mpt, 500); + STAmount amt2(mpt, 1500); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Adding two negative MPT amounts + { + STAmount amt1(mpt, -500); + STAmount amt2(mpt, -1500); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Adding a positive and a negative MPT amount + { + STAmount amt1(mpt, 1000); + STAmount amt2(mpt, -1000); + BEAST_EXPECT(canAdd(amt1, amt2) == true); + } + + // Overflow check for max MPT amounts + { + STAmount amt1( + mpt, std::numeric_limits::max()); + STAmount amt2(mpt, 1); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + + // Overflow check for min MPT amounts + // Note: Cannot check min MPT overflow because you cannot initialize the + // STAmount with a negative MPT amount. + + // Adding MPT and XRP + { + STAmount amt1(XRPAmount(1000)); + STAmount amt2(mpt, 1000); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + + // Adding different MPT issues (non zero) + { + STAmount amt1(mpt2, 500); + STAmount amt2(mpt, 500); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + + // Adding different MPT issues (non zero) + { + STAmount amt1(mpt2, 0); + STAmount amt2(mpt, 500); + BEAST_EXPECT(canAdd(amt1, amt2) == false); + } + } + + void + testCanSubtractXRP() + { + testcase("can subtract xrp"); + + // Subtracting zero + { + STAmount amt1(XRPAmount(1000)); + STAmount amt2(XRPAmount(0)); + BEAST_EXPECT(canSubtract(amt1, amt2) == true); + } + + // Subtracting zero + { + STAmount amt1(XRPAmount(0)); + STAmount amt2(XRPAmount(1000)); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + + // Subtracting two positive XRP amounts + { + STAmount amt1(XRPAmount(1500)); + STAmount amt2(XRPAmount(500)); + BEAST_EXPECT(canSubtract(amt1, amt2) == true); + } + + // Subtracting two negative XRP amounts + { + STAmount amt1(XRPAmount(-1500)); + STAmount amt2(XRPAmount(-500)); + BEAST_EXPECT(canSubtract(amt1, amt2) == true); + } + + // Subtracting a positive and a negative XRP amount + { + STAmount amt1(XRPAmount(1000)); + STAmount amt2(XRPAmount(-1000)); + BEAST_EXPECT(canSubtract(amt1, amt2) == true); + } + + // Underflow check for min XRP amounts + { + STAmount amt1(std::numeric_limits::max()); + amt1 += XRPAmount(1); + STAmount amt2(XRPAmount(1)); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + + // Overflow check for max XRP amounts + { + STAmount amt1(std::numeric_limits::max()); + STAmount amt2(XRPAmount(-1)); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + } + + void + testCanSubtractIOU() + { + testcase("can subtract iou"); + Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)}; + Issue const eur{Currency(0x4555520000000000), AccountID(0x4985601)}; + + // Subtracting two IOU amounts + { + STAmount amt1(usd, 1500); + STAmount amt2(usd, 500); + BEAST_EXPECT(canSubtract(amt1, amt2) == true); + } + + // Subtracting XRP and IOU + { + STAmount amt1(XRPAmount(1000)); + STAmount amt2(usd, 1000); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + + // Subtracting different IOU issues (non zero) + { + STAmount amt1(usd, 1000); + STAmount amt2(eur, 500); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + + // Subtracting different IOU issues (zero) + { + STAmount amt1(usd, 0); + STAmount amt2(eur, 500); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + } + + void + testCanSubtractMPT() + { + testcase("can subtract mpt"); + + MPTIssue const mpt{MPTIssue{makeMptID(1, AccountID(0x4985601))}}; + MPTIssue const mpt2{MPTIssue{makeMptID(2, AccountID(0x4985601))}}; + + // Subtracting zero + { + STAmount amt1(mpt, 1000); + STAmount amt2(mpt, 0); + BEAST_EXPECT(canSubtract(amt1, amt2) == true); + } + + // Subtracting zero + { + STAmount amt1(mpt, 0); + STAmount amt2(mpt, 1000); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + + // Subtracting two positive MPT amounts + { + STAmount amt1(mpt, 1500); + STAmount amt2(mpt, 500); + BEAST_EXPECT(canSubtract(amt1, amt2) == true); + } + + // Subtracting two negative MPT amounts + { + STAmount amt1(mpt, -1500); + STAmount amt2(mpt, -500); + BEAST_EXPECT(canSubtract(amt1, amt2) == true); + } + + // Subtracting a positive and a negative MPT amount + { + STAmount amt1(mpt, 1000); + STAmount amt2(mpt, -1000); + BEAST_EXPECT(canSubtract(amt1, amt2) == true); + } + + // Underflow check for min MPT amounts + // Note: Cannot check min MPT underflow because you cannot initialize + // the STAmount with a negative MPT amount. + + // Overflow check for max positive MPT amounts (should fail) + { + STAmount amt1( + mpt, std::numeric_limits::max()); + STAmount amt2(mpt, -2); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + + // Subtracting MPT and XRP + { + STAmount amt1(XRPAmount(1000)); + STAmount amt2(mpt, 1000); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + + // Subtracting different MPT issues (non zero) + { + STAmount amt1(mpt, 1000); + STAmount amt2(mpt2, 500); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + + // Subtracting different MPT issues (zero) + { + STAmount amt1(mpt, 0); + STAmount amt2(mpt2, 500); + BEAST_EXPECT(canSubtract(amt1, amt2) == false); + } + } + //-------------------------------------------------------------------------- void @@ -681,6 +1043,12 @@ public: testRounding(); testConvertXRP(); testConvertIOU(); + testCanAddXRP(); + testCanAddIOU(); + testCanAddMPT(); + testCanSubtractXRP(); + testCanSubtractIOU(); + testCanSubtractMPT(); } }; diff --git a/src/test/rpc/AMMInfo_test.cpp b/src/test/rpc/AMMInfo_test.cpp index a0985ea104..1c54580aa7 100644 --- a/src/test/rpc/AMMInfo_test.cpp +++ b/src/test/rpc/AMMInfo_test.cpp @@ -203,98 +203,119 @@ public: } void - testVoteAndBid() + testVoteAndBid(FeatureBitset features) { testcase("Vote and Bid"); using namespace jtx; - testAMM([&](AMM& ammAlice, Env& env) { - BEAST_EXPECT(ammAlice.expectAmmRpcInfo( - XRP(10000), USD(10000), IOUAmount{10000000, 0})); - std::unordered_map votes; - votes.insert({alice.human(), 0}); - for (int i = 0; i < 7; ++i) - { - Account a(std::to_string(i)); - votes.insert({a.human(), 50 * (i + 1)}); - fund(env, gw, {a}, {USD(10000)}, Fund::Acct); - ammAlice.deposit(a, 10000000); - ammAlice.vote(a, 50 * (i + 1)); - } - BEAST_EXPECT(ammAlice.expectTradingFee(175)); - Account ed("ed"); - Account bill("bill"); - env.fund(XRP(1000), bob, ed, bill); - env(ammAlice.bid( - {.bidMin = 100, .authAccounts = {carol, bob, ed, bill}})); - BEAST_EXPECT(ammAlice.expectAmmRpcInfo( - XRP(80000), - USD(80000), - IOUAmount{79994400}, - std::nullopt, - std::nullopt, - ammAlice.ammAccount())); - for (auto i = 0; i < 2; ++i) - { - std::unordered_set authAccounts = { - carol.human(), bob.human(), ed.human(), bill.human()}; - auto const ammInfo = i ? ammAlice.ammRpcInfo() - : ammAlice.ammRpcInfo( - std::nullopt, - std::nullopt, - std::nullopt, - std::nullopt, - ammAlice.ammAccount()); - auto const& amm = ammInfo[jss::amm]; - try + testAMM( + [&](AMM& ammAlice, Env& env) { + BEAST_EXPECT(ammAlice.expectAmmRpcInfo( + XRP(10000), USD(10000), IOUAmount{10000000, 0})); + std::unordered_map votes; + votes.insert({alice.human(), 0}); + for (int i = 0; i < 7; ++i) { - // votes - auto const voteSlots = amm[jss::vote_slots]; - auto votesCopy = votes; - for (std::uint8_t i = 0; i < 8; ++i) + Account a(std::to_string(i)); + votes.insert({a.human(), 50 * (i + 1)}); + if (!features[fixAMMv1_3]) + fund(env, gw, {a}, {USD(10000)}, Fund::Acct); + else + fund(env, gw, {a}, {USD(10001)}, Fund::Acct); + ammAlice.deposit(a, 10000000); + ammAlice.vote(a, 50 * (i + 1)); + } + BEAST_EXPECT(ammAlice.expectTradingFee(175)); + Account ed("ed"); + Account bill("bill"); + env.fund(XRP(1000), bob, ed, bill); + env(ammAlice.bid( + {.bidMin = 100, .authAccounts = {carol, bob, ed, bill}})); + if (!features[fixAMMv1_3]) + BEAST_EXPECT(ammAlice.expectAmmRpcInfo( + XRP(80000), + USD(80000), + IOUAmount{79994400}, + std::nullopt, + std::nullopt, + ammAlice.ammAccount())); + else + BEAST_EXPECT(ammAlice.expectAmmRpcInfo( + XRPAmount(80000000005), + STAmount{USD, UINT64_C(80'000'00000000005), -11}, + IOUAmount{79994400}, + std::nullopt, + std::nullopt, + ammAlice.ammAccount())); + for (auto i = 0; i < 2; ++i) + { + std::unordered_set authAccounts = { + carol.human(), bob.human(), ed.human(), bill.human()}; + auto const ammInfo = i ? ammAlice.ammRpcInfo() + : ammAlice.ammRpcInfo( + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + ammAlice.ammAccount()); + auto const& amm = ammInfo[jss::amm]; + try { - if (!BEAST_EXPECT( - votes[voteSlots[i][jss::account].asString()] == - voteSlots[i][jss::trading_fee].asUInt() && - voteSlots[i][jss::vote_weight].asUInt() == - 12500)) + // votes + auto const voteSlots = amm[jss::vote_slots]; + auto votesCopy = votes; + for (std::uint8_t i = 0; i < 8; ++i) + { + if (!BEAST_EXPECT( + votes[voteSlots[i][jss::account] + .asString()] == + voteSlots[i][jss::trading_fee] + .asUInt() && + voteSlots[i][jss::vote_weight].asUInt() == + 12500)) + return; + votes.erase(voteSlots[i][jss::account].asString()); + } + if (!BEAST_EXPECT(votes.empty())) return; - votes.erase(voteSlots[i][jss::account].asString()); - } - if (!BEAST_EXPECT(votes.empty())) - return; - votes = votesCopy; + votes = votesCopy; - // bid - auto const auctionSlot = amm[jss::auction_slot]; - for (std::uint8_t i = 0; i < 4; ++i) - { - if (!BEAST_EXPECT(authAccounts.contains( + // bid + auto const auctionSlot = amm[jss::auction_slot]; + for (std::uint8_t i = 0; i < 4; ++i) + { + if (!BEAST_EXPECT(authAccounts.contains( + auctionSlot[jss::auth_accounts][i] + [jss::account] + .asString()))) + return; + authAccounts.erase( auctionSlot[jss::auth_accounts][i][jss::account] - .asString()))) + .asString()); + } + if (!BEAST_EXPECT(authAccounts.empty())) return; - authAccounts.erase( - auctionSlot[jss::auth_accounts][i][jss::account] - .asString()); + BEAST_EXPECT( + auctionSlot[jss::account].asString() == + alice.human() && + auctionSlot[jss::discounted_fee].asUInt() == 17 && + auctionSlot[jss::price][jss::value].asString() == + "5600" && + auctionSlot[jss::price][jss::currency].asString() == + to_string(ammAlice.lptIssue().currency) && + auctionSlot[jss::price][jss::issuer].asString() == + to_string(ammAlice.lptIssue().account)); + } + catch (std::exception const& e) + { + fail(e.what(), __FILE__, __LINE__); } - if (!BEAST_EXPECT(authAccounts.empty())) - return; - BEAST_EXPECT( - auctionSlot[jss::account].asString() == alice.human() && - auctionSlot[jss::discounted_fee].asUInt() == 17 && - auctionSlot[jss::price][jss::value].asString() == - "5600" && - auctionSlot[jss::price][jss::currency].asString() == - to_string(ammAlice.lptIssue().currency) && - auctionSlot[jss::price][jss::issuer].asString() == - to_string(ammAlice.lptIssue().account)); } - catch (std::exception const& e) - { - fail(e.what(), __FILE__, __LINE__); - } - } - }); + }, + std::nullopt, + 0, + std::nullopt, + {features}); } void @@ -337,9 +358,12 @@ public: void run() override { + using namespace jtx; + auto const all = supported_amendments(); testErrors(); testSimpleRpc(); - testVoteAndBid(); + testVoteAndBid(all); + testVoteAndBid(all - fixAMMv1_3); testFreeze(); testInvalidAmmField(); } diff --git a/src/test/rpc/AccountLines_test.cpp b/src/test/rpc/AccountLines_test.cpp index 42acea4111..9215f4087a 100644 --- a/src/test/rpc/AccountLines_test.cpp +++ b/src/test/rpc/AccountLines_test.cpp @@ -573,21 +573,6 @@ public: env.fund(XRP(10000), alice, becky, gw1); env.close(); - // A couple of helper lambdas - auto escrow = [&env]( - Account const& account, - Account const& to, - STAmount const& amount) { - Json::Value jv; - jv[jss::TransactionType] = jss::EscrowCreate; - jv[jss::Account] = account.human(); - jv[jss::Destination] = to.human(); - jv[jss::Amount] = amount.getJson(JsonOptions::none); - NetClock::time_point finish = env.now() + 1s; - jv[sfFinishAfter.jsonName] = finish.time_since_epoch().count(); - return jv; - }; - auto payChan = [](Account const& account, Account const& to, STAmount const& amount, @@ -623,8 +608,10 @@ public: env.close(); // Escrow, in each direction - env(escrow(alice, becky, XRP(1000))); - env(escrow(becky, alice, XRP(1000))); + env(escrow::create(alice, becky, XRP(1000)), + escrow::finish_time(env.now() + 1s)); + env(escrow::create(becky, alice, XRP(1000)), + escrow::finish_time(env.now() + 1s)); // Pay channels, in each direction env(payChan(alice, becky, XRP(1000), 100s, alice.pk())); diff --git a/src/test/rpc/AccountSet_test.cpp b/src/test/rpc/AccountSet_test.cpp index 7bca51ae96..c056279bf1 100644 --- a/src/test/rpc/AccountSet_test.cpp +++ b/src/test/rpc/AccountSet_test.cpp @@ -99,6 +99,12 @@ public: // is tested elsewhere. continue; } + if (flag == asfAllowTrustLineLocking) + { + // 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/main/Application.cpp b/src/xrpld/app/main/Application.cpp index 5d495aaf06..7771086239 100644 --- a/src/xrpld/app/main/Application.cpp +++ b/src/xrpld/app/main/Application.cpp @@ -256,8 +256,8 @@ public: if ((cores == 1) || ((config.NODE_SIZE == 0) && (cores == 2))) return 1; - // Otherwise, prefer two threads. - return 2; + // Otherwise, prefer six threads. + return 6; #endif } diff --git a/src/xrpld/app/misc/AMMHelpers.h b/src/xrpld/app/misc/AMMHelpers.h index 97554b7e15..8cc39468b1 100644 --- a/src/xrpld/app/misc/AMMHelpers.h +++ b/src/xrpld/app/misc/AMMHelpers.h @@ -48,6 +48,8 @@ reduceOffer(auto const& amount) } // namespace detail +enum class IsDeposit : bool { No = false, Yes = true }; + /** Calculate LP Tokens given AMM pool reserves. * @param asset1 AMM one side of the pool reserve * @param asset2 AMM another side of the pool reserve @@ -67,7 +69,7 @@ ammLPTokens( * @return tokens */ STAmount -lpTokensIn( +lpTokensOut( STAmount const& asset1Balance, STAmount const& asset1Deposit, STAmount const& lptAMMBalance, @@ -96,7 +98,7 @@ ammAssetIn( * @return tokens out amount */ STAmount -lpTokensOut( +lpTokensIn( STAmount const& asset1Balance, STAmount const& asset1Withdraw, STAmount const& lptAMMBalance, @@ -110,7 +112,7 @@ lpTokensOut( * @return calculated asset amount */ STAmount -withdrawByTokens( +ammAssetOut( STAmount const& assetBalance, STAmount const& lptAMMBalance, STAmount const& lpTokens, @@ -608,13 +610,13 @@ square(Number const& n); * withdraw to cancel out the precision loss. * @param lptAMMBalance LPT AMM Balance * @param lpTokens LP tokens to deposit or withdraw - * @param isDeposit true if deposit, false if withdraw + * @param isDeposit Yes if deposit, No if withdraw */ STAmount adjustLPTokens( STAmount const& lptAMMBalance, STAmount const& lpTokens, - bool isDeposit); + IsDeposit isDeposit); /** Calls adjustLPTokens() and adjusts deposit or withdraw amounts if * the adjusted LP tokens are less than the provided LP tokens. @@ -624,7 +626,7 @@ adjustLPTokens( * @param lptAMMBalance LPT AMM Balance * @param lpTokens LP tokens to deposit or withdraw * @param tfee trading fee in basis points - * @param isDeposit true if deposit, false if withdraw + * @param isDeposit Yes if deposit, No if withdraw * @return */ std::tuple, STAmount> @@ -635,7 +637,7 @@ adjustAmountsByLPTokens( STAmount const& lptAMMBalance, STAmount const& lpTokens, std::uint16_t tfee, - bool isDeposit); + IsDeposit isDeposit); /** Positive solution for quadratic equation: * x = (-b + sqrt(b**2 + 4*a*c))/(2*a) @@ -643,6 +645,141 @@ adjustAmountsByLPTokens( Number solveQuadraticEq(Number const& a, Number const& b, Number const& c); +STAmount +multiply(STAmount const& amount, Number const& frac, Number::rounding_mode rm); + +namespace detail { + +inline Number::rounding_mode +getLPTokenRounding(IsDeposit isDeposit) +{ + // Minimize on deposit, maximize on withdraw to ensure + // AMM invariant sqrt(poolAsset1 * poolAsset2) >= LPTokensBalance + return isDeposit == IsDeposit::Yes ? Number::downward : Number::upward; +} + +inline Number::rounding_mode +getAssetRounding(IsDeposit isDeposit) +{ + // Maximize on deposit, minimize on withdraw to ensure + // AMM invariant sqrt(poolAsset1 * poolAsset2) >= LPTokensBalance + return isDeposit == IsDeposit::Yes ? Number::upward : Number::downward; +} + +} // namespace detail + +/** Round AMM equal deposit/withdrawal amount. Deposit/withdrawal formulas + * calculate the amount as a fractional value of the pool balance. The rounding + * takes place on the last step of multiplying the balance by the fraction if + * AMMv1_3 is enabled. + */ +template +STAmount +getRoundedAsset( + Rules const& rules, + STAmount const& balance, + A const& frac, + IsDeposit isDeposit) +{ + if (!rules.enabled(fixAMMv1_3)) + { + if constexpr (std::is_same_v) + return multiply(balance, frac, balance.issue()); + else + return toSTAmount(balance.issue(), balance * frac); + } + auto const rm = detail::getAssetRounding(isDeposit); + return multiply(balance, frac, rm); +} + +/** Round AMM single deposit/withdrawal amount. + * The lambda's are used to delay evaluation until the function + * is executed so that the calculation is not done twice. noRoundCb() is + * called if AMMv1_3 is disabled. Otherwise, the rounding is set and + * the amount is: + * isDeposit is Yes - the balance multiplied by productCb() + * isDeposit is No - the result of productCb(). The rounding is + * the same for all calculations in productCb() + */ +STAmount +getRoundedAsset( + Rules const& rules, + std::function&& noRoundCb, + STAmount const& balance, + std::function&& productCb, + IsDeposit isDeposit); + +/** Round AMM deposit/withdrawal LPToken amount. Deposit/withdrawal formulas + * calculate the lptokens as a fractional value of the AMM total lptokens. + * The rounding takes place on the last step of multiplying the balance by + * the fraction if AMMv1_3 is enabled. The tokens are then + * adjusted to factor in the loss in precision (we only keep 16 significant + * digits) when adding the lptokens to the balance. + */ +STAmount +getRoundedLPTokens( + Rules const& rules, + STAmount const& balance, + Number const& frac, + IsDeposit isDeposit); + +/** Round AMM single deposit/withdrawal LPToken amount. + * The lambda's are used to delay evaluation until the function is executed + * so that the calculations are not done twice. + * noRoundCb() is called if AMMv1_3 is disabled. Otherwise, the rounding is set + * and the lptokens are: + * if isDeposit is Yes - the result of productCb(). The rounding is + * the same for all calculations in productCb() + * if isDeposit is No - the balance multiplied by productCb() + * The lptokens are then adjusted to factor in the loss in precision + * (we only keep 16 significant digits) when adding the lptokens to the balance. + */ +STAmount +getRoundedLPTokens( + Rules const& rules, + std::function&& noRoundCb, + STAmount const& lptAMMBalance, + std::function&& productCb, + IsDeposit isDeposit); + +/* Next two functions adjust asset in/out amount to factor in the adjusted + * lptokens. The lptokens are calculated from the asset in/out. The lptokens are + * then adjusted to factor in the loss in precision. The adjusted lptokens might + * be less than the initially calculated tokens. Therefore, the asset in/out + * must be adjusted. The rounding might result in the adjusted amount being + * greater than the original asset in/out amount. If this happens, + * then the original amount is reduced by the difference in the adjusted amount + * and the original amount. The actual tokens and the actual adjusted amount + * are then recalculated. The minimum of the original and the actual + * adjusted amount is returned. + */ +std::pair +adjustAssetInByTokens( + Rules const& rules, + STAmount const& balance, + STAmount const& amount, + STAmount const& lptAMMBalance, + STAmount const& tokens, + std::uint16_t tfee); +std::pair +adjustAssetOutByTokens( + Rules const& rules, + STAmount const& balance, + STAmount const& amount, + STAmount const& lptAMMBalance, + STAmount const& tokens, + std::uint16_t tfee); + +/** Find a fraction of tokens after the tokens are adjusted. The fraction + * is used to adjust equal deposit/withdraw amount. + */ +Number +adjustFracByTokens( + Rules const& rules, + STAmount const& lptAMMBalance, + STAmount const& tokens, + Number const& frac); + } // namespace ripple #endif // RIPPLE_APP_MISC_AMMHELPERS_H_INCLUDED diff --git a/src/xrpld/app/misc/DelegateUtils.h b/src/xrpld/app/misc/DelegateUtils.h index cad3bed376..8d657e6a09 100644 --- a/src/xrpld/app/misc/DelegateUtils.h +++ b/src/xrpld/app/misc/DelegateUtils.h @@ -31,7 +31,8 @@ namespace ripple { * Check if the delegate account has permission to execute the transaction. * @param delegate The delegate account. * @param tx The transaction that the delegate account intends to execute. - * @return tesSUCCESS if the transaction is allowed, tecNO_PERMISSION if not. + * @return tesSUCCESS if the transaction is allowed, tecNO_DELEGATE_PERMISSION + * if not. */ TER checkTxPermission(std::shared_ptr const& delegate, STTx const& tx); diff --git a/src/xrpld/app/misc/detail/AMMHelpers.cpp b/src/xrpld/app/misc/detail/AMMHelpers.cpp index 8724c413a6..49ad01c3ae 100644 --- a/src/xrpld/app/misc/detail/AMMHelpers.cpp +++ b/src/xrpld/app/misc/detail/AMMHelpers.cpp @@ -27,6 +27,10 @@ ammLPTokens( STAmount const& asset2, Issue const& lptIssue) { + // AMM invariant: sqrt(asset1 * asset2) >= LPTokensBalance + auto const rounding = + isFeatureEnabled(fixAMMv1_3) ? Number::downward : Number::getround(); + NumberRoundModeGuard g(rounding); auto const tokens = root2(asset1 * asset2); return toSTAmount(lptIssue, tokens); } @@ -38,7 +42,7 @@ ammLPTokens( * where f1 = 1 - tfee, f2 = (1 - tfee/2)/f1 */ STAmount -lpTokensIn( +lpTokensOut( STAmount const& asset1Balance, STAmount const& asset1Deposit, STAmount const& lptAMMBalance, @@ -48,8 +52,17 @@ lpTokensIn( auto const f2 = feeMultHalf(tfee) / f1; Number const r = asset1Deposit / asset1Balance; auto const c = root2(f2 * f2 + r / f1) - f2; - auto const t = lptAMMBalance * (r - c) / (1 + c); - return toSTAmount(lptAMMBalance.issue(), t); + if (!isFeatureEnabled(fixAMMv1_3)) + { + auto const t = lptAMMBalance * (r - c) / (1 + c); + return toSTAmount(lptAMMBalance.issue(), t); + } + else + { + // minimize tokens out + auto const frac = (r - c) / (1 + c); + return multiply(lptAMMBalance, frac, Number::downward); + } } /* Equation 4 solves equation 3 for b: @@ -78,8 +91,17 @@ ammAssetIn( auto const a = 1 / (t2 * t2); auto const b = 2 * d / t2 - 1 / f1; auto const c = d * d - f2 * f2; - return toSTAmount( - asset1Balance.issue(), asset1Balance * solveQuadraticEq(a, b, c)); + if (!isFeatureEnabled(fixAMMv1_3)) + { + return toSTAmount( + asset1Balance.issue(), asset1Balance * solveQuadraticEq(a, b, c)); + } + else + { + // maximize deposit + auto const frac = solveQuadraticEq(a, b, c); + return multiply(asset1Balance, frac, Number::upward); + } } /* Equation 7: @@ -87,7 +109,7 @@ ammAssetIn( * where R = b/B, c = R*fee + 2 - fee */ STAmount -lpTokensOut( +lpTokensIn( STAmount const& asset1Balance, STAmount const& asset1Withdraw, STAmount const& lptAMMBalance, @@ -96,8 +118,17 @@ lpTokensOut( Number const fr = asset1Withdraw / asset1Balance; auto const f1 = getFee(tfee); auto const c = fr * f1 + 2 - f1; - auto const t = lptAMMBalance * (c - root2(c * c - 4 * fr)) / 2; - return toSTAmount(lptAMMBalance.issue(), t); + if (!isFeatureEnabled(fixAMMv1_3)) + { + auto const t = lptAMMBalance * (c - root2(c * c - 4 * fr)) / 2; + return toSTAmount(lptAMMBalance.issue(), t); + } + else + { + // maximize tokens in + auto const frac = (c - root2(c * c - 4 * fr)) / 2; + return multiply(lptAMMBalance, frac, Number::upward); + } } /* Equation 8 solves equation 7 for b: @@ -111,7 +142,7 @@ lpTokensOut( * R = (t1**2 + t1*(f - 2)) / (t1*f - 1) */ STAmount -withdrawByTokens( +ammAssetOut( STAmount const& assetBalance, STAmount const& lptAMMBalance, STAmount const& lpTokens, @@ -119,8 +150,17 @@ withdrawByTokens( { auto const f = getFee(tfee); Number const t1 = lpTokens / lptAMMBalance; - auto const b = assetBalance * (t1 * t1 - t1 * (2 - f)) / (t1 * f - 1); - return toSTAmount(assetBalance.issue(), b); + if (!isFeatureEnabled(fixAMMv1_3)) + { + auto const b = assetBalance * (t1 * t1 - t1 * (2 - f)) / (t1 * f - 1); + return toSTAmount(assetBalance.issue(), b); + } + else + { + // minimize withdraw + auto const frac = (t1 * t1 - t1 * (2 - f)) / (t1 * f - 1); + return multiply(assetBalance, frac, Number::downward); + } } Number @@ -133,12 +173,12 @@ STAmount adjustLPTokens( STAmount const& lptAMMBalance, STAmount const& lpTokens, - bool isDeposit) + IsDeposit isDeposit) { // Force rounding downward to ensure adjusted tokens are less or equal // to requested tokens. saveNumberRoundMode rm(Number::setround(Number::rounding_mode::downward)); - if (isDeposit) + if (isDeposit == IsDeposit::Yes) return (lptAMMBalance + lpTokens) - lptAMMBalance; return (lpTokens - lptAMMBalance) + lptAMMBalance; } @@ -151,8 +191,12 @@ adjustAmountsByLPTokens( STAmount const& lptAMMBalance, STAmount const& lpTokens, std::uint16_t tfee, - bool isDeposit) + IsDeposit isDeposit) { + // AMMv1_3 amendment adjusts tokens and amounts in deposit/withdraw + if (isFeatureEnabled(fixAMMv1_3)) + return std::make_tuple(amount, amount2, lpTokens); + auto const lpTokensActual = adjustLPTokens(lptAMMBalance, lpTokens, isDeposit); @@ -191,14 +235,14 @@ adjustAmountsByLPTokens( // Single trade auto const amountActual = [&]() { - if (isDeposit) + if (isDeposit == IsDeposit::Yes) return ammAssetIn( amountBalance, lptAMMBalance, lpTokensActual, tfee); else if (!ammRoundingEnabled) - return withdrawByTokens( + return ammAssetOut( amountBalance, lptAMMBalance, lpTokens, tfee); else - return withdrawByTokens( + return ammAssetOut( amountBalance, lptAMMBalance, lpTokensActual, tfee); }(); if (!ammRoundingEnabled) @@ -237,4 +281,132 @@ solveQuadraticEqSmallest(Number const& a, Number const& b, Number const& c) return (2 * c) / (-b + root2(d)); } +STAmount +multiply(STAmount const& amount, Number const& frac, Number::rounding_mode rm) +{ + NumberRoundModeGuard g(rm); + auto const t = amount * frac; + return toSTAmount(amount.issue(), t, rm); +} + +STAmount +getRoundedAsset( + Rules const& rules, + std::function&& noRoundCb, + STAmount const& balance, + std::function&& productCb, + IsDeposit isDeposit) +{ + if (!rules.enabled(fixAMMv1_3)) + return toSTAmount(balance.issue(), noRoundCb()); + + auto const rm = detail::getAssetRounding(isDeposit); + if (isDeposit == IsDeposit::Yes) + return multiply(balance, productCb(), rm); + NumberRoundModeGuard g(rm); + return toSTAmount(balance.issue(), productCb(), rm); +} + +STAmount +getRoundedLPTokens( + Rules const& rules, + STAmount const& balance, + Number const& frac, + IsDeposit isDeposit) +{ + if (!rules.enabled(fixAMMv1_3)) + return toSTAmount(balance.issue(), balance * frac); + + auto const rm = detail::getLPTokenRounding(isDeposit); + auto const tokens = multiply(balance, frac, rm); + return adjustLPTokens(balance, tokens, isDeposit); +} + +STAmount +getRoundedLPTokens( + Rules const& rules, + std::function&& noRoundCb, + STAmount const& lptAMMBalance, + std::function&& productCb, + IsDeposit isDeposit) +{ + if (!rules.enabled(fixAMMv1_3)) + return toSTAmount(lptAMMBalance.issue(), noRoundCb()); + + auto const tokens = [&] { + auto const rm = detail::getLPTokenRounding(isDeposit); + if (isDeposit == IsDeposit::Yes) + { + NumberRoundModeGuard g(rm); + return toSTAmount(lptAMMBalance.issue(), productCb(), rm); + } + return multiply(lptAMMBalance, productCb(), rm); + }(); + return adjustLPTokens(lptAMMBalance, tokens, isDeposit); +} + +std::pair +adjustAssetInByTokens( + Rules const& rules, + STAmount const& balance, + STAmount const& amount, + STAmount const& lptAMMBalance, + STAmount const& tokens, + std::uint16_t tfee) +{ + if (!rules.enabled(fixAMMv1_3)) + return {tokens, amount}; + auto assetAdj = ammAssetIn(balance, lptAMMBalance, tokens, tfee); + auto tokensAdj = tokens; + // Rounding didn't work the right way. + // Try to adjust the original deposit amount by difference + // in adjust and original amount. Then adjust tokens and deposit amount. + if (assetAdj > amount) + { + auto const adjAmount = amount - (assetAdj - amount); + auto const t = lpTokensOut(balance, adjAmount, lptAMMBalance, tfee); + tokensAdj = adjustLPTokens(lptAMMBalance, t, IsDeposit::Yes); + assetAdj = ammAssetIn(balance, lptAMMBalance, tokensAdj, tfee); + } + return {tokensAdj, std::min(amount, assetAdj)}; +} + +std::pair +adjustAssetOutByTokens( + Rules const& rules, + STAmount const& balance, + STAmount const& amount, + STAmount const& lptAMMBalance, + STAmount const& tokens, + std::uint16_t tfee) +{ + if (!rules.enabled(fixAMMv1_3)) + return {tokens, amount}; + auto assetAdj = ammAssetOut(balance, lptAMMBalance, tokens, tfee); + auto tokensAdj = tokens; + // Rounding didn't work the right way. + // Try to adjust the original deposit amount by difference + // in adjust and original amount. Then adjust tokens and deposit amount. + if (assetAdj > amount) + { + auto const adjAmount = amount - (assetAdj - amount); + auto const t = lpTokensIn(balance, adjAmount, lptAMMBalance, tfee); + tokensAdj = adjustLPTokens(lptAMMBalance, t, IsDeposit::No); + assetAdj = ammAssetOut(balance, lptAMMBalance, tokensAdj, tfee); + } + return {tokensAdj, std::min(amount, assetAdj)}; +} + +Number +adjustFracByTokens( + Rules const& rules, + STAmount const& lptAMMBalance, + STAmount const& tokens, + Number const& frac) +{ + if (!rules.enabled(fixAMMv1_3)) + return frac; + return tokens / lptAMMBalance; +} + } // namespace ripple diff --git a/src/xrpld/app/misc/detail/DelegateUtils.cpp b/src/xrpld/app/misc/detail/DelegateUtils.cpp index 7b7021fe9e..229af555ff 100644 --- a/src/xrpld/app/misc/detail/DelegateUtils.cpp +++ b/src/xrpld/app/misc/detail/DelegateUtils.cpp @@ -26,7 +26,7 @@ TER checkTxPermission(std::shared_ptr const& delegate, STTx const& tx) { if (!delegate) - return tecNO_PERMISSION; // LCOV_EXCL_LINE + return tecNO_DELEGATE_PERMISSION; // LCOV_EXCL_LINE auto const permissionArray = delegate->getFieldArray(sfPermissions); auto const txPermission = tx.getTxnType() + 1; @@ -38,7 +38,7 @@ checkTxPermission(std::shared_ptr const& delegate, STTx const& tx) return tesSUCCESS; } - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; } void diff --git a/src/xrpld/app/tx/detail/AMMBid.cpp b/src/xrpld/app/tx/detail/AMMBid.cpp index f64e90013b..d14b668790 100644 --- a/src/xrpld/app/tx/detail/AMMBid.cpp +++ b/src/xrpld/app/tx/detail/AMMBid.cpp @@ -72,6 +72,21 @@ AMMBid::doPreflight(PreflightContext const& ctx) JLOG(ctx.j.debug()) << "AMM Bid: Invalid number of AuthAccounts."; return temMALFORMED; } + else if (ctx.rules.enabled(fixAMMv1_3)) + { + AccountID account = ctx.tx[sfAccount]; + std::set unique; + for (auto const& obj : authAccounts) + { + auto authAccount = obj[sfAccount]; + if (authAccount == account || unique.contains(authAccount)) + { + JLOG(ctx.j.debug()) << "AMM Bid: Invalid auth.account."; + return temMALFORMED; + } + unique.insert(authAccount); + } + } } return tesSUCCESS; @@ -226,7 +241,9 @@ applyBid( auctionSlot.makeFieldAbsent(sfAuthAccounts); // Burn the remaining bid amount auto const saBurn = adjustLPTokens( - lptAMMBalance, toSTAmount(lptAMMBalance.issue(), burn), false); + lptAMMBalance, + toSTAmount(lptAMMBalance.issue(), burn), + IsDeposit::No); if (saBurn >= lptAMMBalance) { // This error case should never occur. diff --git a/src/xrpld/app/tx/detail/AMMDeposit.cpp b/src/xrpld/app/tx/detail/AMMDeposit.cpp index cdd36c80fe..2aad1e05b9 100644 --- a/src/xrpld/app/tx/detail/AMMDeposit.cpp +++ b/src/xrpld/app/tx/detail/AMMDeposit.cpp @@ -544,7 +544,7 @@ AMMDeposit::deposit( lptAMMBalance, lpTokensDeposit, tfee, - true); + IsDeposit::Yes); if (lpTokensDepositActual <= beast::zero) { @@ -627,6 +627,17 @@ AMMDeposit::deposit( return {tesSUCCESS, lptAMMBalance + lpTokensDepositActual}; } +static STAmount +adjustLPTokensOut( + Rules const& rules, + STAmount const& lptAMMBalance, + STAmount const& lpTokensDeposit) +{ + if (!rules.enabled(fixAMMv1_3)) + return lpTokensDeposit; + return adjustLPTokens(lptAMMBalance, lpTokensDeposit, IsDeposit::Yes); +} + /** Proportional deposit of pools assets in exchange for the specified * amount of LPTokens. */ @@ -644,16 +655,25 @@ AMMDeposit::equalDepositTokens( { try { + auto const tokensAdj = + adjustLPTokensOut(view.rules(), lptAMMBalance, lpTokensDeposit); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; auto const frac = - divide(lpTokensDeposit, lptAMMBalance, lptAMMBalance.issue()); + divide(tokensAdj, lptAMMBalance, lptAMMBalance.issue()); + // amounts factor in the adjusted tokens + auto const amountDeposit = + getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::Yes); + auto const amount2Deposit = + getRoundedAsset(view.rules(), amount2Balance, frac, IsDeposit::Yes); return deposit( view, ammAccount, amountBalance, - multiply(amountBalance, frac, amountBalance.issue()), - multiply(amount2Balance, frac, amount2Balance.issue()), + amountDeposit, + amount2Deposit, lptAMMBalance, - lpTokensDeposit, + tokensAdj, depositMin, deposit2Min, std::nullopt, @@ -710,37 +730,55 @@ AMMDeposit::equalDepositLimit( std::uint16_t tfee) { auto frac = Number{amount} / amountBalance; - auto tokens = toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac); - if (tokens == beast::zero) - return {tecAMM_FAILED, STAmount{}}; - auto const amount2Deposit = amount2Balance * frac; + auto tokensAdj = + getRoundedLPTokens(view.rules(), lptAMMBalance, frac, IsDeposit::Yes); + if (tokensAdj == beast::zero) + { + if (!view.rules().enabled(fixAMMv1_3)) + return {tecAMM_FAILED, STAmount{}}; // LCOV_EXCL_LINE + else + return {tecAMM_INVALID_TOKENS, STAmount{}}; + } + // factor in the adjusted tokens + frac = adjustFracByTokens(view.rules(), lptAMMBalance, tokensAdj, frac); + auto const amount2Deposit = + getRoundedAsset(view.rules(), amount2Balance, frac, IsDeposit::Yes); if (amount2Deposit <= amount2) return deposit( view, ammAccount, amountBalance, amount, - toSTAmount(amount2Balance.issue(), amount2Deposit), + amount2Deposit, lptAMMBalance, - tokens, + tokensAdj, std::nullopt, std::nullopt, lpTokensDepositMin, tfee); frac = Number{amount2} / amount2Balance; - tokens = toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac); - if (tokens == beast::zero) - return {tecAMM_FAILED, STAmount{}}; - auto const amountDeposit = amountBalance * frac; + tokensAdj = + getRoundedLPTokens(view.rules(), lptAMMBalance, frac, IsDeposit::Yes); + if (tokensAdj == beast::zero) + { + if (!view.rules().enabled(fixAMMv1_3)) + return {tecAMM_FAILED, STAmount{}}; // LCOV_EXCL_LINE + else + return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE + } + // factor in the adjusted tokens + frac = adjustFracByTokens(view.rules(), lptAMMBalance, tokensAdj, frac); + auto const amountDeposit = + getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::Yes); if (amountDeposit <= amount) return deposit( view, ammAccount, amountBalance, - toSTAmount(amountBalance.issue(), amountDeposit), + amountDeposit, amount2, lptAMMBalance, - tokens, + tokensAdj, std::nullopt, std::nullopt, lpTokensDepositMin, @@ -766,17 +804,30 @@ AMMDeposit::singleDeposit( std::optional const& lpTokensDepositMin, std::uint16_t tfee) { - auto const tokens = lpTokensIn(amountBalance, amount, lptAMMBalance, tfee); + auto const tokens = adjustLPTokensOut( + view.rules(), + lptAMMBalance, + lpTokensOut(amountBalance, amount, lptAMMBalance, tfee)); if (tokens == beast::zero) - return {tecAMM_FAILED, STAmount{}}; + { + if (!view.rules().enabled(fixAMMv1_3)) + return {tecAMM_FAILED, STAmount{}}; // LCOV_EXCL_LINE + else + return {tecAMM_INVALID_TOKENS, STAmount{}}; + } + // factor in the adjusted tokens + auto const [tokensAdj, amountDepositAdj] = adjustAssetInByTokens( + view.rules(), amountBalance, amount, lptAMMBalance, tokens, tfee); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE return deposit( view, ammAccount, amountBalance, - amount, + amountDepositAdj, std::nullopt, lptAMMBalance, - tokens, + tokensAdj, std::nullopt, std::nullopt, lpTokensDepositMin, @@ -800,8 +851,13 @@ AMMDeposit::singleDepositTokens( STAmount const& lpTokensDeposit, std::uint16_t tfee) { + auto const tokensAdj = + adjustLPTokensOut(view.rules(), lptAMMBalance, lpTokensDeposit); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; + // the adjusted tokens are factored in auto const amountDeposit = - ammAssetIn(amountBalance, lptAMMBalance, lpTokensDeposit, tfee); + ammAssetIn(amountBalance, lptAMMBalance, tokensAdj, tfee); if (amountDeposit > amount) return {tecAMM_FAILED, STAmount{}}; return deposit( @@ -811,7 +867,7 @@ AMMDeposit::singleDepositTokens( amountDeposit, std::nullopt, lptAMMBalance, - lpTokensDeposit, + tokensAdj, std::nullopt, std::nullopt, std::nullopt, @@ -855,20 +911,32 @@ AMMDeposit::singleDepositEPrice( { if (amount != beast::zero) { - auto const tokens = - lpTokensIn(amountBalance, amount, lptAMMBalance, tfee); + auto const tokens = adjustLPTokensOut( + view.rules(), + lptAMMBalance, + lpTokensOut(amountBalance, amount, lptAMMBalance, tfee)); if (tokens <= beast::zero) - return {tecAMM_FAILED, STAmount{}}; - auto const ep = Number{amount} / tokens; + { + if (!view.rules().enabled(fixAMMv1_3)) + return {tecAMM_FAILED, STAmount{}}; // LCOV_EXCL_LINE + else + return {tecAMM_INVALID_TOKENS, STAmount{}}; + } + // factor in the adjusted tokens + auto const [tokensAdj, amountDepositAdj] = adjustAssetInByTokens( + view.rules(), amountBalance, amount, lptAMMBalance, tokens, tfee); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE + auto const ep = Number{amountDepositAdj} / tokensAdj; if (ep <= ePrice) return deposit( view, ammAccount, amountBalance, - amount, + amountDepositAdj, std::nullopt, lptAMMBalance, - tokens, + tokensAdj, std::nullopt, std::nullopt, std::nullopt, @@ -899,21 +967,37 @@ AMMDeposit::singleDepositEPrice( auto const a1 = c * c; auto const b1 = c * c * f2 * f2 + 2 * c - d * d; auto const c1 = 2 * c * f2 * f2 + 1 - 2 * d * f2; - auto const amountDeposit = toSTAmount( - amountBalance.issue(), - f1 * amountBalance * solveQuadraticEq(a1, b1, c1)); + auto amtNoRoundCb = [&] { + return f1 * amountBalance * solveQuadraticEq(a1, b1, c1); + }; + auto amtProdCb = [&] { return f1 * solveQuadraticEq(a1, b1, c1); }; + auto const amountDeposit = getRoundedAsset( + view.rules(), amtNoRoundCb, amountBalance, amtProdCb, IsDeposit::Yes); if (amountDeposit <= beast::zero) return {tecAMM_FAILED, STAmount{}}; - auto const tokens = - toSTAmount(lptAMMBalance.issue(), amountDeposit / ePrice); + auto tokNoRoundCb = [&] { return amountDeposit / ePrice; }; + auto tokProdCb = [&] { return amountDeposit / ePrice; }; + auto const tokens = getRoundedLPTokens( + view.rules(), tokNoRoundCb, lptAMMBalance, tokProdCb, IsDeposit::Yes); + // factor in the adjusted tokens + auto const [tokensAdj, amountDepositAdj] = adjustAssetInByTokens( + view.rules(), + amountBalance, + amountDeposit, + lptAMMBalance, + tokens, + tfee); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE + return deposit( view, ammAccount, amountBalance, - amountDeposit, + amountDepositAdj, std::nullopt, lptAMMBalance, - tokens, + tokensAdj, std::nullopt, std::nullopt, std::nullopt, diff --git a/src/xrpld/app/tx/detail/AMMWithdraw.cpp b/src/xrpld/app/tx/detail/AMMWithdraw.cpp index 68e46cfb9b..712695fd61 100644 --- a/src/xrpld/app/tx/detail/AMMWithdraw.cpp +++ b/src/xrpld/app/tx/detail/AMMWithdraw.cpp @@ -523,7 +523,7 @@ AMMWithdraw::withdraw( lpTokensAMMBalance, lpTokensWithdraw, tfee, - false); + IsDeposit::No); return std::make_tuple( amountWithdraw, amount2Withdraw, lpTokensWithdraw); }(); @@ -684,6 +684,20 @@ AMMWithdraw::withdraw( amount2WithdrawActual); } +static STAmount +adjustLPTokensIn( + Rules const& rules, + STAmount const& lptAMMBalance, + STAmount const& lpTokensWithdraw, + WithdrawAll withdrawAll) +{ + if (!rules.enabled(fixAMMv1_3) || withdrawAll == WithdrawAll::Yes) + return lpTokensWithdraw; + return adjustLPTokens(lptAMMBalance, lpTokensWithdraw, IsDeposit::No); +} + +/** Proportional withdrawal of pool assets for the amount of LPTokens. + */ std::pair AMMWithdraw::equalWithdrawTokens( Sandbox& view, @@ -787,16 +801,22 @@ AMMWithdraw::equalWithdrawTokens( journal); } - auto const frac = divide(lpTokensWithdraw, lptAMMBalance, noIssue()); - auto const withdrawAmount = - multiply(amountBalance, frac, amountBalance.issue()); - auto const withdraw2Amount = - multiply(amount2Balance, frac, amount2Balance.issue()); + auto const tokensAdj = adjustLPTokensIn( + view.rules(), lptAMMBalance, lpTokensWithdraw, withdrawAll); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return { + tecAMM_INVALID_TOKENS, STAmount{}, STAmount{}, std::nullopt}; + // the adjusted tokens are factored in + auto const frac = divide(tokensAdj, lptAMMBalance, noIssue()); + auto const amountWithdraw = + getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::No); + auto const amount2Withdraw = + getRoundedAsset(view.rules(), amount2Balance, frac, IsDeposit::No); // LP is making equal withdrawal by tokens but the requested amount // of LP tokens is likely too small and results in one-sided pool // withdrawal due to round off. Fail so the user withdraws // more tokens. - if (withdrawAmount == beast::zero || withdraw2Amount == beast::zero) + if (amountWithdraw == beast::zero || amount2Withdraw == beast::zero) return {tecAMM_FAILED, STAmount{}, STAmount{}, STAmount{}}; return withdraw( @@ -805,10 +825,10 @@ AMMWithdraw::equalWithdrawTokens( ammAccount, account, amountBalance, - withdrawAmount, - withdraw2Amount, + amountWithdraw, + amount2Withdraw, lptAMMBalance, - lpTokensWithdraw, + tokensAdj, tfee, freezeHanding, withdrawAll, @@ -863,7 +883,16 @@ AMMWithdraw::equalWithdrawLimit( std::uint16_t tfee) { auto frac = Number{amount} / amountBalance; - auto const amount2Withdraw = amount2Balance * frac; + auto amount2Withdraw = + getRoundedAsset(view.rules(), amount2Balance, frac, IsDeposit::No); + auto tokensAdj = + getRoundedLPTokens(view.rules(), lptAMMBalance, frac, IsDeposit::No); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; + // factor in the adjusted tokens + frac = adjustFracByTokens(view.rules(), lptAMMBalance, tokensAdj, frac); + amount2Withdraw = + getRoundedAsset(view.rules(), amount2Balance, frac, IsDeposit::No); if (amount2Withdraw <= amount2) { return withdraw( @@ -872,26 +901,42 @@ AMMWithdraw::equalWithdrawLimit( ammAccount, amountBalance, amount, - toSTAmount(amount2.issue(), amount2Withdraw), + amount2Withdraw, lptAMMBalance, - toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac), + tokensAdj, tfee); } frac = Number{amount2} / amount2Balance; - auto const amountWithdraw = amountBalance * frac; - XRPL_ASSERT( - amountWithdraw <= amount, - "ripple::AMMWithdraw::equalWithdrawLimit : maximum amountWithdraw"); + auto amountWithdraw = + getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::No); + tokensAdj = + getRoundedLPTokens(view.rules(), lptAMMBalance, frac, IsDeposit::No); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE + // factor in the adjusted tokens + frac = adjustFracByTokens(view.rules(), lptAMMBalance, tokensAdj, frac); + amountWithdraw = + getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::No); + if (!view.rules().enabled(fixAMMv1_3)) + { + // LCOV_EXCL_START + XRPL_ASSERT( + amountWithdraw <= amount, + "ripple::AMMWithdraw::equalWithdrawLimit : maximum amountWithdraw"); + // LCOV_EXCL_STOP + } + else if (amountWithdraw > amount) + return {tecAMM_FAILED, STAmount{}}; // LCOV_EXCL_LINE return withdraw( view, ammSle, ammAccount, amountBalance, - toSTAmount(amount.issue(), amountWithdraw), + amountWithdraw, amount2, lptAMMBalance, - toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac), + tokensAdj, tfee); } @@ -910,19 +955,32 @@ AMMWithdraw::singleWithdraw( STAmount const& amount, std::uint16_t tfee) { - auto const tokens = lpTokensOut(amountBalance, amount, lptAMMBalance, tfee); + auto const tokens = adjustLPTokensIn( + view.rules(), + lptAMMBalance, + lpTokensIn(amountBalance, amount, lptAMMBalance, tfee), + isWithdrawAll(ctx_.tx)); if (tokens == beast::zero) - return {tecAMM_FAILED, STAmount{}}; - + { + if (!view.rules().enabled(fixAMMv1_3)) + return {tecAMM_FAILED, STAmount{}}; // LCOV_EXCL_LINE + else + return {tecAMM_INVALID_TOKENS, STAmount{}}; + } + // factor in the adjusted tokens + auto const [tokensAdj, amountWithdrawAdj] = adjustAssetOutByTokens( + view.rules(), amountBalance, amount, lptAMMBalance, tokens, tfee); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE return withdraw( view, ammSle, ammAccount, amountBalance, - amount, + amountWithdrawAdj, std::nullopt, lptAMMBalance, - tokens, + tokensAdj, tfee); } @@ -947,8 +1005,13 @@ AMMWithdraw::singleWithdrawTokens( STAmount const& lpTokensWithdraw, std::uint16_t tfee) { + auto const tokensAdj = adjustLPTokensIn( + view.rules(), lptAMMBalance, lpTokensWithdraw, isWithdrawAll(ctx_.tx)); + if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; + // the adjusted tokens are factored in auto const amountWithdraw = - withdrawByTokens(amountBalance, lptAMMBalance, lpTokensWithdraw, tfee); + ammAssetOut(amountBalance, lptAMMBalance, tokensAdj, tfee); if (amount == beast::zero || amountWithdraw >= amount) { return withdraw( @@ -959,7 +1022,7 @@ AMMWithdraw::singleWithdrawTokens( amountWithdraw, std::nullopt, lptAMMBalance, - lpTokensWithdraw, + tokensAdj, tfee); } @@ -1008,11 +1071,27 @@ AMMWithdraw::singleWithdrawEPrice( // t = T*(T + A*E*(f - 2))/(T*f - A*E) Number const ae = amountBalance * ePrice; auto const f = getFee(tfee); - auto const tokens = lptAMMBalance * (lptAMMBalance + ae * (f - 2)) / - (lptAMMBalance * f - ae); - if (tokens <= 0) - return {tecAMM_FAILED, STAmount{}}; - auto const amountWithdraw = toSTAmount(amount.issue(), tokens / ePrice); + auto tokNoRoundCb = [&] { + return lptAMMBalance * (lptAMMBalance + ae * (f - 2)) / + (lptAMMBalance * f - ae); + }; + auto tokProdCb = [&] { + return (lptAMMBalance + ae * (f - 2)) / (lptAMMBalance * f - ae); + }; + auto const tokensAdj = getRoundedLPTokens( + view.rules(), tokNoRoundCb, lptAMMBalance, tokProdCb, IsDeposit::No); + if (tokensAdj <= beast::zero) + { + if (!view.rules().enabled(fixAMMv1_3)) + return {tecAMM_FAILED, STAmount{}}; + else + return {tecAMM_INVALID_TOKENS, STAmount{}}; + } + auto amtNoRoundCb = [&] { return tokensAdj / ePrice; }; + auto amtProdCb = [&] { return tokensAdj / ePrice; }; + // the adjusted tokens are factored in + auto const amountWithdraw = getRoundedAsset( + view.rules(), amtNoRoundCb, amount, amtProdCb, IsDeposit::No); if (amount == beast::zero || amountWithdraw >= amount) { return withdraw( @@ -1023,7 +1102,7 @@ AMMWithdraw::singleWithdrawEPrice( amountWithdraw, std::nullopt, lptAMMBalance, - toSTAmount(lptAMMBalance.issue(), tokens), + tokensAdj, tfee); } diff --git a/src/xrpld/app/tx/detail/AMMWithdraw.h b/src/xrpld/app/tx/detail/AMMWithdraw.h index 7e6397f535..7afda43e1e 100644 --- a/src/xrpld/app/tx/detail/AMMWithdraw.h +++ b/src/xrpld/app/tx/detail/AMMWithdraw.h @@ -307,7 +307,7 @@ private: std::uint16_t tfee); /** Check from the flags if it's withdraw all */ - WithdrawAll + static WithdrawAll isWithdrawAll(STTx const& tx); }; diff --git a/src/xrpld/app/tx/detail/DelegateSet.cpp b/src/xrpld/app/tx/detail/DelegateSet.cpp index 07ebc83893..ebd52c113e 100644 --- a/src/xrpld/app/tx/detail/DelegateSet.cpp +++ b/src/xrpld/app/tx/detail/DelegateSet.cpp @@ -63,7 +63,7 @@ DelegateSet::preclaim(PreclaimContext const& ctx) return terNO_ACCOUNT; // LCOV_EXCL_LINE if (!ctx.view.exists(keylet::account(ctx.tx[sfAuthorize]))) - return terNO_ACCOUNT; + return tecNO_TARGET; auto const& permissions = ctx.tx.getFieldArray(sfPermissions); for (auto const& permission : permissions) diff --git a/src/xrpld/app/tx/detail/Escrow.cpp b/src/xrpld/app/tx/detail/Escrow.cpp index a3491f479b..eb19fe44b6 100644 --- a/src/xrpld/app/tx/detail/Escrow.cpp +++ b/src/xrpld/app/tx/detail/Escrow.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,7 @@ #include #include #include +#include #include #include @@ -79,7 +81,41 @@ namespace ripple { 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 (amount.native() || amount <= beast::zero) + 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.native() || amount.mpt() > MPTAmount{maxMPTokenAmount} || + amount <= beast::zero) + return temBAD_AMOUNT; + + return tesSUCCESS; } std::uint32_t @@ -92,11 +128,25 @@ EscrowCreate::getFlagsMask(PreflightContext const& ctx) NotTEC EscrowCreate::doPreflight(PreflightContext const& ctx) { - if (!isXRP(ctx.tx[sfAmount])) - return temBAD_AMOUNT; + STAmount const amount{ctx.tx[sfAmount]}; + if (!isXRP(amount)) + { + if (!ctx.rules.enabled(featureTokenEscrow)) + return temBAD_AMOUNT; - if (ctx.tx[sfAmount] <= beast::zero) - return temBAD_AMOUNT; + if (auto const ret = std::visit( + [&](T const&) { + return escrowCreatePreflightHelper(ctx); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + else + { + if (amount <= beast::zero) + return temBAD_AMOUNT; + } // We must specify at least one timeout value if (!ctx.tx[~sfCancelAfter] && !ctx.tx[~sfFinishAfter]) @@ -143,10 +193,181 @@ EscrowCreate::doPreflight(PreflightContext const& ctx) return tesSUCCESS; } +template +static TER +escrowCreatePreclaimHelper( + PreclaimContext const& ctx, + AccountID const& account, + AccountID const& dest, + STAmount const& amount); + +template <> +TER +escrowCreatePreclaimHelper( + PreclaimContext const& ctx, + AccountID const& account, + AccountID const& dest, + STAmount const& amount) +{ + AccountID issuer = amount.getIssuer(); + // If the issuer is the same as the account, return tecNO_PERMISSION + if (issuer == account) + return tecNO_PERMISSION; + + // If the lsfAllowTrustLineLocking is not enabled, return tecNO_PERMISSION + auto const sleIssuer = ctx.view.read(keylet::account(issuer)); + if (!sleIssuer) + return tecNO_ISSUER; + if (!sleIssuer->isFlag(lsfAllowTrustLineLocking)) + 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; // LCOV_EXCL_LINE + + // If balance is negative, issuer must have lower address than account + if (balance < beast::zero && issuer > account) + return tecNO_PERMISSION; // LCOV_EXCL_LINE + + // 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 spendable amount is less than the amount, return + // tecINSUFFICIENT_FUNDS + if (spendableAmount < amount) + return tecINSUFFICIENT_FUNDS; + + // If the amount is not addable to the balance, return tecPRECISION_LOSS + if (!canAdd(spendableAmount, amount)) + return tecPRECISION_LOSS; + + return tesSUCCESS; +} + +template <> +TER +escrowCreatePreclaimHelper( + PreclaimContext const& ctx, + AccountID const& account, + AccountID const& dest, + STAmount const& amount) +{ + AccountID issuer = amount.getIssuer(); + // If the issuer is the same as the account, return tecNO_PERMISSION + if (issuer == account) + return tecNO_PERMISSION; + + // 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 lsfMPTCanEscrow is not enabled, return tecNO_PERMISSION + if (!sleIssuance->isFlag(lsfMPTCanEscrow)) + 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; // LCOV_EXCL_LINE + + // 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; + + // 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, MPTAuthType::WeakAuth); + 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, MPTAuthType::WeakAuth); + ter != tesSUCCESS) + return ter; + + // If the issuer has frozen the account, return tecLOCKED + if (isFrozen(ctx.view, account, mptIssue)) + return tecLOCKED; + + // If the issuer has frozen the destination, return tecLOCKED + if (isFrozen(ctx.view, dest, mptIssue)) + return tecLOCKED; + + // 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; + + // If the spendable amount is less than the amount, return + // tecINSUFFICIENT_FUNDS + if (spendableAmount < amount) + 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; @@ -157,6 +378,77 @@ EscrowCreate::preclaim(PreclaimContext const& ctx) if (isPseudoAccount(sled)) return tecNO_PERMISSION; + if (!isXRP(amount)) + { + if (!ctx.view.rules().enabled(featureTokenEscrow)) + return temDISABLED; // LCOV_EXCL_LINE + + if (auto const ret = std::visit( + [&](T const&) { + return escrowCreatePreclaimHelper( + ctx, account, dest, amount); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + return tesSUCCESS; +} + +template +static TER +escrowLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal); + +template <> +TER +escrowLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal) +{ + // Defensive: Issuer cannot create an escrow + // LCOV_EXCL_START + if (issuer == sender) + return tecINTERNAL; + // LCOV_EXCL_STOP + + auto const ter = rippleCredit( + view, + sender, + issuer, + amount, + amount.holds() ? false : true, + journal); + if (ter != tesSUCCESS) + return ter; // LCOV_EXCL_LINE + return tesSUCCESS; +} + +template <> +TER +escrowLockApplyHelper( + ApplyView& view, + AccountID const& issuer, + AccountID const& sender, + STAmount const& amount, + beast::Journal journal) +{ + // Defensive: Issuer cannot create an escrow + // LCOV_EXCL_START + if (issuer == sender) + return tecINTERNAL; + // LCOV_EXCL_STOP + + auto const ter = rippleLockEscrowMPT(view, sender, amount, journal); + if (ter != tesSUCCESS) + return ter; // LCOV_EXCL_LINE return tesSUCCESS; } @@ -197,21 +489,23 @@ EscrowCreate::doApply() } } - auto const account = ctx_.tx[sfAccount]; - auto const sle = ctx_.view().peek(keylet::account(account)); + auto const sle = ctx_.view().peek(keylet::account(account_)); if (!sle) - return tefINTERNAL; + return tefINTERNAL; // LCOV_EXCL_LINE // Check reserve and funds availability + STAmount const amount{ctx_.tx[sfAmount]}; + + auto const reserve = + ctx_.view().fees().accountReserve((*sle)[sfOwnerCount] + 1); + + if (mSourceBalance < reserve) + return tecINSUFFICIENT_RESERVE; + + // Check reserve and funds availability + if (isXRP(amount)) { - auto const balance = STAmount((*sle)[sfBalance]).xrp(); - auto const reserve = - ctx_.view().fees().accountReserve((*sle)[sfOwnerCount] + 1); - - if (balance < reserve) - return tecINSUFFICIENT_RESERVE; - - if (balance < reserve + STAmount(ctx_.tx[sfAmount]).xrp()) + if (mSourceBalance < reserve + STAmount(amount).xrp()) return tecUNFUNDED; } @@ -234,10 +528,10 @@ EscrowCreate::doApply() // Create escrow in ledger. Note that we we use the value from the // sequence or ticket. For more explanation see comments in SeqProxy.h. - Keylet const escrowKeylet = keylet::escrow(account, ctx_.tx.getSeqValue()); + Keylet const escrowKeylet = keylet::escrow(account_, ctx_.tx.getSeqValue()); auto const slep = std::make_shared(escrowKeylet); - (*slep)[sfAmount] = ctx_.tx[sfAmount]; - (*slep)[sfAccount] = account; + (*slep)[sfAmount] = amount; + (*slep)[sfAccount] = account_; (*slep)[~sfCondition] = ctx_.tx[~sfCondition]; (*slep)[~sfSourceTag] = ctx_.tx[~sfSourceTag]; (*slep)[sfDestination] = ctx_.tx[sfDestination]; @@ -245,32 +539,69 @@ EscrowCreate::doApply() (*slep)[~sfFinishAfter] = ctx_.tx[~sfFinishAfter]; (*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag]; + if (ctx_.view().rules().enabled(featureTokenEscrow) && !isXRP(amount)) + { + auto const xferRate = transferRate(ctx_.view(), amount); + if (xferRate != parityRate) + (*slep)[sfTransferRate] = xferRate.value; + } + ctx_.view().insert(slep); // Add escrow to sender's owner directory { auto page = ctx_.view().dirInsert( - keylet::ownerDir(account), escrowKeylet, describeOwnerDir(account)); + keylet::ownerDir(account_), + escrowKeylet, + describeOwnerDir(account_)); if (!page) - return tecDIR_FULL; + return tecDIR_FULL; // LCOV_EXCL_LINE (*slep)[sfOwnerNode] = *page; } // 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 != account_) { auto page = ctx_.view().dirInsert( keylet::ownerDir(dest), escrowKeylet, describeOwnerDir(dest)); if (!page) - return tecDIR_FULL; + return tecDIR_FULL; // LCOV_EXCL_LINE (*slep)[sfDestinationNode] = *page; } - // Deduct owner's balance, increment owner count - (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount]; + // IOU escrow objects are added to the issuer's owner directory to help + // track the total locked balance. For MPT, this isn't necessary because the + // locked balance is already stored directly in the MPTokenIssuance object. + AccountID const issuer = amount.getIssuer(); + if (!isXRP(amount) && issuer != account_ && issuer != dest && + !amount.holds()) + { + auto page = ctx_.view().dirInsert( + keylet::ownerDir(issuer), escrowKeylet, describeOwnerDir(issuer)); + if (!page) + return tecDIR_FULL; // LCOV_EXCL_LINE + (*slep)[sfIssuerNode] = *page; + } + + // Deduct owner's balance + if (isXRP(amount)) + (*sle)[sfBalance] = (*sle)[sfBalance] - amount; + else + { + if (auto const ret = std::visit( + [&](T const&) { + return escrowLockApplyHelper( + ctx_.view(), issuer, account_, amount, j_); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + + // increment owner count adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal); ctx_.view().update(sle); - return tesSUCCESS; } @@ -365,19 +696,324 @@ EscrowFinish::calculateBaseFee(ReadView const& view, STTx const& tx) return Transactor::calculateBaseFee(view, tx) + extraFee; } +template +static TER +escrowFinishPreclaimHelper( + PreclaimContext const& ctx, + AccountID const& dest, + STAmount const& amount); + +template <> +TER +escrowFinishPreclaimHelper( + PreclaimContext const& ctx, + AccountID const& dest, + STAmount const& amount) +{ + AccountID issuer = amount.getIssuer(); + // If the issuer is the same as the account, return tesSUCCESS + if (issuer == dest) + return tesSUCCESS; + + // 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 deep frozen the destination, return tecFROZEN + if (isDeepFrozen(ctx.view, dest, amount.getCurrency(), amount.getIssuer())) + return tecFROZEN; + + return tesSUCCESS; +} + +template <> +TER +escrowFinishPreclaimHelper( + PreclaimContext const& ctx, + AccountID const& dest, + STAmount const& amount) +{ + AccountID issuer = amount.getIssuer(); + // If the issuer is the same as the dest, return tesSUCCESS + if (issuer == dest) + 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 issuer has requireAuth set, check if the destination is + // authorized + auto const& mptIssue = amount.get(); + if (auto const ter = + requireAuth(ctx.view, mptIssue, dest, MPTAuthType::WeakAuth); + ter != tesSUCCESS) + return ter; + + // If the issuer has frozen the destination, return tecLOCKED + if (isFrozen(ctx.view, dest, mptIssue)) + return tecLOCKED; + + return tesSUCCESS; +} + TER EscrowFinish::preclaim(PreclaimContext const& ctx) { - if (!ctx.view.rules().enabled(featureCredentials)) - return Transactor::preclaim(ctx); + if (ctx.view.rules().enabled(featureCredentials)) + { + if (auto const err = credentials::valid(ctx, ctx.tx[sfAccount]); + !isTesSuccess(err)) + return err; + } - if (auto const err = credentials::valid(ctx, ctx.tx[sfAccount]); - !isTesSuccess(err)) - return err; + auto const k = keylet::escrow(ctx.tx[sfOwner], ctx.tx[sfOfferSequence]); + auto const slep = ctx.view.read(k); + if (!slep) + return tecNO_TARGET; + AccountID const dest = (*slep)[sfDestination]; + STAmount const amount = (*slep)[sfAmount]; + + if (!isXRP(amount)) + { + if (!ctx.view.rules().enabled(featureTokenEscrow)) + return temDISABLED; // LCOV_EXCL_LINE + + if (auto const ret = std::visit( + [&](T const&) { + return escrowFinishPreclaimHelper(ctx, dest, amount); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } return tesSUCCESS; } +template +static TER +escrowUnlockApplyHelper( + ApplyView& view, + Rate lockedRate, + std::shared_ptr const& sleDest, + STAmount const& xrpBalance, + STAmount const& amount, + AccountID const& issuer, + AccountID const& sender, + AccountID const& receiver, + bool createAsset, + beast::Journal journal); + +template <> +TER +escrowUnlockApplyHelper( + ApplyView& view, + Rate lockedRate, + std::shared_ptr const& sleDest, + STAmount const& xrpBalance, + STAmount const& amount, + AccountID const& issuer, + AccountID const& sender, + AccountID const& receiver, + bool createAsset, + beast::Journal journal) +{ + Keylet const trustLineKey = keylet::line(receiver, amount.issue()); + bool const recvLow = issuer > receiver; + bool const senderIssuer = issuer == sender; + bool const receiverIssuer = issuer == receiver; + bool const issuerHigh = issuer > receiver; + + // LCOV_EXCL_START + if (senderIssuer) + return tecINTERNAL; + // LCOV_EXCL_STOP + + if (receiverIssuer) + return tesSUCCESS; + + if (!view.exists(trustLineKey) && createAsset && !receiverIssuer) + { + // Can the account cover the trust line's reserve? + if (std::uint32_t const ownerCount = {sleDest->at(sfOwnerCount)}; + xrpBalance < 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 + recvLow, // is dest low? + issuer, // source + receiver, // 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, receiver), // limit of zero + 0, // quality in + 0, // quality out + journal); // journal + !isTesSuccess(ter)) + { + return ter; // LCOV_EXCL_LINE + } + // clang-format on + + view.update(sleDest); + } + + if (!view.exists(trustLineKey) && !receiverIssuer) + return tecNO_LINE; + + auto const xferRate = transferRate(view, amount); + // 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 (senderIssuer or + // receiverIssuer) + // 2. The locked rate is different from the parity rate + + // NOTE: Transfer fee in escrow works a bit differently from a normal + // payment. In escrow, the fee is deducted from the locked/sending amount, + // whereas in a normal payment, the transfer fee is taken on top of the + // sending amount. + auto finalAmt = amount; + if ((!senderIssuer && !receiverIssuer) && 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; + } + + // validate the line limit if the account submitting txn is not the receiver + // of the funds + if (!createAsset) + { + auto const sleRippleState = view.peek(trustLineKey); + if (!sleRippleState) + return tecINTERNAL; // LCOV_EXCL_LINE + + // if the issuer is the high, then we use the low limit + // otherwise we use the high limit + STAmount const lineLimit = sleRippleState->getFieldAmount( + issuerHigh ? sfLowLimit : sfHighLimit); + + STAmount lineBalance = sleRippleState->getFieldAmount(sfBalance); + + // flip the sign of the line balance if the issuer is not high + if (!issuerHigh) + lineBalance.negate(); + + // add the final amount to the line balance + lineBalance += finalAmt; + + // if the transfer would exceed the line limit return tecLIMIT_EXCEEDED + if (lineLimit < lineBalance) + return tecLIMIT_EXCEEDED; + } + + // if destination is not the issuer then transfer funds + if (!receiverIssuer) + { + auto const ter = + rippleCredit(view, issuer, receiver, finalAmt, true, journal); + if (ter != tesSUCCESS) + return ter; // LCOV_EXCL_LINE + } + return tesSUCCESS; +} + +template <> +TER +escrowUnlockApplyHelper( + ApplyView& view, + Rate lockedRate, + std::shared_ptr const& sleDest, + STAmount const& xrpBalance, + STAmount const& amount, + AccountID const& issuer, + AccountID const& sender, + AccountID const& receiver, + bool createAsset, + beast::Journal journal) +{ + bool const senderIssuer = issuer == sender; + bool const receiverIssuer = issuer == receiver; + + auto const mptID = amount.get().getMptID(); + auto const issuanceKey = keylet::mptIssuance(mptID); + if (!view.exists(keylet::mptoken(issuanceKey.key, receiver)) && + createAsset && !receiverIssuer) + { + if (std::uint32_t const ownerCount = {sleDest->at(sfOwnerCount)}; + xrpBalance < view.fees().accountReserve(ownerCount + 1)) + { + return tecINSUFFICIENT_RESERVE; + } + + if (auto const ter = + MPTokenAuthorize::createMPToken(view, mptID, receiver, 0); + !isTesSuccess(ter)) + { + return ter; // LCOV_EXCL_LINE + } + + // update owner count. + adjustOwnerCount(view, sleDest, 1, journal); + } + + if (!view.exists(keylet::mptoken(issuanceKey.key, receiver)) && + !receiverIssuer) + return tecNO_PERMISSION; + + auto const xferRate = transferRate(view, amount); + // 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 (senderIssuer or + // receiverIssuer) + // 2. The locked rate is different from the parity rate + + // NOTE: Transfer fee in escrow works a bit differently from a normal + // payment. In escrow, the fee is deducted from the locked/sending amount, + // whereas in a normal payment, the transfer fee is taken on top of the + // sending amount. + auto finalAmt = amount; + if ((!senderIssuer && !receiverIssuer) && lockedRate != parityRate) + { + // compute transfer fee, if any + auto const xferFee = amount.value() - + divideRound(amount, lockedRate, amount.asset(), true); + // compute balance to transfer + finalAmt = amount.value() - xferFee; + } + + return rippleUnlockEscrowMPT(view, sender, receiver, finalAmt, journal); +} + TER EscrowFinish::doApply() { @@ -500,8 +1136,50 @@ EscrowFinish::doApply() } } + STAmount const amount = slep->getFieldAmount(sfAmount); // Transfer amount to destination - (*sled)[sfBalance] = (*sled)[sfBalance] + (*slep)[sfAmount]; + if (isXRP(amount)) + (*sled)[sfBalance] = (*sled)[sfBalance] + amount; + else + { + if (!ctx_.view().rules().enabled(featureTokenEscrow)) + return temDISABLED; // LCOV_EXCL_LINE + + 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 escrowUnlockApplyHelper( + ctx_.view(), + 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 (!ctx_.view().dirRemove( + keylet::ownerDir(issuer), *optPage, k.key, true)) + { + JLOG(j_.fatal()) << "Unable to delete Escrow from recipient."; + return tefBAD_LEDGER; // LCOV_EXCL_LINE + } + } + } + ctx_.view().update(sled); // Adjust source owner count @@ -511,7 +1189,6 @@ EscrowFinish::doApply() // Remove escrow from ledger ctx_.view().erase(slep); - return tesSUCCESS; } @@ -530,6 +1207,90 @@ EscrowCancel::doPreflight(PreflightContext const& ctx) return tesSUCCESS; } +template +static TER +escrowCancelPreclaimHelper( + PreclaimContext const& ctx, + AccountID const& account, + STAmount const& amount); + +template <> +TER +escrowCancelPreclaimHelper( + PreclaimContext const& ctx, + AccountID const& account, + STAmount const& amount) +{ + AccountID issuer = amount.getIssuer(); + // If the issuer is the same as the account, return tecINTERNAL + if (issuer == account) + return tecINTERNAL; // LCOV_EXCL_LINE + + // 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; + + return tesSUCCESS; +} + +template <> +TER +escrowCancelPreclaimHelper( + PreclaimContext const& ctx, + AccountID const& account, + STAmount const& amount) +{ + AccountID issuer = amount.getIssuer(); + // If the issuer is the same as the account, return tecINTERNAL + if (issuer == account) + return tecINTERNAL; // LCOV_EXCL_LINE + + // 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; // LCOV_EXCL_LINE + + // 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, MPTAuthType::WeakAuth); + ter != tesSUCCESS) + return ter; + + return tesSUCCESS; +} + +TER +EscrowCancel::preclaim(PreclaimContext const& ctx) +{ + auto const k = keylet::escrow(ctx.tx[sfOwner], ctx.tx[sfOfferSequence]); + auto const slep = ctx.view.read(k); + if (!slep) + return tecNO_TARGET; + + AccountID const account = (*slep)[sfAccount]; + STAmount const amount = (*slep)[sfAmount]; + + if (!isXRP(amount)) + { + if (!ctx.view.rules().enabled(featureTokenEscrow)) + return temDISABLED; // LCOV_EXCL_LINE + + if (auto const ret = std::visit( + [&](T const&) { + return escrowCancelPreclaimHelper(ctx, account, amount); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; + } + return tesSUCCESS; +} + TER EscrowCancel::doApply() { @@ -586,9 +1347,49 @@ 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]; + STAmount const amount = slep->getFieldAmount(sfAmount); + + // Transfer amount back to the owner + if (isXRP(amount)) + (*sle)[sfBalance] = (*sle)[sfBalance] + amount; + else + { + if (!ctx_.view().rules().enabled(featureTokenEscrow)) + return temDISABLED; // LCOV_EXCL_LINE + + auto const issuer = amount.getIssuer(); + bool const createAsset = account == account_; + if (auto const ret = std::visit( + [&](T const&) { + return escrowUnlockApplyHelper( + ctx_.view(), + parityRate, + slep, + mPriorBalance, + amount, + issuer, + account, // sender and receiver are the same + account, + createAsset, + j_); + }, + amount.asset().value()); + !isTesSuccess(ret)) + return ret; // LCOV_EXCL_LINE + + // Remove escrow from issuers owner directory, if present. + if (auto const optPage = (*slep)[~sfIssuerNode]; optPage) + { + if (!ctx_.view().dirRemove( + keylet::ownerDir(issuer), *optPage, k.key, true)) + { + JLOG(j_.fatal()) << "Unable to delete Escrow from recipient."; + return tefBAD_LEDGER; // LCOV_EXCL_LINE + } + } + } + adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal); ctx_.view().update(sle); diff --git a/src/xrpld/app/tx/detail/Escrow.h b/src/xrpld/app/tx/detail/Escrow.h index f857fea9cb..86ae208b67 100644 --- a/src/xrpld/app/tx/detail/Escrow.h +++ b/src/xrpld/app/tx/detail/Escrow.h @@ -96,6 +96,9 @@ public: static NotTEC doPreflight(PreflightContext const& ctx); + static TER + preclaim(PreclaimContext const& ctx); + TER doApply() override; }; diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index a56870c1b9..1fc4afc971 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -17,6 +17,8 @@ */ //============================================================================== +#include +#include #include #include #include @@ -160,7 +162,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; @@ -181,7 +184,7 @@ XRPNotCreated::visitEntry( .drops(); break; case ltESCROW: - if (!isDelete) + if (!isDelete && isXRP((*after)[sfAmount])) drops_ += (*after)[sfAmount].xrp().drops(); break; default: @@ -321,14 +324,35 @@ NoZeroEscrow::visitEntry( std::shared_ptr const& after) { auto isBad = [](STAmount const& amount) { - if (!amount.native()) - return true; + // IOU case + if (amount.holds()) + { + if (amount <= beast::zero) + return true; - if (amount.xrp() <= XRPAmount{0}) - return true; + if (badCurrency() == amount.getCurrency()) + return true; + } - if (amount.xrp() >= INITIAL_XRP) - return true; + // MPT case + if (amount.holds()) + { + if (amount <= beast::zero) + return true; + + if (amount.mpt() > MPTAmount{maxMPTokenAmount}) + return true; + } + + // XRP case + if (amount.native()) + { + if (amount.xrp() <= XRPAmount{0}) + return true; + + if (amount.xrp() >= INITIAL_XRP) + return true; + } return false; }; @@ -338,14 +362,40 @@ NoZeroEscrow::visitEntry( if (after && after->getType() == ltESCROW) bad_ |= isBad((*after)[sfAmount]); + + auto checkAmount = [this](std::int64_t amount) { + if (amount > maxMPTokenAmount || amount < 0) + bad_ = true; + }; + + if (after && after->getType() == ltMPTOKEN_ISSUANCE) + { + auto const outstanding = (*after)[sfOutstandingAmount]; + checkAmount(outstanding); + if (auto const locked = (*after)[~sfLockedAmount]) + { + checkAmount(*locked); + bad_ = outstanding < *locked; + } + } + + if (after && after->getType() == ltMPTOKEN) + { + auto const mptAmount = (*after)[sfMPTAmount]; + checkAmount(mptAmount); + if (auto const locked = (*after)[~sfLockedAmount]) + { + checkAmount(*locked); + } + } } bool NoZeroEscrow::finalize( - STTx const&, + STTx const& txn, TER const, XRPAmount const, - ReadView const&, + ReadView const& rv, beast::Journal const& j) { if (bad_) @@ -1516,6 +1566,9 @@ ValidMPTIssuance::finalize( return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 && mptokensCreated_ == 0 && mptokensDeleted_ == 0; } + + if (tx.getTxnType() == ttESCROW_FINISH) + return true; } if (mptIssuancesCreated_ != 0) @@ -1723,6 +1776,311 @@ ValidPermissionedDEX::finalize( return true; } +void +ValidAMM::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (isDelete) + return; + + if (after) + { + auto const type = after->getType(); + // AMM object changed + if (type == ltAMM) + { + ammAccount_ = after->getAccountID(sfAccount); + lptAMMBalanceAfter_ = after->getFieldAmount(sfLPTokenBalance); + } + // AMM pool changed + else if ( + (type == ltRIPPLE_STATE && after->getFlags() & lsfAMMNode) || + (type == ltACCOUNT_ROOT && after->isFieldPresent(sfAMMID))) + { + ammPoolChanged_ = true; + } + } + + if (before) + { + // AMM object changed + if (before->getType() == ltAMM) + { + lptAMMBalanceBefore_ = before->getFieldAmount(sfLPTokenBalance); + } + } +} + +static bool +validBalances( + STAmount const& amount, + STAmount const& amount2, + STAmount const& lptAMMBalance, + ValidAMM::ZeroAllowed zeroAllowed) +{ + bool const positive = amount > beast::zero && amount2 > beast::zero && + lptAMMBalance > beast::zero; + if (zeroAllowed == ValidAMM::ZeroAllowed::Yes) + return positive || + (amount == beast::zero && amount2 == beast::zero && + lptAMMBalance == beast::zero); + return positive; +} + +bool +ValidAMM::finalizeVote(bool enforce, beast::Journal const& j) const +{ + if (lptAMMBalanceAfter_ != lptAMMBalanceBefore_ || ammPoolChanged_) + { + // LPTokens and the pool can not change on vote + // LCOV_EXCL_START + JLOG(j.error()) << "AMMVote invariant failed: " + << lptAMMBalanceBefore_.value_or(STAmount{}) << " " + << lptAMMBalanceAfter_.value_or(STAmount{}) << " " + << ammPoolChanged_; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + + return true; +} + +bool +ValidAMM::finalizeBid(bool enforce, beast::Journal const& j) const +{ + if (ammPoolChanged_) + { + // The pool can not change on bid + // LCOV_EXCL_START + JLOG(j.error()) << "AMMBid invariant failed: pool changed"; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + // LPTokens are burnt, therefore there should be fewer LPTokens + else if ( + lptAMMBalanceBefore_ && lptAMMBalanceAfter_ && + (*lptAMMBalanceAfter_ > *lptAMMBalanceBefore_ || + *lptAMMBalanceAfter_ <= beast::zero)) + { + // LCOV_EXCL_START + JLOG(j.error()) << "AMMBid invariant failed: " << *lptAMMBalanceBefore_ + << " " << *lptAMMBalanceAfter_; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + + return true; +} + +bool +ValidAMM::finalizeCreate( + STTx const& tx, + ReadView const& view, + bool enforce, + beast::Journal const& j) const +{ + if (!ammAccount_) + { + // LCOV_EXCL_START + JLOG(j.error()) + << "AMMCreate invariant failed: AMM object is not created"; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + else + { + auto const [amount, amount2] = ammPoolHolds( + view, + *ammAccount_, + tx[sfAmount].get(), + tx[sfAmount2].get(), + fhIGNORE_FREEZE, + j); + // Create invariant: + // sqrt(amount * amount2) == LPTokens + // all balances are greater than zero + if (!validBalances( + amount, amount2, *lptAMMBalanceAfter_, ZeroAllowed::No) || + ammLPTokens(amount, amount2, lptAMMBalanceAfter_->issue()) != + *lptAMMBalanceAfter_) + { + JLOG(j.error()) << "AMMCreate invariant failed: " << amount << " " + << amount2 << " " << *lptAMMBalanceAfter_; + if (enforce) + return false; + } + } + + return true; +} + +bool +ValidAMM::finalizeDelete(bool enforce, TER res, beast::Journal const& j) const +{ + if (ammAccount_) + { + // LCOV_EXCL_START + std::string const msg = (res == tesSUCCESS) + ? "AMM object is not deleted on tesSUCCESS" + : "AMM object is changed on tecINCOMPLETE"; + JLOG(j.error()) << "AMMDelete invariant failed: " << msg; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + + return true; +} + +bool +ValidAMM::finalizeDEX(bool enforce, beast::Journal const& j) const +{ + if (ammAccount_) + { + // LCOV_EXCL_START + JLOG(j.error()) << "AMM swap invariant failed: AMM object changed"; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + + return true; +} + +bool +ValidAMM::generalInvariant( + ripple::STTx const& tx, + ripple::ReadView const& view, + ZeroAllowed zeroAllowed, + beast::Journal const& j) const +{ + auto const [amount, amount2] = ammPoolHolds( + view, + *ammAccount_, + tx[sfAsset].get(), + tx[sfAsset2].get(), + fhIGNORE_FREEZE, + j); + // Deposit and Withdrawal invariant: + // sqrt(amount * amount2) >= LPTokens + // all balances are greater than zero + // unless on last withdrawal + auto const poolProductMean = root2(amount * amount2); + bool const nonNegativeBalances = + validBalances(amount, amount2, *lptAMMBalanceAfter_, zeroAllowed); + bool const strongInvariantCheck = poolProductMean >= *lptAMMBalanceAfter_; + // Allow for a small relative error if strongInvariantCheck fails + auto weakInvariantCheck = [&]() { + return *lptAMMBalanceAfter_ != beast::zero && + withinRelativeDistance( + poolProductMean, Number{*lptAMMBalanceAfter_}, Number{1, -11}); + }; + if (!nonNegativeBalances || + (!strongInvariantCheck && !weakInvariantCheck())) + { + JLOG(j.error()) << "AMM " << tx.getTxnType() << " invariant failed: " + << tx.getHash(HashPrefix::transactionID) << " " + << ammPoolChanged_ << " " << amount << " " << amount2 + << " " << poolProductMean << " " + << lptAMMBalanceAfter_->getText() << " " + << ((*lptAMMBalanceAfter_ == beast::zero) + ? Number{1} + : ((*lptAMMBalanceAfter_ - poolProductMean) / + poolProductMean)); + return false; + } + + return true; +} + +bool +ValidAMM::finalizeDeposit( + ripple::STTx const& tx, + ripple::ReadView const& view, + bool enforce, + beast::Journal const& j) const +{ + if (!ammAccount_) + { + // LCOV_EXCL_START + JLOG(j.error()) << "AMMDeposit invariant failed: AMM object is deleted"; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + else if (!generalInvariant(tx, view, ZeroAllowed::No, j) && enforce) + return false; + + return true; +} + +bool +ValidAMM::finalizeWithdraw( + ripple::STTx const& tx, + ripple::ReadView const& view, + bool enforce, + beast::Journal const& j) const +{ + if (!ammAccount_) + { + // Last Withdraw or Clawback deleted AMM + } + else if (!generalInvariant(tx, view, ZeroAllowed::Yes, j)) + { + if (enforce) + return false; + } + + return true; +} + +bool +ValidAMM::finalize( + STTx const& tx, + TER const result, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + // Delete may return tecINCOMPLETE if there are too many + // trustlines to delete. + if (result != tesSUCCESS && result != tecINCOMPLETE) + return true; + + bool const enforce = view.rules().enabled(fixAMMv1_3); + + switch (tx.getTxnType()) + { + case ttAMM_CREATE: + return finalizeCreate(tx, view, enforce, j); + case ttAMM_DEPOSIT: + return finalizeDeposit(tx, view, enforce, j); + case ttAMM_CLAWBACK: + case ttAMM_WITHDRAW: + return finalizeWithdraw(tx, view, enforce, j); + case ttAMM_BID: + return finalizeBid(enforce, j); + case ttAMM_VOTE: + return finalizeVote(enforce, j); + case ttAMM_DELETE: + return finalizeDelete(enforce, result, j); + case ttCHECK_CASH: + case ttOFFER_CREATE: + case ttPAYMENT: + return finalizeDEX(enforce, j); + default: + break; + } + + return true; +} + //------------------------------------------------------------------------------ void diff --git a/src/xrpld/app/tx/detail/InvariantCheck.h b/src/xrpld/app/tx/detail/InvariantCheck.h index f7af0da725..c3725cbc51 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.h +++ b/src/xrpld/app/tx/detail/InvariantCheck.h @@ -648,6 +648,69 @@ public: beast::Journal const&); }; +class ValidAMM +{ + std::optional ammAccount_; + std::optional lptAMMBalanceAfter_; + std::optional lptAMMBalanceBefore_; + bool ammPoolChanged_; + +public: + enum class ZeroAllowed : bool { No = false, Yes = true }; + + ValidAMM() : ammPoolChanged_{false} + { + } + void + visitEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const&); + + bool + finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const&); + +private: + bool + finalizeBid(bool enforce, beast::Journal const&) const; + bool + finalizeVote(bool enforce, beast::Journal const&) const; + bool + finalizeCreate( + STTx const&, + ReadView const&, + bool enforce, + beast::Journal const&) const; + bool + finalizeDelete(bool enforce, TER res, beast::Journal const&) const; + bool + finalizeDeposit( + STTx const&, + ReadView const&, + bool enforce, + beast::Journal const&) const; + // Includes clawback + bool + finalizeWithdraw( + STTx const&, + ReadView const&, + bool enforce, + beast::Journal const&) const; + bool + finalizeDEX(bool enforce, beast::Journal const&) const; + bool + generalInvariant( + STTx const&, + ReadView const&, + ZeroAllowed zeroAllowed, + beast::Journal const&) const; +}; + /** * @brief Invariants: Some fields are unmodifiable * @@ -789,6 +852,7 @@ using InvariantChecks = std::tuple< ValidMPTIssuance, ValidPermissionedDomain, ValidPermissionedDEX, + ValidAMM, NoModifiedUnmodifiableFields, ValidPseudoAccounts, ValidLoanBroker, diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp index 1fec7d9c3e..f4a93ffb3e 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp @@ -86,6 +86,15 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx) return tecHAS_OBLIGATIONS; } + if ((*sleMpt)[~sfLockedAmount].value_or(0) != 0) + { + auto const sleMptIssuance = ctx.view.read( + keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID])); + if (!sleMptIssuance) + return tefINTERNAL; // LCOV_EXCL_LINE + + return tecHAS_OBLIGATIONS; + } if (ctx.view.rules().enabled(featureSingleAssetVault) && sleMpt->isFlag(lsfMPTLocked)) return tecNO_PERMISSION; @@ -143,6 +152,32 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx) return tesSUCCESS; } +TER +MPTokenAuthorize::createMPToken( + ApplyView& view, + MPTID const& mptIssuanceID, + AccountID const& account, + std::uint32_t const flags) +{ + auto const mptokenKey = keylet::mptoken(mptIssuanceID, account); + + auto const ownerNode = view.dirInsert( + keylet::ownerDir(account), mptokenKey, describeOwnerDir(account)); + + if (!ownerNode) + return tecDIR_FULL; // LCOV_EXCL_LINE + + auto mptoken = std::make_shared(mptokenKey); + (*mptoken)[sfAccount] = account; + (*mptoken)[sfMPTokenIssuanceID] = mptIssuanceID; + (*mptoken)[sfFlags] = flags; + (*mptoken)[sfOwnerNode] = *ownerNode; + + view.insert(mptoken); + + return tesSUCCESS; +} + TER MPTokenAuthorize::authorize( ApplyView& view, diff --git a/src/xrpld/app/tx/detail/MPTokenAuthorize.h b/src/xrpld/app/tx/detail/MPTokenAuthorize.h index 5651dbf075..63ca2c8b01 100644 --- a/src/xrpld/app/tx/detail/MPTokenAuthorize.h +++ b/src/xrpld/app/tx/detail/MPTokenAuthorize.h @@ -60,6 +60,13 @@ public: beast::Journal journal, MPTAuthorizeArgs const& args); + static TER + createMPToken( + ApplyView& view, + MPTID const& mptIssuanceID, + AccountID const& account, + std::uint32_t const flags); + TER doApply() override; }; diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp index 3705f8dd6e..3181050e3e 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp @@ -60,6 +60,9 @@ MPTokenIssuanceDestroy::preclaim(PreclaimContext const& ctx) if ((*sleMPT)[sfOutstandingAmount] != 0) return tecHAS_OBLIGATIONS; + if ((*sleMPT)[~sfLockedAmount].value_or(0) != 0) + return tecHAS_OBLIGATIONS; + return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp index 399d24ccd8..a36df80d21 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp @@ -65,7 +65,7 @@ MPTokenIssuanceSet::checkPermission(ReadView const& view, STTx const& tx) auto const sle = view.read(delegateKey); if (!sle) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; if (checkTxPermission(sle, tx) == tesSUCCESS) return tesSUCCESS; @@ -75,18 +75,18 @@ MPTokenIssuanceSet::checkPermission(ReadView const& view, STTx const& tx) // this is added in case more flags will be added for MPTokenIssuanceSet // in the future. Currently unreachable. if (txFlags & tfMPTokenIssuanceSetPermissionMask) - return tecNO_PERMISSION; // LCOV_EXCL_LINE + return tecNO_DELEGATE_PERMISSION; // LCOV_EXCL_LINE std::unordered_set granularPermissions; loadGranularPermission(sle, ttMPTOKEN_ISSUANCE_SET, granularPermissions); if (txFlags & tfMPTLock && !granularPermissions.contains(MPTokenIssuanceLock)) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; if (txFlags & tfMPTUnlock && !granularPermissions.contains(MPTokenIssuanceUnlock)) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp index 194d6f9b52..df56b0c763 100644 --- a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp +++ b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.cpp @@ -163,6 +163,27 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) if ((*so)[sfAmount] > (*bo)[sfAmount] - *brokerFee) return tecINSUFFICIENT_PAYMENT; + + // Check if broker is allowed to receive the fee with these IOUs. + if (!brokerFee->native() && + ctx.view.rules().enabled(fixEnforceNFTokenTrustlineV2)) + { + auto res = nft::checkTrustlineAuthorized( + ctx.view, + ctx.tx[sfAccount], + ctx.j, + brokerFee->asset().get()); + if (res != tesSUCCESS) + return res; + + res = nft::checkTrustlineDeepFrozen( + ctx.view, + ctx.tx[sfAccount], + ctx.j, + brokerFee->asset().get()); + if (res != tesSUCCESS) + return res; + } } } @@ -211,6 +232,38 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) fhZERO_IF_FROZEN, ctx.j) < needed) return tecINSUFFICIENT_FUNDS; + + // Check that the account accepting the buy offer (he's selling the NFT) + // is allowed to receive IOUs. Also check that this offer's creator is + // authorized. But we need to exclude the case when the transaction is + // created by the broker. + if (ctx.view.rules().enabled(fixEnforceNFTokenTrustlineV2) && + !needed.native()) + { + auto res = nft::checkTrustlineAuthorized( + ctx.view, bo->at(sfOwner), ctx.j, needed.asset().get()); + if (res != tesSUCCESS) + return res; + + if (!so) + { + res = nft::checkTrustlineAuthorized( + ctx.view, + ctx.tx[sfAccount], + ctx.j, + needed.asset().get()); + if (res != tesSUCCESS) + return res; + + res = nft::checkTrustlineDeepFrozen( + ctx.view, + ctx.tx[sfAccount], + ctx.j, + needed.asset().get()); + if (res != tesSUCCESS) + return res; + } + } } if (so) @@ -273,42 +326,74 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) } // Make sure that we are allowed to hold what the taker will pay us. - // This is a similar approach taken by usual offers. if (!needed.native()) { - auto const result = checkAcceptAsset( - ctx.view, - ctx.flags, - (*so)[sfOwner], - ctx.j, - needed.asset().get()); - if (result != tesSUCCESS) - return result; + if (ctx.view.rules().enabled(fixEnforceNFTokenTrustlineV2)) + { + auto res = nft::checkTrustlineAuthorized( + ctx.view, + (*so)[sfOwner], + ctx.j, + needed.asset().get()); + if (res != tesSUCCESS) + return res; + + if (!bo) + { + res = nft::checkTrustlineAuthorized( + ctx.view, + ctx.tx[sfAccount], + ctx.j, + needed.asset().get()); + if (res != tesSUCCESS) + return res; + } + } + + auto const res = nft::checkTrustlineDeepFrozen( + ctx.view, (*so)[sfOwner], ctx.j, needed.asset().get()); + if (res != tesSUCCESS) + return res; } } - // Fix a bug where the transfer of an NFToken with a transfer fee could - // give the NFToken issuer an undesired trust line. - if (ctx.view.rules().enabled(fixEnforceNFTokenTrustline)) + // Additional checks are required in case a minter set a transfer fee for + // this nftoken + auto const& offer = bo ? bo : so; + if (!offer) + // Purely defensive, should be caught in preflight. + return tecINTERNAL; + + auto const& tokenID = offer->at(sfNFTokenID); + auto const& amount = offer->at(sfAmount); + auto const nftMinter = nft::getIssuer(tokenID); + + if (nft::getTransferFee(tokenID) != 0 && !amount.native()) { - std::shared_ptr const& offer = bo ? bo : so; - if (!offer) - // Should be caught in preflight. - return tecINTERNAL; - - uint256 const& tokenID = offer->at(sfNFTokenID); - STAmount const& amount = offer->at(sfAmount); - if (nft::getTransferFee(tokenID) != 0 && + // Fix a bug where the transfer of an NFToken with a transfer fee could + // give the NFToken issuer an undesired trust line. + // Issuer doesn't need a trust line to accept their own currency. + if (ctx.view.rules().enabled(fixEnforceNFTokenTrustline) && (nft::getFlags(tokenID) & nft::flagCreateTrustLines) == 0 && - !amount.native()) + nftMinter != amount.getIssuer() && + !ctx.view.read(keylet::line(nftMinter, amount.issue()))) + return tecNO_LINE; + + // Check that the issuer is allowed to receive IOUs. + if (ctx.view.rules().enabled(fixEnforceNFTokenTrustlineV2)) { - auto const issuer = nft::getIssuer(tokenID); - // Issuer doesn't need a trust line to accept their own currency. - if (issuer != amount.getIssuer() && - !ctx.view.read(keylet::line(issuer, amount.issue()))) - return tecNO_LINE; + auto res = nft::checkTrustlineAuthorized( + ctx.view, nftMinter, ctx.j, amount.asset().get()); + if (res != tesSUCCESS) + return res; + + res = nft::checkTrustlineDeepFrozen( + ctx.view, nftMinter, ctx.j, amount.asset().get()); + if (res != tesSUCCESS) + return res; } } + return tesSUCCESS; } @@ -527,62 +612,4 @@ NFTokenAcceptOffer::doApply() return tecINTERNAL; } -TER -NFTokenAcceptOffer::checkAcceptAsset( - ReadView const& view, - ApplyFlags const flags, - AccountID const id, - beast::Journal const j, - Issue const& issue) -{ - // Only valid for custom currencies - - if (!view.rules().enabled(featureDeepFreeze)) - { - return tesSUCCESS; - } - - XRPL_ASSERT( - !isXRP(issue.currency), - "NFTokenAcceptOffer::checkAcceptAsset : valid to check."); - auto const issuerAccount = view.read(keylet::account(issue.account)); - - if (!issuerAccount) - { - JLOG(j.debug()) - << "delay: can't receive IOUs from non-existent issuer: " - << to_string(issue.account); - - return tecNO_ISSUER; - } - - // An account can not create a trustline to itself, so no line can exist - // to be frozen. Additionally, an issuer can always accept its own - // issuance. - if (issue.account == id) - { - return tesSUCCESS; - } - - auto const trustLine = - view.read(keylet::line(id, issue.account, issue.currency)); - - if (!trustLine) - { - return tesSUCCESS; - } - - // There's no difference which side enacted deep freeze, accepting - // tokens shouldn't be possible. - bool const deepFrozen = - (*trustLine)[sfFlags] & (lsfLowDeepFreeze | lsfHighDeepFreeze); - - if (deepFrozen) - { - return tecFROZEN; - } - - return tesSUCCESS; -} - } // namespace ripple diff --git a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.h b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.h index 7effde2d45..637944af92 100644 --- a/src/xrpld/app/tx/detail/NFTokenAcceptOffer.h +++ b/src/xrpld/app/tx/detail/NFTokenAcceptOffer.h @@ -44,14 +44,6 @@ private: AccountID const& seller, uint256 const& nfTokenID); - static TER - checkAcceptAsset( - ReadView const& view, - ApplyFlags const flags, - AccountID const id, - beast::Journal const j, - Issue const& issue); - public: static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; diff --git a/src/xrpld/app/tx/detail/NFTokenUtils.cpp b/src/xrpld/app/tx/detail/NFTokenUtils.cpp index 9c9754aa95..4866a3b385 100644 --- a/src/xrpld/app/tx/detail/NFTokenUtils.cpp +++ b/src/xrpld/app/tx/detail/NFTokenUtils.cpp @@ -1004,6 +1004,18 @@ tokenOfferCreatePreclaim( } } + if (view.rules().enabled(fixEnforceNFTokenTrustlineV2) && !amount.native()) + { + // If this is a sell offer, check that the account is allowed to + // receive IOUs. If this is a buy offer, we have to check that trustline + // is authorized, even though we previosly checked it's balance via + // accountHolds. This is due to a possibility of existence of + // unauthorized trustlines with balance + auto const res = nft::checkTrustlineAuthorized( + view, acctID, j, amount.asset().get()); + if (res != tesSUCCESS) + return res; + } return tesSUCCESS; } @@ -1081,5 +1093,115 @@ tokenOfferCreateApply( return tesSUCCESS; } +TER +checkTrustlineAuthorized( + ReadView const& view, + AccountID const id, + beast::Journal const j, + Issue const& issue) +{ + // Only valid for custom currencies + XRPL_ASSERT( + !isXRP(issue.currency), + "ripple::nft::checkTrustlineAuthorized : valid to check."); + + if (view.rules().enabled(fixEnforceNFTokenTrustlineV2)) + { + auto const issuerAccount = view.read(keylet::account(issue.account)); + if (!issuerAccount) + { + JLOG(j.debug()) << "ripple::nft::checkTrustlineAuthorized: can't " + "receive IOUs from non-existent issuer: " + << to_string(issue.account); + + return tecNO_ISSUER; + } + + // An account can not create a trustline to itself, so no line can + // exist to be authorized. Additionally, an issuer can always accept + // its own issuance. + if (issue.account == id) + { + return tesSUCCESS; + } + + if (issuerAccount->isFlag(lsfRequireAuth)) + { + auto const trustLine = + view.read(keylet::line(id, issue.account, issue.currency)); + + if (!trustLine) + { + return tecNO_LINE; + } + + // Entries have a canonical representation, determined by a + // lexicographical "greater than" comparison employing strict + // weak ordering. Determine which entry we need to access. + if (!trustLine->isFlag( + id > issue.account ? lsfLowAuth : lsfHighAuth)) + { + return tecNO_AUTH; + } + } + } + + return tesSUCCESS; +} + +TER +checkTrustlineDeepFrozen( + ReadView const& view, + AccountID const id, + beast::Journal const j, + Issue const& issue) +{ + // Only valid for custom currencies + XRPL_ASSERT( + !isXRP(issue.currency), + "ripple::nft::checkTrustlineDeepFrozen : valid to check."); + + if (view.rules().enabled(featureDeepFreeze)) + { + auto const issuerAccount = view.read(keylet::account(issue.account)); + if (!issuerAccount) + { + JLOG(j.debug()) << "ripple::nft::checkTrustlineDeepFrozen: can't " + "receive IOUs from non-existent issuer: " + << to_string(issue.account); + + return tecNO_ISSUER; + } + + // An account can not create a trustline to itself, so no line can + // exist to be frozen. Additionally, an issuer can always accept its + // own issuance. + if (issue.account == id) + { + return tesSUCCESS; + } + + auto const trustLine = + view.read(keylet::line(id, issue.account, issue.currency)); + + if (!trustLine) + { + return tesSUCCESS; + } + + // There's no difference which side enacted deep freeze, accepting + // tokens shouldn't be possible. + bool const deepFrozen = + (*trustLine)[sfFlags] & (lsfLowDeepFreeze | lsfHighDeepFreeze); + + if (deepFrozen) + { + return tecFROZEN; + } + } + + return tesSUCCESS; +} + } // namespace nft } // namespace ripple diff --git a/src/xrpld/app/tx/detail/NFTokenUtils.h b/src/xrpld/app/tx/detail/NFTokenUtils.h index 38ced59e9c..7ee0541984 100644 --- a/src/xrpld/app/tx/detail/NFTokenUtils.h +++ b/src/xrpld/app/tx/detail/NFTokenUtils.h @@ -152,6 +152,20 @@ tokenOfferCreateApply( beast::Journal j, std::uint32_t txFlags = lsfSellNFToken); +TER +checkTrustlineAuthorized( + ReadView const& view, + AccountID const id, + beast::Journal const j, + Issue const& issue); + +TER +checkTrustlineDeepFrozen( + ReadView const& view, + AccountID const id, + beast::Journal const j, + Issue const& issue); + } // namespace nft } // namespace ripple diff --git a/src/xrpld/app/tx/detail/Offer.h b/src/xrpld/app/tx/detail/Offer.h index abc0212335..d6ff4c7699 100644 --- a/src/xrpld/app/tx/detail/Offer.h +++ b/src/xrpld/app/tx/detail/Offer.h @@ -22,6 +22,7 @@ #include +#include #include #include #include @@ -170,8 +171,24 @@ public: * always returns true. */ bool - checkInvariant(TAmounts const&, beast::Journal j) const + checkInvariant(TAmounts const& consumed, beast::Journal j) const { + if (!isFeatureEnabled(fixAMMv1_3)) + return true; + + if (consumed.in > m_amounts.in || consumed.out > m_amounts.out) + { + // LCOV_EXCL_START + JLOG(j.error()) + << "AMMOffer::checkInvariant failed: consumed " + << to_string(consumed.in) << " " << to_string(consumed.out) + << " amounts " << to_string(m_amounts.in) << " " + << to_string(m_amounts.out); + + return false; + // LCOV_EXCL_STOP + } + return true; } }; diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index c062abad1c..fd0d639fd5 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -260,7 +260,7 @@ Payment::checkPermission(ReadView const& view, STTx const& tx) auto const sle = view.read(delegateKey); if (!sle) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; if (checkTxPermission(sle, tx) == tesSUCCESS) return tesSUCCESS; @@ -279,7 +279,7 @@ Payment::checkPermission(ReadView const& view, STTx const& tx) amountIssue.account == tx[sfDestination]) return tesSUCCESS; - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; } TER diff --git a/src/xrpld/app/tx/detail/SetAccount.cpp b/src/xrpld/app/tx/detail/SetAccount.cpp index 659ff41f12..9d39670130 100644 --- a/src/xrpld/app/tx/detail/SetAccount.cpp +++ b/src/xrpld/app/tx/detail/SetAccount.cpp @@ -199,7 +199,7 @@ SetAccount::checkPermission(ReadView const& view, STTx const& tx) auto const sle = view.read(delegateKey); if (!sle) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; std::unordered_set granularPermissions; loadGranularPermission(sle, ttACCOUNT_SET, granularPermissions); @@ -212,31 +212,31 @@ SetAccount::checkPermission(ReadView const& view, STTx const& tx) // update the flag on behalf of another account, it is not // authorized. if (uSetFlag != 0 || uClearFlag != 0 || uTxFlags & tfUniversalMask) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; if (tx.isFieldPresent(sfEmailHash) && !granularPermissions.contains(AccountEmailHashSet)) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; if (tx.isFieldPresent(sfWalletLocator) || tx.isFieldPresent(sfNFTokenMinter)) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; if (tx.isFieldPresent(sfMessageKey) && !granularPermissions.contains(AccountMessageKeySet)) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; if (tx.isFieldPresent(sfDomain) && !granularPermissions.contains(AccountDomainSet)) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; if (tx.isFieldPresent(sfTransferRate) && !granularPermissions.contains(AccountTransferRateSet)) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; if (tx.isFieldPresent(sfTickSize) && !granularPermissions.contains(AccountTickSizeSet)) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; return tesSUCCESS; } @@ -647,6 +647,15 @@ SetAccount::doApply() uFlagsOut &= ~lsfDisallowIncomingTrustline; } + // Set or clear flags for disallowing escrow + if (ctx_.view().rules().enabled(featureTokenEscrow)) + { + if (uSetFlag == asfAllowTrustLineLocking) + uFlagsOut |= lsfAllowTrustLineLocking; + else if (uClearFlag == asfAllowTrustLineLocking) + uFlagsOut &= ~lsfAllowTrustLineLocking; + } + // Set flag for clawback if (ctx_.view().rules().enabled(featureClawback) && uSetFlag == asfAllowTrustLineClawback) diff --git a/src/xrpld/app/tx/detail/SetTrust.cpp b/src/xrpld/app/tx/detail/SetTrust.cpp index 01bbff3ba5..f7ae39c1ae 100644 --- a/src/xrpld/app/tx/detail/SetTrust.cpp +++ b/src/xrpld/app/tx/detail/SetTrust.cpp @@ -138,7 +138,7 @@ SetTrust::checkPermission(ReadView const& view, STTx const& tx) auto const sle = view.read(delegateKey); if (!sle) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; if (checkTxPermission(sle, tx) == tesSUCCESS) return tesSUCCESS; @@ -149,10 +149,10 @@ SetTrust::checkPermission(ReadView const& view, STTx const& tx) // TrustlineUnfreeze granular permission. Setting other flags returns // error. if (txFlags & tfTrustSetPermissionMask) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; if (tx.isFieldPresent(sfQualityIn) || tx.isFieldPresent(sfQualityOut)) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; auto const saLimitAmount = tx.getFieldAmount(sfLimitAmount); auto const sleRippleState = view.read(keylet::line( @@ -161,19 +161,19 @@ SetTrust::checkPermission(ReadView const& view, STTx const& tx) // if the trustline does not exist, granular permissions are // not allowed to create trustline if (!sleRippleState) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; std::unordered_set granularPermissions; loadGranularPermission(sle, ttTRUST_SET, granularPermissions); if (txFlags & tfSetfAuth && !granularPermissions.contains(TrustlineAuthorize)) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; if (txFlags & tfSetFreeze && !granularPermissions.contains(TrustlineFreeze)) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; if (txFlags & tfClearFreeze && !granularPermissions.contains(TrustlineUnfreeze)) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; // updating LimitAmount is not allowed only with granular permissions, // unless there's a new granular permission for this in the future. @@ -185,7 +185,7 @@ SetTrust::checkPermission(ReadView const& view, STTx const& tx) saLimitAllow.setIssuer(tx[sfAccount]); if (curLimit != saLimitAllow) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 5d3dca1c81..5a7da4bc3d 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -269,7 +269,7 @@ Transactor::checkPermission(ReadView const& view, STTx const& tx) auto const sle = view.read(delegateKey); if (!sle) - return tecNO_PERMISSION; + return tecNO_DELEGATE_PERMISSION; return checkTxPermission(sle, tx); } diff --git a/src/xrpld/ledger/View.h b/src/xrpld/ledger/View.h index 5642047efc..4f19e34b97 100644 --- a/src/xrpld/ledger/View.h +++ b/src/xrpld/ledger/View.h @@ -357,6 +357,13 @@ transferRate(ReadView const& view, AccountID const& issuer); [[nodiscard]] Rate transferRate(ReadView const& view, MPTID const& issuanceID); +/** Returns the transfer fee as Rate based on the type of token + * @param view The ledger view + * @param amount The amount to transfer + */ +[[nodiscard]] Rate +transferRate(ReadView const& view, STAmount const& amount); + /** Returns `true` if the directory is empty @param key The key of the directory */ @@ -687,6 +694,21 @@ rippleCredit( bool bCheckIssuer, beast::Journal j); +TER +rippleLockEscrowMPT( + ApplyView& view, + AccountID const& uGrantorID, + STAmount const& saAmount, + beast::Journal j); + +TER +rippleUnlockEscrowMPT( + ApplyView& view, + AccountID const& uGrantorID, + AccountID const& uGranteeID, + STAmount const& saAmount, + beast::Journal j); + /** Calls static accountSendIOU if saAmount represents Issue. * Calls static accountSendMPT if saAmount represents MPTIssue. */ diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index 521e1d11ab..cdd25d32b5 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -781,6 +781,19 @@ transferRate(ReadView const& view, MPTID const& issuanceID) return parityRate; } +Rate +transferRate(ReadView const& view, STAmount const& amount) +{ + return std::visit( + [&](TIss const& issue) { + if constexpr (std::is_same_v) + return transferRate(view, issue.getIssuer()); + else + return transferRate(view, issue.getMptID()); + }, + amount.asset().value()); +} + bool areCompatible( ReadView const& validLedger, @@ -2785,6 +2798,249 @@ sharesToAssetsWithdraw( return assets; } +TER +rippleLockEscrowMPT( + ApplyView& view, + AccountID const& sender, + STAmount const& amount, + beast::Journal j) +{ + auto const mptIssue = amount.get(); + auto const mptID = keylet::mptIssuance(mptIssue.getMptID()); + auto sleIssuance = view.peek(mptID); + if (!sleIssuance) + { + JLOG(j.error()) << "rippleLockEscrowMPT: MPT issuance not found for " + << mptIssue.getMptID(); + return tecOBJECT_NOT_FOUND; // LCOV_EXCL_LINE + } + + if (amount.getIssuer() == sender) + { + JLOG(j.error()) + << "rippleLockEscrowMPT: sender is the issuer, cannot lock MPTs."; + return tecINTERNAL; // LCOV_EXCL_LINE + } + + // 1. Decrease the MPT Holder MPTAmount + // 2. Increase the MPT Holder EscrowedAmount + { + auto const mptokenID = keylet::mptoken(mptID.key, sender); + auto sle = view.peek(mptokenID); + if (!sle) + { + JLOG(j.error()) + << "rippleLockEscrowMPT: MPToken not found for " << sender; + return tecOBJECT_NOT_FOUND; // LCOV_EXCL_LINE + } + + auto const amt = sle->getFieldU64(sfMPTAmount); + auto const pay = amount.mpt().value(); + + // Underflow check for subtraction + if (!canSubtract(STAmount(mptIssue, amt), STAmount(mptIssue, pay))) + { + JLOG(j.error()) + << "rippleLockEscrowMPT: insufficient MPTAmount for " + << to_string(sender) << ": " << amt << " < " << pay; + return tecINTERNAL; // LCOV_EXCL_LINE + } + + (*sle)[sfMPTAmount] = amt - pay; + + // Overflow check for addition + uint64_t const locked = (*sle)[~sfLockedAmount].value_or(0); + + if (!canAdd(STAmount(mptIssue, locked), STAmount(mptIssue, pay))) + { + JLOG(j.error()) + << "rippleLockEscrowMPT: overflow on locked amount for " + << to_string(sender) << ": " << locked << " + " << pay; + return tecINTERNAL; // LCOV_EXCL_LINE + } + + if (sle->isFieldPresent(sfLockedAmount)) + (*sle)[sfLockedAmount] += pay; + else + sle->setFieldU64(sfLockedAmount, pay); + + view.update(sle); + } + + // 1. Increase the Issuance EscrowedAmount + // 2. DO NOT change the Issuance OutstandingAmount + { + uint64_t const issuanceEscrowed = + (*sleIssuance)[~sfLockedAmount].value_or(0); + auto const pay = amount.mpt().value(); + + // Overflow check for addition + if (!canAdd( + STAmount(mptIssue, issuanceEscrowed), STAmount(mptIssue, pay))) + { + JLOG(j.error()) << "rippleLockEscrowMPT: overflow on issuance " + "locked amount for " + << mptIssue.getMptID() << ": " << issuanceEscrowed + << " + " << pay; + return tecINTERNAL; // LCOV_EXCL_LINE + } + + if (sleIssuance->isFieldPresent(sfLockedAmount)) + (*sleIssuance)[sfLockedAmount] += pay; + else + sleIssuance->setFieldU64(sfLockedAmount, pay); + + view.update(sleIssuance); + } + return tesSUCCESS; +} + +TER +rippleUnlockEscrowMPT( + ApplyView& view, + AccountID const& sender, + AccountID const& receiver, + STAmount const& amount, + beast::Journal j) +{ + auto const issuer = amount.getIssuer(); + auto const mptIssue = amount.get(); + auto const mptID = keylet::mptIssuance(mptIssue.getMptID()); + auto sleIssuance = view.peek(mptID); + if (!sleIssuance) + { + JLOG(j.error()) << "rippleUnlockEscrowMPT: MPT issuance not found for " + << mptIssue.getMptID(); + return tecOBJECT_NOT_FOUND; // LCOV_EXCL_LINE + } + + // Decrease the Issuance EscrowedAmount + { + if (!sleIssuance->isFieldPresent(sfLockedAmount)) + { + JLOG(j.error()) + << "rippleUnlockEscrowMPT: no locked amount in issuance for " + << mptIssue.getMptID(); + return tecINTERNAL; // LCOV_EXCL_LINE + } + + auto const locked = sleIssuance->getFieldU64(sfLockedAmount); + auto const redeem = amount.mpt().value(); + + // Underflow check for subtraction + if (!canSubtract( + STAmount(mptIssue, locked), STAmount(mptIssue, redeem))) + { + JLOG(j.error()) + << "rippleUnlockEscrowMPT: insufficient locked amount for " + << mptIssue.getMptID() << ": " << locked << " < " << redeem; + return tecINTERNAL; // LCOV_EXCL_LINE + } + + auto const newLocked = locked - redeem; + if (newLocked == 0) + sleIssuance->makeFieldAbsent(sfLockedAmount); + else + sleIssuance->setFieldU64(sfLockedAmount, newLocked); + view.update(sleIssuance); + } + + if (issuer != receiver) + { + // Increase the MPT Holder MPTAmount + auto const mptokenID = keylet::mptoken(mptID.key, receiver); + auto sle = view.peek(mptokenID); + if (!sle) + { + JLOG(j.error()) + << "rippleUnlockEscrowMPT: MPToken not found for " << receiver; + return tecOBJECT_NOT_FOUND; // LCOV_EXCL_LINE + } + + auto current = sle->getFieldU64(sfMPTAmount); + auto delta = amount.mpt().value(); + + // Overflow check for addition + if (!canAdd(STAmount(mptIssue, current), STAmount(mptIssue, delta))) + { + JLOG(j.error()) + << "rippleUnlockEscrowMPT: overflow on MPTAmount for " + << to_string(receiver) << ": " << current << " + " << delta; + return tecINTERNAL; // LCOV_EXCL_LINE + } + + (*sle)[sfMPTAmount] += delta; + view.update(sle); + } + else + { + // Decrease the Issuance OutstandingAmount + auto const outstanding = sleIssuance->getFieldU64(sfOutstandingAmount); + auto const redeem = amount.mpt().value(); + + // Underflow check for subtraction + if (!canSubtract( + STAmount(mptIssue, outstanding), STAmount(mptIssue, redeem))) + { + JLOG(j.error()) + << "rippleUnlockEscrowMPT: insufficient outstanding amount for " + << mptIssue.getMptID() << ": " << outstanding << " < " + << redeem; + return tecINTERNAL; // LCOV_EXCL_LINE + } + + sleIssuance->setFieldU64(sfOutstandingAmount, outstanding - redeem); + view.update(sleIssuance); + } + + if (issuer == sender) + { + JLOG(j.error()) << "rippleUnlockEscrowMPT: sender is the issuer, " + "cannot unlock MPTs."; + return tecINTERNAL; // LCOV_EXCL_LINE + } + else + { + // Decrease the MPT Holder EscrowedAmount + auto const mptokenID = keylet::mptoken(mptID.key, sender); + auto sle = view.peek(mptokenID); + if (!sle) + { + JLOG(j.error()) + << "rippleUnlockEscrowMPT: MPToken not found for " << sender; + return tecOBJECT_NOT_FOUND; // LCOV_EXCL_LINE + } + + if (!sle->isFieldPresent(sfLockedAmount)) + { + JLOG(j.error()) + << "rippleUnlockEscrowMPT: no locked amount in MPToken for " + << to_string(sender); + return tecINTERNAL; // LCOV_EXCL_LINE + } + + auto const locked = sle->getFieldU64(sfLockedAmount); + auto const delta = amount.mpt().value(); + + // Underflow check for subtraction + if (!canSubtract(STAmount(mptIssue, locked), STAmount(mptIssue, delta))) + { + JLOG(j.error()) + << "rippleUnlockEscrowMPT: insufficient locked amount for " + << to_string(sender) << ": " << locked << " < " << delta; + return tecINTERNAL; // LCOV_EXCL_LINE + } + + auto const newLocked = locked - delta; + if (newLocked == 0) + sle->makeFieldAbsent(sfLockedAmount); + else + sle->setFieldU64(sfLockedAmount, newLocked); + view.update(sle); + } + return tesSUCCESS; +} + bool after(NetClock::time_point now, std::uint32_t mark) { diff --git a/src/xrpld/rpc/handlers/GatewayBalances.cpp b/src/xrpld/rpc/handlers/GatewayBalances.cpp index e8b95bd75c..ca9e370c81 100644 --- a/src/xrpld/rpc/handlers/GatewayBalances.cpp +++ b/src/xrpld/rpc/handlers/GatewayBalances.cpp @@ -142,11 +142,41 @@ doGatewayBalances(RPC::JsonContext& context) std::map> hotBalances; std::map> assets; std::map> frozenBalances; + std::map locked; // Traverse the cold wallet's trust lines { forEachItem( *ledger, accountID, [&](std::shared_ptr const& sle) { + if (sle->getType() == ltESCROW) + { + auto const& escrow = sle->getFieldAmount(sfAmount); + auto& bal = locked[escrow.getCurrency()]; + if (bal == beast::zero) + { + // This is needed to set the currency code correctly + bal = escrow; + } + else + { + try + { + bal += escrow; + } + catch (std::runtime_error const&) + { + // Presumably the exception was caused by overflow. + // On overflow return the largest valid STAmount. + // Very large sums of STAmount are approximations + // anyway. + bal = STAmount( + bal.issue(), + STAmount::cMaxValue, + STAmount::cMaxOffset); + } + } + } + auto rs = PathFindTrustLine::makeItem(accountID, sle); if (!rs) @@ -246,6 +276,17 @@ doGatewayBalances(RPC::JsonContext& context) populateResult(frozenBalances, jss::frozen_balances); populateResult(assets, jss::assets); + // Add total escrow to the result + if (!locked.empty()) + { + Json::Value j; + for (auto const& [k, v] : locked) + { + j[to_string(k)] = v.getText(); + } + result[jss::locked] = std::move(j); + } + return result; }