mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-03 08:46:46 +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