Add MPT to ValidAMM invariant. Add ValidAMM invariant unit-tests.

This commit is contained in:
Gregory Tsipenyuk
2026-04-18 10:29:51 -04:00
parent e85f010ef0
commit 62bdfc6bf6
2 changed files with 127 additions and 26 deletions

View File

@@ -27,8 +27,9 @@ ValidAMM::visitEntry(
}
// AMM pool changed
else if (
(type == ltRIPPLE_STATE && ((after->getFlags() & lsfAMMNode) != 0u)) ||
(type == ltACCOUNT_ROOT && after->isFieldPresent(sfAMMID)))
(type == ltRIPPLE_STATE && after->isFlag(lsfAMMNode)) ||
(type == ltACCOUNT_ROOT && after->isFieldPresent(sfAMMID)) ||
(type == ltMPTOKEN && after->isFlag(lsfMPTAMM)))
{
ammPoolChanged_ = true;
}
@@ -68,9 +69,9 @@ ValidAMM::finalizeVote(bool enforce, beast::Journal const& j) const
{
// LPTokens and the pool can not change on vote
// LCOV_EXCL_START
JLOG(j.error()) << "AMMVote invariant failed: " << lptAMMBalanceBefore_.value_or(STAmount{})
<< " " << lptAMMBalanceAfter_.value_or(STAmount{}) << " "
<< ammPoolChanged_;
JLOG(j.error()) << "Invariant failed: AMMVote failed, "
<< lptAMMBalanceBefore_.value_or(STAmount{}) << " "
<< lptAMMBalanceAfter_.value_or(STAmount{}) << " " << ammPoolChanged_;
if (enforce)
return false;
// LCOV_EXCL_STOP
@@ -86,7 +87,7 @@ ValidAMM::finalizeBid(bool enforce, beast::Journal const& j) const
{
// The pool can not change on bid
// LCOV_EXCL_START
JLOG(j.error()) << "AMMBid invariant failed: pool changed";
JLOG(j.error()) << "Invariant failed: AMMBid failed, pool changed";
if (enforce)
return false;
// LCOV_EXCL_STOP
@@ -97,7 +98,7 @@ ValidAMM::finalizeBid(bool enforce, beast::Journal const& j) const
(*lptAMMBalanceAfter_ > *lptAMMBalanceBefore_ || *lptAMMBalanceAfter_ <= beast::zero))
{
// LCOV_EXCL_START
JLOG(j.error()) << "AMMBid invariant failed: " << *lptAMMBalanceBefore_ << " "
JLOG(j.error()) << "Invariant failed: AMMBid failed, " << *lptAMMBalanceBefore_ << " "
<< *lptAMMBalanceAfter_;
if (enforce)
return false;
@@ -117,7 +118,7 @@ ValidAMM::finalizeCreate(
if (!ammAccount_)
{
// LCOV_EXCL_START
JLOG(j.error()) << "AMMCreate invariant failed: AMM object is not created";
JLOG(j.error()) << "Invariant failed: AMMCreate failed, AMM object is not created";
if (enforce)
return false;
// LCOV_EXCL_STOP
@@ -138,8 +139,8 @@ ValidAMM::finalizeCreate(
if (!validBalances(amount, amount2, *lptAMMBalanceAfter_, ZeroAllowed::No) ||
ammLPTokens(amount, amount2, lptAMMBalanceAfter_->get<Issue>()) != *lptAMMBalanceAfter_)
{
JLOG(j.error()) << "AMMCreate invariant failed: " << amount << " " << amount2 << " "
<< *lptAMMBalanceAfter_;
JLOG(j.error()) << "Invariant failed: AMMCreate failed, " << amount << " " << amount2
<< " " << *lptAMMBalanceAfter_;
if (enforce)
return false;
}
@@ -156,7 +157,7 @@ ValidAMM::finalizeDelete(bool enforce, TER res, beast::Journal const& j) const
// LCOV_EXCL_START
std::string const msg = (isTesSuccess(res)) ? "AMM object is not deleted on tesSUCCESS"
: "AMM object is changed on tecINCOMPLETE";
JLOG(j.error()) << "AMMDelete invariant failed: " << msg;
JLOG(j.error()) << "Invariant failed: AMMDelete failed, " << msg;
if (enforce)
return false;
// LCOV_EXCL_STOP
@@ -171,7 +172,7 @@ ValidAMM::finalizeDEX(bool enforce, beast::Journal const& j) const
if (ammAccount_)
{
// LCOV_EXCL_START
JLOG(j.error()) << "AMM swap invariant failed: AMM object changed";
JLOG(j.error()) << "Invariant failed: AMM swap failed, AMM object changed";
if (enforce)
return false;
// LCOV_EXCL_STOP
@@ -204,10 +205,10 @@ ValidAMM::generalInvariant(
};
if (!nonNegativeBalances || (!strongInvariantCheck && !weakInvariantCheck()))
{
JLOG(j.error()) << "AMM " << tx.getTxnType()
<< " invariant failed: " << tx.getHash(HashPrefix::transactionID) << " "
<< ammPoolChanged_ << " " << amount << " " << amount2 << " "
<< poolProductMean << " " << lptAMMBalanceAfter_->getText() << " "
JLOG(j.error()) << "Invariant failed: AMM " << tx.getTxnType() << " failed, "
<< tx.getHash(HashPrefix::transactionID) << " " << ammPoolChanged_ << " "
<< amount << " " << amount2 << " " << poolProductMean << " "
<< lptAMMBalanceAfter_->getText() << " "
<< ((*lptAMMBalanceAfter_ == beast::zero)
? Number{1}
: ((*lptAMMBalanceAfter_ - poolProductMean) / poolProductMean));
@@ -227,7 +228,7 @@ ValidAMM::finalizeDeposit(
if (!ammAccount_)
{
// LCOV_EXCL_START
JLOG(j.error()) << "AMMDeposit invariant failed: AMM object is deleted";
JLOG(j.error()) << "Invariant failed: AMMDeposit failed, AMM object is deleted";
if (enforce)
return false;
// LCOV_EXCL_STOP

View File

@@ -139,11 +139,7 @@ class Invariants_test : public beast::unit_test::suite
for (TER const& terExpect : ters)
{
terActual = ac.checkInvariants(terActual, fee);
if (!BEAST_EXPECTS(terExpect == terActual, std::to_string(TERtoInt(terActual))))
{
std::cout << terExpect << " " << terActual << " "
<< std::to_string(TERtoInt(terActual)) << std::endl;
}
BEAST_EXPECTS(terExpect == terActual, std::to_string(TERtoInt(terActual)));
auto const messages = sink.messages().str();
if (!isTesSuccess(terActual))
@@ -157,10 +153,7 @@ class Invariants_test : public beast::unit_test::suite
// std::cerr << messages << '\n';
for (auto const& m : expect_logs)
{
if (!BEAST_EXPECTS(messages.find(m) != std::string::npos, m))
{
std::cout << messages << " " << m << std::endl;
}
BEAST_EXPECTS(messages.find(m) != std::string::npos, m);
}
}
}
@@ -4084,6 +4077,112 @@ class Invariants_test : public beast::unit_test::suite
}
}
void
testAMM()
{
testcase << "AMM";
using namespace jtx;
MPTID mptID{};
uint256 ammID{};
AccountID ammAccountID{};
Account const gw{"gw"};
Issue lptIssue{};
PrettyAsset poolAsset{xrpIssue()};
auto deleteAMMAccount = [&](ApplyContext& ac, bool) {
auto sle = ac.view().peek(keylet::account(ammAccountID));
if (!sle)
return false;
ac.view().erase(sle);
return true;
};
auto updateLPTokensBalance = [&](ApplyContext& ac, std::int64_t amount) {
auto sle = ac.view().peek(keylet::amm(ammID));
if (!sle)
return false;
sle->setFieldAmount(sfLPTokenBalance, STAmount{lptIssue, amount});
ac.view().update(sle);
return true;
};
auto updateLPTokensBadAmount = [&](ApplyContext& ac, bool) {
return updateLPTokensBalance(ac, -1);
};
auto updateLPTokensBadBalance = [&](ApplyContext& ac, bool) {
return updateLPTokensBalance(ac, 200'000'000);
};
auto updateAMM = [&](ApplyContext& ac, bool) { return updateLPTokensBalance(ac, 10); };
auto updateAMMPool = [&](ApplyContext& ac, bool isMPT) {
if (isMPT)
{
auto sle = ac.view().peek(keylet::mptoken(mptID, ammAccountID));
if (!sle)
return false;
sle->setFieldU64(sfMPTAmount, 1);
ac.view().update(sle);
return true;
}
auto sle = ac.view().peek(keylet::account(ammAccountID));
if (!sle)
return false;
sle->setFieldAmount(sfBalance, XRP(1));
ac.view().update(sle);
return true;
};
auto test =
[&](auto const txType, auto&& update, bool isMPT, TER error = tecINVARIANT_FAILED) {
doInvariantCheck(
{{"AMM"}},
[&](Account const& A1, Account const& A2, ApplyContext& ac) {
return update(ac, isMPT);
},
XRPAmount{},
STTx{txType, [&](STObject& tx) {}},
{tecINVARIANT_FAILED, error},
[&](Account const& A1, Account const& A2, Env& env) {
env.fund(XRP(1'000), gw);
poolAsset = [&]() -> PrettyAsset {
if (isMPT)
{
MPT const mpt = MPTTester({.env = env, .issuer = gw});
mptID = mpt.issuanceID;
return mpt;
}
return gw["USD"];
}();
AMM const amm(env, gw, XRP(100), poolAsset(100));
ammAccountID = amm.ammAccount();
ammID = amm.ammID();
lptIssue = amm.lptIssue();
return true;
});
};
for (bool const isMPT : {false, true})
{
auto const error = isMPT ? TER(tecINVARIANT_FAILED) : TER(tefINVARIANT_FAILED);
for (auto txType : {ttAMM_CREATE, ttAMM_DEPOSIT, ttAMM_CLAWBACK, ttAMM_WITHDRAW})
{
test(txType, deleteAMMAccount, isMPT, tefINVARIANT_FAILED);
test(txType, updateLPTokensBadAmount, isMPT);
test(txType, updateLPTokensBadBalance, isMPT);
}
for (auto txType : {ttAMM_BID, ttAMM_VOTE})
{
test(txType, updateAMMPool, isMPT, error);
test(txType, updateLPTokensBadAmount, isMPT);
test(txType, updateLPTokensBadBalance, isMPT);
}
for (auto txType : {ttAMM_DELETE, ttCHECK_CASH, ttOFFER_CREATE, ttPAYMENT})
{
test(txType, updateAMM, isMPT);
}
}
}
public:
void
run() override
@@ -4110,6 +4209,7 @@ public:
testValidLoanBroker();
testVault();
testMPT();
testAMM();
}
};