feat: Add a new closed ended vault to extend SAV - #7921
Conversation
e23bf46 to
c053476
Compare
c053476 to
c71682a
Compare
gregtatcam
left a comment
There was a problem hiding this comment.
Unit-tests coverage gaps:
- add a
doInvariantCheckthat creates a closed-ended loan with final payment ≥RedemptionDateand asserts theValidLoaninvariant fires RedemptionDate < SubscriptionDateis untested. The invariant "reversed-date" case (Invariants_test.cpp:4503) usesred = sub + kMinInvestmentPeriod - 1— that's red > sub (gap 59), exercising the gap-too-small branch, not the red <= sub guard its comment (:4501) claims. The strict red < sub case is absent at both the transactor and invariant level, so the unsigned-subtraction guard is never exercised. Add red < sub →temMALFORMEDin theVaultCreatetest and a matching invariant fire
|
This PR has conflicts, please resolve them in order for the PR to be reviewed. |
There was a problem hiding this comment.
Pull request overview
This PR adds a new closed-ended vault kind with a deterministic, date-driven lifecycle (Subscription → Investment → Redemption) and enforces phase-based permissions across vault and lending transactions, gated behind featureLendingProtocolV1_1.
Changes:
- Introduces
VaultKind/VaultPhase, protocol bounds (kMinInvestmentPeriod,kMaxInvestmentPeriod), and new Vault fields (sfVaultKind,sfSubscriptionDate,sfRedemptionDate) with immutability enforcement. - Adds shared phase/kind derivation (
VaultHelpers) and applies phase gating inVaultDeposit,VaultWithdraw, andLoanSet(including loan maturity bound vs.RedemptionDate). - Expands invariants and test coverage for creation rules, phase transitions, permission matrix, maturity bounds, and RPC field visibility.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/test/jtx/vault.h | Extends JTx vault create args with kind/subscription/redemption fields. |
| src/test/jtx/impl/vault.cpp | Emits new vault fields into VaultCreate JSON when provided. |
| src/test/app/Vault_test.cpp | Adds closed-ended vault lifecycle/phase gating/RPC tests. |
| src/test/app/Loan_test.cpp | Adds closed-ended vault setup path and LoanSet phase/maturity tests. |
| src/test/app/Invariants_test.cpp | Adds invariant tests for closed-ended immutability and phase rules. |
| src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp | Enforces “no withdraw during Investment” for closed-ended vaults (amendment-gated). |
| src/libxrpl/tx/transactors/vault/VaultDeposit.cpp | Enforces “deposit only during Subscription” for closed-ended vaults (amendment-gated). |
| src/libxrpl/tx/transactors/vault/VaultCreate.cpp | Validates kind/date rules, enforces expiry, and writes new fields on creation. |
| src/libxrpl/tx/transactors/lending/LoanSet.cpp | Adds closed-ended phase gating and maturity bound check vs. RedemptionDate. |
| src/libxrpl/tx/invariants/VaultInvariant.cpp | Adds closed-ended create/phase invariants + LoanSet phase invariant hook. |
| src/libxrpl/tx/invariants/LoanInvariant.cpp | Adds closed-ended maturity invariant on loan creation. |
| src/libxrpl/tx/invariants/InvariantCheck.cpp | Enforces immutability of vault kind/dates via NoModifiedUnmodifiableFields. |
| src/libxrpl/ledger/View.cpp | Extends hasExpired with inclusive/exclusive boundary behavior. |
| src/libxrpl/ledger/helpers/VaultHelpers.cpp | Adds getVaultKind / getVaultPhase helpers. |
| include/xrpl/tx/invariants/VaultInvariant.h | Documents and declares new closed-ended vault invariants and helpers. |
| include/xrpl/tx/invariants/LoanInvariant.h | Documents new closed-ended maturity invariant. |
| include/xrpl/protocol/Protocol.h | Adds enums and protocol constants for closed-ended vaults. |
| include/xrpl/protocol/detail/transactions.macro | Adds new optional fields to ttVAULT_CREATE format. |
| include/xrpl/protocol/detail/sfields.macro | Defines new SFields: sfVaultKind, sfSubscriptionDate, sfRedemptionDate. |
| include/xrpl/protocol/detail/ledger_entries.macro | Adds new fields to ltVAULT schema (kind default, dates optional). |
| include/xrpl/ledger/View.h | Declares ExpiryComparison and updates hasExpired signature/defaults. |
| include/xrpl/ledger/helpers/VaultHelpers.h | Declares new Vault kind/phase helper APIs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Tapanito
left a comment
There was a problem hiding this comment.
Could you also, for now, remove references to the XLS please?
We are updating the XLS process for updating the specs, so there'll be some changes coming to it.
|
All conflicts have been resolved. Assigned reviewers can now start or resume their review. |
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.
Pull request overview
Copilot reviewed 28 out of 28 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/libxrpl/tx/invariants/InvariantCheck.cpp:1195
XRPL_ASSERT(!bad || enforce, ...)will fire in debug builds ifbadis detected while the amendment is not enforced. That contradicts the intent stated above (detection/logging even when not enforced) and can turn a would-be logged condition into a hard assertion failure.
XRPL_ASSERT(
!bad || enforce,
"xrpl::NoModifiedUnmodifiableFields::finalize : no bad "
"changes or enforce invariant");
src/libxrpl/tx/transactors/vault/VaultDeposit.cpp:83
- The PR description says
VaultDepositshould be rejected outside Subscription for closed-ended vaults withtecNO_PERMISSION, but this preclaim gate returnstecEXPIRED. This is an externally visible behavior difference (and tests in this PR also expecttecEXPIRED), so either the implementation or the documented permission matrix needs to be aligned.
if (ctx.view.rules().enabled(featureLendingProtocolV1_1))
{
auto const phase = getVaultPhase(ctx.view, vault);
if (phase == VaultPhase::Investment || phase == VaultPhase::Redemption)
{
JLOG(ctx.j.debug()) << "VaultDeposit: vault deposit is not allowed in the investment "
"or redemption phase.";
return tecEXPIRED;
}
Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com>
|
All conflicts have been resolved. Assigned reviewers can now start or resume their review. |
There was a problem hiding this comment.
This is a large, well-structured feature addition (closed-ended vaults) with extensive test coverage across invariants, transactors, and RPC. The phase-derivation logic (getVaultPhase/hasExpired boundary semantics), the gap validation, and the permission-matrix enforcement in VaultDeposit/VaultWithdraw/LoanSet all line up consistently with the documented boundary semantics (Subscription inclusive of now==SubscriptionDate, Investment strictly after, Redemption inclusive of now==RedemptionDate), and the redundant loan-maturity check duplicated in LoanSet::preclaim and LoanInvariant is a reasonable defense-in-depth. The main item worth a second look is the refactored NoModifiedUnmodifiableFields switch in InvariantCheck.cpp, where the new ltVAULT immutability gate is tied to featureLendingProtocolV1_1 while the shared enforce flag (used in the trailing XRPL_ASSERT) is tied only to the base featureLendingProtocol amendment — this silently assumes V1_1 amendments can never be enabled without the base amendment.
There was a problem hiding this comment.
This is a large, well-structured feature addition (closed-ended vaults) with thorough test coverage across preflight/preclaim gating, invariants, and RPC. I traced the phase-derivation logic (VaultHelpers::getVaultPhase), the new immutability enforcement in NoModifiedUnmodifiableFields, and the new LoanSet/VaultDeposit/VaultWithdraw phase gates against the accompanying tests and didn't find correctness bugs in the changed lines — boundary semantics (inclusive Subscription, exclusive Investment start, inclusive Redemption start), the overflow-safe gap arithmetic, and the amendment-gating in preflight/preclaim/doApply are all internally consistent and exercised by the new tests. One design assumption in the new invariant code is worth a quick sanity check from the author (see below), but nothing here looks blocking.
There was a problem hiding this comment.
Overall this is a well-structured, thoroughly-tested implementation of the closed-ended vault lifecycle. Phase derivation (getVaultPhase), the SubscriptionDate/RedemptionDate gap validation, and the phase-gating added to VaultDeposit/VaultWithdraw/LoanSet are internally consistent with each other and with the mirrored invariant checks (ValidVault::finalizeLoanSet, LoanInvariant's maturity check). Boundary semantics (inclusive Subscription at now==SubscriptionDate, inclusive Redemption at now==RedemptionDate) are correctly implemented via the new ExpiryComparison parameter and validated by dedicated boundary tests. I found one discrepancy worth double-checking against the spec: VaultDeposit returns tecEXPIRED for phase violations, but the MR description states it should return tecNO_PERMISSION — worth confirming which is intended since external tooling built against the XLS-587 spec may depend on the exact code. I did not find correctness, security, or resource-management bugs in the added code.
High Level Overview of Change
Spec: XRPLF/XRPL-Standards#587
Introduces a new closed-ended vault kind that extends the Single Asset Vault (XLS-65) with a deterministic, date-driven lifecycle: Subscription → Investment → Redemption. The vault's phase is derived from the parent ledger close time and two immutable dates stored on the
Vaultledger entry, and phase enforcement is added to the vault and lending transactors so capital is locked for the advertised term. Open-ended vaults are behaviourally unchanged.Concretely, this PR:
Vaultledger entry and toVaultCreate:sfVaultKind(UINT8),sfSubscriptionDate(UINT32),sfRedemptionDate(UINT32). All are immutable after creation.VaultKind,VaultPhase) and two protocol constants (kMinInvestmentPeriod = 60s,kMaxInvestmentPeriod = 30 years) ininclude/xrpl/protocol/Protocol.h.VaultHelpersfor phase derivation (getVaultPhase,getVaultKind) shared by transactors and invariants.VaultDeposit: rejected outsideSubscription(closed-ended) withtecNO_PERMISSION.VaultWithdraw: rejected duringInvestmentwithtecTOO_SOON.LoanSet/LoanAccept: permitted only duringInvestment;LoanSetadditionally requires the loan's final scheduled payment to fall strictly beforeRedemptionDate.VaultInvariantand extendsLoanInvariantto assert the closed-ended invariants (immutability of the three fields,MIN <= RedemptionDate - SubscriptionDate < MAX, per-transaction phase invariants, loan-maturity bound).vault_infoandledger_entryresponses to include the three new fields for closed-ended vaults (omitted for open-ended vaults, which callers MUST interpret asVaultKind = OpenEnded).Context of Change
This is a new feature gated behind an amendment; open-ended vaults are inert with respect to it. The design choice worth calling out is date-driven phases rather than a maintained
LoanCount:LoanCountwould couple the vault to individual loans and require every lending transactor to keep the counter symmetric across activation and resolution paths — a single missed increment or double decrement mis-signals the phase.SubscriptionDate,RedemptionDate, and the parent ledger close time. The vault stays unaware of loans, the lifecycle is monotonic and deterministic, and depositors see the exact subscription window and lock-up term before they commit capital.The trade-off — the Investment lock-up begins on a fixed calendar date rather than on actual capital deployment — is accepted as a more predictable contract for depositors. Rationale, alternatives, and FAQ are covered in §11 and Appendix A of the spec.
API Impact
libxrplchange (any change that may affectlibxrplor dependents oflibxrpl)