Add asfDisallowIncomingTrustline check to OfferCreate and comprehensive tests

This commit is contained in:
copilot-swe-agent[bot]
2026-01-30 16:29:32 +00:00
committed by Mayukha Vadari
parent 3122de86bf
commit c59b26078d
3 changed files with 181 additions and 4 deletions

View File

@@ -15,6 +15,7 @@
// Add new amendments to the top of this list.
// Keep it sorted in reverse chronological order.
XRPL_FIX (Cleanup3_4_0, Supported::No, VoteBehavior::DefaultNo)
XRPL_FEATURE(Sponsor, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(BatchV1_1, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(LendingProtocolV1_1, Supported::No, VoteBehavior::DefaultNo)

View File

@@ -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;

View File

@@ -4324,6 +4324,170 @@ 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;
// Test without fixCleanup3_4_0 amendment
{
Env env{*this, features - fixCleanup3_4_0};
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 fixCleanup3_4_0 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 dan = Account("dan");
auto const gwUSD = gw["USD"];
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
auto const eve = Account("eve");
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 +5331,7 @@ public:
testSelfPayUnlimitedFunds(features);
testRequireAuth(features);
testMissingAuth(features);
testDisallowIncomingTrustline(features);
testRCSmoketest(features);
testSelfAuth(features);
testDeletedOfferIssuer(features);