Compare commits

...

54 Commits

Author SHA1 Message Date
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
30 changed files with 1338 additions and 102 deletions

View File

@@ -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

View File

@@ -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) \

View File

@@ -42,8 +42,8 @@ private:
public:
using value_type = STAmount;
static int const cMinOffset = -96;
static int const cMaxOffset = 80;
static int constexpr cMinOffset = -96;
static int constexpr cMaxOffset = 80;
// Maximum native value supported by the code
constexpr static std::uint64_t cMinValue = 1'000'000'000'000'000ull;
@@ -739,6 +739,21 @@ canAdd(STAmount const& amt1, STAmount const& amt2);
bool
canSubtract(STAmount const& amt1, STAmount const& amt2);
/** Get the scale of a Number for a given asset.
*
* "scale" is similar to "exponent", but from the perspective of STAmount, which has different rules
* and mantissa ranges for determining the exponent than Number.
*
* @param number The Number to get the scale of.
* @param asset The asset to use for determining the scale.
* @return The scale of this Number for the given asset.
*/
inline int
scale(Number const& number, Asset const& asset)
{
return STAmount{asset, number}.exponent();
}
} // namespace xrpl
//------------------------------------------------------------------------------

View File

@@ -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, \
@@ -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.

View File

@@ -17,6 +17,7 @@
// Keep it sorted in reverse chronological order.
XRPL_FIX (Security3_1_3, Supported::no, VoteBehavior::DefaultNo)
XRPL_FEATURE(LendingProtocolV1_1, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (PermissionedDomainInvariant, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (ExpiredNFTokenOfferRemoval, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (BatchInnerSigs, Supported::no, VoteBehavior::DefaultNo)

View File

@@ -886,6 +886,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

@@ -8,6 +8,7 @@
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <optional>
#include <unordered_map>
#include <vector>
@@ -60,11 +61,19 @@ class ValidVault
Shares static make(SLE const&);
};
public:
struct DeltaInfo final
{
Number delta = numZero;
std::optional<int> scale;
};
private:
std::vector<Vault> afterVault_ = {};
std::vector<Shares> afterMPTs_ = {};
std::vector<Vault> beforeVault_ = {};
std::vector<Shares> beforeMPTs_ = {};
std::unordered_map<uint256, Number> deltas_ = {};
std::unordered_map<uint256, DeltaInfo> deltas_ = {};
public:
void
@@ -72,6 +81,10 @@ public:
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
// Compute the coarsest scale required to represent all numbers
[[nodiscard]] static std::int32_t
computeMinScale(Asset const& asset, std::vector<DeltaInfo> const& numbers);
};
} // namespace xrpl

View File

@@ -171,7 +171,7 @@ getAssetsTotalScale(SLE::const_ref vaultSle)
{
if (!vaultSle)
return Number::minExponent - 1; // LCOV_EXCL_LINE
return STAmount{vaultSle->at(sfAsset), vaultSle->at(sfAssetsTotal)}.exponent();
return scale(vaultSle->at(sfAssetsTotal), vaultSle->at(sfAsset));
}
TER

View File

