Skip to content

fix: PermissionedDEX (CreateOffer/Payment) never deletes expired credentials#6827

Open
Kassaking7 wants to merge 6 commits intoXRPLF:developfrom
Kassaking7:DEFI-625
Open

fix: PermissionedDEX (CreateOffer/Payment) never deletes expired credentials#6827
Kassaking7 wants to merge 6 commits intoXRPLF:developfrom
Kassaking7:DEFI-625

Conversation

@Kassaking7
Copy link
Copy Markdown
Collaborator

High Level Overview of Change

The PermissionedDEX code paths (CreateOffer and Payment) check credential validity via accountInDomain() in PermissionedDEXHelpers.cpp, but this function uses a ReadView — making it architecturally impossible to delete expired credential SLEs from the ledger. Expired credentials persist indefinitely, locking owner reserves and causing permanent ledger bloat.

Root Cause

The XRPL uses a two-phase cleanup pattern for expired credentials:

  1. preclaim() (ReadView): identifies expired credentials, suppresses tecEXPIRED to allow doApply() to run

  2. doApply() (ApplyView): calls verifyValidDomain() → removeExpired() → deleteSLE() to actually delete the expired credential from the ledger

This pattern is correctly implemented in VaultDeposit (via enforceMPTokenAuthorization → verifyValidDomain → removeExpired). However, the PermissionedDEX paths skip this entirely:

  • accountInDomain() (PermissionedDEXHelpers.cpp:28-55) takes a ReadView const& parameter. It calls credentials::checkExpired() to skip expired credentials, but cannot call deleteSLE() because ReadView is immutable.

  • Neither CreateOffer::doApply() nor Payment::doApply() have any call to verifyValidDomain() or removeExpired() for the PermissionedDEX credential check path.

Context of Change

Add a verifyValidDomain() call in the doApply() of both CreateOffer and Payment when a DomainID is present. This mirrors how VaultDeposit handles it. The pattern is:

  1. In preclaim: accountInDomain(ReadView) checks validity, suppresses tecEXPIRED
  2. In doApply: call verifyValidDomain(ApplyView) which triggers removeExpired → deleteSLE for any expired credentials encountered

API Impact

  • Public API: New feature (new methods and/or new fields)
  • Public API: Breaking change (in general, breaking changes should only impact the next api_version)
  • libxrpl change (any change that may affect libxrpl or dependents of libxrpl)
  • Peer protocol change (must be backward compatible or bump the peer protocol version)

Copy link
Copy Markdown
Collaborator

@mvadari mvadari left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be amendment gated

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 9, 2026

Codecov Report

❌ Patch coverage is 94.64286% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.1%. Comparing base (d050073) to head (770fb96).

Files with missing lines Patch % Lines
src/libxrpl/tx/transactors/payment/Payment.cpp 94.4% 2 Missing ⚠️
src/libxrpl/tx/transactors/dex/OfferCreate.cpp 95.0% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff            @@
##           develop   #6827     +/-   ##
=========================================
- Coverage     82.1%   82.1%   -0.0%     
=========================================
  Files         1010    1010             
  Lines        76033   76083     +50     
  Branches      7369    7373      +4     
=========================================
+ Hits         62418   62447     +29     
- Misses       13615   13636     +21     
Files with missing lines Coverage Δ
src/libxrpl/tx/transactors/dex/OfferCreate.cpp 93.4% <95.0%> (+<0.1%) ⬆️
src/libxrpl/tx/transactors/payment/Payment.cpp 94.7% <94.4%> (-0.1%) ⬇️

... and 6 files with indirect coverage changes

Impacted file tree graph

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions
Copy link
Copy Markdown

This PR has conflicts, please resolve them in order for the PR to be reviewed.

@github-actions
Copy link
Copy Markdown

All conflicts have been resolved. Assigned reviewers can now start or resume their review.

@Kassaking7
Copy link
Copy Markdown
Collaborator Author

Kassaking7 commented Apr 29, 2026

/ai-review

Copy link
Copy Markdown
Contributor

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues.

Review by Claude Opus 4.6 · Prompt: V15

@Kassaking7 Kassaking7 requested a review from shawnxie999 May 1, 2026 17:42
@Kassaking7
Copy link
Copy Markdown
Collaborator Author

/ai-review

Copy link
Copy Markdown
Collaborator

@PeterChen13579 PeterChen13579 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if

return verifyValidDomain(ctx_.view(), acct, domainID, j_);
};

if (auto const err = cleanupFor(account_); !isTesSuccess(err))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for here, if sender's credential is expired, it deletes only the sender’s credential and returns before checking destination. It would be good to cleanup for both sender and destination in one go

