mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-18 21:10:02 +00:00
feat: Enforce MPT CanTransfer on AMM LPTokens transfers (#7418)
This commit is contained in:
committed by
GitHub
parent
c49789086a
commit
ca6121c5b3
@@ -85,6 +85,26 @@ isLPTokenFrozen(
|
||||
Asset const& asset,
|
||||
Asset const& asset2);
|
||||
|
||||
/**
|
||||
* Check whether an AMM LPToken may be transferred between @p from and @p to.
|
||||
*
|
||||
* @p lpTokenIssuer is the issuer of the LPToken being moved. If it is not an
|
||||
* AMM account the token is not an LPToken and the transfer is unconditionally
|
||||
* permitted. Otherwise, for each MPT pool asset of that AMM, canTransfer() must
|
||||
* permit the transfer (which exempts the MPT issuer). Non-MPT pool assets are
|
||||
* always transferable by this check, so it is implicitly gated by
|
||||
* featureMPTokensV2 (MPTs can only be AMM pool assets once V2 is enabled).
|
||||
*
|
||||
* @return tesSUCCESS if permitted, otherwise the canTransfer() failure code
|
||||
* (e.g. tecNO_AUTH) of the first MPT pool asset that disallows it.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
canTransferLPToken(
|
||||
ReadView const& view,
|
||||
AccountID const& from,
|
||||
AccountID const& to,
|
||||
AccountID const& lpTokenIssuer);
|
||||
|
||||
// Return the list of enabled amendments
|
||||
[[nodiscard]] std::set<uint256>
|
||||
getEnabledAmendments(ReadView const& view);
|
||||
|
||||
@@ -138,6 +138,33 @@ isLPTokenFrozen(
|
||||
return isFrozen(view, account, asset) || isFrozen(view, account, asset2);
|
||||
}
|
||||
|
||||
TER
|
||||
canTransferLPToken(
|
||||
ReadView const& view,
|
||||
AccountID const& from,
|
||||
AccountID const& to,
|
||||
AccountID const& lpTokenIssuer)
|
||||
{
|
||||
// Only AMM-issued LPTokens are subject to this check. The LPToken's issuer
|
||||
// is the AMM account; if it is not an AMM, this is not an LPToken.
|
||||
auto const sleIssuer = view.read(keylet::account(lpTokenIssuer));
|
||||
if (!sleIssuer || !sleIssuer->isFieldPresent(sfAMMID))
|
||||
return tesSUCCESS;
|
||||
|
||||
auto const sleAmm = view.read(keylet::amm((*sleIssuer)[sfAMMID]));
|
||||
if (!sleAmm)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const transferable = [&](Asset const& a) -> TER {
|
||||
if (!a.holds<MPTIssue>())
|
||||
return tesSUCCESS;
|
||||
return canTransfer(view, a.get<MPTIssue>(), from, to);
|
||||
};
|
||||
if (auto const err = transferable((*sleAmm)[sfAsset]); !isTesSuccess(err))
|
||||
return err;
|
||||
return transferable((*sleAmm)[sfAsset2]);
|
||||
}
|
||||
|
||||
bool
|
||||
areCompatible(
|
||||
ReadView const& validLedger,
|
||||
|
||||
@@ -309,6 +309,15 @@ getLineIfUsable(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An LPToken whose AMM pool contains an MPT that forbids transfers is not
|
||||
// spendable. Issuer is the LPToken's AMM account; canTransferLPToken is
|
||||
// a no-op for non-AMM issuers and non-MPT pool assets, so this is implicitly
|
||||
// gated by featureMPTokensV2.
|
||||
if (!isTesSuccess(canTransferLPToken(view, account, account, issuer)))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
return sle;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/ledger/PaymentSandbox.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
@@ -845,8 +846,14 @@ DirectStepI<TDerived>::check(StrandContext const& ctx) const
|
||||
// pure issue/redeem can't be frozen
|
||||
if (!(ctx.isLast && ctx.isFirst))
|
||||
{
|
||||
auto const ter = checkFreeze(ctx.view, src_, dst_, currency_);
|
||||
if (!isTesSuccess(ter))
|
||||
if (auto const ter = checkFreeze(ctx.view, src_, dst_, currency_); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// An LPToken redeemed against its AMM (dst_ is the LPToken issuer on
|
||||
// this hop) cannot move if a pool asset is an MPT that forbids
|
||||
// transfers between these accounts. A no-op unless dst_ is an AMM whose
|
||||
// pool holds such an MPT (so it is implicitly gated by featureMPTokensV2).
|
||||
if (auto const ter = canTransferLPToken(ctx.view, src_, dst_, dst_); !isTesSuccess(ter))
|
||||
return ter;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <test/jtx/TestHelpers.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/check.h>
|
||||
#include <test/jtx/mpt.h>
|
||||
#include <test/jtx/offer.h>
|
||||
#include <test/jtx/owners.h> // IWYU pragma: keep
|
||||
#include <test/jtx/pay.h>
|
||||
@@ -21,6 +22,8 @@
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace xrpl::test {
|
||||
|
||||
class LPTokenTransfer_test : public jtx::AMMTest
|
||||
@@ -433,6 +436,136 @@ class LPTokenTransfer_test : public jtx::AMMTest
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testMPTCanTransferDirectStep(FeatureBitset features)
|
||||
{
|
||||
testcase("MPT CanTransfer DirectStep");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
// An MPT can only be an AMM pool asset once featureMPTokensV2 is
|
||||
// enabled, so this behavior is only meaningful when V2 is present, and
|
||||
// is independent of fixFrozenLPTokenTransfer.
|
||||
if (!features[featureMPTokensV2])
|
||||
return;
|
||||
|
||||
// gw issues an MPT used as one of the AMM pool assets. gw (the MPT
|
||||
// issuer) seeds the pool and hands LP tokens to alice. Transferring LP
|
||||
// tokens between two non-issuer holders is only permitted when the
|
||||
// pool MPT allows transfers (lsfMPTCanTransfer); issuer-involving
|
||||
// transfers are always permitted. The check fires on the redeem step
|
||||
// against the AMM account via canTransferLPToken().
|
||||
auto testLPTokenTransfer = [&](std::uint32_t mptFlags, bool poolXrpToBtc) {
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(30'000), gw_, alice_, bob_);
|
||||
env.close();
|
||||
|
||||
// gw is the MPT issuer, so it may seed the pool regardless of
|
||||
// whether the MPT permits third-party transfers.
|
||||
MPT const btc = MPTTester(
|
||||
{.env = env, .issuer = gw_, .holders = {alice_}, .pay = 1'000, .flags = mptFlags});
|
||||
|
||||
auto const asset1 = poolXrpToBtc ? XRP(10'000) : btc(10'000);
|
||||
auto const asset2 = poolXrpToBtc ? btc(10'000) : XRP(10'000);
|
||||
AMM const amm(env, gw_, asset1, asset2);
|
||||
auto const lpIssue = amm.lptIssue();
|
||||
|
||||
env.trust(STAmount{lpIssue, 100'000}, alice_);
|
||||
env.trust(STAmount{lpIssue, 100'000}, bob_);
|
||||
env.close();
|
||||
|
||||
// Issuer-involving LP token transfer is always allowed (gw is the
|
||||
// pool MPT's issuer), even when the MPT lacks CanTransfer.
|
||||
env(pay(gw_, alice_, STAmount{lpIssue, 1'000}));
|
||||
env.close();
|
||||
|
||||
// Transfer between two non-issuer holders is allowed only if the
|
||||
// pool MPT has CanTransfer set; otherwise the redeem step against
|
||||
// the AMM account blocks it with tecNO_AUTH.
|
||||
if ((mptFlags & tfMPTCanTransfer) != 0u)
|
||||
{
|
||||
env(pay(alice_, bob_, STAmount{lpIssue, 100}));
|
||||
}
|
||||
else
|
||||
{
|
||||
env(pay(alice_, bob_, STAmount{lpIssue, 100}), Ter(tecNO_AUTH));
|
||||
}
|
||||
env.close();
|
||||
};
|
||||
|
||||
// Pool MPT without CanTransfer blocks third-party LP token transfers.
|
||||
testLPTokenTransfer(tfMPTCanTrade, true);
|
||||
testLPTokenTransfer(tfMPTCanTrade, false);
|
||||
|
||||
// Pool MPT with CanTransfer allows them.
|
||||
testLPTokenTransfer(tfMPTCanTrade | tfMPTCanTransfer, true);
|
||||
testLPTokenTransfer(tfMPTCanTrade | tfMPTCanTransfer, false);
|
||||
}
|
||||
|
||||
void
|
||||
testMPTCanTransferOffer(FeatureBitset features)
|
||||
{
|
||||
testcase("MPT CanTransfer Offer");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
if (!features[featureMPTokensV2])
|
||||
return;
|
||||
|
||||
// Parity with frozen LP tokens for the order book: a non-transferable
|
||||
// pool MPT makes the LP token un-spendable (canTransferLPToken zeroes
|
||||
// the spendable balance in accountHolds, just as isLPTokenFrozen does),
|
||||
// so an offer to sell it cannot be funded - the same tecUNFUNDED_OFFER
|
||||
// outcome as freezing a pool asset (see testOfferCreation).
|
||||
auto testLPTokenTransfer = [&](std::uint32_t mptFlags, bool poolXrpToBtc) {
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(30'000), gw_, carol_);
|
||||
env.close();
|
||||
|
||||
MPT const btc = MPTTester(
|
||||
{.env = env, .issuer = gw_, .holders = {carol_}, .pay = 1'000, .flags = mptFlags});
|
||||
|
||||
auto const asset1 = poolXrpToBtc ? XRP(10'000) : btc(10'000);
|
||||
auto const asset2 = poolXrpToBtc ? btc(10'000) : XRP(10'000);
|
||||
AMM const amm(env, gw_, asset1, asset2);
|
||||
auto const lpIssue = amm.lptIssue();
|
||||
|
||||
env.trust(STAmount{lpIssue, 100'000}, carol_);
|
||||
env.close();
|
||||
|
||||
// gw (the pool MPT issuer) seeds carol_ with LP tokens; issuer
|
||||
// involving transfers are always allowed.
|
||||
env(pay(gw_, carol_, STAmount{lpIssue, 1'000}));
|
||||
env.close();
|
||||
|
||||
// carol_ tries to create an offer to sell the LP token.
|
||||
if ((mptFlags & tfMPTCanTransfer) != 0u)
|
||||
{
|
||||
env(offer(carol_, XRP(10), STAmount{lpIssue, 10}), Txflags(tfPassive));
|
||||
env.close();
|
||||
BEAST_EXPECT(expectOffers(env, carol_, 1));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-transferable pool MPT => LP token un-spendable => the
|
||||
// sell offer is unfunded, just as if a pool asset were frozen.
|
||||
env(offer(carol_, XRP(10), STAmount{lpIssue, 10}),
|
||||
Txflags(tfPassive),
|
||||
Ter(tecUNFUNDED_OFFER));
|
||||
env.close();
|
||||
BEAST_EXPECT(expectOffers(env, carol_, 0));
|
||||
}
|
||||
};
|
||||
|
||||
// Pool MPT without CanTransfer: LP token sell offer is unfunded.
|
||||
testLPTokenTransfer(tfMPTCanTrade, true);
|
||||
testLPTokenTransfer(tfMPTCanTrade, false);
|
||||
|
||||
// Pool MPT with CanTransfer: LP token sell offer is created.
|
||||
testLPTokenTransfer(tfMPTCanTrade | tfMPTCanTransfer, true);
|
||||
testLPTokenTransfer(tfMPTCanTrade | tfMPTCanTransfer, false);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -447,6 +580,8 @@ public:
|
||||
testOfferCrossing(features);
|
||||
testCheck(features);
|
||||
testNFTOffers(features);
|
||||
testMPTCanTransferDirectStep(features);
|
||||
testMPTCanTransferOffer(features);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user