Compare commits

...

51 Commits

Author SHA1 Message Date
Vito
ef92373c9f fix: post merge issues 2026-06-08 13:06:46 +02:00
Vito
605e6166d5 fix: Replace adjust/args with kAdjust/kArgs in Invariants_test vault tests
The lending-fix-amendment renamed the adjust/args helper lambdas to
kAdjust/kArgs. Three call sites in the vault donation invariant tests
still referenced the old names, causing build failures.
2026-06-08 12:42:02 +02:00
Vito
af289221f7 Merge tapanito/lending-fix-amendment into tapanito/vault-donation
Conflicts resolved in:
- src/libxrpl/ledger/helpers/VaultHelpers.cpp: combined granular includes
  from lending-fix-amendment with TxFlags.h needed by isVaultDonate;
  kept both isVaultDonate (vault-donation) and isSoleShareholder (lending-fix)
- src/libxrpl/tx/transactors/vault/VaultDeposit.cpp: kept both getFlagsMask
  (vault-donation) and roundToVaultScale (lending-fix); preserved donate path
  logic (assets deposited without share exchange) with accountID_ rename and
  beast::kZero from lending-fix-amendment
- src/libxrpl/tx/invariants/VaultInvariant.cpp: merged includes; preserved
  isDonate invariant checks (donation must not change shares) inside the new
  isDonate/else structure; added makeDelta body and split computeCoarsestScale
  from lending-fix-amendment; used kZero throughout
- src/test/jtx/impl/vault.cpp: used toJson rename from lending-fix-amendment
  while preserving flags support added by vault-donation
2026-06-08 12:17:03 +02:00
Vito
beb8a1872d fix: Regenerate protocol autogenerated files 2026-06-08 12:07: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
934b4b03f6 Merge remote-tracking branch 'origin/tapanito/lending-fix-amendment' into tapanito/vault-donation 2026-03-31 14:54:26 +02:00
Vito
2c0d1c9151 fix: fix build errors 2026-03-31 14:54:14 +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
f5c04c421b Merge remote-tracking branch 'origin/tapanito/lending-fix-amendment' into tapanito/vault-donation 2026-03-24 12:59:06 +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
b97461b8e6 fixes clang tidy 2026-03-23 13:43:21 +01:00
Vito
43547fcacc fix: address PR review comments 2026-03-21 17:45:14 +01:00
Vito
d02f534987 refactor: Rename fixLendingProtocolV1_1 to featureLendingProtocolV1_1 and remove THISLINE 2026-03-21 16:17:18 +01:00
Vito
5d538ca59a Merge remote-tracking branch 'origin/tapanito/lending-fix-amendment' into tapanito/vault-donation
# Conflicts:
#	src/libxrpl/tx/transactors/vault/VaultDeposit.cpp
2026-03-21 15:59:41 +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
8b9ff17ef4 Merge remote-tracking branch 'origin/tapanito/lending-fix-amendment' into tapanito/vault-donation 2026-03-09 13:41:14 +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
662325ace0 adds aditional unit tests 2026-03-04 13:44:03 +01:00
Vito
07a6f77ed2 adds missing inclde 2026-03-04 12:08:27 +01:00
Vito
e4a716f260 Merge branch 'tapanito/lending-fix-amendment' into tapanito/vault-donation 2026-03-04 11:52:57 +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 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 Tumas
3029d10102 Merge branch 'tapanito/lending-fix-amendment' into tapanito/vault-donation 2026-02-24 14:41:07 +01:00
Vito Tumas
3c3bd75991 Merge branch 'develop' into tapanito/lending-fix-amendment 2026-02-24 14:40:31 +01:00
Vito
c89dd9f0a3 Merge branch 'tapanito/lending-fix-amendment' into tapanito/vault-donation 2026-02-24 14:36:51 +01:00
Vito Tumas
7459fe454d Merge branch 'develop' into tapanito/lending-fix-amendment 2026-02-23 12:17:17 +01:00
Vito
3a0cd45f51 Merge branch 'tapanito/lending-fix-amendment' into tapanito/vault-donation 2026-02-19 12:47:57 +01:00
Vito
106bf48725 Merge branch 'develop' into tapanito/lending-fix-amendment 2026-02-18 18:29:08 +01:00
Vito
79be4717f5 adds vault donation feature 2026-02-18 17:59:08 +01:00
Vito Tumas
74c968d4e3 Merge branch 'develop' into tapanito/lending-fix-amendment 2026-02-17 13:51:08 +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
15 changed files with 600 additions and 67 deletions

