diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index 8781eee72b..925776a1b4 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -1751,7 +1751,7 @@ class Invariants_test : public beast::unit_test::suite AccountID account; int amount; }; - struct Adjustements + struct Adjustments { std::optional assetsTotal = {}; std::optional 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; diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 8b2254a840..bdebb70d09 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -43,6 +44,8 @@ #include #include +#include + 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 { + 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(); } }; diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 2cfcb6d258..f7f67ee1d6 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -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( + balanceDelta = static_cast( before->getFieldU64(sfOutstandingAmount)); sign = 1; break; case ltMPTOKEN: - balance = + balanceDelta = static_cast(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( + balanceDelta -= Number(static_cast( after->getFieldU64(sfOutstandingAmount))); sign = 1; break; case ltMPTOKEN: - balance -= Number( + balanceDelta -= Number( static_cast(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 { + 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 { auto const it = [&]() { if (id == afterVault.pseudoId) @@ -2774,20 +2795,7 @@ ValidVault::finalize( if (!issuerDeposit) { - auto const accountDeltaAssets = - [&]() -> std::optional { - 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 { - 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 { 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";