From 0893579ba3cb8487157712de9f77ecc315807e64 Mon Sep 17 00:00:00 2001 From: Denis Angell Date: Thu, 14 May 2026 10:21:03 +0200 Subject: [PATCH] feat: blackholed token escrow Mirrors dangell7/fix-token-escrow-v2 onto the docs-infra base so the doc-review workflow runs on a realistic single-feature diff. --- .../xrpl/ledger/helpers/AccountRootHelpers.h | 9 +- include/xrpl/ledger/helpers/EscrowHelpers.h | 12 +++ include/xrpl/protocol/detail/features.macro | 1 + .../ledger/helpers/AccountRootHelpers.cpp | 36 +++++++- src/libxrpl/tx/invariants/InvariantCheck.cpp | 7 +- src/libxrpl/tx/transactors/dex/AMMCreate.cpp | 4 +- .../tx/transactors/escrow/EscrowCreate.cpp | 31 ++++++- src/test/app/AMM_test.cpp | 27 +++--- src/test/app/EscrowToken_test.cpp | 85 +++++++++++++++++++ 9 files changed, 195 insertions(+), 17 deletions(-) diff --git a/include/xrpl/ledger/helpers/AccountRootHelpers.h b/include/xrpl/ledger/helpers/AccountRootHelpers.h index 353c27fe41..c7c4bd779e 100644 --- a/include/xrpl/ledger/helpers/AccountRootHelpers.h +++ b/include/xrpl/ledger/helpers/AccountRootHelpers.h @@ -99,7 +99,14 @@ isPseudoAccount( * createPseudoAccount. */ [[nodiscard]] Expected, TER> -createPseudoAccount(ApplyView& view, uint256 const& pseudoOwnerKey, SField const& ownerField); +createPseudoAccount( + ApplyView& view, + uint256 const& pseudoOwnerKey, + SField const& ownerField, + std::uint32_t additionalFlags = 0); + +[[nodiscard]] bool +isBlackholed(ReadView const& view, std::shared_ptr const& sle); /** Checks the destination and tag. diff --git a/include/xrpl/ledger/helpers/EscrowHelpers.h b/include/xrpl/ledger/helpers/EscrowHelpers.h index 5aa5214b1f..9cac19dc10 100644 --- a/include/xrpl/ledger/helpers/EscrowHelpers.h +++ b/include/xrpl/ledger/helpers/EscrowHelpers.h @@ -41,6 +41,12 @@ escrowUnlockApplyHelper( bool createAsset, beast::Journal journal) { + if (view.rules().enabled(fixTokenEscrowV1_1)) + { + if (!sleDest || sleDest->getType() != ltACCOUNT_ROOT) + return tecINTERNAL; + } + Issue const& issue = amount.get(); Keylet const trustLineKey = keylet::line(receiver, issue); bool const recvLow = issuer > receiver; @@ -171,6 +177,12 @@ escrowUnlockApplyHelper( bool createAsset, beast::Journal journal) { + if (view.rules().enabled(fixTokenEscrowV1_1)) + { + if (!sleDest || sleDest->getType() != ltACCOUNT_ROOT) + return tecINTERNAL; + } + bool const senderIssuer = issuer == sender; bool const receiverIssuer = issuer == receiver; diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index fd62b74d59..2ea3c627a4 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -15,6 +15,7 @@ // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. +XRPL_FIX (TokenEscrowV1_1, Supported::No, VoteBehavior::DefaultNo) XRPL_FIX (Cleanup3_2_0, Supported::No, VoteBehavior::DefaultNo) XRPL_FEATURE(MPTokensV2, Supported::No, VoteBehavior::DefaultNo) XRPL_FIX (Cleanup3_1_3, Supported::Yes, VoteBehavior::DefaultYes) diff --git a/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp b/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp index 160629d835..fa63a81c44 100644 --- a/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp +++ b/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp @@ -208,8 +208,39 @@ isPseudoAccount( }) > 0; } +[[nodiscard]] bool +isBlackholed(ReadView const& view, std::shared_ptr const& sle) +{ + if (!sle || sle->getType() != ltACCOUNT_ROOT) + return false; + + if (!sle->isFlag(lsfDisableMaster)) + return false; + + if (sle->isFieldPresent(sfRegularKey)) + { + AccountID const rk = sle->getAccountID(sfRegularKey); + static AccountID const kACCOUNT_ZERO(0); + static AccountID const kACCOUNT_ONE(1); + static AccountID const kACCOUNT_TWO(2); + + if (rk != kACCOUNT_ZERO && rk != kACCOUNT_ONE && rk != kACCOUNT_TWO) + return false; + } + + AccountID const account = sle->getAccountID(sfAccount); + if (view.exists(keylet::signers(account))) + return false; + + return true; +} + Expected, TER> -createPseudoAccount(ApplyView& view, uint256 const& pseudoOwnerKey, SField const& ownerField) +createPseudoAccount( + ApplyView& view, + uint256 const& pseudoOwnerKey, + SField const& ownerField, + std::uint32_t additionalFlags) { [[maybe_unused]] auto const& fields = getPseudoAccountFields(); @@ -241,7 +272,8 @@ createPseudoAccount(ApplyView& view, uint256 const& pseudoOwnerKey, SField const // Ignore reserves requirement, disable the master key, allow default // rippling, and enable deposit authorization to prevent payments into // pseudo-account. - account->setFieldU32(sfFlags, lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth); + account->setFieldU32( + sfFlags, lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth | additionalFlags); // Link the pseudo-account with its owner object. account->setFieldH256(ownerField, pseudoOwnerKey); diff --git a/src/libxrpl/tx/invariants/InvariantCheck.cpp b/src/libxrpl/tx/invariants/InvariantCheck.cpp index 59887ad18c..ce62549612 100644 --- a/src/libxrpl/tx/invariants/InvariantCheck.cpp +++ b/src/libxrpl/tx/invariants/InvariantCheck.cpp @@ -771,8 +771,11 @@ ValidNewAccountRoot::finalize( if (pseudoAccount) { - std::uint32_t const expected = (lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth); - if (flags_ != expected) + std::uint32_t const base = (lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth); + bool valid = (flags_ == base); + if (!valid && view.rules().enabled(fixTokenEscrowV1_1)) + valid = (flags_ == (base | lsfAllowTrustLineLocking)); + if (!valid) { JLOG(j.fatal()) << "Invariant failed: pseudo-account created with " "wrong flags"; diff --git a/src/libxrpl/tx/transactors/dex/AMMCreate.cpp b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp index a2557b9abb..c4efa8811e 100644 --- a/src/libxrpl/tx/transactors/dex/AMMCreate.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp @@ -244,7 +244,9 @@ applyCreate(ApplyContext& ctx, Sandbox& sb, AccountID const& account, beast::Jou auto const ammKeylet = keylet::amm(amount.asset(), amount2.asset()); // Mitigate same account exists possibility - auto const maybeAccount = createPseudoAccount(sb, ammKeylet.key, sfAMMID); + std::uint32_t const additionalFlags = + sb.rules().enabled(fixTokenEscrowV1_1) ? lsfAllowTrustLineLocking : 0u; + auto const maybeAccount = createPseudoAccount(sb, ammKeylet.key, sfAMMID, additionalFlags); // AMM account already exists (should not happen) if (!maybeAccount) { diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp index dd9c1b84b4..9816d6d1bd 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp @@ -195,7 +195,19 @@ escrowCreatePreclaimHelper( if (!sleIssuer) return tecNO_ISSUER; if (!sleIssuer->isFlag(lsfAllowTrustLineLocking)) - return tecNO_PERMISSION; + { + if (ctx.view.rules().enabled(fixTokenEscrowV1_1)) + { + bool const isAMM = isPseudoAccount(sleIssuer, {&sfAMMID}); + bool const isBlackholedIssuer = isBlackholed(ctx.view, sleIssuer); + if (!isAMM && !isBlackholedIssuer) + return tecNO_PERMISSION; + } + else + { + return tecNO_PERMISSION; + } + } // If the account does not have a trustline to the issuer, return tecNO_LINE auto const sleRippleState = ctx.view.read(keylet::line(account, issuer, issue.currency)); @@ -469,6 +481,23 @@ EscrowCreate::doApply() auto const xferRate = transferRate(ctx_.view(), amount); if (xferRate != kPARITY_RATE) (*slep)[sfTransferRate] = xferRate.value; + + if (ctx_.view().rules().enabled(fixTokenEscrowV1_1)) + { + AccountID const issuer = amount.getIssuer(); + auto sleIssuer = ctx_.view().peek(keylet::account(issuer)); + if (sleIssuer && !sleIssuer->isFlag(lsfAllowTrustLineLocking)) + { + bool const isAMM = isPseudoAccount(sleIssuer, {&sfAMMID}); + bool const isBlackholedIssuer = isBlackholed(ctx_.view(), sleIssuer); + if (isAMM || isBlackholedIssuer) + { + sleIssuer->setFieldU32( + sfFlags, sleIssuer->getFlags() | lsfAllowTrustLineLocking); + ctx_.view().update(sleIssuer); + } + } + } } ctx_.view().insert(slep); diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index fc8b42f27f..a411529a71 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -4366,19 +4366,25 @@ private: } void - testFlags() + testFlags(FeatureBitset features) { testcase("Flags"); using namespace jtx; - testAMM([&](AMM& ammAlice, Env& env) { - auto const info = env.rpc( - "json", - "account_info", - std::string("{\"account\": \"" + to_string(ammAlice.ammAccount()) + "\"}")); - auto const flags = info[jss::result][jss::account_data][jss::Flags].asUInt(); - BEAST_EXPECT(flags == (lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth)); - }); + Env env{*this, features}; + fund(env, gw_, {alice_}, {USD(20'000)}, Fund::All); + AMM ammAlice(env, alice_, XRP(10'000), USD(10'000)); + auto const ammAccount = ammAlice.ammAccount(); + + auto const sleAMM = env.le(keylet::account(ammAccount)); + if (BEAST_EXPECT(sleAMM)) + { + BEAST_EXPECT(sleAMM->isFlag(lsfDisableMaster)); + BEAST_EXPECT(sleAMM->isFlag(lsfDefaultRipple)); + BEAST_EXPECT(sleAMM->isFlag(lsfDepositAuth)); + bool const flag = sleAMM->isFlag(lsfAllowTrustLineLocking); + BEAST_EXPECT(features[fixTokenEscrowV1_1] ? flag : !flag); + } } void @@ -7195,7 +7201,8 @@ private: testBasicPaymentEngine(all - fixAMMv1_1 - fixAMMv1_3 - fixReducedOffersV2); testAMMTokens(); testAmendment(); - testFlags(); + testFlags(all); + testFlags(all - fixTokenEscrowV1_1); testRippling(); testAMMAndCLOB(all); testAMMAndCLOB(all - fixAMMv1_1 - fixAMMv1_3); diff --git a/src/test/app/EscrowToken_test.cpp b/src/test/app/EscrowToken_test.cpp index bf7cf90bf2..dda549ba4c 100644 --- a/src/test/app/EscrowToken_test.cpp +++ b/src/test/app/EscrowToken_test.cpp @@ -1,4 +1,5 @@ +#include #include #include #include @@ -9,6 +10,8 @@ #include #include #include +#include +#include #include #include #include @@ -372,6 +375,86 @@ struct EscrowToken_test : public beast::unit_test::Suite env.close(); } + // AMM issuer without asfAllowTrustLineLocking + // (succeeds under fixTokenEscrowV1_1, fails otherwise) + { + bool const withFix = features[fixTokenEscrowV1_1]; + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account{"gateway"}; + + env.fund(XRP(30'000), alice, bob, carol, gw); + env.close(); + + auto const usd = gw["USD"]; + env.trust(usd(30'000), alice); + env.close(); + env(pay(gw, alice, usd(10'000))); + env.close(); + + AMM ammAlice(env, alice, XRP(10'000), usd(10'000)); + auto const ammAccount = ammAlice.ammAccount(); + auto const lpIssue = ammAlice.lptIssue(); + + env.trust(STAmount{lpIssue, 10'000}, carol); + env.close(); + env(pay(alice, carol, STAmount{lpIssue, 100})); + env.close(); + + env(escrow::create(carol, bob, STAmount{lpIssue, 50}), + escrow::kFINISH_TIME(env.now() + 1s), + Fee(baseFee * 150), + Ter(withFix ? TER(tesSUCCESS) : TER(tecNO_PERMISSION))); + env.close(); + + if (withFix) + { + auto const sleAMM = env.le(keylet::account(ammAccount)); + BEAST_EXPECT(sleAMM && sleAMM->isFlag(lsfAllowTrustLineLocking)); + } + } + + // Blackholed issuer without asfAllowTrustLineLocking + // (succeeds under fixTokenEscrowV1_1, fails otherwise) + { + bool const withFix = features[fixTokenEscrowV1_1]; + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account{"gateway"}; + auto const usd = gw["USD"]; + + env.fund(XRP(5000), alice, bob, gw); + env.close(); + env.trust(usd(10'000), alice, bob); + env.close(); + env(pay(gw, alice, usd(5000))); + env(pay(gw, bob, usd(5000))); + env.close(); + + Account const blackhole("blackhole", AccountID(1)); + env(regkey(gw, blackhole)); + env.close(); + env(fset(gw, asfDisableMaster), sig(gw)); + env.close(); + + env(escrow::create(alice, bob, usd(100)), + escrow::kFINISH_TIME(env.now() + 1s), + Fee(baseFee * 150), + Ter(withFix ? TER(tesSUCCESS) : TER(tecNO_PERMISSION))); + env.close(); + + if (withFix) + { + auto const sleGW = env.le(keylet::account(gw)); + BEAST_EXPECT(sleGW && sleGW->isFlag(lsfAllowTrustLineLocking)); + } + } + // tecNO_LINE: account does not have a trustline to the issuer { Env env{*this, features}; @@ -3928,8 +4011,10 @@ public: {all - featureSingleAssetVault - featureLendingProtocol, all}) { testIOUWithFeats(feats); + testIOUWithFeats(feats - fixTokenEscrowV1_1); testMPTWithFeats(feats); testMPTWithFeats(feats - fixTokenEscrowV1); + testMPTWithFeats(feats - fixTokenEscrowV1 - fixTokenEscrowV1_1); } } };