fix: Unauthorized trust lines may only return funds to the issuer - #8150
fix: Unauthorized trust lines may only return funds to the issuer#8150ckeshava wants to merge 4 commits into
Conversation
When an issuer requires authorization (lsfRequireAuth), a trust line lacking the issuer-side auth flag may only return funds to the issuer: balance changes are grouped per issuer and currency, unauthorized gains always fail, and unauthorized spends fail whenever the group has a receiver -- the shape that distinguishes a third-party transfer from a redemption or Clawback. Pseudo-account holders are exempt, mirroring requireAuth. Enforcement is gated on fixCleanup3_4_0; without it, violations are logged only, keeping historical replay deterministic. Partially addresses XRPLF#5450; the accountHolds change is deferred. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Review feedback on the ValidTrustLineAuth invariant: authorization must predate the transaction, like the pseudo-account marker, so a buggy transactor cannot stamp the flag and credit the line at once. Also fixes the clang-tidy findings from CI. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
||
| for (auto const& [issue, group] : changes_) | ||
| { | ||
| auto const issuerSle = view.read(keylet::account(issue.account)); |
There was a problem hiding this comment.
🟡 Severity: MEDIUM
The issuer's lsfRequireAuth flag is read from the post-transaction view, unlike the trust line's own auth flags (line 802: (before ? before : after)->isFlag(...)) and pseudo-account markers (line 827), which deliberately use pre-transaction state. A buggy transactor that clears lsfRequireAuth on the issuer while simultaneously crediting an unauthorized line would cause the invariant to see "no RequireAuth" in the post-state and continue past the entire issue group, bypassing the check entirely. The accountRoots_ map already stores pre-transaction roots and should be consulted here.
Helpful? Add 👍 / 👎
💡 Fix Suggestion
Suggestion: Replace the post-transaction view.read() lookup on line 852 with a lookup that consults the accountRoots_ map first (using before ? before : after for pre-transaction state), falling back to view.read() only for unmodified issuers. This mirrors the existing isPseudoHolder pattern at lines 823-830 and the trust-line auth-flag check at line 802, ensuring that a buggy transactor that clears lsfRequireAuth in the same transaction cannot suppress the invariant.
Replace:
auto const issuerSle = view.read(keylet::account(issue.account));With an IIFE that checks accountRoots_ first:
auto const issuerSle = [&]() -> std::shared_ptr<SLE const> {
if (auto const it = accountRoots_.find(issue.account);
it != accountRoots_.end())
{
auto const& [before, after] = it->second;
return before ? before : after;
}
return view.read(keylet::account(issue.account));
}();Note: this also changes issuerKnown (line 860) to use pre-transaction state; for deleted issuers (AMM full withdrawal), the pre-state root is non-null so issuerKnown becomes true, but since AMM accounts do not set lsfRequireAuth the group is already skipped by the continue on line 854, preserving correct behavior.
⚠️ Experimental Feature: This code suggestion is automatically generated. Please review carefully.
| auto const issuerSle = view.read(keylet::account(issue.account)); | |
| // Use pre-transaction state for the issuer's lsfRequireAuth flag, | |
| // consistent with trust-line auth flags and pseudo-account markers: | |
| // a same-transaction flag clear can only be a buggy transactor. | |
| auto const issuerSle = [&]() -> std::shared_ptr<SLE const> { | |
| if (auto const it = accountRoots_.find(issue.account); | |
| it != accountRoots_.end()) | |
| { | |
| auto const& [before, after] = it->second; | |
| return before ? before : after; | |
| } | |
| return view.read(keylet::account(issue.account)); | |
| }(); |
There was a problem hiding this comment.
Good work. Two things:
- The current implementation might be over-complicated. We can simply re-use
requireAuthfrom the helper functions to check authorization. - This doesn't check for the LPToken authorization bug. Whenever we observe a change in balance of an LPToken, we must also check the auth for the two underlying assets
|
We usually don't rely on invariant checks alone as a fix, because invariant checks are meant to be a final safeguard, if one is triggered, it means something has gone fundamentally wrong. |
Under fixCleanup3_4_0: accountHolds/accountFunds gain a required AuthHandling parameter for IOUs and read unauthorized balances as zero in funding checks, and the payment engine rejects unauthorized receives and any spend past the issuer with tecNO_AUTH. Clawback, redemption, and issuer-cashed checks keep seeing the real balance. Completes the accountHolds half of XRPLF#5450. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds a new ledger invariant and supporting plumbing to ensure unauthorized trust lines (when an issuer has lsfRequireAuth but the line lacks issuer-side auth flags) cannot be used to move funds to third parties, permitting only “return-to-issuer” style flows (e.g. redemption / clawback). It also introduces an authorization-handling mode to accountHolds/accountFunds so funding-style reads can treat unauthorized balances as zero under fixCleanup3_4_0, and updates core transactors and tests accordingly.
Changes:
- Introduces
ValidTrustLineAuthinvariant to detect unauthorized trust line balance increases/spends (enforced underfixCleanup3_4_0, logged otherwise). - Extends
accountHolds/accountFundswithAuthHandlingand updates transactors (NFT offers, escrow create, check cash, clawback) to use eitherZeroIfUnauthorizedorIgnoreAuthas appropriate. - Updates payment engine direct-step checks and expands/adjusts unit tests to cover legacy vs post-amendment behavior and the new invariant backstop.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/test/ledger/View_test.cpp | Updates accountHolds/accountFunds calls to pass AuthHandling to preserve prior test semantics. |
| src/test/ledger/PaymentSandbox_test.cpp | Updates sandbox balance assertions to use AuthHandling::IgnoreAuth. |
| src/test/jtx/impl/AMM.cpp | Ensures LP token balance queries ignore auth when reading holdings. |
| src/test/app/TrustSet_test.cpp | Adds a pre/post-fixCleanup3_4_0 behavior matrix for unauthorized payments. |
| src/test/app/SetAuth_test.cpp | Parameterizes expected TER based on fixCleanup3_4_0 and expands feature matrix. |
| src/test/app/PayStrand_test.cpp | Adjusts expected unauthorized-receive result based on fixCleanup3_4_0. |
| src/test/app/NFTokenAuth_test.cpp | Expands tests to cover legacy/invariant/transactor-fix combinations and updated TER ordering. |
| src/test/app/invariants/InvariantsTrustLine_test.cpp | Adds comprehensive invariant tests for unauthorized trust line flows, DEX, checks, pseudo-accounts, and legacy states. |
| src/libxrpl/tx/transactors/token/Clawback.cpp | Uses AuthHandling::IgnoreAuth so clawback can see/drain unauthorized balances. |
| src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp | Uses ZeroIfUnauthorized for funding checks; uses IgnoreAuth for negative-balance guards. |
| src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp | Uses ZeroIfUnauthorized so unauthorized balances don’t fund IOU escrow creation. |
| src/libxrpl/tx/transactors/check/CheckCash.cpp | Uses IgnoreAuth only when issuer cashes (redemption), otherwise hides unauthorized balances. |
| src/libxrpl/tx/paths/DirectStep.cpp | Tightens unauthorized receive/spend rules under fixCleanup3_4_0, allowing only redemption-style use. |
| src/libxrpl/tx/invariants/InvariantCheck.cpp | Implements ValidTrustLineAuth tracking and enforcement logic. |
| src/libxrpl/ledger/helpers/TokenHelpers.cpp | Adds AuthHandling to IOU accountHolds and updates accountFunds to route through it. |
| src/libxrpl/ledger/helpers/NFTokenHelpers.cpp | Updates NFT offer preclaim funding checks to hide unauthorized balances. |
| include/xrpl/tx/invariants/InvariantCheck.h | Declares ValidTrustLineAuth and wires it into the invariant tuple. |
| include/xrpl/ledger/helpers/TokenHelpers.h | Updates helper signatures to include AuthHandling and removes the old accountFunds overload. |
Suppressed comments (3)
src/test/app/invariants/InvariantsTrustLine_test.cpp:362
- This precheck lambda takes
a2but never uses it; omitting the parameter name avoids unused-parameter warnings and matches other tests in this directory.
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
return setHolderBalance(a1, ac, 100);
},
src/test/app/invariants/InvariantsTrustLine_test.cpp:371
- This preclose lambda takes
a2but never uses it; omitting the parameter name avoids unused-parameter warnings and is consistent with other invariant-test setup lambdas.
[&](Account const& a1, Account const& a2, Env& env) {
env.fund(XRP(1000), g1);
env.trust(g1["USD"](10000), a1);
env.close();
return true;
});
src/test/app/invariants/InvariantsTrustLine_test.cpp:289
- This new section has multiple
ApplyContextlambdas that takea2but never use it (e.g. here). Omitting the parameter name avoids unused-parameter warnings and keeps the test consistent with other invariant tests.
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
return setHolderBalance(a1, ac, 100);
},
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
|
||
| // Preclose: g1 requires authorization and a1 opens a line that g1 | ||
| // never authorizes. No valid transaction can give it a balance. | ||
| auto const unauthorizedLine = [&](Account const& a1, Account const& a2, Env& env) { |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
High Level Overview of Change
Adds a
ValidTrustLineAuthinvariant: a trust line whose issuer requires authorization (lsfRequireAuth) but which lacks the issuer-sidelsfLowAuth/lsfHighAuthflag may only move funds back to the issuer.Partially addresses #5450 (the invariants half; the
accountHoldschange will follow separately, ideally under the same amendment).Context of Change
Unauthorized trust lines with non-zero balances exist in ledger history, created through past authorization bypasses (e.g. NFT trades before
fixEnforceNFTokenTrustlineV2). This invariant backstops every current and future transactor:requireAuth; the marker must predate the transaction, and a pseudo-account deleted together with its lines (full AMM withdrawal) stays exempt.fixCleanup3_4_0(violations becometecINVARIANT_FAILED); without the amendment violations are logged only, keeping historical replay deterministic.Type of Change
Test Plan
InvariantsTrustLinecases: unauthorized gains, third-party spends (direct and via DEX offer crossing), redemption allowed, zero-crossing payments, missing issuer root (fail closed), pseudo-account exemption including full AMM withdrawal — each documenting both pre- and post-fixCleanup3_4_0behavior.NFTokenAuthnow runs a three-configuration matrix (legacy / invariant backstop / transactor fix) so all behaviors stay pinned.🤖 Generated with Claude Code