@@ -1,5 +1,6 @@
#pragma once
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/tx/Transactor.h>
namespace xrpl {

View File

@@ -1,5 +1,6 @@
#pragma once
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/tx/Transactor.h>
namespace xrpl {

View File

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

View File

@@ -1,5 +1,6 @@
#pragma once
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/tx/Transactor.h>
namespace xrpl {

View File

@@ -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

View File

@@ -9,6 +9,7 @@
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STNumber.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
@@ -61,10 +62,12 @@ ValidVault::visitEntry(
"xrpl::ValidVault::visitEntry : some object is available");
// Number balanceDelta will capture the difference (delta) between "before"
// state (zero if created) and "after" state (zero if destroyed), so the
// invariants can validate that the change in account balances matches the
// change in vault balances, stored to deltas_ at the end of this function.
Number balanceDelta{};
// state (zero if created) and "after" state (zero if destroyed), and
// preserves value scale (exponent) to round values to the same scale during
// validation. It is used to validate that the change in account
// balances matches the change in vault balances, stored to deltas_ at the
// end of this function.
DeltaInfo balanceDelta{numZero, std::nullopt};
std::int8_t sign = 0;
if (before)
@@ -78,18 +81,34 @@ ValidVault::visitEntry(
// At this moment we have no way of telling if this object holds
// vault shares or something else. Save it for finalize.
beforeMPTs_.push_back(Shares::make(*before));
balanceDelta = static_cast<std::int64_t>(before->getFieldU64(sfOutstandingAmount));
balanceDelta.delta =
static_cast<std::int64_t>(before->getFieldU64(sfOutstandingAmount));
// MPTs are ints, so the scale is always 0.
balanceDelta.scale = 0;
sign = 1;
break;
case ltMPTOKEN:
balanceDelta = static_cast<std::int64_t>(before->getFieldU64(sfMPTAmount));
balanceDelta.delta = static_cast<std::int64_t>(before->getFieldU64(sfMPTAmount));
// MPTs are ints, so the scale is always 0.
balanceDelta.scale = 0;
sign = -1;
break;
case ltACCOUNT_ROOT:
case ltRIPPLE_STATE:
balanceDelta = before->getFieldAmount(sfBalance);
balanceDelta.delta = before->getFieldAmount(sfBalance);
// Account balance is XRP, which is an int, so the scale is
// always 0.
balanceDelta.scale = 0;
sign = -1;
break;
case ltRIPPLE_STATE: {
auto const amount = before->getFieldAmount(sfBalance);
balanceDelta.delta = amount;
// Trust Line balances are STAmounts, so we can use the exponent
// directly to get the scale.
balanceDelta.scale = amount.exponent();
sign = -1;
break;
}
default:;
}
}
@@ -105,19 +124,36 @@ ValidVault::visitEntry(
// At this moment we have no way of telling if this object holds
// vault shares or something else. Save it for finalize.
afterMPTs_.push_back(Shares::make(*after));
balanceDelta -=
balanceDelta.delta -=
Number(static_cast<std::int64_t>(after->getFieldU64(sfOutstandingAmount)));
// MPTs are ints, so the scale is always 0.
balanceDelta.scale = 0;
sign = 1;
break;
case ltMPTOKEN:
balanceDelta -= Number(static_cast<std::int64_t>(after->getFieldU64(sfMPTAmount)));
balanceDelta.delta -=
Number(static_cast<std::int64_t>(after->getFieldU64(sfMPTAmount)));
// MPTs are ints, so the scale is always 0.
balanceDelta.scale = 0;
sign = -1;
break;
case ltACCOUNT_ROOT:
case ltRIPPLE_STATE:
balanceDelta -= Number(after->getFieldAmount(sfBalance));
balanceDelta.delta -= Number(after->getFieldAmount(sfBalance));
// Account balance is XRP, which is an int, so the scale is
// always 0.
balanceDelta.scale = 0;
sign = -1;
break;
case ltRIPPLE_STATE: {
auto const amount = after->getFieldAmount(sfBalance);
balanceDelta.delta -= Number(amount);
// Trust Line balances are STAmounts, so we can use the exponent
// directly to get the scale.
if (amount.exponent() > balanceDelta.scale)
balanceDelta.scale = amount.exponent();
sign = -1;
break;
}
default:;
}
}
@@ -129,7 +165,11 @@ ValidVault::visitEntry(
// transferred to the account. We intentionally do not compare balanceDelta
// against zero, to avoid missing such updates.
if (sign != 0)
deltas_[key] = balanceDelta * sign;
{
XRPL_ASSERT_PARTS(balanceDelta.scale, "xrpl::ValidVault::visitEntry", "scale initialized");
balanceDelta.delta *= sign;
deltas_[key] = balanceDelta;
}
}
bool
@@ -391,13 +431,13 @@ ValidVault::finalize(
}
auto const& vaultAsset = afterVault.asset;
auto const deltaAssets = [&](AccountID const& id) -> std::optional<Number> {
auto const deltaAssets = [&](AccountID const& id) -> std::optional<DeltaInfo> {
auto const get = //
[&](auto const& it, std::int8_t sign = 1) -> std::optional<Number> {
[&](auto const& it, std::int8_t sign = 1) -> std::optional<DeltaInfo> {
if (it == deltas_.end())
return std::nullopt;
return it->second * sign;
return DeltaInfo{it->second.delta * sign, it->second.scale};
};
return std::visit(
@@ -416,7 +456,7 @@ ValidVault::finalize(
},
vaultAsset.value());
};
auto const deltaAssetsTxAccount = [&]() -> std::optional<Number> {
auto const deltaAssetsTxAccount = [&]() -> std::optional<DeltaInfo> {
auto ret = deltaAssets(tx[sfAccount]);
// Nothing returned or not XRP transaction
if (!ret.has_value() || !vaultAsset.native())
@@ -427,20 +467,20 @@ ValidVault::finalize(
delegate.has_value() && *delegate != tx[sfAccount])
return ret;
*ret += fee.drops();
if (*ret == zero)
ret->delta += fee.drops();
if (ret->delta == zero)
return std::nullopt;
return ret;
};
auto const deltaShares = [&](AccountID const& id) -> std::optional<Number> {
auto const deltaShares = [&](AccountID const& id) -> std::optional<DeltaInfo> {
auto const it = [&]() {
if (id == afterVault.pseudoId)
return deltas_.find(keylet::mptIssuance(afterVault.shareMPTID).key);
return deltas_.find(keylet::mptoken(afterVault.shareMPTID, id).key);
}();
return it != deltas_.end() ? std::optional<Number>(it->second) : std::nullopt;
return it != deltas_.end() ? std::optional<DeltaInfo>(it->second) : std::nullopt;
};
auto const vaultHoldsNoAssets = [&](Vault const& vault) {
@@ -567,16 +607,38 @@ ValidVault::finalize(
!beforeVault_.empty(), "xrpl::ValidVault::finalize : deposit updated a vault");
auto const& beforeVault = beforeVault_[0];
auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
if (!vaultDeltaAssets)
auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId);
if (!maybeVaultDeltaAssets)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must change vault balance";
return false; // That's all we can do
}
if (*vaultDeltaAssets > tx[sfAmount])
// Get the coarsest scale to round calculations to
DeltaInfo totalDelta{
afterVault.assetsTotal - beforeVault.assetsTotal,
std::max(
scale(afterVault.assetsTotal, vaultAsset),
scale(beforeVault.assetsTotal, vaultAsset))};
DeltaInfo availableDelta{
afterVault.assetsAvailable - beforeVault.assetsAvailable,
std::max(
scale(afterVault.assetsAvailable, vaultAsset),
scale(beforeVault.assetsAvailable, vaultAsset))};
auto const minScale = computeMinScale(
vaultAsset,
{
*maybeVaultDeltaAssets,
totalDelta,
availableDelta,
});
auto const vaultDeltaAssets =
roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale);
auto const txAmount = roundToAsset(vaultAsset, tx[sfAmount], minScale);
if (vaultDeltaAssets > txAmount)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must not change vault "
@@ -584,7 +646,7 @@ ValidVault::finalize(
result = false;
}
if (*vaultDeltaAssets <= zero)
if (vaultDeltaAssets <= zero)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must increase vault balance";
@@ -601,16 +663,23 @@ ValidVault::finalize(
if (!issuerDeposit)
{
auto const accountDeltaAssets = deltaAssetsTxAccount();
if (!accountDeltaAssets)
auto const maybeAccDeltaAssets = deltaAssetsTxAccount();
if (!maybeAccDeltaAssets)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must change depositor "
"balance";
return false;
}
auto const localMinScale =
std::max(minScale, computeMinScale(vaultAsset, {*maybeAccDeltaAssets}));
if (*accountDeltaAssets >= zero)
auto const accountDeltaAssets =
roundToAsset(vaultAsset, maybeAccDeltaAssets->delta, localMinScale);
auto const localVaultDeltaAssets =
roundToAsset(vaultAsset, vaultDeltaAssets, localMinScale);
if (accountDeltaAssets >= zero)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must decrease depositor "
@@ -618,7 +687,7 @@ ValidVault::finalize(
result = false;
}
if (*accountDeltaAssets * -1 != *vaultDeltaAssets)
if (localVaultDeltaAssets * -1 != accountDeltaAssets)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must change vault and "
@@ -636,16 +705,17 @@ ValidVault::finalize(
result = false;
}
auto const accountDeltaShares = deltaShares(tx[sfAccount]);
if (!accountDeltaShares)
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
}
if (*accountDeltaShares <= zero)
// We don't need to round shares, they are integral MPT
auto const& accountDeltaShares = *maybeAccDeltaShares;
if (accountDeltaShares.delta <= zero)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must increase depositor "
@@ -653,15 +723,17 @@ ValidVault::finalize(
result = false;
}
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
if (!vaultDeltaShares || *vaultDeltaShares == zero)
auto const maybeVaultDeltaShares = deltaShares(afterVault.pseudoId);
if (!maybeVaultDeltaShares || maybeVaultDeltaShares->delta == zero)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must change vault shares";
return false; // That's all we can do
}
if (*vaultDeltaShares * -1 != *accountDeltaShares)
// 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 "
@@ -669,13 +741,18 @@ ValidVault::finalize(
result = false;
}
if (beforeVault.assetsTotal + *vaultDeltaAssets != afterVault.assetsTotal)
auto const assetTotalDelta = roundToAsset(
vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
if (assetTotalDelta != vaultDeltaAssets)
{
JLOG(j.fatal()) << "Invariant failed: deposit and assets "
"outstanding must add up";
result = false;
}
if (beforeVault.assetsAvailable + *vaultDeltaAssets != afterVault.assetsAvailable)
auto const assetAvailableDelta = roundToAsset(
vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
if (assetAvailableDelta != vaultDeltaAssets)
{
JLOG(j.fatal()) << "Invariant failed: deposit and assets "
"available must add up";
@@ -693,16 +770,33 @@ ValidVault::finalize(
"vault");
auto const& beforeVault = beforeVault_[0];
auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId);
if (!vaultDeltaAssets)
if (!maybeVaultDeltaAssets)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal must "
"change vault balance";
return false; // That's all we can do
}
if (*vaultDeltaAssets >= zero)
// Get the most coarse scale to round calculations to
auto const totalDelta = DeltaInfo{
afterVault.assetsTotal - beforeVault.assetsTotal,
std::max(
scale(afterVault.assetsTotal, vaultAsset),
scale(beforeVault.assetsTotal, vaultAsset))};
auto const availableDelta = DeltaInfo{
afterVault.assetsAvailable - beforeVault.assetsAvailable,
std::max(
scale(afterVault.assetsAvailable, vaultAsset),
scale(beforeVault.assetsAvailable, vaultAsset))};
auto const minScale = computeMinScale(
vaultAsset, {*maybeVaultDeltaAssets, totalDelta, availableDelta});
auto const vaultPseudoDeltaAssets =
roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale);
if (vaultPseudoDeltaAssets >= zero)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal must "
"decrease vault balance";
@@ -720,15 +814,15 @@ ValidVault::finalize(
if (!issuerWithdrawal)
{
auto const accountDeltaAssets = deltaAssetsTxAccount();
auto const otherAccountDelta = [&]() -> std::optional<Number> {
auto const maybeAccDelta = deltaAssetsTxAccount();
auto const maybeOtherAccDelta = [&]() -> std::optional<DeltaInfo> {
if (auto const destination = tx[~sfDestination];
destination && *destination != tx[sfAccount])
return deltaAssets(*destination);
return std::nullopt;
}();
if (accountDeltaAssets.has_value() == otherAccountDelta.has_value())
if (maybeAccDelta.has_value() == maybeOtherAccDelta.has_value())
{
JLOG(j.fatal()) << //
"Invariant failed: withdrawal must change one "
@@ -737,9 +831,17 @@ ValidVault::finalize(
}
auto const destinationDelta = //
accountDeltaAssets ? *accountDeltaAssets : *otherAccountDelta;
maybeAccDelta ? *maybeAccDelta : *maybeOtherAccDelta;
if (destinationDelta <= zero)
// the scale of destinationDelta can be coarser than
// minScale, so we take that into account when rounding
auto const localMinScale =
std::max(minScale, computeMinScale(vaultAsset, {destinationDelta}));
auto const roundedDestinationDelta =
roundToAsset(vaultAsset, destinationDelta.delta, localMinScale);
if (roundedDestinationDelta <= zero)
{
JLOG(j.fatal()) << //
"Invariant failed: withdrawal must increase "
@@ -747,7 +849,9 @@ ValidVault::finalize(
result = false;
}
if (*vaultDeltaAssets * -1 != destinationDelta)
auto const localPseudoDeltaAssets =
roundToAsset(vaultAsset, vaultPseudoDeltaAssets, localMinScale);
if (localPseudoDeltaAssets * -1 != roundedDestinationDelta)
{
JLOG(j.fatal()) << //
"Invariant failed: withdrawal must change vault "
@@ -756,6 +860,7 @@ ValidVault::finalize(
}
}
// We don't need to round shares, they are integral MPT
auto const accountDeltaShares = deltaShares(tx[sfAccount]);
if (!accountDeltaShares)
{
@@ -765,7 +870,7 @@ ValidVault::finalize(
return false;
}
if (*accountDeltaShares >= zero)
if (accountDeltaShares->delta >= zero)
{
JLOG(j.fatal()) << //
"Invariant failed: withdrawal must decrease depositor "
@@ -773,15 +878,16 @@ ValidVault::finalize(
result = false;
}
// We don't need to round shares, they are integral MPT
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
if (!vaultDeltaShares || *vaultDeltaShares == zero)
if (!vaultDeltaShares || vaultDeltaShares->delta == zero)
{
JLOG(j.fatal()) << //
"Invariant failed: withdrawal must change vault shares";
return false; // That's all we can do
}
if (*vaultDeltaShares * -1 != *accountDeltaShares)
if (vaultDeltaShares->delta * -1 != accountDeltaShares->delta)
{
JLOG(j.fatal()) << //
"Invariant failed: withdrawal must change depositor "
@@ -789,15 +895,20 @@ ValidVault::finalize(
result = false;
}
auto const assetTotalDelta = roundToAsset(
vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
// Note, vaultBalance is negative (see check above)
if (beforeVault.assetsTotal + *vaultDeltaAssets != afterVault.assetsTotal)
if (assetTotalDelta != vaultPseudoDeltaAssets)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal and "
"assets outstanding must add up";
result = false;
}
if (beforeVault.assetsAvailable + *vaultDeltaAssets != afterVault.assetsAvailable)
auto const assetAvailableDelta = roundToAsset(
vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
if (assetAvailableDelta != vaultPseudoDeltaAssets)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal and "
"assets available must add up";
@@ -828,10 +939,24 @@ ValidVault::finalize(
}
}
auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
if (vaultDeltaAssets)
auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId);
if (maybeVaultDeltaAssets)
{
if (*vaultDeltaAssets >= zero)
auto const totalDelta = DeltaInfo{
afterVault.assetsTotal - beforeVault.assetsTotal,
std::max(
scale(afterVault.assetsTotal, vaultAsset),
scale(beforeVault.assetsTotal, vaultAsset))};
auto const availableDelta = DeltaInfo{
afterVault.assetsAvailable - beforeVault.assetsAvailable,
std::max(
scale(afterVault.assetsAvailable, vaultAsset),
scale(beforeVault.assetsAvailable, vaultAsset))};
auto const minScale = computeMinScale(
vaultAsset, {*maybeVaultDeltaAssets, totalDelta, availableDelta});
auto const vaultDeltaAssets =
roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale);
if (vaultDeltaAssets >= zero)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback must decrease vault "
@@ -839,7 +964,9 @@ ValidVault::finalize(
result = false;
}
if (beforeVault.assetsTotal + *vaultDeltaAssets != afterVault.assetsTotal)
auto const assetsTotalDelta = roundToAsset(
vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
if (assetsTotalDelta != vaultDeltaAssets)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback and assets outstanding "
@@ -847,8 +974,11 @@ ValidVault::finalize(
result = false;
}
if (beforeVault.assetsAvailable + *vaultDeltaAssets !=
afterVault.assetsAvailable)
auto const assetAvailableDelta = roundToAsset(
vaultAsset,
afterVault.assetsAvailable - beforeVault.assetsAvailable,
minScale);
if (assetAvailableDelta != vaultDeltaAssets)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback and assets available "
@@ -863,15 +993,15 @@ ValidVault::finalize(
return false; // That's all we can do
}
auto const accountDeltaShares = deltaShares(tx[sfHolder]);
if (!accountDeltaShares)
// We don't need to round shares, they are integral MPT
auto const maybeAccountDeltaShares = deltaShares(tx[sfHolder]);
if (!maybeAccountDeltaShares)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback must change holder shares";
return false; // That's all we can do
}
if (*accountDeltaShares >= zero)
if (maybeAccountDeltaShares->delta >= zero)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback must decrease holder "
@@ -879,15 +1009,16 @@ ValidVault::finalize(
result = false;
}
// We don't need to round shares, they are integral MPT
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
if (!vaultDeltaShares || *vaultDeltaShares == zero)
if (!vaultDeltaShares || vaultDeltaShares->delta == zero)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback must change vault shares";
return false; // That's all we can do
}
if (*vaultDeltaShares * -1 != *accountDeltaShares)
if (vaultDeltaShares->delta * -1 != maybeAccountDeltaShares->delta)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback must change holder and "
@@ -924,4 +1055,19 @@ ValidVault::finalize(
return true;
}
[[nodiscard]] std::int32_t
ValidVault::computeMinScale(Asset const& asset, std::vector<DeltaInfo> const& numbers)
{
if (numbers.size() == 0)
return 0;
auto const max =
std::max_element(numbers.begin(), numbers.end(), [](auto const& a, auto const& b) -> bool {
return a.scale < b.scale;
});
XRPL_ASSERT_PARTS(
max->scale, "xrpl::ValidVault::computeMinScale", "scale set for destinationDelta");
return max->scale.value_or(STAmount::cMaxOffset);
}
} // namespace xrpl

View File

@@ -123,7 +123,7 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
return roundToAsset(
vaultAsset,
tenthBipsOfValue(currentDebtTotal, TenthBips32(sleBroker->at(sfCoverRateMinimum))),
currentDebtTotal.exponent());
scale(currentDebtTotal, vaultAsset));
}();
if (coverAvail < amount)
return tecINSUFFICIENT_FUNDS;

View File

@@ -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>
@@ -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>
@@ -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)};
}

View File

@@ -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>
@@ -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 {
@@ -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
@@ -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
@@ -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;
@@ -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_);

View File

@@ -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>
@@ -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 {
@@ -21,6 +22,13 @@ 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, maxDataPayloadLength))
return temMALFORMED;
return tesSUCCESS;
}

