Skip to content

Sponsored Fees and Reserves for Attackathon#6842

Merged
kennyzlei merged 245 commits intoripple/attackathon-april-2026from
kennyzlei/sponsor-attackathon
Apr 10, 2026
Merged

Sponsored Fees and Reserves for Attackathon#6842
kennyzlei merged 245 commits intoripple/attackathon-april-2026from
kennyzlei/sponsor-attackathon

Conversation

@kennyzlei
Copy link
Copy Markdown
Collaborator

High Level Overview of Change

Adding Sponsored Fees and Reserves to attackathon branch, mirroring #5887 plus resolving merge conflicts

Context of Change

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
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: V14

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: V14

@shawnxie999
Copy link
Copy Markdown
Collaborator

Tests related to delegation seem to be failing. cc @yinyiqian1 @tequdev

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

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.

ℹ️ Note: This is a large diff (602,283 chars). Complex issues deep in the diff may receive less attention.

Multiple critical and high-severity bugs flagged inline: missing view().update() causing silent balance data loss in SponsorshipSet, sponsor-after-erase use-after-free in XChainBridge and MPTokenIssuanceDestroy, NFTokenAcceptOffer sponsor applied to third-party buyer, reserve bypass for drained co-signing sponsors in View.cpp, wrong lsf/asf flag namespaces in tests, and several other correctness issues.

Review by Claude Opus 4.6 · Prompt: V14

JLOG(ctx.j.debug()) << "AMM Instance: insufficient reserves";
return tecINSUF_RESERVE_LINE;
}
JLOG(ctx.j.fatal()) << "AMM Instance: insufficient reserves";
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.

Misplaced fatal log fires on the success path, not the failure path — remove it.

Suggested change
JLOG(ctx.j.fatal()) << "AMM Instance: insufficient reserves";
}

