Compare commits

...

51 Commits

Author SHA1 Message Date
JCW
c33766b7e1 Fix build errors 2026-03-05 14:22:45 +00:00
JCW
9ed2559b2a Merge branch 'a1q123456/support-lending-in-batch' into a1q123456/default-cover-optimisation
Signed-off-by: JCW <a1q123456@users.noreply.github.com>

# Conflicts:
#	src/test/app/Loan/Loan_test.cpp
#	src/test/jtx/vault.h
2026-03-05 12:23:32 +00:00
JCW
5ce17a342d Merge remote-tracking branch 'origin/tapanito/lending-fix-amendment' into a1q123456/support-lending-in-batch
Signed-off-by: JCW <a1q123456@users.noreply.github.com>

# Conflicts:
#	include/xrpl/tx/InvariantCheck.h
#	include/xrpl/tx/transactors/Lending/LendingHelpers.h
#	src/libxrpl/tx/InvariantCheck.cpp
#	src/libxrpl/tx/transactors/Lending/LoanBrokerCoverWithdraw.cpp
#	src/test/app/Invariants_test.cpp
2026-03-05 12:21:52 +00: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 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
JCW
23445d7eb8 Gate the changes with the amendment
Signed-off-by: JCW <a1q123456@users.noreply.github.com>
2026-02-24 14:31:10 +00:00
JCW
bebf21e1aa Merge remote-tracking branch 'origin/tapanito/lending-vault-invariant' into a1q123456/support-lending-in-batch
Signed-off-by: JCW <a1q123456@users.noreply.github.com>