View File

@@ -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>
@@ -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 {
@@ -62,8 +63,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.";
@@ -71,7 +72,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.";
@@ -79,6 +80,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;
@@ -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

View File

@@ -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);
return atLeastOneFieldPresent || (ctx.tx.getFlags() & expectedFlags);
}
NotTEC
VaultSet::preflight(PreflightContext const& ctx)
{
@@ -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;
}
@@ -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))
{
JLOG(ctx.j.debug()) << "VaultSet: vault deposit is already unblocked";
return tecNO_PERMISSION;
}
}
return tesSUCCESS;
}
@@ -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.

View File

@@ -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>
@@ -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 {

View File

@@ -19,9 +19,14 @@
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/apply.h>
#include <xrpl/tx/invariants/InvariantCheck.h>
#include <boost/algorithm/string/predicate.hpp>
#include <initializer_list>
#include <string>
#include <vector>
namespace xrpl {
namespace test {
@@ -3805,6 +3810,128 @@ class Invariants_test : public beast::unit_test::suite
precloseMpt);
}
void
testVaultComputeMinScale()
{
using namespace jtx;
Account const issuer{"issuer"};
PrettyAsset const vaultAsset = issuer["IOU"];
struct TestCase
{
std::string name;
std::int32_t expectedMinScale;
std::vector<ValidVault::DeltaInfo> values;
};
NumberMantissaScaleGuard g{MantissaRange::large};
auto makeDelta = [&vaultAsset](Number const& n) -> ValidVault::DeltaInfo {
return {n, scale(n, vaultAsset.raw())};
};
auto const testCases = std::vector<TestCase>{
{
.name = "No values",
.expectedMinScale = 0,
.values = {},
},
{
.name = "Mixed integer and Number values",
.expectedMinScale = -15,
.values = {makeDelta(1), makeDelta(-1), makeDelta(Number{10, -1})},
},
{
.name = "Mixed scales",
.expectedMinScale = -17,
.values =
{makeDelta(Number{1, -2}), makeDelta(Number{5, -3}), makeDelta(Number{3, -2})},
},
{
.name = "Equal scales",
.expectedMinScale = -16,
.values =
{makeDelta(Number{1, -1}), makeDelta(Number{5, -1}), makeDelta(Number{1, -1})},
},
{
.name = "Mixed mantissa sizes",
.expectedMinScale = -12,
.values =
{makeDelta(Number{1}),
makeDelta(Number{1234, -3}),
makeDelta(Number{12345, -6}),
makeDelta(Number{123, 1})},
},
};
for (auto const& tc : testCases)
{
testcase("vault computeMinScale: " + tc.name);
auto const actualScale = ValidVault::computeMinScale(vaultAsset, tc.values);
BEAST_EXPECTS(
actualScale == tc.expectedMinScale,
"expected: " + std::to_string(tc.expectedMinScale) +
", actual: " + std::to_string(actualScale));
for (auto const& num : tc.values)
{
// None of these scales are far enough apart that rounding the
// values would lose information, so check that the rounded
// value matches the original.
auto const actualRounded = roundToAsset(vaultAsset, num.delta, actualScale);
BEAST_EXPECTS(
actualRounded == num.delta,
"number " + to_string(num.delta) + " rounded to scale " +
std::to_string(actualScale) + " is " + to_string(actualRounded));
}
}
auto const testCases2 = std::vector<TestCase>{
{
.name = "False equivalence",
.expectedMinScale = -15,
.values =
{
makeDelta(Number{1234567890123456789, -18}),
makeDelta(Number{12345, -4}),
makeDelta(Number{1}),
},
},
};
// Unlike the first set of test cases, the values in these test could
// look equivalent if using the wrong scale.
for (auto const& tc : testCases2)
{
testcase("vault computeMinScale: " + tc.name);
auto const actualScale = ValidVault::computeMinScale(vaultAsset, tc.values);
BEAST_EXPECTS(
actualScale == tc.expectedMinScale,
"expected: " + std::to_string(tc.expectedMinScale) +
", actual: " + std::to_string(actualScale));
std::optional<Number> first;
Number firstRounded;
for (auto const& num : tc.values)
{
if (!first)
{
first = num.delta;
firstRounded = roundToAsset(vaultAsset, num.delta, actualScale);
continue;
}
auto const numRounded = roundToAsset(vaultAsset, num.delta, actualScale);
BEAST_EXPECTS(
numRounded != firstRounded,
"at a scale of " + std::to_string(actualScale) + " " + to_string(num.delta) +
" == " + to_string(*first));
}
}
}
public:
void
run() override
@@ -3830,6 +3957,7 @@ public:
testValidPseudoAccounts();
testValidLoanBroker();
testVault();
testVaultComputeMinScale();
}
};

