mirror of
				https://github.com/XRPLF/rippled.git
				synced 2025-11-04 11:15:56 +00:00 
			
		
		
		
	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:
		@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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";
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user