Compare commits

...

7 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
a45546c775 Fix test: create trustlines before DisallowIncomingTrustline flag is set
Co-authored-by: mvadari <8029314+mvadari@users.noreply.github.com>
2026-01-30 19:24:44 +00:00
Mayukha Vadari
fb8fd2d9e1 fix pre-commit 2026-01-30 12:24:45 -05:00
copilot-swe-agent[bot]
167d66bb3f Rename amendment from fixDisallowIncomingV2 to fixDisallowIncomingV1_1
Co-authored-by: mvadari <8029314+mvadari@users.noreply.github.com>
2026-01-30 17:18:52 +00:00
copilot-swe-agent[bot]
f6bdf961e4 Create new amendment fixDisallowIncomingV2 for OfferCreate blocker
Co-authored-by: mvadari <8029314+mvadari@users.noreply.github.com>
2026-01-30 16:55:59 +00:00
copilot-swe-agent[bot]
2a61f2dbd3 Optimize trustline reading in checkAcceptAsset and improve test documentation
- Move trustline read to common location before both DisallowIncomingV1 and RequireAuth checks to avoid duplicate reads when both flags are set
- Update test comments to accurately reflect what's being tested
- Clarify carol test scenario to explain it tests trustline creation after flag is set
2026-01-30 16:36:52 +00:00
copilot-swe-agent[bot]
32fd267240 Add asfDisallowIncomingTrustline check to OfferCreate and comprehensive tests
Co-authored-by: mvadari <8029314+mvadari@users.noreply.github.com>
2026-01-30 16:29:32 +00:00
copilot-swe-agent[bot]
8765b8cc6d Initial plan 2026-01-30 16:25:57 +00:00
3 changed files with 180 additions and 4 deletions

View File

@@ -16,6 +16,7 @@
// Add new amendments to the top of this list.
// Keep it sorted in reverse chronological order.
XRPL_FIX (DisallowIncomingV1_1, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (BatchInnerSigs, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(LendingProtocol, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(PermissionDelegationV1_1, Supported::no, VoteBehavior::DefaultNo)

View File

@@ -4173,6 +4173,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 fixDisallowIncomingV1_1 amendment
{
Env env{*this, features - fixDisallowIncomingV1_1};
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_1 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));
env.require(balance(dan, gwUSD(none)));
// 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)
{
@@ -5009,6 +5173,7 @@ public:
testSelfPayUnlimitedFunds(features);
testRequireAuth(features);
testMissingAuth(features);
testDisallowIncomingTrustline(features);
testRCSmoketest(features);
testSelfAuth(features);
testDeletedOfferIssuer(features);

View File

@@ -216,10 +216,22 @@ CreateOffer::checkAcceptAsset(
// An account can always accept its own issuance.
return tesSUCCESS;
// Read the trustline once for multiple flag checks to optimize performance
auto const trustLine = view.read(keylet::line(id, issue.account, 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(fixDisallowIncomingV1_1) && ((*issuerAccount)[sfFlags] & lsfDisallowIncomingTrustline))
{
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));
if (!trustLine)
{
return (flags & tapRETRY) ? TER{terNO_LINE} : TER{tecNO_LINE};
@@ -248,8 +260,6 @@ CreateOffer::checkAcceptAsset(
return tesSUCCESS;
}
auto const trustLine = view.read(keylet::line(id, issue.account, issue.currency));
if (!trustLine)
{
return tesSUCCESS;