View File

@@ -3,7 +3,6 @@
#include <test/jtx.h>
#include <test/jtx/Account.h>
#include <test/jtx/amount.h>
#include <test/jtx/mpt.h>
#include <xrpl/beast/xor_shift_engine.h>
#include <xrpl/protocol/SField.h>

View File

@@ -1770,10 +1770,21 @@ class LoanBroker_test : public beast::unit_test::suite
testRIPD4274MPT();
}
void
testFeatureLendingProtocolV1_1enabled()
{
using namespace jtx;
testcase("featureLendingProtocolV1_1 enabled");
Env env{*this};
BEAST_EXPECT(env.enabled(featureLendingProtocolV1_1));
}
public:
void
run() override
{
testFeatureLendingProtocolV1_1enabled();
testLoanBrokerSetDebtMaximum();
testLoanBrokerCoverDepositNullVault();

View File

@@ -2665,7 +2665,7 @@ protected:
env(manage(lender, loanKeylet.key, tfLoanDefault), ter(tecNO_PERMISSION));
});
#if LOANTODO
#if LOAN_TODO
// TODO
/*
@@ -5328,7 +5328,7 @@ protected:
}
}
#if LOANTODO
#if LOAN_TODO
void
testLoanPayLateFullPaymentBypassesPenalties()
{
@@ -6983,14 +6983,145 @@ protected:
BEAST_EXPECT(afterSecondCoverAvailable == 0);
}
// Tests that vault withdrawals work correctly when the vault has unrealized
// loss from an impaired loan, ensuring the invariant check properly
// accounts for the loss.
void
testWithdrawReflectsUnrealizedLoss()
{
using namespace jtx;
using namespace loan;
using namespace std::chrono_literals;
testcase("Vault withdraw reflects sfLossUnrealized");
// Test constants
static constexpr std::int64_t INITIAL_FUNDING = 1'000'000;
static constexpr std::int64_t LENDER_INITIAL_IOU = 5'000'000;
static constexpr std::int64_t DEPOSITOR_INITIAL_IOU = 1'000'000;
static constexpr std::int64_t BORROWER_INITIAL_IOU = 100'000;
static constexpr std::int64_t DEPOSIT_AMOUNT = 5'000;
static constexpr std::int64_t PRINCIPAL_AMOUNT = 99;
static constexpr std::uint64_t EXPECTED_SHARES_PER_DEPOSITOR = 5'000'000'000;
static constexpr std::uint32_t PAYMENT_INTERVAL = 600;
static constexpr std::uint32_t PAYMENT_TOTAL = 2;
Env env(*this, all);
// Setup accounts
Account const issuer{"issuer"};
Account const lender{"lender"};
Account const depositorA{"lpA"};
Account const depositorB{"lpB"};
Account const borrower{"borrowerA"};
env.fund(XRP(INITIAL_FUNDING), issuer, lender, depositorA, depositorB, borrower);
env.close();
// Setup trust lines
PrettyAsset const iouAsset = issuer[iouCurrency];
env(trust(lender, iouAsset(10'000'000)));
env(trust(depositorA, iouAsset(10'000'000)));
env(trust(depositorB, iouAsset(10'000'000)));
env(trust(borrower, iouAsset(10'000'000)));
env.close();
// Fund accounts with IOUs
env(pay(issuer, lender, iouAsset(LENDER_INITIAL_IOU)));
env(pay(issuer, depositorA, iouAsset(DEPOSITOR_INITIAL_IOU)));
env(pay(issuer, depositorB, iouAsset(DEPOSITOR_INITIAL_IOU)));
env(pay(issuer, borrower, iouAsset(BORROWER_INITIAL_IOU)));
env.close();
// Create vault and broker, then add deposits from two depositors
auto const broker = createVaultAndBroker(env, iouAsset, lender);
Vault v{env};
env(v.deposit({
.depositor = depositorA,
.id = broker.vaultKeylet().key,
.amount = iouAsset(DEPOSIT_AMOUNT),
}),
ter(tesSUCCESS));
env(v.deposit({
.depositor = depositorB,
.id = broker.vaultKeylet().key,
.amount = iouAsset(DEPOSIT_AMOUNT),
}),
ter(tesSUCCESS));
env.close();
// Create a loan
auto const sleBroker = env.le(keylet::loanbroker(broker.brokerID));
if (!BEAST_EXPECT(sleBroker))
return;
auto const loanKeylet = keylet::loan(broker.brokerID, sleBroker->at(sfLoanSequence));
env(set(borrower, broker.brokerID, PRINCIPAL_AMOUNT),
sig(sfCounterpartySignature, lender),
paymentTotal(PAYMENT_TOTAL),
paymentInterval(PAYMENT_INTERVAL),
fee(env.current()->fees().base * 2),
ter(tesSUCCESS));
env.close();
// Impair the loan to create unrealized loss
env(manage(lender, loanKeylet.key, tfLoanImpair), ter(tesSUCCESS));
env.close();
// Verify unrealized loss is recorded in the vault
auto const vaultAfterImpair = env.le(broker.vaultKeylet());
if (!BEAST_EXPECT(vaultAfterImpair))
return;
BEAST_EXPECT(
vaultAfterImpair->at(sfLossUnrealized) == broker.asset(PRINCIPAL_AMOUNT).value());
// Helper to get share balance for a depositor
auto const shareAsset = vaultAfterImpair->at(sfShareMPTID);
auto const getShareBalance = [&](Account const& depositor) -> std::uint64_t {
auto const token = env.le(keylet::mptoken(shareAsset, depositor.id()));
return token ? token->getFieldU64(sfMPTAmount) : 0;
};
// Verify both depositors have equal shares
auto const sharesLpA = getShareBalance(depositorA);
auto const sharesLpB = getShareBalance(depositorB);
BEAST_EXPECT(sharesLpA == EXPECTED_SHARES_PER_DEPOSITOR);
BEAST_EXPECT(sharesLpB == EXPECTED_SHARES_PER_DEPOSITOR);
BEAST_EXPECT(sharesLpA == sharesLpB);
// Helper to attempt withdrawal
auto const attemptWithdrawShares = [&](Account const& depositor,
std::uint64_t shareAmount,
TER expected) {
STAmount const shareAmt{MPTIssue{shareAsset}, Number(shareAmount)};
env(v.withdraw(
{.depositor = depositor, .id = broker.vaultKeylet().key, .amount = shareAmt}),
ter(expected));
env.close();
};
// Regression test: Both depositors should successfully withdraw despite
// unrealized loss. Previously failed with invariant violation:
// "withdrawal must change vault and destination balance by equal
// amount". This was caused by sharesToAssetsWithdraw rounding down,
// creating a mismatch where vaultDeltaAssets * -1 != destinationDelta
// when unrealized loss exists.
attemptWithdrawShares(depositorA, sharesLpA, tesSUCCESS);
attemptWithdrawShares(depositorB, sharesLpB, tesSUCCESS);
}
public:
void
run() override
{
#if LOANTODO
#if LOAN_TODO
testLoanPayLateFullPaymentBypassesPenalties();
testLoanCoverMinimumRoundingExploit();
#endif
testWithdrawReflectsUnrealizedLoss();
testInvalidLoanSet();
testCoverDepositWithdrawNonTransferableMPT();

View File

@@ -669,6 +669,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;
@@ -976,13 +984,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 = negativeAmount(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] = negativeAmount(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 = negativeAmount(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));
}
});
@@ -1081,6 +1190,7 @@ class Vault_test : public beast::unit_test::suite
Asset const& asset,
Vault& vault)> test) {
Env env{*this, testable_amendments() | featureSingleAssetVault};
Account issuer{"issuer"};
Account owner{"owner"};
Account depositor{"depositor"};
@@ -2241,6 +2351,106 @@ class Vault_test : public beast::unit_test::suite
// Delete vault with zero balance
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::interestRate(TenthBips32(0)),
paymentInterval(120),
paymentTotal(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 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();
}
});
}
void
@@ -2940,10 +3150,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::interestRate(TenthBips32(0)),
paymentInterval(120),
paymentTotal(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 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;
@@ -2974,7 +3285,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));
@@ -2999,6 +3313,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");
@@ -5231,6 +5567,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 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(maxDataPayloadLength, '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(maxDataPayloadLength + 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(maxDataPayloadLength, 'A'));
env(delTx, ter(tesSUCCESS));
env.close();
}
}
public:
void
run() override
@@ -5242,7 +5802,7 @@ public:
testCreateFailMPT();
testWithMPT();
testWithIOU();
testWithDomainCheck();
testPrivateVault();
testWithDomainCheckXRP();
testNonTransferableShares();
testFailedPseudoAccount();
@@ -5251,6 +5811,8 @@ public:
testVaultClawbackBurnShares();
testVaultClawbackAssets();
testAssetsMaximum();
testVaultDepositBlockGeneral();
testVaultDeleteData();
}
};

View File

@@ -31,6 +31,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

@@ -36,6 +36,7 @@ struct Vault
{
Account owner;
uint256 id;
std::optional<std::uint32_t> flags{};
};
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());
}
}