View File

@@ -4,6 +4,7 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <optional>
@@ -99,4 +100,7 @@ sharesToAssetsWithdraw(
[[nodiscard]] bool
isSoleShareholder(ReadView const& view, AccountID const& account, SLE::const_ref issuance);
[[nodiscard]] bool
isVaultDonate(Rules const& rules, STTx const& tx);
} // namespace xrpl

View File

@@ -188,6 +188,10 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal;
TF_FLAG(tfVaultShareNonTransferable, 0x00020000), \
MASK_ADJ(0)) \
\
TRANSACTION(VaultDeposit, \
TF_FLAG(tfVaultDonate, 0x00010000), \
MASK_ADJ(0)) \
\
TRANSACTION(Batch, \
TF_FLAG(tfAllOrNothing, 0x00010000) \
TF_FLAG(tfOnlyOne, 0x00020000) \

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 NotTEC
preflight(PreflightContext const& ctx);

View File

@@ -4,12 +4,15 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TxFlags.h>
#include <cstdint>
#include <optional>
@@ -117,6 +120,12 @@ sharesToAssetsWithdraw(
return assets;
}
[[nodiscard]] bool
isVaultDonate(Rules const& rules, STTx const& tx)
{
return rules.enabled(featureLendingProtocolV1_1) && tx.isFlag(tfVaultDonate);
}
[[nodiscard]] bool
isSoleShareholder(ReadView const& view, AccountID const& account, SLE::const_ref issuance)
{

View File

@@ -6,6 +6,7 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
@@ -524,10 +525,14 @@ ValidVault::finalize(
return std::nullopt;
}();
if (!beforeShares &&
(tx.getTxnType() == ttVAULT_DEPOSIT || //
tx.getTxnType() == ttVAULT_WITHDRAW || //
tx.getTxnType() == ttVAULT_CLAWBACK))
bool const isDonate = isVaultDonate(view.rules(), tx);
bool const shouldUpdateShares =
// Vault Asset donation is the only operation that can succeed without updating shares
((tx.getTxnType() == ttVAULT_DEPOSIT && !isDonate) || //
tx.getTxnType() == ttVAULT_WITHDRAW || //
tx.getTxnType() == ttVAULT_CLAWBACK);
if (!beforeShares && shouldUpdateShares)
{
JLOG(j.fatal()) << "Invariant failed: vault operation succeeded "
"without updating shares";
@@ -738,34 +743,57 @@ ValidVault::finalize(
result = false;
}
auto const maybeAccDeltaShares = deltaShares(tx[sfAccount]);
if (!maybeAccDeltaShares)
// If assets are donated, check share invariants
if (isDonate)
{
JLOG(j.fatal()) << "Invariant failed: deposit must change depositor shares";
return false; // That's all we can do
}
// We don't round shares, they are integral MPT
auto const& accountDeltaShares = *maybeAccDeltaShares;
if (accountDeltaShares.delta <= kZero)
{
JLOG(j.fatal()) << "Invariant failed: deposit must increase depositor shares";
result = false;
}
auto const accountDeltaShares = deltaShares(tx[sfAccount]);
if (accountDeltaShares)
{
JLOG(j.fatal()) << //
"Invariant failed: donation must not change depositor shares";
return false; // That's all we can do
}
auto const maybeVaultDeltaShares = deltaShares(afterVault.pseudoId);
if (!maybeVaultDeltaShares || maybeVaultDeltaShares->delta == kZero)
{
JLOG(j.fatal()) << "Invariant failed: deposit must change vault shares";
return false; // That's all we can do
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
if (vaultDeltaShares)
{
JLOG(j.fatal()) << //
"Invariant failed: donation must not change vault shares";
return false; // That's all we can do
}
}
// We don't round shares, they are integral MPT
auto const& vaultDeltaShares = *maybeVaultDeltaShares;
if (vaultDeltaShares.delta * -1 != accountDeltaShares.delta)
else
{
JLOG(j.fatal()) << "Invariant failed: " << //
"deposit must change depositor and vault shares by equal amount";
result = false;
auto const maybeAccDeltaShares = deltaShares(tx[sfAccount]);
if (!maybeAccDeltaShares)
{
JLOG(j.fatal()) << "Invariant failed: deposit must change depositor shares";
return false; // That's all we can do
}
// We don't need to round shares, they are integral MPT
auto const& accountDeltaShares = *maybeAccDeltaShares;
if (accountDeltaShares.delta <= kZero)
{
JLOG(j.fatal())
<< "Invariant failed: deposit must increase depositor shares";
result = false;
}
auto const maybeVaultDeltaShares = deltaShares(afterVault.pseudoId);
if (!maybeVaultDeltaShares || maybeVaultDeltaShares->delta == kZero)
{
JLOG(j.fatal()) << "Invariant failed: deposit must change vault shares";
return false; // That's all we can do
}
// We don't need to round shares, they are integral MPT
auto const& vaultDeltaShares = *maybeVaultDeltaShares;
if (vaultDeltaShares.delta * -1 != accountDeltaShares.delta)
{
JLOG(j.fatal()) << "Invariant failed: " << //
"deposit must change depositor and vault shares by equal amount";
result = false;
}
}
auto const assetTotalDelta = roundToAsset(

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

@@ -20,13 +20,24 @@
#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>
#include <stdexcept>
namespace xrpl {
std::uint32_t
VaultDeposit::getFlagsMask(PreflightContext const& ctx)
{
if (ctx.rules.enabled(featureLendingProtocolV1_1))
return tfVaultDepositMask;
return tfVaultDepositMask | tfVaultDonate;
}
[[nodiscard]]
static STAmount
roundToVaultScale(STAmount const& amount, SLE::const_ref vault)
@@ -99,6 +110,22 @@ VaultDeposit::preclaim(PreclaimContext const& ctx)
// LCOV_EXCL_STOP
}
if (isVaultDonate(ctx.view.rules(), ctx.tx))
{
if (account != vault->at(sfOwner))
{
JLOG(ctx.j.debug()) << "VaultDeposit: only owner can donate to vault.";
return tecNO_PERMISSION;
}
// Cannot donate to a vault with no shares
if (sleIssuance->at(sfOutstandingAmount) == 0)
{
JLOG(ctx.j.debug()) << "VaultDeposit: empty vault cannot receive donations.";
return tecNO_PERMISSION;
}
}
if (sleIssuance->isFlag(lsfMPTLocked))
{
// LCOV_EXCL_START
@@ -215,6 +242,8 @@ VaultDeposit::doApply()
// LCOV_EXCL_STOP
}
auto const isDonate = isVaultDonate(ctx_.view().rules(), ctx_.tx);
auto const& vaultAccount = vault->at(sfAccount);
// Note, vault owner is always authorized
if (vault->isFlag(lsfVaultPrivate) && accountID_ != vault->at(sfOwner))
@@ -254,44 +283,54 @@ VaultDeposit::doApply()
return err;
}
}
STAmount sharesCreated = {vault->at(sfShareMPTID)}, assetsDeposited;
try
if (isDonate)
{
// Compute exchange before transferring any amounts.
{
auto const maybeShares = assetsToSharesDeposit(vault, sleIssuance, amount);
if (!maybeShares)
return tecINTERNAL; // LCOV_EXCL_LINE
sharesCreated = *maybeShares;
}
if (sharesCreated == beast::kZero)
return tecPRECISION_LOSS;
auto const maybeAssets = sharesToAssetsDeposit(vault, sleIssuance, sharesCreated);
if (!maybeAssets)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
if (*maybeAssets > amount)
{
// LCOV_EXCL_START
JLOG(j_.error()) << "VaultDeposit: would take more than offered.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
assetsDeposited = *maybeAssets;
XRPL_ASSERT(
accountID_ == vault->at(sfOwner), "xrpl::VaultDeposit::doApply : account is owner");
assetsDeposited = amount;
}
catch (std::overflow_error const&)
else
{
// It's easy to hit this exception from Number with large enough Scale
// so we avoid spamming the log and only use debug here.
JLOG(j_.debug()) //
<< "VaultDeposit: overflow error with"
<< " scale=" << (int)vault->at(sfScale).value() //
<< ", assetsTotal=" << vault->at(sfAssetsTotal).value()
<< ", sharesTotal=" << sleIssuance->at(sfOutstandingAmount) << ", amount=" << amount;
return tecPATH_DRY;
try
{
// Compute exchange before transferring any amounts.
{
auto const maybeShares = assetsToSharesDeposit(vault, sleIssuance, amount);
if (!maybeShares)
return tecINTERNAL; // LCOV_EXCL_LINE
sharesCreated = *maybeShares;
}
if (sharesCreated == beast::kZero)
return tecPRECISION_LOSS;
auto const maybeAssets = sharesToAssetsDeposit(vault, sleIssuance, sharesCreated);
if (!maybeAssets)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
if (*maybeAssets > amount)
{
// LCOV_EXCL_START
JLOG(j_.error()) << "VaultDeposit: would take more than offered.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
assetsDeposited = *maybeAssets;
}
catch (std::overflow_error const&)
{
// It's easy to hit this exception from Number with large enough Scale
// so we avoid spamming the log and only use debug here.
JLOG(j_.debug()) //
<< "VaultDeposit: overflow error with"
<< " scale=" << (int)vault->at(sfScale).value() //
<< ", assetsTotal=" << vault->at(sfAssetsTotal).value()
<< ", sharesTotal=" << sleIssuance->at(sfOutstandingAmount)
<< ", amount=" << amount;
return tecPATH_DRY;
}
}
XRPL_ASSERT(
@@ -335,11 +374,19 @@ VaultDeposit::doApply()
}
}
// Transfer shares from vault to depositor.
if (auto const ter =
accountSend(view(), vaultAccount, accountID_, sharesCreated, j_, WaiveTransferFee::Yes);
!isTesSuccess(ter))
return ter;
if (isDonate)
{
XRPL_ASSERT(
sharesCreated == beast::kZero, "xrpl::VaultDeposit::doApply : donation issued shares");
}
else
{
// Transfer shares from vault to depositor.
if (auto const ter = accountSend(
view(), vaultAccount, accountID_, sharesCreated, j_, WaiveTransferFee::Yes);
!isTesSuccess(ter))
return ter;
}
associateAsset(*vault, vaultAsset);

View File

@@ -3694,6 +3694,21 @@ class Invariants_test : public beast::unit_test::Suite
precloseXrp,
TxAccount::A2);
doInvariantCheck(
Env{*this, testableAmendments() - featureLendingProtocolV1_1},
{"deposit must change depositor shares"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
sample.accountShares.reset();
}));
},
XRPAmount{},
STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseXrp,
TxAccount::A2);
doInvariantCheck(
{"deposit must change depositor shares"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
@@ -3781,6 +3796,45 @@ class Invariants_test : public beast::unit_test::Suite
precloseXrp,
TxAccount::A2);
doInvariantCheck(
{"donation must not change depositor shares"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
sample.accountShares->amount = 10;
}));
},
XRPAmount{},
STTx{
ttVAULT_DEPOSIT,
[](STObject& tx) {
tx[sfAmount] = XRPAmount(10);
tx[sfFlags] = tfVaultDonate;
}},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseXrp,
TxAccount::A2);
doInvariantCheck(
{"donation must not change vault shares"},
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
auto const keylet = keylet::vault(a1.id(), ac.view().seq());
return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
sample.sharesTotal = 10;
sample.accountShares = std::nullopt;
}));
},
XRPAmount{},
STTx{
ttVAULT_DEPOSIT,
[](STObject& tx) {
tx[sfAmount] = XRPAmount(10);
tx[sfFlags] = tfVaultDonate;
}},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseXrp,
TxAccount::A2);
testcase << "Vault withdrawal";
doInvariantCheck(
{"withdrawal must change vault balance"},

View File

@@ -8027,6 +8027,287 @@ class Vault_test : public beast::unit_test::Suite
}
}
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();
}
}
void
testVaultDepositDonate()
{
using namespace test::jtx;
std::string const prefix = "VaultDeposit donate";
Env env{*this};
Vault const vault{env};
auto const vaultShareBalance = [&](Keylet const& vaultKeylet) {
auto const sleVault = env.le(vaultKeylet);
BEAST_EXPECT(sleVault != nullptr);
auto const sleIssuance = env.le(keylet::mptIssuance(sleVault->at(sfShareMPTID)));
BEAST_EXPECT(sleIssuance != nullptr);
return sleIssuance->at(sfOutstandingAmount);
};
auto const vaultAssetBalance = [&](Keylet const& vaultKeylet) {
auto const sleVault = env.le(vaultKeylet);
BEAST_EXPECT(sleVault != nullptr);
return std::make_pair(sleVault->at(sfAssetsAvailable), sleVault->at(sfAssetsTotal));
};
Account const owner{"owner"};
Account const depositor{"depositor"};
env.fund(XRP(1'000'000), owner, depositor);
env.close();
auto const depositAmount = XRP(10);
auto const [tx, keylet] = vault.create({.owner = owner, .asset = xrpIssue()});
env(tx, Ter(tesSUCCESS));
env.close();
// With featureLendingProtocolV1_1 disabled, donations fail
{
testcase(prefix + " fails with featureLendingProtocolV1_1 disabled");
env.disableFeature(featureLendingProtocolV1_1);
auto const tx = vault.deposit({
.depositor = owner,
.id = keylet.key,
.amount = depositAmount,
.flags = tfVaultDonate,
});
env(tx, Ter(temINVALID_FLAG));
env.enableFeature(featureLendingProtocolV1_1);
env.close();
}
// Donation is not allowed to an empty vault
{
testcase(prefix + " fails to an empty vault");
auto const tx = vault.deposit({
.depositor = owner,
.id = keylet.key,
.amount = depositAmount,
.flags = tfVaultDonate,
});
env(tx, Ter(tecNO_PERMISSION));
env.close();
}
// Further unit tests require assets in the Vault
env(vault.deposit({
.depositor = depositor,
.id = keylet.key,
.amount = depositAmount,
}),
Ter(tesSUCCESS));
env.close();
// Donation is not allowed by a non-owner
{
testcase(prefix + " fails by a non-owner");
auto const tx = vault.deposit({
.depositor = depositor,
.id = keylet.key,
.amount = depositAmount,
.flags = tfVaultDonate,
});
env(tx, Ter(tecNO_PERMISSION));
env.close();
}
// Donation cannot exceed assets maximum
{
testcase(prefix + " cannot exceed assets maximum");
auto tx = vault.set({
.owner = owner,
.id = keylet.key,
});
tx[sfAssetsMaximum] = XRP(30).number();
env(tx, Ter(tesSUCCESS));
tx = vault.deposit({
.depositor = owner,
.id = keylet.key,
.amount = depositAmount + XRP(30),
.flags = tfVaultDonate,
});
env(tx, Ter(tecLIMIT_EXCEEDED));
env.close();
}
{
testcase(prefix + " succeeds");
auto const shareBalance = vaultShareBalance(keylet);
auto const [assetsAvailable, assetsTotal] = vaultAssetBalance(keylet);
auto tx = vault.deposit({
.depositor = owner,
.id = keylet.key,
.amount = depositAmount,
.flags = tfVaultDonate,
});
env(tx, Ter(tesSUCCESS));
env.close();
auto const shareBalanceAfterDeposit = vaultShareBalance(keylet);
auto const [assetsAvailableAfterDeposit, assetsTotalAfterDeposit] =
vaultAssetBalance(keylet);
BEAST_EXPECT(shareBalance == shareBalanceAfterDeposit);
BEAST_EXPECT(assetsAvailable + depositAmount.number() == assetsAvailableAfterDeposit);
BEAST_EXPECT(assetsTotal + depositAmount.number() == assetsTotalAfterDeposit);
auto const sleVault = env.le(keylet);
if (!BEAST_EXPECT(sleVault))
return;
// The depositor can withdraw their assets and the donated amount
Asset const shareAsset(sleVault->at(sfShareMPTID));
tx = vault.withdraw(
{.depositor = depositor, .id = keylet.key, .amount = shareAsset(shareBalance)});
env(tx, Ter(tesSUCCESS));
auto const shareBalanceAfterWithdraw = vaultShareBalance(keylet);
auto const [assetsAvailableAfterWithdraw, assetsTotalAfterWithdraw] =
vaultAssetBalance(keylet);
BEAST_EXPECT(shareBalanceAfterWithdraw == 0);
BEAST_EXPECT(assetsAvailableAfterWithdraw == 0);
BEAST_EXPECT(assetsTotalAfterWithdraw == 0);
}
// Test donation with non-1:1 share ratio.
// A prior donation skews the ratio so that 1 share > 1 asset.
// The donated amount must land exactly, not rounded via shares.
{
testcase(prefix + " succeeds with non-1:1 share ratio");
// Create a fresh vault
auto const [createTx, vk] = vault.create({.owner = owner, .asset = xrpIssue()});
env(createTx, Ter(tesSUCCESS));
env.close();
// Depositor puts in 10 XRP → gets 10 shares at 1:1
env(vault.deposit({
.depositor = depositor,
.id = vk.key,
.amount = XRP(10),
}),
Ter(tesSUCCESS));
env.close();
// Owner donates 7 XRP → ratio becomes 17 assets / 10 shares
env(vault.deposit({
.depositor = owner,
.id = vk.key,
.amount = XRP(7),
.flags = tfVaultDonate,
}),
Ter(tesSUCCESS));
env.close();
auto const sharesAfterFirstDonate = vaultShareBalance(vk);
auto const [availAfterFirstDonate, totalAfterFirstDonate] = vaultAssetBalance(vk);
// Shares unchanged (donation doesn't mint shares)
BEAST_EXPECT(sharesAfterFirstDonate == 10'000'000);
// Assets increased by exactly the donated amount
BEAST_EXPECT(availAfterFirstDonate == 17'000'000);
BEAST_EXPECT(totalAfterFirstDonate == 17'000'000);
// Donate again at the skewed 17:10 ratio — 3 XRP
env(vault.deposit({
.depositor = owner,
.id = vk.key,
.amount = XRP(3),
.flags = tfVaultDonate,
}),
Ter(tesSUCCESS));
env.close();
auto const sharesAfterSecondDonate = vaultShareBalance(vk);
auto const [availAfterSecondDonate, totalAfterSecondDonate] = vaultAssetBalance(vk);
// Shares still unchanged
BEAST_EXPECT(sharesAfterSecondDonate == 10'000'000);
// Assets increased by exactly 3 XRP (20 total)
BEAST_EXPECT(availAfterSecondDonate == 20'000'000);
BEAST_EXPECT(totalAfterSecondDonate == 20'000'000);
// Depositor withdraws all shares — should get all 20 XRP
auto const sleVault = env.le(vk);
if (!BEAST_EXPECT(sleVault))
return;
Asset const shareAsset(sleVault->at(sfShareMPTID));
env(vault.withdraw(
{.depositor = depositor,
.id = vk.key,
.amount = shareAsset(sharesAfterSecondDonate)}),
Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(vaultShareBalance(vk) == 0);
BEAST_EXPECT(vaultAssetBalance(vk).first == 0);
BEAST_EXPECT(vaultAssetBalance(vk).second == 0);
}
}
public:
void
run() override
@@ -8055,6 +8336,8 @@ public:
testVaultClawbackAssets();
testVaultEscrowedMPT();
testAssetsMaximum();
testVaultDepositDonate();
testVaultDeleteData();
testBug6LimitBypassWithShares();
testRemoveEmptyHoldingLockedAmount();

View File

@@ -57,6 +57,8 @@ Vault::deposit(DepositArgs const& args)
jv[jss::Account] = args.depositor.human();
jv[sfVaultID] = to_string(args.id);
jv[jss::Amount] = toJson(args.amount);
if (args.flags)
jv[jss::Flags] = *args.flags;
return jv;
}

View File

@@ -54,6 +54,7 @@ struct Vault
Account depositor;
uint256 id;
STAmount amount;
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());
}
}