Skip to content

feat: PaymentChannelClawback - #7936

Open
dangell7 wants to merge 1 commit into
token-paychanfrom
dangell7/token-paychan-clawback
Open

feat: PaymentChannelClawback#7936
dangell7 wants to merge 1 commit into
token-paychanfrom
dangell7/token-paychan-clawback

Conversation

@dangell7

@dangell7 dangell7 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

High Level Overview of Change

Adds PaymentChannelClawback, letting a token issuer claw back IOUs or MPTs locked in an active payment channel. Stacked on #7935 (Token PayChan); it merges into that branch, so this diff is clawback-only.

Split out of the main PR for isolated review. It is part of the same featureTokenPaychan amendment (not a separate one), so it must merge before that amendment activates.

Context of Change

XLS-93d §3.6 deferred clawback with a "close then claw" workaround that does not actually work: the issuer is not a party to a channel and cannot close it, and normal Clawback clamps to accountHolds() which excludes locked funds. This reaches the unclaimed remainder (sfAmount - sfBalance) directly. MPT decrements the holder/issuance sfLockedAmount and issuance sfOutstandingAmount (a redemption, fee waived); IOU retires the channel obligation since the value already sits with the issuer. It reuses the existing clawback opt-in (lsfAllowTrustLineClawback / lsfMPTCanClawback), so it grants issuers no new power, only removes a hiding place.

Also adds a ValidPaymentChannel invariant (gated on featureTokenPaychan) enforcing sfAmount >= sfBalance, asset consistency, and non-decreasing balance across all channel operations.

API Impact

  • Public API: New feature (new PaymentChannelClawback transaction)
  • libxrpl change

Test Plan

testIOU/MPTChannelClawback in PayChanToken_test: full/partial claw, channel deletion and directory cleanup, MPT three-field decrement, subsequent-claim clamp (tecUNFUNDED_PAYMENT), over-claw clamp, wrong-asset (tecWRONG_ASSET), expired-channel claw, transfer-fee waive, and delegation. Full escrow/paychan/MPT/Invariants suites pass.

@dangell7
dangell7 force-pushed the dangell7/token-paychan-clawback branch 2 times, most recently from dd3c590 to 787571d Compare August 3, 2026 16:12
@dangell7
dangell7 force-pushed the dangell7/token-paychan-clawback branch from 787571d to 1e5e8ef Compare August 3, 2026 16:29
@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.59829% with 11 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...sactors/payment_channel/PaymentChannelClawback.cpp 90.2% 6 Missing ⚠️
src/libxrpl/tx/invariants/InvariantCheck.cpp 76.2% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

: TransactionBase(std::move(tx))
{
// Verify transaction type
if (tx_->getTxnType() != txType)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't see anywhere that enforces tx_ to be non null. We might want an assert or a null check.

*/
PaymentChannelClawbackBuilder(std::shared_ptr<STTx const> tx)
{
if (tx->getTxnType() != ttPAYCHAN_CLAWBACK)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same thing here, nothing is enforcing tx to be non null. Maybe need an assert or a null check.

{
throw std::runtime_error("Invalid transaction type for PaymentChannelClawbackBuilder");
}
object_ = *tx;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could probably get around the null check if the constructor parameter was a reference to an STTx instead of a pointer to one.


public:
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);

@TimothyBanks TimothyBanks Aug 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: Can these parameters have names? Or maybe there is a reason they are omitted?

visitEntry(bool, SLE::const_ref, SLE::const_ref);

[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: Can these parameters have names?

public:
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal;

explicit PaymentChannelClawback(ApplyContext& ctx) : Transactor(ctx)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not for this PR, but I was curious if there is a reason to not use uniform initialization?

beast::Journal const& j) const
{
// Token channels only exist once the amendment is active.
if (!view.rules().enabled(featureTokenPaychan))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should this check also be in the visitEntry function? Do we need to perform the computation up there based on this amendment? This is most likely just a misunderstanding on my part.

return ret; // LCOV_EXCL_LINE
}

(*slep)[sfAmount] = newAmount;

@TimothyBanks TimothyBanks Aug 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: Would std::move(newAmount) be useful here? The Asset type is using a std::variant.

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.

2 participants