Compare commits

...

63 Commits

Author SHA1 Message Date
Vito
fee67c8f87 remve unnecessary changes pt2 2026-06-08 12:54:40 +02:00
Vito
357f1a1a29 reduce PR size 2026-06-08 12:52:33 +02:00
Vito
c9f6a606be fix: code gen 2026-06-08 12:38:52 +02:00
Vito
a4f5f28edd fix: post-merge issues 2026-06-08 12:36:01 +02:00
Vito
863995691c Merge branch 'tapanito/lending-fix-amendment' into tapanito/vault-block-deposit
# Conflicts:
#	src/libxrpl/ledger/helpers/VaultHelpers.cpp
#	src/libxrpl/tx/transactors/vault/VaultClawback.cpp
#	src/libxrpl/tx/transactors/vault/VaultCreate.cpp
#	src/libxrpl/tx/transactors/vault/VaultDelete.cpp
#	src/libxrpl/tx/transactors/vault/VaultDeposit.cpp
#	src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp
#	src/test/app/Vault_test.cpp
2026-06-08 12:12:19 +02:00
Vito
24db40e56c fix: remove unnecessary tests & clang-tidy 2026-06-08 11:57:42 +02:00
Vito
da4513d096 post-merge cleanup 2026-06-08 11:47:11 +02:00
Vito
2e2fddefe9 Merge remote-tracking branch 'origin/develop' into tapanito/lending-fix-amendment
# Conflicts:
#	include/xrpl/ledger/helpers/LendingHelpers.h
#	include/xrpl/protocol/STAmount.h
#	include/xrpl/protocol/detail/features.macro
#	include/xrpl/protocol/detail/transactions.macro
#	src/libxrpl/tx/invariants/VaultInvariant.cpp
#	src/test/app/Invariants_test.cpp
#	src/test/app/LoanBroker_test.cpp
#	src/test/app/Loan_test.cpp
#	src/test/app/Vault_test.cpp
2026-06-08 11:28:35 +02:00
Vito
db997ecad9 chore: Reset VaultInvariant to develop state before merge
VaultInvariant changes parked in /tmp/vault-invariant-changes.patch
for later re-evaluation.
2026-06-08 11:19:05 +02:00
Vito
f2495dccf7 Merge remote-tracking branch 'origin/tapanito/lending-fix-amendment' into tapanito/vault-block-deposit 2026-03-31 14:57:04 +02:00
Vito
68e4fbdf2b Merge remote-tracking branch 'origin/develop' into tapanito/lending-fix-amendment 2026-03-31 10:00:59 +02:00
Vito
bb0a09ae21 Merge remote-tracking branch 'origin/develop' into tapanito/lending-fix-amendment 2026-03-26 17:16:49 +01:00
Vito
d94232007f fix: updates autogen files 2026-03-24 14:34:54 +01:00
Vito
8899d346af Merge remote-tracking branch 'origin/tapanito/lending-fix-amendment' into tapanito/vault-block-deposit 2026-03-24 13:03:51 +01:00
Vito
df8bfbe5af fix: errors introduced post-merge 2026-03-24 12:37:06 +01:00
Vito
347d1a19ef Merge remote-tracking branch 'origin/develop' into tapanito/lending-fix-amendment 2026-03-24 12:35:50 +01:00
Vito
5fe99dc2ae fixes failing unit-tests 2026-03-23 13:48:30 +01:00
Vito
b970c66a37 fix: address PR review feedback 2026-03-21 17:43:08 +01:00
Vito Tumas
7e5a4c9d04 Merge branch 'tapanito/lending-fix-amendment' into tapanito/vault-block-deposit 2026-03-21 14:42:12 +01:00
Vito Tumas
d65fab27a1 Merge branch 'develop' into tapanito/lending-fix-amendment 2026-03-21 14:39:10 +01:00
Vito Tumas
b5d25c5ab1 Merge branch 'develop' into tapanito/lending-fix-amendment 2026-03-18 18:39:43 +01:00
Vito Tumas
7222150095 refactor: Rename fixLendingProtocolV1_1 to featureLendingProtocolV1_1 (#6527)
Use XRPL_FEATURE macro instead of XRPL_FIX since
LendingProtocolV1_1 is a feature amendment, not a fix.
Update all references in VaultDelete and related tests.
2026-03-16 09:26:57 +01:00
Vito
68d7555033 Merge remote-tracking branch 'origin/tapanito/lending-fix-amendment' into tapanito/vault-block-deposit 2026-03-09 13:03:27 +01:00
Vito
a67da5c2ed Merge remote-tracking branch 'origin/develop' into tapanito/lending-fix-amendment 2026-03-09 11:34:59 +01:00
Vito Tumas
d2f23b2f5b Merge branch 'develop' into tapanito/lending-fix-amendment 2026-03-05 14:29:35 +01:00
Vito Tumas
4067e5025f Add rounding to Vault invariants (#6217)
Co-authored-by: Ed Hennis <ed@ripple.com>
2026-03-05 10:38:42 +01:00
Vito
cc9dbe2243 fixes typos and improves test coverage 2026-03-04 13:03:51 +01:00
Vito
ebfa659593 fixes formattting issues after merge 2026-03-04 12:09:51 +01:00
Vito Tumas
3deb0de8d7 Merge branch 'tapanito/lending-fix-amendment' into tapanito/vault-block-deposit 2026-03-04 11:29:38 +01:00
Vito Tumas
ed4330a7d6 Merge branch 'develop' into tapanito/lending-fix-amendment 2026-03-04 11:26:33 +01:00
Vito Tumas
feba605998 Merge branch 'develop' into tapanito/lending-fix-amendment 2026-03-03 15:38:14 +01:00
Vito
b322097529 fixes formatting errors 2026-03-03 13:51:15 +01:00
Vito Tumas
e159d27373 Merge branch 'develop' into tapanito/lending-fix-amendment 2026-03-03 13:48:37 +01:00
Vito
f034ca0844 Merge remote-tracking branch 'origin/tapanito/lending-fix-amendment' into tapanito/vault-block-deposit 2026-02-26 14:43:23 +01:00
Vito Tumas
ba53026006 adds sfMemoData field to VaultDelete transaction (#6356)
* adds sfMemoData field to VaultDelete transaction
2026-02-26 14:13:29 +01:00
Vito Tumas
34773080df Merge branch 'develop' into tapanito/lending-fix-amendment 2026-02-25 13:44:20 +01:00
Vito
2b716eb4c7 adds lsfVaultOwnerCanBlockDeposit flag 2026-02-25 12:44:56 +01:00
Vito
872347224d fixes broken unit-tests 2026-02-24 16:19:11 +01:00
Vito Tumas
b08451c118 Merge branch 'tapanito/lending-fix-amendment' into tapanito/vault-block-deposit 2026-02-24 14:41:25 +01:00
Vito Tumas
3c3bd75991 Merge branch 'develop' into tapanito/lending-fix-amendment 2026-02-24 14:40:31 +01:00
Vito
81e69b9317 Merge branch 'tapanito/lending-fix-amendment' into tapanito/vault-block-deposit 2026-02-24 14:33:10 +01:00
Vito
8732e84e54 refactor: Extract vault helper functions into VaultHelpers module
Move vault share/asset conversion functions (assetsToSharesDeposit,
sharesToAssetsDeposit, assetsToSharesWithdraw, sharesToAssetsWithdraw)
and isVaultInsolvent from View.h/View.cpp into a dedicated
VaultHelpers.h/VaultHelpers.cpp module. Reorder includes in Vault
transactor .cpp files to place own header first. Fix VaultSet flag
validation logic.
2026-02-24 12:56:51 +01:00
Vito Tumas
7459fe454d Merge branch 'develop' into tapanito/lending-fix-amendment 2026-02-23 12:17:17 +01:00
Vito
884530e415 adds missing env.close 2026-02-19 11:59:56 +01:00
Vito
d972071979 Merge branch 'tapanito/lending-fix-amendment' into tapanito/vault-block-deposit 2026-02-19 11:40:38 +01:00
Vito
cd1e8ebbc3 debug attempt 2026-02-19 11:40:03 +01:00
Vito
3cfb5fe56d additional unit test 2026-02-19 11:11:35 +01:00
Vito
106bf48725 Merge branch 'develop' into tapanito/lending-fix-amendment 2026-02-18 18:29:08 +01:00
Vito Tumas
71b9f982bb Merge branch 'tapanito/lending-fix-amendment' into tapanito/vault-block-deposit 2026-02-17 13:51:56 +01:00
Vito Tumas
74c968d4e3 Merge branch 'develop' into tapanito/lending-fix-amendment 2026-02-17 13:51:08 +01:00
Vito
f57b715936 removes redundant check 2026-02-17 13:47:50 +01:00
Vito
82b0d57aac minor code improvements 2026-02-17 13:33:47 +01:00
Vito
087a9c1cf3 add vault deposit logic and tests 2026-02-17 11:58:24 +01:00
Vito
1010866ba0 adds amendment validation in flags and better tests 2026-02-13 15:00:01 +01:00
Vito
a2198146a8 adds BLockDeposit flagsto VaultSet 2026-02-12 17:42:27 +01:00
Vito
c808c46049 adds flag definitions 2026-02-12 15:27:19 +01:00
Vito
167147281c Merge branch 'develop' into tapanito/lending-fix-amendment 2026-02-12 15:22:30 +01:00
Vito Tumas
ba60306610 Merge branch 'develop' into tapanito/lending-fix-amendment 2026-02-11 17:46:20 +01:00
Vito Tumas
6674500896 Merge branch 'develop' into tapanito/lending-fix-amendment 2026-02-10 11:48:23 +01:00
Vito
c5d7ebe93d restores missing linebreak 2026-02-05 10:24:14 +01:00
Ed Hennis
d0b5ca9dab Merge branch 'develop' into tapanito/lending-fix-amendment 2026-02-04 18:21:55 -04:00
Vito
5e51893e9b fixes a typo 2026-02-04 11:31:58 +01:00
Vito
3422c11d02 adds lending v1.1 fix amendment 2026-02-04 11:30:41 +01:00
16 changed files with 807 additions and 18 deletions

View File

@@ -99,4 +99,15 @@ sharesToAssetsWithdraw(
[[nodiscard]] bool
isSoleShareholder(ReadView const& view, AccountID const& account, SLE::const_ref issuance);
/** 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(SLE::const_ref vault, SLE::const_ref shareIssuance);
} // namespace xrpl

View File

@@ -198,7 +198,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) \

View File

@@ -185,7 +185,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, \
@@ -215,7 +221,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.

View File

@@ -15,6 +15,7 @@
// Add new amendments to the top of this list.
// Keep it sorted in reverse chronological order.
XRPL_FEATURE(LendingProtocolV1_1, Supported::No, VoteBehavior::DefaultNo)
XRPL_FIX (Cleanup3_2_0, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(MPTokensV2, Supported::No, VoteBehavior::DefaultNo)
XRPL_FIX (Cleanup3_1_3, Supported::Yes, VoteBehavior::DefaultYes)

View File

@@ -887,6 +887,7 @@ TRANSACTION(ttVAULT_DELETE, 67, VaultDelete,
MustDeleteAcct | DestroyMptIssuance | MustModifyVault,
({
{sfVaultID, SoeRequired},
{sfMemoData, SoeOptional},
}))
/** This transaction trades assets for shares with a vault. */

View File

@@ -57,6 +57,32 @@ public:
{
return this->tx_->at(sfVaultID);
}
/**
* @brief Get sfMemoData (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getMemoData() const
{
if (hasMemoData())
{
return this->tx_->at(sfMemoData);
}
return std::nullopt;
}
/**
* @brief Check if sfMemoData is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasMemoData() const
{
return this->tx_->isFieldPresent(sfMemoData);
}
};
/**
@@ -112,6 +138,17 @@ public:
return *this;
}
/**
* @brief Set sfMemoData (SoeOptional)
* @return Reference to this builder for method chaining.
*/
VaultDeleteBuilder&
setMemoData(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfMemoData] = value;
return *this;
}
/**
* @brief Build and return the VaultDelete wrapper.
* @param publicKey The public key for signing.

View File

@@ -13,6 +13,9 @@ public:
{
}
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static bool
checkExtraFeatures(PreflightContext const& ctx);

View File

@@ -137,4 +137,18 @@ isSoleShareholder(ReadView const& view, AccountID const& account, SLE::const_ref
return sleToken->getFieldU64(sfMPTAmount) == outstanding;
}
[[nodiscard]] bool
isVaultInsolvent(SLE::const_ref vault, SLE::const_ref 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

View File

@@ -48,7 +48,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
@@ -178,6 +181,7 @@ VaultCreate::doApply()
std::uint32_t mptFlags = 0;
if (!tx.isFlag(tfVaultShareNonTransferable))
mptFlags |= (lsfMPTCanEscrow | lsfMPTCanTrade | lsfMPTCanTransfer);
if (tx.isFlag(tfVaultPrivate))
mptFlags |= lsfMPTRequireAuth;
@@ -216,7 +220,13 @@ VaultCreate::doApply()
auto const& mptIssuanceID = *maybeShare;
vault->setFieldIssue(sfAsset, STIssue{sfAsset, asset});
vault->at(sfFlags) = tx.getFlags() & 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) = accountID_;
vault->at(sfAccount) = pseudoId;
@@ -249,7 +259,7 @@ VaultCreate::doApply()
return err;
// If the vault is private, set the authorized flag for the vault owner
if (tx.isFlag(tfVaultPrivate))
if (vault->isFlag(lsfVaultPrivate))
{
if (auto const err = authorizeMPToken(
view(), preFeeBalance_, mptIssuanceID, pseudoId, ctx_.journal, {}, accountID_);

View File

@@ -7,8 +7,10 @@
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
@@ -28,6 +30,14 @@ VaultDelete::preflight(PreflightContext const& ctx)
return temMALFORMED;
}
if (ctx.tx.isFieldPresent(sfMemoData) && !ctx.rules.enabled(featureLendingProtocolV1_1))
return temDISABLED;
// The sfMemoData field is an optional field used to record the deletion reason.
if (auto const data = ctx.tx[~sfMemoData];
data && !validDataLength(data, kMaxDataPayloadLength))
return temMALFORMED;
return tesSUCCESS;
}

View File

@@ -90,8 +90,8 @@ 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.";
@@ -99,7 +99,7 @@ VaultDeposit::preclaim(PreclaimContext const& ctx)
// 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.";
@@ -107,6 +107,24 @@ VaultDeposit::preclaim(PreclaimContext const& ctx)
// 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))
{
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;
}
}
// Cannot deposit inside Vault an Asset frozen for the depositor
if (isFrozen(ctx.view, account, vaultAsset))
return vaultAsset.holds<Issue>() ? tecFROZEN : tecLOCKED;
@@ -117,7 +135,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

View File

@@ -12,9 +12,12 @@
#include <xrpl/protocol/STTakesAsset.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/Transactor.h>
#include <cstdint>
namespace xrpl {
bool
@@ -23,6 +26,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);
return atLeastOneFieldPresent || ((ctx.tx.getFlags() & expectedFlags) != 0u);
}
NotTEC
VaultSet::preflight(PreflightContext const& ctx)
{
@@ -50,13 +76,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;
}
@@ -110,6 +142,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))
{
JLOG(ctx.j.debug()) << "VaultSet: vault deposit is already unblocked";
return tecNO_PERMISSION;
}
}
return tesSUCCESS;
}
@@ -167,6 +222,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.

View File

@@ -707,6 +707,14 @@ class Vault_test : public beast::unit_test::Suite
tx[sfFlags] = tfClearDeepFreeze;
env(tx, Ter{temINVALID_FLAG});
{
env.disableFeature(featureLendingProtocolV1_1);
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
tx[sfFlags] = tfVaultOwnerCanBlockDeposit;
env(tx, Ter(temINVALID_FLAG));
env.enableFeature(featureLendingProtocolV1_1);
}
{
auto tx = vault.set({.owner = owner, .id = keylet.key});
tx[sfFlags] = tfClearDeepFreeze;
@@ -988,7 +996,7 @@ class Vault_test : public beast::unit_test::Suite
{
auto tx = vault.set({.owner = owner, .id = keylet.key});
tx[sfAssetsMaximum] = kNegativeAmount(asset).number();
env(tx, Ter{temMALFORMED});
env(tx, Ter(temMALFORMED));
}
});
@@ -1013,13 +1021,114 @@ class Vault_test : public beast::unit_test::Suite
testCase(
[&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
testcase("invalid set immutable flag");
testcase("invalid withdraw amount");
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
{
auto tx = vault.withdraw(
{.depositor = owner, .id = keylet.key, .amount = kNegativeAmount(asset)});
env(tx, Ter(temBAD_AMOUNT));
}
{
auto tx =
vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(0)});
env(tx, Ter(temBAD_AMOUNT));
}
});
testCase(
[&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
testcase("set nothing updated");
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
{
auto tx = vault.set({.owner = owner, .id = keylet.key});
tx[sfFlags] = tfVaultPrivate;
env(tx, Ter(temMALFORMED));
}
});
testCase(
[&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
testcase("create with invalid metadata");
auto [tx1, keylet] = vault.create({.owner = owner, .asset = asset});
{
auto tx = tx1;
tx[sfMPTokenMetadata] = "";
env(tx, Ter(temMALFORMED));
}
{
auto tx = tx1;
// This metadata is for the share token.
// A hexadecimal string of 1025 bytes.
tx[sfMPTokenMetadata] = std::string(2050, 'B');
env(tx, Ter(temMALFORMED));
}
});
testCase(
[&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
testcase("set negative maximum");
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
{
auto tx = vault.set({.owner = owner, .id = keylet.key});
tx[sfAssetsMaximum] = kNegativeAmount(asset).number();
env(tx, Ter(temMALFORMED));
}
});
testCase(
[&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
testcase("invalid deposit amount");
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
{
auto tx = vault.deposit(
{.depositor = owner, .id = keylet.key, .amount = kNegativeAmount(asset)});
env(tx, Ter(temBAD_AMOUNT));
}
{
auto tx =
vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(0)});
env(tx, Ter(temBAD_AMOUNT));
}
});
testCase(
[&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
testcase("set flags fail without featureLendingProtocolV1_1");
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
{
env.disableFeature(featureLendingProtocolV1_1);
env(vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock}),
Ter(temINVALID_FLAG));
env(vault.set(
{.owner = owner, .id = keylet.key, .flags = tfVaultDepositUnblock}),
Ter(temINVALID_FLAG));
env.enableFeature(featureLendingProtocolV1_1);
}
});
testCase(
[&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
testcase("invalid set flag combination");
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
{
auto tx = vault.set({.owner = owner, .id = keylet.key});
tx[sfFlags] = tfVaultDepositBlock | tfVaultDepositUnblock;
env(tx, Ter(temINVALID_FLAG));
}
});
@@ -2276,6 +2385,106 @@ class Vault_test : public beast::unit_test::Suite
env(vault.del({.owner = owner, .id = keylet.key}));
});
testCase([&, this](
Env& env,
Account const&,
Account const& owner,
Account const& depositor,
PrettyAsset const& asset,
Vault& vault,
MPTTester const& mptt) {
testcase("MPT insolvent vault blocks deposits");
auto const depositAmount = asset(20);
auto const [tx, vaultKeylet] = vault.create({.owner = owner, .asset = asset});
env(tx);
env.close();
// First deposit assets to later show that withdrawals are not blocked
{
auto const tx = vault.deposit(
{.depositor = depositor, .id = vaultKeylet.key, .amount = depositAmount});
env(tx, Ter(tesSUCCESS));
env.close();
}
auto const& brokerKeylet = keylet::loanbroker(owner.id(), env.seq(owner));
auto const& loanKeylet = keylet::loan(brokerKeylet.key, 1);
// Create a LoanBroker and a Loan, to drain the vault
{
using namespace loanBroker;
using namespace loan;
env(set(owner, vaultKeylet.key));
env.close();
// Create a simple Loan for the full amount of Vault assets
env(set(depositor, brokerKeylet.key, depositAmount.value()),
loan::kInterestRate(TenthBips32(0)),
kPaymentInterval(120),
kPaymentTotal(1),
Sig(sfCounterpartySignature, owner),
Fee(env.current()->fees().base * 2),
Ter(tesSUCCESS));
env.close(std::chrono::seconds{120 + 60});
env(manage(owner, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
env.close();
auto const sleVault = env.le(vaultKeylet);
if (!BEAST_EXPECT(sleVault))
return;
auto const sleIssuance = env.le(keylet::mptIssuance(sleVault->at(sfShareMPTID)));
if (!BEAST_EXPECT(sleIssuance))
return;
auto const shareBalance = sleIssuance->at(sfOutstandingAmount);
auto const expectedShares = Number{
depositAmount.number().mantissa(),
depositAmount.number().exponent() + sleVault->at(sfScale)};
// verify that the vault is insolvent
if (!BEAST_EXPECT(
sleVault->at(sfAssetsTotal) == 0 && sleVault->at(sfAssetsAvailable) == 0 &&
shareBalance == expectedShares))
return;
}
// The vault is insolvent, deposit must fail
{
auto const tx = vault.deposit(
{.depositor = depositor, .id = vaultKeylet.key, .amount = asset(20)});
env(tx, Ter(tecLOCKED));
env.close();
}
// Clean up the vault to delete it
{
auto const sleVault = env.le(vaultKeylet);
if (!BEAST_EXPECT(sleVault))
return;
Asset const share = sleVault->at(sfShareMPTID);
env(vault.clawback(
{.issuer = owner,
.id = vaultKeylet.key,
.holder = depositor,
.amount = share(0)}),
Ter(tesSUCCESS));
env.close();
}
{
env(loan::del(owner, loanKeylet.key), Ter(tesSUCCESS));
env(loanBroker::del(owner, brokerKeylet.key), Ter(tesSUCCESS));
env(vault.del({.owner = owner, .id = vaultKeylet.key}));
env.close();
}
});
{
testcase("MPT non-transferable: pre-fixCleanup3_2_0 withdraw blocked");
@@ -3330,10 +3539,111 @@ class Vault_test : public beast::unit_test::Suite
env(vault.del({.owner = owner, .id = keylet.key}));
env.close();
});
testCase([&, this](
Env& env,
Account const& owner,
Account const& issuer,
Account const&,
auto vaultAccount,
Vault& vault,
PrettyAsset const& asset,
auto&&...) {
testcase("IOU insolvent vault blocks deposits");
auto const depositAmount = asset(20);
auto const [tx, vaultKeylet] = vault.create({.owner = owner, .asset = asset});
env(tx);
env.close();
// First deposit assets to later show that withdrawals are not blocked
{
auto const tx = vault.deposit(
{.depositor = issuer, .id = vaultKeylet.key, .amount = depositAmount});
env(tx, Ter(tesSUCCESS));
env.close();
}
auto const brokerKeylet = keylet::loanbroker(owner.id(), env.seq(owner));
auto const loanKeylet = keylet::loan(brokerKeylet.key, 1);
// Create a LoanBroker and a Loan, to drain the vault
{
using namespace loanBroker;
using namespace loan;
env(set(owner, vaultKeylet.key), Ter(tesSUCCESS));
env.close();
// Create a simple Loan for the full amount of Vault assets
env(set(issuer, brokerKeylet.key, depositAmount.value()),
loan::kInterestRate(TenthBips32(0)),
kPaymentInterval(120),
kPaymentTotal(1),
Sig(sfCounterpartySignature, owner),
Fee(env.current()->fees().base * 2),
Ter(tesSUCCESS));
env.close(std::chrono::seconds{120 + 60});
env(manage(owner, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
env.close();
auto const sleVault = env.le(vaultKeylet);
if (!BEAST_EXPECT(sleVault))
return;
auto const sleIssuance = env.le(keylet::mptIssuance(sleVault->at(sfShareMPTID)));
if (!BEAST_EXPECT(sleIssuance))
return;
auto const shareBalance = sleIssuance->at(sfOutstandingAmount);
auto const expectedShares = Number{
depositAmount.number().mantissa(),
depositAmount.number().exponent() + sleVault->at(sfScale)};
// verify that the vault is insolvent
if (!BEAST_EXPECT(
sleVault->at(sfAssetsTotal) == 0 && sleVault->at(sfAssetsAvailable) == 0 &&
shareBalance == expectedShares))
return;
}
// The vault is insolvent, deposit must fail
{
auto const tx = vault.deposit(
{.depositor = issuer, .id = vaultKeylet.key, .amount = asset(20)});
env(tx, Ter(tecLOCKED));
env.close();
}
// Clean up the vault to delete it
{
auto const sleVault = env.le(vaultKeylet);
if (!BEAST_EXPECT(sleVault))
return;
Asset const share = sleVault->at(sfShareMPTID);
env(vault.clawback(
{.issuer = owner,
.id = vaultKeylet.key,
.holder = issuer,
.amount = share(0)}),
Ter(tesSUCCESS));
env.close();
}
{
env(loan::del(owner, loanKeylet.key), Ter(tesSUCCESS));
env(loanBroker::del(owner, brokerKeylet.key), Ter(tesSUCCESS));
env(vault.del({.owner = owner, .id = vaultKeylet.key}));
env.close();
}
});
}
void
testWithDomainCheck()
testPrivateVault()
{
using namespace test::jtx;
@@ -3364,7 +3674,10 @@ class Vault_test : public beast::unit_test::Suite
env(pay(issuer, charlie, asset(5)));
env.close();
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset, .flags = tfVaultPrivate});
auto [tx, keylet] = vault.create(
{.owner = owner,
.asset = asset,
.flags = tfVaultPrivate | tfVaultOwnerCanBlockDeposit});
env(tx);
env.close();
BEAST_EXPECT(env.le(keylet));
@@ -3389,6 +3702,28 @@ class Vault_test : public beast::unit_test::Suite
env(tx, Ter{tecOBJECT_NOT_FOUND});
}
{
testcase("blocking a private vault does not change lsfVaultPrivate flag");
auto tx = vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock});
env(tx, Ter(tesSUCCESS));
auto const sleVault = env.le(keylet);
if (!BEAST_EXPECT(sleVault))
return;
BEAST_EXPECT(sleVault->isFlag(lsfVaultDepositBlocked));
BEAST_EXPECT(sleVault->isFlag(lsfVaultPrivate));
}
{
testcase("unblocking a private vault does not change lsfVaultPrivate flag");
auto tx = vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositUnblock});
env(tx, Ter(tesSUCCESS));
auto const sleVault = env.le(keylet);
if (!BEAST_EXPECT(sleVault))
return;
BEAST_EXPECT(!sleVault->isFlag(lsfVaultDepositBlocked));
BEAST_EXPECT(sleVault->isFlag(lsfVaultPrivate));
}
{
testcase("private vault set domainId");
@@ -8027,6 +8362,230 @@ class Vault_test : public beast::unit_test::Suite
}
}
void
testVaultDepositBlockGeneral()
{
using namespace test::jtx;
Env env{*this};
Account const owner{"owner"};
Account const other{"other"};
env.fund(XRP(100'000'000), owner, other);
Vault vault{env};
PrettyAsset const asset = xrpIssue();
std::string const prefix = "VaultDepositBlock: ";
auto const blockVault = [&](TER expectedTer, Keylet const& keylet) {
env(vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositBlock}),
Ter(expectedTer));
};
auto const unblockVault = [&](TER expectedTer, Keylet const& keylet) {
env(vault.set({.owner = owner, .id = keylet.key, .flags = tfVaultDepositUnblock}),
Ter(expectedTer));
};
// Blocking Vault with the amendment disabled fails
{
testcase(prefix + "block/unblock fails when amendment is disabled");
env.disableFeature(featureLendingProtocolV1_1);
auto const [tx, keylet] = vault.create(
{.owner = owner, .asset = asset, .flags = tfVaultOwnerCanBlockDeposit});
env(tx, Ter(temINVALID_FLAG));
env.close();
blockVault(temINVALID_FLAG, keylet);
unblockVault(temINVALID_FLAG, keylet);
env.enableFeature(featureLendingProtocolV1_1);
}
// Block Vault deposits fails if the vault is not configured to allow blocking deposits
{
testcase(prefix + "block/unblock fails when vault is not configured");
auto const [tx, keylet] = vault.create({.owner = owner, .asset = asset});
env(tx);
env.close();
blockVault(tecNO_PERMISSION, keylet);
unblockVault(tecNO_PERMISSION, keylet);
env(vault.del({.owner = owner, .id = keylet.key}), Ter(tesSUCCESS));
env.close();
}
auto const [tx, keylet] =
vault.create({.owner = owner, .asset = asset, .flags = tfVaultOwnerCanBlockDeposit});
env(tx);
env.close();
{
testcase(prefix + "block/unblock succeeds");
// deposit assets to show that blocking deposit does not block withdrawals
env(vault.deposit({
.depositor = owner,
.id = keylet.key,
.amount = XRP(10'000),
}),
Ter(tesSUCCESS));
env(vault.deposit({
.depositor = other,
.id = keylet.key,
.amount = XRP(10'000),
}),
Ter(tesSUCCESS));
blockVault(tesSUCCESS, keylet);
// Owner is blocked from depositing to the vault
env(vault.deposit({
.depositor = owner,
.id = keylet.key,
.amount = XRP(10'000),
}),
Ter(tecNO_PERMISSION));
// Other accounts are also blocked from depositing to the vault
env(vault.deposit({
.depositor = other,
.id = keylet.key,
.amount = XRP(10'000),
}),
Ter(tecNO_PERMISSION));
// Block vault withdrawal works as normal
env(vault.withdraw({
.depositor = owner,
.id = keylet.key,
.amount = XRP(10'000),
}),
Ter(tesSUCCESS));
env(vault.withdraw({
.depositor = other,
.id = keylet.key,
.amount = XRP(10'000),
}),
Ter(tesSUCCESS));
unblockVault(tesSUCCESS, keylet);
env(vault.deposit({
.depositor = owner,
.id = keylet.key,
.amount = XRP(10'000),
}),
Ter(tesSUCCESS));
env(vault.deposit({
.depositor = other,
.id = keylet.key,
.amount = XRP(10'000),
}),
Ter(tesSUCCESS));
// Withdraw to keep the vault empty
env(vault.withdraw({
.depositor = owner,
.id = keylet.key,
.amount = XRP(10'000),
}),
Ter(tesSUCCESS));
env(vault.withdraw({
.depositor = other,
.id = keylet.key,
.amount = XRP(10'000),
}),
Ter(tesSUCCESS));
}
{
testcase(prefix + "block/unblock fails when caller is not owner");
env(vault.set({.owner = other, .id = keylet.key, .flags = tfVaultDepositBlock}),
Ter(tecNO_PERMISSION));
blockVault(tesSUCCESS, keylet);
env(vault.set({.owner = other, .id = keylet.key, .flags = tfVaultDepositUnblock}),
Ter(tecNO_PERMISSION));
unblockVault(tesSUCCESS, keylet);
}
{
testcase(prefix + "unblock fails when vault is already unblocked");
unblockVault(tecNO_PERMISSION, keylet);
}
{
testcase(prefix + "block fails when vault is already blocked");
blockVault(tesSUCCESS, keylet);
blockVault(tecNO_PERMISSION, keylet);
unblockVault(tesSUCCESS, keylet);
}
env(vault.del({.owner = owner, .id = keylet.key}));
}
void
testVaultDeleteData()
{
using namespace test::jtx;
Env env{*this};
Account const owner{"owner"};
env.fund(XRP(1'000'000), owner);
env.close();
Vault const vault{env};
auto const keylet = keylet::vault(owner.id(), 1);
auto delTx = vault.del({.owner = owner, .id = keylet.key});
// Test VaultDelete with featureLendingProtocolV1_1 disabled
// Transaction fails if the data field is provided
{
testcase("VaultDelete data featureLendingProtocolV1_1 disabled");
env.disableFeature(featureLendingProtocolV1_1);
delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength, 'A'));
env(delTx, Ter(temDISABLED));
env.close();
env.enableFeature(featureLendingProtocolV1_1);
}
// Transaction fails if the data field is too large
{
testcase("VaultDelete data featureLendingProtocolV1_1 enabled data too large");
delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength + 1, 'A'));
env(delTx, Ter(temMALFORMED));
env.close();
}
// Transaction fails if the data field is set, but is empty
{
testcase("VaultDelete data featureLendingProtocolV1_1 enabled data empty");
delTx[sfMemoData] = strHex(std::string(0, 'A'));
env(delTx, Ter(temMALFORMED));
env.close();
}
{
testcase("VaultDelete data featureLendingProtocolV1_1 enabled data valid");
PrettyAsset const xrpAsset = xrpIssue();
auto [tx, keylet] = vault.create({.owner = owner, .asset = xrpAsset});
env(tx, Ter(tesSUCCESS));
env.close();
// Recreate the transaction as the vault keylet changed
auto delTx = vault.del({.owner = owner, .id = keylet.key});
delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength, 'A'));
env(delTx, Ter(tesSUCCESS));
env.close();
}
}
public:
void
run() override
@@ -8045,7 +8604,7 @@ public:
testCreateFailMPT();
testWithMPT();
testWithIOU();
testWithDomainCheck();
testPrivateVault();
testWithDomainChecXRP();
testNonTransferableShares();
testFailedPseudoAccount();
@@ -8055,6 +8614,8 @@ public:
testVaultClawbackAssets();
testVaultEscrowedMPT();
testAssetsMaximum();
testVaultDepositBlockGeneral();
testVaultDeleteData();
testBug6LimitBypassWithShares();
testRemoveEmptyHoldingLockedAmount();

View File

@@ -36,6 +36,8 @@ Vault::set(SetArgs const& args)
jv[jss::TransactionType] = jss::VaultSet;
jv[jss::Account] = args.owner.human();
jv[sfVaultID] = to_string(args.id);
if (args.flags)
jv[jss::Flags] = *args.flags;
return jv;
}

View File

@@ -35,6 +35,7 @@ struct Vault
{
Account owner;
uint256 id;
std::optional<std::uint32_t> flags = std::nullopt;
};
static json::Value

View File

@@ -30,6 +30,7 @@ TEST(TransactionsVaultDeleteTests, BuilderSettersRoundTrip)
// Transaction-specific field values
auto const vaultIDValue = canonical_UINT256();
auto const memoDataValue = canonical_VL();
VaultDeleteBuilder builder{
accountValue,
@@ -39,6 +40,7 @@ TEST(TransactionsVaultDeleteTests, BuilderSettersRoundTrip)
};
// Set optional fields
builder.setMemoData(memoDataValue);
auto tx = builder.build(publicKey, secretKey);
@@ -62,6 +64,14 @@ TEST(TransactionsVaultDeleteTests, BuilderSettersRoundTrip)
}
// Verify optional fields
{
auto const& expected = memoDataValue;
auto const actualOpt = tx.getMemoData();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMemoData should be present";
expectEqualField(expected, *actualOpt, "sfMemoData");
EXPECT_TRUE(tx.hasMemoData());
}
}
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
@@ -79,6 +89,7 @@ TEST(TransactionsVaultDeleteTests, BuilderFromStTxRoundTrip)
// Transaction-specific field values
auto const vaultIDValue = canonical_UINT256();
auto const memoDataValue = canonical_VL();
// Build an initial transaction
VaultDeleteBuilder initialBuilder{
@@ -88,6 +99,7 @@ TEST(TransactionsVaultDeleteTests, BuilderFromStTxRoundTrip)
feeValue
};
initialBuilder.setMemoData(memoDataValue);
auto initialTx = initialBuilder.build(publicKey, secretKey);
@@ -112,6 +124,13 @@ TEST(TransactionsVaultDeleteTests, BuilderFromStTxRoundTrip)
}
// Verify optional fields
{
auto const& expected = memoDataValue;
auto const actualOpt = rebuiltTx.getMemoData();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMemoData should be present";
expectEqualField(expected, *actualOpt, "sfMemoData");
}
}
// 3) Verify wrapper throws when constructed from wrong transaction type.
@@ -142,5 +161,35 @@ TEST(TransactionsVaultDeleteTests, BuilderThrowsOnWrongTxType)
EXPECT_THROW(VaultDeleteBuilder{wrongTx.getSTTx()}, std::runtime_error);
}
// 5) Build with only required fields and verify optional fields return nullopt.
TEST(TransactionsVaultDeleteTests, OptionalFieldsReturnNullopt)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testVaultDeleteNullopt"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 3;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific required field values
auto const vaultIDValue = canonical_UINT256();
VaultDeleteBuilder builder{
accountValue,
vaultIDValue,
sequenceValue,
feeValue
};
// Do NOT set optional fields
auto tx = builder.build(publicKey, secretKey);
// Verify optional fields are not present
EXPECT_FALSE(tx.hasMemoData());
EXPECT_FALSE(tx.getMemoData().has_value());
}
}