Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c808c46
adds flag definitions
Tapanito Feb 12, 2026
a219814
adds BLockDeposit flagsto VaultSet
Tapanito Feb 12, 2026
1010866
adds amendment validation in flags and better tests
Tapanito Feb 13, 2026
087a9c1
add vault deposit logic and tests
Tapanito Feb 17, 2026
82b0d57
minor code improvements
Tapanito Feb 17, 2026
f57b715
removes redundant check
Tapanito Feb 17, 2026
71b9f98
Merge branch 'tapanito/lending-fix-amendment' into tapanito/vault-blo…
Tapanito Feb 17, 2026
3cfb5fe
additional unit test
Tapanito Feb 17, 2026
cd1e8eb
debug attempt
Tapanito Feb 19, 2026
d972071
Merge branch 'tapanito/lending-fix-amendment' into tapanito/vault-blo…
Tapanito Feb 19, 2026
884530e
adds missing env.close
Tapanito Feb 19, 2026
8732e84
refactor: Extract vault helper functions into VaultHelpers module
Tapanito Feb 24, 2026
81e69b9
Merge branch 'tapanito/lending-fix-amendment' into tapanito/vault-blo…
Tapanito Feb 24, 2026
b08451c
Merge branch 'tapanito/lending-fix-amendment' into tapanito/vault-blo…
Tapanito Feb 24, 2026
8723472
fixes broken unit-tests
Tapanito Feb 24, 2026
2b716eb
adds lsfVaultOwnerCanBlockDeposit flag
Tapanito Feb 25, 2026
f034ca0
Merge remote-tracking branch 'origin/tapanito/lending-fix-amendment' …
Tapanito Feb 26, 2026
3deb0de
Merge branch 'tapanito/lending-fix-amendment' into tapanito/vault-blo…
Tapanito Mar 4, 2026
ebfa659
fixes formattting issues after merge
Tapanito Mar 4, 2026
cc9dbe2
fixes typos and improves test coverage
Tapanito Mar 4, 2026
68d7555
Merge remote-tracking branch 'origin/tapanito/lending-fix-amendment' …
Tapanito Mar 9, 2026
7e5a4c9
Merge branch 'tapanito/lending-fix-amendment' into tapanito/vault-blo…
Tapanito Mar 21, 2026
b970c66
fix: address PR review feedback
Tapanito Mar 21, 2026
5fe99dc
fixes failing unit-tests
Tapanito Mar 23, 2026
8899d34
Merge remote-tracking branch 'origin/tapanito/lending-fix-amendment' …
Tapanito Mar 24, 2026
f2495dc
Merge remote-tracking branch 'origin/tapanito/lending-fix-amendment' …
Tapanito Mar 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions include/xrpl/ledger/helpers/VaultHelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,17 @@ sharesToAssetsWithdraw(
std::shared_ptr<SLE const> const& issuance,
STAmount const& shares);

