> loading_
# ──────────────────────────────────────────────────────────────
# Walkthrough 1: Auditing Permission Delegation No-Op Bypass Patterns
# ──────────────────────────────────────────────────────────────
# Start in the transaction processing modules:
# src/ripple/app/tx/impl/
#
# Look at delegation-related transactors (DelegateSet.cpp,
# SetRegularKey.cpp, SignerListSet.cpp) and any authorization
# helpers in src/ripple/ledger/impl/.
#
# The bug pattern: delegation checks that return tesSUCCESS
# (or a pass-through/no-op) when no valid delegation exists,
# instead of returning tecNO_PERMISSION or similar.
# BEFORE (vulnerable pattern — empty delegate list treated as allow-all):
# --------------------------------------------------------------------
# TER checkDelegatedPermission(ReadView const& view,
# AccountID const& account,
# AccountID const& delegate)
# {
# auto const sleDelegate = view.read(
# keylet::delegation(account, delegate));
#
# if (!sleDelegate)
# return tesSUCCESS; // <— NO-OP BYPASS: missing delegation
# // object silently passes the check
#
# // ... scope validation logic ...
# return tesSUCCESS;
# }
# AFTER (fixed — enforce default-deny when no delegation exists):
# ---------------------------------------------------------------
# TER checkDelegatedPermission(ReadView const& view,
# AccountID const& account,
# AccountID const& delegate)
# {
# auto const sleDelegate = view.read(
# keylet::delegation(account, delegate));
#
# if (!sleDelegate)
# return tecNO_PERMISSION; // <— DEFAULT DENY: no delegation
# // object means no authorization
#
# // Explicitly validate that the delegation scope covers
# // the requested transaction type
# auto const& scopes = sleDelegate->getFieldArray(sfPermissions);
# if (scopes.empty())
# return tecNO_PERMISSION; // <— Empty scope list is also deny
#
# bool found = false;
# for (auto const& scope : scopes)
# {
# if (scope.getFieldU16(sfTransactionType) == requestedTxType)
# {
# found = true;
# break;
# }
# }
#
# return found ? tesSUCCESS : tecNO_PERMISSION;
# }
# Key review points:
# - Every code path must reach an explicit allow OR deny decision
# - Short-circuit returns must default to deny, not success
# - Empty/missing delegation scopes must never be treated as wildcard
# ──────────────────────────────────────────────────────────────
# Walkthrough 2: Permission Delegation Lifecycle Cleanup
# ──────────────────────────────────────────────────────────────
# Key files:
# - src/ripple/app/tx/impl/DeleteAccount.cpp
# - src/ripple/app/tx/impl/DelegateSet.cpp / DelegateDelete.cpp
# - src/ripple/ledger/impl/ (owner directory management)
# When an account is deleted, all ltDELEGATION objects where
# that account is grantor OR grantee must be removed.
# BEFORE (DeleteAccount.cpp — delegations not cleaned up):
# --------------------------------------------------------
# TER DeleteAccount::preflight(PreflightContext const& ctx)
# {
# // ... existing checks for offers, escrows, paychans ...
# // NOTE: No check for outstanding delegation objects!
# return preflight2(ctx);
# }
# AFTER (DeleteAccount.cpp — cascade-delete delegations):
# -------------------------------------------------------
# TER DeleteAccount::doApply()
# {
# // ... existing cleanup logic ...
#
# // Remove all delegation objects owned by this account
# auto const ownerDirKey = keylet::ownerDir(account_);
# for (auto const& item : view().dirRange(ownerDirKey))
# {
# auto const sle = view().peek(item);
# if (sle && sle->getType() == ltDELEGATION)
# {
# // Remove from grantee's owner directory as well
# auto const grantee = sle->getAccountID(sfDestination);
# auto const granteeDir = keylet::ownerDir(grantee);
# view().dirRemove(
# granteeDir, sle->getFieldU64(sfDestDirNode),
# sle->key(), false);
#
# // Remove the delegation object itself
# view().dirRemove(
# ownerDirKey, sle->getFieldU64(sfOwnerDirNode),
# sle->key(), false);
# view().erase(sle);
# }
# }
#
# // ... continue with account deletion ...
# }
# Also check expiration pruning during ledger advancement:
# - Expired ltDELEGATION objects should be detected and removed
# in the same pass that handles offer/escrow expiration
# - Verify cascading revocation: if account B delegates to C
# based on delegation from A→B, revoking A→B must invalidate B→C
# ──────────────────────────────────────────────────────────────
# Walkthrough 3: Batch Transactions OnlyOne Mode tec Fix
# ──────────────────────────────────────────────────────────────
# Key file: src/ripple/app/tx/impl/Batch.cpp
# The OnlyOne mode should commit state changes from exactly one
# inner transaction. The bug: tec results from MULTIPLE inner
# txns each apply their state changes (fees, sequences, partial
# side effects) into the parent view.
# BEFORE (broken — each tec applies state to parent view):
# --------------------------------------------------------
# TER Batch::applyOnlyOne(Sandbox& parentView,
# std::vector<STTx> const& innerTxns)
# {
# TER finalResult = tecBATCH_FAILURE;
#
# for (auto const& tx : innerTxns)
# {
# Sandbox innerSandbox(&parentView); // <— branches from
# // ALREADY-MUTATED parent
# auto const result = applyTransaction(innerSandbox, tx);
#
# if (isTecClaim(result))
# {
# innerSandbox.apply(parentView); // <— BUG: applies EVERY
# // tec to parent, not
# // just one
# finalResult = result;
# // NOTE: no break — loop continues, accumulating state
# }
# }
# return finalResult;
# }
# AFTER (fixed — only commit the first successful/tec result):
# ------------------------------------------------------------
# TER Batch::applyOnlyOne(Sandbox& parentView,
# std::vector<STTx> const& innerTxns)
# {
# // Take a snapshot of the clean parent state
# // so each inner txn runs against the SAME base state
# Sandbox cleanSnapshot(&parentView);
#
# for (auto const& tx : innerTxns)
# {
# // Each inner txn gets its own sandbox forked from
# // the ORIGINAL parent — not from accumulated mutations
# Sandbox innerSandbox(&cleanSnapshot);
# auto const result = applyTransaction(innerSandbox, tx);
#
# if (isTesSuccess(result))
# {
# // Success — commit ONLY this txn's changes and stop
# innerSandbox.apply(parentView);
# return result;
# }
#
# if (isTecClaim(result))
# {
# // tec — commit ONLY this txn's state changes and stop
# // (fee claim + sequence bump for exactly one txn)
# innerSandbox.apply(parentView);
# return result;
# }
#
# // tef/tem/tel — discard sandbox, try next inner txn
# }
#
# // No inner txn succeeded or claimed — entire batch fails
# return tecBATCH_FAILURE;
# }
# Key invariant to verify:
# - Only ONE inner transaction's state changes (including tec-level
# side effects like fee burns and sequence consumption) persist
# - Each inner txn must be evaluated against the SAME base state
# - The execution loop must break after the first commit
# - Write tests that submit OnlyOne batches where multiple inner
# txns would tec, and assert that only one fee is claimed and
# only one sequence is consumed