mirror of
https://github.com/XRPLF/rippled.git
synced 2026-04-29 15:37:57 +00:00
Per XLS-0095, we are taking steps to rename ripple(d) to xrpl(d). This change specifically removes all copyright notices referencing Ripple, XRPLF, and certain affiliated contributors upon mutual agreement, so the notice in the LICENSE.md file applies throughout. Copyright notices referencing external contributions remain as-is. Duplicate verbiage is also removed.
468 lines
16 KiB
C++
468 lines
16 KiB
C++
#include <test/jtx.h>
|
|
#include <test/jtx/AMM.h>
|
|
#include <test/jtx/AMMTest.h>
|
|
|
|
namespace ripple {
|
|
namespace test {
|
|
|
|
class LPTokenTransfer_test : public jtx::AMMTest
|
|
{
|
|
void
|
|
testDirectStep(FeatureBitset features)
|
|
{
|
|
testcase("DirectStep");
|
|
|
|
using namespace jtx;
|
|
Env env{*this, features};
|
|
fund(env, gw, {alice}, {USD(20'000), BTC(0.5)}, Fund::All);
|
|
env.close();
|
|
|
|
AMM ammAlice(env, alice, USD(20'000), BTC(0.5));
|
|
BEAST_EXPECT(
|
|
ammAlice.expectBalances(USD(20'000), BTC(0.5), IOUAmount{100, 0}));
|
|
|
|
fund(env, gw, {carol}, {USD(4'000), BTC(1)}, Fund::Acct);
|
|
ammAlice.deposit(carol, 10);
|
|
BEAST_EXPECT(
|
|
ammAlice.expectBalances(USD(22'000), BTC(0.55), IOUAmount{110, 0}));
|
|
|
|
fund(env, gw, {bob}, {USD(4'000), BTC(1)}, Fund::Acct);
|
|
ammAlice.deposit(bob, 10);
|
|
BEAST_EXPECT(
|
|
ammAlice.expectBalances(USD(24'000), BTC(0.60), IOUAmount{120, 0}));
|
|
|
|
auto const lpIssue = ammAlice.lptIssue();
|
|
env.trust(STAmount{lpIssue, 500}, alice);
|
|
env.trust(STAmount{lpIssue, 500}, bob);
|
|
env.trust(STAmount{lpIssue, 500}, carol);
|
|
env.close();
|
|
|
|
// gateway freezes carol's USD
|
|
env(trust(gw, carol["USD"](0), tfSetFreeze));
|
|
env.close();
|
|
|
|
// bob can still send lptoken to carol even tho carol's USD is
|
|
// frozen, regardless of whether fixFrozenLPTokenTransfer is enabled or
|
|
// not
|
|
// Note: Deep freeze is not considered for LPToken transfer
|
|
env(pay(bob, carol, STAmount{lpIssue, 5}));
|
|
env.close();
|
|
|
|
// cannot transfer to an amm account
|
|
env(pay(carol, lpIssue.getIssuer(), STAmount{lpIssue, 5}),
|
|
ter(tecNO_PERMISSION));
|
|
env.close();
|
|
|
|
if (features[fixFrozenLPTokenTransfer])
|
|
{
|
|
// carol is frozen on USD and therefore can't send lptoken to bob
|
|
env(pay(carol, bob, STAmount{lpIssue, 5}), ter(tecPATH_DRY));
|
|
}
|
|
else
|
|
{
|
|
// carol can still send lptoken with frozen USD
|
|
env(pay(carol, bob, STAmount{lpIssue, 5}));
|
|
}
|
|
}
|
|
|
|
void
|
|
testBookStep(FeatureBitset features)
|
|
{
|
|
testcase("BookStep");
|
|
|
|
using namespace jtx;
|
|
Env env{*this, features};
|
|
|
|
fund(
|
|
env,
|
|
gw,
|
|
{alice, bob, carol},
|
|
{USD(10'000), EUR(10'000)},
|
|
Fund::All);
|
|
AMM ammAlice(env, alice, USD(10'000), EUR(10'000));
|
|
ammAlice.deposit(carol, 1'000);
|
|
ammAlice.deposit(bob, 1'000);
|
|
|
|
auto const lpIssue = ammAlice.lptIssue();
|
|
|
|
// carols creates an offer to sell lptoken
|
|
env(offer(carol, XRP(10), STAmount{lpIssue, 10}), txflags(tfPassive));
|
|
env.close();
|
|
BEAST_EXPECT(expectOffers(env, carol, 1));
|
|
|
|
env.trust(STAmount{lpIssue, 1'000'000'000}, alice);
|
|
env.trust(STAmount{lpIssue, 1'000'000'000}, bob);
|
|
env.trust(STAmount{lpIssue, 1'000'000'000}, carol);
|
|
env.close();
|
|
|
|
// gateway freezes carol's USD
|
|
env(trust(gw, carol["USD"](0), tfSetFreeze));
|
|
env.close();
|
|
|
|
// exercises alice's ability to consume carol's offer to sell lptoken
|
|
// when carol's USD is frozen pre/post fixFrozenLPTokenTransfer
|
|
// amendment
|
|
if (features[fixFrozenLPTokenTransfer])
|
|
{
|
|
// with fixFrozenLPTokenTransfer, alice fails to consume carol's
|
|
// offer since carol's USD is frozen
|
|
env(pay(alice, bob, STAmount{lpIssue, 10}),
|
|
txflags(tfPartialPayment),
|
|
sendmax(XRP(10)),
|
|
ter(tecPATH_DRY));
|
|
env.close();
|
|
BEAST_EXPECT(expectOffers(env, carol, 1));
|
|
|
|
// gateway unfreezes carol's USD
|
|
env(trust(gw, carol["USD"](1'000'000'000), tfClearFreeze));
|
|
env.close();
|
|
|
|
// alice successfully consumes carol's offer
|
|
env(pay(alice, bob, STAmount{lpIssue, 10}),
|
|
txflags(tfPartialPayment),
|
|
sendmax(XRP(10)));
|
|
env.close();
|
|
BEAST_EXPECT(expectOffers(env, carol, 0));
|
|
}
|
|
else
|
|
{
|
|
// without fixFrozenLPTokenTransfer, alice can consume carol's offer
|
|
// even when carol's USD is frozen
|
|
env(pay(alice, bob, STAmount{lpIssue, 10}),
|
|
txflags(tfPartialPayment),
|
|
sendmax(XRP(10)));
|
|
env.close();
|
|
BEAST_EXPECT(expectOffers(env, carol, 0));
|
|
}
|
|
|
|
// make sure carol's USD is not frozen
|
|
env(trust(gw, carol["USD"](1'000'000'000), tfClearFreeze));
|
|
env.close();
|
|
|
|
// ensure that carol's offer to buy lptoken can be consumed by alice
|
|
// even when carol's USD is frozen
|
|
{
|
|
// carol creates an offer to buy lptoken
|
|
env(offer(carol, STAmount{lpIssue, 10}, XRP(10)),
|
|
txflags(tfPassive));
|
|
env.close();
|
|
BEAST_EXPECT(expectOffers(env, carol, 1));
|
|
|
|
// gateway freezes carol's USD
|
|
env(trust(gw, carol["USD"](0), tfSetFreeze));
|
|
env.close();
|
|
|
|
// alice successfully consumes carol's offer
|
|
env(pay(alice, bob, XRP(10)),
|
|
txflags(tfPartialPayment),
|
|
sendmax(STAmount{lpIssue, 10}));
|
|
env.close();
|
|
BEAST_EXPECT(expectOffers(env, carol, 0));
|
|
}
|
|
}
|
|
|
|
void
|
|
testOfferCreation(FeatureBitset features)
|
|
{
|
|
testcase("Create offer");
|
|
|
|
using namespace jtx;
|
|
Env env{*this, features};
|
|
|
|
fund(
|
|
env,
|
|
gw,
|
|
{alice, bob, carol},
|
|
{USD(10'000), EUR(10'000)},
|
|
Fund::All);
|
|
AMM ammAlice(env, alice, USD(10'000), EUR(10'000));
|
|
ammAlice.deposit(carol, 1'000);
|
|
ammAlice.deposit(bob, 1'000);
|
|
|
|
auto const lpIssue = ammAlice.lptIssue();
|
|
|
|
// gateway freezes carol's USD
|
|
env(trust(gw, carol["USD"](0), tfSetFreeze));
|
|
env.close();
|
|
|
|
// exercises carol's ability to create a new offer to sell lptoken with
|
|
// frozen USD, before and after fixFrozenLPTokenTransfer
|
|
if (features[fixFrozenLPTokenTransfer])
|
|
{
|
|
// with fixFrozenLPTokenTransfer, carol can't create an offer to
|
|
// sell lptoken when one of the assets is frozen
|
|
|
|
// carol can't create an offer to sell lptoken
|
|
env(offer(carol, XRP(10), STAmount{lpIssue, 10}),
|
|
txflags(tfPassive),
|
|
ter(tecUNFUNDED_OFFER));
|
|
env.close();
|
|
BEAST_EXPECT(expectOffers(env, carol, 0));
|
|
|
|
// gateway unfreezes carol's USD
|
|
env(trust(gw, carol["USD"](1'000'000'000), tfClearFreeze));
|
|
env.close();
|
|
|
|
// carol can create an offer to sell lptoken after USD is unfrozen
|
|
env(offer(carol, XRP(10), STAmount{lpIssue, 10}),
|
|
txflags(tfPassive));
|
|
env.close();
|
|
BEAST_EXPECT(expectOffers(env, carol, 1));
|
|
}
|
|
else
|
|
{
|
|
// without fixFrozenLPTokenTransfer, carol can create an offer
|
|
env(offer(carol, XRP(10), STAmount{lpIssue, 10}),
|
|
txflags(tfPassive));
|
|
env.close();
|
|
BEAST_EXPECT(expectOffers(env, carol, 1));
|
|
}
|
|
|
|
// gateway freezes carol's USD
|
|
env(trust(gw, carol["USD"](0), tfSetFreeze));
|
|
env.close();
|
|
|
|
// carol can create offer to buy lptoken even if USD is frozen
|
|
env(offer(carol, STAmount{lpIssue, 10}, XRP(5)), txflags(tfPassive));
|
|
env.close();
|
|
BEAST_EXPECT(expectOffers(env, carol, 2));
|
|
}
|
|
|
|
void
|
|
testOfferCrossing(FeatureBitset features)
|
|
{
|
|
testcase("Offer crossing");
|
|
|
|
using namespace jtx;
|
|
Env env{*this, features};
|
|
|
|
// Offer crossing with two AMM LPTokens.
|
|
fund(env, gw, {alice, carol}, {USD(10'000)}, Fund::All);
|
|
AMM ammAlice1(env, alice, XRP(10'000), USD(10'000));
|
|
ammAlice1.deposit(carol, 10'000'000);
|
|
|
|
fund(env, gw, {alice, carol}, {EUR(10'000)}, Fund::IOUOnly);
|
|
AMM ammAlice2(env, alice, XRP(10'000), EUR(10'000));
|
|
ammAlice2.deposit(carol, 10'000'000);
|
|
auto const token1 = ammAlice1.lptIssue();
|
|
auto const token2 = ammAlice2.lptIssue();
|
|
|
|
// carol creates offer
|
|
env(offer(carol, STAmount{token2, 100}, STAmount{token1, 100}));
|
|
env.close();
|
|
BEAST_EXPECT(expectOffers(env, carol, 1));
|
|
|
|
// gateway freezes carol's USD, carol's token1 should be frozen as well
|
|
env(trust(gw, carol["USD"](0), tfSetFreeze));
|
|
env.close();
|
|
|
|
// alice creates an offer which exhibits different behavior on offer
|
|
// crossing depending on if fixFrozenLPTokenTransfer is enabled
|
|
env(offer(alice, STAmount{token1, 100}, STAmount{token2, 100}));
|
|
env.close();
|
|
|
|
// exercises carol's offer's ability to cross with alice's offer when
|
|
// carol's USD is frozen, before and after fixFrozenLPTokenTransfer
|
|
if (features[fixFrozenLPTokenTransfer])
|
|
{
|
|
// with fixFrozenLPTokenTransfer enabled, alice's offer can no
|
|
// longer cross with carol's offer
|
|
BEAST_EXPECT(
|
|
expectHolding(env, alice, STAmount{token1, 10'000'000}) &&
|
|
expectHolding(env, alice, STAmount{token2, 10'000'000}));
|
|
BEAST_EXPECT(
|
|
expectHolding(env, carol, STAmount{token2, 10'000'000}) &&
|
|
expectHolding(env, carol, STAmount{token1, 10'000'000}));
|
|
BEAST_EXPECT(
|
|
expectOffers(env, alice, 1) && expectOffers(env, carol, 0));
|
|
}
|
|
else
|
|
{
|
|
// alice's offer still crosses with carol's offer despite carol's
|
|
// token1 is frozen
|
|
BEAST_EXPECT(
|
|
expectHolding(env, alice, STAmount{token1, 10'000'100}) &&
|
|
expectHolding(env, alice, STAmount{token2, 9'999'900}));
|
|
BEAST_EXPECT(
|
|
expectHolding(env, carol, STAmount{token2, 10'000'100}) &&
|
|
expectHolding(env, carol, STAmount{token1, 9'999'900}));
|
|
BEAST_EXPECT(
|
|
expectOffers(env, alice, 0) && expectOffers(env, carol, 0));
|
|
}
|
|
}
|
|
|
|
void
|
|
testCheck(FeatureBitset features)
|
|
{
|
|
testcase("Check");
|
|
|
|
using namespace jtx;
|
|
Env env{*this, features};
|
|
|
|
fund(
|
|
env,
|
|
gw,
|
|
{alice, bob, carol},
|
|
{USD(10'000), EUR(10'000)},
|
|
Fund::All);
|
|
AMM ammAlice(env, alice, USD(10'000), EUR(10'000));
|
|
ammAlice.deposit(carol, 1'000);
|
|
ammAlice.deposit(bob, 1'000);
|
|
|
|
auto const lpIssue = ammAlice.lptIssue();
|
|
|
|
// gateway freezes carol's USD
|
|
env(trust(gw, carol["USD"](0), tfSetFreeze));
|
|
env.close();
|
|
|
|
// carol can always create a check with lptoken that has frozen
|
|
// token
|
|
uint256 const carolChkId{keylet::check(carol, env.seq(carol)).key};
|
|
env(check::create(carol, bob, STAmount{lpIssue, 10}));
|
|
env.close();
|
|
|
|
// with fixFrozenLPTokenTransfer enabled, bob fails to cash the check
|
|
if (features[fixFrozenLPTokenTransfer])
|
|
env(check::cash(bob, carolChkId, STAmount{lpIssue, 10}),
|
|
ter(tecPATH_PARTIAL));
|
|
else
|
|
env(check::cash(bob, carolChkId, STAmount{lpIssue, 10}));
|
|
|
|
env.close();
|
|
|
|
// bob creates a check
|
|
uint256 const bobChkId{keylet::check(bob, env.seq(bob)).key};
|
|
env(check::create(bob, carol, STAmount{lpIssue, 10}));
|
|
env.close();
|
|
|
|
// carol cashes the bob's check. Even though carol is frozen, she can
|
|
// still receive LPToken
|
|
env(check::cash(carol, bobChkId, STAmount{lpIssue, 10}));
|
|
env.close();
|
|
}
|
|
|
|
void
|
|
testNFTOffers(FeatureBitset features)
|
|
{
|
|
testcase("NFT Offers");
|
|
using namespace test::jtx;
|
|
|
|
Env env{*this, features};
|
|
|
|
// Setup AMM
|
|
fund(
|
|
env,
|
|
gw,
|
|
{alice, bob, carol},
|
|
{USD(10'000), EUR(10'000)},
|
|
Fund::All);
|
|
AMM ammAlice(env, alice, USD(10'000), EUR(10'000));
|
|
ammAlice.deposit(carol, 1'000);
|
|
ammAlice.deposit(bob, 1'000);
|
|
|
|
auto const lpIssue = ammAlice.lptIssue();
|
|
|
|
// bob mints a nft
|
|
uint256 const nftID{token::getNextID(env, bob, 0u, tfTransferable)};
|
|
env(token::mint(bob, 0), txflags(tfTransferable));
|
|
env.close();
|
|
|
|
// bob creates a sell offer for lptoken
|
|
uint256 const sellOfferIndex = keylet::nftoffer(bob, env.seq(bob)).key;
|
|
env(token::createOffer(bob, nftID, STAmount{lpIssue, 10}),
|
|
txflags(tfSellNFToken));
|
|
env.close();
|
|
|
|
// gateway freezes carol's USD
|
|
env(trust(gw, carol["USD"](0), tfSetFreeze));
|
|
env.close();
|
|
|
|
// exercises one's ability to transfer NFT using lptoken when one of the
|
|
// assets is frozen
|
|
if (features[fixFrozenLPTokenTransfer])
|
|
{
|
|
// with fixFrozenLPTokenTransfer, freezing USD will prevent buy/sell
|
|
// offers with lptokens from being created/accepted
|
|
|
|
// carol fails to accept bob's offer with lptoken because carol's
|
|
// USD is frozen
|
|
env(token::acceptSellOffer(carol, sellOfferIndex),
|
|
ter(tecINSUFFICIENT_FUNDS));
|
|
env.close();
|
|
|
|
// gateway unfreezes carol's USD
|
|
env(trust(gw, carol["USD"](1'000'000), tfClearFreeze));
|
|
env.close();
|
|
|
|
// carol can now accept the offer and own the nft
|
|
env(token::acceptSellOffer(carol, sellOfferIndex));
|
|
env.close();
|
|
|
|
// gateway freezes bobs's USD
|
|
env(trust(gw, bob["USD"](0), tfSetFreeze));
|
|
env.close();
|
|
|
|
// bob fails to create a buy offer with lptoken for carol's nft
|
|
// since bob's USD is frozen
|
|
env(token::createOffer(bob, nftID, STAmount{lpIssue, 10}),
|
|
token::owner(carol),
|
|
ter(tecUNFUNDED_OFFER));
|
|
env.close();
|
|
|
|
// gateway unfreezes bob's USD
|
|
env(trust(gw, bob["USD"](1'000'000), tfClearFreeze));
|
|
env.close();
|
|
|
|
// bob can now create a buy offer
|
|
env(token::createOffer(bob, nftID, STAmount{lpIssue, 10}),
|
|
token::owner(carol));
|
|
env.close();
|
|
}
|
|
else
|
|
{
|
|
// without fixFrozenLPTokenTransfer, freezing USD will still allow
|
|
// buy/sell offers to be created/accepted with lptoken
|
|
|
|
// carol can still accept bob's offer despite carol's USD is frozen
|
|
env(token::acceptSellOffer(carol, sellOfferIndex));
|
|
env.close();
|
|
|
|
// gateway freezes bob's USD
|
|
env(trust(gw, bob["USD"](0), tfSetFreeze));
|
|
env.close();
|
|
|
|
// bob creates a buy offer with lptoken despite bob's USD is frozen
|
|
uint256 const buyOfferIndex =
|
|
keylet::nftoffer(bob, env.seq(bob)).key;
|
|
env(token::createOffer(bob, nftID, STAmount{lpIssue, 10}),
|
|
token::owner(carol));
|
|
env.close();
|
|
|
|
// carol accepts bob's offer
|
|
env(token::acceptBuyOffer(carol, buyOfferIndex));
|
|
env.close();
|
|
}
|
|
}
|
|
|
|
public:
|
|
void
|
|
run() override
|
|
{
|
|
FeatureBitset const all{jtx::testable_amendments()};
|
|
|
|
for (auto const features : {all, all - fixFrozenLPTokenTransfer})
|
|
{
|
|
testDirectStep(features);
|
|
testBookStep(features);
|
|
testOfferCreation(features);
|
|
testOfferCrossing(features);
|
|
testCheck(features);
|
|
testNFTOffers(features);
|
|
}
|
|
}
|
|
};
|
|
|
|
BEAST_DEFINE_TESTSUITE(LPTokenTransfer, app, ripple);
|
|
} // namespace test
|
|
} // namespace ripple
|