diff --git a/include/xrpl/protocol/Feature.h b/include/xrpl/protocol/Feature.h index eb975f39ae0..a2510c63000 100644 --- a/include/xrpl/protocol/Feature.h +++ b/include/xrpl/protocol/Feature.h @@ -80,7 +80,7 @@ namespace detail { // Feature.cpp. Because it's only used to reserve storage, and determine how // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than // the actual number of amendments. A LogicError on startup will verify this. -static constexpr std::size_t numFeatures = 80; +static constexpr std::size_t numFeatures = 81; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index b0374db1c29..0657ce8cf76 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -160,10 +160,12 @@ 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 - lsfAMMNode = 0x01000000, // True, trust line to AMM. Used by client - // apps to identify payments via AMM. + lsfLowFreeze = 0x00400000, // True, low side has set freeze flag + lsfHighFreeze = 0x00800000, // True, high side has set freeze flag + lsfAMMNode = 0x01000000, // True, trust line to AMM. Used by client + // apps to identify payments via AMM. + lsfLowDeepFreeze = 0x02000000, // True, low side has set deep freeze flag + lsfHighDeepFreeze = 0x04000000, // True, low side has set deep freeze flag // ltSIGNER_LIST lsfOneOwnerCount = 0x00010000, // True, uses only one OwnerCount diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index 4894f48a7f9..2e230595081 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -114,9 +114,11 @@ constexpr std::uint32_t tfSetNoRipple = 0x00020000; constexpr std::uint32_t tfClearNoRipple = 0x00040000; constexpr std::uint32_t tfSetFreeze = 0x00100000; constexpr std::uint32_t tfClearFreeze = 0x00200000; +constexpr std::uint32_t tfSetDeepFreeze = 0x00400000; +constexpr std::uint32_t tfClearDeepFreeze = 0x00800000; constexpr std::uint32_t tfTrustSetMask = ~(tfUniversal | tfSetfAuth | tfSetNoRipple | tfClearNoRipple | tfSetFreeze | - tfClearFreeze); + tfClearFreeze | tfSetDeepFreeze | tfClearDeepFreeze); // EnableAmendment flags: constexpr std::uint32_t tfGotMajority = 0x00010000; diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 3a8d77e2bab..0da0db2161a 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -28,10 +28,12 @@ // Keep it sorted in reverse chronological order. // If you add an amendment here, then do not forget to increment `numFeatures` // in include/xrpl/protocol/Feature.h. +// clang-format off +XRPL_FEATURE(DeepFreeze, Supported::yes, VoteBehavior::DefaultNo) +XRPL_FEATURE(MPTokensV1, Supported::yes, VoteBehavior::DefaultNo) // InvariantsV1_1 will be changes to Supported::yes when all the // invariants expected to be included under it are complete. -XRPL_FEATURE(MPTokensV1, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(InvariantsV1_1, Supported::no, VoteBehavior::DefaultNo) XRPL_FIX (NFTokenPageLinks, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (InnerObjTemplate2, Supported::yes, VoteBehavior::DefaultNo) @@ -111,3 +113,5 @@ XRPL_FIX (NFTokenNegOffer, Supported::yes, VoteBehavior::Obsolete) XRPL_FIX (NFTokenDirV1, Supported::yes, VoteBehavior::Obsolete) XRPL_FEATURE(NonFungibleTokensV1, Supported::yes, VoteBehavior::Obsolete) XRPL_FEATURE(CryptoConditionsSuite, Supported::yes, VoteBehavior::Obsolete) + +// clang-format on \ No newline at end of file diff --git a/src/test/app/AMMExtended_test.cpp b/src/test/app/AMMExtended_test.cpp index 96053b93b44..0c04bec8fea 100644 --- a/src/test/app/AMMExtended_test.cpp +++ b/src/test/app/AMMExtended_test.cpp @@ -4129,6 +4129,10 @@ struct AMMExtended_test : public jtx::AMMTest { using namespace test::jtx; auto const sa = supported_amendments(); + testRippleState(sa - featureDeepFreeze); + testGlobalFreeze(sa - featureDeepFreeze); + testOffersWhenFrozen(sa - featureDeepFreeze); + testRippleState(sa); testGlobalFreeze(sa); testOffersWhenFrozen(sa); diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index ceddc019504..24b6ff668ec 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -4397,6 +4397,28 @@ struct AMM_test : public jtx::AMMTest 0, std::nullopt, {features}); + + // Individually deep frozen account + if (features[featureDeepFreeze]) + { + testAMM( + [&](AMM& ammAlice, Env& env) { + env(trust( + gw, carol["USD"](0), tfSetFreeze | tfSetDeepFreeze)); + env(trust( + gw, alice["USD"](0), tfSetFreeze | tfSetDeepFreeze)); + env.close(); + env(pay(alice, carol, USD(1)), + path(~USD), + sendmax(XRP(10)), + txflags(tfNoRippleDirect | tfPartialPayment), + ter(tecPATH_DRY)); + }, + std::nullopt, + 0, + std::nullopt, + {features}); + } } void @@ -6882,6 +6904,7 @@ struct AMM_test : public jtx::AMMTest testBasicPaymentEngine(all - fixAMMv1_1); testBasicPaymentEngine(all - fixReducedOffersV2); testBasicPaymentEngine(all - fixAMMv1_1 - fixReducedOffersV2); + testBasicPaymentEngine(all - featureDeepFreeze); testAMMTokens(); testAmendment(); testFlags(); diff --git a/src/test/app/Check_test.cpp b/src/test/app/Check_test.cpp index 31b45abf43a..9739cf36c63 100644 --- a/src/test/app/Check_test.cpp +++ b/src/test/app/Check_test.cpp @@ -546,6 +546,72 @@ class Check_test : public beast::unit_test::suite env(trust(alice, USD(0), tfClearFreeze)); env.close(); } + { + // Deep Frozen trust line. Check creation should be similar to + // payment behavior in the face of frozen trust lines. + env.trust(USD(1000), alice); + env.trust(USD(1000), bob); + env.close(); + env(pay(gw1, alice, USD(25))); + env(pay(gw1, bob, USD(25))); + env.close(); + + // Setting trustline deep freeze in one direction prevents alice + // from creating a check for USD. And bob and gw1 should not be + // able to create a check for USD to alice. + env(trust(gw1, alice["USD"](0), tfSetFreeze | tfSetDeepFreeze)); + env.close(); + env(check::create(alice, bob, USD(50)), ter(tecFROZEN)); + env.close(); + env(pay(alice, bob, USD(1)), ter(tecPATH_DRY)); + env.close(); + env(check::create(bob, alice, USD(50))); + env.close(); + if (features[featureDeepFreeze]) + env(pay(bob, alice, USD(1)), ter(tecPATH_DRY)); + else + env(pay(bob, alice, USD(1))); + env.close(); + env(check::create(gw1, alice, USD(50))); + env.close(); + env(pay(gw1, alice, USD(1))); + env.close(); + + // Clear that freeze. Now check creation works. + env(trust(gw1, alice["USD"](0), tfClearFreeze | tfClearDeepFreeze)); + env.close(); + env(check::create(alice, bob, USD(50))); + env.close(); + env(check::create(bob, alice, USD(50))); + env.close(); + env(check::create(gw1, alice, USD(50))); + env.close(); + + // Deep Freezing in the other direction does effect alice's USD + // check creation, and prevents bob and gw1 from writing a check + // for USD to alice. + env(trust(alice, USD(0), tfSetFreeze | tfSetDeepFreeze)); + env.close(); + env(check::create(alice, bob, USD(50))); + env.close(); + if (features[featureDeepFreeze]) + env(pay(alice, bob, USD(1)), ter(tecPATH_DRY)); + else + env(pay(alice, bob, USD(1))); + env.close(); + env(check::create(bob, alice, USD(50)), ter(tecFROZEN)); + env.close(); + env(pay(bob, alice, USD(1)), ter(tecPATH_DRY)); + env.close(); + env(check::create(gw1, alice, USD(50)), ter(tecFROZEN)); + env.close(); + env(pay(gw1, alice, USD(1)), ter(tecPATH_DRY)); + env.close(); + + // Clear that deep freeze. + env(trust(alice, USD(0), tfClearFreeze | tfClearDeepFreeze)); + env.close(); + } // Expired expiration. env(check::create(alice, bob, USD(50)), @@ -2719,6 +2785,7 @@ class Check_test : public beast::unit_test::suite auto const sa = supported_amendments(); testWithFeats(sa - featureCheckCashMakesTrustLine); testWithFeats(sa - disallowIncoming); + testWithFeats(sa - featureDeepFreeze); testWithFeats(sa); testTrustLineCreation(sa); // Test with featureCheckCashMakesTrustLine diff --git a/src/test/app/Freeze_test.cpp b/src/test/app/Freeze_test.cpp index 0c54f0e1f39..a9b73795755 100644 --- a/src/test/app/Freeze_test.cpp +++ b/src/test/app/Freeze_test.cpp @@ -186,6 +186,181 @@ class Freeze_test : public beast::unit_test::suite } } + void + testDeepFreeze(FeatureBitset features) + { + testcase("Deep Freeze"); + + using namespace test::jtx; + Env env(*this, features); + + 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, 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, 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, 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, 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(temDISABLED)); + + // test: clearing deep freeze before amendment fails + env(trust(G1, A1["USD"](0), tfClearDeepFreeze), ter(temDISABLED)); + } + } + + 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); + + 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, 1u)); // means no trustline changes + } + } + } + void testGlobalFreeze(FeatureBitset features) { @@ -354,15 +529,43 @@ 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, 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, 2u, 1u); + BEAST_EXPECT(!(flags & (lsfLowFreeze | lsfLowDeepFreeze))); + BEAST_EXPECT(flags & lsfHighFreeze); + BEAST_EXPECT(flags & lsfHighDeepFreeze); + } + } env.close(); // TrustSet NoFreeze @@ -388,15 +591,46 @@ class Freeze_test : public beast::unit_test::suite 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, 1u))) - return; + 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 + 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, 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, 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, 2u, 1u); + BEAST_EXPECT(flags & lsfHighFreeze); + BEAST_EXPECT(!(flags & lsfHighDeepFreeze)); + } + } } void @@ -506,19 +740,358 @@ 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"}; + + env.fund(XRP(10000), G1, A1, A2, A3); + env.close(); + + env.trust(G1["USD"](1000), A1, A2, A3); + env.close(); + + env(pay(G1, A1, G1["USD"](1000))); + env(pay(G1, A2, G1["USD"](1000))); + env.close(); + + // Making large passive sell offer + // Wants to sell 50 USD for 100 XRP + env(offer(A2, XRP(100), G1["USD"](50)), txflags(tfPassive)); + env.close(); + // Making large passive buy offer + // Wants to buy 100 USD for 100 XRP + env(offer(A3, G1["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, G1["USD"](1), XRP(2))); + env.close(); + env.require(balance(A1, G1["USD"](1001)), balance(A2, G1["USD"](999))); + + // Checking A1 can sell to A3 by crossing it's offer + env(offer(A1, XRP(1), G1["USD"](1))); + env.close(); + env.require(balance(A1, G1["USD"](1000)), balance(A3, G1["USD"](1))); + + // Testing aggressive and passive offer placing A1 frozen by issuer + { + env(trust(G1, A1["USD"](0), tfSetFreeze)); + env.close(); + + // test: can still make passive buy offer + env(offer(A1, G1["USD"](1), XRP(0.5)), txflags(tfPassive)); + env.close(); + env.require(balance(A1, G1["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, G1["USD"](1), XRP(2))); + env.close(); + env.require( + balance(A1, G1["USD"](1001)), + balance(A2, G1["USD"](998)), + offers(A1, 0)); + + // test: cannot create passive sell offer + env(offer(A1, XRP(2), G1["USD"](1)), ter(tecUNFUNDED_OFFER)); + env.close(); + env.require(balance(A1, G1["USD"](1001)), offers(A1, 0)); + + // test: cannot sell to A3 + env(offer(A1, XRP(1), G1["USD"](1)), ter(tecUNFUNDED_OFFER)); + env.close(); + env.require(balance(A1, G1["USD"](1001)), offers(A1, 0)); + + env(trust(G1, A1["USD"](0), tfClearFreeze)); + env.close(); + } + + // Testing aggressive and passive offer placing A1 deep frozen by issuer + if (features[featureDeepFreeze]) + { + env(trust(G1, A1["USD"](0), tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // test: cannot create passive buy offer + env(offer(A1, G1["USD"](1), XRP(0.5)), ter(tecFROZEN)); + env.close(); + + // test: cannot buy from A2 + env(offer(A1, G1["USD"](1), XRP(2)), ter(tecFROZEN)); + env.close(); + + // test: cannot create passive sell offer + env(offer(A1, XRP(2), G1["USD"](1)), ter(tecUNFUNDED_OFFER)); + env.close(); + + // test: cannot sell to A3 + env(offer(A1, XRP(1), G1["USD"](1)), ter(tecUNFUNDED_OFFER)); + env.close(); + + env(trust(G1, A1["USD"](0), tfClearFreeze | tfClearDeepFreeze)); + env.close(); + env.require(balance(A1, G1["USD"](1001)), offers(A1, 0)); + } + + // Testing already existing offers behavior after trustline is frozen + { + env.require(balance(A1, G1["USD"](1001))); + env(offer(A1, XRP(1.9), G1["USD"](1))); + env(offer(A1, G1["USD"](1), XRP(1.1))); + env.close(); + env.require(balance(A1, G1["USD"](1001)), offers(A1, 2)); + + env(trust(G1, A1["USD"](0), tfSetFreeze)); + env.close(); + + // A2 wants to sell to A1, must succeed + env.require( + balance(A1, G1["USD"](1001)), balance(A2, G1["USD"](998))); + env(offer(A2, XRP(1.1), G1["USD"](1)), txflags(tfFillOrKill)); + env.close(); + env.require( + balance(A1, G1["USD"](1002)), + balance(A2, G1["USD"](997)), + offers(A1, 1)); + + // A3 wants to buy from A1, must fail + env.require( + balance(A1, G1["USD"](1002)), + balance(A3, G1["USD"](1)), + offers(A1, 1)); + env(offer(A3, G1["USD"](1), XRP(1.9)), + txflags(tfFillOrKill), + ter(tecKILLED)); + env.close(); + env.require( + balance(A1, G1["USD"](1002)), + balance(A3, G1["USD"](1)), + offers(A1, 0)); + + env(trust(G1, A1["USD"](0), tfClearFreeze)); + env.close(); + } + + // Testing existing offers behavior after trustline is deep frozen + if (features[featureDeepFreeze]) + { + env.require(balance(A1, G1["USD"](1002))); + env(offer(A1, XRP(1.9), G1["USD"](1))); + env(offer(A1, G1["USD"](1), XRP(1.1))); + env.close(); + env.require(balance(A1, G1["USD"](1002)), offers(A1, 2)); + + env(trust(G1, A1["USD"](0), tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // A2 wants to sell to A1, must fail + env.require( + balance(A1, G1["USD"](1002)), balance(A2, G1["USD"](997))); + env(offer(A2, XRP(1.1), G1["USD"](1)), + txflags(tfFillOrKill), + ter(tecKILLED)); + env.close(); + env.require( + balance(A1, G1["USD"](1002)), + balance(A2, G1["USD"](997)), + offers(A1, 1)); + + // A3 wants to buy from A1, must fail + env.require( + balance(A1, G1["USD"](1002)), + balance(A3, G1["USD"](1)), + offers(A1, 1)); + env(offer(A3, G1["USD"](1), XRP(1.9)), + txflags(tfFillOrKill), + ter(tecKILLED)); + env.close(); + env.require( + balance(A1, G1["USD"](1002)), + balance(A3, G1["USD"](1)), + offers(A1, 0)); + + env(trust(G1, A1["USD"](0), tfClearFreeze | tfClearDeepFreeze)); + 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"}; + + env.fund(XRP(10000), G1, A1, A2); + env.close(); + + env.trust(G1["USD"](1000), A1, A2); + env.close(); + + env(pay(G1, A1, G1["USD"](1000))); + env(pay(G1, A2, G1["USD"](1000))); + env.close(); + + // Checking payments before freeze + // To issuer: + env(pay(A1, G1, G1["USD"](1))); + env(pay(A2, G1, G1["USD"](1))); + env.close(); + + // To each other: + env(pay(A1, A2, G1["USD"](1))); + env(pay(A2, A1, G1["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, G1["USD"](1))); + env(pay(G1, A1, G1["USD"](1))); + env.close(); + + // A1 cannot send tokens to A2 + env(pay(A1, A2, G1["USD"](1)), ter(tecPATH_DRY)); + + // A2 can still send to A1 + env(pay(A2, A1, G1["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, G1["USD"](1))); + env(pay(G1, A1, G1["USD"](1))); + env.close(); + + // A1 cannot send tokens to A2 + env(pay(A1, A2, G1["USD"](1)), ter(tecPATH_DRY)); + + // A2 cannot send tokens to A1 + env(pay(A2, A1, G1["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, G1["USD"](0), tfSetFreeze)); + env.close(); + + // Issuer and A2 must not be affected + env(pay(A2, G1, G1["USD"](1))); + env(pay(G1, A2, G1["USD"](1))); + env.close(); + + // A1 can send tokens to the issuer + env(pay(A1, G1, G1["USD"](1))); + env.close(); + // A1 can send tokens to A2 + env(pay(A1, A2, G1["USD"](1))); + env.close(); + + // Issuer cannot sent tokens to A1 + env(pay(G1, A1, G1["USD"](1)), ter(tecPATH_DRY)); + // A2 cannot send tokens to A1 + env(pay(A2, A1, G1["USD"](1)), ter(tecPATH_DRY)); + + if (features[featureDeepFreeze]) + { + // A1 deep freezes trust line + env(trust(A1, G1["USD"](0), tfSetDeepFreeze)); + env.close(); + + // Issuer and A2 must not be affected + env(pay(A2, G1, G1["USD"](1))); + env(pay(G1, A2, G1["USD"](1))); + env.close(); + + // A1 can still send token to issuer + env(pay(A1, G1, G1["USD"](1))); + env.close(); + + // Issuer cannot send tokens to A1 + env(pay(G1, A1, G1["USD"](1)), ter(tecPATH_DRY)); + // A2 cannot send tokens to A1 + env(pay(A2, A1, G1["USD"](1)), ter(tecPATH_DRY)); + // A1 cannot send tokens to A2 + env(pay(A1, A2, G1["USD"](1)), ter(tecPATH_DRY)); + } + } + + 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) + { + return affected[expectedArrayIndex][sfModifiedNode.fieldName] + [sfFinalFields.fieldName][jss::Flags] + .asUInt(); + } + + return affected[expectedArrayIndex][sfCreatedNode.fieldName] + [sfNewFields.fieldName][jss::Flags] + .asUInt(); + } + 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); }; using namespace test::jtx; auto const sa = supported_amendments(); testAll(sa - featureFlowCross); + testAll(sa - featureDeepFreeze); testAll(sa); } }; diff --git a/src/xrpld/app/paths/detail/StepChecks.h b/src/xrpld/app/paths/detail/StepChecks.h index 140c9d1fe46..4e8db6c9d08 100644 --- a/src/xrpld/app/paths/detail/StepChecks.h +++ b/src/xrpld/app/paths/detail/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/xrpld/app/tx/detail/CashCheck.cpp b/src/xrpld/app/tx/detail/CashCheck.cpp index 8b5ef79b6d4..f6e5f6f3e3f 100644 --- a/src/xrpld/app/tx/detail/CashCheck.cpp +++ b/src/xrpld/app/tx/detail/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/xrpld/app/tx/detail/CreateOffer.cpp b/src/xrpld/app/tx/detail/CreateOffer.cpp index 2a5145594a1..41c99530053 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.cpp +++ b/src/xrpld/app/tx/detail/CreateOffer.cpp @@ -257,6 +257,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/xrpld/app/tx/detail/OfferStream.cpp b/src/xrpld/app/tx/detail/OfferStream.cpp index b963195259a..5a4dd3b7580 100644 --- a/src/xrpld/app/tx/detail/OfferStream.cpp +++ b/src/xrpld/app/tx/detail/OfferStream.cpp @@ -272,6 +272,19 @@ TOfferStreamBase::step() continue; } + bool const deepFrozen = isDeepFrozen( + view_, + offer_.owner(), + offer_.issueIn().currency, + offer_.issueIn().account); + if (deepFrozen) + { + permRmOffer(entry->key()); + JLOG(j_.trace()) + << "Removing deep frozen unfunded offer " << entry->key(); + continue; + } + // Calculate owner funds ownerFunds_ = accountFundsHelper( view_, diff --git a/src/xrpld/app/tx/detail/SetTrust.cpp b/src/xrpld/app/tx/detail/SetTrust.cpp index 954fc6543f1..f9597307c0d 100644 --- a/src/xrpld/app/tx/detail/SetTrust.cpp +++ b/src/xrpld/app/tx/detail/SetTrust.cpp @@ -26,6 +26,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 @@ -45,6 +81,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 temDISABLED; + } + } + STAmount const saLimitAmount(tx.getFieldAmount(sfLimitAmount)); if (!isLegalNet(saLimitAmount)) @@ -182,6 +228,60 @@ SetTrust::preclaim(PreclaimContext const& ctx) } } + // 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) + { + if (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; } @@ -197,7 +297,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_)); @@ -242,13 +342,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) { @@ -408,14 +510,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; @@ -498,8 +602,8 @@ SetTrust::doApply() // Reserve is not scaled by load. else if (bReserveIncrease && mPriorBalance < reserveCreate) { - JLOG(j_.trace()) - << "Delay transaction: Insufficent reserve to add trust line."; + JLOG(j_.trace()) << "Delay transaction: Insufficent reserve to " + "add trust line."; // Another transaction could provide XRP to the account and then // this transaction would succeed. @@ -515,17 +619,18 @@ 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()) << "Redundant: Setting non-existent ripple line to defaults."; return tecNO_LINE_REDUNDANT; } - else if (mPriorBalance < reserveCreate) // Reserve is not scaled by load. + else if (mPriorBalance < reserveCreate) // Reserve is not scaled by + // load. { JLOG(j_.trace()) << "Delay transaction: Line does not exist. " "Insufficent reserve to create line."; @@ -555,6 +660,7 @@ SetTrust::doApply() bSetAuth, bSetNoRipple && !bClearNoRipple, bSetFreeze && !bClearFreeze, + bSetDeepFreeze, saBalance, saLimitAllow, // Limit for who is being charged. uQualityIn, diff --git a/src/xrpld/ledger/View.h b/src/xrpld/ledger/View.h index 74027752486..b964fc0ee76 100644 --- a/src/xrpld/ledger/View.h +++ b/src/xrpld/ledger/View.h @@ -153,6 +153,13 @@ isFrozen(ReadView const& view, AccountID const& account, Asset const& asset) asset.value()); } +[[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. @@ -438,6 +445,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. diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index ae4eb095017..56ffd78e9b6 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -265,6 +265,29 @@ isFrozen( isIndividualFrozen(view, account, mptIssue); } +bool +isDeepFrozen( + ReadView const& view, + AccountID const& account, + Currency const& currency, + AccountID const& issuer) +{ + if (isXRP(currency)) + { + return false; + } + + if (issuer == account) + { + return false; + } + + // Check if the line is deep frozen + auto const sle = view.read(keylet::line(account, issuer, currency)); + return sle && + (sle->isFlag(lsfHighDeepFreeze) || sle->isFlag(lsfLowDeepFreeze)); +} + STAmount accountHolds( ReadView const& view, @@ -282,17 +305,25 @@ accountHolds( // IOU: Return balance on trust line modulo freeze auto const sle = view.read(keylet::line(account, issuer, currency)); - if (!sle) - { - amount.clear(Issue{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) @@ -302,6 +333,11 @@ accountHolds( } amount.setIssuer(issuer); } + else + { + amount.clear(Issue{currency, issuer}); + } + JLOG(j.trace()) << "accountHolds:" << " account=" << to_string(account) << " amount=" << amount.getFullText(); @@ -857,6 +893,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. @@ -937,7 +974,11 @@ trustCreate( } if (bFreeze) { - uFlags |= (!bSetHigh ? lsfLowFreeze : lsfHighFreeze); + uFlags |= (bSetHigh ? lsfHighFreeze : lsfLowFreeze); + } + if (bDeepFreeze) + { + uFlags |= (bSetHigh ? lsfHighDeepFreeze : lsfLowDeepFreeze); } if ((slePeer->getFlags() & lsfDefaultRipple) == 0) @@ -1174,6 +1215,7 @@ rippleCreditIOU( false, noRipple, false, + false, saBalance, saReceiverLimit, 0, @@ -1660,6 +1702,7 @@ issueIOU( false, noRipple, false, + false, final_balance, limit, 0,