auto const trustlineExists =
view.exists(keylet::line(account_, lpIssue.account, lpIssue.currency));
auto const ownerCountAdj = trustlineExists ? 0 : 1;
if (xrpLiquid(view, sponsor.value_or(account_), sponsor ? ownerCountAdj : 0, j_) >=
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.

Reserve bug: when no sponsor, sponsor ? ownerCountAdj : 0 is always 0, skipping the trustline reserve. Use xrpLiquid with the account and ownerCountAdj for the non-sponsored case.

Suggested change
if (xrpLiquid(view, sponsor.value_or(account_), sponsor ? ownerCountAdj : 0, j_) >=
if (xrpLiquid(view, account_, ownerCountAdj, j_) >=

// transfer feeAmount to ledger entry
if (feeAmountDelta != beast::zero)
{
(*sponsorAccSle)[sfBalance] -= feeAmountDelta;
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.

Balance change to sponsorAccSle is never persisted — view().update(sponsorAccSle) is missing in the update path. The invariant checker reads stale state and won't catch this.

if (flags != (*sponsorObjSle)[sfFlags])
(*sponsorObjSle)[sfFlags] = flags;

view().update(sponsorObjSle);
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.

Missing view().update(sponsorAccSle) — balance modified at line 247 is never written to the ledger.

    view().update(sponsorObjSle);
    view().update(sponsorAccSle);

{
if (balance < reserve + STAmount(amount).xrp())
if (auto const ret = checkInsufficientReserve(
ctx_.view(), ctx_.tx, sle, balance - STAmount(amount).xrp(), {}, 1);
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.

Second reserve check ignores sponsorship ({}), rejecting valid sponsored XRP escrows. Pass sponsor to match the first check.

Suggested change
ctx_.view(), ctx_.tx, sle, balance - STAmount(amount).xrp(), {}, 1);
ctx_.view(), ctx_.tx, sle, balance - STAmount(amount).xrp(), sponsor, 1);

Account const alie{"alie", KeyType::secp256k1};
env(regkey(alice, alie));
env(fset(alice, asfDisableMaster), sig(alice));
env(fset(alice, lsfDisableMaster), sig(alice));
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.

fset() expects asf* constants; lsfDisableMaster (0x00100000) won't match any case and throws "unknown flag" at runtime.

Suggested change
env(fset(alice, lsfDisableMaster), sig(alice));
env(fset(alice, asfDisableMaster), sig(alice));

See: std::runtime_error

if (reserveCountAllowed < ownerCountDelta)
return tecINSUFFICIENT_RESERVE;

return tesSUCCESS;
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.

Co-signing + existing sponsorship entry returns tesSUCCESS after checking only sfReserveCount, without verifying the sponsor's actual XRP balance. A drained sponsor can bypass reserve requirements indefinitely.

auto const hasSponsorSignature = tx.isFieldPresent(sfSponsorSignature);

if (hasSponsorSignature)
return tesSUCCESS;
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.

Co-signed sponsorship returns tesSUCCESS before checking lsfSponsorshipRequireSignForFee / lsfSponsorshipRequireSignForReserve flags. Sponsors who set these flags get no protection against co-signed transactions.

outerSb.erase(sleClaimID);

adjustOwnerCount(outerSb, sleOwner, -1, j);
auto const sponsor = getLedgerEntryReserveSponsor(outerSb, sleClaimID);
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.

Sponsor lookup after outerSb.erase(sleClaimID)peek() on an erased entry returns nullptr, silently failing to credit freed reserve to the sponsor. Move the lookup before erase.

            auto const sponsor = getLedgerEntryReserveSponsor(outerSb, sleClaimID);
            // Remove the claim id from the ledger
            outerSb.erase(sleClaimID);

            adjustOwnerCount(outerSb, sleOwner, sponsor, -1, j);


// Invalid flags
oracle.remove({.flags = tfSellNFToken, .fee = baseFee, .err = ter(temINVALID_FLAG)});
oracle.remove({.flags = lsfSellNFToken, .fee = baseFee, .err = ter(temINVALID_FLAG)});
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.

lsfSellNFToken is a ledger flag, not a transaction flag. Both are 0x00000001 so the test passes, but the wrong namespace is misleading.

Suggested change
oracle.remove({.flags = lsfSellNFToken, .fee = baseFee, .err = ter(temINVALID_FLAG)});
oracle.remove({.flags = tfSellNFToken, .fee = baseFee, .err = ter(temINVALID_FLAG)});

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 10, 2026

Codecov Report

❌ Patch coverage is 96.70200% with 56 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (ripple/attackathon-april-2026@24241a6). Learn more about missing BASE report.

Files with missing lines Patch % Lines
...rpl/tx/transactors/Sponsor/SponsorshipTransfer.cpp 94.1% 17 Missing ⚠️
src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp 0.0% 10 Missing ⚠️
src/libxrpl/tx/transactors/vault/VaultCreate.cpp 66.7% 6 Missing ⚠️
src/libxrpl/ledger/helpers/NFTokenHelpers.cpp 89.6% 5 Missing ⚠️
src/libxrpl/tx/transactors/dex/AMMCreate.cpp 85.7% 4 Missing ⚠️
...xrpl/tx/invariants/PermissionedDomainInvariant.cpp 62.5% 3 Missing ⚠️
src/libxrpl/tx/transactors/system/Batch.cpp 70.0% 3 Missing ⚠️
src/xrpld/rpc/handlers/account/AccountObjects.cpp 94.9% 2 Missing ⚠️
src/libxrpl/ledger/helpers/MPTokenHelpers.cpp 95.0% 1 Missing ⚠️
src/libxrpl/tx/Transactor.cpp 99.0% 1 Missing ⚠️
... and 4 more
Additional details and impacted files

Impacted file tree graph

@@                       Coverage Diff                       @@
##             ripple/attackathon-april-2026   #6842   +/-   ##
===============================================================
  Coverage                                 ?   82.1%           
===============================================================
  Files                                    ?    1036           
  Lines                                    ?   78676           
  Branches                                 ?    7661           
===============================================================
  Hits                                     ?   64595           
  Misses                                   ?   14081           
  Partials                                 ?       0           
Files with missing lines Coverage Δ
include/xrpl/ledger/View.h 100.0% <ø> (ø)
include/xrpl/ledger/helpers/AccountRootHelpers.h 100.0% <ø> (ø)
include/xrpl/ledger/helpers/EscrowHelpers.h 98.9% <100.0%> (ø)
include/xrpl/ledger/helpers/NFTokenHelpers.h 100.0% <ø> (ø)
include/xrpl/ledger/helpers/RippleStateHelpers.h 100.0% <ø> (ø)
include/xrpl/ledger/helpers/SponsorHelpers.h 100.0% <100.0%> (ø)
include/xrpl/protocol/Fees.h 100.0% <100.0%> (ø)
include/xrpl/protocol/Indexes.h 100.0% <ø> (ø)
include/xrpl/protocol/LedgerFormats.h 100.0% <ø> (ø)
include/xrpl/protocol/TER.h 100.0% <ø> (ø)
... and 94 more

Impacted file tree graph

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

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.

ℹ️ Note: This is a large diff (602,790 chars). Complex issues deep in the diff may receive less attention.

Multiple high/critical bugs flagged inline: auth bypass in checkSponsor, use-after-erase in XChainBridge and MPTokenIssuanceDestroy, UNFUNDED check bypass in PaymentChannelCreate, unpersisted balance in SponsorshipSet, missing isXRP() guard in InvariantCheck, wrong sponsor in brokered NFT trades, stale sponsor on Oracle reserve-count change, and several medium-severity issues. See inline comments.

Review by Claude Opus 4.6 · Prompt: V14


// Invalid flags
oracle.remove({.flags = tfSellNFToken, .fee = baseFee, .err = ter(temINVALID_FLAG)});
oracle.remove({.flags = lsfSellNFToken, .fee = baseFee, .err = ter(temINVALID_FLAG)});
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.

Wrong flag type — lsfSellNFToken is a ledger state flag; use the transaction flag tfSellNFToken:

Suggested change
oracle.remove({.flags = lsfSellNFToken, .fee = baseFee, .err = ter(temINVALID_FLAG)});
oracle.remove({.flags = tfSellNFToken, .fee = baseFee, .err = ter(temINVALID_FLAG)});

Account const alie{"alie", KeyType::secp256k1};
env(regkey(alice, alie));
env(fset(alice, asfDisableMaster), sig(alice));
env(fset(alice, lsfDisableMaster), sig(alice));
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.

fset() sets sfSetFlag which expects asf* values — lsfDisableMaster is a bitmask (0x00100000), not the AccountSet flag value. This won't disable the master key:

Suggested change
env(fset(alice, lsfDisableMaster), sig(alice));
env(fset(alice, asfDisableMaster), sig(alice));

if (!objSle)
return tefINTERNAL; // LCOV_EXCL_LINE

auto const ownerAccountID = getLedgerEntryOwner(view(), objSle, account_);
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.

Bug: account_ should be sponseeAccountID. When sfSponsee is set (allowed for tfSponsorshipEnd), these differ, causing tefINTERNAL for ltSIGNER_LIST and ltRIPPLE_STATE:

Suggested change
auto const ownerAccountID = getLedgerEntryOwner(view(), objSle, account_);
auto const ownerAccountID = getLedgerEntryOwner(view(), objSle, sponseeAccountID);

outerSb.erase(sleClaimID);

adjustOwnerCount(outerSb, sleOwner, -1, j);
auto const sponsor = getLedgerEntryReserveSponsor(outerSb, sleClaimID);
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.

Use-after-erase: getLedgerEntryReserveSponsor is called on sleClaimID after it was erased on line 708. Every other call site retrieves the sponsor before erasing. Move this line above the erase call.

drops_ -= (*before)[sfAmount].xrp().drops();
break;
case ltSPONSORSHIP:
if (before->isFieldPresent(sfFeeAmount))
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.

Missing isXRP() guard before calling .xrp() on sfFeeAmount — compare the sfAmount pattern at lines 117-118. If sfFeeAmount ever holds an IOU amount, this will assert or throw.

auto const trustlineExists =
view.exists(keylet::line(account_, lpIssue.account, lpIssue.currency));
auto const ownerCountAdj = trustlineExists ? 0 : 1;
if (xrpLiquid(view, sponsor.value_or(account_), sponsor ? ownerCountAdj : 0, j_) >=
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.

Reserve check regression: when there's no sponsor the third arg becomes 0 instead of ownerCountAdj, skipping the trustline reserve check for the account:

Suggested change
if (xrpLiquid(view, sponsor.value_or(account_), sponsor ? ownerCountAdj : 0, j_) >=
if (xrpLiquid(view, sponsor.value_or(account_), ownerCountAdj, j_) >=

// then the ledger is corrupted. Rather than make things worse we
// reject the transaction.
payerSle->setFieldAmount(sfBalance, balance - fee);
payerSle->setFieldAmount(payer.balanceField, balance - fee);
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.

reset() sets sfFeeAmount to zero rather than making it absent. payFee() explicitly calls makeFieldAbsent when sfFeeAmount reaches zero (since it's soeOptional), but reset() doesn't — creating inconsistent state for callers that check isFieldPresent(sfFeeAmount).


auto const hasSponsorSignature = tx.isFieldPresent(sfSponsorSignature);

if (hasSponsorSignature)
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.

Auth bypass: checkSponsor returns tesSUCCESS unconditionally when sfSponsorSignature is present, before the sponsorship-object flag checks. A co-signed tx passes regardless of lsfSponsorshipRequireSignForFee/lsfSponsorshipRequireSignForReserve.

auto const adjust = newCount - oldCount;
if (adjust != 0 && !adjustOwnerCount(ctx_, adjust))
auto const newCount = calculateOracleReserve(pairs.size());
int32_t const adjust = newCount - oldCount;
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.

When adjust == 0 but the sponsor changed, neither branch executes and the sponsor on the ledger entry is never updated. preclaim handles sponsor changes by setting adjustReserve = newCount when sponsors differ, but doApply computes adjust = newCount - oldCount without that logic.


if (maxFee)
{
if (*maxFee == XRPAmount(0))
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.

In the Update path, (*sponsorAccSle)[sfBalance] -= feeAmountDelta modifies the SLE but view().update(sponsorAccSle) is never called — only view().update(sponsorObjSle) is. The sponsor's XRP balance change is silently dropped. Add view().update(sponsorAccSle) alongside the existing view().update(sponsorObjSle) at line 289.

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.

ℹ️ Note: This is a large diff (606,982 chars). Complex issues deep in the diff may receive less attention.

Multiple correctness and security issues flagged inline — highlights include: auth bypass in checkSponsor (co-sign path skips flag validation), critical reserve check regression in AMMDeposit, PaymentChannelCreate/EscrowCreate using opposite incorrect patterns for the two-check checkInsufficientReserve contract, missing isXRP() guards in the invariant checker, wrong flag constants in tests, stray FATAL log on the success path, missing view().update() in SponsorshipSet, and getLedgerEntryReserveSponsor called after erase in two transactors.

Review by Claude Opus 4.6 · Prompt: V14

view().erase(mpt);

adjustOwnerCount(view(), view().peek(keylet::account(account_)), -1, j_);
auto const sponsor = getLedgerEntryReserveSponsor(view(), mpt);
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.

getLedgerEntryReserveSponsor called after view().erase(mpt) — read sponsor before erasing:

    auto const sponsor = getLedgerEntryReserveSponsor(view(), mpt);
    view().erase(mpt);

    auto const sponsor = getLedgerEntryReserveSponsor(view(), mpt);

Replace lines 47-49 — move the sponsor read before the erase call to match all other call sites in this PR.

view().erase(mpt);

adjustOwnerCount(view(), view().peek(keylet::account(account_)), -1, j_);
auto const sponsor = getLedgerEntryReserveSponsor(view(), mpt);
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.

getLedgerEntryReserveSponsor is called after view().erase(mpt). Every other call site reads the sponsor before erasing. Move the sponsor read before the erase to be consistent and safe.

Suggested fix: move line 49 above line 47 so the result is:

    auto const sponsor = getLedgerEntryReserveSponsor(view(), mpt);
    view().erase(mpt);
    adjustOwnerCount(...);

if (sle)
{
auto const reserveCountAllowed = sle->getFieldU32(sfReserveCount);
if (reserveCountAllowed < ownerCountDelta)
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.

Signed/unsigned comparison: reserveCountAllowed is uint32_t, ownerCountDelta is int32_t. A negative delta promotes to ~4B unsigned, making this check almost always true and returning spurious tecINSUFFICIENT_RESERVE. Same issue at line ~357.

Suggested change
if (reserveCountAllowed < ownerCountDelta)
if (static_cast<std::int64_t>(reserveCountAllowed) < ownerCountDelta)

STAmount const sponsorReserve{view.fees().accountReserve(
sponsorSle->getFieldU32(sfOwnerCount),
sponsorSle->getFieldU32(sfSponsoredOwnerCount),
sponsorSle->getFieldU32(sfSponsoringOwnerCount) + ownerCountDelta,
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.

Unsigned wrap: getFieldU32() + negative int32_t ownerCountDelta promotes to uint32_t, wrapping to ~4B on underflow. This produces an astronomically wrong reserve value. Same pattern on line 345 with accountCountDelta, and on lines ~364/368 in the unsponsored path. Cast to a wider signed type before arithmetic.

JLOG(ctx.j.debug()) << "AMM Instance: insufficient reserves";
return tecINSUF_RESERVE_LINE;
}
JLOG(ctx.j.fatal()) << "AMM Instance: insufficient reserves";
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.

Stray fatal log on the success path — logs "insufficient reserves" at FATAL for every valid non-sponsored IOU deposit by a new LP. Remove this line.

Suggested change
JLOG(ctx.j.fatal()) << "AMM Instance: insufficient reserves";

return ret;

if (auto const ret = checkInsufficientReserve(
ctx.view, ctx.tx, sle, balance - ctx.tx[sfAmount], sponsor, 1);
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.

Second checkInsufficientReserve passes sponsor instead of {}. When a sponsor exists, only the sponsor's reserve is checked and the account's own balance after deduction is never validated. Compare EscrowCreate which correctly passes {} on the second check.

        if (auto const ret = checkInsufficientReserve(
                ctx.view, ctx.tx, sle, balance - ctx.tx[sfAmount], {}, 1);

{
if (balance < reserve + STAmount(amount).xrp())
if (auto const ret = checkInsufficientReserve(
ctx_.view(), ctx_.tx, sle, balance - STAmount(amount).xrp(), {}, 1);
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.

Opposite error to PaymentChannelCreate: here the second check passes {} (no sponsor) when a sponsor is present, over-restricting and causing spurious tecUNFUNDED. Together these two files reveal a systematic misunderstanding of the two-check checkInsufficientReserve API contract — one under-restricts (PaymentChannel), one over-restricts (Escrow). The correct pattern should be agreed upon and applied consistently.

auto const trustlineExists =
view.exists(keylet::line(account_, lpIssue.account, lpIssue.currency));
auto const ownerCountAdj = trustlineExists ? 0 : 1;
if (xrpLiquid(view, sponsor.value_or(account_), sponsor ? ownerCountAdj : 0, j_) >=
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.

Reserve check regression: sponsor ? ownerCountAdj : 0 passes 0 when there is no sponsor, but the correct value is ownerCountAdj (1 when the trustline doesn't exist). Non-sponsored XRP deposits skip the new-trustline reserve, allowing under-reserved deposits.

            if (xrpLiquid(view, sponsor.value_or(account_), ownerCountAdj, j_) >=
                depositAmount)


auto const setSponsorFieldU32 = [](auto const& sle, auto const& field, auto const& delta) {
int32_t const newValue = static_cast<int32_t>(sle->getFieldU32(field)) + delta;
if (newValue <= 0)
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.

newValue <= 0 silently masks underflow — a negative count (state corruption) is treated the same as zero and cleared without error. adjustReserveCount in this same file correctly returns tefINTERNAL for negative results. Add an assert or explicit negative check.

    auto const setSponsorFieldU32 = [](auto const& sle, auto const& field, int32_t const delta) {
        int32_t const newValue = static_cast<int32_t>(sle->getFieldU32(field)) + delta;
        XRPL_ASSERT(newValue >= 0, "setSponsorFieldU32 : result must not be negative");
        if (newValue == 0)

!typeMatchesFilter(typeFilter.value(), sleNode->getType()))
canAppend = false;

auto const getSponsor = [&sleNode]() -> std::optional<AccountID> {
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.

getSponsor doesn't check which side of the RippleState the requesting account is on. It returns sfHighSponsor first regardless, so accounts on the LOW side will see the counterparty's sponsor, producing false positives in the sponsored filter. Capture account and select the correct sponsor field based on high/low side.

@kennyzlei kennyzlei merged commit 2570c7d into ripple/attackathon-april-2026 Apr 10, 2026
33 checks passed
@kennyzlei kennyzlei deleted the kennyzlei/sponsor-attackathon branch April 10, 2026 20:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Attackathon Used to label PRs created for Attackathon branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants