DEV Community

ohmygod
ohmygod

Posted on

The XRPL Batch Amendment Near-Miss: How a Loop Exit Bug Almost Let Attackers Drain Any Wallet Without a Private Key

A single premature loop exit in the XRP Ledger's proposed Batch amendment (XLS-56) would have allowed attackers to execute transactions from any account on the network — without possessing the private key. Discovered by researcher Pranamya Keshkamat and Cantina AI's Apex tool on February 19, 2026, this critical signature-validation flaw was patched before mainnet activation. But the vulnerability pattern — batch authorization bypass via early loop termination — applies far beyond XRPL.

If you build or audit anything that processes grouped transactions (ERC-4337 bundlers, Solana versioned transactions, CosmWasm batch executions), this is required reading.


The Batch Amendment: What It Was Supposed to Do

XLS-56 introduced atomic batch transactions to the XRPL — group multiple "inner" transactions into a single outer transaction, signed by designated "batch signers." The inner transactions skip individual signature verification; the outer batch signature authorizes the entire group.

The design goal was gas efficiency and UX improvement: one signature to authorize a complex multi-step operation (swap + bridge + stake).

┌─────────────────────────────────┐
│ Batch Transaction (outer)       │
│ Signer: Alice's key             │
│                                 │
│  ├─ Inner Tx 1: Payment 100 XRP│
│  ├─ Inner Tx 2: TrustSet       │
│  └─ Inner Tx 3: OfferCreate    │
│                                 │
│ All execute atomically          │
└─────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

This is conceptually identical to ERC-4337's UserOperation bundling and Solana's instruction batching within a single transaction.


The Bug: Premature Loop Exit on Non-Existent Account Match

The signature validation function iterated over all declared batch signers to verify their authorization. Here's the pseudocode of the vulnerable path:

// VULNERABLE: rippled batch signer validation (simplified)
bool validateBatchSigners(BatchTx const& batch) {
    for (auto const& signer : batch.outerSigners) {
        auto const accountInfo = ledger.lookup(signer.account);

        if (!accountInfo) {
            // Account doesn't exist on ledger yet
            if (signer.signingKey == deriveKey(signer.account)) {
                // Key matches the non-existent account
                return true;  // BUG: exits loop early, skips remaining signers
            }
            return false;
        }

        // Normal validation for existing accounts...
        if (!verifySignature(signer, accountInfo)) {
            return false;
        }
    }
    return true;
}
Enter fullscreen mode Exit fullscreen mode

The critical flaw: when the loop encounters a signer whose account doesn't exist on the ledger, and the signing key matches that non-existent account's derived key, the function returns true immediately — skipping validation of all remaining signers.

The Attack

  1. Attacker creates a Batch transaction targeting victim's account
  2. Declares two outer signers: (a) attacker-controlled non-existent account, (b) victim's account
  3. Signs with the attacker's own key (which matches the non-existent account)
  4. Validation loop hits signer (a), finds non-existent account, key matches → return true
  5. Signer (b) — the victim — is never validated
  6. Inner transactions execute as if authorized by the victim
┌────────────────────────────────────┐
│ Malicious Batch Transaction        │
│                                    │
│ Outer Signers:                     │
│  [0] attacker_phantom (not on ledger)│
│  [1] victim_account (NEVER CHECKED)│
│                                    │
│ Inner Transactions:                │
│  ├─ Payment: victim → attacker     │
│  │   (all XRP)                     │
│  ├─ TrustSet: victim trusts scam   │
│  └─ OfferCreate: dump victim NFTs  │
│                                    │
│ Validation: ✓ (loop exits at [0])  │
└────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Impact (If Activated)

  • Drain any account: Transfer all XRP from any wallet
  • Modify trust lines: Set arbitrary trust lines on victim accounts
  • Delete accounts: Execute AccountDelete on behalf of victims
  • Network-wide chaos: Every account on XRPL would be vulnerable simultaneously

Why This Pattern Is Dangerous Beyond XRPL

The "early loop exit in batch signer validation" is a vulnerability class, not just a one-off bug. Let's map it to other ecosystems:

ERC-4337 Bundler Signature Validation

ERC-4337 smart account wallets validate UserOperation signatures in a similar loop. If a bundler or paymaster implementation short-circuits validation on encountering an uninitialized account:

// VULNERABLE PATTERN: ERC-4337 multi-signer validation
function validateSignatures(
    UserOperation calldata userOp,
    address[] calldata requiredSigners
) internal view returns (bool) {
    for (uint i = 0; i < requiredSigners.length; i++) {
        if (requiredSigners[i].code.length == 0) {
            // EOA — check if signature matches
            if (ecrecover(hash, sig) == requiredSigners[i]) {
                return true; // BUG: same pattern — early exit
            }
            return false;
        }
        // Contract wallet validation...
        if (!IAccount(requiredSigners[i]).isValidSignature(hash, sig)) {
            return false;
        }
    }
    return true;
}

