From 532506541ff521e1b1d336156d828a4ec6d676ed Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 08:17:32 -0400 Subject: [PATCH] fix: Apply asfDisallowIncomingTrustline blocker to OfferCreate (#6307) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Mayukha Vadari Co-authored-by: Mayukha Vadari --- .../tx/transactors/dex/OfferCreate.cpp | 19 ++- src/test/app/Offer_test.cpp | 160 ++++++++++++++++++ 2 files changed, 175 insertions(+), 4 deletions(-) diff --git a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp index fb47cf0f97..b95d1001e1 100644 --- a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp +++ b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp @@ -283,10 +283,23 @@ OfferCreate::checkAcceptAsset( return asset.visit( [&](Issue const& issue) -> TER { auto const& issuer = issue.getIssuer(); + auto const trustLine = view.read(keylet::trustLine(id, issuer, issue.currency)); + + // Check if the issuer has lsfDisallowIncomingTrustline set. + // If so, the account must already have a trustline to receive tokens. + if (view.rules().enabled(fixCleanup3_4_0) && + issuerAccount->isFlag(lsfDisallowIncomingTrustline)) + { + if (!trustLine) + { + JLOG(j.debug()) << "delay: can't receive IOUs from issuer with " + "DisallowIncomingTrustline set"; + return ((flags & TapRetry) != 0u) ? TER{terNO_LINE} : TER{tecNO_LINE}; + } + } + if (issuerAccount->isFlag(lsfRequireAuth)) { - auto const trustLine = view.read(keylet::trustLine(id, issuer, issue.currency)); - if (!trustLine) { return ((flags & TapRetry) != 0u) ? TER{terNO_LINE} : TER{tecNO_LINE}; @@ -309,8 +322,6 @@ OfferCreate::checkAcceptAsset( } } - auto const trustLine = view.read(keylet::trustLine(id, issue.account, issue.currency)); - if (!trustLine) { return tesSUCCESS; diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index 7fc7161e36..33721e91d8 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -4324,6 +4324,165 @@ public: env.require(Balance(bob, gwUSD(10))); } + void + testDisallowIncomingTrustline(FeatureBitset features) + { + testcase("DisallowIncomingTrustline in OfferCreate"); + + // Test that asfDisallowIncomingTrustline flag prevents offer crossing + // when the taker doesn't have a trustline. + // + // 1. alice creates a trustline and sells USD/gw tokens. + // + // 2. gw sets asfDisallowIncomingTrustline flag. + // + // 3. An account without a trustline tries to create an offer for USD/gw. + // Without amendment: succeeds and crosses alice's offer (backward compatible). + // With amendment: fails with tecNO_LINE (new behavior). + // + // 4. An account WITH an existing trustline can create an offer. + // The offer succeeds and crosses alice's offer. + // + // Note: The DisallowIncomingTrustline flag also prevents NEW trustlines + // from being created via TrustSet (enforced by fixDisallowIncomingV1). + // So accounts must create trustlines BEFORE the issuer sets the flag. + + using namespace jtx; + auto const gw = Account("gw"); + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const dan = Account("dan"); + auto const eve = Account("eve"); + auto const gwUSD = gw["USD"]; + + // Test without fixCleanup3_4_0 amendment + { + Env env{*this, features - fixCleanup3_4_0}; + + env.fund(XRP(400000), gw, alice, bob); + env.close(); + + // Alice creates trustline and gets some USD + env(trust(alice, gwUSD(100))); + env.close(); + env(pay(gw, alice, gwUSD(50))); + env.close(); + + // Alice creates sell offer + env(offer(alice, XRP(4000), gwUSD(40))); + env.close(); + env.require(offers(alice, 1)); + + // GW sets DisallowIncomingTrustline flag + env(fset(gw, asfDisallowIncomingTrustline)); + env.close(); + + // Without the amendment, bob can still create offer without trustline + // and the offer should cross (old behavior) + env(offer(bob, gwUSD(40), XRP(4000))); + env.close(); + + // Offer should have crossed + env.require(offers(alice, 0)); + env.require(offers(bob, 0)); + env.require(Balance(bob, gwUSD(40))); + } + + // Test with fixCleanup3_4_0 amendment + { + Env env{*this, features}; + + env.fund(XRP(400000), gw, alice, bob, carol, dan); + env.close(); + + // Alice creates trustline and gets some USD + env(trust(alice, gwUSD(100))); + env.close(); + env(pay(gw, alice, gwUSD(50))); + env.close(); + + // Bob and carol create trustlines BEFORE the flag is set + env(trust(bob, gwUSD(100))); + env.close(); + env(trust(carol, gwUSD(100))); + env.close(); + + // Alice creates sell offer + env(offer(alice, XRP(4000), gwUSD(40))); + env.close(); + env.require(offers(alice, 1)); + env.require(Balance(alice, gwUSD(50))); + + // GW sets DisallowIncomingTrustline flag + env(fset(gw, asfDisallowIncomingTrustline)); + env.close(); + + // Dan tries to create offer without trustline - should fail + env(offer(dan, gwUSD(40), XRP(4000)), Ter(tecNO_LINE)); + env.close(); + + // Alice's offer should still exist + env.require(offers(alice, 1)); + env.require(Balance(alice, gwUSD(50))); + + // Dan shouldn't have any offers or balance + env.require(offers(dan, 0)); + BEAST_EXPECT(env.le(keylet::trustLine(dan, gwUSD)) == nullptr); + + // Bob already has trustline, so his offer should succeed and cross + env(offer(bob, gwUSD(40), XRP(4000))); + env.close(); + + // Offer should have crossed + env.require(offers(alice, 0)); + env.require(offers(bob, 0)); + env.require(Balance(alice, gwUSD(10))); + env.require(Balance(bob, gwUSD(40))); + + // Test scenario where carol already has a trustline (created before flag was set) + // Carol should be able to create offer since trustline already exists + env(pay(gw, alice, gwUSD(50))); + env.close(); + env(offer(alice, XRP(1000), gwUSD(10))); + env.close(); + env.require(offers(alice, 1)); + + env(offer(carol, gwUSD(10), XRP(1000))); + env.close(); + + // Offer should have crossed + env.require(offers(alice, 0)); + env.require(offers(carol, 0)); + env.require(Balance(alice, gwUSD(50))); + env.require(Balance(carol, gwUSD(10))); + + // Test that gw can clear the flag + env(fclear(gw, asfDisallowIncomingTrustline)); + env.close(); + + // Create new account eve without trustline + env.fund(XRP(400000), eve); + env.close(); + + // Bob creates another sell offer + env(pay(gw, bob, gwUSD(50))); + env.close(); + env(offer(bob, XRP(5000), gwUSD(50))); + env.close(); + env.require(offers(bob, 1)); + + // Eve should now be able to create offer without trustline (flag is cleared) + env(offer(eve, gwUSD(50), XRP(5000))); + env.close(); + + // Offer should have crossed + env.require(offers(bob, 0)); + env.require(offers(eve, 0)); + env.require(Balance(eve, gwUSD(50))); + } + } + void testRCSmoketest(FeatureBitset features) { @@ -5167,6 +5326,7 @@ public: testSelfPayUnlimitedFunds(features); testRequireAuth(features); testMissingAuth(features); + testDisallowIncomingTrustline(features); testRCSmoketest(features); testSelfAuth(features); testDeletedOfferIssuer(features);