/** Determine if a vault is insolvent. A vault is considered insolvent when
the total assets in the vault are zero, and outstanding shares are non-zero.

@param vault The vault SLE.
@param shareIssuance The MPTokenIssuance SLE for the vault's shares.

@return True if the vault is insolvent, false otherwise.
*/
[[nodiscard]] bool
isVaultInsolvent(
std::shared_ptr<SLE const> const& vault,
std::shared_ptr<SLE const> const& shareIssuance);

} // namespace xrpl
4 changes: 3 additions & 1 deletion include/xrpl/protocol/LedgerFormats.h
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,9 @@ enum LedgerEntryType : std::uint16_t {
LSF_FLAG(lsfAccepted, 0x00010000)) \
\
LEDGER_OBJECT(Vault, \
LSF_FLAG(lsfVaultPrivate, 0x00010000)) \
LSF_FLAG(lsfVaultPrivate, 0x00010000) \
LSF_FLAG(lsfVaultDepositBlocked, 0x00020000) /* True, vault deposit is blocked */ \
LSF_FLAG(lsfVaultOwnerCanBlockDeposit, 0x00040000)) /* True, vault owner can block deposit */ \
\
LEDGER_OBJECT(Loan, \
LSF_FLAG(lsfLoanDefault, 0x00010000) \
Expand Down
9 changes: 7 additions & 2 deletions include/xrpl/protocol/TxFlags.h
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,13 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal;
\
TRANSACTION(VaultCreate, \
TF_FLAG(tfVaultPrivate, lsfVaultPrivate) \
TF_FLAG(tfVaultShareNonTransferable, 0x00020000), \
TF_FLAG(tfVaultShareNonTransferable, 0x00020000) \
TF_FLAG(tfVaultOwnerCanBlockDeposit, lsfVaultOwnerCanBlockDeposit), \
MASK_ADJ(0)) \
\
TRANSACTION(VaultSet, \
TF_FLAG(tfVaultDepositBlock, 0x00010000) \
TF_FLAG(tfVaultDepositUnblock, 0x00020000), \
MASK_ADJ(0)) \
\
TRANSACTION(Batch, \
Expand Down Expand Up @@ -213,7 +219,6 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal;
TF_FLAG(tfLoanImpair, 0x00020000) \
TF_FLAG(tfLoanUnimpair, 0x00040000), \
MASK_ADJ(0))

// clang-format on

// Create all the flag values.
Expand Down
1 change: 1 addition & 0 deletions include/xrpl/tx/transactors/vault/VaultClawback.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/tx/Transactor.h>

namespace xrpl {
Expand Down
1 change: 1 addition & 0 deletions include/xrpl/tx/transactors/vault/VaultDeposit.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/tx/Transactor.h>

namespace xrpl {
Expand Down
3 changes: 3 additions & 0 deletions include/xrpl/tx/transactors/vault/VaultSet.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ class VaultSet : public Transactor
{
}

static std::uint32_t
getFlagsMask(PreflightContext const& ctx);

static bool
checkExtraFeatures(PreflightContext const& ctx);

Expand Down
1 change: 1 addition & 0 deletions include/xrpl/tx/transactors/vault/VaultWithdraw.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/tx/Transactor.h>

namespace xrpl {
Expand Down
16 changes: 16 additions & 0 deletions src/libxrpl/ledger/helpers/VaultHelpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,20 @@ sharesToAssetsWithdraw(
return assets;
}

[[nodiscard]] bool
isVaultInsolvent(
std::shared_ptr<SLE const> const& vault,
std::shared_ptr<SLE const> const& shareIssuance)
{
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::isVaultInsolvent : Vault sle");
XRPL_ASSERT(
shareIssuance && shareIssuance->getType() == ltMPTOKEN_ISSUANCE,
"xrpl::isVaultInsolvent : MPTokenIssuance sle");

auto const assetsTotal = vault->at(sfAssetsTotal);
auto const sharesOutstanding = shareIssuance->at(sfOutstandingAmount);

return assetsTotal == 0 && sharesOutstanding > 0;
}

} // namespace xrpl
7 changes: 3 additions & 4 deletions src/libxrpl/tx/transactors/vault/VaultClawback.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/tx/transactors/vault/VaultClawback.h>
//
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/ledger/helpers/VaultHelpers.h>
Expand All @@ -10,7 +11,6 @@
#include <xrpl/protocol/STTakesAsset.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/tx/transactors/vault/VaultClawback.h>

#include <optional>

Expand Down Expand Up @@ -51,9 +51,8 @@ clawbackAmount(
if (maybeAmount)
return *maybeAmount;

Asset const share = MPTIssue{vault->at(sfShareMPTID)};
if (account == vault->at(sfOwner))
return STAmount{share};
return STAmount{MPTIssue{vault->at(sfShareMPTID)}};

return STAmount{vault->at(sfAsset)};
}
Expand Down
24 changes: 17 additions & 7 deletions src/libxrpl/tx/transactors/vault/VaultCreate.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#include <xrpl/tx/transactors/vault/VaultCreate.h>
//
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
Expand All @@ -15,7 +17,6 @@
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/tx/transactors/token/MPTokenAuthorize.h>
#include <xrpl/tx/transactors/token/MPTokenIssuanceCreate.h>
#include <xrpl/tx/transactors/vault/VaultCreate.h>

namespace xrpl {

Expand All @@ -34,7 +35,10 @@ VaultCreate::checkExtraFeatures(PreflightContext const& ctx)
std::uint32_t
VaultCreate::getFlagsMask(PreflightContext const& ctx)
{
return tfVaultCreateMask;
if (ctx.rules.enabled(featureLendingProtocolV1_1))
return tfVaultCreateMask;

return tfVaultCreateMask | tfVaultOwnerCanBlockDeposit;
}

NotTEC
Expand Down Expand Up @@ -161,11 +165,11 @@ VaultCreate::doApply()
? 0
: ctx_.tx[~sfScale].value_or(vaultDefaultIOUScale);

auto txFlags = tx.getFlags();
std::uint32_t mptFlags = 0;
if ((txFlags & tfVaultShareNonTransferable) == 0)
if (!tx.isFlag(tfVaultShareNonTransferable))
mptFlags |= (lsfMPTCanEscrow | lsfMPTCanTrade | lsfMPTCanTransfer);
if ((txFlags & tfVaultPrivate) != 0u)

if (tx.isFlag(tfVaultPrivate))
mptFlags |= lsfMPTRequireAuth;

// Note, here we are **not** creating an MPToken for the assets held in
Expand All @@ -189,7 +193,13 @@ VaultCreate::doApply()
auto const& mptIssuanceID = *maybeShare;

vault->setFieldIssue(sfAsset, STIssue{sfAsset, asset});
vault->at(sfFlags) = txFlags & tfVaultPrivate;
if (tx.isFlag(tfVaultPrivate))
vault->setFlag(lsfVaultPrivate);

if (view().rules().enabled(featureLendingProtocolV1_1) &&
tx.isFlag(tfVaultOwnerCanBlockDeposit))
vault->setFlag(lsfVaultOwnerCanBlockDeposit);

vault->at(sfSequence) = sequence;
vault->at(sfOwner) = account_;
vault->at(sfAccount) = pseudoId;
Expand Down Expand Up @@ -222,7 +232,7 @@ VaultCreate::doApply()
return err;

// If the vault is private, set the authorized flag for the vault owner
if ((txFlags & tfVaultPrivate) != 0u)
if (vault->isFlag(lsfVaultPrivate))
{
if (auto const err = authorizeMPToken(
view(), preFeeBalance_, mptIssuanceID, pseudoId, ctx_.journal, {}, account_);
Expand Down
3 changes: 2 additions & 1 deletion src/libxrpl/tx/transactors/vault/VaultDelete.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#include <xrpl/tx/transactors/vault/VaultDelete.h>
//
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
Expand All @@ -8,7 +10,6 @@
#include <xrpl/protocol/STTakesAsset.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/tx/transactors/vault/VaultDelete.h>

namespace xrpl {

Expand Down
29 changes: 24 additions & 5 deletions src/libxrpl/tx/transactors/vault/VaultDeposit.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#include <xrpl/tx/transactors/vault/VaultDeposit.h>
//
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
Expand All @@ -13,7 +15,6 @@
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/tx/transactors/token/MPTokenAuthorize.h>
#include <xrpl/tx/transactors/vault/VaultDeposit.h>

namespace xrpl {

Expand Down Expand Up @@ -62,23 +63,41 @@ VaultDeposit::preclaim(PreclaimContext const& ctx)
// LCOV_EXCL_STOP
}

auto const sleIssuance = ctx.view.read(keylet::mptIssuance(mptIssuanceID));
if (!sleIssuance)
auto const sleShareIssuance = ctx.view.read(keylet::mptIssuance(mptIssuanceID));
if (!sleShareIssuance)
{
// LCOV_EXCL_START
JLOG(ctx.j.error()) << "VaultDeposit: missing issuance of vault shares.";
return tefINTERNAL;
// LCOV_EXCL_STOP
}

if (sleIssuance->isFlag(lsfMPTLocked))
if (sleShareIssuance->isFlag(lsfMPTLocked))
{
// LCOV_EXCL_START
JLOG(ctx.j.error()) << "VaultDeposit: issuance of vault shares is locked.";
return tefINTERNAL;
// LCOV_EXCL_STOP
}

if (ctx.view.rules().enabled(featureLendingProtocolV1_1))
{
// Perform these checks early to avoid unnecessary processing

// The Vault is insolvent, deposits are not allowed
if (isVaultInsolvent(vault, sleShareIssuance))
Comment thread
Tapanito marked this conversation as resolved.
{
JLOG(ctx.j.debug()) << "VaultDeposit: Vault is insolvent, deposits are not allowed";
return tecLOCKED;
}

if (vault->isFlag(lsfVaultDepositBlocked))
{
JLOG(ctx.j.debug()) << "VaultDeposit: Vault deposits are blocked";
return tecNO_PERMISSION;
Comment thread
Tapanito marked this conversation as resolved.
}
}

// Cannot deposit inside Vault an Asset frozen for the depositor
if (isFrozen(ctx.view, account, vaultAsset))
return vaultAsset.holds<Issue>() ? tecFROZEN : tecLOCKED;
Expand All @@ -89,7 +108,7 @@ VaultDeposit::preclaim(PreclaimContext const& ctx)

if (vault->isFlag(lsfVaultPrivate) && account != vault->at(sfOwner))
{
auto const maybeDomainID = sleIssuance->at(~sfDomainID);
auto const maybeDomainID = sleShareIssuance->at(~sfDomainID);
// Since this is a private vault and the account is not its owner, we
// perform authorization check based on DomainID read from sleIssuance.
// Had the vault shares been a regular MPToken, we would allow
Expand Down
65 changes: 63 additions & 2 deletions src/libxrpl/tx/transactors/vault/VaultSet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,29 @@ VaultSet::checkExtraFeatures(PreflightContext const& ctx)
return !ctx.tx.isFieldPresent(sfDomainID) || ctx.rules.enabled(featurePermissionedDomains);
}

std::uint32_t
VaultSet::getFlagsMask(PreflightContext const& ctx)
{
if (ctx.rules.enabled(featureLendingProtocolV1_1))
return tfVaultSetMask;

// Add tfVaultDepositBlock and tfVaultDepositUnblock flags to indicate they are disabled
return tfVaultSetMask | tfVaultDepositBlock | tfVaultDepositUnblock;
}

static bool
isValidVaultUpdate(PreflightContext const& ctx)
{
auto const atLeastOneFieldPresent = ctx.tx.isFieldPresent(sfDomainID) ||
ctx.tx.isFieldPresent(sfAssetsMaximum) || ctx.tx.isFieldPresent(sfData);

// Mask of valid, non-universal flags: any bit set here means the
// transaction is requesting a meaningful flag change.
auto const expectedFlags = ~(VaultSet::getFlagsMask(ctx) | tfUniversal);
Comment thread
Tapanito marked this conversation as resolved.

return atLeastOneFieldPresent || (ctx.tx.getFlags() & expectedFlags);
}

NotTEC
VaultSet::preflight(PreflightContext const& ctx)
{
Expand Down Expand Up @@ -44,13 +67,19 @@ VaultSet::preflight(PreflightContext const& ctx)
}
}

if (!ctx.tx.isFieldPresent(sfDomainID) && !ctx.tx.isFieldPresent(sfAssetsMaximum) &&
!ctx.tx.isFieldPresent(sfData))
if (!isValidVaultUpdate(ctx))
{
JLOG(ctx.j.debug()) << "VaultSet: nothing is being updated.";
return temMALFORMED;
}

if (ctx.tx.isFlag(tfVaultDepositBlock) && ctx.tx.isFlag(tfVaultDepositUnblock))
{
JLOG(ctx.j.debug())
<< "VaultSet: cannot set tfVaultDepositBlock and tfVaultDepositUnblock simultaneously.";
return temINVALID_FLAG;
}

return tesSUCCESS;
}

Expand Down Expand Up @@ -104,6 +133,29 @@ VaultSet::preclaim(PreclaimContext const& ctx)
}
}

if (ctx.view.rules().enabled(featureLendingProtocolV1_1))
{
// The Vault is not configured to support deposit blocking
if (!vault->isFlag(lsfVaultOwnerCanBlockDeposit) &&
(ctx.tx.isFlag(tfVaultDepositBlock) || ctx.tx.isFlag(tfVaultDepositUnblock)))
{
JLOG(ctx.j.debug()) << "VaultSet: vault does not support blocking deposits";
return tecNO_PERMISSION;
}

if (vault->isFlag(lsfVaultDepositBlocked) && ctx.tx.isFlag(tfVaultDepositBlock))
{
JLOG(ctx.j.debug()) << "VaultSet: vault deposit is already blocked";
return tecNO_PERMISSION;
}

if (!vault->isFlag(lsfVaultDepositBlocked) && ctx.tx.isFlag(tfVaultDepositUnblock))
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.

I think it may make sense to allow cases where the flag we want to add already exists or the flag we want to remove doesn't exist.

We don't return an error when a transaction is updating another field and the value is the same as the current value, but we do return an error when the flag is already added, which feels inconsistent.

{
JLOG(ctx.j.debug()) << "VaultSet: vault deposit is already unblocked";
return tecNO_PERMISSION;
}
Comment thread
Tapanito marked this conversation as resolved.
}

return tesSUCCESS;
}

Expand Down Expand Up @@ -161,6 +213,15 @@ VaultSet::doApply()
view().update(sleIssuance);
}

if (view().rules().enabled(featureLendingProtocolV1_1))
{
if (tx.isFlag(tfVaultDepositBlock))
vault->setFlag(lsfVaultDepositBlocked);

if (tx.isFlag(tfVaultDepositUnblock))
vault->clearFlag(lsfVaultDepositBlocked);
}

// Note, we must update Vault object even if only DomainID is being updated
// in Issuance object. Otherwise it's really difficult for Vault invariants
// to verify the operation.
Expand Down
3 changes: 2 additions & 1 deletion src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#include <xrpl/tx/transactors/vault/VaultWithdraw.h>
//
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
Expand All @@ -9,7 +11,6 @@
#include <xrpl/protocol/STTakesAsset.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/tx/transactors/vault/VaultWithdraw.h>

namespace xrpl {

Expand Down
Loading
Loading