// FIXED: Validate ALL signers, accumulate results
function validateSignaturesFixed(
    UserOperation calldata userOp,
    address[] calldata requiredSigners
) internal view returns (bool) {
    uint validCount = 0;
    for (uint i = 0; i < requiredSigners.length; i++) {
        if (_validateSingleSigner(requiredSigners[i], hash, sig)) {
            validCount++;
        } else {
            return false; // Any failure = reject entire batch
        }
    }
    return validCount == requiredSigners.length;
}
Enter fullscreen mode Exit fullscreen mode

Solana Batch/Versioned Transaction CPI Validation

Solana programs that process grouped instructions with delegated authority face the same risk:

// VULNERABLE: Solana batch instruction validator
pub fn validate_batch_signers(
    ctx: &Context<BatchExecute>,
    batch: &BatchInstruction,
) -> Result<()> {
    for signer_info in batch.required_signers.iter() {
        let signer_account = ctx.remaining_accounts
            .iter()
            .find(|a| a.key == &signer_info.pubkey);

        match signer_account {
            None => {
                // Account not provided — check if PDA matches
                let (derived, _) = Pubkey::find_program_address(
                    &[b"batch_signer", signer_info.seed.as_ref()],
                    ctx.program_id,
                );
                if derived == signer_info.pubkey {
                    return Ok(()); // BUG: early return, skips remaining
                }
                return Err(ErrorCode::MissingSigner.into());
            }
            Some(account) => {
                require!(account.is_signer, ErrorCode::NotSigned);
            }
        }
    }
    Ok(())
}

