mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-12 14:48:10 +00:00
Compare commits
3 Commits
mvadari/re
...
gregtatcam
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec1ec7a77c | ||
|
|
cb75e1f4d7 | ||
|
|
ee2f76a142 |
@@ -6,6 +6,7 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/core/ServiceRegistry.h>
|
||||
#include <xrpl/ledger/PaymentSandbox.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
|
||||
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
@@ -874,15 +875,18 @@ MPTEndpointStep<TDerived>::check(StrandContext const& ctx) const
|
||||
|
||||
// pure issue/redeem can't be frozen (issuer/holder)
|
||||
// For the first step: check global freeze of the step's own asset.
|
||||
// For the last step: check only the per-holder MPToken lock.
|
||||
// For the last step: check only the per-holder lock.
|
||||
// Global freeze of the deliver asset is not checked here
|
||||
// because MPT semantics allow issuer<->holder transfers even when globally
|
||||
// locked — only holder-to-holder DEX paths are restricted.
|
||||
// For both steps: a vault share inherits the freeze of the vault's
|
||||
// underlying asset.
|
||||
if (!(ctx.isLast && ctx.isFirst))
|
||||
{
|
||||
auto const& account = ctx.isFirst ? src_ : dst_;
|
||||
bool const frozen = (ctx.isFirst && isGlobalFrozen(ctx.view, mptIssue_)) ||
|
||||
isIndividualFrozen(ctx.view, account, mptIssue_);
|
||||
isIndividualFrozen(ctx.view, account, mptIssue_) ||
|
||||
isVaultPseudoAccountFrozen(ctx.view, account, mptIssue_, 0);
|
||||
if (frozen)
|
||||
return terLOCKED;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <test/jtx/ter.h>
|
||||
#include <test/jtx/ticket.h>
|
||||
#include <test/jtx/trust.h>
|
||||
#include <test/jtx/txflags.h>
|
||||
#include <test/jtx/vault.h>
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
@@ -1245,26 +1246,261 @@ private:
|
||||
env.close();
|
||||
|
||||
// Direct vault share payment inherits the underlying lock via
|
||||
// sfReferenceHolding.
|
||||
// sfReferenceHolding. Caught in the strand check, like a global
|
||||
// or individual share lock.
|
||||
BEAST_EXPECT(shareBalance(alice) == 499);
|
||||
BEAST_EXPECT(shareBalance(bob) == 501);
|
||||
env(pay(alice, bob, shares(1)), Ter{tecLOCKED});
|
||||
env(pay(alice, bob, shares(1)), Ter{tecPATH_DRY});
|
||||
env.close();
|
||||
BEAST_EXPECT(shareBalance(alice) == 499);
|
||||
BEAST_EXPECT(shareBalance(bob) == 501);
|
||||
|
||||
// The same inherited lock must also block DEX payment paths that
|
||||
// would consume an offer selling vault shares.
|
||||
// would consume an offer selling vault shares. bob, the
|
||||
// destination, inherits the lock and is caught in the strand
|
||||
// check.
|
||||
env(pay(carol, bob, shares(1)),
|
||||
Sendmax(XRP(1)),
|
||||
Path(BookSpec{shares.raw()}),
|
||||
Ter{tecPATH_PARTIAL});
|
||||
Ter{tecPATH_DRY});
|
||||
env.close();
|
||||
BEAST_EXPECT(shareBalance(alice) == 499);
|
||||
BEAST_EXPECT(shareBalance(bob) == 501);
|
||||
BEAST_EXPECT(expectOffers(env, alice, 1));
|
||||
}
|
||||
|
||||
// A holder individually frozen on the underlying can neither sell
|
||||
// nor buy vault shares through an unfrozen holder's offer. The
|
||||
// inherited freeze is caught when the strand is built, before the
|
||||
// offer is consumed.
|
||||
auto testShareDEXEndpointFrozen = [&](Env& env,
|
||||
PrettyAsset const& asset,
|
||||
Account const& owner,
|
||||
Account const& alice,
|
||||
Account const& bob,
|
||||
Account const& carol,
|
||||
std::function<void(bool)> const& setAliceFrozen) {
|
||||
Vault const vault{env};
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(500)}));
|
||||
env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(500)}));
|
||||
env.close();
|
||||
|
||||
PrettyAsset const shares = MPTIssue(env.le(keylet)->at(sfShareMPTID));
|
||||
auto const shareMptID = shares.raw().get<MPTIssue>().getMptID();
|
||||
auto const shareBalance = [&](Account const& account) {
|
||||
auto const sle = env.le(keylet::mptoken(shareMptID, account));
|
||||
return sle ? sle->at(sfMPTAmount) : 0;
|
||||
};
|
||||
|
||||
// bob bids XRP for shares and asks more XRP for shares, so that
|
||||
// his offers don't cross
|
||||
env(offer(bob, shares(1), XRP(1)));
|
||||
env(offer(bob, XRP(2), shares(1)));
|
||||
env.close();
|
||||
|
||||
setAliceFrozen(true);
|
||||
|
||||
auto const aliceShares = shareBalance(alice);
|
||||
auto const bobShares = shareBalance(bob);
|
||||
auto const expectUnchanged = [&]() {
|
||||
BEAST_EXPECT(shareBalance(alice) == aliceShares);
|
||||
BEAST_EXPECT(shareBalance(bob) == bobShares);
|
||||
BEAST_EXPECT(expectOffers(env, bob, 2));
|
||||
};
|
||||
|
||||
// alice sells shares: the first step's source is frozen
|
||||
env(pay(alice, carol, XRP(1)),
|
||||
Sendmax(shares(1)),
|
||||
Path(~XRP),
|
||||
Txflags(tfNoRippleDirect),
|
||||
Ter{tecPATH_DRY});
|
||||
env.close();
|
||||
expectUnchanged();
|
||||
|
||||
// carol buys shares for alice: the last step's destination is
|
||||
// frozen
|
||||
env(pay(carol, alice, shares(1)),
|
||||
Sendmax(XRP(2)),
|
||||
Path(BookSpec{shares.raw()}),
|
||||
Txflags(tfNoRippleDirect),
|
||||
Ter{tecPATH_DRY});
|
||||
env.close();
|
||||
expectUnchanged();
|
||||
|
||||
// Both succeed once alice is unfrozen
|
||||
setAliceFrozen(false);
|
||||
|
||||
env(pay(alice, carol, XRP(1)),
|
||||
Sendmax(shares(1)),
|
||||
Path(~XRP),
|
||||
Txflags(tfNoRippleDirect));
|
||||
env.close();
|
||||
BEAST_EXPECT(shareBalance(bob) == bobShares + 1);
|
||||
|
||||
env(pay(carol, alice, shares(1)),
|
||||
Sendmax(XRP(2)),
|
||||
Path(BookSpec{shares.raw()}),
|
||||
Txflags(tfNoRippleDirect));
|
||||
env.close();
|
||||
BEAST_EXPECT(shareBalance(alice) == aliceShares);
|
||||
BEAST_EXPECT(shareBalance(bob) == bobShares);
|
||||
BEAST_EXPECT(expectOffers(env, bob, 0));
|
||||
};
|
||||
|
||||
// Vault shares as the intermediate asset between two books. Only the
|
||||
// offer owners hold the shares in transit. An owner individually
|
||||
// frozen on the underlying can neither give nor receive the shares.
|
||||
auto testShareDEXMiddleFrozen = [&](Env& env,
|
||||
PrettyAsset const& asset,
|
||||
Account const& owner,
|
||||
Account const& alice,
|
||||
Account const& bob,
|
||||
Account const& carol,
|
||||
std::function<void(bool)> const& setAliceFrozen) {
|
||||
Vault const vault{env};
|
||||
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
|
||||
env(tx);
|
||||
env.close();
|
||||
|
||||
env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(500)}));
|
||||
env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(500)}));
|
||||
env.close();
|
||||
|
||||
PrettyAsset const shares = MPTIssue(env.le(keylet)->at(sfShareMPTID));
|
||||
auto const shareMptID = shares.raw().get<MPTIssue>().getMptID();
|
||||
auto const shareBalance = [&](Account const& account) {
|
||||
auto const sle = env.le(keylet::mptoken(shareMptID, account));
|
||||
return sle ? sle->at(sfMPTAmount) : 0;
|
||||
};
|
||||
|
||||
// alice gives the shares: XRP -> shares (alice) -> asset (bob)
|
||||
{
|
||||
env(offer(alice, XRP(1), shares(1)));
|
||||
env(offer(bob, shares(1), asset(1)));
|
||||
env.close();
|
||||
|
||||
setAliceFrozen(true);
|
||||
|
||||
auto const aliceShares = shareBalance(alice);
|
||||
auto const bobShares = shareBalance(bob);
|
||||
env(pay(carol, owner, asset(1)),
|
||||
Sendmax(XRP(1)),
|
||||
Path(BookSpec{shares.raw()}, BookSpec{asset.raw()}),
|
||||
Txflags(tfNoRippleDirect),
|
||||
Ter{tecPATH_PARTIAL});
|
||||
env.close();
|
||||
BEAST_EXPECT(shareBalance(alice) == aliceShares);
|
||||
BEAST_EXPECT(shareBalance(bob) == bobShares);
|
||||
|
||||
setAliceFrozen(false);
|
||||
|
||||
env(pay(carol, owner, asset(1)),
|
||||
Sendmax(XRP(1)),
|
||||
Path(BookSpec{shares.raw()}, BookSpec{asset.raw()}),
|
||||
Txflags(tfNoRippleDirect));
|
||||
env.close();
|
||||
BEAST_EXPECT(shareBalance(alice) == aliceShares - 1);
|
||||
BEAST_EXPECT(shareBalance(bob) == bobShares + 1);
|
||||
BEAST_EXPECT(expectOffers(env, alice, 0));
|
||||
BEAST_EXPECT(expectOffers(env, bob, 0));
|
||||
}
|
||||
|
||||
// alice receives the shares: asset -> shares (bob) -> XRP (alice)
|
||||
{
|
||||
env(offer(bob, asset(1), shares(1)));
|
||||
env(offer(alice, shares(1), XRP(1)));
|
||||
env.close();
|
||||
|
||||
setAliceFrozen(true);
|
||||
|
||||
auto const aliceShares = shareBalance(alice);
|
||||
auto const bobShares = shareBalance(bob);
|
||||
env(pay(owner, carol, XRP(1)),
|
||||
Sendmax(asset(1)),
|
||||
Path(BookSpec{shares.raw()}, ~XRP),
|
||||
Txflags(tfNoRippleDirect),
|
||||
Ter{tecPATH_PARTIAL});
|
||||
env.close();
|
||||
BEAST_EXPECT(shareBalance(alice) == aliceShares);
|
||||
BEAST_EXPECT(shareBalance(bob) == bobShares);
|
||||
|
||||
setAliceFrozen(false);
|
||||
|
||||
env(pay(owner, carol, XRP(1)),
|
||||
Sendmax(asset(1)),
|
||||
Path(BookSpec{shares.raw()}, ~XRP),
|
||||
Txflags(tfNoRippleDirect));
|
||||
env.close();
|
||||
BEAST_EXPECT(shareBalance(alice) == aliceShares + 1);
|
||||
BEAST_EXPECT(shareBalance(bob) == bobShares - 1);
|
||||
BEAST_EXPECT(expectOffers(env, alice, 0));
|
||||
BEAST_EXPECT(expectOffers(env, bob, 0));
|
||||
}
|
||||
};
|
||||
|
||||
{
|
||||
testcase("MPT individually locked: vault share DEX inherits underlying lock");
|
||||
|
||||
Env env{*this, testableAmendments()};
|
||||
Account const issuer{"issuer"};
|
||||
Account const owner{"owner"};
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
Account const carol{"carol"};
|
||||
env.fund(XRP(10'000), issuer, owner, alice, bob, carol);
|
||||
env.close();
|
||||
|
||||
MPTTester asset{
|
||||
{.env = env,
|
||||
.issuer = issuer,
|
||||
.holders = {owner, alice, bob},
|
||||
.flags = tfMPTCanTransfer | tfMPTCanTrade | tfMPTCanLock}};
|
||||
env(pay(issuer, owner, asset(1'000)));
|
||||
env(pay(issuer, alice, asset(2'000)));
|
||||
env(pay(issuer, bob, asset(2'000)));
|
||||
env.close();
|
||||
|
||||
auto const setAliceFrozen = [&](bool frozen) {
|
||||
asset.set({.holder = alice, .flags = frozen ? tfMPTLock : tfMPTUnlock});
|
||||
env.close();
|
||||
};
|
||||
testShareDEXEndpointFrozen(env, asset, owner, alice, bob, carol, setAliceFrozen);
|
||||
testShareDEXMiddleFrozen(env, asset, owner, alice, bob, carol, setAliceFrozen);
|
||||
}
|
||||
|
||||
{
|
||||
testcase("IOU individually frozen: vault share DEX inherits underlying freeze");
|
||||
|
||||
Env env{*this, testableAmendments()};
|
||||
Account const issuer{"issuer"};
|
||||
Account const owner{"owner"};
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
Account const carol{"carol"};
|
||||
env.fund(XRP(10'000), issuer, owner, alice, bob, carol);
|
||||
env.close();
|
||||
|
||||
// Deliver the IOU from a book to a holder
|
||||
env(fset(issuer, asfDefaultRipple));
|
||||
PrettyAsset const asset = issuer["IOU"];
|
||||
env.trust(asset(10'000), owner, alice, bob);
|
||||
env(pay(issuer, owner, asset(1'000)));
|
||||
env(pay(issuer, alice, asset(2'000)));
|
||||
env(pay(issuer, bob, asset(2'000)));
|
||||
env.close();
|
||||
|
||||
auto const setAliceFrozen = [&](bool frozen) {
|
||||
env(trust(issuer, asset(0), alice, frozen ? tfSetFreeze : tfClearFreeze));
|
||||
env.close();
|
||||
};
|
||||
testShareDEXEndpointFrozen(env, asset, owner, alice, bob, carol, setAliceFrozen);
|
||||
testShareDEXMiddleFrozen(env, asset, owner, alice, bob, carol, setAliceFrozen);
|
||||
}
|
||||
|
||||
{
|
||||
testcase("MPT CanTrade governance: share inherits underlying on DEX and AMM");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user