mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-23 23:30:54 +00:00
Compare commits
69 Commits
bthomee/rp
...
tapanito/v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95df046451 | ||
|
|
028ccea7a1 | ||
|
|
df85d43d8a | ||
|
|
3fefe54956 | ||
|
|
722b973d98 | ||
|
|
49e64a5d92 | ||
|
|
eba05cf5d0 | ||
|
|
bd1afbdb2b | ||
|
|
5b77cad04c | ||
|
|
80ab03a1f4 | ||
|
|
63e8e607a9 | ||
|
|
88cc3dd15a | ||
|
|
2a9d394b69 | ||
|
|
2fa8305e95 | ||
|
|
3dd44ae18a | ||
|
|
9eaebf2eea | ||
|
|
664b9ba5fe | ||
|
|
189f2d60bd | ||
|
|
ef92373c9f | ||
|
|
605e6166d5 | ||
|
|
af289221f7 | ||
|
|
beb8a1872d | ||
|
|
24db40e56c | ||
|
|
da4513d096 | ||
|
|
2e2fddefe9 | ||
|
|
db997ecad9 | ||
|
|
934b4b03f6 | ||
|
|
2c0d1c9151 | ||
|
|
68e4fbdf2b | ||
|
|
bb0a09ae21 | ||
|
|
d94232007f | ||
|
|
f5c04c421b | ||
|
|
df8bfbe5af | ||
|
|
347d1a19ef | ||
|
|
b97461b8e6 | ||
|
|
43547fcacc | ||
|
|
d02f534987 | ||
|
|
5d538ca59a | ||
|
|
d65fab27a1 | ||
|
|
b5d25c5ab1 | ||
|
|
7222150095 | ||
|
|
8b9ff17ef4 | ||
|
|
a67da5c2ed | ||
|
|
d2f23b2f5b | ||
|
|
4067e5025f | ||
|
|
662325ace0 | ||
|
|
07a6f77ed2 | ||
|
|
e4a716f260 | ||
|
|
ed4330a7d6 | ||
|
|
feba605998 | ||
|
|
b322097529 | ||
|
|
e159d27373 | ||
|
|
ba53026006 | ||
|
|
34773080df | ||
|
|
3029d10102 | ||
|
|
3c3bd75991 | ||
|
|
c89dd9f0a3 | ||
|
|
7459fe454d | ||
|
|
3a0cd45f51 | ||
|
|
106bf48725 | ||
|
|
79be4717f5 | ||
|
|
74c968d4e3 | ||
|
|
167147281c | ||
|
|
ba60306610 | ||
|
|
6674500896 | ||
|
|
c5d7ebe93d | ||
|
|
d0b5ca9dab | ||
|
|
5e51893e9b | ||
|
|
3422c11d02 |
@@ -5,6 +5,7 @@
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
@@ -126,6 +127,9 @@ isSoleShareholder(ReadView const& view, AccountID const& account, SLE::const_ref
|
||||
[[nodiscard]] VaultVersion
|
||||
getVaultVersion(SLE::const_ref vault);
|
||||
|
||||
[[nodiscard]] bool
|
||||
isVaultDonate(Rules const& rules, STTx const& tx);
|
||||
|
||||
/**
|
||||
* Resolves the VaultKind of a vault SLE. Returns VaultKind::ClosedEnded when
|
||||
* sfVaultKind is present and equal to that value; anything else (including an
|
||||
|
||||
@@ -198,6 +198,10 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal;
|
||||
TF_FLAG(tfVaultShareNonTransferable, 0x00020000), \
|
||||
MASK_ADJ(0)) \
|
||||
\
|
||||
TRANSACTION(VaultDeposit, \
|
||||
TF_FLAG(tfVaultDonate, 0x00010000), \
|
||||
MASK_ADJ(0)) \
|
||||
\
|
||||
TRANSACTION(Batch, \
|
||||
TF_FLAG(tfAllOrNothing, 0x00010000) \
|
||||
TF_FLAG(tfOnlyOne, 0x00020000) \
|
||||
|
||||
@@ -20,6 +20,9 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
static std::uint32_t
|
||||
getFlagsMask(PreflightContext const& ctx);
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
|
||||
@@ -2,13 +2,6 @@ ARG BASE_IMAGE=debian:bookworm
|
||||
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
# Packaging runs in a vanilla distro image, so the tooling has to come
|
||||
# from the distro's archive: debhelper for deb, rpm-build (and the
|
||||
# systemd / find-debuginfo macros it depends on) for rpm.
|
||||
# The container also uses git (real history) for
|
||||
# build_pkg.sh's SOURCE_DATE_EPOCH; otherwise it falls back to a tarball
|
||||
# download and the timestamp comes from wall-clock time.
|
||||
|
||||
COPY package/install-packaging-tools.sh /tmp/install-packaging-tools.sh
|
||||
|
||||
RUN /tmp/install-packaging-tools.sh
|
||||
|
||||
@@ -22,12 +22,23 @@ case "${ID}" in
|
||||
;;
|
||||
esac
|
||||
|
||||
# Packaging runs in a vanilla distro image, so the tooling comes from the distro's
|
||||
# archive rather than from nixpkgs:
|
||||
#
|
||||
# - debhelper and dpkg-dev build the DEB
|
||||
# - rpm-build builds the RPM, with systemd-rpm-macros and redhat-rpm-config
|
||||
# supplying the systemd and find-debuginfo macros the spec uses
|
||||
# - git gives build_pkg.sh a real history to read SOURCE_DATE_EPOCH from;
|
||||
# without one the timestamp falls back to the wall clock
|
||||
# - curl uploads the finished packages in publish_pkg.sh
|
||||
# - ca-certificates lets curl and git verify TLS
|
||||
function install() {
|
||||
case "${ID}" in
|
||||
debian | ubuntu)
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
curl \
|
||||
debhelper \
|
||||
debhelper-compat \
|
||||
dpkg-dev \
|
||||
@@ -36,6 +47,7 @@ function install() {
|
||||
|
||||
rhel | centos | rocky | almalinux)
|
||||
dnf install -y --setopt=install_weak_deps=False \
|
||||
curl-minimal \
|
||||
git \
|
||||
rpm-build \
|
||||
redhat-rpm-config \
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
@@ -13,6 +14,7 @@
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
@@ -121,6 +123,12 @@ sharesToAssetsWithdraw(
|
||||
return assets;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
isVaultDonate(Rules const& rules, STTx const& tx)
|
||||
{
|
||||
return rules.enabled(featureLendingProtocolV1_1) && tx.isFlag(tfVaultDonate);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
isSoleShareholder(ReadView const& view, AccountID const& account, SLE::const_ref issuance)
|
||||
{
|
||||
|
||||
@@ -587,10 +587,14 @@ ValidVault::finalize(
|
||||
return std::nullopt;
|
||||
}();
|
||||
|
||||
if (!beforeShares &&
|
||||
(tx.getTxnType() == ttVAULT_DEPOSIT || //
|
||||
tx.getTxnType() == ttVAULT_WITHDRAW || //
|
||||
tx.getTxnType() == ttVAULT_CLAWBACK))
|
||||
bool const isDonate = isVaultDonate(view.rules(), tx);
|
||||
bool const shouldUpdateShares =
|
||||
// Vault Asset donation is the only operation that can succeed without updating shares
|
||||
((tx.getTxnType() == ttVAULT_DEPOSIT && !isDonate) || //
|
||||
tx.getTxnType() == ttVAULT_WITHDRAW || //
|
||||
tx.getTxnType() == ttVAULT_CLAWBACK);
|
||||
|
||||
if (!beforeShares && shouldUpdateShares)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: vault operation succeeded "
|
||||
"without updating shares";
|
||||
@@ -836,34 +840,57 @@ ValidVault::finalize(
|
||||
result = false;
|
||||
}
|
||||
|
||||
auto const maybeAccDeltaShares = deltaShares(tx[sfAccount]);
|
||||
if (!maybeAccDeltaShares)
|
||||
// If assets are donated, check share invariants
|
||||
if (isDonate)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: deposit must change depositor shares";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
// We don't round shares, they are integral MPT
|
||||
auto const& accountDeltaShares = *maybeAccDeltaShares;
|
||||
if (accountDeltaShares.delta <= kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: deposit must increase depositor shares";
|
||||
result = false;
|
||||
}
|
||||
auto const accountDeltaShares = deltaShares(tx[sfAccount]);
|
||||
if (accountDeltaShares)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: donation must not change depositor shares";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
|
||||
auto const maybeVaultDeltaShares = deltaShares(afterVault.pseudoId);
|
||||
if (!maybeVaultDeltaShares || maybeVaultDeltaShares->delta == kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: deposit must change vault shares";
|
||||
return false; // That's all we can do
|
||||
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
|
||||
if (vaultDeltaShares)
|
||||
{
|
||||
JLOG(j.fatal()) << //
|
||||
"Invariant failed: donation must not change vault shares";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
}
|
||||
|
||||
// We don't round shares, they are integral MPT
|
||||
auto const& vaultDeltaShares = *maybeVaultDeltaShares;
|
||||
if (vaultDeltaShares.delta * -1 != accountDeltaShares.delta)
|
||||
else
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: " << //
|
||||
"deposit must change depositor and vault shares by equal amount";
|
||||
result = false;
|
||||
auto const maybeAccDeltaShares = deltaShares(tx[sfAccount]);
|
||||
if (!maybeAccDeltaShares)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: deposit must change depositor shares";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
// We don't need to round shares, they are integral MPT
|
||||
auto const& accountDeltaShares = *maybeAccDeltaShares;
|
||||
if (accountDeltaShares.delta <= kZero)
|
||||
{
|
||||
JLOG(j.fatal())
|
||||
<< "Invariant failed: deposit must increase depositor shares";
|
||||
result = false;
|
||||
}
|
||||
|
||||
auto const maybeVaultDeltaShares = deltaShares(afterVault.pseudoId);
|
||||
if (!maybeVaultDeltaShares || maybeVaultDeltaShares->delta == kZero)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: deposit must change vault shares";
|
||||
return false; // That's all we can do
|
||||
}
|
||||
|
||||
// We don't need to round shares, they are integral MPT
|
||||
auto const& vaultDeltaShares = *maybeVaultDeltaShares;
|
||||
if (vaultDeltaShares.delta * -1 != accountDeltaShares.delta)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: " << //
|
||||
"deposit must change depositor and vault shares by equal amount";
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
|
||||
auto const assetTotalDelta = roundToAsset(
|
||||
|
||||
@@ -33,6 +33,7 @@ VaultDelete::preflight(PreflightContext const& ctx)
|
||||
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 (!validDataLength(ctx.tx[~sfMemoData], kMaxDataPayloadLength))
|
||||
return temMALFORMED;
|
||||
|
||||
|
||||
@@ -21,14 +21,25 @@
|
||||
#include <xrpl/protocol/STTakesAsset.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
std::uint32_t
|
||||
VaultDeposit::getFlagsMask(PreflightContext const& ctx)
|
||||
{
|
||||
if (ctx.rules.enabled(featureLendingProtocolV1_1))
|
||||
return tfVaultDepositMask;
|
||||
|
||||
return tfVaultDepositMask | tfVaultDonate;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
static STAmount
|
||||
roundToVaultScale(STAmount const& amount, SLE::const_ref vault)
|
||||
@@ -115,6 +126,22 @@ VaultDeposit::preclaim(PreclaimContext const& ctx)
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
if (isVaultDonate(ctx.view.rules(), ctx.tx))
|
||||
{
|
||||
if (account != vault->at(sfOwner))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "VaultDeposit: only owner can donate to vault.";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
// Cannot donate to a vault with no shares
|
||||
if (sleIssuance->at(sfOutstandingAmount) == 0)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "VaultDeposit: empty vault cannot receive donations.";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
}
|
||||
|
||||
if (sleIssuance->isFlag(lsfMPTLocked))
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
@@ -239,6 +266,8 @@ VaultDeposit::doApply()
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
auto const isDonate = isVaultDonate(ctx_.view().rules(), ctx_.tx);
|
||||
|
||||
auto const& vaultAccount = vault->at(sfAccount);
|
||||
// Note, vault owner is always authorized
|
||||
if (vault->isFlag(lsfVaultPrivate) && accountID_ != vault->at(sfOwner))
|
||||
@@ -282,44 +311,54 @@ VaultDeposit::doApply()
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
STAmount sharesCreated = {vault->at(sfShareMPTID)}, assetsDeposited;
|
||||
try
|
||||
if (isDonate)
|
||||
{
|
||||
// Compute exchange before transferring any amounts.
|
||||
{
|
||||
auto const maybeShares = assetsToSharesDeposit(vault, sleIssuance, amount);
|
||||
if (!maybeShares)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
sharesCreated = *maybeShares;
|
||||
}
|
||||
if (sharesCreated == beast::kZero)
|
||||
return tecPRECISION_LOSS;
|
||||
|
||||
auto const maybeAssets = sharesToAssetsDeposit(vault, sleIssuance, sharesCreated);
|
||||
if (!maybeAssets)
|
||||
{
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
}
|
||||
if (*maybeAssets > amount)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j_.error()) << "VaultDeposit: would take more than offered.";
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
assetsDeposited = *maybeAssets;
|
||||
XRPL_ASSERT(
|
||||
accountID_ == vault->at(sfOwner), "xrpl::VaultDeposit::doApply : account is owner");
|
||||
assetsDeposited = amount;
|
||||
}
|
||||
catch (std::overflow_error const&)
|
||||
else
|
||||
{
|
||||
// It's easy to hit this exception from Number with large enough Scale
|
||||
// so we avoid spamming the log and only use debug here.
|
||||
JLOG(j_.debug()) //
|
||||
<< "VaultDeposit: overflow error with"
|
||||
<< " scale=" << (int)vault->at(sfScale).value() //
|
||||
<< ", assetsTotal=" << vault->at(sfAssetsTotal).value()
|
||||
<< ", sharesTotal=" << sleIssuance->at(sfOutstandingAmount) << ", amount=" << amount;
|
||||
return tecPATH_DRY;
|
||||
try
|
||||
{
|
||||
// Compute exchange before transferring any amounts.
|
||||
{
|
||||
auto const maybeShares = assetsToSharesDeposit(vault, sleIssuance, amount);
|
||||
if (!maybeShares)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
sharesCreated = *maybeShares;
|
||||
}
|
||||
if (sharesCreated == beast::kZero)
|
||||
return tecPRECISION_LOSS;
|
||||
|
||||
auto const maybeAssets = sharesToAssetsDeposit(vault, sleIssuance, sharesCreated);
|
||||
if (!maybeAssets)
|
||||
{
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
if (*maybeAssets > amount)
|
||||
{
|
||||
// LCOV_EXCL_START
|
||||
JLOG(j_.error()) << "VaultDeposit: would take more than offered.";
|
||||
return tecINTERNAL;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
assetsDeposited = *maybeAssets;
|
||||
}
|
||||
catch (std::overflow_error const&)
|
||||
{
|
||||
// It's easy to hit this exception from Number with large enough Scale
|
||||
// so we avoid spamming the log and only use debug here.
|
||||
JLOG(j_.debug()) //
|
||||
<< "VaultDeposit: overflow error with"
|
||||
<< " scale=" << (int)vault->at(sfScale).value() //
|
||||
<< ", assetsTotal=" << vault->at(sfAssetsTotal).value()
|
||||
<< ", sharesTotal=" << sleIssuance->at(sfOutstandingAmount)
|
||||
<< ", amount=" << amount;
|
||||
return tecPATH_DRY;
|
||||
}
|
||||
}
|
||||
|
||||
XRPL_ASSERT(
|
||||
@@ -363,11 +402,19 @@ VaultDeposit::doApply()
|
||||
}
|
||||
}
|
||||
|
||||
// Transfer shares from vault to depositor.
|
||||
if (auto const ter = accountSend(
|
||||
view(), vaultAccount, accountID_, sharesCreated, j_, {}, WaiveTransferFee::Yes);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
if (isDonate)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
sharesCreated == beast::kZero, "xrpl::VaultDeposit::doApply : donation issued shares");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Transfer shares from vault to depositor.
|
||||
if (auto const ter = accountSend(
|
||||
view(), vaultAccount, accountID_, sharesCreated, j_, {}, WaiveTransferFee::Yes);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
}
|
||||
|
||||
associateAsset(*vault, vaultAsset);
|
||||
|
||||
|
||||
@@ -3992,6 +3992,21 @@ class Invariants_test : public beast::unit_test::Suite
|
||||
precloseXrp,
|
||||
TxAccount::A2);
|
||||
|
||||
doInvariantCheck(
|
||||
Env{*this, testableAmendments() - featureLendingProtocolV1_1},
|
||||
{"deposit must change depositor shares"},
|
||||
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
|
||||
auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
|
||||
return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
|
||||
sample.accountShares.reset();
|
||||
}));
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
|
||||
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
|
||||
precloseXrp,
|
||||
TxAccount::A2);
|
||||
|
||||
doInvariantCheck(
|
||||
{"deposit must change depositor shares"},
|
||||
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
|
||||
@@ -4079,6 +4094,45 @@ class Invariants_test : public beast::unit_test::Suite
|
||||
precloseXrp,
|
||||
TxAccount::A2);
|
||||
|
||||
doInvariantCheck(
|
||||
{"donation must not change depositor shares"},
|
||||
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
|
||||
auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
|
||||
return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
|
||||
sample.accountShares->amount = 10;
|
||||
}));
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{
|
||||
ttVAULT_DEPOSIT,
|
||||
[](STObject& tx) {
|
||||
tx[sfAmount] = XRPAmount(10);
|
||||
tx[sfFlags] = tfVaultDonate;
|
||||
}},
|
||||
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
|
||||
precloseXrp,
|
||||
TxAccount::A2);
|
||||
|
||||
doInvariantCheck(
|
||||
{"donation must not change vault shares"},
|
||||
[&](Account const& a1, Account const& a2, ApplyContext& ac) {
|
||||
auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
|
||||
return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
|
||||
sample.sharesTotal = 10;
|
||||
sample.accountShares = std::nullopt;
|
||||
}));
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{
|
||||
ttVAULT_DEPOSIT,
|
||||
[](STObject& tx) {
|
||||
tx[sfAmount] = XRPAmount(10);
|
||||
tx[sfFlags] = tfVaultDonate;
|
||||
}},
|
||||
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
|
||||
precloseXrp,
|
||||
TxAccount::A2);
|
||||
|
||||
testcase << "Vault withdrawal";
|
||||
doInvariantCheck(
|
||||
{"withdrawal must change vault balance"},
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/random.h>
|
||||
#include <xrpl/beast/net/IPAddress.h>
|
||||
#include <xrpl/beast/net/IPEndpoint.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
@@ -53,6 +52,7 @@
|
||||
#include <xrpl.pb.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
@@ -402,7 +402,7 @@ public:
|
||||
|
||||
enum class PeerSetBehavior {
|
||||
Good,
|
||||
Drop50,
|
||||
DropAlternate,
|
||||
DropAll,
|
||||
DropSkipListReply,
|
||||
DropLedgerDeltaReply,
|
||||
@@ -445,17 +445,13 @@ struct TestPeerSet : public PeerSet
|
||||
protocol::MessageType type,
|
||||
std::shared_ptr<Peer> const& peer) override
|
||||
{
|
||||
int dropRate = 0;
|
||||
if (behavior == PeerSetBehavior::Drop50)
|
||||
{
|
||||
dropRate = 50;
|
||||
}
|
||||
else if (behavior == PeerSetBehavior::DropAll)
|
||||
{
|
||||
dropRate = 100;
|
||||
}
|
||||
if (behavior == PeerSetBehavior::DropAll)
|
||||
return;
|
||||
|
||||
if (randInt(1, 100) <= dropRate)
|
||||
// Drop every other message deterministically. Alternating drops
|
||||
// still exercise the timeout/retry path while guaranteeing every
|
||||
// subtask eventually gets a reply.
|
||||
if (behavior == PeerSetBehavior::DropAlternate && sendCount++ % 2 == 0)
|
||||
return;
|
||||
|
||||
switch (type)
|
||||
@@ -500,6 +496,7 @@ struct TestPeerSet : public PeerSet
|
||||
LedgerReplayMsgHandler& remote;
|
||||
std::shared_ptr<TestPeer> dummyPeer;
|
||||
PeerSetBehavior behavior;
|
||||
std::atomic<int> sendCount{0};
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1397,7 +1394,7 @@ struct LedgerReplayer_test : public beast::unit_test::Suite
|
||||
case PeerSetBehavior::Good:
|
||||
testcase("good network");
|
||||
break;
|
||||
case PeerSetBehavior::Drop50:
|
||||
case PeerSetBehavior::DropAlternate:
|
||||
testcase("network drops 50% messages");
|
||||
break;
|
||||
case PeerSetBehavior::Repeat:
|
||||
@@ -1613,7 +1610,7 @@ struct LedgerReplayer_test : public beast::unit_test::Suite
|
||||
testAllInboundLedgers(4);
|
||||
testPeerSetBehavior(PeerSetBehavior::Good, 1);
|
||||
testPeerSetBehavior(PeerSetBehavior::Good);
|
||||
testPeerSetBehavior(PeerSetBehavior::Drop50);
|
||||
testPeerSetBehavior(PeerSetBehavior::DropAlternate);
|
||||
testPeerSetBehavior(PeerSetBehavior::Repeat);
|
||||
testStop();
|
||||
testSkipListBadReply();
|
||||
|
||||
117
src/test/app/VaultDepositLossInvariantRounding_test.cpp
Normal file
117
src/test/app/VaultDepositLossInvariantRounding_test.cpp
Normal file
@@ -0,0 +1,117 @@
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/pay.h>
|
||||
#include <test/jtx/ter.h>
|
||||
#include <test/jtx/trust.h>
|
||||
#include <test/jtx/vault.h>
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/ledger/OpenView.h>
|
||||
#include <xrpl/ledger/Sandbox.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STNumber.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
// A plain VaultDeposit donation can break the loss invariant by decreasing
|
||||
// assetsTotal - assetsAvailable.
|
||||
//
|
||||
// A donation adds the same amount to sfAssetsTotal and sfAssetsAvailable, but each is a
|
||||
// separate field rounded on its own to the 16-digit IOU grid. When the two totals have
|
||||
// different low digits, the exact sums round in opposite directions, so
|
||||
// assetsTotal - assetsAvailable decreases by up to 1 ULP. A valid loan-written state with
|
||||
// lossUnrealized exactly equal to that difference then fails "loss must not exceed
|
||||
// assetsTotal - assetsAvailable" after the donation, and a legitimate donation dies with
|
||||
// tecINVARIANT_FAILED. Present on develop too (the invariant and the two-field update are
|
||||
// identical), so not a regression.
|
||||
//
|
||||
// The test expects the donation to succeed and fails on tecINVARIANT_FAILED. Manual suite.
|
||||
class VaultDepositLossInvariantRounding_test : public beast::unit_test::Suite
|
||||
{
|
||||
// Force a valid loan-written pre-state: loss == assetsTotal - assetsAvailable exactly.
|
||||
// Every value is 16-digit representable, so staging is exact.
|
||||
void
|
||||
stageState(
|
||||
test::jtx::Env& env,
|
||||
Keylet const& vaultKeylet,
|
||||
Number const& assetsTotal,
|
||||
Number const& assetsAvailable,
|
||||
Number const& loss)
|
||||
{
|
||||
BEAST_EXPECT(env.app().getOpenLedger().modify( //
|
||||
[&](OpenView& view, beast::Journal) -> bool {
|
||||
Sandbox sb(&view, TapNone);
|
||||
auto v = sb.peek(vaultKeylet);
|
||||
if (!v)
|
||||
return false;
|
||||
v->at(sfAssetsTotal) = assetsTotal;
|
||||
v->at(sfAssetsAvailable) = assetsAvailable;
|
||||
v->at(sfLossUnrealized) = loss;
|
||||
sb.update(v);
|
||||
sb.apply(view);
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
|
||||
// pre-state assetsTotal 2.000000000000001
|
||||
// assetsAvailable 1.000000000000006
|
||||
// loss 0.999999999999995 (== assetsTotal - assetsAvailable, valid)
|
||||
// donate 50 (added to each field, so the difference should not change)
|
||||
// total 2.000000000000001 + 50 = 52.000000000000001 -> stores 52 (rounds down)
|
||||
// available 1.000000000000006 + 50 = 51.000000000000006 -> stores 51.00000000000001 (up)
|
||||
// diff 52 - 51.00000000000001 = 0.99999999999999 < loss 0.999999999999995
|
||||
// result tecINVARIANT_FAILED, should be tesSUCCESS
|
||||
void
|
||||
testDonationBreaksLossInvariant()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
testcase("donation breaks the loss invariant by rounding");
|
||||
|
||||
Env env(*this);
|
||||
Account const owner{"owner"};
|
||||
Account const issuer{"issuer"};
|
||||
env.fund(XRP(1'000'000), owner, issuer);
|
||||
env.close();
|
||||
|
||||
PrettyAsset const asset = issuer["USD"];
|
||||
env(trust(owner, asset(1'000'000)));
|
||||
env.close();
|
||||
env(pay(issuer, owner, asset(100)));
|
||||
env.close();
|
||||
|
||||
// Seed so the vault has outstanding shares (a donation needs them), then force the
|
||||
// lawful loss pre-state.
|
||||
Vault vault{env};
|
||||
auto const [tx, keylet] = vault.create({.owner = owner, .asset = asset.raw()});
|
||||
env(tx);
|
||||
env.close();
|
||||
env(Vault::deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
|
||||
env.close();
|
||||
stageState(
|
||||
env,
|
||||
keylet,
|
||||
Number{2'000'000'000'000'001LL, -15},
|
||||
Number{1'000'000'000'000'006LL, -15},
|
||||
Number{999'999'999'999'995LL, -15});
|
||||
|
||||
env(Vault::deposit(
|
||||
{.depositor = owner,
|
||||
.id = keylet.key,
|
||||
.amount = asset(50),
|
||||
.flags = tfVaultDonate}),
|
||||
Ter(tesSUCCESS));
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testDonationBreaksLossInvariant();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE_MANUAL(VaultDepositLossInvariantRounding, formal_verification, xrpl);
|
||||
|
||||
} // namespace xrpl
|
||||
210
src/test/app/VaultDustDebit_test.cpp
Normal file
210
src/test/app/VaultDustDebit_test.cpp
Normal file
@@ -0,0 +1,210 @@
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/fee.h>
|
||||
#include <test/jtx/flags.h>
|
||||
#include <test/jtx/pay.h>
|
||||
#include <test/jtx/ter.h>
|
||||
#include <test/jtx/trust.h>
|
||||
#include <test/jtx/vault.h>
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Keylet.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STNumber.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
// A vault debit too small to change the stored total dies on an invariant, not a clean error.
|
||||
//
|
||||
// A clawback recovery or a withdrawal payout can be smaller than half a precision step of
|
||||
// sfAssetsTotal, so subtracting it rounds the balance back unchanged. No transactor check
|
||||
// rejects this, the shares move, and ValidVault then fails the transaction with "must
|
||||
// decrease vault balance": the user pays a fee and gets tecINVARIANT_FAILED plus a fatal
|
||||
// log, where an upfront tecPRECISION_LOSS is expected (as in the zero-share case).
|
||||
class VaultDustDebit_test : public beast::unit_test::Suite
|
||||
{
|
||||
struct VaultData
|
||||
{
|
||||
Number assetsTotal;
|
||||
Number assetsAvailable;
|
||||
Number pseudoLine; // vault pseudo-account trust line
|
||||
std::uint64_t sharesTotal;
|
||||
std::uint64_t holderShares;
|
||||
};
|
||||
|
||||
static VaultData
|
||||
readVaultData(test::jtx::Env& env, Keylet const& vaultKeylet)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
auto const sle = env.le(vaultKeylet);
|
||||
auto const shareMptId = sle->at(sfShareMPTID);
|
||||
PrettyAsset const asset = Account{"issuer"}["USD"];
|
||||
Account const pseudo{"pseudo", sle->at(sfAccount)};
|
||||
return {
|
||||
.assetsTotal = sle->at(sfAssetsTotal),
|
||||
.assetsAvailable = sle->at(sfAssetsAvailable),
|
||||
.pseudoLine = static_cast<Number>(env.balance(pseudo, asset.raw()).value()),
|
||||
.sharesTotal = env.le(keylet::mptokenIssuance(shareMptId))->at(sfOutstandingAmount),
|
||||
.holderShares =
|
||||
env.le(keylet::mptoken(shareMptId, Account{"holder"}))->at(sfMPTAmount)};
|
||||
}
|
||||
|
||||
static void
|
||||
vaultDeposit(
|
||||
test::jtx::Env& env,
|
||||
Keylet const& vaultKeylet,
|
||||
test::jtx::Account const& depositor,
|
||||
Number const& amount,
|
||||
bool isDonation = false)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
PrettyAsset const asset = Account{"issuer"}["USD"];
|
||||
Vault::DepositArgs args{
|
||||
.depositor = depositor, .id = vaultKeylet.key, .amount = asset(amount)};
|
||||
if (isDonation)
|
||||
args.flags = tfVaultDonate;
|
||||
env(Vault::deposit(args));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Fund everyone, the holder seeds (scale 6, shares mint exactly), the owner donates to
|
||||
// set the share price without minting shares.
|
||||
Keylet
|
||||
buildSeededVault(test::jtx::Env& env, Number const& seed, Number const& donation)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
Account const owner{"owner"};
|
||||
Account const issuer{"issuer"};
|
||||
Account const holder{"holder"};
|
||||
env.fund(XRP(1'000'000), owner, issuer, holder);
|
||||
env.close();
|
||||
// IOU clawback must be enabled while the issuer has no trust line yet.
|
||||
env(fset(issuer, asfAllowTrustLineClawback));
|
||||
env.close();
|
||||
|
||||
PrettyAsset const asset = issuer["USD"];
|
||||
env(trust(owner, asset(100'000'000'000'000LL)));
|
||||
env(trust(holder, asset(100'000'000'000'000LL)));
|
||||
env.close();
|
||||
env(pay(issuer, holder, asset(seed)));
|
||||
env(pay(issuer, owner, asset(donation)));
|
||||
env.close();
|
||||
|
||||
Vault vault{env};
|
||||
auto const [tx, keylet] = vault.create({.owner = owner, .asset = asset.raw()});
|
||||
env(tx);
|
||||
env.close();
|
||||
vaultDeposit(env, keylet, holder, seed);
|
||||
vaultDeposit(env, keylet, owner, donation, true);
|
||||
|
||||
VaultData const initial = readVaultData(env, keylet);
|
||||
BEAST_EXPECTS(initial.assetsTotal == seed + donation, to_string(initial.assetsTotal));
|
||||
BEAST_EXPECTS(initial.pseudoLine == seed + donation, to_string(initial.pseudoLine));
|
||||
return keylet;
|
||||
}
|
||||
|
||||
// The rejection must leave every stored balance and share count untouched.
|
||||
void
|
||||
expectUnchanged(VaultData const& before, VaultData const& after)
|
||||
{
|
||||
BEAST_EXPECTS(after.assetsTotal == before.assetsTotal, to_string(after.assetsTotal));
|
||||
BEAST_EXPECTS(
|
||||
after.assetsAvailable == before.assetsAvailable, to_string(after.assetsAvailable));
|
||||
BEAST_EXPECTS(after.pseudoLine == before.pseudoLine, to_string(after.pseudoLine));
|
||||
BEAST_EXPECTS(after.sharesTotal == before.sharesTotal, std::to_string(after.sharesTotal));
|
||||
BEAST_EXPECTS(
|
||||
after.holderShares == before.holderShares, std::to_string(after.holderShares));
|
||||
}
|
||||
|
||||
// Claw back an amount whose recovery cannot change the stored total. The clean outcome
|
||||
// is tecPRECISION_LOSS, but C++ runs into the invariant.
|
||||
void
|
||||
runDustClawback(Number const& seed, Number const& donation, Number const& clawAmount)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
testcase("clawback " + to_string(clawAmount) + " from " + to_string(seed + donation));
|
||||
|
||||
Env env(*this);
|
||||
Keylet const vaultKeylet = buildSeededVault(env, seed, donation);
|
||||
Account const issuer{"issuer"};
|
||||
PrettyAsset const asset = issuer["USD"];
|
||||
|
||||
VaultData const before = readVaultData(env, vaultKeylet);
|
||||
env(Vault::clawback(
|
||||
{.issuer = issuer,
|
||||
.id = vaultKeylet.key,
|
||||
.holder = Account{"holder"},
|
||||
.amount = asset(clawAmount)}),
|
||||
Ter(tecPRECISION_LOSS));
|
||||
env.close();
|
||||
expectUnchanged(before, readVaultData(env, vaultKeylet));
|
||||
}
|
||||
|
||||
// Redeem a share count whose payout cannot change the stored total. The clean outcome
|
||||
// is tecPRECISION_LOSS, but C++ runs into the invariant.
|
||||
void
|
||||
runDustWithdraw(Number const& seed, Number const& donation, std::int64_t redeemShares)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
testcase(
|
||||
"withdraw " + std::to_string(redeemShares) + " shares from " +
|
||||
to_string(seed + donation));
|
||||
|
||||
Env env(*this);
|
||||
Keylet const vaultKeylet = buildSeededVault(env, seed, donation);
|
||||
MPTIssue const share{env.le(vaultKeylet)->at(sfShareMPTID)};
|
||||
|
||||
VaultData const before = readVaultData(env, vaultKeylet);
|
||||
env(Vault::withdraw(
|
||||
{.depositor = Account{"holder"},
|
||||
.id = vaultKeylet.key,
|
||||
.amount = STAmount{share, redeemShares}}),
|
||||
Ter(tecPRECISION_LOSS));
|
||||
env.close();
|
||||
expectUnchanged(before, readVaultData(env, vaultKeylet));
|
||||
}
|
||||
|
||||
// Caught by the 16-digit STAmount round: the exact new total fits Number, then rounds
|
||||
// back when the field is stored.
|
||||
// vault AssetsTotal 2e12 (seed 1e12 + donate 1e12), 1e18 shares -> 1 share = 2e-6
|
||||
// debit clawback 2e-6, or withdraw 1 share, both take 2e-6
|
||||
// exact 2e12 - 2e-6 = 1999999999999.999998 (19 digits, fits Number)
|
||||
// stored 2e12 (16-digit round brings it back)
|
||||
// result tecINVARIANT_FAILED, should be tecPRECISION_LOSS
|
||||
void
|
||||
testBelowStoredPrecision()
|
||||
{
|
||||
runDustClawback(Number{1, 12}, Number{1, 12}, Number{2, -6});
|
||||
runDustWithdraw(Number{1, 12}, Number{1, 12}, 1);
|
||||
}
|
||||
|
||||
// Caught one layer down, by the 19-digit Number subtraction itself.
|
||||
// vault AssetsTotal 1.5e13 (seed 9.2e12 + donate 5.8e12), 9.2e18 shares
|
||||
// share 1 share = 1.63e-6
|
||||
// debit clawback 2e-6, or withdraw 1 share, both take ~1.63e-6
|
||||
// exact 1.5e13 - 1.63e-6 needs 20 digits -> rounds back to 1.5e13 in Number
|
||||
// result tecINVARIANT_FAILED, should be tecPRECISION_LOSS
|
||||
void
|
||||
testBelowNumberPrecision()
|
||||
{
|
||||
runDustClawback(Number{92, 11}, Number{58, 11}, Number{2, -6});
|
||||
runDustWithdraw(Number{92, 11}, Number{58, 11}, 1);
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testBelowStoredPrecision();
|
||||
testBelowNumberPrecision();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE_MANUAL(VaultDustDebit, formal_verification, xrpl);
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -9664,6 +9664,230 @@ class Vault_test : public beast::unit_test::Suite
|
||||
env.enableFeature(fixCleanup3_3_0);
|
||||
}
|
||||
|
||||
void
|
||||
testVaultDepositDonate()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
std::string const prefix = "VaultDeposit donate";
|
||||
|
||||
Env env{*this};
|
||||
Vault const vault{env};
|
||||
|
||||
auto const vaultShareBalance = [&](Keylet const& vaultKeylet) {
|
||||
auto const sleVault = env.le(vaultKeylet);
|
||||
BEAST_EXPECT(sleVault != nullptr);
|
||||
|
||||
auto const sleIssuance = env.le(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
|
||||
BEAST_EXPECT(sleIssuance != nullptr);
|
||||
|
||||
return sleIssuance->at(sfOutstandingAmount);
|
||||
};
|
||||
|
||||
auto const vaultAssetBalance = [&](Keylet const& vaultKeylet) {
|
||||
auto const sleVault = env.le(vaultKeylet);
|
||||
BEAST_EXPECT(sleVault != nullptr);
|
||||
|
||||
return std::make_pair(sleVault->at(sfAssetsAvailable), sleVault->at(sfAssetsTotal));
|
||||
};
|
||||
|
||||
Account const owner{"owner"};
|
||||
Account const depositor{"depositor"};
|
||||
env.fund(XRP(1'000'000), owner, depositor);
|
||||
env.close();
|
||||
|
||||
auto const depositAmount = XRP(10);
|
||||
|
||||
auto const [tx, keylet] = vault.create({.owner = owner, .asset = xrpIssue()});
|
||||
env(tx, Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// With featureLendingProtocolV1_1 disabled, donations fail
|
||||
{
|
||||
testcase(prefix + " fails with featureLendingProtocolV1_1 disabled");
|
||||
env.disableFeature(featureLendingProtocolV1_1);
|
||||
auto const tx = vault.deposit({
|
||||
.depositor = owner,
|
||||
.id = keylet.key,
|
||||
.amount = depositAmount,
|
||||
.flags = tfVaultDonate,
|
||||
});
|
||||
env(tx, Ter(temINVALID_FLAG));
|
||||
env.enableFeature(featureLendingProtocolV1_1);
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Donation is not allowed to an empty vault
|
||||
{
|
||||
testcase(prefix + " fails to an empty vault");
|
||||
auto const tx = vault.deposit({
|
||||
.depositor = owner,
|
||||
.id = keylet.key,
|
||||
.amount = depositAmount,
|
||||
.flags = tfVaultDonate,
|
||||
});
|
||||
env(tx, Ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Further unit tests require assets in the Vault
|
||||
env(vault.deposit({
|
||||
.depositor = depositor,
|
||||
.id = keylet.key,
|
||||
.amount = depositAmount,
|
||||
}),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// Donation is not allowed by a non-owner
|
||||
{
|
||||
testcase(prefix + " fails by a non-owner");
|
||||
auto const tx = vault.deposit({
|
||||
.depositor = depositor,
|
||||
.id = keylet.key,
|
||||
.amount = depositAmount,
|
||||
.flags = tfVaultDonate,
|
||||
});
|
||||
env(tx, Ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Donation cannot exceed assets maximum
|
||||
{
|
||||
testcase(prefix + " cannot exceed assets maximum");
|
||||
auto tx = vault.set({
|
||||
.owner = owner,
|
||||
.id = keylet.key,
|
||||
});
|
||||
tx[sfAssetsMaximum] = XRP(30).number();
|
||||
env(tx, Ter(tesSUCCESS));
|
||||
|
||||
tx = vault.deposit({
|
||||
.depositor = owner,
|
||||
.id = keylet.key,
|
||||
.amount = depositAmount + XRP(30),
|
||||
.flags = tfVaultDonate,
|
||||
});
|
||||
|
||||
env(tx, Ter(tecLIMIT_EXCEEDED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
testcase(prefix + " succeeds");
|
||||
auto const shareBalance = vaultShareBalance(keylet);
|
||||
auto const [assetsAvailable, assetsTotal] = vaultAssetBalance(keylet);
|
||||
|
||||
auto tx = vault.deposit({
|
||||
.depositor = owner,
|
||||
.id = keylet.key,
|
||||
.amount = depositAmount,
|
||||
.flags = tfVaultDonate,
|
||||
});
|
||||
env(tx, Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
auto const shareBalanceAfterDeposit = vaultShareBalance(keylet);
|
||||
auto const [assetsAvailableAfterDeposit, assetsTotalAfterDeposit] =
|
||||
vaultAssetBalance(keylet);
|
||||
|
||||
BEAST_EXPECT(shareBalance == shareBalanceAfterDeposit);
|
||||
BEAST_EXPECT(assetsAvailable + depositAmount.number() == assetsAvailableAfterDeposit);
|
||||
BEAST_EXPECT(assetsTotal + depositAmount.number() == assetsTotalAfterDeposit);
|
||||
|
||||
auto const sleVault = env.le(keylet);
|
||||
if (!BEAST_EXPECT(sleVault))
|
||||
return;
|
||||
|
||||
// The depositor can withdraw their assets and the donated amount
|
||||
Asset const shareAsset(sleVault->at(sfShareMPTID));
|
||||
tx = vault.withdraw(
|
||||
{.depositor = depositor, .id = keylet.key, .amount = shareAsset(shareBalance)});
|
||||
env(tx, Ter(tesSUCCESS));
|
||||
|
||||
auto const shareBalanceAfterWithdraw = vaultShareBalance(keylet);
|
||||
auto const [assetsAvailableAfterWithdraw, assetsTotalAfterWithdraw] =
|
||||
vaultAssetBalance(keylet);
|
||||
BEAST_EXPECT(shareBalanceAfterWithdraw == 0);
|
||||
BEAST_EXPECT(assetsAvailableAfterWithdraw == 0);
|
||||
BEAST_EXPECT(assetsTotalAfterWithdraw == 0);
|
||||
}
|
||||
|
||||
// Test donation with non-1:1 share ratio.
|
||||
// A prior donation skews the ratio so that 1 share > 1 asset.
|
||||
// The donated amount must land exactly, not rounded via shares.
|
||||
{
|
||||
testcase(prefix + " succeeds with non-1:1 share ratio");
|
||||
|
||||
// Create a fresh vault
|
||||
auto const [createTx, vk] = vault.create({.owner = owner, .asset = xrpIssue()});
|
||||
env(createTx, Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// Depositor puts in 10 XRP → gets 10 shares at 1:1
|
||||
env(vault.deposit({
|
||||
.depositor = depositor,
|
||||
.id = vk.key,
|
||||
.amount = XRP(10),
|
||||
}),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// Owner donates 7 XRP → ratio becomes 17 assets / 10 shares
|
||||
env(vault.deposit({
|
||||
.depositor = owner,
|
||||
.id = vk.key,
|
||||
.amount = XRP(7),
|
||||
.flags = tfVaultDonate,
|
||||
}),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
auto const sharesAfterFirstDonate = vaultShareBalance(vk);
|
||||
auto const [availAfterFirstDonate, totalAfterFirstDonate] = vaultAssetBalance(vk);
|
||||
|
||||
// Shares unchanged (donation doesn't mint shares)
|
||||
BEAST_EXPECT(sharesAfterFirstDonate == 10'000'000);
|
||||
// Assets increased by exactly the donated amount
|
||||
BEAST_EXPECT(availAfterFirstDonate == 17'000'000);
|
||||
BEAST_EXPECT(totalAfterFirstDonate == 17'000'000);
|
||||
|
||||
// Donate again at the skewed 17:10 ratio — 3 XRP
|
||||
env(vault.deposit({
|
||||
.depositor = owner,
|
||||
.id = vk.key,
|
||||
.amount = XRP(3),
|
||||
.flags = tfVaultDonate,
|
||||
}),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
auto const sharesAfterSecondDonate = vaultShareBalance(vk);
|
||||
auto const [availAfterSecondDonate, totalAfterSecondDonate] = vaultAssetBalance(vk);
|
||||
|
||||
// Shares still unchanged
|
||||
BEAST_EXPECT(sharesAfterSecondDonate == 10'000'000);
|
||||
// Assets increased by exactly 3 XRP (20 total)
|
||||
BEAST_EXPECT(availAfterSecondDonate == 20'000'000);
|
||||
BEAST_EXPECT(totalAfterSecondDonate == 20'000'000);
|
||||
|
||||
// Depositor withdraws all shares — should get all 20 XRP
|
||||
auto const sleVault = env.le(vk);
|
||||
if (!BEAST_EXPECT(sleVault))
|
||||
return;
|
||||
Asset const shareAsset(sleVault->at(sfShareMPTID));
|
||||
env(vault.withdraw(
|
||||
{.depositor = depositor,
|
||||
.id = vk.key,
|
||||
.amount = shareAsset(sharesAfterSecondDonate)}),
|
||||
Ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(vaultShareBalance(vk) == 0);
|
||||
BEAST_EXPECT(vaultAssetBalance(vk).first == 0);
|
||||
BEAST_EXPECT(vaultAssetBalance(vk).second == 0);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -9707,6 +9931,7 @@ public:
|
||||
testAssetsMaximum();
|
||||
testVaultDeleteMemoData();
|
||||
testVaultCreateLEVersion();
|
||||
testVaultDepositDonate();
|
||||
testBug6LimitBypassWithShares();
|
||||
testRemoveEmptyHoldingLockedAmount();
|
||||
testRemoveEmptyHoldingConfidentialBalances();
|
||||
|
||||
@@ -65,6 +65,8 @@ Vault::deposit(DepositArgs const& args)
|
||||
jv[jss::Account] = args.depositor.human();
|
||||
jv[sfVaultID] = to_string(args.id);
|
||||
jv[jss::Amount] = toJson(args.amount);
|
||||
if (args.flags)
|
||||
jv[jss::Flags] = *args.flags;
|
||||
return jv;
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ struct Vault
|
||||
Account depositor;
|
||||
uint256 id;
|
||||
STAmount amount;
|
||||
std::optional<std::uint32_t> flags = std::nullopt;
|
||||
};
|
||||
|
||||
static json::Value
|
||||
|
||||
Reference in New Issue
Block a user