diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index 25d9258a5..04ee720b8 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -914,6 +914,7 @@ if (tests) src/test/jtx/impl/seq.cpp src/test/jtx/impl/sig.cpp src/test/jtx/impl/tag.cpp + src/test/jtx/impl/TestHelpers.cpp src/test/jtx/impl/ticket.cpp src/test/jtx/impl/token.cpp src/test/jtx/impl/trust.cpp diff --git a/src/ripple/app/paths/TrustLine.h b/src/ripple/app/paths/TrustLine.h index 6d7fcd66f..dbff1a29f 100644 --- a/src/ripple/app/paths/TrustLine.h +++ b/src/ripple/app/paths/TrustLine.h @@ -139,6 +139,13 @@ public: return mFlags & (mViewLowest ? lsfLowFreeze : lsfHighFreeze); } + /** Have we set the deep freeze flag on our peer */ + bool + getDeepFreeze() const + { + return mFlags & (mViewLowest ? lsfLowDeepFreeze : lsfHighDeepFreeze); + } + /** Has the peer set the freeze flag on us */ bool getFreezePeer() const @@ -146,6 +153,13 @@ public: return mFlags & (!mViewLowest ? lsfLowFreeze : lsfHighFreeze); } + /** Has the peer set the deep freeze flag on us */ + bool + getDeepFreezePeer() const + { + return mFlags & (!mViewLowest ? lsfLowDeepFreeze : lsfHighDeepFreeze); + } + STAmount const& getBalance() const { diff --git a/src/ripple/app/paths/impl/StepChecks.h b/src/ripple/app/paths/impl/StepChecks.h index 9d8664a8d..f61f0e166 100644 --- a/src/ripple/app/paths/impl/StepChecks.h +++ b/src/ripple/app/paths/impl/StepChecks.h @@ -52,6 +52,12 @@ checkFreeze( { return terNO_LINE; } + // Unlike normal freeze, a deep frozen trust line acts the same + // regardless of which side froze it + if (sle->isFlag(lsfHighDeepFreeze) || sle->isFlag(lsfLowDeepFreeze)) + { + return terNO_LINE; + } } return tesSUCCESS; diff --git a/src/ripple/app/tx/impl/CashCheck.cpp b/src/ripple/app/tx/impl/CashCheck.cpp index 2ffb13ba6..4b26f8a3e 100644 --- a/src/ripple/app/tx/impl/CashCheck.cpp +++ b/src/ripple/app/tx/impl/CashCheck.cpp @@ -392,6 +392,7 @@ CashCheck::doApply() false, // authorize account (sleDst->getFlags() & lsfDefaultRipple) == 0, false, // freeze trust line + false, // deep freeze trust line initialBalance, // zero initial balance Issue(currency, account_), // limit of zero 0, // quality in diff --git a/src/ripple/app/tx/impl/CreateOffer.cpp b/src/ripple/app/tx/impl/CreateOffer.cpp index 05136fcfa..17a695743 100644 --- a/src/ripple/app/tx/impl/CreateOffer.cpp +++ b/src/ripple/app/tx/impl/CreateOffer.cpp @@ -275,6 +275,32 @@ CreateOffer::checkAcceptAsset( } } + // 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; } diff --git a/src/ripple/app/tx/impl/Escrow.cpp b/src/ripple/app/tx/impl/Escrow.cpp index 552a0ebaa..ebfea0b4d 100644 --- a/src/ripple/app/tx/impl/Escrow.cpp +++ b/src/ripple/app/tx/impl/Escrow.cpp @@ -240,7 +240,8 @@ EscrowCreate::doApply() ctx_.view(), {account, ctx_.tx[sfDestination]}, amount.issue(), - ctx_.journal); + ctx_.journal, + lhLOCKING); JLOG(ctx_.journal.trace()) << "EscrowCreate::doApply trustTransferAllowed result=" << result; diff --git a/src/ripple/app/tx/impl/InvariantCheck.cpp b/src/ripple/app/tx/impl/InvariantCheck.cpp index e0843eb42..2a8a92a8b 100644 --- a/src/ripple/app/tx/impl/InvariantCheck.cpp +++ b/src/ripple/app/tx/impl/InvariantCheck.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -560,6 +561,303 @@ NoXRPTrustLines::finalize( //------------------------------------------------------------------------------ +void +NoDeepFreezeTrustLinesWithoutFreeze::visitEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const& after) +{ + if (after && after->getType() == ltRIPPLE_STATE) + { + std::uint32_t const uFlags = after->getFieldU32(sfFlags); + bool const lowFreeze = uFlags & lsfLowFreeze; + bool const lowDeepFreeze = uFlags & lsfLowDeepFreeze; + + bool const highFreeze = uFlags & lsfHighFreeze; + bool const highDeepFreeze = uFlags & lsfHighDeepFreeze; + + deepFreezeWithoutFreeze_ = + (lowDeepFreeze && !lowFreeze) || (highDeepFreeze && !highFreeze); + } +} + +bool +NoDeepFreezeTrustLinesWithoutFreeze::finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const& j) +{ + if (!deepFreezeWithoutFreeze_) + return true; + + JLOG(j.fatal()) << "Invariant failed: a trust line with deep freeze flag " + "without normal freeze was created"; + return false; +} + +//------------------------------------------------------------------------------ + +void +TransfersNotFrozen::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + /* + * A trust line freeze state alone doesn't determine if a transfer is + * frozen. The transfer must be examined "end-to-end" because both sides of + * the transfer may have different freeze states and freeze impact depends + * on the transfer direction. This is why first we need to track the + * transfers using IssuerChanges senders/receivers. + * + * Only in validateIssuerChanges, after we collected all changes can we + * determine if the transfer is valid. + */ + if (!isValidEntry(before, after)) + { + return; + } + + auto const balanceChange = calculateBalanceChange(before, after, isDelete); + if (balanceChange.signum() == 0) + { + return; + } + + recordBalanceChanges(after, balanceChange); +} + +bool +TransfersNotFrozen::finalize( + STTx const& tx, + TER const ter, + XRPAmount const fee, + ReadView const& view, + beast::Journal const& j) +{ + /* + * We check this invariant regardless of deep freeze amendment status, + * allowing for detection and logging of potential issues even when the + * amendment is disabled. + * + * If an exploit that allows moving frozen assets is discovered, + * we can alert operators who monitor fatal messages and trigger assert in + * debug builds for an early warning. + * + * In an unlikely event that an exploit is found, this early detection + * enables encouraging the UNL to expedite deep freeze amendment activation + * or deploy hotfixes via new amendments. In case of a new amendment, we'd + * only have to change this line setting 'enforce' variable. + * enforce = view.rules().enabled(featureDeepFreeze) || + * view.rules().enabled(fixFreezeExploit); + */ + [[maybe_unused]] bool const enforce = + view.rules().enabled(featureDeepFreeze); + + for (auto const& [issue, changes] : balanceChanges_) + { + auto const issuerSle = findIssuer(issue.account, view); + // It should be impossible for the issuer to not be found, but check + // just in case so rippled doesn't crash in release. + if (!issuerSle) + { + assert(enforce); + if (enforce) + { + return false; + } + continue; + } + + if (!validateIssuerChanges(issuerSle, changes, tx, j, enforce)) + { + return false; + } + } + + return true; +} + +bool +TransfersNotFrozen::isValidEntry( + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + // `after` can never be null, even if the trust line is deleted. + assert(after); + if (!after) + { + return false; + } + + if (after->getType() == ltACCOUNT_ROOT) + { + possibleIssuers_.emplace(after->getAccountID(sfAccount), after); + return false; + } + + /* While LedgerEntryTypesMatch invariant also checks types, all invariants + * are processed regardless of previous failures. + * + * This type check is still necessary here because it prevents potential + * issues in subsequent processing. + */ + return after->getType() == ltRIPPLE_STATE && + (!before || before->getType() == ltRIPPLE_STATE); +} + +STAmount +TransfersNotFrozen::calculateBalanceChange( + std::shared_ptr const& before, + std::shared_ptr const& after, + bool isDelete) +{ + auto const getBalance = [](auto const& line, auto const& other, bool zero) { + STAmount amt = line ? line->getFieldAmount(sfBalance) + : other->getFieldAmount(sfBalance).zeroed(); + return zero ? amt.zeroed() : amt; + }; + + /* Trust lines can be created dynamically by other transactions such as + * Payment and OfferCreate that cross offers. Such trust line won't be + * created frozen, but the sender might be, so the starting balance must be + * treated as zero. + */ + auto const balanceBefore = getBalance(before, after, false); + + /* Same as above, trust lines can be dynamically deleted, and for frozen + * trust lines, payments not involving the issuer must be blocked. This is + * achieved by treating the final balance as zero when isDelete=true to + * ensure frozen line restrictions are enforced even during deletion. + */ + auto const balanceAfter = getBalance(after, before, isDelete); + + return balanceAfter - balanceBefore; +} + +void +TransfersNotFrozen::recordBalance(Issue const& issue, BalanceChange change) +{ + assert(change.balanceChangeSign); + auto& changes = balanceChanges_[issue]; + if (change.balanceChangeSign < 0) + changes.senders.emplace_back(std::move(change)); + else + changes.receivers.emplace_back(std::move(change)); +} + +void +TransfersNotFrozen::recordBalanceChanges( + std::shared_ptr const& after, + STAmount const& balanceChange) +{ + auto const balanceChangeSign = balanceChange.signum(); + auto const currency = after->getFieldAmount(sfBalance).getCurrency(); + + // Change from low account's perspective, which is trust line default + recordBalance( + {currency, after->getFieldAmount(sfHighLimit).getIssuer()}, + {after, balanceChangeSign}); + + // Change from high account's perspective, which reverses the sign. + recordBalance( + {currency, after->getFieldAmount(sfLowLimit).getIssuer()}, + {after, -balanceChangeSign}); +} + +std::shared_ptr +TransfersNotFrozen::findIssuer(AccountID const& issuerID, ReadView const& view) +{ + if (auto it = possibleIssuers_.find(issuerID); it != possibleIssuers_.end()) + { + return it->second; + } + + return view.read(keylet::account(issuerID)); +} + +bool +TransfersNotFrozen::validateIssuerChanges( + std::shared_ptr const& issuer, + IssuerChanges const& changes, + STTx const& tx, + beast::Journal const& j, + bool enforce) +{ + if (!issuer) + { + return false; + } + + bool const globalFreeze = issuer->isFlag(lsfGlobalFreeze); + if (changes.receivers.empty() || changes.senders.empty()) + { + /* If there are no receivers, then the holder(s) are returning + * their tokens to the issuer. Likewise, if there are no + * senders, then the issuer is issuing tokens to the holder(s). + * This is allowed regardless of the issuer's freeze flags. (The + * holder may have contradicting freeze flags, but that will be + * checked when the holder is treated as issuer.) + */ + return true; + } + + for (auto const& actors : {changes.senders, changes.receivers}) + { + for (auto const& change : actors) + { + bool const high = + change.line->getFieldAmount(sfLowLimit).getIssuer() == + issuer->getAccountID(sfAccount); + + if (!validateFrozenState( + change, high, tx, j, enforce, globalFreeze)) + { + return false; + } + } + } + return true; +} + +bool +TransfersNotFrozen::validateFrozenState( + BalanceChange const& change, + bool high, + STTx const& tx, + beast::Journal const& j, + bool enforce, + bool globalFreeze) +{ + bool const freeze = change.balanceChangeSign < 0 && + change.line->isFlag(high ? lsfLowFreeze : lsfHighFreeze); + bool const deepFreeze = + change.line->isFlag(high ? lsfLowDeepFreeze : lsfHighDeepFreeze); + bool const frozen = globalFreeze || deepFreeze || freeze; + + // bool const isAMMLine = change.line->isFlag(lsfAMMNode); + + if (!frozen) + { + return true; + } + + JLOG(j.fatal()) << "Invariant failed: Attempting to move frozen funds for " + << tx.getTransactionID(); + assert(enforce); + + if (enforce) + { + return false; + } + + return true; +} + +//------------------------------------------------------------------------------ + void ValidNewAccountRoot::visitEntry( bool, diff --git a/src/ripple/app/tx/impl/InvariantCheck.h b/src/ripple/app/tx/impl/InvariantCheck.h index 47d33a2ed..92700181d 100644 --- a/src/ripple/app/tx/impl/InvariantCheck.h +++ b/src/ripple/app/tx/impl/InvariantCheck.h @@ -242,6 +242,114 @@ public: beast::Journal const&); }; +/** + * @brief Invariant: Trust lines with deep freeze flag are not allowed if normal + * freeze flag is not set. + * + * We iterate all the trust lines created by this transaction and ensure + * that they don't have deep freeze flag set without normal freeze flag set. + */ +class NoDeepFreezeTrustLinesWithoutFreeze +{ + bool deepFreezeWithoutFreeze_ = false; + +public: + void + visitEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const&); + + bool + finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const&); +}; + +/** + * @brief Invariant: frozen trust line balance change is not allowed. + * + * We iterate all affected trust lines and ensure that they don't have + * unexpected change of balance if they're frozen. + */ +class TransfersNotFrozen +{ + struct BalanceChange + { + std::shared_ptr const line; + int const balanceChangeSign; + }; + + struct IssuerChanges + { + std::vector senders; + std::vector receivers; + }; + + using ByIssuer = std::map; + ByIssuer balanceChanges_; + + std::map const> possibleIssuers_; + +public: + 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 + isValidEntry( + std::shared_ptr const& before, + std::shared_ptr const& after); + + STAmount + calculateBalanceChange( + std::shared_ptr const& before, + std::shared_ptr const& after, + bool isDelete); + + void + recordBalance(Issue const& issue, BalanceChange change); + + void + recordBalanceChanges( + std::shared_ptr const& after, + STAmount const& balanceChange); + + std::shared_ptr + findIssuer(AccountID const& issuerID, ReadView const& view); + + bool + validateIssuerChanges( + std::shared_ptr const& issuer, + IssuerChanges const& changes, + STTx const& tx, + beast::Journal const& j, + bool enforce); + + bool + validateFrozenState( + BalanceChange const& change, + bool high, + STTx const& tx, + beast::Journal const& j, + bool enforce, + bool globalFreeze); +}; + /** * @brief Invariant: offers should be for non-negative amounts and must not * be XRP to XRP. @@ -399,6 +507,8 @@ using InvariantChecks = std::tuple< XRPBalanceChecks, XRPNotCreated, NoXRPTrustLines, + NoDeepFreezeTrustLinesWithoutFreeze, + TransfersNotFrozen, NoBadOffers, NoZeroEscrow, ValidNewAccountRoot, diff --git a/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp b/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp index 9e6376762..7d8b2e8bc 100644 --- a/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp +++ b/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp @@ -268,6 +268,16 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) ctx.j) < needed) return tecINSUFFICIENT_FUNDS; } + + // 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.issue()); + if (result != tesSUCCESS) + return result; + } } return tesSUCCESS; @@ -453,4 +463,60 @@ 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; + } + + assert(!isXRP(issue.currency)); + 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/ripple/app/tx/impl/NFTokenAcceptOffer.h b/src/ripple/app/tx/impl/NFTokenAcceptOffer.h index 2d1b14ba2..80a624a11 100644 --- a/src/ripple/app/tx/impl/NFTokenAcceptOffer.h +++ b/src/ripple/app/tx/impl/NFTokenAcceptOffer.h @@ -38,6 +38,14 @@ private: std::shared_ptr const& buy, std::shared_ptr const& sell); + 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/ripple/app/tx/impl/OfferStream.cpp b/src/ripple/app/tx/impl/OfferStream.cpp index 58fd209ca..78cf623f4 100644 --- a/src/ripple/app/tx/impl/OfferStream.cpp +++ b/src/ripple/app/tx/impl/OfferStream.cpp @@ -256,6 +256,20 @@ TOfferStreamBase::step() continue; } + bool const deepFrozen = isDeepFrozen( + view_, + offer_.owner(), + offer_.issueIn().currency, + offer_.issueIn().account); + if (deepFrozen) + { + JLOG(j_.trace()) + << "Removing deep frozen unfunded offer " << entry->key(); + permRmOffer(entry->key()); + offer_ = TOffer{}; + continue; + } + // Calculate owner funds ownerFunds_ = accountFundsHelper( view_, diff --git a/src/ripple/app/tx/impl/PayChan.cpp b/src/ripple/app/tx/impl/PayChan.cpp index 2b15bc232..102eafed3 100644 --- a/src/ripple/app/tx/impl/PayChan.cpp +++ b/src/ripple/app/tx/impl/PayChan.cpp @@ -273,7 +273,7 @@ PayChanCreate::preclaim(PreclaimContext const& ctx) // between these accounts for this asset { TER const result = trustTransferAllowed( - ctx.view, {account, dst}, amount.issue(), ctx.j); + ctx.view, {account, dst}, amount.issue(), ctx.j, lhLOCKING); JLOG(ctx.j.trace()) << "PayChanCreate::preclaim trustTransferAllowed result=" << result; diff --git a/src/ripple/app/tx/impl/SetTrust.cpp b/src/ripple/app/tx/impl/SetTrust.cpp index 2655c18f4..ab1ec6402 100644 --- a/src/ripple/app/tx/impl/SetTrust.cpp +++ b/src/ripple/app/tx/impl/SetTrust.cpp @@ -25,6 +25,42 @@ #include #include +namespace { + +uint32_t +computeFreezeFlags( + uint32_t uFlags, + bool bHigh, + bool bNoFreeze, + bool bSetFreeze, + bool bClearFreeze, + bool bSetDeepFreeze, + bool bClearDeepFreeze) +{ + if (bSetFreeze && !bClearFreeze && !bNoFreeze) + { + uFlags |= (bHigh ? ripple::lsfHighFreeze : ripple::lsfLowFreeze); + } + else if (bClearFreeze && !bSetFreeze) + { + uFlags &= ~(bHigh ? ripple::lsfHighFreeze : ripple::lsfLowFreeze); + } + if (bSetDeepFreeze && !bClearDeepFreeze && !bNoFreeze) + { + uFlags |= + (bHigh ? ripple::lsfHighDeepFreeze : ripple::lsfLowDeepFreeze); + } + else if (bClearDeepFreeze && !bSetDeepFreeze) + { + uFlags &= + ~(bHigh ? ripple::lsfHighDeepFreeze : ripple::lsfLowDeepFreeze); + } + + return uFlags; +} + +} // namespace + namespace ripple { NotTEC @@ -44,6 +80,16 @@ SetTrust::preflight(PreflightContext const& ctx) return temINVALID_FLAG; } + if (!ctx.rules.enabled(featureDeepFreeze)) + { + // Even though the deep freeze flags are included in the + // `tfTrustSetMask`, they are not valid if the amendment is not enabled. + if (uTxFlags & (tfSetDeepFreeze | tfClearDeepFreeze)) + { + return temINVALID_FLAG; + } + } + STAmount const saLimitAmount(tx.getFieldAmount(sfLimitAmount)); if (!isLegalNet(saLimitAmount)) @@ -142,6 +188,58 @@ SetTrust::preclaim(PreclaimContext const& ctx) return tecNO_PERMISSION; } + // Checking all freeze/deep freeze flag invariants. + if (ctx.view.rules().enabled(featureDeepFreeze)) + { + bool const bNoFreeze = sle->isFlag(lsfNoFreeze); + bool const bSetFreeze = (uTxFlags & tfSetFreeze); + bool const bSetDeepFreeze = (uTxFlags & tfSetDeepFreeze); + + if (bNoFreeze && (bSetFreeze || bSetDeepFreeze)) + { + // Cannot freeze the trust line if NoFreeze is set + return tecNO_PERMISSION; + } + + bool const bClearFreeze = (uTxFlags & tfClearFreeze); + bool const bClearDeepFreeze = (uTxFlags & tfClearDeepFreeze); + if ((bSetFreeze || bSetDeepFreeze) && + (bClearFreeze || bClearDeepFreeze)) + { + // Freezing and unfreezing in the same transaction should be + // illegal + return tecNO_PERMISSION; + } + + bool const bHigh = id > uDstAccountID; + // Fetching current state of trust line + auto const sleRippleState = + ctx.view.read(keylet::line(id, uDstAccountID, currency)); + std::uint32_t uFlags = + sleRippleState ? sleRippleState->getFieldU32(sfFlags) : 0u; + // Computing expected trust line state + uFlags = computeFreezeFlags( + uFlags, + bHigh, + bNoFreeze, + bSetFreeze, + bClearFreeze, + bSetDeepFreeze, + bClearDeepFreeze); + + auto const frozen = uFlags & (bHigh ? lsfHighFreeze : lsfLowFreeze); + auto const deepFrozen = + uFlags & (bHigh ? lsfHighDeepFreeze : lsfLowDeepFreeze); + + // Trying to set deep freeze on not already frozen trust line must + // fail. This also checks that clearing normal freeze while deep + // frozen must not work + if (deepFrozen && !frozen) + { + return tecNO_PERMISSION; + } + } + return tesSUCCESS; } @@ -157,7 +255,7 @@ SetTrust::doApply() Currency const currency(saLimitAmount.getCurrency()); AccountID uDstAccountID(saLimitAmount.getIssuer()); - // true, iff current is high account. + // true, if current is high account. bool const bHigh = account_ > uDstAccountID; auto const sle = view().peek(keylet::account(account_)); @@ -202,13 +300,15 @@ SetTrust::doApply() bool const bClearNoRipple = (uTxFlags & tfClearNoRipple); bool const bSetFreeze = (uTxFlags & tfSetFreeze); bool const bClearFreeze = (uTxFlags & tfClearFreeze); + bool const bSetDeepFreeze = (uTxFlags & tfSetDeepFreeze); + bool const bClearDeepFreeze = (uTxFlags & tfClearDeepFreeze); auto viewJ = ctx_.app.journal("View"); - // Trust lines to self are impossible but because of the old bug there are - // two on 19-02-2022. This code was here to allow those trust lines to be - // deleted. The fixTrustLinesToSelf fix amendment will remove them when it - // enables so this code will no longer be needed. + // Trust lines to self are impossible but because of the old bug there + // are two on 19-02-2022. This code was here to allow those trust lines + // to be deleted. The fixTrustLinesToSelf fix amendment will remove them + // when it enables so this code will no longer be needed. if (!view().rules().enabled(fixTrustLinesToSelf) && account_ == uDstAccountID) { @@ -368,14 +468,16 @@ SetTrust::doApply() uFlagsOut &= ~(bHigh ? lsfHighNoRipple : lsfLowNoRipple); } - if (bSetFreeze && !bClearFreeze && !sle->isFlag(lsfNoFreeze)) - { - uFlagsOut |= (bHigh ? lsfHighFreeze : lsfLowFreeze); - } - else if (bClearFreeze && !bSetFreeze) - { - uFlagsOut &= ~(bHigh ? lsfHighFreeze : lsfLowFreeze); - } + // Have to use lsfNoFreeze to maintain pre-deep freeze behavior + bool const bNoFreeze = sle->isFlag(lsfNoFreeze); + uFlagsOut = computeFreezeFlags( + uFlagsOut, + bHigh, + bNoFreeze, + bSetFreeze, + bClearFreeze, + bSetDeepFreeze, + bClearDeepFreeze); if (QUALITY_ONE == uLowQualityOut) uLowQualityOut = 0; @@ -459,7 +561,7 @@ SetTrust::doApply() else if (bReserveIncrease && mPriorBalance < reserveCreate) { JLOG(j_.trace()) - << "Delay transaction: Insufficent reserve to add trust line."; + << "Delay transaction: Insufficient reserve to add trust line."; // Another transaction could provide XRP to the account and then // this transaction would succeed. @@ -475,10 +577,10 @@ SetTrust::doApply() // Line does not exist. else if ( !saLimitAmount && // Setting default limit. - (!bQualityIn || !uQualityIn) && // Not setting quality in or setting - // default quality in. - (!bQualityOut || !uQualityOut) && // Not setting quality out or setting - // default quality out. + (!bQualityIn || !uQualityIn) && // Not setting quality in or + // setting default quality in. + (!bQualityOut || !uQualityOut) && // Not setting quality out or + // setting default quality out. (!bSetAuth)) { JLOG(j_.trace()) @@ -515,6 +617,7 @@ SetTrust::doApply() bSetAuth, bSetNoRipple && !bClearNoRipple, bSetFreeze && !bClearFreeze, + bSetDeepFreeze, saBalance, saLimitAllow, // Limit for who is being charged. uQualityIn, diff --git a/src/ripple/app/tx/impl/URIToken.cpp b/src/ripple/app/tx/impl/URIToken.cpp index bcb57f9eb..6f0da5334 100644 --- a/src/ripple/app/tx/impl/URIToken.cpp +++ b/src/ripple/app/tx/impl/URIToken.cpp @@ -832,6 +832,7 @@ URIToken::doApply() false, // authorize account (sleOwner->getFlags() & lsfDefaultRipple) == 0, false, // freeze trust line + false, // deepfreeze trust line *dstAmt, // initial balance zero Issue( purchaseAmount.getCurrency(), diff --git a/src/ripple/ledger/View.h b/src/ripple/ledger/View.h index 86ccf93d8..a23d5ffc4 100644 --- a/src/ripple/ledger/View.h +++ b/src/ripple/ledger/View.h @@ -78,6 +78,9 @@ hasExpired(ReadView const& view, std::optional const& exp); /** Controls the treatment of frozen account balances */ enum FreezeHandling { fhIGNORE_FREEZE, fhZERO_IF_FROZEN }; +/** Controls the treatment of locked balances */ +enum LockHandling { lhLOCKING, lhUNLOCKING_RETURN, lhUNLOCKING_FORWARD }; + [[nodiscard]] bool isGlobalFrozen(ReadView const& view, AccountID const& issuer); @@ -88,6 +91,13 @@ isFrozen( Currency const& currency, AccountID const& issuer); +[[nodiscard]] bool +isDeepFrozen( + ReadView const& view, + AccountID const& account, + Currency const& currency, + AccountID const& issuer); + // Returns the amount an account can spend without going into debt. // // <-- saAmount: amount of currency held by account. May be negative. @@ -343,6 +353,7 @@ trustCreate( const bool bAuth, // --> authorize account. const bool bNoRipple, // --> others cannot ripple through const bool bFreeze, // --> funds cannot leave + bool bDeepFreeze, // --> can neither receive nor send funds STAmount const& saBalance, // --> balance of account being set. // Issuer should be noAccount() STAmount const& saLimit, // --> limit for account being set. @@ -521,8 +532,12 @@ trustAdjustLockedBalance( // check for freezes & auth { - TER const result = - trustTransferAllowed(view, parties, deltaAmt.issue(), j); + TER const result = trustTransferAllowed( + view, + parties, + deltaAmt.issue(), + j, + deltaLockCount == 1 ? lhLOCKING : lhUNLOCKING_RETURN); JLOG(j.trace()) << "trustAdjustLockedBalance: trustTransferAllowed result=" @@ -625,7 +640,8 @@ trustTransferAllowed( V& view, std::vector const& parties, Issue const& issue, - beast::Journal const& j) + beast::Journal const& j, + LockHandling lockHandling = lhUNLOCKING_FORWARD) { static_assert( std::is_same::value || @@ -661,6 +677,16 @@ trustTransferAllowed( if (p == issue.account) continue; + if (lockHandling != lhUNLOCKING_RETURN && + isDeepFrozen(view, p, issue.currency, issue.account)) + { + JLOG(j.trace()) << "trustTransferAllowed: " + // << "parties=[" << parties << "], " + << "issuer: " << issue.account << " " + << "has deep freeze on party: " << p; + return tecFROZEN; + } + auto const line = view.read(keylet::line(p, issue.account, issue.currency)); if (!line) @@ -971,6 +997,7 @@ trustTransferLockedBalance( false, // authorize account (sleDstAcc->getFlags() & lsfDefaultRipple) == 0, false, // freeze trust line + false, // deep freeze trust line dstAmt, // initial balance Issue(currency, dstAccID), // limit of zero 0, // quality in diff --git a/src/ripple/ledger/impl/View.cpp b/src/ripple/ledger/impl/View.cpp index 42173982c..975ec6ef9 100644 --- a/src/ripple/ledger/impl/View.cpp +++ b/src/ripple/ledger/impl/View.cpp @@ -219,6 +219,26 @@ isFrozen( return false; } +bool +isDeepFrozen( + ReadView const& view, + AccountID const& account, + Currency const& currency, + AccountID const& issuer) +{ + if (isXRP(currency)) + return false; + + if (issuer == account) + return false; + + auto const sle = view.read(keylet::line(account, issuer, currency)); + if (!sle) + return false; + + return sle->isFlag(lsfHighDeepFreeze) || sle->isFlag(lsfLowDeepFreeze); +} + STAmount accountHolds( ReadView const& view, @@ -236,17 +256,22 @@ accountHolds( // IOU: Return balance on trust line modulo freeze auto const sle = view.read(keylet::line(account, issuer, currency)); - if (!sle) - { - amount.clear({currency, issuer}); - } - else if ( - (zeroIfFrozen == fhZERO_IF_FROZEN) && - isFrozen(view, account, currency, issuer)) - { - amount.clear(Issue(currency, issuer)); - } - else + auto const allowBalance = [&]() { + if (!sle) + return false; + + if (zeroIfFrozen == fhZERO_IF_FROZEN) + { + if (isFrozen(view, account, currency, issuer) || + isDeepFrozen(view, account, currency, issuer)) + { + return false; + } + } + return true; + }(); + + if (allowBalance) { amount = sle->getFieldAmount(sfBalance); if (account > issuer) @@ -279,6 +304,10 @@ accountHolds( amount.setIssuer(issuer); } + else + { + amount.clear(Issue{currency, issuer}); + } JLOG(j.trace()) << "accountHolds:" << " account=" << to_string(account) << " amount=" << amount.getFullText(); @@ -769,6 +798,7 @@ trustCreate( const bool bAuth, // --> authorize account. const bool bNoRipple, // --> others cannot ripple through const bool bFreeze, // --> funds cannot leave + const bool bDeepFreeze, // --> can neither receive nor send funds STAmount const& saBalance, // --> balance of account being set. // Issuer should be noAccount() STAmount const& saLimit, // --> limit for account being set. @@ -850,7 +880,11 @@ trustCreate( } if (bFreeze) { - uFlags |= (!bSetHigh ? lsfLowFreeze : lsfHighFreeze); + uFlags |= (bSetHigh ? lsfHighFreeze : lsfLowFreeze); + } + if (bDeepFreeze) + { + uFlags |= (bSetHigh ? lsfHighDeepFreeze : lsfLowDeepFreeze); } if ((slePeer->getFlags() & lsfDefaultRipple) == 0) @@ -1140,6 +1174,7 @@ rippleCredit( false, noRipple, false, + false, saBalance, saReceiverLimit, 0, @@ -1464,6 +1499,7 @@ issueIOU( false, noRipple, false, + false, final_balance, limit, 0, diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index 800bbef4d..aac88b0cf 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -74,7 +74,7 @@ namespace detail { // Feature.cpp. Because it's only used to reserve storage, and determine how // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than // the actual number of amendments. A LogicError on startup will verify this. -static constexpr std::size_t numFeatures = 82; +static constexpr std::size_t numFeatures = 83; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -370,6 +370,7 @@ extern uint256 const fix20250131; extern uint256 const featureHookCanEmit; extern uint256 const fixRewardClaimFlags; extern uint256 const fixProvisionalDoubleThreading; +extern uint256 const featureDeepFreeze; } // namespace ripple diff --git a/src/ripple/protocol/LedgerFormats.h b/src/ripple/protocol/LedgerFormats.h index 56321df7a..b26c07d59 100644 --- a/src/ripple/protocol/LedgerFormats.h +++ b/src/ripple/protocol/LedgerFormats.h @@ -299,8 +299,10 @@ enum LedgerSpecificFlags { lsfHighAuth = 0x00080000, lsfLowNoRipple = 0x00100000, lsfHighNoRipple = 0x00200000, - lsfLowFreeze = 0x00400000, // True, low side has set freeze flag - lsfHighFreeze = 0x00800000, // True, high side has set freeze flag + lsfLowFreeze = 0x00400000, // True, low side has set freeze flag + lsfHighFreeze = 0x00800000, // True, high side has set freeze flag + lsfLowDeepFreeze = 0x02000000, // True, low side has set deep freeze flag + lsfHighDeepFreeze = 0x04000000, // True, high side has set deep freeze flag // ltSIGNER_LIST lsfOneOwnerCount = 0x00010000, // True, uses only one OwnerCount diff --git a/src/ripple/protocol/TxFlags.h b/src/ripple/protocol/TxFlags.h index 2f660ff80..09ff9f1bc 100644 --- a/src/ripple/protocol/TxFlags.h +++ b/src/ripple/protocol/TxFlags.h @@ -120,10 +120,12 @@ enum TrustSetFlags : uint32_t { tfClearNoRipple = 0x00040000, tfSetFreeze = 0x00100000, tfClearFreeze = 0x00200000, + tfSetDeepFreeze = 0x00400000, + tfClearDeepFreeze = 0x00800000 }; constexpr std::uint32_t tfTrustSetMask = ~(tfUniversal | tfSetfAuth | tfSetNoRipple | tfClearNoRipple | tfSetFreeze | - tfClearFreeze); + tfClearFreeze | tfSetDeepFreeze | tfClearDeepFreeze); // EnableAmendment flags: enum EnableAmendmentFlags : std::uint32_t { diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index 24ab474c1..8e4d9f449 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -476,6 +476,7 @@ REGISTER_FIX (fix20250131, Supported::yes, VoteBehavior::De REGISTER_FEATURE(HookCanEmit, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FIX (fixRewardClaimFlags, Supported::yes, VoteBehavior::DefaultYes); REGISTER_FIX (fixProvisionalDoubleThreading, Supported::yes, VoteBehavior::DefaultYes); +REGISTER_FEATURE(DeepFreeze, Supported::yes, VoteBehavior::DefaultNo); // The following amendments are obsolete, but must remain supported // because they could potentially get enabled. diff --git a/src/ripple/protocol/jss.h b/src/ripple/protocol/jss.h index 0faa053f6..802727583 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -344,6 +344,8 @@ JSS(force); // in: catalogue JSS(forward); // in: AccountTx JSS(freeze); // out: AccountLines JSS(freeze_peer); // out: AccountLines +JSS(deep_freeze); // out: AccountLines +JSS(deep_freeze_peer); // out: AccountLines JSS(frozen_balances); // out: GatewayBalances JSS(full); // in: LedgerClearer, handlers/Ledger JSS(full_reply); // out: PathFind diff --git a/src/ripple/rpc/handlers/AccountLines.cpp b/src/ripple/rpc/handlers/AccountLines.cpp index ace5b3898..c726e048b 100644 --- a/src/ripple/rpc/handlers/AccountLines.cpp +++ b/src/ripple/rpc/handlers/AccountLines.cpp @@ -74,6 +74,10 @@ addLine(Json::Value& jsonLines, RPCTrustLine const& line) jPeer[jss::freeze] = true; if (line.getFreezePeer()) jPeer[jss::freeze_peer] = true; + if (line.getDeepFreeze()) + jPeer[jss::deep_freeze] = true; + if (line.getDeepFreezePeer()) + jPeer[jss::deep_freeze_peer] = true; } // { diff --git a/src/test/app/Check_test.cpp b/src/test/app/Check_test.cpp index 8f0c0ec46..bdc453a41 100644 --- a/src/test/app/Check_test.cpp +++ b/src/test/app/Check_test.cpp @@ -108,16 +108,6 @@ class Check_test : public beast::unit_test::suite return result; } - // Helper function that returns the owner count on an account. - static std::uint32_t - ownerCount(test::jtx::Env const& env, test::jtx::Account const& account) - { - std::uint32_t ret{0}; - if (auto const sleAccount = env.le(account)) - ret = sleAccount->getFieldU32(sfOwnerCount); - return ret; - } - // Helper function that verifies the expected DeliveredAmount is present. // // NOTE: the function _infers_ the transaction to operate on by calling diff --git a/src/test/app/Escrow_test.cpp b/src/test/app/Escrow_test.cpp index d60f8cc6c..db8474fef 100644 --- a/src/test/app/Escrow_test.cpp +++ b/src/test/app/Escrow_test.cpp @@ -4072,7 +4072,89 @@ struct Escrow_test : public beast::unit_test::suite fee(1500)); 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(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(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(cb1), + escrow::fulfillment(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(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 testIOUTLINSF(FeatureBitset features) { diff --git a/src/test/app/Flow_test.cpp b/src/test/app/Flow_test.cpp index 3a0138884..131cad6f0 100644 --- a/src/test/app/Flow_test.cpp +++ b/src/test/app/Flow_test.cpp @@ -49,14 +49,6 @@ getNoRippleFlag( return false; // silence warning } -jtx::PrettyAmount -xrpMinusFee(jtx::Env const& env, std::int64_t xrpAmount) -{ - using namespace jtx; - auto feeDrops = env.current()->fees().base; - return drops(dropsPerXRP * xrpAmount - feeDrops); -}; - struct Flow_test : public beast::unit_test::suite { void diff --git a/src/test/app/Freeze_test.cpp b/src/test/app/Freeze_test.cpp index 6402f84c5..970869fde 100644 --- a/src/test/app/Freeze_test.cpp +++ b/src/test/app/Freeze_test.cpp @@ -27,32 +27,6 @@ namespace ripple { class Freeze_test : public beast::unit_test::suite { - static Json::Value - getAccountLines(test::jtx::Env& env, test::jtx::Account const& account) - { - Json::Value jq; - jq[jss::account] = account.human(); - return env.rpc("json", "account_lines", to_string(jq))[jss::result]; - } - - static Json::Value - getAccountOffers( - test::jtx::Env& env, - test::jtx::Account const& account, - bool current = false) - { - Json::Value jq; - jq[jss::account] = account.human(); - jq[jss::ledger_index] = current ? "current" : "validated"; - return env.rpc("json", "account_offers", to_string(jq))[jss::result]; - } - - static bool - checkArraySize(Json::Value const& val, unsigned int size) - { - return val.isArray() && val.size() == size; - } - void testRippleState(FeatureBitset features) { @@ -213,6 +187,203 @@ class Freeze_test : public beast::unit_test::suite } } + void + testDeepFreeze(FeatureBitset features) + { + testcase("Deep Freeze"); + + using namespace test::jtx; + Env env(*this, features); + + bool const withTouch = env.current()->rules().enabled(featureTouch); + + Account G1{"G1"}; + Account A1{"A1"}; + + env.fund(XRP(10000), G1, A1); + env.close(); + + env.trust(G1["USD"](1000), A1); + env.close(); + + if (features[featureDeepFreeze]) + { + // test: Issuer deep freezing the trust line in a single + // transaction + env(trust(G1, A1["USD"](0), tfSetFreeze | tfSetDeepFreeze)); + { + auto const flags = getTrustlineFlags( + env, withTouch ? 3u : 2u, withTouch ? 2u : 1u); + BEAST_EXPECT(flags & lsfLowFreeze); + BEAST_EXPECT(flags & lsfLowDeepFreeze); + BEAST_EXPECT(!(flags & (lsfHighFreeze | lsfHighDeepFreeze))); + env.close(); + } + + // test: Issuer clearing deep freeze and normal freeze in a single + // transaction + env(trust(G1, A1["USD"](0), tfClearFreeze | tfClearDeepFreeze)); + { + auto const flags = getTrustlineFlags( + env, withTouch ? 3u : 2u, withTouch ? 2u : 1u); + BEAST_EXPECT(!(flags & (lsfLowFreeze | lsfLowDeepFreeze))); + BEAST_EXPECT(!(flags & (lsfHighFreeze | lsfHighDeepFreeze))); + env.close(); + } + + // test: Issuer deep freezing not already frozen line must fail + env(trust(G1, A1["USD"](0), tfSetDeepFreeze), + ter(tecNO_PERMISSION)); + + env(trust(G1, A1["USD"](0), tfSetFreeze)); + env.close(); + + // test: Issuer deep freezing already frozen trust line + env(trust(G1, A1["USD"](0), tfSetDeepFreeze)); + { + auto const flags = getTrustlineFlags( + env, withTouch ? 3u : 2u, withTouch ? 2u : 1u); + BEAST_EXPECT(flags & lsfLowFreeze); + BEAST_EXPECT(flags & lsfLowDeepFreeze); + BEAST_EXPECT(!(flags & (lsfHighFreeze | lsfHighDeepFreeze))); + env.close(); + } + + // test: Holder clearing freeze flags has no effect. Each sides' + // flags are independent + env(trust(A1, G1["USD"](0), tfClearFreeze | tfClearDeepFreeze)); + { + auto const flags = getTrustlineFlags( + env, withTouch ? 3u : 2u, withTouch ? 2u : 1u); + BEAST_EXPECT(flags & lsfLowFreeze); + BEAST_EXPECT(flags & lsfLowDeepFreeze); + BEAST_EXPECT(!(flags & (lsfHighFreeze | lsfHighDeepFreeze))); + env.close(); + } + + // test: Issuer can't clear normal freeze when line is deep frozen + env(trust(G1, A1["USD"](0), tfClearFreeze), ter(tecNO_PERMISSION)); + + // test: Issuer clearing deep freeze but normal freeze is still in + // effect + env(trust(G1, A1["USD"](0), tfClearDeepFreeze)); + { + auto const flags = getTrustlineFlags( + env, withTouch ? 3u : 2u, withTouch ? 2u : 1u); + BEAST_EXPECT(flags & lsfLowFreeze); + BEAST_EXPECT(!(flags & lsfLowDeepFreeze)); + BEAST_EXPECT(!(flags & (lsfHighFreeze | lsfHighDeepFreeze))); + env.close(); + } + } + else + { + // test: applying deep freeze before amendment fails + env(trust(G1, A1["USD"](0), tfSetDeepFreeze), ter(temINVALID_FLAG)); + + // test: clearing deep freeze before amendment fails + env(trust(G1, A1["USD"](0), tfClearDeepFreeze), + ter(temINVALID_FLAG)); + } + } + + void + testCreateFrozenTrustline(FeatureBitset features) + { + testcase("Create Frozen Trustline"); + + using namespace test::jtx; + Env env(*this, features); + + Account G1{"G1"}; + Account A1{"A1"}; + + env.fund(XRP(10000), G1, A1); + env.close(); + + // test: can create frozen trustline + { + env(trust(G1, A1["USD"](1000), tfSetFreeze)); + auto const flags = getTrustlineFlags(env, 5u, 3u, false); + BEAST_EXPECT(flags & lsfLowFreeze); + env.close(); + env.require(lines(A1, 1)); + } + + // Cleanup + env(trust(G1, A1["USD"](0), tfClearFreeze)); + env.close(); + env.require(lines(G1, 0)); + env.require(lines(A1, 0)); + + // test: cannot create deep frozen trustline without normal freeze + if (features[featureDeepFreeze]) + { + env(trust(G1, A1["USD"](1000), tfSetDeepFreeze), + ter(tecNO_PERMISSION)); + env.close(); + env.require(lines(A1, 0)); + } + + // test: can create deep frozen trustline together with normal freeze + if (features[featureDeepFreeze]) + { + env(trust(G1, A1["USD"](1000), tfSetFreeze | tfSetDeepFreeze)); + auto const flags = getTrustlineFlags(env, 5u, 3u, false); + BEAST_EXPECT(flags & lsfLowFreeze); + BEAST_EXPECT(flags & lsfLowDeepFreeze); + env.close(); + env.require(lines(A1, 1)); + } + } + + void + testSetAndClear(FeatureBitset features) + { + testcase("Freeze Set and Clear"); + + using namespace test::jtx; + Env env(*this, features); + + bool const withTouch = env.current()->rules().enabled(featureTouch); + + Account G1{"G1"}; + Account A1{"A1"}; + + env.fund(XRP(10000), G1, A1); + env.close(); + + env.trust(G1["USD"](1000), A1); + env.close(); + + if (features[featureDeepFreeze]) + { + // test: can't have both set and clear flag families in the same + // transaction + env(trust(G1, A1["USD"](0), tfSetFreeze | tfClearFreeze), + ter(tecNO_PERMISSION)); + env(trust(G1, A1["USD"](0), tfSetFreeze | tfClearDeepFreeze), + ter(tecNO_PERMISSION)); + env(trust(G1, A1["USD"](0), tfSetDeepFreeze | tfClearFreeze), + ter(tecNO_PERMISSION)); + env(trust(G1, A1["USD"](0), tfSetDeepFreeze | tfClearDeepFreeze), + ter(tecNO_PERMISSION)); + } + else + { + // test: old behavior, transaction succeed with no effect on a + // trust line + env(trust(G1, A1["USD"](0), tfSetFreeze | tfClearFreeze)); + { + auto affected = env.meta()->getJson( + JsonOptions::none)[sfAffectedNodes.fieldName]; + BEAST_EXPECT(checkArraySize( + affected, + withTouch ? 2u : 1u)); // means no trustline changes + } + } + } + void testGlobalFreeze(FeatureBitset features) { @@ -382,15 +553,44 @@ class Freeze_test : public beast::unit_test::suite Account G1{"G1"}; Account A1{"A1"}; + Account frozenAcc{"A2"}; + Account deepFrozenAcc{"A3"}; env.fund(XRP(12000), G1); env.fund(XRP(1000), A1); + env.fund(XRP(1000), frozenAcc); + env.fund(XRP(1000), deepFrozenAcc); env.close(); env.trust(G1["USD"](1000), A1); + env.trust(G1["USD"](1000), frozenAcc); + env.trust(G1["USD"](1000), deepFrozenAcc); env.close(); env(pay(G1, A1, G1["USD"](1000))); + env(pay(G1, frozenAcc, G1["USD"](1000))); + env(pay(G1, deepFrozenAcc, G1["USD"](1000))); + + // Freezing and deep freezing some of the trust lines to check deep + // freeze and clearing of freeze separately + env(trust(G1, frozenAcc["USD"](0), tfSetFreeze)); + { + auto const flags = getTrustlineFlags(env, withTouch ? 3u : 2u, 1u); + BEAST_EXPECT(flags & lsfLowFreeze); + BEAST_EXPECT(!(flags & lsfHighFreeze)); + } + if (features[featureDeepFreeze]) + { + env(trust( + G1, deepFrozenAcc["USD"](0), tfSetFreeze | tfSetDeepFreeze)); + { + auto const flags = + getTrustlineFlags(env, withTouch ? 3u : 2u, 1u); + BEAST_EXPECT(!(flags & (lsfLowFreeze | lsfLowDeepFreeze))); + BEAST_EXPECT(flags & lsfHighFreeze); + BEAST_EXPECT(flags & lsfHighDeepFreeze); + } + } env.close(); // TrustSet NoFreeze @@ -415,16 +615,49 @@ class Freeze_test : public beast::unit_test::suite env.require(flags(G1, asfNoFreeze)); env.require(flags(G1, asfGlobalFreeze)); - // test: trustlines can't be frozen - env(trust(G1, A1["USD"](0), tfSetFreeze)); - auto affected = - env.meta()->getJson(JsonOptions::none)[sfAffectedNodes.fieldName]; - if (!BEAST_EXPECT(checkArraySize(affected, withTouch ? 2u : 1u))) - return; + // test: trustlines can't be frozen when no freeze enacted + if (features[featureDeepFreeze]) + { + env(trust(G1, A1["USD"](0), tfSetFreeze), ter(tecNO_PERMISSION)); - auto let = - affected[0u][sfModifiedNode.fieldName][sfLedgerEntryType.fieldName]; - BEAST_EXPECT(let == jss::AccountRoot); + // test: cannot deep freeze already frozen line when no freeze + // enacted + env(trust(G1, frozenAcc["USD"](0), tfSetDeepFreeze), + ter(tecNO_PERMISSION)); + } + else + { + // test: previous functionality, checking there's no changes to a + // trust line + env(trust(G1, A1["USD"](0), tfSetFreeze)); + auto affected = env.meta()->getJson( + JsonOptions::none)[sfAffectedNodes.fieldName]; + if (!BEAST_EXPECT(checkArraySize(affected, withTouch ? 2u : 1u))) + return; + + auto let = affected[0u][sfModifiedNode.fieldName] + [sfLedgerEntryType.fieldName]; + BEAST_EXPECT(let == jss::AccountRoot); + } + + // test: can clear freeze on account + env(trust(G1, frozenAcc["USD"](0), tfClearFreeze)); + { + auto const flags = getTrustlineFlags(env, withTouch ? 3u : 2u, 1u); + BEAST_EXPECT(!(flags & lsfLowFreeze)); + } + + if (features[featureDeepFreeze]) + { + // test: can clear deep freeze on account + env(trust(G1, deepFrozenAcc["USD"](0), tfClearDeepFreeze)); + { + auto const flags = + getTrustlineFlags(env, withTouch ? 3u : 2u, 1u); + BEAST_EXPECT(flags & lsfHighFreeze); + BEAST_EXPECT(!(flags & lsfHighDeepFreeze)); + } + } } void @@ -536,23 +769,1174 @@ class Freeze_test : public beast::unit_test::suite return; } + void + testOffersWhenDeepFrozen(FeatureBitset features) + { + testcase("Offers on frozen trust lines"); + + using namespace test::jtx; + Env env(*this, features); + + Account G1{"G1"}; + Account A1{"A1"}; + Account A2{"A2"}; + Account A3{"A3"}; + auto const USD{G1["USD"]}; + + env.fund(XRP(10000), G1, A1, A2, A3); + env.close(); + + auto const limit = USD(10000); + env.trust(limit, A1, A2, A3); + env.close(); + + env(pay(G1, A1, USD(1000))); + env(pay(G1, A2, USD(1000))); + env.close(); + + // Making large passive sell offer + // Wants to sell 50 USD for 100 XRP + env(offer(A2, XRP(100), USD(50)), txflags(tfPassive)); + env.close(); + // Making large passive buy offer + // Wants to buy 100 USD for 100 XRP + env(offer(A3, USD(100), XRP(100)), txflags(tfPassive)); + env.close(); + env.require(offers(A2, 1), offers(A3, 1)); + + // Checking A1 can buy from A2 by crossing it's offer + env(offer(A1, USD(1), XRP(2)), txflags(tfFillOrKill)); + env.close(); + env.require(balance(A1, USD(1001)), balance(A2, USD(999))); + + // Checking A1 can sell to A3 by crossing it's offer + env(offer(A1, XRP(1), USD(1)), txflags(tfFillOrKill)); + env.close(); + env.require(balance(A1, USD(1000)), balance(A3, USD(1))); + + // Testing aggressive and passive offer placing, trustline frozen by + // the issuer + { + env(trust(G1, A1["USD"](0), tfSetFreeze)); + env.close(); + + // test: can still make passive buy offer + env(offer(A1, USD(1), XRP(0.5)), txflags(tfPassive)); + env.close(); + env.require(balance(A1, USD(1000)), offers(A1, 1)); + // Cleanup + env(offer_cancel(A1, env.seq(A1) - 1)); + env.require(offers(A1, 0)); + env.close(); + + // test: can still buy from A2 + env(offer(A1, USD(1), XRP(2)), txflags(tfFillOrKill)); + env.close(); + env.require( + balance(A1, USD(1001)), balance(A2, USD(998)), offers(A1, 0)); + + // test: cannot create passive sell offer + env(offer(A1, XRP(2), USD(1)), + txflags(tfPassive), + ter(tecUNFUNDED_OFFER)); + env.close(); + env.require(balance(A1, USD(1001)), offers(A1, 0)); + + // test: cannot sell to A3 + env(offer(A1, XRP(1), USD(1)), + txflags(tfFillOrKill), + ter(tecUNFUNDED_OFFER)); + env.close(); + env.require(balance(A1, USD(1001)), offers(A1, 0)); + + env(trust(G1, A1["USD"](0), tfClearFreeze)); + env.close(); + } + + // Testing aggressive and passive offer placing, trustline deep frozen + // by the issuer + if (features[featureDeepFreeze]) + { + env(trust(G1, A1["USD"](0), tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // test: cannot create passive buy offer + env(offer(A1, USD(1), XRP(0.5)), + txflags(tfPassive), + ter(tecFROZEN)); + env.close(); + + // test: cannot buy from A2 + env(offer(A1, USD(1), XRP(2)), + txflags(tfFillOrKill), + ter(tecFROZEN)); + env.close(); + + // test: cannot create passive sell offer + env(offer(A1, XRP(2), USD(1)), + txflags(tfPassive), + ter(tecUNFUNDED_OFFER)); + env.close(); + + // test: cannot sell to A3 + env(offer(A1, XRP(1), USD(1)), + txflags(tfFillOrKill), + ter(tecUNFUNDED_OFFER)); + env.close(); + + env(trust(G1, A1["USD"](0), tfClearFreeze | tfClearDeepFreeze)); + env.close(); + env.require(balance(A1, USD(1001)), offers(A1, 0)); + } + + // Testing already existing offers behavior after trustline is frozen by + // the issuer + { + env.require(balance(A1, USD(1001))); + env(offer(A1, XRP(1.9), USD(1))); + env(offer(A1, USD(1), XRP(1.1))); + env.close(); + env.require(balance(A1, USD(1001)), offers(A1, 2)); + + env(trust(G1, A1["USD"](0), tfSetFreeze)); + env.close(); + + // test: A2 wants to sell to A1, must succeed + env.require(balance(A1, USD(1001)), balance(A2, USD(998))); + env(offer(A2, XRP(1.1), USD(1)), txflags(tfFillOrKill)); + env.close(); + env.require( + balance(A1, USD(1002)), balance(A2, USD(997)), offers(A1, 1)); + + // test: A3 wants to buy from A1, must fail + env.require( + balance(A1, USD(1002)), balance(A3, USD(1)), offers(A1, 1)); + env(offer(A3, USD(1), XRP(1.9)), + txflags(tfFillOrKill), + ter(tecKILLED)); + env.close(); + env.require( + balance(A1, USD(1002)), balance(A3, USD(1)), offers(A1, 0)); + + env(trust(G1, A1["USD"](0), tfClearFreeze)); + env.close(); + } + + // Testing existing offers behavior after trustline is deep frozen by + // the issuer + if (features[featureDeepFreeze]) + { + env.require(balance(A1, USD(1002))); + env(offer(A1, XRP(1.9), USD(1))); + env(offer(A1, USD(1), XRP(1.1))); + env.close(); + env.require(balance(A1, USD(1002)), offers(A1, 2)); + + env(trust(G1, A1["USD"](0), tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // test: A2 wants to sell to A1, must fail + env.require(balance(A1, USD(1002)), balance(A2, USD(997))); + env(offer(A2, XRP(1.1), USD(1)), + txflags(tfFillOrKill), + ter(tecKILLED)); + env.close(); + env.require( + balance(A1, USD(1002)), balance(A2, USD(997)), offers(A1, 1)); + + // test: A3 wants to buy from A1, must fail + env.require( + balance(A1, USD(1002)), balance(A3, USD(1)), offers(A1, 1)); + env(offer(A3, USD(1), XRP(1.9)), + txflags(tfFillOrKill), + ter(tecKILLED)); + env.close(); + env.require( + balance(A1, USD(1002)), balance(A3, USD(1)), offers(A1, 0)); + + env(trust(G1, A1["USD"](0), tfClearFreeze | tfClearDeepFreeze)); + env.close(); + } + + // Testing aggressive and passive offer placing, trustline frozen by + // the holder + { + env(trust(A1, limit, tfSetFreeze)); + env.close(); + + // test: A1 can make passive buy offer + env(offer(A1, USD(1), XRP(0.5)), txflags(tfPassive)); + env.close(); + env.require(balance(A1, USD(1002)), offers(A1, 1)); + // Cleanup + env(offer_cancel(A1, env.seq(A1) - 1)); + env.require(offers(A1, 0)); + env.close(); + + // test: A1 wants to buy, must fail + if (features[featureFlowCross]) + { + env(offer(A1, USD(1), XRP(2)), + txflags(tfFillOrKill), + ter(tecKILLED)); + env.close(); + env.require( + balance(A1, USD(1002)), + balance(A2, USD(997)), + offers(A1, 0)); + } + else + { + // The transaction that should be here would succeed. + // I don't want to adjust balances in following tests. Flow + // cross feature flag is not relevant to this particular test + // case so we're not missing out some corner cases checks. + } + + // test: A1 can create passive sell offer + env(offer(A1, XRP(2), USD(1)), txflags(tfPassive)); + env.close(); + env.require(balance(A1, USD(1002)), offers(A1, 1)); + // Cleanup + env(offer_cancel(A1, env.seq(A1) - 1)); + env.require(offers(A1, 0)); + env.close(); + + // test: A1 can sell to A3 + env(offer(A1, XRP(1), USD(1)), txflags(tfFillOrKill)); + env.close(); + env.require(balance(A1, USD(1001)), offers(A1, 0)); + + env(trust(A1, limit, tfClearFreeze)); + env.close(); + } + + // Testing aggressive and passive offer placing, trustline deep frozen + // by the holder + if (features[featureDeepFreeze]) + { + env(trust(A1, limit, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // test: A1 cannot create passive buy offer + env(offer(A1, USD(1), XRP(0.5)), + txflags(tfPassive), + ter(tecFROZEN)); + env.close(); + + // test: A1 cannot buy, must fail + env(offer(A1, USD(1), XRP(2)), + txflags(tfFillOrKill), + ter(tecFROZEN)); + env.close(); + + // test: A1 cannot create passive sell offer + env(offer(A1, XRP(2), USD(1)), + txflags(tfPassive), + ter(tecUNFUNDED_OFFER)); + env.close(); + + // test: A1 cannot sell to A3 + env(offer(A1, XRP(1), USD(1)), + txflags(tfFillOrKill), + ter(tecUNFUNDED_OFFER)); + env.close(); + + env(trust(A1, limit, tfClearFreeze | tfClearDeepFreeze)); + env.close(); + } + } + + void + testPathsWhenFrozen(FeatureBitset features) + { + testcase("Longer paths payment on frozen trust lines"); + using namespace test::jtx; + using path = test::jtx::path; + + 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.close(); + + auto const limit = USD(10000); + env.trust(limit, A1, A2); + env.close(); + + env(pay(G1, A1, USD(1000))); + env(pay(G1, A2, USD(1000))); + env.close(); + + env(offer(A2, XRP(100), USD(100)), txflags(tfPassive)); + env.close(); + + // Testing payments A1 <-> G1 using offer from A2 frozen by issuer. + { + env(trust(G1, A2["USD"](0), tfSetFreeze)); + env.close(); + + // test: A1 cannot send USD using XRP through A2 offer + env(pay(A1, G1, USD(10)), + path(~USD), + sendmax(XRP(11)), + txflags(tfNoRippleDirect), + ter(tecPATH_PARTIAL)); + env.close(); + + // test: G1 cannot send USD using XRP through A2 offer + env(pay(G1, A1, USD(10)), + path(~USD), + sendmax(XRP(11)), + txflags(tfNoRippleDirect), + ter(tecPATH_PARTIAL)); + env.close(); + + env(trust(G1, A2["USD"](0), tfClearFreeze)); + env.close(); + } + + // Testing payments A1 <-> G1 using offer from A2 deep frozen by issuer. + if (features[featureDeepFreeze]) + { + env(trust(G1, A2["USD"](0), tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // test: A1 cannot send USD using XRP through A2 offer + env(pay(A1, G1, USD(10)), + path(~USD), + sendmax(XRP(11)), + txflags(tfNoRippleDirect), + ter(tecPATH_PARTIAL)); + env.close(); + + // test: G1 cannot send USD using XRP through A2 offer + env(pay(G1, A1, USD(10)), + path(~USD), + sendmax(XRP(11)), + txflags(tfNoRippleDirect), + ter(tecPATH_PARTIAL)); + env.close(); + + env(trust(G1, A2["USD"](0), tfClearFreeze | tfClearDeepFreeze)); + env.close(); + } + + // Testing payments A1 <-> G1 using offer from A2 frozen by currency + // holder. + { + env(trust(A2, limit, tfSetFreeze)); + env.close(); + + // test: A1 can send USD using XRP through A2 offer + env(pay(A1, G1, USD(10)), + path(~USD), + sendmax(XRP(11)), + txflags(tfNoRippleDirect)); + env.close(); + + // test: G1 can send USD using XRP through A2 offer + env(pay(G1, A1, USD(10)), + path(~USD), + sendmax(XRP(11)), + txflags(tfNoRippleDirect)); + env.close(); + + env(trust(A2, limit, tfClearFreeze)); + env.close(); + } + + // Testing payments A1 <-> G1 using offer from A2 deep frozen by + // currency holder. + if (features[featureDeepFreeze]) + { + env(trust(A2, limit, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // test: A1 cannot send USD using XRP through A2 offer + env(pay(A1, G1, USD(10)), + path(~USD), + sendmax(XRP(11)), + txflags(tfNoRippleDirect), + ter(tecPATH_PARTIAL)); + env.close(); + + // test: G1 cannot send USD using XRP through A2 offer + env(pay(G1, A1, USD(10)), + path(~USD), + sendmax(XRP(11)), + txflags(tfNoRippleDirect), + ter(tecPATH_PARTIAL)); + env.close(); + + env(trust(A2, limit, tfClearFreeze | tfClearDeepFreeze)); + env.close(); + } + + // Cleanup + env(offer_cancel(A1, env.seq(A1) - 1)); + env.require(offers(A1, 0)); + env.close(); + + env(offer(A2, USD(100), XRP(100)), txflags(tfPassive)); + env.close(); + + // Testing payments A1 <-> G1 using offer from A2 frozen by issuer. + { + env(trust(G1, A2["USD"](0), tfSetFreeze)); + env.close(); + + // test: A1 can send XRP using USD through A2 offer + env(pay(A1, G1, XRP(10)), + path(~XRP), + sendmax(USD(11)), + txflags(tfNoRippleDirect)); + env.close(); + + // test: G1 can send XRP using USD through A2 offer + env(pay(G1, A1, XRP(10)), + path(~XRP), + sendmax(USD(11)), + txflags(tfNoRippleDirect)); + env.close(); + + env(trust(G1, A2["USD"](0), tfClearFreeze)); + env.close(); + } + + // Testing payments A1 <-> G1 using offer from A2 deep frozen by + // issuer. + if (features[featureDeepFreeze]) + { + env(trust(G1, A2["USD"](0), tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // test: A1 cannot send XRP using USD through A2 offer + env(pay(A1, G1, XRP(10)), + path(~XRP), + sendmax(USD(11)), + txflags(tfNoRippleDirect), + ter(tecPATH_PARTIAL)); + env.close(); + + // test: G1 cannot send XRP using USD through A2 offer + env(pay(G1, A1, XRP(10)), + path(~XRP), + sendmax(USD(11)), + txflags(tfNoRippleDirect), + ter(tecPATH_PARTIAL)); + env.close(); + + env(trust(G1, A2["USD"](0), tfClearFreeze | tfClearDeepFreeze)); + env.close(); + } + + // Testing payments A1 <-> G1 using offer from A2 frozen by currency + // holder. + { + env(trust(A2, limit, tfSetFreeze)); + env.close(); + + // test: A1 can send XRP using USD through A2 offer + env(pay(A1, G1, XRP(10)), + path(~XRP), + sendmax(USD(11)), + txflags(tfNoRippleDirect)); + env.close(); + + // test: G1 can send XRP using USD through A2 offer + env(pay(G1, A1, XRP(10)), + path(~XRP), + sendmax(USD(11)), + txflags(tfNoRippleDirect)); + env.close(); + + env(trust(A2, limit, tfClearFreeze)); + env.close(); + } + + // Testing payments A1 <-> G1 using offer from A2 deep frozen by + // currency holder. + if (features[featureDeepFreeze]) + { + env(trust(A2, limit, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // test: A1 cannot send XRP using USD through A2 offer + env(pay(A1, G1, XRP(10)), + path(~XRP), + sendmax(USD(11)), + txflags(tfNoRippleDirect), + ter(tecPATH_PARTIAL)); + env.close(); + + // test: G1 cannot send XRP using USD through A2 offer + env(pay(G1, A1, XRP(10)), + path(~XRP), + sendmax(USD(11)), + txflags(tfNoRippleDirect), + ter(tecPATH_PARTIAL)); + env.close(); + + env(trust(A2, limit, tfClearFreeze | tfClearDeepFreeze)); + env.close(); + } + + // Cleanup + env(offer_cancel(A1, env.seq(A1) - 1)); + env.require(offers(A1, 0)); + env.close(); + } + + void + testPaymentsWhenDeepFrozen(FeatureBitset features) + { + testcase("Direct payments on frozen trust lines"); + + 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.close(); + + auto const limit = USD(10000); + env.trust(limit, A1, A2); + env.close(); + + env(pay(G1, A1, USD(1000))); + env(pay(G1, A2, USD(1000))); + env.close(); + + // Checking payments before freeze + // To issuer: + env(pay(A1, G1, USD(1))); + env(pay(A2, G1, USD(1))); + env.close(); + + // To each other: + env(pay(A1, A2, USD(1))); + env(pay(A2, A1, USD(1))); + env.close(); + + // Freeze A1 + env(trust(G1, A1["USD"](0), tfSetFreeze)); + env.close(); + + // Issuer and A1 can send payments to each other + env(pay(A1, G1, USD(1))); + env(pay(G1, A1, USD(1))); + env.close(); + + // A1 cannot send tokens to A2 + env(pay(A1, A2, USD(1)), ter(tecPATH_DRY)); + + // A2 can still send to A1 + env(pay(A2, A1, USD(1))); + env.close(); + + if (features[featureDeepFreeze]) + { + // Deep freeze A1 + env(trust(G1, A1["USD"](0), tfSetDeepFreeze)); + env.close(); + + // Issuer and A1 can send payments to each other + env(pay(A1, G1, USD(1))); + env(pay(G1, A1, USD(1))); + env.close(); + + // A1 cannot send tokens to A2 + env(pay(A1, A2, USD(1)), ter(tecPATH_DRY)); + + // A2 cannot send tokens to A1 + env(pay(A2, A1, USD(1)), ter(tecPATH_DRY)); + + // Clear deep freeze on A1 + env(trust(G1, A1["USD"](0), tfClearDeepFreeze)); + env.close(); + } + + // Clear freeze on A1 + env(trust(G1, A1["USD"](0), tfClearFreeze)); + env.close(); + + // A1 freezes trust line + env(trust(A1, limit, tfSetFreeze)); + env.close(); + + // Issuer and A2 must not be affected + env(pay(A2, G1, USD(1))); + env(pay(G1, A2, USD(1))); + env.close(); + + // A1 can send tokens to the issuer + env(pay(A1, G1, USD(1))); + env.close(); + // A1 can send tokens to A2 + env(pay(A1, A2, USD(1))); + env.close(); + + // Issuer can sent tokens to A1 + env(pay(G1, A1, USD(1))); + // A2 cannot send tokens to A1 + env(pay(A2, A1, USD(1)), ter(tecPATH_DRY)); + + if (features[featureDeepFreeze]) + { + // A1 deep freezes trust line + env(trust(A1, limit, tfSetDeepFreeze)); + env.close(); + + // Issuer and A2 must not be affected + env(pay(A2, G1, USD(1))); + env(pay(G1, A2, USD(1))); + env.close(); + + // A1 can still send token to issuer + env(pay(A1, G1, USD(1))); + env.close(); + + // Issuer can send tokens to A1 + env(pay(G1, A1, USD(1))); + // A2 cannot send tokens to A1 + env(pay(A2, A1, USD(1)), ter(tecPATH_DRY)); + // A1 cannot send tokens to A2 + env(pay(A1, A2, USD(1)), ter(tecPATH_DRY)); + } + } + + void + testChecksWhenFrozen(FeatureBitset features) + { + testcase("Checks on frozen trust lines"); + + 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.close(); + + auto const limit = USD(10000); + env.trust(limit, A1, A2); + env.close(); + + env(pay(G1, A1, USD(1000))); + env(pay(G1, A2, USD(1000))); + env.close(); + + // Confirming we can write and cash checks + { + uint256 const checkId{getCheckIndex(G1, env.seq(G1))}; + env(check::create(G1, A1, USD(10))); + env.close(); + env(check::cash(A1, checkId, USD(10))); + env.close(); + } + + { + uint256 const checkId{getCheckIndex(G1, env.seq(G1))}; + env(check::create(G1, A2, USD(10))); + env.close(); + env(check::cash(A2, checkId, USD(10))); + env.close(); + } + + { + uint256 const checkId{getCheckIndex(A1, env.seq(A1))}; + env(check::create(A1, G1, USD(10))); + env.close(); + env(check::cash(G1, checkId, USD(10))); + env.close(); + } + + { + uint256 const checkId{getCheckIndex(A1, env.seq(A1))}; + env(check::create(A1, A2, USD(10))); + env.close(); + env(check::cash(A2, checkId, USD(10))); + env.close(); + } + + { + uint256 const checkId{getCheckIndex(A2, env.seq(A2))}; + env(check::create(A2, G1, USD(10))); + env.close(); + env(check::cash(G1, checkId, USD(10))); + env.close(); + } + + { + uint256 const checkId{getCheckIndex(A2, env.seq(A2))}; + env(check::create(A2, A1, USD(10))); + env.close(); + env(check::cash(A1, checkId, USD(10))); + env.close(); + } + + // Testing creation and cashing of checks on a trustline frozen by + // issuer + { + env(trust(G1, A1["USD"](0), tfSetFreeze)); + env.close(); + + // test: issuer writes check to A1. + { + uint256 const checkId{getCheckIndex(G1, env.seq(G1))}; + env(check::create(G1, A1, USD(10))); + env.close(); + env(check::cash(A1, checkId, USD(10)), ter(tecFROZEN)); + env.close(); + } + + // test: A2 writes check to A1. + { + uint256 const checkId{getCheckIndex(A2, env.seq(A2))}; + env(check::create(A2, A1, USD(10))); + env.close(); + // Same as previous test + env(check::cash(A1, checkId, USD(10)), ter(tecFROZEN)); + env.close(); + } + + // test: A1 writes check to issuer + { + env(check::create(A1, G1, USD(10)), ter(tecFROZEN)); + env.close(); + } + + // test: A1 writes check to A2 + { + // Same as previous test + env(check::create(A1, A2, USD(10)), ter(tecFROZEN)); + env.close(); + } + + // Unfreeze the trustline to create a couple of checks so that we + // could try to cash them later when the trustline is frozen again. + env(trust(G1, A1["USD"](0), tfClearFreeze)); + env.close(); + + uint256 const checkId1{getCheckIndex(A1, env.seq(A1))}; + env(check::create(A1, G1, USD(10))); + env.close(); + uint256 const checkId2{getCheckIndex(A1, env.seq(A1))}; + env(check::create(A1, A2, USD(10))); + env.close(); + + env(trust(G1, A1["USD"](0), tfSetFreeze)); + env.close(); + + // test: issuer tries to cash the check from A1 + { + env(check::cash(G1, checkId1, USD(10)), ter(tecPATH_PARTIAL)); + env.close(); + } + + // test: A2 tries to cash the check from A1 + { + env(check::cash(A2, checkId2, USD(10)), ter(tecPATH_PARTIAL)); + env.close(); + } + + env(trust(G1, A1["USD"](0), tfClearFreeze)); + env.close(); + } + + // Testing creation and cashing of checks on a trustline deep frozen by + // issuer + if (features[featureDeepFreeze]) + { + env(trust(G1, A1["USD"](0), tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // test: issuer writes check to A1. + { + uint256 const checkId{getCheckIndex(G1, env.seq(G1))}; + env(check::create(G1, A1, USD(10))); + env.close(); + + env(check::cash(A1, checkId, USD(10)), ter(tecFROZEN)); + env.close(); + } + + // test: A2 writes check to A1. + { + uint256 const checkId{getCheckIndex(A2, env.seq(A2))}; + env(check::create(A2, A1, USD(10))); + env.close(); + // Same as previous test + env(check::cash(A1, checkId, USD(10)), ter(tecFROZEN)); + env.close(); + } + + // test: A1 writes check to issuer + { + env(check::create(A1, G1, USD(10)), ter(tecFROZEN)); + env.close(); + } + + // test: A1 writes check to A2 + { + // Same as previous test + env(check::create(A1, A2, USD(10)), ter(tecFROZEN)); + env.close(); + } + + // Unfreeze the trustline to create a couple of checks so that we + // could try to cash them later when the trustline is frozen again. + env(trust(G1, A1["USD"](0), tfClearFreeze | tfClearDeepFreeze)); + env.close(); + + uint256 const checkId1{getCheckIndex(A1, env.seq(A1))}; + env(check::create(A1, G1, USD(10))); + env.close(); + uint256 const checkId2{getCheckIndex(A1, env.seq(A1))}; + env(check::create(A1, A2, USD(10))); + env.close(); + + env(trust(G1, A1["USD"](0), tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // test: issuer tries to cash the check from A1 + { + env(check::cash(G1, checkId1, USD(10)), ter(tecPATH_PARTIAL)); + env.close(); + } + + // test: A2 tries to cash the check from A1 + { + env(check::cash(A2, checkId2, USD(10)), ter(tecPATH_PARTIAL)); + env.close(); + } + + env(trust(G1, A1["USD"](0), tfClearFreeze | tfClearDeepFreeze)); + env.close(); + } + + // Testing creation and cashing of checks on a trustline frozen by + // a currency holder + { + env(trust(A1, limit, tfSetFreeze)); + env.close(); + + // test: issuer writes check to A1. + { + env(check::create(G1, A1, USD(10)), ter(tecFROZEN)); + env.close(); + } + + // test: A2 writes check to A1. + { + env(check::create(A2, A1, USD(10)), ter(tecFROZEN)); + env.close(); + } + + // test: A1 writes check to issuer + { + uint256 const checkId{getCheckIndex(A1, env.seq(A1))}; + env(check::create(A1, G1, USD(10))); + env.close(); + env(check::cash(G1, checkId, USD(10))); + env.close(); + } + + // test: A1 writes check to A2 + { + uint256 const checkId{getCheckIndex(A1, env.seq(A1))}; + env(check::create(A1, A2, USD(10))); + env.close(); + env(check::cash(A2, checkId, USD(10))); + env.close(); + } + + env(trust(A1, limit, tfClearFreeze)); + env.close(); + } + + // Testing creation and cashing of checks on a trustline deep frozen by + // a currency holder + if (features[featureDeepFreeze]) + { + env(trust(A1, limit, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // test: issuer writes check to A1. + { + env(check::create(G1, A1, USD(10)), ter(tecFROZEN)); + env.close(); + } + + // test: A2 writes check to A1. + { + env(check::create(A2, A1, USD(10)), ter(tecFROZEN)); + env.close(); + } + + // test: A1 writes check to issuer + { + uint256 const checkId{getCheckIndex(A1, env.seq(A1))}; + env(check::create(A1, G1, USD(10))); + env.close(); + env(check::cash(G1, checkId, USD(10)), ter(tecPATH_PARTIAL)); + env.close(); + } + + // test: A1 writes check to A2 + { + uint256 const checkId{getCheckIndex(A1, env.seq(A1))}; + env(check::create(A1, A2, USD(10))); + env.close(); + env(check::cash(A2, checkId, USD(10)), ter(tecPATH_PARTIAL)); + env.close(); + } + + env(trust(A1, limit, tfClearFreeze | tfClearDeepFreeze)); + env.close(); + } + } + + void + testNFTOffersWhenFreeze(FeatureBitset features) + { + testcase("NFT offers on frozen trust lines"); + 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.close(); + + auto const limit = USD(10000); + env.trust(limit, A1, A2); + env.close(); + + env(pay(G1, A1, USD(1000))); + env(pay(G1, A2, USD(1000))); + env.close(); + + // Testing A2 nft offer sell when A2 frozen by issuer + { + auto const sellOfferIndex = createNFTSellOffer(env, A2, USD(10)); + env(trust(G1, A2["USD"](0), tfSetFreeze)); + env.close(); + + // test: A2 can still receive USD for his NFT + env(token::acceptSellOffer(A1, sellOfferIndex)); + env.close(); + + env(trust(G1, A2["USD"](0), tfClearFreeze)); + env.close(); + } + + // Testing A2 nft offer sell when A2 deep frozen by issuer + if (features[featureDeepFreeze]) + { + auto const sellOfferIndex = createNFTSellOffer(env, A2, USD(10)); + + env(trust(G1, A2["USD"](0), tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // test: A2 cannot receive USD for his NFT + env(token::acceptSellOffer(A1, sellOfferIndex), ter(tecFROZEN)); + env.close(); + + env(trust(G1, A2["USD"](0), tfClearFreeze | tfClearDeepFreeze)); + env.close(); + } + + // Testing A1 nft offer sell when A2 frozen by issuer + { + auto const sellOfferIndex = createNFTSellOffer(env, A1, USD(10)); + env(trust(G1, A2["USD"](0), tfSetFreeze)); + env.close(); + + // test: A2 cannot send USD for NFT + env(token::acceptSellOffer(A2, sellOfferIndex), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); + + env(trust(G1, A2["USD"](0), tfClearFreeze)); + env.close(); + } + + // Testing A1 nft offer sell when A2 deep frozen by issuer + if (features[featureDeepFreeze]) + { + auto const sellOfferIndex = createNFTSellOffer(env, A1, USD(10)); + env(trust(G1, A2["USD"](0), tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // test: A2 cannot send USD for NFT + env(token::acceptSellOffer(A2, sellOfferIndex), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); + + env(trust(G1, A2["USD"](0), tfClearFreeze | tfClearDeepFreeze)); + env.close(); + } + + // Testing A2 nft offer sell when A2 frozen by currency holder + { + auto const sellOfferIndex = createNFTSellOffer(env, A2, USD(10)); + env(trust(A2, limit, tfSetFreeze)); + env.close(); + + // test: offer can still be accepted. + env(token::acceptSellOffer(A1, sellOfferIndex)); + env.close(); + + env(trust(A2, limit, tfClearFreeze)); + env.close(); + } + + // Testing A2 nft offer sell when A2 deep frozen by currency holder + if (features[featureDeepFreeze]) + { + auto const sellOfferIndex = createNFTSellOffer(env, A2, USD(10)); + + env(trust(A2, limit, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // test: A2 cannot receive USD for his NFT + env(token::acceptSellOffer(A1, sellOfferIndex), ter(tecFROZEN)); + env.close(); + + env(trust(A2, limit, tfClearFreeze | tfClearDeepFreeze)); + env.close(); + } + + // Testing A1 nft offer sell when A2 frozen by currency holder + { + auto const sellOfferIndex = createNFTSellOffer(env, A1, USD(10)); + env(trust(A2, limit, tfSetFreeze)); + env.close(); + + // test: A2 cannot send USD for NFT + env(token::acceptSellOffer(A2, sellOfferIndex)); + env.close(); + + env(trust(A2, limit, tfClearFreeze)); + env.close(); + } + + // Testing A1 nft offer sell when A2 deep frozen by currency holder + if (features[featureDeepFreeze]) + { + auto const sellOfferIndex = createNFTSellOffer(env, A1, USD(10)); + env(trust(A2, limit, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // test: A2 cannot send USD for NFT + env(token::acceptSellOffer(A2, sellOfferIndex), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); + + env(trust(A2, limit, tfClearFreeze | tfClearDeepFreeze)); + env.close(); + } + } + + // Helper function to extract trustline flags from open ledger + uint32_t + getTrustlineFlags( + test::jtx::Env& env, + size_t expectedArraySize, + size_t expectedArrayIndex, + bool modified = true) + { + using namespace test::jtx; + auto const affected = + env.meta()->getJson(JsonOptions::none)[sfAffectedNodes.fieldName]; + if (!BEAST_EXPECT(checkArraySize(affected, expectedArraySize))) + return 0; + + if (modified) + { + auto const node = + affected[expectedArrayIndex][sfModifiedNode.fieldName]; + if (!BEAST_EXPECT( + node[sfLedgerEntryType.fieldName] == "RippleState")) + return 0; + return node[sfFinalFields.fieldName][jss::Flags].asUInt(); + } + + auto const node = affected[expectedArrayIndex][sfCreatedNode.fieldName]; + if (!BEAST_EXPECT(node[sfLedgerEntryType.fieldName] == "RippleState")) + return 0; + return node[sfNewFields.fieldName][jss::Flags].asUInt(); + } + + // Helper function that returns the index of the next check on account + uint256 + getCheckIndex(AccountID const& account, std::uint32_t uSequence) + { + return keylet::check(account, uSequence).key; + } + + uint256 + createNFTSellOffer( + test::jtx::Env& env, + test::jtx::Account const& account, + test::jtx::PrettyAmount const& currency) + { + using namespace test::jtx; + uint256 const nftID{token::getNextID(env, account, 0u, tfTransferable)}; + env(token::mint(account, 0), txflags(tfTransferable)); + env.close(); + + uint256 const sellOfferIndex = + keylet::nftoffer(account, env.seq(account)).key; + env(token::createOffer(account, nftID, currency), + txflags(tfSellNFToken)); + env.close(); + + return sellOfferIndex; + } + public: void run() override { auto testAll = [this](FeatureBitset features) { testRippleState(features); + testDeepFreeze(features); + testCreateFrozenTrustline(features); + testSetAndClear(features); testGlobalFreeze(features); testNoFreeze(features); testOffersWhenFrozen(features); + testOffersWhenDeepFrozen(features); + testPaymentsWhenDeepFrozen(features); + testChecksWhenFrozen(features); + testPathsWhenFrozen(features); + testNFTOffersWhenFreeze(features); }; using namespace test::jtx; auto const sa = supported_amendments(); + testAll(sa - featureFlowCross - featureDeepFreeze); testAll(sa - featureFlowCross); testAll(sa - featureTouch); + testAll(sa - featureDeepFreeze); testAll(sa); } }; BEAST_DEFINE_TESTSUITE(Freeze, app, ripple); -} // namespace ripple +} // namespace ripple \ No newline at end of file diff --git a/src/test/app/NFTokenBurn_test.cpp b/src/test/app/NFTokenBurn_test.cpp index d0dff9519..4e18ea5b2 100644 --- a/src/test/app/NFTokenBurn_test.cpp +++ b/src/test/app/NFTokenBurn_test.cpp @@ -28,16 +28,6 @@ namespace ripple { class NFTokenBurn0_test : public beast::unit_test::suite { - // Helper function that returns the owner count of an account root. - static std::uint32_t - ownerCount(test::jtx::Env const& env, test::jtx::Account const& acct) - { - std::uint32_t ret{0}; - if (auto const sleAcct = env.le(acct)) - ret = sleAcct->at(sfOwnerCount); - return ret; - } - // Helper function that returns the number of nfts owned by an account. static std::uint32_t nftCount(test::jtx::Env& env, test::jtx::Account const& acct) diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index ea2e5a157..a83c6ee7d 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -31,16 +31,6 @@ class NFToken0_test : public beast::unit_test::suite { FeatureBitset const disallowIncoming{featureDisallowIncoming}; - // Helper function that returns the owner count of an account root. - static std::uint32_t - ownerCount(test::jtx::Env const& env, test::jtx::Account const& acct) - { - std::uint32_t ret{0}; - if (auto const sleAcct = env.le(acct)) - ret = sleAcct->at(sfOwnerCount); - return ret; - } - // Helper function that returns the number of NFTs minted by an issuer. static std::uint32_t mintedCount(test::jtx::Env const& env, test::jtx::Account const& issuer) @@ -3936,7 +3926,7 @@ class NFToken0_test : public beast::unit_test::suite for (Account const& acct : accounts) { if (std::uint32_t ownerCount = - this->ownerCount(env, acct); + test::jtx::ownerCount(env, acct); ownerCount != 1) { std::stringstream ss; diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index 300b14a47..23e994642 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -41,42 +41,6 @@ class Offer0_test : public beast::unit_test::suite return env.current()->info().parentCloseTime.time_since_epoch().count(); } - static auto - xrpMinusFee(jtx::Env const& env, std::int64_t xrpAmount) - -> jtx::PrettyAmount - { - using namespace jtx; - auto feeDrops = env.current()->fees().base; - return drops(dropsPerXRP * xrpAmount - feeDrops); - } - - static auto - ledgerEntryState( - jtx::Env& env, - jtx::Account const& acct_a, - jtx::Account const& acct_b, - std::string const& currency) - { - Json::Value jvParams; - jvParams[jss::ledger_index] = "current"; - jvParams[jss::ripple_state][jss::currency] = currency; - jvParams[jss::ripple_state][jss::accounts] = Json::arrayValue; - jvParams[jss::ripple_state][jss::accounts].append(acct_a.human()); - jvParams[jss::ripple_state][jss::accounts].append(acct_b.human()); - return env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - } - - static auto - ledgerEntryRoot(jtx::Env& env, jtx::Account const& acct) - { - Json::Value jvParams; - jvParams[jss::ledger_index] = "current"; - jvParams[jss::account_root] = acct.human(); - return env.rpc( - "json", "ledger_entry", to_string(jvParams))[jss::result]; - } - static auto ledgerEntryOffer( jtx::Env& env, diff --git a/src/test/app/PayChan_test.cpp b/src/test/app/PayChan_test.cpp index 0177d4f5b..39ff8c335 100644 --- a/src/test/app/PayChan_test.cpp +++ b/src/test/app/PayChan_test.cpp @@ -5297,6 +5297,103 @@ struct PayChan_test : public beast::unit_test::suite reqBal = chanBal + delta; authAmt = reqBal + USD(100); + // bob claim paychan success + sig = signClaimIOUAuth(alice.pk(), alice.sk(), chan, authAmt); + env(paychan::claim( + bob, chan, reqBal, authAmt, Slice(sig), alice.pk())); + env.close(); + } + // test Deep Freeze + { + // Env Setup + Env env{*this, features}; + env.fund(XRP(10000), alice, bob, gw); + env.close(); + env(trust(alice, USD(100000))); + env(trust(bob, USD(100000))); + env.close(); + env(pay(gw, alice, USD(10000))); + env(pay(gw, bob, USD(10000))); + env.close(); + + // set freeze on alice trustline + env(trust(gw, USD(100000), alice, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // setup transaction + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto chan = channel(alice, bob, env.seq(alice)); + + // create paychan fails - frozen trustline + env(paychan::create(alice, bob, USD(1000), settleDelay, pk), + ter(tecFROZEN)); + env.close(); + + // clear freeze on alice trustline + env(trust( + gw, USD(100000), alice, tfClearFreeze | tfClearDeepFreeze)); + env.close(); + + // create paychan success + chan = channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, USD(1000), settleDelay, pk)); + env.close(); + + // set freeze on alice trustline + env(trust(gw, USD(100000), alice, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // paychan fields + auto chanBal = channelBalance(*env.current(), chan); + auto chanAmt = channelAmount(*env.current(), chan); + auto const delta = USD(10); + auto reqBal = chanBal + delta; + auto authAmt = reqBal + USD(100); + + // alice claim paychan fails - frozen trustline + env(paychan::claim(alice, chan, reqBal, authAmt), ter(tecFROZEN)); + + // bob claim paychan fails - frozen trustline + auto sig = signClaimIOUAuth(alice.pk(), alice.sk(), chan, authAmt); + env(paychan::claim( + bob, chan, reqBal, authAmt, Slice(sig), alice.pk()), + ter(tecFROZEN)); + env.close(); + + // clear freeze on alice trustline + env(trust( + gw, USD(100000), alice, tfClearFreeze | tfClearDeepFreeze)); + env.close(); + + // alice close paychan success + env(paychan::claim(alice, chan, reqBal, authAmt), + txflags(tfClose), + ter(tesSUCCESS)); + env.close(); + + // create paychan success + chan = channel(alice, bob, env.seq(alice)); + env(paychan::create(alice, bob, USD(1000), settleDelay, pk)); + env.close(); + + // clear freeze on bob trustline + env(trust(gw, USD(100000), bob, tfClearFreeze | tfClearDeepFreeze)); + // clear freeze on alice trustline + env(trust( + gw, USD(100000), alice, tfClearFreeze | tfClearDeepFreeze)); + env.close(); + + // alice claim paychan success + env(paychan::claim(alice, chan, reqBal, authAmt)); + env.close(); + + // paychan fields + chanBal = channelBalance(*env.current(), chan); + chanAmt = channelAmount(*env.current(), chan); + reqBal = chanBal + delta; + authAmt = reqBal + USD(100); + // bob claim paychan success sig = signClaimIOUAuth(alice.pk(), alice.sk(), chan, authAmt); env(paychan::claim( diff --git a/src/test/app/Remit_test.cpp b/src/test/app/Remit_test.cpp index 8b615960f..a26779d26 100644 --- a/src/test/app/Remit_test.cpp +++ b/src/test/app/Remit_test.cpp @@ -2390,12 +2390,18 @@ struct Remit_test : public beast::unit_test::suite env(trust(gw, USD(100000), alice, tfSetFreeze)); env.close(); - // remit fails - frozen trustline + // outgoing remit fails - frozen trustline env(remit::remit(alice, bob), remit::amts({XRP(1), USD(1)}), ter(tecFROZEN)); env.close(); + // incoming remit success - frozen trustline + // env(remit::remit(bob, alice), + // remit::amts({XRP(1), USD(1)}), + // ter(tesSUCCESS)); + // env.close(); + // clear freeze on alice trustline env(trust(gw, USD(100000), alice, tfClearFreeze)); env.close(); @@ -2405,6 +2411,76 @@ struct Remit_test : public beast::unit_test::suite remit::amts({XRP(1), USD(1)}), ter(tesSUCCESS)); env.close(); + + // incoming remit success + env(remit::remit(bob, alice), + remit::amts({XRP(1), USD(1)}), + ter(tesSUCCESS)); + env.close(); + } + } + + void + testTLDeepFreeze(FeatureBitset features) + { + testcase("trustline deep 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"]; + + auto const aliceUSD = alice["USD"]; + auto const bobUSD = bob["USD"]; + + // test Deep Freeze + { + // Env Setup + Env env{*this, features}; + env.fund(XRP(10000), alice, bob, gw); + env.close(); + env(trust(alice, USD(100000))); + env(trust(bob, USD(100000))); + env.close(); + env(pay(gw, alice, USD(10000))); + env(pay(gw, bob, USD(10000))); + env.close(); + + // set deep freeze on alice trustline + env(trust(gw, USD(100000), alice, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // outgoing remit fails - deep frozen trustline + env(remit::remit(alice, bob), + remit::amts({XRP(1), USD(1)}), + ter(tecFROZEN)); + env.close(); + + // incoming remit fails - deep frozen trustline + env(remit::remit(bob, alice), + remit::amts({XRP(1), USD(1)}), + ter(tecFROZEN)); + env.close(); + + // clear freeze on alice trustline + env(trust( + gw, USD(100000), alice, tfClearFreeze | tfClearDeepFreeze)); + env.close(); + + // outgoing remit success + env(remit::remit(alice, bob), + remit::amts({XRP(1), USD(1)}), + ter(tesSUCCESS)); + env.close(); + + // incoming remit success + env(remit::remit(bob, alice), + remit::amts({XRP(1), USD(1)}), + ter(tesSUCCESS)); + env.close(); } } @@ -2817,6 +2893,7 @@ struct Remit_test : public beast::unit_test::suite testRequireAuth(features); testDepositAuth(features); testTLFreeze(features); + testTLDeepFreeze(features); testRippling(features); testURIToken(features); testOptionals(features); diff --git a/src/test/jtx.h b/src/test/jtx.h index 8d9b53bfe..0220cbed0 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index 9b1ac2b13..64a168860 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -421,6 +421,12 @@ public: PrettyAmount balance(Account const& account, Issue const& issue) const; + /** Return the number of objects owned by an account. + * Returns 0 if the account does not exist. + */ + std::uint32_t + ownerCount(Account const& account) const; + /** Return an account root. @return empty if the account does not exist. */ diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h new file mode 100644 index 000000000..7dc06fca0 --- /dev/null +++ b/src/test/jtx/TestHelpers.h @@ -0,0 +1,105 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 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_TESTHELPERS_H_INCLUDED +#define RIPPLE_TEST_JTX_TESTHELPERS_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace ripple { +namespace test { +namespace jtx { + +// Functions used in debugging +Json::Value +getAccountOffers(Env& env, AccountID const& acct, bool current = false); + +inline Json::Value +getAccountOffers(Env& env, Account const& acct, bool current = false) +{ + return getAccountOffers(env, acct.id(), current); +} + +Json::Value +getAccountLines(Env& env, AccountID const& acctId); + +inline Json::Value +getAccountLines(Env& env, Account const& acct) +{ + return getAccountLines(env, acct.id()); +} + +template +Json::Value +getAccountLines(Env& env, AccountID const& acctId, IOU... ious) +{ + auto const jrr = getAccountLines(env, acctId); + Json::Value res; + for (auto const& line : jrr[jss::lines]) + { + for (auto const& iou : {ious...}) + { + if (line[jss::currency].asString() == to_string(iou.currency)) + { + Json::Value v; + v[jss::currency] = line[jss::currency]; + v[jss::balance] = line[jss::balance]; + v[jss::limit] = line[jss::limit]; + v[jss::account] = line[jss::account]; + res[jss::lines].append(v); + } + } + } + if (!res.isNull()) + return res; + return jrr; +} + +[[nodiscard]] bool +checkArraySize(Json::Value const& val, unsigned int size); + +// Helper function that returns the owner count on an account. +std::uint32_t +ownerCount(test::jtx::Env const& env, test::jtx::Account const& account); + +PrettyAmount +xrpMinusFee(Env const& env, std::int64_t xrpAmount); + +Json::Value +ledgerEntryRoot(Env& env, Account const& acct); + +Json::Value +ledgerEntryState( + Env& env, + Account const& acct_a, + Account const& acct_b, + std::string const& currency); + +} // namespace jtx +} // namespace test +} // namespace ripple + +#endif // RIPPLE_TEST_JTX_TESTHELPERS_H_INCLUDED \ No newline at end of file diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index a26d1a25f..8a25ed3c7 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -203,6 +203,15 @@ Env::balance(Account const& account, Issue const& issue) const return {amount, lookup(issue.account).name()}; } +std::uint32_t +Env::ownerCount(Account const& account) const +{ + auto const sle = le(account); + if (!sle) + Throw("missing account root"); + return sle->getFieldU32(sfOwnerCount); +} + std::uint32_t Env::seq(Account const& account) const { diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp new file mode 100644 index 000000000..0b014663e --- /dev/null +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -0,0 +1,109 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 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 + +namespace ripple { +namespace test { +namespace jtx { + +// Functions used in debugging +Json::Value +getAccountOffers(Env& env, AccountID const& acct, bool current) +{ + Json::Value jv; + jv[jss::account] = to_string(acct); + return env.rpc("json", "account_offers", to_string(jv))[jss::result]; +} + +Json::Value +getAccountLines(Env& env, AccountID const& acctId) +{ + Json::Value jv; + jv[jss::account] = to_string(acctId); + return env.rpc("json", "account_lines", to_string(jv))[jss::result]; +} + +bool +checkArraySize(Json::Value const& val, unsigned int size) +{ + return val.isArray() && val.size() == size; +} + +std::uint32_t +ownerCount(Env const& env, Account const& account) +{ + return env.ownerCount(account); +} + +PrettyAmount +xrpMinusFee(Env const& env, std::int64_t xrpAmount) +{ + auto feeDrops = env.current()->fees().base; + return drops(dropsPerXRP * xrpAmount - feeDrops); +}; + +Json::Value +ledgerEntryRoot(Env& env, Account const& acct) +{ + Json::Value jvParams; + jvParams[jss::ledger_index] = "current"; + jvParams[jss::account_root] = acct.human(); + return env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result]; +} + +Json::Value +ledgerEntryState( + Env& env, + Account const& acct_a, + Account const& acct_b, + std::string const& currency) +{ + Json::Value jvParams; + jvParams[jss::ledger_index] = "current"; + jvParams[jss::ripple_state][jss::currency] = currency; + jvParams[jss::ripple_state][jss::accounts] = Json::arrayValue; + jvParams[jss::ripple_state][jss::accounts].append(acct_a.human()); + jvParams[jss::ripple_state][jss::accounts].append(acct_b.human()); + return env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result]; +} + +Json::Value +accountBalance(Env& env, Account const& acct) +{ + auto const jrr = ledgerEntryRoot(env, acct); + return jrr[jss::node][sfBalance.fieldName]; +} + +[[nodiscard]] bool +expectLedgerEntryRoot( + Env& env, + Account const& acct, + STAmount const& expectedValue) +{ + return accountBalance(env, acct) == to_string(expectedValue.xrp()); +} + +} // namespace jtx +} // namespace test +} // namespace ripple \ No newline at end of file diff --git a/src/test/ledger/Invariants_test.cpp b/src/test/ledger/Invariants_test.cpp index 86ceb508c..2e5c2f402 100644 --- a/src/test/ledger/Invariants_test.cpp +++ b/src/test/ledger/Invariants_test.cpp @@ -30,6 +30,15 @@ namespace ripple { class Invariants_test : public beast::unit_test::suite { + // The optional Preclose function is used to process additional transactions + // on the ledger after creating two accounts, but before closing it, and + // before the Precheck function. These should only be valid functions, and + // not direct manipulations. Preclose is not commonly used. + using Preclose = std::function; + // this is common setup/method for running a failing invariant check. The // precheck function is used to manipulate the ApplyContext with view // changes that will cause the check to fail. @@ -44,16 +53,18 @@ class Invariants_test : public beast::unit_test::suite Precheck const& precheck, XRPAmount fee = XRPAmount{}, STTx tx = STTx{ttACCOUNT_SET, [](STObject&) {}}, - std::initializer_list ters = { - tecINVARIANT_FAILED, - tefINVARIANT_FAILED}) + std::initializer_list ters = + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + Preclose const& preclose = {}) { using namespace test::jtx; Env env{*this}; - Account A1{"A1"}; - Account A2{"A2"}; + Account const A1{"A1"}; + Account const A2{"A2"}; env.fund(XRP(1000), A1, A2); + if (preclose) + BEAST_EXPECT(preclose(A1, A2, env)); env.close(); OpenView ov{*env.current()}; @@ -80,16 +91,17 @@ class Invariants_test : public beast::unit_test::suite terActual = ac.checkInvariants(terActual, fee); BEAST_EXPECT(terExpect == terActual); BEAST_EXPECT( - boost::starts_with( - sink.messages().str(), "Invariant failed:") || - boost::starts_with( - sink.messages().str(), "Transaction caused an exception")); - // uncomment if you want to log the invariant failure message - // log << " --> " << sink.messages().str() << std::endl; + sink.messages().str().starts_with("Invariant failed:") || + sink.messages().str().starts_with( + "Transaction caused an exception")); for (auto const& m : expect_logs) { - BEAST_EXPECT( - sink.messages().str().find(m) != std::string::npos); + if (sink.messages().str().find(m) == std::string::npos) + { + // uncomment if you want to log the invariant failure + // message log << " --> " << m << std::endl; + fail(); + } } } } @@ -218,6 +230,183 @@ class Invariants_test : public beast::unit_test::suite }); } + void + testNoDeepFreezeTrustLinesWithoutFreeze() + { + using namespace test::jtx; + testcase << "trust lines with deep freeze flag without freeze " + "not allowed"; + doInvariantCheck( + {{"a trust line with deep freeze flag without normal freeze was " + "created"}}, + [](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const sleNew = std::make_shared( + keylet::line(A1, A2, A1["USD"].currency)); + sleNew->setFieldAmount(sfLowLimit, A1["USD"](0)); + sleNew->setFieldAmount(sfHighLimit, A1["USD"](0)); + + std::uint32_t uFlags = 0u; + uFlags |= lsfLowDeepFreeze; + sleNew->setFieldU32(sfFlags, uFlags); + ac.view().insert(sleNew); + return true; + }); + + doInvariantCheck( + {{"a trust line with deep freeze flag without normal freeze was " + "created"}}, + [](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const sleNew = std::make_shared( + keylet::line(A1, A2, A1["USD"].currency)); + sleNew->setFieldAmount(sfLowLimit, A1["USD"](0)); + sleNew->setFieldAmount(sfHighLimit, A1["USD"](0)); + std::uint32_t uFlags = 0u; + uFlags |= lsfHighDeepFreeze; + sleNew->setFieldU32(sfFlags, uFlags); + ac.view().insert(sleNew); + return true; + }); + + doInvariantCheck( + {{"a trust line with deep freeze flag without normal freeze was " + "created"}}, + [](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const sleNew = std::make_shared( + keylet::line(A1, A2, A1["USD"].currency)); + sleNew->setFieldAmount(sfLowLimit, A1["USD"](0)); + sleNew->setFieldAmount(sfHighLimit, A1["USD"](0)); + std::uint32_t uFlags = 0u; + uFlags |= lsfLowDeepFreeze | lsfHighDeepFreeze; + sleNew->setFieldU32(sfFlags, uFlags); + ac.view().insert(sleNew); + return true; + }); + + doInvariantCheck( + {{"a trust line with deep freeze flag without normal freeze was " + "created"}}, + [](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const sleNew = std::make_shared( + keylet::line(A1, A2, A1["USD"].currency)); + sleNew->setFieldAmount(sfLowLimit, A1["USD"](0)); + sleNew->setFieldAmount(sfHighLimit, A1["USD"](0)); + std::uint32_t uFlags = 0u; + uFlags |= lsfLowDeepFreeze | lsfHighFreeze; + sleNew->setFieldU32(sfFlags, uFlags); + ac.view().insert(sleNew); + return true; + }); + + doInvariantCheck( + {{"a trust line with deep freeze flag without normal freeze was " + "created"}}, + [](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const sleNew = std::make_shared( + keylet::line(A1, A2, A1["USD"].currency)); + sleNew->setFieldAmount(sfLowLimit, A1["USD"](0)); + sleNew->setFieldAmount(sfHighLimit, A1["USD"](0)); + std::uint32_t uFlags = 0u; + uFlags |= lsfLowFreeze | lsfHighDeepFreeze; + sleNew->setFieldU32(sfFlags, uFlags); + ac.view().insert(sleNew); + return true; + }); + } + + void + testTransfersNotFrozen() + { + using namespace test::jtx; + testcase << "transfers when frozen"; + + Account G1{"G1"}; + // Helper function to establish the trustlines + auto const createTrustlines = + [&](Account const& A1, Account const& A2, Env& env) { + // Preclose callback to establish trust lines with gateway + env.fund(XRP(1000), G1); + + env.trust(G1["USD"](10000), A1); + env.trust(G1["USD"](10000), A2); + env.close(); + + env(pay(G1, A1, G1["USD"](1000))); + env(pay(G1, A2, G1["USD"](1000))); + env.close(); + + return true; + }; + + auto const A1FrozenByIssuer = + [&](Account const& A1, Account const& A2, Env& env) { + createTrustlines(A1, A2, env); + env(trust(G1, A1["USD"](10000), tfSetFreeze)); + env.close(); + + return true; + }; + + auto const A1DeepFrozenByIssuer = + [&](Account const& A1, Account const& A2, Env& env) { + A1FrozenByIssuer(A1, A2, env); + env(trust(G1, A1["USD"](10000), tfSetDeepFreeze)); + env.close(); + + return true; + }; + + auto const changeBalances = [&](Account const& A1, + Account const& A2, + ApplyContext& ac, + int A1Balance, + int A2Balance) { + auto const sleA1 = ac.view().peek(keylet::line(A1, G1["USD"])); + auto const sleA2 = ac.view().peek(keylet::line(A2, G1["USD"])); + + sleA1->setFieldAmount(sfBalance, G1["USD"](A1Balance)); + sleA2->setFieldAmount(sfBalance, G1["USD"](A2Balance)); + + ac.view().update(sleA1); + ac.view().update(sleA2); + }; + + // test: imitating frozen A1 making a payment to A2. + doInvariantCheck( + {{"Attempting to move frozen funds"}}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + changeBalances(A1, A2, ac, -900, -1100); + return true; + }, + XRPAmount{}, + STTx{ttPAYMENT, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + A1FrozenByIssuer); + + // test: imitating deep frozen A1 making a payment to A2. + doInvariantCheck( + {{"Attempting to move frozen funds"}}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + changeBalances(A1, A2, ac, -900, -1100); + return true; + }, + XRPAmount{}, + STTx{ttPAYMENT, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + A1DeepFrozenByIssuer); + + // test: imitating A2 making a payment to deep frozen A1. + doInvariantCheck( + {{"Attempting to move frozen funds"}}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + changeBalances(A1, A2, ac, -1100, -900); + return true; + }, + XRPAmount{}, + STTx{ttPAYMENT, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + A1DeepFrozenByIssuer); + } + void testXRPBalanceCheck() { @@ -454,6 +643,8 @@ public: testAccountRootsNotRemoved(); testTypesMatch(); testNoXRPTrustLine(); + testNoDeepFreezeTrustLinesWithoutFreeze(); + testTransfersNotFrozen(); testXRPBalanceCheck(); testTransactionFeeCheck(); testNoBadOffers(); diff --git a/src/test/rpc/AccountLinesRPC_test.cpp b/src/test/rpc/AccountLinesRPC_test.cpp index 04688156d..b8444f2c4 100644 --- a/src/test/rpc/AccountLinesRPC_test.cpp +++ b/src/test/rpc/AccountLinesRPC_test.cpp @@ -149,6 +149,12 @@ public: // Set flags on gw2 trust lines so we can look for them. env(trust(alice, gw2Currency(0), gw2, tfSetNoRipple | tfSetFreeze)); + + env(trust( + alice, + gw2Currency(0), + gw2, + tfSetNoRipple | tfSetFreeze | tfSetDeepFreeze)); } env.close(); LedgerInfo const ledger58Info = env.closed()->info(); @@ -325,6 +331,7 @@ public: gw2.human() + R"("})"); auto const& line = lines[jss::result][jss::lines][0u]; BEAST_EXPECT(line[jss::freeze].asBool() == true); + BEAST_EXPECT(line[jss::deep_freeze].asBool() == true); BEAST_EXPECT(line[jss::no_ripple].asBool() == true); BEAST_EXPECT(line[jss::peer_authorized].asBool() == true); } @@ -340,6 +347,7 @@ public: alice.human() + R"("})"); auto const& lineA = linesA[jss::result][jss::lines][0u]; BEAST_EXPECT(lineA[jss::freeze_peer].asBool() == true); + BEAST_EXPECT(lineA[jss::deep_freeze_peer].asBool() == true); BEAST_EXPECT(lineA[jss::no_ripple_peer].asBool() == true); BEAST_EXPECT(lineA[jss::authorized].asBool() == true);