Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions include/xrpl/ledger/helpers/AMMHelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,19 @@ ammPoolHolds(
AuthHandling authHandling,
beast::Journal const j);

/** Check AMM pool product invariant after deposit or withdraw.
* Returns tecPRECISION_LOSS if sqrt(asset1 * asset2) < newLPTokenBalance,
* tesSUCCESS otherwise. Skips check when newLPTokenBalance is zero (last withdrawal).
*/
TER
checkAMMPrecisionLoss(
ReadView const& view,
AccountID const& ammAccountID,
Asset const& asset1,
Asset const& asset2,
STAmount const& newLPTokenBalance,
beast::Journal j);

/** Get AMM pool and LP token balances. If both optIssue are
* provided then they are used as the AMM token pair issues.
* Otherwise the missing issues are fetched from ammSle.
Expand Down
23 changes: 23 additions & 0 deletions src/libxrpl/ledger/helpers/AMMHelpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,29 @@ ammPoolHolds(
return std::make_pair(assetInBalance, assetOutBalance);
}

TER
checkAMMPrecisionLoss(
ReadView const& view,
AccountID const& ammAccountID,
Asset const& asset1,
Asset const& asset2,
STAmount const& newLPTokenBalance,
beast::Journal j)
{
if (newLPTokenBalance <= beast::zero)
return tesSUCCESS;
auto const [amount, amount2] =
ammPoolHolds(view, ammAccountID, asset1, asset2, fhIGNORE_FREEZE, ahIGNORE_AUTH, j);
auto const poolProductMean = root2(amount * amount2);
if (poolProductMean >= newLPTokenBalance)
return tesSUCCESS;
// Strong check failed. Allow the same relative tolerance as the invariant
// checker's weak check. Only return tecPRECISION_LOSS when both fail.
if (withinRelativeDistance(poolProductMean, Number{newLPTokenBalance}, Number{1, -11}))
return tesSUCCESS;
return tecPRECISION_LOSS;
}

Expected<std::tuple<STAmount, STAmount, STAmount>, TER>
ammHolds(
ReadView const& view,
Expand Down
5 changes: 4 additions & 1 deletion src/libxrpl/tx/invariants/AMMInvariant.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,10 @@ ValidAMM::generalInvariant(
bool const nonNegativeBalances =
validBalances(amount, amount2, *lptAMMBalanceAfter_, zeroAllowed);
bool const strongInvariantCheck = poolProductMean >= *lptAMMBalanceAfter_;
// Allow for a small relative error if strongInvariantCheck fails
// Allow for a small relative error if strongInvariantCheck fails.
// With fixCleanup3_2_0, the "both fail" case is caught earlier in the
// transaction layer (tecPRECISION_LOSS), so this invariant is only
// reached when the strong check passes or the weak check saves it.
auto weakInvariantCheck = [&]() {
return *lptAMMBalanceAfter_ != beast::zero &&
withinRelativeDistance(poolProductMean, Number{*lptAMMBalanceAfter_}, Number{1, -11});
Expand Down
10 changes: 10 additions & 0 deletions src/libxrpl/tx/transactors/dex/AMMClawback.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,16 @@ AMMClawback::applyGuts(Sandbox& sb)
if (!isTesSuccess(result))
return result; // LCOV_EXCL_LINE

if (sb.rules().enabled(fixCleanup3_2_0) && sb.rules().enabled(fixAMMv1_3))
{
if (auto const ter =
checkAMMPrecisionLoss(sb, ammAccount, asset, asset2, newLPTokenBalance, j_);
!isTesSuccess(ter))
{
return ter;
}
}

auto const res =
AMMWithdraw::deleteAMMAccountIfEmpty(sb, ammSle, newLPTokenBalance, asset, asset2, j_);
if (!res.second)
Expand Down
12 changes: 12 additions & 0 deletions src/libxrpl/tx/transactors/dex/AMMDeposit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,18 @@ AMMDeposit::applyGuts(Sandbox& sb)
XRPL_ASSERT(
newLPTokenBalance > beast::zero,
"xrpl::AMMDeposit::applyGuts : valid new LP token balance");
// Defensive check: deposit formulas with fixAMMv1_3 round LP tokens
// down and asset amounts up, so sqrt(pool1*pool2) >= newLPTokenBalance
// is guaranteed to hold. This branch is not expected to be reachable.
if (sb.rules().enabled(fixCleanup3_2_0) && sb.rules().enabled(fixAMMv1_3))
{
if (auto const ter = checkAMMPrecisionLoss(
sb, ammAccountID, ctx_.tx[sfAsset], ctx_.tx[sfAsset2], newLPTokenBalance, j_);
!isTesSuccess(ter))
{
return {ter, false}; // LCOV_EXCL_LINE
}
}
ammSle->setFieldAmount(sfLPTokenBalance, newLPTokenBalance);
// LP depositing into AMM empty state gets the auction slot
// and the voting
Expand Down
10 changes: 10 additions & 0 deletions src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,16 @@ AMMWithdraw::applyGuts(Sandbox& sb)
if (!isTesSuccess(result))
return {result, false};

if (sb.rules().enabled(fixCleanup3_2_0) && sb.rules().enabled(fixAMMv1_3))
{
if (auto const ter = checkAMMPrecisionLoss(
sb, ammAccountID, ctx_.tx[sfAsset], ctx_.tx[sfAsset2], newLPTokenBalance, j_);
!isTesSuccess(ter))
{
return {ter, false};
}
}

auto const res = deleteAMMAccountIfEmpty(
sb, ammSle, newLPTokenBalance, ctx_.tx[sfAsset], ctx_.tx[sfAsset2], j_);
// LCOV_EXCL_START
Expand Down
22 changes: 18 additions & 4 deletions src/test/app/AMMClawback_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2485,10 +2485,19 @@ class AMMClawback_test : public beast::unit_test::suite
}
else if (!features[fixAMMClawbackRounding])
{
// sqrt(amount * amount2) >= LPTokens and exceeds the allowed
// tolerance
env(amm::ammClawback(gw, alice, USD, EUR, USD(1)), ter(tecINVARIANT_FAILED));
BEAST_EXPECT(amm.ammExists());
// sqrt(amount * amount2) >= LPTokens and exceeds the allowed tolerance.
// With fixCleanup3_2_0 this is caught in the transaction layer;
// without it the invariant checker fires instead.
if (features[fixCleanup3_2_0])
{
env(amm::ammClawback(gw, alice, USD, EUR, USD(1)), ter(tecPRECISION_LOSS));
BEAST_EXPECT(amm.ammExists());
}
else
{
env(amm::ammClawback(gw, alice, USD, EUR, USD(1)), ter(tecINVARIANT_FAILED));
BEAST_EXPECT(amm.ammExists());
}
}
else if (features[fixAMMv1_3] && features[fixAMMClawbackRounding])
{
Expand All @@ -2514,6 +2523,11 @@ class AMMClawback_test : public beast::unit_test::suite
testFeatureDisabled(all - featureAMMClawback);
for (auto const& features :
{all - fixAMMv1_3 - fixAMMClawbackRounding - featureMPTokensV2,
// fixAMMv1_3 on, fixAMMClawbackRounding off, fixCleanup3_2_0 off:
// precision loss caught by invariant checker -> tecINVARIANT_FAILED
all - fixAMMClawbackRounding - fixCleanup3_2_0 - featureMPTokensV2,
// fixAMMv1_3 on, fixAMMClawbackRounding off, fixCleanup3_2_0 on:
// precision loss caught in transaction layer -> tecPRECISION_LOSS
all - fixAMMClawbackRounding - featureMPTokensV2,
all - featureMPTokensV2,
all})
Expand Down
16 changes: 13 additions & 3 deletions src/test/app/AMM_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1841,16 +1841,26 @@ struct AMM_test : public jtx::AMMTest
// are rounded to all LP tokens.
testAMM(
[&](AMM& ammAlice, Env& env) {
auto const err =
env.enabled(fixAMMv1_3) ? ter(tecINVARIANT_FAILED) : ter(tecAMM_BALANCE);
// Without fixAMMv1_3: sub-method returns tecAMM_BALANCE early.
// With fixAMMv1_3 but without fixCleanup3_2_0: sub-method succeeds
// but invariant check catches the precision violation.
// With fixCleanup3_2_0: caught in the transaction layer before
// the invariant checker runs.
auto const err = [&] {
if (!env.enabled(fixAMMv1_3))
return ter(tecAMM_BALANCE);
if (env.enabled(fixCleanup3_2_0))
return ter(tecPRECISION_LOSS);
return ter(tecINVARIANT_FAILED);
}();
ammAlice.withdraw(
alice,
STAmount{USD, UINT64_C(9'999'999999999999), -12},
std::nullopt,
std::nullopt,
err);
},
{.features = {all, all - fixAMMv1_3}, .noLog = true});
{.features = {all, all - fixAMMv1_3, all - fixCleanup3_2_0}, .noLog = true});

// Tiny withdraw
testAMM([&](AMM& ammAlice, Env&) {
Expand Down
Loading