Conversation
Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
This is a small, well-scoped change that gates Batch's existing Vault/Lending inner-transaction rejection behind featureLendingProtocolV1_1. The logic in Batch.cpp is correct: pre-amendment it preserves the original any_of check against kDisabledTxTypes, post-amendment it bypasses that check entirely, matching the stated intent of allowing these transaction types as Batch inners once the amendment is enabled. The accompanying test updates (Batch_test.cpp, LoanLifecycle_test.cpp) consistently replace the old kDisabledTxTypes-based enablement checks with direct featureLendingProtocolV1_1 flag checks, and the new testLendingAmendment case exercises both the pre- and post-amendment paths for Vault create/deposit. No correctness, security, or resource-management issues were found in the changed lines; the removed includes (, LedgerFormats.h, TxFormats.h, Batch.h) line up with the removed std::ranges::any_of/kDisabledTxTypes usage in the test files.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
🟢 Approval recommended
The amendment gate and representative Vault/Loan Batch paths are covered without identified correctness issues.
Pull request overview
Allows Vault and Lending Protocol transactions inside Batch once featureLendingProtocolV1_1 is enabled, while preserving legacy rejection behavior.
Changes:
- Gates Batch’s disabled transaction list on the lending amendment.
- Adds pre/post-amendment Vault Batch coverage.
- Updates LoanSet Batch tests to use the amendment feature flag.
File summaries
| File | Description |
|---|---|
src/libxrpl/tx/transactors/system/Batch.cpp |
Applies amendment-gated inner transaction validation. |
src/test/app/Batch_test.cpp |
Tests Vault and Lending Batch behavior. |
src/test/app/lending/LoanLifecycle_test.cpp |
Updates LoanSet counterparty bypass coverage. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
The change cleanly gates the previously-hardcoded Batch inner-transaction rejection list (kDisabledTxTypes) behind featureLendingProtocolV1_2, correctly inverting the amendment check (isDisabledTxType is now false whenever the amendment is enabled, regardless of tx type). Test refactors consistently replace the old kDisabledTxTypes-based enablement checks with a direct features[featureLendingProtocolV1_2] flag, and the new testLendingAmendment test exercises both pre- and post-amendment Vault-in-Batch behavior. No correctness, security, or resource-leak issues were found in the added lines.
| void | ||
| testLendingAmendment(FeatureBitset features) | ||
| { | ||
| testcase("lending amendment"); | ||
|
|
||
| using namespace test::jtx; | ||
|
|
||
| // Before LendingProtocolV1_2 a Vault inner transaction rejects the whole batch. | ||
| auto const checkVaultBatch = [this](FeatureBitset amendments) { | ||
| bool const lendingBatchEnabled = amendments[featureLendingProtocolV1_2]; | ||
| Env env{*this, amendments}; | ||
|
|
||
| Account const payer{"payer"}; | ||
| Account const lender{"lender"}; | ||
| env.fund(XRP(100'000), payer, lender); | ||
| env.close(); | ||
|
|
||
| Vault const vault{env}; | ||
| auto [create, vaultKeylet] = vault.create({.owner = lender, .asset = xrpIssue()}); | ||
|
|
||
| auto const payerSeq = env.seq(payer); | ||
| auto const lenderSeq = env.seq(lender); | ||
| auto const batchFee = batch::calcBatchFee(env, 1, 2); | ||
| submitBatch( | ||
| env, | ||
| lendingBatchEnabled ? TER{tesSUCCESS} : TER{temINVALID_INNER_BATCH}, | ||
| batch::outer(payer, payerSeq, batchFee, tfAllOrNothing), | ||
| batch::Inner(create, lenderSeq), | ||
| batch::Inner( | ||
| vault.deposit( | ||
| {.depositor = lender, .id = vaultKeylet.key, .amount = XRP(1'000)}), | ||
| lenderSeq + 1), | ||
| batch::Sig(lender)); | ||
| env.close(); | ||
|
|
||
| BEAST_EXPECT(static_cast<bool>(env.le(vaultKeylet)) == lendingBatchEnabled); | ||
| }; | ||
|
|
||
| checkVaultBatch(features - featureLendingProtocolV1_2); | ||
| checkVaultBatch(features); | ||
| } |
There was a problem hiding this comment.
Let's add more tests:
- Full lifecycle, VaultCreate,Deposit,Withdraw,Delete
- VaulCreate, Deposit, LoanBrokerCreate, LoanSet, LoanPay
There was a problem hiding this comment.
@Tapanito Looks like we read the test scope differently, so here is what I propose to cover.
All with LendingProtocolV1_2 on, tfAllOrNothing unless noted:
- VaultCreate → VaultDeposit → VaultWithdraw → VaultDelete
- VaultCreate → VaultDeposit → LoanBrokerCreate → LoanSet → LoanPay
(borrower co-signs the batch, LoanPay is an early full payment) - Risk-free arbitrage from the original request: borrow → DEX buy → DEX sell → LoanPay.
One run succeeds, one where the sell offer does not fill and the whole batch rolls back. - LoanSet → LoanManage (default) → LoanBrokerCoverWithdraw
- The same chains with tfIndependent, where one failing inner must not block the rest.
The existing pre-amendment check stays as is: a batch with a Vault inner is rejected with
temINVALID_INNER_BATCH. I don't plan to repeat it per transaction type.
Does this cover what you had in mind, or should I add or drop anything?
There was a problem hiding this comment.
Yeah, the aim is to do a full integration test of vault/lending transactions and batch. Your plan sounds good!
Add the LendingBatch suite: vault lifecycle, loan lifecycle on open-ended and closed-ended vaults, arbitrage with rollback, default with cover withdraw, and tfIndependent chains, each run with LendingProtocolV1_1 on and off.
…ing_batch_support
There was a problem hiding this comment.
The core production change in Batch.cpp is a small, well-contained amendment gate: the pre-existing rejection of Vault/Lending inner transaction types is now skipped when featureLendingProtocolV1_2 is enabled. The logic is correct (short-circuiting && avoids the any_of scan once the amendment is live, and the early return is preserved). The remainder of the diff is test code (Batch_test.cpp simplification, new LendingBatch_test.cpp suite, LoanLifecycle_test.cpp cleanup) that exercises the new behavior thoroughly — vault lifecycle, loan lifecycle, arbitrage, independent-batch chains, and rollback scenarios all look internally consistent (sequence numbers, signer sets, and TER expectations line up with the described scenarios). I did not find any correctness, security, or resource-leak issues in the changed lines that meet the bar for flagging; the special LoanSet counterparty-via-outer-Batch-signature authorization logic mentioned in the MR description isn't present in this diff (it must live in code not shown/unchanged here), so there's nothing to verify there.
| auto const loanPayTx = | ||
| loan::pay(borrower, loanKeylet.key, principal.value(), tfLoanFullPayment); | ||
|
|
||
| auto const batchFee = batch::calcBatchFee(env, 1, 5); |
There was a problem hiding this comment.
The core production change is a small, well-scoped amendment gate in Batch.cpp: the existing kDisabledTxTypes rejection for Vault/Lending inner transactions is now skipped when featureLendingProtocolV1_2 is enabled. The logic reads correctly (temINVALID_INNER_BATCH is only returned when the amendment is inactive AND the tx type is disabled), matching the described behavior and the pre-existing pattern of amendment-gating in preflight. The rest of the diff is test-only: Batch_test.cpp simplifies two ad-hoc checks to read the amendment flag directly (consistent with the removed kDisabledTxTypes-based checks and the now-unneeded algorithm/LedgerFormats includes), and a large new LendingBatch_test.cpp adds thorough coverage of vault/loan lifecycles, arbitrage, defaults, and tfIndependent chains as batch inners. I did not find correctness, security, or resource-management issues in the changed lines; the test logic (fee/balance math, keylet sequencing, signer sets) is internally consistent with the scenarios it documents.
Tapanito
left a comment
There was a problem hiding this comment.
I checked that the counterparty still has to sign the Batch for an inner LoanSet (LoanSet::preflight requires sfCounterparty on inners, and Batch::preflightSigValidated adds it to the required signers). The gate itself looks correct. The comments are mostly about test precision. The main question is why the impair inner in testLoan was dropped.
| lenderSeq + 1), | ||
| batch::Inner(manage(lender, loanKeylet.key, tfLoanImpair), lenderSeq + 2), | ||
| batch::Inner( | ||
| loan_broker::coverDeposit(lender, brokerKeylet.key, asset(100).value()), |
There was a problem hiding this comment.
The inner LoanManage(tfLoanImpair) was replaced with LoanBrokerCoverDeposit, and the lsfLoanImpaired check was removed. Why? Batch_test no longer runs impairment inside a Batch, and LendingBatch only covers the default path. If impair still works as an inner, can we keep it, or add it to LendingBatch?
| { | ||
| BEAST_EXPECT(sleLoan->isFlag(lsfLoanImpaired)); | ||
| } | ||
| BEAST_EXPECT(static_cast<bool>(env.le(loanKeylet)) == lendingBatchEnabled); |
There was a problem hiding this comment.
This only checks that the loan exists. Can we also check that the cover deposit inner applied, e.g. that sfCoverAvailable on the broker went up by 100 when lendingBatchEnabled is set?
|
|
||
| if (auto const vaultSle = env.le(vaultKeylet); BEAST_EXPECT(vaultSle)) | ||
| BEAST_EXPECT(vaultSle->at(sfAssetsTotal) == amount.value()); | ||
| BEAST_EXPECT(env.seq(owner) == seq + 5); |
There was a problem hiding this comment.
The comments above describe specific inner failures (VaultWithdraw fails, VaultDelete returns tecHAS_OBLIGATIONS), but the test only checks the final state. Nothing shows that the withdraw inner actually failed. Could we assert each inner's result? Batch_test has validateInnerTxn for this. It could move into test/jtx/batch.h so both suites can use it.
| env.close(); | ||
|
|
||
| if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle)) | ||
| BEAST_EXPECT(loanSle->at(sfPrincipalOutstanding) == principal.value()); |
There was a problem hiding this comment.
Same as the vault chain: can we assert that the LoanPay inner returned tecINSUFFICIENT_FUNDS and the cover deposit returned tesSUCCESS, instead of inferring it from the final state?
| testIndependentLoanChain(FeatureBitset features) | ||
| { | ||
| // Same claim as testIndependentVaultChain, on a loan chain. The | ||
| // borrower starts with no IOU balance, so a LoanPay for twice the |
There was a problem hiding this comment.
Nit: the borrower holds the 1,000 principal by the time LoanPay runs. It fails because 2,000 is more than that balance, not because the balance is zero.
| void | ||
| run() override | ||
| { | ||
| for (auto const& features : {all_, all_ | featureLendingProtocolV1_1}) |
There was a problem hiding this comment.
This suite relies on all_ containing featureLendingProtocolV1_2, which is only true because testableAmendments() includes Supported::No amendments. Could we make it explicit (all_ | featureLendingProtocolV1_2)? Otherwise, if that changes, every case fails with temINVALID_INNER_BATCH and it's not obvious why.
| { | ||
| // Pre-LendingProtocolV1_2: SAV and Lending transactions cannot be Batch inners. | ||
| // Post-LendingProtocolV1_2: they continue through the normal Batch checks. | ||
| bool const isDisabledTxType = !ctx.rules.enabled(featureLendingProtocolV1_2) && |
There was a problem hiding this comment.
Nit: once V1_2 is enabled, this list only matters for replaying older ledgers. Maybe rename it to something like rejectedPreV1_2, and add a note on kDisabledTxTypes in Batch.h that it can go away when the amendment is retired.
|
rippled/src/libxrpl/tx/transactors/system/Batch.cpp Lines 360 to 365 in 14f27e6 With |

High Level Overview of Change
Allows Single Asset Vault and Lending Protocol transactions to run as Batch inner transactions
when
featureLendingProtocolV1_2is enabled. Before the amendment, these transaction types remaininvalid inside a Batch.
Context of Change
Batch already applies inner transactions through the normal transaction pipeline, but explicitly
rejects all Vault and Lending transaction types.
This change gates that restriction on
featureLendingProtocolV1_2. It also verifies the specialauthorization flow for
LoanSet, where both parties sign the outer Batch instead of using aninner
CounterpartySignature.API Impact
libxrplchange (any change that may affectlibxrplor dependents oflibxrpl)Test Plan
Batch_test:LoanSetcounterparty and Batch signer validation;LoanSetand cover deposit in one Batch;LoanSetwhen a later inner transaction fails.New
LendingBatchsuite, each case run withLendingProtocolV1_1on and off:VaultCreate,VaultDeposit,VaultWithdraw,VaultDelete,on an open-ended vault and on a closed-ended vault in the Subscription phase;
VaultCreate,VaultDeposit,LoanBrokerSet,LoanSet,LoanPay. One Batchon an open-ended vault with V1_1 off. Two Batches on a closed-ended vault with V1_1 on, split
at the Subscription to Investment boundary, because
VaultDepositandLoanSetare onlyvalid in different phases;
tfAllOrNothing:LoanSet, DEX buy, DEX sell,LoanPay. One run thatprofits and one where the sell offer cannot fill and nothing persists;
LoanManagedefault andLoanBrokerCoverWithdrawin one Batch, once the grace period of aloan created earlier has passed;
tfIndependentchains where a failing inner (VaultWithdraw,VaultDelete,LoanPay) doesnot stop the inners after it.