// FIXED: Track all validations, enforce completeness
pub fn validate_batch_signers_fixed(
    ctx: &Context<BatchExecute>,
    batch: &BatchInstruction,
) -> Result<()> {
    let mut validated_count: u64 = 0;
    let required_count = batch.required_signers.len() as u64;

    for signer_info in batch.required_signers.iter() {
        validate_single_signer(ctx, signer_info)?;
        validated_count = validated_count
            .checked_add(1)
            .ok_or(ErrorCode::Overflow)?;
    }

    require!(
        validated_count == required_count,
        ErrorCode::IncompleteBatchValidation
    );
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

The AI Discovery Angle

This vulnerability was co-discovered by Cantina AI's Apex tool — an AI-powered security scanner. The human researcher (Pranamya Keshkamat) identified the vulnerability class, and Apex independently flagged the same code path through automated semantic analysis.

This matters because:

  1. Loop logic bugs are notoriously hard to catch in traditional code review — the code "looks correct" at each step
  2. AI tools excel at path analysis — they can trace all possible execution paths through a loop and identify states where early exits skip critical checks
  3. The pattern is detectable by rule: any loop that validates N items but can return success after validating fewer than N items is a candidate

Detection Script: Early Loop Exit Scanner

#!/usr/bin/env python3
"""
Scan for early-return-in-validation-loop patterns.
Works on Solidity, Rust, C++, and TypeScript.
"""
import re
import sys
from pathlib import Path

PATTERNS = [
    # return true/Ok(()) inside a for/while loop in a validate/verify function
    r'(?:fn|function|bool)\s+(?:validate|verify|check|authorize)\w*\s*\([^)]*\)[^{]*\{[^}]*(?:for|while)\s*\([^)]*\)\s*\{[^}]*return\s+(?:true|Ok\(\(\)\))',
    # Early break with success flag
    r'(?:for|while)\s*\([^)]*\)\s*\{[^}]*(?:is_valid|authorized|verified)\s*=\s*true[^}]*break',
]

def scan_file(path: Path) -> list:
    findings = []
    content = path.read_text(errors='ignore')
    for i, pattern in enumerate(PATTERNS):
        for match in re.finditer(pattern, content, re.DOTALL):
            line_no = content[:match.start()].count('\n') + 1
            findings.append({
                'file': str(path),
                'line': line_no,
                'pattern': f'EARLY_LOOP_EXIT_{i+1}',
                'snippet': match.group()[:120]
            })
    return findings

def main():
    target = Path(sys.argv[1]) if len(sys.argv) > 1 else Path('.')
    files = list(target.rglob('*.sol')) + list(target.rglob('*.rs')) + \
            list(target.rglob('*.cpp')) + list(target.rglob('*.ts'))

    total = 0
    for f in files:
        for finding in scan_file(f):
            print(f"[{finding['pattern']}] {finding['file']}:{finding['line']}")
            print(f"  {finding['snippet']}...")
            total += 1

    print(f"\n{'='*60}")
    print(f"Scanned {len(files)} files, found {total} potential early-exit patterns")
    if total > 0:
        print("⚠️  Manual review required — not all matches are vulnerabilities")

if __name__ == '__main__':
    main()
Enter fullscreen mode Exit fullscreen mode

The Fix: Complete Validation Enforcement

The rippled 3.1.1 emergency patch took a two-step approach:

  1. Immediate: Mark Batch and fixBatchInnerSigs amendments as unsupported (prevent activation)
  2. Long-term: Rewrite the validation loop to eliminate the early-exit path (upcoming BatchV1_1)

The correct pattern for any batch signer validation:

WRONG:  for each signer { if valid → return success }
RIGHT:  for each signer { if invalid → return failure } → return success only after ALL checked
Enter fullscreen mode Exit fullscreen mode

This is the "validate all or reject" principle. Here's a Solidity implementation:

// SECURE: Complete batch authorization verification
contract BatchValidator {
    error IncompleteBatchValidation(uint256 validated, uint256 required);
    error SignerValidationFailed(address signer, uint256 index);
    error BatchEmpty();

    function validateBatch(
        bytes32 batchHash,
        address[] calldata signers,
        bytes[] calldata signatures
    ) public view returns (bool) {
        uint256 requiredCount = signers.length;
        if (requiredCount == 0) revert BatchEmpty();
        if (signatures.length != requiredCount) {
            revert IncompleteBatchValidation(signatures.length, requiredCount);
        }

        uint256 validatedCount = 0;

        for (uint256 i = 0; i < requiredCount; i++) {
            // Validate EVERY signer — no early exits on success
            address recovered = ECDSA.recover(batchHash, signatures[i]);

            if (recovered != signers[i]) {
                revert SignerValidationFailed(signers[i], i);
            }

            // Check account exists (prevent phantom account bypass)
            if (signers[i].code.length == 0 && signers[i].balance == 0) {
                // Additional check: is this a real account or phantom?
                // For smart accounts, verify implementation exists
                revert SignerValidationFailed(signers[i], i);
            }

            unchecked { validatedCount++; }
        }

        // Final completeness check
        assert(validatedCount == requiredCount);
        return true;
    }
}
Enter fullscreen mode Exit fullscreen mode

7-Point Batch Transaction Security Checklist

Authorization Layer

  • [ ] No early loop exits on success — validation loops must check ALL signers before returning true
  • [ ] Phantom account rejection — non-existent accounts cannot authorize transactions for existing accounts
  • [ ] Signature-to-account binding — each signature is verified against the specific account it claims to authorize

Atomicity Layer

  • [ ] All-or-nothing execution — if any inner transaction fails, the entire batch reverts
  • [ ] No partial authorization — M-of-N schemes validate exact threshold, not "at least one"
  • [ ] Signer deduplication — same account cannot appear twice to inflate the valid count

Testing Layer

  • [ ] Boundary condition fuzzing — test with 0 signers, 1 signer, max signers, non-existent accounts, and mixed valid/invalid batches

Broader Lessons

1. Pre-Activation Auditing Saved $Billions

This vulnerability was caught before the Batch amendment went live. If XRPL had shipped XLS-56 without this review, every wallet on the network — holding billions in XRP — would have been instantly vulnerable. The lesson: amendment/upgrade voting periods are a critical security window, not just governance theater.

2. AI + Human = Better Coverage

Neither the human researcher nor the AI tool would have been as effective alone. The human identified the vulnerability class (batch authorization bypass); the AI independently confirmed the specific code path. This "AI co-pilot for auditing" model is becoming the standard in 2026.

3. The Pattern Is Universal

Any system that processes grouped operations with delegated authority needs to audit its validation loops for:

  • Early returns on success (the XRPL bug)
  • Off-by-one errors in threshold counting
  • Phantom entity bypass (non-existent accounts/contracts treated as valid)
  • Short-circuit evaluation that skips later signers

This applies to ERC-4337 bundlers, Solana versioned transactions, Cosmos batch messages, and any custom multi-operation processor.


Conclusion

The XRPL Batch amendment vulnerability is a textbook case of a logic bug hiding in plain sight. The code was syntactically correct, the tests probably passed (because they tested the happy path), and the design was sound. The flaw was a single return true placed one level too deep.

In DeFi security, the most dangerous bugs aren't the ones that look wrong — they're the ones that look right until you trace every possible path through the validation logic. If your protocol processes batch transactions, check your loops tonight.


Sources: XRPL Foundation vulnerability disclosure (March 2026), Common Prefix security report, Cantina AI Apex disclosure, rippled 3.1.1 emergency release notes (February 23, 2026)

Top comments (0)