# Conflicts:
#	src/libxrpl/tx/transactors/Batch.cpp
#	src/test/app/Loan/Loan_test.cpp
2026-02-24 14:26:19 +00:00
JCW
fb78cc2c02 Revert changes
Signed-off-by: JCW <a1q123456@users.noreply.github.com>
2026-02-24 14:21:05 +00:00
Vito Tumas
3c3bd75991 Merge branch 'develop' into tapanito/lending-fix-amendment 2026-02-24 14:40:31 +01:00
JCW
fca4ddc833 Remove the amendment
Signed-off-by: JCW <a1q123456@users.noreply.github.com>
2026-02-24 13:39:32 +00:00
Vito
b091a56c0c Merge branch 'tapanito/lending-fix-amendment' into tapanito/lending-vault-invariant 2026-02-24 14:15:24 +01:00
Vito Tumas
7459fe454d Merge branch 'develop' into tapanito/lending-fix-amendment 2026-02-23 12:17:17 +01:00
JCW
a8e64d9f88 Fix build errors
Signed-off-by: JCW <a1q123456@users.noreply.github.com>
2026-02-19 11:47:29 +00:00
Vito
cb84a6f1c0 adds missing includes 2026-02-19 12:34:53 +01:00
Vito
72b094c12f Merge branch 'tapanito/lending-fix-amendment' into tapanito/lending-vault-invariant 2026-02-19 11:49:59 +01:00
Vito
106bf48725 Merge branch 'develop' into tapanito/lending-fix-amendment 2026-02-18 18:29:08 +01:00
JCW
8cea191339 Default cover optimisation 2026-02-18 16:14:53 +00:00
Vito
a6b7d4ef7d additional formatting 2026-02-17 14:16:26 +01:00
Vito
0e4d042d70 formatting 2026-02-17 14:05:31 +01:00
Vito Tumas
d6e5c971b5 Merge branch 'tapanito/lending-fix-amendment' into tapanito/lending-vault-invariant 2026-02-17 13:52:12 +01:00
Vito Tumas
74c968d4e3 Merge branch 'develop' into tapanito/lending-fix-amendment 2026-02-17 13:51:08 +01:00
JCW
281dbd2873 Add unit tests 2026-02-12 16:19:51 +00:00
Vito
167147281c Merge branch 'develop' into tapanito/lending-fix-amendment 2026-02-12 15:22:30 +01:00
JCW
93e98ac714 Allow Lending transactions in Batch 2026-02-12 11:17:34 +00:00
Vito Tumas
79b3b4ffea Merge branch 'tapanito/lending-fix-amendment' into tapanito/lending-vault-invariant 2026-02-11 18:28:23 +01:00
Vito Tumas
ba60306610 Merge branch 'develop' into tapanito/lending-fix-amendment 2026-02-11 17:46:20 +01:00
Vito
5040048e49 Merge branch 'tapanito/lending-fix-amendment' into tapanito/lending-vault-invariant 2026-02-10 14:58:30 +01:00
Vito Tumas
6674500896 Merge branch 'develop' into tapanito/lending-fix-amendment 2026-02-10 11:48:23 +01:00
Ed Hennis
7085aa3d55 Add canonical "scale" computation to Number (#6246)
* Add canonical "scale" computation to Number

- Requires a template for STAmount and Asset.
- Update tests and computeMinScale from #6217 to use scale.
- Convert a few other places to use "scale" correctly.

* ValidVault tracks scale of original operands alongside deltas

* Update src/xrpld/app/tx/detail/InvariantCheck.cpp

Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com>

* Change ValidVault::DeltaInfo::scale to an optional

* Change computeMinScale to use a vector instead of an initializer_list

* Fix compile errors

---------

Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com>
2026-02-10 11:41:55 +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
Vito Tumas
f8d441bb6b Merge branch 'develop' into tapanito/lending-vault-invariant 2026-01-22 23:36:25 +01:00
Vito Tumas
5c87c4ffb0 Merge branch 'develop' into tapanito/lending-vault-invariant 2026-01-21 18:58:09 +01:00
Vito
0a9436def4 refactors vault invariant to use relative distance 2026-01-21 18:57:33 +01:00
Vito
f76bf5340c flyby change removing unused includes 2026-01-21 11:50:38 +01:00
Vito
1af0f4bd43 addreses review comments 2026-01-21 11:50:18 +01:00
Vito
c6821ab842 adds invariant test 2026-01-21 11:42:03 +01:00
Vito
aa12210fcd fixes a minor min bug 2026-01-20 18:01:14 +01:00
Vito
9235ec483a adds missing incldues 2026-01-20 17:06:23 +01:00
Vito Tumas
ffe0a3cc61 Merge branch 'develop' into tapanito/lending-vault-invariant 2026-01-16 11:26:28 +01:00
Vito
add9071b20 fixes formatting 2026-01-16 11:26:12 +01:00
Vito Tumas
465e7b6d91 Merge branch 'develop' into tapanito/lending-vault-invariant 2026-01-15 16:10:25 +01:00
Vito
6223ebe05e improves VaultWithdraw invariant rounding 2026-01-15 16:09:13 +01:00
Vito
4fe50c2d31 attempt to fix rounding issues 2026-01-14 20:58:04 +01:00
19 changed files with 4170 additions and 1514 deletions

View File

@@ -105,6 +105,9 @@ private:
template <class T>
concept Integral64 = std::is_same_v<T, std::int64_t> || std::is_same_v<T, std::uint64_t>;
template <class STAmount, class Asset>
concept CanUseAsScale = requires(Asset a, Number n) { STAmount(a, n); } && requires(STAmount s) { s.exponent(); };
/** Number is a floating point type that can represent a wide range of values.
*
* It can represent all values that can be represented by an STAmount -
@@ -257,6 +260,26 @@ public:
constexpr int
exponent() const noexcept;
/** Get the scale of this Number for the given asset.
*
* "scale" is similar to "exponent", but from the perspective of STAmount,
* which has different rules for determining the exponent than Number.
*
* Because Number does not have access to STAmount or Asset, this function
* is implemented as a template, with the expectation that it will only be
* used by those types. Any types that fit the requirements will work,
* though, if there's a need.
*
* @tparam STAmount The STAmount type.
* @tparam Asset The Asset type.
* @param asset The asset to use for determining the scale.
* @return The scale of this Number for the given asset.
*/
template <class STAmount, class Asset>
int
scale(Asset const& asset) const
requires CanUseAsScale<STAmount, Asset>;
constexpr Number
operator+() const noexcept;
constexpr Number
@@ -584,6 +607,14 @@ Number::exponent() const noexcept
return e;
}
template <class STAmount, class Asset>
int
Number::scale(Asset const& asset) const
requires CanUseAsScale<STAmount, Asset>
{
return STAmount{asset, *this}.exponent();
}
inline constexpr Number
Number::operator+() const noexcept
{

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

@@ -16,6 +16,7 @@
// Add new amendments to the top of this list.
// Keep it sorted in reverse chronological order.
XRPL_FIX (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

@@ -868,6 +868,7 @@ TRANSACTION(ttVAULT_DELETE, 67, VaultDelete,
mustDeleteAcct | destroyMPTIssuance | mustModifyVault,
({
{sfVaultID, soeREQUIRED},
{sfMemoData, soeOPTIONAL},
}))
/** This transaction trades assets for shares with a vault. */

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

@@ -8,6 +8,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>
@@ -60,10 +61,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)
@@ -77,18 +80,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:;
}
}
@@ -104,19 +123,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:;
}
}
@@ -128,7 +164,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
@@ -390,13 +430,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(
@@ -415,7 +455,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())
@@ -426,20 +466,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) {
@@ -566,16 +606,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 "
@@ -583,7 +645,7 @@ ValidVault::finalize(
result = false;
}
if (*vaultDeltaAssets <= zero)
if (vaultDeltaAssets <= zero)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must increase vault balance";
@@ -600,16 +662,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 "
@@ -617,7 +686,7 @@ ValidVault::finalize(
result = false;
}
if (*accountDeltaAssets * -1 != *vaultDeltaAssets)
if (localVaultDeltaAssets * -1 != accountDeltaAssets)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must change vault and "
@@ -635,16 +704,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 "
@@ -652,15 +722,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 "
@@ -668,13 +740,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";
@@ -692,16 +769,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";
@@ -719,15 +813,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 "
@@ -736,9 +830,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 "
@@ -746,7 +848,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 "
@@ -755,6 +859,7 @@ ValidVault::finalize(
}
}
// We don't need to round shares, they are integral MPT
auto const accountDeltaShares = deltaShares(tx[sfAccount]);
if (!accountDeltaShares)
{
@@ -764,7 +869,7 @@ ValidVault::finalize(
return false;
}
if (*accountDeltaShares >= zero)
if (accountDeltaShares->delta >= zero)
{
JLOG(j.fatal()) << //
"Invariant failed: withdrawal must decrease depositor "
@@ -772,15 +877,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 "
@@ -788,15 +894,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";
@@ -827,10 +938,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 "
@@ -838,7 +963,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 "
@@ -846,8 +973,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 "
@@ -862,15 +992,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 "
@@ -878,15 +1008,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 "
@@ -923,4 +1054,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

@@ -258,7 +258,8 @@ Batch::preflight(PreflightContext const& ctx)
return temINVALID;
}
if (std::any_of(
if (!ctx.rules.enabled(fixLendingProtocolV1_1) &&
std::any_of(
disabledTxTypes.begin(), disabledTxTypes.end(), [txType](auto const& disabled) {
return txType == disabled;
}))

View File

@@ -121,7 +121,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

@@ -141,20 +141,39 @@ LoanManage::defaultLoan(
// Apply the First-Loss Capital to the Default Amount
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
TenthBips32 const coverRateLiquidation{brokerSle->at(sfCoverRateLiquidation)};
auto const covered = [&]() {
// Always round the minimum required up.
NumberRoundModeGuard mg(Number::upward);
if (view.rules().enabled(fixLendingProtocolV1_1))
{
// New formula: DefaultCovered = min(DefaultAmount × CoverRateMinimum, CoverAvailable)
// Round the liquidation amount up
return roundToAsset(
vaultAsset, tenthBipsOfValue(totalDefaultAmount, coverRateMinimum), loanScale);
}
else
{
auto const minimumCover =
tenthBipsOfValue(brokerDebtTotalProxy.value(), coverRateMinimum);
// Round the liquidation amount up
return roundToAsset(
vaultAsset,
/*
* This formula is from the XLS-66 spec, section 3.2.3.2 (State
* Changes), specifically "if the `tfLoanDefault` flag is set" /
* "Apply the First-Loss Capital to the Default Amount"
*/
std::min(tenthBipsOfValue(minimumCover, coverRateLiquidation), totalDefaultAmount),
loanScale);
}
}();
auto const defaultCovered = [&]() {
// Always round the minimum required up.
NumberRoundModeGuard mg(Number::upward);
auto const minimumCover = tenthBipsOfValue(brokerDebtTotalProxy.value(), coverRateMinimum);
// Round the liquidation amount up, too
auto const covered = roundToAsset(
vaultAsset,
/*
* This formula is from the XLS-66 spec, section 3.2.3.2 (State
* Changes), specifically "if the `tfLoanDefault` flag is set" /
* "Apply the First-Loss Capital to the Default Amount"
*/
std::min(tenthBipsOfValue(minimumCover, coverRateLiquidation), totalDefaultAmount),
loanScale);
auto const coverAvailable = *brokerSle->at(sfCoverAvailable);
return std::min(covered, coverAvailable);

View File

@@ -18,6 +18,13 @@ VaultDelete::preflight(PreflightContext const& ctx)
return temMALFORMED;
}
if (ctx.tx.isFieldPresent(sfMemoData) && !ctx.rules.enabled(fixLendingProtocolV1_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

@@ -2600,16 +2600,18 @@ class Batch_test : public beast::unit_test::suite
{
testcase("loan");
bool const lendingBatchEnabled = !std::any_of(
Batch::disabledTxTypes.begin(), Batch::disabledTxTypes.end(), [](auto const& disabled) {
return disabled == ttLOAN_BROKER_SET;
});
using namespace test::jtx;
test::jtx::Env env{
*this, features | featureSingleAssetVault | featureLendingProtocol | featureMPTokensV1};
bool const lendingBatchEnabled =
!std::any_of(
Batch::disabledTxTypes.begin(),
Batch::disabledTxTypes.end(),
[](auto const& disabled) { return disabled == ttLOAN_BROKER_SET; }) ||
env.enabled(fixLendingProtocolV1_1);
Account const issuer{"issuer"};
// For simplicity, lender will be the sole actor for the vault &
// brokers.

View File

@@ -16,9 +16,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 {
@@ -3793,6 +3798,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
@@ -3818,6 +3945,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>

2694
src/test/app/Loan/LoanBase.h Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,728 @@
#include <xrpl/beast/unit_test/suite.h>
//
#include <test/app/Loan/LoanBase.h>
#include <test/jtx.h>
#include <xrpl/protocol/SField.h>
#include <chrono>
namespace xrpl {
namespace test {
class LoanBatch_test : public LoanBase
{
protected:
void
testDisabled()
{
testcase("Disabled");
// Lending Protocol depends on Single Asset Vault (SAV). Test
// combinations of the two amendments.
// Single Asset Vault depends on MPTokensV1, but don't test every combo
// of that.
using namespace jtx;
auto failAll = [this](FeatureBitset features) {
using namespace loan;
using namespace std::chrono_literals;
Env env(*this, features);
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(10000), alice, bob);
env.close();
auto const keylet = keylet::loanbroker(alice, env.seq(alice));
auto const loanKeylet = keylet::loan(keylet.key, env.seq(alice));
auto const aliceSeq = env.seq(alice);
auto const bobSeq = env.seq(bob);
auto const batchFee = batch::calcBatchFee(env, 1, 4);
auto loanSet = set(alice, keylet.key, Number(10000));
loanSet[sfCounterparty] = bob.human();
auto batchTxn = env.jt(
batch::outer(bob, bobSeq, batchFee, tfAllOrNothing),
batch::inner(loanSet, aliceSeq),
batch::inner(del(alice, loanKeylet.key), aliceSeq + 1),
batch::inner(manage(alice, loanKeylet.key, tfLoanImpair), aliceSeq + 2),
batch::inner(pay(alice, loanKeylet.key, XRP(500)), aliceSeq + 3),
batch::sig(alice));
env(batchTxn, ter(temINVALID_INNER_BATCH));
};
failAll(all - featureMPTokensV1);
failAll(all - featureSingleAssetVault - featureLendingProtocol);
failAll(all - featureSingleAssetVault);
failAll(all - featureLendingProtocol);
}
void
testCreateAsset()
{
testcase("CreateAsset");
// Checks if a single asset vault can be created in a batch.
using namespace jtx;
Env env(*this, all);
Account const issuer{"issuer"};
Account const broker{"broker"};
Account const borrower{"borrower"};
auto const IOU = issuer["IOU"];
env.fund(XRP(20'000), issuer, broker, borrower);
env.close();
env(trust(broker, IOU(20'000'000)));
env(pay(issuer, broker, IOU(10'000'000)));
env.close();
auto brokerSeq = env.seq(broker);
// The starting sequence should be brokerSeq + 1 because the batch
// outer transaction will consume the first sequence.
auto txns = createVaultAndBrokerTransactions(env, IOU, broker, BrokerParameters::defaults(), brokerSeq + 1);
auto const batchFee = batch::calcBatchFee(env, 0, 4);
auto batchTxn = env.jt(
batch::outer(broker, brokerSeq, batchFee, tfAllOrNothing),
batch::inner(txns.vaultCreateTx, brokerSeq + 1),
batch::inner(txns.vaultDepositTx, brokerSeq + 2),
batch::inner(txns.brokerSetTx, brokerSeq + 3),
batch::inner(*txns.coverDepositTx, brokerSeq + 4));
env(batchTxn);
env.close();
checkVaultAndBroker(env, txns);
}
void
testLoanSetAndDelete()
{
testcase("LoanSetAndDelete");
// Checks if LoanSet works in a batch.
using namespace jtx;
using namespace jtx::loan;
Env env(*this, all);
Account const issuer{"issuer"};
Account const broker{"broker"};
Account const borrower{"borrower"};
auto const IOU = issuer["IOU"];
env.fund(XRP(20'000), issuer, broker, borrower);
env.close();
env(trust(broker, IOU(20'000'000)));
env(pay(issuer, broker, IOU(10'000'000)));
env.close();
auto brokerInfo = createVaultAndBroker(env, IOU, broker);
LoanParameters const loanParams{
.account = broker,
.counter = borrower,
.principalRequest = Number{100},
.interest = TenthBips32{1922},
.payTotal = 5816,
.payInterval = 86400 * 6,
.gracePd = 86400 * 5,
};
auto loanSetTx = loanParams.getTransaction(env, brokerInfo);
// Get the loan keylet that will be created by the LoanSet
auto const brokerSeq = env.seq(broker);
// The loan keylet is based on the broker's LoanSequence
auto const brokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
auto const loanSequence = brokerSle->at(sfLoanSequence);
auto const loanKeylet = keylet::loan(brokerInfo.brokerID, loanSequence);
// Create the loan delete transaction
auto loanDelTx = del(broker, loanKeylet.key);
// Calculate batch fee: 1 signer (borrower) + 2 transactions
auto const batchFee = batch::calcBatchFee(env, 1, 2);
// Create the batch transaction with both LoanSet and LoanDelete
auto batchTxn = env.jt(
batch::outer(broker, brokerSeq, batchFee, tfAllOrNothing),
batch::inner(loanSetTx, brokerSeq + 1),
batch::inner(loanDelTx, brokerSeq + 2),
batch::sig(borrower));
env(batchTxn);
env.close();
// Verify the loan is not there.
BEAST_EXPECT(!env.le(loanKeylet));
}
void
testLoanSetAndImpair()
{
testcase("LoanSetAndManage");
// Creates a loan and impairs it in a single batch.
// When a loan is impaired, NextPaymentDueDate is set to currentTime.
using namespace jtx;
using namespace jtx::loan;
using namespace std::chrono_literals;
Env env(*this, all);
Account const issuer{"issuer"};
Account const broker{"broker"};
Account const borrower{"borrower"};
auto const IOU = issuer["IOU"];
env.fund(XRP(20'000), issuer, broker, borrower);
env.close();
env(trust(broker, IOU(20'000'000)));
env(pay(issuer, broker, IOU(10'000'000)));
env.close();
auto brokerInfo = createVaultAndBroker(env, IOU, broker);
LoanParameters const loanParams{
.account = broker,
.counter = borrower,
.principalRequest = Number{100},
.interest = TenthBips32{1922},
.payTotal = 5816,
.payInterval = 86400 * 6, // 6 days
.gracePd = 86400 * 5, // 5 days grace period
};
auto loanSetTx = loanParams.getTransaction(env, brokerInfo);
// Get the loan keylet that will be created by the LoanSet
auto const brokerSeq = env.seq(broker);
// The loan keylet is based on the broker's LoanSequence
auto const brokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
auto const loanSequence = brokerSle->at(sfLoanSequence);
auto const loanKeylet = keylet::loan(brokerInfo.brokerID, loanSequence);
// Create the manage transaction to impair the loan
auto impairTx = manage(broker, loanKeylet.key, tfLoanImpair);
// Calculate batch fee: 1 signer (borrower) + 2 transactions
auto const batchFee = batch::calcBatchFee(env, 1, 2);
// Create the batch transaction with LoanSet and impair
auto batchTxn = env.jt(
batch::outer(broker, brokerSeq, batchFee, tfAllOrNothing),
batch::inner(loanSetTx, brokerSeq + 1),
batch::inner(impairTx, brokerSeq + 2),
batch::sig(borrower));
auto currentTime = env.now().time_since_epoch().count();
env(batchTxn);
env.close();
// Verify the loan was created and impaired
auto const finalLoanSle = env.le(loanKeylet);
BEAST_EXPECT(finalLoanSle);
BEAST_EXPECT(finalLoanSle->isFlag(lsfLoanImpaired));
// When impaired, NextPaymentDueDate should be set to current time
BEAST_EXPECT(finalLoanSle->at(sfNextPaymentDueDate) == currentTime);
// Verify Vault.LossUnrealized was increased
auto const finalBrokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
BEAST_EXPECT(finalBrokerSle);
if (finalBrokerSle)
{
auto const vaultKeylet = keylet::vault(finalBrokerSle->at(sfVaultID));
auto const vaultSle = env.le(vaultKeylet);
BEAST_EXPECT(vaultSle);
if (vaultSle)
{
// LossUnrealized = TotalValueOutstanding - ManagementFeeOutstanding
auto const expectedLoss =
finalLoanSle->at(sfTotalValueOutstanding) - finalLoanSle->at(sfManagementFeeOutstanding);
BEAST_EXPECT(vaultSle->at(sfLossUnrealized) == expectedLoss);
}
}
}
void
testLoanDefaultWithdrawAndPay(FeatureBitset features)
{
testcase("LoanDefaultWithdrawAndPay");
// Creates a loan, advances time to make it defaultable, then in a batch:
// defaults the loan, withdraws the DefaultCovered amount, and makes a payment.
using namespace jtx;
using namespace jtx::loan;
using namespace std::chrono_literals;
Env env(*this, features);
Account const issuer{"issuer"};
Account const broker{"broker"};
Account const borrower{"borrower"};
Account const recipient{"recipient"};
auto const IOU = issuer[iouCurrency];
env.fund(XRP(20'000), issuer, broker, borrower, recipient);
env.close();
env(trust(broker, IOU(20'000'000)));
env(trust(recipient, IOU(20'000'000)));
env(pay(issuer, broker, IOU(10'000'000)));
env.close();
// Create vault and broker with specific parameters to ensure DefaultCovered > vault available
BrokerParameters brokerParams = BrokerParameters::defaults();
brokerParams.vaultDeposit = 100; // Small vault deposit
brokerParams.coverDeposit = 1000; // Large cover deposit
auto brokerInfo = createVaultAndBroker(env, IOU, broker, brokerParams);
LoanParameters const loanParams{
.account = broker,
.counter = borrower,
.principalRequest = Number{100},
.interest = TenthBips32{1922},
.payTotal = 5816,
.payInterval = 86400 * 6, // 6 days
.gracePd = 86400 * 5, // 5 days grace period
};
// Create the loan first (not in batch)
env(loanParams(env, brokerInfo));
env.close();
// Get the loan keylet
auto const brokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
auto const loanSequence = brokerSle->at(sfLoanSequence) - 1;
auto const loanKeylet = keylet::loan(brokerInfo.brokerID, loanSequence);
// Verify loan exists
BEAST_EXPECT(env.le(loanKeylet));
// Advance time past the payment due date + grace period to make the loan defaultable
auto const loanSle = env.le(loanKeylet);
auto const nextPaymentDue = loanSle->at(sfNextPaymentDueDate);
auto const gracePeriod = loanSle->at(sfGracePeriod);
env.close(std::chrono::seconds{nextPaymentDue + gracePeriod + 60});
// Calculate DefaultCovered before defaulting
auto const totalValue = loanSle->at(sfTotalValueOutstanding);
auto const managementFee = loanSle->at(sfManagementFeeOutstanding);
auto const defaultAmount = totalValue - managementFee;
auto const debtTotal = brokerSle->at(sfDebtTotal);
auto const coverRateMin = TenthBips32{brokerSle->at(sfCoverRateMinimum)};
auto const coverRateLiq = TenthBips32{brokerSle->at(sfCoverRateLiquidation)};
auto const coverAvailable = brokerSle->at(sfCoverAvailable);
Number const defaultCovered = [&] {
// New formula: DefaultCovered = min(DefaultAmount × CoverRateMinimum, CoverAvailable)
if (env.enabled(fixLendingProtocolV1_1))
{
return std::min(defaultAmount * coverRateMin.value() / tenthBipsPerUnity.value(), coverAvailable);
}
else
{
// MinimumCover = DebtTotal x CoverRateMinimum
Number const minimumCover = debtTotal * coverRateMin.value() / tenthBipsPerUnity.value();
// DefaultCovered = min(MinimumCover x CoverRateLiquidation, DefaultAmount, CoverAvailable)
return std::min(
{minimumCover * coverRateLiq.value() / tenthBipsPerUnity.value(), defaultAmount, coverAvailable});
}
}();
// Get vault available before default
auto const vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID)));
auto const vaultAvailableBefore = vaultSle->at(sfAssetsAvailable);
// Verify DefaultCovered will be greater than vault available
BEAST_EXPECT(defaultCovered > vaultAvailableBefore);
// Now batch: default, withdraw DefaultCovered, and make a payment
auto const brokerSeq = env.seq(broker);
auto defaultTx = manage(broker, loanKeylet.key, tfLoanDefault);
// Withdraw the DefaultCovered amount from vault
Vault vault{env};
auto withdrawTx =
vault.withdraw({.depositor = broker, .id = brokerInfo.vaultID, .amount = brokerInfo.asset(defaultCovered)});
// Make a payment to recipient
auto paymentTx = pay(broker, recipient, brokerInfo.asset(defaultCovered / 2));
// Calculate batch fee: 0 signers (broker signs outer) + 3 transactions
auto const batchFee = batch::calcBatchFee(env, 0, 3);
// Create the batch transaction
auto batchTxn = env.jt(
batch::outer(broker, brokerSeq, batchFee, tfAllOrNothing),
batch::inner(defaultTx, brokerSeq + 1),
batch::inner(withdrawTx, brokerSeq + 2),
batch::inner(paymentTx, brokerSeq + 3));
env(batchTxn);
env.close();
// Verify the loan is defaulted
auto const finalLoanSle = env.le(loanKeylet);
BEAST_EXPECT(finalLoanSle);
if (finalLoanSle)
{
BEAST_EXPECT(finalLoanSle->isFlag(lsfLoanDefault));
BEAST_EXPECT(finalLoanSle->at(sfPaymentRemaining) == 0);
BEAST_EXPECT(finalLoanSle->at(sfTotalValueOutstanding) == 0);
BEAST_EXPECT(finalLoanSle->at(sfPrincipalOutstanding) == 0);
BEAST_EXPECT(finalLoanSle->at(sfManagementFeeOutstanding) == 0);
BEAST_EXPECT(finalLoanSle->at(sfNextPaymentDueDate) == 0);
}
// Verify vault state after default and withdrawal
auto const finalVaultSle = env.le(keylet::vault(brokerInfo.vaultID));
BEAST_EXPECT(finalVaultSle);
if (finalVaultSle)
{
// Vault should have received DefaultCovered, then withdrawn it
auto const expectedAvailable = vaultAvailableBefore + defaultCovered - defaultCovered;
BEAST_EXPECT(finalVaultSle->at(sfAssetsAvailable) == expectedAvailable);
}
// Verify recipient received payment
BEAST_EXPECT(env.balance(recipient, IOU) == brokerInfo.asset(defaultCovered / 2));
}
void
testRiskFreeArbitrage()
{
testcase("RiskFreeArbitrage");
// Demonstrates risk-free arbitrage using batch transactions:
// 1. Borrow funds from a loan
// 2. Buy XRP at one price
// 3. Sell XRP at a higher price
// 4. Repay the loan with interest
// All in a single atomic batch - if any step fails, everything reverts.
using namespace jtx;
using namespace jtx::loan;
using namespace std::chrono_literals;
Env env(*this, all);
Account const issuer{"issuer"};
Account const broker{"broker"};
Account const borrower{"borrower"};
Account const marketMaker1{"mm1"}; // Sells XRP at $2.50
Account const marketMaker2{"mm2"}; // Buys XRP at $2.52
auto const IOU = issuer[iouCurrency];
env.fund(XRP(50'000), issuer, broker, borrower, marketMaker1, marketMaker2);
env.close();
// Set up trust lines
env(trust(broker, IOU(20'000'000)));
env(trust(borrower, IOU(20'000'000)));
env(trust(marketMaker1, IOU(20'000'000)));
env(trust(marketMaker2, IOU(20'000'000)));
env.close();
// Fund accounts
env(pay(issuer, broker, IOU(15'000'000)));
env(pay(issuer, marketMaker1, IOU(11'000'000)));
env(pay(issuer, marketMaker2, IOU(11'000'000)));
env.close();
// Create vault and broker
auto brokerInfo = createVaultAndBroker(env, IOU, broker);
// Get the loan keylet BEFORE creating the loan
auto const brokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
auto const loanSequence = brokerSle->at(sfLoanSequence);
auto const loanKeylet = keylet::loan(brokerInfo.brokerID, loanSequence);
// Market makers create offers:
// MM1: Sells 400 XRP for $1000 (price: $2.50 per XRP)
// MM2: Buys 400 XRP for $1008 (price: $2.52 per XRP)
env(offer(marketMaker1, IOU(1000), XRP(400)));
env(offer(marketMaker2, XRP(400), IOU(1008)));
env.close();
// Record vault state before batch
auto const initialVaultSle = env.le(keylet::vault(brokerInfo.vaultID));
auto const initialVaultPseudoAccountID = initialVaultSle->at(sfAccount);
Account const initialVaultPseudoAccount("VaultPseudo", initialVaultPseudoAccountID);
auto const initialVaultBalance = env.balance(initialVaultPseudoAccount, IOU);
auto const initialAssetsAvailable = initialVaultSle->at(sfAssetsAvailable);
// Now create the arbitrage batch - ALL IN ONE ATOMIC TRANSACTION:
// 1. Create loan (borrow $1000)
// 2. Buy 400 XRP at $2.50 (cost: $1000)
// 3. Sell 400 XRP at $2.52 (revenue: $1008)
// 4. Repay loan principal + interest ($1000 + $1 = $1001)
// Profit: $1008 - $1001 = $7
auto const borrowerSeq = env.seq(borrower);
// Transaction 1: Create the loan (borrower receives $1000)
LoanParameters const loanParams{
.account = borrower,
.counter = broker,
.principalRequest = Number{1000},
.interest = TenthBips32{100}, // 0.1% interest
.payTotal = 1,
.payInterval = 86400, // 1 day
.gracePd = 86400, // 1 day grace period
};
auto loanSetTx = loanParams.getTransaction(env, brokerInfo);
// Transaction 2: Buy 400 XRP for $1000 from MM1
auto buyTx = offer(borrower, XRP(400), IOU(1000));
// Transaction 3: Sell 400 XRP for $1008 to MM2
auto sellTx = offer(borrower, IOU(1008), XRP(400));
// Transaction 4: Repay the loan
// We know the total will be $1001 (principal $1000 + interest $1)
auto payTx = pay(borrower, loanKeylet.key, IOU(1001));
// Calculate batch fee: 1 signer (broker as counterparty) + 4 transactions
auto const batchFee = batch::calcBatchFee(env, 1, 4);
// Create the batch transaction with tfAllOrNothing
auto batchTxn = env.jt(
batch::outer(borrower, borrowerSeq, batchFee, tfAllOrNothing),
batch::inner(loanSetTx, borrowerSeq + 1),
batch::inner(buyTx, borrowerSeq + 2),
batch::inner(sellTx, borrowerSeq + 3),
batch::inner(payTx, borrowerSeq + 4),
batch::sig(broker));
env(batchTxn);
env.close();
// Verify the arbitrage was successful:
// 1. Borrower should have profit (~$7)
// 2. Loan should be fully paid
// 3. Market makers' offers should be consumed
// 4. Vault should have received the interest payment
auto const finalLoanSle = env.le(loanKeylet);
BEAST_EXPECT(finalLoanSle);
if (finalLoanSle)
{
// Loan should be fully paid (or nearly so, depending on rounding)
BEAST_EXPECT(finalLoanSle->at(sfTotalValueOutstanding) < IOU(1).value());
}
// Borrower should have profit:
// Started with $1000 from loan, spent $1000 on XRP, earned $1008 from selling XRP, paid $1001 to repay loan
// Net: $1000 - $1000 + $1008 - $1001 = $7 profit
auto const borrowerBalance = env.balance(borrower, IOU);
BEAST_EXPECT(borrowerBalance > IOU(6) && borrowerBalance < IOU(8));
// Verify offers were consumed
env.require(offers(marketMaker1, 0));
env.require(offers(marketMaker2, 0));
// Verify vault received the interest payment
// The vault should have received $1001 (principal $1000 + interest $1)
auto const finalVaultSle = env.le(keylet::vault(brokerInfo.vaultID));
BEAST_EXPECT(finalVaultSle);
if (finalVaultSle)
{
auto const finalVaultPseudoAccountID = finalVaultSle->at(sfAccount);
BEAST_EXPECT(finalVaultPseudoAccountID == initialVaultPseudoAccountID);
// Check vault pseudo-account balance increased
auto const finalVaultBalance = env.balance(initialVaultPseudoAccount, IOU);
auto const vaultBalanceIncrease = finalVaultBalance - initialVaultBalance;
BEAST_EXPECT(vaultBalanceIncrease > IOU(0) && vaultBalanceIncrease < IOU(3));
// Check AssetsAvailable increased
auto const finalAssetsAvailable = finalVaultSle->at(sfAssetsAvailable);
auto const assetsAvailableIncrease = finalAssetsAvailable - initialAssetsAvailable;
BEAST_EXPECT(assetsAvailableIncrease > Number{0} && assetsAvailableIncrease < Number{2});
}
}
void
testRiskFreeArbitrageFails()
{
testcase("RiskFreeArbitrageFails");
// Demonstrates that batch transactions with tfAllOrNothing revert completely
// when any transaction fails. In this case, we make it impossible to sell
// the XRP at the higher price, which causes the entire batch to fail,
// including the loan creation.
using namespace jtx;
using namespace jtx::loan;
using namespace std::chrono_literals;
Env env(*this, all);
Account const issuer{"issuer"};
Account const broker{"broker"};
Account const borrower{"borrower"};
Account const marketMaker1{"mm1"}; // Sells XRP at $2.50
auto const IOU = issuer[iouCurrency];
env.fund(XRP(50'000), issuer, broker, borrower, marketMaker1);
env.close();
// Set up trust lines
env(trust(broker, IOU(20'000'000)));
env(trust(borrower, IOU(20'000'000)));
env(trust(marketMaker1, IOU(20'000'000)));
env.close();
// Fund accounts
env(pay(issuer, broker, IOU(15'000'000)));
env(pay(issuer, marketMaker1, IOU(11'000'000)));
env.close();
// Create vault and broker
auto brokerInfo = createVaultAndBroker(env, IOU, broker);
// Get the loan keylet BEFORE creating the loan
auto const brokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
auto const loanSequence = brokerSle->at(sfLoanSequence);
auto const loanKeylet = keylet::loan(brokerInfo.brokerID, loanSequence);
// Market maker creates only ONE offer:
// MM1: Sells 400 XRP for $1000 (price: $2.50 per XRP)
// NOTE: No MM2 offer to buy XRP at higher price!
env(offer(marketMaker1, IOU(1000), XRP(400)));
env.close();
// Record initial state
auto const initialBorrowerBalance = env.balance(borrower, IOU);
auto const initialBrokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
auto const initialLoanSequence = initialBrokerSle->at(sfLoanSequence);
// Record vault state before batch
auto const initialVaultSle = env.le(keylet::vault(brokerInfo.vaultID));
auto const initialVaultPseudoAccountID = initialVaultSle->at(sfAccount);
Account const initialVaultPseudoAccount("VaultPseudo", initialVaultPseudoAccountID);
auto const initialVaultBalance = env.balance(initialVaultPseudoAccount, IOU);
auto const initialAssetsAvailable = initialVaultSle->at(sfAssetsAvailable);
// Try to create the arbitrage batch - this should FAIL:
// 1. Create loan (borrow $1000)
// 2. Buy 400 XRP at $2.50 (cost: $1000) - succeeds
// 3. Sell 400 XRP at $2.52 (revenue: $1008) - FAILS (no offer available)
// 4. Repay loan principal + interest ($1000 + $1 = $1001)
// Because of tfAllOrNothing, the entire batch should revert!
auto const borrowerSeq = env.seq(borrower);
// Transaction 1: Create the loan (borrower receives $1000)
LoanParameters const loanParams{
.account = borrower,
.counter = broker,
.principalRequest = Number{1000},
.interest = TenthBips32{100}, // 0.1% interest
.payTotal = 1,
.payInterval = 86400, // 1 day
.gracePd = 86400, // 1 day grace period
};
auto loanSetTx = loanParams.getTransaction(env, brokerInfo);
// Transaction 2: Buy 400 XRP for $1000 from MM1
auto buyTx = offer(borrower, XRP(400), IOU(1000));
buyTx[jss::Flags] = tfFillOrKill;
// Transaction 3: Try to sell 400 XRP for $1008 - this will FAIL
auto sellTx = offer(borrower, XRP(400), IOU(1008));
sellTx[jss::Flags] = tfFillOrKill;
// Transaction 4: Repay the loan
auto payTx = pay(borrower, loanKeylet.key, IOU(1001));
// Calculate batch fee: 1 signer (broker as counterparty) + 4 transactions
auto const batchFee = batch::calcBatchFee(env, 1, 4);
// Create the batch transaction with tfAllOrNothing
auto batchTxn = env.jt(
batch::outer(borrower, borrowerSeq, batchFee, tfAllOrNothing),
batch::inner(loanSetTx, borrowerSeq + 1),
batch::inner(buyTx, borrowerSeq + 2),
batch::inner(sellTx, borrowerSeq + 3),
batch::inner(payTx, borrowerSeq + 4),
batch::sig(broker));
env(batchTxn);
env.close();
// Verify that EVERYTHING was reverted:
// 1. Loan was NOT created
// 2. Borrower balance unchanged (except for fee)
// 3. LoanSequence unchanged
// 4. Market maker's offer still exists
// 5. Vault received NOTHING
// Loan should NOT exist
auto const finalLoanSle = env.le(loanKeylet);
BEAST_EXPECT(!finalLoanSle);
// Borrower balance should be unchanged (except for batch fee)
auto const finalBorrowerBalance = env.balance(borrower, IOU);
BEAST_EXPECT(finalBorrowerBalance == initialBorrowerBalance);
// LoanSequence should be unchanged (loan was never created)
auto const finalBrokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
auto const finalLoanSequence = finalBrokerSle->at(sfLoanSequence);
BEAST_EXPECT(finalLoanSequence == initialLoanSequence);
// Market maker's offer should still exist (not consumed)
env.require(offers(marketMaker1, 1));
// Verify vault received NOTHING (no loan was created, so no repayment)
auto const finalVaultSle = env.le(keylet::vault(brokerInfo.vaultID));
BEAST_EXPECT(finalVaultSle);
if (finalVaultSle)
{
auto const finalVaultPseudoAccountID = finalVaultSle->at(sfAccount);
BEAST_EXPECT(finalVaultPseudoAccountID == initialVaultPseudoAccountID);
// Vault pseudo-account balance should be unchanged
auto const finalVaultBalance = env.balance(initialVaultPseudoAccount, IOU);
BEAST_EXPECT(finalVaultBalance == initialVaultBalance);
// AssetsAvailable should be unchanged
auto const finalAssetsAvailable = finalVaultSle->at(sfAssetsAvailable);
BEAST_EXPECT(finalAssetsAvailable == initialAssetsAvailable);
}
}
public:
void
run() override
{
testDisabled();
testCreateAsset();
testLoanSetAndDelete();
testLoanSetAndImpair();
testLoanDefaultWithdrawAndPay(all - fixLendingProtocolV1_1);
testLoanDefaultWithdrawAndPay(all);
testRiskFreeArbitrage();
testRiskFreeArbitrageFails();
}
};
BEAST_DEFINE_TESTSUITE(LoanBatch, tx, xrpl);
} // namespace test
} // namespace xrpl

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1073,6 +1073,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"};
@@ -5357,6 +5358,63 @@ 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 vault{env};
auto const keylet = keylet::vault(owner.id(), 1);
auto delTx = vault.del({.owner = owner, .id = keylet.key});
// Test VaultDelete with fixLendingProtocolV1_1 disabled
// Transaction fails if the data field is provided
{
testcase("VaultDelete data fixLendingProtocolV1_1 disabled");
env.disableFeature(fixLendingProtocolV1_1);
delTx[sfMemoData] = strHex(std::string(maxDataPayloadLength, 'A'));
env(delTx, ter(temDISABLED), THISLINE);
env.close();
env.enableFeature(fixLendingProtocolV1_1);
}
// Transaction fails if the data field is too large
{
testcase("VaultDelete data fixLendingProtocolV1_1 enabled data too large");
delTx[sfMemoData] = strHex(std::string(maxDataPayloadLength + 1, 'A'));
env(delTx, ter(temMALFORMED), THISLINE);
env.close();
}
// Transaction fails if the data field is set, but is empty
{
testcase("VaultDelete data fixLendingProtocolV1_1 enabled data empty");
delTx[sfMemoData] = strHex(std::string(0, 'A'));
env(delTx, ter(temMALFORMED), THISLINE);
env.close();
}
{
testcase("VaultDelete data fixLendingProtocolV1_1 enabled data valid");
PrettyAsset const xrpAsset = xrpIssue();
auto [tx, keylet] = vault.create({.owner = owner, .asset = xrpAsset});
env(tx, ter(tesSUCCESS), THISLINE);
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), THISLINE);
env.close();
}
}
public:
void
run() override
@@ -5378,6 +5436,7 @@ public:
testVaultClawbackBurnShares();
testVaultClawbackAssets();
testAssetsMaximum();
testVaultDeleteData();
}
};