mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-22 14:50:54 +00:00
fix: Add ValidPermissionedDEX invariant track for fully consumed offer (#6736)
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
@@ -18,8 +19,13 @@
|
||||
namespace xrpl {
|
||||
|
||||
void
|
||||
ValidPermissionedDEX::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
|
||||
ValidPermissionedDEX::visitEntry(bool isDelete, SLE::const_ref, SLE::const_ref after)
|
||||
{
|
||||
// Post-fixCleanup3_4_0: skip when after is null (defensive).
|
||||
// Pre-amendment: original after-only path via the `if (after && ...)` checks below.
|
||||
if (isFeatureEnabled(fixCleanup3_4_0) && !after)
|
||||
return;
|
||||
|
||||
auto trackDomain = [this, isDelete](uint256 const& domain) {
|
||||
domainsOld_.insert(domain);
|
||||
if (!isDelete)
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
#include <xrpl/tx/applySteps.h>
|
||||
#include <xrpl/tx/invariants/AMMInvariant.h>
|
||||
#include <xrpl/tx/invariants/DirectoryInvariant.h>
|
||||
#include <xrpl/tx/invariants/PermissionedDEXInvariant.h>
|
||||
#include <xrpl/tx/invariants/VaultInvariant.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -2247,6 +2248,90 @@ class Invariants_test : public beast::unit_test::Suite
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testPermissionedDEXDeletedOfferFallback()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
testcase << "PermissionedDEX null after";
|
||||
|
||||
// Tx is OfferCreate on pd2. Tracking pd1 fails the invariant iff that
|
||||
// domain lands in the set finalize consults. after == null is never
|
||||
// tracked (pre-340: after-only; post-340: early return) — same result,
|
||||
// both sides are coverage/regression that we do not fall back to before.
|
||||
auto const check = [this](
|
||||
FeatureBitset features,
|
||||
bool const afterIsNull,
|
||||
bool const isDelete,
|
||||
bool const expectInvariantFailure) {
|
||||
Env env(*this, features);
|
||||
|
||||
Account const a1{"A1"};
|
||||
Account const a2{"A2"};
|
||||
env.fund(XRP(1000), a1, a2);
|
||||
env.close();
|
||||
|
||||
[[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env, a1, a2);
|
||||
[[maybe_unused]] auto [seq2, pd2] = createPermissionedDomainEnv(env, a1, a2);
|
||||
env.close();
|
||||
|
||||
auto sleOffer =
|
||||
std::make_shared<SLE>(keylet::offer(a2.id(), SeqProxy::rawSequence(10)));
|
||||
sleOffer->setAccountID(sfAccount, a2);
|
||||
sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
|
||||
sleOffer->setFieldAmount(sfTakerGets, XRP(1));
|
||||
sleOffer->setFieldH256(sfDomainID, pd1);
|
||||
|
||||
CurrentTransactionRulesGuard const rulesGuard(env.current()->rules());
|
||||
|
||||
ValidPermissionedDEX invariant;
|
||||
if (afterIsNull)
|
||||
{
|
||||
// Defensive path: after is null. Must not fall back to before.
|
||||
invariant.visitEntry(isDelete, sleOffer, nullptr);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Normal / real-erase path: after is the offer on pd1.
|
||||
invariant.visitEntry(isDelete, nullptr, sleOffer);
|
||||
}
|
||||
|
||||
STTx const tx{ttOFFER_CREATE, [&pd2, &a1](STObject& tx) {
|
||||
tx.setFieldH256(sfDomainID, pd2);
|
||||
tx.setFieldAmount(sfTakerPays, a1["USD"](10));
|
||||
tx.setFieldAmount(sfTakerGets, XRP(1));
|
||||
}};
|
||||
|
||||
test::StreamSink sink{beast::Severity::Warning};
|
||||
beast::Journal const jlog{sink};
|
||||
bool const passed =
|
||||
invariant.finalize(tx, tesSUCCESS, XRPAmount{}, *env.current(), jlog);
|
||||
BEAST_EXPECT(passed != expectInvariantFailure);
|
||||
if (expectInvariantFailure)
|
||||
{
|
||||
BEAST_EXPECT(sink.messages().str().contains("transaction consumed wrong domains"));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(sink.messages().str().empty());
|
||||
}
|
||||
};
|
||||
|
||||
auto const pre = defaultAmendments() - fixCleanup3_4_0;
|
||||
auto const post = defaultAmendments() | fixCleanup3_4_0;
|
||||
|
||||
// after == null: not tracked
|
||||
check(pre, true, true, false);
|
||||
check(post, true, true, false);
|
||||
|
||||
// after == offer on pd1
|
||||
// pre-340: domainsOld_ (delete still inserted) → fail
|
||||
check(pre, false, true, true);
|
||||
// post-340: isDelete → only domainsOld_ → pass; !isDelete → domains_ → fail
|
||||
check(post, false, true, false);
|
||||
check(post, false, false, true);
|
||||
}
|
||||
|
||||
void
|
||||
testBookDirectoryExchangeRate()
|
||||
{
|
||||
@@ -6571,6 +6656,7 @@ public:
|
||||
testPermissionedDomainInvariants(defaultAmendments() - fixCleanup3_1_3);
|
||||
testPermissionedDEX(defaultAmendments() | fixCleanup3_1_3);
|
||||
testPermissionedDEX(defaultAmendments() - fixCleanup3_1_3);
|
||||
testPermissionedDEXDeletedOfferFallback();
|
||||
testBookDirectoryExchangeRate();
|
||||
testNoModifiedUnmodifiableFields();
|
||||
testValidPseudoAccounts();
|
||||
|
||||
Reference in New Issue
Block a user