fix: invariant error in fee-sized VaultWithdraw (#5876)

This changes fixes an invariant error where the amount withdrawn is equal to the transaction fee.

Co-authored-by: Bart Thomee <11445373+bthomee@users.noreply.github.com>
This commit is contained in:
Bronek Kozicki
2025-10-28 18:12:11 +00:00
parent a8b1a01d9e
commit 8951419dbe
3 changed files with 338 additions and 88 deletions

View File

@@ -1751,7 +1751,7 @@ class Invariants_test : public beast::unit_test::suite
AccountID account;
int amount;
};
struct Adjustements
struct Adjustments
{
std::optional<int> assetsTotal = {};
std::optional<int> assetsAvailable = {};
@@ -1764,7 +1764,7 @@ class Invariants_test : public beast::unit_test::suite
};
auto constexpr adjust = [&](ApplyView& ac,
ripple::Keylet keylet,
Adjustements args) {
Adjustments args) {
auto sleVault = ac.peek(keylet);
if (!sleVault)
return false;
@@ -1790,9 +1790,11 @@ class Invariants_test : public beast::unit_test::suite
ac.update(sleVault);
if (args.sharesTotal)
{
(*sleShares)[sfOutstandingAmount] =
*(*sleShares)[sfOutstandingAmount] + *args.sharesTotal;
ac.update(sleShares);
ac.update(sleShares);
}
auto const assets = *(*sleVault)[sfAsset];
auto const pseudoId = *(*sleVault)[sfAccount];
@@ -1863,17 +1865,17 @@ class Invariants_test : public beast::unit_test::suite
};
constexpr auto args =
[](AccountID id, int adjustement, auto fn) -> Adjustements {
Adjustements sample = {
.assetsTotal = adjustement,
.assetsAvailable = adjustement,
[](AccountID id, int adjustment, auto fn) -> Adjustments {
Adjustments sample = {
.assetsTotal = adjustment,
.assetsAvailable = adjustment,
.lossUnrealized = 0,
.sharesTotal = adjustement,
.vaultAssets = adjustement,
.sharesTotal = adjustment,
.vaultAssets = adjustment,
.accountAssets = //
AccountAmount{id, -adjustement},
AccountAmount{id, -adjustment},
.accountShares = //
AccountAmount{id, adjustement}};
AccountAmount{id, adjustment}};
fn(sample);
return sample;
};
@@ -2285,7 +2287,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 0, [&](Adjustements& sample) {
args(A2.id(), 0, [&](Adjustments& sample) {
sample.assetsAvailable = (DROPS_PER_XRP * -100).value();
sample.assetsTotal = (DROPS_PER_XRP * -200).value();
sample.sharesTotal = -1;
@@ -2354,7 +2356,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 0, [&](Adjustements& sample) {
args(A2.id(), 0, [&](Adjustments& sample) {
sample.lossUnrealized = 13;
sample.assetsTotal = 20;
}));
@@ -2374,7 +2376,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 100, [&](Adjustements& sample) {
args(A2.id(), 100, [&](Adjustments& sample) {
sample.lossUnrealized = 13;
}));
},
@@ -2395,7 +2397,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 0, [&](Adjustements& sample) {
args(A2.id(), 0, [&](Adjustments& sample) {
sample.assetsMaximum = 1;
}));
},
@@ -2412,7 +2414,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 0, [&](Adjustements& sample) {
args(A2.id(), 0, [&](Adjustments& sample) {
sample.assetsMaximum = -1;
}));
},
@@ -2461,7 +2463,7 @@ class Invariants_test : public beast::unit_test::suite
ac.view().update(sleShares);
return adjust(
ac.view(), keylet, args(A2.id(), 10, [](Adjustements&) {}));
ac.view(), keylet, args(A2.id(), 10, [](Adjustments&) {}));
},
XRPAmount{},
STTx{ttVAULT_DEPOSIT, [](STObject&) {}},
@@ -2474,7 +2476,7 @@ class Invariants_test : public beast::unit_test::suite
[&](Account const& A1, Account const& A2, ApplyContext& ac) {
auto const keylet = keylet::vault(A1.id(), ac.view().seq());
adjust(
ac.view(), keylet, args(A2.id(), 10, [](Adjustements&) {}));
ac.view(), keylet, args(A2.id(), 10, [](Adjustments&) {}));
auto sleVault = ac.view().peek(keylet);
if (!sleVault)
@@ -2850,7 +2852,9 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 0, [&](Adjustements& sample) {}));
args(A2.id(), 0, [](Adjustments& sample) {
sample.vaultAssets.reset();
}));
},
XRPAmount{},
STTx{ttVAULT_DEPOSIT, [](STObject&) {}},
@@ -2864,7 +2868,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 200, [&](Adjustements& sample) {
args(A2.id(), 200, [&](Adjustments& sample) {
sample.assetsMaximum = 1;
}));
},
@@ -2898,7 +2902,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A3.id(), -10, [&](Adjustements& sample) {
args(A3.id(), -10, [&](Adjustments& sample) {
sample.accountAssets->amount = -100;
}));
},
@@ -2931,7 +2935,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 10, [&](Adjustements& sample) {
args(A2.id(), 10, [&](Adjustments& sample) {
sample.vaultAssets = -20;
sample.accountAssets->amount = 10;
}));
@@ -2959,7 +2963,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 10, [&](Adjustements& sample) {
args(A2.id(), 10, [&](Adjustments& sample) {
sample.accountAssets->amount = 0;
}));
},
@@ -2978,8 +2982,8 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 10, [&](Adjustements& sample) {
sample.accountShares->amount = 0;
args(A2.id(), 10, [&](Adjustments& sample) {
sample.accountShares.reset();
}));
},
XRPAmount{},
@@ -2994,10 +2998,11 @@ class Invariants_test : public beast::unit_test::suite
{"deposit must change vault shares"},
[&](Account const& A1, Account const& A2, ApplyContext& ac) {
auto const keylet = keylet::vault(A1.id(), ac.view().seq());
return adjust(
ac.view(),
keylet,
args(A2.id(), 10, [&](Adjustements& sample) {
args(A2.id(), 10, [](Adjustments& sample) {
sample.sharesTotal = 0;
}));
},
@@ -3019,7 +3024,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 10, [&](Adjustements& sample) {
args(A2.id(), 10, [&](Adjustments& sample) {
sample.accountShares->amount = -5;
sample.sharesTotal = -10;
}));
@@ -3032,6 +3037,33 @@ class Invariants_test : public beast::unit_test::suite
precloseXrp,
TxAccount::A2);
doInvariantCheck(
{"deposit and assets outstanding must add up"},
[&](Account const& A1, Account const& A2, ApplyContext& ac) {
auto sleA3 = ac.view().peek(keylet::account(A3.id()));
(*sleA3)[sfBalance] = *(*sleA3)[sfBalance] - 2000;
ac.view().update(sleA3);
auto const keylet = keylet::vault(A1.id(), ac.view().seq());
return adjust(
ac.view(),
keylet,
args(A2.id(), 10, [&](Adjustments& sample) {
sample.assetsTotal = 11;
}));
},
XRPAmount{2000},
STTx{
ttVAULT_DEPOSIT,
[&](STObject& tx) {
tx[sfAmount] = XRPAmount(10);
tx[sfDelegate] = A3.id();
tx[sfFee] = XRPAmount(2000);
}},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseXrp,
TxAccount::A2);
doInvariantCheck(
{"deposit and assets outstanding must add up",
"deposit and assets available must add up"},
@@ -3040,7 +3072,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 10, [&](Adjustements& sample) {
args(A2.id(), 10, [&](Adjustments& sample) {
sample.assetsTotal = 7;
sample.assetsAvailable = 7;
}));
@@ -3061,7 +3093,9 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 0, [&](Adjustements& sample) {}));
args(A2.id(), 0, [](Adjustments& sample) {
sample.vaultAssets.reset();
}));
},
XRPAmount{},
STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
@@ -3087,7 +3121,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A3.id(), -10, [&](Adjustements& sample) {
args(A3.id(), -10, [&](Adjustments& sample) {
sample.accountAssets->amount = -100;
}));
},
@@ -3123,7 +3157,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), -10, [&](Adjustements& sample) {
args(A2.id(), -10, [&](Adjustments& sample) {
sample.vaultAssets = 10;
sample.accountAssets->amount = -20;
}));
@@ -3141,7 +3175,7 @@ class Invariants_test : public beast::unit_test::suite
if (!adjust(
ac.view(),
keylet,
args(A2.id(), -10, [&](Adjustements& sample) {
args(A2.id(), -10, [&](Adjustments& sample) {
*sample.vaultAssets -= 5;
})))
return false;
@@ -3167,8 +3201,8 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), -10, [&](Adjustements& sample) {
sample.accountShares->amount = 0;
args(A2.id(), -10, [&](Adjustments& sample) {
sample.accountShares.reset();
}));
},
XRPAmount{},
@@ -3184,7 +3218,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), -10, [&](Adjustements& sample) {
args(A2.id(), -10, [](Adjustments& sample) {
sample.sharesTotal = 0;
}));
},
@@ -3203,7 +3237,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), -10, [&](Adjustements& sample) {
args(A2.id(), -10, [&](Adjustments& sample) {
sample.accountShares->amount = 5;
sample.sharesTotal = 10;
}));
@@ -3222,7 +3256,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), -10, [&](Adjustements& sample) {
args(A2.id(), -10, [&](Adjustments& sample) {
sample.assetsTotal = -15;
sample.assetsAvailable = -15;
}));
@@ -3233,6 +3267,33 @@ class Invariants_test : public beast::unit_test::suite
precloseXrp,
TxAccount::A2);
doInvariantCheck(
{"withdrawal and assets outstanding must add up"},
[&](Account const& A1, Account const& A2, ApplyContext& ac) {
auto sleA3 = ac.view().peek(keylet::account(A3.id()));
(*sleA3)[sfBalance] = *(*sleA3)[sfBalance] - 2000;
ac.view().update(sleA3);
auto const keylet = keylet::vault(A1.id(), ac.view().seq());
return adjust(
ac.view(),
keylet,
args(A2.id(), -10, [&](Adjustments& sample) {
sample.assetsTotal = -7;
}));
},
XRPAmount{2000},
STTx{
ttVAULT_WITHDRAW,
[&](STObject& tx) {
tx[sfAmount] = XRPAmount(10);
tx[sfDelegate] = A3.id();
tx[sfFee] = XRPAmount(2000);
}},
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
precloseXrp,
TxAccount::A2);
auto const precloseMpt =
[&](Account const& A1, Account const& A2, Env& env) -> bool {
env.fund(XRP(1000), A3, A4);
@@ -3292,7 +3353,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), -10, [&](Adjustements& sample) {
args(A2.id(), -10, [&](Adjustments& sample) {
sample.accountShares->amount = 5;
}));
},
@@ -3312,8 +3373,8 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), -1, [&](Adjustements& sample) {
sample.vaultAssets = 0;
args(A2.id(), -1, [&](Adjustments& sample) {
sample.vaultAssets.reset();
}));
},
XRPAmount{},
@@ -3331,7 +3392,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 0, [&](Adjustements& sample) {}));
args(A2.id(), 0, [&](Adjustments& sample) {}));
},
XRPAmount{},
STTx{ttVAULT_CLAWBACK, [](STObject&) {}},
@@ -3346,7 +3407,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A2.id(), 0, [&](Adjustements& sample) {}));
args(A2.id(), 0, [&](Adjustments& sample) {}));
},
XRPAmount{},
STTx{
@@ -3364,7 +3425,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A4.id(), 10, [&](Adjustements& sample) {
args(A4.id(), 10, [&](Adjustments& sample) {
sample.sharesTotal = 0;
}));
},
@@ -3385,8 +3446,8 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A4.id(), -10, [&](Adjustements& sample) {
sample.accountShares->amount = 0;
args(A4.id(), -10, [&](Adjustments& sample) {
sample.accountShares.reset();
}));
},
XRPAmount{},
@@ -3408,7 +3469,7 @@ class Invariants_test : public beast::unit_test::suite
return adjust(
ac.view(),
keylet,
args(A4.id(), -10, [&](Adjustements& sample) {
args(A4.id(), -10, [&](Adjustments& sample) {
sample.accountShares->amount = -8;
sample.assetsTotal = -7;
sample.assetsAvailable = -7;

View File

@@ -19,6 +19,7 @@
#include <test/jtx.h>
#include <test/jtx/AMMTest.h>
#include <test/jtx/Env.h>
#include <test/jtx/amount.h>
#include <xrpl/basics/base_uint.h>
@@ -43,6 +44,8 @@
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol/jss.h>
#include <optional>
namespace ripple {
class Vault_test : public beast::unit_test::suite
@@ -303,6 +306,55 @@ class Vault_test : public beast::unit_test::suite
BEAST_EXPECT(
env.balance(depositor, shares) == share(200 * scale));
}
else
{
testcase(prefix + " deposit/withdrawal same or less than fee");
auto const amount = env.current()->fees().base;
auto tx = vault.deposit(
{.depositor = depositor,
.id = keylet.key,
.amount = amount});
env(tx);
env.close();
tx = vault.withdraw(
{.depositor = depositor,
.id = keylet.key,
.amount = amount});
env(tx);
env.close();
tx = vault.deposit(
{.depositor = depositor,
.id = keylet.key,
.amount = amount});
env(tx);
env.close();
// Withdraw to 3rd party
tx = vault.withdraw(
{.depositor = depositor,
.id = keylet.key,
.amount = amount});
tx[sfDestination] = charlie.human();
env(tx);
env.close();
tx = vault.deposit(
{.depositor = depositor,
.id = keylet.key,
.amount = amount - 1});
env(tx);
env.close();
tx = vault.withdraw(
{.depositor = depositor,
.id = keylet.key,
.amount = amount - 1});
env(tx);
env.close();
}
{
testcase(
@@ -4795,6 +4847,147 @@ class Vault_test : public beast::unit_test::suite
}
}
void
testDelegate()
{
using namespace test::jtx;
Env env(*this, testable_amendments());
Account alice{"alice"};
Account bob{"bob"};
Account carol{"carol"};
struct CaseArgs
{
PrettyAsset asset = xrpIssue();
};
auto const xrpBalance =
[this](
Env const& env, Account const& account) -> std::optional<long> {
auto sle = env.le(keylet::account(account.id()));
if (BEAST_EXPECT(sle != nullptr))
return sle->getFieldAmount(sfBalance).xrp().drops();
return std::nullopt;
};
auto testCase = [&, this](auto test, CaseArgs args = {}) {
Env env{*this, testable_amendments() | featureSingleAssetVault};
Vault vault{env};
// use different initial amount to distinguish the source balance
env.fund(XRP(10000), alice);
env.fund(XRP(20000), bob);
env.fund(XRP(30000), carol);
env.close();
env(delegate::set(
carol,
alice,
{"Payment",
"VaultCreate",
"VaultSet",
"VaultDelete",
"VaultDeposit",
"VaultWithdraw",
"VaultClawback"}));
test(env, vault, args.asset);
};
testCase([&, this](Env& env, Vault& vault, PrettyAsset const& asset) {
testcase("delegated vault creation");
auto startBalance = xrpBalance(env, carol);
if (!BEAST_EXPECT(startBalance.has_value()))
return;
auto [tx, keylet] = vault.create({.owner = carol, .asset = asset});
env(tx, delegate::as(alice));
env.close();
BEAST_EXPECT(xrpBalance(env, carol) == *startBalance);
});
testCase([&, this](Env& env, Vault& vault, PrettyAsset const& asset) {
testcase("delegated deposit and withdrawal");
auto [tx, keylet] = vault.create({.owner = carol, .asset = asset});
env(tx);
env.close();
auto const amount = 1513;
auto const baseFee = env.current()->fees().base;
auto startBalance = xrpBalance(env, carol);
if (!BEAST_EXPECT(startBalance.has_value()))
return;
tx = vault.deposit(
{.depositor = carol,
.id = keylet.key,
.amount = asset(amount)});
env(tx, delegate::as(alice));
env.close();
BEAST_EXPECT(xrpBalance(env, carol) == *startBalance - amount);
tx = vault.withdraw(
{.depositor = carol,
.id = keylet.key,
.amount = asset(amount - 1)});
env(tx, delegate::as(alice));
env.close();
BEAST_EXPECT(xrpBalance(env, carol) == *startBalance - 1);
tx = vault.withdraw(
{.depositor = carol, .id = keylet.key, .amount = asset(1)});
env(tx);
env.close();
BEAST_EXPECT(xrpBalance(env, carol) == *startBalance - baseFee);
});
testCase([&, this](Env& env, Vault& vault, PrettyAsset const& asset) {
testcase("delegated withdrawal same as base fee and deletion");
auto [tx, keylet] = vault.create({.owner = carol, .asset = asset});
env(tx);
env.close();
auto const amount = 25537;
auto const baseFee = env.current()->fees().base;
auto startBalance = xrpBalance(env, carol);
if (!BEAST_EXPECT(startBalance.has_value()))
return;
tx = vault.deposit(
{.depositor = carol,
.id = keylet.key,
.amount = asset(amount)});
env(tx);
env.close();
BEAST_EXPECT(
xrpBalance(env, carol) == *startBalance - amount - baseFee);
tx = vault.withdraw(
{.depositor = carol,
.id = keylet.key,
.amount = asset(baseFee)});
env(tx, delegate::as(alice));
env.close();
BEAST_EXPECT(xrpBalance(env, carol) == *startBalance - amount);
tx = vault.withdraw(
{.depositor = carol,
.id = keylet.key,
.amount = asset(amount - baseFee)});
env(tx, delegate::as(alice));
env.close();
BEAST_EXPECT(xrpBalance(env, carol) == *startBalance - baseFee);
tx = vault.del({.owner = carol, .id = keylet.key});
env(tx, delegate::as(alice));
env.close();
});
}
public:
void
run() override
@@ -4812,6 +5005,7 @@ public:
testFailedPseudoAccount();
testScaleIOU();
testRPC();
testDelegate();
}
};

View File

@@ -2230,13 +2230,12 @@ ValidVault::visitEntry(
after != nullptr && (before != nullptr || !isDelete),
"ripple::ValidVault::visitEntry : some object is available");
// `Number balance` will capture the difference (delta) between "before"
// 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 balance{};
Number balanceDelta{};
// By default do not add anything to deltas
std::int8_t sign = 0;
if (before)
{
@@ -2249,18 +2248,18 @@ 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));
balance = static_cast<std::int64_t>(
balanceDelta = static_cast<std::int64_t>(
before->getFieldU64(sfOutstandingAmount));
sign = 1;
break;
case ltMPTOKEN:
balance =
balanceDelta =
static_cast<std::int64_t>(before->getFieldU64(sfMPTAmount));
sign = -1;
break;
case ltACCOUNT_ROOT:
case ltRIPPLE_STATE:
balance = before->getFieldAmount(sfBalance);
balanceDelta = before->getFieldAmount(sfBalance);
sign = -1;
break;
default:;
@@ -2278,18 +2277,18 @@ 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));
balance -= Number(static_cast<std::int64_t>(
balanceDelta -= Number(static_cast<std::int64_t>(
after->getFieldU64(sfOutstandingAmount)));
sign = 1;
break;
case ltMPTOKEN:
balance -= Number(
balanceDelta -= Number(
static_cast<std::int64_t>(after->getFieldU64(sfMPTAmount)));
sign = -1;
break;
case ltACCOUNT_ROOT:
case ltRIPPLE_STATE:
balance -= Number(after->getFieldAmount(sfBalance));
balanceDelta -= Number(after->getFieldAmount(sfBalance));
sign = -1;
break;
default:;
@@ -2297,8 +2296,13 @@ ValidVault::visitEntry(
}
uint256 const key = (before ? before->key() : after->key());
if (sign && balance != zero)
deltas_[key] = balance * sign;
// Append to deltas if sign is non-zero, i.e. an object of an interesting
// type has been updated. A transaction may update an object even when
// its balance has not changed, e.g. transaction fee equals the amount
// transferred to the account. We intentionally do not compare balanceDelta
// against zero, to avoid missing such updates.
if (sign != 0)
deltas_[key] = balanceDelta * sign;
}
bool
@@ -2604,6 +2608,23 @@ ValidVault::finalize(
},
vaultAsset.value());
};
auto const deltaAssetsTxAccount = [&]() -> std::optional<Number> {
auto ret = deltaAssets(tx[sfAccount]);
// Nothing returned or not XRP transaction
if (!ret.has_value() || !vaultAsset.native())
return ret;
// Delegated transaction; no need to compensate for fees
if (auto const delegate = tx[~sfDelegate];
delegate.has_value() && *delegate != tx[sfAccount])
return ret;
*ret += fee.drops();
if (*ret == zero)
return std::nullopt;
return ret;
};
auto const deltaShares = [&](AccountID const& id) -> std::optional<Number> {
auto const it = [&]() {
if (id == afterVault.pseudoId)
@@ -2774,20 +2795,7 @@ ValidVault::finalize(
if (!issuerDeposit)
{
auto const accountDeltaAssets =
[&]() -> std::optional<Number> {
if (auto ret = deltaAssets(tx[sfAccount]); ret)
{
// Compensate for transaction fee deduced from
// sfAccount
if (vaultAsset.native())
*ret += fee.drops();
if (*ret != zero)
return ret;
}
return std::nullopt;
}();
auto const accountDeltaAssets = deltaAssetsTxAccount();
if (!accountDeltaAssets)
{
JLOG(j.fatal()) << //
@@ -2840,7 +2848,7 @@ ValidVault::finalize(
}
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
if (!vaultDeltaShares)
if (!vaultDeltaShares || *vaultDeltaShares == zero)
{
JLOG(j.fatal()) << //
"Invariant failed: deposit must change vault shares";
@@ -2909,20 +2917,7 @@ ValidVault::finalize(
if (!issuerWithdrawal)
{
auto const accountDeltaAssets =
[&]() -> std::optional<Number> {
if (auto ret = deltaAssets(tx[sfAccount]); ret)
{
// Compensate for transaction fee deduced from
// sfAccount
if (vaultAsset.native())
*ret += fee.drops();
if (*ret != zero)
return ret;
}
return std::nullopt;
}();
auto const accountDeltaAssets = deltaAssetsTxAccount();
auto const otherAccountDelta =
[&]() -> std::optional<Number> {
if (auto const destination = tx[~sfDestination];
@@ -2979,7 +2974,7 @@ ValidVault::finalize(
}
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
if (!vaultDeltaShares)
if (!vaultDeltaShares || *vaultDeltaShares == zero)
{
JLOG(j.fatal()) << //
"Invariant failed: withdrawal must change vault shares";
@@ -3064,7 +3059,7 @@ ValidVault::finalize(
}
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
if (!vaultDeltaShares)
if (!vaultDeltaShares || *vaultDeltaShares == zero)
{
JLOG(j.fatal()) << //
"Invariant failed: clawback must change vault shares";