{
if (!permissioned_dex::accountInDomain(ctx.view, id, ctx.tx[sfDomainID]))
return tecNO_PERMISSION;
if (ctx.view.rules().enabled(fixSecurity3_1_3))
Copy link
Copy Markdown
Collaborator

@PeterChen13579 PeterChen13579 May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How to we usually name these? Can it be better names 😄

Copy link
Copy Markdown
Collaborator

@shawnxie999 shawnxie999 May 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the right name - it's what they came up for the series of bug fixes. This is to group the numerous bug fixes into one amendment

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 3, 2026

This PR has conflicts, please resolve them in order for the PR to be reviewed.

{
if (!permissioned_dex::accountInDomain(ctx.view, id, ctx.tx[sfDomainID]))
return tecNO_PERMISSION;
if (ctx.view.rules().enabled(fixSecurity3_1_3))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure that this PR is going into fixSecurity3_1_3? not 3.2? Hasn't 3.1.3 already made a cut off?

auto const domainID = ctx_.tx[sfDomainID];
auto const sleDomain = ctx_.view().read(keylet::permissionedDomain(domainID));
if (!sleDomain)
return tecNO_PERMISSION;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should return tecINTERNAL here and exclude line coverage // LCOV_EXCL_LINE, the existence of domain was already checked in preclaim but we still need to check here for nullptr, but it should never be reachable

@godexsoft
Copy link
Copy Markdown
Contributor

We recently merged a refactor to develop that enables clang-tidy's readability-identifier-naming. Your branch now has heavy conflicts that are largely mechanical. Below is a workflow that aligns your branch's naming with develop before merging, which should minimize the merge conflicts.

One-time setup

If you don't already have clang-tidy working in your env, on macOS:

brew install llvm@21
# Follow brew's hint to put $(brew --prefix llvm@21)/bin on PATH so run-clang-tidy is found.

Workflow on your branch (before merging develop)

1. Grab the new .clang-tidy from develop without pulling anything else. Sync your fork on GitHub first, then:

git remote -v   # should show 'upstream' among others; if not:
# git remote set-url upstream git@github.com:XRPLF/rippled.git
git fetch upstream
git checkout upstream/develop -- .clang-tidy

2. Reconfigure conan/cmake so compile_commands.json is fresh.

3. Apply renames for the files modified in your PR:

git diff --name-only $(git merge-base HEAD upstream/develop) HEAD \
  | grep -E '\.(cpp|h|hpp|ipp)$' \
  | xargs run-clang-tidy -p build -fix -allow-no-checks
# or -p .build, or whatever your build dir is called

4. Build + test, then commit as a single dedicated commit:

cmake --build build -j8
git commit -am "refactor: Align identifier naming with develop"

5. Now merge develop:

git merge upstream/develop

Extra

Run clang-tidy once more after the merge to catch any stragglers introduced from develop's side:

run-clang-tidy -p build -fix -allow-no-checks src tests
# or -p .build, or whatever your build dir is called

Comment thread src/test/app/PermissionedDEX_test.cpp Outdated
Comment on lines +259 to +262
// devin cannot create offer with expired cred
env(offer(devin, XRP(10), USD(10)), domain(domainID), ter(tecNO_PERMISSION));
// devin cannot create offer with expired cred; the expired credential
// SLE is deleted by doApply and tecEXPIRED is returned
env(offer(devin, XRP(10), USD(10)), domain(domainID), ter(tecEXPIRED));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should not change the result of an existing test, behavior should not change without a amendment gate. Make sure you have a behavior that switches when amendment is on/off

Comment on lines +1496 to +1500
// doApply now runs, deletes the expired SLE, returns tecEXPIRED
env(offer(devin, XRP(10), USD(10)), domain(domainID), ter(tecEXPIRED));
env.close();

BEAST_EXPECT(!env.le(credKey)); // credential was deleted
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please have a single test setup for the same scenario and then switch the behavior with the amendment on/off. And call the test with

testExpiredCredentialCleanup(all);
testExpiredCredentialCleanup(all - amednemtn);

This would be much easier to maintain and read.

Comment on lines +1468 to +1469
// Scenario 1: OfferCreate with an expired credential deletes the
// credential SLE from the ledger and returns tecEXPIRED
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't have a pre-fix test for this scenario. Even if it's tested elsewhere, it'd be good to have it for maintainability and readability - it should be easy to do by gating the test behavior with the amendment. see the comment above

domain(domainID),
ter(tecNO_PERMISSION));
env.close();
BEAST_EXPECT(env.le(credKey)); // credential was NOT deleted
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor - would be good to that the bob's offer is still alive and bob's cred is alive

Comment on lines +1428 to +1447
Account const devin("devin");
env.fund(XRP(1000), devin);
env.close();
env.trust(USD(1000), devin);
env.close();
env(pay(gw, devin, USD(100)));
env.close();

auto jv = credentials::create(devin, domainOwner, credType);
uint32_t const t = env.current()->header().parentCloseTime.time_since_epoch().count();
jv[sfExpiration.jsonName] = t + 20;
env(jv);
env(credentials::accept(devin, domainOwner, credType));
env.close();

auto const credKey =
keylet::credential(devin.id(), domainOwner.id(), makeSlice(credType));
BEAST_EXPECT(env.le(credKey));

env.close(std::chrono::seconds(20));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seemed to be shared code between the new tests, can be refactored into a lambda

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 4, 2026

All conflicts have been resolved. Assigned reviewers can now start or resume their review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants