From fa0cc0abcd718bfc59ab1c691d71b495ba92997d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:29:32 +0000 Subject: [PATCH] Add asfDisallowIncomingTrustline check to OfferCreate and comprehensive tests Co-authored-by: mvadari <8029314+mvadari@users.noreply.github.com> --- src/test/app/Offer_test.cpp | 160 ++++++++++++++++++++++++ src/xrpld/app/tx/detail/CreateOffer.cpp | 14 +++ 2 files changed, 174 insertions(+) diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index 09d7e41141..52723f5d6f 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -4173,6 +4173,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 an offer to acquire USD/gw, an asset for which + // she does not have a trust line. The offer is created successfully. + // + // 2. gw sets asfDisallowIncomingTrustline flag. + // + // 3. bob tries to create an offer for USD/gw without a trustline. + // This should fail with tecNO_LINE. + // + // 4. bob creates a trustline to USD/gw, then creates an offer. + // The offer should succeed and cross alice's offer. + + using namespace jtx; + + // Test without fixDisallowIncomingV1 amendment + { + Env env{*this, features - fixDisallowIncomingV1}; + + auto const gw = Account("gw"); + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gwUSD = gw["USD"]; + + 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 fixDisallowIncomingV1 amendment + { + Env env{*this, features}; + + auto const gw = Account("gw"); + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gwUSD = gw["USD"]; + + env.fund(XRP(400000), gw, alice, bob, carol); + 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)); + env.require(balance(alice, gwUSD(50))); + + // GW sets DisallowIncomingTrustline flag + env(fset(gw, asfDisallowIncomingTrustline)); + env.close(); + + // Bob tries to create offer without trustline - should fail + env(offer(bob, 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))); + + // Bob shouldn't have any offers or balance + env.require(offers(bob, 0)); + env.require(balance(bob, gwUSD(none))); + + // Bob creates trustline first + env(trust(bob, gwUSD(100))); + env.close(); + + // Now bob can create an offer and it should 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 before flag is set + env(trust(carol, gwUSD(100))); + env.close(); + + // Alice creates another sell offer + env(offer(alice, XRP(1000), gwUSD(10))); + env.close(); + env.require(offers(alice, 1)); + + // Carol should be able to create offer since trustline already exists + 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(0))); + env.require(balance(carol, gwUSD(10))); + + // Test that gw can clear the flag + env(fclear(gw, asfDisallowIncomingTrustline)); + env.close(); + + // Create new account dan without trustline + auto const dan = Account("dan"); + env.fund(XRP(400000), dan); + 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)); + + // Dan should now be able to create offer without trustline (flag is cleared) + env(offer(dan, gwUSD(50), XRP(5000))); + env.close(); + + // Offer should have crossed + env.require(offers(bob, 0)); + env.require(offers(dan, 0)); + env.require(balance(dan, gwUSD(50))); + } + } + void testRCSmoketest(FeatureBitset features) { @@ -5009,6 +5168,7 @@ public: testSelfPayUnlimitedFunds(features); testRequireAuth(features); testMissingAuth(features); + testDisallowIncomingTrustline(features); testRCSmoketest(features); testSelfAuth(features); testDeletedOfferIssuer(features); diff --git a/src/xrpld/app/tx/detail/CreateOffer.cpp b/src/xrpld/app/tx/detail/CreateOffer.cpp index fab406189b..6d58e09466 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.cpp +++ b/src/xrpld/app/tx/detail/CreateOffer.cpp @@ -216,6 +216,20 @@ CreateOffer::checkAcceptAsset( // An account can always accept its own issuance. return tesSUCCESS; + // Check if the issuer has lsfDisallowIncomingTrustline set + // If so, the account must already have a trustline to receive tokens + if (view.rules().enabled(fixDisallowIncomingV1) && + ((*issuerAccount)[sfFlags] & lsfDisallowIncomingTrustline)) + { + auto const trustLine = view.read(keylet::line(id, issue.account, issue.currency)); + + if (!trustLine) + { + JLOG(j.debug()) << "delay: can't receive IOUs from issuer with DisallowIncomingTrustline set."; + return (flags & tapRETRY) ? TER{terNO_LINE} : TER{tecNO_LINE}; + } + } + if ((*issuerAccount)[sfFlags] & lsfRequireAuth) { auto const trustLine = view.read(keylet::line(id, issue.account, issue.currency));